1use std::sync::Arc;
2
3use crate::agents::agent::{ActiveAgent, Agent};
4use crate::agents::code_review::CodeReviewAgent;
5use crate::agents::coder::CoderAgent;
6use crate::ai::model::Model;
7use crate::ai::tweaks::resolve_from_settings;
8use crate::ai::{Content, ContentBlock, Message, MessageRole, ToolResultData, ToolUseData};
9use crate::chat::actor::ActorState;
10use crate::chat::events::{ChatEvent, ChatMessage, ToolExecutionResult, ToolRequest};
11use crate::settings::config::{ReviewLevel, SpawnContextMode, ToolCallStyle};
12use crate::tools::r#trait::{
13 ContinuationPreference, ToolCallHandle, ToolCategory, ToolExecutor, ToolOutput,
14};
15use crate::tools::registry::ToolRegistry;
16use crate::tools::ToolName;
17use anyhow::Result;
18use serde_json::json;
19use tracing::{info, warn};
20
21use crate::chat::events::ToolRequestType;
22
23#[derive(Debug)]
24pub struct ToolResults {
25 pub continue_conversation: bool,
26}
27
28struct ToolCallResult {
29 content_block: ContentBlock,
30 continuation_preference: ContinuationPreference,
31}
32
33impl ToolCallResult {
34 fn immediate(
35 content_block: ContentBlock,
36 continuation_preference: ContinuationPreference,
37 ) -> Self {
38 Self {
39 content_block,
40 continuation_preference,
41 }
42 }
43}
44
45enum DeferredAction {
46 PushAgent {
47 agent: Arc<dyn Agent>,
48 task: String,
49 tool_use_id: String,
50 agent_type: String,
51 },
52 PopAgent {
53 success: bool,
54 result: String,
55 tool_use_id: String,
56 },
57}
58
59pub fn current_agent<F, R>(state: &ActorState, f: F) -> R
61where
62 F: FnOnce(&ActiveAgent) -> R,
63{
64 state
65 .spawn_module
66 .with_current_agent(f)
67 .expect("No active agent")
68}
69
70pub fn current_agent_mut<F, R>(state: &ActorState, f: F) -> R
71where
72 F: FnOnce(&mut ActiveAgent) -> R,
73{
74 state
75 .spawn_module
76 .with_current_agent_mut(f)
77 .expect("No active agent")
78}
79
80fn find_minimum_category(
82 tool_calls: &[ToolUseData],
83 tool_registry: &ToolRegistry,
84) -> Option<ToolCategory> {
85 tool_calls
86 .iter()
87 .filter_map(|tool_call| {
88 tool_registry
90 .get_tool_executor_by_name(&tool_call.name)
91 .map(|executor| executor.category())
92 })
93 .min()
94}
95
96fn filter_tool_calls_by_minimum_category(
97 state: &mut ActorState,
98 tool_calls: Vec<ToolUseData>,
99 tool_registry: &ToolRegistry,
100) -> (Vec<ToolUseData>, Vec<ContentBlock>) {
101 let mut always_allowed_calls = Vec::new();
103 let mut other_calls = Vec::new();
104
105 for tool_call in tool_calls {
106 let category = tool_registry
107 .get_tool_executor_by_name(&tool_call.name)
108 .map(|executor| executor.category());
109
110 if category == Some(ToolCategory::TaskList) {
111 always_allowed_calls.push(tool_call);
112 } else {
113 other_calls.push(tool_call);
114 }
115 }
116
117 if other_calls.is_empty() {
119 return (always_allowed_calls, vec![]);
120 }
121
122 let minimum_category = match find_minimum_category(&other_calls, tool_registry) {
124 Some(cat) => cat,
125 None => {
126 let mut all_calls = always_allowed_calls;
128 all_calls.extend(other_calls);
129 return (all_calls, vec![]);
130 }
131 };
132
133 let original_other_calls = other_calls.clone();
135
136 let filtered_calls: Vec<ToolUseData> = other_calls
137 .into_iter()
138 .filter(|tool_call| {
139 tool_registry
140 .get_tool_executor_by_name(&tool_call.name)
141 .map(|executor| executor.category() == minimum_category)
142 .unwrap_or(false)
143 })
144 .collect();
145
146 let mut error_responses = vec![];
147
148 if filtered_calls.len() != original_other_calls.len() {
149 let dropped_count = original_other_calls.len() - filtered_calls.len();
150 let min_cat_clone = minimum_category.clone();
151 warn!(
152 "Filtered out {} tool calls from higher categories than {:?}",
153 dropped_count, min_cat_clone
154 );
155
156 for tool_call in original_other_calls.iter() {
158 let category = tool_registry
159 .get_tool_executor_by_name(&tool_call.name)
160 .map(|executor| executor.category());
161
162 if category != Some(min_cat_clone.clone()) {
163 warn!(
164 tool_name = %tool_call.name,
165 category = ?category,
166 min_category = ?min_cat_clone,
167 "Dropping tool call due to higher priority category"
168 );
169
170 let error_msg = format!(
171 "Tool call '{}' from category {:?} was dropped because there are tool calls in a lower priority category ({:?}). Only the lowest priority category tools are executed.",
172 tool_call.name, category, min_cat_clone
173 );
174
175 if let Ok(error_result) = handle_tool_error(state, tool_call, error_msg) {
176 error_responses.push(error_result.content_block);
177 }
178 }
179 }
180 }
181
182 let mut result = always_allowed_calls;
184 result.extend(filtered_calls);
185
186 (result, error_responses)
187}
188
189pub async fn execute_tool_calls(
190 state: &mut ActorState,
191 tool_calls: Vec<ToolUseData>,
192 model: Model,
193) -> Result<ToolResults> {
194 state.transition_timing_state(crate::chat::actor::TimingState::ExecutingTools);
195
196 info!(
197 tool_count = tool_calls.len(),
198 tools = ?tool_calls.iter().map(|t| &t.name).collect::<Vec<_>>(),
199 "Executing tool calls"
200 );
201
202 let allowed_tool_names: Vec<ToolName> = current_agent(state, |a| a.agent.available_tools());
204
205 let module_tools: Vec<Arc<dyn ToolExecutor>> =
206 state.modules.iter().flat_map(|m| m.tools()).collect();
207 let all_tools: Vec<Arc<dyn ToolExecutor>> =
208 state.tools.iter().cloned().chain(module_tools).collect();
209 let tool_registry = ToolRegistry::new(all_tools);
210
211 let (tool_calls, error_responses) =
213 filter_tool_calls_by_minimum_category(state, tool_calls, &tool_registry);
214 let mut all_results = error_responses;
215
216 let mut preferences = vec![];
218
219 let mut validated: Vec<(ToolUseData, Box<dyn ToolCallHandle>)> = vec![];
220 let mut invalid_tool_results = vec![];
221 for tool_use in tool_calls {
222 match tool_registry
223 .process_tools(&tool_use, &allowed_tool_names)
224 .await
225 {
226 Ok(handle) => validated.push((tool_use, handle)),
227 Err(error) => {
228 warn!(
229 tool_name = %tool_use.name,
230 error = %error,
231 "Tool call validation failed, will return error response"
232 );
233 if let Ok(error_result) = handle_tool_error(state, &tool_use, error) {
234 invalid_tool_results.push(error_result.content_block);
235 preferences.push(error_result.continuation_preference);
236 }
237 }
238 }
239 }
240
241 let mut results = Vec::new();
242 let mut deferred_actions = Vec::new();
243 for (raw, handle) in validated {
244 state
245 .event_sender
246 .send(ChatEvent::ToolRequest(handle.tool_request()));
247
248 let output = handle.execute().await;
249
250 match output {
251 ToolOutput::Result {
252 content,
253 is_error,
254 continuation,
255 ui_result,
256 } => {
257 let result = ToolResultData {
258 tool_use_id: raw.id.clone(),
259 content,
260 is_error,
261 };
262
263 let event = ChatEvent::ToolExecutionCompleted {
264 tool_call_id: raw.id.clone(),
265 tool_name: raw.name.clone(),
266 tool_result: ui_result,
267 success: !is_error,
268 error: None,
269 };
270 state.event_sender.send(event);
271
272 results.push(ContentBlock::ToolResult(result));
273 preferences.push(continuation);
274 }
275 ToolOutput::PushAgent { agent, task } => {
276 let agent_type = agent.name().to_string();
277 let acknowledgment = ContentBlock::ToolResult(ToolResultData {
278 tool_use_id: raw.id.clone(),
279 content: json!({
280 "status": "spawned",
281 "agent_type": agent_type,
282 "task": task
283 })
284 .to_string(),
285 is_error: false,
286 });
287 results.push(acknowledgment);
288 deferred_actions.push(DeferredAction::PushAgent {
289 agent,
290 task,
291 tool_use_id: raw.id.clone(),
292 agent_type,
293 });
294 preferences.push(ContinuationPreference::Continue);
295 }
296 ToolOutput::PopAgent { success, result } => {
297 let is_root = state.spawn_module.stack_depth() <= 1;
298 let preference = if is_root {
299 ContinuationPreference::Stop
300 } else {
301 ContinuationPreference::Continue
302 };
303
304 let acknowledgment = ContentBlock::ToolResult(ToolResultData {
305 tool_use_id: raw.id.clone(),
306 content: json!({
307 "status": "completing",
308 "success": success,
309 "result": result
310 })
311 .to_string(),
312 is_error: false,
313 });
314 results.push(acknowledgment);
315 deferred_actions.push(DeferredAction::PopAgent {
316 success,
317 result,
318 tool_use_id: raw.id.clone(),
319 });
320 preferences.push(preference);
321 }
322 ToolOutput::PromptUser { question } => {
323 let result = ToolResultData {
324 tool_use_id: raw.id.clone(),
325 content: json!({}).to_string(),
326 is_error: false,
327 };
328
329 let agent_name = current_agent(state, |a| a.agent.name().to_string());
330 state.event_sender.send_message(ChatMessage::assistant(
331 agent_name,
332 question,
333 vec![],
334 crate::chat::events::ModelInfo { model: Model::None },
335 crate::ai::types::TokenUsage::empty(),
336 None,
337 ));
338
339 results.push(ContentBlock::ToolResult(result));
340 preferences.push(ContinuationPreference::Stop);
341 }
342 }
343 }
344
345 let continue_conversation = if preferences
349 .iter()
350 .any(|p| *p == ContinuationPreference::Stop)
351 {
352 false
353 } else {
354 preferences
355 .iter()
356 .any(|p| *p == ContinuationPreference::Continue)
357 };
358
359 all_results.extend(invalid_tool_results);
361 all_results.extend(results);
362
363 if !all_results.is_empty() {
365 let settings_snapshot = state.settings.settings();
366 let resolved_tweaks =
367 resolve_from_settings(&settings_snapshot, state.provider.as_ref(), model);
368
369 let content = if resolved_tweaks.tool_call_style == ToolCallStyle::Xml {
371 let xml_results: Vec<ContentBlock> = all_results
372 .into_iter()
373 .map(convert_tool_result_to_xml)
374 .collect();
375 Content::from(xml_results)
376 } else {
377 Content::from(all_results)
378 };
379
380 current_agent_mut(state, |a| {
381 a.conversation.push(Message {
382 role: MessageRole::User,
383 content,
384 })
385 });
386 }
387
388 for action in deferred_actions {
390 execute_deferred_action(state, action).await;
391 }
392
393 state.transition_timing_state(crate::chat::actor::TimingState::Idle);
394
395 if let Err(e) = state.save_session() {
396 tracing::warn!("Failed to auto-save session after tool execution: {}", e);
397 }
398
399 Ok(ToolResults {
400 continue_conversation,
401 })
402}
403
404fn convert_tool_result_to_xml(block: ContentBlock) -> ContentBlock {
405 let ContentBlock::ToolResult(result) = block else {
406 return block;
407 };
408 let error_attr = if result.is_error {
409 " is_error=\"true\""
410 } else {
411 ""
412 };
413 let xml = format!(
414 "<tool_result tool_use_id=\"{}\"{}>{}</tool_result>",
415 result.tool_use_id, error_attr, result.content
416 );
417 ContentBlock::Text(xml)
418}
419
420fn create_short_message(detailed: &str) -> String {
421 let first_line = detailed.lines().next().unwrap_or(detailed);
422 if first_line.chars().count() > 100 {
423 format!("{}...", first_line.chars().take(100).collect::<String>())
424 } else {
425 first_line.to_string()
426 }
427}
428
429fn handle_tool_error(
430 state: &mut ActorState,
431 tool_use: &ToolUseData,
432 error: String,
433) -> Result<ToolCallResult> {
434 let short_message = create_short_message(&error);
435
436 let result = ToolResultData {
437 tool_use_id: tool_use.id.clone(),
438 content: error.clone(),
439 is_error: true,
440 };
441
442 info!(
443 tool_name = %tool_use.name,
444 ?result,
445 "Tool execution failed"
446 );
447
448 let event = ChatEvent::ToolExecutionCompleted {
449 tool_call_id: tool_use.id.clone(),
450 tool_name: tool_use.name.clone(),
451 tool_result: ToolExecutionResult::Error {
452 short_message,
453 detailed_message: error.clone(),
454 },
455 success: false,
456 error: Some(error),
457 };
458
459 state.event_sender.send(event);
460
461 Ok(ToolCallResult::immediate(
462 ContentBlock::ToolResult(result),
463 ContinuationPreference::Continue,
464 ))
465}
466
467async fn execute_deferred_action(state: &mut ActorState, action: DeferredAction) {
468 match action {
469 DeferredAction::PushAgent {
470 agent,
471 task,
472 tool_use_id,
473 agent_type,
474 } => {
475 execute_push_agent(state, agent, task, tool_use_id, agent_type).await;
476 }
477 DeferredAction::PopAgent {
478 success,
479 result,
480 tool_use_id,
481 } => {
482 execute_pop_agent(state, success, result, tool_use_id).await;
483 }
484 }
485}
486
487async fn execute_push_agent(
488 state: &mut ActorState,
489 agent: Arc<dyn Agent>,
490 task: String,
491 tool_use_id: String,
492 agent_type: String,
493) {
494 info!("Pushing new agent: task={}", task);
495
496 let initial_message = task.clone();
497
498 let mut new_agent = ActiveAgent::new(agent);
499
500 let spawn_mode = state.settings.settings().spawn_context_mode.clone();
502 if spawn_mode == SpawnContextMode::Fork {
503 if let Some(parent_conv) = state
504 .spawn_module
505 .with_current_agent(|a| a.conversation.clone())
506 {
507 new_agent.conversation = parent_conv;
508 }
509
510 let orientation = format!(
512 "--- AGENT TRANSITION ---\n\
513 You are now a {} sub-agent spawned to handle a specific task. \
514 The conversation above is from the parent agent - use it for context only. \
515 Focus on completing your assigned task below. \
516 When done, use complete_task to return control to the parent.",
517 agent_type
518 );
519 new_agent.conversation.push(Message {
520 role: MessageRole::User,
521 content: Content::text_only(orientation),
522 });
523 }
524
525 new_agent.conversation.push(Message {
526 role: MessageRole::User,
527 content: Content::text_only(initial_message.clone()),
528 });
529
530 state.spawn_module.push_agent(new_agent);
531
532 state.event_sender.send_message(ChatMessage::system(format!(
533 "🔄 Spawning agent for task: {task}"
534 )));
535
536 let tool_name = match agent_type.as_str() {
537 "coder" => "spawn_coder",
538 "recon" => "spawn_recon",
539 _ => "spawn_agent",
540 };
541
542 let event = ChatEvent::ToolExecutionCompleted {
543 tool_call_id: tool_use_id,
544 tool_name: tool_name.to_string(),
545 tool_result: ToolExecutionResult::Other {
546 result: json!({ "agent_type": agent_type, "task": task }),
547 },
548 success: true,
549 error: None,
550 };
551
552 state.event_sender.send(event);
553}
554
555async fn execute_pop_agent(
556 state: &mut ActorState,
557 success: bool,
558 result: String,
559 tool_use_id: String,
560) {
561 info!("Popping agent: success={}, result={}", success, result);
562
563 let event = ChatEvent::ToolRequest(ToolRequest {
564 tool_call_id: tool_use_id.clone(),
565 tool_name: "complete_task".to_string(),
566 tool_type: ToolRequestType::Other { args: json!({}) },
567 });
568 state.event_sender.send(event);
569
570 if state.spawn_module.stack_depth() <= 1 {
572 let event = ChatEvent::ToolExecutionCompleted {
573 tool_call_id: tool_use_id,
574 tool_name: "complete_task".to_string(),
575 tool_result: ToolExecutionResult::Other {
576 result: serde_json::to_value(&result).unwrap(),
577 },
578 success: true,
579 error: None,
580 };
581 state.event_sender.send(event);
582
583 state.event_sender.send_message(ChatMessage::system(format!(
584 "Task completed [success={success}]: {result}"
585 )));
586 return;
587 }
588
589 let current_agent_name = current_agent(state, |a| a.agent.name().to_string());
590 let review_enabled = state.settings.settings().review_level == ReviewLevel::Task;
591
592 if current_agent_name == CoderAgent::NAME && review_enabled && success {
593 info!("Intercepting coder completion to spawn review agent");
594
595 current_agent_mut(state, |a| a.completion_result = Some(result.clone()));
596
597 let event = ChatEvent::ToolExecutionCompleted {
598 tool_call_id: tool_use_id,
599 tool_name: "complete_task".to_string(),
600 tool_result: ToolExecutionResult::Other {
601 result: serde_json::to_value(&result).unwrap(),
602 },
603 success,
604 error: None,
605 };
606 state.event_sender.send(event);
607
608 let review_agent: Arc<dyn Agent> = Arc::new(CodeReviewAgent::new());
609 let review_task = format!(
610 "Review the code changes for the following completed task: {}",
611 result
612 );
613
614 let mut review_active = ActiveAgent::new(review_agent);
615 review_active.conversation.push(Message {
616 role: MessageRole::User,
617 content: Content::text_only(review_task.clone()),
618 });
619
620 state.spawn_module.push_agent(review_active);
621
622 state.event_sender.add_message(ChatMessage::system(
623 "🔍 Spawning review agent to validate code changes".to_string(),
624 ));
625 return;
626 }
627
628 if current_agent_name == CodeReviewAgent::NAME {
629 info!("Review agent completing: success={}", success);
630
631 state.spawn_module.pop_agent();
632
633 if success {
634 info!("Review approved, popping coder agent");
635
636 let coder_result = current_agent(state, |a| a.completion_result.clone())
637 .expect("completion_result must be set before review agent spawns");
638
639 state.spawn_module.pop_agent();
640
641 current_agent_mut(state, |a| {
642 a.conversation.push(Message {
643 role: MessageRole::User,
644 content: Content::text_only(format!(
645 "Code review feedback from the review agent: {}",
646 result
647 )),
648 })
649 });
650
651 state.event_sender.add_message(ChatMessage::system(format!(
652 "✅ Code review approved. Task completed: {}",
653 coder_result
654 )));
655 } else {
656 info!("Review rejected, sending feedback to coder");
657
658 current_agent_mut(state, |a| {
659 a.conversation.push(Message {
660 role: MessageRole::User,
661 content: Content::text_only(format!(
662 "Code review feedback from the review agent: {}",
663 result
664 )),
665 })
666 });
667
668 state.event_sender.add_message(ChatMessage::system(format!(
669 "❌ Code review rejected. Feedback sent to coder: {}",
670 result
671 )));
672 }
673
674 let event = ChatEvent::ToolExecutionCompleted {
675 tool_call_id: tool_use_id,
676 tool_name: "complete_task".to_string(),
677 tool_result: ToolExecutionResult::Other {
678 result: serde_json::to_value(&result).unwrap(),
679 },
680 success,
681 error: None,
682 };
683 state.event_sender.send(event);
684
685 return;
686 }
687
688 state.spawn_module.pop_agent();
689
690 current_agent_mut(state, |a| {
691 a.conversation.push(Message {
692 role: MessageRole::User,
693 content: Content::text_only(format!(
694 "Sub-agent completed [success={}]: {}",
695 success, result
696 )),
697 })
698 });
699
700 let result_message = if success {
701 format!("✅ Sub-agent completed successfully:\n{result}")
702 } else {
703 format!("❌ Sub-agent failed:\n{result}")
704 };
705
706 let event = ChatEvent::ToolExecutionCompleted {
707 tool_call_id: tool_use_id,
708 tool_name: "complete_task".to_string(),
709 tool_result: ToolExecutionResult::Other {
710 result: serde_json::to_value(&result).unwrap(),
711 },
712 success,
713 error: None,
714 };
715 state.event_sender.send(event);
716
717 state
718 .event_sender
719 .send_message(ChatMessage::system(result_message));
720}