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