1use serde_json::{json, Value};
8use std::sync::atomic::Ordering;
9use std::sync::{Arc, RwLock};
10use std::time::Duration;
11use tokio::sync::{mpsc, oneshot};
12
13use crate::{Result, RuntimeError, LlmEvent, SessionEvent, AgentEvent};
14use super::super::{Tool, ToolContext, NEXT_SUBAGENT_ID};
15use crate::runtime::subagent::{SubagentHandle, SubagentResult, SubagentStatus, SubagentState};
16
17pub struct SubagentResumeTool;
18
19#[async_trait::async_trait]
20impl Tool for SubagentResumeTool {
21 fn name(&self) -> &str { "subagent_resume" }
22
23 fn description(&self) -> &str {
24 "Resume a finished or timed-out reactive subagent with new instructions. \
25 The previous subagent's conversation state is prepended as context so the \
26 new run has full history. Returns a new handle_id — the original handle \
27 remains readable for comparison. Only works on subagents in \
28 finished/timed_out/failed state."
29 }
30
31 fn parameters(&self) -> Value {
32 json!({
33 "type": "object",
34 "properties": {
35 "handle_id": {
36 "type": "string",
37 "description": "Handle ID of the completed subagent to resume (e.g. \"sa_3\")."
38 },
39 "instructions": {
40 "type": "string",
41 "description": "New task or context to prepend to the resumed subagent. \
42 Injected before the prior conversation history."
43 }
44 },
45 "required": ["handle_id", "instructions"]
46 })
47 }
48
49 async fn execute(&self, params: Value, ctx: ToolContext) -> Result<String> {
50 let prior_handle_id = params["handle_id"].as_str()
51 .ok_or_else(|| RuntimeError::Tool("Missing 'handle_id' parameter".to_string()))?
52 .to_string();
53
54 let instructions = params["instructions"].as_str()
55 .ok_or_else(|| RuntimeError::Tool("Missing 'instructions' parameter".to_string()))?
56 .to_string();
57
58 let registry = ctx.capabilities.subagent_registry.as_ref()
59 .ok_or_else(|| RuntimeError::Tool(
60 "SubagentRegistry not available on this ToolContext".to_string()
61 ))?;
62
63 let (agent_name, model, prior_context, prior_system_prompt, prior_timeout) = {
65 let reg = registry.lock().unwrap();
66 let handle = reg.get(&prior_handle_id)
67 .ok_or_else(|| RuntimeError::Tool(
68 format!("No subagent found with handle_id '{}'", prior_handle_id)
69 ))?;
70
71 if handle.status() == SubagentStatus::Running {
72 return Err(RuntimeError::Tool(format!(
73 "Subagent '{}' is still running. Call subagent_collect first, \
74 or wait until it finishes.",
75 prior_handle_id
76 )));
77 }
78
79 let prior = {
80 let state = handle.conversation_state();
81 if state.is_empty() {
82 handle.partial_output()
83 } else {
84 serde_json::to_string(&state).unwrap_or_else(|_| handle.partial_output())
85 }
86 };
87
88 (handle.agent_name.clone(), handle.model.clone(), prior, handle.system_prompt.clone(), handle.timeout_secs)
89 };
90
91 let resumed_task = format!(
93 "{instructions}\n\n\
94 ---\n\
95 [Prior conversation context from handle {prior_handle_id}]\n\
96 {prior_context}"
97 );
98
99 let system_prompt = prior_system_prompt;
101 let timeout_secs = prior_timeout;
102 let label = agent_name.clone();
103 let task_preview: String = resumed_task.chars().take(80).collect();
104 let task_full = resumed_task.clone();
105 let subagent_id = NEXT_SUBAGENT_ID.fetch_add(1, Ordering::Relaxed);
106 let handle_id = format!("sa_{}", subagent_id);
107
108 tracing::info!(
109 "subagent_resume: dispatching '{}' (id={}, resumed_from={}) model={}",
110 label, handle_id, prior_handle_id, model
111 );
112
113 let state = Arc::new(RwLock::new(SubagentState::new()));
114
115 let (steer_tx, steer_rx) = mpsc::unbounded_channel::<String>();
116 let (shutdown_tx, shutdown_rx) = oneshot::channel::<()>();
117 let (result_tx, result_rx) = oneshot::channel::<SubagentResult>();
118
119 if let Some(ref tx) = ctx.channels.tx_events {
120 let _ = tx.send(crate::StreamEvent::Agent(AgentEvent::SubagentStart {
121 subagent_id,
122 agent_name: label.clone(),
123 task_preview: task_preview.clone(),
124 }));
125 }
126
127 let state_t = Arc::clone(&state);
128 let task_full_a = task_full.clone();
129 let label_inner = label.clone();
130 let model_inner = model.clone();
131 let tx_events_inner = ctx.channels.tx_events.clone();
132 let start_time = std::time::Instant::now();
133 let parent_queue = ctx.capabilities.event_queue.clone();
134 let handle_id_inner = handle_id.clone();
135 let prior_handle_for_finalizer = prior_handle_id.clone();
136
137 let system_prompt_for_handle = system_prompt.clone();
139 let handle = SubagentHandle::new(
140 handle_id.clone(),
141 subagent_id,
142 label.clone(),
143 task_preview,
144 model.clone(),
145 system_prompt_for_handle,
146 timeout_secs,
147 Arc::clone(&state),
148 Some(steer_tx),
149 Some(shutdown_tx),
150 Some(result_rx),
151 );
152 {
153 let mut reg = registry.lock().unwrap();
154 reg.register(handle);
155 }
156
157 let thread_handle = std::thread::spawn(move || {
159 let state_for_finalizer = Arc::clone(&state_t);
161 let label_for_finalizer = label_inner.clone();
162
163 let panic_result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
164 let rt = match tokio::runtime::Builder::new_current_thread()
165 .enable_all()
166 .build()
167 {
168 Ok(rt) => rt,
169 Err(e) => {
170 state_t.write().unwrap().status =
171 SubagentStatus::Failed(format!("tokio runtime: {}", e));
172 return;
173 }
174 };
175
176 let state_a = Arc::clone(&state_t);
177 let label_a = label_inner.clone();
178 let model_a = model_inner.clone();
179 let tx_events_a = tx_events_inner.clone();
180 let task_for_timeout = task_full_a.clone();
181 let task_for_complete = task_full_a.clone();
182 let task_for_stream = task_full_a;
183
184 let outcome: std::result::Result<SubagentResult, String> = rt.block_on(async move {
185 use futures::StreamExt;
186
187 let mut runtime = match crate::Runtime::new().await {
188 Ok(r) => r,
189 Err(e) => return Err(format!("Failed to create subagent runtime: {}", e)),
190 };
191
192 super::apply_subagent_runtime_policy(&mut runtime, &crate::config::load_config());
197 runtime.set_system_prompt(system_prompt);
198 runtime.set_model(model_a.clone());
199 runtime.set_tools(super::subagent_tools().await);
200
201 let cancel = crate::CancellationToken::new();
202 let cancel_inner = cancel.clone();
203 tokio::spawn(async move {
204 let _ = shutdown_rx.await;
205 cancel_inner.cancel();
206 });
207
208 let mut stream = runtime.run_stream_with_messages(
209 vec![std::sync::Arc::new(serde_json::json!({"role": "user", "content": task_for_stream}))],
210 cancel,
211 Some(steer_rx),
212 None,
213 false,
214 ).await;
215
216 let mut tool_count = 0u32;
217 let mut total_input_tokens = 0u64;
218 let mut total_output_tokens = 0u64;
219 let mut total_cache_read = 0u64;
220 let mut total_cache_creation = 0u64;
221 let mut total_cache_5m: Option<u64> = None;
223 let mut total_cache_1h: Option<u64> = None;
224
225 let timeout_fut = tokio::time::sleep(Duration::from_secs(timeout_secs));
226 tokio::pin!(timeout_fut);
227
228 loop {
229 tokio::select! {
230 event = stream.next() => {
231 let Some(event) = event else { break };
232 match event {
233 crate::StreamEvent::Llm(LlmEvent::Thinking(_)) => {
234 if let Some(ref tx) = tx_events_a {
235 let _ = tx.send(crate::StreamEvent::Agent(AgentEvent::SubagentUpdate {
236 subagent_id,
237 agent_name: label_a.clone(),
238 status: "💭 thinking...".to_string(),
239 }));
240 }
241 }
242 crate::StreamEvent::Llm(LlmEvent::Text(text)) => {
243 state_a.write().unwrap().partial_text.push_str(&text);
244 }
245 crate::StreamEvent::Llm(LlmEvent::ToolUseStart { tool_name: name, .. }) => {
246 tool_count += 1;
247 if let Some(ref tx) = tx_events_a {
248 let _ = tx.send(crate::StreamEvent::Agent(AgentEvent::SubagentUpdate {
249 subagent_id,
250 agent_name: label_a.clone(),
251 status: format!("⚙ {} (tool #{})", name, tool_count),
252 }));
253 }
254 }
255 crate::StreamEvent::Llm(LlmEvent::ToolUse { tool_name, input, .. }) => {
256 let input_str = input.to_string();
257 let input_preview: String = input_str.chars().take(200).collect();
258 state_a.write().unwrap().tool_log
259 .push(format!("[tool_use]: {} — {}", tool_name, input_preview));
260 let detail = match tool_name.as_str() {
261 "bash" => {
262 let cmd = input["command"].as_str().unwrap_or("");
263 let preview: String = cmd.chars().take(60).collect();
264 format!("$ {}", preview)
265 }
266 "read" => format!("reading {}", input["path"].as_str().unwrap_or("?").rsplit('/').next().unwrap_or("?")),
267 "write" => format!("writing {}", input["path"].as_str().unwrap_or("?").rsplit('/').next().unwrap_or("?")),
268 "edit" => format!("editing {}", input["path"].as_str().unwrap_or("?").rsplit('/').next().unwrap_or("?")),
269 "grep" => format!("grep /{}/", input["pattern"].as_str().unwrap_or("?").chars().take(30).collect::<String>()),
270 "find" => format!("find {}", input["pattern"].as_str().unwrap_or("?")),
271 "ls" => format!("ls {}", input["path"].as_str().unwrap_or(".").rsplit('/').next().unwrap_or(".")),
272 other => {
273 if other.starts_with("ext__") {
274 other.splitn(3, "__").last().unwrap_or(other).to_string()
275 } else {
276 other.to_string()
277 }
278 }
279 };
280 if let Some(ref tx) = tx_events_a {
281 let _ = tx.send(crate::StreamEvent::Agent(AgentEvent::SubagentUpdate {
282 subagent_id,
283 agent_name: label_a.clone(),
284 status: detail,
285 }));
286 }
287 }
288 crate::StreamEvent::Llm(LlmEvent::ToolResult { result, .. }) => {
289 let preview: String = result.chars().take(300).collect();
290 state_a.write().unwrap().tool_log
291 .push(format!("[tool_result]: {}", preview));
292 }
293 crate::StreamEvent::Session(SessionEvent::Usage {
294 input_tokens, output_tokens,
295 cache_read_input_tokens, cache_creation_input_tokens,
296 cache_creation_5m, cache_creation_1h,
297 model: _,
298 }) => {
299 total_input_tokens += input_tokens;
300 total_output_tokens += output_tokens;
301 total_cache_read += cache_read_input_tokens;
302 total_cache_creation += cache_creation_input_tokens;
303 crate::core::rpc_dispatch::merge_split(&mut total_cache_5m, cache_creation_5m);
304 crate::core::rpc_dispatch::merge_split(&mut total_cache_1h, cache_creation_1h);
305 }
306 crate::StreamEvent::Session(SessionEvent::Error(e)) => return Err(e),
307 crate::StreamEvent::Session(SessionEvent::Done) => break,
308 _ => {}
309 }
310 }
311 _ = &mut timeout_fut => {
312 let (partial, log) = {
313 let mut s = state_a.write().unwrap();
314 s.status = SubagentStatus::TimedOut;
315 s.conversation_state = vec![
316 serde_json::json!({"role": "user", "content": task_for_timeout.clone()}),
317 serde_json::json!({"role": "assistant", "content": &s.partial_text}),
318 ];
319 (s.partial_text.clone(), s.tool_log.clone())
320 };
321 let mut text = format!("[TIMED OUT after {}s — partial results below]\n\n", timeout_secs);
322 if !log.is_empty() {
323 text.push_str(&log.join("\n"));
324 text.push('\n');
325 }
326 if !partial.is_empty() {
327 text.push_str("\n[partial response]:\n");
328 text.push_str(&partial);
329 }
330 state_a.write().unwrap_or_else(|p| p.into_inner()).partial_text = text.clone();
331 return Ok(SubagentResult {
332 text,
333 model: model_a.clone(),
334 input_tokens: total_input_tokens,
335 output_tokens: total_output_tokens,
336 cache_read: total_cache_read,
337 cache_creation: total_cache_creation,
338 cache_creation_5m: total_cache_5m,
339 cache_creation_1h: total_cache_1h,
340 tool_count,
341 });
342 }
343 }
344 }
345
346 Ok(SubagentResult {
347 text: state_a.write().unwrap().partial_text.clone(),
348 model: model_a.clone(),
349 input_tokens: total_input_tokens,
350 output_tokens: total_output_tokens,
351 cache_read: total_cache_read,
352 cache_creation: total_cache_creation,
353 cache_creation_5m: total_cache_5m,
354 cache_creation_1h: total_cache_1h,
355 tool_count,
356 })
357 });
358
359 match outcome {
360 Ok(sa_result) => {
361 {
362 let mut s = state_t.write().unwrap();
363 if matches!(s.status, SubagentStatus::Running) && !s.cancel_requested {
364 s.status = SubagentStatus::Completed;
365 s.conversation_state = vec![
366 serde_json::json!({"role": "user", "content": task_for_complete.clone()}),
367 serde_json::json!({"role": "assistant", "content": sa_result.text.clone()}),
368 ];
369 }
370 }
371 let elapsed = start_time.elapsed().as_secs_f64();
372 let preview: String = sa_result.text.chars().take(120).collect();
373 if let Some(ref tx) = tx_events_inner {
374 let _ = tx.send(crate::StreamEvent::Agent(AgentEvent::SubagentDone {
375 subagent_id,
376 agent_name: label_inner.clone(),
377 result_preview: preview,
378 duration_secs: elapsed,
379 }));
380 }
381 let _ = result_tx.send(sa_result);
382 }
383 Err(e) => {
384 state_t.write().unwrap().status = SubagentStatus::Failed(e.clone());
385 let elapsed = start_time.elapsed().as_secs_f64();
386 if let Some(ref tx) = tx_events_inner {
387 let _ = tx.send(crate::StreamEvent::Agent(AgentEvent::SubagentDone {
388 subagent_id,
389 agent_name: label_inner.clone(),
390 result_preview: format!("ERROR: {}", e),
391 duration_secs: elapsed,
392 }));
393 }
394 }
395 }
396 }));
397
398 if let Err(panic_info) = panic_result {
399 let msg = if let Some(s) = panic_info.downcast_ref::<&str>() {
400 s.to_string()
401 } else if let Some(s) = panic_info.downcast_ref::<String>() {
402 s.clone()
403 } else {
404 "unknown panic".to_string()
405 };
406 tracing::error!("Resumed subagent thread panicked: {}", msg);
407 state_t.write().unwrap_or_else(|p| p.into_inner()).status = SubagentStatus::Failed(format!("panic: {}", msg));
408 }
409
410 super::finalize::finalize_subagent(
414 &state_for_finalizer,
415 parent_queue.as_ref(),
416 &handle_id_inner,
417 subagent_id,
418 &label_for_finalizer,
419 start_time,
420 Some(&prior_handle_for_finalizer),
421 );
422 });
423
424 {
426 let mut reg = registry.lock().unwrap();
427 if let Some(h) = reg.get_mut(&handle_id) {
428 h.set_thread_handle(thread_handle);
429 }
430 }
431
432 Ok(json!({
433 "handle_id": handle_id,
434 "resumed_from": prior_handle_id,
435 "agent_name": label,
436 "status": "running"
437 }).to_string())
438 }
439}