agent_engine/tools/subagent/
status.rs1use super::super::{Tool, ToolContext};
8use crate::{Result, RuntimeError};
9use serde_json::{json, Value};
10
11pub struct SubagentStatusTool;
12
13#[async_trait::async_trait]
14impl Tool for SubagentStatusTool {
15 fn origin(&self) -> crate::tools::ToolOrigin {
16 crate::tools::ToolOrigin::Builtin
17 }
18
19 fn name(&self) -> &str {
20 "subagent_status"
21 }
22
23 fn description(&self) -> &str {
24 "Poll the current state of a reactive subagent. Returns status \
25 (running/completed/cancelled/timed_out/failed), the last 500 characters of output \
26 produced so far, elapsed time in seconds, and the number of tool calls \
27 made. If the status is 'failed', an 'error' field contains the failure reason. \
28 Non-blocking — returns immediately. Use for an occasional progress peek only: \
29 subagents are reactive and push a completion event that wakes you when they \
30 finish, so never call this in a sleep/poll loop."
31 }
32
33 fn parameters(&self) -> Value {
34 json!({
35 "type": "object",
36 "properties": {
37 "handle_id": {
38 "type": "string",
39 "description": "Handle ID returned by subagent_start (e.g. \"sa_3\")."
40 }
41 },
42 "required": ["handle_id"]
43 })
44 }
45
46 async fn execute(&self, params: Value, ctx: ToolContext) -> Result<String> {
47 let handle_id = params["handle_id"]
48 .as_str()
49 .ok_or_else(|| RuntimeError::Tool("Missing 'handle_id' parameter".to_string()))?
50 .to_string();
51
52 let registry = ctx.capabilities.subagent_registry.as_ref().ok_or_else(|| {
55 RuntimeError::Tool("SubagentRegistry not available on this ToolContext".to_string())
56 })?;
57
58 let reg = registry.lock().unwrap();
59
60 let Some(handle) = reg.get(&handle_id) else {
61 let archive_path = reg.archive_path(&handle_id);
64 let stopped_by_reload = reg.was_stopped_by_reload(&handle_id);
65 drop(reg);
66 return archived_status(&handle_id, archive_path, stopped_by_reload);
67 };
68
69 let full: String = handle.partial_output();
72 let agent_name = handle.agent_name.clone();
73 let status = handle.status();
74 let status_str = status.as_str().to_string();
75 let elapsed = handle.elapsed_secs();
76 let tool_count = handle.tool_log().len();
77 let model = handle.model.clone();
78 let terminal = handle.terminal_diagnostic();
79 let authorization = handle.authorization.clone();
80 let archive_error = handle.archive_error();
81 let _ = handle;
82 drop(reg);
83
84 let char_count = full.chars().count();
85 let partial_output: String = if char_count > 500 {
86 full.chars().skip(char_count - 500).collect()
87 } else {
88 full
89 };
90
91 let output_length = partial_output.chars().count();
92 if let Some(orchestration) = &ctx.capabilities.orchestration {
93 orchestration
94 .poll(&handle_id, &format!("{}:{}", status_str, tool_count))
95 .map_err(RuntimeError::Tool)?;
96 }
97
98 let mut resp = json!({
99 "handle_id": handle_id,
100 "agent_name": agent_name,
101 "status": status_str,
102 "partial_output": partial_output,
103 "elapsed_secs": (elapsed * 10.0).round() / 10.0,
104 "tool_count": tool_count,
105 "model": model,
106 "terminal_cause": terminal,
107 "authorization": authorization,
108 "archiveError": archive_error,
109 "output_length": output_length
110 });
111 if let Some(reason) = status.failure_reason() {
112 resp["error"] = json!(reason);
113 }
114 Ok(resp.to_string())
115 }
116}
117
118fn archived_status(
120 handle_id: &str,
121 archive_path: Option<std::path::PathBuf>,
122 stopped_by_reload: bool,
123) -> Result<String> {
124 let not_found = || RuntimeError::Tool(format!("No subagent found with handle_id '{}'", handle_id));
125 let path = archive_path.ok_or_else(not_found)?;
126 let archive = super::archive::read_archive(&path).map_err(|e| {
127 RuntimeError::Tool(format!(
128 "Subagent '{handle_id}' is no longer live and its archive ({}) could not be read: {e}",
129 path.display()
130 ))
131 })?;
132 let full = archive.output;
133 let char_count = full.chars().count();
134 let partial_output: String = if char_count > 500 {
135 full.chars().skip(char_count - 500).collect()
136 } else {
137 full
138 };
139 let note = if stopped_by_reload {
140 "Stopped by a daemon reload (not by the user); not running. subagent_resume continues it \
141 from this archive; subagent_collect reads its result."
142 } else {
143 "Not running: finished and reaped. subagent_collect reads its result from this archive; \
144 subagent_resume continues it."
145 };
146 Ok(json!({
147 "handle_id": handle_id,
148 "agent_name": archive.meta.agent_name,
149 "status": archive.status,
150 "error": archive.error,
153 "partial_output": partial_output,
154 "model": archive.meta.model,
155 "archived": true,
156 "stopped_by_reload": stopped_by_reload,
157 "archive_path": path,
158 "note": note,
159 })
160 .to_string())
161}
162
163#[cfg(test)]
164mod tests {
165 use super::*;
166
167 #[test]
170 fn archived_status_reports_a_reload_stopped_worker() {
171 let dir = tempfile::tempdir().unwrap();
172 let meta = crate::runtime::subagent::SubagentArchiveMeta {
173 agent_name: "integrator".into(),
174 model: "m".into(),
175 system_prompt: "s".into(),
176 timeout_secs: 60,
177 };
178 let path = super::super::archive::write_archive(
179 dir.path(),
180 "sa_288",
181 &meta,
182 "cancelled",
183 Some("boom"),
184 "partial words",
185 &[],
186 )
187 .unwrap();
188 let v: Value =
189 serde_json::from_str(&archived_status("sa_288", Some(path.clone()), true).unwrap()).unwrap();
190 assert_eq!(v["status"], "cancelled");
191 assert_eq!(v["error"], "boom", "archived status keeps the archive's error");
192 assert_eq!(v["agent_name"], "integrator");
193 assert_eq!(v["archived"], true);
194 assert_eq!(v["stopped_by_reload"], true);
195 assert_eq!(v["partial_output"], "partial words");
196 assert!(v["note"].as_str().unwrap().contains("daemon reload"));
197
198 let v: Value =
199 serde_json::from_str(&archived_status("sa_288", Some(path), false).unwrap()).unwrap();
200 assert!(!v["note"].as_str().unwrap().contains("daemon reload"));
201
202 let err = archived_status("sa_9", None, false).unwrap_err().to_string();
203 assert!(err.contains("No subagent found"), "{err}");
204 }
205}