1use std::{collections::HashMap, future::Future, pin::Pin, sync::Arc};
14
15use schemars::{JsonSchema, schema_for};
16use serde::{Serialize, de::DeserializeOwned};
17
18use crate::{
19 error::{Error, ProviderKind, Result, ToolArgumentIssue},
20 generation::normalize_strict_json_schema,
21 message::{Message, ToolCall, ToolDefinition},
22};
23
24type ToolFuture = Pin<Box<dyn Future<Output = Result<serde_json::Value>> + Send>>;
25type ToolHandler = dyn Fn(serde_json::Value, ToolContext) -> ToolFuture + Send + Sync;
26
27fn format_instance_path(path: impl ToString) -> String {
28 let path = path.to_string();
29 if path.is_empty() {
30 "$".to_string()
31 } else {
32 path
33 }
34}
35
36fn collect_validation_issues(
37 validator: &jsonschema::Validator,
38 value: &serde_json::Value,
39) -> Vec<ToolArgumentIssue> {
40 let mut issues: Vec<_> = validator
41 .iter_errors(value)
42 .map(|error| ToolArgumentIssue {
43 path: format_instance_path(error.instance_path()),
44 schema_path: error.schema_path().to_string(),
45 message: error.to_string(),
46 })
47 .collect();
48
49 issues.sort_by(|a, b| {
50 a.path
51 .cmp(&b.path)
52 .then_with(|| a.schema_path.cmp(&b.schema_path))
53 .then_with(|| a.message.cmp(&b.message))
54 });
55 issues.dedup();
56 issues
57}
58
59fn summarize_validation_issues(issues: &[ToolArgumentIssue]) -> Option<String> {
60 if issues.is_empty() {
61 return None;
62 }
63
64 let messages: Vec<_> = issues.iter().map(|i| i.message.clone()).collect();
65 Some(match messages.as_slice() {
66 [single] => single.clone(),
67 many => format!("{} validation errors: {}", many.len(), many.join("; ")),
68 })
69}
70
71fn tool_error_content(error: Error) -> Result<String> {
72 match error {
73 Error::ToolArguments {
74 name,
75 message,
76 issues,
77 } => serde_json::to_string(&serde_json::json!({
78 "error": {
79 "type": "tool_argument_validation",
80 "tool": name,
81 "retryable": true,
82 "message": "Tool arguments failed validation. Call the tool again with corrected arguments.",
83 "summary": message,
84 "issues": issues,
85 }
86 }))
87 .map_err(Into::into),
88 error => serde_json::to_string(&serde_json::json!({ "error": error.to_string() }))
89 .map_err(Into::into),
90 }
91}
92
93#[derive(Debug, Clone)]
115pub struct ToolContext {
116 pub provider: ProviderKind,
118 pub model: String,
120 pub round: usize,
125 pub tool_name: String,
127 pub tool_call_id: String,
129}
130
131#[derive(Clone)]
156pub struct Tool {
157 name: String,
158 description: Option<String>,
159 input_schema: Option<serde_json::Value>,
160 handler: Option<Arc<ToolHandler>>,
161}
162
163impl Tool {
164 pub fn new(name: impl Into<String>) -> Self {
166 Self {
167 name: name.into(),
168 description: None,
169 input_schema: None,
170 handler: None,
171 }
172 }
173
174 pub fn description(mut self, description: impl Into<String>) -> Self {
176 self.description = Some(description.into());
177 self
178 }
179
180 pub fn json_schema(mut self, input_schema: serde_json::Value) -> Self {
182 self.input_schema = Some(input_schema);
183 self
184 }
185
186 pub fn handler<Args, Output, F, Fut>(mut self, handler: F) -> Result<Self>
191 where
192 Args: DeserializeOwned + JsonSchema + Send + 'static,
193 Output: Serialize + Send + 'static,
194 F: Fn(Args, ToolContext) -> Fut + Send + Sync + 'static,
195 Fut: Future<Output = Result<Output>> + Send + 'static,
196 {
197 let tool_name = self.name.clone();
198 let schema = schema_for!(Args);
199 let mut generated_schema = serde_json::to_value(schema)?;
200 normalize_strict_json_schema(&mut generated_schema);
201
202 let mut input_schema = self.input_schema.take().unwrap_or(generated_schema);
203 normalize_strict_json_schema(&mut input_schema);
204
205 let validator = Arc::new(jsonschema::validator_for(&input_schema).map_err(|error| {
206 Error::InvalidRequest(format!(
207 "Tool '{}' has an invalid input schema: {error}",
208 self.name
209 ))
210 })?);
211
212 self.input_schema = Some(input_schema);
213
214 self.handler = Some(Arc::new(move |value, ctx| {
215 let tool_name = tool_name.clone();
216 let validator = validator.clone();
217
218 let issues = collect_validation_issues(&validator, &value);
219
220 if let Some(message) = summarize_validation_issues(&issues) {
221 return Box::pin(async move {
222 Err(Error::ToolArguments {
223 name: tool_name,
224 message,
225 issues,
226 })
227 });
228 }
229
230 let future = match serde_json::from_value::<Args>(value) {
231 Ok(args) => handler(args, ctx),
232 Err(error) => {
233 return Box::pin(async move {
234 Err(Error::ToolArguments {
235 name: tool_name,
236 message: error.to_string(),
237 issues: Vec::new(),
238 })
239 });
240 }
241 };
242
243 Box::pin(async move {
244 let output = future.await?;
245 serde_json::to_value(output).map_err(Into::into)
246 })
247 }));
248
249 Ok(self)
250 }
251
252 fn into_registered(self) -> Result<RegisteredTool> {
253 if self.name.trim().is_empty() {
254 return Err(Error::InvalidRequest(
255 "Tool name cannot be empty".to_string(),
256 ));
257 }
258
259 let input_schema = self.input_schema.ok_or_else(|| {
260 Error::InvalidRequest(format!("Tool '{}' is missing an input schema", self.name))
261 })?;
262
263 let handler = self.handler.ok_or_else(|| {
264 Error::InvalidRequest(format!("Tool '{}' is missing a handler", self.name))
265 })?;
266
267 Ok(RegisteredTool {
268 definition: ToolDefinition {
269 name: self.name,
270 description: self.description,
271 input_schema,
272 },
273 handler,
274 })
275 }
276}
277
278#[derive(Clone)]
279struct RegisteredTool {
280 definition: ToolDefinition,
281 handler: Arc<ToolHandler>,
282}
283
284#[derive(Clone, Default)]
286pub(crate) struct ToolRegistry {
287 tools: HashMap<String, RegisteredTool>,
288}
289
290impl ToolRegistry {
291 pub(crate) fn new() -> Self {
292 Self {
293 tools: HashMap::new(),
294 }
295 }
296
297 pub(crate) fn register(&mut self, tool: Tool) -> Result<()> {
298 let tool = tool.into_registered()?;
299 let name = tool.definition.name.clone();
300
301 if self.tools.contains_key(&name) {
302 return Err(Error::InvalidRequest(format!(
303 "Tool '{}' is already registered",
304 name
305 )));
306 }
307
308 self.tools.insert(name, tool);
309 Ok(())
310 }
311
312 pub(crate) fn extend<T>(&mut self, tools: T) -> Result<()>
313 where
314 T: IntoIterator<Item = Tool>,
315 {
316 for tool in tools {
317 self.register(tool)?;
318 }
319 Ok(())
320 }
321
322 pub(crate) fn is_empty(&self) -> bool {
323 self.tools.is_empty()
324 }
325
326 pub(crate) fn definitions(&self) -> Vec<ToolDefinition> {
327 self.tools
328 .values()
329 .map(|tool| tool.definition.clone())
330 .collect()
331 }
332
333 pub(crate) async fn execute(
339 &self,
340 tool_call: &ToolCall,
341 context: ToolContext,
342 ) -> Result<Message> {
343 let registered = self
344 .tools
345 .get(&tool_call.name)
346 .ok_or_else(|| Error::ToolNotFound {
347 name: tool_call.name.clone(),
348 })?;
349
350 match (registered.handler)(tool_call.arguments.clone(), context).await {
351 Ok(result) => Ok(Message::tool(
352 serde_json::to_string(&result)?,
353 tool_call.id.clone(),
354 )),
355 Err(error) => Ok(Message::tool_error(
356 tool_error_content(error)?,
357 tool_call.id.clone(),
358 )),
359 }
360 }
361}
362
363#[cfg(test)]
364mod tests {
365 use super::*;
366 use schemars::JsonSchema;
367 use serde::Deserialize;
368 use std::sync::Arc;
369
370 #[derive(Debug, Deserialize, JsonSchema)]
371 struct AddArgs {
372 a: i32,
373 b: i32,
374 }
375
376 #[derive(Debug, Deserialize, JsonSchema)]
377 #[schemars(deny_unknown_fields)]
378 struct SearchArgs {
379 #[schemars(
380 description = "Customer name or email substring to search for",
381 length(min = 1)
382 )]
383 query: String,
384 #[schemars(
385 description = "Maximum number of results to return",
386 range(min = 1, max = 10)
387 )]
388 limit: Option<usize>,
389 }
390
391 #[derive(Debug, Deserialize, JsonSchema)]
392 struct OptionalOnlyArgs {
393 #[allow(dead_code)]
395 #[serde(default)]
396 query: Option<String>,
397 }
398
399 #[derive(Debug, serde::Serialize)]
400 struct AddResult {
401 sum: i32,
402 }
403
404 fn test_tool_context(tool_name: &str) -> ToolContext {
405 ToolContext {
406 provider: ProviderKind::OpenAI,
407 model: "gpt-4o-mini".to_string(),
408 round: 0,
409 tool_name: tool_name.to_string(),
410 tool_call_id: "call_123".to_string(),
411 }
412 }
413
414 fn tool_error_payload(message: &Message) -> serde_json::Value {
415 serde_json::from_str(&message.content).expect("tool error content should be valid json")
416 }
417
418 #[tokio::test]
419 async fn tool_registry_executes_registered_handler() {
420 let calls = Arc::new(tokio::sync::Mutex::new(Vec::new()));
421 let mut registry = ToolRegistry::new();
422 registry
423 .register(
424 Tool::new("add")
425 .description("Add two numbers")
426 .handler({
427 let calls = calls.clone();
428 move |args: AddArgs, _ctx| {
429 let calls = calls.clone();
430 async move {
431 calls.lock().await.push((args.a, args.b));
432 Ok(AddResult {
433 sum: args.a + args.b,
434 })
435 }
436 }
437 })
438 .expect("tool should build"),
439 )
440 .expect("tool should register");
441
442 let message = registry
443 .execute(
444 &ToolCall {
445 id: "call_123".to_string(),
446 name: "add".to_string(),
447 arguments: serde_json::json!({ "a": 2, "b": 3 }),
448 },
449 test_tool_context("add"),
450 )
451 .await
452 .expect("tool execution should succeed");
453
454 assert_eq!(message.content, "{\"sum\":5}");
455 assert_eq!(message.tool_call_id.as_deref(), Some("call_123"));
456 assert!(!message.tool_error);
457 assert_eq!(calls.lock().await.as_slice(), &[(2, 3)]);
458 }
459
460 #[tokio::test]
461 async fn tool_registry_aggregates_schema_validation_errors_before_handler() {
462 let calls = Arc::new(tokio::sync::Mutex::new(Vec::new()));
463 let mut registry = ToolRegistry::new();
464 registry
465 .register(
466 Tool::new("search_customers")
467 .description("Search customers by email or name")
468 .handler({
469 let calls = calls.clone();
470 move |args: SearchArgs, _ctx| {
471 let calls = calls.clone();
472 async move {
473 calls.lock().await.push(args.query);
474 Ok(serde_json::json!({ "limit": args.limit }))
475 }
476 }
477 })
478 .expect("tool should build"),
479 )
480 .expect("tool should register");
481
482 let message = registry
483 .execute(
484 &ToolCall {
485 id: "call_123".to_string(),
486 name: "search_customers".to_string(),
487 arguments: serde_json::json!({ "query": "", "limit": 25 }),
488 },
489 test_tool_context("search_customers"),
490 )
491 .await
492 .expect("tool execution should return a tool message");
493
494 let payload = tool_error_payload(&message);
495
496 assert!(message.tool_error);
497 assert_eq!(payload["error"]["type"], "tool_argument_validation");
498 assert_eq!(payload["error"]["tool"], "search_customers");
499 assert_eq!(payload["error"]["retryable"], true);
500 assert_eq!(payload["error"]["issues"].as_array().map(Vec::len), Some(2));
501 assert!(
502 payload["error"]["summary"]
503 .as_str()
504 .unwrap_or_default()
505 .contains("validation errors")
506 );
507 assert!(calls.lock().await.is_empty());
508 }
509
510 #[tokio::test]
511 async fn tool_registry_rejects_unknown_fields_in_arguments() {
512 let calls = Arc::new(tokio::sync::Mutex::new(Vec::new()));
513 let mut registry = ToolRegistry::new();
514 registry
515 .register(
516 Tool::new("add")
517 .description("Add two numbers")
518 .handler({
519 let calls = calls.clone();
520 move |args: AddArgs, _ctx| {
521 let calls = calls.clone();
522 async move {
523 calls.lock().await.push((args.a, args.b));
524 Ok(AddResult {
525 sum: args.a + args.b,
526 })
527 }
528 }
529 })
530 .expect("tool should build"),
531 )
532 .expect("tool should register");
533
534 let message = registry
535 .execute(
536 &ToolCall {
537 id: "call_123".to_string(),
538 name: "add".to_string(),
539 arguments: serde_json::json!({ "a": 2, "b": 3, "c": 4 }),
540 },
541 test_tool_context("add"),
542 )
543 .await
544 .expect("tool execution should return a tool message");
545
546 let payload = tool_error_payload(&message);
547
548 assert!(message.tool_error);
549 assert_eq!(payload["error"]["type"], "tool_argument_validation");
550 assert_eq!(payload["error"]["tool"], "add");
551 assert_eq!(payload["error"]["issues"].as_array().map(Vec::len), Some(1));
552 assert!(calls.lock().await.is_empty());
553 }
554
555 #[test]
556 fn tool_builder_rejects_invalid_custom_input_schema() {
557 let result = Tool::new("add")
558 .json_schema(serde_json::json!({ "type": "not-a-real-json-schema-type" }))
559 .handler(|_args: AddArgs, _ctx| async move { Ok(AddResult { sum: 0 }) });
560
561 let error = match result {
562 Ok(_) => panic!("invalid custom schemas should fail during registration"),
563 Err(error) => error,
564 };
565
566 assert!(matches!(error, Error::InvalidRequest(_)));
567 assert!(error.to_string().contains("invalid input schema"));
568 }
569
570 #[test]
571 fn tool_builder_adds_root_object_type_for_optional_only_args() {
572 let mut registry = ToolRegistry::new();
573 registry
574 .register(
575 Tool::new("lookup_workspace")
576 .description("Look up workspace information")
577 .handler(|_args: OptionalOnlyArgs, _ctx| async move {
578 Ok(serde_json::json!({ "ok": true }))
579 })
580 .expect("tool should build"),
581 )
582 .expect("tool should register");
583
584 let definitions = registry.definitions();
585 let schema = &definitions[0].input_schema;
586
587 assert_eq!(
588 schema["type"],
589 serde_json::Value::String("object".to_string())
590 );
591 assert_eq!(
592 schema["additionalProperties"],
593 serde_json::Value::Bool(false)
594 );
595 assert!(schema.get("properties").is_some());
596 }
597}