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

  1. El modelo decide que necesita una herramienta y emite FunctionCallDone { name, arguments, call_id }.
  2. RealtimeRunner busca el manejador de name y lo ejecuta.
  3. El resultado de JSON del manejador se devuelve al modelo como la salida de la herramienta.
  4. 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 comoDespachoPolítica aplicada
adk_tool(...) — un ADK ToolLa canalización de políticas de integraciónPlugins configurados, grabación de transcripción, persistencia de eventos de herramienta
Un controlador nativo en tiempo realRealtimeRunner despachoNinguna — 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 callbackEfecto
Ok(None)La herramienta se ejecuta
Ok(Some(content)) desde un callback beforeEl contenido se convierte en el resultado; la herramienta no se ejecuta
Err(e) desde una devolución de llamada beforeEl 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 afterEl contenido reemplaza el resultado de la herramienta
Err(e) desde una devolución de llamada afterEl 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:

CapacidadFuente
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, None para secretos, y None para 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_blocking para 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.