1mod agent_dir_script_tool;
13mod artifacts;
14pub(crate) mod builtin;
15mod immutable_content;
16mod invocation;
17mod pagination;
18mod presentation;
19pub(crate) mod process;
20mod program_tool;
21mod registry;
22mod result_transform;
23mod selector;
24pub mod skill;
25pub mod task;
26mod types;
27
28pub use crate::dynamic_workflow::register_dynamic_workflow;
29pub use agent_dir_script_tool::AgentDirScriptTool;
30pub use artifacts::{ArtifactStore, ArtifactStoreLimits, ToolArtifact};
31pub use builtin::{
32 register_generate_object, register_program, register_program_with_catalog, register_task,
33 register_task_with_mcp, register_task_with_mcp_managers,
34};
35pub(crate) use builtin::{
36 register_skill, register_task_with_mcp_managers_and_scheduler,
37 register_task_with_mcp_sources_and_scheduler,
38};
39pub use immutable_content::{
40 ImmutableContentAdapter, ImmutableContentAdapterBindingV1, ImmutableContentAdapterSession,
41 ImmutableContentDescriptorV1, ImmutableContentError, ImmutableContentKindV1,
42 ImmutableContentReferenceV1, ImmutableContentResult, ImmutableContentWriteRequestV1,
43 IMMUTABLE_CONTENT_ADAPTER_BINDING_SCHEMA_V1, IMMUTABLE_CONTENT_DESCRIPTOR_SCHEMA_V1,
44 IMMUTABLE_CONTENT_REFERENCE_SCHEMA_V1, TOOL_RESULT_CONTENT_MEDIA_TYPE,
45};
46pub(crate) use invocation::{
47 registry_tool_invoker, HostDirectPolicy, InvocationOrigin, ToolInvocation, ToolInvoker,
48};
49pub(crate) use presentation::{
50 canonical_source as canonical_presentation_source, estimated_definition_tokens,
51 is_definition_subset,
52};
53pub use presentation::{
54 ToolPresentationError, ToolPresentationModeV1, ToolPresentationProfileV1,
55 TOOL_PRESENTATION_PROFILE_V1_SCHEMA,
56};
57pub use program_tool::{ProgramTool, MAX_PROGRAM_SCRIPT_SOURCE_BYTES};
58pub use registry::ToolRegistry;
59pub(crate) use registry::ToolRegistrySnapshotError;
60pub use result_transform::{
61 ToolResultTransformBindingV1, ToolResultTransformPolicyV1, TOOL_RESULT_TRANSFORM_ALGORITHM_V1,
62 TOOL_RESULT_TRANSFORM_BINDING_METADATA_KEY, TOOL_RESULT_TRANSFORM_BINDING_SCHEMA_V1,
63 TOOL_RESULT_TRANSFORM_POLICY_DIGEST_DOMAIN_V1, TOOL_RESULT_TRANSFORM_SCHEMA_V1,
64};
65pub(crate) use selector::is_standalone_conversation;
66pub use selector::{select_tools_for_messages, select_tools_for_prompt};
67pub use task::{
68 parallel_task_params_schema, task_params_schema, ParallelTaskParams, ParallelTaskTool,
69 TaskExecutor, TaskParams, TaskResult, TaskTool,
70};
71pub(crate) use types::{AgentEventBarrier, AgentEventBarrierReceiver};
72pub use types::{
73 InvocationRuntime, Tool, ToolCapabilities, ToolContext, ToolErrorKind, ToolEventSender,
74 ToolOutput, ToolOutputKind, ToolStreamEvent,
75};
76
77use crate::llm::ToolDefinition;
78use anyhow::Result;
79use serde::{Deserialize, Serialize};
80use sha2::{Digest, Sha256};
81use std::collections::HashMap;
82use std::path::PathBuf;
83use std::sync::Arc;
84
85pub const MAX_OUTPUT_SIZE: usize = 100 * 1024; pub const MAX_READ_LINES: usize = 2000;
90
91pub const MAX_LINE_LENGTH: usize = 2000;
93
94#[derive(Debug, Clone, PartialEq, Eq)]
95pub(crate) struct ToolOutputArtifact {
96 pub artifact_id: String,
97 pub artifact_uri: String,
98 pub original_bytes: usize,
99 pub shown_bytes: usize,
100 pub content_reference: Option<ImmutableContentReferenceV1>,
101}
102
103#[derive(Debug, Clone)]
104pub(crate) struct TruncatedToolOutput {
105 pub content: String,
106 pub artifact: Option<ToolOutputArtifact>,
107 pub loss_mode: ToolResultLossModeV1,
108}
109
110#[cfg(test)]
111pub(crate) fn truncate_tool_output_with_artifact(
112 tool_name: &str,
113 output: &str,
114) -> TruncatedToolOutput {
115 transform_tool_output_with_artifact(
116 tool_name,
117 output,
118 &ToolResultTransformPolicyV1::conservative(),
119 )
120}
121
122pub(crate) fn transform_tool_output_with_artifact(
123 tool_name: &str,
124 output: &str,
125 policy: &ToolResultTransformPolicyV1,
126) -> TruncatedToolOutput {
127 let transformed = result_transform::transform(output, policy);
128 if transformed.loss_mode == ToolResultLossModeV1::None {
129 return TruncatedToolOutput {
130 content: transformed.content,
131 artifact: None,
132 loss_mode: transformed.loss_mode,
133 };
134 }
135 let artifact = tool_output_artifact(tool_name, output, transformed.retained_original_bytes);
136 let artifact_uri = artifact.artifact_uri.clone();
137 let content = format!(
138 "{}\n\n[Full output artifact: {artifact_uri}]",
139 transformed.content
140 );
141
142 TruncatedToolOutput {
143 content,
144 artifact: Some(artifact),
145 loss_mode: transformed.loss_mode,
146 }
147}
148
149pub(crate) fn tool_output_artifact(
150 tool_name: &str,
151 output: &str,
152 shown_bytes: usize,
153) -> ToolOutputArtifact {
154 let digest = format!("{:x}", Sha256::digest(output.as_bytes()));
155 let sanitized_tool = tool_name
156 .chars()
157 .map(|ch| {
158 if ch.is_ascii_alphanumeric() || ch == '_' || ch == '-' {
159 ch
160 } else {
161 '_'
162 }
163 })
164 .collect::<String>();
165 let artifact_id = format!("tool-output:{sanitized_tool}:{digest}");
166 let artifact_uri = format!("a3s://tool-output/{sanitized_tool}/{digest}");
167
168 ToolOutputArtifact {
169 artifact_id,
170 artifact_uri,
171 original_bytes: output.len(),
172 shown_bytes,
173 content_reference: None,
174 }
175}
176
177pub(crate) fn merge_tool_output_artifact_metadata(
178 metadata: Option<serde_json::Value>,
179 artifact: &ToolOutputArtifact,
180) -> serde_json::Value {
181 let mut artifact_json = serde_json::json!({
182 "artifact_id": artifact.artifact_id,
183 "artifact_uri": artifact.artifact_uri,
184 "original_bytes": artifact.original_bytes,
185 "shown_bytes": artifact.shown_bytes,
186 });
187 if let Some(reference) = &artifact.content_reference {
188 artifact_json["content_reference"] = serde_json::json!(reference);
189 }
190
191 match metadata {
192 Some(serde_json::Value::Object(mut object)) => {
193 object.insert("artifact".to_string(), artifact_json);
194 serde_json::Value::Object(object)
195 }
196 Some(value) => serde_json::json!({
197 "artifact": artifact_json,
198 "previous_metadata": value,
199 }),
200 None => serde_json::json!({
201 "artifact": artifact_json,
202 }),
203 }
204}
205
206pub const TOOL_RESULT_EVIDENCE_SCHEMA_V1: &str = "a3s.code.tool-result-evidence.v1";
207pub const TOOL_RESULT_TOKEN_ESTIMATOR_V1: &str = "utf8-bytes-ceil-div-4/v1";
208
209#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
210#[serde(deny_unknown_fields)]
211pub struct ToolResultEvidenceV1 {
212 pub schema: String,
213 pub original_bytes: usize,
214 pub projected_bytes: usize,
215 pub original_estimated_tokens: usize,
216 pub projected_estimated_tokens: usize,
217 pub token_estimator: String,
218 #[serde(default, skip_serializing_if = "Option::is_none")]
221 pub transform_algorithm: Option<String>,
222 pub content_digest: String,
223 #[serde(default, skip_serializing_if = "Option::is_none")]
224 pub projected_digest: Option<String>,
225 pub repeat_key: String,
226 pub content_ref: String,
227 pub loss_mode: ToolResultLossModeV1,
228 #[serde(default, skip_serializing_if = "Option::is_none")]
229 pub byte_delta: Option<i64>,
230 #[serde(default, skip_serializing_if = "Option::is_none")]
231 pub estimated_token_delta: Option<i64>,
232}
233
234#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
235#[serde(rename_all = "snake_case")]
236pub enum ToolResultLossModeV1 {
237 None,
238 BoundedPreview,
239 HeadTail,
240 DeterministicTransform,
241 Composite,
242}
243
244pub(crate) fn attach_tool_result_evidence(
245 metadata: Option<serde_json::Value>,
246 original: &str,
247 projected: &str,
248 loss_mode: ToolResultLossModeV1,
249) -> serde_json::Value {
250 let digest = format!("sha256:{:x}", Sha256::digest(original.as_bytes()));
251 let projected_digest = format!("sha256:{:x}", Sha256::digest(projected.as_bytes()));
252 let artifact_uri = metadata
253 .as_ref()
254 .and_then(|value| value.pointer("/artifact/artifact_uri"))
255 .and_then(serde_json::Value::as_str);
256 let evidence = ToolResultEvidenceV1 {
257 schema: TOOL_RESULT_EVIDENCE_SCHEMA_V1.to_string(),
258 original_bytes: original.len(),
259 projected_bytes: projected.len(),
260 original_estimated_tokens: estimated_text_tokens(original),
261 projected_estimated_tokens: estimated_text_tokens(projected),
262 token_estimator: TOOL_RESULT_TOKEN_ESTIMATOR_V1.to_string(),
263 transform_algorithm: Some(TOOL_RESULT_TRANSFORM_ALGORITHM_V1.to_string()),
264 content_digest: digest.clone(),
265 projected_digest: Some(projected_digest),
266 repeat_key: digest.clone(),
267 content_ref: artifact_uri
268 .map(str::to_owned)
269 .unwrap_or_else(|| format!("inline:{digest}")),
270 loss_mode,
271 byte_delta: Some(signed_delta(projected.len(), original.len())),
272 estimated_token_delta: Some(signed_delta(
273 estimated_text_tokens(projected),
274 estimated_text_tokens(original),
275 )),
276 };
277 let evidence = serde_json::json!({
278 "schema": evidence.schema,
279 "original_bytes": evidence.original_bytes,
280 "projected_bytes": evidence.projected_bytes,
281 "original_estimated_tokens": evidence.original_estimated_tokens,
282 "projected_estimated_tokens": evidence.projected_estimated_tokens,
283 "token_estimator": evidence.token_estimator,
284 "transform_algorithm": evidence.transform_algorithm,
285 "content_digest": evidence.content_digest,
286 "projected_digest": evidence.projected_digest,
287 "repeat_key": evidence.repeat_key,
288 "content_ref": evidence.content_ref,
289 "loss_mode": evidence.loss_mode,
290 "byte_delta": evidence.byte_delta,
291 "estimated_token_delta": evidence.estimated_token_delta,
292 });
293 match metadata {
294 Some(serde_json::Value::Object(mut object)) => {
295 object.insert("a3s_tool_result_evidence".to_string(), evidence);
296 serde_json::Value::Object(object)
297 }
298 Some(value) => serde_json::json!({
299 "a3s_tool_result_evidence": evidence,
300 "previous_metadata": value,
301 }),
302 None => serde_json::json!({"a3s_tool_result_evidence": evidence}),
303 }
304}
305
306pub(crate) fn attach_tool_result_evidence_with_transform_binding(
307 metadata: Option<serde_json::Value>,
308 original: &str,
309 projected: &str,
310 loss_mode: ToolResultLossModeV1,
311 transform_binding: &ToolResultTransformBindingV1,
312) -> Result<serde_json::Value> {
313 transform_binding.validate()?;
314 let mut metadata = attach_tool_result_evidence(metadata, original, projected, loss_mode);
315 let encoded_binding = serde_json::to_value(transform_binding)?;
316 let serde_json::Value::Object(object) = &mut metadata else {
317 anyhow::bail!("Tool result evidence metadata must be an object");
318 };
319 object.insert(
320 TOOL_RESULT_TRANSFORM_BINDING_METADATA_KEY.to_string(),
321 encoded_binding,
322 );
323 Ok(metadata)
324}
325
326pub(crate) fn ensure_tool_result_evidence(
327 metadata: Option<serde_json::Value>,
328 output: &str,
329) -> serde_json::Value {
330 match metadata {
331 Some(value) if value.get("a3s_tool_result_evidence").is_some() => value,
332 metadata => {
333 attach_tool_result_evidence(metadata, output, output, ToolResultLossModeV1::None)
334 }
335 }
336}
337
338pub(crate) fn ensure_tool_result_evidence_with_transform_binding(
339 metadata: Option<serde_json::Value>,
340 output: &str,
341 transform_binding: &ToolResultTransformBindingV1,
342) -> Result<serde_json::Value> {
343 if let Some(value) = metadata {
344 if value.get("a3s_tool_result_evidence").is_some() {
345 if let Some(encoded_binding) = value.get(TOOL_RESULT_TRANSFORM_BINDING_METADATA_KEY) {
346 let retained: ToolResultTransformBindingV1 =
347 serde_json::from_value(encoded_binding.clone())?;
348 retained.validate()?;
349 anyhow::ensure!(
350 &retained == transform_binding,
351 "Tool result transform binding drifted before result release"
352 );
353 return Ok(value);
354 }
355 }
356 return attach_tool_result_evidence_with_transform_binding(
357 Some(value),
358 output,
359 output,
360 ToolResultLossModeV1::None,
361 transform_binding,
362 );
363 }
364
365 attach_tool_result_evidence_with_transform_binding(
366 None,
367 output,
368 output,
369 ToolResultLossModeV1::None,
370 transform_binding,
371 )
372}
373
374pub(crate) fn has_tool_metadata_beyond_evidence(metadata: Option<&serde_json::Value>) -> bool {
375 match metadata {
376 None => false,
377 Some(serde_json::Value::Object(object)) => object.keys().any(|key| {
378 key != "a3s_tool_result_evidence" && key != TOOL_RESULT_TRANSFORM_BINDING_METADATA_KEY
379 }),
380 Some(_) => true,
381 }
382}
383
384fn estimated_text_tokens(value: &str) -> usize {
385 value.len().saturating_add(3) / 4
386}
387
388fn signed_delta(value: usize, baseline: usize) -> i64 {
389 if value >= baseline {
390 i64::try_from(value - baseline).unwrap_or(i64::MAX)
391 } else {
392 -i64::try_from(baseline - value).unwrap_or(i64::MAX)
393 }
394}
395
396#[derive(Debug, Clone, Serialize, Deserialize)]
398pub struct ToolResult {
399 pub name: String,
400 pub output: String,
401 pub exit_code: i32,
402 #[serde(skip_serializing_if = "Option::is_none")]
403 pub metadata: Option<serde_json::Value>,
404 #[serde(skip)]
406 pub images: Vec<crate::llm::Attachment>,
407 #[serde(skip_serializing_if = "Option::is_none")]
413 pub error_kind: Option<types::ToolErrorKind>,
414}
415
416impl ToolResult {
417 pub fn success(name: &str, output: String) -> Self {
418 Self {
419 name: name.to_string(),
420 output,
421 exit_code: 0,
422 metadata: None,
423 images: Vec::new(),
424 error_kind: None,
425 }
426 }
427
428 pub fn error(name: &str, message: String) -> Self {
429 Self {
430 name: name.to_string(),
431 output: message,
432 exit_code: 1,
433 metadata: None,
434 images: Vec::new(),
435 error_kind: None,
436 }
437 }
438
439 pub fn error_with_kind(name: &str, message: String, kind: types::ToolErrorKind) -> Self {
440 let mut result = Self::error(name, message);
441 result.error_kind = Some(kind);
442 result
443 }
444}
445
446impl From<ToolOutput> for ToolResult {
447 fn from(output: ToolOutput) -> Self {
448 Self {
449 name: String::new(),
450 output: output.content,
451 exit_code: if output.success { 0 } else { 1 },
452 metadata: output.metadata,
453 images: output.images,
454 error_kind: output.error_kind,
455 }
456 }
457}
458
459pub struct ToolExecutor {
463 workspace: PathBuf,
464 registry: Arc<ToolRegistry>,
465 command_env: Option<Arc<HashMap<String, String>>>,
466}
467
468fn redacted_tool_log_summary(name: &str, args: &serde_json::Value) -> String {
478 let arg_keys: Vec<&str> = match args.as_object() {
479 Some(map) => {
480 let mut keys: Vec<&str> = map.keys().map(String::as_str).collect();
481 keys.sort_unstable();
482 keys
483 }
484 None => Vec::new(),
485 };
486 format!(
487 "Executing tool: {} (arg_keys={:?}, {} bytes)",
488 name,
489 arg_keys,
490 args.to_string().len()
491 )
492}
493
494fn log_tool_invocation(name: &str, args: &serde_json::Value) {
497 tracing::info!("{}", redacted_tool_log_summary(name, args));
498}
499
500impl ToolExecutor {
501 pub fn new(workspace: String) -> Self {
502 let workspace_services =
503 crate::workspace::WorkspaceServices::local(PathBuf::from(&workspace));
504 Self::build(
505 workspace,
506 None,
507 ArtifactStoreLimits::default(),
508 workspace_services,
509 None,
510 )
511 }
512
513 pub fn new_with_artifact_limits(
514 workspace: String,
515 artifact_limits: ArtifactStoreLimits,
516 ) -> Self {
517 let workspace_services =
518 crate::workspace::WorkspaceServices::local(PathBuf::from(&workspace));
519 Self::build(workspace, None, artifact_limits, workspace_services, None)
520 }
521
522 pub fn new_with_workspace_services(
523 workspace: String,
524 workspace_services: Arc<crate::workspace::WorkspaceServices>,
525 ) -> Self {
526 Self::build(
527 workspace,
528 None,
529 ArtifactStoreLimits::default(),
530 workspace_services,
531 None,
532 )
533 }
534
535 pub fn new_with_workspace_services_and_artifact_limits(
536 workspace: String,
537 workspace_services: Arc<crate::workspace::WorkspaceServices>,
538 artifact_limits: ArtifactStoreLimits,
539 ) -> Self {
540 Self::build(workspace, None, artifact_limits, workspace_services, None)
541 }
542
543 pub fn new_with_immutable_content_adapter(
546 workspace: String,
547 adapter: ImmutableContentAdapterSession,
548 ) -> Self {
549 let workspace_services =
550 crate::workspace::WorkspaceServices::local(PathBuf::from(&workspace));
551 Self::build(
552 workspace,
553 None,
554 ArtifactStoreLimits::default(),
555 workspace_services,
556 Some(adapter),
557 )
558 }
559
560 pub(crate) fn new_with_workspace_services_artifact_limits_and_immutable_content_adapter(
561 workspace: String,
562 workspace_services: Arc<crate::workspace::WorkspaceServices>,
563 artifact_limits: ArtifactStoreLimits,
564 adapter: Option<ImmutableContentAdapterSession>,
565 ) -> Self {
566 Self::build(
567 workspace,
568 None,
569 artifact_limits,
570 workspace_services,
571 adapter,
572 )
573 }
574
575 fn build(
576 workspace: String,
577 command_env: Option<HashMap<String, String>>,
578 artifact_limits: ArtifactStoreLimits,
579 workspace_services: Arc<crate::workspace::WorkspaceServices>,
580 immutable_content_adapter: Option<ImmutableContentAdapterSession>,
581 ) -> Self {
582 let workspace_path = PathBuf::from(&workspace);
583 let command_env = command_env.map(Arc::new);
584 let registry = Arc::new(
585 ToolRegistry::with_workspace_services_artifact_limits_and_immutable_content_adapter(
586 workspace_path.clone(),
587 artifact_limits,
588 Arc::clone(&workspace_services),
589 immutable_content_adapter,
590 ),
591 );
592 if let Some(env) = command_env.clone() {
593 registry.set_command_env(env);
594 }
595
596 builtin::register_builtins(®istry, &workspace_services);
600 builtin::register_batch(®istry);
602 builtin::register_program(®istry);
603
604 Self {
605 workspace: workspace_path,
606 registry,
607 command_env,
608 }
609 }
610
611 fn check_workspace_boundary(
612 name: &str,
613 args: &serde_json::Value,
614 ctx: &ToolContext,
615 ) -> Result<()> {
616 let path_field = match name {
617 "read" | "write" | "edit" | "patch" | "download" => Some("file_path"),
618 "ls" | "search" | "code_symbols" | "code_navigation" | "code_diagnostics" => {
619 Some("path")
620 }
621 _ => None,
622 };
623
624 if let Some(field) = path_field {
625 if let Some(path_str) = args.get(field).and_then(|v| v.as_str()) {
626 ctx.resolve_workspace_path(path_str).map_err(|e| {
627 anyhow::anyhow!(
628 "Workspace boundary check failed for tool '{}' path '{}': {}",
629 name,
630 path_str,
631 e
632 )
633 })?;
634 }
635 }
636
637 Ok(())
638 }
639
640 pub fn workspace(&self) -> &PathBuf {
641 &self.workspace
642 }
643
644 pub fn registry(&self) -> &Arc<ToolRegistry> {
645 &self.registry
646 }
647
648 pub(crate) fn snapshot_with_external_tools(
649 &self,
650 external: impl IntoIterator<Item = Arc<dyn Tool>>,
651 ) -> Result<Self, ToolRegistrySnapshotError> {
652 Ok(Self {
653 workspace: self.workspace.clone(),
654 registry: Arc::new(self.registry.snapshot_with_external_tools(external)?),
655 command_env: self.command_env.clone(),
656 })
657 }
658
659 pub fn get_artifact(&self, artifact_uri: &str) -> Option<ToolArtifact> {
661 self.registry.get_artifact(artifact_uri)
662 }
663
664 pub fn artifact_store(&self) -> ArtifactStore {
666 self.registry.artifact_store()
667 }
668
669 pub fn set_trace_sink(&self, sink: Arc<dyn crate::trace::TraceSink>) {
671 self.registry.set_trace_sink(sink);
672 }
673
674 pub fn trace_sink(&self) -> Arc<dyn crate::trace::TraceSink> {
676 self.registry.trace_sink()
677 }
678
679 pub fn command_env(&self) -> Option<Arc<HashMap<String, String>>> {
680 self.command_env.clone()
681 }
682
683 pub fn register_dynamic_tool(&self, tool: Arc<dyn Tool>) {
684 self.registry.register(tool);
685 }
686
687 pub(crate) fn register_dynamic_tool_with_shadow(
688 &self,
689 tool: Arc<dyn Tool>,
690 ) -> (bool, Option<Arc<dyn Tool>>) {
691 self.registry.register_with_shadow(tool)
692 }
693
694 pub(crate) fn restore_dynamic_tool_if_same(
695 &self,
696 name: &str,
697 expected: &Arc<dyn Tool>,
698 replacement: Option<Arc<dyn Tool>>,
699 ) -> bool {
700 self.registry.restore_if_same(name, expected, replacement)
701 }
702
703 pub(crate) fn register_dynamic_tool_if_absent(&self, tool: Arc<dyn Tool>) -> bool {
704 self.registry.register_if_absent(tool)
705 }
706
707 pub fn unregister_dynamic_tool(&self, name: &str) {
708 self.registry.unregister(name);
709 }
710
711 pub fn unregister_tools_by_prefix(&self, prefix: &str) {
713 self.registry.unregister_by_prefix(prefix);
714 }
715
716 pub fn register_program_catalog(&self, catalog: crate::program::ProgramCatalog) {
718 builtin::register_program_with_catalog(&self.registry, catalog);
719 }
720
721 pub async fn execute(&self, name: &str, args: &serde_json::Value) -> Result<ToolResult> {
730 let ctx = self.registry.context();
731 if let Err(e) = Self::check_workspace_boundary(name, args, &ctx) {
732 return Ok(ToolResult::error(name, e.to_string()));
733 }
734
735 log_tool_invocation(name, args);
736 let mut result = self.registry.execute_with_context(name, args, &ctx).await;
737 if let Ok(ref mut r) = result {
738 self.attach_diff_metadata(name, args, r);
739 }
740 match &result {
741 Ok(r) => tracing::info!("Tool {} completed with exit_code={}", name, r.exit_code),
742 Err(e) => tracing::error!("Tool {} failed: {}", name, e),
743 }
744 result
745 }
746
747 pub async fn execute_with_context(
753 &self,
754 name: &str,
755 args: &serde_json::Value,
756 ctx: &ToolContext,
757 ) -> Result<ToolResult> {
758 Self::check_workspace_boundary(name, args, ctx)?;
759 log_tool_invocation(name, args);
760 let mut result = self.registry.execute_with_context(name, args, ctx).await;
761 if let Ok(ref mut r) = result {
762 self.attach_diff_metadata(name, args, r);
763 }
764 match &result {
765 Ok(r) => tracing::info!("Tool {} completed with exit_code={}", name, r.exit_code),
766 Err(e) => tracing::error!("Tool {} failed: {}", name, e),
767 }
768 result
769 }
770
771 fn attach_diff_metadata(&self, name: &str, args: &serde_json::Value, result: &mut ToolResult) {
772 if !matches!(name, "write" | "edit" | "patch") {
773 return;
774 }
775 let Some(file_path) = args.get("file_path").and_then(serde_json::Value::as_str) else {
776 return;
777 };
778 let meta = result.metadata.get_or_insert_with(|| serde_json::json!({}));
781 meta["file_path"] = serde_json::Value::String(file_path.to_string());
782 }
783
784 pub fn definitions(&self) -> Vec<ToolDefinition> {
785 self.registry.definitions()
786 }
787}
788
789#[cfg(test)]
790#[path = "tests.rs"]
791mod tests;