1use af_context::{RunId, SessionId, ToolCallId};
10use std::collections::HashMap;
11use std::fmt;
12use std::future::Future;
13use std::pin::Pin;
14use std::sync::atomic::{AtomicBool, Ordering};
15use std::sync::Arc;
16use std::time::Instant;
17
18use async_trait::async_trait;
19use serde_json::Value;
20use sha2::{Digest, Sha256};
21
22use af_llm::Tool as LlmTool;
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub enum ToolSurface {
27 Llm,
29 Chassis,
31}
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub enum ToolConcurrency {
36 Concurrent,
38 Exclusive,
40}
41
42#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
44#[serde(rename_all = "snake_case")]
45pub enum ToolRiskClass {
46 #[default]
48 Unclassified,
49 Read,
51 LowRiskWrite,
53 ConfirmedWrite,
55 Funds,
57}
58
59impl ToolRiskClass {
60 pub const fn as_str(self) -> &'static str {
62 match self {
63 Self::Unclassified => "unclassified",
64 Self::Read => "read",
65 Self::LowRiskWrite => "low_risk_write",
66 Self::ConfirmedWrite => "confirmed_write",
67 Self::Funds => "funds",
68 }
69 }
70}
71
72#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
74#[serde(rename_all = "snake_case")]
75pub enum ToolExecutionOwner {
76 #[default]
78 Unclassified,
79 Runtime,
81 ProductHost,
83}
84
85impl ToolExecutionOwner {
86 pub const fn as_str(self) -> &'static str {
88 match self {
89 Self::Unclassified => "unclassified",
90 Self::Runtime => "runtime",
91 Self::ProductHost => "product_host",
92 }
93 }
94}
95
96#[derive(Debug, Clone, Copy, PartialEq, Eq)]
98pub struct ToolMeta {
99 pub surface: ToolSurface,
101 pub cost_units: u64,
103 pub timeout_secs: u64,
105 pub core: bool,
107 pub concurrency: ToolConcurrency,
109 pub risk_class: ToolRiskClass,
111 pub execution_owner: ToolExecutionOwner,
113 pub requires_confirmation: bool,
116}
117
118#[derive(Debug, Clone, Default)]
121pub struct CancellationToken(Arc<CancellationState>);
122
123#[derive(Debug, Default)]
124struct CancellationState {
125 cancelled: AtomicBool,
126 notify: tokio::sync::Notify,
127 parent: Option<CancellationToken>,
128}
129
130impl CancellationToken {
131 pub fn cancel(&self) {
133 self.0.cancelled.store(true, Ordering::Release);
134 self.0.notify.notify_waiters();
135 }
136 pub fn is_cancelled(&self) -> bool {
138 self.0.cancelled.load(Ordering::Acquire)
139 || self
140 .0
141 .parent
142 .as_ref()
143 .is_some_and(CancellationToken::is_cancelled)
144 }
145 pub fn child(&self) -> Self {
147 Self(Arc::new(CancellationState {
148 cancelled: AtomicBool::new(false),
149 notify: tokio::sync::Notify::new(),
150 parent: Some(self.clone()),
151 }))
152 }
153 pub fn cancelled(&self) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
155 Box::pin(async move {
156 let notified = self.0.notify.notified();
157 tokio::pin!(notified);
158 notified.as_mut().enable();
159 if self.is_cancelled() {
160 return;
161 }
162 match &self.0.parent {
163 Some(parent) => tokio::select! {
164 _ = notified => {}
165 _ = parent.cancelled() => {}
166 },
167 None => notified.await,
168 }
169 })
170 }
171}
172
173#[derive(Debug, Clone)]
175pub struct ToolExecutionContext {
176 pub request: af_context::RequestContext,
178 pub session_id: SessionId,
180 pub run_id: RunId,
182 pub step: u32,
184 pub call_id: ToolCallId,
186 pub source_event_seq: u64,
188 pub risk_class: ToolRiskClass,
190 pub execution_owner: ToolExecutionOwner,
192 pub interaction_resolution: Option<af_agent_session::InteractionResolution>,
194 pub cancellation: CancellationToken,
196 pub deadline: Instant,
198}
199
200#[derive(Debug, Clone, PartialEq)]
202pub struct ToolCompletionAction {
203 pub tool: String,
205 pub arguments: Value,
207}
208
209impl Default for ToolMeta {
210 fn default() -> Self {
211 Self {
212 surface: ToolSurface::Llm,
213 cost_units: 1,
214 timeout_secs: 15,
215 core: false,
216 concurrency: ToolConcurrency::Exclusive,
217 risk_class: ToolRiskClass::Unclassified,
218 execution_owner: ToolExecutionOwner::Unclassified,
219 requires_confirmation: false,
220 }
221 }
222}
223
224#[async_trait]
226pub trait Tool: Send + Sync {
227 fn name(&self) -> &str;
229 fn implementation_version(&self) -> &str {
231 ""
232 }
233 fn description(&self) -> &str;
235 fn parameters(&self) -> Value;
237 fn output_schema(&self) -> Value;
239 fn meta(&self) -> ToolMeta {
241 ToolMeta::default()
242 }
243
244 async fn call(&self, args: Value) -> Result<Value, String>;
248 async fn call_with_context(
250 &self,
251 _context: &ToolExecutionContext,
252 args: Value,
253 ) -> Result<Value, String> {
254 self.call(args).await
255 }
256 fn completion_action(&self, _result: &Value) -> Option<ToolCompletionAction> {
259 None
260 }
261}
262
263#[derive(Default, Clone)]
265pub struct ToolRegistry {
266 tools: HashMap<String, Arc<dyn Tool>>,
267 wire_names: HashMap<String, String>,
268 validators: HashMap<String, Arc<jsonschema::Validator>>,
269 output_validators: HashMap<String, Arc<jsonschema::Validator>>,
270}
271
272impl ToolRegistry {
273 pub fn new() -> Self {
275 Self::default()
276 }
277
278 pub fn register(&mut self, tool: Arc<dyn Tool>) -> Result<&mut Self, String> {
280 let name = tool.name().to_string();
281 if self.tools.contains_key(&name) {
282 return Err(format!("duplicate tool '{name}'"));
283 }
284 let wire_name = model_tool_name(&name);
285 if self.wire_names.contains_key(&wire_name)
286 || (wire_name != name && self.tools.contains_key(&wire_name))
287 || self.wire_names.contains_key(&name)
288 {
289 return Err(format!(
290 "tool name '{name}' collides on provider name '{wire_name}'"
291 ));
292 }
293 let validator = jsonschema::validator_for(&tool.parameters())
294 .map_err(|error| format!("invalid schema for tool '{name}': {error}"))?;
295 let output_validator = jsonschema::validator_for(&tool.output_schema())
296 .map_err(|error| format!("invalid output schema for tool '{name}': {error}"))?;
297 self.tools.insert(name.clone(), tool);
298 self.wire_names.insert(wire_name, name.clone());
299 self.validators.insert(name.clone(), Arc::new(validator));
300 self.output_validators
301 .insert(name, Arc::new(output_validator));
302 Ok(self)
303 }
304
305 pub fn extend(&mut self, other: &Self) -> Result<(), String> {
307 for tool in other.tools.values() {
308 self.register(Arc::clone(tool))?;
309 }
310 Ok(())
311 }
312
313 pub fn is_empty(&self) -> bool {
315 self.tools.is_empty()
316 }
317
318 pub fn len(&self) -> usize {
320 self.tools.len()
321 }
322
323 pub fn contains(&self, name: &str) -> bool {
325 self.tools.contains_key(name)
326 }
327
328 pub fn get(&self, name: &str) -> Option<Arc<dyn Tool>> {
330 self.canonical_name(name)
331 .and_then(|name| self.tools.get(name))
332 .cloned()
333 }
334
335 pub fn validate_arguments(&self, name: &str, arguments: &Value) -> Result<(), String> {
337 let name = self
338 .canonical_name(name)
339 .ok_or_else(|| self.unknown_tool_error(name))?;
340 self.validators
341 .get(name)
342 .ok_or_else(|| self.unknown_tool_error(name))?
343 .validate(arguments)
344 .map_err(|error| format!("invalid tool arguments: {error}"))
345 }
346
347 pub fn validate_output(&self, name: &str, output: &Value) -> Result<(), String> {
349 let name = self
350 .canonical_name(name)
351 .ok_or_else(|| self.unknown_tool_error(name))?;
352 self.output_validators
353 .get(name)
354 .ok_or_else(|| self.unknown_tool_error(name))?
355 .validate(output)
356 .map_err(|error| format!("invalid tool output: {error}"))
357 }
358
359 pub fn names(&self) -> Vec<&str> {
361 let mut names = self.tools.keys().map(String::as_str).collect::<Vec<_>>();
362 names.sort_unstable();
363 names
364 }
365
366 pub fn confirmation_required_names(&self) -> impl Iterator<Item = &str> {
368 self.tools
369 .values()
370 .filter(|tool| tool.meta().requires_confirmation)
371 .map(|tool| tool.name())
372 }
373
374 pub fn filtered<'a>(&self, allowed: impl IntoIterator<Item = &'a str>) -> Self {
376 let mut filtered = Self::new();
377 for name in allowed {
378 if let Some(tool) = self.tools.get(name) {
379 let _ = filtered.register(Arc::clone(tool));
382 }
383 }
384 filtered
385 }
386
387 pub fn specs(&self) -> Vec<LlmTool> {
389 let mut specs = self
390 .tools
391 .values()
392 .filter(|tool| tool.meta().surface == ToolSurface::Llm)
393 .map(|t| LlmTool::function(model_tool_name(t.name()), t.description(), t.parameters()))
394 .collect::<Vec<_>>();
395 specs.sort_by(|left, right| left.function.name.cmp(&right.function.name));
396 specs
397 }
398
399 pub fn runtime_manifest(&self) -> Result<Vec<Value>, String> {
401 self.names()
402 .into_iter()
403 .map(|name| {
404 let tool = self
405 .tools
406 .get(name)
407 .ok_or_else(|| self.unknown_tool_error(name))?;
408 let version = tool.implementation_version().trim();
409 if version.is_empty() {
410 return Err(format!(
411 "tool '{name}' requires a stable implementation version"
412 ));
413 }
414 let meta = tool.meta();
415 if meta.risk_class == ToolRiskClass::Unclassified
416 || meta.execution_owner == ToolExecutionOwner::Unclassified
417 {
418 return Err(format!(
419 "tool '{name}' requires explicit risk_class and execution_owner"
420 ));
421 }
422 Ok(serde_json::json!({
423 "name": name,
424 "implementation_version": version,
425 "description": tool.description(),
426 "parameters": tool.parameters(),
427 "output_schema": tool.output_schema(),
428 "surface": match meta.surface { ToolSurface::Llm => "llm", ToolSurface::Chassis => "chassis" },
429 "timeout_secs": meta.timeout_secs,
430 "concurrency": match meta.concurrency { ToolConcurrency::Concurrent => "concurrent", ToolConcurrency::Exclusive => "exclusive" },
431 "cost_units": meta.cost_units,
432 "core": meta.core,
433 "risk_class": meta.risk_class.as_str(),
434 "execution_owner": meta.execution_owner.as_str(),
435 "requires_confirmation": meta.requires_confirmation,
436 }))
437 })
438 .collect()
439 }
440
441 pub fn suggest_name(&self, name: &str) -> Option<&str> {
444 let name = name.trim();
445 if name.is_empty() || self.tools.contains_key(name) {
446 return None;
447 }
448 if let Some((canonical, _)) = self
449 .tools
450 .iter()
451 .find(|(canonical, _)| canonical.eq_ignore_ascii_case(name))
452 {
453 return Some(canonical);
454 }
455
456 let lower = name.to_ascii_lowercase();
457 self.tools
458 .keys()
459 .filter(|canonical| {
460 let canonical = canonical.to_ascii_lowercase();
461 let prefix_len = lower.len().checked_sub(canonical.len());
462 let plural_prefix_len = lower
463 .strip_suffix('s')
464 .and_then(|singular| singular.len().checked_sub(canonical.len()))
465 .filter(|_| lower[..lower.len() - 1].ends_with(&canonical));
466 prefix_len
467 .filter(|_| lower.ends_with(&canonical))
468 .or(plural_prefix_len)
469 .is_some_and(|len| {
470 len > 0
471 && name
472 .as_bytes()
473 .get(..len)
474 .is_some_and(|prefix| prefix.iter().all(u8::is_ascii_alphabetic))
475 })
476 })
477 .max_by_key(|canonical| canonical.len())
478 .map(String::as_str)
479 }
480
481 fn unknown_tool_error(&self, name: &str) -> String {
482 let available = if self.tools.is_empty() {
483 "(none registered)".to_string()
484 } else {
485 self.names().join(", ")
486 };
487 match self.suggest_name(name) {
488 Some(suggestion) => format!(
489 "unknown tool '{name}'. Did you mean '{suggestion}'? Call tools by their exact registered name. Available: {available}"
490 ),
491 None => format!(
492 "unknown tool '{name}'. Call one of the registered tools by exact name. Available: {available}"
493 ),
494 }
495 }
496
497 pub async fn execute_with_context(
499 &self,
500 name: &str,
501 context: &ToolExecutionContext,
502 args: Value,
503 ) -> Result<Value, String> {
504 if let Some(canonical) = self.canonical_name(name) {
505 self.validate_arguments(canonical, &args)?;
506 let tool = self
507 .tools
508 .get(canonical)
509 .ok_or_else(|| self.unknown_tool_error(canonical))?;
510 let result = tool.call_with_context(context, args).await;
511 let value = result?;
512 self.validate_output(canonical, &value)?;
513 return Ok(value);
514 }
515 Err(self.unknown_tool_error(name))
516 }
517
518 pub fn canonical_name<'a>(&'a self, name: &'a str) -> Option<&'a str> {
520 if self.tools.contains_key(name) {
521 return Some(name);
522 }
523 self.wire_names
524 .get(name)
525 .map(String::as_str)
526 .or_else(|| self.suggest_name(name))
527 }
528}
529
530pub fn validate_json_schema(schema: &Value, value: &Value) -> Result<(), String> {
532 validate_json_schema_value(schema, value, "tool arguments")
533}
534
535pub fn validate_json_schema_definition(schema: &Value) -> Result<(), String> {
537 jsonschema::validator_for(schema)
538 .map(|_| ())
539 .map_err(|error| format!("invalid JSON schema: {error}"))
540}
541
542pub fn validate_json_schema_value(
544 schema: &Value,
545 value: &Value,
546 subject: &str,
547) -> Result<(), String> {
548 let validator = jsonschema::validator_for(schema)
549 .map_err(|error| format!("invalid JSON schema: {error}"))?;
550 validator
551 .validate(value)
552 .map_err(|error| format!("invalid {subject}: {error}"))
553}
554
555pub fn model_tool_name(internal: &str) -> String {
557 if !internal.is_empty()
558 && internal.len() <= 64
559 && internal
560 .bytes()
561 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-'))
562 {
563 return internal.to_string();
564 }
565 let mut prefix = internal
566 .bytes()
567 .map(|byte| {
568 if byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-') {
569 byte as char
570 } else {
571 '_'
572 }
573 })
574 .take(47)
575 .collect::<String>();
576 if prefix.is_empty() {
577 prefix.push_str("tool");
578 }
579 let digest = format!("{:x}", Sha256::digest(internal.as_bytes()));
580 format!("{prefix}_{}", &digest[..16])
581}
582
583pub mod support {
585 use serde::de::DeserializeOwned;
586 use serde_json::Value;
587
588 pub trait RawToolSchema {
590 fn parameters() -> Value;
592 }
593
594 pub fn extract_required<T: DeserializeOwned>(args: &Value, name: &str) -> Result<T, String> {
596 let value = args
597 .get(name)
598 .cloned()
599 .ok_or_else(|| format!("missing required argument '{name}'"))?;
600 serde_json::from_value(value).map_err(|error| format!("invalid argument '{name}': {error}"))
601 }
602
603 pub fn extract_optional<T: DeserializeOwned>(
605 args: &Value,
606 name: &str,
607 ) -> Result<Option<T>, String> {
608 match args.get(name) {
609 None | Some(Value::Null) => Ok(None),
610 Some(value) => serde_json::from_value(value.clone())
611 .map(Some)
612 .map_err(|error| format!("invalid argument '{name}': {error}")),
613 }
614 }
615}
616
617impl fmt::Debug for ToolRegistry {
618 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
619 f.debug_struct("ToolRegistry")
620 .field("tools", &self.names())
621 .finish()
622 }
623}
624
625#[cfg(test)]
626mod tests {
627 use super::*;
628
629 fn execution() -> ToolExecutionContext {
630 ToolExecutionContext {
631 request: crate::RequestContext {
632 tenant_id: "tenant".parse().unwrap(),
633 subject_id: "subject".parse().unwrap(),
634 roles: Default::default(),
635 locale: "en".into(),
636 request_id: "request".parse().unwrap(),
637 entitlements: Default::default(),
638 },
639 session_id: "session".parse().unwrap(),
640 run_id: "run".parse().unwrap(),
641 step: 1,
642 call_id: "call".parse().unwrap(),
643 source_event_seq: 1,
644 risk_class: ToolRiskClass::Read,
645 execution_owner: ToolExecutionOwner::Runtime,
646 interaction_resolution: None,
647 cancellation: CancellationToken::default(),
648 deadline: Instant::now() + std::time::Duration::from_secs(1),
649 }
650 }
651
652 struct TestTool(&'static str);
653 #[async_trait]
654 impl Tool for TestTool {
655 fn name(&self) -> &str {
656 self.0
657 }
658 fn description(&self) -> &str {
659 "test"
660 }
661 fn parameters(&self) -> Value {
662 serde_json::json!({
663 "type":"object",
664 "required":["items","mode"],
665 "additionalProperties":false,
666 "properties":{
667 "items":{"type":"array","items":{"type":"object","required":["id"],"properties":{"id":{"type":"integer"}}}},
668 "mode":{"enum":["safe","fast"]},
669 "version":{"const":1},
670 "choice":{"oneOf":[{"type":"string"},{"type":"number"}]}
671 }
672 })
673 }
674 fn output_schema(&self) -> Value {
675 serde_json::json!({"type":"object"})
676 }
677 fn implementation_version(&self) -> &str {
678 "1"
679 }
680 async fn call(&self, args: Value) -> Result<Value, String> {
681 Ok(args)
682 }
683 }
684
685 #[test]
686 fn runtime_manifest_rejects_missing_execution_metadata() {
687 let mut registry = ToolRegistry::new();
688 registry
689 .register(Arc::new(TestTool("unclassified")))
690 .unwrap();
691 assert!(registry
692 .runtime_manifest()
693 .unwrap_err()
694 .contains("explicit risk_class and execution_owner"));
695 }
696
697 #[tokio::test]
698 async fn registry_rejects_duplicates_and_validates_full_schema() {
699 let mut registry = ToolRegistry::new();
700 registry
701 .register(Arc::new(TestTool("nested.tool")))
702 .unwrap();
703 assert!(registry
704 .register(Arc::new(TestTool("nested.tool")))
705 .is_err());
706 let valid = serde_json::json!({"items":[{"id":1}],"mode":"safe","version":1,"choice":"x"});
707 assert_eq!(
708 registry
709 .execute_with_context("nested.tool", &execution(), valid.clone())
710 .await
711 .unwrap(),
712 valid
713 );
714 for invalid in [
715 serde_json::json!({"items":[{}],"mode":"safe"}),
716 serde_json::json!({"items":[{"id":1}],"mode":"unsafe"}),
717 serde_json::json!({"items":[{"id":1}],"mode":"safe","extra":true}),
718 serde_json::json!({"items":[{"id":1}],"mode":"safe","version":2}),
719 serde_json::json!({"items":[{"id":1}],"mode":"safe","choice":true}),
720 ] {
721 assert!(registry
722 .execute_with_context("nested.tool", &execution(), invalid)
723 .await
724 .is_err());
725 }
726 }
727
728 #[test]
729 fn model_names_are_provider_safe_and_reversible() {
730 let mut registry = ToolRegistry::new();
731 registry.register(Arc::new(TestTool("namespace.tool with unicode-工具-and-a-name-that-is-far-too-long-for-provider-contracts"))).unwrap();
732 let spec = registry.specs().pop().unwrap().function.name;
733 assert!(spec.len() <= 64);
734 assert!(spec
735 .bytes()
736 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-')));
737 assert_eq!(registry.canonical_name(&spec), Some("namespace.tool with unicode-工具-and-a-name-that-is-far-too-long-for-provider-contracts"));
738
739 let mut collision = ToolRegistry::new();
740 collision.register(Arc::new(TestTool("a.b"))).unwrap();
741 assert_eq!(model_tool_name("a.b"), "a_b_2e7336dc8eba87ef");
742 assert!(collision
743 .register(Arc::new(TestTool("a_b_2e7336dc8eba87ef")))
744 .is_err());
745 }
746
747 #[tokio::test]
748 async fn provider_junk_prefixes_resolve_once_at_the_registry_boundary() {
749 let mut registry = ToolRegistry::new();
750 registry
751 .register(Arc::new(TestTool("analyze_wallet")))
752 .unwrap();
753 let args = serde_json::json!({"items":[{"id":1}],"mode":"safe"});
754
755 assert_eq!(
756 registry
757 .execute_with_context("Notebookanalyze_wallet", &execution(), args.clone())
758 .await
759 .unwrap(),
760 args
761 );
762 assert!(registry
763 .execute_with_context("Listanalyze_wallets", &execution(), args.clone())
764 .await
765 .is_ok());
766 assert!(registry
767 .execute_with_context("namespace.analyze_wallet", &execution(), args)
768 .await
769 .is_err());
770 }
771
772 #[test]
773 fn invalid_schema_is_rejected_at_registration() {
774 struct Invalid;
775 #[async_trait]
776 impl Tool for Invalid {
777 fn name(&self) -> &str {
778 "invalid"
779 }
780 fn description(&self) -> &str {
781 "invalid"
782 }
783 fn parameters(&self) -> Value {
784 serde_json::json!({"type":"not-a-type"})
785 }
786 fn output_schema(&self) -> Value {
787 serde_json::json!({"type":"object"})
788 }
789 async fn call(&self, _: Value) -> Result<Value, String> {
790 Ok(Value::Null)
791 }
792 }
793 assert!(ToolRegistry::new().register(Arc::new(Invalid)).is_err());
794 }
795
796 #[tokio::test]
797 async fn successful_output_is_validated_before_materialization() {
798 struct InvalidOutput;
799 #[async_trait]
800 impl Tool for InvalidOutput {
801 fn name(&self) -> &str {
802 "invalid-output"
803 }
804 fn description(&self) -> &str {
805 "invalid output"
806 }
807 fn parameters(&self) -> Value {
808 serde_json::json!({"type":"object"})
809 }
810 fn output_schema(&self) -> Value {
811 serde_json::json!({"type":"object"})
812 }
813 async fn call(&self, _: Value) -> Result<Value, String> {
814 Ok(Value::String("bad".into()))
815 }
816 }
817 let mut registry = ToolRegistry::new();
818 registry.register(Arc::new(InvalidOutput)).unwrap();
819 assert!(registry
820 .execute_with_context("invalid-output", &execution(), serde_json::json!({}))
821 .await
822 .unwrap_err()
823 .contains("invalid tool output"));
824 }
825}