Function tools & #[tool]
Turning plain async Rust functions into agent tools, either with the #[tool] proc-macro or the explicit FunctionTool and LongRunningFunctionTool wrappers.
Most custom tools are just an async function. The #[tool] attribute macro turns one into a fully-declared Arc<dyn Tool> — schema derived from your args struct, description lifted from your doc comment. When you need runtime-built names, schemas, or confirmation flags, drop down to FunctionTool.
The #[tool] macro
The macro lives in the sibling adk-rs-macros crate and is re-exported as adk_rs::tool (and adk_rs::tools::tool) behind the macros feature. It accepts an async function with exactly two parameters: a typed args struct and &mut ToolContext.
- The function must be
async fn— the macro rejects sync functions at compile time. - The first parameter is your args struct; it must implement
serde::Deserializeandschemars::JsonSchema. Doc comments on its fields flow into the generated parameter schema. - The second parameter must be
&mut ToolContext. Receivers (self) are not supported. - The return type is
adk_rs::Result<T>whereT: serde::Serialize— the value is serialized to JSON for theFunctionResponse. - The doc comment on the function becomes the tool’s
description()(lines are joined with newlines).
What it generates
For async fn get_weather(...), the macro emits a PascalCase unit struct GetWeather, a DynTool impl on it, and a free constructor function get_weather() -> Arc<dyn DynTool> that shadows the original fn name. The impl: name() returns "get_weather"; declaration() builds a FunctionDeclaration whose parameters come from schemars::schema_for!(Args) converted through Schema::from_schemars; run() deserializes the JSON args into your struct — returning ToolError::InvalidArgs with a useful message on mismatch — awaits your body, and serializes the result.
Full example: a weather tool
use adk_rs::agents::LlmAgent;
use adk_rs::core::ToolContext;
use adk_rs::tool;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
#[derive(Deserialize, JsonSchema)]
struct GetWeatherArgs {
/// City name in English (e.g. "Paris").
city: String,
}
#[derive(Serialize)]
struct WeatherReport {
city: String,
temp_c: f32,
description: String,
}
/// Look up the current weather in `args.city`.
#[tool]
async fn get_weather(
args: GetWeatherArgs,
_ctx: &mut ToolContext,
) -> adk_rs::Result<WeatherReport> {
Ok(WeatherReport {
city: args.city,
temp_c: 22.0,
description: "sunny".into(),
})
}
let agent = LlmAgent::builder("weather")
.model(model)
.instruction("Use get_weather to answer questions about cities' weather.")
.tool(get_weather()) // the generated constructor
.build()?;FunctionTool: the explicit fallback
FunctionTool wraps an async fn(Value, &mut ToolContext) -> Result<Value> with explicitly supplied metadata. Use it when names or schemas are only known at runtime, or when you need the confirmation/long-running switches the macro does not expose.
FunctionTool::new(name, description, parameters: Option<Schema>, f: FunctionToolFn) -> Self- Core constructor taking a boxed-future closure.
parameters: Noneadvertises an empty object schema. FunctionTool::from_async(name, description, parameters, f) -> Self- Convenience wrapper accepting any
Fn(Value, &mut ToolContext) -> impl Future<Output = Result<Value>>— no manual boxing. fn with_long_running(self, yes: bool) -> Self- Makes
is_long_running()returntrue, so calls pause the invocation (see below). fn require_confirmation(self, yes: bool) -> Self- Requires explicit user approval before every call; the agent pauses with an
adk_request_confirmationrequest. See tool confirmation. fn with_confirmation_hint(self, hint: impl Into<String>) -> Self- Custom prompt shown to the user instead of the generic “Approve execution of tool …?”.
use adk_rs::core::ToolContext;
use adk_rs::genai_types::Schema;
use adk_rs::tools::FunctionTool;
use serde_json::{Value, json};
use std::sync::Arc;
let delete = FunctionTool::from_async(
"delete_record",
"Delete a record by id. Irreversible.",
Some(Schema::object().property("id", Schema::string()).require("id")),
|args: Value, _ctx: &mut ToolContext| async move {
// ... perform the deletion ...
Ok(json!({"deleted": args["id"]}))
},
)
.require_confirmation(true)
.with_confirmation_hint("Permanently delete this record?");
let tool: Arc<dyn adk_rs::Tool> = Arc::new(delete);LongRunningFunctionTool and will_continue
LongRunningFunctionTool::new(name, description, schema: Option<Schema>, handler) returns an Arc<Self> whose is_long_running() is true and whose run also sets ctx.long_running = true. The handler should return quickly with an operation handle (a ticket id, a status URL), not the final result.
The agent then emits the tool’s FunctionResponse with will_continue: Some(true), records the call id in the event’s long_running_tool_ids, marks the invocation paused, and yields the stream. Your application completes the work out of band and resumes by submitting a fresh FunctionResponse carrying the final result on a follow-up invocation — the agent replays it into the conversation and the model continues from there. The same pause/resume mechanics back the built-in get_user_choice tool and the confirmation/auth gates. See cancellation & resume.
Macro or manual?
| Situation | Reach for |
|---|---|
| Typed args, compile-time name, description from docs | #[tool] |
| Name/description/schema computed at runtime | FunctionTool::from_async |
Needs require_confirmation / with_confirmation_hint | FunctionTool |
| Returns an operation handle, completes out of band | LongRunningFunctionTool (or FunctionTool::with_long_running) |
Custom auth_config, per-args confirmation, or process_llm_request logic | implement DynTool directly |
Related pages
- Tools overview — the
DynTooltrait andToolContextin full. - Built-in tools — what ships in the box.
- Weather agent example — the macro in a complete program.