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

Rendering architecture…

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

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

  1. El usuario pregunta: "¿Qué tiempo hace en Tokio?"
  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: "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 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 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:

  1. La herramienta de inicio 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) 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ímiteValorAl excederlo
Profundidad de la cola por lote de herramientas256 eventosLa herramienta espera hasta 100 ms para obtener espacio y, después, se descarta el fragmento
Bytes por fragmento8 KiBEl fragmento se trunca en un límite de caracteres
Bytes por llamada de herramienta1 MiBEl 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_bash para ver una interfaz web completa que muestra en tiempo real la salida de bash y los resultados de herramientas de una sola ejecución (read_file, grep, glob) desde un único flujo de eventos. La herramienta de bash en streaming se encuentra en adk-devtools.


Ejemplos de ejecución

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

Prácticas recomendadas

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



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:

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

Herramientas de función - Documentación ADK-Rust | ADK-Rust