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 gives a sub-agent a follow-up turn while preserving its
15// existing session context. Every explicit follow-up is accepted durably and
16// runs FIFO after any active resident turn. An explicit bounded wait may return
17// that turn's completion or expire while the child stays active.
18type SubagentResumeTool struct {
19 ResidentManager *subagents.ResidentManager
20 Enabled func() bool
21}
22
23type subagentResumeArgs struct {
24 AgentID string `json:"agent_id"`
25 Prompt string `json:"prompt"`
26 Wait *int `json:"wait,omitempty"`
27}
28
29// Name returns the shared facade name: this type is an internal
30// implementation and is never registered on its own.
31func (t *SubagentResumeTool) Name() string { return SubagentToolName }
32
33func (t *SubagentResumeTool) Execute(ctx context.Context, raw json.RawMessage, _ func(string)) (core.ToolResult, error) {
34 if ctx != nil {
35 select {
36 case <-ctx.Done():
37 return core.ToolResult{}, ctx.Err()
38 default:
39 }
40 }
41 prefix := t.Name()
42 if t.ResidentManager == nil {
43 return protocolToolError(prefix + ": subagent runtime not available in this mode")
44 }
45 if t.Enabled == nil || !t.Enabled() {
46 return protocolToolError(prefix + ": subagent management is unavailable in this mode")
47 }
48
49 var args subagentResumeArgs
50 if err := decodeSubagentArgs(raw, &args); err != nil {
51 return core.ToolResult{}, err
52 }
53 id := strings.TrimSpace(args.AgentID)
54 if id == "" {
55 return protocolToolError(prefix + ": agent_id is required")
56 }
57 if strings.TrimSpace(args.Prompt) == "" {
58 return protocolToolError(prefix + ": prompt is required")
59 }
60 if args.Wait != nil && (*args.Wait < 1 || *args.Wait > maxSubagentWaitSeconds) {
61 return protocolToolError(fmt.Sprintf("%s: wait must be between 1 and %d seconds", prefix, maxSubagentWaitSeconds))
62 }
63 snapshot, ok := findResidentStatusSnapshot(t.ResidentManager.Snapshot(), id)
64 if !ok {
65 return protocolToolError(fmt.Sprintf("%s: no such agent %q", prefix, id))
66 }
67 // The watch is registered before the follow-up is accepted, so a turn that
68 // finishes immediately cannot complete before this call subscribes.
69 turnID := uuid.NewString()
70 var completionResult <-chan subagents.ResidentCompletion
71 cancelWait := func() {}
72 if args.Wait != nil {
73 completionResult, cancelWait = t.ResidentManager.WatchCompletion(snapshot.ID, turnID)
74 }
75 defer cancelWait()
76 if err := t.ResidentManager.ResumeWithTurn(ctx, snapshot.ID, args.Prompt, turnID); err != nil {
77 return protocolToolError(prefix + ": " + err.Error())
78 }
79 var outcome *subagentWaitOutcome
80 if args.Wait != nil {
81 completion, timedOut, err := waitForResidentCompletion(ctx, completionResult, *args.Wait, prefix)
82 if err != nil {
83 return core.ToolResult{}, err
84 }
85 outcome = &subagentWaitOutcome{Seconds: *args.Wait, TimedOut: timedOut}
86 if !timedOut {
87 result := completion.Completion()
88 outcome.Status = result.Status
89 outcome.Error = result.Error
90 outcome.Summary = result.Summary
91 }
92 }
93 if updated, ok := t.ResidentManager.SnapshotFor(snapshot.ID); ok {
94 snapshot = updated
95 }
96 return renderResidentActionWait("resumed", publicResidentStatus(snapshot), outcome)
97}