Herramientas en sesiones en tiempo real
La característica definitoria de un agent en tiempo real (frente a un bot de voz) es que puede realizar acciones reales en medio de la conversación: buscar algo, procesar un reembolso, derivar a una persona — y luego decir el resultado. Las herramientas se ejecutan del lado del servidor, así que tu lógica de negocio y tus credenciales nunca tocan el cliente.
Cómo fluye un turno de herramienta
- El modelo decide que necesita una herramienta y emite
FunctionCallDone { name, arguments, call_id }. RealtimeRunnerbusca el manejador denamey lo ejecuta.- El resultado de JSON del manejador se devuelve al modelo como la salida de la herramienta.
- El runner activa una respuesta de seguimiento; el modelo dice la respuesta, fundamentada en el resultado.
Nunca llamas a create_response() para esto — el runner maneja el ida y vuelta
cuando auto_respond_tools está activado (el valor predeterminado).
Herramientas nativas: ToolDefinition + FnToolHandler
La ruta ligera. Una ToolDefinition es el esquema JSON que el modelo ve; un
FnToolHandler es un cierre síncrono que se ejecuta cuando se invoca.
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}.") }))
})
}
Regístralo en el builder con .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()?;
El manejador devuelve un serde_json::Value; lo que sea que devuelvas es lo que el modelo
ve, así que incluye una message legible por humanos que el agent pueda parafrasear.
Los manejadores se ejecutan del lado del servidor y de forma síncrona dentro del bucle de eventos. Mantenlos rápidos; para trabajo lento, devuelve un estado de "started" y haz seguimiento fuera de banda.
Herramientas puenteadas: cualquier adk_core::Tool
Si ya tienes herramientas adk-core (tus propios FunctionTool, o adk-tool
integrados como el remember/relate del grafo de conocimiento), adjúntalos con
.adk_tool(...) — sin reescritura. La capa de integración envuelve cada uno en un
ToolHandler y sintetiza un ToolContext limitado al (app_name, user_id, session_id) de la sesión:
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()?;
Así es como el agent cura su propia memoria. El puente funciona bien con
herramientas ejecutadas localmente e independientes del contexto; las herramientas que necesitan un estado rico del agent
se escriben mejor como FnToolHandler nativos.
Llamadas paralelas a herramientas
Un modelo puede solicitar varias herramientas en una respuesta (por ejemplo, "¿cuál es el clima
y la hora en Londres?"). ADK-Rust maneja esto correctamente: envía la salida de cada herramienta a medida que
se completa, y luego emite exactamente una response.create una vez que termina la
respuesta de dispatch.
Esto importa porque el enfoque ingenuo — emitir una respuesta por herramienta — provoca el error
OpenAI de "conversation already has an active response in progress" y
bloquea la sesión. El runner lo evita separando "enviar salida de herramienta"
(send_tool_output) de "activar la respuesta" (respond_after_tools, llamada
una vez en el ResponseDone de dispatch). Obtienes esto gratis; solo ten en cuenta al
leer eventos que un turno de herramienta abarca dos respuestas
(consulta Arquitectura).
Leer eventos de herramientas en una UI
Para mostrar la actividad de la herramienta (por ejemplo, un chip de "Procesando reembolso…"), observa
FunctionCallDone:
ServerEvent::FunctionCallDone { name, arguments, .. } => {
// `arguments` is a JSON string of the call args
ui_show_tool_activity(&name, &arguments);
}
La confirmación hablada llega después como TranscriptDelta una vez que el resultado de la herramienta
se incorpora a la respuesta de seguimiento.
Pruébalo
El ejemplo customer_service conecta
process_refund y connect_to_human; el ejemplo realtime_tools
es una prueba sin interfaz que ejercita turnos de una sola herramienta, de herramientas en paralelo y
de calculadora en ambos proveedores.
Siguiente: Multimodal →
Qué herramientas están gobernadas
IntegratedRealtimeRunner enruta las llamadas a herramientas según cómo se registró la herramienta:
| Registrado como | Despacho | Política aplicada |
|---|---|---|
adk_tool(...) — un ADK Tool | La canalización de políticas de integración | Plugins configurados, grabación de transcripción, persistencia de eventos de herramienta |
| Un controlador nativo en tiempo real | RealtimeRunner despacho | Ninguna — el controlador es de confianza por construcción |
Una herramienta ADK antes llegaba al proveedor a través de un ToolBridgeAdapter, que crea un
contexto y llama a Tool::execute sin plugins, callbacks ni confirmación. Por lo tanto, una herramienta regida
en el bucle estándar del agente se ejecutaba sin control en tiempo real. El bypass del controlador nativo ahora es
la excepción explícita en lugar del valor predeterminado para todo.
Los fallos de los plugins fallan de forma cerrada
Si la canalización de before_tool_call devuelve un error, la herramienta es rechazada:
{ "error": "tool guarded was refused: its before-tool plugin pipeline failed (...). Execution is refused rather than proceeding without policy." }
Importante: antes esta ruta registraba el error del plugin como no fatal y luego ejecutaba la herramienta. La autorización, la redacción y la política residen en los plugins antes de la herramienta, así que un guardián roto se convertía en ningún guardián.
Los errores de los plugins después de la herramienta dejan en su lugar el resultado propio de la herramienta, ya que la herramienta ya se ha ejecutado.
Callbacks de herramientas en el agente directo
RealtimeAgent aplica callbacks antes y después de la herramienta con el mismo contrato que el
bucle estándar del agente:
| Valor devuelto por el callback | Efecto |
|---|---|
Ok(None) | La herramienta se ejecuta |
Ok(Some(content)) desde un callback before | El contenido se convierte en el resultado; la herramienta no se ejecuta |
Err(e) desde una devolución de llamada before | El error se convierte en el resultado, la herramienta no se ejecuta y se omiten las devoluciones de llamada after |
Ok(Some(content)) desde una devolución de llamada after | El contenido reemplaza el resultado de la herramienta |
Err(e) desde una devolución de llamada after | El error reemplaza el resultado de la herramienta |
La Content de un callback se convierte en el resultado de JSON que el proveedor espera: una parte FunctionResponse aporta su carga útil, cualquier otra aporta su texto bajo una clave result.
Importante: antes de que este contrato fuera respetado, la decisión de un before-callback se calculaba y se descartaba, por lo que la herramienta se ejecutaba de todos modos — una puerta que informaba una denegación sin hacerla efectiva. Los resultados de los after-callback, incluidos los errores, se descartaban.
Contexto de herramienta en tiempo real
Una herramienta invocada desde RealtimeAgent ve las mismas capacidades que ve bajo una Runner:
| Capacidad | Fuente |
|---|---|
user_scopes() | El contexto de invocación padre |
get_secret(name) | El contexto de invocación padre |
shared_state() | El contexto de invocación del padre |
search_memory(query) | El servicio de memoria del padre |
Identity (app_name, user_id, session_id, branch) | El contexto de invocación del padre |
Nota: antes estas situaciones se deslizaban hasta los valores predeterminados del trait — una lista de ámbitos vacía,
Nonepara secretos, yNonepara estado compartido — así que una herramienta de comprobación de ámbitos o secretos se comportaba de forma distinta en tiempo real que bajo un Runner, y no podía distinguir a un llamador no autenticado de un contexto que simplemente no pasó los ámbitos.
Concurrencia de herramientas
RunnerConfig::max_concurrent_tools (predeterminado 4) limita cuántos controladores de herramientas se ejecutan a
la vez. Cuando una respuesta despacha varias llamadas, el runner las encola a cada una en
su bucle de eventos y las admite a ejecución a medida que se libera un permiso:
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()?;
De esto se derivan dos propiedades, y ambas están cubiertas por pruebas:
- La entrada de eventos continúa durante la ejecución de herramientas. Los deltas de audio, las transcripciones y las interrupciones se gestionan mientras las herramientas se ejecutan. Un controlador que espera a algo que llegue más tarde en la sesión ya no bloquea la sesión.
- Una sola respuesta de seguimiento, después de la última salida. Cuando la salida de la herramienta se envía
automáticamente, al modelo se le debe una única
create_response. Se emite una vez que tanto la respuesta que despacha se ha cerrado como cada herramienta despachada ha informado — en cualquiera de los órdenes, ya que ahora una respuesta puede cerrarse mientras las herramientas siguen ejecutándose.
Importante: el límite regula la concurrencia, no el paralelismo. Los controladores comparten la tarea del runner, así que un controlador que bloquea el hilo — E/S síncrona de archivos o red, computación pesada — sigue deteniendo el bucle. Usa
tokio::task::spawn_blockingpara esos casos.
Política de desconexión
El runner no se reconecta automáticamente. Al perder el transporte, deja que las herramientas despachadas
terminen, llama a EventHandler::on_disconnect, y 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(())
}
}
La reconexión queda en manos del llamador porque requiere decidir qué contexto reproducir
y, en Gemini, si un token de reanudación almacenado sigue siendo válido. El hook de
on_disconnect existe para que la pérdida de transporte pueda distinguirse de una close
graciosa — run devuelve Ok(()) para ambas.