Per-Tool Policies
Per-tool policies let you attach execution constraints to individual tools
without changing their implementation. They are expressed in the T:: namespace
and composed with the same | operator as the rest of the tool algebra.
PolicyTool
Section titled “PolicyTool”Internally, T::timeout, T::cached, and T::confirm all wrap the target
tool in a PolicyTool — a ToolFunction decorator that carries a ToolPolicy
and enforces it on every call.
pub struct ToolPolicy { pub timeout: Option<Duration>, pub cache: bool, pub confirm: bool, pub confirm_message: Option<String>,}Policies compose: wrapping the same tool twice (e.g.,
T::cached(T::timeout(tool, dur))) applies both timeout and cache.
Timeout
Section titled “Timeout”T::timeout(tool, duration) races each call against tokio::time::timeout.
If the tool’s future does not complete within the given duration the call
returns ToolError::Timeout(duration) and the inner future is dropped.
use std::time::Duration;
Live::builder() .tools( T::timeout( T::simple("search_kb", "Search the knowledge base", |args| async move { // potentially slow call Ok(call_search_api(args).await?) }), Duration::from_secs(10), ) | T::google_search() )Timeout enforcement is fully implemented: tokio::time::timeout wraps the
inner call future, and any elapse produces ToolError::Timeout.
Caching
Section titled “Caching”T::cached(tool) memoizes successful results keyed by (tool name, canonical-JSON args). Repeat calls with identical arguments return the cached
value without re-invoking the tool. Errors are never cached.
Canonical JSON sorts object keys lexicographically, so {"b":2,"a":1} and
{"a":1,"b":2} produce the same cache key.
// Weather results cached for the lifetime of the sessionLive::builder() .tools( T::cached(T::simple("get_weather", "Get weather for a city", |args| async move { Ok(call_weather_api(&args["city"]).await?) })) )Cache entries live for the lifetime of the PolicyTool instance (i.e., the
session). There is no TTL or maximum entry count. If you need time-bounded
caching, combine with a timeout and manage expiry in your tool implementation.
Caching is fully implemented in PolicyTool::call.
Confirmation flag
Section titled “Confirmation flag”T::confirm(tool, message) marks a tool as requiring user confirmation before
it runs. The flag and optional hint message are recorded on the ToolPolicy
and surfaced at runtime via PolicyTool::requires_confirmation().
Live::builder() .tools( T::confirm( T::simple("send_email", "Send an email to the customer", |args| async move { Ok(do_send_email(args).await?) }), "This will send an email to the customer. Are you sure?", ) )Enforcing confirmation with a provider
Section titled “Enforcing confirmation with a provider”Confirmation is enforced at dispatch when you wire a ConfirmationProvider.
Before running any tool that reports requires_confirmation(), the
ToolDispatcher consults the provider; a denied decision returns
ToolError::Cancelled instead of executing the tool. Enforcement is opt-in —
with no provider configured, confirmation-gated tools run normally (the flag is
still surfaced via requires_confirmation()).
Wire one onto a dispatcher directly, or via Live::confirmation_provider:
use std::sync::Arc;
let handle = Live::builder() .tools(T::confirm(send_email_tool, "Send this email?")) // Any async closure `Fn(ConfirmationRequest) -> impl Future<Output = ToolConfirmation>` // works, or implement the `ConfirmationProvider` trait. .confirmation_provider(Arc::new(|req: ConfirmationRequest| async move { if approved_by_operator(&req.tool_name, &req.args).await { ToolConfirmation::confirmed() } else { ToolConfirmation::denied("operator rejected the action") } })) .connect_from_env() .await?;For tests and simple defaults, StaticConfirmation::allow_all() /
StaticConfirmation::deny_all("reason") provide uniform providers. The same
ToolDispatcher::with_confirmation_provider / set_confirmation_provider API
gates tools in text-agent pipelines too.
Async / Background Tool Execution
Section titled “Async / Background Tool Execution”For tools that take significant time — database queries, external API calls, LLM sub-pipelines — background execution eliminates dead air in voice sessions.
ToolExecutionMode
Section titled “ToolExecutionMode”pub enum ToolExecutionMode { Standard, // tool runs inline; model waits for the result (default) Background { formatter: Option<Arc<dyn ResultFormatter>>, scheduling: Option<FunctionResponseScheduling>, },}How background execution works
Section titled “How background execution works”- The model sends a
FunctionCallfor a background-declared tool. - An immediate “running” acknowledgment is sent to the model.
- The tool is spawned as a Tokio task. The model continues speaking.
- When the task completes, the result is injected into the conversation using
the configured
FunctionResponseSchedulingmode.
FunctionResponseScheduling modes
Section titled “FunctionResponseScheduling modes”| Mode | Behaviour |
|---|---|
Interrupt | Model halts current output and immediately handles the result |
WhenIdle | Model waits until it finishes current output before handling (default) |
Silent | Model integrates the result without notifying the user |
Platform support: async tool calling is only supported on Google AI.
On Vertex AI, behavior: NonBlocking is automatically stripped from
FunctionDeclaration setup messages and scheduling is stripped from
FunctionResponse. You can set these fields unconditionally; the SDK handles
the platform difference. Use config.supports_async_tools() to check at runtime.
L2 fluent API
Section titled “L2 fluent API”Live::builder() .dispatcher(dispatcher) .tool_background("search_kb") // WhenIdle scheduling by default .tool_background_with_scheduling( "log_event", FunctionResponseScheduling::Silent, // quiet integration ) .connect_from_env() .await?;L1 builder API
Section titled “L1 builder API”LiveSessionBuilder::new(config) .dispatcher(dispatcher) .tool_execution_mode("search_kb", ToolExecutionMode::Background { formatter: None, scheduling: Some(FunctionResponseScheduling::WhenIdle), }) .connect() .await?;Custom result formatting
Section titled “Custom result formatting”Implement ResultFormatter to control the JSON shape of acknowledgment and
completion messages:
use gemini_adk_rs::live::background_tool::{ResultFormatter, DefaultResultFormatter};
struct VoiceFriendlyFormatter;
impl ResultFormatter for VoiceFriendlyFormatter { fn format_running(&self, call: &FunctionCall) -> Value { json!({ "status": "searching", "for": call.args["query"] }) }
fn format_result(&self, call: &FunctionCall, result: Result<Value, ToolError>) -> Value { match result { Ok(v) => json!({ "status": "found", "tool": call.name, "data": v }), Err(e) => json!({ "status": "failed", "tool": call.name, "error": e.to_string() }), } }
fn format_cancelled(&self, call_id: &str) -> Value { json!({ "status": "cancelled", "id": call_id }) }}
// Register with custom formatter:Live::builder() .tool_background_with_formatter("search_kb", Arc::new(VoiceFriendlyFormatter))If formatter is None, DefaultResultFormatter is used, which produces:
{ "status": "running", "tool": "search_kb" }{ "status": "completed", "tool": "search_kb", "result": { ... } }{ "status": "error", "tool": "search_kb", "error": "..." }{ "status": "cancelled", "call_id": "fc_123" }Cancellation
Section titled “Cancellation”Background tasks are cancelled when:
- The server sends
ToolCallCancellation - The session disconnects
LiveHandleis dropped
BackgroundToolTracker provides belt-and-suspenders cleanup: both the
CancellationToken is triggered and the JoinHandle is aborted.
Combining policies
Section titled “Combining policies”Policies and execution modes compose freely:
Live::builder() .tools( // 10-second timeout + in-session cache T::cached(T::timeout( T::simple("get_stock_price", "Get a stock price", |args| async move { Ok(fetch_price(&args["ticker"]).await?) }), Duration::from_secs(10), )) // confirmation required on dangerous action | T::confirm( T::simple("cancel_order", "Cancel an order", |args| async move { Ok(do_cancel(&args["order_id"]).await?) }), "Cancel this order — are you sure?", ) | T::google_search() ) .tool_background("get_stock_price") // also run it in backgroundSee also
Section titled “See also”- Tools —
SimpleTool,TypedTool,ToolDispatcher, and tool registration - MCP Tools — applying policies to Model Context Protocol tools
- Middleware —
before_tool/after_toolhooks in agent pipelines - cookbook 34 — tool policies