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 "github.com/google/uuid"
12)
13
14// SubagentResumeTool sends a sub-agent a follow-up while preserving its session
15// context. In steer mode, the default, a follow-up to a running child joins its
16// current turn at the next tool or model boundary; otherwise it is accepted
17// durably as a new turn that runs FIFO after any active one. An explicit
18// bounded wait may return the answering turn's completion or expire while the
19// child stays active.
20type SubagentResumeTool struct {
21 ResidentManager *subagents.ResidentManager
22 Enabled func() bool
23}
24
25type subagentResumeArgs struct {
26 AgentID string `json:"agent_id"`
27 Prompt string `json:"prompt"`
28 Mode string `json:"mode,omitempty"`
29 Wait *int `json:"wait,omitempty"`
30}
31
32// subagentResumeResponse reports how a follow-up was delivered.
33type subagentResumeResponse struct {
34 Action string `json:"action"`
35 Delivery string `json:"delivery"`
36 Agent subagentStatusEntry `json:"agent"`
37 Wait *subagentWaitOutcome `json:"wait,omitempty"`
38}
39
40// Name returns the shared facade name: this type is an internal
41// implementation and is never registered on its own.
42func (t *SubagentResumeTool) Name() string { return SubagentToolName }
43
44func (t *SubagentResumeTool) Execute(ctx context.Context, raw json.RawMessage, _ func(string)) (core.ToolResult, error) {
45 if ctx != nil {
46 select {
47 case <-ctx.Done():
48 return core.ToolResult{}, ctx.Err()
49 default:
50 }
51 }
52 prefix := t.Name()
53 if t.ResidentManager == nil {
54 return protocolToolError(prefix + ": subagent runtime not available in this mode")
55 }
56 if t.Enabled == nil || !t.Enabled() {
57 return protocolToolError(prefix + ": subagent management is unavailable in this mode")
58 }
59
60 var args subagentResumeArgs
61 if err := decodeSubagentArgs(raw, &args); err != nil {
62 return core.ToolResult{}, err
63 }
64 id := strings.TrimSpace(args.AgentID)
65 if id == "" {
66 return protocolToolError(prefix + ": agent_id is required")
67 }
68 if strings.TrimSpace(args.Prompt) == "" {
69 return protocolToolError(prefix + ": prompt is required")
70 }
71 mode := subagents.ResumeMode(strings.TrimSpace(args.Mode))
72 if mode == "" {
73 mode = subagents.ResumeSteer
74 }
75 if mode != subagents.ResumeSteer && mode != subagents.ResumeQueue {
76 return protocolToolError(prefix + ": mode must be steer or queue")
77 }
78 if args.Wait != nil && (*args.Wait < 1 || *args.Wait > maxSubagentWaitSeconds) {
79 return protocolToolError(fmt.Sprintf("%s: wait must be between 1 and %d seconds", prefix, maxSubagentWaitSeconds))
80 }
81 snapshot, ok := findResidentStatusSnapshot(t.ResidentManager.Snapshot(), id)
82 if !ok {
83 return protocolToolError(fmt.Sprintf("%s: no such agent %q", prefix, id))
84 }
85 // Each watch is registered before its turn can finish: the queued turn's
86 // before acceptance, and the steered turn's from the child's control loop
87 // before that turn can report completion.
88 turnID := uuid.NewString()
89 var completionResult <-chan subagents.ResidentCompletion
90 cancelWait := func() {}
91 var onSteered func(string)
92 if args.Wait != nil {
93 completionResult, cancelWait = t.ResidentManager.WatchCompletion(snapshot.ID, turnID)
94 onSteered = func(activeTurn string) {
95 cancelWait()
96 completionResult, cancelWait = t.ResidentManager.WatchCompletion(snapshot.ID, activeTurn)
97 }
98 }
99 defer func() { cancelWait() }()
100 delivered, err := t.ResidentManager.ResumeFollowUp(ctx, snapshot.ID, args.Prompt, mode, turnID, onSteered)
101 if err != nil {
102 return protocolToolError(prefix + ": " + err.Error())
103 }
104 delivery := "queued"
105 if delivered.Steered {
106 delivery = "steered"
107 }
108 var outcome *subagentWaitOutcome
109 if args.Wait != nil {
110 completion, timedOut, err := waitForResidentCompletion(ctx, completionResult, *args.Wait, prefix)
111 if err != nil {
112 return core.ToolResult{}, err
113 }
114 outcome = &subagentWaitOutcome{Seconds: *args.Wait, TimedOut: timedOut}
115 if !timedOut {
116 result := completion.Completion()
117 outcome.Status = result.Status
118 outcome.Error = result.Error
119 outcome.Summary = result.Summary
120 outcome.Undelivered = result.Undelivered
121 }
122 }
123 if updated, ok := t.ResidentManager.SnapshotFor(snapshot.ID); ok {
124 snapshot = updated
125 }
126 return renderSubagentResponse(subagentResumeResponse{Action: "resumed", Delivery: delivery, Agent: publicResidentStatus(snapshot), Wait: outcome})
127}