数据截至 (上游 commit 38277815ed44)
主循环:运行时编排与 Grounding Agent 回合循环
30 秒导读: OpenSpace 把"跑完一个任务"拆成两层。外层是运行时编排:
OpenSpace.execute(ExecutionRequest)(application.py:772)只是薄壳,委托给ExecutionLifecycle.execute(runtime/execution_lifecycle.py:80)做会话准备、录制、回合触发与收尾。 内层是GroundingAgent.process(grounding_agent.py:644)的回合循环:一个 "想 → 调工具 → 看结果 → 再想"的迭代,直到模型不再调用工具即判定完成。 本章只讲这条主线;技能怎么被发现、怎么进化、工具怎么被预选,分别在 第2章、第3章、第4章。
上游重构提示: 本章旧版讲的"两阶段执行(技能引导 → 失败清空 workspace 回退纯工具)"与
<COMPLETE>完工令牌已被移除:旧总控tool_layer.py拆成了application.py+runtime/+agents/turns/; 完工判定改为"模型没有工具调用即完成"(_build_final_result的 docstring,grounding_agent.py:1109),<COMPLETE>仅作为废弃常量保留给 ShellAgent 兼容 (prompts/grounding_agent_prompts.py:72)。本章按新版源码重写。
1. 这是什么(先建立直觉)
OpenSpace 是一个自进化技能引擎(全景见 index.md)。当你给它一个任务——比如 "把这个 CSV 转成折线图并存到 workspace"——它需要一条清晰的主线把任务跑完。
这条主线要回答三个问题,正好是本章的三块内容:
| 问题 | 谁负责 | 在哪 |
|---|---|---|
| 一个任务怎么被跑完? | GroundingAgent.process 的回合循环 | agents/grounding_agent.py:644 |
| 运行时怎么编排一次任务? | ExecutionLifecycle.execute + 一组协作器 | runtime/execution_lifecycle.py:80 |
| 结束后怎么收尾? | ExecutionFinalizer.finalize + 任务后进化 | runtime/execution_finalizer.py:23 |
一句类比:ExecutionLifecycle 像项目经理——备场地(会话/workspace)、开录像(录制)、
派工(回合循环)、写收尾报告;GroundingAgent 像干活的工人——反复"动手、看效果、再动手",
哪一轮不再伸手要工具,活儿就算干完了。
本节不碰代码细节。记住一句话就够:外层编排、内层执行,分工明确。
2. 顶层全景:execute() 一条主线
OpenSpace.execute(request)(openspace/application.py:772)接收一个
ExecutionRequest(runtime/execution_request.py:11,字段含 prompt、workspace_dir、
session_id、max_iterations、capture_skill_dir、resume、abort_event 等),
校验已初始化后直接委托 self._runtime.execute(request)(application.py:785)。
运行时侧的编排者是 ExecutionLifecycle.execute(runtime/execution_lifecycle.py:80),
骨架仍是一个大 try / except / finally:try 里跑任务、finally 里无论成败都做收尾。
怎么读下面这张图: 从上往下是时间顺序;中间是回合循环的心跳;右侧标注关键符号。
OpenSpace.execute(ExecutionRequest) [application.py:772]
│ 薄壳转发
▼
ExecutionLifecycle.execute [execution_lifecycle.py:80]
│
├─ 等待空闲 wait_until_idle (32) ← 单实例串行:上一任务没完先排队
├─ 构建上下文 build_initial_context ← task_id / 会话 / workspace
├─ start_recording ← 起录制 (RecordingManager)
├─ resolve_workspace → capture_skill_dir(捕获技能的默认落点)
├─ scheduler.install_ensure / maybe_start [execution_scheduler.py]
├─ resolve_max_iterations (241) ← 结算迭代预算
│
├─ run_turns [execution_events.py:72]
│ └─ TurnRunner.run [turn_runner.py:30]
│ └─ GroundingAgent.process [grounding_agent.py:644]
│ · construct_messages [message_builder.py:210]
│ · skill_listing 目录注入 [protocol.py:247]
│ · DiscoverSkills 预取 [protocol.py:358]
│ · while: 模型调用 → 工具回合 → 看停止条件
│ (没工具调用 ⇒ completed) [model_call_controller.py:1089]
│ · _build_final_result [grounding_agent.py:1095]
│
└─ finally → ExecutionFinalizer.finalize [execution_finalizer.py:23]
· 落证据(task_finished_pre_persist / task_session_persisted)
· persist 会话 + 各类 checkpoint 扫描
· 任务后进化:inline 或 background 两种模式
· state.running=False 放行下一个任务
部件一句话职责:
| 部件 | 干什么 | 位置 |
|---|---|---|
OpenSpaceConfig | dataclass 参数总表(模型、预算、录制、进化开关) | openspace/application.py:121 |
OpenSpaceRuntime | 持有全部服务与可变状态;initialize_services 装配 | openspace/runtime/app.py:146 |
ExecutionLifecycle | 单任务编排主线 | openspace/runtime/execution_lifecycle.py:42 |
ExecutionEventEmitter | 发任务开始/进度事件;run_turns 触发回合 | openspace/runtime/execution_events.py:10 |
GroundingAgent.process | 真正的回合执行循环 | openspace/agents/grounding_agent.py:644 |
ExecutionFinalizer | 收尾:证据、持久化、任务后进化调度 | openspace/runtime/execution_finalizer.py:13 |
下面三节顺着这条主线往下钻。
3. 门面与初始化:主线之前的两步
3.1 OpenSpaceConfig —— 一张参数总表
所有可调项集中在 @dataclass OpenSpaceConfig(openspace/application.py:121)。影响主线的几个字段:
| 字段 | 默认值 | 对主线的影响 |
|---|---|---|
post_execution_mode | "inline" | 任务后进化是"内联等它跑完"还是"甩后台"("background") |
post_execution_timeout_s | 0.0 | 内联任务后进化的时限;超时打 post_execution_timed_out 标记 |
max_iterations | 15 | 回合循环的默认预算(见 §4.4) |
workspace_dir | None | 任务产物与捕获技能目录落哪 |
evolution_engine_enabled | False | 是否启用新版进化流水线(第3章);关闭则退回 legacy 分析 |
__post_init__(application.py:563-564)仍只硬性检查一件事:llm_model 为空直接报错——模型是硬依赖。
3.2 OpenSpaceRuntime.initialize_services —— 把零件装好
initialize()(application.py:713)转发到 OpenSpaceRuntime.initialize_services
(openspace/runtime/app.py:324),在第一次跑任务前调用一次(或 async with 自动触发),
按顺序装配 LLM 客户端、接地层、录制、GroundingAgent、技能引擎、以及新版进化所需的一整套服务
(evidence store、trigger engine、evolution engine、decision engine 等,装配细节见第3章)。
一个值得记的设计依旧:技能引擎是"可选增强"。grounding_config.skills.enabled
开着才建 SkillRegistry(runtime/app.py:776-781);进化引擎由
evolution_engine_enabled 单独开关(runtime/app.py:644-645),关掉时任务后走
legacy 的 _run_legacy_execution_analysis(runtime/app.py:2017)兜底——主线照跑。
4. 内层:GroundingAgent.process 怎么把任务跑完
先讲内层再讲外层调度,因为外层每次任务就是"调用 process 一次"。
4.1 process 的一趟流程
process(context)(openspace/agents/grounding_agent.py:644)接收一个 context 字典
(里面有 instruction、workspace_dir、max_iterations 等),返回一个结果字典。开跑前做四件 准备
(openspace/agents/turns/loop.py):
process(context) [grounding_agent.py:644] 实现主体在 agents/turns/loop.py
│
├─ ① 生命周期 hooks(UserPromptSubmit 可拦停) loop.py:547-595
├─ ② construct_messages 组装初始消息 loop.py:612 [message_builder.py:210]
│ system 提示 + 会话历史 + 用户指令
├─ ③ 技能目录注入 skill_listing delta loop.py:622 [protocol.py:247]
│ + DiscoverSkills 预取(turn0_prefetch) loop.py:624-632 [protocol.py:358]
└─ ④ while current_iteration < max_iterations loop.py:638(主循环)
调一次模型 → 分类响应 → 需要则执行工具回合 → 检查停止条件
↓
_build_final_result → status: success / incomplete [grounding_agent.py:1095]
4.2 回合循环:一次"想—做—看"的心跳
主循环(openspace/agents/turns/loop.py:638 起)每一圈依次经过几个控制器,
每个控制器是 agents/turns/ 下的独立模块:
- 消息输入:先排空外部注入消息(
_drain_messages)、注入 bench 收尾提示(若有)。 - 上下文压缩:
compaction_controller.maybe_time_based_microcompact(基于时间的微压缩, 不调 LLM,只清旧工具结果)和maybe_auto_compact(自动压缩) (openspace/agents/turns/compaction_controller.py:55、:97)——上下文膨胀在每圈开头治理。 - 工具刷新:
tool_turn_controller.refresh_tools_for_iteration(openspace/agents/turns/tool_turn_controller.py:52) 按需重新预选工具(第4章)。 - 模型调用:
model_call_controller.call_model_with_recovery(openspace/agents/turns/model_call_controller.py:360) 带恢复逻辑调一次模型(API 错误、max_output_tokens截断都有恢复路径,恢复上限MAX_OUTPUT_TOKENS_RECOVERY_LIMIT = 3,grounding_agent.py:26)。 - 响应分类:
handle_model_response(同文件:636)把响应分文末/带工具调用两类。 - 工具回合:
execute_tool_turn(tool_turn_controller.py:258)执行模型要的工具、 把结果拼回消息。
4.3 完工判定:没有工具调用即完成(不再有 <COMPLETE>)
这是新版最重要的语义变化。旧版要求模型显式吐出 <COMPLETE> 令牌才算成功;新版对齐
Claude Code 式的回合协议:模型哪一轮只给文字、不再要工具,任务即完成。
判定发生在 handle_model_response(model_call_controller.py:636)的末端:
- 模型响应没有工具调用 → 先过
handle_stop_hooks(Stop hooks 可以拦着不让停,model_call_controller.py:1008-1033)→ 再查 token 预算(预算未满可以注入"继续"提示,model_call_controller.py:1036-1076)→ 都通过则stop_reason_final = "completed"并 break(model_call_controller.py:1088-1090)。 - 带工具调用 → 进入工具回合,下一圈继续。
<COMPLETE> 常量还在但已标注废弃:TASK_COMPLETE = "<COMPLETE>" # DEPRECATED - kept only for backward compat with ShellAgent and skill_engine_prompts(openspace/prompts/grounding_agent_prompts.py:72)。
停止原因(stop_reason)一览(model_call_controller.py 与 stop_policy.py):
| stop_reason | 含义 |
|---|