Ferramentas em Sessões Realtime

A característica definidora de um agent realtime (em vez de um bot de voz) é que ele pode realizar ações reais no meio da conversa: consultar algo, processar um reembolso, encaminhar para um humano — e então falar o resultado. As ferramentas executam no lado do servidor, então sua lógica de negócio e credenciais nunca tocam o cliente.

Como o fluxo de uma ferramenta funciona

  1. O modelo decide que precisa de uma ferramenta e emite FunctionCallDone { name, arguments, call_id }.
  2. RealtimeRunner procura o handler para name e o executa.
  3. O resultado JSON do handler é enviado de volta ao modelo como a saída da ferramenta.
  4. O runner dispara uma resposta de acompanhamento; o modelo fala a resposta, fundamentada no resultado.

Você nunca chama create_response() para isso — o runner cuida da ida e volta quando auto_respond_tools está ativado (o padrão).

Ferramentas nativas: ToolDefinition + FnToolHandler

O caminho leve. Uma ToolDefinition é o schema JSON que o modelo vê; um FnToolHandler é um closure síncrono que é executado quando chamado.

use adk_realtime::config::ToolDefinition;
use adk_realtime::events::ToolCall;
use adk_realtime::runner::FnToolHandler;
use serde_json::json;

fn process_refund_def() -> ToolDefinition {
    ToolDefinition {
        name: "process_refund".into(),
        description: Some("Issue a refund for an order. Only when clearly warranted.".into()),
        parameters: Some(json!({
            "type": "object",
            "properties": {
                "order_id": { "type": "string", "description": "e.g. 'A-10293'" },
                "reason":   { "type": "string", "description": "Short reason" }
            },
            "required": ["order_id", "reason"]
        })),
    }
}

fn process_refund_tool()
-> FnToolHandler<impl Fn(&ToolCall) -> adk_realtime::error::Result<serde_json::Value> + Send + Sync> {
    FnToolHandler::new(|call: &ToolCall| {
        let order = call.arguments.get("order_id").and_then(|v| v.as_str()).unwrap_or("unknown");
        // …do the work…
        Ok(json!({ "status": "approved", "order_id": order,
                   "message": format!("Refund approved for {order}.") }))
    })
}

Registre isso no builder com .tool(definition, handler):

let runner = IntegratedRealtimeRunner::builder()
    .model(model)
    .config(config)
    .identity("support", "customer", &session_id)
    .session_service(sessions)
    .tool(process_refund_def(), process_refund_tool())
    .tool(connect_to_human_def(), connect_to_human_tool())
    .build()?;

O handler retorna um serde_json::Value; tudo o que você retornar é o que o modelo vê, então inclua um message legível para humanos que o agent possa parafrasear.

Handlers rodam no lado do servidor e de forma síncrona dentro do event loop. Mantenha-os rápidos; para trabalho lento, retorne um status "started" e faça o acompanhamento fora da banda.

Ferramentas conectadas: qualquer adk_core::Tool

Se você já tem ferramentas adk-core (seus próprios FunctionTools, ou adk-tool embutidos como o knowledge-graph remember/relate), anexe-os com .adk_tool(...) — sem reescrita. A camada de integração envolve cada um em um ToolHandler e sintetiza um ToolContext limitado ao (app_name, user_id, session_id) da sessão:

use adk_tool::{RememberTool, RelateTool};

let runner = IntegratedRealtimeRunner::builder()
    .model(model).config(config).identity("app", "user", &sid)
    .memory_service(kg.clone())
    .adk_tool(Arc::new(RememberTool::new(kg.clone())))   // adk_core::Tool
    .adk_tool(Arc::new(RelateTool::new(kg)))
    .tool(get_weather_def(), get_weather())              // native handler — mix freely
    .build()?;

É assim que o agent organiza sua própria memory. A ponte funciona bem para ferramentas executadas localmente e independentes de contexto; ferramentas que precisam de estado rico do agent são melhor escritas como FnToolHandlers nativas.

Chamadas paralelas de ferramentas

Um modelo pode solicitar várias ferramentas em uma resposta (por exemplo, "qual é o clima e a hora em Londres?"). ADK-Rust lida corretamente com isso: ele envia a saída de cada ferramenta conforme ela é concluída, e então emite exatamente uma response.create quando a resposta de despacho termina.

Isso importa porque a abordagem ingênua — disparar uma resposta por ferramenta — aciona o erro de OpenAI "conversation already has an active response in progress" e trava a sessão. O runner evita isso ao separar "enviar saída da ferramenta" (send_tool_output) de "disparar a resposta" (respond_after_tools, chamado uma vez na ResponseDone de despacho). Você recebe isso gratuitamente; só tenha em mente ao ler eventos que uma passagem por ferramenta abrange duas respostas (veja Architecture).

Lendo eventos de ferramentas em uma UI

Para exibir a atividade da ferramenta (por exemplo, um chip "Processando reembolso…"), observe FunctionCallDone:

ServerEvent::FunctionCallDone { name, arguments, .. } => {
    // `arguments` is a JSON string of the call args
    ui_show_tool_activity(&name, &arguments);
}

A confirmação falada chega depois como TranscriptDelta, uma vez que o resultado da ferramenta é incorporado à resposta de acompanhamento.

Veja funcionando

O exemplo customer_service conecta process_refund e connect_to_human; o exemplo realtime_tools é uma verificação sem interface que exercita turnos de ferramenta única, ferramentas paralelas e calculadora em ambos os providers.

Próximo: Multimodal →

Quais ferramentas são governadas

IntegratedRealtimeRunner roteia chamadas de ferramenta pelo modo como a ferramenta foi registrada:

Registrado comoDespachoPolítica aplicada
adk_tool(...) — um ADK ToolO pipeline de política de integraçãoPlugins configurados, gravação de transcrição, persistência de eventos de ferramenta
Um manipulador realtime nativoRealtimeRunner despachoNenhuma — o manipulador é confiável por construção

Uma ADK tool anteriormente alcançava o provider por meio de um ToolBridgeAdapter, que cria um contexto e chama Tool::execute sem plugins, callbacks ou confirmação. Portanto, uma tool regida no loop padrão de agent era executada sem governança em tempo real. O bypass do native-handler agora é a exceção explícita, e não o padrão para tudo.

Falhas de plugin falham fechado

Se o pipeline before_tool_call retornar um erro, a tool é recusada:

{ "error": "tool guarded was refused: its before-tool plugin pipeline failed (...). Execution is refused rather than proceeding without policy." }

Importante: anteriormente, este caminho registrava o erro do plugin como não fatal e então executava a tool. Autorização, redaction e policy vivem em plugins before-tool, então uma guard quebrada virava nenhuma guard.

Erros de plugin after-tool deixam o próprio resultado da tool em vigor, já que a tool já foi executada.

Callbacks da tool no agent direto

RealtimeAgent aplica callbacks before-tool e after-tool com o mesmo contrato do loop padrão de agent:

Retorno do callbackEfeito
Ok(None)A tool é executada
Ok(Some(content)) de um callback beforeO conteúdo se torna o resultado; a tool não é executada
Err(e) de um callback beforeO erro se torna o resultado, a tool não é executada e os callbacks after são ignorados
Ok(Some(content)) de um callback afterO conteúdo substitui o resultado da tool
Err(e) de um callback afterO erro substitui o resultado da tool

O Content de um callback é convertido no resultado JSON que o provedor espera: uma parte FunctionResponse contribui com seu payload, qualquer outra coisa contribui com seu texto sob uma chave result.

Importante: antes que este contrato fosse respeitado, a decisão de um before-callback era calculada e descartada, então a tool era executada de qualquer forma — uma gate que relatava uma negação sem aplicá-la. Os resultados de after-callback, incluindo erros, eram descartados.

Contexto da tool em tempo real

Uma tool invocada a partir de RealtimeAgent vê as mesmas capacidades que vê sob um Runner:

CapacidadeFonte
user_scopes()O contexto da invocação pai
get_secret(name)O contexto da invocação pai
shared_state()O contexto de invocação pai
search_memory(query)O serviço de memória do pai
Identidade (app_name, user_id, session_id, branch)O contexto de invocação pai

Nota: antes, isso caía nos padrões do trait — uma lista de escopos vazia, None para segredos, e None para estado compartilhado — então uma ferramenta que verificasse escopos ou segredos se comportava de forma diferente em tempo real do que sob um Runner, e não conseguia distinguir um chamador não autenticado de um contexto que simplesmente deixou de repassar os escopos.

Concorrência de ferramentas

RunnerConfig::max_concurrent_tools (padrão 4) limita quantos manipuladores de ferramentas executam ao mesmo tempo. Quando uma resposta despacha várias chamadas, o runner coloca cada uma em sua fila de eventos e a admite para execução à medida que uma permissão é liberada:

use adk_realtime::{RealtimeRunner, RunnerConfig};

let runner = RealtimeRunner::builder()
    .model(model)
    .runner_config(RunnerConfig {
        auto_execute_tools: true,
        auto_respond_tools: true,
        max_concurrent_tools: 3,
    })
    .build()?;

Duas propriedades decorrem disso, e ambas são cobertas por testes:

  • A entrada de eventos continua durante a execução da ferramenta. Deltas de áudio, transcrições e interrupções são tratados enquanto as ferramentas executam. Um manipulador que espera algo que chegue mais tarde na sessão não trava mais a sessão.
  • Uma única resposta de acompanhamento, depois da última saída. Quando a saída da ferramenta é enviada automaticamente, o modelo recebe uma única create_response. Ela é emitida uma vez que tanto a resposta que fez o despacho tenha sido encerrada quanto cada ferramenta despachada tenha informado — em qualquer ordem, já que uma resposta agora pode ser encerrada enquanto as ferramentas ainda estão em execução.

Importante: o limite governa concorrência, não paralelismo. Os manipuladores compartilham a tarefa do runner, então um manipulador que bloqueia a thread — E/S síncrona de arquivo ou rede, computação pesada — ainda trava o loop. Use tokio::task::spawn_blocking para isso.

Política de desconexão

O runner não reconecta automaticamente. Em caso de perda de transporte, ele deixa as ferramentas despachadas terminarem, chama EventHandler::on_disconnect, e retorna de run:

use adk_realtime::{EventHandler, Result};

struct Reconnecting;

#[async_trait::async_trait]
impl EventHandler for Reconnecting {
    async fn on_disconnect(&self) -> Result<()> {
        tracing::warn!("realtime transport ended");
        Ok(())
    }
}

A reconexão fica a cargo do chamador porque exige decidir qual contexto deve ser reproduzido e, no Gemini, se um token de retomada armazenado ainda é válido. O hook on_disconnect existe para que a perda de transporte possa ser distinguida de uma close graciosa — run retorna Ok(()) para ambas.