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:
- El modelo emite un único bloque de código delimitado (un script).
- 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. - El script devuelve un
ScriptOutputetiquetado: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 abreviaturastemperature/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 (funcionalidadskills). - Historial:
include_contents. - Herramientas:
tools estáticos ytoolsets por invocación;tool_timeout,default_retry_budget/tool_retry_budget,circuit_breaker_thresholdy alternativas deon_tool_error. - Autorización:
ToolConfirmationPolicy(require_tool_confirmation/require_tool_confirmation_for_all). - Transferencia:
sub_agents ydisallow_transfer_to_parent/disallow_transfer_to_peers. - Salida:
output_key,output_schema/output_typecon un bucle de corrección y reintento (output_max_retries). - Callbacks:
before_callback/after_callback,before_model_callback/after_model_callbackybefore_tool_callback/after_tool_callback/after_tool_callback_full. Los callbacks posteriores a las herramientas pueden inspeccionar los metadatos estructurados de ejecución medianteCallbackContext::tool_outcome(). - Controlado por características: protecciones de entrada/salida (
guardrails) y la canalizaciónEnhancedPlugin(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_strategyparalelo. - No existe una opción de construcción
skip_summarization: el modelo finaliza el bucle por sí mismo mediantefinal_result; sin embargo, una herramienta que establezcaskip_summarizationen 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 */ }
}
RunStepes un conjunto de variantes destruct:Call { call, stdout },Complete { value, stdout }yRaised { message, stdout }. Constrúyelas con los asistentesRunStep::call/RunStep::complete/RunStep::raisedy adjunta la salida capturada con.with_stdout(..).RunStep::Callmuestra exactamente una llamada pendiente; reanúdala con un valor o un error, odump()su continuación para suspenderla. Elstdoutque 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
PendingCallinforma de sus argumentos tal como los produjo el intérprete:positional_args()ykeyword_args()por separado. No asignes argumentos posicionales a nombres por tu cuenta: el controlador los vincula centralmente a los parámetros de la herramienta medianteadk_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 yrender_toolspuede 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).RuntimeErrorse 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_suspensiondebe sertruepara habilitar HITL y la delegación de larga duración;promptdescribe 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
/yjoinpath(),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.