@sema-agent/client-core 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +35 -4
- package/dist/adapt.d.ts +43 -4
- package/dist/adapt.js +850 -362
- package/dist/adapter/downstream/eventToSdkMessage.d.ts +81 -0
- package/dist/adapter/downstream/eventToSdkMessage.js +330 -0
- package/dist/adapter/downstream/terminalToSdkResult.d.ts +71 -0
- package/dist/adapter/downstream/terminalToSdkResult.js +370 -0
- package/dist/adapter/downstream/turnUsageToModelUsage.d.ts +26 -0
- package/dist/adapter/downstream/turnUsageToModelUsage.js +12 -0
- package/dist/adapter/runStream.d.ts +38 -0
- package/dist/adapter/runStream.js +177 -0
- package/dist/adapter/types.d.ts +73 -0
- package/dist/adapter/types.js +23 -0
- package/dist/clientSlice.d.ts +169 -0
- package/dist/clientSlice.js +62 -0
- package/dist/engineWireSdk.d.ts +25 -1
- package/dist/engineWireSdk.js +30 -4
- package/dist/host.d.ts +90 -0
- package/dist/host.js +84 -0
- package/dist/index.d.ts +35 -1
- package/dist/index.js +47 -3
- package/dist/request/taskRequest.d.ts +135 -0
- package/dist/request/taskRequest.js +176 -0
- package/dist/scratchpadWireCaps.d.ts +16 -0
- package/dist/scratchpadWireCaps.js +65 -0
- package/dist/seam.d.ts +178 -11
- package/dist/seam.js +30 -0
- package/package.json +2 -2
package/dist/adapt.js
CHANGED
|
@@ -1,12 +1,43 @@
|
|
|
1
1
|
import { deriveTranscriptId } from './seam.js';
|
|
2
|
-
import { normalizeTaskNotification, renderTaskNotificationXml, taskNotificationDedupKey } from './notifications.js';
|
|
2
|
+
import { dropQueuedNotificationsForRun, normalizeTaskNotification, renderTaskNotificationXml, taskNotificationDedupKey, } from './notifications.js';
|
|
3
3
|
import { mapBrainStatusToRetry } from './retryStatus.js';
|
|
4
4
|
import { steeringInjectedToAttachments } from './steering.js';
|
|
5
5
|
import { projectDiagnosticsFrame } from './diagnostics.js';
|
|
6
|
+
// ── B4:A 层搬入后适配器**自持**的四个本包台账(写者与读者同包 = R4 单实例纪律;
|
|
7
|
+
// 这四件都没有 UI 订阅面,做成 chrome 臂只会凭空长出一条「宿主不实现就静默失效」的义务)。
|
|
8
|
+
import { recordEngineToolLabel } from './engineToolLabelStore.js';
|
|
9
|
+
import { registerSubagentAlias } from './subagentContentStore.js';
|
|
10
|
+
import { isWorkflowAgentTaskId, recordWorkflowAgentTaskId } from './workflow.js';
|
|
11
|
+
import { clearEnginePanelTaskResident, markEnginePanelTaskResident } from './engineAgentPanelStore.js';
|
|
6
12
|
/** 主车道证明——本批实现的臂全在 leader lane(子代/workflow lane 的 tick 面留 #52b)。 */
|
|
7
13
|
const MAIN = { lane: 'main' };
|
|
8
|
-
/**
|
|
14
|
+
/**
|
|
15
|
+
* 活体增量攒批节拍的**默认值**(时钟从 ctx.now() 取;宿主可用 `ctx.coalesceIntervalMs` 覆盖)。
|
|
16
|
+
*
|
|
17
|
+
* 取值依据(两个都是实测,别把其中一个当"唯一真相"):
|
|
18
|
+
* · **100ms** = 我们对标的 CC 2.1.207 CLI 口径(187 时代是 16ms,207 提到 100ms 降重绘频率,
|
|
19
|
+
* 长回答滚动更稳)—— 壳是 207 fork,所以库的默认值取它,壳消费本包时行为零变化;
|
|
20
|
+
* · **16ms** = CC **桌面端**宿主 `makeStreamDeltaCoalescer` 的单帧攒批节拍(设计稿 R1 实测)。
|
|
21
|
+
* ⇒ 结论:**节拍是宿主渲染策略,不是 wire 语义** —— CC 自己两个端就是两个值。web/桌面宿主
|
|
22
|
+
* 想要 16ms 就传 `coalesceIntervalMs: 16`,合并**语义**(思考先于文本、段边界重开块、
|
|
23
|
+
* committed 恒随后)在库里,一份。
|
|
24
|
+
*
|
|
25
|
+
* 🔴 顺带钉一句反面事实(R1 证伪,别再写进任何注释/文档):**"UI 从不见裸 token" 是错的** ——
|
|
26
|
+
* CC 攒批后 flush 的载荷仍是 `stream_event` 增量 delta,CC 的 UI 照样消费增量;
|
|
27
|
+
* 完整 assistant 消息是 SDK 另发的一路。我们这里的 `stream_delta` chrome 臂同构。
|
|
28
|
+
*/
|
|
9
29
|
export const FLUSH_INTERVAL_MS = 100;
|
|
30
|
+
/**
|
|
31
|
+
* T40 IDLE-FLUSH 竞速窗(cli upstreamBridge `IDLE_FLUSH_MS` 逐字同值)。
|
|
32
|
+
*
|
|
33
|
+
* 病:模型转入长工具参数生成(大 Write 的 input_json 不经本桥)时 SSE 增量停摆,而 thinking/
|
|
34
|
+
* answerSegment 里**已经生成**的内容还押着陪等(pty 确证:11s 整屏 hash 恒定)。
|
|
35
|
+
* 修:增量停 ≥1.5s 且缓冲非空 ⇒ 先 drainLive()(活体预览语义不变),再提交思考+文本段
|
|
36
|
+
* (段序铁律不破:本来就该在下一张工具卡前 flush,这里只是提前)。
|
|
37
|
+
* 🔴 这是**过渡修**,正解在 core [817](wire 侧给事件);且**需要宿主定时器**:
|
|
38
|
+
* `ctx.setTimer` 缺席 ⇒ 整条竞速不启用(纯等下一帧),与 0.4.0 逐字同行为。
|
|
39
|
+
*/
|
|
40
|
+
export const IDLE_FLUSH_MS = 1500;
|
|
10
41
|
/** cli SUBAGENT_TOOL_NAMES / WORKFLOW_TOOL_NAMES 同源(判别资产,别各自重写)。 */
|
|
11
42
|
export const SUBAGENT_TOOL_NAMES = new Set(['Task', 'Agent', 'Fork']);
|
|
12
43
|
export const WORKFLOW_TOOL_NAMES = new Set(['Workflow', 'RunWorkflow', 'run_workflow']);
|
|
@@ -37,6 +68,32 @@ export function estimateCjkTokens(s) {
|
|
|
37
68
|
return Math.round(t);
|
|
38
69
|
}
|
|
39
70
|
/** 引擎 subagent 卡的短标签(cli shortTaskLabel 逐字)。 */
|
|
71
|
+
/**
|
|
72
|
+
* T8 —— 压平 §E1 wire `output`(**非均匀**:`string` | `(TextContent|ImageContent)[]`,events.d.ts:78)
|
|
73
|
+
* 成一条纯文本(cli `flattenWireOutput` 逐字)。text 块贡献 `.text`,image 块贡献中性占位 `[image]`
|
|
74
|
+
* (转录卡是文本粒度),裸字符串直通。
|
|
75
|
+
* 🔴 载荷 UNTRUSTED + 仅供观测(events.d.ts:80-85):渲染可以,**绝不回喂模型**;`truncated` 的更不行。
|
|
76
|
+
* B4 把它从 B 层(`wireOutputToBody`)提到独立导出:A 层的 `settlePanelTasks(report)` 也要用它
|
|
77
|
+
* (关卡时把子代最终报告串到面板行 end 事件上),两处必须同一份实现。
|
|
78
|
+
*/
|
|
79
|
+
export function flattenWireOutput(output) {
|
|
80
|
+
if (typeof output === 'string')
|
|
81
|
+
return output;
|
|
82
|
+
if (Array.isArray(output)) {
|
|
83
|
+
const parts = [];
|
|
84
|
+
for (const block of output) {
|
|
85
|
+
if (block && typeof block === 'object') {
|
|
86
|
+
const b = block;
|
|
87
|
+
if (b.type === 'text' && typeof b.text === 'string')
|
|
88
|
+
parts.push(b.text);
|
|
89
|
+
else if (b.type === 'image')
|
|
90
|
+
parts.push('[image]');
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return parts.join('');
|
|
94
|
+
}
|
|
95
|
+
return '';
|
|
96
|
+
}
|
|
40
97
|
export function shortTaskLabel(task) {
|
|
41
98
|
const firstLine = (task.split('\n').find(l => l.trim()) ?? task).trim();
|
|
42
99
|
const words = firstLine.split(/\s+/).slice(0, 6).join(' ');
|
|
@@ -57,6 +114,13 @@ export function sanitizeToolUseBlock(block) {
|
|
|
57
114
|
const rawInput = out.input;
|
|
58
115
|
const eventId = typeof out.eventId === 'string' ? out.eventId : undefined;
|
|
59
116
|
const parentToolCallId = typeof out.parentToolCallId === 'string' ? out.parentToolCallId : undefined;
|
|
117
|
+
// 🔴 B4 补漏(0.5.0 真缺口,差分守卫此前无 fixture 携 label ⇒ 漏得静默):core 的**展示名**
|
|
118
|
+
// `tool_start.label` 同样是侧信道,必须在渲染前摘掉 —— tool_use block 会进壳 transcript,
|
|
119
|
+
// /compact 时原样直发 provider,多一个键 = 400 invalid_request(cli 头注记的「件1 事故同款」)。
|
|
120
|
+
// 摘下来的值由调用方交给 engineToolLabelStore(纯函数不做副作用),unknown-tool 卡按
|
|
121
|
+
// toolCallId 查。今天唯一的真命中族 = MCP 远端工具(name=mcp__srv__tool / label=srv:tool)。
|
|
122
|
+
const label = typeof out.label === 'string' ? out.label : undefined;
|
|
123
|
+
delete out.label;
|
|
60
124
|
delete out.eventId;
|
|
61
125
|
delete out.parentToolCallId;
|
|
62
126
|
if (out.input && typeof out.input === 'object') {
|
|
@@ -89,7 +153,13 @@ export function sanitizeToolUseBlock(block) {
|
|
|
89
153
|
delete inp.subagent_type;
|
|
90
154
|
}
|
|
91
155
|
}
|
|
92
|
-
return {
|
|
156
|
+
return {
|
|
157
|
+
block: out,
|
|
158
|
+
rawInput,
|
|
159
|
+
...(eventId !== undefined ? { eventId } : {}),
|
|
160
|
+
...(parentToolCallId !== undefined ? { parentToolCallId } : {}),
|
|
161
|
+
...(label !== undefined ? { label } : {}),
|
|
162
|
+
};
|
|
93
163
|
}
|
|
94
164
|
/** seam 的 reserved decision 哨兵(cli decisionOf 逐字)。 */
|
|
95
165
|
function decisionOf(block) {
|
|
@@ -112,8 +182,10 @@ export const ADAPTER_COVERAGE = {
|
|
|
112
182
|
'diagnostics',
|
|
113
183
|
'steering_injected',
|
|
114
184
|
'workspace_changed',
|
|
185
|
+
'prompt_suggestions',
|
|
115
186
|
'retry_status',
|
|
116
|
-
'
|
|
187
|
+
'task_progress',
|
|
188
|
+
'tool_end_result(label 补位 + 关卡 settle + response-id 复位 + 开卡台账出栈;不铸 tool_result)',
|
|
117
189
|
],
|
|
118
190
|
/** 已落码臂产出的 transcript 消息类目(差分守卫的比对域)。 */
|
|
119
191
|
transcriptKinds: [
|
|
@@ -124,16 +196,29 @@ export const ADAPTER_COVERAGE = {
|
|
|
124
196
|
'user_task_notification',
|
|
125
197
|
'system_passthrough',
|
|
126
198
|
],
|
|
199
|
+
/** B3(0.5.0)新落码的臂/半场 —— 从 todo 移过来的,别再在 todo 里留同名条目。 */
|
|
200
|
+
b3: [
|
|
201
|
+
'T38 三清第三清 + T57 第五处断闸:prompt_suggestions(turn 开场作废 + 子流断闸)',
|
|
202
|
+
'T40 IDLE-FLUSH 1.5s 竞速(需 ctx.setTimer;缺席=不启用)',
|
|
203
|
+
'T58 手动 iterator 的 finally cleanup(early break/throw 都要关上游生成器)',
|
|
204
|
+
'T59 abort 半场(ctx.signal;中断后不产终答)',
|
|
205
|
+
'T60 无 result 收尾腿(泄尾增量 → 清 liveness → 尾思考 → 尾文本段)',
|
|
206
|
+
],
|
|
207
|
+
/** B4(0.6.0)新落码的臂/半场 —— A 层收官批,同样别再在 todo 里留同名条目。 */
|
|
208
|
+
b4: [
|
|
209
|
+
'A 层第 14 臂 task_progress:workflow lane 三级门 + 判据④ belt 回植 + C1 alias + bindTaskCard(显式父/FIFO/含糊即 unbound)+ inline stats + T50 双形读 + SubagentStart + #6 resident 台账 + 面板 tick',
|
|
210
|
+
'settlePanelTasks 三处 sweep 全接线(tool_end 显式关卡带报告 / result 防御 sweep / turn 末 + abort sweep)',
|
|
211
|
+
'P0-2 tool label 侧信道(sanitizeToolUseBlock 摘 label + engineToolLabelStore 登记;0.5.0 真缺口,label 会随 tool_use block 落转录 → /compact 直发 provider 400)',
|
|
212
|
+
'task_notification 的两件宿主义务收回库内自持:dropQueuedNotificationsForRun + clearEnginePanelTaskResident;stop hook 的 if-started 门由库解',
|
|
213
|
+
'T8 flattenWireOutput 提为独立导出(A 层 settle 报告与 B 层卡体同一份)',
|
|
214
|
+
],
|
|
127
215
|
/** 如实留白——写不动/依赖未搬运资产的臂,#52b 起分批接。 */
|
|
128
216
|
todo: [
|
|
129
|
-
'tool_end_result → user tool_result 正常路径(需搬 wireOutputToBody/structuredToToolUseResult/mock 合成器 + TodoWrite/ReportFindings 富卡片,约 600 行;workflow 轮询信封的人话投影已在 workflow.ts 就位,等这条接线)',
|
|
130
|
-
'tool_end_result 的两个侧产物:Remember → memory_saved 系统消息、task/task-list structured → 面板台账全量同步',
|
|
131
|
-
'result/turn 末的开卡兜底关闭(同上,依赖合成器)',
|
|
132
|
-
'
|
|
133
|
-
'
|
|
134
|
-
'#117a bg Bash 回执探测(detectEngineBgShellReceipt 文案锚定探测器)',
|
|
135
|
-
'abort/Esc 半场(AdapterContext 无 signal 位——见 SEAM-GAP-4)',
|
|
136
|
-
'IDLE_FLUSH 1.5s 竞速(件1 大 Write 静默窗;需 ctx 侧定时器能力)',
|
|
217
|
+
'tool_end_result → user tool_result 正常路径(需搬 wireOutputToBody/structuredToToolUseResult/mock 合成器 + TodoWrite/ReportFindings 富卡片,约 600 行;workflow 轮询信封的人话投影已在 workflow.ts 就位,等这条接线)= B5',
|
|
218
|
+
'tool_end_result 的两个侧产物:Remember → memory_saved 系统消息、task/task-list structured → 面板台账全量同步 = B5',
|
|
219
|
+
'result/turn 末的开卡兜底关闭(同上,依赖合成器)= B5',
|
|
220
|
+
'#117a bg Bash 回执探测(detectEngineBgShellReceipt 文案锚定探测器)= B5(E 层按工具名分臂)',
|
|
221
|
+
'abort 半场里依赖开卡合成器的那件(兜底关卡)= B5;行 sweep 半场 B4 已接',
|
|
137
222
|
'macrotask 让渡(cli 在提交前让出宏任务给 Ink;宿主渲染策略,不进库)',
|
|
138
223
|
],
|
|
139
224
|
};
|
|
@@ -143,8 +228,20 @@ export const ADAPTER_DIVERGENCES = [
|
|
|
143
228
|
'现役 wire 不产 user 帧(eventToSdkMessage 无该出口),故对真流是惰性的;保留是为了 desktop 重放存量转录。',
|
|
144
229
|
'DIVERGENCE-2:transcript uuid 取值形——cli 铸随机 v4 / 原样透传帧 uuid,adapt 一律走 ' +
|
|
145
230
|
'deriveTranscriptId(稳定键前缀形 `wid_*`)。差分守卫按「别名结构」比对(同一侧内的复用/区分关系),不比字面值。',
|
|
146
|
-
'DIVERGENCE-3:合并节流的**分片边界**不比对——cli 用 Date.now() 真时钟,adapt 用 ctx.now()
|
|
231
|
+
'DIVERGENCE-3:合并节流的**分片边界**不比对——cli 用 Date.now() 真时钟,adapt 用 ctx.now();' +
|
|
232
|
+
'且 B3 起节拍可由宿主 `ctx.coalesceIntervalMs` 覆盖(缺席=cli 同值 100ms)。' +
|
|
147
233
|
'守卫比对的是 chrome 增量按 channel 拼接后的全文与顺序(语义不变量),不是 chunk 切分。',
|
|
234
|
+
'DIVERGENCE-4(B3):流探针的**开关与时间戳**不同形——cli 是 `process.env.SEMA_STREAM_PROBE` ' +
|
|
235
|
+
'文件探针写 `${Date.now()} text_flush len=N`,adapt 是 `ctx.probe(\'stream\', …)` 注入口写 ' +
|
|
236
|
+
'`${ctx.now()} text_flush len=N`。行格式与触发点(每次文本 flush 一次)逐字相同,' +
|
|
237
|
+
'差在时钟源与落地方式(宿主决定落文件/上报/丢弃)。',
|
|
238
|
+
'DIVERGENCE-5(B4):A 层行台账的三个探针同 DIVERGENCE-4 处理 —— cli 的 `SEMA_BIND_PROBE` / ' +
|
|
239
|
+
'`SEMA_TICK_PROBE` / `SEMA_HOOK_PROBE` 三个文件探针,在本包是 `ctx.probe(\'bind\'|\'tick\'|\'hook\', line)`。' +
|
|
240
|
+
'行载荷逐字同形(tick 是同键序 JSON),只把 `Date.now()` 换成 `ctx.now()`。',
|
|
241
|
+
'DIVERGENCE-6(B4):`subagent_lifecycle` 的**去重与 if-started 门在库里解**,cli 在 module 级 Set 上解。' +
|
|
242
|
+
'可观测后果相同(同一 taskId 的 Start 恒一次;没 Start 过的 taskId 收不到通知臂的 Stop),' +
|
|
243
|
+
'差在「谁持台账」:cli=进程级、本包=适配器实例级(= 一个 session)。多 session 宿主(web 一页两会话)' +
|
|
244
|
+
'上本包语义**更正确**——cli 的 module 形会让两个会话互吞对方的 Start。',
|
|
148
245
|
];
|
|
149
246
|
const TASK_NOTIFICATION_TAG = 'task-notification';
|
|
150
247
|
const TASK_ID_TAG = 'task-id';
|
|
@@ -159,6 +256,14 @@ class WireToCcAdapterImpl {
|
|
|
159
256
|
cardEnqueuedRuns = new Set();
|
|
160
257
|
/** W1 三级判据素材:本壳见过的 Workflow 工具 tool_use id(module 级台账的实例化)。 */
|
|
161
258
|
seenWorkflowToolCallIds = new Set();
|
|
259
|
+
/**
|
|
260
|
+
* B4 —— SubagentStart 一生只许一次(frame-lane-matrix #4)。cli 是 **module 级** Set:跨 turn 存活的
|
|
261
|
+
* bg 子代在每个新 generator 的首 tick 都会走到 fire 点(`livePanelTasks` 是 per-turn 的),没有它
|
|
262
|
+
* 就每 turn 重 fire 一次。本包放**实例级** = 一个 session 一份,语义等价且不污染进程(多 session
|
|
263
|
+
* 宿主如 web 一个页面开两个会话,cli 的 module 形反而会互相吞)。
|
|
264
|
+
* 同时它也是 `task_notification` 臂发 stop 的门(cli:`firedSubagentStartHookTaskIds.has(taskId)`)。
|
|
265
|
+
*/
|
|
266
|
+
firedSubagentStartHookTaskIds = new Set();
|
|
162
267
|
exportLedger() {
|
|
163
268
|
return {
|
|
164
269
|
version: 1,
|
|
@@ -243,6 +348,10 @@ class WireToCcAdapterImpl {
|
|
|
243
348
|
this.cardEnqueuedRuns.add(runId);
|
|
244
349
|
}
|
|
245
350
|
async *adapt(frames, ctx) {
|
|
351
|
+
// 下面几个 `function*` 表达式(非箭头 —— generator 不能写成箭头)要访问实例台账,
|
|
352
|
+
// 它们的 `this` 不是适配器,所以在这里固定一个引用。
|
|
353
|
+
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
354
|
+
const self = this;
|
|
246
355
|
const turnStartAt = ctx.now();
|
|
247
356
|
// ── id 派生 ────────────────────────────────────────────────────────────────────────────────
|
|
248
357
|
// [1653]:稳定键优先序 = 帧 id > seq > toolCallId,全缺才 ctx.uuid()。现役 wire 的稳定键落在
|
|
@@ -270,6 +379,10 @@ class WireToCcAdapterImpl {
|
|
|
270
379
|
let textBlockOpen = false;
|
|
271
380
|
let thinkingBlockOpen = false;
|
|
272
381
|
let lastFlushAt = 0;
|
|
382
|
+
/** T29 攒批节拍:宿主策略优先(桌面/web 可传 16ms 对齐 CC 桌面端),缺席=207 CLI 口径 100ms。 */
|
|
383
|
+
const flushEveryMs = typeof ctx.coalesceIntervalMs === 'number' && ctx.coalesceIntervalMs >= 0
|
|
384
|
+
? ctx.coalesceIntervalMs
|
|
385
|
+
: FLUSH_INTERVAL_MS;
|
|
273
386
|
let messageStartEmitted = false;
|
|
274
387
|
let endEmitted = false;
|
|
275
388
|
let assistantResponseId = null;
|
|
@@ -277,12 +390,140 @@ class WireToCcAdapterImpl {
|
|
|
277
390
|
let segmentAnchor = null;
|
|
278
391
|
let thinkingAnchor = null;
|
|
279
392
|
const pending = new Map();
|
|
393
|
+
// ── B4 / MF-10:面板行台账(cli generator 局部变量逐个对位)────────────────────────────────
|
|
394
|
+
// 引擎侧子代的 tick 只带 rollup 数字({toolUses,totalTokens}) = CC `AgentProgress` 同形,
|
|
395
|
+
// 所以忠实的 CC-187 面是 footer 的 CoordinatorTaskPanel(CC 自己对进程外 agent 的先例:
|
|
396
|
+
// 面板行 + task-notification,从不合成 inline 消息轨)。下面这组把「tick → 哪张委派卡」的
|
|
397
|
+
// 归属、行的活性(live/inert/unbound)、以及 settle 的三处 sweep 全部管起来。
|
|
398
|
+
/** taskId → 绑定的 open tool_use id。 */
|
|
399
|
+
const taskCardBinding = new Map();
|
|
400
|
+
/** 已被某 taskId 认领的 tool_use id(一张卡只认一个子代)。 */
|
|
401
|
+
const boundToolCalls = new Set();
|
|
402
|
+
/** 本 turn 发布过面板行的全部 taskId(绑定与否都算)。 */
|
|
403
|
+
const livePanelTasks = new Set();
|
|
404
|
+
/** 已落终态的行(settle 幂等)。 */
|
|
405
|
+
const endedPanelTasks = new Set();
|
|
406
|
+
/**
|
|
407
|
+
* #6 —— 绑定时那张卡**正开着**的 taskId(= "live" 绑定)。只有这些行属本 turn 生命周期、
|
|
408
|
+
* 可被 turn 末防御 sweep 结掉。inert(显式父指向已关/没见过的卡 —— 跨 turn bg 子代的典型形)
|
|
409
|
+
* 与 unbound 行是 **session 常驻**:终态只认 task_notification / bg_notification。
|
|
410
|
+
*/
|
|
411
|
+
const liveBoundPanelTasks = new Set();
|
|
412
|
+
/** taskId → 子代类型(Stop hook 查它;cli 同为 turn 局部)。 */
|
|
413
|
+
const subagentTypeById = new Map();
|
|
280
414
|
const chrome = (event) => ({ plane: 'chrome', event });
|
|
281
415
|
const transcript = (message) => ({
|
|
282
416
|
plane: 'transcript',
|
|
283
417
|
message: message,
|
|
284
418
|
});
|
|
285
419
|
const mintResponseId = (frame) => (assistantResponseId ??= `msg_sema_${idOf(frame, 'resp')}`);
|
|
420
|
+
// ── B4 / MF-10:行绑定 + 三处 sweep(cli bindTaskCard / unboundSubagentCandidates /
|
|
421
|
+
// settlePanelTasks 逐条对位)───────────────────────────────────────────────────────────
|
|
422
|
+
/** 行的车道证明:绑到卡的 = subagent lane(带父卡 id),未绑的 = 主 lane(taskId 键的独立行)。 */
|
|
423
|
+
const laneOfTask = (taskId) => {
|
|
424
|
+
const card = taskCardBinding.get(taskId);
|
|
425
|
+
return card !== undefined ? { lane: 'subagent', parentToolCallId: card } : MAIN;
|
|
426
|
+
};
|
|
427
|
+
const bindTaskCard = (taskId, parentToolCallId) => {
|
|
428
|
+
const existing = taskCardBinding.get(taskId);
|
|
429
|
+
if (existing !== undefined)
|
|
430
|
+
return existing;
|
|
431
|
+
let target;
|
|
432
|
+
if (parentToolCallId !== undefined) {
|
|
433
|
+
// P1② 精确绑定 —— wire 点名了委派卡。信但不无条件信([1617]①,frame-lane-matrix #1):
|
|
434
|
+
// 被点名的卡**开着**且**不是** subagent 形工具(Task/Agent/Fork)时拒绝绑定 —— 否则
|
|
435
|
+
// workflow/Bash 卡一关就会把这条行 settle 掉、还会收走它的 inline stats(错归因)。
|
|
436
|
+
// 行保持 unbound(taskId 键的 rollup 仍是真的)。已关/没见过的 id 仍**惰性绑定**(inert):
|
|
437
|
+
// 终态由通知落,严格优于错绑另一张开着的卡。
|
|
438
|
+
const named = pending.get(parentToolCallId);
|
|
439
|
+
if (named !== undefined && !SUBAGENT_TOOL_NAMES.has(named.name))
|
|
440
|
+
return undefined;
|
|
441
|
+
target = parentToolCallId;
|
|
442
|
+
}
|
|
443
|
+
else {
|
|
444
|
+
for (const [id, p] of pending) {
|
|
445
|
+
if (!boundToolCalls.has(id) && SUBAGENT_TOOL_NAMES.has(p.name)) {
|
|
446
|
+
target = id;
|
|
447
|
+
break;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
if (target === undefined)
|
|
452
|
+
return undefined;
|
|
453
|
+
taskCardBinding.set(taskId, target);
|
|
454
|
+
boundToolCalls.add(target);
|
|
455
|
+
// #6 live vs inert:只有绑到**此刻开着**的卡才把行系进本 turn 的生命周期
|
|
456
|
+
// (FIFO 目标恒开着;显式目标可能已关 = inert)。
|
|
457
|
+
if (pending.has(target))
|
|
458
|
+
liveBoundPanelTasks.add(taskId);
|
|
459
|
+
// cli 的 `SEMA_BIND_PROBE` 文件探针 → 注入口(载荷逐字同形,见 ADAPTER_DIVERGENCES-5)。
|
|
460
|
+
ctx.probe?.('bind', `bind ${taskId} -> ${target} via=${parentToolCallId !== undefined ? 'explicit' : 'fifo'}`);
|
|
461
|
+
return target;
|
|
462
|
+
};
|
|
463
|
+
/** 对抗复审修(w0zwpa251 #1):≥2 张未绑的 subagent 形卡开着且 wire 没点名 ⇒ 任何猜都是错归因。 */
|
|
464
|
+
const unboundSubagentCandidates = () => {
|
|
465
|
+
let n = 0;
|
|
466
|
+
for (const [id, p] of pending) {
|
|
467
|
+
if (!boundToolCalls.has(id) && SUBAGENT_TOOL_NAMES.has(p.name))
|
|
468
|
+
n++;
|
|
469
|
+
}
|
|
470
|
+
return n;
|
|
471
|
+
};
|
|
472
|
+
/** SubagentStart(cli fireSubagentStartHook 的臂化):类型恒记,fire 一生一次。 */
|
|
473
|
+
const fireSubagentStart = function* (taskId, agentType) {
|
|
474
|
+
subagentTypeById.set(taskId, agentType); // Stop 的类型查找恒记(fire 与否无关)
|
|
475
|
+
if (self.firedSubagentStartHookTaskIds.has(taskId))
|
|
476
|
+
return;
|
|
477
|
+
self.firedSubagentStartHookTaskIds.add(taskId);
|
|
478
|
+
ctx.probe?.('hook', `start ${taskId} type=${agentType}`);
|
|
479
|
+
yield chrome({ kind: 'subagent_lifecycle', laneProof: laneOfTask(taskId), phase: 'start', taskId, agentType });
|
|
480
|
+
};
|
|
481
|
+
/**
|
|
482
|
+
* 结掉一张关闭中的卡所绑的面板行(`cardId`),或 turn 末/abort 结掉**本 turn 活绑**的全部行
|
|
483
|
+
* (`cardId===null` 的防御 sweep —— 子代从不发终态 tick)。
|
|
484
|
+
* 拍1 查看态:显式关卡臂携委派卡的 output(= 子代最终报告)到 end 事件上;sweep 无报告。
|
|
485
|
+
*/
|
|
486
|
+
const settlePanelTasks = function* (cardId, isError, report) {
|
|
487
|
+
// inline 群组行孪生(clay dogfood 2026-07-03):冻结该卡的 tick 终值,免得"N agents finished"
|
|
488
|
+
// 行把真实工具数/token 重置回 0。null-cardId 的 sweep 结掉每一条活着的 inline 条目。
|
|
489
|
+
if (cardId !== null) {
|
|
490
|
+
yield chrome({ kind: 'inline_task_stats', laneProof: { lane: 'subagent', parentToolCallId: cardId }, op: 'settle', cardId, isError });
|
|
491
|
+
}
|
|
492
|
+
else {
|
|
493
|
+
yield chrome({ kind: 'inline_task_stats', laneProof: MAIN, op: 'settle_all' });
|
|
494
|
+
}
|
|
495
|
+
for (const taskId of livePanelTasks) {
|
|
496
|
+
if (cardId !== null && taskCardBinding.get(taskId) !== cardId)
|
|
497
|
+
continue;
|
|
498
|
+
// #6 —— 防御 sweep 只结**本 turn 活绑**的行(流被截断的形)。session 常驻行
|
|
499
|
+
// (inert 绑定 / unbound = 跨 turn bg 子代)绝不在这里冻成假 completed:它的终态真相
|
|
500
|
+
// 由 task_notification / bg_notification 落(通知-settle 边)。
|
|
501
|
+
if (cardId === null && !liveBoundPanelTasks.has(taskId))
|
|
502
|
+
continue;
|
|
503
|
+
if (endedPanelTasks.has(taskId))
|
|
504
|
+
continue;
|
|
505
|
+
endedPanelTasks.add(taskId);
|
|
506
|
+
const lane = laneOfTask(taskId);
|
|
507
|
+
const agentType = subagentTypeById.get(taskId);
|
|
508
|
+
yield chrome({
|
|
509
|
+
kind: 'subagent_lifecycle',
|
|
510
|
+
laneProof: lane,
|
|
511
|
+
phase: 'stop',
|
|
512
|
+
taskId,
|
|
513
|
+
...(agentType !== undefined ? { agentType } : {}),
|
|
514
|
+
});
|
|
515
|
+
yield chrome({
|
|
516
|
+
kind: 'panel_task',
|
|
517
|
+
laneProof: lane,
|
|
518
|
+
event: {
|
|
519
|
+
kind: 'end',
|
|
520
|
+
taskId,
|
|
521
|
+
isError,
|
|
522
|
+
...(cardId !== null && report !== undefined ? { report } : {}),
|
|
523
|
+
},
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
};
|
|
286
527
|
/** 提交累积的思考块(cli takeThinking:committed 形 + 关活体块 + 收活动行)。 */
|
|
287
528
|
function* takeThinking() {
|
|
288
529
|
if (thinking.length === 0)
|
|
@@ -338,6 +579,10 @@ class WireToCcAdapterImpl {
|
|
|
338
579
|
if (textPending.length > 0) {
|
|
339
580
|
const opening = !textBlockOpen;
|
|
340
581
|
textBlockOpen = true;
|
|
582
|
+
// B3:cli 的 `SEMA_STREAM_PROBE` 文件探针换成 ctx.probe 口(体检 #5 同款用途:Ink 挂载后
|
|
583
|
+
// console 被吞,harness 只能靠这条证明「引擎是**逐 token 增量**到的」而不是整块一次性 dump)。
|
|
584
|
+
// 载荷格式与 cli 逐字同形,只把 Date.now() 换成 ctx.now()(注入时钟纪律)。
|
|
585
|
+
ctx.probe?.('stream', `${ctx.now()} text_flush len=${textPending.length}`);
|
|
341
586
|
yield chrome({
|
|
342
587
|
kind: 'stream_delta',
|
|
343
588
|
laneProof: MAIN,
|
|
@@ -349,417 +594,660 @@ class WireToCcAdapterImpl {
|
|
|
349
594
|
textPending = '';
|
|
350
595
|
}
|
|
351
596
|
}
|
|
352
|
-
// turn 开场卫生(cli:清上一 turn 的 retry 覆盖层 +
|
|
597
|
+
// turn 开场卫生(T38 三清,cli 逐条同序):清上一 turn 的 retry 覆盖层 + **上一轮的建议批** +
|
|
598
|
+
// 思考活动行,再发 request-start(T39)。三清缺一都是"上一轮的东西挂着不掉"。
|
|
353
599
|
yield chrome({ kind: 'retry_status', laneProof: MAIN, status: null });
|
|
600
|
+
yield chrome({ kind: 'prompt_suggestions', laneProof: MAIN, suggestions: null });
|
|
354
601
|
yield chrome({ kind: 'thinking_activity', laneProof: MAIN, active: false });
|
|
355
602
|
yield chrome({ kind: 'request_start', laneProof: MAIN });
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
case 'assistant': {
|
|
374
|
-
const message = m.message;
|
|
375
|
-
const blocks = Array.isArray(message?.content)
|
|
376
|
-
? message.content
|
|
377
|
-
: [];
|
|
378
|
-
const toolUseIdx = blocks.findIndex(b => b?.type === 'tool_use');
|
|
379
|
-
if (toolUseIdx >= 0) {
|
|
380
|
-
const original = blocks[toolUseIdx];
|
|
381
|
-
const decision = decisionOf(original);
|
|
382
|
-
const san = sanitizeToolUseBlock(original);
|
|
383
|
-
const renderBlocks = blocks.map((b, i) => (i === toolUseIdx ? san.block : b));
|
|
384
|
-
const toolUseId = String(san.block.id);
|
|
385
|
-
const toolName = typeof san.block.name === 'string' ? san.block.name : 'unknown';
|
|
386
|
-
// 引擎模式下壳工具 call() 从不执行 —— ctrl+t 面板台账只能靠这条侧信道(宿主义务)。
|
|
387
|
-
if (TASK_TOOL_NAMES.has(toolName)) {
|
|
388
|
-
yield chrome({
|
|
389
|
-
kind: 'task_ledger_sync',
|
|
390
|
-
laneProof: MAIN,
|
|
391
|
-
source: 'tool_use',
|
|
392
|
-
toolName,
|
|
393
|
-
toolUseId,
|
|
394
|
-
input: san.block.input,
|
|
395
|
-
});
|
|
396
|
-
}
|
|
397
|
-
// #27 段序:先把这张卡之前的散文提交,再提交它之前的思考(思考恒在卡上方)。
|
|
398
|
-
yield* takeAnswerSegment();
|
|
399
|
-
yield* takeThinking();
|
|
400
|
-
yield transcript({
|
|
401
|
-
type: 'assistant',
|
|
402
|
-
// S1:per-RESPONSE 铸 id —— CC 的 grouped-agent 渲染按 `${message.id}:${toolName}`
|
|
403
|
-
// 分组;id-less 帧会把整个 session 的 Agent 卡塌成一张。tool_use 帧才盖章。
|
|
404
|
-
message: { id: mintResponseId(m), role: 'assistant', content: renderBlocks },
|
|
405
|
-
uuid: idOf(m),
|
|
406
|
-
session_id: ctx.sessionId,
|
|
407
|
-
parent_tool_use_id: null,
|
|
408
|
-
});
|
|
409
|
-
if (decision === 'reject' || decision === 'cancel') {
|
|
410
|
-
yield transcript({
|
|
411
|
-
type: 'user',
|
|
412
|
-
message: {
|
|
413
|
-
role: 'user',
|
|
414
|
-
content: [
|
|
415
|
-
{
|
|
416
|
-
type: 'tool_result',
|
|
417
|
-
tool_use_id: san.block.id,
|
|
418
|
-
content: decision === 'reject' ? REJECT_MESSAGE : CANCEL_MESSAGE,
|
|
419
|
-
is_error: false,
|
|
420
|
-
},
|
|
421
|
-
],
|
|
422
|
-
},
|
|
423
|
-
uuid: idOf(m, decision),
|
|
424
|
-
session_id: ctx.sessionId,
|
|
425
|
-
});
|
|
426
|
-
assistantResponseId = null; // 一个结果关闭该 model response
|
|
427
|
-
break;
|
|
428
|
-
}
|
|
429
|
-
if (decision === 'error') {
|
|
430
|
-
yield transcript({
|
|
431
|
-
type: 'user',
|
|
432
|
-
message: {
|
|
433
|
-
role: 'user',
|
|
434
|
-
content: [
|
|
435
|
-
{
|
|
436
|
-
type: 'tool_result',
|
|
437
|
-
tool_use_id: san.block.id,
|
|
438
|
-
content: 'The tool failed to execute.',
|
|
439
|
-
is_error: true,
|
|
440
|
-
},
|
|
441
|
-
],
|
|
442
|
-
},
|
|
443
|
-
uuid: idOf(m, 'error'),
|
|
444
|
-
session_id: ctx.sessionId,
|
|
445
|
-
});
|
|
446
|
-
assistantResponseId = null;
|
|
447
|
-
break;
|
|
448
|
-
}
|
|
449
|
-
pending.set(toolUseId, {
|
|
450
|
-
id: toolUseId,
|
|
451
|
-
name: toolName,
|
|
452
|
-
rawInput: san.rawInput,
|
|
453
|
-
...(san.eventId !== undefined ? { eventId: san.eventId } : {}),
|
|
454
|
-
...(san.parentToolCallId !== undefined ? { parentToolCallId: san.parentToolCallId } : {}),
|
|
455
|
-
});
|
|
456
|
-
if (WORKFLOW_TOOL_NAMES.has(toolName))
|
|
457
|
-
this.seenWorkflowToolCallIds.add(toolUseId);
|
|
603
|
+
// ── T40/T58/T59:手动 iterator + IDLE-FLUSH 竞速 + abort 断流 ────────────────────────────────
|
|
604
|
+
// `for await` 换成手动 next():① 才能与 idle 计时器 race(T40);② 才能在 abort 时立刻停;
|
|
605
|
+
// 代价是失去 for-await 的隐式 cleanup ⇒ finally 里手动 return()(T58,少了它 = SSE 不拆)。
|
|
606
|
+
const iterator = frames[Symbol.asyncIterator]();
|
|
607
|
+
let iteratorDone = false;
|
|
608
|
+
try {
|
|
609
|
+
for (;;) {
|
|
610
|
+
const nextPromise = iterator.next();
|
|
611
|
+
let step;
|
|
612
|
+
while (step === undefined) {
|
|
613
|
+
const hasPendingContent = thinking.length > 0 ||
|
|
614
|
+
answerSegment.length > 0 ||
|
|
615
|
+
thinkingPending.length > 0 ||
|
|
616
|
+
textPending.length > 0;
|
|
617
|
+
// 缓冲全空 ⇒ 零开销路径;宿主没给定时器 ⇒ 竞速整条不启用(绝不 setTimeout 兜底)。
|
|
618
|
+
if (!hasPendingContent || ctx.setTimer === undefined) {
|
|
619
|
+
step = await nextPromise;
|
|
458
620
|
break;
|
|
459
621
|
}
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
emittedAssistantText = true;
|
|
622
|
+
let idleTimer;
|
|
623
|
+
const raced = await Promise.race([
|
|
624
|
+
nextPromise.then(r => ({ tag: 'next', r })),
|
|
625
|
+
new Promise(resolve => {
|
|
626
|
+
idleTimer = ctx.setTimer?.(IDLE_FLUSH_MS, () => resolve({ tag: 'idle' }));
|
|
627
|
+
}),
|
|
628
|
+
]);
|
|
629
|
+
if (idleTimer !== undefined)
|
|
630
|
+
ctx.clearTimer?.(idleTimer);
|
|
631
|
+
if (raced.tag === 'next') {
|
|
632
|
+
step = raced.r;
|
|
472
633
|
}
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
634
|
+
else {
|
|
635
|
+
if (ctx.signal?.aborted)
|
|
636
|
+
break;
|
|
637
|
+
// Idle ≥1.5s 且有已生成未提交的内容:现在就上屏(活体预览在前,再按 思考→文本 的
|
|
638
|
+
// committed 序 —— 与工具卡前的边界 flush 逐字同序,只是提前发生)。
|
|
639
|
+
yield* drainLive();
|
|
640
|
+
yield* takeThinking();
|
|
641
|
+
yield* takeAnswerSegment();
|
|
642
|
+
// 缓冲已空 ⇒ 下一圈直接 plain-await。
|
|
482
643
|
}
|
|
483
|
-
break;
|
|
484
644
|
}
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
yield transcript({ ...m, uuid: idOf(m), session_id: ctx.sessionId });
|
|
645
|
+
if (step === undefined)
|
|
646
|
+
break; // abort while idle-waiting
|
|
647
|
+
if (step.done) {
|
|
648
|
+
iteratorDone = true;
|
|
490
649
|
break;
|
|
491
650
|
}
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
const callId = typeof m.toolCallId === 'string' ? m.toolCallId : undefined;
|
|
496
|
-
if (callId !== undefined && pending.has(callId)) {
|
|
497
|
-
pending.delete(callId);
|
|
498
|
-
assistantResponseId = null;
|
|
499
|
-
}
|
|
651
|
+
const rawFrame = step.value;
|
|
652
|
+
// T59 — 信号一跳就停止投影(后续任何臂都不再渲染)。
|
|
653
|
+
if (ctx.signal?.aborted)
|
|
500
654
|
break;
|
|
655
|
+
const m = rawFrame;
|
|
656
|
+
if (m === null || typeof m !== 'object')
|
|
657
|
+
continue;
|
|
658
|
+
if (m.type === 'stream_event' && !messageStartEmitted) {
|
|
659
|
+
messageStartEmitted = true;
|
|
660
|
+
yield chrome({
|
|
661
|
+
kind: 'response_metrics',
|
|
662
|
+
laneProof: MAIN,
|
|
663
|
+
phase: 'start',
|
|
664
|
+
ttftMs: ctx.now() - turnStartAt,
|
|
665
|
+
});
|
|
501
666
|
}
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
667
|
+
// 持久臂之前先把活体增量泄干净(活体预览恒先于它 settle 成的 committed 消息)。
|
|
668
|
+
if (m.type !== 'stream_event')
|
|
669
|
+
yield* drainLive();
|
|
670
|
+
switch (m.type) {
|
|
671
|
+
case 'assistant': {
|
|
672
|
+
const message = m.message;
|
|
673
|
+
const blocks = Array.isArray(message?.content)
|
|
674
|
+
? message.content
|
|
675
|
+
: [];
|
|
676
|
+
const toolUseIdx = blocks.findIndex(b => b?.type === 'tool_use');
|
|
677
|
+
if (toolUseIdx >= 0) {
|
|
678
|
+
const original = blocks[toolUseIdx];
|
|
679
|
+
const decision = decisionOf(original);
|
|
680
|
+
const san = sanitizeToolUseBlock(original);
|
|
681
|
+
const renderBlocks = blocks.map((b, i) => (i === toolUseIdx ? san.block : b));
|
|
682
|
+
const toolUseId = String(san.block.id);
|
|
683
|
+
const toolName = typeof san.block.name === 'string' ? san.block.name : 'unknown';
|
|
684
|
+
// P0-2 侧信道:core 展示名进本包台账(渲染块上已被 sanitize 摘掉)。cli 同位、同顺序
|
|
685
|
+
// (在任何 yield 之前),对 reject/cancel/error 三条早退路径同样生效。
|
|
686
|
+
recordEngineToolLabel(toolUseId, san.label);
|
|
687
|
+
// 引擎模式下壳工具 call() 从不执行 —— ctrl+t 面板台账只能靠这条侧信道(宿主义务)。
|
|
688
|
+
if (TASK_TOOL_NAMES.has(toolName)) {
|
|
689
|
+
yield chrome({
|
|
690
|
+
kind: 'task_ledger_sync',
|
|
691
|
+
laneProof: MAIN,
|
|
692
|
+
source: 'tool_use',
|
|
693
|
+
toolName,
|
|
694
|
+
toolUseId,
|
|
695
|
+
input: san.block.input,
|
|
696
|
+
});
|
|
697
|
+
}
|
|
698
|
+
// #27 段序:先把这张卡之前的散文提交,再提交它之前的思考(思考恒在卡上方)。
|
|
699
|
+
yield* takeAnswerSegment();
|
|
508
700
|
yield* takeThinking();
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
701
|
+
yield transcript({
|
|
702
|
+
type: 'assistant',
|
|
703
|
+
// S1:per-RESPONSE 铸 id —— CC 的 grouped-agent 渲染按 `${message.id}:${toolName}`
|
|
704
|
+
// 分组;id-less 帧会把整个 session 的 Agent 卡塌成一张。tool_use 帧才盖章。
|
|
705
|
+
message: { id: mintResponseId(m), role: 'assistant', content: renderBlocks },
|
|
706
|
+
uuid: idOf(m),
|
|
707
|
+
session_id: ctx.sessionId,
|
|
708
|
+
parent_tool_use_id: null,
|
|
709
|
+
});
|
|
710
|
+
if (decision === 'reject' || decision === 'cancel') {
|
|
711
|
+
yield transcript({
|
|
712
|
+
type: 'user',
|
|
713
|
+
message: {
|
|
714
|
+
role: 'user',
|
|
715
|
+
content: [
|
|
716
|
+
{
|
|
717
|
+
type: 'tool_result',
|
|
718
|
+
tool_use_id: san.block.id,
|
|
719
|
+
content: decision === 'reject' ? REJECT_MESSAGE : CANCEL_MESSAGE,
|
|
720
|
+
is_error: false,
|
|
721
|
+
},
|
|
722
|
+
],
|
|
723
|
+
},
|
|
724
|
+
uuid: idOf(m, decision),
|
|
725
|
+
session_id: ctx.sessionId,
|
|
726
|
+
});
|
|
727
|
+
assistantResponseId = null; // 一个结果关闭该 model response
|
|
728
|
+
break;
|
|
729
|
+
}
|
|
730
|
+
if (decision === 'error') {
|
|
731
|
+
yield transcript({
|
|
732
|
+
type: 'user',
|
|
733
|
+
message: {
|
|
734
|
+
role: 'user',
|
|
735
|
+
content: [
|
|
736
|
+
{
|
|
737
|
+
type: 'tool_result',
|
|
738
|
+
tool_use_id: san.block.id,
|
|
739
|
+
content: 'The tool failed to execute.',
|
|
740
|
+
is_error: true,
|
|
741
|
+
},
|
|
742
|
+
],
|
|
743
|
+
},
|
|
744
|
+
uuid: idOf(m, 'error'),
|
|
745
|
+
session_id: ctx.sessionId,
|
|
746
|
+
});
|
|
747
|
+
assistantResponseId = null;
|
|
748
|
+
break;
|
|
749
|
+
}
|
|
750
|
+
pending.set(toolUseId, {
|
|
751
|
+
id: toolUseId,
|
|
752
|
+
name: toolName,
|
|
753
|
+
rawInput: san.rawInput,
|
|
754
|
+
...(san.eventId !== undefined ? { eventId: san.eventId } : {}),
|
|
755
|
+
...(san.parentToolCallId !== undefined ? { parentToolCallId: san.parentToolCallId } : {}),
|
|
756
|
+
});
|
|
757
|
+
if (WORKFLOW_TOOL_NAMES.has(toolName))
|
|
758
|
+
this.seenWorkflowToolCallIds.add(toolUseId);
|
|
519
759
|
break;
|
|
520
|
-
if (thinking.length === 0) {
|
|
521
|
-
thinkingAnchor = m;
|
|
522
|
-
// P2d:elapsed 锚在**首条** leader 推理增量(token 累计由 stream_delta.estimatedTokens
|
|
523
|
-
// 承载,宿主自加;>30s 无增量的 stall 提示同样由宿主按增量时间戳判)。
|
|
524
|
-
yield chrome({ kind: 'thinking_activity', laneProof: MAIN, active: true });
|
|
525
760
|
}
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
endEmitted = true;
|
|
537
|
-
messageStartEmitted = false; // 下一 model round 重新压基线
|
|
538
|
-
}
|
|
539
|
-
break;
|
|
540
|
-
}
|
|
541
|
-
case 'result': {
|
|
542
|
-
yield chrome({ kind: 'retry_status', laneProof: MAIN, status: null });
|
|
543
|
-
yield* takeThinking();
|
|
544
|
-
if (!endEmitted) {
|
|
545
|
-
const u = m.usage;
|
|
546
|
-
if (typeof u?.outputTokens === 'number') {
|
|
547
|
-
yield chrome({
|
|
548
|
-
kind: 'response_metrics',
|
|
549
|
-
laneProof: MAIN,
|
|
550
|
-
phase: 'end',
|
|
551
|
-
outputTokens: u.outputTokens,
|
|
761
|
+
const textBlock = blocks.find(b => b?.type === 'text');
|
|
762
|
+
if (textBlock && typeof textBlock.text === 'string' && textBlock.text.length > 0) {
|
|
763
|
+
answer += textBlock.text;
|
|
764
|
+
yield* takeThinking();
|
|
765
|
+
yield transcript({
|
|
766
|
+
type: 'assistant',
|
|
767
|
+
message: { role: 'assistant', content: [{ type: 'text', text: textBlock.text }] },
|
|
768
|
+
uuid: idOf(m),
|
|
769
|
+
session_id: ctx.sessionId,
|
|
770
|
+
parent_tool_use_id: null,
|
|
552
771
|
});
|
|
553
|
-
|
|
772
|
+
emittedAssistantText = true;
|
|
554
773
|
}
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
if (!emittedAssistantText) {
|
|
558
|
-
const text = typeof m.result === 'string' ? m.result : '';
|
|
559
|
-
if (text.length > 0) {
|
|
774
|
+
const thinkingBlock = blocks.find(b => b?.type === 'thinking');
|
|
775
|
+
if (thinkingBlock && typeof thinkingBlock.thinking === 'string') {
|
|
560
776
|
yield transcript({
|
|
561
777
|
type: 'assistant',
|
|
562
|
-
message: { role: 'assistant', content: [
|
|
563
|
-
uuid: idOf(m, '
|
|
778
|
+
message: { role: 'assistant', content: [thinkingBlock] },
|
|
779
|
+
uuid: idOf(m, 'durable-thinking'),
|
|
564
780
|
session_id: ctx.sessionId,
|
|
565
781
|
parent_tool_use_id: null,
|
|
566
782
|
});
|
|
567
|
-
emittedAssistantText = true;
|
|
568
783
|
}
|
|
784
|
+
break;
|
|
569
785
|
}
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
786
|
+
// DIVERGENCE-1 —— 现役 wire 不产 user 帧;透传是给 desktop 重放存量转录留的门。
|
|
787
|
+
case 'user': {
|
|
788
|
+
if (m.message === undefined)
|
|
789
|
+
break;
|
|
790
|
+
yield transcript({ ...m, uuid: idOf(m), session_id: ctx.sessionId });
|
|
791
|
+
break;
|
|
792
|
+
}
|
|
793
|
+
case 'tool_end_result': {
|
|
794
|
+
// B4 起本臂做三件:① P0-2 label 补位(只有 tool_end 带 label 的场景;台账自身幂等不覆盖)
|
|
795
|
+
// ② MF-10 **关卡 settle**(子代从不发终态 tick,父卡的 tool_end 就是完成信号;卡的 output
|
|
796
|
+
// 文本 = 子代最终报告,串到行 end 事件上)③ 卡台账出栈 + response-id 复位(S1)。
|
|
797
|
+
// tool_result 消息本体(B 层结构化卡)仍见 ADAPTER_COVERAGE.todo → B5。
|
|
798
|
+
const callId = typeof m.toolCallId === 'string' ? m.toolCallId : undefined;
|
|
799
|
+
recordEngineToolLabel(callId, m.label);
|
|
800
|
+
if (callId !== undefined) {
|
|
801
|
+
yield* settlePanelTasks(callId, m.isError === true, m.output !== undefined ? flattenWireOutput(m.output) : undefined);
|
|
802
|
+
}
|
|
803
|
+
if (callId !== undefined && pending.has(callId)) {
|
|
804
|
+
pending.delete(callId);
|
|
805
|
+
assistantResponseId = null;
|
|
806
|
+
}
|
|
807
|
+
break;
|
|
808
|
+
}
|
|
809
|
+
case 'task_progress': {
|
|
810
|
+
// ── B4 A 层最后一臂(MF-10)—— 引擎侧子代的累计 usage tick。
|
|
811
|
+
// 只带 rollup 数字 = CC `AgentProgress` 同形 ⇒ 忠实的 CC-187 面是 footer 面板行
|
|
812
|
+
// (CC 自己对进程外 agent 的先例:面板行 + task-notification,从不合成 inline 消息轨)。
|
|
813
|
+
// **不产任何 transcript 消息**:全是 chrome。
|
|
814
|
+
const taskId = typeof m.taskId === 'string' ? m.taskId : undefined;
|
|
815
|
+
if (taskId === undefined)
|
|
816
|
+
break;
|
|
817
|
+
const explicitParent = typeof m.parentToolCallId === 'string' ? m.parentToolCallId : undefined;
|
|
818
|
+
// ── W1 段2([1617]②,frame-lane-matrix #2/#8)workflow lane 门:workflow 子代的 tick 属
|
|
819
|
+
// fleet lane 自描述行(footer 只渲 workflow 聚合行),**绝不**进前台 subagent lane
|
|
820
|
+
// (不登 alias、不 bind、不 livePanelTasks、不发 panel tick —— 修前每个 workflow agent
|
|
821
|
+
// 都在 footer 铸一条幽灵 local_agent 行,还能骑错 run_workflow 卡的 inline stats)。
|
|
822
|
+
// 判据三级:① 帧带 workflowRunId(一手证据)② wa+16hex 形 id(core BCE 合成 id)
|
|
823
|
+
// ③ 显式父指向本会话见过的 Workflow 工具卡(实例台账,覆盖 uuid 形子代 id + 跨 turn 迟到 tick)。
|
|
824
|
+
const workflowRunId = typeof m.workflowRunId === 'string' ? m.workflowRunId : undefined;
|
|
825
|
+
if (workflowRunId !== undefined ||
|
|
826
|
+
isWorkflowAgentTaskId(taskId) ||
|
|
827
|
+
(explicitParent !== undefined && this.seenWorkflowToolCallIds.has(explicitParent))) {
|
|
828
|
+
// 判据④ 回植([1641] workflow 启动闪 subagent 行案):本门是「这条 taskId 属 workflow」的
|
|
829
|
+
// **权威判定**。同一 taskId 在 fleet 面还有一条 run 发布器双生行(`${runId} ${taskId}`)
|
|
830
|
+
// 且那行不带 workflowRunId —— 登进 belt 台账,fleet 行按尾段查它,双生行就再进不了
|
|
831
|
+
// footer task 树。🔴 写者在本包、读者(isKnownWorkflowAgentTaskId)也在本包 = R4 纪律,
|
|
832
|
+
// 做成 chrome 臂反而会让「宿主没接线 ⇒ Set 恒空 ⇒ 判据④ 静默失效」复活。
|
|
833
|
+
recordWorkflowAgentTaskId(taskId);
|
|
834
|
+
ctx.probe?.('tick', JSON.stringify({
|
|
835
|
+
t: ctx.now(),
|
|
836
|
+
taskId,
|
|
837
|
+
lane: 'workflow',
|
|
838
|
+
via: workflowRunId !== undefined
|
|
839
|
+
? 'workflowRunId'
|
|
840
|
+
: isWorkflowAgentTaskId(taskId)
|
|
841
|
+
? 'wa-id'
|
|
842
|
+
: 'workflow-card-parent',
|
|
843
|
+
}));
|
|
844
|
+
break;
|
|
845
|
+
}
|
|
846
|
+
// C1 alias:content 帧只带 parentToolCallId(EventIdentity 上没有 taskId),这条 tick
|
|
847
|
+
// **两者都带** —— 是 store 学到「停放的内容属哪条引擎行」的唯一位置(查看态按引擎 task id 查)。
|
|
848
|
+
if (explicitParent !== undefined)
|
|
849
|
+
registerSubagentAlias(explicitParent, taskId);
|
|
850
|
+
// 复审修 #1:含糊回落(无显式父 + >1 张未绑候选)⇒ 保持 unbound。
|
|
851
|
+
const cardId = explicitParent === undefined && !taskCardBinding.has(taskId) && unboundSubagentCandidates() > 1
|
|
852
|
+
? undefined
|
|
853
|
+
: bindTaskCard(taskId, explicitParent);
|
|
854
|
+
const usage = (m.usage ?? {});
|
|
855
|
+
ctx.probe?.('tick', JSON.stringify({
|
|
856
|
+
t: ctx.now(),
|
|
857
|
+
taskId,
|
|
858
|
+
lane: 'foreground',
|
|
859
|
+
parent: explicitParent ?? null,
|
|
860
|
+
cardId: cardId ?? null,
|
|
861
|
+
currentAction: m.currentAction ?? null,
|
|
862
|
+
toolUses: usage.toolUses ?? null,
|
|
863
|
+
}));
|
|
864
|
+
// core 1.262.0 [558]B / service 1.154.0 [579] —— 子代的活体"我在干什么"一行
|
|
865
|
+
// ("Bash npm test"),service 已 redact。PROGRESS 语义(喂状态/摘要显示槽),不是诊断。
|
|
866
|
+
// 老引擎缺席 ⇒ 两个消费面维持今天的显示。
|
|
867
|
+
const currentAction = typeof m.currentAction === 'string' && m.currentAction.length > 0 ? m.currentAction : undefined;
|
|
868
|
+
// inline 群组行孪生:**绑定了**的 tick 同时喂转录里那条行的统计(按委派卡 id 键)。
|
|
869
|
+
if (cardId !== undefined) {
|
|
582
870
|
yield chrome({
|
|
583
|
-
kind: '
|
|
584
|
-
laneProof:
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
871
|
+
kind: 'inline_task_stats',
|
|
872
|
+
laneProof: { lane: 'subagent', parentToolCallId: cardId },
|
|
873
|
+
op: 'tick',
|
|
874
|
+
cardId,
|
|
875
|
+
stats: {
|
|
876
|
+
toolUses: typeof usage.toolUses === 'number' ? usage.toolUses : 0,
|
|
877
|
+
totalTokens: typeof usage.totalTokens === 'number' ? usage.totalTokens : 0,
|
|
878
|
+
durationMs: typeof usage.durationMs === 'number' ? usage.durationMs : 0,
|
|
879
|
+
...(currentAction !== undefined ? { currentAction } : {}),
|
|
590
880
|
},
|
|
591
881
|
});
|
|
592
882
|
}
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
883
|
+
const card = cardId !== undefined ? pending.get(cardId) : undefined;
|
|
884
|
+
// T50 双形读:`rawInput` 是**改名前**捕获的,真 wire 上引擎子代的 input 是
|
|
885
|
+
// {agent, task, taskName} —— 两形都读(CC 名优先)。
|
|
886
|
+
const input = (card?.rawInput ?? {});
|
|
887
|
+
const description = typeof input.description === 'string'
|
|
888
|
+
? input.description
|
|
889
|
+
: typeof input.taskName === 'string' && input.taskName.trim().length > 0
|
|
890
|
+
? input.taskName
|
|
891
|
+
: typeof input.task === 'string'
|
|
892
|
+
? shortTaskLabel(input.task)
|
|
893
|
+
: undefined;
|
|
894
|
+
const prompt = typeof input.prompt === 'string'
|
|
895
|
+
? input.prompt
|
|
896
|
+
: typeof input.task === 'string'
|
|
897
|
+
? input.task
|
|
898
|
+
: undefined;
|
|
899
|
+
if (!livePanelTasks.has(taskId)) {
|
|
900
|
+
// 这条引擎子代在壳侧**可观测的起点** = 本 turn 的首 tick。
|
|
901
|
+
const agentType = typeof m.name === 'string' && m.name.length > 0 ? m.name : 'subagent';
|
|
902
|
+
yield* fireSubagentStart(taskId, agentType);
|
|
903
|
+
}
|
|
904
|
+
// #6 session 常驻台账:live-bound(卡本 turn 开着)= 本 turn 生命周期,turn 末可 sweep;
|
|
905
|
+
// inert/unbound(跨 turn bg 子代形)= 常驻,两处 sweep 都放行,终态只认通知帧。
|
|
906
|
+
if (liveBoundPanelTasks.has(taskId))
|
|
907
|
+
clearEnginePanelTaskResident(taskId);
|
|
908
|
+
else
|
|
909
|
+
markEnginePanelTaskResident(taskId);
|
|
910
|
+
livePanelTasks.add(taskId);
|
|
608
911
|
yield chrome({
|
|
609
|
-
kind: '
|
|
610
|
-
laneProof:
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
? {
|
|
615
|
-
: {}),
|
|
912
|
+
kind: 'panel_task',
|
|
913
|
+
laneProof: laneOfTask(taskId),
|
|
914
|
+
event: {
|
|
915
|
+
kind: 'tick',
|
|
916
|
+
taskId,
|
|
917
|
+
...(typeof m.name === 'string' && m.name.length > 0 ? { name: m.name } : {}),
|
|
918
|
+
...(description !== undefined ? { description } : {}),
|
|
919
|
+
...(prompt !== undefined ? { prompt } : {}),
|
|
920
|
+
...(currentAction !== undefined ? { currentAction } : {}),
|
|
921
|
+
toolUses: typeof usage.toolUses === 'number' ? usage.toolUses : 0,
|
|
922
|
+
totalTokens: typeof usage.totalTokens === 'number' ? usage.totalTokens : 0,
|
|
923
|
+
},
|
|
616
924
|
});
|
|
925
|
+
break;
|
|
617
926
|
}
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
927
|
+
case 'stream_event': {
|
|
928
|
+
const ev = m.event;
|
|
929
|
+
if (ev && ev.type === 'text_delta' && typeof ev.delta === 'string') {
|
|
930
|
+
// MOD-1 思考→回答边界:回答一起头就把思考块 committed 上屏(它推理的内容之前)。
|
|
931
|
+
if (thinking.length > 0 || thinkingPending.length > 0) {
|
|
932
|
+
yield* drainLive();
|
|
933
|
+
yield* takeThinking();
|
|
934
|
+
}
|
|
935
|
+
if (answerSegment.length === 0)
|
|
936
|
+
segmentAnchor = m;
|
|
937
|
+
answer += ev.delta;
|
|
938
|
+
answerSegment += ev.delta;
|
|
939
|
+
textPending += ev.delta;
|
|
940
|
+
}
|
|
941
|
+
else if (ev && ev.type === 'thinking_delta' && typeof ev.delta === 'string') {
|
|
942
|
+
// 子流(parentToolCallId)的推理绝不喂 leader 的计数与预览(#9 的壳侧半场)。
|
|
943
|
+
if (ev.parentToolCallId !== undefined)
|
|
944
|
+
break;
|
|
945
|
+
if (thinking.length === 0) {
|
|
946
|
+
thinkingAnchor = m;
|
|
947
|
+
// P2d:elapsed 锚在**首条** leader 推理增量(token 累计由 stream_delta.estimatedTokens
|
|
948
|
+
// 承载,宿主自加;>30s 无增量的 stall 提示同样由宿主按增量时间戳判)。
|
|
949
|
+
yield chrome({ kind: 'thinking_activity', laneProof: MAIN, active: true });
|
|
950
|
+
}
|
|
951
|
+
thinking += ev.delta;
|
|
952
|
+
thinkingPending += ev.delta;
|
|
953
|
+
}
|
|
625
954
|
break;
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
laneProof: MAIN,
|
|
955
|
+
}
|
|
956
|
+
case 'turn_usage': {
|
|
957
|
+
const ot = typeof m.outputTokens === 'number' ? m.outputTokens : undefined;
|
|
958
|
+
if (ot != null) {
|
|
959
|
+
yield* drainLive();
|
|
960
|
+
yield chrome({ kind: 'response_metrics', laneProof: MAIN, phase: 'end', outputTokens: ot });
|
|
961
|
+
endEmitted = true;
|
|
962
|
+
messageStartEmitted = false; // 下一 model round 重新压基线
|
|
963
|
+
}
|
|
964
|
+
break;
|
|
965
|
+
}
|
|
966
|
+
case 'result': {
|
|
967
|
+
yield chrome({ kind: 'retry_status', laneProof: MAIN, status: null });
|
|
968
|
+
// MF-10 防御 sweep(sweep 点 1/3):结掉那些卡从没关过的行(流被截断)。
|
|
969
|
+
// cli 同位:setRetryStatus(null) 之后、开卡兜底关闭之前。
|
|
970
|
+
yield* settlePanelTasks(null, false);
|
|
971
|
+
yield* takeThinking();
|
|
972
|
+
if (!endEmitted) {
|
|
973
|
+
const u = m.usage;
|
|
974
|
+
if (typeof u?.outputTokens === 'number') {
|
|
975
|
+
yield chrome({
|
|
976
|
+
kind: 'response_metrics',
|
|
977
|
+
laneProof: MAIN,
|
|
978
|
+
phase: 'end',
|
|
979
|
+
outputTokens: u.outputTokens,
|
|
980
|
+
});
|
|
981
|
+
endEmitted = true;
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
yield* takeAnswerSegment();
|
|
985
|
+
if (!emittedAssistantText) {
|
|
986
|
+
const text = typeof m.result === 'string' ? m.result : '';
|
|
987
|
+
if (text.length > 0) {
|
|
988
|
+
yield transcript({
|
|
989
|
+
type: 'assistant',
|
|
990
|
+
message: { role: 'assistant', content: [{ type: 'text', text }] },
|
|
991
|
+
uuid: idOf(m, 'terminal-text'),
|
|
992
|
+
session_id: ctx.sessionId,
|
|
993
|
+
parent_tool_use_id: null,
|
|
994
|
+
});
|
|
995
|
+
emittedAssistantText = true;
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
break;
|
|
999
|
+
}
|
|
1000
|
+
case 'system': {
|
|
1001
|
+
yield transcript(m);
|
|
1002
|
+
// core 1.219 attachWorkingFiles:compact_boundary 可携 attachedFiles → CC 的
|
|
1003
|
+
// "Referenced file {path}" 行(post-compact attachment,零新 UI)。
|
|
1004
|
+
const attachedFiles = m.attachedFiles;
|
|
1005
|
+
if (Array.isArray(attachedFiles)) {
|
|
1006
|
+
let i = 0;
|
|
1007
|
+
for (const f of attachedFiles) {
|
|
1008
|
+
if (typeof f?.path !== 'string' || f.path.length === 0)
|
|
1009
|
+
continue;
|
|
1010
|
+
yield chrome({
|
|
1011
|
+
kind: 'attachment',
|
|
1012
|
+
laneProof: MAIN,
|
|
1013
|
+
id: idOf(m, `attached-file-${i++}`),
|
|
1014
|
+
attachment: {
|
|
1015
|
+
type: 'compact_file_reference',
|
|
1016
|
+
filename: f.path,
|
|
1017
|
+
displayPath: f.path,
|
|
1018
|
+
},
|
|
1019
|
+
});
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
break;
|
|
1023
|
+
}
|
|
1024
|
+
case 'task_notification': {
|
|
1025
|
+
const n = (m.notification ?? {});
|
|
1026
|
+
const fields = normalizeTaskNotification(n);
|
|
1027
|
+
if (fields === null)
|
|
1028
|
+
break; // 空/缺 task_id:RB-75 同族,丢弃
|
|
1029
|
+
const taskId = fields.taskId;
|
|
1030
|
+
const rawStatus = fields.status;
|
|
1031
|
+
const terminal = rawStatus === 'completed' || rawStatus === 'failed' || rawStatus === 'killed';
|
|
1032
|
+
// #117a settle 半场 —— **故意在渲染去重之前**:settle 幂等,重放帧仍须 settle 一条
|
|
1033
|
+
// 首投之后才注册的行。#47 矩阵 #7:只有 bash 行(或老引擎无 task_type)能被 settle。
|
|
1034
|
+
const settleAllowed = n.task_type === undefined || n.task_type === 'background_bash';
|
|
1035
|
+
if (settleAllowed && terminal) {
|
|
1036
|
+
yield chrome({
|
|
1037
|
+
kind: 'bgshell_settle',
|
|
1038
|
+
laneProof: MAIN,
|
|
1039
|
+
taskId,
|
|
1040
|
+
status: rawStatus,
|
|
1041
|
+
...(typeof n.output_file === 'string' && n.output_file.length > 0
|
|
1042
|
+
? { outputPath: n.output_file }
|
|
1043
|
+
: {}),
|
|
1044
|
+
});
|
|
1045
|
+
}
|
|
1046
|
+
const dedupKey = taskNotificationDedupKey({
|
|
632
1047
|
taskId,
|
|
633
1048
|
status: rawStatus,
|
|
1049
|
+
...(typeof n.seq === 'number' || typeof n.seq === 'string' ? { seq: n.seq } : {}),
|
|
634
1050
|
...(typeof n.task_type === 'string' ? { taskType: n.task_type } : {}),
|
|
635
1051
|
});
|
|
636
|
-
if (
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
});
|
|
1052
|
+
if (this.renderedNotifications.has(dedupKey))
|
|
1053
|
+
break;
|
|
1054
|
+
this.renderedNotifications.add(dedupKey);
|
|
1055
|
+
// B1 + #6 —— 必须在 cardEnqueued 早退**之前**:probe 合成先入队的场景恰是双投主形。
|
|
1056
|
+
if (n.task_type !== 'external' && terminal) {
|
|
642
1057
|
yield chrome({
|
|
643
|
-
kind: '
|
|
1058
|
+
kind: 'notification_terminal',
|
|
644
1059
|
laneProof: MAIN,
|
|
645
|
-
phase: 'stop',
|
|
646
1060
|
taskId,
|
|
647
|
-
|
|
1061
|
+
status: rawStatus,
|
|
1062
|
+
...(typeof n.task_type === 'string' ? { taskType: n.task_type } : {}),
|
|
648
1063
|
});
|
|
1064
|
+
// B1 —— 这帧 = 引擎 server-side steer-inject 的回声,模型已在引擎侧收到一遍;把还躺在
|
|
1065
|
+
// 队列里的同 run Path B 条目(workflow_complete/probe/bg feeder)在 turn 收口 drain 前
|
|
1066
|
+
// 丢弃,免得 idle auto-submit 再喂模型一遍(跨进程双投的客户端半场)。
|
|
1067
|
+
// 🔴 B4 起由**适配器自持**(B2 已把队列端口搬进本包):此前挂在 `notification_terminal`
|
|
1068
|
+
// 的宿主义务①,是「说得通但没人做」的典型 —— 宿主没接线就静默双投。
|
|
1069
|
+
dropQueuedNotificationsForRun(taskId);
|
|
1070
|
+
if (n.task_type !== 'background_bash') {
|
|
1071
|
+
// #6 通知-settle 边(推送半场):session 常驻的 bg 子代行不再被 turn sweep 假结,
|
|
1072
|
+
// 真终态在此落行(failed/killed ⇒ isError 真归因)。
|
|
1073
|
+
clearEnginePanelTaskResident(taskId);
|
|
1074
|
+
yield chrome({
|
|
1075
|
+
kind: 'panel_task',
|
|
1076
|
+
laneProof: MAIN,
|
|
1077
|
+
event: { kind: 'end', taskId, isError: rawStatus !== 'completed' },
|
|
1078
|
+
});
|
|
1079
|
+
if (!endedPanelTasks.has(taskId)) {
|
|
1080
|
+
endedPanelTasks.add(taskId);
|
|
1081
|
+
// SubagentStop 只对**真 fire 过 Start** 的子代(workflow runId / 外来 id 不配对乱 fire)。
|
|
1082
|
+
// cli 同门 `firedSubagentStartHookTaskIds.has(taskId)` —— B4 前本臂无条件发 stop,
|
|
1083
|
+
// 宿主若照 guard 注释自建台账才不会乱 fire;台账在库里就该库来判。
|
|
1084
|
+
if (this.firedSubagentStartHookTaskIds.has(taskId)) {
|
|
1085
|
+
yield chrome({
|
|
1086
|
+
kind: 'subagent_lifecycle',
|
|
1087
|
+
laneProof: MAIN,
|
|
1088
|
+
phase: 'stop',
|
|
1089
|
+
taskId,
|
|
1090
|
+
guard: 'if-started',
|
|
1091
|
+
});
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
649
1095
|
}
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
});
|
|
665
|
-
break;
|
|
666
|
-
}
|
|
667
|
-
case 'workflow_complete': {
|
|
668
|
-
const runId = typeof m.runId === 'string' ? m.runId : undefined;
|
|
669
|
-
if (runId === undefined)
|
|
670
|
-
break;
|
|
671
|
-
// 跨通道去重(cli enqueueEngineWorkflowNotification 首行):该 run 已由推送帧记账过
|
|
672
|
-
// (引擎 server-side 注入过)⇒ Path B 绝不再喂模型一遍。适配器自持,不摊给宿主。
|
|
673
|
-
if (this.notifiedRuns.has(runId))
|
|
1096
|
+
// 双通道反向去重:probe 合成的完成卡已入队 ⇒ 后到的推送帧只做上面的记账,不渲第二条。
|
|
1097
|
+
if (n.task_type !== 'external' && this.cardEnqueuedRuns.has(taskId))
|
|
1098
|
+
break;
|
|
1099
|
+
// 跨通道记账(cli markEngineWorkflowNotified 同位、同条件:非 external,不看终态):
|
|
1100
|
+
// 这帧到达 = 引擎已 server-side steer-inject 过,补发通道(workflow_complete)不得再注入。
|
|
1101
|
+
// external 豁免([769] 对抗复审收紧:external 从不经补发通道,mark 只会缴械 probe 兜底)。
|
|
1102
|
+
if (n.task_type !== 'external')
|
|
1103
|
+
this.notifiedRuns.add(taskId);
|
|
1104
|
+
yield transcript({
|
|
1105
|
+
type: 'user',
|
|
1106
|
+
message: { role: 'user', content: [{ type: 'text', text: renderTaskNotificationXml(fields) }] },
|
|
1107
|
+
uuid: idOf(m),
|
|
1108
|
+
session_id: ctx.sessionId,
|
|
1109
|
+
});
|
|
674
1110
|
break;
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
1111
|
+
}
|
|
1112
|
+
case 'workflow_complete': {
|
|
1113
|
+
const runId = typeof m.runId === 'string' ? m.runId : undefined;
|
|
1114
|
+
if (runId === undefined)
|
|
1115
|
+
break;
|
|
1116
|
+
// 跨通道去重(cli enqueueEngineWorkflowNotification 首行):该 run 已由推送帧记账过
|
|
1117
|
+
// (引擎 server-side 注入过)⇒ Path B 绝不再喂模型一遍。适配器自持,不摊给宿主。
|
|
1118
|
+
if (this.notifiedRuns.has(runId))
|
|
1119
|
+
break;
|
|
1120
|
+
this.notifiedRuns.add(runId);
|
|
1121
|
+
const wfStatus = m.status === 'failed' ? 'failed' : 'completed';
|
|
1122
|
+
const wfSummary = typeof m.summary === 'string' ? m.summary : '';
|
|
1123
|
+
yield chrome({
|
|
1124
|
+
kind: 'workflow_notification_enqueue',
|
|
1125
|
+
laneProof: MAIN,
|
|
1126
|
+
runId,
|
|
1127
|
+
status: wfStatus,
|
|
1128
|
+
summary: wfSummary,
|
|
1129
|
+
// 模型面正文照铸(宿主直接入队即可,不必各自拼 XML;与 core 的 tag/段序同形)
|
|
1130
|
+
message: renderTaskNotificationXml({ taskId: runId, status: wfStatus, summary: wfSummary }),
|
|
1131
|
+
});
|
|
1132
|
+
this.cardEnqueuedRuns.add(runId);
|
|
695
1133
|
break;
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
1134
|
+
}
|
|
1135
|
+
case 'diagnostics': {
|
|
1136
|
+
if (typeof m.parentToolCallId === 'string')
|
|
1137
|
+
break; // 子流帧不上 leader 面
|
|
1138
|
+
const att = projectDiagnosticsFrame(m);
|
|
1139
|
+
if (att === null)
|
|
1140
|
+
break;
|
|
1141
|
+
yield chrome({ kind: 'attachment', laneProof: MAIN, id: idOf(m), attachment: att });
|
|
701
1142
|
break;
|
|
702
|
-
|
|
703
|
-
|
|
1143
|
+
}
|
|
1144
|
+
case 'steering_injected': {
|
|
1145
|
+
if (typeof m.parentToolCallId === 'string')
|
|
1146
|
+
break;
|
|
1147
|
+
const source = typeof m.source === 'string' ? m.source : '';
|
|
1148
|
+
if (source.length === 0)
|
|
1149
|
+
break;
|
|
1150
|
+
const preview = typeof m.preview === 'string' ? m.preview : '';
|
|
1151
|
+
let i = 0;
|
|
1152
|
+
for (const att of steeringInjectedToAttachments(source, preview)) {
|
|
1153
|
+
yield chrome({
|
|
1154
|
+
kind: 'attachment',
|
|
1155
|
+
laneProof: MAIN,
|
|
1156
|
+
id: idOf(m, `steering-${i++}`),
|
|
1157
|
+
attachment: att,
|
|
1158
|
+
});
|
|
1159
|
+
}
|
|
704
1160
|
break;
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
1161
|
+
}
|
|
1162
|
+
case 'workspace_changed': {
|
|
1163
|
+
if (typeof m.parentToolCallId === 'string')
|
|
1164
|
+
break;
|
|
1165
|
+
const cwd = typeof m.cwd === 'string' ? m.cwd : '';
|
|
1166
|
+
if (cwd.length === 0)
|
|
1167
|
+
break;
|
|
708
1168
|
yield chrome({
|
|
709
1169
|
kind: 'attachment',
|
|
710
1170
|
laneProof: MAIN,
|
|
711
|
-
id: idOf(m
|
|
712
|
-
attachment:
|
|
1171
|
+
id: idOf(m),
|
|
1172
|
+
attachment: { type: 'workspace_changed', cwd },
|
|
713
1173
|
});
|
|
1174
|
+
break;
|
|
714
1175
|
}
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
1176
|
+
case 'prompt_suggestions': {
|
|
1177
|
+
// T57 第五处断闸(#47 矩阵 #5 同族):子流(parentToolCallId 标)的建议绝不骑主 composer。
|
|
1178
|
+
if (typeof m.parentToolCallId === 'string')
|
|
1179
|
+
break;
|
|
1180
|
+
const list = Array.isArray(m.suggestions)
|
|
1181
|
+
? m.suggestions.filter((s) => typeof s === 'string')
|
|
1182
|
+
: [];
|
|
1183
|
+
// 空批不发(cli 同门:`if (list.length > 0)`)—— 作废由 turn 开场那一发负责,
|
|
1184
|
+
// 这里再发一次 null 会把本轮真产出的建议在极端交错下擦掉。
|
|
1185
|
+
if (list.length > 0) {
|
|
1186
|
+
yield chrome({ kind: 'prompt_suggestions', laneProof: MAIN, suggestions: list });
|
|
1187
|
+
}
|
|
719
1188
|
break;
|
|
720
|
-
|
|
721
|
-
|
|
1189
|
+
}
|
|
1190
|
+
case 'retry_status': {
|
|
1191
|
+
// #47 矩阵 #5:子代 lane 帧绝不骑主 spinner(无判别守卫的无条件 store 写=默认可疑)。
|
|
1192
|
+
if (typeof m.parentToolCallId === 'string')
|
|
1193
|
+
break;
|
|
1194
|
+
yield chrome({
|
|
1195
|
+
kind: 'retry_status',
|
|
1196
|
+
laneProof: MAIN,
|
|
1197
|
+
status: mapBrainStatusToRetry({
|
|
1198
|
+
phase: typeof m.phase === 'string' ? m.phase : '',
|
|
1199
|
+
...(typeof m.detail === 'string' ? { detail: m.detail } : {}),
|
|
1200
|
+
...(typeof m.retryInSec === 'number' ? { retryInSec: m.retryInSec } : {}),
|
|
1201
|
+
}, ctx.now()),
|
|
1202
|
+
});
|
|
722
1203
|
break;
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
laneProof: MAIN,
|
|
726
|
-
id: idOf(m),
|
|
727
|
-
attachment: { type: 'workspace_changed', cwd },
|
|
728
|
-
});
|
|
729
|
-
break;
|
|
730
|
-
}
|
|
731
|
-
case 'retry_status': {
|
|
732
|
-
// #47 矩阵 #5:子代 lane 帧绝不骑主 spinner(无判别守卫的无条件 store 写=默认可疑)。
|
|
733
|
-
if (typeof m.parentToolCallId === 'string')
|
|
1204
|
+
}
|
|
1205
|
+
default:
|
|
734
1206
|
break;
|
|
735
|
-
yield chrome({
|
|
736
|
-
kind: 'retry_status',
|
|
737
|
-
laneProof: MAIN,
|
|
738
|
-
status: mapBrainStatusToRetry({
|
|
739
|
-
phase: typeof m.phase === 'string' ? m.phase : '',
|
|
740
|
-
...(typeof m.detail === 'string' ? { detail: m.detail } : {}),
|
|
741
|
-
...(typeof m.retryInSec === 'number' ? { retryInSec: m.retryInSec } : {}),
|
|
742
|
-
}, ctx.now()),
|
|
743
|
-
});
|
|
744
|
-
break;
|
|
745
1207
|
}
|
|
746
|
-
|
|
747
|
-
|
|
1208
|
+
// COALESCER 节拍:一个间隔至多一次 flush(同步 SSE 突发先攒着,过闸再一次泄干)。
|
|
1209
|
+
if (m.type === 'stream_event') {
|
|
1210
|
+
const now = ctx.now();
|
|
1211
|
+
if ((textPending.length > 0 || thinkingPending.length > 0) && now - lastFlushAt >= flushEveryMs) {
|
|
1212
|
+
yield* drainLive();
|
|
1213
|
+
lastFlushAt = now;
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
748
1216
|
}
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
1217
|
+
}
|
|
1218
|
+
finally {
|
|
1219
|
+
// T58 — 手动 iterator 没有 for-await 的隐式 cleanup:early break(abort)或抛错都必须关上游
|
|
1220
|
+
// 生成器(SSE 拆链)。正常跑完(done)不用关,上游自己结束了。
|
|
1221
|
+
if (!iteratorDone) {
|
|
1222
|
+
try {
|
|
1223
|
+
void iterator.return?.();
|
|
1224
|
+
}
|
|
1225
|
+
catch {
|
|
1226
|
+
// best-effort close — 绝不遮蔽原本的退出路径
|
|
755
1227
|
}
|
|
756
1228
|
}
|
|
757
1229
|
}
|
|
758
|
-
//
|
|
1230
|
+
// T59 — abort 后到此为止:不发合成终答、不做兜底(这个 turn 被打断了;CC 自己的 abort 路径
|
|
1231
|
+
// 渲 InterruptedByUser chrome,那是宿主的事)。但**瞬态覆盖层必须收干净**,否则 Esc 之后
|
|
1232
|
+
// 上一轮的 retry 行/思考活动行会一直挂着。
|
|
1233
|
+
if (ctx.signal?.aborted) {
|
|
1234
|
+
yield chrome({ kind: 'retry_status', laneProof: MAIN, status: null });
|
|
1235
|
+
yield chrome({ kind: 'thinking_activity', laneProof: MAIN, active: false });
|
|
1236
|
+
// 复审修 #2a:被打断的 turn 也**必须**结掉自己的面板行,否则 Esc 之后 footer 药丸永远
|
|
1237
|
+
// 显示 "1 local agent"(sweep 点 2/3)。
|
|
1238
|
+
yield* settlePanelTasks(null, false);
|
|
1239
|
+
ctx.log?.('debug', 'turn aborted (Esc) — no terminal answer emitted');
|
|
1240
|
+
return;
|
|
1241
|
+
}
|
|
1242
|
+
// T60 —— turn 收口(cli 尾部同序,覆盖"流结束了但没有 result 臂"的路径):
|
|
1243
|
+
// 泄尾增量 → 清 liveness → 提交尾思考 → 提交尾文本段。
|
|
759
1244
|
yield* drainLive();
|
|
760
1245
|
yield chrome({ kind: 'retry_status', laneProof: MAIN, status: null });
|
|
1246
|
+
// 复审修 #2a 孪生:没有 `result` 臂的收尾路径同样 sweep(sweep 点 3/3)。
|
|
1247
|
+
yield* settlePanelTasks(null, false);
|
|
761
1248
|
yield* takeThinking();
|
|
762
1249
|
yield* takeAnswerSegment();
|
|
1250
|
+
ctx.log?.('debug', `turn done · emittedAssistantText=${emittedAssistantText} answerLen=${answer.length}`);
|
|
763
1251
|
void answer; // 全流无 text 臂时的兜底源(result 臂已消费);保留供调试/后续批
|
|
764
1252
|
}
|
|
765
1253
|
}
|