1package tools
2
3import (
4 "context"
5 "encoding/json"
6 "fmt"
7 "strings"
8
9 "github.com/bnema/zut/packages/agent/subagents"
10 "github.com/bnema/zut/packages/core"
11)
12
13// SubagentResumeTool gives a sub-agent a follow-up turn while preserving its
14// existing session context. Every explicit follow-up starts a fresh max_turns
15// budget. For an idle live worker, the follow-up is delivered directly; for an
16// active live worker, it is queued for the next available message turn; for a
17// terminal worker, the supervisor restarts the retained session with the
18// follow-up as its initial turn.
19type SubagentResumeTool struct {
20 Supervisor *subagents.Supervisor
21 Enabled func() bool
22
23 // OnResumed receives the worker and follow-up after a successful resume.
24 // It runs synchronously, so callbacks must only register non-blocking
25 // tracking work. The interactive host uses it to watch this new delegated
26 // turn and deliver its outcome back to the manager automatically.
27 OnResumed func(agent *subagents.Agent, prompt string)
28}
29
30type subagentResumeArgs struct {
31 AgentID string `json:"agent_id"`
32 Prompt string `json:"prompt"`
33}
34
35const subagentResumeSchema = `{
36 "type": "object",
37 "properties": {
38 "agent_id": {
39 "type": "string",
40 "description": "Worker id or unique id prefix for the sub-agent to continue."
41 },
42 "prompt": {
43 "type": "string",
44 "description": "New manager follow-up for the sub-agent. Its earlier task and conversation remain available in the retained session."
45 }
46 },
47 "required": ["agent_id", "prompt"]
48}`
49
50func (t *SubagentResumeTool) Name() string { return "subagent_resume" }
51
52func (t *SubagentResumeTool) Description() string {
53 return "Continue a sub-agent with a fresh turn budget and its retained session; deliver immediately when idle, queue while active, or restart a stopped worker."
54}
55
56func (t *SubagentResumeTool) Schema() json.RawMessage {
57 return json.RawMessage(subagentResumeSchema)
58}
59
60func (t *SubagentResumeTool) Execute(ctx context.Context, raw json.RawMessage, _ func(string)) (core.ToolResult, error) {
61 if ctx != nil {
62 select {
63 case <-ctx.Done():
64 return core.ToolResult{}, ctx.Err()
65 default:
66 }
67 }
68 prefix := t.Name()
69 if t.Supervisor == nil {
70 return protocolToolError(prefix + ": subagent supervisor not available in this mode")
71 }
72 if t.Enabled == nil || !t.Enabled() {
73 return protocolToolError(prefix + ": subagent management is unavailable in this mode")
74 }
75
76 var args subagentResumeArgs
77 if err := json.Unmarshal(raw, &args); err != nil {
78 return core.ToolResult{}, fmt.Errorf("invalid args: %w", err)
79 }
80 id := strings.TrimSpace(args.AgentID)
81 if id == "" {
82 return protocolToolError(prefix + ": agent_id is required")
83 }
84 if strings.TrimSpace(args.Prompt) == "" {
85 return protocolToolError(prefix + ": prompt is required")
86 }
87 snapshot, ok := findSubagentStatusSnapshot(t.Supervisor.SnapshotAll(), id)
88 if !ok {
89 return protocolToolError(fmt.Sprintf("%s: no such agent %q", prefix, id))
90 }
91 resumed, err := t.Supervisor.ResumeWithPrompt(ctx, snapshot.ID, args.Prompt)
92 if err != nil {
93 return protocolToolError(prefix + ": " + err.Error())
94 }
95 if t.OnResumed != nil {
96 t.OnResumed(resumed, args.Prompt)
97 }
98
99 snapshot, ok = findSubagentStatusSnapshot(t.Supervisor.SnapshotAll(), snapshot.ID)
100 if !ok {
101 return core.ToolResult{}, fmt.Errorf("%s: resumed agent disappeared from supervisor", prefix)
102 }
103 return renderSubagentAction("resumed", snapshot)
104}