1use std::collections::HashMap;
10use std::fmt;
11use std::sync::atomic::{AtomicBool, Ordering};
12use std::sync::Arc;
13use std::time::Instant;
14
15use async_trait::async_trait;
16use serde_json::Value;
17use sha2::{Digest, Sha256};
18
19use af_llm::Tool as LlmTool;
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum ToolSurface {
23 Llm,
24 Chassis,
25}
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub enum ToolConcurrency {
29 Concurrent,
30 Exclusive,
31}
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub struct ToolMeta {
35 pub surface: ToolSurface,
36 pub cost_units: u64,
37 pub timeout_secs: u64,
38 pub core: bool,
39 pub concurrency: ToolConcurrency,
40}
41
42#[derive(Debug, Clone, Default)]
43pub struct CancellationToken(Arc<CancellationState>);
44
45#[derive(Debug, Default)]
46struct CancellationState {
47 cancelled: AtomicBool,
48 parent: Option<CancellationToken>,
49}
50
51impl CancellationToken {
52 pub fn cancel(&self) {
53 self.0.cancelled.store(true, Ordering::Release);
54 }
55 pub fn is_cancelled(&self) -> bool {
56 self.0.cancelled.load(Ordering::Acquire)
57 || self
58 .0
59 .parent
60 .as_ref()
61 .is_some_and(CancellationToken::is_cancelled)
62 }
63 pub fn child(&self) -> Self {
64 Self(Arc::new(CancellationState {
65 cancelled: AtomicBool::new(false),
66 parent: Some(self.clone()),
67 }))
68 }
69}
70
71#[derive(Debug, Clone)]
72pub struct ToolExecutionContext {
73 pub request: af_context::RequestContext,
74 pub session_id: String,
75 pub run_id: String,
76 pub step: u32,
77 pub call_id: String,
78 pub source_event_seq: u64,
79 pub interaction_resolution: Option<af_agent_session::InteractionResolution>,
80 pub cancellation: CancellationToken,
81 pub deadline: Instant,
82}
83
84impl Default for ToolMeta {
85 fn default() -> Self {
86 Self {
87 surface: ToolSurface::Llm,
88 cost_units: 1,
89 timeout_secs: 15,
90 core: false,
91 concurrency: ToolConcurrency::Exclusive,
92 }
93 }
94}
95
96#[async_trait]
98pub trait Tool: Send + Sync {
99 fn name(&self) -> &str;
100 fn implementation_version(&self) -> &str {
101 ""
102 }
103 fn description(&self) -> &str;
104 fn parameters(&self) -> Value;
105 fn output_schema(&self) -> Value;
106 fn meta(&self) -> ToolMeta {
107 ToolMeta::default()
108 }
109
110 async fn call(&self, args: Value) -> Result<Value, String>;
114 async fn call_with_context(
115 &self,
116 _context: &ToolExecutionContext,
117 args: Value,
118 ) -> Result<Value, String> {
119 self.call(args).await
120 }
121}
122
123#[derive(Default, Clone)]
125pub struct ToolRegistry {
126 tools: HashMap<String, Arc<dyn Tool>>,
127 wire_names: HashMap<String, String>,
128 validators: HashMap<String, Arc<jsonschema::Validator>>,
129 output_validators: HashMap<String, Arc<jsonschema::Validator>>,
130}
131
132impl ToolRegistry {
133 pub fn new() -> Self {
134 Self::default()
135 }
136
137 pub fn register(&mut self, tool: Arc<dyn Tool>) -> Result<&mut Self, String> {
138 let name = tool.name().to_string();
139 if self.tools.contains_key(&name) {
140 return Err(format!("duplicate tool '{name}'"));
141 }
142 let wire_name = model_tool_name(&name);
143 if self.wire_names.contains_key(&wire_name)
144 || (wire_name != name && self.tools.contains_key(&wire_name))
145 || self.wire_names.contains_key(&name)
146 {
147 return Err(format!(
148 "tool name '{name}' collides on provider name '{wire_name}'"
149 ));
150 }
151 let validator = jsonschema::validator_for(&tool.parameters())
152 .map_err(|error| format!("invalid schema for tool '{name}': {error}"))?;
153 let output_validator = jsonschema::validator_for(&tool.output_schema())
154 .map_err(|error| format!("invalid output schema for tool '{name}': {error}"))?;
155 self.tools.insert(name.clone(), tool);
156 self.wire_names.insert(wire_name, name.clone());
157 self.validators.insert(name.clone(), Arc::new(validator));
158 self.output_validators
159 .insert(name, Arc::new(output_validator));
160 Ok(self)
161 }
162
163 pub fn extend(&mut self, other: &Self) -> Result<(), String> {
164 for tool in other.tools.values() {
165 self.register(Arc::clone(tool))?;
166 }
167 Ok(())
168 }
169
170 pub fn is_empty(&self) -> bool {
171 self.tools.is_empty()
172 }
173
174 pub fn len(&self) -> usize {
175 self.tools.len()
176 }
177
178 pub fn contains(&self, name: &str) -> bool {
179 self.tools.contains_key(name)
180 }
181
182 pub fn get(&self, name: &str) -> Option<Arc<dyn Tool>> {
183 self.canonical_name(name)
184 .and_then(|name| self.tools.get(name))
185 .cloned()
186 }
187
188 pub fn validate_arguments(&self, name: &str, arguments: &Value) -> Result<(), String> {
189 let name = self
190 .canonical_name(name)
191 .ok_or_else(|| self.unknown_tool_error(name))?;
192 self.validators
193 .get(name)
194 .expect("registered tools have compiled schemas")
195 .validate(arguments)
196 .map_err(|error| format!("invalid tool arguments: {error}"))
197 }
198
199 pub fn validate_output(&self, name: &str, output: &Value) -> Result<(), String> {
200 let name = self
201 .canonical_name(name)
202 .ok_or_else(|| self.unknown_tool_error(name))?;
203 self.output_validators
204 .get(name)
205 .expect("registered tools have compiled output schemas")
206 .validate(output)
207 .map_err(|error| format!("invalid tool output: {error}"))
208 }
209
210 pub fn names(&self) -> Vec<&str> {
212 let mut names = self.tools.keys().map(String::as_str).collect::<Vec<_>>();
213 names.sort_unstable();
214 names
215 }
216
217 pub fn filtered<'a>(&self, allowed: impl IntoIterator<Item = &'a str>) -> Self {
219 let mut filtered = Self::new();
220 for name in allowed {
221 if let Some(tool) = self.tools.get(name) {
222 filtered
223 .register(Arc::clone(tool))
224 .expect("a subset of a valid registry remains valid");
225 }
226 }
227 filtered
228 }
229
230 pub fn specs(&self) -> Vec<LlmTool> {
232 let mut specs = self
233 .tools
234 .values()
235 .filter(|tool| tool.meta().surface == ToolSurface::Llm)
236 .map(|t| LlmTool::function(model_tool_name(t.name()), t.description(), t.parameters()))
237 .collect::<Vec<_>>();
238 specs.sort_by(|left, right| left.function.name.cmp(&right.function.name));
239 specs
240 }
241
242 pub fn runtime_manifest(&self) -> Result<Vec<Value>, String> {
244 self.names()
245 .into_iter()
246 .map(|name| {
247 let tool = self.tools.get(name).expect("name came from this registry");
248 let version = tool.implementation_version().trim();
249 if version.is_empty() {
250 return Err(format!(
251 "tool '{name}' requires a stable implementation version"
252 ));
253 }
254 let meta = tool.meta();
255 Ok(serde_json::json!({
256 "name": name,
257 "implementation_version": version,
258 "description": tool.description(),
259 "parameters": tool.parameters(),
260 "output_schema": tool.output_schema(),
261 "surface": match meta.surface { ToolSurface::Llm => "llm", ToolSurface::Chassis => "chassis" },
262 "timeout_secs": meta.timeout_secs,
263 "concurrency": match meta.concurrency { ToolConcurrency::Concurrent => "concurrent", ToolConcurrency::Exclusive => "exclusive" },
264 "cost_units": meta.cost_units,
265 "core": meta.core,
266 }))
267 })
268 .collect()
269 }
270
271 pub fn suggest_name(&self, name: &str) -> Option<&str> {
274 let name = name.trim();
275 if name.is_empty() || self.tools.contains_key(name) {
276 return None;
277 }
278 if let Some((canonical, _)) = self
279 .tools
280 .iter()
281 .find(|(canonical, _)| canonical.eq_ignore_ascii_case(name))
282 {
283 return Some(canonical);
284 }
285
286 let lower = name.to_ascii_lowercase();
287 self.tools
288 .keys()
289 .filter(|canonical| {
290 name.len() > canonical.len() && lower.ends_with(&canonical.to_ascii_lowercase())
291 })
292 .max_by_key(|canonical| canonical.len())
293 .map(String::as_str)
294 }
295
296 fn unknown_tool_error(&self, name: &str) -> String {
297 let available = if self.tools.is_empty() {
298 "(none registered)".to_string()
299 } else {
300 self.names().join(", ")
301 };
302 match self.suggest_name(name) {
303 Some(suggestion) => format!(
304 "unknown tool '{name}'. Did you mean '{suggestion}'? Call tools by their exact registered name. Available: {available}"
305 ),
306 None => format!(
307 "unknown tool '{name}'. Call one of the registered tools by exact name. Available: {available}"
308 ),
309 }
310 }
311
312 pub async fn execute_with_context(
313 &self,
314 name: &str,
315 context: &ToolExecutionContext,
316 args: Value,
317 ) -> Result<Value, String> {
318 if let Some(canonical) = self.canonical_name(name) {
319 self.validate_arguments(canonical, &args)?;
320 let tool = self
321 .tools
322 .get(canonical)
323 .expect("resolved tool names are registered");
324 let result = tool.call_with_context(context, args).await;
325 let value = result?;
326 self.validate_output(canonical, &value)?;
327 return Ok(value);
328 }
329 Err(self.unknown_tool_error(name))
330 }
331
332 pub fn canonical_name<'a>(&'a self, name: &'a str) -> Option<&'a str> {
333 if self.tools.contains_key(name) {
334 return Some(name);
335 }
336 self.wire_names.get(name).map(String::as_str)
337 }
338}
339
340pub fn validate_json_schema(schema: &Value, value: &Value) -> Result<(), String> {
341 validate_json_schema_value(schema, value, "tool arguments")
342}
343
344pub fn validate_json_schema_definition(schema: &Value) -> Result<(), String> {
345 jsonschema::validator_for(schema)
346 .map(|_| ())
347 .map_err(|error| format!("invalid JSON schema: {error}"))
348}
349
350pub fn validate_json_schema_value(
351 schema: &Value,
352 value: &Value,
353 subject: &str,
354) -> Result<(), String> {
355 let validator = jsonschema::validator_for(schema)
356 .map_err(|error| format!("invalid JSON schema: {error}"))?;
357 validator
358 .validate(value)
359 .map_err(|error| format!("invalid {subject}: {error}"))
360}
361
362pub fn model_tool_name(internal: &str) -> String {
363 if !internal.is_empty()
364 && internal.len() <= 64
365 && internal
366 .bytes()
367 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-'))
368 {
369 return internal.to_string();
370 }
371 let mut prefix = internal
372 .bytes()
373 .map(|byte| {
374 if byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-') {
375 byte as char
376 } else {
377 '_'
378 }
379 })
380 .take(47)
381 .collect::<String>();
382 if prefix.is_empty() {
383 prefix.push_str("tool");
384 }
385 let digest = format!("{:x}", Sha256::digest(internal.as_bytes()));
386 format!("{prefix}_{}", &digest[..16])
387}
388
389pub mod support {
390 use serde::de::DeserializeOwned;
391 use serde_json::Value;
392
393 pub trait RawToolSchema {
394 fn parameters() -> Value;
395 }
396
397 pub fn extract_required<T: DeserializeOwned>(args: &Value, name: &str) -> Result<T, String> {
398 let value = args
399 .get(name)
400 .cloned()
401 .ok_or_else(|| format!("missing required argument '{name}'"))?;
402 serde_json::from_value(value).map_err(|error| format!("invalid argument '{name}': {error}"))
403 }
404
405 pub fn extract_optional<T: DeserializeOwned>(
406 args: &Value,
407 name: &str,
408 ) -> Result<Option<T>, String> {
409 match args.get(name) {
410 None | Some(Value::Null) => Ok(None),
411 Some(value) => serde_json::from_value(value.clone())
412 .map(Some)
413 .map_err(|error| format!("invalid argument '{name}': {error}")),
414 }
415 }
416}
417
418impl fmt::Debug for ToolRegistry {
419 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
420 f.debug_struct("ToolRegistry")
421 .field("tools", &self.names())
422 .finish()
423 }
424}
425
426#[cfg(test)]
427mod tests {
428 use super::*;
429
430 fn execution() -> ToolExecutionContext {
431 ToolExecutionContext {
432 request: crate::RequestContext {
433 tenant_id: "tenant".into(),
434 subject_id: "subject".into(),
435 roles: Default::default(),
436 locale: "en".into(),
437 request_id: "request".into(),
438 entitlements: Default::default(),
439 },
440 session_id: "session".into(),
441 run_id: "run".into(),
442 step: 1,
443 call_id: "call".into(),
444 source_event_seq: 1,
445 interaction_resolution: None,
446 cancellation: CancellationToken::default(),
447 deadline: Instant::now() + std::time::Duration::from_secs(1),
448 }
449 }
450
451 struct TestTool(&'static str);
452 #[async_trait]
453 impl Tool for TestTool {
454 fn name(&self) -> &str {
455 self.0
456 }
457 fn description(&self) -> &str {
458 "test"
459 }
460 fn parameters(&self) -> Value {
461 serde_json::json!({
462 "type":"object",
463 "required":["items","mode"],
464 "additionalProperties":false,
465 "properties":{
466 "items":{"type":"array","items":{"type":"object","required":["id"],"properties":{"id":{"type":"integer"}}}},
467 "mode":{"enum":["safe","fast"]},
468 "version":{"const":1},
469 "choice":{"oneOf":[{"type":"string"},{"type":"number"}]}
470 }
471 })
472 }
473 fn output_schema(&self) -> Value {
474 serde_json::json!({"type":"object"})
475 }
476 async fn call(&self, args: Value) -> Result<Value, String> {
477 Ok(args)
478 }
479 }
480
481 #[tokio::test]
482 async fn registry_rejects_duplicates_and_validates_full_schema() {
483 let mut registry = ToolRegistry::new();
484 registry
485 .register(Arc::new(TestTool("nested.tool")))
486 .unwrap();
487 assert!(registry
488 .register(Arc::new(TestTool("nested.tool")))
489 .is_err());
490 let valid = serde_json::json!({"items":[{"id":1}],"mode":"safe","version":1,"choice":"x"});
491 assert_eq!(
492 registry
493 .execute_with_context("nested.tool", &execution(), valid.clone())
494 .await
495 .unwrap(),
496 valid
497 );
498 for invalid in [
499 serde_json::json!({"items":[{}],"mode":"safe"}),
500 serde_json::json!({"items":[{"id":1}],"mode":"unsafe"}),
501 serde_json::json!({"items":[{"id":1}],"mode":"safe","extra":true}),
502 serde_json::json!({"items":[{"id":1}],"mode":"safe","version":2}),
503 serde_json::json!({"items":[{"id":1}],"mode":"safe","choice":true}),
504 ] {
505 assert!(registry
506 .execute_with_context("nested.tool", &execution(), invalid)
507 .await
508 .is_err());
509 }
510 }
511
512 #[test]
513 fn model_names_are_provider_safe_and_reversible() {
514 let mut registry = ToolRegistry::new();
515 registry.register(Arc::new(TestTool("namespace.tool with unicode-工具-and-a-name-that-is-far-too-long-for-provider-contracts"))).unwrap();
516 let spec = registry.specs().pop().unwrap().function.name;
517 assert!(spec.len() <= 64);
518 assert!(spec
519 .bytes()
520 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-')));
521 assert_eq!(registry.canonical_name(&spec), Some("namespace.tool with unicode-工具-and-a-name-that-is-far-too-long-for-provider-contracts"));
522
523 let mut collision = ToolRegistry::new();
524 collision.register(Arc::new(TestTool("a.b"))).unwrap();
525 assert_eq!(model_tool_name("a.b"), "a_b_2e7336dc8eba87ef");
526 assert!(collision
527 .register(Arc::new(TestTool("a_b_2e7336dc8eba87ef")))
528 .is_err());
529 }
530
531 #[test]
532 fn invalid_schema_is_rejected_at_registration() {
533 struct Invalid;
534 #[async_trait]
535 impl Tool for Invalid {
536 fn name(&self) -> &str {
537 "invalid"
538 }
539 fn description(&self) -> &str {
540 "invalid"
541 }
542 fn parameters(&self) -> Value {
543 serde_json::json!({"type":"not-a-type"})
544 }
545 fn output_schema(&self) -> Value {
546 serde_json::json!({"type":"object"})
547 }
548 async fn call(&self, _: Value) -> Result<Value, String> {
549 Ok(Value::Null)
550 }
551 }
552 assert!(ToolRegistry::new().register(Arc::new(Invalid)).is_err());
553 }
554
555 #[tokio::test]
556 async fn successful_output_is_validated_before_materialization() {
557 struct InvalidOutput;
558 #[async_trait]
559 impl Tool for InvalidOutput {
560 fn name(&self) -> &str {
561 "invalid-output"
562 }
563 fn description(&self) -> &str {
564 "invalid output"
565 }
566 fn parameters(&self) -> Value {
567 serde_json::json!({"type":"object"})
568 }
569 fn output_schema(&self) -> Value {
570 serde_json::json!({"type":"object"})
571 }
572 async fn call(&self, _: Value) -> Result<Value, String> {
573 Ok(Value::String("bad".into()))
574 }
575 }
576 let mut registry = ToolRegistry::new();
577 registry.register(Arc::new(InvalidOutput)).unwrap();
578 assert!(registry
579 .execute_with_context("invalid-output", &execution(), serde_json::json!({}))
580 .await
581 .unwrap_err()
582 .contains("invalid tool output"));
583 }
584}