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