1use anyhow::Result;
8use std::time::{Duration, Instant};
9use tokio_util::sync::CancellationToken;
10
11use crate::approval::ApprovalManager;
12use crate::observability::{Observer, ObserverEvent};
13use crate::tools::Tool;
14
15use super::loop_::{ParsedToolCall, ToolLoopCancelled, scrub_credentials};
17
18pub fn find_tool<'a>(tools: &'a [Box<dyn Tool>], name: &str) -> Option<&'a dyn Tool> {
22 tools.iter().find(|t| t.name() == name).map(|t| t.as_ref())
23}
24
25pub struct ToolExecutionOutcome {
28 pub output: String,
29 pub success: bool,
30 pub error_reason: Option<String>,
31 pub duration: Duration,
32 pub receipt: Option<String>,
35}
36
37pub async fn execute_one_tool(
40 call_name: &str,
41 call_arguments: serde_json::Value,
42 tool_call_id: Option<&str>,
43 tools_registry: &[Box<dyn Tool>],
44 activated_tools: Option<&std::sync::Arc<std::sync::Mutex<crate::tools::ActivatedToolSet>>>,
45 observer: &dyn Observer,
46 cancellation_token: Option<&CancellationToken>,
47 receipt_generator: Option<&super::tool_receipts::ReceiptGenerator>,
48) -> Result<ToolExecutionOutcome> {
49 let full_args = call_arguments.to_string();
56 let tool_call_id_owned = tool_call_id.map(str::to_string);
57 observer.record_event(&ObserverEvent::ToolCallStart {
58 tool: call_name.to_string(),
59 tool_call_id: tool_call_id_owned.clone(),
60 arguments: Some(full_args.clone()),
61 });
62 let start = Instant::now();
63
64 let static_tool = find_tool(tools_registry, call_name);
65 let activated_arc = if static_tool.is_none() {
66 activated_tools.and_then(|at| at.lock().unwrap().get_resolved(call_name))
67 } else {
68 None
69 };
70 let Some(tool) = static_tool.or(activated_arc.as_deref()) else {
71 let reason = format!("Unknown tool: {call_name}");
72 let duration = start.elapsed();
73 let scrubbed_reason = scrub_credentials(&reason);
74 observer.record_event(&ObserverEvent::ToolCall {
75 tool: call_name.to_string(),
76 tool_call_id: tool_call_id_owned.clone(),
77 duration,
78 success: false,
79 arguments: Some(full_args.clone()),
80 result: Some(scrubbed_reason.clone()),
81 });
82 return Ok(ToolExecutionOutcome {
83 output: reason,
84 success: false,
85 error_reason: Some(scrubbed_reason),
86 duration,
87 receipt: None,
88 });
89 };
90
91 use ::zeroclaw_log::Instrument;
92 let tool_span = ::zeroclaw_log::info_span!(
93 target: "zeroclaw_log_internal_scope",
94 "zeroclaw_scope",
95 tool = %call_name,
96 );
97
98 let _start_guard = tool_span.clone().entered();
102 ::zeroclaw_log::record!(
103 INFO,
104 ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Invoke)
105 .with_category(::zeroclaw_log::EventCategory::Tool)
106 .with_attrs(::serde_json::json!({"input": call_arguments})),
107 "tool invocation start"
108 );
109 drop(_start_guard);
110
111 let tool_future = tool
112 .execute(call_arguments.clone())
113 .instrument(tool_span.clone());
114 let tool_result = if let Some(token) = cancellation_token {
115 tokio::select! {
116 () = token.cancelled() => return Err(ToolLoopCancelled.into()),
117 result = tool_future => result,
118 }
119 } else {
120 tool_future.await
121 };
122
123 let _result_guard = tool_span.entered();
124 match tool_result {
125 Ok(r) => {
126 let duration = start.elapsed();
127 if r.success {
128 ::zeroclaw_log::record!(
129 INFO,
130 ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Complete)
131 .with_category(::zeroclaw_log::EventCategory::Tool)
132 .with_outcome(::zeroclaw_log::EventOutcome::Success)
133 .with_duration(duration.as_millis() as u64)
134 .with_attrs(::serde_json::json!({"output": r.output})),
135 "tool invocation complete"
136 );
137 } else {
138 ::zeroclaw_log::record!(WARN, ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Fail).with_category(::zeroclaw_log::EventCategory::Tool).with_outcome(::zeroclaw_log::EventOutcome::Failure).with_duration(duration.as_millis() as u64).with_attrs(::serde_json::json!({"error": r.error.clone().unwrap_or_default(), "output": r.output})), "tool invocation failed");
139 }
140 if r.success {
141 let normalized_output = if r.output.is_empty() {
142 "(no output)"
143 } else {
144 &r.output
145 };
146 let output = scrub_credentials(normalized_output);
147 let receipt = receipt_generator.map(|receipt_gen| {
148 receipt_gen.generate_now(call_name, &call_arguments, &output)
149 });
150 observer.record_event(&ObserverEvent::ToolCall {
151 tool: call_name.to_string(),
152 tool_call_id: tool_call_id_owned.clone(),
153 duration,
154 success: true,
155 arguments: Some(full_args.clone()),
156 result: Some(output.clone()),
157 });
158 Ok(ToolExecutionOutcome {
159 output,
160 success: true,
161 error_reason: None,
162 duration,
163 receipt,
164 })
165 } else {
166 let reason = r.error.unwrap_or(r.output);
167 let scrubbed_reason = scrub_credentials(&reason);
168 observer.record_event(&ObserverEvent::ToolCall {
169 tool: call_name.to_string(),
170 tool_call_id: tool_call_id_owned.clone(),
171 duration,
172 success: false,
173 arguments: Some(full_args.clone()),
174 result: Some(scrubbed_reason.clone()),
175 });
176 Ok(ToolExecutionOutcome {
177 output: format!("Error: {reason}"),
178 success: false,
179 error_reason: Some(scrubbed_reason),
180 duration,
181 receipt: None,
182 })
183 }
184 }
185 Err(e) => {
186 let duration = start.elapsed();
187 ::zeroclaw_log::record!(
188 ERROR,
189 ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Fail)
190 .with_category(::zeroclaw_log::EventCategory::Tool)
191 .with_outcome(::zeroclaw_log::EventOutcome::Failure)
192 .with_duration(duration.as_millis() as u64)
193 .with_attrs(::serde_json::json!({"error": format!("{e:?}")})),
194 "tool invocation errored"
195 );
196 let reason = format!("Error executing {call_name}: {e}");
197 let scrubbed_reason = scrub_credentials(&reason);
198 observer.record_event(&ObserverEvent::ToolCall {
199 tool: call_name.to_string(),
200 tool_call_id: tool_call_id_owned.clone(),
201 duration,
202 success: false,
203 arguments: Some(full_args.clone()),
204 result: Some(scrubbed_reason.clone()),
205 });
206 Ok(ToolExecutionOutcome {
207 output: reason,
208 success: false,
209 error_reason: Some(scrubbed_reason),
210 duration,
211 receipt: None,
212 })
213 }
214 }
215}
216
217pub fn should_execute_tools_in_parallel(
220 tool_calls: &[ParsedToolCall],
221 approval: Option<&ApprovalManager>,
222) -> bool {
223 if tool_calls.len() <= 1 {
224 return false;
225 }
226
227 if tool_calls.iter().any(|call| call.name == "tool_search") {
232 return false;
233 }
234
235 if let Some(mgr) = approval
236 && tool_calls.iter().any(|call| mgr.needs_approval(&call.name))
237 {
238 return false;
241 }
242
243 true
244}
245
246pub async fn execute_tools_parallel(
249 tool_calls: &[ParsedToolCall],
250 tools_registry: &[Box<dyn Tool>],
251 activated_tools: Option<&std::sync::Arc<std::sync::Mutex<crate::tools::ActivatedToolSet>>>,
252 observer: &dyn Observer,
253 cancellation_token: Option<&CancellationToken>,
254 receipt_generator: Option<&super::tool_receipts::ReceiptGenerator>,
255) -> Result<Vec<ToolExecutionOutcome>> {
256 let futures: Vec<_> = tool_calls
257 .iter()
258 .map(|call| {
259 execute_one_tool(
260 &call.name,
261 call.arguments.clone(),
262 call.tool_call_id.as_deref(),
263 tools_registry,
264 activated_tools,
265 observer,
266 cancellation_token,
267 receipt_generator,
268 )
269 })
270 .collect();
271
272 let results = futures_util::future::join_all(futures).await;
273 results.into_iter().collect()
274}
275
276pub async fn execute_tools_sequential(
279 tool_calls: &[ParsedToolCall],
280 tools_registry: &[Box<dyn Tool>],
281 activated_tools: Option<&std::sync::Arc<std::sync::Mutex<crate::tools::ActivatedToolSet>>>,
282 observer: &dyn Observer,
283 cancellation_token: Option<&CancellationToken>,
284 receipt_generator: Option<&super::tool_receipts::ReceiptGenerator>,
285) -> Result<Vec<ToolExecutionOutcome>> {
286 let mut outcomes = Vec::with_capacity(tool_calls.len());
287
288 for call in tool_calls {
289 outcomes.push(
290 execute_one_tool(
291 &call.name,
292 call.arguments.clone(),
293 call.tool_call_id.as_deref(),
294 tools_registry,
295 activated_tools,
296 observer,
297 cancellation_token,
298 receipt_generator,
299 )
300 .await?,
301 );
302 }
303
304 Ok(outcomes)
305}