1use super::artifacts::{ArtifactStore, ArtifactStoreLimits, ToolArtifact};
7use super::types::{Tool, ToolCapabilities, ToolContext, ToolOutput};
8use super::ToolResult;
9use super::{
10 merge_tool_output_artifact_metadata, tool_output_artifact, transform_tool_output_with_artifact,
11 ImmutableContentAdapterSession, ImmutableContentError, ImmutableContentKindV1,
12 ToolOutputArtifact, ToolResultTransformBindingV1, ToolResultTransformPolicyV1,
13 TOOL_RESULT_CONTENT_MEDIA_TYPE,
14};
15use crate::llm::ToolDefinition;
16use crate::trace::{InMemoryTraceSink, TraceEvent, TraceSink};
17use anyhow::Result;
18use std::collections::HashMap;
19use std::hash::{Hash, Hasher};
20use std::path::PathBuf;
21use std::sync::{Arc, RwLock};
22use thiserror::Error;
23
24const MAX_TOOL_SCHEMA_BYTES: usize = 256 * 1024;
25const MAX_ARGUMENT_VALIDATION_ERRORS: usize = 8;
26const MAX_ARGUMENT_VALIDATION_MESSAGE_BYTES: usize = 4 * 1024;
27const MAX_INLINE_CHANGE_BYTES: usize = 64 * 1024;
28const CHANGE_SIDE_PREVIEW_BYTES: usize = 8 * 1024;
29const CHANGE_DIFF_PREVIEW_BYTES: usize = 32 * 1024;
30const MAX_DIFF_COMPUTE_BYTES: usize = 1024 * 1024;
31
32#[derive(Debug, Error)]
33#[error("projected tool name '{name}' conflicts with the compatibility registry")]
34pub(crate) struct ToolRegistrySnapshotError {
35 name: String,
36}
37
38impl ToolRegistrySnapshotError {
39 pub(crate) fn name(&self) -> &str {
40 &self.name
41 }
42}
43
44#[derive(Clone)]
45enum CachedArgumentValidator {
46 Valid(Arc<jsonschema::Validator>),
47 Invalid(String),
48}
49
50#[derive(Clone)]
51struct ArgumentValidatorCacheEntry {
52 schema_fingerprint: u64,
53 validator: CachedArgumentValidator,
54}
55
56pub struct ToolRegistry {
58 tools: RwLock<HashMap<String, Arc<dyn Tool>>>,
59 builtins: RwLock<std::collections::HashSet<String>>,
61 context: RwLock<ToolContext>,
62 artifact_store: ArtifactStore,
63 immutable_content_adapter: Option<ImmutableContentAdapterSession>,
64 trace_sink: RwLock<Arc<dyn TraceSink>>,
65 argument_validators: RwLock<HashMap<String, ArgumentValidatorCacheEntry>>,
66 transform_policy: RwLock<ToolResultTransformPolicyV1>,
67}
68
69struct PreparedToolOutput {
76 output: Option<ToolOutput>,
77 transform_binding: ToolResultTransformBindingV1,
78}
79
80impl ToolRegistry {
81 pub fn new(workspace: PathBuf) -> Self {
83 Self::with_artifact_limits(workspace, ArtifactStoreLimits::default())
84 }
85
86 pub fn with_artifact_limits(workspace: PathBuf, artifact_limits: ArtifactStoreLimits) -> Self {
88 Self::with_artifact_limits_and_workspace_services(
89 workspace.clone(),
90 artifact_limits,
91 crate::workspace::WorkspaceServices::local(workspace),
92 )
93 }
94
95 pub fn with_artifact_limits_and_workspace_services(
97 workspace: PathBuf,
98 artifact_limits: ArtifactStoreLimits,
99 workspace_services: Arc<crate::workspace::WorkspaceServices>,
100 ) -> Self {
101 Self::with_workspace_services_artifact_limits_and_immutable_content_adapter(
102 workspace,
103 artifact_limits,
104 workspace_services,
105 None,
106 )
107 }
108
109 pub(crate) fn with_workspace_services_artifact_limits_and_immutable_content_adapter(
110 workspace: PathBuf,
111 artifact_limits: ArtifactStoreLimits,
112 workspace_services: Arc<crate::workspace::WorkspaceServices>,
113 immutable_content_adapter: Option<ImmutableContentAdapterSession>,
114 ) -> Self {
115 let context = ToolContext::new(workspace).with_workspace_services(workspace_services);
116 Self {
117 tools: RwLock::new(HashMap::new()),
118 builtins: RwLock::new(std::collections::HashSet::new()),
119 context: RwLock::new(context),
120 artifact_store: ArtifactStore::with_limits(artifact_limits),
121 immutable_content_adapter,
122 trace_sink: RwLock::new(Arc::new(InMemoryTraceSink::default())),
123 argument_validators: RwLock::new(HashMap::new()),
124 transform_policy: RwLock::new(ToolResultTransformPolicyV1::default()),
125 }
126 }
127
128 pub(crate) fn snapshot_with_external_tools(
132 &self,
133 external: impl IntoIterator<Item = Arc<dyn Tool>>,
134 ) -> Result<Self, ToolRegistrySnapshotError> {
135 let mut tools = self.tools.read().unwrap().clone();
139 let builtins = self.builtins.read().unwrap().clone();
140 for tool in external {
141 let name = tool.name().to_owned();
142 if tools.contains_key(&name) {
143 return Err(ToolRegistrySnapshotError { name });
144 }
145 tools.insert(name, tool);
146 }
147 Ok(Self {
148 tools: RwLock::new(tools),
149 builtins: RwLock::new(builtins),
150 context: RwLock::new(self.context.read().unwrap().clone()),
151 artifact_store: self.artifact_store.clone(),
152 immutable_content_adapter: self.immutable_content_adapter.clone(),
153 trace_sink: RwLock::new(Arc::clone(&self.trace_sink.read().unwrap())),
154 argument_validators: RwLock::new(self.argument_validators.read().unwrap().clone()),
155 transform_policy: RwLock::new(self.transform_policy.read().unwrap().clone()),
156 })
157 }
158
159 pub(crate) fn set_tool_result_transform_policy(
160 &self,
161 policy: ToolResultTransformPolicyV1,
162 ) -> Result<()> {
163 policy.validate()?;
164 *self.transform_policy.write().unwrap() = policy;
165 Ok(())
166 }
167
168 pub fn register_builtin(&self, tool: Arc<dyn Tool>) {
170 let name = tool.name().to_string();
171 let mut tools = self.tools.write().unwrap();
172 let mut builtins = self.builtins.write().unwrap();
173 tracing::debug!("Registering builtin tool: {}", name);
174 tools.insert(name.clone(), tool);
175 builtins.insert(name);
176 }
177
178 pub fn register(&self, tool: Arc<dyn Tool>) {
183 let name = tool.name().to_string();
184 let mut tools = self.tools.write().unwrap();
188 let builtins = self.builtins.read().unwrap();
189 if builtins.contains(&name) {
190 tracing::warn!(
191 "Rejected registration of tool '{}': cannot shadow builtin",
192 name
193 );
194 return;
195 }
196 tracing::debug!("Registering tool: {}", name);
197 tools.insert(name, tool);
198 }
199
200 pub(crate) fn register_with_shadow(
207 &self,
208 tool: Arc<dyn Tool>,
209 ) -> (bool, Option<Arc<dyn Tool>>) {
210 let name = tool.name().to_string();
211 let mut tools = self.tools.write().unwrap();
212 let builtins = self.builtins.read().unwrap();
213 if builtins.contains(&name) {
214 tracing::warn!(
215 "Rejected registration of tool '{}': cannot shadow builtin",
216 name
217 );
218 return (false, None);
219 }
220 tracing::debug!("Registering owned dynamic tool: {}", name);
221 (true, tools.insert(name, tool))
222 }
223
224 pub(crate) fn restore_if_same(
230 &self,
231 name: &str,
232 expected: &Arc<dyn Tool>,
233 replacement: Option<Arc<dyn Tool>>,
234 ) -> bool {
235 let mut tools = self.tools.write().unwrap();
236 let Some(current) = tools.get(name) else {
237 return false;
238 };
239 if !Arc::ptr_eq(current, expected) {
240 return false;
241 }
242
243 match replacement {
244 Some(tool) => {
245 tools.insert(name.to_string(), tool);
246 }
247 None => {
248 tools.remove(name);
249 }
250 }
251 true
252 }
253
254 pub(crate) fn register_if_absent(&self, tool: Arc<dyn Tool>) -> bool {
256 let name = tool.name().to_string();
257 let mut tools = self.tools.write().unwrap();
258 if tools.contains_key(&name) {
259 return false;
260 }
261 tracing::debug!("Registering previously absent dynamic tool: {}", name);
262 tools.insert(name, tool);
263 true
264 }
265
266 pub fn unregister(&self, name: &str) -> bool {
270 let mut tools = self.tools.write().unwrap();
271 let builtins = self.builtins.read().unwrap();
272 if builtins.contains(name) {
273 tracing::warn!(
274 "Rejected unregister of tool '{}': builtin tools cannot be removed through dynamic unregister",
275 name
276 );
277 return false;
278 }
279 tracing::debug!("Unregistering tool: {}", name);
280 tools.remove(name).is_some()
281 }
282
283 pub fn unregister_by_prefix(&self, prefix: &str) {
285 let mut tools = self.tools.write().unwrap();
286 let builtins = self.builtins.read().unwrap();
287 tools.retain(|name, _| builtins.contains(name) || !name.starts_with(prefix));
288 tracing::debug!("Unregistered tools with prefix: {}", prefix);
289 }
290
291 pub fn get(&self, name: &str) -> Option<Arc<dyn Tool>> {
293 let tools = self.tools.read().unwrap();
294 let canonical = crate::tool_name::canonical_tool_name(name);
295 if let Some(tool) = tools.get(canonical) {
296 return Some(tool.clone());
297 }
298 let lower = canonical.to_ascii_lowercase();
299 if lower != canonical {
300 if let Some(tool) = tools.get(&lower) {
301 return Some(tool.clone());
302 }
303 }
304 tools
305 .iter()
306 .find(|(registered, _)| registered.eq_ignore_ascii_case(canonical))
307 .map(|(_, tool)| tool.clone())
308 }
309
310 pub(crate) fn capabilities(
311 &self,
312 name: &str,
313 args: &serde_json::Value,
314 ) -> Option<ToolCapabilities> {
315 self.get(name).map(|tool| tool.capabilities(args))
316 }
317
318 pub(crate) fn requires_confirmation(&self, name: &str, args: &serde_json::Value) -> bool {
319 self.get(name)
320 .is_some_and(|tool| tool.requires_confirmation(args))
321 }
322
323 pub fn contains(&self, name: &str) -> bool {
325 self.get(name).is_some()
326 }
327
328 pub fn definitions(&self) -> Vec<ToolDefinition> {
330 let tools = self.tools.read().unwrap();
331 let mut definitions = tools
332 .values()
333 .filter(|tool| tool.is_model_visible())
334 .map(|tool| tool.definition())
335 .collect::<Vec<_>>();
336 definitions.sort_by(|a, b| a.name.cmp(&b.name));
337 definitions
338 }
339
340 pub fn list(&self) -> Vec<String> {
342 let tools = self.tools.read().unwrap();
343 let mut names = tools.keys().cloned().collect::<Vec<_>>();
344 names.sort();
345 names
346 }
347
348 pub(crate) fn validate_arguments(
355 &self,
356 name: &str,
357 args: &serde_json::Value,
358 ) -> std::result::Result<(), String> {
359 let Some(tool) = self.get(name) else {
360 return Ok(());
361 };
362 let schema = tool.parameters();
363 let schema_bytes = serde_json::to_vec(&schema)
364 .map_err(|error| format!("tool parameter schema is not serializable: {error}"))?;
365 if schema_bytes.len() > MAX_TOOL_SCHEMA_BYTES {
366 return Err(format!(
367 "tool parameter schema exceeds the {} byte safety limit",
368 MAX_TOOL_SCHEMA_BYTES
369 ));
370 }
371 let mut hasher = std::collections::hash_map::DefaultHasher::new();
372 schema_bytes.hash(&mut hasher);
373 let schema_fingerprint = hasher.finish();
374 let cached = self
375 .argument_validators
376 .read()
377 .unwrap()
378 .get(name)
379 .filter(|entry| entry.schema_fingerprint == schema_fingerprint)
380 .cloned();
381 let validator = match cached.map(|entry| entry.validator) {
382 Some(CachedArgumentValidator::Valid(validator)) => validator,
383 Some(CachedArgumentValidator::Invalid(error)) => return Err(error),
384 None => {
385 let compiled = match jsonschema::draft202012::options().build(&schema) {
386 Ok(validator) => CachedArgumentValidator::Valid(Arc::new(validator)),
387 Err(error) => CachedArgumentValidator::Invalid(format!(
388 "tool has an invalid parameter schema: {error}"
389 )),
390 };
391 self.argument_validators.write().unwrap().insert(
392 name.to_string(),
393 ArgumentValidatorCacheEntry {
394 schema_fingerprint,
395 validator: compiled.clone(),
396 },
397 );
398 match compiled {
399 CachedArgumentValidator::Valid(validator) => validator,
400 CachedArgumentValidator::Invalid(error) => return Err(error),
401 }
402 }
403 };
404 let mut errors = validator
405 .iter_errors(args)
406 .take(MAX_ARGUMENT_VALIDATION_ERRORS + 1)
407 .map(|error| {
408 let path = error.instance_path().to_string();
409 if path.is_empty() {
410 format!("$: {error}")
411 } else {
412 format!("{path}: {error}")
413 }
414 })
415 .collect::<Vec<_>>();
416 if errors.is_empty() {
417 return Ok(());
418 }
419
420 let omitted = errors.len() > MAX_ARGUMENT_VALIDATION_ERRORS;
421 errors.truncate(MAX_ARGUMENT_VALIDATION_ERRORS);
422 let mut message = errors.join("; ");
423 if omitted {
424 message.push_str("; additional validation errors omitted");
425 }
426 Err(crate::text::truncate_utf8(&message, MAX_ARGUMENT_VALIDATION_MESSAGE_BYTES).to_string())
427 }
428
429 pub fn len(&self) -> usize {
431 let tools = self.tools.read().unwrap();
432 tools.len()
433 }
434
435 pub fn is_empty(&self) -> bool {
437 self.len() == 0
438 }
439
440 pub fn context(&self) -> ToolContext {
442 self.context.read().unwrap().clone()
443 }
444
445 pub fn artifact_store(&self) -> ArtifactStore {
447 self.artifact_store.clone()
448 }
449
450 pub fn immutable_content_adapter(&self) -> Option<&ImmutableContentAdapterSession> {
452 self.immutable_content_adapter.as_ref()
453 }
454
455 pub fn get_artifact(&self, artifact_uri: &str) -> Option<ToolArtifact> {
457 self.artifact_store.get(artifact_uri)
458 }
459
460 pub fn set_trace_sink(&self, sink: Arc<dyn TraceSink>) {
462 *self.trace_sink.write().unwrap() = sink;
463 }
464
465 pub fn trace_sink(&self) -> Arc<dyn TraceSink> {
467 Arc::clone(&self.trace_sink.read().unwrap())
468 }
469
470 pub fn set_search_config(&self, config: crate::config::SearchConfig) {
472 let mut ctx = self.context.write().unwrap();
473 *ctx = ctx.clone().with_search_config(config);
474 }
475
476 pub fn set_sandbox(&self, sandbox: std::sync::Arc<dyn crate::sandbox::BashSandbox>) {
479 let mut ctx = self.context.write().unwrap();
480 *ctx = ctx.clone().with_sandbox(sandbox);
481 }
482
483 pub fn set_command_env(&self, env: Arc<HashMap<String, String>>) {
486 let mut ctx = self.context.write().unwrap();
487 *ctx = ctx.clone().with_command_env(env);
488 }
489
490 pub async fn execute(&self, name: &str, args: &serde_json::Value) -> Result<ToolResult> {
496 let ctx = self.context();
497 self.execute_with_context(name, args, &ctx).await
498 }
499
500 pub async fn execute_with_context(
505 &self,
506 name: &str,
507 args: &serde_json::Value,
508 ctx: &ToolContext,
509 ) -> Result<ToolResult> {
510 let start = std::time::Instant::now();
511 let prepared = self.prepare_output_with_context(name, args, ctx).await?;
512 let transform_binding = prepared.transform_binding;
513 let mut result = match prepared.output {
514 Some(output) => Ok(ToolResult {
515 name: name.to_string(),
516 output: output.content,
517 exit_code: if output.success { 0 } else { 1 },
518 metadata: output.metadata,
519 images: output.images,
520 error_kind: output.error_kind,
521 trust: crate::tools::ToolResultTrustV1::WorkspaceData,
522 }),
523 None => Ok(ToolResult::error(name, format!("Unknown tool: {name}"))),
524 };
525
526 if let Ok(result) = &mut result {
527 result.metadata = Some(super::ensure_tool_result_evidence_with_transform_binding(
528 result.metadata.take(),
529 &result.output,
530 &transform_binding,
531 )?);
532 }
533
534 if let Ok(ref r) = result {
535 crate::telemetry::record_tool_result(r.exit_code, start.elapsed());
536 self.record_trace_event(name, r, start.elapsed());
537 }
538
539 result
540 }
541
542 pub async fn execute_raw(
545 &self,
546 name: &str,
547 args: &serde_json::Value,
548 ) -> Result<Option<ToolOutput>> {
549 let ctx = self.context();
550 self.execute_raw_with_context(name, args, &ctx).await
551 }
552
553 pub async fn execute_raw_with_context(
556 &self,
557 name: &str,
558 args: &serde_json::Value,
559 ctx: &ToolContext,
560 ) -> Result<Option<ToolOutput>> {
561 Ok(self
562 .prepare_output_with_context(name, args, ctx)
563 .await?
564 .output)
565 }
566
567 async fn prepare_output_with_context(
568 &self,
569 name: &str,
570 args: &serde_json::Value,
571 ctx: &ToolContext,
572 ) -> Result<PreparedToolOutput> {
573 let policy = self.transform_policy.read().unwrap().clone();
574 let transform_binding = ToolResultTransformBindingV1::from_policy(&policy)?;
575 let Some(tool) = self.get(name) else {
576 return Ok(PreparedToolOutput {
577 output: None,
578 transform_binding,
579 });
580 };
581
582 let mut output = tool.execute(args, ctx).await?;
583 self.compact_change_metadata(name, &mut output.metadata, ctx)
584 .await?;
585 let original_content = output.content.clone();
586 let truncated = transform_tool_output_with_artifact(name, &output.content, &policy);
587 output.content = truncated.content;
588 let loss_mode = truncated.loss_mode;
589 let projected_artifact_reference = truncated.artifact.is_some();
590 let artifact = truncated.artifact.or_else(|| {
591 self.immutable_content_adapter
592 .as_ref()
593 .map(|_| super::tool_output_artifact(name, &original_content, output.content.len()))
594 });
595 if let Some(mut artifact) = artifact {
596 let compatibility_uri = artifact.artifact_uri.clone();
597 self.store_tool_artifact(
598 name,
599 &original_content,
600 &mut artifact,
601 ImmutableContentKindV1::ToolResultOriginal,
602 ctx,
603 )
604 .await?;
605 if projected_artifact_reference {
606 rewrite_projected_artifact_uri(
607 &mut output.content,
608 &compatibility_uri,
609 &artifact.artifact_uri,
610 )?;
611 }
612 output.metadata = Some(merge_tool_output_artifact_metadata(
613 output.metadata,
614 &artifact,
615 ));
616 }
617 output.metadata = Some(super::attach_tool_result_evidence_with_transform_binding(
618 output.metadata,
619 &original_content,
620 &output.content,
621 loss_mode,
622 &transform_binding,
623 )?);
624 Ok(PreparedToolOutput {
625 output: Some(output),
626 transform_binding,
627 })
628 }
629
630 async fn store_tool_artifact(
631 &self,
632 tool_name: &str,
633 content: &str,
634 artifact: &mut ToolOutputArtifact,
635 kind: ImmutableContentKindV1,
636 ctx: &ToolContext,
637 ) -> Result<()> {
638 if let Some(adapter) = &self.immutable_content_adapter {
639 let cancellation = ctx.cancellation_token();
640 let retained = tokio::select! {
641 biased;
642 _ = cancellation.cancelled() => {
643 return Err(ImmutableContentError::Cancelled.into());
644 }
645 result = adapter.put(kind, TOOL_RESULT_CONTENT_MEDIA_TYPE, content.as_bytes()) => {
646 result.map_err(|error| anyhow::anyhow!(
647 "immutable content adapter '{}' rejected Tool content: {}",
648 adapter.adapter_name(),
649 error.redacted_message(),
650 ))?
651 }
652 };
653 artifact.artifact_uri.clone_from(&retained.uri);
654 artifact.content_reference = Some(retained);
655 return Ok(());
656 }
657
658 self.artifact_store
659 .put_content_addressed(ToolArtifact {
660 artifact_id: artifact.artifact_id.clone(),
661 artifact_uri: artifact.artifact_uri.clone(),
662 tool_name: tool_name.to_string(),
663 content: content.to_string(),
664 original_bytes: artifact.original_bytes,
665 shown_bytes: artifact.shown_bytes,
666 })
667 .map_err(|error| anyhow::anyhow!("immutable Tool artifact conflict: {error}"))?;
668 Ok(())
669 }
670
671 async fn compact_change_metadata(
672 &self,
673 tool_name: &str,
674 metadata: &mut Option<serde_json::Value>,
675 ctx: &ToolContext,
676 ) -> Result<()> {
677 let Some(serde_json::Value::Object(object)) = metadata.as_mut() else {
678 return Ok(());
679 };
680 let before = object
681 .get("before")
682 .and_then(serde_json::Value::as_str)
683 .map(ToString::to_string);
684 let after = object
685 .get("after")
686 .and_then(serde_json::Value::as_str)
687 .map(ToString::to_string);
688 if before.is_none() && after.is_none() {
689 return Ok(());
690 }
691
692 let before_bytes = before.as_ref().map_or(0, String::len);
693 let after_bytes = after.as_ref().map_or(0, String::len);
694 let total_bytes = before_bytes.saturating_add(after_bytes);
695 let compacted = total_bytes > MAX_INLINE_CHANGE_BYTES;
696 let before_artifact = match before.as_deref() {
697 Some(content) => {
698 self.store_change_artifact(
699 tool_name,
700 "before",
701 content,
702 compacted,
703 ImmutableContentKindV1::ToolChangeBefore,
704 ctx,
705 )
706 .await?
707 }
708 None => None,
709 };
710 let after_artifact = match after.as_deref() {
711 Some(content) => {
712 self.store_change_artifact(
713 tool_name,
714 "after",
715 content,
716 compacted,
717 ImmutableContentKindV1::ToolChangeAfter,
718 ctx,
719 )
720 .await?
721 }
722 None => None,
723 };
724
725 let unified_diff = if compacted && total_bytes <= MAX_DIFF_COMPUTE_BYTES {
726 let diff = similar::TextDiff::from_lines(
727 before.as_deref().unwrap_or_default(),
728 after.as_deref().unwrap_or_default(),
729 )
730 .unified_diff()
731 .context_radius(3)
732 .header("before", "after")
733 .to_string();
734 Some(bounded_head_tail(&diff, CHANGE_DIFF_PREVIEW_BYTES))
735 } else {
736 None
737 };
738
739 if compacted {
740 if let Some(content) = before.as_deref() {
741 object.insert(
742 "before".to_string(),
743 serde_json::Value::String(bounded_head_tail(
744 content,
745 CHANGE_SIDE_PREVIEW_BYTES,
746 )),
747 );
748 }
749 if let Some(content) = after.as_deref() {
750 object.insert(
751 "after".to_string(),
752 serde_json::Value::String(bounded_head_tail(
753 content,
754 CHANGE_SIDE_PREVIEW_BYTES,
755 )),
756 );
757 }
758 }
759
760 object.insert(
761 "change".to_string(),
762 serde_json::json!({
763 "compacted": compacted,
764 "before": before.as_deref().map(|content| serde_json::json!({
765 "bytes": content.len(),
766 "sha256": sha256::digest(content.as_bytes()),
767 "artifact": before_artifact,
768 })),
769 "after": after.as_deref().map(|content| serde_json::json!({
770 "bytes": content.len(),
771 "sha256": sha256::digest(content.as_bytes()),
772 "artifact": after_artifact,
773 })),
774 "unified_diff": unified_diff,
775 "diff_omitted": compacted && total_bytes > MAX_DIFF_COMPUTE_BYTES,
776 }),
777 );
778 Ok(())
779 }
780
781 async fn store_change_artifact(
782 &self,
783 tool_name: &str,
784 side: &str,
785 content: &str,
786 store: bool,
787 kind: ImmutableContentKindV1,
788 ctx: &ToolContext,
789 ) -> Result<Option<serde_json::Value>> {
790 if !store
791 || self.immutable_content_adapter.is_none()
792 && content.len() > self.artifact_store.limits().max_bytes
793 {
794 return Ok(None);
795 }
796 let mut artifact = tool_output_artifact(&format!("{tool_name}-{side}"), content, 0);
797 self.store_tool_artifact(tool_name, content, &mut artifact, kind, ctx)
798 .await?;
799 let mut metadata = serde_json::json!({
800 "artifact_id": artifact.artifact_id,
801 "artifact_uri": artifact.artifact_uri,
802 });
803 if let Some(reference) = artifact.content_reference {
804 metadata["content_reference"] = serde_json::json!(reference);
805 }
806 Ok(Some(metadata))
807 }
808
809 fn record_trace_event(&self, name: &str, result: &ToolResult, duration: std::time::Duration) {
810 let sink = self.trace_sink();
811 sink.record(TraceEvent::tool_execution(
812 name,
813 result.exit_code == 0,
814 result.exit_code,
815 duration,
816 result.output.len(),
817 result.metadata.as_ref(),
818 ));
819
820 if name == "program" {
821 sink.record(TraceEvent::program_execution(
822 name,
823 result.exit_code == 0,
824 result.exit_code,
825 duration,
826 result.output.len(),
827 result.metadata.as_ref(),
828 ));
829 }
830 }
831}
832
833fn rewrite_projected_artifact_uri(
834 projected: &mut String,
835 compatibility_uri: &str,
836 retained_uri: &str,
837) -> Result<()> {
838 if compatibility_uri == retained_uri {
839 return Ok(());
840 }
841 let start = projected.rfind(compatibility_uri).ok_or_else(|| {
842 anyhow::anyhow!(
843 "Tool result projection lost its compatibility artifact reference before retention"
844 )
845 })?;
846 let end = start + compatibility_uri.len();
847 projected.replace_range(start..end, retained_uri);
848 Ok(())
849}
850
851fn bounded_head_tail(content: &str, max_bytes: usize) -> String {
852 if content.len() <= max_bytes {
853 return content.to_string();
854 }
855 let head_limit = max_bytes / 2;
856 let tail_limit = max_bytes.saturating_sub(head_limit);
857 let head = crate::text::truncate_utf8(content, head_limit);
858 let mut tail_start = content.len().saturating_sub(tail_limit);
859 while tail_start < content.len() && !content.is_char_boundary(tail_start) {
860 tail_start += 1;
861 }
862 format!(
863 "{}\n\n... [{} bytes omitted from middle] ...\n\n{}",
864 head,
865 content
866 .len()
867 .saturating_sub(head.len())
868 .saturating_sub(content.len().saturating_sub(tail_start)),
869 &content[tail_start..]
870 )
871}
872
873#[cfg(test)]
874#[path = "registry/tests.rs"]
875mod tests;