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:

  1. O modelo emite um bloco de código delimitado (um script).
  2. 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.
  3. O script retorna um ScriptOutput identificado:
    • 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 atalhos temperature/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 (recurso skills).
  • Histórico: include_contents.
  • Ferramentas: tools estáticos e toolsets por invocação; tool_timeout, default_retry_budget/tool_retry_budget, circuit_breaker_threshold e alternativas on_tool_error.
  • Autorização: ToolConfirmationPolicy (require_tool_confirmation/require_tool_confirmation_for_all).
  • Transferência: sub_agents e disallow_transfer_to_parent/ disallow_transfer_to_peers.
  • Saída: output_key, output_schema/output_type com um loop de correção e nova tentativa (output_max_retries).
  • Retornos de chamada: before_callback/after_callback, before_model_callback/after_model_callback e before_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 de CallbackContext::tool_outcome().
  • Condicionado a recursos: proteções de entrada/saída (guardrails) e o pipeline EnhancedPlugin (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_strategy paralelo.
  • Não há uma opção de construtor skip_summarization — o modelo encerra o loop por conta própria via final_result — embora uma ferramenta que defina skip_summarization em 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 } e Raised { message, stdout }. Construa-as com os auxiliares RunStep::call / RunStep::complete / RunStep::raised e anexe a saída capturada com .with_stdout(..). RunStep::Call expõe exatamente uma chamada pendente; retome-a com um valor ou um erro, ou dump() sua continuação para suspendê-la. O stdout anexado por um runtime é exposto de volta ao modelo e persistido nos checkpoints, portanto sobrevive à suspensão/retomada.
  • Um PendingCall relata seus argumentos da forma como o interpretador os produziu — positional_args() e keyword_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 de adk_agent::codeact::bind_call_args, portanto um runtime não precisa de um schema de ferramenta no limite da chamada e render_tools pode 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_suspension deve ser true para habilitar HITL e a postergação de longa duração; prompt descreve 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 propriedades joinpath(), 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.

CodeActAgent (CodeAct) - Documentação ADK-Rust | ADK-Rust