跳到主要内容

数据截至 (上游 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,字段含 promptworkspace_dirsession_idmax_iterationscapture_skill_dirresumeabort_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 放行下一个任务

部件一句话职责:

部件干什么位置
OpenSpaceConfigdataclass 参数总表(模型、预算、录制、进化开关)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_s0.0内联任务后进化的时限;超时打 post_execution_timed_out 标记
max_iterations15回合循环的默认预算(见 §4.4)
workspace_dirNone任务产物与捕获技能目录落哪
evolution_engine_enabledFalse是否启用新版进化流水线(第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 字典 (里面有 instructionworkspace_dirmax_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.pystop_policy.py):

stop_reason含义位置
completed模型不再调工具,自然收尾model_call_controller.py:1089
max_turns跑满 max_iterationsstop_policy.py:194 max_iterations_stop_reason
empty_response连续多次空回复model_call_controller.py:654
aborted外部 abort 信号stop_policy.py:142 abort_stop_reason
model_error / prompt_too_longAPI/上下文超限且不可恢复model_call_controller.py:494:520
stop_hook_preventedStop hook 阻止继续model_call_controller.py:1025

4.4 迭代预算从哪来

max_iterations 有两处协调:

  • 外层结算:ExecutionContextManager.resolve_max_iterations(openspace/runtime/execution_context.py:241) 把请求值与配置值取大,防止调用方把预算调得过低而"饿死"智能体;
  • 内层读取:TurnState.from_agent_context(openspace/agents/turns/state.py:55) 从 context 建回合状态(含预算、空回复计数、token 预算追踪器)。

4.5 结果如何定性:success vs incomplete

循环结束后,_build_final_result(openspace/agents/grounding_agent.py:1095)给任务定性:

  • success:stop_reason 属于 ("completed", "stop_hook_prevented", "bench_finalize_budget") (grounding_agent.py:1122-1126)。响应文本直接取模型最后一条 assistant 消息—— 不额外再调一次 LLM 做总结。
  • incomplete:stop_reason 是 max_turns / empty_response,附一条 warning 提示可能需要更多步数或澄清(grounding_agent.py:1153-1165)。
  • 其余(aborted、model_error 等):status 直接用 stop_reason 命名,并带上 error 字段。

结果的 active_skills_extract_skill_ids_from_messages(grounding_agent.py:1170) 从消息里提取——技能协议把"用过哪些技能"记录在对话轨迹里(第2章)。

4.6 技能如何"接进"这条循环

技能不再在执行前整段注入,而是走Skill Protocol 三个挂载点(细节见第2章):

  1. 轻量目录——SkillListingService.get_listing_delta(openspace/skill_engine/protocol.py:247) 在回合开始往 messages 追加一条只含技能名+描述的 skill_listing 附件(loop.py:622)。
  2. 按需发现——SkillDiscoveryService(protocol.py:358)驱动的 DiscoverSkills 工具; turn0 还会做一次预取(loop.py:624-632,source="turn0_prefetch")。
  3. 按需加载全文——SkillTool(protocol.py:735,工具名就叫 Skill)是唯一默认加载 SKILL.md 全文的路径。

跨回合还有协议状态恢复:restore_skill_state_from_messages(loop.py:616-621)从历史 消息里恢复"已披露/已调用过哪些技能",避免重复注入。

4.7 工具从哪来:每圈刷新的预选

refresh_tools_for_iteration(tool_turn_controller.py:52)负责维护本回合的活跃工具集: 初始集来自接地层的 get_tools_with_auto_preselection(第4章),回合中模型也可用 tool_search 发现延迟加载的工具(grounding/core/tool_discovery.py:18ToolSearchTool)。

4.8 _check_workspace_artifacts:任务可能已经做完了?

_check_workspace_artifacts(grounding_agent.py:1050)在开跑前扫一眼 workspace 目录: 列出已有文件、标出最近 600 秒内改动过的、并用正则从指令里抠出被提到的文件名去和现有文件比对。 目的是避免重复劳动——如果目标文件已经在,模型也许一步就能确认完工。这一机制从旧版完整保留。


5. 外层:运行时编排(一次任务的生命周期)

理解了 process 是"调一次跑一趟",外层就是把它嵌进一个完整的服务生命周期。

5.1 排队与准备

ExecutionLifecycle.execute(execution_lifecycle.py:80)开头:

  • 串行闸门:wait_until_idle(execution_context.py:32)等上一个任务结束 (state.running / state.task_done,单实例串行语义保留)。
  • 会话准备:session_runtime.prepare 处理新会话/恢复(resume)/分叉。
  • 录制启动:start_recording(task_id, task),RecordingManagerconversations.jsonl / metadata.json
  • workspace 解析:resolve_workspace;capture_skill_dir 解析出捕获技能的落点 (优先 ExecutionRequest.capture_skill_dir → config → 环境变量 → <workspace>/.openspace/skills, execution_lifecycle.py:72 _default_capture_skill_dir)。

5.2 回合触发与事件

events.run_turns(openspace/runtime/execution_events.py:72)发完进度事件后,把预算塞进 execution_context["max_iterations"],交给 TurnRunner.run(runtime/turn_runner.py:30)—— 一个薄边界,直接调 grounding_agent.process 并校验返回是 dict。事件流 (task_startedagent_task_update 等)让 TUI/宿主能实时观察进度。

5.3 异常与取消

try 块里两类异常分别处理(execution_lifecycle.py:223-287):

  • CancelledError:发 background_session_update(status="cancelled"),结果置 cancelled, 记下异常在 finally 之后重新抛出(不让调用方误以为正常完成)。
  • 其他异常:发 task_error,结果置 error 带上截断的 traceback。

6. finally:录制落盘与任务后进化

不管 try 里成功、失败、还是被取消,finally(execution_lifecycle.py:289-297)都会跑 ExecutionFinalizer.finalize(openspace/runtime/execution_finalizer.py:23)。它干四件事:

  1. 组装 final_result:合入 task_idsession_idexecution_timeskills_used (取 active_skills)、evolved_skillscapture_skill_dirsession_capability_state (execution_finalizer.py:36-53)。
  2. 落证据与持久化:按顺序发 task_finished_pre_persist 证据(execution_finalizer.py:54)→ 排空记忆后台任务 → session_runtime.persist 持久化会话 → 扫描 session/skill/tool-quality 三类证据 checkpoint → 发 task_session_persisted 证据(execution_finalizer.py:77)→ 扫 QUALITY_SIGNAL checkpoint (execution_finalizer.py:85)。这些证据正是第3章触发器引擎的输入。
  3. 任务后进化:post_execution_mode == "inline" 时直接 await run_post_execution_tasks (带 post_execution_timeout_s 超时保护,超时打 post_execution_timed_out, execution_finalizer.py:90-126);== "background"schedule_post_execution_tasks 甩给后台 supervisor(runtime/app.py:2264)。 取消的任务不跑任务后进化
  4. 释放运行锁:state.running = Falsestate.task_done.set() 放行下一个排队任务 (execution_lifecycle.py:293-296)。

关于 run_post_execution_tasks(runtime/app.py:1966)对主线的意义,记三点:

  • 有进化引擎时:_ensure_analysis_trigger_jobs(runtime/app.py:2042)按 task_session_persisted checkpoint 建 TriggerJob,drain_evolution_jobs(runtime/app.py:2068) 认领并执行——完整机制见第3章
  • 没有进化引擎时:退回 _run_legacy_execution_analysis(runtime/app.py:2017)直接调 ExecutionAnalyzer.analyze_execution,异常只记 debug 日志。
  • 产出的 evolved_skill_records 会汇进 final_result["evolved_skills"] 返回给调用方—— "跑完顺带自我进化"这条闭环的落点就在这里

一个健壮性约定与旧版一致:任务后进化整体包在 try/except 与超时里, 进化再重要,也不能拖垮把任务跑完这件正事


7. 支撑件:base.pyturns/message_utils.py

7.1 BaseAgent —— 智能体基类

GroundingAgent 继承自 BaseAgent(openspace/agents/base.py:18),一个 ABC。基类提供共用管线:

  • 持有 name / backend_scope / llm_client / grounding_client / recording_manager / step
  • 两个抽象方法子类必须实现:process(base.py:83)和 construct_messages(base.py:87)。
  • 通用工具:get_llm_response(base.py:94)薄封装一次 LLM 调用;response_to_dict(base.py:161) 把可能裹着 ```json 围栏的模型输出解析成字典;increment_step(base.py:193)推进步数。
  • AgentRegistry(base.py:214)按类名登记/取类。

7.2 agents/turns/message_utils.py —— 消息卫生

旧的 cap_message_content / truncate_messages 已移除,消息体量治理改由压缩控制器 (§4.2 的 microcompact / auto compact)承担。现存的 message_utils 负责外部输入规整:

函数作用位置
normalize_external_history把外部传入的会话历史规整成 {role, content},只收 user/assistantturns/message_utils.py:16
build_channel_context_message把通信渠道元数据(平台/会话/附件)拼成一条提示turns/message_utils.py:311

另有一套技能附件的清洗/保留逻辑(_sanitize_skill_attachment 等,turns/message_utils.py:187), 服务跨回合恢复技能协议状态。


8. 边界与局限(诚实)

  • 完工判定强依赖"模型不再调工具"。 模型若习惯性继续要工具(哪怕活已干完),任务会跑到 max_turnsincomplete;反之模型过早只回文字也可能"假完成"。护栏是 Stop hooks、 token 预算续跑与迭代预算,而非语义校验。
  • 回退机制已移除。 旧版"技能引导失败 → 清空 workspace → 纯工具重跑"的两阶段设计不复存在; 新版技能以协议方式渐进进入对话,失败不再触发整趟重跑(旧机制仅存于历史版本)。
  • 单实例串行执行。 wait_until_idle + state.running 让并发任务排队,它不是为高并发设计的。
  • 技能引擎与进化引擎均可选。 未启用时主线退化为纯工具执行 + legacy 分析,不影响"把任务跑完"。
  • 任务后进化可能超时/半成品。 inline 模式受 post_execution_timeout_s 限时,超时即返回 post_execution_timed_out=True;后台模式的作业有恢复机制(evolution/recovery.py)但仍可能延后完成。

9. 代码地图(导航索引)

主题文件路径符号名
配置总表openspace/application.py:121OpenSpaceConfig
顶层门面 executeopenspace/application.py:772OpenSpace.execute
一次性装配openspace/runtime/app.py:324OpenSpaceRuntime.initialize_services
单任务编排openspace/runtime/execution_lifecycle.py:80ExecutionLifecycle.execute
串行闸门openspace/runtime/execution_context.py:32ExecutionContextManager.wait_until_idle
迭代预算结算openspace/runtime/execution_context.py:241resolve_max_iterations
回合触发openspace/runtime/execution_events.py:72run_turns
回合边界openspace/runtime/turn_runner.py:30TurnRunner.run
执行智能体循环openspace/agents/grounding_agent.py:644GroundingAgent.process
回合主循环实现openspace/agents/turns/loop.py:638(主 while 循环)
初始消息组装openspace/agents/turns/message_builder.py:210construct_messages
技能目录注入(回合头)openspace/agents/turns/loop.py:622_append_skill_listing_delta 调用点
技能发现预取(turn0)openspace/agents/turns/loop.py:624_append_skill_discovery_delta_async
回合状态openspace/agents/turns/state.py:16TurnState.from_agent_context
模型调用+恢复openspace/agents/turns/model_call_controller.py:360call_model_with_recovery
响应分类/完成判定openspace/agents/turns/model_call_controller.py:636handle_model_response
completed 停止点openspace/agents/turns/model_call_controller.py:1089(stop_reason_final = "completed")
停止策略openspace/agents/turns/stop_policy.py:194max_iterations_stop_reasonabort_stop_reason
上下文压缩openspace/agents/turns/compaction_controller.py:55:97maybe_time_based_microcompact / maybe_auto_compact
工具回合openspace/agents/turns/tool_turn_controller.py:258execute_tool_turn
结果定性openspace/agents/grounding_agent.py:1095GroundingAgent._build_final_result
workspace 产物检查openspace/agents/grounding_agent.py:1050GroundingAgent._check_workspace_artifacts
废弃完工令牌openspace/prompts/grounding_agent_prompts.py:72GroundingAgentPrompts.TASK_COMPLETE(DEPRECATED)
任务收尾openspace/runtime/execution_finalizer.py:23ExecutionFinalizer.finalize
任务后进化编排openspace/runtime/app.py:1966run_post_execution_tasks
legacy 分析兜底openspace/runtime/app.py:2017_run_legacy_execution_analysis
智能体基类openspace/agents/base.py:18BaseAgent
消息卫生工具openspace/agents/turns/message_utils.py:16:311normalize_external_history / build_channel_context_message

相邻章节: 总览见 index.md;技能协议与发现见 第2章; 触发器作业与自进化见 第3章;工具统一后端与预选见 第4章;MCP 服务与云端社区见 第5章