Ferramentas de Função

Estenda as capacidades do agente com funções Rust personalizadas.


O que são Ferramentas de Função?

As ferramentas de função permitem que você dê aos agentes habilidades além da conversa — chamando APIs, realizando cálculos, acessando bancos de dados ou qualquer lógica personalizada. O LLM decide quando usar uma ferramenta com base na solicitação do usuário.

Destaques principais:

  • 🚀 #[tool] macro - registro de ferramenta sem boilerplate (recomendado)
  • 🔧 FunctionTool::new() - encapsule qualquer função async manualmente
  • 📝 JSON parameters - entrada/saída flexível
  • 🎯 Esquemas seguros de tipo - JSON Schema automático a partir de tipos via schemars
  • 🔗 Acesso ao contexto - estado da sessão, artifacts, memória

Pipeline de Execução da Ferramenta

Rendering architecture…

A maneira mais rápida de criar ferramentas. A macro lê seu comentário de documentação como a descrição e deriva o schema JSON a partir do tipo dos seus 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))

Se sua ferramenta precisar de contexto de sessão, adicione Arc<dyn ToolContext> como o primeiro 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 Metadados da Ferramenta

Marque ferramentas como somente leitura, seguras para concorrência ou de longa execução diretamente na 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 disponíveis (todos opcionais, combine livremente):

AtributoEfeito
read_onlyis_read_only() → true — um dos dois sinais necessários para o despacho concorrente de Auto
concurrency_safeis_concurrency_safe() → true — um dos dois sinais necessários para o despacho concorrente de Auto
long_runningis_long_running() → true — impede LLM de chamar novamente uma ferramenta pendente

Texto simples #[tool] sem atributos mantém os padrões (todos false), então o código existente não é afetado.


Alternativa: FunctionTool::new()

Para ferramentas dinâmicas ou quando você prefere registro explícito:

Crie uma ferramenta com FunctionTool::new() e sempre adicione um schema para que o LLM saiba quais parâmetros passar:

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: Sempre use .with_parameters_schema<T>() - sem ele, o LLM não saberá quais parâmetros passar e pode não chamar a ferramenta.

Como funciona:

  1. O usuário pergunta: "What's the weather in Tokyo?"
  2. LLM decide chamar get_weather com {"location": "Tokyo"}
  3. A ferramenta retorna {"location": "Tokyo", "temperature": "22°C", "conditions": "sunny"}
  4. LLM formata a resposta: "The weather in Tokyo is sunny at 22°C."

Etapa 2: Tratamento de Parâmetros

Extraia parâmetros do 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"
        }))
    },
);

Etapa 3: Parâmetros Tipados com Schema

Para ferramentas complexas, use structs tipadas com Schema 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>();

O schema é gerado automaticamente a partir dos tipos Rust usando schemars.


Etapa 4: Agente com Múltiplas Ferramentas

Adicione várias ferramentas a um único 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()?;

O LLM escolhe automaticamente a ferramenta certa com base na solicitação do usuário.


Tratamento de Erros

Retorne erros com o componente Tool para falhas específicas da ferramenta:

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 migração rápida, a forma abreviada compatível com versões anteriores também funciona:

Err(AdkError::tool("Parameter 'a' is required"))

As mensagens de erro são passadas para o LLM, que pode tentar novamente ou pedir uma entrada diferente.


Contexto da Ferramenta

Acesse as informações da sessão via 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 disponível:

  • ctx.user_id() - ID atual do usuário
  • ctx.session_id() - ID atual da sessão
  • ctx.agent_name() - Nome do agente
  • ctx.artifacts() - Acesso ao armazenamento de artefatos
  • ctx.search_memory(query) - Serviço de busca na memória

Ferramentas de Longa Duração

Para operações que levam bastante tempo (processamento de dados, APIs externos), use o padrão não bloqueante:

  1. Iniciar ferramenta retorna imediatamente com um task_id
  2. Trabalho em segundo plano é executado de forma assíncrona
  3. Ferramenta de status permite que os usuários verifiquem o progresso
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>();

Pontos principais:

  • .with_long_running(true) informa ao agente que esta ferramenta retorna um status pendente
  • A ferramenta inicia o trabalho com tokio::spawn() e retorna imediatamente
  • Forneça uma ferramenta de verificação de status para que os usuários possam consultar o progresso

Isso adiciona uma nota para evitar que o LLM chame a ferramenta repetidamente.


Streaming de Progresso de uma Ferramenta

Ferramentas de longa duração podem enviar saída intermediária para a UI enquanto ainda estão executando, para que o usuário veja o stdout de um comando de shell, os logs de um build ou os bytes de um download ao vivo, em vez de esperar pelo resultado final. Chame ToolContext::emit_progress conforme a saída chega:

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

A assinatura:

async fn emit_progress(&self, stream: &str, chunk: &str)
  • stream — um rótulo para o bloco: "stdout", "stderr" ou qualquer canal personalizado.
  • chunk — o texto a emitir (emita por linha para saída no estilo terminal).

Como isso chega à UI. O framework encaminha cada bloco como uma Event parcial no EventStream do agente. Um consumidor o detecta com event.tool_progress_stream() e o renderiza ao vivo. Não há segundo canal nem análise de logs — progresso, texto do modelo e o resultado final da ferramenta chegam todos em um único fluxo ordenado.

Compatível com versões anteriores. O emit_progress padrão é no-op, então ferramentas e executores existentes que não fazem streaming não são afetados. Apenas ferramentas que optam por isso emitem progresso, e apenas consumidores que verificam tool_progress_stream() o observam.

O progresso é limitado e pode ser perdido. Uma ferramenta pode produzir saída mais rápido do que um cliente consome — um log de compilador, um comando de shell, um loop descontrolado — então o framework limita o que ele vai armazenar e encaminhar em vez de crescer sem limite:

LimiteValorAo excedê-lo
Profundidade da fila por lote de ferramenta256 eventosA ferramenta aguarda até 100 ms por espaço e, em seguida, o bloco é descartado
Bytes por bloco8 KiBO bloco é truncado em um limite de caractere
Bytes por chamada de ferramenta1 MiBO progresso restante não é encaminhado

Quando a saída é descartada por qualquer um desses motivos, exatamente um evento de progresso contendo o texto [adk: tool progress truncated] é emitido para essa chamada, então uma lacuna sempre fica visível em vez de silenciosa. Assim, um consumidor lento apenas torna a ferramenta mais lenta por um breve momento, mas nunca pode travá-la indefinidamente nem esgotar a memória.

Esses limites se aplicam apenas ao progresso. O resultado final de uma ferramenta não é afetado, então trunque resultados grandes dentro da ferramenta se isso for importante para você.

Veja o exemplo streaming_bash para uma UI web completa que renderiza saída bash ao vivo e resultados de ferramenta de uma vez só (read_file, grep, glob) a partir de um único feed de eventos. A ferramenta de streaming bash em si vive em adk-devtools.


Exemplos de Execução

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

Boas Práticas

  1. Descrições claras - Ajude o LLM a entender quando usar a ferramenta
  2. Valide entradas - Retorne mensagens de erro úteis para parâmetros ausentes
  3. Retorne JSON estruturado - Use nomes de campos claros
  4. Mantenha as ferramentas focadas - Cada ferramenta deve fazer uma coisa bem
  5. Use esquemas - Para ferramentas complexas, defina esquemas de parâmetros
  6. Marque ferramentas seguras de somente leitura - Defina tanto .with_read_only(true) quanto .with_concurrency_safe(true) para que o despacho Auto possa incluí-las em seu subconjunto concorrente

Metadados da Ferramenta: Somente Leitura e Concorrência

Marque as ferramentas como somente leitura ou seguras para concorrência para habilitar um despacho mais 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}))
});

Quando ToolExecutionStrategy::Auto está ativo, o loop de despacho primeiro executa as chamadas concorrentemente quando as ferramentas selecionadas retornam true tanto de is_read_only() quanto de is_concurrency_safe(). Em seguida, ele executa todas as chamadas restantes sequencialmente. ToolExecutionStrategy::Parallel é uma substituição explícita que contorna esses sinais, então seu chamador assume a segurança de concorrência.


SimpleToolContext: Usando Ferramentas Fora do Loop do Agente

Quando você precisa chamar uma ferramenta fora do loop do agente (testes, modo servidor MCP, delegação para subagente), use SimpleToolContext em vez de implementar a hierarquia completa da 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?;

Padrões: user_id()"anonymous", session_id() / branch()"", artifacts()None, search_memory() → empty vec. Tanto invocation_id quanto function_call_id são UUIDs gerados automaticamente. Defina uma sessão real com with_session_id(...) quando uma ferramenta encaminhar ou persistir estado por sessão.


StatefulTool: Estado Compartilhado Entre Invocações

Para ferramentas que precisam manter estado entre chamadas (contadores, caches, pools de conexão), use 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 o Arc<S> em cada invocação (aumento barato do contador de referências), então todas as execuções compartilham o mesmo estado subjacente. Ele suporta os mesmos métodos de construtor que FunctionTool: with_long_running, with_parameters_schema, with_response_schema, with_scopes, with_read_only e with_concurrency_safe.



Respostas de Função Multimodais

Os modelos Gemini 3 suportam receber imagens, áudio, PDFs e referências de arquivos em respostas de função — não apenas JSON. As ferramentas podem retornar dados multimodais incluindo arrays inline_data e/ou file_data em seu 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
        }]
    }))
}

O framework automaticamente:

  1. Detecta inline_data/file_data via FunctionResponseData::from_tool_result()
  2. Codifica dados binários inline em Base64
  3. Aninha as partes dentro do objeto de wire functionResponse (correspondendo ao formato Gemini 3 API)

Referências de Arquivo

Para arquivos grandes armazenados externamente, use file_data com um URI em vez de embutir 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"
    }]
}))

Construção Direta

Para código em nível de framework (agentes personalizados, camadas de conversão), construa FunctionResponseData diretamente:

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: Respostas de função multimodais exigem modelos da série Gemini 3 (gemini-3-flash-preview, gemini-3-pro-preview). Modelos anteriores retornam um erro 400.

Veja examples/multimodal_function_response/ para um exemplo completo e funcional.


Anterior: ← mistral.rs | Próximo: Ferramentas Integradas →