• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

smallnest / goclaw / 21973009851

13 Feb 2026 03:02AM UTC coverage: 5.778%. Remained the same
21973009851

push

github

chaoyuepan
improve goreleaser

4 of 131 new or added lines in 21 files covered. (3.05%)

2 existing lines in 2 files now uncovered.

1514 of 26201 relevant lines covered (5.78%)

0.55 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/agent/tools/subagent_spawn_tool.go
1
package tools
2

3
import (
4
        "context"
5
        "fmt"
6
        "strings"
7

8
        "github.com/google/uuid"
9
        "github.com/smallnest/goclaw/config"
10
        "github.com/smallnest/goclaw/internal/logger"
11
        "go.uber.org/zap"
12
)
13

14
// SubagentTypes - 分身相关类型定义(避免循环导入)
15

16
// DeliveryContext 传递上下文
17
type DeliveryContext struct {
18
        Channel   string `json:"channel,omitempty"`
19
        AccountID string `json:"account_id,omitempty"`
20
        To        string `json:"to,omitempty"`
21
        ThreadID  string `json:"thread_id,omitempty"`
22
}
23

24
// SubagentRunOutcome 分身运行结果
25
type SubagentRunOutcome struct {
26
        Status string `json:"status"` // ok, error, timeout, unknown
27
        Error  string `json:"error,omitempty"`
28
}
29

30
// SubagentRunParams 分身运行参数
31
type SubagentRunParams struct {
32
        RunID               string
33
        ChildSessionKey     string
34
        RequesterSessionKey string
35
        RequesterOrigin     *DeliveryContext
36
        RequesterDisplayKey string
37
        Task                string
38
        Cleanup             string
39
        Label               string
40
        ArchiveAfterMinutes int
41
}
42

43
// SubagentSystemPromptParams 系统提示词参数
44
type SubagentSystemPromptParams struct {
45
        RequesterSessionKey string
46
        RequesterOrigin     *DeliveryContext
47
        ChildSessionKey     string
48
        Label               string
49
        Task                string
50
}
51

52
// BuildSubagentSystemPrompt 构建分身系统提示词
53
func BuildSubagentSystemPrompt(params *SubagentSystemPromptParams) string {
×
54
        taskText := normalizeText(params.Task)
×
55
        if taskText == "" {
×
56
                taskText = "{{TASK_DESCRIPTION}}"
×
57
        }
×
58

59
        lines := []string{
×
60
                "# Subagent Context",
×
61
                "",
×
62
                "You are a **subagent** spawned by the main agent for a specific task.",
×
63
                "",
×
64
                "## Your Role",
×
65
                fmt.Sprintf("- You were created to handle: %s", taskText),
×
66
                "- Complete this task. That's your entire purpose.",
×
67
                "- You are NOT the main agent. Don't try to be.",
×
68
                "",
×
69
                "## Rules",
×
70
                "1. **Stay focused** - Do your assigned task, nothing else",
×
71
                "2. **Complete the task** - Your final message will be automatically reported to the main agent",
×
72
                "3. **Don't initiate** - No heartbeats, no proactive actions, no side quests",
×
73
                "4. **Be ephemeral** - You may be terminated after task completion. That's fine.",
×
74
                "",
×
75
                "## Output Format",
×
76
                "When complete, your final response should include:",
×
77
                "- What you accomplished or found",
×
78
                "- Any relevant details the main agent should know",
×
79
                "- Keep it concise but informative",
×
80
                "",
×
81
                "## What You DON'T Do",
×
82
                "- NO user conversations (that's main agent's job)",
×
83
                "- NO external messages (email, tweets, etc.) unless explicitly tasked",
×
84
                "- NO cron jobs or persistent state",
×
85
                "- NO pretending to be the main agent",
×
86
        }
×
87

×
88
        if params.Label != "" {
×
89
                lines = append(lines, "")
×
90
                lines = append(lines, fmt.Sprintf("- Label: %s", params.Label))
×
91
        }
×
92
        if params.RequesterSessionKey != "" {
×
93
                lines = append(lines, fmt.Sprintf("- Requester session: %s", params.RequesterSessionKey))
×
94
        }
×
95
        if params.RequesterOrigin != nil && params.RequesterOrigin.Channel != "" {
×
96
                lines = append(lines, fmt.Sprintf("- Requester channel: %s", params.RequesterOrigin.Channel))
×
97
        }
×
98
        lines = append(lines, fmt.Sprintf("- Your session: %s", params.ChildSessionKey))
×
99
        lines = append(lines, "")
×
100

×
101
        return joinLines(lines)
×
102
}
103

104
// normalizeText 规范化文本
105
func normalizeText(s string) string {
×
106
        inSpace := false
×
107
        var result []rune
×
108
        for _, r := range s {
×
109
                if r == ' ' || r == '\t' || r == '\n' {
×
110
                        if !inSpace {
×
111
                                result = append(result, ' ')
×
112
                                inSpace = true
×
113
                        }
×
114
                } else {
×
115
                        result = append(result, r)
×
116
                        inSpace = false
×
117
                }
×
118
        }
119
        return string(result)
×
120
}
121

122
// joinLines 连接行
123
func joinLines(lines []string) string {
×
124
        if len(lines) == 0 {
×
125
                return ""
×
126
        }
×
127
        result := lines[0]
×
128
        for i := 1; i < len(lines); i++ {
×
129
                result += "\n" + lines[i]
×
130
        }
×
131
        return result
×
132
}
133

134
// GenerateChildSessionKey 生成子会话密钥
135
func GenerateChildSessionKey(agentID string) string {
×
136
        u := uuid.New()
×
137
        return fmt.Sprintf("agent:%s:subagent:%s", agentID, u.String())
×
138
}
×
139

140
// GenerateRunID 生成运行ID
141
func GenerateRunID() string {
×
142
        return uuid.New().String()
×
143
}
×
144

145
// End SubagentTypes
146

147
// SubagentSpawnToolParams 分身生成工具参数
148
type SubagentSpawnToolParams struct {
149
        Task              string `json:"task"`                          // 任务描述(必填)
150
        Label             string `json:"label,omitempty"`               // 可选标签
151
        AgentID           string `json:"agent_id,omitempty"`            // 目标 Agent ID
152
        Model             string `json:"model,omitempty"`               // 模型覆盖
153
        Thinking          string `json:"thinking,omitempty"`            // 思考级别
154
        RunTimeoutSeconds int    `json:"run_timeout_seconds,omitempty"` // 超时时间
155
        Cleanup           string `json:"cleanup,omitempty"`             // 清理策略
156
}
157

158
// SubagentSpawnResult 分身生成结果
159
type SubagentSpawnResult struct {
160
        Status          string `json:"status"` // accepted, forbidden, error
161
        ChildSessionKey string `json:"child_session_key,omitempty"`
162
        RunID           string `json:"run_id,omitempty"`
163
        Error           string `json:"error,omitempty"`
164
        ModelApplied    bool   `json:"model_applied,omitempty"`
165
        Warning         string `json:"warning,omitempty"`
166
}
167

168
// SubagentRegistryInterface 分身注册表接口
169
type SubagentRegistryInterface interface {
170
        RegisterRun(params *SubagentRunParams) error
171
}
172

173
// SubagentSpawnTool 分身生成工具
174
type SubagentSpawnTool struct {
175
        registry         SubagentRegistryInterface
176
        getAgentConfig   func(agentID string) *config.AgentConfig
177
        getDefaultConfig func() *config.AgentDefaults
178
        getAgentID       func(sessionKey string) string
179
        onSpawn          func(spawnParams *SubagentSpawnResult) error
180
}
181

182
// NewSubagentSpawnTool 创建分身生成工具
183
func NewSubagentSpawnTool(registry SubagentRegistryInterface) *SubagentSpawnTool {
×
184
        return &SubagentSpawnTool{
×
185
                registry: registry,
×
186
        }
×
187
}
×
188

189
// SetAgentConfigGetter 设置 Agent 配置获取器
190
func (t *SubagentSpawnTool) SetAgentConfigGetter(getter func(agentID string) *config.AgentConfig) {
×
191
        t.getAgentConfig = getter
×
192
}
×
193

194
// SetDefaultConfigGetter 设置默认配置获取器
195
func (t *SubagentSpawnTool) SetDefaultConfigGetter(getter func() *config.AgentDefaults) {
×
196
        t.getDefaultConfig = getter
×
197
}
×
198

199
// SetAgentIDGetter 设置 Agent ID 获取器
200
func (t *SubagentSpawnTool) SetAgentIDGetter(getter func(sessionKey string) string) {
×
201
        t.getAgentID = getter
×
202
}
×
203

204
// SetOnSpawn 设置分身生成回调
205
func (t *SubagentSpawnTool) SetOnSpawn(fn func(spawnParams *SubagentSpawnResult) error) {
×
206
        t.onSpawn = fn
×
207
}
×
208

209
// Name 返回工具名称
210
func (t *SubagentSpawnTool) Name() string {
×
211
        return "sessions_spawn"
×
212
}
×
213

214
// Description 返回工具描述
215
func (t *SubagentSpawnTool) Description() string {
×
216
        return "Spawn a background sub-agent run in an isolated session and announce the result back to the requester chat."
×
217
}
×
218

219
// Parameters 返回工具参数定义
220
func (t *SubagentSpawnTool) Parameters() map[string]interface{} {
×
221
        return map[string]interface{}{
×
222
                "type": "object",
×
223
                "properties": map[string]interface{}{
×
224
                        "task": map[string]interface{}{
×
225
                                "type":        "string",
×
226
                                "description": "The task description for the sub-agent to complete.",
×
227
                        },
×
228
                        "label": map[string]interface{}{
×
229
                                "type":        "string",
×
230
                                "description": "Optional label for the sub-agent run.",
×
231
                        },
×
232
                        "agent_id": map[string]interface{}{
×
233
                                "type":        "string",
×
234
                                "description": "Optional target agent ID to spawn the sub-agent under.",
×
235
                        },
×
236
                        "model": map[string]interface{}{
×
237
                                "type":        "string",
×
238
                                "description": "Optional model override for the sub-agent.",
×
239
                        },
×
240
                        "thinking": map[string]interface{}{
×
241
                                "type":        "string",
×
242
                                "description": "Optional thinking level override (low, medium, high).",
×
243
                        },
×
244
                        "run_timeout_seconds": map[string]interface{}{
×
245
                                "type":        "integer",
×
246
                                "description": "Optional timeout in seconds for the sub-agent run.",
×
247
                        },
×
248
                        "cleanup": map[string]interface{}{
×
249
                                "type":        "string",
×
250
                                "description": "Cleanup strategy: 'delete' to remove immediately, 'keep' to archive after timeout.",
×
251
                                "enum":        []string{"delete", "keep"},
×
252
                        },
×
253
                },
×
254
                "required": []string{"task"},
×
255
        }
×
256
}
×
257

258
// Execute 执行工具
259
func (t *SubagentSpawnTool) Execute(ctx context.Context, params map[string]interface{}) (string, error) {
×
260
        // 解析参数
×
261
        spawnParams, err := t.parseParams(params)
×
262
        if err != nil {
×
263
                result := &SubagentSpawnResult{
×
264
                        Status: "error",
×
265
                        Error:  err.Error(),
×
266
                }
×
267
                return t.marshalResult(result), nil
×
268
        }
×
269

270
        // 验证任务不为空
271
        if strings.TrimSpace(spawnParams.Task) == "" {
×
272
                result := &SubagentSpawnResult{
×
273
                        Status: "error",
×
274
                        Error:  "task is required",
×
275
                }
×
276
                return t.marshalResult(result), nil
×
277
        }
×
278

279
        // 规范化清理策略
280
        if spawnParams.Cleanup != "delete" && spawnParams.Cleanup != "keep" {
×
281
                spawnParams.Cleanup = "keep"
×
282
        }
×
283

284
        // 获取请求者会话信息(从上下文获取)
285
        // TODO: 从 context 中获取请求者会话密钥、agent ID 等
286
        requesterSessionKey := "main" // 默认值
×
287
        requesterAgentID := t.getAgentID(requesterSessionKey)
×
288
        if requesterAgentID == "" {
×
289
                requesterAgentID = "default"
×
290
        }
×
291

292
        // 确定目标 Agent ID
293
        targetAgentID := requesterAgentID
×
294
        if spawnParams.AgentID != "" {
×
295
                targetAgentID = spawnParams.AgentID
×
296
        }
×
297

298
        // 验证跨 Agent 创建权限
299
        if targetAgentID != requesterAgentID {
×
300
                if !t.checkCrossAgentPermission(requesterAgentID, targetAgentID) {
×
301
                        result := &SubagentSpawnResult{
×
302
                                Status: "forbidden",
×
303
                                Error:  fmt.Sprintf("agentId %s is not allowed for sessions_spawn", targetAgentID),
×
304
                        }
×
305
                        return t.marshalResult(result), nil
×
306
                }
×
307
        }
308

309
        // 解析请求者来源
310
        requesterOrigin := &DeliveryContext{
×
311
                Channel:   "cli", // 默认值
×
312
                AccountID: "default",
×
313
        }
×
314

×
315
        // 生成子会话密钥
×
316
        childSessionKey := GenerateChildSessionKey(targetAgentID)
×
317

×
318
        // 生成运行 ID
×
319
        runID := GenerateRunID()
×
320

×
321
        // 构建分身系统提示词
×
322
        childSystemPrompt := BuildSubagentSystemPrompt(&SubagentSystemPromptParams{
×
323
                RequesterSessionKey: requesterSessionKey,
×
324
                RequesterOrigin:     requesterOrigin,
×
325
                ChildSessionKey:     childSessionKey,
×
326
                Label:               spawnParams.Label,
×
327
                Task:                spawnParams.Task,
×
328
        })
×
329
        _ = childSystemPrompt // TODO: 传递给分身实例使用
×
330

×
331
        // 获取归档时间
×
332
        archiveAfterMinutes := 60 // 默认值
×
333
        if defCfg := t.getDefaultConfig(); defCfg != nil && defCfg.Subagents != nil {
×
334
                if defCfg.Subagents.ArchiveAfterMinutes > 0 {
×
335
                        archiveAfterMinutes = defCfg.Subagents.ArchiveAfterMinutes
×
336
                }
×
337
        }
338

339
        // 注册分身运行
340
        if err := t.registry.RegisterRun(&SubagentRunParams{
×
NEW
341
                RunID:               runID,
×
NEW
342
                ChildSessionKey:     childSessionKey,
×
NEW
343
                RequesterSessionKey: requesterSessionKey,
×
NEW
344
                RequesterOrigin:     requesterOrigin,
×
NEW
345
                RequesterDisplayKey: requesterSessionKey,
×
NEW
346
                Task:                spawnParams.Task,
×
NEW
347
                Cleanup:             spawnParams.Cleanup,
×
NEW
348
                Label:               spawnParams.Label,
×
NEW
349
                ArchiveAfterMinutes: archiveAfterMinutes,
×
350
        }); err != nil {
×
351
                result := &SubagentSpawnResult{
×
352
                        Status: "error",
×
353
                        Error:  fmt.Sprintf("failed to register subagent: %v", err),
×
354
                }
×
355
                return t.marshalResult(result), nil
×
356
        }
×
357

358
        // 调用生成回调
359
        if t.onSpawn != nil {
×
360
                spawnResult := &SubagentSpawnResult{
×
361
                        Status:          "accepted",
×
362
                        ChildSessionKey: childSessionKey,
×
363
                        RunID:           runID,
×
364
                }
×
365
                if err := t.onSpawn(spawnResult); err != nil {
×
366
                        logger.Error("Failed to handle subagent spawn",
×
367
                                zap.String("run_id", runID),
×
368
                                zap.Error(err))
×
369
                }
×
370
        }
371

372
        // 构建结果
373
        result := &SubagentSpawnResult{
×
374
                Status:          "accepted",
×
375
                ChildSessionKey: childSessionKey,
×
376
                RunID:           runID,
×
377
        }
×
378

×
379
        logger.Info("Subagent spawned",
×
380
                zap.String("run_id", runID),
×
381
                zap.String("task", spawnParams.Task),
×
382
                zap.String("child_session_key", childSessionKey),
×
383
                zap.String("target_agent_id", targetAgentID))
×
384

×
385
        return t.marshalResult(result), nil
×
386
}
387

388
// parseParams 解析参数
389
func (t *SubagentSpawnTool) parseParams(params map[string]interface{}) (*SubagentSpawnToolParams, error) {
×
390
        result := &SubagentSpawnToolParams{
×
391
                Cleanup: "keep",
×
392
        }
×
393

×
394
        // 解析 task
×
395
        if val, ok := params["task"]; ok {
×
396
                if str, ok := val.(string); ok {
×
397
                        result.Task = str
×
398
                }
×
399
        }
400

401
        // 解析 label
402
        if val, ok := params["label"]; ok {
×
403
                if str, ok := val.(string); ok {
×
404
                        result.Label = str
×
405
                }
×
406
        }
407

408
        // 解析 agent_id
409
        if val, ok := params["agent_id"]; ok {
×
410
                if str, ok := val.(string); ok {
×
411
                        result.AgentID = str
×
412
                }
×
413
        }
414

415
        // 解析 model
416
        if val, ok := params["model"]; ok {
×
417
                if str, ok := val.(string); ok {
×
418
                        result.Model = str
×
419
                }
×
420
        }
421

422
        // 解析 thinking
423
        if val, ok := params["thinking"]; ok {
×
424
                if str, ok := val.(string); ok {
×
425
                        result.Thinking = str
×
426
                }
×
427
        }
428

429
        // 解析 run_timeout_seconds
430
        if val, ok := params["run_timeout_seconds"]; ok {
×
431
                switch v := val.(type) {
×
432
                case float64:
×
433
                        result.RunTimeoutSeconds = int(v)
×
434
                case int:
×
435
                        result.RunTimeoutSeconds = v
×
436
                }
437
        }
438

439
        // 解析 cleanup
440
        if val, ok := params["cleanup"]; ok {
×
441
                if str, ok := val.(string); ok {
×
442
                        result.Cleanup = str
×
443
                }
×
444
        }
445

446
        return result, nil
×
447
}
448

449
// marshalResult 序列化结果
450
func (t *SubagentSpawnTool) marshalResult(result *SubagentSpawnResult) string {
×
451
        // 简化输出
×
452
        switch result.Status {
×
453
        case "accepted":
×
454
                return fmt.Sprintf("Subagent spawned successfully. Run ID: %s, Session: %s",
×
455
                        result.RunID, result.ChildSessionKey)
×
456
        case "forbidden":
×
457
                return fmt.Sprintf("Forbidden: %s", result.Error)
×
458
        case "error":
×
459
                return fmt.Sprintf("Error: %s", result.Error)
×
460
        default:
×
461
                return fmt.Sprintf("Unknown status: %s", result.Status)
×
462
        }
463
}
464

465
// checkCrossAgentPermission 检查跨 Agent 创建权限
466
func (t *SubagentSpawnTool) checkCrossAgentPermission(requesterID, targetID string) bool {
×
467
        if t.getAgentConfig == nil {
×
468
                return false
×
469
        }
×
470

471
        agentCfg := t.getAgentConfig(requesterID)
×
472
        if agentCfg == nil || agentCfg.Subagents == nil {
×
473
                return false
×
474
        }
×
475

476
        // 检查是否允许所有
477
        allowAgents := agentCfg.Subagents.AllowAgents
×
478
        for _, agent := range allowAgents {
×
479
                if strings.TrimSpace(agent) == "*" {
×
480
                        return true
×
481
                }
×
482
        }
483

484
        // 检查是否包含目标 Agent
485
        for _, agent := range allowAgents {
×
486
                if strings.EqualFold(strings.TrimSpace(agent), targetID) {
×
487
                        return true
×
488
                }
×
489
        }
490

491
        return false
×
492
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc