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

smallnest / goclaw / 22353571065

24 Feb 2026 01:46PM UTC coverage: 4.3% (-0.02%) from 4.317%
22353571065

push

github

smallnest
improve agent

1 of 157 new or added lines in 8 files covered. (0.64%)

5 existing lines in 3 files now uncovered.

1058 of 24602 relevant lines covered (4.3%)

0.32 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
        ChildSystemPrompt string `json:"child_system_prompt,omitempty"` // System prompt for the child agent
167
}
168

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

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

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

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

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

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

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

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

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

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

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

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

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

285
        // Get requester session info from context
NEW
286
        requesterSessionKey := "main" // default
×
NEW
287
        if sk := ctx.Value("session_key"); sk != nil {
×
NEW
288
                if key, ok := sk.(string); ok {
×
NEW
289
                        requesterSessionKey = key
×
NEW
290
                }
×
291
        }
292
        requesterAgentID := t.getAgentID(requesterSessionKey)
×
293
        if requesterAgentID == "" {
×
294
                requesterAgentID = "default"
×
295
        }
×
296

297
        // 确定目标 Agent ID
298
        targetAgentID := requesterAgentID
×
299
        if spawnParams.AgentID != "" {
×
300
                targetAgentID = spawnParams.AgentID
×
301
        }
×
302

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

314
        // 解析请求者来源
315
        requesterOrigin := &DeliveryContext{
×
316
                Channel:   "cli", // 默认值
×
317
                AccountID: "default",
×
318
        }
×
319

×
320
        // 生成子会话密钥
×
321
        childSessionKey := GenerateChildSessionKey(targetAgentID)
×
322

×
323
        // 生成运行 ID
×
324
        runID := GenerateRunID()
×
325

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

×
336
        // 获取归档时间
×
337
        archiveAfterMinutes := 60 // 默认值
×
338
        if defCfg := t.getDefaultConfig(); defCfg != nil && defCfg.Subagents != nil {
×
339
                if defCfg.Subagents.ArchiveAfterMinutes > 0 {
×
340
                        archiveAfterMinutes = defCfg.Subagents.ArchiveAfterMinutes
×
341
                }
×
342
        }
343

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

363
        // 调用生成回调
364
        if t.onSpawn != nil {
×
365
                spawnResult := &SubagentSpawnResult{
×
NEW
366
                        Status:            "accepted",
×
NEW
367
                        ChildSessionKey:   childSessionKey,
×
NEW
368
                        RunID:             runID,
×
NEW
369
                        ChildSystemPrompt: childSystemPrompt,
×
370
                }
×
371
                if err := t.onSpawn(spawnResult); err != nil {
×
372
                        logger.Error("Failed to handle subagent spawn",
×
373
                                zap.String("run_id", runID),
×
374
                                zap.Error(err))
×
375
                }
×
376
        }
377

378
        // 构建结果
379
        result := &SubagentSpawnResult{
×
NEW
380
                Status:            "accepted",
×
NEW
381
                ChildSessionKey:   childSessionKey,
×
NEW
382
                RunID:             runID,
×
NEW
383
                ChildSystemPrompt: childSystemPrompt,
×
384
        }
×
385

×
386
        logger.Info("Subagent spawned",
×
387
                zap.String("run_id", runID),
×
388
                zap.String("task", spawnParams.Task),
×
389
                zap.String("child_session_key", childSessionKey),
×
390
                zap.String("target_agent_id", targetAgentID))
×
391

×
392
        return t.marshalResult(result), nil
×
393
}
394

395
// parseParams 解析参数
396
func (t *SubagentSpawnTool) parseParams(params map[string]interface{}) (*SubagentSpawnToolParams, error) {
×
397
        result := &SubagentSpawnToolParams{
×
398
                Cleanup: "keep",
×
399
        }
×
400

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

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

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

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

429
        // 解析 thinking
430
        if val, ok := params["thinking"]; ok {
×
431
                if str, ok := val.(string); ok {
×
432
                        result.Thinking = str
×
433
                }
×
434
        }
435

436
        // 解析 run_timeout_seconds
437
        if val, ok := params["run_timeout_seconds"]; ok {
×
438
                switch v := val.(type) {
×
439
                case float64:
×
440
                        result.RunTimeoutSeconds = int(v)
×
441
                case int:
×
442
                        result.RunTimeoutSeconds = v
×
443
                }
444
        }
445

446
        // 解析 cleanup
447
        if val, ok := params["cleanup"]; ok {
×
448
                if str, ok := val.(string); ok {
×
449
                        result.Cleanup = str
×
450
                }
×
451
        }
452

453
        return result, nil
×
454
}
455

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

472
// checkCrossAgentPermission 检查跨 Agent 创建权限
473
func (t *SubagentSpawnTool) checkCrossAgentPermission(requesterID, targetID string) bool {
×
474
        if t.getAgentConfig == nil {
×
475
                return false
×
476
        }
×
477

478
        agentCfg := t.getAgentConfig(requesterID)
×
479
        if agentCfg == nil || agentCfg.Subagents == nil {
×
480
                return false
×
481
        }
×
482

483
        // 检查是否允许所有
484
        allowAgents := agentCfg.Subagents.AllowAgents
×
485
        for _, agent := range allowAgents {
×
486
                if strings.TrimSpace(agent) == "*" {
×
487
                        return true
×
488
                }
×
489
        }
490

491
        // 检查是否包含目标 Agent
492
        for _, agent := range allowAgents {
×
493
                if strings.EqualFold(strings.TrimSpace(agent), targetID) {
×
494
                        return true
×
495
                }
×
496
        }
497

498
        return false
×
499
}
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