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