Python 代码执行(Monty)

ADK-Rust 通过 Pydantic Monty 解释器在进程内运行模型编写的 Python 代码 — 无需容器、无需子进程,启动时间仅为微秒级。此功能分为两层:

  • adk-codeembedded-python 功能)— MontyExecutorBuilder 以及两个执行器产品 MontyOneShotExecutorMontyReplExecutor,二者均实现 CodeExecutor
  • adk-toolcode-embedded-python 功能)— MontyPythonCodeToolmonty_python_code),面向 agent、基于上述执行器的工具。

这是容器支持的 PythonCodeToolpython_code)的补充;后者在 Docker 中运行完整的 CPython — 当脚本需要真正的 Python 生态系统(pip 软件包、C 扩展、完整的标准库)时,应使用它。Monty 实现了 Python 的一个子集,换来的是进程内速度、可序列化的解释器状态,以及从设计上保证的无网络/无子进程特性。

[dependencies]
adk-tool = { version = "2.1.0", features = ["code-embedded-python"] }

或者通过总 crate:

[dependencies]
adk-rust = { version = "2.1.0", features = ["minimal", "code-embedded-python"] }

一次性执行与 REPL

一个构建器可以生成这两种产品;模式编码在类型中,而不是标志中:

模式构建状态并发
单次执行build_one_shot()每次调用使用全新的解释器支持并发
REPLbuild_repl()变量、函数和导入内容会在多次调用之间持续存在每个会话中的调用按顺序执行
use adk_code::{MontyExecutorBuilder, PathAccess};

let builder = MontyExecutorBuilder::new()
    .allow_path("/data", "/srv/agent/data", PathAccess::ReadOnly)
    .allow_path("/out", "/srv/agent/out", PathAccess::ReadWrite)
    .environ_var("PROJECT", "acme")
    .system_clock();

let one_shot = builder.clone().build_one_shot()?;
let repl = builder.build_repl()?;

REPL 执行器会在调用之间保存序列化的解释器。Monty 通过 Python 层面的异常保留会话,因此失败的代码片段不会销毁已累积的状态。CodeExecutor 生命周期方法管理会话:start() 对其进行初始化,stop() 丢弃会话,restart() 重置会话,而 execute() 会在 start() 之前延迟初始化会话。

安全模型

隔离通过显式策略与基于禁止的强制执行相结合:

  • 文件系统。 只有通过 allow_path 授权的目录才可访问;每个目录均为只读或读写,并通过针对虚拟挂载路径的 pathlib.Path 进行访问。Monty 的挂载表强制执行边界(规范化 + 符号链接逃逸检测)。任何其他路径都会引发可捕获的 OSError(存在性检查则返回 False)。
  • 环境。 os.getenv / os.environ 只能读取构造时授予的显式映射——主机进程环境绝不会被暴露。
  • 时钟。 只有在授予了 .system_clock() 时,date.today() / datetime.now() 才能工作;否则会引发 OSError
  • 网络和子进程。 Monty 没有提供这两者的接口——无论配置如何,都不可能使用。
  • 超时。 SandboxPolicy::timeout 映射到 Monty 的 ResourceLimits::max_duration(每次调用都进行真正的虚拟机内抢占)。内存上限(默认为 256 MiB)限制堆;在 REPL 模式下,它限制的是会话堆的累计大小。

授权与请求策略。 构建器的授权是任何脚本可以拥有的最大访问权限。每个请求的 SandboxPolicy 只能在这些授权范围内进一步收窄——超出授权的请求会以失败关闭方式被拒绝,并由 ExecutionError::UnsupportedPolicy 指明超出的部分,且这一切发生在任何代码运行之前。授权涵盖其整个目录子树:请求已授权的挂载点或其中的任意子目录都会成功,实际挂载点是所请求的路径,并由匹配的主机子目录提供支持。使用 granted_policy() 请求执行器实际提供的确切内容。

REPL 会话的有效策略在各次调用之间不得发生变化;策略不同于会话既定策略的调用会被拒绝,并给出 restart() 的指导。

主机函数

已注册的 Rust 函数(同步或异步)会成为可调用的 Python 函数,脚本可以通过裸名称访问:

use adk_code::MontyExecutorBuilder;
use serde_json::json;

let executor = MontyExecutorBuilder::new()
    .function_fn("row_count", "Count rows in the loaded dataset.", |args, _kwargs| async move {
        Ok(json!(args.len()))
    })
    .build_one_shot()?;

对于完整的 trait 形式,请实现 HostFunctionnamedescription,用于 LLM 提示的可选 signature,以及带有由 JSON 转换的位置参数和关键字参数的异步 call)。注册表验证发生在 build_*():名称必须是有效的 Python 标识符、彼此唯一,并且不得与 Python 内置名称冲突。

在脚本内部,主机函数以同步方式调用——绝不能使用 await。返回的 Err 会变成可捕获的 Python 异常,并携带相应消息;调用未注册的名称会引发纠正性异常,其中列出已注册的名称。主机函数执行有其自身的墙上时钟限制(host_function_timeout,默认为 30 秒),因此挂起的函数不会阻塞 execute()

注意: 主机函数以主机代码运行。它们属于用户自己的信任边界,而不是 Monty 的信任边界——解释器沙箱不会限制它们的副作用。

自描述执行器

两个执行器都实现了 CodeExecutor::prompt_snippet(),用于呈现其 构建后的能力:模式语义、带访问级别的文件系统根目录、 环境变量名称(绝不会呈现其值)、时钟可用性、 无网络/无子进程保证、输出契约,以及用于已注册主机函数的 Python 存根代码块。 MontyPythonCodeTool 会将该代码片段附加到面向 LLM 的描述中,因此提示和解释器内的行为 都源自同一配置,不会发生偏差。

MontyPythonCodeTool

面向代理的工具(monty_python_code,作用域为 code:execute)与 JavaScriptCodeTool 保持一致:将错误作为信息处理的 JSON、camelCase 输出键,以及在功能 禁用时使用结构化的 "rejected" 回退方案。

use adk_code::PathAccess;
use adk_tool::MontyPythonCodeTool;
use serde_json::json;
use std::sync::Arc;

let tool = MontyPythonCodeTool::builder()
    .allow_path("/out", "/srv/agent/out", PathAccess::ReadWrite)
    .environ_var("PROJECT", "acme")
    .system_clock()
    .function_fn("get_weather", "Current weather for a city.", |args, _kwargs| async move {
        Ok(json!({ "temp_c": 21 }))
    })
    .build_repl()?;

let agent = LlmAgentBuilder::new("data_agent")
    .instruction("Use monty_python_code for calculations and data work.")
    .model(model)
    .tool(Arc::new(tool))
    .build()?;

MontyPythonCodeTool::new() 构建完全沙箱化的一次性工具; MontyPythonCodeTool::repl() 构建完全沙箱化的 REPL 工具。

会话作用域

在 REPL 模式下,解释器会话以完整的 ADK 会话身份作为键, 包括应用名称、用户 ID 和会话 ID,因此即使不同用户之间重复使用会话 ID 字符串, 状态也不会泄漏。所有会话共享相同的授权和主机函数注册表——只有解释器状态是 按会话隔离的。会话映射受 LRU 上限约束(max_sessions,默认为 100;0 按 1 处理);被逐出的会话在下一次调用时会透明地启动一个新的解释器。

工具参数

参数类型描述
code字符串(必需)要执行的 Python 源代码
inputany可选的 JSON 值,绑定到 input 变量
timeout_secsinteger解释器时间预算(默认为 30,限制在 1–300 范围内)
resetboolean仅限 REPL 模式:执行前丢弃持久会话

输出封装

{ "status": "success", "stdout": "", "stderr": "", "output": {"n": 42},
  "stdoutTruncated": false, "stderrTruncated": false, "durationMs": 3 }

不存在 exitCode — 执行在进程内进行,不会生成进程; status 是成功/失败信号。stdoutTruncated / stderrTruncated 会报告捕获的输出是否因沙箱策略的字节限制而被截断(默认每项 1 MB)。

脚本最终表达式的值作为 output 返回;print() 输出会被捕获为 stdout。失败状态:"failed"(Python 异常 — 回溯信息位于 stderr,包括主机 函数引发的异常)、"timeout"(超出时间预算)、"rejected"(参数错误或 功能已禁用)。绝不会出现 ToolError

两种模式使用固定的封装,因此工具通过 Tool::response_schema() 声明该封装 — 能够提供响应架构的提供方会在工具声明中,将其与 parameters 一并提供。

与 CodeAct 的关系

CodeActAgent + adk-codeact-monty 路径同样通过 Monty 运行 Python, 但会从脚本内部使用 ADK Tool 进行调度(call_tool(...)),并且 支持跨代理轮次的挂起/恢复。MontyPythonCodeTool 有意排除 这两者 — 它是一个自包含的代码执行工具,其可扩展接口是 主机函数注册表。请参阅 编码代理 了解 CodeAct。

示例

examples/monty_python_code_tool 运行一个 LlmAgent,其中配置了 REPL 模式的 MontyPythonCodeTool,并设置了读写挂载、一个环境变量和一个已注册的主机函数 — 展示变量在多轮之间的持久性,以及从模型编写的 Python 中调用主机函数。