# SSE 事件总线 > 15 类事件的完整规格。每条带 event 名、payload schema、发出时机、消费者。前端 Dashboard 是主消费者;CLI 工具也可订阅做实时监听。9 个核心 + 6 个 Agent 持久化事件。 ## 订阅协议 ```js # 普通订阅 GET /api/devloop/projects/:pid/stream Accept: text/event-stream Authorization: Bearer # 断线重连 GET /api/devloop/projects/:pid/stream Last-Event-ID: 1234 # 上次收到的最大 id # 服务端响应(标准 SSE 格式) HTTP/1.1 200 OK Content-Type: text/event-stream Cache-Control: no-cache Connection: keep-alive id: 1235 event: unit.state data: {"unit_id":"auth-be","from":"coding","to":"testing"} :keepalive # 每 25s 一次 ``` ## 核心事件(9 类) ### ① `prd.updated` · PM 写完 PRDL3 ```js { "rev": 3, "by": "pm-agent", "origin": "yibo", "changeSummary": "+ Section: OAuth login", "hasConflict": false } ``` **发出时机:**PM Agent 写完 prd.html,DocStore 校验通过后 **消费者:**Dashboard PRD diff 面板 · Architect 队列触发 ### ② `sdd.updated` · Architect 写完 SDDL3 + L2 ```js { "rev": 5, "mode": "generate" | "patch", "derived_from": "prd@rev3", "units_diff": { "added": ["oauth-be", "oauth-fe"], "changed": [], "removed": [] } } ``` ### ③ `unit.state` · Unit 状态机转换L1 ```js { "unit_id": "auth-be", "from": "coding", "to": "testing", "retries": 1, "reason": "coder_done" } ``` **消费者:**Dashboard DAG 节点重染色 ### ④ `agent.stream` · Agent stdout/stderr 流L1 + L2 ```js { "agent": "tester", "unit_id": "auth-fe", "stream": "stdout" | "stderr", "chunk": "FAIL tests/login.spec.ts > invalid pwd\n" } ``` **注:**高频事件。前端 LiveAgentStream 用 RAF 批渲染,避免每条都触发 reactive 更新。 ### ⑤ `integration.result` · L2 结果L2 ```js { "run_id": "run-int-20260513-1031", "ok": false, "failures_count": 1, "duration_ms": 8421 } ``` ### ⑥ `triage.result` · BugTriager 归因完成L2 ```js { "category": "contract_mismatch", "confidence": 0.86, "affected_units": ["auth-be", "auth-fe"], "will_auto_apply": true } ``` ### ⑦ `human_intervention_needed` · 需要人类介入L2 / L1 blocked ```js { "reason": "low_confidence_triage" | "unit_blocked" | "l2_exhausted" | "prd_conflict", "suggestion": "SDD token 续期策略未规定,需要决策", "context": { /* triage.json / unit info / etc */ } } ``` **消费者:**Dashboard 弹紫色 toast。 ### ⑧ `project.shipped` · 项目交付L2 ```js { "final_rev": { "prd": 3, "sdd": 5 }, "total_duration_ms": 1_842_000, "total_tokens": 487_321, "units": 12 } ``` **UI 反应:**整页 confetti 5s + L2 状态卡变深绿。 ### ⑨ `project.exhausted` · L2 预算耗尽L2 ```js { "rounds": 3, "last_triage": { /* triage.json */ }, "next_action": "awaiting_human_decision" } ``` ## Agent 持久化事件(6 类)· P-8 触发 由 P-8 `callAgentWithPersistence` 流程发出,用于 Dashboard 渲染「Agent at step 3/5」+ 崩溃恢复对话框。 ### ⑩ `agent.started` · Agent 调用开始 ```js { "agent": "architect", "call_id": "call-20260513-1031-7f3a", "unit_id": null, "input_summary": "mode=generate, prd@rev3" } ``` **发出时机:**statePath 写入 status=running 后立即 ### ⑪ `agent.todolist` · Agent 写完 todolist ```js { "agent": "architect", "call_id": "call-...", "items": [ { "id":"step-001", "description":"读 prd.html", "status":"todo" }, { "id":"step-002", "description":"识别功能列表", "status":"todo" }, ... ] } ``` **消费者:**Dashboard 展开 UnitDrawer / Agent panel 显示子任务列表 ### ⑫ `agent.step` · 子任务进度更新 ```js { "agent": "architect", "call_id": "call-...", "item_id": "step-003", "from": "doing", "to": "done", "progress_pct": 60, "current_desc": "为 OAuth 拆 3 个 Unit" } ``` **消费者:**Dashboard 顶部 Agent 卡片显示「Architect: 3/5」+ desc ### ⑬ `agent.done` · 成功完成 ```js { "agent": "architect", "call_id": "call-...", "duration_ms": 42_318, "tokens_used": 8_421, "output_ref": "agent-state/snapshots/call-..-out.json" } ``` ### ⑭ `agent.error` · 异常退出 ```js { "agent": "coder", "call_id": "call-...", "code": "SANDBOX_VIOLATION" | "TIMEOUT" | "AGENT_CRASH", "message": "试图写 ../profile-be/code/server.js", "recoverable": true } ``` ### ⑮ `resume_decision_needed` · 重启后等决策 ```js { "agent": "architect", "call_id": "call-...", "last_step": 3, "total": 5, "paused_at": "2026-05-13T10:42:13Z", "resume_token": "rt-...", "options": ["resume_from_step", "restart_fresh", "abort"] } ``` **发出时机:**P-8 `recoverOnBoot` 扫到 status=running(即上次进程被杀) **UI 反应:**Dashboard 顶部 banner 加紫色「⚠ 上次启动有 N 个 Agent 中断,需要决策」 ## 事件全表 | id | event | 层 | 频率 | 关键 payload | | --- | --- | --- | --- | --- | | 1 | `prd.updated` | L3 | 低 | rev, changeSummary, hasConflict | | 2 | `sdd.updated` | L2/L3 | 低 | rev, mode, units_diff | | 3 | `unit.state` | L1 | 中 | unit_id, from, to, retries | | 4 | `agent.stream` | L1/L2 | 高(千 / 项目) | chunk | | 5 | `integration.result` | L2 | 低(1-3 次 / 项目) | ok, failures_count | | 6 | `triage.result` | L2 | 低 | category, confidence, affected_units | | 7 | `human_intervention_needed` | — | 极低 | reason, suggestion | | 8 | `project.shipped` | — | 1 次 | final_rev, total_tokens | | 9 | `project.exhausted` | — | 极低 | rounds, last_triage | | 10 | `agent.started` | L1/L2/L3 | 中 | agent, call_id, input_summary | | 11 | `agent.todolist` | 同上 | 中 | items[] | | 12 | `agent.step` | 同上 | 中 | item_id, from, to, progress_pct | | 13 | `agent.done` | 同上 | 中 | duration_ms, tokens_used | | 14 | `agent.error` | 同上 | 低 | code, message, recoverable | | 15 | `resume_decision_needed` | boot | 极低 | options[] | ## 客户端订阅样板 ```js // Vue 3 composable function useDevLoopStream(pid) { const es = new EventSource(`/api/devloop/projects/${pid}/stream`); es.addEventListener('unit.state', e => handleUnitState(JSON.parse(e.data))); es.addEventListener('agent.stream', e => handleStream(JSON.parse(e.data))); es.addEventListener('prd.updated', e => refreshPrd()); es.addEventListener('human_intervention_needed', e => showToast(JSON.parse(e.data))); es.addEventListener('project.shipped', () => celebrate()); // 浏览器原生支持 Last-Event-ID 自动重连,不需要手动写 onUnmounted(() => es.close()); } ``` ## 事件序号与 Replay 每条事件在 `stream.log` 中按顺序持久化。Last-Event-ID 重连时,broker 从 log 文件读 id > 该值的所有事件批量补发,再切实时流。 ```js # stream.log 格式(NDJSON) {"id":1234,"event":"unit.state","data":{...},"ts":1747120731} {"id":1235,"event":"agent.stream","data":{...},"ts":1747120732} ``` > _log 保留 7 天,超出自动 gzip 归档。_