1use std::collections::HashMap;
2use std::sync::Arc;
3
4use async_trait::async_trait;
5use serde_json::{Value, json};
6use tokio::sync::mpsc;
7
8use crate::engine::SessionStore;
9use crate::llm::StreamClient;
10use crate::types::{AgentError, AgentResult, SessionId, UserEvent};
11
12pub mod auto_continue;
13pub mod policy;
14pub mod update_plan;
15
16pub use auto_continue::AutoContinueTool;
17pub use update_plan::UpdatePlanTool;
18
19pub use policy::{DenyAllToolPolicy, ToolPolicy};
20
21#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
27#[serde(tag = "type", rename_all = "snake_case")]
28pub enum Content {
29 Text {
30 text: String,
31 },
32 Image {
34 data: String,
35 mime_type: String,
36 },
37}
38
39impl Content {
40 pub fn text(s: impl Into<String>) -> Self {
41 Content::Text { text: s.into() }
42 }
43
44 pub fn image(data: impl Into<String>, mime_type: impl Into<String>) -> Self {
45 Content::Image {
46 data: data.into(),
47 mime_type: mime_type.into(),
48 }
49 }
50}
51
52impl From<Content> for Vec<Content> {
53 fn from(c: Content) -> Self {
54 vec![c]
55 }
56}
57
58pub fn content_text(contents: &[Content]) -> String {
61 contents
62 .iter()
63 .filter_map(|c| match c {
64 Content::Text { text } => Some(text.as_str()),
65 Content::Image { .. } => None,
66 })
67 .collect::<Vec<_>>()
68 .join("\n")
69}
70
71#[derive(Clone)]
72pub struct ToolContext {
73 pub session_id: SessionId,
74 pub user_event_tx: mpsc::UnboundedSender<UserEvent>,
77 pub llm_client: Option<Arc<dyn StreamClient>>,
78 pub session_store: Option<Arc<dyn SessionStore>>,
79 pub language: crate::types::Language,
82 pub cancel_token: tokio_util::sync::CancellationToken,
84 pub max_output_chars: Option<usize>,
89 pub(crate) event_bus: crate::engine::EventBus,
92}
93
94impl ToolContext {
95 pub fn emit_user_event(&self, event: UserEvent) {
97 let _ = self.user_event_tx.send(event);
98 }
99
100 pub fn emit_progress(&self, text: impl Into<String>) {
102 self.emit_user_event(UserEvent::Progress { text: text.into() });
103 }
104
105 pub fn emit_partial_result(
108 &self,
109 tool_call_id: &str,
110 content: impl Into<String>,
111 is_partial: bool,
112 ) {
113 self.emit_user_event(UserEvent::ToolPartialResult {
114 tool_call_id: tool_call_id.to_string(),
115 content: content.into(),
116 is_partial,
117 });
118 }
119
120 pub fn for_test() -> Self {
124 let (tx, _rx) = mpsc::unbounded_channel();
125 ToolContext {
126 session_id: SessionId::new(0),
127 user_event_tx: tx,
128 llm_client: None,
129 session_store: None,
130 language: crate::types::Language::En,
131 cancel_token: tokio_util::sync::CancellationToken::new(),
132 max_output_chars: None,
133 event_bus: crate::engine::EventBus::new(1),
134 }
135 }
136}
137
138#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
142pub struct ToolMetadata {
143 pub name: String,
145 pub description: String,
147 pub origin: String,
151 pub version: String,
153 pub requirements: Vec<String>,
157}
158
159#[async_trait]
160pub trait Tool: Send + Sync {
161 fn name(&self) -> &'static str;
162 fn description(&self) -> &'static str;
164 fn schema(&self) -> Value;
167 async fn call(&self, args: &Value, ctx: &ToolContext) -> AgentResult<Vec<Content>>;
168
169 fn timeout_ms(&self) -> Option<u64> {
175 None }
177
178 fn metadata(&self) -> ToolMetadata {
185 ToolMetadata {
186 name: self.name().to_string(),
187 description: self.description().to_string(),
188 origin: "custom".to_string(),
189 version: "unknown".to_string(),
190 requirements: vec![],
191 }
192 }
193}
194
195#[async_trait]
196pub trait TypedTool: Send + Sync {
197 type Args: serde::de::DeserializeOwned + schemars::JsonSchema;
198 type Output: serde::Serialize;
199
200 fn name(&self) -> &'static str;
201 fn description(&self) -> &'static str;
202 async fn call_typed(&self, args: Self::Args, ctx: &ToolContext) -> AgentResult<Self::Output>;
203
204 fn format_output(&self, output: Self::Output) -> Content {
205 match serde_json::to_value(&output) {
210 Ok(serde_json::Value::String(s)) => Content::text(s),
211 Ok(other) => Content::text(other.to_string()),
212 Err(_) => Content::text(String::new()),
213 }
214 }
215
216 fn origin(&self) -> &'static str {
218 "custom"
219 }
220
221 fn version(&self) -> &'static str {
223 "unknown"
224 }
225}
226
227#[async_trait]
228impl<T: TypedTool + Send + Sync + 'static> Tool for T {
229 fn name(&self) -> &'static str {
230 TypedTool::name(self)
231 }
232
233 fn description(&self) -> &'static str {
234 TypedTool::description(self)
235 }
236
237 fn schema(&self) -> Value {
238 let settings = schemars::generate::SchemaSettings::draft07().with(|s| {
244 s.inline_subschemas = true;
245 s.meta_schema = None;
246 });
247 let generator = schemars::SchemaGenerator::new(settings);
248 let schema = generator.into_root_schema_for::<T::Args>();
249 serde_json::to_value(schema).unwrap_or(Value::Null)
250 }
251
252 fn metadata(&self) -> ToolMetadata {
253 ToolMetadata {
254 name: self.name().to_string(),
255 description: self.description().to_string(),
256 origin: self.origin().to_string(),
257 version: self.version().to_string(),
258 requirements: vec![],
259 }
260 }
261
262 async fn call(&self, args: &Value, ctx: &ToolContext) -> AgentResult<Vec<Content>> {
263 let typed_args: T::Args =
264 serde_json::from_value(args.clone()).map_err(|_| AgentError::ToolArgsInvalid {
265 name: self.name().to_string(),
266 raw: args.to_string(),
267 })?;
268 let output = self.call_typed(typed_args, ctx).await?;
269 Ok(vec![self.format_output(output)])
270 }
271}
272
273pub fn render_tool_definition(tool: &dyn Tool) -> Value {
278 json!({
279 "type": "function",
280 "function": {
281 "name": tool.name(),
282 "description": tool.description(),
283 "parameters": tool.schema(),
284 }
285 })
286}
287
288pub(crate) type ToolRef = Arc<dyn Tool>;
289
290#[derive(Clone, Default)]
291pub struct ToolRegistry {
292 tools: HashMap<String, ToolRef>,
293}
294
295impl ToolRegistry {
296 pub fn register(&mut self, tool: impl Tool + 'static) {
297 self.tools.insert(tool.name().to_string(), Arc::new(tool));
298 }
299
300 pub fn register_arc(&mut self, tool: Arc<dyn Tool>) {
301 self.tools.insert(tool.name().to_string(), tool);
302 }
303
304 pub fn remove(&mut self, name: &str) {
306 self.tools.remove(name);
307 }
308
309 pub fn get(&self, name: &str) -> Option<ToolRef> {
310 self.tools.get(name).cloned()
311 }
312
313 pub fn definitions(&self) -> Vec<Value> {
314 let mut tools: Vec<_> = self.tools.values().collect();
315 tools.sort_by_key(|t| t.name());
316 tools
317 .into_iter()
318 .map(|t| render_tool_definition(t.as_ref()))
319 .collect()
320 }
321
322 pub fn len(&self) -> usize {
323 self.tools.len()
324 }
325
326 pub fn is_empty(&self) -> bool {
327 self.tools.is_empty()
328 }
329
330 pub fn metadatas(&self) -> Vec<ToolMetadata> {
336 let mut list: Vec<_> = self.tools.values().map(|tool| tool.metadata()).collect();
337 list.sort_by(|a, b| a.name.cmp(&b.name));
338 list
339 }
340}
341
342#[cfg(test)]
343mod tests {
344 use super::*;
345
346 #[test]
347 fn content_text_ctor_and_into_vec() {
348 let c = Content::text("hello");
349 let v: Vec<Content> = c.clone().into();
350 assert_eq!(v.len(), 1);
351 assert!(matches!(v[0], Content::Text { .. }));
352 assert!(matches!(&c, Content::Text { text } if text == "hello"));
353 }
354
355 #[test]
356 fn content_serializes_with_type_tag() {
357 let c = Content::text("hi");
358 let j = serde_json::to_value(&c).unwrap();
359 assert_eq!(j["type"], "text");
360 assert_eq!(j["text"], "hi");
361 }
362
363 #[test]
364 fn tool_context_for_test_constructs() {
365 let ctx = ToolContext::for_test();
366 assert!(ctx.llm_client.is_none());
367 assert!(ctx.session_store.is_none());
368 assert!(!ctx.cancel_token.is_cancelled());
369 ctx.emit_progress("hello");
370 }
371
372 #[test]
373 fn typed_tool_schema_is_derived_from_args() {
374 #[derive(schemars::JsonSchema, serde::Deserialize, serde::Serialize)]
375 struct GreetArgs {
376 name: String,
377 #[serde(default)]
378 times: u32,
379 }
380
381 let schema = schemars::schema_for!(GreetArgs);
382 let j = serde_json::to_value(&schema).unwrap();
383 assert!(j["properties"]["name"].is_object());
385 assert!(j["properties"]["times"].is_object());
386 }
387
388 #[test]
389 fn typed_tool_schema_is_provider_safe_for_nested_enum() {
390 #[derive(schemars::JsonSchema, serde::Deserialize, serde::Serialize)]
391 enum Status {
392 Active,
393 Paused,
394 }
395
396 #[derive(schemars::JsonSchema, serde::Deserialize, serde::Serialize)]
397 struct Args {
398 name: String,
399 status: Status,
400 }
401
402 #[derive(Default)]
403 struct NestedTool;
404 #[async_trait]
405 impl TypedTool for NestedTool {
406 type Args = Args;
407 type Output = String;
408 fn name(&self) -> &'static str {
409 "nested"
410 }
411 fn description(&self) -> &'static str {
412 ""
413 }
414 async fn call_typed(
415 &self,
416 _args: Args,
417 _ctx: &ToolContext,
418 ) -> crate::types::AgentResult<String> {
419 Ok(String::new())
420 }
421 }
422
423 let schema = Tool::schema(&NestedTool);
424 let raw = schema.to_string();
425 assert!(!raw.contains("$ref"), "schema contains $ref: {raw}");
428 assert!(!raw.contains("$defs"), "schema contains $defs: {raw}");
429 assert!(
430 !raw.contains("definitions"),
431 "schema has definitions: {raw}"
432 );
433 assert!(schema.get("$schema").is_none(), "schema has $schema key");
434
435 let variants: Vec<&str> = schema["properties"]["status"]["enum"]
437 .as_array()
438 .unwrap()
439 .iter()
440 .map(|v| v.as_str().unwrap())
441 .collect();
442 assert!(variants.contains(&"Active"), "missing Active: {variants:?}");
443 assert!(variants.contains(&"Paused"), "missing Paused: {variants:?}");
444 }
445
446 #[test]
447 fn definitions_are_sorted_by_name() {
448 struct NamedTool(&'static str);
449 #[async_trait::async_trait]
450 impl Tool for NamedTool {
451 fn name(&self) -> &'static str {
452 self.0
453 }
454 fn description(&self) -> &'static str {
455 ""
456 }
457 fn schema(&self) -> serde_json::Value {
458 serde_json::Value::Null
459 }
460 async fn call(
461 &self,
462 _args: &serde_json::Value,
463 _ctx: &ToolContext,
464 ) -> crate::types::AgentResult<Vec<Content>> {
465 Ok(vec![])
466 }
467 }
468
469 let mut registry = ToolRegistry::default();
470 registry.register(NamedTool("zeta"));
471 registry.register(NamedTool("alpha"));
472 registry.register(NamedTool("mike"));
473
474 let defs = registry.definitions();
475 let names: Vec<&str> = defs
476 .iter()
477 .map(|d| d["function"]["name"].as_str().unwrap())
478 .collect();
479 assert_eq!(names, vec!["alpha", "mike", "zeta"]);
480 }
481
482 #[test]
485 fn content_image_and_content_text_skips_images() {
486 let img = Content::image("base64data", "image/png");
487 assert!(
488 matches!(&img, Content::Image { data, mime_type } if data == "base64data" && mime_type == "image/png")
489 );
490
491 let text = content_text(&[
492 Content::text("a"),
493 Content::image("b", "image/png"),
494 Content::text("c"),
495 ]);
496 assert_eq!(text, "a\nc");
497 }
498
499 #[test]
500 fn emit_partial_result_sends_event() {
501 let (tx, mut rx) = mpsc::unbounded_channel();
502 let ctx = ToolContext {
503 session_id: SessionId::new(0),
504 user_event_tx: tx,
505 llm_client: None,
506 session_store: None,
507 language: crate::types::Language::En,
508 cancel_token: tokio_util::sync::CancellationToken::new(),
509 max_output_chars: None,
510 event_bus: crate::engine::EventBus::new(1),
511 };
512 ctx.emit_partial_result("tc1", "partial", true);
513 match rx.try_recv().unwrap() {
514 UserEvent::ToolPartialResult {
515 tool_call_id,
516 content,
517 is_partial,
518 } => {
519 assert_eq!(tool_call_id, "tc1");
520 assert_eq!(content, "partial");
521 assert!(is_partial);
522 }
523 other => panic!("unexpected event: {other:?}"),
524 }
525 }
526
527 #[derive(schemars::JsonSchema, serde::Deserialize, serde::Serialize)]
529 struct GreetArgs {
530 name: String,
531 }
532
533 struct GreetTool;
534 #[async_trait]
535 impl TypedTool for GreetTool {
536 type Args = GreetArgs;
537 type Output = String;
538 fn name(&self) -> &'static str {
539 "greet"
540 }
541 fn description(&self) -> &'static str {
542 "greets a name"
543 }
544 fn origin(&self) -> &'static str {
545 "test-crate"
546 }
547 fn version(&self) -> &'static str {
548 "1.0.0"
549 }
550 async fn call_typed(&self, args: GreetArgs, _ctx: &ToolContext) -> AgentResult<String> {
551 Ok(format!("Hello, {}!", args.name))
552 }
553 }
554
555 #[test]
556 fn typed_tool_blanket_delegates_name_description() {
557 let t = GreetTool;
558 assert_eq!(Tool::name(&t), "greet");
559 assert_eq!(Tool::description(&t), "greets a name");
560 }
561
562 #[test]
563 fn typed_tool_metadata_uses_origin_and_version() {
564 let m = Tool::metadata(&GreetTool);
565 assert_eq!(m.name, "greet");
566 assert_eq!(m.description, "greets a name");
567 assert_eq!(m.origin, "test-crate");
568 assert_eq!(m.version, "1.0.0");
569 assert!(m.requirements.is_empty());
570 }
571
572 #[tokio::test]
573 async fn typed_tool_call_deserializes_and_formats() {
574 let ctx = ToolContext::for_test();
575 let out = Tool::call(&GreetTool, &json!({"name": "world"}), &ctx)
576 .await
577 .unwrap();
578 assert_eq!(content_text(&out), "Hello, world!");
580 }
581
582 #[derive(serde::Serialize)]
584 struct GreetResult {
585 message: String,
586 }
587
588 struct GreetStructTool;
589 #[async_trait]
590 impl TypedTool for GreetStructTool {
591 type Args = GreetArgs;
592 type Output = GreetResult;
593 fn name(&self) -> &'static str {
594 "greet_struct"
595 }
596 fn description(&self) -> &'static str {
597 "greets as json"
598 }
599 async fn call_typed(
600 &self,
601 args: GreetArgs,
602 _ctx: &ToolContext,
603 ) -> AgentResult<GreetResult> {
604 Ok(GreetResult {
605 message: format!("Hello, {}!", args.name),
606 })
607 }
608 }
609
610 #[test]
611 fn format_output_json_serializes_struct() {
612 let out = GreetStructTool.format_output(GreetResult {
613 message: "hi".into(),
614 });
615 assert_eq!(content_text(&[out]), r#"{"message":"hi"}"#);
616 }
617
618 #[tokio::test]
619 async fn typed_tool_call_invalid_args_is_tool_args_invalid() {
620 let ctx = ToolContext::for_test();
621 let err = Tool::call(&GreetTool, &json!({"nope": 1}), &ctx)
622 .await
623 .unwrap_err();
624 assert!(matches!(err, AgentError::ToolArgsInvalid { .. }));
625 }
626
627 struct NamedTool(&'static str);
628 #[async_trait]
629 impl Tool for NamedTool {
630 fn name(&self) -> &'static str {
631 self.0
632 }
633 fn description(&self) -> &'static str {
634 ""
635 }
636 fn schema(&self) -> serde_json::Value {
637 serde_json::Value::Null
638 }
639 async fn call(
640 &self,
641 _args: &serde_json::Value,
642 _ctx: &ToolContext,
643 ) -> AgentResult<Vec<Content>> {
644 Ok(vec![])
645 }
646 }
647
648 #[test]
649 fn registry_register_arc_get_remove_len_is_empty() {
650 let mut r = ToolRegistry::default();
651 assert!(r.is_empty());
652 assert_eq!(r.len(), 0);
653
654 let t: Arc<dyn Tool> = Arc::new(NamedTool("x"));
655 r.register_arc(t);
656 assert!(!r.is_empty());
657 assert_eq!(r.len(), 1);
658 assert!(r.get("x").is_some());
659 assert!(r.get("missing").is_none());
660
661 r.remove("x");
662 assert!(r.is_empty());
663 }
664
665 #[test]
666 fn metadatas_are_sorted_by_name() {
667 let mut r = ToolRegistry::default();
668 r.register(NamedTool("zeta"));
669 r.register(NamedTool("alpha"));
670
671 let metas = r.metadatas();
672 let names: Vec<&str> = metas.iter().map(|m| m.name.as_str()).collect();
673 assert_eq!(names, vec!["alpha", "zeta"]);
674 assert_eq!(metas[0].origin, "custom");
675 assert_eq!(metas[0].version, "unknown");
676 }
677}