mirror of
https://github.com/sipeed/picoclaw.git
synced 2026-06-12 18:08:54 +00:00
aef1e8e8c4
* fix: eliminate data races on shared tool instances Signed-off-by: Boris Bliznioukov <blib@mail.com> * fix: remove unused indirect dependency on github.com/gdamore/tcell/v2 Signed-off-by: Boris Bliznioukov <blib@mail.com> * fix: reviewer comments improve context handling for tool execution and ensure defaults for non-conversation callers Signed-off-by: Boris Bliznioukov <blib@mail.com> --------- Signed-off-by: Boris Bliznioukov <blib@mail.com>
107 lines
3.0 KiB
Go
107 lines
3.0 KiB
Go
package tools
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
)
|
|
|
|
type SpawnTool struct {
|
|
manager *SubagentManager
|
|
allowlistCheck func(targetAgentID string) bool
|
|
}
|
|
|
|
// Compile-time check: SpawnTool implements AsyncExecutor.
|
|
var _ AsyncExecutor = (*SpawnTool)(nil)
|
|
|
|
func NewSpawnTool(manager *SubagentManager) *SpawnTool {
|
|
return &SpawnTool{
|
|
manager: manager,
|
|
}
|
|
}
|
|
|
|
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) SetAllowlistChecker(check func(targetAgentID string) bool) {
|
|
t.allowlistCheck = check
|
|
}
|
|
|
|
func (t *SpawnTool) Execute(ctx context.Context, args map[string]any) *ToolResult {
|
|
return t.execute(ctx, args, nil)
|
|
}
|
|
|
|
// ExecuteAsync implements AsyncExecutor. The callback is passed through to the
|
|
// subagent manager as a call parameter — never stored on the SpawnTool instance.
|
|
func (t *SpawnTool) ExecuteAsync(ctx context.Context, args map[string]any, cb AsyncCallback) *ToolResult {
|
|
return t.execute(ctx, args, cb)
|
|
}
|
|
|
|
func (t *SpawnTool) execute(ctx context.Context, args map[string]any, cb AsyncCallback) *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")
|
|
}
|
|
|
|
// Read channel/chatID from context (injected by registry).
|
|
// Fall back to "cli"/"direct" for non-conversation callers (e.g., CLI, tests)
|
|
// to preserve the same defaults as the original NewSpawnTool constructor.
|
|
channel := ToolChannel(ctx)
|
|
if channel == "" {
|
|
channel = "cli"
|
|
}
|
|
chatID := ToolChatID(ctx)
|
|
if chatID == "" {
|
|
chatID = "direct"
|
|
}
|
|
|
|
// Pass callback to manager for async completion notification
|
|
result, err := t.manager.Spawn(ctx, task, label, agentID, channel, chatID, cb)
|
|
if err != nil {
|
|
return ErrorResult(fmt.Sprintf("failed to spawn subagent: %v", err))
|
|
}
|
|
|
|
// Return AsyncResult since the task runs in background
|
|
return AsyncResult(result)
|
|
}
|