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