运行时#
Agent 执行运行时 —— 运行循环、worktree、异步任务、流式传输/恢复、DAG 模型,以及回退层。
API 用法见 Runtime API。以下章节集中说明运行时契约及对应的设计取舍。
1 runtime = 1 session#
每个 Runtime 实例绑定一个 provider session,生命周期 1:1:
create_runtime() = 开 session
runtime.exec() = session 里发一次请求
runtime.close() = 关 session
不提供 reset() / new_session(),要新 session 就再 create_runtime()。
为什么不让一个 Runtime 复用多 session?
- CLI provider(Claude Code / Codex / Gemini CLI)的 session 状态在子进程里。 复用就要在 Runtime 上挂"当前哪个 session id"这种可变状态,引入并发竞态。
- API provider 本身无状态,做"多 session"也只是上层 dict,跟"多个 Runtime" 等价,没新功能。
- ContextVar 自动注入(下条)依赖"runtime 跟当前函数树绑定"这个简单模型; 多 session 会让注入语义复杂化。
代价:用户想跑两套独立对话要管两个 runtime 对象。可以接受,因为这种场景少。
ContextVar 自动注入 runtime#
Agent method 装饰器读 _current_runtime ContextVar,如果当前函数
没传 runtime= 参数就用它;入口函数也没有,就自动 create_runtime()。
为什么不让函数显式声明 runtime?
显式声明的话每个 agentic function 都得在签名里加 runtime: Runtime,
而且每次嵌套调用都得显式传 runtime=runtime 透传——纯粹是 plumbing
样板代码,跟函数逻辑无关。ContextVar 把它隐去:子函数自然继承父函数的
runtime,入口处自动起一个,出口处自动关。
为什么不用 module-level singleton?
singleton 跨线程 / 跨协程共享,两个并发 agent 会互相踩 session 状态。 ContextVar 按线程 + 协程隔离,天然并发安全。
Session-provider vs API-provider 共用一套抽象#
无论底层是 Claude Code CLI(有 session)还是 Anthropic API(无 session),
对 Agent method 作者都是一样的接口 llm(...) / agent(...),两者最终都走 runtime.exec。
框架靠 has_session 属性区分两类 provider 在内部走不同路径:
| session provider (CLI) | API provider | |
|---|---|---|
| 对话记忆 | 子进程自己管 | 无,每次 exec 独立 |
| 上下文注入 | 跳过 DAG render,只发 docstring + 当次 content | 通过 render_context + render_dag_messages 从 DAG 拼历史 |
render_range.subcalls |
不生效(session 自己记得对话) | 生效(用来限制注入历史的窗口) |
为什么不分两套独立的 Runtime 类?
作者写 gui_agent 时不应该关心后端是哪种 provider。强行分开就要每个函数
两份实现,违背"函数描述任务、provider 描述执行通道"的分层。共用抽象的
代价是 has_session 这点条件分支,值得。
Retry 在 runtime 层,不在 provider 层#
exec() / async_exec() 内置 max_retries 默认 2,任何 provider 都
享受重试。
为什么不在每个 provider 类里各自加 retry?
- 重试策略对所有 provider 一致(网络超时 / 速率限制 / 5xx),没必要重复
- 失败报告统一格式(
Attempt N: ErrorType: msg),便于排查 TypeError/NotImplementedError这类编程错误统一不重试(只有 runtime 层知道"这是 provider 实现 bug,重试也没用")
provider 层只关心"把请求发出去、把回复拿回来"。重试 / 节流 / 缓存这种 横切关注点全在 runtime。
DAG 写入:进出函数都写 code 节点,exec 写 llm 节点#
进入 Agent method → 写一个 code 节点 (status=running)
→ 设 _call_id ContextVar 指向此节点
函数体里 runtime.exec() → 在当前 _call_id 下写一个 llm 节点
→ 节点的 caller = _call_id
退出函数(return / except) → 回填同一 code 节点的 output / status
expose="hidden" 时跳过 code 节点写入(但 _call_id 仍设了一个 phantom
id,以便函数体内 LLM 调用有 frame 可参照)。
为什么不只在退出时写一个完成态节点?
- 函数还在跑时 webui visualizer 需要立刻能看到"它在跑"(显示 spinner)
- 异常退出时也要有节点存在(才能记错误信息)
写两次(entry + exit 回填)比写一次(完成时)更适合实时可观察。
嵌入接缝#
核心可以嵌在不是 OpenProgram 的宿主里运行——别人的服务或 agent 框架把
Agent method + 执行 DAG 当作一个组件来用(见
嵌入到你自己的技术栈)。
这份契约由 tests/component/runtime/test_standalone_embed.py 执行,不只是纸面声明:
- 库面永不 import UI 表面。
import openprogram以及一次完整的Runtime(call=...).exec()往返,不得拉起webui、FastAPI、Uvicorn 或 Textual。 - 没有隐式状态路径。 用
SessionStore(root_path=...)+session_scope(store, session_id)时,整个运行不查openprogram.paths(~/.openprogram);不绑 store 时照常执行、只是不持久化。
平台需要伸进核心的地方,方向一律倒转成注册钩子,与 add_pre_invocation_hook
同款:
| 接缝 | 核心默认值 | 平台注册方 |
|---|---|---|
set_cancellation_check |
空实现——没有任何东西会取消 | openprogram.agent.run_control 在 import 时注册精确 execution 检查 |
set_session_id_provider |
None——runtime.can_ask() 为 False |
openprogram.agent.run_control 提供当前会话 id |
session_scope(store, id) |
未绑定——不持久化 | dispatcher 自己绑定每 turn 的 store |
为什么用钩子而不是 Web UI 取消模块? 原来的 session 级模块重复维护取消状态,
并让核心依赖 Web UI。现在由 run_control 检查精确 execution token,同时保持依赖
方向为平台 → 核心。
exec() 里的 ImportError 按永久错误处理,不算瞬态。 retry 循环把它归为不可
重试:缺失的子系统不会因为重试而出现,烧完整个退避表只是把 0 秒的失败拖成约 52 秒。
相关实现文件#
openprogram/agentic_programming/runtime.py— Runtime 基类、exec/_call协议、retry 循环openprogram/agentic_programming/call_state.py— 装饰器 /_inject_runtime/_call_id/_current_runtimeContextVaropenprogram/providers/__init__.py—detect_provider/create_runtime自动检测openprogram/providers/<vendor>/runtime.py— 各 provider 的_call实现openprogram/context/nodes.pyrender_context— DAG → reads 计算(render_range的实际语义)openprogram/context/render.pyrender_dag_messages— reads → provider messages 转换
相关设计#
-
dag/overview.md— 权威:历史记录数据模型(一整张图 / 三种节点 user·llm·code / caller+predecessor 边 / render_context 上下文检索) -
dag/rendering.md— 权威渲染规范:布局/连线/图例/默认可见性,12 场景 -
dag/branch-collaboration.md— 分支协作(通信 / 派活 / 合并)设计与实现步骤 -
execution/agent-call-flow.md— 调用流程骨架(turn / loop,跟节点模型正交) -
execution/control.html— 权威设计:统一暂停、继续、单步、调整、取消、检查点、revision 与恢复合同 -
execution/dispatcher-split.md— 将agent/dispatcher.py拆分为按职责划分的包(遵循「单文件不超过 1000 行」规则) -
operations/file-management.html— 文件归因、Review、Undo、Restore、分支对齐和多 agent 所有权的权威设计 -
session/— Session 子系统:数据模型、存储、命名、列举、生命周期、广播 -
嵌套
llm()流式设计 — 完整设计:RuntimeBlock 按节点 token 流(协议、FE 缓冲、落盘、取消/重连、in-process+subprocess) -
operations/user-input-requests.md— 暂停正在运行的函数以向用户提问(runtime.ask/confirm),问题注册表 + WS/REST 协议 + 子进程桥接 -
agent-collaboration.md— 权威:agent 协作收敛成一个分支间通信原语 —— 四个域、工具面、三个预算(工具面图示、八家参考实现对照) -
sandbox-architecture.html— 唯一权威执行安全设计:Authority 权限档、Permission 模式与审批、沙箱执法、框架对照和实现证据。permission-model.md与sandbox.md只保留为稳定旧链接入口。