Herramientas de función
Amplía las capacidades del agente con funciones personalizadas de Rust.
¿Qué son las herramientas de función?
Las herramientas de función permiten proporcionar 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. LLM decide cuándo usar una herramienta según la solicitud del usuario.
Aspectos destacados:
- 🚀 Macro
#[tool]: registro de herramientas sin código repetitivo (recomendado)- 🔧
FunctionTool::new(): envolver manualmente cualquier función asíncrona- 📝 Parámetros JSON: entrada y salida flexibles
- 🎯 Esquemas seguros respecto a tipos: JSON Schema automático a partir de tipos mediante schemars
- 🔗 Acceso al contexto: estado de la sesión, artefactos y memoria
Canalización de ejecución de herramientas
Recomendado: macro #[tool]
La forma más rápida de crear herramientas. La macro lee el comentario de documentación como descripción y deriva el esquema JSON a partir del tipo de los 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 la herramienta necesita el contexto de la 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 las herramientas
Marca las herramientas como de solo lectura, seguras para la concurrencia o de ejecución prolongada 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, se pueden combinar libremente):
| Atributo | Efecto |
|---|---|
read_only | is_read_only() → true — una de las dos señales necesarias para el despacho concurrente de Auto |
concurrency_safe | is_concurrency_safe() → true — una de las dos señales necesarias para el despacho concurrente de Auto |
long_running | is_long_running() → true — evita 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 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-3.7-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, LLM no sabrá qué parámetros pasar y es posible que no llame a la herramienta.
Cómo funciona:
- El usuario pregunta: "¿Qué tiempo hace en Tokio?"
- LLM decide llamar a
get_weathercon{"location": "Tokyo"} - La herramienta devuelve
{"location": "Tokyo", "temperature": "22°C", "conditions": "sunny"} - LLM formatea la respuesta: "El tiempo en Tokio es soleado, con 22 °C."
Paso 2: Gestión de parámetros
Extrae los parámetros de 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 un esquema
Para herramientas complejas, usa estructuras tipadas con el esquema de JSON:
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 los tipos de Rust mediante schemars.
Paso 4: Agente con varias herramientas
Añade varias herramientas a un mismo 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()?;
LLM elige automáticamente la herramienta adecuada según la solicitud del usuario.
Gestión de errores
Devuelve errores con el componente Tool para errores 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 forma abreviada compatible con versiones anteriores:
Err(AdkError::tool("Parameter 'a' is required"))
Los mensajes de error se pasan a LLM, que puede reintentar o solicitar 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 en la memoria
Herramientas de larga duración
Para las operaciones que requieren un tiempo considerable (procesamiento de datos, APIs externos), utiliza el patrón no bloqueante:
- La herramienta de inicio 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)indica al agente que esta herramienta devuelve un estado pendiente- La herramienta inicia el trabajo con
tokio::spawn()y devuelve el control 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 LLM vuelva a llamar a la herramienta repetidamente.
Transmisión del progreso desde una herramienta
Las herramientas de larga duración pueden enviar resultados intermedios a la interfaz mientras siguen ejecutándose, de modo que el usuario vea en directo la salida stdout de un comando de shell, los registros de una compilación o los bytes de una descarga, en lugar de tener que esperar al resultado final. Llama a ToolContext::emit_progress a medida que llegue 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 una línea por vez para obtener una salida de estilo terminal).
Cómo llega a la interfaz. El framework reenvía cada fragmento como un Event parcial en el
EventStream del agente. Un consumidor lo detecta con event.tool_progress_stream() y
lo representa en directo. No hay un segundo canal ni extracción de registros: el progreso, el texto del modelo y el resultado final de la herramienta llegan en un único flujo ordenado.
Compatibilidad con versiones anteriores. El emit_progress predeterminado no hace nada, por lo que las herramientas y los ejecutores existentes que no transmiten datos no se ven afectados. Solo las herramientas que optan por habilitar esta función emiten progreso, y solo los consumidores que comprueban tool_progress_stream() lo observan.
El progreso tiene límites y es propenso a pérdidas. Una herramienta puede producir resultados más rápido de lo que un cliente los consume —un registro del compilador, un comando de shell, un bucle descontrolado—, por lo que el framework limita la cantidad que conservará y reenviará en lugar de crecer sin límite:
| Límite | Valor | Al excederlo |
|---|---|---|
| Profundidad de la cola por lote de herramientas | 256 eventos | La herramienta espera hasta 100 ms para obtener espacio y, después, se descarta el fragmento |
| Bytes por fragmento | 8 KiB | El fragmento se trunca en un límite de caracteres |
| Bytes por llamada de herramienta | 1 MiB | El progreso restante no se reenvía |
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 brecha visible en lugar de silenciosa. Por lo tanto, un consumidor lento ralentiza brevemente la herramienta, pero nunca puede bloquearla indefinidamente ni agotar la memoria.
Estos límites se aplican únicamente al progreso. El resultado final de una herramienta no se ve afectado, así que trunca los resultados grandes dentro de la herramienta si eso te resulta relevante.
Consulta el ejemplo
streaming_bashpara ver una interfaz web completa que muestra en tiempo real la salida debashy los resultados de herramientas de una sola ejecución (read_file,grep,glob) desde un único flujo de eventos. La herramienta debashen streaming se encuentra enadk-devtools.
Ejemplos de ejecución
cargo adk new tool_agent --template tools
cd tool_agent
cargo run
Prácticas recomendadas
- Descripciones claras: Ayuda a LLM a entender cuándo usar la herramienta
- Valida las entradas: Devuelve mensajes de error útiles cuando falten parámetros
- Devuelve JSON estructurado: Usa nombres de campos claros
- Mantén las herramientas enfocadas: Cada herramienta debe hacer una sola cosa bien
- Usa esquemas: Para herramientas complejas, define esquemas de parámetros
- Marca las herramientas seguras y de solo lectura: Establece tanto
.with_read_only(true)como.with_concurrency_safe(true)para queAutopueda incluirlas en su subconjunto concurrente
Metadatos de herramientas: solo lectura y concurrencia
Marca las herramientas como de solo lectura o seguras para la concurrencia a fin de permitir un despacho 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 despacho primero ejecuta las llamadas de forma concurrente cuando las herramientas seleccionadas devuelven true tanto de is_read_only() como de is_concurrency_safe(). Después ejecuta secuencialmente todas las llamadas restantes. ToolExecutionStrategy::Parallel es una anulación explícita que omite estas señales, por lo que la seguridad de la concurrencia queda a cargo de quien realiza la llamada.
SimpleToolContext: Uso de herramientas fuera del bucle del agente
Cuando necesites llamar a una herramienta fuera del bucle del agente (pruebas, modo de servidor de MCP, delegación a subagentes), usa SimpleToolContext en lugar de implementar toda la jerarquía de traits 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() → vector vacío. Tanto invocation_id como function_call_id son UUIDs generados automáticamente. Establece una sesión real con with_session_id(...) cuando una herramienta enrute o persista el estado por sesión.
StatefulTool: Estado compartido entre invocaciones
Para las herramientas que necesiten mantener el estado entre llamadas (contadores, cachés, grupos de conexiones), 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 incremento económico del contador de referencias), por lo que todas las ejecuciones comparten el mismo estado subyacente. Admite los mismos métodos del 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 prediseñadas (GoogleSearch, ExitLoop)
- Herramientas de MCP - Integración con Model Context Protocol
- 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 funciones, 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 insertados
- Anida las partes dentro del objeto de red
functionResponse(con el formato API de Gemini 3)
Referencias a archivos
Para archivos grandes almacenados externamente, usa file_data con un URI en lugar de insertar los 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 del 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 funciones multimodales requieren modelos de la serie Gemini 3, como
gemini-3.7-flash. Los modelos anteriores devuelven un error 400.
Consulta examples/multimodal_function_response/ para ver un ejemplo completo en funcionamiento.
Anterior: ← mistral.rs | Siguiente: Herramientas integradas →