CodeActAgent(CodeAct)

CodeActAgentLlmAgent 的同类方案,它通过编写和运行代码来执行操作,而不是一次只发出一个工具调用。每一轮中,模型生成一个脚本;工具以可调用函数的形式提供,脚本可以对这些函数进行组合;脚本则通过返回带标签的值来传达其结果。

这就是 CodeAct 模式:模型不是 call tool A → observe → call tool B,而是在一个脚本中编写 b(a(x)),因此多步骤工作可以在单轮中完成。该模式由 adk-agent 上的 codeact 功能启用。

何时使用

  • 每轮需要串联或组合多个工具的任务(数据处理、批量操作、胶合逻辑)。
  • 针对代码生成进行后训练的模型。
  • 可使用真实解释器(例如 Python)作为操作执行基础的工作流。

对于原生工具调用,优先使用 LlmAgent。对于沙箱化的文件/ shell 编码工具框架,请参阅 编码代理

循环的工作方式

每一轮:

  1. 模型发出一个围栏代码块(脚本)。
  2. 脚本在 [CodeRuntime] 上运行;工具调用会传递给主机,由主机执行工具,然后将结果带回脚本并恢复其运行。
  3. 脚本返回一个带标签的 ScriptOutput
    • observation — 反馈给模型;循环继续。
    • error — 作为消息反馈;循环继续。
    • final_result — 返回给调用方;循环结束。
    • transfer_to_agent — 将控制权交给另一个代理;循环结束。

该框架与语言无关CodeRuntime trait 是逐步解释器的衔接点,并通过自由格式提示向模型报告自身的语言/环境。预期的生产适配器会封装 Monty,这是一个原生 Rust 的 Python 解释器。

持久性:暂停与恢复

CodeActAgent 在不同调用之间是无状态的——持久状态存储在会话中,与 LlmAgent 完全相同。以下两种情况会暂停运行:

  • 尚未做出决定的确认门控工具(HITL),以及
  • 结果在带外到达的长时间运行工具。

暂停时,正在运行的解释器延续状态会被序列化为 CodeActCheckpoint 并写入会话状态;下一次 run() 会将其读回 并恢复运行——确认决定通过 RunConfig::tool_confirmation_decisions 到达,而长时间运行工具的结果会在下一条消息中作为 FunctionResponse 到达。内联工具调用会使用预写(SAVE-BEFORE)和 SAVE-AFTER 检查点进行包围:一旦 SAVE-AFTER 检查点持久化,恢复时就会使用存储的结果,绝不会重新运行工具。如果工具产生副作用后、其 SAVE-AFTER 检查点写入前的狭窄时间窗口内发生崩溃,恢复时将重新运行该工具,因此非幂等工具应对此进行防护(与 LlmAgent 相同的至少一次 边界)。

这要求运行时能够对暂停的调用进行快照。无法做到这一点的运行时会以内联方式运行长时间运行工具,并拒绝确认暂停。

构建 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()?;

modelruntime 是必需的;其他所有内容都有默认值。

与 LlmAgent 的一致性

构建器镜像了 LlmAgentBuilder

  • 模型generate_content_config,以及 temperature/top_p/top_k/ max_output_tokens 简写形式。
  • 指令instruction/instruction_providerglobal_instruction/global_instruction_provider,通过 {state.key} 模板注入;以及技能(skills 功能)。
  • 历史记录include_contents
  • 工具:静态 tool 和按调用生成的 toolsettool_timeoutdefault_retry_budget/tool_retry_budgetcircuit_breaker_threshold,以及 on_tool_error 回退机制。
  • 授权ToolConfirmationPolicyrequire_tool_confirmation/require_tool_confirmation_for_all)。
  • 转移sub_agentdisallow_transfer_to_parent/ disallow_transfer_to_peers
  • 输出output_keyoutput_schema/output_type,以及带有 修正重试循环(output_max_retries)。
  • 回调before_callback/after_callbackbefore_model_callback/after_model_callback,以及 before_tool_callback/after_tool_callback/after_tool_callback_full。 工具执行后的回调可以通过 CallbackContext::tool_outcome() 检查结构化的执行元数据。
  • 受功能开关控制:输入/输出防护措施(guardrails)以及 EnhancedPlugin 流水线(enhanced-plugins)。

每次工具调用都会获得一个全新的 ToolContext,其中携带解释器调用 ID, 并将工件、记忆、共享状态、用户作用域和机密信息委托给实时调用——因此工具在 CodeActAgentLlmAgent 下的行为完全一致。

有意的差异

  • 代码执行沙箱由 CodeRuntime 负责,而不是通过附加组件实现。
  • 工具调度按设计顺序执行(单个延续在一次调用边界处进行快照),因此不存在并行 tool_execution_strategy
  • 没有 skip_summarization 构建器选项——模型通过 final_result 自行结束循环——不过,工具若在其操作上设置 skip_summarization,仍会结束运行。

示例

一个可运行、无依赖的端到端演示——由一个自包含的 CodeRuntime 和 一个确定性模型组成——位于 examples/codeact_agent

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

实现 CodeRuntime

CodeRuntime 会解析并逐步执行脚本,一次呈现一个外部调用:

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 是一组结构体变体——Call { call, stdout }Complete { value, stdout }Raised { message, stdout }。使用 RunStep::call / RunStep::complete / RunStep::raised 辅助函数构造它们,并通过 .with_stdout(..) 附加捕获的输出。RunStep::Call 恰好呈现一个待处理的调用;使用值或错误恢复该调用,或者 dump() 其继续部分以暂停执行。运行时附加的 stdout 会重新呈现给模型并持久化到检查点中,因此在暂停/恢复后仍然保留。
  • PendingCall 会按照解释器生成参数的方式报告参数——分别报告 positional_args()keyword_args()不要自行将位置参数映射到名称:驱动程序会通过 adk_agent::codeact::bind_call_args 在中心位置将它们绑定到工具参数,因此运行时在调用边界无需工具模式,而 render_tools 可以只是工具切片的纯函数。
  • 脚本错误与宿主错误。 模型可以通过编写不同代码来修复的任何问题——语法/解析错误、未捕获的异常、资源限制导致的取消——都是 RunStep::Raised(原样反馈给模型的不透明字符串)。RuntimeError 专用于真正的宿主故障(快照的序列化/反序列化、解释器内部错误),并会中止运行。
  • RuntimeCapabilities::supports_suspension 必须是 true,才能支持 HITL 和长时间运行的延迟处理;prompt 会向模型描述语言/环境。

请参阅 examples/codeact_agent/src/runtime.rs,其中提供了支持暂停/恢复的完整、最小实现。

通过 Monty 使用 Python

预期用于生产环境的适配器是 adk-codeact-monty, 这是一个由 Pydantic Monty 支持的 CodeRuntime。它 让模型通过编写 Python来执行操作,在进程内运行,无需容器或 子进程,并将暂停的运行快照保存为字节——这正是挂起/恢复 所需的功能。Monty 解释器通过 adk-codeembedded-python 内核使用,该内核将 monty crate 集中固定在同一处;需要 rustc 1.95+。

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

或者通过 umbrella crate(重新导出为 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(),
);

操作系统访问

脚本尝试执行的操作系统效果——文件系统读写、 os.getenv/os.environ,以及 date.today()/datetime.now()——均根据 由主机控制的策略就地处理。它们不是工具,也不会暂停 代理循环。默认情况下,运行时处于完全沙箱化状态(无文件系统访问权限、 环境为空,但启用主机时钟)。使用构建器授予特定访问权限:

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(),
);

网络和子进程访问没有 Monty 操作系统调用接口,无论策略如何设置都不可用。 授予的访问权限会在系统提示中告知模型,因此模型知道可以读取或写入哪些路径, 以及存在哪些环境变量。

Monty 仅实现了 pathlib.Path 的一个子集,因此挂载路径时, 提示会列出确切支持的方法(任何其他方法都会引发 AttributeError):

  • 读取/查询(任意挂载):exists()is_file()is_dir()is_symlink()read_text()read_bytes()stat()iterdir()resolve()absolute()open("r")
  • 写入(仅限读写挂载):write_text()write_bytes()append_text()append_bytes()mkdir()unlink()rmdir()rename()open("w")/open("a")
  • 纯路径操作(无 I/O):/ 运算符以及 joinpath()is_absolute()with_name()with_stem()with_suffix()as_posix(), 以及 .name.parent.stem.suffix.suffixes.parts 属性。

工具通过单个内置函数调用,即 call_tool("name", {"arg": value, ...})——这是调用工具的唯一方式;它们绝不会以裸可调用对象的形式处于作用域中。工具名称是字符串字面量,每个参数都是一个字典中的字符串键条目,因此真实名称会包含在序列化的延续中(挂起/恢复时无需依赖宿主端名称表即可保留),工具及其每个参数都可以使用任意名称(不必是有效的 Python 标识符,例如 "fetch-cart"、Python 关键字,甚至可以是 "call_tool"),驱动程序会严格按名称绑定字典条目——不会进行位置推断。每个工具都会在提示中以一行 call_tool("name", {...}) 用法的形式出现,并附带其参数和描述。除这一种形式之外的任何调用方式——裸 fetch_cart(...)、关键字参数、非字典参数或非字符串键——都会被拒绝并返回纠正性错误,而不是静默分发,因此模型只需学习一种调用形式。

可运行的 examples/codeact_monty_agent 会在完全离线的情况下,使用真实的 Python 驱动 CodeActAgent

CodeActAgent(CodeAct) - ADK-Rust 文档 | ADK-Rust