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