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
- O modelo decide que precisa de uma ferramenta e emite
FunctionCallDone { name, arguments, call_id }. RealtimeRunnerprocura o handler paranamee o executa.- O resultado JSON do handler é enviado de volta ao modelo como a saída da ferramenta.
- 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 como | Despacho | Política aplicada |
|---|---|---|
adk_tool(...) — um ADK Tool | O pipeline de política de integração | Plugins configurados, gravação de transcrição, persistência de eventos de ferramenta |
| Um manipulador realtime nativo | RealtimeRunner despacho | Nenhuma — 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 callback | Efeito |
|---|---|
Ok(None) | A tool é executada |
Ok(Some(content)) de um callback before | O conteúdo se torna o resultado; a tool não é executada |
Err(e) de um callback before | O erro se torna o resultado, a tool não é executada e os callbacks after são ignorados |
Ok(Some(content)) de um callback after | O conteúdo substitui o resultado da tool |
Err(e) de um callback after | O 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:
| Capacidade | Fonte |
|---|---|
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,
Nonepara segredos, eNonepara 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_blockingpara 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.