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