Herramientas de función
Amplía las capacidades del agente con funciones Rust personalizadas.
¿Qué son las herramientas de función?
Las herramientas de función te permiten dar a los agentes capacidades más allá de la conversación: llamar a APIs, realizar cálculos, acceder a bases de datos o ejecutar cualquier lógica personalizada. El LLM decide cuándo usar una herramienta según la solicitud del usuario.
Aspectos clave:
- 🚀
#[tool]macro - registro de herramientas sin código repetitivo (recomendado)- 🔧
FunctionTool::new()- encapsula manualmente cualquier función async- 📝 JSON parameters - entrada/salida flexible
- 🎯 Esquemas seguros en tipos - Schema de JSON automático a partir de tipos mediante schemars
- 🔗 Acceso al contexto - estado de la sesión, artefactos, memoria
Canalización de ejecución de herramientas
Recomendado: macro #[tool]
La forma más rápida de crear herramientas. La macro toma tu comentario de documentación como descripción y deriva el esquema de JSON a partir del tipo de tus argumentos:
use adk_tool::tool;
use adk_core::AdkError;
use schemars::JsonSchema;
use serde::Deserialize;
use serde_json::{json, Value};
#[derive(Deserialize, JsonSchema)]
struct WeatherArgs {
/// The city to look up
city: String,
/// Temperature unit (celsius or fahrenheit)
unit: Option<String>,
}
/// Get the current weather for a city.
#[tool]
async fn get_weather(args: WeatherArgs) -> Result<Value, AdkError> {
Ok(json!({ "temp": 22, "city": args.city }))
}
// Generated: pub struct GetWeather; — implements adk_core::Tool
// Use it: agent_builder.tool(Arc::new(GetWeather))
Si tu herramienta necesita contexto de sesión, añade Arc<dyn ToolContext> como primer parámetro:
use adk_core::ToolContext;
use std::sync::Arc;
/// Search the user's saved documents.
#[tool]
async fn search_docs(
ctx: Arc<dyn ToolContext>,
args: SearchArgs,
) -> Result<Value, AdkError> {
let user_id = ctx.user_id();
// ... use context for scoped access
}
Atributos de metadatos de la herramienta
Marca las herramientas como de solo lectura, seguras para concurrencia o de larga duración directamente en la macro:
/// Look up cached data — no side effects, safe for parallel dispatch.
#[tool(read_only, concurrency_safe)]
async fn cache_lookup(args: LookupArgs) -> Result<Value, AdkError> {
Ok(json!({"result": "cached"}))
}
/// Start a long-running background report.
#[tool(long_running)]
async fn generate_report(args: ReportArgs) -> Result<Value, AdkError> {
Ok(json!({"task_id": "abc123", "status": "processing"}))
}
Atributos disponibles (todos opcionales, combinables libremente):
| Atributo | Efecto |
|---|---|
read_only | is_read_only() → true — una de dos señales requeridas para el despacho concurrente de Auto |
concurrency_safe | is_concurrency_safe() → true — una de dos señales requeridas para el despacho concurrente de Auto |
long_running | is_long_running() → true — previene que LLM vuelva a llamar a una herramienta pendiente |
Plain #[tool] sin atributos conserva los valores predeterminados (todos false), por lo que el código existente no se ve afectado.
Alternativa: FunctionTool::new()
Para herramientas dinámicas o cuando prefieras un registro explícito:
Crea una herramienta con FunctionTool::new() y añade siempre un esquema para que el LLM sepa qué parámetros pasar:
use adk_rust::prelude::*;
use adk_rust::Launcher;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_json::json;
use std::sync::Arc;
#[derive(JsonSchema, Serialize, Deserialize)]
struct WeatherParams {
/// The city or location to get weather for
location: String,
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
dotenvy::dotenv().ok();
let api_key = std::env::var("GOOGLE_API_KEY")?;
let model = GeminiModel::new(&api_key, "gemini-2.5-flash")?;
// Weather tool with proper schema
let weather_tool = FunctionTool::new(
"get_weather",
"Get current weather for a location",
|_ctx, args| async move {
let location = args.get("location")
.and_then(|v| v.as_str())
.unwrap_or("unknown");
Ok(json!({
"location": location,
"temperature": "22°C",
"conditions": "sunny"
}))
},
)
.with_parameters_schema::<WeatherParams>(); // Required for LLM to call correctly!
let agent = LlmAgentBuilder::new("weather_agent")
.instruction("You help users check the weather. Always use the get_weather tool.")
.model(Arc::new(model))
.tool(Arc::new(weather_tool))
.build()?;
Launcher::new(Arc::new(agent)).run().await?;
Ok(())
}
⚠️ Importante: Usa siempre
.with_parameters_schema<T>(); sin él, el LLM no sabrá qué parámetros pasar y puede que no llame a la herramienta.
Cómo funciona:
- El usuario pregunta: "What's the weather in Tokyo?"
- LLM decide llamar a
get_weathercon{"location": "Tokyo"} - La herramienta devuelve
{"location": "Tokyo", "temperature": "22°C", "conditions": "sunny"} - LLM formatea la respuesta: "The weather in Tokyo is sunny at 22°C."
Paso 2: Manejo de parámetros
Extrae los parámetros de la JSON args:
let order_tool = FunctionTool::new(
"process_order",
"Process an order. Parameters: product_id (required), quantity (required), priority (optional)",
|_ctx, args| async move {
// Required parameters - return error if missing
let product_id = args.get("product_id")
.and_then(|v| v.as_str())
.ok_or_else(|| adk_core::AdkError::tool("product_id is required"))?;
let quantity = args.get("quantity")
.and_then(|v| v.as_i64())
.ok_or_else(|| adk_core::AdkError::tool("quantity is required"))?;
// Optional parameter with default
let priority = args.get("priority")
.and_then(|v| v.as_str())
.unwrap_or("normal");
Ok(json!({
"order_id": "ORD-12345",
"product_id": product_id,
"quantity": quantity,
"priority": priority,
"status": "confirmed"
}))
},
);
Paso 3: Parámetros tipados con Schema
Para herramientas complejas, usa structs tipados con JSON Schema:
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(JsonSchema, Serialize, Deserialize)]
struct CalculatorParams {
/// The arithmetic operation to perform
operation: Operation,
/// First operand
a: f64,
/// Second operand
b: f64,
}
#[derive(JsonSchema, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
enum Operation {
Add,
Subtract,
Multiply,
Divide,
}
let calculator = FunctionTool::new(
"calculator",
"Perform arithmetic operations",
|_ctx, args| async move {
let params: CalculatorParams = serde_json::from_value(args)?;
let result = match params.operation {
Operation::Add => params.a + params.b,
Operation::Subtract => params.a - params.b,
Operation::Multiply => params.a * params.b,
Operation::Divide if params.b != 0.0 => params.a / params.b,
Operation::Divide => return Err(adk_core::AdkError::tool("Cannot divide by zero")),
};
Ok(json!({ "result": result }))
},
)
.with_parameters_schema::<CalculatorParams>();
El esquema se genera automáticamente a partir de tipos de Rust usando schemars.
Paso 4: Agente con múltiples herramientas
Añade varias herramientas a un solo agente:
let agent = LlmAgentBuilder::new("assistant")
.instruction("Help with calculations, conversions, and weather.")
.model(Arc::new(model))
.tool(Arc::new(calc_tool))
.tool(Arc::new(convert_tool))
.tool(Arc::new(weather_tool))
.build()?;
El LLM elige automáticamente la herramienta correcta según la solicitud del usuario.
Manejo de errores
Devuelve errores con el componente Tool para fallos específicos de la herramienta:
use adk_core::{AdkError, ErrorComponent, ErrorCategory};
let divide_tool = FunctionTool::new(
"divide",
"Divide two numbers",
|_ctx, args| async move {
let a = args.get("a").and_then(|v| v.as_f64())
.ok_or_else(|| AdkError::new(
ErrorComponent::Tool,
ErrorCategory::InvalidInput,
"tool.divide.missing_param",
"Parameter 'a' is required",
))?;
let b = args.get("b").and_then(|v| v.as_f64())
.ok_or_else(|| AdkError::new(
ErrorComponent::Tool,
ErrorCategory::InvalidInput,
"tool.divide.missing_param",
"Parameter 'b' is required",
))?;
if b == 0.0 {
return Err(AdkError::new(
ErrorComponent::Tool,
ErrorCategory::InvalidInput,
"tool.divide.division_by_zero",
"Cannot divide by zero",
));
}
Ok(json!({ "result": a / b }))
},
);
Para una migración rápida, también funciona la abreviatura compatible hacia atrás:
Err(AdkError::tool("Parameter 'a' is required"))
Los mensajes de error se pasan al LLM, que puede reintentar o pedir una entrada diferente.
Contexto de la herramienta
Accede a la información de la sesión mediante ToolContext:
#[derive(JsonSchema, Serialize, Deserialize)]
struct GreetParams {
#[serde(default)]
message: Option<String>,
}
let greet_tool = FunctionTool::new(
"greet",
"Greet the user with session info",
|ctx, _args| async move {
let user_id = ctx.user_id();
let session_id = ctx.session_id();
let agent_name = ctx.agent_name();
Ok(json!({
"greeting": format!("Hello, user {}!", user_id),
"session": session_id,
"served_by": agent_name
}))
},
)
.with_parameters_schema::<GreetParams>();
Contexto disponible:
ctx.user_id()- ID del usuario actualctx.session_id()- ID de la sesión actualctx.agent_name()- Nombre del agentectx.artifacts()- Acceso al almacenamiento de artefactosctx.search_memory(query)- Servicio de búsqueda de memoria
Herramientas de larga duración
Para operaciones que tardan bastante tiempo (procesamiento de datos, APIs externo), usa el patrón no bloqueante:
- Iniciar herramienta devuelve inmediatamente un task_id
- El trabajo en segundo plano se ejecuta de forma asíncrona
- La herramienta de estado permite a los usuarios comprobar el progreso
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::RwLock;
#[derive(JsonSchema, Serialize, Deserialize)]
struct ReportParams {
topic: String,
}
#[derive(JsonSchema, Serialize, Deserialize)]
struct StatusParams {
task_id: String,
}
// Shared task store
let tasks: Arc<RwLock<HashMap<String, TaskState>>> = Arc::new(RwLock::new(HashMap::new()));
let tasks1 = tasks.clone();
let tasks2 = tasks.clone();
// Tool 1: Start (returns immediately)
let start_tool = FunctionTool::new(
"generate_report",
"Start generating a report. Returns task_id immediately.",
move |_ctx, args| {
let tasks = tasks1.clone();
async move {
let topic = args.get("topic").and_then(|v| v.as_str()).unwrap_or("general").to_string();
let task_id = format!("task_{}", rand::random::<u32>());
// Store initial state
tasks.write().await.insert(task_id.clone(), TaskState {
status: "processing".to_string(),
progress: 0,
result: None,
});
// Spawn background work (non-blocking!)
let tasks_bg = tasks.clone();
let tid = task_id.clone();
tokio::spawn(async move {
// Simulate work...
tokio::time::sleep(tokio::time::Duration::from_secs(10)).await;
if let Some(t) = tasks_bg.write().await.get_mut(&tid) {
t.status = "completed".to_string();
t.result = Some("Report complete".to_string());
}
});
// Return immediately with task_id
Ok(json!({"task_id": task_id, "status": "processing"}))
}
},
)
.with_parameters_schema::<ReportParams>()
.with_long_running(true); // Mark as long-running
// Tool 2: Check status
let status_tool = FunctionTool::new(
"check_report_status",
"Check report generation status",
move |_ctx, args| {
let tasks = tasks2.clone();
async move {
let task_id = args.get("task_id").and_then(|v| v.as_str()).unwrap_or("");
if let Some(t) = tasks.read().await.get(task_id) {
Ok(json!({"status": t.status, "result": t.result}))
} else {
Ok(json!({"error": "Task not found"}))
}
}
},
)
.with_parameters_schema::<StatusParams>();
Puntos clave:
.with_long_running(true)le indica al agente que esta herramienta devuelve un estado pendiente- La herramienta inicia el trabajo con
tokio::spawn()y devuelve el resultado inmediatamente - Proporciona una herramienta de comprobación de estado para que los usuarios puedan consultar el progreso
Esto añade una nota para evitar que el LLM llame a la herramienta repetidamente.
Streaming del progreso desde una herramienta
Las herramientas de larga duración pueden enviar salida intermedia a la interfaz mientras siguen
ejecutándose, de modo que el usuario ve en vivo la salida estándar de un comando de shell, los registros de una compilación o
los bytes de una descarga, en lugar de esperar al resultado final. Llama a
ToolContext::emit_progress a medida que llega la salida:
use adk_core::{Result, Tool, ToolContext};
use std::sync::Arc;
#[async_trait::async_trait]
impl Tool for BuildTool {
// ... name(), description(), parameters_schema() ...
async fn execute(&self, ctx: Arc<dyn ToolContext>, args: serde_json::Value) -> Result<serde_json::Value> {
// Emit chunks as they arrive — each becomes a partial Event on the
// agent's EventStream, the SAME stream the model's reply travels on.
ctx.emit_progress("stdout", "Compiling project...\n").await;
ctx.emit_progress("stdout", "Build finished in 4.2s\n").await;
ctx.emit_progress("stderr", "warning: unused variable `x`\n").await;
// The final return value is still the complete result the model consumes.
Ok(serde_json::json!({ "status": "ok", "warnings": 1 }))
}
}
La firma:
async fn emit_progress(&self, stream: &str, chunk: &str)
stream— una etiqueta para el fragmento:"stdout","stderr", o cualquier canal personalizado.chunk— el texto que se emitirá (emite por línea para salida de estilo terminal).
Cómo llega a la interfaz. El framework reenvía cada fragmento como una
Event parcial en la
EventStream del agente. Un consumidor lo detecta con event.tool_progress_stream() y
lo renderiza en vivo. No hay un segundo canal ni extracción de registros — el progreso,
el texto del modelo y el resultado final de la herramienta llegan todos en un único flujo ordenado.
Compatible con versiones anteriores. El emit_progress predeterminado no hace nada, así que las herramientas y ejecutores existentes que no transmiten datos no se ven afectados. Solo las herramientas que optan por ello emiten progreso, y solo los consumidores que comprueban tool_progress_stream() lo observan.
El progreso tiene límites y es con pérdida. Una herramienta puede producir salida más rápido de lo que un cliente la consume — un registro de compilación, un comando de shell, un bucle descontrolado —, así que el framework limita lo que conservará y reenviará en lugar de crecer sin límite:
| Límite | Valor | Al superarlo |
|---|---|---|
| Profundidad de cola por lote de herramienta | 256 eventos | La herramienta espera hasta 100 ms para disponer de espacio, luego el fragmento se descarta |
| Bytes por fragmento | 8 KiB | El fragmento se trunca en un límite de carácter |
| Bytes por llamada de herramienta | 1 MiB | No se reenvía el progreso restante |
Cuando la salida se descarta por cualquiera de estos motivos, se emite exactamente un evento de progreso que contiene el texto [adk: tool progress truncated] para esa llamada, de modo que siempre haya una pausa visible y no sea silenciosa. Por lo tanto, un consumidor lento ralentiza la herramienta brevemente, pero nunca puede bloquearla indefinidamente ni agotar la memoria.
Estos límites se aplican solo al progreso. El resultado final de una herramienta no se ve afectado, así que recorta los resultados grandes dentro de la herramienta si eso te importa.
Consulta el ejemplo de
streaming_bashpara una interfaz web completa que representa la salida en vivo debashy los resultados puntuales de las herramientas (read_file,grep,glob) a partir de un único flujo de eventos. La herramienta de streamingbashvive enadk-devtools.
Ejemplos de ejecución
cargo adk new tool_agent --template tools
cd tool_agent
cargo run
Mejores prácticas
- Descripciones claras - Ayuda al LLM a entender cuándo usar la herramienta
- Valida las entradas - Devuelve mensajes de error útiles para parámetros faltantes
- Devuelve JSON estructurado - Usa nombres de campo claros
- Mantén las herramientas enfocadas - Cada herramienta debe hacer bien una sola cosa
- Usa esquemas - Para herramientas complejas, define esquemas de parámetros
- Marca las herramientas de solo lectura seguras - Establece tanto
.with_read_only(true)como.with_concurrency_safe(true)para que el dispatch deAutopueda incluirlas en su subconjunto concurrente
Metadatos de la herramienta: solo lectura y concurrencia
Marca las herramientas como de solo lectura o seguras para concurrencia para habilitar un dispatch más inteligente:
// A lookup tool that performs no side effects
let lookup = FunctionTool::new("lookup", "Look up data", |_ctx, args| async move {
Ok(json!({"result": "cached data"}))
})
.with_read_only(true)
.with_concurrency_safe(true); // Auto mode requires both signals
// A mutation tool (defaults: read_only=false, concurrency_safe=false)
let update = FunctionTool::new("update", "Update record", |_ctx, args| async move {
Ok(json!({"updated": true}))
});
Cuando ToolExecutionStrategy::Auto está activo, el bucle de dispatch primero ejecuta las llamadas concurrentemente cuando las herramientas seleccionadas devuelven true tanto de is_read_only() como de is_concurrency_safe(). Luego ejecuta secuencialmente todas las llamadas restantes. ToolExecutionStrategy::Parallel es una anulación explícita que omite estas señales, por lo que su llamador asume la seguridad de concurrencia.
SimpleToolContext: usar herramientas fuera del bucle del agente
Cuando necesites llamar a una herramienta fuera del bucle del agente (pruebas, modo de servidor MCP, delegación a subagentes), usa SimpleToolContext en lugar de implementar toda la jerarquía del trait ToolContext:
use adk_tool::SimpleToolContext;
use adk_core::ToolContext;
use std::sync::Arc;
// Construct with just a caller name — all other fields get sensible defaults
let ctx = SimpleToolContext::new("my-test-harness");
// Optionally override the function call ID
let ctx = SimpleToolContext::new("my-mcp-server")
.with_function_call_id("custom-call-id");
// Bind a session ID so session-aware tools (and MCP servers that key state by
// session) see a stable identifier instead of the empty default.
let ctx = SimpleToolContext::new("my-mcp-server")
.with_session_id("session-42");
// Use it to execute any tool
let tool_ctx: Arc<dyn ToolContext> = Arc::new(ctx);
let result = my_tool.execute(tool_ctx, json!({"key": "value"})).await?;
Valores predeterminados: user_id() → "anonymous", session_id() / branch() → "", artifacts() → None, search_memory() → vec vacío. Tanto invocation_id como function_call_id se generan automáticamente UUIDs. Establece una sesión real con with_session_id(...) cuando una herramienta enruta o persiste estado por sesión.
StatefulTool: estado compartido entre invocaciones
Para herramientas que necesitan mantener estado entre llamadas (contadores, cachés, pools de conexión), usa StatefulTool<S>:
use adk_tool::StatefulTool;
use adk_core::ToolContext;
use std::sync::Arc;
use tokio::sync::RwLock;
struct AppCache {
entries: RwLock<HashMap<String, String>>,
}
let cache = Arc::new(AppCache {
entries: RwLock::new(HashMap::new()),
});
let cache_tool = StatefulTool::new(
"cache_lookup",
"Look up a value in the application cache",
cache.clone(),
|state, _ctx, args| async move {
let key = args["key"].as_str().unwrap_or("");
let entries = state.entries.read().await;
let value = entries.get(key).cloned().unwrap_or_default();
Ok(json!({"key": key, "value": value}))
},
)
.with_read_only(true)
.with_concurrency_safe(true);
StatefulTool clona el Arc<S> en cada invocación (un aumento barato del contador de referencias), por lo que todas las ejecuciones comparten el mismo estado subyacente. Admite los mismos métodos de constructor que FunctionTool: with_long_running, with_parameters_schema, with_response_schema, with_scopes, with_read_only y with_concurrency_safe.
Relacionado
- Herramientas integradas - Herramientas preconstruidas (GoogleSearch, ExitLoop)
- Herramientas MCP - Integración del Protocolo de Contexto del Modelo
- LlmAgent - Añadir herramientas a los agentes
Respuestas de funciones multimodales
Los modelos Gemini 3 admiten recibir imágenes, audio, PDFs y referencias a archivos en las respuestas de función, no solo JSON. Las herramientas pueden devolver datos multimodales incluyendo matrices inline_data y/o file_data en su valor de retorno JSON:
/// Tool that returns a chart image alongside JSON metadata.
async fn generate_chart(
_ctx: Arc<dyn ToolContext>,
args: serde_json::Value,
) -> Result<serde_json::Value> {
let png_bytes: Vec<u8> = render_chart(&args);
// Include inline_data in the return value — the framework extracts it automatically
Ok(json!({
"response": {
"title": "Q4 Sales",
"chart_type": "bar"
},
"inline_data": [{
"mime_type": "image/png",
"data": png_bytes
}]
}))
}
El framework automáticamente:
- Detecta
inline_data/file_datamedianteFunctionResponseData::from_tool_result() - Codifica en Base64 los datos binarios en línea
- Anida las partes dentro del objeto de cable
functionResponse(coincidiendo con el formato API de Gemini 3)
Referencias de archivos
Para archivos grandes almacenados externamente, usa file_data con un URI en lugar de incrustar bytes:
Ok(json!({
"response": { "document_id": "report-2024", "pages": 12 },
"file_data": [{
"mime_type": "application/pdf",
"file_uri": "gs://my-bucket/reports/report-2024.pdf"
}]
}))
Construcción directa
Para código a nivel de framework (agentes personalizados, capas de conversión), construye FunctionResponseData directamente:
use adk_core::{FunctionResponseData, InlineDataPart, FileDataPart};
// JSON + inline image
let frd = FunctionResponseData::with_inline_data(
"chart_tool",
json!({"title": "Q4 Chart"}),
vec![InlineDataPart { mime_type: "image/png".into(), data: png_bytes }],
);
// JSON + file reference
let frd = FunctionResponseData::with_file_data(
"doc_tool",
json!({"status": "ok"}),
vec![FileDataPart { mime_type: "application/pdf".into(), file_uri: "gs://bucket/file.pdf".into() }],
);
// JSON + both
let frd = FunctionResponseData::with_multimodal("tool", json, inline_parts, file_parts);
Nota: Las respuestas de función multimodales requieren modelos de la serie Gemini 3 (
gemini-3-flash-preview,gemini-3-pro-preview). Los modelos anteriores devuelven un error 400.
Consulta examples/multimodal_function_response/ para un ejemplo completo y funcional.
Anterior: ← mistral.rs | Siguiente: Herramientas integradas →