@sema-agent/client-core 0.12.2 → 0.13.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.
Files changed (127) hide show
  1. package/README.md +14 -2
  2. package/dist/abortableSleep.d.ts +29 -0
  3. package/dist/abortableSleep.js +43 -0
  4. package/dist/adapt/arms.d.ts +58 -0
  5. package/dist/adapt/arms.js +613 -0
  6. package/dist/adapt/ids.d.ts +22 -0
  7. package/dist/adapt/ids.js +34 -0
  8. package/dist/adapt/instanceLedger.d.ts +36 -0
  9. package/dist/adapt/instanceLedger.js +50 -0
  10. package/dist/adapt/panelTasks.d.ts +54 -0
  11. package/dist/adapt/panelTasks.js +193 -0
  12. package/dist/adapt/textStream.d.ts +49 -0
  13. package/dist/adapt/textStream.js +141 -0
  14. package/dist/adapt/toolCards.d.ts +65 -0
  15. package/dist/adapt/toolCards.js +100 -0
  16. package/dist/adapt/turnFlags.d.ts +33 -0
  17. package/dist/adapt/turnFlags.js +55 -0
  18. package/dist/adapt/wireShapes.d.ts +93 -0
  19. package/dist/adapt/wireShapes.js +167 -0
  20. package/dist/adapt.d.ts +32 -59
  21. package/dist/adapt.js +78 -1206
  22. package/dist/adapter/downstream/eventToSdkMessage.d.ts +55 -13
  23. package/dist/adapter/downstream/eventToSdkMessage.js +166 -106
  24. package/dist/adapter/downstream/terminalToSdkResult.js +149 -160
  25. package/dist/adapter/downstream/turnUsageToModelUsage.d.ts +36 -0
  26. package/dist/adapter/downstream/turnUsageToModelUsage.js +34 -6
  27. package/dist/adapter/runStream.d.ts +29 -4
  28. package/dist/adapter/runStream.js +129 -13
  29. package/dist/adapter/types.d.ts +2 -2
  30. package/dist/agentSession/backgroundView.js +4 -15
  31. package/dist/agentsWireCaps.d.ts +10 -6
  32. package/dist/agentsWireCaps.js +21 -7
  33. package/dist/argvFlagValue.d.ts +41 -0
  34. package/dist/argvFlagValue.js +69 -0
  35. package/dist/attachmentsWireCaps.d.ts +3 -2
  36. package/dist/attachmentsWireCaps.js +5 -3
  37. package/dist/classifierVerdictWire.d.ts +8 -0
  38. package/dist/classifierVerdictWire.js +8 -0
  39. package/dist/cloudConfigWireCaps.d.ts +28 -1
  40. package/dist/cloudConfigWireCaps.js +51 -11
  41. package/dist/controlRouter.d.ts +14 -8
  42. package/dist/controlRouter.js +15 -21
  43. package/dist/detachWire.d.ts +15 -5
  44. package/dist/detachWire.js +17 -7
  45. package/dist/effortWire.d.ts +0 -21
  46. package/dist/effortWire.js +6 -20
  47. package/dist/engineInlineTaskStats.d.ts +12 -6
  48. package/dist/engineWireSdk.d.ts +12 -0
  49. package/dist/env/localeGeo.js +2 -1
  50. package/dist/envFlag.d.ts +39 -0
  51. package/dist/envFlag.js +51 -0
  52. package/dist/finalVerifyWire.d.ts +7 -5
  53. package/dist/finalVerifyWire.js +6 -5
  54. package/dist/fleet/fleetLedger.d.ts +32 -9
  55. package/dist/fleet/fleetLedger.js +119 -34
  56. package/dist/fleet/fleetProjection.d.ts +44 -6
  57. package/dist/fleet/fleetProjection.js +52 -10
  58. package/dist/fleetAgentPanelProjection.d.ts +18 -1
  59. package/dist/fleetAgentPanelProjection.js +61 -14
  60. package/dist/forkWireCaps.d.ts +2 -1
  61. package/dist/forkWireCaps.js +5 -11
  62. package/dist/goalStopHook.d.ts +142 -0
  63. package/dist/goalStopHook.js +258 -0
  64. package/dist/headlessPermissionModeWire.d.ts +10 -0
  65. package/dist/headlessPermissionModeWire.js +24 -21
  66. package/dist/headlessReconnectWire.d.ts +7 -1
  67. package/dist/headlessReconnectWire.js +20 -2
  68. package/dist/hitl/approvalsFeed.js +31 -8
  69. package/dist/hitl/askGateWire.d.ts +27 -96
  70. package/dist/hitl/askGateWire.js +69 -618
  71. package/dist/hitl/frameRouter.d.ts +86 -0
  72. package/dist/hitl/frameRouter.js +342 -0
  73. package/dist/hitl/gateLedger.d.ts +107 -0
  74. package/dist/hitl/gateLedger.js +113 -0
  75. package/dist/hitl/hitlBridge.d.ts +75 -15
  76. package/dist/hitl/hitlBridge.js +94 -22
  77. package/dist/hitl/hitlHostSurface.d.ts +49 -0
  78. package/dist/hitl/hitlHostSurface.js +155 -0
  79. package/dist/hitl/parkResolver.d.ts +74 -0
  80. package/dist/hitl/parkResolver.js +241 -0
  81. package/dist/hitl/planReviewWire.d.ts +60 -2
  82. package/dist/hitl/planReviewWire.js +152 -74
  83. package/dist/hitl/toolApprovalWire.d.ts +67 -4
  84. package/dist/hitl/toolApprovalWire.js +119 -31
  85. package/dist/hooksWireCaps.d.ts +1 -82
  86. package/dist/hooksWireCaps.js +34 -235
  87. package/dist/host.d.ts +16 -5
  88. package/dist/index.d.ts +2 -0
  89. package/dist/index.js +15 -1
  90. package/dist/interactiveToolsWire.d.ts +15 -4
  91. package/dist/interactiveToolsWire.js +24 -26
  92. package/dist/limitsWire.js +7 -40
  93. package/dist/liveInitToolFace.d.ts +51 -6
  94. package/dist/liveQuestionStore.d.ts +5 -6
  95. package/dist/model/providerPresets.js +11 -1
  96. package/dist/notifications.d.ts +48 -2
  97. package/dist/notifications.js +223 -46
  98. package/dist/retainBackgroundWireCaps.d.ts +3 -2
  99. package/dist/retainBackgroundWireCaps.js +5 -9
  100. package/dist/sandboxWire.d.ts +9 -31
  101. package/dist/sandboxWire.js +51 -50
  102. package/dist/scenarioWire.d.ts +1 -1
  103. package/dist/scenarioWire.js +25 -36
  104. package/dist/seam.d.ts +23 -6
  105. package/dist/seam.js +40 -30
  106. package/dist/seatContract.d.ts +369 -83
  107. package/dist/seatContract.js +585 -198
  108. package/dist/selfOrchestrationWireCaps.d.ts +6 -5
  109. package/dist/selfOrchestrationWireCaps.js +8 -12
  110. package/dist/sessionSlot.d.ts +8 -0
  111. package/dist/sessionSlot.js +1 -0
  112. package/dist/steering.js +2 -2
  113. package/dist/subagent/engineTaskHandleWire.d.ts +3 -0
  114. package/dist/subagent/engineTaskHandleWire.js +17 -2
  115. package/dist/subagentContentStore.d.ts +5 -5
  116. package/dist/toolResult.d.ts +89 -8
  117. package/dist/toolResult.js +99 -30
  118. package/dist/typePins.d.ts +17 -0
  119. package/dist/typePins.js +1 -0
  120. package/dist/ultracodeWireCaps.js +5 -6
  121. package/dist/unrefTimer.d.ts +19 -0
  122. package/dist/unrefTimer.js +5 -0
  123. package/dist/workflow.d.ts +3 -2
  124. package/dist/workflow.js +3 -2
  125. package/dist/workflowClient.d.ts +7 -0
  126. package/dist/workflowClient.js +47 -12
  127. package/package.json +3 -3
@@ -0,0 +1,613 @@
1
+ import { dropQueuedNotificationsForRun, isEngineWorkflowNotified, isWorkflowCompletionCardEnqueued, markEngineWorkflowNotified, normalizeTaskNotification, noteWorkflowCompletionCardEnqueued, renderTaskNotificationXml, taskNotificationDedupKey } from '../notifications.js';
2
+ import { mapBrainStatusToRetry } from '../retryStatus.js';
3
+ import { steeringInjectedToAttachments } from '../steering.js';
4
+ import { projectDiagnosticsFrame } from '../diagnostics.js';
5
+ import { recordEngineToolLabel } from '../engineToolLabelStore.js';
6
+ import { registerSubagentAlias } from '../subagentContentStore.js';
7
+ import { isWorkflowAgentTaskId, recordWorkflowAgentTaskId } from '../workflow.js';
8
+ import { chrome, MAIN, transcript } from './ids.js';
9
+ import { CANCEL_MESSAGE, decisionOf, flattenWireOutput, REJECT_MESSAGE, sanitizeToolUseBlock, shortTaskLabel, TASK_TOOL_NAMES, WORKFLOW_TOOL_NAMES, } from './wireShapes.js';
10
+ // ══ M2/M1 —— assistant(tool_use 半场 + durable 文本/思考半场)══════════════════════════════════
11
+ const assistantArm = function* (m, { ctx, idOf, text, cards, inst }) {
12
+ const message = m.message;
13
+ const blocks = Array.isArray(message?.content)
14
+ ? message.content
15
+ : [];
16
+ const toolUseIdx = blocks.findIndex(b => b?.type === 'tool_use');
17
+ // 拆分同批消掉一处 `!` 非空断言(规范禁用):`findIndex >= 0` 与「取到的不是 undefined」
18
+ // 本来就是同一件事(判别式 `b?.type === 'tool_use'` 对 undefined 恒假),写成显式取值即可。
19
+ // 收货裁定(codex 复审 #4):旧版此处直接取值使用,畸形帧(accessor/稀疏形,非 JSON 数据帧)
20
+ // 会抛 TypeError;guard 保留=有意的健壮性变化 —— 正常 wire 帧(JSON.parse 产物)下
21
+ // `findIndex >= 0 ⇒ 恒非 undefined`,零行为差。
22
+ const original = toolUseIdx >= 0 ? blocks[toolUseIdx] : undefined;
23
+ if (original !== undefined) {
24
+ const decision = decisionOf(original);
25
+ const san = sanitizeToolUseBlock(original);
26
+ const renderBlocks = blocks.map((b, i) => (i === toolUseIdx ? san.block : b));
27
+ const toolUseId = String(san.block.id);
28
+ const toolName = typeof san.block.name === 'string' ? san.block.name : 'unknown';
29
+ // P0-2 侧信道:core 展示名进本包台账(渲染块上已被 sanitize 摘掉)。cli 同位、同顺序
30
+ // (在任何 yield 之前),对 reject/cancel/error 三条早退路径同样生效。
31
+ // 🔴 时序留钉(矩阵 §2.1①):这必须是臂体的**第一条**副作用语句 —— 挪到下面任何 yield 之后,
32
+ // reject/cancel/error 三条早退路径就永远登记不上 label,unknown-tool 卡按 id 查不到展示名。
33
+ recordEngineToolLabel(toolUseId, san.label);
34
+ // 引擎模式下壳工具 call() 从不执行 —— ctrl+t 面板台账只能靠这条侧信道(宿主义务)。
35
+ if (TASK_TOOL_NAMES.has(toolName)) {
36
+ yield chrome({
37
+ kind: 'task_ledger_sync',
38
+ laneProof: MAIN,
39
+ source: 'tool_use',
40
+ toolName,
41
+ toolUseId,
42
+ input: san.block.input,
43
+ });
44
+ }
45
+ // #27 段序:先把这张卡之前的散文提交,再提交它之前的思考(思考恒在卡上方)。
46
+ yield* text.takeAnswerSegment();
47
+ yield* text.takeThinking();
48
+ yield transcript({
49
+ type: 'assistant',
50
+ // S1:per-RESPONSE 铸 id —— CC 的 grouped-agent 渲染按 `${message.id}:${toolName}`
51
+ // 分组;id-less 帧会把整个 session 的 Agent 卡塌成一张。tool_use 帧才盖章。
52
+ message: { id: cards.mintResponseId(m), role: 'assistant', content: renderBlocks },
53
+ uuid: idOf(m),
54
+ session_id: ctx.sessionId,
55
+ parent_tool_use_id: null,
56
+ });
57
+ if (decision === 'reject' || decision === 'cancel') {
58
+ // S1:一个结果关闭该 model response。**yield 之前**复位 —— 矩阵 §2.2 定性为
59
+ // 「形不一致的技术债、不是缺陷」(L1 turn 级、下一句就 return、早退时无读者),
60
+ // 拆分同批前移成与 tool_end_result / sweep 一致的形,零行为差。
61
+ cards.resetResponseId();
62
+ yield transcript({
63
+ type: 'user',
64
+ message: {
65
+ role: 'user',
66
+ content: [
67
+ {
68
+ type: 'tool_result',
69
+ tool_use_id: san.block.id,
70
+ content: decision === 'reject' ? REJECT_MESSAGE : CANCEL_MESSAGE,
71
+ is_error: false,
72
+ },
73
+ ],
74
+ },
75
+ uuid: idOf(m, decision),
76
+ session_id: ctx.sessionId,
77
+ });
78
+ return;
79
+ }
80
+ if (decision === 'error') {
81
+ cards.resetResponseId(); // S1,同上:yield 之前复位
82
+ yield transcript({
83
+ type: 'user',
84
+ message: {
85
+ role: 'user',
86
+ content: [
87
+ {
88
+ type: 'tool_result',
89
+ tool_use_id: san.block.id,
90
+ content: 'The tool failed to execute.',
91
+ is_error: true,
92
+ },
93
+ ],
94
+ },
95
+ uuid: idOf(m, 'error'),
96
+ session_id: ctx.sessionId,
97
+ });
98
+ return;
99
+ }
100
+ // ⟨帧序耦合 1/7 + 3/7⟩ 开卡:后到的 task_progress 才猜得到父、后到的 tool_end 才关得掉卡。
101
+ cards.open({
102
+ id: toolUseId,
103
+ name: toolName,
104
+ rawInput: san.rawInput,
105
+ ...(san.eventId !== undefined ? { eventId: san.eventId } : {}),
106
+ ...(san.parentToolCallId !== undefined ? { parentToolCallId: san.parentToolCallId } : {}),
107
+ });
108
+ // ⟨帧序耦合 2/7⟩ W1 判据③ 的素材:只有先见过这张 Workflow 卡,后到的 tick 才判得出 workflow lane。
109
+ if (WORKFLOW_TOOL_NAMES.has(toolName))
110
+ inst.markWorkflowCard(toolUseId);
111
+ return;
112
+ }
113
+ const textBlock = blocks.find(b => b?.type === 'text');
114
+ if (textBlock && typeof textBlock.text === 'string' && textBlock.text.length > 0) {
115
+ text.appendAnswer(textBlock.text);
116
+ yield* text.takeThinking();
117
+ yield transcript({
118
+ type: 'assistant',
119
+ message: { role: 'assistant', content: [{ type: 'text', text: textBlock.text }] },
120
+ uuid: idOf(m),
121
+ session_id: ctx.sessionId,
122
+ parent_tool_use_id: null,
123
+ });
124
+ text.markEmittedText();
125
+ }
126
+ const thinkingBlock = blocks.find(b => b?.type === 'thinking');
127
+ if (thinkingBlock && typeof thinkingBlock.thinking === 'string') {
128
+ yield transcript({
129
+ type: 'assistant',
130
+ message: { role: 'assistant', content: [thinkingBlock] },
131
+ uuid: idOf(m, 'durable-thinking'),
132
+ session_id: ctx.sessionId,
133
+ parent_tool_use_id: null,
134
+ });
135
+ }
136
+ };
137
+ // ══ M5 纯投影臂(零适配器状态,类型即证明)══════════════════════════════════════════════════════
138
+ /** DIVERGENCE-1 —— 现役 wire 不产 user 帧;透传是给 desktop 重放存量转录留的门。 */
139
+ const userArm = function* (m, { ctx, idOf }) {
140
+ if (m.message === undefined)
141
+ return;
142
+ yield transcript({ ...m, uuid: idOf(m), session_id: ctx.sessionId });
143
+ };
144
+ const systemArm = function* (m, { idOf }) {
145
+ yield transcript(m);
146
+ // core 1.219 attachWorkingFiles:compact_boundary 可携 attachedFiles → CC 的
147
+ // "Referenced file {path}" 行(post-compact attachment,零新 UI)。
148
+ const attachedFiles = m.attachedFiles;
149
+ if (Array.isArray(attachedFiles)) {
150
+ let i = 0;
151
+ for (const f of attachedFiles) {
152
+ if (typeof f?.path !== 'string' || f.path.length === 0)
153
+ continue;
154
+ yield chrome({
155
+ kind: 'attachment',
156
+ laneProof: MAIN,
157
+ id: idOf(m, `attached-file-${i++}`),
158
+ attachment: {
159
+ type: 'compact_file_reference',
160
+ filename: f.path,
161
+ displayPath: f.path,
162
+ },
163
+ });
164
+ }
165
+ }
166
+ };
167
+ const diagnosticsArm = function* (m, { idOf }) {
168
+ if (typeof m.parentToolCallId === 'string')
169
+ return; // 子流帧不上 leader 面
170
+ const att = projectDiagnosticsFrame(m);
171
+ if (att === null)
172
+ return;
173
+ yield chrome({ kind: 'attachment', laneProof: MAIN, id: idOf(m), attachment: att });
174
+ };
175
+ const steeringInjectedArm = function* (m, { idOf }) {
176
+ if (typeof m.parentToolCallId === 'string')
177
+ return;
178
+ const source = typeof m.source === 'string' ? m.source : '';
179
+ if (source.length === 0)
180
+ return;
181
+ const preview = typeof m.preview === 'string' ? m.preview : '';
182
+ let i = 0;
183
+ for (const att of steeringInjectedToAttachments(source, preview)) {
184
+ yield chrome({
185
+ kind: 'attachment',
186
+ laneProof: MAIN,
187
+ id: idOf(m, `steering-${i++}`),
188
+ attachment: att,
189
+ });
190
+ }
191
+ };
192
+ const workspaceChangedArm = function* (m, { idOf }) {
193
+ if (typeof m.parentToolCallId === 'string')
194
+ return;
195
+ const cwd = typeof m.cwd === 'string' ? m.cwd : '';
196
+ if (cwd.length === 0)
197
+ return;
198
+ yield chrome({
199
+ kind: 'attachment',
200
+ laneProof: MAIN,
201
+ id: idOf(m),
202
+ attachment: { type: 'workspace_changed', cwd },
203
+ });
204
+ };
205
+ const promptSuggestionsArm = function* (m) {
206
+ // T57 第五处断闸(#47 矩阵 #5 同族):子流(parentToolCallId 标)的建议绝不骑主 composer。
207
+ if (typeof m.parentToolCallId === 'string')
208
+ return;
209
+ const list = Array.isArray(m.suggestions)
210
+ ? m.suggestions.filter((s) => typeof s === 'string')
211
+ : [];
212
+ // 空批不发(cli 同门:`if (list.length > 0)`)—— 作废由 turn 开场那一发负责,
213
+ // 这里再发一次 null 会把本轮真产出的建议在极端交错下擦掉。
214
+ if (list.length > 0) {
215
+ yield chrome({ kind: 'prompt_suggestions', laneProof: MAIN, suggestions: list });
216
+ }
217
+ };
218
+ // ══ M2 —— tool_end_result(关卡 + B/D/E 层卡本体 + 两个侧产物)══════════════════════════════════
219
+ const toolEndResultArm = function* (m, { ctx, idOf, cards, panel }) {
220
+ // B4 起本臂做三件:① P0-2 label 补位(只有 tool_end 带 label 的场景;台账自身幂等不覆盖)
221
+ // ② MF-10 **关卡 settle**(子代从不发终态 tick,父卡的 tool_end 就是完成信号;卡的 output
222
+ // 文本 = 子代最终报告,串到行 end 事件上)③ 卡台账出栈 + response-id 复位(S1)。
223
+ // 🆕 B5:④ **B/D/E 层** —— 铸 `tool_result` 卡本体 + 三个侧产物
224
+ // (Remember→memory_saved / task 族 structured 台账 / #117a bg Bash 桥)。
225
+ const callId = typeof m.toolCallId === 'string' ? m.toolCallId : undefined;
226
+ // 🔴 时序留钉(矩阵 §2.1①):同 assistant 臂 —— label 登记恒在任何 yield 之前。
227
+ recordEngineToolLabel(callId, m.label);
228
+ if (callId !== undefined) {
229
+ // ⟨帧序耦合 4/7⟩ 关卡 settle(sweep 点 1/4)只结**已发布**的面板行:tick 没到过就没有行可结。
230
+ yield* panel.settle(callId, m.isError === true, m.output !== undefined ? flattenWireOutput(m.output) : undefined);
231
+ }
232
+ const p = callId !== undefined ? cards.peek(callId) : undefined;
233
+ // ⟨帧序耦合 3/7⟩ 无对应开卡的 tool_end(乱序 / 重复)直接丢(cli 同)。
234
+ if (p === undefined || callId === undefined)
235
+ return;
236
+ const structured = m.structured;
237
+ // 出栈 + S1 复位(**yield 之前**)已收进 close() —— 见 toolCards.ts 的拆分硬约束。
238
+ yield* cards.close(p, {
239
+ output: m.output,
240
+ structured,
241
+ truncated: m.truncated === true,
242
+ isError: m.isError === true,
243
+ eventId: typeof m.eventId === 'string' ? m.eventId : undefined,
244
+ parentToolCallId: typeof m.parentToolCallId === 'string' ? m.parentToolCallId : undefined,
245
+ });
246
+ const s = structured !== undefined && structured !== null && typeof structured === 'object'
247
+ ? structured
248
+ : undefined;
249
+ // ── E 层 ①:CC198 gap #2 —— 引擎 Remember 工具 → CC198 automemory「Saved N memories」卡
250
+ // (system{subtype:'memory_saved'})。引擎不发写入路径,note 本身就是那一条 writtenPaths。
251
+ // 上面的 tool_result 照旧走泛化兜底卡(双覆盖);structuredToToolUseResult 的白名单不动
252
+ // (Remember 从不进那个 switch)。core 1.217 形 {type:'memory-saved', ok, note, …}。
253
+ if (p.name === 'Remember' && m.isError !== true) {
254
+ if (s && s.ok === true && typeof s.note === 'string' && s.note.length > 0) {
255
+ yield transcript({
256
+ type: 'system',
257
+ subtype: 'memory_saved',
258
+ writtenPaths: [s.note],
259
+ // cli 用 `new Date().toISOString()` + randomUUID();本包一律走 ctx(可重放)。
260
+ // 见 DIVERGENCE-7。
261
+ timestamp: new Date(ctx.now()).toISOString(),
262
+ uuid: idOf(m, 'memory-saved'),
263
+ isMeta: false,
264
+ });
265
+ }
266
+ }
267
+ // ── E 层 ②:core 1.217 task 族 structured —— 权威全量态精确同步 ctrl+t 面板(引擎真实 id)。
268
+ // 落盘是宿主职责(T44 拆缝:判定+归一进库、落盘留宿主)。
269
+ if (s && (s.type === 'task' || s.type === 'task-list')) {
270
+ yield chrome({
271
+ kind: 'task_ledger_sync',
272
+ laneProof: MAIN,
273
+ source: 'structured',
274
+ structuredType: s.type,
275
+ structured: s,
276
+ });
277
+ }
278
+ };
279
+ // ══ M3 —— task_progress(引擎侧子代的累计 usage tick)══════════════════════════════════════════
280
+ const taskProgressArm = function* (m, { ctx, cards, panel, inst }) {
281
+ // ── B4 A 层最后一臂(MF-10)—— 引擎侧子代的累计 usage tick。
282
+ // 只带 rollup 数字 = CC `AgentProgress` 同形 ⇒ 忠实的 CC-187 面是 footer 面板行
283
+ // (CC 自己对进程外 agent 的先例:面板行 + task-notification,从不合成 inline 消息轨)。
284
+ // **不产任何 transcript 消息**:全是 chrome。
285
+ const taskId = typeof m.taskId === 'string' ? m.taskId : undefined;
286
+ if (taskId === undefined)
287
+ return;
288
+ const explicitParent = typeof m.parentToolCallId === 'string' ? m.parentToolCallId : undefined;
289
+ // ── W1 段2([1617]②,frame-lane-matrix #2/#8)workflow lane 门:workflow 子代的 tick 属
290
+ // fleet lane 自描述行(footer 只渲 workflow 聚合行),**绝不**进前台 subagent lane
291
+ // (不登 alias、不 bind、不 livePanelTasks、不发 panel tick —— 修前每个 workflow agent
292
+ // 都在 footer 铸一条幽灵 local_agent 行,还能骑错 run_workflow 卡的 inline stats)。
293
+ // 判据三级:① 帧带 workflowRunId(一手证据)② wa+16hex 形 id(core BCE 合成 id)
294
+ // ③ 显式父指向本会话见过的 Workflow 工具卡(实例台账,覆盖 uuid 形子代 id + 跨 turn 迟到 tick)。
295
+ // 🔴 codex 独立发现的拆分风险:**本门必须先于任何 panel 写**,否则重造 workflow 幽灵前台行 ——
296
+ // 这也是它在本臂第一段、`bind()` 之前的原因,别按「先绑定再分类」重排。
297
+ const workflowRunId = typeof m.workflowRunId === 'string' ? m.workflowRunId : undefined;
298
+ if (workflowRunId !== undefined ||
299
+ isWorkflowAgentTaskId(taskId) ||
300
+ // ⟨帧序耦合 2/7⟩ 依赖 assistant 臂先登记过那张 Workflow 卡。
301
+ (explicitParent !== undefined && inst.isWorkflowCard(explicitParent))) {
302
+ // 判据④ 回植([1641] workflow 启动闪 subagent 行案):本门是「这条 taskId 属 workflow」的
303
+ // **权威判定**。同一 taskId 在 fleet 面还有一条 run 发布器双生行(`${runId} ${taskId}`)
304
+ // 且那行不带 workflowRunId —— 登进 belt 台账,fleet 行按尾段查它,双生行就再进不了
305
+ // footer task 树。🔴 写者在本包、读者(isKnownWorkflowAgentTaskId)也在本包 = R4 纪律,
306
+ // 做成 chrome 臂反而会让「宿主没接线 ⇒ Set 恒空 ⇒ 判据④ 静默失效」复活。
307
+ recordWorkflowAgentTaskId(taskId);
308
+ ctx.probe?.('tick', JSON.stringify({
309
+ t: ctx.now(),
310
+ taskId,
311
+ lane: 'workflow',
312
+ via: workflowRunId !== undefined
313
+ ? 'workflowRunId'
314
+ : isWorkflowAgentTaskId(taskId)
315
+ ? 'wa-id'
316
+ : 'workflow-card-parent',
317
+ }));
318
+ return;
319
+ }
320
+ // C1 alias:content 帧只带 parentToolCallId(EventIdentity 上没有 taskId),这条 tick
321
+ // **两者都带** —— 是 store 学到「停放的内容属哪条引擎行」的唯一位置(查看态按引擎 task id 查)。
322
+ if (explicitParent !== undefined)
323
+ registerSubagentAlias(explicitParent, taskId);
324
+ // 复审修 #1:含糊回落(无显式父 + >1 张未绑候选)⇒ 保持 unbound。
325
+ // ⟨帧序耦合 1/7⟩ FIFO 猜父只看**此刻还开着**的卡,所以 assistant 开卡帧必须先到。
326
+ const cardId = explicitParent === undefined && !panel.isBound(taskId) && panel.unboundCandidates() > 1
327
+ ? undefined
328
+ : panel.bind(taskId, explicitParent);
329
+ const usage = (m.usage ?? {});
330
+ ctx.probe?.('tick', JSON.stringify({
331
+ t: ctx.now(),
332
+ taskId,
333
+ lane: 'foreground',
334
+ parent: explicitParent ?? null,
335
+ cardId: cardId ?? null,
336
+ currentAction: m.currentAction ?? null,
337
+ toolUses: usage.toolUses ?? null,
338
+ }));
339
+ // core 1.262.0 [558]B / service 1.154.0 [579] —— 子代的活体"我在干什么"一行
340
+ // ("Bash npm test"),service 已 redact。PROGRESS 语义(喂状态/摘要显示槽),不是诊断。
341
+ // 老引擎缺席 ⇒ 两个消费面维持今天的显示。
342
+ const currentAction = typeof m.currentAction === 'string' && m.currentAction.length > 0 ? m.currentAction : undefined;
343
+ // inline 群组行孪生:**绑定了**的 tick 同时喂转录里那条行的统计(按委派卡 id 键)。
344
+ if (cardId !== undefined) {
345
+ yield chrome({
346
+ kind: 'inline_task_stats',
347
+ laneProof: { lane: 'subagent', parentToolCallId: cardId },
348
+ op: 'tick',
349
+ cardId,
350
+ stats: {
351
+ toolUses: typeof usage.toolUses === 'number' ? usage.toolUses : 0,
352
+ totalTokens: typeof usage.totalTokens === 'number' ? usage.totalTokens : 0,
353
+ durationMs: typeof usage.durationMs === 'number' ? usage.durationMs : 0,
354
+ ...(currentAction !== undefined ? { currentAction } : {}),
355
+ },
356
+ });
357
+ }
358
+ const card = cardId !== undefined ? cards.peek(cardId) : undefined;
359
+ // T50 双形读:`rawInput` 是**改名前**捕获的,真 wire 上引擎子代的 input 是
360
+ // {agent, task, taskName} —— 两形都读(CC 名优先)。
361
+ const input = (card?.rawInput ?? {});
362
+ const description = typeof input.description === 'string'
363
+ ? input.description
364
+ : typeof input.taskName === 'string' && input.taskName.trim().length > 0
365
+ ? input.taskName
366
+ : typeof input.task === 'string'
367
+ ? shortTaskLabel(input.task)
368
+ : undefined;
369
+ const prompt = typeof input.prompt === 'string'
370
+ ? input.prompt
371
+ : typeof input.task === 'string'
372
+ ? input.task
373
+ : undefined;
374
+ // ⟨帧序耦合 5/7⟩ 首 tick 才 fire Start,而通知臂的 if-started 门读的就是它。
375
+ if (!panel.isLive(taskId)) {
376
+ // 这条引擎子代在壳侧**可观测的起点** = 本 turn 的首 tick。
377
+ const agentType = typeof m.name === 'string' && m.name.length > 0 ? m.name : 'subagent';
378
+ yield* panel.start(taskId, agentType);
379
+ }
380
+ // #6 常驻台账的两个写口整臂让位给 M3(module store 不许两个模块各写各的)。
381
+ panel.noteResidency(taskId);
382
+ panel.markLive(taskId);
383
+ yield chrome({
384
+ kind: 'panel_task',
385
+ laneProof: panel.laneOf(taskId),
386
+ event: {
387
+ kind: 'tick',
388
+ taskId,
389
+ ...(typeof m.name === 'string' && m.name.length > 0 ? { name: m.name } : {}),
390
+ ...(description !== undefined ? { description } : {}),
391
+ ...(prompt !== undefined ? { prompt } : {}),
392
+ ...(currentAction !== undefined ? { currentAction } : {}),
393
+ // 🔴 这里的 `: 0` 与 `fleetAgentPanelProjection` 那句「绝不在本层 `?? 0`」**不矛盾**,
394
+ // 因为不是同一条 lane 的同一种供给形(两层的分层实情写在 `engineAgentPanelStore.ts`
395
+ // 的 `PANEL_TOOLUSES_LANE_POLICY` 上,两条 lane 的可选性由编译钉锁住):
396
+ // · **tick lane(本处)**:`usage` 是 core `task_progress` 臂的**必填**字段
397
+ // (core 2.12.0 `dist/core/types.d.ts:606-610` 无 `?`),两处发帧点
398
+ // (`runner/runtask.js:1080` 每轮边界 / `:2925` 终态 settle 拍)**无条件**构造
399
+ // `{ totalTokens, toolUses: stats.toolCalls, durationMs }`,server 白名单整键透传
400
+ // (`trace/project.js:118`)⇒ 缺席在这条 lane 上不是「不知道」,是**没有这种帧**。
401
+ // 值本身是**累计**计数(`stats.toolCalls`,从 0 起算),所以真值域含真 0。
402
+ // · **fleet-row lane(那处)**:同名键是 server 1.278.0 才**新加**的可选位,老引擎
403
+ // 真的不发 ⇒ 缺席就是「不知道」,兜 0 会把它冒充成「跑了 0 个工具」,而且会把
404
+ // 台账里已知的累计值**擦回 0**。
405
+ // ⚠️ 别把两层「统一」:把本处改成传播缺席要先把事件形改成可选,而那正是本 lane
406
+ // 刻意不做的事(`engineAgentPanelStore.ts:54-57` 记的就是这条 —— 复用同一个可选形
407
+ // 会让 fleet-row lane 每秒把前台子代由真 tick 写进去的进度数字清一次)。
408
+ toolUses: typeof usage.toolUses === 'number' ? usage.toolUses : 0,
409
+ totalTokens: typeof usage.totalTokens === 'number' ? usage.totalTokens : 0,
410
+ },
411
+ });
412
+ };
413
+ // ══ M1 / M6a —— stream_event / turn_usage ══════════════════════════════════════════════════════
414
+ const streamEventArm = function* (m, { text }) {
415
+ const ev = m.event;
416
+ if (ev && ev.type === 'text_delta' && typeof ev.delta === 'string') {
417
+ yield* text.feedText(ev.delta, m);
418
+ }
419
+ else if (ev && ev.type === 'thinking_delta' && typeof ev.delta === 'string') {
420
+ // 子流(parentToolCallId)的推理绝不喂 leader 的计数与预览(#9 的壳侧半场)。
421
+ if (ev.parentToolCallId !== undefined)
422
+ return;
423
+ yield* text.feedThinking(ev.delta, m);
424
+ }
425
+ };
426
+ const turnUsageArm = function* (m, { text, flags }) {
427
+ const ot = typeof m.outputTokens === 'number' ? m.outputTokens : undefined;
428
+ if (ot !== undefined) {
429
+ yield* text.drainLive();
430
+ // ⟨帧序耦合 7/7⟩ 这一拍置 endEmitted,后到的 result 臂据它决定要不要补发 end 度量。
431
+ yield* flags.emitTurnUsageEnd(ot);
432
+ }
433
+ };
434
+ // ══ M0 编排半场 —— result(收口三形之一;与 D7/D8 的次序差异见矩阵 §2.4,不可归一)═══════════════
435
+ const resultArm = function* (m, { ctx, idOf, text, cards, panel, flags }) {
436
+ yield chrome({ kind: 'retry_status', laneProof: MAIN, status: null });
437
+ // MF-10 防御 sweep(sweep 点 2/4):结掉那些卡从没关过的行(流被截断)。
438
+ // cli 同位:setRetryStatus(null) 之后、开卡兜底关闭之前。
439
+ yield* panel.settle(null, false);
440
+ // B5:开卡兜底关闭(cli 同位 —— 在终答文本**之前**,卡才不会挂着空白)。
441
+ yield* cards.sweep();
442
+ // 🔴 矩阵 §2.4(b):本臂 takeThinking 在 metrics **之前**、takeAnswerSegment 在 metrics **之后**,
443
+ // 与 T60 尾的连续两拍**不同序**。看着像可以归一,不能。
444
+ yield* text.takeThinking();
445
+ yield* flags.emitResultEndIfNeeded(() => {
446
+ const u = m.usage;
447
+ return typeof u?.outputTokens === 'number' ? u.outputTokens : undefined;
448
+ });
449
+ yield* text.takeAnswerSegment();
450
+ if (!text.emittedAssistantText) {
451
+ // 具名成 terminalText:M1 收编后 `text` 是流合并器的名字,原来的同名局部会遮蔽它。
452
+ const terminalText = typeof m.result === 'string' ? m.result : '';
453
+ if (terminalText.length > 0) {
454
+ yield transcript({
455
+ type: 'assistant',
456
+ message: { role: 'assistant', content: [{ type: 'text', text: terminalText }] },
457
+ uuid: idOf(m, 'terminal-text'),
458
+ session_id: ctx.sessionId,
459
+ parent_tool_use_id: null,
460
+ });
461
+ text.markEmittedText();
462
+ }
463
+ }
464
+ };
465
+ // ══ M4 —— 通知族 ═══════════════════════════════════════════════════════════════════════════════
466
+ const taskNotificationArm = function* (m, { ctx, idOf, panel, inst }) {
467
+ const n = (m.notification ?? {});
468
+ const fields = normalizeTaskNotification(n);
469
+ if (fields === null)
470
+ return; // 空/缺 task_id:RB-75 同族,丢弃
471
+ const taskId = fields.taskId;
472
+ const rawStatus = fields.status;
473
+ const terminal = rawStatus === 'completed' || rawStatus === 'failed' || rawStatus === 'killed';
474
+ // #117a settle 半场 —— **故意在渲染去重之前**:settle 幂等,重放帧仍须 settle 一条
475
+ // 首投之后才注册的行。#47 矩阵 #7:只有 bash 行(或老引擎无 task_type)能被 settle。
476
+ const settleAllowed = n.task_type === undefined || n.task_type === 'background_bash';
477
+ if (settleAllowed && terminal) {
478
+ yield chrome({
479
+ kind: 'bgshell_settle',
480
+ laneProof: MAIN,
481
+ taskId,
482
+ status: rawStatus,
483
+ ...(typeof n.output_file === 'string' && n.output_file.length > 0
484
+ ? { outputPath: n.output_file }
485
+ : {}),
486
+ });
487
+ }
488
+ const dedupKey = taskNotificationDedupKey({
489
+ taskId,
490
+ status: rawStatus,
491
+ ...(typeof n.seq === 'number' || typeof n.seq === 'string' ? { seq: n.seq } : {}),
492
+ ...(typeof n.task_type === 'string' ? { taskType: n.task_type } : {}),
493
+ });
494
+ // 🔴 时序留钉(矩阵 §2.1④ 的 1377 行判定:**有意序**,别挪):记号在 yield 之前落 ——
495
+ // 代价是早退窗内该通知既不渲也不再重放(丢一条),而反向(add-after-yield)是重放双渲,更坏。
496
+ if (inst.hasRenderedNotification(dedupKey))
497
+ return;
498
+ inst.addRenderedNotification(dedupKey);
499
+ // B1 + #6 —— 必须在 cardEnqueued 早退**之前**:probe 合成先入队的场景恰是双投主形。
500
+ if (n.task_type !== 'external' && terminal) {
501
+ yield chrome({
502
+ kind: 'notification_terminal',
503
+ laneProof: MAIN,
504
+ taskId,
505
+ status: rawStatus,
506
+ ...(typeof n.task_type === 'string' ? { taskType: n.task_type } : {}),
507
+ });
508
+ // B1 —— 这帧 = 引擎 server-side steer-inject 的回声,模型已在引擎侧收到一遍;把还躺在
509
+ // 队列里的同 run Path B 条目(workflow_complete/probe/bg feeder)在 turn 收口 drain 前
510
+ // 丢弃,免得 idle auto-submit 再喂模型一遍(跨进程双投的客户端半场)。
511
+ // 🔴 B4 起由**适配器自持**(B2 已把队列端口搬进本包):此前挂在 `notification_terminal`
512
+ // 的宿主义务①,是「说得通但没人做」的典型 —— 宿主没接线就静默双投。
513
+ dropQueuedNotificationsForRun(taskId);
514
+ if (n.task_type !== 'background_bash') {
515
+ // ⟨帧序耦合 5/7⟩ #6 通知-settle 边(推送半场)整段让位给 M3 —— 拆分前这里直写
516
+ // endedPanelTasks 又直读实例台账的 firedSubagentStartHookTaskIds,是矩阵点名的两条跨模块面。
517
+ yield* panel.settleFromNotification(taskId, rawStatus !== 'completed');
518
+ }
519
+ }
520
+ // 双通道反向去重:probe 合成的完成卡已入队 ⇒ 后到的推送帧只做上面的记账,不渲第二条。
521
+ // 🔴 B5 ①:读的是 **module 台账**(cli `isWorkflowCompletionCardEnqueued` 逐字)——
522
+ // 记账方(两个 enqueue + idle watcher)与清账方(上面的 drop)都在那儿。
523
+ if (n.task_type !== 'external' && isWorkflowCompletionCardEnqueued(taskId))
524
+ return;
525
+ // 跨通道记账(cli markEngineWorkflowNotified 同位、同条件:非 external,不看终态):
526
+ // 这帧到达 = 引擎已 server-side steer-inject 过,补发通道(workflow_complete)不得再注入。
527
+ // external 豁免([769] 对抗复审收紧:external 从不经补发通道,mark 只会缴械 probe 兜底)。
528
+ if (n.task_type !== 'external')
529
+ markEngineWorkflowNotified(taskId);
530
+ yield transcript({
531
+ type: 'user',
532
+ message: { role: 'user', content: [{ type: 'text', text: renderTaskNotificationXml(fields) }] },
533
+ uuid: idOf(m),
534
+ session_id: ctx.sessionId,
535
+ });
536
+ };
537
+ /**
538
+ * ⚠️ 本臂声明成 `ProjectionArmFn`(碰不到五个适配器状态模块),但它**有副作用** ——
539
+ * 写 notifications.ts 的 module 台账。两级 deps 证明的是「不碰适配器状态」,不是「纯」。
540
+ */
541
+ const workflowCompleteArm = function* (m) {
542
+ const runId = typeof m.runId === 'string' ? m.runId : undefined;
543
+ if (runId === undefined)
544
+ return;
545
+ // 跨通道去重(cli enqueueEngineWorkflowNotification 首行):该 run 已由推送帧记账过
546
+ // (引擎 server-side 注入过)⇒ Path B 绝不再喂模型一遍。
547
+ // 🔴 B5 ①:门与记账都在 module 台账 —— cli 里这一整段就是 enqueueEngineWorkflowNotification
548
+ // 的前三行,probe/idle watcher 合成的那两条链写的也是它。
549
+ // 🔴 B6 契约钉死(见 seam.ts 本臂注释「门归包、投递归宿主」):门 + 下面两条记号
550
+ // (markEngineWorkflowNotified / noteWorkflowCompletionCardEnqueued)**都在库内**,
551
+ // 宿主拿到臂只管裸入队。宿主若借自带同一道门的老函数投递 ⇒ 恒早退、一条都进不了队列。
552
+ if (isEngineWorkflowNotified(runId))
553
+ return;
554
+ markEngineWorkflowNotified(runId);
555
+ const wfStatus = m.status === 'failed' ? 'failed' : 'completed';
556
+ const wfSummary = typeof m.summary === 'string' ? m.summary : '';
557
+ // 🔴 B6 时序修(先红后绿,pure 门「早退 ⇒ cardEnqueued 仍落」):这一行原本在 `yield`
558
+ // **之后** —— 生成器在 yield 处挂起,后面的语句只有消费者**再要下一个值**时才跑;
559
+ // 消费者早退(turn 中止 / `it.return()`)时它**永不执行**。而同臂的
560
+ // `markEngineWorkflowNotified` 在 yield 之前恒执行 ⇒ 两条记号一条恒落一条条件落,
561
+ // 且方向正好是坏的那个:notified 落了 ⇒ idle watcher 不补发;cardEnqueued 没落 ⇒
562
+ // 推送帧到达时判不出卡的所有权 ⇒ **渲第二张完成卡**(B1c 反钉②)。
563
+ // 现存驱动全是走完整流的 `for await`,所以这条路径此前从未被断言过([1521]F1 同形)。
564
+ // 两条记号必须同在 yield 之前落 —— 「臂已发出」这件事在 yield 那一刻就已成立。
565
+ noteWorkflowCompletionCardEnqueued(runId);
566
+ yield chrome({
567
+ kind: 'workflow_notification_enqueue',
568
+ laneProof: MAIN,
569
+ runId,
570
+ status: wfStatus,
571
+ summary: wfSummary,
572
+ // 模型面正文照铸(宿主直接入队即可,不必各自拼 XML;与 core 的 tag/段序同形)
573
+ message: renderTaskNotificationXml({ taskId: runId, status: wfStatus, summary: wfSummary }),
574
+ });
575
+ };
576
+ // ══ M6b —— retry_status ════════════════════════════════════════════════════════════════════════
577
+ const retryStatusArm = function* (m, { ctx, flags }) {
578
+ // #47 矩阵 #5:子代 lane 帧绝不骑主 spinner(无判别守卫的无条件 store 写=默认可疑)。
579
+ if (typeof m.parentToolCallId === 'string')
580
+ return;
581
+ yield chrome({
582
+ kind: 'retry_status',
583
+ laneProof: MAIN,
584
+ status: mapBrainStatusToRetry({
585
+ phase: typeof m.phase === 'string' ? m.phase : '',
586
+ ...(typeof m.detail === 'string' ? { detail: m.detail } : {}),
587
+ ...(typeof m.retryInSec === 'number' ? { retryInSec: m.retryInSec } : {}),
588
+ }, ctx.now()),
589
+ });
590
+ // ⟨帧序耦合 6/7⟩ 挂旗;下一个主 lane 内容帧到达时由 TurnFlags 的帧前置清掉(恢复即清)。
591
+ flags.noteRetryOverlay();
592
+ };
593
+ /**
594
+ * 帧类型 → 臂。**Map 而不是对象字面量**:`{type:'__proto__'}` 这种 UNTRUSTED 帧命中对象表
595
+ * 就是原型污染(codex verdict 3)。表里查不到 = 拆分前 `switch` 的 `default:` 空臂,行为同。
596
+ */
597
+ export const ARMS = new Map([
598
+ ['assistant', assistantArm],
599
+ ['user', userArm],
600
+ ['tool_end_result', toolEndResultArm],
601
+ ['task_progress', taskProgressArm],
602
+ ['stream_event', streamEventArm],
603
+ ['turn_usage', turnUsageArm],
604
+ ['result', resultArm],
605
+ ['system', systemArm],
606
+ ['task_notification', taskNotificationArm],
607
+ ['workflow_complete', workflowCompleteArm],
608
+ ['diagnostics', diagnosticsArm],
609
+ ['steering_injected', steeringInjectedArm],
610
+ ['workspace_changed', workspaceChangedArm],
611
+ ['prompt_suggestions', promptSuggestionsArm],
612
+ ['retry_status', retryStatusArm],
613
+ ]);
@@ -0,0 +1,22 @@
1
+ import type { AdapterContext, AdapterOutput, ChromeEvent, LaneProof } from '../seam.js';
2
+ /** 帧 = wire 投影出来的中性 arm(cli seam/adapter downstream 的 SDKMessage 形),结构化读取。 */
3
+ export type Frame = Record<string, unknown>;
4
+ /** 主车道证明——本批实现的臂全在 leader lane(子代/workflow lane 的 tick 面留 #52b)。 */
5
+ export declare const MAIN: LaneProof;
6
+ /** 帧 → 稳定转录 id(`suffix` = 同一帧派生多条消息时的去撞位)。 */
7
+ export type IdOf = (frame: Frame, suffix?: string) => string;
8
+ export declare const chrome: (event: ChromeEvent) => AdapterOutput;
9
+ /**
10
+ * 🔴 入参从裸 `unknown` 收窄成 `Frame`(拆分同批的真收窄,不是搬运):调用点全是「按 CC 词汇
11
+ * 现拼的对象字面量」或「原样透传的 wire 帧」,两者都是 `Record<string, unknown>` ——
12
+ * 裸 `unknown` 把窄化义务外包给了每一个调用点,而这里本来就知道自己收的是什么形。
13
+ * 到 `SDKMessage` 的那一步仍是 cast:CC 词汇的字面形与 SDK 类型有已登记的分歧(DIVERGENCE-2/9),
14
+ * 这条 cast 是**有意**的边界,不是偷懒。
15
+ */
16
+ export declare const transcript: (message: Frame) => AdapterOutput;
17
+ /**
18
+ * [1653]:稳定键优先序 = 帧 id > seq > toolCallId,全缺才 ctx.uuid()。现役 wire 的稳定键落在
19
+ * `uuid` 字段(§E2 core 铸的 eventId / stamp 铸的帧 id),故映到 `id` 位喂进去(见 SEAM-GAP-1)。
20
+ * `suffix` = 同一帧派生多条消息时的去撞位(如 tool_use 卡 + 同帧的 reject 结果)。
21
+ */
22
+ export declare function makeIdOf(ctx: AdapterContext): IdOf;