CodeActAgent (CodeAct)

CodeActAgent es un equivalente de LlmAgent que actúa escribiendo y ejecutando código en lugar de emitir una llamada a herramienta cada vez. En cada turno, el modelo produce un único script; las herramientas se exponen como funciones invocables que el script puede combinar; y el script comunica su resultado devolviendo un valor etiquetado.

Este es el patrón CodeAct: en lugar de call tool A → observe → call tool B, el modelo escribe b(a(x)) en un único script, de modo que el trabajo de varios pasos ocurre en un solo turno. Está habilitado por la funcionalidad codeact de adk-agent.

Cuándo usarlo

  • Tareas que encadenan o combinan varias herramientas por turno (manipulación de datos, operaciones por lotes, lógica de integración).
  • Modelos sometidos a un entrenamiento posterior para la generación de código.
  • Flujos de trabajo en los que hay disponible un intérprete real (por ejemplo, Python) como base de ejecución de acciones.

Para la invocación nativa de herramientas, se recomienda LlmAgent. Para un entorno aislado de programación con archivos y shell, consulta el Agente de programación.

Cómo funciona el bucle

En cada turno:

  1. El modelo emite un único bloque de código delimitado (un script).
  2. El script se ejecuta en un [CodeRuntime]; las llamadas a herramientas llegan al anfitrión, que ejecuta la herramienta y reanuda el script con el resultado.
  3. El script devuelve un ScriptOutput etiquetado:
    • observation — se devuelve al modelo; el bucle continúa.
    • error — se devuelve como mensaje; el bucle continúa.
    • final_result — se devuelve a quien realizó la llamada; el bucle termina.
    • transfer_to_agent — transfiere el control a otro agente; el bucle termina.

El marco es independiente del lenguaje: el trait CodeRuntime es el punto de integración del intérprete paso a paso, y comunica su propio lenguaje y entorno al modelo mediante una instrucción libre. El adaptador de producción previsto envuelve Monty, un intérprete de Python nativo de Rust.

Durabilidad: suspender y reanudar

CodeActAgent no tiene estado entre invocaciones: el estado persistente vive en la sesión, exactamente igual que LlmAgent. Hay dos situaciones que suspenden la ejecución:

  • una herramienta cuya confirmación es obligatoria y que aún no tiene una decisión (HITL), y
  • una herramienta de larga duración cuyo resultado llega fuera de banda.

Al suspenderse, la continuación del intérprete en ejecución se serializa en un CodeActCheckpoint y se escribe en el estado de la sesión; el siguiente run() lo lee y reanuda la ejecución: la decisión de confirmación llega mediante RunConfig::tool_confirmation_decisions, y el resultado de una operación de larga duración llega como un FunctionResponse en el mensaje siguiente. Las llamadas a herramientas en línea están delimitadas por puntos de control de escritura anticipada (SAVE-BEFORE) y SAVE-AFTER: una vez que se persiste el punto de control SAVE-AFTER, la recuperación reanuda la ejecución con el resultado almacenado y nunca vuelve a ejecutar la herramienta. Un fallo en el breve intervalo posterior al efecto secundario de una herramienta, pero anterior a que se registre su punto de control SAVE-AFTER, hará que la herramienta se vuelva a ejecutar durante la recuperación, por lo que las herramientas que no sean idempotentes deben protegerse contra ello (el mismo límite de ejecución al menos una vez que LlmAgent).

Esto requiere un entorno de ejecución capaz de tomar una instantánea de una llamada pausada. Un entorno de ejecución que no pueda hacerlo ejecuta las herramientas de larga duración en línea y rechaza las pausas de confirmación.

Creación de un CodeActAgent

use adk_agent::codeact::CodeActAgent;
use std::sync::Arc;

// `model` implements `adk_core::Llm`; `runtime` implements `CodeRuntime`.
let agent = CodeActAgent::builder()
    .name("analyst")
    .model(model)
    .runtime(runtime)
    .instruction("Prefer concise, composable steps.")
    .tool(Arc::new(load_csv_tool))
    .output_key("report")
    .build()?;

Se requieren model y runtime; todo lo demás tiene un valor predeterminado.

Paridad con LlmAgent

El constructor refleja LlmAgentBuilder:

  • Modelo: generate_content_config, además de las abreviaturas temperature/top_p/top_k/ max_output_tokens.
  • Instrucciones: instruction/instruction_provider, global_instruction/global_instruction_provider, con inyección de plantillas de {state.key}; además de habilidades (funcionalidad skills).
  • Historial: include_contents.
  • Herramientas: tools estáticos y toolsets por invocación; tool_timeout, default_retry_budget/tool_retry_budget, circuit_breaker_threshold y alternativas de on_tool_error.
  • Autorización: ToolConfirmationPolicy (require_tool_confirmation/require_tool_confirmation_for_all).
  • Transferencia: sub_agents y disallow_transfer_to_parent/ disallow_transfer_to_peers.
  • Salida: output_key, output_schema/output_type con un bucle de corrección y reintento (output_max_retries).
  • Callbacks: before_callback/after_callback, before_model_callback/after_model_callback y before_tool_callback/after_tool_callback/after_tool_callback_full. Los callbacks posteriores a las herramientas pueden inspeccionar los metadatos estructurados de ejecución mediante CallbackContext::tool_outcome().
  • Controlado por características: protecciones de entrada/salida (guardrails) y la canalización EnhancedPlugin (enhanced-plugins).

Cada llamada a una herramienta obtiene un ToolContext nuevo que transporta el id de llamada del intérprete y delega los artefactos, la memoria, el estado compartido, los ámbitos de usuario y los secretos a la invocación activa, por lo que una herramienta se comporta de forma idéntica bajo CodeActAgent o LlmAgent.

Diferencias deliberadas

  • El aislamiento de la ejecución de código es responsabilidad de CodeRuntime, no un complemento añadido.
  • El envío de herramientas es secuencial por diseño (una única continuación se captura en un límite de llamada), por lo que no existe tool_execution_strategy paralelo.
  • No existe una opción de construcción skip_summarization: el modelo finaliza el bucle por sí mismo mediante final_result; sin embargo, una herramienta que establezca skip_summarization en sus acciones también finaliza la ejecución.

Ejemplo

Una demostración ejecutable de extremo a extremo y sin dependencias —un CodeRuntime autónomo junto con un modelo determinista— se encuentra en examples/codeact_agent:

cargo run --manifest-path examples/codeact_agent/Cargo.toml

Implementación de un CodeRuntime

Un CodeRuntime analiza y ejecuta paso a paso un script, mostrando una llamada externa cada vez:

pub trait CodeRuntime: Send + Sync {
    fn start(&self, script: &str, script_name: &str) -> Result<RunStep, RuntimeError>;
    fn resume(&self, snapshot: &[u8], with: ResumeWith) -> Result<RunStep, RuntimeError>;
    fn capabilities(&self) -> RuntimeCapabilities { /* default */ }
    fn render_tools(&self, tools: &[Arc<dyn Tool>]) -> String { /* default */ }
}
  • RunStep es un conjunto de variantes de struct: Call { call, stdout }, Complete { value, stdout } y Raised { message, stdout }. Constrúyelas con los asistentes RunStep::call / RunStep::complete / RunStep::raised y adjunta la salida capturada con .with_stdout(..). RunStep::Call muestra exactamente una llamada pendiente; reanúdala con un valor o un error, o dump() su continuación para suspenderla. El stdout que adjunta un entorno de ejecución se muestra de nuevo al modelo y se conserva en los puntos de control, por lo que sobrevive a la suspensión y reanudación.
  • Un PendingCall informa de sus argumentos tal como los produjo el intérprete: positional_args() y keyword_args() por separado. No asignes argumentos posicionales a nombres por tu cuenta: el controlador los vincula centralmente a los parámetros de la herramienta mediante adk_agent::codeact::bind_call_args, por lo que un entorno de ejecución no necesita ningún esquema de herramienta en el límite de la llamada y render_tools puede ser una función pura del segmento de herramientas.
  • Errores del script frente a errores del host. Todo aquello que el modelo podría corregir escribiendo un código diferente —un error de sintaxis/análisis, una excepción no capturada o una cancelación por límite de recursos— es un RunStep::Raised (una cadena opaca que se devuelve al modelo literalmente). RuntimeError se reserva para fallos reales del host (serialización/deserialización de instantáneas, errores internos del intérprete) y aborta la ejecución.
  • RuntimeCapabilities::supports_suspension debe ser true para habilitar HITL y la delegación de larga duración; prompt describe el lenguaje y el entorno al modelo.

Consulta examples/codeact_agent/src/runtime.rs para ver una implementación completa y mínima que admita la suspensión y reanudación.

Python mediante Monty

El adaptador de producción previsto es adk-codeact-monty, un CodeRuntime respaldado por Pydantic Monty. Permite que el modelo actúe escribiendo Python, se ejecuta en el mismo proceso sin contenedor ni subproceso y toma instantáneas de una ejecución pausada en bytes — exactamente lo que necesita la suspensión/reanudación. El intérprete de Monty se consume a través del kernel embedded-python de adk-code, que fija los crates monty en un solo lugar; se requiere rustc 1.95+.

[dependencies]
adk-agent = { version = "2.1.0", features = ["codeact"] }
adk-codeact-monty = "2.1.0"

O mediante el crate paraguas (reexportado como adk_rust::codeact_monty):

[dependencies]
adk-rust = { version = "2.1.0", features = ["minimal", "codeact-monty"] }
use adk_codeact_monty::MontyRuntime;

// Conservative default resource limits (per-advance time + memory caps) make
// `new()` safe for untrusted, LLM-generated code.
let runtime = Arc::new(MontyRuntime::new());

// Tighten or relax with the builder; `unlimited()` removes the caps for
// trusted scripts only.
let runtime = Arc::new(
    MontyRuntime::builder()
        .max_duration(std::time::Duration::from_secs(2))
        .max_memory(64 * 1024 * 1024)
        .build(),
);

Acceso al sistema operativo

Los efectos del sistema operativo que intenta un script — lecturas/escrituras del sistema de archivos, os.getenv/os.environ y date.today()/datetime.now() — se gestionan en el mismo lugar conforme a una política controlada por el host. No son herramientas y nunca pausan el bucle del agente. De forma predeterminada, un runtime está completamente aislado (sin acceso al sistema de archivos, un entorno vacío y el reloj del host habilitado). Concede acceso específico con el builder:

use adk_codeact_monty::{MontyRuntime, PathAccess};

let runtime = Arc::new(
    MontyRuntime::builder()
        // Mount host directories at virtual paths; Monty enforces the boundary
        // (canonicalization + symlink-escape detection) so a script can never
        // escape a mount. Reads/writes outside every mount raise PermissionError.
        .allow_path("/data", "/srv/agent/data", PathAccess::ReadOnly)
        .allow_path("/out", "/srv/agent/out", PathAccess::ReadWrite)
        // Expose an explicit environment map to os.getenv / os.environ. Empty by
        // default — the host process environment is never exposed implicitly.
        .environ_var("PROJECT", "acme")
        // date.today() / datetime.now() read the host clock (enabled by default).
        .system_clock(true)
        .build(),
);

El acceso a la red y a subprocesos no tiene una superficie de llamadas al sistema operativo de Monty y permanece no disponible independientemente de la política. El acceso concedido se describe al modelo en el prompt del sistema, por lo que sabe qué rutas puede leer o escribir y qué variables de entorno existen.

Monty implementa solo un subconjunto de pathlib.Path, por lo que, cuando se montan rutas, el prompt enumera los métodos compatibles exactos (cualquier otro genera AttributeError):

  • Lectura/consulta (cualquier montaje): exists(), is_file(), is_dir(), is_symlink(), read_text(), read_bytes(), stat(), iterdir(), resolve(), absolute(), open("r").
  • Escritura (solo montajes de lectura y escritura): write_text(), write_bytes(), append_text(), append_bytes(), mkdir(), unlink(), rmdir(), rename(), open("w")/open("a").
  • Operaciones de ruta puras (sin E/S): el operador / y joinpath(), is_absolute(), with_name(), with_stem(), with_suffix(), as_posix(), y las propiedades .name, .parent, .stem, .suffix, .suffixes, .parts.

Las herramientas se invocan mediante una única función integrada, call_tool("name", {"arg": value, ...}) — la única forma de llamar a una herramienta; nunca están disponibles como invocables independientes. El nombre de la herramienta es un literal de cadena y cada argumento es una entrada con clave de cadena en un único diccionario, por lo que el nombre real viaja dentro de la continuación serializada (sobrevive a la suspensión/reanudación sin una tabla de nombres en el host), una herramienta y cada argumento pueden tener cualquier nombre (no necesariamente un identificador válido de Python como "fetch-cart", una palabra clave de Python o incluso "call_tool"), y el controlador vincula las entradas del diccionario exactamente por nombre, sin inferencia posicional. Cada herramienta aparece en el mensaje como una línea de uso de call_tool("name", {...}) con sus parámetros y descripción. Cualquier otra forma — un fetch_cart(...) independiente, argumentos con nombre, un argumento que no sea un diccionario o una clave que no sea una cadena— se rechaza con un error correctivo en lugar de enviarse silenciosamente, de modo que el modelo tiene exactamente una forma de llamada que aprender.

El examples/codeact_monty_agent ejecutable ejecuta un CodeActAgent contra Python real completamente sin conexión.