mirror of
https://github.com/sipeed/picoclaw.git
synced 2026-06-12 18:08:54 +00:00
ec6da7a530
The spawn tool accepts empty strings as valid task arguments, which causes a subagent to run with no meaningful work. The subagent's completion message is then routed back to the originating channel (e.g. Signal, Discord), where the main agent processes it and may hallucinate an unrelated response that gets sent to users. Validate that the task parameter is non-empty after trimming whitespace. Related: #545 Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
97 lines
2.6 KiB
Go
97 lines
2.6 KiB
Go
package tools
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
)
|
|
|
|
type SpawnTool struct {
|
|
manager *SubagentManager
|
|
originChannel string
|
|
originChatID string
|
|
allowlistCheck func(targetAgentID string) bool
|
|
callback AsyncCallback // For async completion notification
|
|
}
|
|
|
|
func NewSpawnTool(manager *SubagentManager) *SpawnTool {
|
|
return &SpawnTool{
|
|
manager: manager,
|
|
originChannel: "cli",
|
|
originChatID: "direct",
|
|
}
|
|
}
|
|
|
|
// SetCallback implements AsyncTool interface for async completion notification
|
|
func (t *SpawnTool) SetCallback(cb AsyncCallback) {
|
|
t.callback = cb
|
|
}
|
|
|
|
func (t *SpawnTool) Name() string {
|
|
return "spawn"
|
|
}
|
|
|
|
func (t *SpawnTool) Description() string {
|
|
return "Spawn a subagent to handle a task in the background. Use this for complex or time-consuming tasks that can run independently. The subagent will complete the task and report back when done."
|
|
}
|
|
|
|
func (t *SpawnTool) Parameters() map[string]any {
|
|
return map[string]any{
|
|
"type": "object",
|
|
"properties": map[string]any{
|
|
"task": map[string]any{
|
|
"type": "string",
|
|
"description": "The task for subagent to complete",
|
|
},
|
|
"label": map[string]any{
|
|
"type": "string",
|
|
"description": "Optional short label for the task (for display)",
|
|
},
|
|
"agent_id": map[string]any{
|
|
"type": "string",
|
|
"description": "Optional target agent ID to delegate the task to",
|
|
},
|
|
},
|
|
"required": []string{"task"},
|
|
}
|
|
}
|
|
|
|
func (t *SpawnTool) SetContext(channel, chatID string) {
|
|
t.originChannel = channel
|
|
t.originChatID = chatID
|
|
}
|
|
|
|
func (t *SpawnTool) SetAllowlistChecker(check func(targetAgentID string) bool) {
|
|
t.allowlistCheck = check
|
|
}
|
|
|
|
func (t *SpawnTool) Execute(ctx context.Context, args map[string]any) *ToolResult {
|
|
task, ok := args["task"].(string)
|
|
if !ok || strings.TrimSpace(task) == "" {
|
|
return ErrorResult("task is required and must be a non-empty string")
|
|
}
|
|
|
|
label, _ := args["label"].(string)
|
|
agentID, _ := args["agent_id"].(string)
|
|
|
|
// Check allowlist if targeting a specific agent
|
|
if agentID != "" && t.allowlistCheck != nil {
|
|
if !t.allowlistCheck(agentID) {
|
|
return ErrorResult(fmt.Sprintf("not allowed to spawn agent '%s'", agentID))
|
|
}
|
|
}
|
|
|
|
if t.manager == nil {
|
|
return ErrorResult("Subagent manager not configured")
|
|
}
|
|
|
|
// Pass callback to manager for async completion notification
|
|
result, err := t.manager.Spawn(ctx, task, label, agentID, t.originChannel, t.originChatID, t.callback)
|
|
if err != nil {
|
|
return ErrorResult(fmt.Sprintf("failed to spawn subagent: %v", err))
|
|
}
|
|
|
|
// Return AsyncResult since the task runs in background
|
|
return AsyncResult(result)
|
|
}
|