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(crate) event_bus: crate::engine::EventBus,
87}
88
89impl ToolContext {
90 pub fn emit_user_event(&self, event: UserEvent) {
92 let _ = self.user_event_tx.send(event);
93 }
94
95 pub fn emit_progress(&self, text: impl Into<String>) {
97 self.emit_user_event(UserEvent::Progress { text: text.into() });
98 }
99
100 pub fn emit_partial_result(
103 &self,
104 tool_call_id: &str,
105 content: impl Into<String>,
106 is_partial: bool,
107 ) {
108 self.emit_user_event(UserEvent::ToolPartialResult {
109 tool_call_id: tool_call_id.to_string(),
110 content: content.into(),
111 is_partial,
112 });
113 }
114
115 pub fn for_test() -> Self {
119 let (tx, _rx) = mpsc::unbounded_channel();
120 ToolContext {
121 session_id: SessionId::new(0),
122 user_event_tx: tx,
123 llm_client: None,
124 session_store: None,
125 language: crate::types::Language::En,
126 cancel_token: tokio_util::sync::CancellationToken::new(),
127 event_bus: crate::engine::EventBus::new(1),
128 }
129 }
130}
131
132#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
136pub struct ToolMetadata {
137 pub name: String,
139 pub description: String,
141 pub origin: String,
145 pub version: String,
147 pub requirements: Vec<String>,
151}
152
153#[async_trait]
154pub trait Tool: Send + Sync {
155 fn name(&self) -> &'static str;
156 fn description(&self) -> &'static str;
158 fn schema(&self) -> Value;
161 async fn call(&self, args: &Value, ctx: &ToolContext) -> AgentResult<Vec<Content>>;
162
163 fn metadata(&self) -> ToolMetadata {
170 ToolMetadata {
171 name: self.name().to_string(),
172 description: self.description().to_string(),
173 origin: "custom".to_string(),
174 version: "unknown".to_string(),
175 requirements: vec![],
176 }
177 }
178}
179
180#[async_trait]
181pub trait TypedTool: Send + Sync {
182 type Args: serde::de::DeserializeOwned + schemars::JsonSchema;
183 type Output: serde::Serialize;
184
185 fn name(&self) -> &'static str;
186 fn description(&self) -> &'static str;
187 async fn call_typed(&self, args: Self::Args, ctx: &ToolContext) -> AgentResult<Self::Output>;
188
189 fn format_output(&self, output: Self::Output) -> Content {
190 match serde_json::to_value(&output) {
195 Ok(serde_json::Value::String(s)) => Content::text(s),
196 Ok(other) => Content::text(other.to_string()),
197 Err(_) => Content::text(String::new()),
198 }
199 }
200
201 fn origin(&self) -> &'static str {
203 "custom"
204 }
205
206 fn version(&self) -> &'static str {
208 "unknown"
209 }
210}
211
212#[async_trait]
213impl<T: TypedTool + Send + Sync + 'static> Tool for T {
214 fn name(&self) -> &'static str {
215 TypedTool::name(self)
216 }
217
218 fn description(&self) -> &'static str {
219 TypedTool::description(self)
220 }
221
222 fn schema(&self) -> Value {
223 let settings = schemars::generate::SchemaSettings::draft07().with(|s| {
229 s.inline_subschemas = true;
230 s.meta_schema = None;
231 });
232 let generator = schemars::SchemaGenerator::new(settings);
233 let schema = generator.into_root_schema_for::<T::Args>();
234 serde_json::to_value(schema).unwrap_or(Value::Null)
235 }
236
237 fn metadata(&self) -> ToolMetadata {
238 ToolMetadata {
239 name: self.name().to_string(),
240 description: self.description().to_string(),
241 origin: self.origin().to_string(),
242 version: self.version().to_string(),
243 requirements: vec![],
244 }
245 }
246
247 async fn call(&self, args: &Value, ctx: &ToolContext) -> AgentResult<Vec<Content>> {
248 let typed_args: T::Args =
249 serde_json::from_value(args.clone()).map_err(|_| AgentError::ToolArgsInvalid {
250 name: self.name().to_string(),
251 raw: args.to_string(),
252 })?;
253 let output = self.call_typed(typed_args, ctx).await?;
254 Ok(vec![self.format_output(output)])
255 }
256}
257
258pub fn render_tool_definition(tool: &dyn Tool) -> Value {
263 json!({
264 "type": "function",
265 "function": {
266 "name": tool.name(),
267 "description": tool.description(),
268 "parameters": tool.schema(),
269 }
270 })
271}
272
273pub(crate) type ToolRef = Arc<dyn Tool>;
274
275#[derive(Clone, Default)]
276pub struct ToolRegistry {
277 tools: HashMap<String, ToolRef>,
278}
279
280impl ToolRegistry {
281 pub fn register(&mut self, tool: impl Tool + 'static) {
282 self.tools.insert(tool.name().to_string(), Arc::new(tool));
283 }
284
285 pub fn register_arc(&mut self, tool: Arc<dyn Tool>) {
286 self.tools.insert(tool.name().to_string(), tool);
287 }
288
289 pub fn remove(&mut self, name: &str) {
291 self.tools.remove(name);
292 }
293
294 pub fn get(&self, name: &str) -> Option<ToolRef> {
295 self.tools.get(name).cloned()
296 }
297
298 pub fn definitions(&self) -> Vec<Value> {
299 let mut tools: Vec<_> = self.tools.values().collect();
300 tools.sort_by_key(|t| t.name());
301 tools
302 .into_iter()
303 .map(|t| render_tool_definition(t.as_ref()))
304 .collect()
305 }
306
307 pub fn len(&self) -> usize {
308 self.tools.len()
309 }
310
311 pub fn is_empty(&self) -> bool {
312 self.tools.is_empty()
313 }
314
315 pub fn metadatas(&self) -> Vec<ToolMetadata> {
321 let mut list: Vec<_> = self.tools.values().map(|tool| tool.metadata()).collect();
322 list.sort_by(|a, b| a.name.cmp(&b.name));
323 list
324 }
325}
326
327#[cfg(test)]
328mod tests {
329 use super::*;
330
331 #[test]
332 fn content_text_ctor_and_into_vec() {
333 let c = Content::text("hello");
334 let v: Vec<Content> = c.clone().into();
335 assert_eq!(v.len(), 1);
336 assert!(matches!(v[0], Content::Text { .. }));
337 assert!(matches!(&c, Content::Text { text } if text == "hello"));
338 }
339
340 #[test]
341 fn content_serializes_with_type_tag() {
342 let c = Content::text("hi");
343 let j = serde_json::to_value(&c).unwrap();
344 assert_eq!(j["type"], "text");
345 assert_eq!(j["text"], "hi");
346 }
347
348 #[test]
349 fn tool_context_for_test_constructs() {
350 let ctx = ToolContext::for_test();
351 assert!(ctx.llm_client.is_none());
352 assert!(ctx.session_store.is_none());
353 assert!(!ctx.cancel_token.is_cancelled());
354 ctx.emit_progress("hello");
355 }
356
357 #[test]
358 fn typed_tool_schema_is_derived_from_args() {
359 #[derive(schemars::JsonSchema, serde::Deserialize, serde::Serialize)]
360 struct GreetArgs {
361 name: String,
362 #[serde(default)]
363 times: u32,
364 }
365
366 let schema = schemars::schema_for!(GreetArgs);
367 let j = serde_json::to_value(&schema).unwrap();
368 assert!(j["properties"]["name"].is_object());
370 assert!(j["properties"]["times"].is_object());
371 }
372
373 #[test]
374 fn typed_tool_schema_is_provider_safe_for_nested_enum() {
375 #[derive(schemars::JsonSchema, serde::Deserialize, serde::Serialize)]
376 enum Status {
377 Active,
378 Paused,
379 }
380
381 #[derive(schemars::JsonSchema, serde::Deserialize, serde::Serialize)]
382 struct Args {
383 name: String,
384 status: Status,
385 }
386
387 #[derive(Default)]
388 struct NestedTool;
389 #[async_trait]
390 impl TypedTool for NestedTool {
391 type Args = Args;
392 type Output = String;
393 fn name(&self) -> &'static str {
394 "nested"
395 }
396 fn description(&self) -> &'static str {
397 ""
398 }
399 async fn call_typed(
400 &self,
401 _args: Args,
402 _ctx: &ToolContext,
403 ) -> crate::types::AgentResult<String> {
404 Ok(String::new())
405 }
406 }
407
408 let schema = Tool::schema(&NestedTool);
409 let raw = schema.to_string();
410 assert!(!raw.contains("$ref"), "schema contains $ref: {raw}");
413 assert!(!raw.contains("$defs"), "schema contains $defs: {raw}");
414 assert!(
415 !raw.contains("definitions"),
416 "schema has definitions: {raw}"
417 );
418 assert!(schema.get("$schema").is_none(), "schema has $schema key");
419
420 let variants: Vec<&str> = schema["properties"]["status"]["enum"]
422 .as_array()
423 .unwrap()
424 .iter()
425 .map(|v| v.as_str().unwrap())
426 .collect();
427 assert!(variants.contains(&"Active"), "missing Active: {variants:?}");
428 assert!(variants.contains(&"Paused"), "missing Paused: {variants:?}");
429 }
430
431 #[test]
432 fn definitions_are_sorted_by_name() {
433 struct NamedTool(&'static str);
434 #[async_trait::async_trait]
435 impl Tool for NamedTool {
436 fn name(&self) -> &'static str {
437 self.0
438 }
439 fn description(&self) -> &'static str {
440 ""
441 }
442 fn schema(&self) -> serde_json::Value {
443 serde_json::Value::Null
444 }
445 async fn call(
446 &self,
447 _args: &serde_json::Value,
448 _ctx: &ToolContext,
449 ) -> crate::types::AgentResult<Vec<Content>> {
450 Ok(vec![])
451 }
452 }
453
454 let mut registry = ToolRegistry::default();
455 registry.register(NamedTool("zeta"));
456 registry.register(NamedTool("alpha"));
457 registry.register(NamedTool("mike"));
458
459 let defs = registry.definitions();
460 let names: Vec<&str> = defs
461 .iter()
462 .map(|d| d["function"]["name"].as_str().unwrap())
463 .collect();
464 assert_eq!(names, vec!["alpha", "mike", "zeta"]);
465 }
466
467 #[test]
470 fn content_image_and_content_text_skips_images() {
471 let img = Content::image("base64data", "image/png");
472 assert!(
473 matches!(&img, Content::Image { data, mime_type } if data == "base64data" && mime_type == "image/png")
474 );
475
476 let text = content_text(&[
477 Content::text("a"),
478 Content::image("b", "image/png"),
479 Content::text("c"),
480 ]);
481 assert_eq!(text, "a\nc");
482 }
483
484 #[test]
485 fn emit_partial_result_sends_event() {
486 let (tx, mut rx) = mpsc::unbounded_channel();
487 let ctx = ToolContext {
488 session_id: SessionId::new(0),
489 user_event_tx: tx,
490 llm_client: None,
491 session_store: None,
492 language: crate::types::Language::En,
493 cancel_token: tokio_util::sync::CancellationToken::new(),
494 event_bus: crate::engine::EventBus::new(1),
495 };
496 ctx.emit_partial_result("tc1", "partial", true);
497 match rx.try_recv().unwrap() {
498 UserEvent::ToolPartialResult {
499 tool_call_id,
500 content,
501 is_partial,
502 } => {
503 assert_eq!(tool_call_id, "tc1");
504 assert_eq!(content, "partial");
505 assert!(is_partial);
506 }
507 other => panic!("unexpected event: {other:?}"),
508 }
509 }
510
511 #[derive(schemars::JsonSchema, serde::Deserialize, serde::Serialize)]
513 struct GreetArgs {
514 name: String,
515 }
516
517 struct GreetTool;
518 #[async_trait]
519 impl TypedTool for GreetTool {
520 type Args = GreetArgs;
521 type Output = String;
522 fn name(&self) -> &'static str {
523 "greet"
524 }
525 fn description(&self) -> &'static str {
526 "greets a name"
527 }
528 fn origin(&self) -> &'static str {
529 "test-crate"
530 }
531 fn version(&self) -> &'static str {
532 "1.0.0"
533 }
534 async fn call_typed(&self, args: GreetArgs, _ctx: &ToolContext) -> AgentResult<String> {
535 Ok(format!("Hello, {}!", args.name))
536 }
537 }
538
539 #[test]
540 fn typed_tool_blanket_delegates_name_description() {
541 let t = GreetTool;
542 assert_eq!(Tool::name(&t), "greet");
543 assert_eq!(Tool::description(&t), "greets a name");
544 }
545
546 #[test]
547 fn typed_tool_metadata_uses_origin_and_version() {
548 let m = Tool::metadata(&GreetTool);
549 assert_eq!(m.name, "greet");
550 assert_eq!(m.description, "greets a name");
551 assert_eq!(m.origin, "test-crate");
552 assert_eq!(m.version, "1.0.0");
553 assert!(m.requirements.is_empty());
554 }
555
556 #[tokio::test]
557 async fn typed_tool_call_deserializes_and_formats() {
558 let ctx = ToolContext::for_test();
559 let out = Tool::call(&GreetTool, &json!({"name": "world"}), &ctx)
560 .await
561 .unwrap();
562 assert_eq!(content_text(&out), "Hello, world!");
564 }
565
566 #[derive(serde::Serialize)]
568 struct GreetResult {
569 message: String,
570 }
571
572 struct GreetStructTool;
573 #[async_trait]
574 impl TypedTool for GreetStructTool {
575 type Args = GreetArgs;
576 type Output = GreetResult;
577 fn name(&self) -> &'static str {
578 "greet_struct"
579 }
580 fn description(&self) -> &'static str {
581 "greets as json"
582 }
583 async fn call_typed(
584 &self,
585 args: GreetArgs,
586 _ctx: &ToolContext,
587 ) -> AgentResult<GreetResult> {
588 Ok(GreetResult {
589 message: format!("Hello, {}!", args.name),
590 })
591 }
592 }
593
594 #[test]
595 fn format_output_json_serializes_struct() {
596 let out = GreetStructTool.format_output(GreetResult {
597 message: "hi".into(),
598 });
599 assert_eq!(content_text(&[out]), r#"{"message":"hi"}"#);
600 }
601
602 #[tokio::test]
603 async fn typed_tool_call_invalid_args_is_tool_args_invalid() {
604 let ctx = ToolContext::for_test();
605 let err = Tool::call(&GreetTool, &json!({"nope": 1}), &ctx)
606 .await
607 .unwrap_err();
608 assert!(matches!(err, AgentError::ToolArgsInvalid { .. }));
609 }
610
611 struct NamedTool(&'static str);
612 #[async_trait]
613 impl Tool for NamedTool {
614 fn name(&self) -> &'static str {
615 self.0
616 }
617 fn description(&self) -> &'static str {
618 ""
619 }
620 fn schema(&self) -> serde_json::Value {
621 serde_json::Value::Null
622 }
623 async fn call(
624 &self,
625 _args: &serde_json::Value,
626 _ctx: &ToolContext,
627 ) -> AgentResult<Vec<Content>> {
628 Ok(vec![])
629 }
630 }
631
632 #[test]
633 fn registry_register_arc_get_remove_len_is_empty() {
634 let mut r = ToolRegistry::default();
635 assert!(r.is_empty());
636 assert_eq!(r.len(), 0);
637
638 let t: Arc<dyn Tool> = Arc::new(NamedTool("x"));
639 r.register_arc(t);
640 assert!(!r.is_empty());
641 assert_eq!(r.len(), 1);
642 assert!(r.get("x").is_some());
643 assert!(r.get("missing").is_none());
644
645 r.remove("x");
646 assert!(r.is_empty());
647 }
648
649 #[test]
650 fn metadatas_are_sorted_by_name() {
651 let mut r = ToolRegistry::default();
652 r.register(NamedTool("zeta"));
653 r.register(NamedTool("alpha"));
654
655 let metas = r.metadatas();
656 let names: Vec<&str> = metas.iter().map(|m| m.name.as_str()).collect();
657 assert_eq!(names, vec!["alpha", "zeta"]);
658 assert_eq!(metas[0].origin, "custom");
659 assert_eq!(metas[0].version, "unknown");
660 }
661}