1package tools
2
3import (
4 "context"
5 "encoding/json"
6 "fmt"
7 "os"
8 "strings"
9
10 "github.com/bnema/zut/packages/agent/subagents"
11 "github.com/bnema/zut/packages/core"
12 "github.com/bnema/zut/packages/provider"
13)
14
15// SubagentStatusTool reports live state for background sub-agents without
16// waiting for a child to finish. An omitted agent_id lists resident children;
17// an agent_id queries one child.
18// By default the result contains metadata only. Explicit include_result reads
19// the bounded durable outcome/handoff for one child without model execution.
20type SubagentStatusTool struct {
21 ResidentManager *subagents.ResidentManager
22 Enabled func() bool
23}
24
25type subagentStatusArgs struct {
26 AgentID string `json:"agent_id,omitempty"`
27 IncludeResult bool `json:"include_result,omitempty"`
28}
29
30type subagentStatusResponse struct {
31 Agent *subagentStatusEntry `json:"agent,omitempty"`
32 Agents []subagentStatusEntry `json:"agents"`
33 Result *subagents.ResidentResult `json:"result,omitempty"`
34}
35
36type subagentStatusEntry struct {
37 ID string `json:"agent_id"`
38 State subagents.ResidentState `json:"state"`
39 Profile string `json:"profile,omitempty"`
40 Provider string `json:"provider"`
41 Model string `json:"model"`
42 Workspace subagents.WorkspaceMode `json:"workspace_mode,omitempty"`
43 Required bool `json:"required,omitempty"`
44 OwnedElsewhere bool `json:"owned_elsewhere,omitempty"`
45}
46
47// Name returns the shared facade name: this type is an internal
48// implementation and is never registered on its own.
49func (t *SubagentStatusTool) Name() string { return SubagentToolName }
50
51func (t *SubagentStatusTool) Execute(ctx context.Context, raw json.RawMessage, _ func(string)) (core.ToolResult, error) {
52 if ctx != nil {
53 select {
54 case <-ctx.Done():
55 return core.ToolResult{}, ctx.Err()
56 default:
57 }
58 }
59 prefix := t.Name()
60 if t.ResidentManager == nil {
61 return protocolToolError(prefix + ": subagent runtime not available in this mode")
62 }
63 if t.Enabled == nil || !t.Enabled() {
64 return protocolToolError(prefix + ": subagent status is unavailable in this mode")
65 }
66
67 var args subagentStatusArgs
68 if err := decodeSubagentArgs(raw, &args); err != nil {
69 return core.ToolResult{}, err
70 }
71 snapshots := t.ResidentManager.Snapshot()
72 id := strings.TrimSpace(args.AgentID)
73 if id == "" {
74 if args.IncludeResult {
75 return protocolToolError(prefix + ": include_result requires agent_id")
76 }
77 entries := make([]subagentStatusEntry, 0, len(snapshots))
78 for _, snapshot := range snapshots {
79 entries = append(entries, publicResidentStatus(snapshot))
80 }
81 return renderSubagentStatus(subagentStatusResponse{Agents: entries})
82 }
83 snapshot, ok := findResidentStatusSnapshot(snapshots, id)
84 if !ok {
85 return protocolToolError(fmt.Sprintf("%s: no such agent %q", prefix, id))
86 }
87 entry := publicResidentStatus(snapshot)
88 response := subagentStatusResponse{Agent: &entry}
89 if args.IncludeResult {
90 result, err := t.ResidentManager.Result(snapshot.ID)
91 if err != nil {
92 message := "saved result is unavailable"
93 switch {
94 case snapshot.OwnedElsewhere:
95 message += ": child is owned by another zut process"
96 case os.IsNotExist(err):
97 message += ": no saved result exists yet"
98 case os.IsPermission(err):
99 message += ": permission denied"
100 default:
101 message += ": could not read or decode the saved result"
102 }
103 return protocolToolError(prefix + ": " + message)
104 }
105 response.Result = &result
106 }
107 return renderSubagentStatus(response)
108}
109
110func findResidentStatusSnapshot(snapshots []subagents.ResidentSnapshot, id string) (subagents.ResidentSnapshot, bool) {
111 for _, snapshot := range snapshots {
112 if snapshot.ID == id {
113 return snapshot, true
114 }
115 }
116 var match subagents.ResidentSnapshot
117 hits := 0
118 for _, snapshot := range snapshots {
119 if strings.HasPrefix(snapshot.ID, id) {
120 match = snapshot
121 hits++
122 }
123 }
124 return match, hits == 1
125}
126
127func publicResidentStatus(snapshot subagents.ResidentSnapshot) subagentStatusEntry {
128 return subagentStatusEntry{ID: snapshot.ID, State: snapshot.State, Profile: snapshot.Profile, Provider: snapshot.Provider, Model: snapshot.Model, Workspace: snapshot.WorkspaceMode, Required: snapshot.Required, OwnedElsewhere: snapshot.OwnedElsewhere}
129}
130
131func renderSubagentStatus(response subagentStatusResponse) (core.ToolResult, error) {
132 data, err := json.Marshal(response)
133 if err != nil {
134 return core.ToolResult{}, fmt.Errorf("%s: encode status: %w", SubagentToolName, err)
135 }
136 return core.ToolResult{
137 Content: []provider.Content{provider.TextBlock{Text: string(data)}},
138 Details: response,
139 }, nil
140}