vv_agent/runtime/tool_call_runner/
runner.rs1use crate::runtime::hooks::RuntimeHookManager;
2use crate::tools::ToolRegistry;
3use crate::types::ToolDirective;
4
5use super::outcome::ToolRunOutcome;
6use super::request::ToolRunRequest;
7use super::results::{
8 execute_tool_result, image_notification_from_tool_result, needs_tool_call_id,
9 skipped_tool_result,
10};
11
12pub struct ToolCallRunner {
13 tool_registry: ToolRegistry,
14 hook_manager: RuntimeHookManager,
15}
16
17impl ToolCallRunner {
18 pub fn new(tool_registry: ToolRegistry) -> Self {
19 Self {
20 tool_registry,
21 hook_manager: RuntimeHookManager::default(),
22 }
23 }
24
25 pub fn with_hook_manager(mut self, hook_manager: RuntimeHookManager) -> Self {
26 self.hook_manager = hook_manager;
27 self
28 }
29
30 pub fn run(&self, mut request: ToolRunRequest<'_>) -> Result<ToolRunOutcome, String> {
31 let mut directive_result = None;
32 let mut interruption_messages = Vec::new();
33 let mut image_notifications = Vec::new();
34
35 for (index, call) in request.tool_calls.iter().enumerate() {
36 if let Some(context) = request.execution_context {
37 context.check_cancelled()?;
38 }
39 let (patched_call, short_circuit_result) = self.hook_manager.apply_before_tool_call(
40 request.task,
41 request.context.cycle_index,
42 call.clone(),
43 request.context,
44 );
45 let mut result = match short_circuit_result {
46 Some(mut result) => {
47 if needs_tool_call_id(&result.tool_call_id) {
48 result.tool_call_id = call.id.clone();
49 }
50 result
51 }
52 None => {
53 let mut result =
54 execute_tool_result(&self.tool_registry, &patched_call, request.context);
55 if needs_tool_call_id(&result.tool_call_id) {
56 result.tool_call_id = patched_call.id.clone();
57 }
58 result
59 }
60 };
61 result = self.hook_manager.apply_after_tool_call(
62 request.task,
63 request.context.cycle_index,
64 &patched_call,
65 request.context,
66 result,
67 );
68 if needs_tool_call_id(&result.tool_call_id) {
69 result.tool_call_id = patched_call.id.clone();
70 }
71
72 request.messages.push(result.to_message());
73 if let Some(image_notification) =
74 image_notification_from_tool_result(&result, request.task.native_multimodal)
75 {
76 image_notifications.push(image_notification);
77 }
78 request.cycle_record.tool_results.push(result.clone());
79 if let Some(callback) = request.on_tool_result.as_deref_mut() {
80 callback(call, &result);
81 }
82
83 if result.directive != ToolDirective::Continue {
84 directive_result = Some(result.clone());
85 let (error_code, message) = match result.directive {
86 ToolDirective::WaitUser => (
87 "skipped_due_to_wait_user",
88 "Tool skipped because a previous tool requested user input.",
89 ),
90 ToolDirective::Finish => (
91 "skipped_due_to_finish",
92 "Tool skipped because a previous tool finished the task.",
93 ),
94 ToolDirective::Continue => ("skipped_due_to_directive", "Tool skipped."),
95 };
96 for skipped_call in request.tool_calls.iter().skip(index + 1) {
97 let skipped = skipped_tool_result(skipped_call, error_code, message);
98 request.messages.push(skipped.to_message());
99 request.cycle_record.tool_results.push(skipped.clone());
100 if let Some(callback) = request.on_tool_result.as_deref_mut() {
101 callback(skipped_call, &skipped);
102 }
103 }
104 break;
105 }
106
107 if let Some(provider) = request.interruption_provider {
108 let pending_messages = provider();
109 if !pending_messages.is_empty() {
110 interruption_messages.extend(pending_messages);
111 for skipped_call in request.tool_calls.iter().skip(index + 1) {
112 let skipped = skipped_tool_result(
113 skipped_call,
114 "skipped_due_to_steering",
115 "Tool skipped due to queued steering message.",
116 );
117 request.messages.push(skipped.to_message());
118 request.cycle_record.tool_results.push(skipped.clone());
119 if let Some(callback) = request.on_tool_result.as_deref_mut() {
120 callback(skipped_call, &skipped);
121 }
122 }
123 break;
124 }
125 }
126 }
127
128 request.messages.extend(image_notifications);
129 Ok(ToolRunOutcome {
130 directive_result,
131 interruption_messages,
132 })
133 }
134}