Skip to main content

tycode_core/chat/
tools.rs

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
59// Helper functions for ActorState - delegate to spawn_module (closure-only API)
60pub 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
80/// Find the minimum category from a list of tool calls
81fn 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            // Look up the tool executor and get its category
89            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    // Separate AlwaysAllowed tools from other tool calls
102    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 there are no other calls, just return the AlwaysAllowed ones
118    if other_calls.is_empty() {
119        return (always_allowed_calls, vec![]);
120    }
121
122    // Find minimum category among non-AlwaysAllowed tools
123    let minimum_category = match find_minimum_category(&other_calls, tool_registry) {
124        Some(cat) => cat,
125        None => {
126            // If we can't find a minimum, return all calls
127            let mut all_calls = always_allowed_calls;
128            all_calls.extend(other_calls);
129            return (all_calls, vec![]);
130        }
131    };
132
133    // Store the original calls before filtering
134    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        // Generate error responses for dropped calls using handle_tool_error
157        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    // Combine AlwaysAllowed tools with filtered tools
183    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    // Get allowed tools for security checks
203    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    // Filter tool calls by minimum category
212    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    // Initialize preferences vector early to track all error and success preferences
217    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    // Implement truth table for continuation preferences:
346    // - Any Stop → stop conversation
347    // - Otherwise, any Continue → continue conversation
348    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    // Combine invalid tool error responses with valid tool execution results
360    all_results.extend(invalid_tool_results);
361    all_results.extend(results);
362
363    // Add all tool results as a single message
364    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        // XML mode: Convert ToolResult blocks to XML text to avoid Bedrock's toolConfig requirement
370        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    // Execute deferred actions after conversation update
389    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    // Why: Fork mode copies parent conversation for continuity; Fresh mode starts clean
501    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        // Orientation message helps spawned agent understand its context
511        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    // Don't pop if we're at the root agent
571    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}