1package tools
2
3import (
4 "context"
5 "encoding/json"
6 "fmt"
7 "strings"
8
9 "github.com/patriceckhart/zot/packages/agent/swarm"
10 "github.com/patriceckhart/zot/packages/core"
11 "github.com/patriceckhart/zot/packages/provider"
12)
13
14// SwarmSpawnTool lets the main agent fork a background sub-agent
15// against the host's cwd via swarm.Swarm.SpawnReq. The sub-agent runs
16// in parallel: the tool returns the agent id immediately and the main
17// turn continues uninterrupted. The user can monitor / chat with the
18// spawned agent via /swarm.
19//
20// Gated by the auto_swarm_enabled config flag at call time so a user
21// can flip it off mid-session and the next call refuses cleanly
22// without re-registering the tool.
23type SwarmSpawnTool struct {
24 // Swarm is the supervisor used to spawn agents. Nil means
25 // "auto-swarm not available in this mode" and the tool always
26 // errors.
27 Swarm *swarm.Swarm
28
29 // Enabled reads the live config flag. Lets users toggle from
30 // /settings without rebuilding the agent. When nil, the tool
31 // is treated as disabled.
32 Enabled func() bool
33
34 // OnSpawned, if set, is called after every successful spawn with
35 // the new agent + the task it was started with. Used by the
36 // interactive host to track agents and surface a summary back
37 // in the main chat once they all finish.
38 OnSpawned func(agent *swarm.Agent, task string)
39}
40
41type swarmSpawnArgs struct {
42 Task string `json:"task"`
43 Model string `json:"model,omitempty"`
44 Provider string `json:"provider,omitempty"`
45}
46
47const swarmSpawnSchema = `{
48 "type": "object",
49 "properties": {
50 "task": {
51 "type": "string",
52 "description": "The full task description for the sub-agent. Be specific: the sub-agent has the same tools (read/write/edit/bash) and shares this working directory, but starts with NO context from this conversation."
53 },
54 "model": {
55 "type": "string",
56 "description": "Optional model id to pin the sub-agent to (e.g. \"claude-sonnet-4-5\", \"gpt-5\"). Defaults to the host's current model."
57 },
58 "provider": {
59 "type": "string",
60 "description": "Optional provider id (e.g. \"anthropic\", \"openai\"). Usually paired with model."
61 }
62 },
63 "required": ["task"]
64}`
65
66func (t *SwarmSpawnTool) Name() string { return "swarm_spawn" }
67func (t *SwarmSpawnTool) Description() string {
68 return "Spawn a background sub-agent to work on a parallel sub-task. Returns the sub-agent id immediately; the sub-agent keeps running while this conversation continues. Useful for splitting independent work (write tests while implementing a feature, refactor module A while drafting module B). The sub-agent shares this working directory and has the same tools."
69}
70func (t *SwarmSpawnTool) Schema() json.RawMessage { return json.RawMessage(swarmSpawnSchema) }
71
72func (t *SwarmSpawnTool) Execute(ctx context.Context, raw json.RawMessage, progress func(string)) (core.ToolResult, error) {
73 if t.Swarm == nil {
74 return toolErr("swarm_spawn: swarm supervisor not available in this mode"), nil
75 }
76 if t.Enabled == nil || !t.Enabled() {
77 return toolErr("swarm_spawn: auto-swarm is disabled. Ask the user to enable it from /settings before delegating sub-tasks."), nil
78 }
79 var a swarmSpawnArgs
80 if err := json.Unmarshal(raw, &a); err != nil {
81 return core.ToolResult{}, fmt.Errorf("invalid args: %w", err)
82 }
83 task := strings.TrimSpace(a.Task)
84 if task == "" {
85 return toolErr("swarm_spawn: task is required"), nil
86 }
87
88 agent, err := t.Swarm.SpawnReq(ctx, swarm.SpawnRequest{
89 Task: task,
90 Model: strings.TrimSpace(a.Model),
91 Provider: strings.TrimSpace(a.Provider),
92 })
93 if err != nil {
94 return core.ToolResult{}, fmt.Errorf("swarm_spawn: %w", err)
95 }
96 if t.OnSpawned != nil {
97 t.OnSpawned(agent, task)
98 }
99
100 var sb strings.Builder
101 fmt.Fprintf(&sb, "spawned sub-agent %s\n", agent.ID)
102 fmt.Fprintf(&sb, "task: %s\n", truncateTask(task, 200))
103 if a.Model != "" {
104 fmt.Fprintf(&sb, "model: %s\n", a.Model)
105 }
106 if a.Provider != "" {
107 fmt.Fprintf(&sb, "provider: %s\n", a.Provider)
108 }
109 sb.WriteString("\nThe sub-agent is running in the background. Use /swarm in the TUI to monitor it. ")
110 sb.WriteString("This conversation continues immediately; do not wait for the sub-agent to finish before working on the next thing.")
111 return core.ToolResult{
112 Content: []provider.Content{provider.TextBlock{Text: sb.String()}},
113 Details: map[string]any{
114 "agent_id": agent.ID,
115 "task": task,
116 "model": a.Model,
117 "provider": a.Provider,
118 },
119 }, nil
120}
121
122func toolErr(msg string) core.ToolResult {
123 return core.ToolResult{
124 Content: []provider.Content{provider.TextBlock{Text: msg}},
125 IsError: true,
126 }
127}
128
129func truncateTask(s string, n int) string {
130 if len(s) <= n {
131 return s
132 }
133 return s[:n-3] + "..."
134}