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