api-sse.html
18.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" /><meta name="viewport" content="width=device-width,initial-scale=1" />
<title>SSE 事件总线 · LemonBoss PRD</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,300;9..144,500;9..144,700&family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;700&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="assets/shell.css" />
</head>
<body data-page="api-sse.html">
<div class="shell">
<aside id="sidebar"></aside>
<main class="page">
<header class="page-head">
<div class="crumb"><span class="pill">06 · API</span><span class="sep">/</span><span>SSE 事件总线</span></div>
<h1>SSE 事件总线</h1>
<p class="deck">15 类事件的完整规格。每条带 event 名、payload schema、发出时机、消费者。前端 Dashboard 是主消费者;CLI 工具也可订阅做实时监听。9 个核心 + 6 个 Agent 持久化事件。</p>
</header>
<article class="body">
<section>
<h2>订阅协议</h2>
<pre class="code"><span class="c"># 普通订阅</span>
GET /api/devloop/projects/:pid/stream
Accept: text/event-stream
Authorization: Bearer <jwt>
<span class="c"># 断线重连</span>
GET /api/devloop/projects/:pid/stream
Last-Event-ID: 1234 <span class="c"># 上次收到的最大 id</span>
<span class="c"># 服务端响应(标准 SSE 格式)</span>
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 <span class="c"># 每 25s 一次</span></pre>
</section>
<section>
<h2>核心事件(9 类)</h2>
<details class="acc" open>
<summary>① <code>prd.updated</code> · PM 写完 PRD<span class="role">L3</span></summary>
<div class="inner">
<pre class="code"><span class="p">{</span>
<span class="n">"rev"</span>: <span class="v">3</span>,
<span class="n">"by"</span>: <span class="s">"pm-agent"</span>,
<span class="n">"origin"</span>: <span class="s">"yibo"</span>,
<span class="n">"changeSummary"</span>: <span class="s">"+ Section: OAuth login"</span>,
<span class="n">"hasConflict"</span>: <span class="v">false</span>
<span class="p">}</span></pre>
<p><b>发出时机:</b>PM Agent 写完 prd.html,DocStore 校验通过后</p>
<p><b>消费者:</b>Dashboard PRD diff 面板 · Architect 队列触发</p>
</div>
</details>
<details class="acc">
<summary>② <code>sdd.updated</code> · Architect 写完 SDD<span class="role">L3 + L2</span></summary>
<div class="inner">
<pre class="code"><span class="p">{</span>
<span class="n">"rev"</span>: <span class="v">5</span>,
<span class="n">"mode"</span>: <span class="s">"generate"</span> | <span class="s">"patch"</span>,
<span class="n">"derived_from"</span>: <span class="s">"prd@rev3"</span>,
<span class="n">"units_diff"</span>: <span class="p">{</span>
<span class="n">"added"</span>: <span class="p">[</span><span class="s">"oauth-be"</span>, <span class="s">"oauth-fe"</span><span class="p">]</span>,
<span class="n">"changed"</span>: <span class="p">[]</span>,
<span class="n">"removed"</span>: <span class="p">[]</span>
<span class="p">}</span>
<span class="p">}</span></pre>
</div>
</details>
<details class="acc">
<summary>③ <code>unit.state</code> · Unit 状态机转换<span class="role">L1</span></summary>
<div class="inner">
<pre class="code"><span class="p">{</span>
<span class="n">"unit_id"</span>: <span class="s">"auth-be"</span>,
<span class="n">"from"</span>: <span class="s">"coding"</span>,
<span class="n">"to"</span>: <span class="s">"testing"</span>,
<span class="n">"retries"</span>: <span class="v">1</span>,
<span class="n">"reason"</span>: <span class="s">"coder_done"</span>
<span class="p">}</span></pre>
<p><b>消费者:</b>Dashboard DAG 节点重染色</p>
</div>
</details>
<details class="acc">
<summary>④ <code>agent.stream</code> · Agent stdout/stderr 流<span class="role">L1 + L2</span></summary>
<div class="inner">
<pre class="code"><span class="p">{</span>
<span class="n">"agent"</span>: <span class="s">"tester"</span>,
<span class="n">"unit_id"</span>: <span class="s">"auth-fe"</span>,
<span class="n">"stream"</span>: <span class="s">"stdout"</span> | <span class="s">"stderr"</span>,
<span class="n">"chunk"</span>: <span class="s">"FAIL tests/login.spec.ts > invalid pwd\n"</span>
<span class="p">}</span></pre>
<p><b>注:</b>高频事件。前端 LiveAgentStream 用 RAF 批渲染,避免每条都触发 reactive 更新。</p>
</div>
</details>
<details class="acc">
<summary>⑤ <code>integration.result</code> · L2 结果<span class="role">L2</span></summary>
<div class="inner">
<pre class="code"><span class="p">{</span>
<span class="n">"run_id"</span>: <span class="s">"run-int-20260513-1031"</span>,
<span class="n">"ok"</span>: <span class="v">false</span>,
<span class="n">"failures_count"</span>: <span class="v">1</span>,
<span class="n">"duration_ms"</span>: <span class="v">8421</span>
<span class="p">}</span></pre>
</div>
</details>
<details class="acc">
<summary>⑥ <code>triage.result</code> · BugTriager 归因完成<span class="role">L2</span></summary>
<div class="inner">
<pre class="code"><span class="p">{</span>
<span class="n">"category"</span>: <span class="s">"contract_mismatch"</span>,
<span class="n">"confidence"</span>: <span class="v">0.86</span>,
<span class="n">"affected_units"</span>: <span class="p">[</span><span class="s">"auth-be"</span>, <span class="s">"auth-fe"</span><span class="p">]</span>,
<span class="n">"will_auto_apply"</span>: <span class="v">true</span>
<span class="p">}</span></pre>
</div>
</details>
<details class="acc">
<summary>⑦ <code>human_intervention_needed</code> · 需要人类介入<span class="role">L2 / L1 blocked</span></summary>
<div class="inner">
<pre class="code"><span class="p">{</span>
<span class="n">"reason"</span>: <span class="s">"low_confidence_triage"</span> | <span class="s">"unit_blocked"</span> | <span class="s">"l2_exhausted"</span> | <span class="s">"prd_conflict"</span>,
<span class="n">"suggestion"</span>: <span class="s">"SDD token 续期策略未规定,需要决策"</span>,
<span class="n">"context"</span>: <span class="p">{</span> /* triage.json / unit info / etc */ <span class="p">}</span>
<span class="p">}</span></pre>
<p><b>消费者:</b>Dashboard 弹紫色 toast。</p>
</div>
</details>
<details class="acc">
<summary>⑧ <code>project.shipped</code> · 项目交付<span class="role">L2</span></summary>
<div class="inner">
<pre class="code"><span class="p">{</span>
<span class="n">"final_rev"</span>: <span class="p">{</span> <span class="n">"prd"</span>: <span class="v">3</span>, <span class="n">"sdd"</span>: <span class="v">5</span> <span class="p">}</span>,
<span class="n">"total_duration_ms"</span>: <span class="v">1_842_000</span>,
<span class="n">"total_tokens"</span>: <span class="v">487_321</span>,
<span class="n">"units"</span>: <span class="v">12</span>
<span class="p">}</span></pre>
<p><b>UI 反应:</b>整页 confetti 5s + L2 状态卡变深绿。</p>
</div>
</details>
<details class="acc">
<summary>⑨ <code>project.exhausted</code> · L2 预算耗尽<span class="role">L2</span></summary>
<div class="inner">
<pre class="code"><span class="p">{</span>
<span class="n">"rounds"</span>: <span class="v">3</span>,
<span class="n">"last_triage"</span>: <span class="p">{</span> /* triage.json */ <span class="p">}</span>,
<span class="n">"next_action"</span>: <span class="s">"awaiting_human_decision"</span>
<span class="p">}</span></pre>
</div>
</details>
</section>
<section>
<h2>Agent 持久化事件(6 类)· P-8 触发</h2>
<p>由 P-8 <code>callAgentWithPersistence</code> 流程发出,用于 Dashboard 渲染「Agent at step 3/5」+ 崩溃恢复对话框。</p>
<details class="acc" open>
<summary>⑩ <code>agent.started</code> · Agent 调用开始</summary>
<div class="inner">
<pre class="code"><span class="p">{</span>
<span class="n">"agent"</span>: <span class="s">"architect"</span>,
<span class="n">"call_id"</span>: <span class="s">"call-20260513-1031-7f3a"</span>,
<span class="n">"unit_id"</span>: <span class="v">null</span>,
<span class="n">"input_summary"</span>: <span class="s">"mode=generate, prd@rev3"</span>
<span class="p">}</span></pre>
<p><b>发出时机:</b>statePath 写入 status=running 后立即</p>
</div>
</details>
<details class="acc">
<summary>⑪ <code>agent.todolist</code> · Agent 写完 todolist</summary>
<div class="inner">
<pre class="code"><span class="p">{</span>
<span class="n">"agent"</span>: <span class="s">"architect"</span>,
<span class="n">"call_id"</span>: <span class="s">"call-..."</span>,
<span class="n">"items"</span>: <span class="p">[</span>
<span class="p">{</span> <span class="n">"id"</span>:<span class="s">"step-001"</span>, <span class="n">"description"</span>:<span class="s">"读 prd.html"</span>, <span class="n">"status"</span>:<span class="s">"todo"</span> <span class="p">}</span>,
<span class="p">{</span> <span class="n">"id"</span>:<span class="s">"step-002"</span>, <span class="n">"description"</span>:<span class="s">"识别功能列表"</span>, <span class="n">"status"</span>:<span class="s">"todo"</span> <span class="p">}</span>,
...
<span class="p">]</span>
<span class="p">}</span></pre>
<p><b>消费者:</b>Dashboard 展开 UnitDrawer / Agent panel 显示子任务列表</p>
</div>
</details>
<details class="acc">
<summary>⑫ <code>agent.step</code> · 子任务进度更新</summary>
<div class="inner">
<pre class="code"><span class="p">{</span>
<span class="n">"agent"</span>: <span class="s">"architect"</span>,
<span class="n">"call_id"</span>: <span class="s">"call-..."</span>,
<span class="n">"item_id"</span>: <span class="s">"step-003"</span>,
<span class="n">"from"</span>: <span class="s">"doing"</span>,
<span class="n">"to"</span>: <span class="s">"done"</span>,
<span class="n">"progress_pct"</span>: <span class="v">60</span>,
<span class="n">"current_desc"</span>: <span class="s">"为 OAuth 拆 3 个 Unit"</span>
<span class="p">}</span></pre>
<p><b>消费者:</b>Dashboard 顶部 Agent 卡片显示「Architect: 3/5」+ desc</p>
</div>
</details>
<details class="acc">
<summary>⑬ <code>agent.done</code> · 成功完成</summary>
<div class="inner">
<pre class="code"><span class="p">{</span>
<span class="n">"agent"</span>: <span class="s">"architect"</span>,
<span class="n">"call_id"</span>: <span class="s">"call-..."</span>,
<span class="n">"duration_ms"</span>: <span class="v">42_318</span>,
<span class="n">"tokens_used"</span>: <span class="v">8_421</span>,
<span class="n">"output_ref"</span>: <span class="s">"agent-state/snapshots/call-..-out.json"</span>
<span class="p">}</span></pre>
</div>
</details>
<details class="acc">
<summary>⑭ <code>agent.error</code> · 异常退出</summary>
<div class="inner">
<pre class="code"><span class="p">{</span>
<span class="n">"agent"</span>: <span class="s">"coder"</span>,
<span class="n">"call_id"</span>: <span class="s">"call-..."</span>,
<span class="n">"code"</span>: <span class="s">"SANDBOX_VIOLATION"</span> | <span class="s">"TIMEOUT"</span> | <span class="s">"AGENT_CRASH"</span>,
<span class="n">"message"</span>: <span class="s">"试图写 ../profile-be/code/server.js"</span>,
<span class="n">"recoverable"</span>: <span class="v">true</span>
<span class="p">}</span></pre>
</div>
</details>
<details class="acc">
<summary>⑮ <code>resume_decision_needed</code> · 重启后等决策</summary>
<div class="inner">
<pre class="code"><span class="p">{</span>
<span class="n">"agent"</span>: <span class="s">"architect"</span>,
<span class="n">"call_id"</span>: <span class="s">"call-..."</span>,
<span class="n">"last_step"</span>: <span class="v">3</span>, <span class="n">"total"</span>: <span class="v">5</span>,
<span class="n">"paused_at"</span>: <span class="s">"2026-05-13T10:42:13Z"</span>,
<span class="n">"resume_token"</span>: <span class="s">"rt-..."</span>,
<span class="n">"options"</span>: <span class="p">[</span><span class="s">"resume_from_step"</span>, <span class="s">"restart_fresh"</span>, <span class="s">"abort"</span><span class="p">]</span>
<span class="p">}</span></pre>
<p><b>发出时机:</b>P-8 <code>recoverOnBoot</code> 扫到 status=running(即上次进程被杀)</p>
<p><b>UI 反应:</b>Dashboard 顶部 banner 加紫色「⚠ 上次启动有 N 个 Agent 中断,需要决策」</p>
</div>
</details>
</section>
<section>
<h2>事件全表</h2>
<table class="dense">
<thead><tr><th>id</th><th>event</th><th>层</th><th>频率</th><th>关键 payload</th></tr></thead>
<tbody>
<tr><td>1</td><td><code>prd.updated</code></td><td>L3</td><td>低</td><td>rev, changeSummary, hasConflict</td></tr>
<tr><td>2</td><td><code>sdd.updated</code></td><td>L2/L3</td><td>低</td><td>rev, mode, units_diff</td></tr>
<tr><td>3</td><td><code>unit.state</code></td><td>L1</td><td>中</td><td>unit_id, from, to, retries</td></tr>
<tr><td>4</td><td><code>agent.stream</code></td><td>L1/L2</td><td>高(千 / 项目)</td><td>chunk</td></tr>
<tr><td>5</td><td><code>integration.result</code></td><td>L2</td><td>低(1-3 次 / 项目)</td><td>ok, failures_count</td></tr>
<tr><td>6</td><td><code>triage.result</code></td><td>L2</td><td>低</td><td>category, confidence, affected_units</td></tr>
<tr><td>7</td><td><code>human_intervention_needed</code></td><td>—</td><td>极低</td><td>reason, suggestion</td></tr>
<tr><td>8</td><td><code>project.shipped</code></td><td>—</td><td>1 次</td><td>final_rev, total_tokens</td></tr>
<tr><td>9</td><td><code>project.exhausted</code></td><td>—</td><td>极低</td><td>rounds, last_triage</td></tr>
<tr><td>10</td><td><code>agent.started</code></td><td>L1/L2/L3</td><td>中</td><td>agent, call_id, input_summary</td></tr>
<tr><td>11</td><td><code>agent.todolist</code></td><td>同上</td><td>中</td><td>items[]</td></tr>
<tr><td>12</td><td><code>agent.step</code></td><td>同上</td><td>中</td><td>item_id, from, to, progress_pct</td></tr>
<tr><td>13</td><td><code>agent.done</code></td><td>同上</td><td>中</td><td>duration_ms, tokens_used</td></tr>
<tr><td>14</td><td><code>agent.error</code></td><td>同上</td><td>低</td><td>code, message, recoverable</td></tr>
<tr><td>15</td><td><code>resume_decision_needed</code></td><td>boot</td><td>极低</td><td>options[]</td></tr>
</tbody>
</table>
</section>
<section>
<h2>客户端订阅样板</h2>
<pre class="code"><span class="c">// Vue 3 composable</span>
<span class="k">function</span> <span class="n">useDevLoopStream</span>(pid) <span class="p">{</span>
<span class="k">const</span> es = <span class="k">new</span> <span class="n">EventSource</span>(<span class="s">`/api/devloop/projects/</span><span class="v">${pid}</span><span class="s">/stream`</span>);
es.<span class="n">addEventListener</span>(<span class="s">'unit.state'</span>, e => <span class="n">handleUnitState</span>(<span class="n">JSON.parse</span>(e.<span class="n">data</span>)));
es.<span class="n">addEventListener</span>(<span class="s">'agent.stream'</span>, e => <span class="n">handleStream</span>(<span class="n">JSON.parse</span>(e.<span class="n">data</span>)));
es.<span class="n">addEventListener</span>(<span class="s">'prd.updated'</span>, e => <span class="n">refreshPrd</span>());
es.<span class="n">addEventListener</span>(<span class="s">'human_intervention_needed'</span>, e => <span class="n">showToast</span>(<span class="n">JSON.parse</span>(e.<span class="n">data</span>)));
es.<span class="n">addEventListener</span>(<span class="s">'project.shipped'</span>, () => <span class="n">celebrate</span>());
<span class="c">// 浏览器原生支持 Last-Event-ID 自动重连,不需要手动写</span>
onUnmounted(() => es.<span class="n">close</span>());
<span class="p">}</span></pre>
</section>
<section>
<h2>事件序号与 Replay</h2>
<p>每条事件在 <code>stream.log</code> 中按顺序持久化。Last-Event-ID 重连时,broker 从 log 文件读 id > 该值的所有事件批量补发,再切实时流。</p>
<pre class="code"><span class="c"># stream.log 格式(NDJSON)</span>
<span class="p">{</span><span class="n">"id"</span>:<span class="v">1234</span>,<span class="n">"event"</span>:<span class="s">"unit.state"</span>,<span class="n">"data"</span>:<span class="p">{</span>...<span class="p">}</span>,<span class="n">"ts"</span>:<span class="v">1747120731</span><span class="p">}</span>
<span class="p">{</span><span class="n">"id"</span>:<span class="v">1235</span>,<span class="n">"event"</span>:<span class="s">"agent.stream"</span>,<span class="n">"data"</span>:<span class="p">{</span>...<span class="p">}</span>,<span class="n">"ts"</span>:<span class="v">1747120732</span><span class="p">}</span></pre>
<p class="sig">log 保留 7 天,超出自动 gzip 归档。</p>
</section>
</article>
<nav class="prev-next"></nav>
<footer class="site"><div><b>Section</b>06 · API</div><div><b>Page</b>api-sse.html</div></footer>
</main>
</div>
<script src="assets/shell.js"></script>
</body>
</html>