1use agents_core::tools::{Tool, ToolBox, ToolContext, ToolParameterSchema, ToolResult, ToolSchema};
7use async_trait::async_trait;
8use serde_json::Value;
9use std::future::Future;
10use std::pin::Pin;
11use std::sync::Arc;
12
13type BoxFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
14
15pub type AsyncToolFn =
17 Arc<dyn Fn(Value, ToolContext) -> BoxFuture<'static, anyhow::Result<ToolResult>> + Send + Sync>;
18
19pub type SyncToolFn = Arc<dyn Fn(Value, ToolContext) -> anyhow::Result<ToolResult> + Send + Sync>;
21
22pub struct FunctionTool {
24 schema: ToolSchema,
25 handler: AsyncToolFn,
26}
27
28#[async_trait]
29impl Tool for FunctionTool {
30 fn schema(&self) -> ToolSchema {
31 self.schema.clone()
32 }
33
34 async fn execute(&self, args: Value, ctx: ToolContext) -> anyhow::Result<ToolResult> {
35 (self.handler)(args, ctx).await
36 }
37}
38
39pub struct ToolBuilder {
41 name: String,
42 description: String,
43 parameters: Option<ToolParameterSchema>,
44}
45
46impl ToolBuilder {
47 pub fn new(name: impl Into<String>, description: impl Into<String>) -> Self {
49 Self {
50 name: name.into(),
51 description: description.into(),
52 parameters: None,
53 }
54 }
55
56 pub fn with_parameters(mut self, parameters: ToolParameterSchema) -> Self {
58 self.parameters = Some(parameters);
59 self
60 }
61
62 pub fn build_async<F, Fut>(self, handler: F) -> ToolBox
64 where
65 F: Fn(Value, ToolContext) -> Fut + Send + Sync + 'static,
66 Fut: Future<Output = anyhow::Result<ToolResult>> + Send + 'static,
67 {
68 let schema = ToolSchema::new(
69 self.name,
70 self.description,
71 self.parameters.unwrap_or_else(|| {
72 ToolParameterSchema::object("No parameters", Default::default(), Vec::new())
73 }),
74 );
75
76 let handler: AsyncToolFn = Arc::new(move |args, ctx| Box::pin(handler(args, ctx)));
77
78 Arc::new(FunctionTool { schema, handler })
79 }
80
81 pub fn build_sync<F>(self, handler: F) -> ToolBox
83 where
84 F: Fn(Value, ToolContext) -> anyhow::Result<ToolResult> + Send + Sync + 'static,
85 {
86 let handler = Arc::new(handler);
87 self.build_async(move |args, ctx| {
88 let handler = handler.clone();
89 async move { handler(args, ctx) }
90 })
91 }
92}
93
94pub fn tool<F, Fut>(
96 name: impl Into<String>,
97 description: impl Into<String>,
98 parameters: ToolParameterSchema,
99 handler: F,
100) -> ToolBox
101where
102 F: Fn(Value, ToolContext) -> Fut + Send + Sync + 'static,
103 Fut: Future<Output = anyhow::Result<ToolResult>> + Send + 'static,
104{
105 ToolBuilder::new(name, description)
106 .with_parameters(parameters)
107 .build_async(handler)
108}
109
110pub fn tool_sync<F>(
112 name: impl Into<String>,
113 description: impl Into<String>,
114 parameters: ToolParameterSchema,
115 handler: F,
116) -> ToolBox
117where
118 F: Fn(Value, ToolContext) -> anyhow::Result<ToolResult> + Send + Sync + 'static,
119{
120 ToolBuilder::new(name, description)
121 .with_parameters(parameters)
122 .build_sync(handler)
123}
124
125#[cfg(test)]
126mod tests {
127 use super::*;
128 use agents_core::state::AgentStateSnapshot;
129 use serde_json::json;
130
131 #[tokio::test]
132 async fn function_tool_executes_handler() {
133 let tool = ToolBuilder::new("echo", "Echoes input")
134 .with_parameters(ToolParameterSchema::object(
135 "Echo parameters",
136 [(
137 "message".to_string(),
138 ToolParameterSchema::string("Message to echo"),
139 )]
140 .into_iter()
141 .collect(),
142 vec!["message".to_string()],
143 ))
144 .build_async(|args, ctx| async move {
145 let msg = args["message"].as_str().unwrap_or("empty");
146 Ok(ToolResult::text(&ctx, format!("Echo: {}", msg)))
147 });
148
149 let schema = tool.schema();
150 assert_eq!(schema.name, "echo");
151 assert_eq!(schema.description, "Echoes input");
152
153 let ctx = ToolContext::new(Arc::new(AgentStateSnapshot::default()));
154 let result = tool
155 .execute(json!({"message": "hello"}), ctx)
156 .await
157 .unwrap();
158
159 match result {
160 ToolResult::Message(msg) => {
161 assert_eq!(msg.content.as_text().unwrap(), "Echo: hello");
162 }
163 _ => panic!("Expected message result"),
164 }
165 }
166
167 #[tokio::test]
168 async fn sync_tool_works() {
169 let tool = tool_sync(
170 "add",
171 "Adds two numbers",
172 ToolParameterSchema::object(
173 "Add parameters",
174 [
175 ("a".to_string(), ToolParameterSchema::number("First number")),
176 (
177 "b".to_string(),
178 ToolParameterSchema::number("Second number"),
179 ),
180 ]
181 .into_iter()
182 .collect(),
183 vec!["a".to_string(), "b".to_string()],
184 ),
185 |args, ctx| {
186 let a = args["a"].as_f64().unwrap_or(0.0);
187 let b = args["b"].as_f64().unwrap_or(0.0);
188 let sum = a + b;
189 Ok(ToolResult::text(&ctx, format!("Sum: {}", sum)))
190 },
191 );
192
193 let ctx = ToolContext::new(Arc::new(AgentStateSnapshot::default()));
194 let result = tool.execute(json!({"a": 5, "b": 3}), ctx).await.unwrap();
195
196 match result {
197 ToolResult::Message(msg) => {
198 assert_eq!(msg.content.as_text().unwrap(), "Sum: 8");
199 }
200 _ => panic!("Expected message result"),
201 }
202 }
203}