Agent Runtime 解耦审查问题修复计划¶
状态:Implemented(F1–F6 全部落地,含针对性回归测试;2026-08-08) 范围:修复解耦重构审查中已核实、具有实际正确性或可诊断性价值的问题。
原则:先修高风险生命周期问题,再收敛线程归属,最后完成架构依赖迁移。
1. 目标¶
本计划处理以下六项已确认问题:
| ID | 优先级 | 问题 | 主要影响 |
|---|---|---|---|
| F1 | P0 | 正常完成路径以 1.5 秒等待 bound_future,超时会被当作 turn failure |
正常请求可能被误报失败 |
| F2 | P0 | TextualTurnEventRenderer 渲染异常后静默关闭 |
后续工具和回答消失,且缺乏诊断 |
| F3 | P1 | RuntimeManager.submit 在 acquire 后、try 前存在 lock 泄漏窗口 |
单 session 后续提交可能永久阻塞 |
| F4 | P1 | tool-output refresh 的 pending/dirty 状态跨线程读写 | 刷新合并存在竞争和状态不一致风险 |
| F5 | P1 | MCP 后台 worker 直接写 _mcp_reloading |
UI 状态线程归属不一致 |
| F6 | P2 | AgentTurnRuntime 默认动态导入 synapse.ui.stream |
runtime/UI 解耦不彻底,导入护栏存在盲区 |
2. 非目标¶
- 不修改模型是否并行发出 tool calls 的策略。
- 不把跨模型响应的连续工具调用强行合并为同一 UI 工具组。
- 不重写
afe9ac2等已存在提交历史。 - 不在本计划内重命名或拆分
TurnPersistenceController;该项是低风险职责清理,可单独重构。 - 不引入 daemon、多进程或新的 RPC 边界。
3. 实施阶段¶
阶段 A:turn 正确性与可诊断性¶
F1:修复正常完成超时误判¶
涉及文件:
src/synapse/runtime/streaming/runtime.pytests/test_runtime_streaming.py- 必要时增加专用 async cleanup fixture
实施要求:
- 区分“事件生产完成”和“graph/checkpointer 清理完成”。
- 正常路径不得因固定 1.5 秒清理等待而产生
TimeoutErrorturn failure。 - 取消路径仍必须等待 graph 退出,不能提前报告 cancelled 后留下后台写入。
- 不允许无界卡死;如必须设置上限,应形成独立 cleanup timeout 状态或诊断,而不是伪装成 provider failure。
- worker thread 与 bound loop 两条路径保持一致语义。
测试:
- graph 已发完事件但清理超过 1.5 秒,turn 仍正常完成。
- graph 主体真实抛错时仍传播原始异常。
- cancel 时等待 cleanup 完成后才结束。
- cleanup 不留下活跃 task/thread。
F2:Renderer 异常诊断¶
涉及文件:
src/synapse/ui/turn/event_renderer.pytests/test_turn_event_renderer.py- 复用项目现有 logging/observability 设施
实施要求:
- 捕获渲染异常时记录 bounded warning。
- 日志至少包含
thread_id、turn_id、event kind、sequence 和异常类型。 - 不记录完整 payload、用户正文、工具输出或凭据。
- renderer 仍可关闭,且异常不得反向导致 Agent turn 失败。
- 如项目已有 debug capture,按既有模式接入,不新增重复日志系统。
测试:
- host 在工具事件渲染时抛错,renderer 关闭但 Agent/event producer 不受影响。
- warning 包含定位字段,不包含 payload 正文。
- stale generation 和重复 sequence 仍保持静默忽略,不误报异常。
阶段 A 门禁:
uv run --no-sync pytest tests/test_runtime_streaming.py tests/test_turn_event_renderer.py tests/test_agent_turn_runtime.py -q
uv run --no-sync ruff check src/synapse/runtime/streaming src/synapse/ui/turn tests/test_runtime_streaming.py tests/test_turn_event_renderer.py tests/test_agent_turn_runtime.py
阶段 B:并发状态生命周期¶
F3:闭合 RuntimeManager.submit 资源生命周期¶
涉及文件:
src/synapse/runtime/sessions/manager.pytests/test_runtime_manager.py
实施要求:
submit_lock.acquire()成功后立即进入受保护的try/finally。_get_semaphore()、mark_queued()、semaphore.acquire()、mark_starting()、session.submit()任意失败均正确回滚。- submit lock、semaphore permit、queued/starting 状态各自最多释放或清理一次。
- 不改变同一 session 单 turn、跨 session 有界并发的现有语义。
测试:
mark_queued()抛错后可再次提交。- semaphore 获取期间取消不会泄漏 lock/permit。
mark_starting()和session.submit()抛错后状态恢复。- 正常 settlement 后资源只释放一次。
F4:统一 tool-output refresh 的线程归属¶
涉及文件:
src/synapse/ui/chrome/controller.pysrc/synapse/ui/tui.py(仅必要状态初始化)- 对应 chrome/tool-output 测试
首选方案:
- pending/dirty 的读写全部封送到 Textual UI thread;worker 只计算 SQLite snapshot,通过
call_from_thread发布结果。
备选方案:
- 若 callback 无法可靠封送,则使用小范围 lock 保护 pending/dirty,并避免持锁调用 UI 或 SQLite。
实施要求:
- 消除跨线程 check-then-set 竞争。
- 切 session 时旧 thread 结果不得覆盖新 thread chrome。
- dirty 期间至少触发一次后续刷新,不要求一事件一刷新。
- 保持 debounce 和 bounded refresh 行为。
测试:
- refresh pending 时多个 metrics changed 合并为一次后续刷新。
- 切 session 后旧结果被丢弃并刷新当前 session。
- worker 完成和新 dirty 信号交错时不丢最终刷新。
F5:MCP reload 状态回到 UI thread¶
涉及文件:
src/synapse/ui/dialogs/controller.pytests/test_dialogs.py或对应 MCP dialog/controller 测试
实施要求:
_mcp_reloading的 UI 可见状态只在 UI thread 写入。- success、slash error、exception 三条路径统一通过一个 finish helper 收尾。
- finish helper 同时恢复 activity,并确保只执行一次。
- 不扩大锁范围,不在 UI thread 执行 MCP reload 或配置 I/O。
测试:
- toggle/save/reload 的成功与失败均清理 reloading 状态。
- worker 不直接修改 UI 状态。
- 重复点击在 reload 期间仍被抑制。
阶段 B 门禁:
uv run --no-sync pytest tests/test_runtime_manager.py tests/test_dialogs.py -q
uv run --no-sync ruff check src/synapse/runtime/sessions/manager.py src/synapse/ui/chrome/controller.py src/synapse/ui/dialogs/controller.py tests/test_runtime_manager.py tests/test_dialogs.py
阶段 C:完成 runtime/UI parser 依赖迁移¶
F6:移除 runtime 到 synapse.ui.stream 的默认依赖¶
涉及文件预期:
- 将
stream_agent的语义解析主循环迁至src/synapse/runtime/streaming/下的明确模块 src/synapse/ui/stream.py保留兼容 wrapper/re-exportsrc/synapse/runtime/agent_loop/turn.py- streaming、CLI、TUI 和 import-boundary 测试
实施步骤:
- 先移动实现,不重写解析算法,保持 diff 可审查。
- runtime 模块只依赖 runtime/domain 类型,不导入 Textual 或
synapse.ui。 - Rich/Textual sink 默认选择留在 CLI/TUI 装配层;headless runtime 使用 no-op renderer + semantic event sink。
AgentTurnRuntime从 runtime 路径直接导入 parser,或在构造时强制注入;删除动态字符串导入 UI 的 fallback。synapse.ui.stream.stream_agent保留兼容导出,避免破坏现有扩展和测试。- 增加整个
src/synapse/runtime/的 AST/import 护栏,而非只检查runtime/streaming/。
测试:
- runtime 包导入时
sys.modules不出现textual和synapse.ui。 - headless turn 的 answer/reasoning/tool/usage/cancel/HITL 语义 trace 不变。
- CLI Rich sink 与 TUI event renderer 行为不变。
- 旧导入路径仍可用。
阶段 C 门禁:
uv run --no-sync pytest tests/test_runtime_streaming.py tests/test_stream_semantic_fixtures.py tests/test_agent_turn_runtime.py tests/test_stream_tool_items.py tests/test_turn_event_renderer.py tests/test_stream_cancel.py -q
uv run --no-sync ruff check .
4. 实施顺序与提交建议¶
建议拆为三个独立提交:
fix(runtime): harden stream completion and renderer diagnosticsfix(runtime): close session and UI worker state racesrefactor(runtime): move stream parser out of UI package
阶段 A、B 可独立合入;阶段 C 风险较高,必须在 A、B 稳定后实施。每个提交只包含对应代码和测试,避免再次形成难以回滚的大提交。
5. 最终验证¶
完成所有阶段后执行:
还需进行一次人工 TUI 冒烟:
- 单工具调用正确显示。
- 同一模型响应中的多个工具调用归为一个工具组。
- 思考 → 工具 → 思考 → 工具保持两个真实批次,不错误合并。
- 工具失败后后续回答仍可渲染。
- 会话 detach/attach 后工具 item 生命周期可 replay。
- MCP reload 成功和失败后 UI 均恢复可操作。
- 使用 async checkpointer 的 turn 在慢 cleanup 下不误报失败。
6. 完成定义¶
- F1-F6 均有针对性回归测试。
- 正常 turn 不因固定 cleanup timeout 被误判失败。
- renderer failure 有安全、可定位且不泄露 payload 的日志。
- session submit 不泄漏 lock、semaphore 或 queued 状态。
- tool-output 与 MCP UI 状态遵守明确线程归属。
AgentTurnRuntime默认路径不再导入synapse.ui。- 兼容导入、CLI 和 TUI 行为不回退。
- Ruff、全量 pytest、MkDocs build 和人工 TUI 冒烟通过。