1use 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
27pub type ToolResult<T> = Result<T, ToolError>;
29
30pub trait ToolArgs: Serialize + DeserializeOwned + Send + Sync + 'static {
32 const SCHEMA_ID: &'static str;
34 const SCHEMA_VERSION: SchemaVersion;
36
37 fn schema() -> Value;
39}
40
41pub trait ToolOutput: Serialize + Send + Sync + 'static {
43 fn redacted_summary(&self) -> String {
45 "typed tool output".to_string()
46 }
47}
48
49#[derive(Clone, Debug, Eq, PartialEq)]
51pub struct ToolIdentity {
52 pub name: CanonicalToolName,
54 pub version: String,
56 pub capability_id: CapabilityId,
58 pub namespace: CapabilityNamespace,
60 pub executor_ref: ExecutorRef,
62 pub schema_ref: PackageSidecarRef,
64}
65
66impl ToolIdentity {
67 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 pub fn capability_id(mut self, id: CapabilityId) -> Self {
93 self.capability_id = id;
94 self
95 }
96
97 pub fn executor_ref(mut self, executor_ref: ExecutorRef) -> Self {
99 self.executor_ref = executor_ref;
100 self
101 }
102
103 pub fn schema_ref(mut self, schema_ref: PackageSidecarRef) -> Self {
105 self.schema_ref = schema_ref;
106 self
107 }
108}
109
110#[derive(Clone, Debug, Eq, PartialEq)]
112pub struct ToolSchemaSnapshot {
113 pub schema_ref: PackageSidecarRef,
115 pub redacted_schema: Value,
117 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#[derive(Clone)]
139pub struct TypedToolContext {
140 pub request: ToolExecutionRequest,
142}
143
144pub trait JsonToolArgumentStore: Send + Sync {
146 fn load_json(&self, content_ref: &ContentRefId) -> Result<Value, AgentError>;
148}
149
150pub trait JsonToolContentStore: Send + Sync {
152 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#[derive(Clone, Debug, Eq, PartialEq)]
176pub enum ToolErrorKind {
177 InvalidArguments,
179 HandlerFailed,
181 OutputSerialization,
183 ContentStore,
185 Cancelled,
187 TimedOut,
189}
190
191#[derive(Clone, Debug, Eq, PartialEq)]
193pub struct ToolError {
194 pub kind: ToolErrorKind,
196 pub code: String,
198 pub redacted_summary: String,
200}
201
202impl ToolError {
203 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 pub fn handler_failed(code: impl Into<String>, summary: impl Into<String>) -> Self {
218 Self::new(ToolErrorKind::HandlerFailed, code, summary)
219 }
220}
221
222pub trait AsyncToolRunner: Send + Sync {
224 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
233pub 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 pub fn builder(identity: ToolIdentity) -> TypedToolBuilder<A, R> {
250 TypedToolBuilder::new(identity)
251 }
252
253 pub fn schema_snapshot(&self) -> &ToolSchemaSnapshot {
255 &self.schema
256 }
257
258 pub fn require_approval(mut self) -> Self {
260 self.require_approval = true;
261 self.risk_class = RiskClass::High;
262 self
263 }
264
265 pub fn approval_required(&self) -> bool {
267 self.require_approval
268 }
269
270 pub fn tool(&self) -> Result<Tool, AgentError> {
272 self.tool_builder().build()
273 }
274
275 pub fn async_tool(&self) -> Result<AsyncTool, AgentError> {
277 self.tool_builder().build_async()
278 }
279
280 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 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
331pub 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 pub fn policy_ref(mut self, policy_ref: PolicyRef) -> Self {
361 self.policy_ref = policy_ref;
362 self
363 }
364
365 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 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 pub fn input_schema(mut self, schema: Value) -> Self {
382 self.input_schema = Some(schema);
383 self
384 }
385
386 pub fn read_only(mut self) -> Self {
388 self.effect_class = EffectClass::Read;
389 self.risk_class = RiskClass::Low;
390 self
391 }
392
393 pub fn write_effect(mut self) -> Self {
395 self.effect_class = EffectClass::Write;
396 self.risk_class = RiskClass::High;
397 self
398 }
399
400 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 pub fn required_permission(mut self, permission: CapabilityPermission) -> Self {
409 self.required_permissions.push(permission);
410 self
411 }
412
413 pub fn timeout_ms(mut self, timeout_ms: u64) -> Self {
415 self.timeout_ms = timeout_ms;
416 self
417 }
418
419 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 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 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
465pub struct FunctionTool;
471
472impl FunctionTool {
473 pub fn builder(name: impl Into<String>) -> FunctionToolBuilder<(), ()> {
475 FunctionToolBuilder::new(name)
476 }
477}
478
479pub 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 pub fn version(mut self, version: impl Into<String>) -> Self {
513 self.version = version.into();
514 self
515 }
516
517 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 pub fn input_schema(mut self, schema: Value) -> Self {
528 self.input_schema = Some(schema);
529 self
530 }
531
532 pub fn policy_ref(mut self, policy_ref: PolicyRef) -> Self {
534 self.policy_ref = policy_ref;
535 self
536 }
537
538 pub fn read_only(mut self) -> Self {
540 self.effect_class = EffectClass::Read;
541 self.risk_class = RiskClass::Low;
542 self
543 }
544
545 pub fn write_effect(mut self) -> Self {
547 self.effect_class = EffectClass::Write;
548 self.risk_class = RiskClass::High;
549 self
550 }
551
552 pub fn require_approval(mut self) -> Self {
554 self.require_approval = true;
555 self.risk_class = RiskClass::High;
556 self
557 }
558
559 pub fn required_permission(mut self, permission: CapabilityPermission) -> Self {
561 self.required_permissions.push(permission);
562 self
563 }
564
565 pub fn timeout_ms(mut self, timeout_ms: u64) -> Self {
567 self.timeout_ms = timeout_ms;
568 self
569 }
570}
571
572impl FunctionToolBuilder<(), ()> {
573 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 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 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")]
735pub 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}