api-sse.md 6.96 KB

SSE 事件总线

15 类事件的完整规格。每条带 event 名、payload schema、发出时机、消费者。前端 Dashboard 是主消费者;CLI 工具也可订阅做实时监听。9 个核心 + 6 个 Agent 持久化事件。

订阅协议

# 普通订阅
GET /api/devloop/projects/:pid/stream
Accept: text/event-stream
Authorization: Bearer <jwt>

# 断线重连
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

{
  "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

{
  "rev": 5,
  "mode": "generate" | "patch",
  "derived_from": "prd@rev3",
  "units_diff": {
    "added":   ["oauth-be", "oauth-fe"],
    "changed": [],
    "removed": []
  }
}

③ unit.state · Unit 状态机转换L1

{
  "unit_id": "auth-be",
  "from": "coding",
  "to": "testing",
  "retries": 1,
  "reason": "coder_done"
}

消费者:Dashboard DAG 节点重染色

④ agent.stream · Agent stdout/stderr 流L1 + L2

{
  "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

{
  "run_id": "run-int-20260513-1031",
  "ok": false,
  "failures_count": 1,
  "duration_ms": 8421
}

⑥ triage.result · BugTriager 归因完成L2

{
  "category": "contract_mismatch",
  "confidence": 0.86,
  "affected_units": ["auth-be", "auth-fe"],
  "will_auto_apply": true
}

⑦ human_intervention_needed · 需要人类介入L2 / L1 blocked

{
  "reason": "low_confidence_triage" | "unit_blocked" | "l2_exhausted" | "prd_conflict",
  "suggestion": "SDD token 续期策略未规定,需要决策",
  "context": { /* triage.json / unit info / etc */ }
}

消费者:Dashboard 弹紫色 toast。

⑧ project.shipped · 项目交付L2

{
  "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

{
  "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 调用开始

{
  "agent": "architect",
  "call_id": "call-20260513-1031-7f3a",
  "unit_id": null,
  "input_summary": "mode=generate, prd@rev3"
}

发出时机:statePath 写入 status=running 后立即

⑪ agent.todolist · Agent 写完 todolist

{
  "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 · 子任务进度更新

{
  "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 · 成功完成

{
  "agent": "architect",
  "call_id": "call-...",
  "duration_ms": 42_318,
  "tokens_used": 8_421,
  "output_ref": "agent-state/snapshots/call-..-out.json"
}

⑭ agent.error · 异常退出

{
  "agent": "coder",
  "call_id": "call-...",
  "code": "SANDBOX_VIOLATION" | "TIMEOUT" | "AGENT_CRASH",
  "message": "试图写 ../profile-be/code/server.js",
  "recoverable": true
}

⑮ resume_decision_needed · 重启后等决策

{
  "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[]

客户端订阅样板

// 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 > 该值的所有事件批量补发,再切实时流。

# stream.log 格式(NDJSON)
{"id":1234,"event":"unit.state","data":{...},"ts":1747120731}
{"id":1235,"event":"agent.stream","data":{...},"ts":1747120732}

log 保留 7 天,超出自动 gzip 归档。