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 name.len() > canonical.len() && lower.ends_with(&canonical.to_ascii_lowercase())
374 })
375 .max_by_key(|canonical| canonical.len())
376 .map(String::as_str)
377 }
378
379 fn unknown_tool_error(&self, name: &str) -> String {
380 let available = if self.tools.is_empty() {
381 "(none registered)".to_string()
382 } else {
383 self.names().join(", ")
384 };
385 match self.suggest_name(name) {
386 Some(suggestion) => format!(
387 "unknown tool '{name}'. Did you mean '{suggestion}'? Call tools by their exact registered name. Available: {available}"
388 ),
389 None => format!(
390 "unknown tool '{name}'. Call one of the registered tools by exact name. Available: {available}"
391 ),
392 }
393 }
394
395 pub async fn execute_with_context(
397 &self,
398 name: &str,
399 context: &ToolExecutionContext,
400 args: Value,
401 ) -> Result<Value, String> {
402 if let Some(canonical) = self.canonical_name(name) {
403 self.validate_arguments(canonical, &args)?;
404 let tool = self
405 .tools
406 .get(canonical)
407 .ok_or_else(|| self.unknown_tool_error(canonical))?;
408 let result = tool.call_with_context(context, args).await;
409 let value = result?;
410 self.validate_output(canonical, &value)?;
411 return Ok(value);
412 }
413 Err(self.unknown_tool_error(name))
414 }
415
416 pub fn canonical_name<'a>(&'a self, name: &'a str) -> Option<&'a str> {
418 if self.tools.contains_key(name) {
419 return Some(name);
420 }
421 self.wire_names.get(name).map(String::as_str)
422 }
423}
424
425pub fn validate_json_schema(schema: &Value, value: &Value) -> Result<(), String> {
427 validate_json_schema_value(schema, value, "tool arguments")
428}
429
430pub fn validate_json_schema_definition(schema: &Value) -> Result<(), String> {
432 jsonschema::validator_for(schema)
433 .map(|_| ())
434 .map_err(|error| format!("invalid JSON schema: {error}"))
435}
436
437pub fn validate_json_schema_value(
439 schema: &Value,
440 value: &Value,
441 subject: &str,
442) -> Result<(), String> {
443 let validator = jsonschema::validator_for(schema)
444 .map_err(|error| format!("invalid JSON schema: {error}"))?;
445 validator
446 .validate(value)
447 .map_err(|error| format!("invalid {subject}: {error}"))
448}
449
450pub fn model_tool_name(internal: &str) -> String {
452 if !internal.is_empty()
453 && internal.len() <= 64
454 && internal
455 .bytes()
456 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-'))
457 {
458 return internal.to_string();
459 }
460 let mut prefix = internal
461 .bytes()
462 .map(|byte| {
463 if byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-') {
464 byte as char
465 } else {
466 '_'
467 }
468 })
469 .take(47)
470 .collect::<String>();
471 if prefix.is_empty() {
472 prefix.push_str("tool");
473 }
474 let digest = format!("{:x}", Sha256::digest(internal.as_bytes()));
475 format!("{prefix}_{}", &digest[..16])
476}
477
478pub mod support {
480 use serde::de::DeserializeOwned;
481 use serde_json::Value;
482
483 pub trait RawToolSchema {
485 fn parameters() -> Value;
487 }
488
489 pub fn extract_required<T: DeserializeOwned>(args: &Value, name: &str) -> Result<T, String> {
491 let value = args
492 .get(name)
493 .cloned()
494 .ok_or_else(|| format!("missing required argument '{name}'"))?;
495 serde_json::from_value(value).map_err(|error| format!("invalid argument '{name}': {error}"))
496 }
497
498 pub fn extract_optional<T: DeserializeOwned>(
500 args: &Value,
501 name: &str,
502 ) -> Result<Option<T>, String> {
503 match args.get(name) {
504 None | Some(Value::Null) => Ok(None),
505 Some(value) => serde_json::from_value(value.clone())
506 .map(Some)
507 .map_err(|error| format!("invalid argument '{name}': {error}")),
508 }
509 }
510}
511
512impl fmt::Debug for ToolRegistry {
513 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
514 f.debug_struct("ToolRegistry")
515 .field("tools", &self.names())
516 .finish()
517 }
518}
519
520#[cfg(test)]
521mod tests {
522 use super::*;
523
524 fn execution() -> ToolExecutionContext {
525 ToolExecutionContext {
526 request: crate::RequestContext {
527 tenant_id: "tenant".parse().unwrap(),
528 subject_id: "subject".parse().unwrap(),
529 roles: Default::default(),
530 locale: "en".into(),
531 request_id: "request".parse().unwrap(),
532 entitlements: Default::default(),
533 },
534 session_id: "session".parse().unwrap(),
535 run_id: "run".parse().unwrap(),
536 step: 1,
537 call_id: "call".parse().unwrap(),
538 source_event_seq: 1,
539 interaction_resolution: None,
540 cancellation: CancellationToken::default(),
541 deadline: Instant::now() + std::time::Duration::from_secs(1),
542 }
543 }
544
545 struct TestTool(&'static str);
546 #[async_trait]
547 impl Tool for TestTool {
548 fn name(&self) -> &str {
549 self.0
550 }
551 fn description(&self) -> &str {
552 "test"
553 }
554 fn parameters(&self) -> Value {
555 serde_json::json!({
556 "type":"object",
557 "required":["items","mode"],
558 "additionalProperties":false,
559 "properties":{
560 "items":{"type":"array","items":{"type":"object","required":["id"],"properties":{"id":{"type":"integer"}}}},
561 "mode":{"enum":["safe","fast"]},
562 "version":{"const":1},
563 "choice":{"oneOf":[{"type":"string"},{"type":"number"}]}
564 }
565 })
566 }
567 fn output_schema(&self) -> Value {
568 serde_json::json!({"type":"object"})
569 }
570 async fn call(&self, args: Value) -> Result<Value, String> {
571 Ok(args)
572 }
573 }
574
575 #[tokio::test]
576 async fn registry_rejects_duplicates_and_validates_full_schema() {
577 let mut registry = ToolRegistry::new();
578 registry
579 .register(Arc::new(TestTool("nested.tool")))
580 .unwrap();
581 assert!(registry
582 .register(Arc::new(TestTool("nested.tool")))
583 .is_err());
584 let valid = serde_json::json!({"items":[{"id":1}],"mode":"safe","version":1,"choice":"x"});
585 assert_eq!(
586 registry
587 .execute_with_context("nested.tool", &execution(), valid.clone())
588 .await
589 .unwrap(),
590 valid
591 );
592 for invalid in [
593 serde_json::json!({"items":[{}],"mode":"safe"}),
594 serde_json::json!({"items":[{"id":1}],"mode":"unsafe"}),
595 serde_json::json!({"items":[{"id":1}],"mode":"safe","extra":true}),
596 serde_json::json!({"items":[{"id":1}],"mode":"safe","version":2}),
597 serde_json::json!({"items":[{"id":1}],"mode":"safe","choice":true}),
598 ] {
599 assert!(registry
600 .execute_with_context("nested.tool", &execution(), invalid)
601 .await
602 .is_err());
603 }
604 }
605
606 #[test]
607 fn model_names_are_provider_safe_and_reversible() {
608 let mut registry = ToolRegistry::new();
609 registry.register(Arc::new(TestTool("namespace.tool with unicode-工具-and-a-name-that-is-far-too-long-for-provider-contracts"))).unwrap();
610 let spec = registry.specs().pop().unwrap().function.name;
611 assert!(spec.len() <= 64);
612 assert!(spec
613 .bytes()
614 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-')));
615 assert_eq!(registry.canonical_name(&spec), Some("namespace.tool with unicode-工具-and-a-name-that-is-far-too-long-for-provider-contracts"));
616
617 let mut collision = ToolRegistry::new();
618 collision.register(Arc::new(TestTool("a.b"))).unwrap();
619 assert_eq!(model_tool_name("a.b"), "a_b_2e7336dc8eba87ef");
620 assert!(collision
621 .register(Arc::new(TestTool("a_b_2e7336dc8eba87ef")))
622 .is_err());
623 }
624
625 #[test]
626 fn invalid_schema_is_rejected_at_registration() {
627 struct Invalid;
628 #[async_trait]
629 impl Tool for Invalid {
630 fn name(&self) -> &str {
631 "invalid"
632 }
633 fn description(&self) -> &str {
634 "invalid"
635 }
636 fn parameters(&self) -> Value {
637 serde_json::json!({"type":"not-a-type"})
638 }
639 fn output_schema(&self) -> Value {
640 serde_json::json!({"type":"object"})
641 }
642 async fn call(&self, _: Value) -> Result<Value, String> {
643 Ok(Value::Null)
644 }
645 }
646 assert!(ToolRegistry::new().register(Arc::new(Invalid)).is_err());
647 }
648
649 #[tokio::test]
650 async fn successful_output_is_validated_before_materialization() {
651 struct InvalidOutput;
652 #[async_trait]
653 impl Tool for InvalidOutput {
654 fn name(&self) -> &str {
655 "invalid-output"
656 }
657 fn description(&self) -> &str {
658 "invalid output"
659 }
660 fn parameters(&self) -> Value {
661 serde_json::json!({"type":"object"})
662 }
663 fn output_schema(&self) -> Value {
664 serde_json::json!({"type":"object"})
665 }
666 async fn call(&self, _: Value) -> Result<Value, String> {
667 Ok(Value::String("bad".into()))
668 }
669 }
670 let mut registry = ToolRegistry::new();
671 registry.register(Arc::new(InvalidOutput)).unwrap();
672 assert!(registry
673 .execute_with_context("invalid-output", &execution(), serde_json::json!({}))
674 .await
675 .unwrap_err()
676 .contains("invalid tool output"));
677 }
678}