Skip to main content

vv_agent/runtime/tool_call_runner/
runner.rs

1use 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}