Skip to main content

agent_sdk_toolkit/
typed_tool.rs

1//! Typed tool authoring helpers layered over core tool execution ports.
2//! Helpers in this module build tool declarations and executors; execution,
3//! policy, approval, journals, events, and recovery remain owned by
4//! `agent-sdk-core`.
5
6use std::{future::Future, marker::PhantomData, pin::Pin, sync::Arc};
7
8use agent_sdk_core::{
9    AgentError, CapabilityId, CapabilityNamespace, CapabilityPermission, ExecutorRef,
10    PackageSidecarRef, PolicyKind, PolicyRef, ProviderArgumentStore, SourceRef,
11    ToolExecutionOutput, ToolExecutionRequest, ToolExecutor,
12    domain::ContentRef as ContentRefId,
13    output::SchemaVersion,
14    policy::{EffectClass, RiskClass},
15    tool_records::CanonicalToolName,
16};
17use serde::{Serialize, de::DeserializeOwned};
18use serde_json::Value;
19use sha2::{Digest, Sha256};
20
21use crate::{
22    AsyncTool, Tool, ToolkitPackBundle,
23    packs::{ToolBuilder, ToolPackBuilder},
24    testing::{InMemoryJsonArgumentStore, InMemoryToolkitContentStore},
25};
26
27/// Result type returned by typed tool handlers.
28pub type ToolResult<T> = Result<T, ToolError>;
29
30/// Typed arguments for a toolkit-authored tool.
31pub trait ToolArgs: Serialize + DeserializeOwned + Send + Sync + 'static {
32    /// Stable schema id for the argument shape.
33    const SCHEMA_ID: &'static str;
34    /// Semantic schema version for the argument shape.
35    const SCHEMA_VERSION: SchemaVersion;
36
37    /// Returns a provider-safe JSON schema for these arguments.
38    fn schema() -> Value;
39}
40
41/// Typed output returned by a toolkit-authored tool.
42pub trait ToolOutput: Serialize + Send + Sync + 'static {
43    /// Returns a bounded summary safe for journals, events, logs, and prompts.
44    fn redacted_summary(&self) -> String {
45        "typed tool output".to_string()
46    }
47}
48
49/// Stable identity for a typed tool declaration.
50#[derive(Clone, Debug, Eq, PartialEq)]
51pub struct ToolIdentity {
52    /// Provider-visible canonical tool name.
53    pub name: CanonicalToolName,
54    /// Tool version.
55    pub version: String,
56    /// Runtime package capability id.
57    pub capability_id: CapabilityId,
58    /// Capability namespace.
59    pub namespace: CapabilityNamespace,
60    /// Executor ref registered in the runtime.
61    pub executor_ref: ExecutorRef,
62    /// Schema sidecar ref used by provider projection and fingerprints.
63    pub schema_ref: PackageSidecarRef,
64}
65
66impl ToolIdentity {
67    /// Creates a deterministic typed tool identity from name and version.
68    pub fn new(name: impl Into<String>, version: impl Into<String>) -> Result<Self, AgentError> {
69        let name = name.into();
70        let version = version.into();
71        CanonicalToolName::try_new(name.clone()).map_err(|error| {
72            AgentError::contract_violation(format!("invalid tool name: {error}"))
73        })?;
74        if version.trim().is_empty() {
75            return Err(AgentError::missing_required_field("typed_tool.version"));
76        }
77        Ok(Self {
78            name: CanonicalToolName::new(name.clone()),
79            version: version.clone(),
80            capability_id: CapabilityId::new(format!("cap.tool.{name}")),
81            namespace: CapabilityNamespace::new(format!("tool.{name}")),
82            executor_ref: ExecutorRef::new(format!("executor.{name}.{version}")),
83            schema_ref: PackageSidecarRef::new(
84                format!("schema.{name}.{version}"),
85                "json_schema",
86                version,
87            ),
88        })
89    }
90
91    /// Sets an explicit capability id.
92    pub fn capability_id(mut self, id: CapabilityId) -> Self {
93        self.capability_id = id;
94        self
95    }
96
97    /// Sets an explicit executor ref.
98    pub fn executor_ref(mut self, executor_ref: ExecutorRef) -> Self {
99        self.executor_ref = executor_ref;
100        self
101    }
102
103    /// Sets an explicit schema ref.
104    pub fn schema_ref(mut self, schema_ref: PackageSidecarRef) -> Self {
105        self.schema_ref = schema_ref;
106        self
107    }
108}
109
110/// Provider-safe schema snapshot for a typed tool.
111#[derive(Clone, Debug, Eq, PartialEq)]
112pub struct ToolSchemaSnapshot {
113    /// Schema sidecar ref with content hash populated.
114    pub schema_ref: PackageSidecarRef,
115    /// Redacted schema body.
116    pub redacted_schema: Value,
117    /// Hash of the normalized schema body.
118    pub content_hash: String,
119}
120
121impl ToolSchemaSnapshot {
122    fn new(mut schema_ref: PackageSidecarRef, schema: Value) -> Result<Self, AgentError> {
123        let normalized = normalize_json_value(schema);
124        let bytes = serde_json::to_vec(&normalized).map_err(|error| {
125            AgentError::contract_violation(format!("tool schema serialization failed: {error}"))
126        })?;
127        let content_hash = format!("sha256:{}", hex_lower(&Sha256::digest(bytes)));
128        schema_ref.content_hash = Some(content_hash.clone());
129        Ok(Self {
130            schema_ref,
131            redacted_schema: normalized,
132            content_hash,
133        })
134    }
135}
136
137/// Execution context passed to typed tool handlers.
138#[derive(Clone)]
139pub struct TypedToolContext {
140    /// Canonical core execution request.
141    pub request: ToolExecutionRequest,
142}
143
144/// JSON argument store for typed tool execution.
145pub trait JsonToolArgumentStore: Send + Sync {
146    /// Loads the JSON arguments behind a content ref.
147    fn load_json(&self, content_ref: &ContentRefId) -> Result<Value, AgentError>;
148}
149
150/// JSON content store for typed tool outputs.
151pub trait JsonToolContentStore: Send + Sync {
152    /// Stores one JSON result behind a content ref.
153    fn put_json(&self, content_ref: ContentRefId, value: Value) -> Result<(), AgentError>;
154}
155
156impl JsonToolArgumentStore for InMemoryJsonArgumentStore {
157    fn load_json(&self, content_ref: &ContentRefId) -> Result<Value, AgentError> {
158        self.get(content_ref)
159    }
160}
161
162impl JsonToolArgumentStore for Arc<dyn ProviderArgumentStore> {
163    fn load_json(&self, content_ref: &ContentRefId) -> Result<Value, AgentError> {
164        self.load_provider_arguments_json(content_ref)
165    }
166}
167
168impl JsonToolContentStore for InMemoryToolkitContentStore {
169    fn put_json(&self, content_ref: ContentRefId, value: Value) -> Result<(), AgentError> {
170        self.put(content_ref, &value)
171    }
172}
173
174/// Typed tool error kind.
175#[derive(Clone, Debug, Eq, PartialEq)]
176pub enum ToolErrorKind {
177    /// Tool arguments could not be decoded.
178    InvalidArguments,
179    /// Handler returned a failure.
180    HandlerFailed,
181    /// Output could not be serialized.
182    OutputSerialization,
183    /// Content store failed.
184    ContentStore,
185    /// Tool was cancelled.
186    Cancelled,
187    /// Tool timed out.
188    TimedOut,
189}
190
191/// Structured typed tool error.
192#[derive(Clone, Debug, Eq, PartialEq)]
193pub struct ToolError {
194    /// Finite error kind.
195    pub kind: ToolErrorKind,
196    /// Stable error code.
197    pub code: String,
198    /// Redacted summary safe for durable records.
199    pub redacted_summary: String,
200}
201
202impl ToolError {
203    /// Creates a structured typed tool error.
204    pub fn new(
205        kind: ToolErrorKind,
206        code: impl Into<String>,
207        redacted_summary: impl Into<String>,
208    ) -> Self {
209        Self {
210            kind,
211            code: code.into(),
212            redacted_summary: redacted_summary.into(),
213        }
214    }
215
216    /// Creates a handler-failed error.
217    pub fn handler_failed(code: impl Into<String>, summary: impl Into<String>) -> Self {
218        Self::new(ToolErrorKind::HandlerFailed, code, summary)
219    }
220}
221
222/// Host-provided runner for async typed handlers while core execution remains sync.
223pub trait AsyncToolRunner: Send + Sync {
224    /// Drives an async typed tool future to completion.
225    fn block_on_tool<R: ToolOutput>(
226        &self,
227        future: Pin<Box<dyn Future<Output = ToolResult<R>> + Send>>,
228    ) -> ToolResult<R>;
229}
230
231type SyncHandler<A, R> = dyn Fn(A, TypedToolContext) -> ToolResult<R> + Send + Sync;
232
233/// Typed tool declaration plus handler adapter.
234pub struct TypedTool<A: ToolArgs, R: ToolOutput> {
235    identity: ToolIdentity,
236    schema: ToolSchemaSnapshot,
237    description: Option<String>,
238    policy_ref: PolicyRef,
239    required_permissions: Vec<CapabilityPermission>,
240    effect_class: EffectClass,
241    risk_class: RiskClass,
242    timeout_ms: u64,
243    require_approval: bool,
244    handler: Arc<SyncHandler<A, R>>,
245}
246
247impl<A: ToolArgs, R: ToolOutput> TypedTool<A, R> {
248    /// Starts a typed tool builder.
249    pub fn builder(identity: ToolIdentity) -> TypedToolBuilder<A, R> {
250        TypedToolBuilder::new(identity)
251    }
252
253    /// Returns the schema snapshot.
254    pub fn schema_snapshot(&self) -> &ToolSchemaSnapshot {
255        &self.schema
256    }
257
258    /// Marks this tool as requiring host approval before execution.
259    pub fn require_approval(mut self) -> Self {
260        self.require_approval = true;
261        self.risk_class = RiskClass::High;
262        self
263    }
264
265    /// Returns whether approval is required for this tool.
266    pub fn approval_required(&self) -> bool {
267        self.require_approval
268    }
269
270    /// Lowers to an ergonomic toolkit tool declaration.
271    pub fn tool(&self) -> Result<Tool, AgentError> {
272        self.tool_builder().build()
273    }
274
275    /// Lowers to an ergonomic async toolkit declaration.
276    pub fn async_tool(&self) -> Result<AsyncTool, AgentError> {
277        self.tool_builder().build_async()
278    }
279
280    /// Creates a core tool executor adapter for this typed tool.
281    pub fn executor(
282        &self,
283        args: Arc<dyn JsonToolArgumentStore>,
284        out: Arc<dyn JsonToolContentStore>,
285    ) -> Arc<dyn ToolExecutor> {
286        Arc::new(TypedToolExecutor {
287            executor_ref: self.identity.executor_ref.clone(),
288            args,
289            out,
290            handler: self.handler.clone(),
291            _args: PhantomData::<A>,
292            _output: PhantomData::<R>,
293        })
294    }
295
296    /// Builds a toolkit pack bundle containing this tool declaration.
297    pub fn pack_bundle(&self, source: SourceRef) -> Result<ToolkitPackBundle, AgentError> {
298        ToolPackBuilder::new(
299            agent_sdk_core::ToolPackId::new(format!("toolpack.{}", self.identity.name.as_str())),
300            agent_sdk_core::ToolPackKind::External,
301            self.identity.version.clone(),
302            source,
303        )
304        .listen(self.tool()?)
305        .build()
306    }
307
308    fn tool_builder(&self) -> ToolBuilder {
309        let mut builder = Tool::builder(
310            self.identity.name.as_str(),
311            self.identity.executor_ref.as_str(),
312            self.schema.schema_ref.sidecar_id.clone(),
313            self.policy_ref.clone(),
314        )
315        .description_opt(self.description.clone())
316        .capability_id(self.identity.capability_id.clone())
317        .namespace(self.identity.namespace.clone())
318        .redacted_schema(self.schema.redacted_schema.clone())
319        .effect(self.effect_class.clone(), self.risk_class.clone())
320        .timeout_ms(self.timeout_ms);
321        for permission in &self.required_permissions {
322            builder = builder.required_permission(permission.clone());
323        }
324        if self.require_approval {
325            builder = builder.require_approval();
326        }
327        builder
328    }
329}
330
331/// Builder for a typed tool.
332pub struct TypedToolBuilder<A: ToolArgs, R: ToolOutput> {
333    identity: ToolIdentity,
334    policy_ref: PolicyRef,
335    description: Option<String>,
336    input_schema: Option<Value>,
337    required_permissions: Vec<CapabilityPermission>,
338    effect_class: EffectClass,
339    risk_class: RiskClass,
340    timeout_ms: u64,
341    handler: Option<Arc<SyncHandler<A, R>>>,
342}
343
344impl<A: ToolArgs, R: ToolOutput> TypedToolBuilder<A, R> {
345    fn new(identity: ToolIdentity) -> Self {
346        Self {
347            identity,
348            policy_ref: PolicyRef::with_kind(PolicyKind::RuntimePackage, "policy.tool.typed"),
349            description: None,
350            input_schema: None,
351            required_permissions: Vec::new(),
352            effect_class: EffectClass::Read,
353            risk_class: RiskClass::Low,
354            timeout_ms: 10_000,
355            handler: None,
356        }
357    }
358
359    /// Sets an explicit policy ref.
360    pub fn policy_ref(mut self, policy_ref: PolicyRef) -> Self {
361        self.policy_ref = policy_ref;
362        self
363    }
364
365    /// Sets a provider-visible description.
366    pub fn description(mut self, description: impl Into<String>) -> Self {
367        let description = description.into();
368        if !description.trim().is_empty() {
369            self.description = Some(description);
370        }
371        self
372    }
373
374    /// Sets provider-visible description metadata when present.
375    pub fn description_opt(mut self, description: Option<String>) -> Self {
376        self.description = description.filter(|description| !description.trim().is_empty());
377        self
378    }
379
380    /// Sets an explicit provider-safe input schema.
381    pub fn input_schema(mut self, schema: Value) -> Self {
382        self.input_schema = Some(schema);
383        self
384    }
385
386    /// Marks the tool read-only.
387    pub fn read_only(mut self) -> Self {
388        self.effect_class = EffectClass::Read;
389        self.risk_class = RiskClass::Low;
390        self
391    }
392
393    /// Marks the tool as write-like.
394    pub fn write_effect(mut self) -> Self {
395        self.effect_class = EffectClass::Write;
396        self.risk_class = RiskClass::High;
397        self
398    }
399
400    /// Sets explicit effect and risk classes.
401    pub fn effect(mut self, effect_class: EffectClass, risk_class: RiskClass) -> Self {
402        self.effect_class = effect_class;
403        self.risk_class = risk_class;
404        self
405    }
406
407    /// Adds a required permission.
408    pub fn required_permission(mut self, permission: CapabilityPermission) -> Self {
409        self.required_permissions.push(permission);
410        self
411    }
412
413    /// Sets execution timeout metadata.
414    pub fn timeout_ms(mut self, timeout_ms: u64) -> Self {
415        self.timeout_ms = timeout_ms;
416        self
417    }
418
419    /// Sets the sync handler.
420    pub fn sync_handler<F>(mut self, handler: F) -> Self
421    where
422        F: Fn(A, TypedToolContext) -> ToolResult<R> + Send + Sync + 'static,
423    {
424        self.handler = Some(Arc::new(handler));
425        self
426    }
427
428    /// Sets an async handler through a host-owned runner.
429    pub fn async_handler<F, Fut, Runner>(mut self, runner: Arc<Runner>, handler: F) -> Self
430    where
431        F: Fn(A, TypedToolContext) -> Fut + Send + Sync + 'static,
432        Fut: Future<Output = ToolResult<R>> + Send + 'static,
433        Runner: AsyncToolRunner + 'static,
434    {
435        self.handler = Some(Arc::new(move |args, context| {
436            runner.block_on_tool(Box::pin(handler(args, context)))
437        }));
438        self
439    }
440
441    /// Builds the typed tool declaration and executor adapter.
442    pub fn build(self) -> Result<TypedTool<A, R>, AgentError> {
443        let schema = ToolSchemaSnapshot::new(
444            self.identity.schema_ref.clone(),
445            self.input_schema.unwrap_or_else(A::schema),
446        )?;
447        let handler = self
448            .handler
449            .ok_or_else(|| AgentError::missing_required_field("typed_tool.handler"))?;
450        Ok(TypedTool {
451            identity: self.identity,
452            schema,
453            description: self.description,
454            policy_ref: self.policy_ref,
455            required_permissions: self.required_permissions,
456            effect_class: self.effect_class,
457            risk_class: self.risk_class,
458            timeout_ms: self.timeout_ms,
459            require_approval: false,
460            handler,
461        })
462    }
463}
464
465/// Builder-first function-tool authoring entry point.
466///
467/// `FunctionTool` is a namespace for a simple builder. The built value is the
468/// same `TypedTool` used by the canonical typed-tool execution path, so policy,
469/// approval, journals, events, and output content refs are unchanged.
470pub struct FunctionTool;
471
472impl FunctionTool {
473    /// Starts a builder for a typed function tool.
474    pub fn builder(name: impl Into<String>) -> FunctionToolBuilder<(), ()> {
475        FunctionToolBuilder::new(name)
476    }
477}
478
479/// Builder for `FunctionTool`.
480pub struct FunctionToolBuilder<A, R> {
481    name: String,
482    version: String,
483    description: Option<String>,
484    input_schema: Option<Value>,
485    policy_ref: PolicyRef,
486    required_permissions: Vec<CapabilityPermission>,
487    effect_class: EffectClass,
488    risk_class: RiskClass,
489    timeout_ms: u64,
490    require_approval: bool,
491    handler: Option<Arc<SyncHandler<A, R>>>,
492}
493
494impl<A, R> FunctionToolBuilder<A, R> {
495    fn new(name: impl Into<String>) -> Self {
496        Self {
497            name: name.into(),
498            version: "v1".to_string(),
499            description: None,
500            input_schema: None,
501            policy_ref: PolicyRef::with_kind(PolicyKind::RuntimePackage, "policy.tool.function"),
502            required_permissions: Vec::new(),
503            effect_class: EffectClass::Read,
504            risk_class: RiskClass::Low,
505            timeout_ms: 10_000,
506            require_approval: false,
507            handler: None,
508        }
509    }
510
511    /// Sets an explicit semantic tool version. The default is `v1`.
512    pub fn version(mut self, version: impl Into<String>) -> Self {
513        self.version = version.into();
514        self
515    }
516
517    /// Sets a provider-visible description.
518    pub fn description(mut self, description: impl Into<String>) -> Self {
519        let description = description.into();
520        if !description.trim().is_empty() {
521            self.description = Some(description);
522        }
523        self
524    }
525
526    /// Sets an explicit provider-safe input schema.
527    pub fn input_schema(mut self, schema: Value) -> Self {
528        self.input_schema = Some(schema);
529        self
530    }
531
532    /// Sets an explicit policy ref.
533    pub fn policy_ref(mut self, policy_ref: PolicyRef) -> Self {
534        self.policy_ref = policy_ref;
535        self
536    }
537
538    /// Marks the tool read-only.
539    pub fn read_only(mut self) -> Self {
540        self.effect_class = EffectClass::Read;
541        self.risk_class = RiskClass::Low;
542        self
543    }
544
545    /// Marks the tool as write-like.
546    pub fn write_effect(mut self) -> Self {
547        self.effect_class = EffectClass::Write;
548        self.risk_class = RiskClass::High;
549        self
550    }
551
552    /// Requires host approval before core releases the executor.
553    pub fn require_approval(mut self) -> Self {
554        self.require_approval = true;
555        self.risk_class = RiskClass::High;
556        self
557    }
558
559    /// Adds a required permission.
560    pub fn required_permission(mut self, permission: CapabilityPermission) -> Self {
561        self.required_permissions.push(permission);
562        self
563    }
564
565    /// Sets execution timeout metadata.
566    pub fn timeout_ms(mut self, timeout_ms: u64) -> Self {
567        self.timeout_ms = timeout_ms;
568        self
569    }
570}
571
572impl FunctionToolBuilder<(), ()> {
573    /// Sets a typed executor that does not need execution context.
574    pub fn executor<A, R, F>(self, handler: F) -> FunctionToolBuilder<A, R>
575    where
576        A: ToolArgs,
577        R: ToolOutput,
578        F: Fn(A) -> ToolResult<R> + Send + Sync + 'static,
579    {
580        FunctionToolBuilder {
581            name: self.name,
582            version: self.version,
583            description: self.description,
584            input_schema: self.input_schema,
585            policy_ref: self.policy_ref,
586            required_permissions: self.required_permissions,
587            effect_class: self.effect_class,
588            risk_class: self.risk_class,
589            timeout_ms: self.timeout_ms,
590            require_approval: self.require_approval,
591            handler: Some(Arc::new(move |args, _context| handler(args))),
592        }
593    }
594
595    /// Sets a typed executor that receives the core execution context.
596    pub fn executor_with_context<A, R, F>(self, handler: F) -> FunctionToolBuilder<A, R>
597    where
598        A: ToolArgs,
599        R: ToolOutput,
600        F: Fn(A, TypedToolContext) -> ToolResult<R> + Send + Sync + 'static,
601    {
602        FunctionToolBuilder {
603            name: self.name,
604            version: self.version,
605            description: self.description,
606            input_schema: self.input_schema,
607            policy_ref: self.policy_ref,
608            required_permissions: self.required_permissions,
609            effect_class: self.effect_class,
610            risk_class: self.risk_class,
611            timeout_ms: self.timeout_ms,
612            require_approval: self.require_approval,
613            handler: Some(Arc::new(handler)),
614        }
615    }
616}
617
618impl<A, R> FunctionToolBuilder<A, R>
619where
620    A: ToolArgs,
621    R: ToolOutput,
622{
623    /// Builds the typed tool declaration and executor adapter.
624    pub fn build(self) -> Result<TypedTool<A, R>, AgentError> {
625        let mut builder = TypedTool::<A, R>::builder(ToolIdentity::new(self.name, self.version)?)
626            .description_opt(self.description)
627            .policy_ref(self.policy_ref)
628            .effect(self.effect_class, self.risk_class)
629            .timeout_ms(self.timeout_ms);
630        if let Some(schema) = self.input_schema {
631            builder = builder.input_schema(schema);
632        }
633        for permission in self.required_permissions {
634            builder = builder.required_permission(permission);
635        }
636        let handler = self
637            .handler
638            .ok_or_else(|| AgentError::missing_required_field("function_tool.executor"))?;
639        let mut tool = builder
640            .sync_handler(move |args, context| handler(args, context))
641            .build()?;
642        if self.require_approval {
643            tool = tool.require_approval();
644        }
645        Ok(tool)
646    }
647}
648
649struct TypedToolExecutor<A: ToolArgs, R: ToolOutput> {
650    executor_ref: ExecutorRef,
651    args: Arc<dyn JsonToolArgumentStore>,
652    out: Arc<dyn JsonToolContentStore>,
653    handler: Arc<SyncHandler<A, R>>,
654    _args: PhantomData<A>,
655    _output: PhantomData<R>,
656}
657
658impl<A: ToolArgs, R: ToolOutput> ToolExecutor for TypedToolExecutor<A, R> {
659    fn executor_ref(&self) -> &ExecutorRef {
660        &self.executor_ref
661    }
662
663    fn execute(&self, request: &ToolExecutionRequest) -> Result<ToolExecutionOutput, AgentError> {
664        let Some(args_ref) = request
665            .resolved_call
666            .request
667            .requested_args_refs
668            .first()
669            .cloned()
670        else {
671            return Ok(ToolExecutionOutput::failed(
672                "typed tool arguments were missing",
673                "typed_tool.invalid_arguments",
674            ));
675        };
676        let args_json = match self.args.load_json(&args_ref) {
677            Ok(value) => value,
678            Err(error) => {
679                return Ok(ToolExecutionOutput::failed(
680                    "typed tool arguments could not be loaded",
681                    format!("typed_tool.argument_store.{:?}", error.kind()),
682                ));
683            }
684        };
685        let args = match serde_json::from_value::<A>(args_json) {
686            Ok(args) => args,
687            Err(error) => {
688                return Ok(ToolExecutionOutput::failed(
689                    "typed tool arguments failed schema decoding",
690                    format!("typed_tool.invalid_arguments.{error}"),
691                ));
692            }
693        };
694        let output = match (self.handler)(
695            args,
696            TypedToolContext {
697                request: request.clone(),
698            },
699        ) {
700            Ok(output) => output,
701            Err(error) => {
702                return Ok(ToolExecutionOutput::failed(
703                    error.redacted_summary,
704                    error.code,
705                ));
706            }
707        };
708        let output_summary = output.redacted_summary();
709        let output_json = match serde_json::to_value(&output) {
710            Ok(value) => value,
711            Err(error) => {
712                return Ok(ToolExecutionOutput::failed(
713                    "typed tool output could not be serialized",
714                    format!("typed_tool.output_serialization.{error}"),
715                ));
716            }
717        };
718        let result_ref = ContentRefId::new(format!(
719            "content.tool.{}.result",
720            request.effect_intent.effect_id.as_str()
721        ));
722        if let Err(error) = self.out.put_json(result_ref.clone(), output_json) {
723            return Ok(ToolExecutionOutput::failed(
724                "typed tool output could not be stored",
725                format!("typed_tool.content_store.{:?}", error.kind()),
726            ));
727        }
728        let mut output = ToolExecutionOutput::completed(output_summary);
729        output.content_refs.push(result_ref);
730        Ok(output)
731    }
732}
733
734#[cfg(feature = "schema-generation")]
735/// Generates a normalized schema value for a `schemars` type.
736pub fn schemars_schema<T: schemars::JsonSchema>() -> Value {
737    serde_json::to_value(schemars::schema_for!(T)).expect("schemars schema serializes")
738}
739
740fn hex_lower(bytes: &[u8]) -> String {
741    const HEX: &[u8; 16] = b"0123456789abcdef";
742    let mut out = String::with_capacity(bytes.len() * 2);
743    for byte in bytes {
744        out.push(HEX[(byte >> 4) as usize] as char);
745        out.push(HEX[(byte & 0x0f) as usize] as char);
746    }
747    out
748}
749
750fn normalize_json_value(value: Value) -> Value {
751    match value {
752        Value::Array(values) => {
753            Value::Array(values.into_iter().map(normalize_json_value).collect())
754        }
755        Value::Object(map) => {
756            let mut entries = map
757                .into_iter()
758                .map(|(key, value)| (key, normalize_json_value(value)))
759                .collect::<Vec<_>>();
760            entries.sort_by(|left, right| left.0.cmp(&right.0));
761            Value::Object(entries.into_iter().collect())
762        }
763        other => other,
764    }
765}