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

Rendering architecture…

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):

AtributoEfecto
read_onlyis_read_only() → true — una de dos señales requeridas para el despacho concurrente de Auto
concurrency_safeis_concurrency_safe() → true — una de dos señales requeridas para el despacho concurrente de Auto
long_runningis_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:

  1. El usuario pregunta: "What's the weather in Tokyo?"
  2. LLM decide llamar a get_weather con {"location": "Tokyo"}
  3. La herramienta devuelve {"location": "Tokyo", "temperature": "22°C", "conditions": "sunny"}
  4. 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 actual
  • ctx.session_id() - ID de la sesión actual
  • ctx.agent_name() - Nombre del agente
  • ctx.artifacts() - Acceso al almacenamiento de artefactos
  • ctx.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:

  1. Iniciar herramienta devuelve inmediatamente un task_id
  2. El trabajo en segundo plano se ejecuta de forma asíncrona
  3. 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ímiteValorAl superarlo
Profundidad de cola por lote de herramienta256 eventosLa herramienta espera hasta 100 ms para disponer de espacio, luego el fragmento se descarta
Bytes por fragmento8 KiBEl fragmento se trunca en un límite de carácter
Bytes por llamada de herramienta1 MiBNo 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_bash para una interfaz web completa que representa la salida en vivo de bash y los resultados puntuales de las herramientas (read_file, grep, glob) a partir de un único flujo de eventos. La herramienta de streaming bash vive en adk-devtools.


Ejemplos de ejecución

cargo adk new tool_agent --template tools
cd tool_agent
cargo run

Mejores prácticas

  1. Descripciones claras - Ayuda al LLM a entender cuándo usar la herramienta
  2. Valida las entradas - Devuelve mensajes de error útiles para parámetros faltantes
  3. Devuelve JSON estructurado - Usa nombres de campo claros
  4. Mantén las herramientas enfocadas - Cada herramienta debe hacer bien una sola cosa
  5. Usa esquemas - Para herramientas complejas, define esquemas de parámetros
  6. Marca las herramientas de solo lectura seguras - Establece tanto .with_read_only(true) como .with_concurrency_safe(true) para que el dispatch de Auto pueda 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.



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:

  1. Detecta inline_data/file_data mediante FunctionResponseData::from_tool_result()
  2. Codifica en Base64 los datos binarios en línea
  3. 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 →