Python 代码执行(Monty)
ADK-Rust 通过 Pydantic Monty 解释器在进程内运行模型编写的 Python 代码 — 无需容器、无需子进程,启动时间仅为微秒级。此功能分为两层:
adk-code(embedded-python功能)—MontyExecutorBuilder以及两个执行器产品MontyOneShotExecutor和MontyReplExecutor,二者均实现CodeExecutor。adk-tool(code-embedded-python功能)—MontyPythonCodeTool(monty_python_code),面向 agent、基于上述执行器的工具。
这是容器支持的 PythonCodeTool(python_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() | 每次调用使用全新的解释器 | 支持并发 |
| REPL | build_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 形式,请实现 HostFunction(name、description,用于 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 源代码 |
input | any | 可选的 JSON 值,绑定到 input 变量 |
timeout_secs | integer | 解释器时间预算(默认为 30,限制在 1–300 范围内) |
reset | boolean | 仅限 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 中调用主机函数。