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, PartialEq, Eq)]
44pub struct ToolMeta {
45 pub surface: ToolSurface,
47 pub cost_units: u64,
49 pub timeout_secs: u64,
51 pub core: bool,
53 pub concurrency: ToolConcurrency,
55 pub requires_confirmation: bool,
58}
59
60#[derive(Debug, Clone, Default)]
63pub struct CancellationToken(Arc<CancellationState>);
64
65#[derive(Debug, Default)]
66struct CancellationState {
67 cancelled: AtomicBool,
68 notify: tokio::sync::Notify,
69 parent: Option<CancellationToken>,
70}
71
72impl CancellationToken {
73 pub fn cancel(&self) {
75 self.0.cancelled.store(true, Ordering::Release);
76 self.0.notify.notify_waiters();
77 }
78 pub fn is_cancelled(&self) -> bool {
80 self.0.cancelled.load(Ordering::Acquire)
81 || self
82 .0
83 .parent
84 .as_ref()
85 .is_some_and(CancellationToken::is_cancelled)
86 }
87 pub fn child(&self) -> Self {
89 Self(Arc::new(CancellationState {
90 cancelled: AtomicBool::new(false),
91 notify: tokio::sync::Notify::new(),
92 parent: Some(self.clone()),
93 }))
94 }
95 pub fn cancelled(&self) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
97 Box::pin(async move {
98 let notified = self.0.notify.notified();
99 tokio::pin!(notified);
100 notified.as_mut().enable();
101 if self.is_cancelled() {
102 return;
103 }
104 match &self.0.parent {
105 Some(parent) => tokio::select! {
106 _ = notified => {}
107 _ = parent.cancelled() => {}
108 },
109 None => notified.await,
110 }
111 })
112 }
113}
114
115#[derive(Debug, Clone)]
117pub struct ToolExecutionContext {
118 pub request: af_context::RequestContext,
120 pub session_id: SessionId,
122 pub run_id: RunId,
124 pub step: u32,
126 pub call_id: ToolCallId,
128 pub source_event_seq: u64,
130 pub interaction_resolution: Option<af_agent_session::InteractionResolution>,
132 pub cancellation: CancellationToken,
134 pub deadline: Instant,
136}
137
138#[derive(Debug, Clone, PartialEq)]
140pub struct ToolCompletionAction {
141 pub tool: String,
143 pub arguments: Value,
145}
146
147impl Default for ToolMeta {
148 fn default() -> Self {
149 Self {
150 surface: ToolSurface::Llm,
151 cost_units: 1,
152 timeout_secs: 15,
153 core: false,
154 concurrency: ToolConcurrency::Exclusive,
155 requires_confirmation: false,
156 }
157 }
158}
159
160#[async_trait]
162pub trait Tool: Send + Sync {
163 fn name(&self) -> &str;
165 fn implementation_version(&self) -> &str {
167 ""
168 }
169 fn description(&self) -> &str;
171 fn parameters(&self) -> Value;
173 fn output_schema(&self) -> Value;
175 fn meta(&self) -> ToolMeta {
177 ToolMeta::default()
178 }
179
180 async fn call(&self, args: Value) -> Result<Value, String>;
184 async fn call_with_context(
186 &self,
187 _context: &ToolExecutionContext,
188 args: Value,
189 ) -> Result<Value, String> {
190 self.call(args).await
191 }
192 fn completion_action(&self, _result: &Value) -> Option<ToolCompletionAction> {
195 None
196 }
197}
198
199#[derive(Default, Clone)]
201pub struct ToolRegistry {
202 tools: HashMap<String, Arc<dyn Tool>>,
203 wire_names: HashMap<String, String>,
204 validators: HashMap<String, Arc<jsonschema::Validator>>,
205 output_validators: HashMap<String, Arc<jsonschema::Validator>>,
206}
207
208impl ToolRegistry {
209 pub fn new() -> Self {
211 Self::default()
212 }
213
214 pub fn register(&mut self, tool: Arc<dyn Tool>) -> Result<&mut Self, String> {
216 let name = tool.name().to_string();
217 if self.tools.contains_key(&name) {
218 return Err(format!("duplicate tool '{name}'"));
219 }
220 let wire_name = model_tool_name(&name);
221 if self.wire_names.contains_key(&wire_name)
222 || (wire_name != name && self.tools.contains_key(&wire_name))
223 || self.wire_names.contains_key(&name)
224 {
225 return Err(format!(
226 "tool name '{name}' collides on provider name '{wire_name}'"
227 ));
228 }
229 let validator = jsonschema::validator_for(&tool.parameters())
230 .map_err(|error| format!("invalid schema for tool '{name}': {error}"))?;
231 let output_validator = jsonschema::validator_for(&tool.output_schema())
232 .map_err(|error| format!("invalid output schema for tool '{name}': {error}"))?;
233 self.tools.insert(name.clone(), tool);
234 self.wire_names.insert(wire_name, name.clone());
235 self.validators.insert(name.clone(), Arc::new(validator));
236 self.output_validators
237 .insert(name, Arc::new(output_validator));
238 Ok(self)
239 }
240
241 pub fn extend(&mut self, other: &Self) -> Result<(), String> {
243 for tool in other.tools.values() {
244 self.register(Arc::clone(tool))?;
245 }
246 Ok(())
247 }
248
249 pub fn is_empty(&self) -> bool {
251 self.tools.is_empty()
252 }
253
254 pub fn len(&self) -> usize {
256 self.tools.len()
257 }
258
259 pub fn contains(&self, name: &str) -> bool {
261 self.tools.contains_key(name)
262 }
263
264 pub fn get(&self, name: &str) -> Option<Arc<dyn Tool>> {
266 self.canonical_name(name)
267 .and_then(|name| self.tools.get(name))
268 .cloned()
269 }
270
271 pub fn validate_arguments(&self, name: &str, arguments: &Value) -> Result<(), String> {
273 let name = self
274 .canonical_name(name)
275 .ok_or_else(|| self.unknown_tool_error(name))?;
276 self.validators
277 .get(name)
278 .ok_or_else(|| self.unknown_tool_error(name))?
279 .validate(arguments)
280 .map_err(|error| format!("invalid tool arguments: {error}"))
281 }
282
283 pub fn validate_output(&self, name: &str, output: &Value) -> Result<(), String> {
285 let name = self
286 .canonical_name(name)
287 .ok_or_else(|| self.unknown_tool_error(name))?;
288 self.output_validators
289 .get(name)
290 .ok_or_else(|| self.unknown_tool_error(name))?
291 .validate(output)
292 .map_err(|error| format!("invalid tool output: {error}"))
293 }
294
295 pub fn names(&self) -> Vec<&str> {
297 let mut names = self.tools.keys().map(String::as_str).collect::<Vec<_>>();
298 names.sort_unstable();
299 names
300 }
301
302 pub fn confirmation_required_names(&self) -> impl Iterator<Item = &str> {
304 self.tools
305 .values()
306 .filter(|tool| tool.meta().requires_confirmation)
307 .map(|tool| tool.name())
308 }
309
310 pub fn filtered<'a>(&self, allowed: impl IntoIterator<Item = &'a str>) -> Self {
312 let mut filtered = Self::new();
313 for name in allowed {
314 if let Some(tool) = self.tools.get(name) {
315 let _ = filtered.register(Arc::clone(tool));
318 }
319 }
320 filtered
321 }
322
323 pub fn specs(&self) -> Vec<LlmTool> {
325 let mut specs = self
326 .tools
327 .values()
328 .filter(|tool| tool.meta().surface == ToolSurface::Llm)
329 .map(|t| LlmTool::function(model_tool_name(t.name()), t.description(), t.parameters()))
330 .collect::<Vec<_>>();
331 specs.sort_by(|left, right| left.function.name.cmp(&right.function.name));
332 specs
333 }
334
335 pub fn runtime_manifest(&self) -> Result<Vec<Value>, String> {
337 self.names()
338 .into_iter()
339 .map(|name| {
340 let tool = self
341 .tools
342 .get(name)
343 .ok_or_else(|| self.unknown_tool_error(name))?;
344 let version = tool.implementation_version().trim();
345 if version.is_empty() {
346 return Err(format!(
347 "tool '{name}' requires a stable implementation version"
348 ));
349 }
350 let meta = tool.meta();
351 Ok(serde_json::json!({
352 "name": name,
353 "implementation_version": version,
354 "description": tool.description(),
355 "parameters": tool.parameters(),
356 "output_schema": tool.output_schema(),
357 "surface": match meta.surface { ToolSurface::Llm => "llm", ToolSurface::Chassis => "chassis" },
358 "timeout_secs": meta.timeout_secs,
359 "concurrency": match meta.concurrency { ToolConcurrency::Concurrent => "concurrent", ToolConcurrency::Exclusive => "exclusive" },
360 "cost_units": meta.cost_units,
361 "core": meta.core,
362 "requires_confirmation": meta.requires_confirmation,
363 }))
364 })
365 .collect()
366 }
367
368 pub fn suggest_name(&self, name: &str) -> Option<&str> {
371 let name = name.trim();
372 if name.is_empty() || self.tools.contains_key(name) {
373 return None;
374 }
375 if let Some((canonical, _)) = self
376 .tools
377 .iter()
378 .find(|(canonical, _)| canonical.eq_ignore_ascii_case(name))
379 {
380 return Some(canonical);
381 }
382
383 let lower = name.to_ascii_lowercase();
384 self.tools
385 .keys()
386 .filter(|canonical| {
387 let canonical = canonical.to_ascii_lowercase();
388 let prefix_len = lower.len().checked_sub(canonical.len());
389 let plural_prefix_len = lower
390 .strip_suffix('s')
391 .and_then(|singular| singular.len().checked_sub(canonical.len()))
392 .filter(|_| lower[..lower.len() - 1].ends_with(&canonical));
393 prefix_len
394 .filter(|_| lower.ends_with(&canonical))
395 .or(plural_prefix_len)
396 .is_some_and(|len| {
397 len > 0
398 && name
399 .as_bytes()
400 .get(..len)
401 .is_some_and(|prefix| prefix.iter().all(u8::is_ascii_alphabetic))
402 })
403 })
404 .max_by_key(|canonical| canonical.len())
405 .map(String::as_str)
406 }
407
408 fn unknown_tool_error(&self, name: &str) -> String {
409 let available = if self.tools.is_empty() {
410 "(none registered)".to_string()
411 } else {
412 self.names().join(", ")
413 };
414 match self.suggest_name(name) {
415 Some(suggestion) => format!(
416 "unknown tool '{name}'. Did you mean '{suggestion}'? Call tools by their exact registered name. Available: {available}"
417 ),
418 None => format!(
419 "unknown tool '{name}'. Call one of the registered tools by exact name. Available: {available}"
420 ),
421 }
422 }
423
424 pub async fn execute_with_context(
426 &self,
427 name: &str,
428 context: &ToolExecutionContext,
429 args: Value,
430 ) -> Result<Value, String> {
431 if let Some(canonical) = self.canonical_name(name) {
432 self.validate_arguments(canonical, &args)?;
433 let tool = self
434 .tools
435 .get(canonical)
436 .ok_or_else(|| self.unknown_tool_error(canonical))?;
437 let result = tool.call_with_context(context, args).await;
438 let value = result?;
439 self.validate_output(canonical, &value)?;
440 return Ok(value);
441 }
442 Err(self.unknown_tool_error(name))
443 }
444
445 pub fn canonical_name<'a>(&'a self, name: &'a str) -> Option<&'a str> {
447 if self.tools.contains_key(name) {
448 return Some(name);
449 }
450 self.wire_names
451 .get(name)
452 .map(String::as_str)
453 .or_else(|| self.suggest_name(name))
454 }
455}
456
457pub fn validate_json_schema(schema: &Value, value: &Value) -> Result<(), String> {
459 validate_json_schema_value(schema, value, "tool arguments")
460}
461
462pub fn validate_json_schema_definition(schema: &Value) -> Result<(), String> {
464 jsonschema::validator_for(schema)
465 .map(|_| ())
466 .map_err(|error| format!("invalid JSON schema: {error}"))
467}
468
469pub fn validate_json_schema_value(
471 schema: &Value,
472 value: &Value,
473 subject: &str,
474) -> Result<(), String> {
475 let validator = jsonschema::validator_for(schema)
476 .map_err(|error| format!("invalid JSON schema: {error}"))?;
477 validator
478 .validate(value)
479 .map_err(|error| format!("invalid {subject}: {error}"))
480}
481
482pub fn model_tool_name(internal: &str) -> String {
484 if !internal.is_empty()
485 && internal.len() <= 64
486 && internal
487 .bytes()
488 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-'))
489 {
490 return internal.to_string();
491 }
492 let mut prefix = internal
493 .bytes()
494 .map(|byte| {
495 if byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-') {
496 byte as char
497 } else {
498 '_'
499 }
500 })
501 .take(47)
502 .collect::<String>();
503 if prefix.is_empty() {
504 prefix.push_str("tool");
505 }
506 let digest = format!("{:x}", Sha256::digest(internal.as_bytes()));
507 format!("{prefix}_{}", &digest[..16])
508}
509
510pub mod support {
512 use serde::de::DeserializeOwned;
513 use serde_json::Value;
514
515 pub trait RawToolSchema {
517 fn parameters() -> Value;
519 }
520
521 pub fn extract_required<T: DeserializeOwned>(args: &Value, name: &str) -> Result<T, String> {
523 let value = args
524 .get(name)
525 .cloned()
526 .ok_or_else(|| format!("missing required argument '{name}'"))?;
527 serde_json::from_value(value).map_err(|error| format!("invalid argument '{name}': {error}"))
528 }
529
530 pub fn extract_optional<T: DeserializeOwned>(
532 args: &Value,
533 name: &str,
534 ) -> Result<Option<T>, String> {
535 match args.get(name) {
536 None | Some(Value::Null) => Ok(None),
537 Some(value) => serde_json::from_value(value.clone())
538 .map(Some)
539 .map_err(|error| format!("invalid argument '{name}': {error}")),
540 }
541 }
542}
543
544impl fmt::Debug for ToolRegistry {
545 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
546 f.debug_struct("ToolRegistry")
547 .field("tools", &self.names())
548 .finish()
549 }
550}
551
552#[cfg(test)]
553mod tests {
554 use super::*;
555
556 fn execution() -> ToolExecutionContext {
557 ToolExecutionContext {
558 request: crate::RequestContext {
559 tenant_id: "tenant".parse().unwrap(),
560 subject_id: "subject".parse().unwrap(),
561 roles: Default::default(),
562 locale: "en".into(),
563 request_id: "request".parse().unwrap(),
564 entitlements: Default::default(),
565 },
566 session_id: "session".parse().unwrap(),
567 run_id: "run".parse().unwrap(),
568 step: 1,
569 call_id: "call".parse().unwrap(),
570 source_event_seq: 1,
571 interaction_resolution: None,
572 cancellation: CancellationToken::default(),
573 deadline: Instant::now() + std::time::Duration::from_secs(1),
574 }
575 }
576
577 struct TestTool(&'static str);
578 #[async_trait]
579 impl Tool for TestTool {
580 fn name(&self) -> &str {
581 self.0
582 }
583 fn description(&self) -> &str {
584 "test"
585 }
586 fn parameters(&self) -> Value {
587 serde_json::json!({
588 "type":"object",
589 "required":["items","mode"],
590 "additionalProperties":false,
591 "properties":{
592 "items":{"type":"array","items":{"type":"object","required":["id"],"properties":{"id":{"type":"integer"}}}},
593 "mode":{"enum":["safe","fast"]},
594 "version":{"const":1},
595 "choice":{"oneOf":[{"type":"string"},{"type":"number"}]}
596 }
597 })
598 }
599 fn output_schema(&self) -> Value {
600 serde_json::json!({"type":"object"})
601 }
602 async fn call(&self, args: Value) -> Result<Value, String> {
603 Ok(args)
604 }
605 }
606
607 #[tokio::test]
608 async fn registry_rejects_duplicates_and_validates_full_schema() {
609 let mut registry = ToolRegistry::new();
610 registry
611 .register(Arc::new(TestTool("nested.tool")))
612 .unwrap();
613 assert!(registry
614 .register(Arc::new(TestTool("nested.tool")))
615 .is_err());
616 let valid = serde_json::json!({"items":[{"id":1}],"mode":"safe","version":1,"choice":"x"});
617 assert_eq!(
618 registry
619 .execute_with_context("nested.tool", &execution(), valid.clone())
620 .await
621 .unwrap(),
622 valid
623 );
624 for invalid in [
625 serde_json::json!({"items":[{}],"mode":"safe"}),
626 serde_json::json!({"items":[{"id":1}],"mode":"unsafe"}),
627 serde_json::json!({"items":[{"id":1}],"mode":"safe","extra":true}),
628 serde_json::json!({"items":[{"id":1}],"mode":"safe","version":2}),
629 serde_json::json!({"items":[{"id":1}],"mode":"safe","choice":true}),
630 ] {
631 assert!(registry
632 .execute_with_context("nested.tool", &execution(), invalid)
633 .await
634 .is_err());
635 }
636 }
637
638 #[test]
639 fn model_names_are_provider_safe_and_reversible() {
640 let mut registry = ToolRegistry::new();
641 registry.register(Arc::new(TestTool("namespace.tool with unicode-工具-and-a-name-that-is-far-too-long-for-provider-contracts"))).unwrap();
642 let spec = registry.specs().pop().unwrap().function.name;
643 assert!(spec.len() <= 64);
644 assert!(spec
645 .bytes()
646 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-')));
647 assert_eq!(registry.canonical_name(&spec), Some("namespace.tool with unicode-工具-and-a-name-that-is-far-too-long-for-provider-contracts"));
648
649 let mut collision = ToolRegistry::new();
650 collision.register(Arc::new(TestTool("a.b"))).unwrap();
651 assert_eq!(model_tool_name("a.b"), "a_b_2e7336dc8eba87ef");
652 assert!(collision
653 .register(Arc::new(TestTool("a_b_2e7336dc8eba87ef")))
654 .is_err());
655 }
656
657 #[tokio::test]
658 async fn provider_junk_prefixes_resolve_once_at_the_registry_boundary() {
659 let mut registry = ToolRegistry::new();
660 registry
661 .register(Arc::new(TestTool("analyze_wallet")))
662 .unwrap();
663 let args = serde_json::json!({"items":[{"id":1}],"mode":"safe"});
664
665 assert_eq!(
666 registry
667 .execute_with_context("Notebookanalyze_wallet", &execution(), args.clone())
668 .await
669 .unwrap(),
670 args
671 );
672 assert!(registry
673 .execute_with_context("Listanalyze_wallets", &execution(), args.clone())
674 .await
675 .is_ok());
676 assert!(registry
677 .execute_with_context("namespace.analyze_wallet", &execution(), args)
678 .await
679 .is_err());
680 }
681
682 #[test]
683 fn invalid_schema_is_rejected_at_registration() {
684 struct Invalid;
685 #[async_trait]
686 impl Tool for Invalid {
687 fn name(&self) -> &str {
688 "invalid"
689 }
690 fn description(&self) -> &str {
691 "invalid"
692 }
693 fn parameters(&self) -> Value {
694 serde_json::json!({"type":"not-a-type"})
695 }
696 fn output_schema(&self) -> Value {
697 serde_json::json!({"type":"object"})
698 }
699 async fn call(&self, _: Value) -> Result<Value, String> {
700 Ok(Value::Null)
701 }
702 }
703 assert!(ToolRegistry::new().register(Arc::new(Invalid)).is_err());
704 }
705
706 #[tokio::test]
707 async fn successful_output_is_validated_before_materialization() {
708 struct InvalidOutput;
709 #[async_trait]
710 impl Tool for InvalidOutput {
711 fn name(&self) -> &str {
712 "invalid-output"
713 }
714 fn description(&self) -> &str {
715 "invalid output"
716 }
717 fn parameters(&self) -> Value {
718 serde_json::json!({"type":"object"})
719 }
720 fn output_schema(&self) -> Value {
721 serde_json::json!({"type":"object"})
722 }
723 async fn call(&self, _: Value) -> Result<Value, String> {
724 Ok(Value::String("bad".into()))
725 }
726 }
727 let mut registry = ToolRegistry::new();
728 registry.register(Arc::new(InvalidOutput)).unwrap();
729 assert!(registry
730 .execute_with_context("invalid-output", &execution(), serde_json::json!({}))
731 .await
732 .unwrap_err()
733 .contains("invalid tool output"));
734 }
735}