运行时
运行时决定智能体「内层循环」如何执行——也就是每一轮怎样调用模型、解析意图、调用工具。默认运行时基于 Google ADK 的内置执行流程,开箱即用。你也可以切换到其他执行后端,或在执行流程上插入统一的处理逻辑(如鉴权、日志、重试),而无需改动智能体本身。
基本使用
默认即为 ADK 运行时,无需任何额外配置:
import asyncio
from veadk import Agent, Runner
agent = Agent(name="assistant") # 默认使用 ADK 运行时
print(asyncio.run(Runner(agent=agent).run("你好")))切换执行后端
通过 runtime 选择内层循环的执行后端:
agent = Agent(name="assistant", runtime="codex")| 取值 | 说明 |
|---|---|
adk(默认) | 使用 Google ADK 内置的执行流程,适用于绝大多数场景。 |
codex | 使用 OpenAI Codex SDK 驱动内层循环,在沙箱中执行命令与文件操作。 |
piagent | 使用本地 Pi coding agent 二进制(通过其 RPC 模式)驱动内层循环。二进制会在首次运行时自动下载,也可用 PIAGENT_BINARY 指定已有路径。 |
定位:codex / piagent 是「沙箱执行运行时」,不是 ADK 执行流程的等价替代品。
它们把整个内层循环交给外部 agent harness,因此 ADK 请求/响应处理器(planner、code_executor、example store、知识库注入)、逐次模型调用的回调与 span 不会在这层流程里执行。VeADK 会把 sub_agents 桥接成 transfer_to_agent 工具,但这些运行时仍更适合「模型需要在受控沙箱里跑命令、改文件」的任务,而不是所有 ADK 能力的透明后端替换。具体差异见下方支持矩阵。
使用 Codex runtime 前安装可选依赖:
pip install "veadk-python[codex]"什么时候该用 codex 运行时
上面的定位说明可以压成一条判断标准:codex 运行时只有一个真正的优势——模型可以写下一个文件、把它跑起来、读到报错、改掉、再跑一遍,整个循环发生在一个 OS 沙箱里,而你不需要事先把每一步都定义成工具。 除此之外的每一条轴上,它都比 runtime="adk" 更贵。
适合:
- 步骤无法事先枚举成工具——临时数据分析、日志排障、脏数据清洗、按需改代码。这类任务的下一步取决于上一步的输出,而不是取决于你写智能体时的设想。
- 工作对象是一个文件系统——中间产物留在 workspace 里,同一 Session 的下一轮仍然看得见(见 Workspace 生命周期)。
- 第一次做错是正常的,且重试很便宜——报错信息本身就是模型的下一份输入。在 ADK 运行时里,一次失败的工具调用只是一条错误字符串;在这里,它是一段可以被读、被诊断、被修好的 traceback。
不适合:
- 一次固定的工具调用 + 一段格式化回答。 这正是 ADK 运行时的主场:更快、更便宜,而且不受下面任何一条限制。
- 需要
output_schema、planner、code_executor或generate_content_config的智能体。 这些配置在非 adk 运行时下会直接报错,而不是降级——完整清单见支持矩阵。 - 对延迟敏感的链路。 每一次 invocation 都会重建一个临时
CODEX_HOME、拉起一个 Codex 子进程、开一个 ephemeral thread;这笔固定开销每回合都要付。 run_live。 非 adk 运行时没有 live/bidi 实现,见下文。
成本,如实说清楚:
- 每次 invocation 一个 Codex 子进程。 runtime 每回合都会重建
CODEX_HOME并进入AsyncCodex(...),thread 以ephemeral=True创建。 - 每回合都会把整段 ADK 会话历史重新序列化进 prompt。 历史被渲染成一个
<conversation_history>JSON 块,附在当前消息前面;codex 没有增量续接机制,所以 prompt token 随对话轮数增长。这也是include_contents="none"被拒绝(而不是被忽略)的原因。 - ADK 工具的结果会穿过模型上下文。 见下方 Workspace 是数据面——这是本运行时最容易踩的一个坑。
一句话版本:能把任务写成一串确定的工具调用,就用 runtime="adk";只有当你只能描述目标、路径必须试出来时,才用 runtime="codex"。
仓库里四个 codex 示例的分工:
| 示例 | 讲什么 |
|---|---|
codex_data_analysis | 本运行时是干什么用的。 Codex 写分析脚本 → 运行 → 在脏数据上撞到真实报错 → 自己改好 → 重跑 → 出报告。自迭代循环。 |
codex_ops_assistant | 本运行时是干什么用的。 ADK 工具把日志、指标与发布记录落进 workspace,Codex 写一次性脚本把三者关联起来、定位根因,全程在断网沙箱里。 |
codex_with_skill_and_mcp | 怎么接线。 本地 skill 和 MCP 工具在这个运行时下分别走哪条路。 |
codex_runtime_on_agentkit | 怎么部署。 把一个 runtime="codex" 智能体发布到火山引擎 AgentKit。 |
Codex 安全配置
Codex 默认使用独立 Session workspace、workspace_write 沙箱、禁用网络,并拒绝一切需要提权的操作:
from veadk import Agent
from veadk.runtime.codex import CodexRuntimeConfig
agent = Agent(
name="assistant",
runtime="codex",
codex_runtime_config=CodexRuntimeConfig(
sandbox="workspace_write",
approval_mode="deny_all", # 默认值;auto_review 等同于全自动批准,见下
network_access=True,
# 若需在已有工程内工作,显式指定目录;默认使用 Session 隔离目录。
workspace_root="/workspace/codex",
),
)approval_mode="auto_review" 不是「人工复核」,而是「全自动批准」。
Codex SDK 内置的审批处理器对每一个 requestApproval 通知都回答 accept,而 AsyncCodex 没有提供替换该处理器的接口。因此 auto_review 会自动批准每一次沙箱提权和文件修改,既不询问人工,也不经过 ADK。只有默认的 deny_all 才真正把 Codex 约束在沙箱内。在多租户或不可信输入场景中,请勿使用 auto_review。
关于沙箱与网络的组合,有一个容易误判的点:network_access 只会写入 Codex config.toml 的 [sandbox_workspace_write] 段,只有 workspace_write 沙箱会读取它。
sandbox | network_access 是否生效 |
|---|---|
workspace_write | 生效。 |
read_only | 不生效(设为 True 会记录一条警告)。 |
full_access | 不生效——danger-full-access 本身就意味着完全的网络与文件系统访问。 |
因此 CodexRuntimeConfig(sandbox="full_access", network_access=False) 会直接抛出 ValueError:这个组合读起来像「禁用网络」,实际却给了完全访问权限。需要 full_access 时必须显式写上 network_access=True 以确认风险。
full_access 和 reuse_workspace=True 会放宽不同调用之间的文件系统边界,只应在受信环境中开启。
安全基线组合
把上面几项拼起来,就是在不可信输入下跑 codex 的默认配方。四个设置各挡一个方向,缺一不可:
from google.adk.agents import RunConfig
from veadk import Agent, Runner
from veadk.runtime.codex import CodexRuntimeConfig
agent = Agent(
name="analyst",
runtime="codex",
tools=[load_orders, publish_report], # 唯一的进出通道
codex_runtime_config=CodexRuntimeConfig(
sandbox="workspace_write", # 只能写 workspace
network_access=False, # 沙箱内没有出网通道
approval_mode="deny_all", # 默认值;拒绝一切提权请求
),
)
await Runner(agent=agent).run(
"...",
run_config=RunConfig(max_llm_calls=40), # 单次 invocation 的成本上限
)它们之所以能组合成一套,是因为关掉网络之后,你自己挂上去的那几个 ADK 工具就是唯一的出站通道:模型可以在沙箱里对敏感数据任意计算、任意重试,但要把任何东西送出去,只能经过一个你审计过、有签名、有日志的函数。换句话说,工具列表就是你的数据出口策略——这条边界要成立,前提是每个工具自己也不提供任意外发能力。
配套的三条,不在这里重复展开:
approval_mode="auto_review"会拆掉这套配方的地基,理由见上方的红色警告。sandbox与network_access的组合语义见上表:只有workspace_write会读network_access。VEADK_CODEX_*环境变量的优先级高于这里的 Python 配置,见 Codex 环境变量。部署环境里一个VEADK_CODEX_SANDBOX=full_access就能推翻上面整段代码。
RunConfig(max_llm_calls=...) 不设时,VeADK 的 Runner 会用环境变量 MODEL_AGENT_MAX_LLM_CALLS(默认 100)兜底。在这个运行时下建议显式写一个更小的值:模型自己决定跑多少轮脚本,只有这个配额是硬上限。
Workspace 生命周期
默认情况下,workspace 由 app_name / user_id / session_id / agent 名四元组定位:同一 Session 的多次调用共享同一目录,回合结束后不会删除,因此上一轮写下的文件下一轮仍然可见。进程退出时整棵目录树被清理;进程存活期间,空闲超过 6 小时的 Session workspace 会在下一次创建 workspace 时被顺带回收,以限制长期运行的服务进程的磁盘增长。
工具不需要自己推导这个路径:不做任何配置,current_workspace() 就会返回当前调用它的这一轮的 workspace,见下方 Workspace 是数据面。
显式传入 workspace_root 时,这块目录属于你自己:runtime 既不会删除它,也不会对它执行上述空闲回收——它仍会在其下为每个 Session 建独立子目录,但这些子目录需要你自行清理。reuse_workspace=True 只有在同时设置了 workspace_root 时才有意义,它让所有 Session 直接共用该目录本身,而不再做 Session 隔离。
Workspace 是数据面
这是用好 codex 运行时最重要、也最容易被忽略的一条:ADK 工具的参数和返回值是控制面,workspace 才是数据面。
原因在工具的执行路径上。ADK/MCP 工具不由 Codex 自己调用,而是由 runtime 的 Responses shim 执行:shim 拿到工具返回值后 json.dumps 成字符串,作为一条 function_call_output 塞回模型的 input 数组。而且因为 Codex 每完成一次原生工具调用就会重建整个 input,shim 还必须在本回合后续的每一次后端请求里,把这对 function_call / function_call_output 重放一遍。于是:
一个返回 5 万行日志的工具,会把这 5 万行灌进模型上下文,并在同一回合里重复灌若干次。这不是慢一点的问题,是这一回合直接废掉。
正确的做法是让工具交出一个路径,而不是一份数据:把数据写进 workspace(Codex 的 cwd 就是它),只返回位置和少量元信息。数据落在磁盘上,模型用 shell 去读、去切片、去统计,上下文里只留下一行。
from pathlib import Path
from veadk.runtime.codex import current_workspace
def load_orders(day: str) -> dict:
"""把某一天的订单写成 CSV,放进你的工作目录。
只返回一张回执,不返回订单本身——请用你自己的代码去读返回路径上的 CSV。
"""
workspace = current_workspace() # 本轮的工作目录,或者 None
if workspace is None: # 不在 Codex 轮次里:返回模型能读懂的错误
return {"status": "error", "message": "no codex workspace on this call"}
rows = warehouse.query(day)
name = f"orders-{day}.csv"
write_csv(Path(workspace) / name, rows)
# 回执,不是数据:告诉模型文件在哪、有多大形状。
return {"path": name, "rows": len(rows), "columns": list(rows[0])}这里的 docstring 是真正干活的部分:模型读到的就是它,所以必须写清楚「返回的是回执」。 否则模型会直接让工具把数据交出来,你又回到了原点。
反方向同理:Codex 在 workspace 里写好报告之后,「发布」工具应该收下它写的那个文件的路径,由工具自己读盘再送往外部系统——而不是让模型把整篇报告当作工具参数复述一遍(那同样要过一遍上下文,而且内容可能在复述中走样)。
工具怎么知道 workspace 在哪:
current_workspace()——首选做法,也是多租户唯一可行的做法。from veadk.runtime.codex import current_workspace,返回当前调用该工具的这一轮 workspace 的绝对路径。它不需要任何配置,workspace_root与reuse_workspace都不设时同样有效。这个值是在每次工具调用前后绑定的,而不是从调用方的环境上下文里读,所以同一进程里并发的多个回合各自看到自己的目录。调用栈上没有 Codex 回合时它返回None,而不是抛异常——同一个工具对象也会被别的 runtime、被AgentTool、被单元测试执行——因此请判断None并返回你自己的{"status": "error", ...}让模型去处理,而不是抛异常,也不要退回到某个你自己选定的目录。- 固定路径——单租户,并且你想在跑完之后翻这个目录。 设置
workspace_root并配合reuse_workspace=True,此时 workspace 就是workspace_root本身,工具和CodexRuntimeConfig可以共用同一个常量,而且进程退出后目录仍在磁盘上。代价是所有 Session 共用它,见 Workspace 生命周期。
无论哪种做法,来自模型的路径都是不可信输入:打开之前请把它解析回 workspace,并拒绝一切越界的路径(..、绝对路径、指向外部的符号链接)。
Codex 执行参数
| 参数 | 默认值 | 说明 |
|---|---|---|
reasoning_effort | "medium" | 推理强度,可选 minimal/low/medium/high/xhigh。越高越慢、消耗 token 越多。 |
personality | "pragmatic" | Codex 自带的回复风格,可选 none/friendly/pragmatic。 |
max_tool_iterations | 32 | 整个 Codex turn 内 shim 可执行的 ADK/MCP 工具轮次上限(1–256)。见下方行为变更说明。 |
tool_timeout_seconds | 120.0 | 单个 ADK/MCP 工具调用的超时秒数;设为 None 表示不超时。 |
reuse_workspace | False | 仅在同时设置了 workspace_root 时生效,而且只值得在单租户场景下开启,见上方 Workspace 生命周期。否则两个字段都别设,让工具调用 current_workspace(),见 Workspace 是数据面。 |
行为变更:max_tool_iterations 的含义已改变,默认值从 8 提高到 32。
它现在约束的是整个 Codex turn,而不再是单次后端请求。Codex 每完成一次原生工具调用就会发起一次新的后端请求,所以旧的「每请求」计数实际允许的执行次数是「请求轮数 × budget」。默认值同时调高,是为了让「先跑若干轮原生工具、之后才调用 ADK 工具」的回合不被提前截断。如果你此前靠 8 这个值来兜底控制成本,请重新评估——现在更合适的成本上限是 RunConfig(max_llm_calls=...)(见下)。
指令是怎么下发给 Codex 的
Codex 自带一份约 20KB、针对自身工具链(apply_patch、update_plan、shell、AGENTS.md)调优过的系统提示词。VeADK 不会通过 base_instructions 覆盖它——Codex 在该字段被设置时会整体替换内置模板,这些指导会被全部删掉(Codex 官方文档也强烈不建议这么做)。
因此,Agent 的身份块与 instruction 走的是 Codex 原生的 developer_instructions 通道:Codex 把它渲染成一条独立的 developer 消息,与 AGENTS.md、skills、环境上下文并列,是纯追加而非替换。
副作用是 personality 现在真的会生效:它渲染在 Codex 内置的系统提示词模板里,而以前只要设置了 base_instructions,整个模板连同 personality 一起被替换掉,该字段形同虚设。
同一条通道上还会追加两条更正——Codex 被保留下来的系统提示词描述的工具链,这座桥并不能完整提供。runtime 会在每一轮的 developer instructions 后面追加一段简短的工具可用性说明:
- 本次运行没有
apply_patch:shim 只转发function类型的工具,而 Codex 的文件编辑工具不是,所以创建和修改文件要用exec_command(例如cat > file <<'EOF'heredoc)。 request_user_input确实被通告了,但没有人能回答它:一次 ADK 调用没有交互通道,调用它只会让这一轮什么都没做就结束。说明里会要求模型用手上已有的信息作判断,并在最终回复里讲清楚缺了什么。
这两点你不需要再在自己 Agent 的 instruction 里手写一遍。
Codex 环境变量
以下四个环境变量覆盖 Python 中的 CodexRuntimeConfig,而不是作为它的默认值。也就是说,即使代码里写死了 sandbox="workspace_write",部署环境里的 VEADK_CODEX_SANDBOX=full_access 依然会生效。这一优先级与大多数配置项相反,在容器/函数计算等由平台注入环境变量的场景中尤其需要注意。
| 环境变量 | 覆盖的配置项 | 说明 |
|---|---|---|
VEADK_CODEX_SANDBOX | sandbox | 取值同 sandbox 字段。 |
VEADK_CODEX_APPROVAL_MODE | approval_mode | 取值同 approval_mode 字段。 |
VEADK_CODEX_WORKSPACE_ROOT | workspace_root | 工作目录绝对路径。 |
VEADK_CODEX_NETWORK_ACCESS | network_access | 1/true/yes/on 视为开启,其余视为关闭。 |
另有四个用于 Responses→Chat 转换层(shim)的环境变量。它们不参与上面的覆盖规则,只是 shim 自身的调参:
| 环境变量 | 默认值 | 说明 |
|---|---|---|
CODEX_SHIM_NUM_RETRIES | 2 | 后端请求遇到 429/5xx/超时等瞬时错误时的重试次数。 |
CODEX_SHIM_TIMEOUT | 0(不超时) | 单次后端请求的超时秒数。 |
CODEX_SHIM_START_TIMEOUT | 10 | 等待 shim 的本地 HTTP 服务起来的秒数;超时即判定启动失败并明确报错,而不是让 Codex 一直连不上。 |
CODEX_SHIM_CACHE_MAX | 8 | 进程内缓存的 shim 实例上限(按后端地址+凭证区分),用于限制多租户进程里的服务与端口数量。 |
Codex 可观测性
Codex 原生生命周期和 ADK Function/MCP 工具调用都会转换为 ADK Event。运行日志使用稳定的 codex_* 事件名,并包含 invocation_id、call_id、tool、status、duration_ms 等可归因字段。日志不会记录工具参数、工具结果、API Token、凭证或后端地址;Token Usage 通过 codex_event_type=token_usage 事件及对应日志提供。
此外:
call_llmspan:ADK 的call_llmspan 本由它自己的 LLM flow 打开,而该 flow 已被 codex 替换,因此 runtime 会自行打开一个同名 span,并在回合结束时写入 prompt、响应与 token 用量。VeADK 的整条遥测链路——内存 exporter 的 Session 索引、Trace 上报、Portal 指标、以及基于 Trace 的评测——因此都能看到 codex 回合。注意粒度:每回合一个 span,而不是每次内层模型调用一个。usage_metadata:整轮的 token 用量汇总后,只挂在每回合一个的合并后终态 Event 上(而不是逐条事件累加),避免下游做求和统计时重复计数。RunConfig(max_llm_calls=...)会被强制执行:shim 在每一次真实的后端模型调用前扣减配额。超限时 Codex 侧收到一个429 llm_calls_limit(而不是它会重试的 500——重试会把本回合的工具副作用重跑一遍),回合结束后 runtime 再向调用方重新抛出LlmCallsLimitExceededError,而不是返回 Codex 勉强拼出的半截答案。
支持矩阵
runtime="codex" 和 runtime="piagent" 会替换整个 ADK LLM 流程,因此 Agent 上相当一部分配置不会生效。VeADK 会在智能体构造时以及每次调用前检查这些配置:产生错误结果的配置直接报错(ValueError),只是被忽略的配置记录一次警告。
报错:直接拒绝运行
| 配置 | 原因 |
|---|---|
model=... | 运行时从 model_name 解析模型,完全忽略 model 对象,其 api_base、请求头和 fallback 都会丢失。请改用 model_name。 |
generate_content_config=... | 只有 system_instruction 会被转发,temperature、max_output_tokens、thinking_config 等都会被丢弃。 |
output_schema=... | schema 既不会下发给后端,也不会进入提示词,模型从未被要求按 schema 输出;state[output_key] 要么是未校验的回复,要么直接缺失。 |
planner=... / code_executor=... | 二者以 ADK 请求/响应处理器的形式运行,外部运行时不执行这一层。 |
include_contents="none" | 外部运行时始终发送完整会话历史,该设置会被静默忽略并泄漏历史轮次。 |
enable_supervisor=True | 监督流程挂在 ADK LLM flow 上,而该流程已被替换,不会有任何监督生效。 |
runtime="codex" 和 runtime="piagent" 都支持 sub_agents:VeADK 会注入一个 transfer_to_agent 工具供外部 agent 调用。把一个 runtime="codex" 智能体放进 SequentialAgent / ParallelAgent 的 sub_agents 中也支持——这种场景由父级负责调度。
警告:被忽略但仍可运行
| 配置 | 行为 |
|---|---|
model_name=[主, 备...] | 只使用第一个模型,fallback 链不生效。 |
model_provider(非 openai) | 运行时始终以 OpenAI 兼容协议访问 model_api_base。 |
model_extra_config | 仅对 piagent 成立。codex 会转发它:extra_headers 与 extra_body(含 VeADK 默认的请求加密与 prompt 缓存配置)由 shim 原样带到后端请求上,与 adk 路径一致。 |
enable_responses / enable_responses_cache | 不使用 Ark Responses API,previous_response_id 续接与响应缓存均不生效。 |
example_store | 由 ExampleTool.process_llm_request 注入,外部运行时不调用该钩子,few-shot 示例不会进入 prompt。 |
knowledgebase | 知识库被静默禁用。 同样由 LoadKnowledgebaseTool.process_llm_request 注入使用说明,外部运行时不调用,模型不知道知识库的存在,检索不会被触发。 |
skills_mode | 外部运行时在桥接工具时会跳过 VeADK 的 SkillsToolset,指令里宣称的 execute_skills/skills_tool 并未注册。 |
enable_skills_checklist | 依赖 ADK 的 before-tool 回调与 skills 工具集,两者都不生效。 |
after_model_callback | 语义不同:每回合在合并后的最终文本上触发一次,而不是每次模型调用触发一次。 |
tracers | 粒度不同:codex 会补一个每回合一个的 call_llm span(含 prompt、响应、整轮 token 用量),但内层模型循环跑在外部 harness 里,仍然没有逐次模型调用的 span。 |
codex_runtime_config(配合 runtime="piagent") | 只对 codex 运行时有效,piagent 完全忽略。 |
RunConfig(max_llm_calls=...) | 仅对 piagent 成立——它的模型循环在 harness 内部自行计数,不受该配额约束。codex 已支持:见上方可观测性。 |
不支持 run_live
非 adk 运行时没有 live/bidi 实现。通过 ADK get_fast_api_app 暴露的 /run_live(veadk web 使用)对这类智能体会抛出 NotImplementedError,而不是静默回退到 ADK 流程——否则会用完全不同的模型循环和工具集执行。live 场景请使用 runtime="adk",其余场景使用 runner.run_async。
请求处理
在不侵入业务逻辑的前提下,可为每次执行插入统一的横切处理——鉴权、日志、重试、性能监控等。把处理器传入 run_processor 即可,例如接入身份认证做登录态校验:
from veadk import Agent
from veadk.integrations.ve_identity import AuthRequestProcessor
agent = Agent(name="assistant", run_processor=AuthRequestProcessor())不设置时使用默认的空处理器,不改变任何行为。