CodeActAgent (CodeAct)
CodeActAgent é uma alternativa a LlmAgent que atua escrevendo e
executando código em vez de emitir uma chamada de ferramenta por vez. A cada turno, o modelo
produz um único script; as ferramentas são expostas como funções chamáveis que o script pode
compor; e o script comunica seu resultado retornando um valor identificado.
Este é o padrão CodeAct: em vez de call tool A → observe → call tool B,
o modelo escreve b(a(x)) em um único script, de modo que o trabalho em várias etapas ocorra em um único
turno. Ele é habilitado pelo recurso codeact em adk-agent.
Quando usá-lo
- Tarefas que encadeiam ou combinam várias ferramentas por turno (tratamento de dados, operações em lote, lógica de integração).
- Modelos treinados posteriormente para geração de código.
- Fluxos de trabalho em que um interpretador real (por exemplo, Python) está disponível como substrato de ação.
Para chamadas nativas de ferramentas, prefira LlmAgent. Para um
ambiente isolado de execução para codificação com arquivos e shell, consulte o Agente de
Codificação.
Como o loop funciona
A cada turno:
- O modelo emite um bloco de código delimitado (um script).
- O script é executado em um [
CodeRuntime]; as chamadas de ferramentas são encaminhadas ao host, que executa a ferramenta e retoma o script com o resultado. - O script retorna um
ScriptOutputidentificado:observation— enviado de volta ao modelo; o loop continua.error— enviado de volta como uma mensagem; o loop continua.final_result— retornado ao chamador; o loop termina.transfer_to_agent— transfere o controle para outro agente; o loop termina.
O framework é independente de linguagem: o trait CodeRuntime é a interface do interpretador
passo a passo, e ele informa sua própria linguagem/ambiente ao modelo por meio de um prompt
de texto livre. O adaptador de produção pretendido encapsula
Monty, um interpretador Python nativo em Rust.
Durabilidade: suspender e retomar
CodeActAgent não mantém estado entre invocações — o estado durável reside na
sessão, exatamente como em LlmAgent. Duas situações suspendem a execução:
- uma ferramenta condicionada à confirmação, ainda sem decisão (HITL); e
- uma ferramenta de execução longa cujo resultado chega fora de banda.
Ao suspender, a continuação do interpretador em execução é serializada em um
CodeActCheckpoint e gravada no estado da sessão; o próximo run() a lê
novamente e retoma a execução — a decisão de confirmação chega por meio de
RunConfig::tool_confirmation_decisions, e um resultado de execução longa chega como um
FunctionResponse na próxima mensagem. As chamadas de ferramentas embutidas são
delimitadas por pontos de verificação de gravação antecipada (SAVE-BEFORE) e
SAVE-AFTER: depois que o ponto de verificação SAVE-AFTER é persistido, a
recuperação retoma a execução com o resultado armazenado e nunca executa
novamente a ferramenta. Uma falha na pequena janela após o efeito colateral de
uma ferramenta, mas antes que seu ponto de verificação SAVE-AFTER seja
registrado, fará com que a ferramenta seja executada novamente durante a
recuperação; portanto, ferramentas que não são idempotentes devem se proteger
contra isso (o mesmo limite de entrega pelo menos uma vez de LlmAgent).
Isso exige um ambiente de execução capaz de criar um instantâneo de uma chamada pausada. Um ambiente de execução que não seja capaz disso executa ferramentas de execução longa em linha e rejeita pausas para confirmação.
Criando um 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()?;
model e runtime são obrigatórios; todo o restante tem um valor padrão.
Paridade com LlmAgent
O construtor espelha LlmAgentBuilder:
- Modelo:
generate_content_config, além dos atalhostemperature/top_p/top_k/max_output_tokens. - Instruções:
instruction/instruction_provider,global_instruction/global_instruction_provider, com injeção de template{state.key}; além de habilidades (recursoskills). - Histórico:
include_contents. - Ferramentas:
tools estáticos etoolsets por invocação;tool_timeout,default_retry_budget/tool_retry_budget,circuit_breaker_thresholde alternativason_tool_error. - Autorização:
ToolConfirmationPolicy(require_tool_confirmation/require_tool_confirmation_for_all). - Transferência:
sub_agents edisallow_transfer_to_parent/disallow_transfer_to_peers. - Saída:
output_key,output_schema/output_typecom um loop de correção e nova tentativa (output_max_retries). - Retornos de chamada:
before_callback/after_callback,before_model_callback/after_model_callbackebefore_tool_callback/after_tool_callback/after_tool_callback_full. Os retornos de chamada após a ferramenta podem inspecionar metadados estruturados de execução por meio deCallbackContext::tool_outcome(). - Condicionado a recursos: proteções de entrada/saída (
guardrails) e o pipelineEnhancedPlugin(enhanced-plugins).
Cada chamada de ferramenta recebe um novo ToolContext que carrega o id da chamada do interpretador
e delega artefatos, memória, estado compartilhado, escopos do usuário e segredos à invocação em execução — assim, uma ferramenta se comporta de forma idêntica sob CodeActAgent ou LlmAgent.
Diferenças deliberadas
- O isolamento de execução de código é responsabilidade de
CodeRuntime, não um complemento. - O despacho de ferramentas é sequencial por design (uma única continuação é
capturada em um limite de chamada), portanto não há
tool_execution_strategyparalelo. - Não há uma opção de construtor
skip_summarization— o modelo encerra o loop por conta própria viafinal_result— embora uma ferramenta que definaskip_summarizationem suas ações ainda encerre a execução.
Exemplo
Uma demonstração completa executável e sem dependências — um CodeRuntime autocontido mais
um modelo determinístico — está em
examples/codeact_agent:
cargo run --manifest-path examples/codeact_agent/Cargo.toml
Implementando um CodeRuntime
Um CodeRuntime analisa e executa um script passo a passo, expondo uma chamada externa por 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é um conjunto de variantes de struct —Call { call, stdout },Complete { value, stdout }eRaised { message, stdout }. Construa-as com os auxiliaresRunStep::call/RunStep::complete/RunStep::raisede anexe a saída capturada com.with_stdout(..).RunStep::Callexpõe exatamente uma chamada pendente; retome-a com um valor ou um erro, oudump()sua continuação para suspendê-la. Ostdoutanexado por um runtime é exposto de volta ao modelo e persistido nos checkpoints, portanto sobrevive à suspensão/retomada.- Um
PendingCallrelata seus argumentos da forma como o interpretador os produziu —positional_args()ekeyword_args()separadamente. Não mapeie argumentos posicionais para nomes por conta própria: o driver os associa aos parâmetros da ferramenta centralmente por meio deadk_agent::codeact::bind_call_args, portanto um runtime não precisa de um schema de ferramenta no limite da chamada erender_toolspode ser uma função pura da fatia de ferramentas. - Erros de script vs. erros do host. Tudo que o modelo poderia corrigir escrevendo
um código diferente — um erro de sintaxe/análise, uma exceção não capturada, um
cancelamento por limite de recursos — é um
RunStep::Raised(uma string opaca enviada de volta ao modelo literalmente).RuntimeErroré reservado para falhas genuínas do host (serialização/desserialização de snapshot, erros internos do interpretador) e interrompe a execução. RuntimeCapabilities::supports_suspensiondeve sertruepara habilitar HITL e a postergação de longa duração;promptdescreve a linguagem/ambiente para o modelo.
Consulte examples/codeact_agent/src/runtime.rs para obter uma implementação completa e mínima
que oferece suporte à suspensão/retomada.
Python via Monty
O adaptador de produção pretendido é
adk-codeact-monty,
um CodeRuntime baseado no Pydantic Monty. Ele
permite que o modelo aja escrevendo Python, é executado no processo, sem
contêiner ou subprocesso, e tira um snapshot de uma execução pausada em bytes —
exatamente o que a suspensão/retomada exige. O interpretador Monty é consumido
por meio do kernel embedded-python de adk-code, que fixa os crates monty em um só
lugar; é necessário o rustc 1.95 ou superior.
[dependencies]
adk-agent = { version = "2.1.0", features = ["codeact"] }
adk-codeact-monty = "2.1.0"
Ou por meio do crate agregador (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(),
);
Acesso ao SO
Os efeitos do sistema operacional que um script tenta executar — leituras e
gravações no sistema de arquivos, os.getenv/os.environ e
date.today()/datetime.now() — são atendidos diretamente de acordo com uma política
controlada pelo host. Eles não são ferramentas e nunca pausam o loop do
agente. Por padrão, um runtime é totalmente isolado (sem acesso ao sistema de
arquivos, com um ambiente vazio e o relógio do host habilitado). Conceda acesso
específico usando o construtor:
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(),
);
O acesso à rede e a subprocessos não possui uma superfície de chamadas de SO do Monty e continua indisponível independentemente da política. O acesso concedido é descrito para o modelo no prompt do sistema, para que ele saiba quais caminhos pode ler ou gravar e quais variáveis de ambiente existem.
O Monty implementa apenas um subconjunto de pathlib.Path; portanto, quando caminhos
são montados, o prompt lista os métodos exatos compatíveis (qualquer outro gera
AttributeError):
- Leitura/consulta (qualquer montagem):
exists(),is_file(),is_dir(),is_symlink(),read_text(),read_bytes(),stat(),iterdir(),resolve(),absolute(),open("r"). - Escrita (somente montagens de leitura e escrita):
write_text(),write_bytes(),append_text(),append_bytes(),mkdir(),unlink(),rmdir(),rename(),open("w")/open("a"). - Operações puras de caminho (sem E/S): o operador
/e as propriedadesjoinpath(),is_absolute(),with_name(),with_stem(),with_suffix(),as_posix(), além das propriedades.name,.parent,.stem,.suffix,.suffixes,.parts.
As ferramentas são invocadas por meio de uma única função integrada,
call_tool("name", {"arg": value, ...}) — a única forma de chamar uma ferramenta; elas
nunca ficam disponíveis como funções chamáveis isoladas. O nome da ferramenta é uma
string literal e cada argumento é uma entrada indexada por string em um único dicionário, portanto o nome real é transportado dentro da continuação serializada (sobrevivendo à suspensão/retomada sem uma tabela de nomes no host),
uma ferramenta e cada argumento pode ter qualquer nome (não um identificador Python válido como
"fetch-cart", uma palavra-chave Python ou até mesmo "call_tool"), e o driver associa as
entradas do dicionário pelo nome exatamente — sem inferência posicional. Cada ferramenta aparece no
prompt como uma linha de uso call_tool("name", {...}) com seus parâmetros e
descrição. Qualquer outra forma além desta — um fetch_cart(...) isolado, argumentos
nomeados, um argumento que não seja um dicionário ou uma chave que não seja uma string — é recusada com um erro corretivo em vez de ser despachada silenciosamente, de modo que o modelo tem exatamente uma forma de chamada
para aprender.
O executável
examples/codeact_monty_agent
executa um CodeActAgent em relação ao Python real, totalmente offline.