Skip to main content

adk_tool/
function_tool.rs

1use adk_core::{Result, Tool, ToolContext};
2use async_trait::async_trait;
3use schemars::{
4    JsonSchema,
5    generate::{SchemaGenerator, SchemaSettings},
6};
7use serde::Serialize;
8use serde_json::Value;
9use std::future::Future;
10use std::pin::Pin;
11use std::sync::Arc;
12
13type AsyncHandler = Box<
14    dyn Fn(Arc<dyn ToolContext>, Value) -> Pin<Box<dyn Future<Output = Result<Value>> + Send>>
15        + Send
16        + Sync,
17>;
18
19/// A tool created from an async Rust function.
20///
21/// `FunctionTool` wraps an async closure and exposes it as a [`Tool`] that
22/// LLM agents can invoke. Use the builder methods to configure schema,
23/// execution flags, and required scopes.
24pub struct FunctionTool {
25    name: String,
26    description: String,
27    handler: AsyncHandler,
28    long_running: bool,
29    read_only: bool,
30    concurrency_safe: bool,
31    parameters_schema: Option<Value>,
32    response_schema: Option<Value>,
33    scopes: Vec<&'static str>,
34}
35
36impl FunctionTool {
37    /// Create a new `FunctionTool` from an async handler function.
38    pub fn new<F, Fut>(name: impl Into<String>, description: impl Into<String>, handler: F) -> Self
39    where
40        F: Fn(Arc<dyn ToolContext>, Value) -> Fut + Send + Sync + 'static,
41        Fut: Future<Output = Result<Value>> + Send + 'static,
42    {
43        Self {
44            name: name.into(),
45            description: description.into(),
46            handler: Box::new(move |ctx, args| Box::pin(handler(ctx, args))),
47            long_running: false,
48            read_only: false,
49            concurrency_safe: false,
50            parameters_schema: None,
51            response_schema: None,
52            scopes: Vec::new(),
53        }
54    }
55
56    /// Mark this tool as long-running (prevents duplicate invocations).
57    pub fn with_long_running(mut self, long_running: bool) -> Self {
58        self.long_running = long_running;
59        self
60    }
61
62    /// Mark this tool as read-only (safe for parallel dispatch).
63    pub fn with_read_only(mut self, read_only: bool) -> Self {
64        self.read_only = read_only;
65        self
66    }
67
68    /// Mark this tool as concurrency-safe (can run in parallel with other tools).
69    pub fn with_concurrency_safe(mut self, concurrency_safe: bool) -> Self {
70        self.concurrency_safe = concurrency_safe;
71        self
72    }
73
74    /// Derive the parameters JSON Schema from a type implementing `JsonSchema`.
75    pub fn with_parameters_schema<T>(mut self) -> Self
76    where
77        T: JsonSchema + Serialize,
78    {
79        self.parameters_schema = Some(generate_schema::<T>());
80        self
81    }
82
83    /// Derive the response JSON Schema from a type implementing `JsonSchema`.
84    pub fn with_response_schema<T>(mut self) -> Self
85    where
86        T: JsonSchema + Serialize,
87    {
88        self.response_schema = Some(generate_schema::<T>());
89        self
90    }
91
92    /// Declare the scopes required to execute this tool.
93    ///
94    /// When set, the framework will enforce that the calling user possesses
95    /// **all** listed scopes before dispatching `execute()`.
96    ///
97    /// # Example
98    ///
99    /// ```rust,ignore
100    /// let tool = FunctionTool::new("transfer", "Transfer funds", handler)
101    ///     .with_scopes(&["finance:write", "verified"]);
102    /// ```
103    pub fn with_scopes(mut self, scopes: &[&'static str]) -> Self {
104        self.scopes = scopes.to_vec();
105        self
106    }
107
108    /// Get the parameters schema, if set.
109    pub fn parameters_schema(&self) -> Option<&Value> {
110        self.parameters_schema.as_ref()
111    }
112
113    /// Get the response schema, if set.
114    pub fn response_schema(&self) -> Option<&Value> {
115        self.response_schema.as_ref()
116    }
117}
118
119/// The note appended to long-running tool descriptions to prevent duplicate calls.
120const LONG_RUNNING_NOTE: &str = "NOTE: This is a long-running operation. Do not call this tool again if it has already returned some intermediate or pending status.";
121
122#[async_trait]
123impl Tool for FunctionTool {
124    fn name(&self) -> &str {
125        &self.name
126    }
127
128    fn description(&self) -> &str {
129        &self.description
130    }
131
132    /// Returns an enhanced description for long-running tools that includes
133    /// a note warning the model not to call the tool again if it's already pending.
134    fn enhanced_description(&self) -> String {
135        if self.long_running {
136            if self.description.is_empty() {
137                LONG_RUNNING_NOTE.to_string()
138            } else {
139                format!("{}\n\n{}", self.description, LONG_RUNNING_NOTE)
140            }
141        } else {
142            self.description.clone()
143        }
144    }
145
146    fn is_long_running(&self) -> bool {
147        self.long_running
148    }
149
150    fn is_read_only(&self) -> bool {
151        self.read_only
152    }
153
154    fn is_concurrency_safe(&self) -> bool {
155        self.concurrency_safe
156    }
157
158    fn parameters_schema(&self) -> Option<Value> {
159        self.parameters_schema.clone()
160    }
161
162    fn response_schema(&self) -> Option<Value> {
163        self.response_schema.clone()
164    }
165
166    fn required_scopes(&self) -> &[&str] {
167        &self.scopes
168    }
169
170    #[adk_telemetry::instrument(
171        skip(self, ctx, args),
172        fields(
173            tool.name = %self.name,
174            tool.description = %self.description,
175            tool.long_running = %self.long_running,
176            function_call.id = %ctx.function_call_id()
177        )
178    )]
179    async fn execute(&self, ctx: Arc<dyn ToolContext>, args: Value) -> Result<Value> {
180        adk_telemetry::debug!("Executing tool");
181        (self.handler)(ctx, args).await
182    }
183}
184
185fn generate_schema<T>() -> Value
186where
187    T: JsonSchema + Serialize,
188{
189    let settings = SchemaSettings::openapi3().with(|s| {
190        s.inline_subschemas = true;
191        s.meta_schema = None;
192    });
193    let generator = SchemaGenerator::new(settings);
194    let mut schema = generator.into_root_schema_for::<T>();
195    if let Some(object) = schema.as_object_mut() {
196        object.remove("title");
197    }
198    serde_json::to_value(schema).unwrap()
199}