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