@sema-agent/client-core 0.12.1 → 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.
- package/README.md +15 -2
- package/dist/abortableSleep.d.ts +29 -0
- package/dist/abortableSleep.js +43 -0
- package/dist/adapt/arms.d.ts +58 -0
- package/dist/adapt/arms.js +613 -0
- package/dist/adapt/ids.d.ts +22 -0
- package/dist/adapt/ids.js +34 -0
- package/dist/adapt/instanceLedger.d.ts +36 -0
- package/dist/adapt/instanceLedger.js +50 -0
- package/dist/adapt/panelTasks.d.ts +54 -0
- package/dist/adapt/panelTasks.js +193 -0
- package/dist/adapt/textStream.d.ts +49 -0
- package/dist/adapt/textStream.js +141 -0
- package/dist/adapt/toolCards.d.ts +65 -0
- package/dist/adapt/toolCards.js +100 -0
- package/dist/adapt/turnFlags.d.ts +33 -0
- package/dist/adapt/turnFlags.js +55 -0
- package/dist/adapt/wireShapes.d.ts +93 -0
- package/dist/adapt/wireShapes.js +167 -0
- package/dist/adapt.d.ts +32 -59
- package/dist/adapt.js +78 -1206
- package/dist/adapter/downstream/eventToSdkMessage.d.ts +55 -13
- package/dist/adapter/downstream/eventToSdkMessage.js +166 -106
- package/dist/adapter/downstream/terminalToSdkResult.js +149 -160
- package/dist/adapter/downstream/turnUsageToModelUsage.d.ts +36 -0
- package/dist/adapter/downstream/turnUsageToModelUsage.js +34 -6
- package/dist/adapter/runStream.d.ts +29 -4
- package/dist/adapter/runStream.js +129 -13
- package/dist/adapter/types.d.ts +2 -2
- package/dist/agentSession/backgroundView.js +4 -15
- package/dist/agentsWireCaps.d.ts +10 -6
- package/dist/agentsWireCaps.js +21 -7
- package/dist/argvFlagValue.d.ts +41 -0
- package/dist/argvFlagValue.js +69 -0
- package/dist/attachmentsWireCaps.d.ts +3 -2
- package/dist/attachmentsWireCaps.js +5 -3
- package/dist/classifierVerdictWire.d.ts +8 -0
- package/dist/classifierVerdictWire.js +8 -0
- package/dist/cloudConfigWireCaps.d.ts +28 -1
- package/dist/cloudConfigWireCaps.js +51 -11
- package/dist/controlRouter.d.ts +14 -8
- package/dist/controlRouter.js +15 -21
- package/dist/detachWire.d.ts +15 -5
- package/dist/detachWire.js +17 -7
- package/dist/effortWire.d.ts +0 -21
- package/dist/effortWire.js +6 -20
- package/dist/engineInlineTaskStats.d.ts +12 -6
- package/dist/engineWireSdk.d.ts +12 -0
- package/dist/env/localeGeo.js +2 -1
- package/dist/envFlag.d.ts +39 -0
- package/dist/envFlag.js +51 -0
- package/dist/finalVerifyWire.d.ts +7 -5
- package/dist/finalVerifyWire.js +6 -5
- package/dist/fleet/fleetLedger.d.ts +32 -9
- package/dist/fleet/fleetLedger.js +119 -34
- package/dist/fleet/fleetProjection.d.ts +44 -6
- package/dist/fleet/fleetProjection.js +52 -10
- package/dist/fleetAgentPanelProjection.d.ts +18 -1
- package/dist/fleetAgentPanelProjection.js +61 -14
- package/dist/forkWireCaps.d.ts +2 -1
- package/dist/forkWireCaps.js +5 -11
- package/dist/goalStopHook.d.ts +142 -0
- package/dist/goalStopHook.js +258 -0
- package/dist/headlessPermissionModeWire.d.ts +10 -0
- package/dist/headlessPermissionModeWire.js +24 -21
- package/dist/headlessReconnectWire.d.ts +7 -1
- package/dist/headlessReconnectWire.js +20 -2
- package/dist/hitl/approvalsFeed.js +31 -8
- package/dist/hitl/askGateWire.d.ts +27 -96
- package/dist/hitl/askGateWire.js +69 -546
- package/dist/hitl/frameRouter.d.ts +86 -0
- package/dist/hitl/frameRouter.js +342 -0
- package/dist/hitl/gateLedger.d.ts +107 -0
- package/dist/hitl/gateLedger.js +113 -0
- package/dist/hitl/hitlBridge.d.ts +75 -15
- package/dist/hitl/hitlBridge.js +94 -22
- package/dist/hitl/hitlHostSurface.d.ts +49 -0
- package/dist/hitl/hitlHostSurface.js +155 -0
- package/dist/hitl/parkResolver.d.ts +74 -0
- package/dist/hitl/parkResolver.js +241 -0
- package/dist/hitl/planReviewWire.d.ts +60 -2
- package/dist/hitl/planReviewWire.js +152 -74
- package/dist/hitl/toolApprovalWire.d.ts +67 -4
- package/dist/hitl/toolApprovalWire.js +119 -31
- package/dist/hooksWireCaps.d.ts +1 -82
- package/dist/hooksWireCaps.js +34 -235
- package/dist/host.d.ts +16 -5
- package/dist/index.d.ts +2 -0
- package/dist/index.js +15 -1
- package/dist/interactiveToolsWire.d.ts +15 -4
- package/dist/interactiveToolsWire.js +24 -26
- package/dist/limitsWire.js +7 -40
- package/dist/liveInitToolFace.d.ts +51 -6
- package/dist/liveQuestionStore.d.ts +5 -6
- package/dist/model/providerPresets.js +11 -1
- package/dist/notifications.d.ts +48 -2
- package/dist/notifications.js +223 -46
- package/dist/retainBackgroundWireCaps.d.ts +3 -2
- package/dist/retainBackgroundWireCaps.js +5 -9
- package/dist/sandboxWire.d.ts +9 -31
- package/dist/sandboxWire.js +51 -50
- package/dist/scenarioWire.d.ts +1 -1
- package/dist/scenarioWire.js +25 -36
- package/dist/seam.d.ts +23 -6
- package/dist/seam.js +40 -30
- package/dist/seatContract.d.ts +369 -83
- package/dist/seatContract.js +585 -198
- package/dist/selfOrchestrationWireCaps.d.ts +6 -5
- package/dist/selfOrchestrationWireCaps.js +8 -12
- package/dist/sessionSlot.d.ts +8 -0
- package/dist/sessionSlot.js +1 -0
- package/dist/steering.js +2 -2
- package/dist/subagent/engineTaskHandleWire.d.ts +3 -0
- package/dist/subagent/engineTaskHandleWire.js +17 -2
- package/dist/subagentContentStore.d.ts +5 -5
- package/dist/toolResult.d.ts +89 -8
- package/dist/toolResult.js +99 -30
- package/dist/typePins.d.ts +17 -0
- package/dist/typePins.js +1 -0
- package/dist/ultracodeWireCaps.js +5 -6
- package/dist/unrefTimer.d.ts +19 -0
- package/dist/unrefTimer.js +5 -0
- package/dist/workflow.d.ts +3 -2
- package/dist/workflow.js +3 -2
- package/dist/workflowClient.d.ts +7 -0
- package/dist/workflowClient.js +47 -12
- package/package.json +3 -3
package/dist/adapt.js
CHANGED
|
@@ -1,174 +1,21 @@
|
|
|
1
|
-
|
|
2
|
-
import {
|
|
3
|
-
import { mapBrainStatusToRetry } from './retryStatus.js';
|
|
4
|
-
import { steeringInjectedToAttachments } from './steering.js';
|
|
5
|
-
import { projectDiagnosticsFrame } from './diagnostics.js';
|
|
6
|
-
// ── B4:A 层搬入后适配器**自持**的四个本包台账(写者与读者同包 = R4 单实例纪律;
|
|
7
|
-
// 这四件都没有 UI 订阅面,做成 chrome 臂只会凭空长出一条「宿主不实现就静默失效」的义务)。
|
|
8
|
-
// ── B5:B/D/E 层(工具结果卡)。纯投影 + 三个本包台账副作用,零宿主依赖。
|
|
9
|
-
import { degradedToolResultBody, detectEngineBgShellReceipt, reportFindingsToolUseResult, structuredToToolUseResult, todoWriteToolUseResult, wireOutputToBody, } from './toolResult.js';
|
|
10
|
-
import { recordEngineToolLabel } from './engineToolLabelStore.js';
|
|
11
|
-
import { registerSubagentAlias } from './subagentContentStore.js';
|
|
12
|
-
import { isWorkflowAgentTaskId, recordWorkflowAgentTaskId } from './workflow.js';
|
|
13
|
-
import { clearEnginePanelTaskResident, markEnginePanelTaskResident } from './engineAgentPanelStore.js';
|
|
14
|
-
/** 主车道证明——本批实现的臂全在 leader lane(子代/workflow lane 的 tick 面留 #52b)。 */
|
|
15
|
-
const MAIN = { lane: 'main' };
|
|
1
|
+
// 本文件只剩台账 import/export 那四个方法要用的四个口;15 条臂的全部依赖已随臂表落 adapt/arms.ts。
|
|
2
|
+
import { isEngineWorkflowNotified, isWorkflowCompletionCardEnqueued, listNotifiedRuns, listWorkflowCompletionCardsEnqueued, markEngineWorkflowNotified, noteWorkflowCompletionCardEnqueued, parseTranscriptNotificationSeeds, } from './notifications.js';
|
|
16
3
|
/**
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* · **16ms** = CC **桌面端**宿主 `makeStreamDeltaCoalescer` 的单帧攒批节拍(设计稿 R1 实测)。
|
|
23
|
-
* ⇒ 结论:**节拍是宿主渲染策略,不是 wire 语义** —— CC 自己两个端就是两个值。web/桌面宿主
|
|
24
|
-
* 想要 16ms 就传 `coalesceIntervalMs: 16`,合并**语义**(思考先于文本、段边界重开块、
|
|
25
|
-
* committed 恒随后)在库里,一份。
|
|
26
|
-
*
|
|
27
|
-
* 🔴 顺带钉一句反面事实(R1 证伪,别再写进任何注释/文档):**"UI 从不见裸 token" 是错的** ——
|
|
28
|
-
* CC 攒批后 flush 的载荷仍是 `stream_event` 增量 delta,CC 的 UI 照样消费增量;
|
|
29
|
-
* 完整 assistant 消息是 SDK 另发的一路。我们这里的 `stream_delta` chrome 臂同构。
|
|
4
|
+
* ── A 族拆分(REF-CC-SPLIT-01,2026-08-02)—— 帧形词表与双平面共用件已落 `src/adapt/` ─────────
|
|
5
|
+
* 本文件从此只留:公面契约表(coverage/divergences/ledger 形)+ 适配器实例(台账)+ **驱动壳**。
|
|
6
|
+
* 词表与纯助手在 `adapt/wireShapes.ts`,双平面包装与 id 派生在 `adapt/ids.ts`。
|
|
7
|
+
* 🔴 公面用**具名再导出**(不是 `export *`):`TASK_TOOL_NAMES` / `decisionOf` 是内部件,
|
|
8
|
+
* `export *` 会把它们推上 npm 公面,public-export-baseline 双向门当场红。
|
|
30
9
|
*/
|
|
31
|
-
export
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
* `ctx.setTimer` 缺席 ⇒ 整条竞速不启用(纯等下一帧),与 0.4.0 逐字同行为。
|
|
41
|
-
*/
|
|
42
|
-
export const IDLE_FLUSH_MS = 1500;
|
|
43
|
-
/** cli SUBAGENT_TOOL_NAMES / WORKFLOW_TOOL_NAMES 同源(判别资产,别各自重写)。 */
|
|
44
|
-
export const SUBAGENT_TOOL_NAMES = new Set(['Task', 'Agent', 'Fork']);
|
|
45
|
-
export const WORKFLOW_TOOL_NAMES = new Set(['Workflow', 'RunWorkflow', 'run_workflow']);
|
|
46
|
-
/** 引擎侧 task 族工具(ctrl+t 面板台账同步的侧信道触发名;cli handleEngineTaskToolUse 受理的三名:
|
|
47
|
-
* TodoWrite 全量表 upsert + TaskCreate/TaskUpdate;其余工具该函数直接 return)。 */
|
|
48
|
-
const TASK_TOOL_NAMES = new Set(['TodoWrite', 'TaskCreate', 'TaskUpdate']);
|
|
49
|
-
/**
|
|
50
|
-
* CC 自己的 reject/cancel 哨兵串(**数据**,非 CC 逻辑;cli src/utils/messages.ts:210-212 逐字)。
|
|
51
|
-
* CC 的 UserToolResultMessage 按 content 串前缀路由到 reject/cancel 卡,不看 is_error 位。
|
|
52
|
-
*/
|
|
53
|
-
export const REJECT_MESSAGE = "The user doesn't want to proceed with this tool use. The tool use was rejected (eg. if it was a file edit, the new_string was NOT written to the file). STOP what you are doing and wait for the user to tell you how to proceed.";
|
|
54
|
-
export const CANCEL_MESSAGE = "The user doesn't want to take this action right now. STOP what you are doing and wait for the user to tell you how to proceed.";
|
|
55
|
-
/** CJK 加权 token 估算(cli estimateCjkTokens 逐字:CJK≈0.6,其余≈0.25 tok/char)。 */
|
|
56
|
-
export function estimateCjkTokens(s) {
|
|
57
|
-
let t = 0;
|
|
58
|
-
for (const c of s) {
|
|
59
|
-
const cp = c.codePointAt(0) ?? 0;
|
|
60
|
-
t +=
|
|
61
|
-
(cp >= 0x2e80 && cp <= 0x9fff) ||
|
|
62
|
-
(cp >= 0x3000 && cp <= 0x30ff) ||
|
|
63
|
-
(cp >= 0xac00 && cp <= 0xd7a3) ||
|
|
64
|
-
(cp >= 0xf900 && cp <= 0xfaff) ||
|
|
65
|
-
(cp >= 0xff00 && cp <= 0xffef) ||
|
|
66
|
-
(cp >= 0x20000 && cp <= 0x3ffff)
|
|
67
|
-
? 0.6
|
|
68
|
-
: 0.25;
|
|
69
|
-
}
|
|
70
|
-
return Math.round(t);
|
|
71
|
-
}
|
|
72
|
-
/** 引擎 subagent 卡的短标签(cli shortTaskLabel 逐字)。 */
|
|
73
|
-
/**
|
|
74
|
-
* T8 —— 压平 §E1 wire `output`(**非均匀**:`string` | `(TextContent|ImageContent)[]`,events.d.ts:78)
|
|
75
|
-
* 成一条纯文本(cli `flattenWireOutput` 逐字)。text 块贡献 `.text`,image 块贡献中性占位 `[image]`
|
|
76
|
-
* (转录卡是文本粒度),裸字符串直通。
|
|
77
|
-
* 🔴 载荷 UNTRUSTED + 仅供观测(events.d.ts:80-85):渲染可以,**绝不回喂模型**;`truncated` 的更不行。
|
|
78
|
-
* B4 把它从 B 层(`wireOutputToBody`)提到独立导出:A 层的 `settlePanelTasks(report)` 也要用它
|
|
79
|
-
* (关卡时把子代最终报告串到面板行 end 事件上),两处必须同一份实现。
|
|
80
|
-
*/
|
|
81
|
-
export function flattenWireOutput(output) {
|
|
82
|
-
if (typeof output === 'string')
|
|
83
|
-
return output;
|
|
84
|
-
if (Array.isArray(output)) {
|
|
85
|
-
const parts = [];
|
|
86
|
-
for (const block of output) {
|
|
87
|
-
if (block && typeof block === 'object') {
|
|
88
|
-
const b = block;
|
|
89
|
-
if (b.type === 'text' && typeof b.text === 'string')
|
|
90
|
-
parts.push(b.text);
|
|
91
|
-
else if (b.type === 'image')
|
|
92
|
-
parts.push('[image]');
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
return parts.join('');
|
|
96
|
-
}
|
|
97
|
-
return '';
|
|
98
|
-
}
|
|
99
|
-
export function shortTaskLabel(task) {
|
|
100
|
-
const firstLine = (task.split('\n').find(l => l.trim()) ?? task).trim();
|
|
101
|
-
const words = firstLine.split(/\s+/).slice(0, 6).join(' ');
|
|
102
|
-
const label = words.length > 48 ? words.slice(0, 47) + '…' : words;
|
|
103
|
-
return label || 'subagent';
|
|
104
|
-
}
|
|
105
|
-
/**
|
|
106
|
-
* tool_use 块的渲染面消毒(cli assistant 臂逐条搬运,**纯函数**,不改入参):
|
|
107
|
-
* ① 摘 §E2 wire 身份(eventId/parentToolCallId)并从渲染块剥掉;
|
|
108
|
-
* ② 剥所有 `_sema_*` 保留哨兵(CC 的 per-tool 渲染器 safeParse strictObject,多一个键就降级成通用卡);
|
|
109
|
-
* ③ 引擎 subagent 参数名 {agent,task,taskName} → CC 名 {subagent_type,description,prompt}
|
|
110
|
-
* (strict schema 拒绝多余键 ⇒ REPLACE 不 merge;taskName 缺席时从 task 派生短标签);
|
|
111
|
-
* ④ 件8 类型标签诚实化:引擎通道兑现不了的 subagent_type 值一律删(只留 fork/general-purpose/worker),
|
|
112
|
-
* 否则卡头会显示 `Plan(…)` 而实跑 general-purpose。
|
|
113
|
-
*/
|
|
114
|
-
export function sanitizeToolUseBlock(block) {
|
|
115
|
-
const out = { ...block };
|
|
116
|
-
const rawInput = out.input;
|
|
117
|
-
const eventId = typeof out.eventId === 'string' ? out.eventId : undefined;
|
|
118
|
-
const parentToolCallId = typeof out.parentToolCallId === 'string' ? out.parentToolCallId : undefined;
|
|
119
|
-
// 🔴 B4 补漏(0.5.0 真缺口,差分守卫此前无 fixture 携 label ⇒ 漏得静默):core 的**展示名**
|
|
120
|
-
// `tool_start.label` 同样是侧信道,必须在渲染前摘掉 —— tool_use block 会进壳 transcript,
|
|
121
|
-
// /compact 时原样直发 provider,多一个键 = 400 invalid_request(cli 头注记的「件1 事故同款」)。
|
|
122
|
-
// 摘下来的值由调用方交给 engineToolLabelStore(纯函数不做副作用),unknown-tool 卡按
|
|
123
|
-
// toolCallId 查。今天唯一的真命中族 = MCP 远端工具(name=mcp__srv__tool / label=srv:tool)。
|
|
124
|
-
const label = typeof out.label === 'string' ? out.label : undefined;
|
|
125
|
-
delete out.label;
|
|
126
|
-
delete out.eventId;
|
|
127
|
-
delete out.parentToolCallId;
|
|
128
|
-
if (out.input && typeof out.input === 'object') {
|
|
129
|
-
const cleaned = {};
|
|
130
|
-
for (const [k, val] of Object.entries(out.input)) {
|
|
131
|
-
if (!k.startsWith('_sema_'))
|
|
132
|
-
cleaned[k] = val;
|
|
133
|
-
}
|
|
134
|
-
out.input = cleaned;
|
|
135
|
-
}
|
|
136
|
-
if (out.input && typeof out.input === 'object') {
|
|
137
|
-
const inp = out.input;
|
|
138
|
-
const looksEngineSubagent = typeof inp.task === 'string' && inp.prompt === undefined && inp.description === undefined;
|
|
139
|
-
if (looksEngineSubagent) {
|
|
140
|
-
const task = inp.task;
|
|
141
|
-
const taskName = typeof inp.taskName === 'string' ? inp.taskName.trim() : '';
|
|
142
|
-
const label = taskName || shortTaskLabel(task);
|
|
143
|
-
const remapped = { description: label, prompt: task };
|
|
144
|
-
if (typeof inp.agent === 'string')
|
|
145
|
-
remapped.subagent_type = inp.agent;
|
|
146
|
-
if (typeof inp.model === 'string')
|
|
147
|
-
remapped.model = inp.model;
|
|
148
|
-
out.input = remapped;
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
if (SUBAGENT_TOOL_NAMES.has(out.name) && out.input && typeof out.input === 'object') {
|
|
152
|
-
const inp = out.input;
|
|
153
|
-
if (typeof inp.subagent_type === 'string' &&
|
|
154
|
-
!['fork', 'general-purpose', 'worker'].includes(inp.subagent_type)) {
|
|
155
|
-
delete inp.subagent_type;
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
return {
|
|
159
|
-
block: out,
|
|
160
|
-
rawInput,
|
|
161
|
-
...(eventId !== undefined ? { eventId } : {}),
|
|
162
|
-
...(parentToolCallId !== undefined ? { parentToolCallId } : {}),
|
|
163
|
-
...(label !== undefined ? { label } : {}),
|
|
164
|
-
};
|
|
165
|
-
}
|
|
166
|
-
/** seam 的 reserved decision 哨兵(cli decisionOf 逐字)。 */
|
|
167
|
-
function decisionOf(block) {
|
|
168
|
-
const input = block.input;
|
|
169
|
-
const d = input?._sema_decision;
|
|
170
|
-
return d === 'reject' || d === 'cancel' || d === 'error' ? d : undefined;
|
|
171
|
-
}
|
|
10
|
+
export { CANCEL_MESSAGE, estimateCjkTokens, flattenWireOutput, FLUSH_INTERVAL_MS, IDLE_FLUSH_MS, REJECT_MESSAGE, sanitizeToolUseBlock, shortTaskLabel, SUBAGENT_TOOL_NAMES, WORKFLOW_TOOL_NAMES, } from './adapt/wireShapes.js';
|
|
11
|
+
import { IDLE_FLUSH_MS } from './adapt/wireShapes.js';
|
|
12
|
+
import { chrome, MAIN, makeIdOf } from './adapt/ids.js';
|
|
13
|
+
import { createTextStream } from './adapt/textStream.js';
|
|
14
|
+
import { createToolCardLedger } from './adapt/toolCards.js';
|
|
15
|
+
import { createPanelTaskLedger } from './adapt/panelTasks.js';
|
|
16
|
+
import { createAdapterInstanceLedger } from './adapt/instanceLedger.js';
|
|
17
|
+
import { createTurnFlags } from './adapt/turnFlags.js';
|
|
18
|
+
import { ARMS } from './adapt/arms.js';
|
|
172
19
|
/** 本批覆盖清单——差分守卫按它区分「已覆盖臂必须逐字段等价」与「已声明未覆盖臂」。 */
|
|
173
20
|
export const ADAPTER_COVERAGE = {
|
|
174
21
|
/** 已落码的帧臂。 */
|
|
@@ -269,33 +116,25 @@ export const ADAPTER_DIVERGENCES = [
|
|
|
269
116
|
'DIVERGENCE-9(B5):`tool_result` 消息的 uuid —— cli 在缺 `eventId` 时铸随机 v4,本包派生 ' +
|
|
270
117
|
'`wtc_<toolUseId>`(DIVERGENCE-2 同族:同流重放 ⇒ 同 id 序列,[1653])。守卫按别名结构比,不比字面。',
|
|
271
118
|
];
|
|
272
|
-
const TASK_NOTIFICATION_TAG = 'task-notification';
|
|
273
|
-
const TASK_ID_TAG = 'task-id';
|
|
274
|
-
const STATUS_TAG = 'status';
|
|
275
119
|
/**
|
|
276
120
|
* 适配器实例 —— **台账**(通知去重/跨通道记账)挂实例,`adapt()` 每 turn 调一次,turn 级状态在
|
|
277
121
|
* 调用内。跨 turn 语义(进程级去重)因此靠「同一个实例贯穿 session」保证,重启靠 import/export。
|
|
278
122
|
*/
|
|
279
123
|
class WireToCcAdapterImpl {
|
|
280
|
-
renderedNotifications = new Set();
|
|
281
|
-
// 🔴 B5 ①:`notifiedRuns` / `cardEnqueuedRuns` **不在实例上** —— 它们的配对半场(清账的
|
|
282
|
-
// dropQueuedNotificationsForRun、记账的两个 enqueue、idle watcher)全在 notifications.ts 的
|
|
283
|
-
// module 台账上,放实例 = 前提不共享 = 双卡/零显示/模型二收(0.6.0 真缺口,[1857])。
|
|
284
|
-
// 适配器一律经 notifications.ts 的四个口读写,别在这里再长一份影子副本。
|
|
285
|
-
/** W1 三级判据素材:本壳见过的 Workflow 工具 tool_use id(module 级台账的实例化)。 */
|
|
286
|
-
seenWorkflowToolCallIds = new Set();
|
|
287
124
|
/**
|
|
288
|
-
*
|
|
289
|
-
*
|
|
290
|
-
*
|
|
291
|
-
*
|
|
292
|
-
*
|
|
125
|
+
* 三个**实例级** Set 的持有者(A 族 FIX-05:收成一个显式对象,消 `self.`/`this.` 双路径)——
|
|
126
|
+
* renderedNotifications / seenWorkflowToolCallIds / firedSubagentStartHookTaskIds,
|
|
127
|
+
* 逐条语义见 adapt/instanceLedger.ts 头注(含 DIVERGENCE-6 的实例级 vs module 级分界)。
|
|
128
|
+
* 🔴 B5 ①:`notifiedRuns` / `cardEnqueuedRuns` **不在实例上** —— 它们的配对半场(清账的
|
|
129
|
+
* dropQueuedNotificationsForRun、记账的两个 enqueue、idle watcher)全在 notifications.ts 的
|
|
130
|
+
* module 台账上,放实例 = 前提不共享 = 双卡/零显示/模型二收(0.6.0 真缺口,[1857])。
|
|
131
|
+
* 适配器一律经 notifications.ts 的四个口读写,别在这里再长一份影子副本。
|
|
293
132
|
*/
|
|
294
|
-
|
|
133
|
+
ledger = createAdapterInstanceLedger();
|
|
295
134
|
exportLedger() {
|
|
296
135
|
return {
|
|
297
136
|
version: 1,
|
|
298
|
-
renderedNotifications:
|
|
137
|
+
renderedNotifications: this.ledger.listRenderedNotifications(),
|
|
299
138
|
// B5 ①:这两项是 module 台账的快照(不是实例私有),跨实例一致。
|
|
300
139
|
notifiedRuns: listNotifiedRuns(),
|
|
301
140
|
cardEnqueuedRuns: listWorkflowCompletionCardsEnqueued(),
|
|
@@ -303,17 +142,7 @@ class WireToCcAdapterImpl {
|
|
|
303
142
|
}
|
|
304
143
|
importLedger(state) {
|
|
305
144
|
let n = 0;
|
|
306
|
-
|
|
307
|
-
if (!Array.isArray(v))
|
|
308
|
-
return;
|
|
309
|
-
for (const k of v) {
|
|
310
|
-
if (typeof k === 'string' && k.length > 0 && !into.has(k)) {
|
|
311
|
-
into.add(k);
|
|
312
|
-
n++;
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
};
|
|
316
|
-
/** B5 ①:module 台账那两项经真源写口回植(has 判重 = 与 take 同样只数新增)。 */
|
|
145
|
+
/** B5 ①:module 台账那两项经真源写口回植(has 判重 = 与实例台账那条同样只数新增)。 */
|
|
317
146
|
const takeModule = (v, has, add) => {
|
|
318
147
|
if (!Array.isArray(v))
|
|
319
148
|
return;
|
|
@@ -325,7 +154,10 @@ class WireToCcAdapterImpl {
|
|
|
325
154
|
}
|
|
326
155
|
};
|
|
327
156
|
const s = state;
|
|
328
|
-
|
|
157
|
+
// 单次快照(codex 复审 #2 收货修):拆分前这里对 `renderedNotifications` 恰一次属性读。
|
|
158
|
+
const rendered = s.renderedNotifications;
|
|
159
|
+
if (Array.isArray(rendered))
|
|
160
|
+
n += this.ledger.takeRenderedNotifications(rendered);
|
|
329
161
|
takeModule(s.notifiedRuns, isEngineWorkflowNotified, markEngineWorkflowNotified);
|
|
330
162
|
takeModule(s.cardEnqueuedRuns, isWorkflowCompletionCardEnqueued, noteWorkflowCompletionCardEnqueued);
|
|
331
163
|
return n;
|
|
@@ -342,7 +174,7 @@ class WireToCcAdapterImpl {
|
|
|
342
174
|
// 看着都对)。壳侧 shim 化后两个入口都落到同一个解析器上。
|
|
343
175
|
const { renderedKeys, notifiedKeys, seeded } = parseTranscriptNotificationSeeds(messages);
|
|
344
176
|
for (const k of renderedKeys)
|
|
345
|
-
this.
|
|
177
|
+
this.ledger.addRenderedNotification(k);
|
|
346
178
|
for (const taskId of notifiedKeys) {
|
|
347
179
|
// cli seedEngineTaskNotificationDedupFromTranscript 同位:回植走 module 台账
|
|
348
180
|
// (outstanding 药丸 / idle watcher 收摊那半场读的就是它)。
|
|
@@ -356,344 +188,22 @@ class WireToCcAdapterImpl {
|
|
|
356
188
|
noteWorkflowCompletionCardEnqueued(runId);
|
|
357
189
|
}
|
|
358
190
|
async *adapt(frames, ctx) {
|
|
359
|
-
//
|
|
360
|
-
|
|
361
|
-
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
362
|
-
const self = this;
|
|
191
|
+
// FIX-05:实例台账走**唯一一条**显式路径(拆分前是 `self.` / `this.` 两条)。
|
|
192
|
+
const inst = this.ledger;
|
|
363
193
|
const turnStartAt = ctx.now();
|
|
364
|
-
// ── id 派生
|
|
365
|
-
|
|
366
|
-
//
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
...(typeof frame.toolCallId === 'string' ? { toolCallId: frame.toolCallId } : {}),
|
|
378
|
-
}, ctx);
|
|
379
|
-
};
|
|
380
|
-
// ── turn 级状态(cli generator 局部变量的等价) ──────────────────────────────────────────────
|
|
381
|
-
let answer = '';
|
|
382
|
-
let answerSegment = '';
|
|
383
|
-
let thinking = '';
|
|
384
|
-
let emittedAssistantText = false;
|
|
385
|
-
let textPending = '';
|
|
386
|
-
let thinkingPending = '';
|
|
387
|
-
let textBlockOpen = false;
|
|
388
|
-
let thinkingBlockOpen = false;
|
|
389
|
-
let lastFlushAt = 0;
|
|
390
|
-
/** T29 攒批节拍:宿主策略优先(桌面/web 可传 16ms 对齐 CC 桌面端),缺席=207 CLI 口径 100ms。 */
|
|
391
|
-
const flushEveryMs = typeof ctx.coalesceIntervalMs === 'number' && ctx.coalesceIntervalMs >= 0
|
|
392
|
-
? ctx.coalesceIntervalMs
|
|
393
|
-
: FLUSH_INTERVAL_MS;
|
|
394
|
-
let messageStartEmitted = false;
|
|
395
|
-
let endEmitted = false;
|
|
396
|
-
let assistantResponseId = null;
|
|
397
|
-
/** committed 消息的 id 锚 —— 该段/该思考块的**第一帧**(重放确定性:同流同 id)。 */
|
|
398
|
-
let segmentAnchor = null;
|
|
399
|
-
let thinkingAnchor = null;
|
|
400
|
-
const pending = new Map();
|
|
401
|
-
// ── B4 / MF-10:面板行台账(cli generator 局部变量逐个对位)────────────────────────────────
|
|
402
|
-
// 引擎侧子代的 tick 只带 rollup 数字({toolUses,totalTokens}) = CC `AgentProgress` 同形,
|
|
403
|
-
// 所以忠实的 CC-187 面是 footer 的 CoordinatorTaskPanel(CC 自己对进程外 agent 的先例:
|
|
404
|
-
// 面板行 + task-notification,从不合成 inline 消息轨)。下面这组把「tick → 哪张委派卡」的
|
|
405
|
-
// 归属、行的活性(live/inert/unbound)、以及 settle 的三处 sweep 全部管起来。
|
|
406
|
-
/** taskId → 绑定的 open tool_use id。 */
|
|
407
|
-
const taskCardBinding = new Map();
|
|
408
|
-
/** 已被某 taskId 认领的 tool_use id(一张卡只认一个子代)。 */
|
|
409
|
-
const boundToolCalls = new Set();
|
|
410
|
-
/** 本 turn 发布过面板行的全部 taskId(绑定与否都算)。 */
|
|
411
|
-
const livePanelTasks = new Set();
|
|
412
|
-
/** 已落终态的行(settle 幂等)。 */
|
|
413
|
-
const endedPanelTasks = new Set();
|
|
414
|
-
/**
|
|
415
|
-
* #6 —— 绑定时那张卡**正开着**的 taskId(= "live" 绑定)。只有这些行属本 turn 生命周期、
|
|
416
|
-
* 可被 turn 末防御 sweep 结掉。inert(显式父指向已关/没见过的卡 —— 跨 turn bg 子代的典型形)
|
|
417
|
-
* 与 unbound 行是 **session 常驻**:终态只认 task_notification / bg_notification。
|
|
418
|
-
*/
|
|
419
|
-
const liveBoundPanelTasks = new Set();
|
|
420
|
-
/** taskId → 子代类型(Stop hook 查它;cli 同为 turn 局部)。 */
|
|
421
|
-
const subagentTypeById = new Map();
|
|
422
|
-
const chrome = (event) => ({ plane: 'chrome', event });
|
|
423
|
-
const transcript = (message) => ({
|
|
424
|
-
plane: 'transcript',
|
|
425
|
-
message: message,
|
|
426
|
-
});
|
|
427
|
-
const mintResponseId = (frame) => (assistantResponseId ??= `msg_sema_${idOf(frame, 'resp')}`);
|
|
428
|
-
// ── B4 / MF-10:行绑定 + 三处 sweep(cli bindTaskCard / unboundSubagentCandidates /
|
|
429
|
-
// settlePanelTasks 逐条对位)───────────────────────────────────────────────────────────
|
|
430
|
-
/** 行的车道证明:绑到卡的 = subagent lane(带父卡 id),未绑的 = 主 lane(taskId 键的独立行)。 */
|
|
431
|
-
const laneOfTask = (taskId) => {
|
|
432
|
-
const card = taskCardBinding.get(taskId);
|
|
433
|
-
return card !== undefined ? { lane: 'subagent', parentToolCallId: card } : MAIN;
|
|
434
|
-
};
|
|
435
|
-
/**
|
|
436
|
-
* T32 行绑定。**typed 双键在场恒赢**(B6 定案):wire 点名了 `parentToolCallId` 时,下面的
|
|
437
|
-
* FIFO 臂**一次都不会被考虑** —— 连「点名的卡不合格」那一支也是直接返回 unbound,而不是
|
|
438
|
-
* 退回去猜。判据锚在这里:显式支的两条出口(绑定 / 拒绝)都不落进 else。
|
|
439
|
-
*/
|
|
440
|
-
const bindTaskCard = (taskId, parentToolCallId) => {
|
|
441
|
-
const existing = taskCardBinding.get(taskId);
|
|
442
|
-
if (existing !== undefined)
|
|
443
|
-
return existing;
|
|
444
|
-
let target;
|
|
445
|
-
if (parentToolCallId !== undefined) {
|
|
446
|
-
// P1② 精确绑定 —— wire 点名了委派卡。信但不无条件信([1617]①,frame-lane-matrix #1):
|
|
447
|
-
// 被点名的卡**开着**且**不是** subagent 形工具(Task/Agent/Fork)时拒绝绑定 —— 否则
|
|
448
|
-
// workflow/Bash 卡一关就会把这条行 settle 掉、还会收走它的 inline stats(错归因)。
|
|
449
|
-
// 行保持 unbound(taskId 键的 rollup 仍是真的)。已关/没见过的 id 仍**惰性绑定**(inert):
|
|
450
|
-
// 终态由通知落,严格优于错绑另一张开着的卡。
|
|
451
|
-
const named = pending.get(parentToolCallId);
|
|
452
|
-
if (named !== undefined && !SUBAGENT_TOOL_NAMES.has(named.name))
|
|
453
|
-
return undefined;
|
|
454
|
-
target = parentToolCallId;
|
|
455
|
-
}
|
|
456
|
-
else {
|
|
457
|
-
// ── old-engine fallback(FIFO 猜父)—— B6 P1-2 归属定案:**不删** ────────────────────
|
|
458
|
-
// 🔴 判据先摆正:这条臂不是「P1-2 之前的临时权宜」,它是 **tolerate-absent 的保护臂**。
|
|
459
|
-
// 决定后果方向的量 = 「这条子代行有没有归属」:
|
|
460
|
-
// · 回落在 ⇒ 老引擎(不发 parentToolCallId)上行仍绑到唯一那张开着的委派卡 ⇒ 行有归属、
|
|
461
|
-
// inline stats 有去处、关卡时被 settle;
|
|
462
|
-
// · 回落删掉 ⇒ 老引擎上行恒 unbound ⇒ 转录里那条群组行永远不长统计、卡关了也不结,
|
|
463
|
-
// 用户看到一条转圈到 turn 末的行。
|
|
464
|
-
// ⇒ **保护型缺席 = 漏保护**,与 `parked → awaiting approval` 同判(不删)。
|
|
465
|
-
// 猜错的风险已被两道门收窄:只考虑**未绑**且**subagent 形**的卡;≥2 张候选时上层
|
|
466
|
-
// 直接判含糊、保持 unbound(见 task_progress 臂的 unboundSubagentCandidates() > 1)。
|
|
467
|
-
// 退役条件(登记在 compensations.ts T32):P1-2 到货 **且** 达标线 ≥ server 1.290 —— 即
|
|
468
|
-
// 「不再支持不发 typed 双键的引擎」那一天,才是删它的日子;只要还能连老引擎就得留着。
|
|
469
|
-
// 覆盖:diff 门 14 条 task_progress 帧里有 6 条不带 parentToolCallId(其中 2 条走 workflow
|
|
470
|
-
// lane 门、不到这里),真正压这条回落臂的是 4 条(FIFO 命中 / 含糊拒绝 / settle / 跨 turn)。
|
|
471
|
-
for (const [id, p] of pending) {
|
|
472
|
-
if (!boundToolCalls.has(id) && SUBAGENT_TOOL_NAMES.has(p.name)) {
|
|
473
|
-
target = id;
|
|
474
|
-
break;
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
}
|
|
478
|
-
if (target === undefined)
|
|
479
|
-
return undefined;
|
|
480
|
-
taskCardBinding.set(taskId, target);
|
|
481
|
-
boundToolCalls.add(target);
|
|
482
|
-
// #6 live vs inert:只有绑到**此刻开着**的卡才把行系进本 turn 的生命周期
|
|
483
|
-
// (FIFO 目标恒开着;显式目标可能已关 = inert)。
|
|
484
|
-
if (pending.has(target))
|
|
485
|
-
liveBoundPanelTasks.add(taskId);
|
|
486
|
-
// cli 的 `SEMA_BIND_PROBE` 文件探针 → 注入口(载荷逐字同形,见 ADAPTER_DIVERGENCES-5)。
|
|
487
|
-
ctx.probe?.('bind', `bind ${taskId} -> ${target} via=${parentToolCallId !== undefined ? 'explicit' : 'fifo'}`);
|
|
488
|
-
return target;
|
|
489
|
-
};
|
|
490
|
-
/** 对抗复审修(w0zwpa251 #1):≥2 张未绑的 subagent 形卡开着且 wire 没点名 ⇒ 任何猜都是错归因。 */
|
|
491
|
-
const unboundSubagentCandidates = () => {
|
|
492
|
-
let n = 0;
|
|
493
|
-
for (const [id, p] of pending) {
|
|
494
|
-
if (!boundToolCalls.has(id) && SUBAGENT_TOOL_NAMES.has(p.name))
|
|
495
|
-
n++;
|
|
496
|
-
}
|
|
497
|
-
return n;
|
|
498
|
-
};
|
|
499
|
-
/** SubagentStart(cli fireSubagentStartHook 的臂化):类型恒记,fire 一生一次。 */
|
|
500
|
-
const fireSubagentStart = function* (taskId, agentType) {
|
|
501
|
-
subagentTypeById.set(taskId, agentType); // Stop 的类型查找恒记(fire 与否无关)
|
|
502
|
-
if (self.firedSubagentStartHookTaskIds.has(taskId))
|
|
503
|
-
return;
|
|
504
|
-
self.firedSubagentStartHookTaskIds.add(taskId);
|
|
505
|
-
ctx.probe?.('hook', `start ${taskId} type=${agentType}`);
|
|
506
|
-
yield chrome({ kind: 'subagent_lifecycle', laneProof: laneOfTask(taskId), phase: 'start', taskId, agentType });
|
|
507
|
-
};
|
|
508
|
-
/**
|
|
509
|
-
* 结掉一张关闭中的卡所绑的面板行(`cardId`),或 turn 末/abort 结掉**本 turn 活绑**的全部行
|
|
510
|
-
* (`cardId===null` 的防御 sweep —— 子代从不发终态 tick)。
|
|
511
|
-
* 拍1 查看态:显式关卡臂携委派卡的 output(= 子代最终报告)到 end 事件上;sweep 无报告。
|
|
512
|
-
*/
|
|
513
|
-
const settlePanelTasks = function* (cardId, isError, report) {
|
|
514
|
-
// inline 群组行孪生(clay dogfood 2026-07-03):冻结该卡的 tick 终值,免得"N agents finished"
|
|
515
|
-
// 行把真实工具数/token 重置回 0。null-cardId 的 sweep 结掉每一条活着的 inline 条目。
|
|
516
|
-
if (cardId !== null) {
|
|
517
|
-
yield chrome({ kind: 'inline_task_stats', laneProof: { lane: 'subagent', parentToolCallId: cardId }, op: 'settle', cardId, isError });
|
|
518
|
-
}
|
|
519
|
-
else {
|
|
520
|
-
yield chrome({ kind: 'inline_task_stats', laneProof: MAIN, op: 'settle_all' });
|
|
521
|
-
}
|
|
522
|
-
for (const taskId of livePanelTasks) {
|
|
523
|
-
if (cardId !== null && taskCardBinding.get(taskId) !== cardId)
|
|
524
|
-
continue;
|
|
525
|
-
// #6 —— 防御 sweep 只结**本 turn 活绑**的行(流被截断的形)。session 常驻行
|
|
526
|
-
// (inert 绑定 / unbound = 跨 turn bg 子代)绝不在这里冻成假 completed:它的终态真相
|
|
527
|
-
// 由 task_notification / bg_notification 落(通知-settle 边)。
|
|
528
|
-
if (cardId === null && !liveBoundPanelTasks.has(taskId))
|
|
529
|
-
continue;
|
|
530
|
-
if (endedPanelTasks.has(taskId))
|
|
531
|
-
continue;
|
|
532
|
-
endedPanelTasks.add(taskId);
|
|
533
|
-
const lane = laneOfTask(taskId);
|
|
534
|
-
const agentType = subagentTypeById.get(taskId);
|
|
535
|
-
yield chrome({
|
|
536
|
-
kind: 'subagent_lifecycle',
|
|
537
|
-
laneProof: lane,
|
|
538
|
-
phase: 'stop',
|
|
539
|
-
taskId,
|
|
540
|
-
...(agentType !== undefined ? { agentType } : {}),
|
|
541
|
-
});
|
|
542
|
-
yield chrome({
|
|
543
|
-
kind: 'panel_task',
|
|
544
|
-
laneProof: lane,
|
|
545
|
-
event: {
|
|
546
|
-
kind: 'end',
|
|
547
|
-
taskId,
|
|
548
|
-
isError,
|
|
549
|
-
...(cardId !== null && report !== undefined ? { report } : {}),
|
|
550
|
-
},
|
|
551
|
-
});
|
|
552
|
-
}
|
|
553
|
-
};
|
|
554
|
-
// ══ B5 —— B/D/E 层:开卡 → `tool_result` 转录消息 ═══════════════════════════════════════
|
|
555
|
-
/**
|
|
556
|
-
* TodoWrite 划删线 diff 的 `oldTodos`。
|
|
557
|
-
* 🔴 **turn 级**(cli 同:`let lastLiveTodos` 声明在 generator 体内,不是 module 级 ——
|
|
558
|
-
* cli 的注释写着「module-level, per-process」但代码不是,以**代码**为准,差分守卫认代码)。
|
|
559
|
-
*/
|
|
560
|
-
let lastLiveTodos = [];
|
|
561
|
-
/**
|
|
562
|
-
* 一张开卡的收口(cli `buildToolResult` + 它内联的 #117a 桥)。产出顺序与 cli 的
|
|
563
|
-
* 「副作用先于 yield」一致:先 chrome(bg shell 建行),再 transcript(tool_result 卡)。
|
|
564
|
-
*/
|
|
565
|
-
const buildToolResult = function* (p, end) {
|
|
566
|
-
const isError = end?.isError ?? false;
|
|
567
|
-
// D 层 / T7:wire 带正文 ⇒ 真身投影;没带 ⇒ **诚实缺席**(§8-4,不再 mock 反造)。
|
|
568
|
-
const body = end && end.output !== undefined
|
|
569
|
-
? wireOutputToBody(p.name, end.output, isError, end.truncated === true, flattenWireOutput, end.structured)
|
|
570
|
-
: degradedToolResultBody(isError);
|
|
571
|
-
// ── E 层 ③:#117a 模型发起的后台 Bash 回执 → ctrl+B 面板行(判定在库、执行留宿主)。
|
|
572
|
-
if (p.name === 'Bash' && !isError && end?.output !== undefined) {
|
|
573
|
-
const reg = detectEngineBgShellReceipt(p.rawInput, flattenWireOutput(end.output));
|
|
574
|
-
if (reg) {
|
|
575
|
-
yield chrome({ kind: 'bgshell_register', laneProof: MAIN, taskId: reg.taskId, registration: reg });
|
|
576
|
-
}
|
|
577
|
-
}
|
|
578
|
-
// B 层:structured → 逐工具 typed toolUseResult(卡的 outputSchema safeParse 过才渲富卡)。
|
|
579
|
-
let rich = end?.structured !== undefined
|
|
580
|
-
? structuredToToolUseResult(end.structured, end.output !== undefined ? flattenWireOutput(end.output) : undefined)
|
|
581
|
-
: null;
|
|
582
|
-
// E 层 ④/⑤:两条按**工具名**的补位(引擎不发 type 判别位,B 层 switch 认领不了)。
|
|
583
|
-
if (rich === null && p.name === 'TodoWrite') {
|
|
584
|
-
const todo = todoWriteToolUseResult(p.rawInput, lastLiveTodos, isError);
|
|
585
|
-
if (todo !== null) {
|
|
586
|
-
rich = { toolUseResult: todo.toolUseResult, isError: todo.isError };
|
|
587
|
-
lastLiveTodos = todo.newTodos;
|
|
588
|
-
}
|
|
589
|
-
}
|
|
590
|
-
if (rich === null && !isError && p.name === 'ReportFindings') {
|
|
591
|
-
rich = reportFindingsToolUseResult(end?.structured, p.rawInput);
|
|
592
|
-
}
|
|
593
|
-
const eventId = end?.eventId ?? p.eventId;
|
|
594
|
-
const parentToolCallId = end?.parentToolCallId ?? p.parentToolCallId;
|
|
595
|
-
yield transcript({
|
|
596
|
-
type: 'user',
|
|
597
|
-
message: {
|
|
598
|
-
role: 'user',
|
|
599
|
-
content: [
|
|
600
|
-
{
|
|
601
|
-
type: 'tool_result',
|
|
602
|
-
tool_use_id: p.id,
|
|
603
|
-
content: body.content,
|
|
604
|
-
is_error: rich?.isError === true || body.isError,
|
|
605
|
-
},
|
|
606
|
-
],
|
|
607
|
-
},
|
|
608
|
-
// DIVERGENCE-2:cli 在缺 eventId 时铸随机 v4,本包一律**稳定键派生**
|
|
609
|
-
// (`wtc_<toolUseId>` —— 一张卡一个结果,天然唯一;同流重放 ⇒ 同 id 序列,[1653])。
|
|
610
|
-
uuid: eventId ?? deriveTranscriptId({ toolCallId: p.id }, ctx),
|
|
611
|
-
session_id: ctx.sessionId,
|
|
612
|
-
...(parentToolCallId !== undefined ? { parent_tool_use_id: parentToolCallId } : {}),
|
|
613
|
-
// CC 结果卡读 sidecar 上的 typed 输出(不是 block content)。
|
|
614
|
-
toolUseResult: rich !== null ? rich.toolUseResult : (body.toolUseResult ?? body.content),
|
|
615
|
-
// T7:wire 没带正文的诚实标记 —— 在场 = 这张卡没有真实执行结果(UI 显示决策留端)。
|
|
616
|
-
...(body.degraded !== undefined ? { _sema_degraded: body.degraded } : {}),
|
|
617
|
-
});
|
|
618
|
-
};
|
|
619
|
-
/** 关掉所有还开着的卡(流被截断 / 无 result 收尾;cli 两处同形,abort 路径**不做**)。 */
|
|
620
|
-
const sweepOpenCards = function* () {
|
|
621
|
-
for (const [id, p] of pending) {
|
|
622
|
-
pending.delete(id);
|
|
623
|
-
assistantResponseId = null; // S1
|
|
624
|
-
yield* buildToolResult(p);
|
|
625
|
-
}
|
|
626
|
-
};
|
|
627
|
-
/** 提交累积的思考块(cli takeThinking:committed 形 + 关活体块 + 收活动行)。 */
|
|
628
|
-
function* takeThinking() {
|
|
629
|
-
if (thinking.length === 0)
|
|
630
|
-
return;
|
|
631
|
-
const anchor = thinkingAnchor ?? {};
|
|
632
|
-
const msg = {
|
|
633
|
-
type: 'assistant',
|
|
634
|
-
message: { role: 'assistant', content: [{ type: 'thinking', thinking }] },
|
|
635
|
-
uuid: idOf(anchor, 'thinking'),
|
|
636
|
-
session_id: ctx.sessionId,
|
|
637
|
-
parent_tool_use_id: null,
|
|
638
|
-
};
|
|
639
|
-
thinking = '';
|
|
640
|
-
thinkingAnchor = null;
|
|
641
|
-
thinkingBlockOpen = false;
|
|
642
|
-
// P2d:活体 "∴ Thinking…" 行在 committed 块落地的**同一刻**收掉(先收行再上消息,cli 同序)
|
|
643
|
-
yield chrome({ kind: 'thinking_activity', laneProof: MAIN, active: false });
|
|
644
|
-
yield transcript(msg);
|
|
645
|
-
}
|
|
646
|
-
/** 提交累积的文本段(cli takeAnswerSegment:#27 段序铁律——工具卡前/turn 末各 flush 一次)。 */
|
|
647
|
-
function* takeAnswerSegment() {
|
|
648
|
-
if (answerSegment.length === 0)
|
|
649
|
-
return;
|
|
650
|
-
const anchor = segmentAnchor ?? {};
|
|
651
|
-
const msg = {
|
|
652
|
-
type: 'assistant',
|
|
653
|
-
message: { role: 'assistant', content: [{ type: 'text', text: answerSegment }] },
|
|
654
|
-
uuid: idOf(anchor, 'text'),
|
|
655
|
-
session_id: ctx.sessionId,
|
|
656
|
-
parent_tool_use_id: null,
|
|
657
|
-
};
|
|
658
|
-
answerSegment = '';
|
|
659
|
-
segmentAnchor = null;
|
|
660
|
-
emittedAssistantText = true;
|
|
661
|
-
textBlockOpen = false;
|
|
662
|
-
yield transcript(msg);
|
|
663
|
-
}
|
|
664
|
-
/** 合并后的活体增量下泄(cli drainLive:思考先于文本 = 持久臂序)。 */
|
|
665
|
-
function* drainLive() {
|
|
666
|
-
if (thinkingPending.length > 0) {
|
|
667
|
-
const opening = !thinkingBlockOpen;
|
|
668
|
-
thinkingBlockOpen = true;
|
|
669
|
-
yield chrome({
|
|
670
|
-
kind: 'stream_delta',
|
|
671
|
-
laneProof: MAIN,
|
|
672
|
-
channel: 'thinking',
|
|
673
|
-
text: thinkingPending,
|
|
674
|
-
estimatedTokens: estimateCjkTokens(thinkingPending),
|
|
675
|
-
opening,
|
|
676
|
-
});
|
|
677
|
-
thinkingPending = '';
|
|
678
|
-
}
|
|
679
|
-
if (textPending.length > 0) {
|
|
680
|
-
const opening = !textBlockOpen;
|
|
681
|
-
textBlockOpen = true;
|
|
682
|
-
// B3:cli 的 `SEMA_STREAM_PROBE` 文件探针换成 ctx.probe 口(体检 #5 同款用途:Ink 挂载后
|
|
683
|
-
// console 被吞,harness 只能靠这条证明「引擎是**逐 token 增量**到的」而不是整块一次性 dump)。
|
|
684
|
-
// 载荷格式与 cli 逐字同形,只把 Date.now() 换成 ctx.now()(注入时钟纪律)。
|
|
685
|
-
ctx.probe?.('stream', `${ctx.now()} text_flush len=${textPending.length}`);
|
|
686
|
-
yield chrome({
|
|
687
|
-
kind: 'stream_delta',
|
|
688
|
-
laneProof: MAIN,
|
|
689
|
-
channel: 'text',
|
|
690
|
-
text: textPending,
|
|
691
|
-
estimatedTokens: estimateCjkTokens(textPending),
|
|
692
|
-
opening,
|
|
693
|
-
});
|
|
694
|
-
textPending = '';
|
|
695
|
-
}
|
|
696
|
-
}
|
|
194
|
+
// ── id 派生(实现落 adapt/ids.ts;工厂形只为把 ctx 绑进去)────────────────────────────────
|
|
195
|
+
const idOf = makeIdOf(ctx);
|
|
196
|
+
// ── M1 流合并器(矩阵 §1.1 的 12 行状态整体落 adapt/textStream.ts)────────────────────────
|
|
197
|
+
const text = createTextStream(ctx, idOf);
|
|
198
|
+
// ── M2 工具卡台账(矩阵 §1.2 的 3 行状态整体落 adapt/toolCards.ts)────────────────────────
|
|
199
|
+
const cards = createToolCardLedger(ctx, idOf);
|
|
200
|
+
// ── M3 面板行台账(矩阵 §1.3 的 6 行状态 + 五个闭包整体落 adapt/panelTasks.ts)──────────
|
|
201
|
+
// 依赖只有两件:M2 的**只读**开卡视图、实例台账。反过来 M2 不依赖 M3(单向边)。
|
|
202
|
+
const panel = createPanelTaskLedger(ctx, cards, inst);
|
|
203
|
+
// ── M6a 响应度量 + M6b 重试覆盖层(两个小状态机;矩阵 §3.3:必须连驱动壳那一半一起搬)────
|
|
204
|
+
const flags = createTurnFlags(ctx, turnStartAt);
|
|
205
|
+
/** 臂表的注入面 —— 矩阵 §3.5 收窄成的六件,不做全量注入(全量注入 = 封闭性声明失去判别力)。 */
|
|
206
|
+
const deps = { ctx, idOf, text, cards, panel, flags, inst };
|
|
697
207
|
// turn 开场卫生(T38 三清,cli 逐条同序):清上一 turn 的 retry 覆盖层 + **上一轮的建议批** +
|
|
698
208
|
// 思考活动行,再发 request-start(T39)。三清缺一都是"上一轮的东西挂着不掉"。
|
|
699
209
|
yield chrome({ kind: 'retry_status', laneProof: MAIN, status: null });
|
|
@@ -705,22 +215,14 @@ class WireToCcAdapterImpl {
|
|
|
705
215
|
// 代价是失去 for-await 的隐式 cleanup ⇒ finally 里手动 return()(T58,少了它 = SSE 不拆)。
|
|
706
216
|
const iterator = frames[Symbol.asyncIterator]();
|
|
707
217
|
let iteratorDone = false;
|
|
708
|
-
// R1(clay 五报 07-31):retry 覆盖层的四个清除点全在 turn 边界,缺「重试成功→清」的转移 ——
|
|
709
|
-
// 引擎重试成功那一刻什么都不发(BrainStatusPhase 无恢复终态,候 core [2163]),所以恢复只能由
|
|
710
|
-
// 「主 lane 又有内容在流」这个事实来证明。挂旗后首个主 lane 内容帧即清,CC 同位 = 重试循环
|
|
711
|
-
// 退出即 onRetryStatus(null)(pretty220 finally 清)。
|
|
712
|
-
let retryOverlayActive = false;
|
|
713
218
|
try {
|
|
714
219
|
for (;;) {
|
|
715
220
|
const nextPromise = iterator.next();
|
|
716
221
|
let step;
|
|
717
222
|
while (step === undefined) {
|
|
718
|
-
const hasPendingContent = thinking.length > 0 ||
|
|
719
|
-
answerSegment.length > 0 ||
|
|
720
|
-
thinkingPending.length > 0 ||
|
|
721
|
-
textPending.length > 0;
|
|
722
223
|
// 缓冲全空 ⇒ 零开销路径;宿主没给定时器 ⇒ 竞速整条不启用(绝不 setTimeout 兜底)。
|
|
723
|
-
|
|
224
|
+
// 判据本体(四项 or)已随 12 行状态收进 M1 —— 这里读的是它的具名形。
|
|
225
|
+
if (!text.hasPendingContent() || ctx.setTimer === undefined) {
|
|
724
226
|
step = await nextPromise;
|
|
725
227
|
break;
|
|
726
228
|
}
|
|
@@ -741,9 +243,9 @@ class WireToCcAdapterImpl {
|
|
|
741
243
|
break;
|
|
742
244
|
// Idle ≥1.5s 且有已生成未提交的内容:现在就上屏(活体预览在前,再按 思考→文本 的
|
|
743
245
|
// committed 序 —— 与工具卡前的边界 flush 逐字同序,只是提前发生)。
|
|
744
|
-
yield* drainLive();
|
|
745
|
-
yield* takeThinking();
|
|
746
|
-
yield* takeAnswerSegment();
|
|
246
|
+
yield* text.drainLive();
|
|
247
|
+
yield* text.takeThinking();
|
|
248
|
+
yield* text.takeAnswerSegment();
|
|
747
249
|
// 缓冲已空 ⇒ 下一圈直接 plain-await。
|
|
748
250
|
}
|
|
749
251
|
}
|
|
@@ -760,655 +262,26 @@ class WireToCcAdapterImpl {
|
|
|
760
262
|
const m = rawFrame;
|
|
761
263
|
if (m === null || typeof m !== 'object')
|
|
762
264
|
continue;
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
m.type === 'tool_end_result' ||
|
|
767
|
-
m.type === 'turn_usage') &&
|
|
768
|
-
typeof m.parentToolCallId !== 'string' &&
|
|
769
|
-
// stream_event 的子流标记在 event 里(:1230 thinking 臂同款判位),帧顶层没有。
|
|
770
|
-
typeof m.event?.parentToolCallId !== 'string') {
|
|
771
|
-
// 恢复即清:主 lane 内容帧到达 = 引擎那次重试已成功;子流帧(parentToolCallId)不算证据
|
|
772
|
-
// (主 brain 仍可能在退避等待,#47 矩阵 #5 同族判别)。
|
|
773
|
-
retryOverlayActive = false;
|
|
774
|
-
yield chrome({ kind: 'retry_status', laneProof: MAIN, status: null });
|
|
775
|
-
}
|
|
776
|
-
if (m.type === 'stream_event' && !messageStartEmitted) {
|
|
777
|
-
messageStartEmitted = true;
|
|
778
|
-
yield chrome({
|
|
779
|
-
kind: 'response_metrics',
|
|
780
|
-
laneProof: MAIN,
|
|
781
|
-
phase: 'start',
|
|
782
|
-
ttftMs: ctx.now() - turnStartAt,
|
|
783
|
-
});
|
|
784
|
-
}
|
|
265
|
+
// ── 帧前置(M6a/M6b 两个小状态机的驱动壳那一半;两半同文件,见 turnFlags.ts 头注)──
|
|
266
|
+
// ① R1 重试覆盖层「恢复即清」;② 首个 stream_event 的 TTFT 起拍。次序与拆分前逐字一致。
|
|
267
|
+
yield* flags.onFrameBeforeDispatch(m);
|
|
785
268
|
// 持久臂之前先把活体增量泄干净(活体预览恒先于它 settle 成的 committed 消息)。
|
|
269
|
+
// 🔴 矩阵 §2.4(a)留钉:`result` 臂**自己不带 drainLive**,靠的就是这一拍 ——
|
|
270
|
+
// 臂表化时若把「非 stream_event 先泄活体」的责任也挪进臂里,result 会漏泄。
|
|
786
271
|
if (m.type !== 'stream_event')
|
|
787
|
-
yield* drainLive();
|
|
788
|
-
switch
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
const toolUseId = String(san.block.id);
|
|
801
|
-
const toolName = typeof san.block.name === 'string' ? san.block.name : 'unknown';
|
|
802
|
-
// P0-2 侧信道:core 展示名进本包台账(渲染块上已被 sanitize 摘掉)。cli 同位、同顺序
|
|
803
|
-
// (在任何 yield 之前),对 reject/cancel/error 三条早退路径同样生效。
|
|
804
|
-
recordEngineToolLabel(toolUseId, san.label);
|
|
805
|
-
// 引擎模式下壳工具 call() 从不执行 —— ctrl+t 面板台账只能靠这条侧信道(宿主义务)。
|
|
806
|
-
if (TASK_TOOL_NAMES.has(toolName)) {
|
|
807
|
-
yield chrome({
|
|
808
|
-
kind: 'task_ledger_sync',
|
|
809
|
-
laneProof: MAIN,
|
|
810
|
-
source: 'tool_use',
|
|
811
|
-
toolName,
|
|
812
|
-
toolUseId,
|
|
813
|
-
input: san.block.input,
|
|
814
|
-
});
|
|
815
|
-
}
|
|
816
|
-
// #27 段序:先把这张卡之前的散文提交,再提交它之前的思考(思考恒在卡上方)。
|
|
817
|
-
yield* takeAnswerSegment();
|
|
818
|
-
yield* takeThinking();
|
|
819
|
-
yield transcript({
|
|
820
|
-
type: 'assistant',
|
|
821
|
-
// S1:per-RESPONSE 铸 id —— CC 的 grouped-agent 渲染按 `${message.id}:${toolName}`
|
|
822
|
-
// 分组;id-less 帧会把整个 session 的 Agent 卡塌成一张。tool_use 帧才盖章。
|
|
823
|
-
message: { id: mintResponseId(m), role: 'assistant', content: renderBlocks },
|
|
824
|
-
uuid: idOf(m),
|
|
825
|
-
session_id: ctx.sessionId,
|
|
826
|
-
parent_tool_use_id: null,
|
|
827
|
-
});
|
|
828
|
-
if (decision === 'reject' || decision === 'cancel') {
|
|
829
|
-
yield transcript({
|
|
830
|
-
type: 'user',
|
|
831
|
-
message: {
|
|
832
|
-
role: 'user',
|
|
833
|
-
content: [
|
|
834
|
-
{
|
|
835
|
-
type: 'tool_result',
|
|
836
|
-
tool_use_id: san.block.id,
|
|
837
|
-
content: decision === 'reject' ? REJECT_MESSAGE : CANCEL_MESSAGE,
|
|
838
|
-
is_error: false,
|
|
839
|
-
},
|
|
840
|
-
],
|
|
841
|
-
},
|
|
842
|
-
uuid: idOf(m, decision),
|
|
843
|
-
session_id: ctx.sessionId,
|
|
844
|
-
});
|
|
845
|
-
assistantResponseId = null; // 一个结果关闭该 model response
|
|
846
|
-
break;
|
|
847
|
-
}
|
|
848
|
-
if (decision === 'error') {
|
|
849
|
-
yield transcript({
|
|
850
|
-
type: 'user',
|
|
851
|
-
message: {
|
|
852
|
-
role: 'user',
|
|
853
|
-
content: [
|
|
854
|
-
{
|
|
855
|
-
type: 'tool_result',
|
|
856
|
-
tool_use_id: san.block.id,
|
|
857
|
-
content: 'The tool failed to execute.',
|
|
858
|
-
is_error: true,
|
|
859
|
-
},
|
|
860
|
-
],
|
|
861
|
-
},
|
|
862
|
-
uuid: idOf(m, 'error'),
|
|
863
|
-
session_id: ctx.sessionId,
|
|
864
|
-
});
|
|
865
|
-
assistantResponseId = null;
|
|
866
|
-
break;
|
|
867
|
-
}
|
|
868
|
-
pending.set(toolUseId, {
|
|
869
|
-
id: toolUseId,
|
|
870
|
-
name: toolName,
|
|
871
|
-
rawInput: san.rawInput,
|
|
872
|
-
...(san.eventId !== undefined ? { eventId: san.eventId } : {}),
|
|
873
|
-
...(san.parentToolCallId !== undefined ? { parentToolCallId: san.parentToolCallId } : {}),
|
|
874
|
-
});
|
|
875
|
-
if (WORKFLOW_TOOL_NAMES.has(toolName))
|
|
876
|
-
this.seenWorkflowToolCallIds.add(toolUseId);
|
|
877
|
-
break;
|
|
878
|
-
}
|
|
879
|
-
const textBlock = blocks.find(b => b?.type === 'text');
|
|
880
|
-
if (textBlock && typeof textBlock.text === 'string' && textBlock.text.length > 0) {
|
|
881
|
-
answer += textBlock.text;
|
|
882
|
-
yield* takeThinking();
|
|
883
|
-
yield transcript({
|
|
884
|
-
type: 'assistant',
|
|
885
|
-
message: { role: 'assistant', content: [{ type: 'text', text: textBlock.text }] },
|
|
886
|
-
uuid: idOf(m),
|
|
887
|
-
session_id: ctx.sessionId,
|
|
888
|
-
parent_tool_use_id: null,
|
|
889
|
-
});
|
|
890
|
-
emittedAssistantText = true;
|
|
891
|
-
}
|
|
892
|
-
const thinkingBlock = blocks.find(b => b?.type === 'thinking');
|
|
893
|
-
if (thinkingBlock && typeof thinkingBlock.thinking === 'string') {
|
|
894
|
-
yield transcript({
|
|
895
|
-
type: 'assistant',
|
|
896
|
-
message: { role: 'assistant', content: [thinkingBlock] },
|
|
897
|
-
uuid: idOf(m, 'durable-thinking'),
|
|
898
|
-
session_id: ctx.sessionId,
|
|
899
|
-
parent_tool_use_id: null,
|
|
900
|
-
});
|
|
901
|
-
}
|
|
902
|
-
break;
|
|
903
|
-
}
|
|
904
|
-
// DIVERGENCE-1 —— 现役 wire 不产 user 帧;透传是给 desktop 重放存量转录留的门。
|
|
905
|
-
case 'user': {
|
|
906
|
-
if (m.message === undefined)
|
|
907
|
-
break;
|
|
908
|
-
yield transcript({ ...m, uuid: idOf(m), session_id: ctx.sessionId });
|
|
909
|
-
break;
|
|
910
|
-
}
|
|
911
|
-
case 'tool_end_result': {
|
|
912
|
-
// B4 起本臂做三件:① P0-2 label 补位(只有 tool_end 带 label 的场景;台账自身幂等不覆盖)
|
|
913
|
-
// ② MF-10 **关卡 settle**(子代从不发终态 tick,父卡的 tool_end 就是完成信号;卡的 output
|
|
914
|
-
// 文本 = 子代最终报告,串到行 end 事件上)③ 卡台账出栈 + response-id 复位(S1)。
|
|
915
|
-
// 🆕 B5:④ **B/D/E 层** —— 铸 `tool_result` 卡本体 + 三个侧产物
|
|
916
|
-
// (Remember→memory_saved / task 族 structured 台账 / #117a bg Bash 桥)。
|
|
917
|
-
const callId = typeof m.toolCallId === 'string' ? m.toolCallId : undefined;
|
|
918
|
-
recordEngineToolLabel(callId, m.label);
|
|
919
|
-
if (callId !== undefined) {
|
|
920
|
-
yield* settlePanelTasks(callId, m.isError === true, m.output !== undefined ? flattenWireOutput(m.output) : undefined);
|
|
921
|
-
}
|
|
922
|
-
const p = callId !== undefined ? pending.get(callId) : undefined;
|
|
923
|
-
// 无对应开卡的 tool_end(乱序 / 重复)直接丢(cli 同)。
|
|
924
|
-
if (p === undefined || callId === undefined)
|
|
925
|
-
break;
|
|
926
|
-
pending.delete(callId);
|
|
927
|
-
// S1:结果关闭该 model response —— **yield 之前**复位(生成器会在 yield 上挂起)。
|
|
928
|
-
assistantResponseId = null;
|
|
929
|
-
const structured = m.structured;
|
|
930
|
-
yield* buildToolResult(p, {
|
|
931
|
-
output: m.output,
|
|
932
|
-
structured,
|
|
933
|
-
truncated: m.truncated === true,
|
|
934
|
-
isError: m.isError === true,
|
|
935
|
-
eventId: typeof m.eventId === 'string' ? m.eventId : undefined,
|
|
936
|
-
parentToolCallId: typeof m.parentToolCallId === 'string' ? m.parentToolCallId : undefined,
|
|
937
|
-
});
|
|
938
|
-
const s = structured !== undefined && structured !== null && typeof structured === 'object'
|
|
939
|
-
? structured
|
|
940
|
-
: undefined;
|
|
941
|
-
// ── E 层 ①:CC198 gap #2 —— 引擎 Remember 工具 → CC198 automemory「Saved N memories」卡
|
|
942
|
-
// (system{subtype:'memory_saved'})。引擎不发写入路径,note 本身就是那一条 writtenPaths。
|
|
943
|
-
// 上面的 tool_result 照旧走泛化兜底卡(双覆盖);structuredToToolUseResult 的白名单不动
|
|
944
|
-
// (Remember 从不进那个 switch)。core 1.217 形 {type:'memory-saved', ok, note, …}。
|
|
945
|
-
if (p.name === 'Remember' && m.isError !== true) {
|
|
946
|
-
if (s && s.ok === true && typeof s.note === 'string' && s.note.length > 0) {
|
|
947
|
-
yield transcript({
|
|
948
|
-
type: 'system',
|
|
949
|
-
subtype: 'memory_saved',
|
|
950
|
-
writtenPaths: [s.note],
|
|
951
|
-
// cli 用 `new Date().toISOString()` + randomUUID();本包一律走 ctx(可重放)。
|
|
952
|
-
// 见 DIVERGENCE-7。
|
|
953
|
-
timestamp: new Date(ctx.now()).toISOString(),
|
|
954
|
-
uuid: idOf(m, 'memory-saved'),
|
|
955
|
-
isMeta: false,
|
|
956
|
-
});
|
|
957
|
-
}
|
|
958
|
-
}
|
|
959
|
-
// ── E 层 ②:core 1.217 task 族 structured —— 权威全量态精确同步 ctrl+t 面板(引擎真实 id)。
|
|
960
|
-
// 落盘是宿主职责(T44 拆缝:判定+归一进库、落盘留宿主)。
|
|
961
|
-
if (s && (s.type === 'task' || s.type === 'task-list')) {
|
|
962
|
-
yield chrome({
|
|
963
|
-
kind: 'task_ledger_sync',
|
|
964
|
-
laneProof: MAIN,
|
|
965
|
-
source: 'structured',
|
|
966
|
-
structuredType: s.type,
|
|
967
|
-
structured: s,
|
|
968
|
-
});
|
|
969
|
-
}
|
|
970
|
-
break;
|
|
971
|
-
}
|
|
972
|
-
case 'task_progress': {
|
|
973
|
-
// ── B4 A 层最后一臂(MF-10)—— 引擎侧子代的累计 usage tick。
|
|
974
|
-
// 只带 rollup 数字 = CC `AgentProgress` 同形 ⇒ 忠实的 CC-187 面是 footer 面板行
|
|
975
|
-
// (CC 自己对进程外 agent 的先例:面板行 + task-notification,从不合成 inline 消息轨)。
|
|
976
|
-
// **不产任何 transcript 消息**:全是 chrome。
|
|
977
|
-
const taskId = typeof m.taskId === 'string' ? m.taskId : undefined;
|
|
978
|
-
if (taskId === undefined)
|
|
979
|
-
break;
|
|
980
|
-
const explicitParent = typeof m.parentToolCallId === 'string' ? m.parentToolCallId : undefined;
|
|
981
|
-
// ── W1 段2([1617]②,frame-lane-matrix #2/#8)workflow lane 门:workflow 子代的 tick 属
|
|
982
|
-
// fleet lane 自描述行(footer 只渲 workflow 聚合行),**绝不**进前台 subagent lane
|
|
983
|
-
// (不登 alias、不 bind、不 livePanelTasks、不发 panel tick —— 修前每个 workflow agent
|
|
984
|
-
// 都在 footer 铸一条幽灵 local_agent 行,还能骑错 run_workflow 卡的 inline stats)。
|
|
985
|
-
// 判据三级:① 帧带 workflowRunId(一手证据)② wa+16hex 形 id(core BCE 合成 id)
|
|
986
|
-
// ③ 显式父指向本会话见过的 Workflow 工具卡(实例台账,覆盖 uuid 形子代 id + 跨 turn 迟到 tick)。
|
|
987
|
-
const workflowRunId = typeof m.workflowRunId === 'string' ? m.workflowRunId : undefined;
|
|
988
|
-
if (workflowRunId !== undefined ||
|
|
989
|
-
isWorkflowAgentTaskId(taskId) ||
|
|
990
|
-
(explicitParent !== undefined && this.seenWorkflowToolCallIds.has(explicitParent))) {
|
|
991
|
-
// 判据④ 回植([1641] workflow 启动闪 subagent 行案):本门是「这条 taskId 属 workflow」的
|
|
992
|
-
// **权威判定**。同一 taskId 在 fleet 面还有一条 run 发布器双生行(`${runId} ${taskId}`)
|
|
993
|
-
// 且那行不带 workflowRunId —— 登进 belt 台账,fleet 行按尾段查它,双生行就再进不了
|
|
994
|
-
// footer task 树。🔴 写者在本包、读者(isKnownWorkflowAgentTaskId)也在本包 = R4 纪律,
|
|
995
|
-
// 做成 chrome 臂反而会让「宿主没接线 ⇒ Set 恒空 ⇒ 判据④ 静默失效」复活。
|
|
996
|
-
recordWorkflowAgentTaskId(taskId);
|
|
997
|
-
ctx.probe?.('tick', JSON.stringify({
|
|
998
|
-
t: ctx.now(),
|
|
999
|
-
taskId,
|
|
1000
|
-
lane: 'workflow',
|
|
1001
|
-
via: workflowRunId !== undefined
|
|
1002
|
-
? 'workflowRunId'
|
|
1003
|
-
: isWorkflowAgentTaskId(taskId)
|
|
1004
|
-
? 'wa-id'
|
|
1005
|
-
: 'workflow-card-parent',
|
|
1006
|
-
}));
|
|
1007
|
-
break;
|
|
1008
|
-
}
|
|
1009
|
-
// C1 alias:content 帧只带 parentToolCallId(EventIdentity 上没有 taskId),这条 tick
|
|
1010
|
-
// **两者都带** —— 是 store 学到「停放的内容属哪条引擎行」的唯一位置(查看态按引擎 task id 查)。
|
|
1011
|
-
if (explicitParent !== undefined)
|
|
1012
|
-
registerSubagentAlias(explicitParent, taskId);
|
|
1013
|
-
// 复审修 #1:含糊回落(无显式父 + >1 张未绑候选)⇒ 保持 unbound。
|
|
1014
|
-
const cardId = explicitParent === undefined && !taskCardBinding.has(taskId) && unboundSubagentCandidates() > 1
|
|
1015
|
-
? undefined
|
|
1016
|
-
: bindTaskCard(taskId, explicitParent);
|
|
1017
|
-
const usage = (m.usage ?? {});
|
|
1018
|
-
ctx.probe?.('tick', JSON.stringify({
|
|
1019
|
-
t: ctx.now(),
|
|
1020
|
-
taskId,
|
|
1021
|
-
lane: 'foreground',
|
|
1022
|
-
parent: explicitParent ?? null,
|
|
1023
|
-
cardId: cardId ?? null,
|
|
1024
|
-
currentAction: m.currentAction ?? null,
|
|
1025
|
-
toolUses: usage.toolUses ?? null,
|
|
1026
|
-
}));
|
|
1027
|
-
// core 1.262.0 [558]B / service 1.154.0 [579] —— 子代的活体"我在干什么"一行
|
|
1028
|
-
// ("Bash npm test"),service 已 redact。PROGRESS 语义(喂状态/摘要显示槽),不是诊断。
|
|
1029
|
-
// 老引擎缺席 ⇒ 两个消费面维持今天的显示。
|
|
1030
|
-
const currentAction = typeof m.currentAction === 'string' && m.currentAction.length > 0 ? m.currentAction : undefined;
|
|
1031
|
-
// inline 群组行孪生:**绑定了**的 tick 同时喂转录里那条行的统计(按委派卡 id 键)。
|
|
1032
|
-
if (cardId !== undefined) {
|
|
1033
|
-
yield chrome({
|
|
1034
|
-
kind: 'inline_task_stats',
|
|
1035
|
-
laneProof: { lane: 'subagent', parentToolCallId: cardId },
|
|
1036
|
-
op: 'tick',
|
|
1037
|
-
cardId,
|
|
1038
|
-
stats: {
|
|
1039
|
-
toolUses: typeof usage.toolUses === 'number' ? usage.toolUses : 0,
|
|
1040
|
-
totalTokens: typeof usage.totalTokens === 'number' ? usage.totalTokens : 0,
|
|
1041
|
-
durationMs: typeof usage.durationMs === 'number' ? usage.durationMs : 0,
|
|
1042
|
-
...(currentAction !== undefined ? { currentAction } : {}),
|
|
1043
|
-
},
|
|
1044
|
-
});
|
|
1045
|
-
}
|
|
1046
|
-
const card = cardId !== undefined ? pending.get(cardId) : undefined;
|
|
1047
|
-
// T50 双形读:`rawInput` 是**改名前**捕获的,真 wire 上引擎子代的 input 是
|
|
1048
|
-
// {agent, task, taskName} —— 两形都读(CC 名优先)。
|
|
1049
|
-
const input = (card?.rawInput ?? {});
|
|
1050
|
-
const description = typeof input.description === 'string'
|
|
1051
|
-
? input.description
|
|
1052
|
-
: typeof input.taskName === 'string' && input.taskName.trim().length > 0
|
|
1053
|
-
? input.taskName
|
|
1054
|
-
: typeof input.task === 'string'
|
|
1055
|
-
? shortTaskLabel(input.task)
|
|
1056
|
-
: undefined;
|
|
1057
|
-
const prompt = typeof input.prompt === 'string'
|
|
1058
|
-
? input.prompt
|
|
1059
|
-
: typeof input.task === 'string'
|
|
1060
|
-
? input.task
|
|
1061
|
-
: undefined;
|
|
1062
|
-
if (!livePanelTasks.has(taskId)) {
|
|
1063
|
-
// 这条引擎子代在壳侧**可观测的起点** = 本 turn 的首 tick。
|
|
1064
|
-
const agentType = typeof m.name === 'string' && m.name.length > 0 ? m.name : 'subagent';
|
|
1065
|
-
yield* fireSubagentStart(taskId, agentType);
|
|
1066
|
-
}
|
|
1067
|
-
// #6 session 常驻台账:live-bound(卡本 turn 开着)= 本 turn 生命周期,turn 末可 sweep;
|
|
1068
|
-
// inert/unbound(跨 turn bg 子代形)= 常驻,两处 sweep 都放行,终态只认通知帧。
|
|
1069
|
-
if (liveBoundPanelTasks.has(taskId))
|
|
1070
|
-
clearEnginePanelTaskResident(taskId);
|
|
1071
|
-
else
|
|
1072
|
-
markEnginePanelTaskResident(taskId);
|
|
1073
|
-
livePanelTasks.add(taskId);
|
|
1074
|
-
yield chrome({
|
|
1075
|
-
kind: 'panel_task',
|
|
1076
|
-
laneProof: laneOfTask(taskId),
|
|
1077
|
-
event: {
|
|
1078
|
-
kind: 'tick',
|
|
1079
|
-
taskId,
|
|
1080
|
-
...(typeof m.name === 'string' && m.name.length > 0 ? { name: m.name } : {}),
|
|
1081
|
-
...(description !== undefined ? { description } : {}),
|
|
1082
|
-
...(prompt !== undefined ? { prompt } : {}),
|
|
1083
|
-
...(currentAction !== undefined ? { currentAction } : {}),
|
|
1084
|
-
// 🔴 这里的 `: 0` 与 `fleetAgentPanelProjection` 那句「绝不在本层 `?? 0`」**不矛盾**,
|
|
1085
|
-
// 因为不是同一条 lane 的同一种供给形(两层的分层实情写在 `engineAgentPanelStore.ts`
|
|
1086
|
-
// 的 `PANEL_TOOLUSES_LANE_POLICY` 上,两条 lane 的可选性由编译钉锁住):
|
|
1087
|
-
// · **tick lane(本处)**:`usage` 是 core `task_progress` 臂的**必填**字段
|
|
1088
|
-
// (core 2.12.0 `dist/core/types.d.ts:606-610` 无 `?`),两处发帧点
|
|
1089
|
-
// (`runner/runtask.js:1080` 每轮边界 / `:2925` 终态 settle 拍)**无条件**构造
|
|
1090
|
-
// `{ totalTokens, toolUses: stats.toolCalls, durationMs }`,server 白名单整键透传
|
|
1091
|
-
// (`trace/project.js:118`)⇒ 缺席在这条 lane 上不是「不知道」,是**没有这种帧**。
|
|
1092
|
-
// 值本身是**累计**计数(`stats.toolCalls`,从 0 起算),所以真值域含真 0。
|
|
1093
|
-
// · **fleet-row lane(那处)**:同名键是 server 1.278.0 才**新加**的可选位,老引擎
|
|
1094
|
-
// 真的不发 ⇒ 缺席就是「不知道」,兜 0 会把它冒充成「跑了 0 个工具」,而且会把
|
|
1095
|
-
// 台账里已知的累计值**擦回 0**。
|
|
1096
|
-
// ⚠️ 别把两层「统一」:把本处改成传播缺席要先把事件形改成可选,而那正是本 lane
|
|
1097
|
-
// 刻意不做的事(`engineAgentPanelStore.ts:54-57` 记的就是这条 —— 复用同一个可选形
|
|
1098
|
-
// 会让 fleet-row lane 每秒把前台子代由真 tick 写进去的进度数字清一次)。
|
|
1099
|
-
toolUses: typeof usage.toolUses === 'number' ? usage.toolUses : 0,
|
|
1100
|
-
totalTokens: typeof usage.totalTokens === 'number' ? usage.totalTokens : 0,
|
|
1101
|
-
},
|
|
1102
|
-
});
|
|
1103
|
-
break;
|
|
1104
|
-
}
|
|
1105
|
-
case 'stream_event': {
|
|
1106
|
-
const ev = m.event;
|
|
1107
|
-
if (ev && ev.type === 'text_delta' && typeof ev.delta === 'string') {
|
|
1108
|
-
// MOD-1 思考→回答边界:回答一起头就把思考块 committed 上屏(它推理的内容之前)。
|
|
1109
|
-
if (thinking.length > 0 || thinkingPending.length > 0) {
|
|
1110
|
-
yield* drainLive();
|
|
1111
|
-
yield* takeThinking();
|
|
1112
|
-
}
|
|
1113
|
-
if (answerSegment.length === 0)
|
|
1114
|
-
segmentAnchor = m;
|
|
1115
|
-
answer += ev.delta;
|
|
1116
|
-
answerSegment += ev.delta;
|
|
1117
|
-
textPending += ev.delta;
|
|
1118
|
-
}
|
|
1119
|
-
else if (ev && ev.type === 'thinking_delta' && typeof ev.delta === 'string') {
|
|
1120
|
-
// 子流(parentToolCallId)的推理绝不喂 leader 的计数与预览(#9 的壳侧半场)。
|
|
1121
|
-
if (ev.parentToolCallId !== undefined)
|
|
1122
|
-
break;
|
|
1123
|
-
if (thinking.length === 0) {
|
|
1124
|
-
thinkingAnchor = m;
|
|
1125
|
-
// P2d:elapsed 锚在**首条** leader 推理增量(token 累计由 stream_delta.estimatedTokens
|
|
1126
|
-
// 承载,宿主自加;>30s 无增量的 stall 提示同样由宿主按增量时间戳判)。
|
|
1127
|
-
yield chrome({ kind: 'thinking_activity', laneProof: MAIN, active: true });
|
|
1128
|
-
}
|
|
1129
|
-
thinking += ev.delta;
|
|
1130
|
-
thinkingPending += ev.delta;
|
|
1131
|
-
}
|
|
1132
|
-
break;
|
|
1133
|
-
}
|
|
1134
|
-
case 'turn_usage': {
|
|
1135
|
-
const ot = typeof m.outputTokens === 'number' ? m.outputTokens : undefined;
|
|
1136
|
-
if (ot != null) {
|
|
1137
|
-
yield* drainLive();
|
|
1138
|
-
yield chrome({ kind: 'response_metrics', laneProof: MAIN, phase: 'end', outputTokens: ot });
|
|
1139
|
-
endEmitted = true;
|
|
1140
|
-
messageStartEmitted = false; // 下一 model round 重新压基线
|
|
1141
|
-
}
|
|
1142
|
-
break;
|
|
1143
|
-
}
|
|
1144
|
-
case 'result': {
|
|
1145
|
-
yield chrome({ kind: 'retry_status', laneProof: MAIN, status: null });
|
|
1146
|
-
// MF-10 防御 sweep(sweep 点 1/3):结掉那些卡从没关过的行(流被截断)。
|
|
1147
|
-
// cli 同位:setRetryStatus(null) 之后、开卡兜底关闭之前。
|
|
1148
|
-
yield* settlePanelTasks(null, false);
|
|
1149
|
-
// B5:开卡兜底关闭(cli 同位 —— 在终答文本**之前**,卡才不会挂着空白)。
|
|
1150
|
-
yield* sweepOpenCards();
|
|
1151
|
-
yield* takeThinking();
|
|
1152
|
-
if (!endEmitted) {
|
|
1153
|
-
const u = m.usage;
|
|
1154
|
-
if (typeof u?.outputTokens === 'number') {
|
|
1155
|
-
yield chrome({
|
|
1156
|
-
kind: 'response_metrics',
|
|
1157
|
-
laneProof: MAIN,
|
|
1158
|
-
phase: 'end',
|
|
1159
|
-
outputTokens: u.outputTokens,
|
|
1160
|
-
});
|
|
1161
|
-
endEmitted = true;
|
|
1162
|
-
}
|
|
1163
|
-
}
|
|
1164
|
-
yield* takeAnswerSegment();
|
|
1165
|
-
if (!emittedAssistantText) {
|
|
1166
|
-
const text = typeof m.result === 'string' ? m.result : '';
|
|
1167
|
-
if (text.length > 0) {
|
|
1168
|
-
yield transcript({
|
|
1169
|
-
type: 'assistant',
|
|
1170
|
-
message: { role: 'assistant', content: [{ type: 'text', text }] },
|
|
1171
|
-
uuid: idOf(m, 'terminal-text'),
|
|
1172
|
-
session_id: ctx.sessionId,
|
|
1173
|
-
parent_tool_use_id: null,
|
|
1174
|
-
});
|
|
1175
|
-
emittedAssistantText = true;
|
|
1176
|
-
}
|
|
1177
|
-
}
|
|
1178
|
-
break;
|
|
1179
|
-
}
|
|
1180
|
-
case 'system': {
|
|
1181
|
-
yield transcript(m);
|
|
1182
|
-
// core 1.219 attachWorkingFiles:compact_boundary 可携 attachedFiles → CC 的
|
|
1183
|
-
// "Referenced file {path}" 行(post-compact attachment,零新 UI)。
|
|
1184
|
-
const attachedFiles = m.attachedFiles;
|
|
1185
|
-
if (Array.isArray(attachedFiles)) {
|
|
1186
|
-
let i = 0;
|
|
1187
|
-
for (const f of attachedFiles) {
|
|
1188
|
-
if (typeof f?.path !== 'string' || f.path.length === 0)
|
|
1189
|
-
continue;
|
|
1190
|
-
yield chrome({
|
|
1191
|
-
kind: 'attachment',
|
|
1192
|
-
laneProof: MAIN,
|
|
1193
|
-
id: idOf(m, `attached-file-${i++}`),
|
|
1194
|
-
attachment: {
|
|
1195
|
-
type: 'compact_file_reference',
|
|
1196
|
-
filename: f.path,
|
|
1197
|
-
displayPath: f.path,
|
|
1198
|
-
},
|
|
1199
|
-
});
|
|
1200
|
-
}
|
|
1201
|
-
}
|
|
1202
|
-
break;
|
|
1203
|
-
}
|
|
1204
|
-
case 'task_notification': {
|
|
1205
|
-
const n = (m.notification ?? {});
|
|
1206
|
-
const fields = normalizeTaskNotification(n);
|
|
1207
|
-
if (fields === null)
|
|
1208
|
-
break; // 空/缺 task_id:RB-75 同族,丢弃
|
|
1209
|
-
const taskId = fields.taskId;
|
|
1210
|
-
const rawStatus = fields.status;
|
|
1211
|
-
const terminal = rawStatus === 'completed' || rawStatus === 'failed' || rawStatus === 'killed';
|
|
1212
|
-
// #117a settle 半场 —— **故意在渲染去重之前**:settle 幂等,重放帧仍须 settle 一条
|
|
1213
|
-
// 首投之后才注册的行。#47 矩阵 #7:只有 bash 行(或老引擎无 task_type)能被 settle。
|
|
1214
|
-
const settleAllowed = n.task_type === undefined || n.task_type === 'background_bash';
|
|
1215
|
-
if (settleAllowed && terminal) {
|
|
1216
|
-
yield chrome({
|
|
1217
|
-
kind: 'bgshell_settle',
|
|
1218
|
-
laneProof: MAIN,
|
|
1219
|
-
taskId,
|
|
1220
|
-
status: rawStatus,
|
|
1221
|
-
...(typeof n.output_file === 'string' && n.output_file.length > 0
|
|
1222
|
-
? { outputPath: n.output_file }
|
|
1223
|
-
: {}),
|
|
1224
|
-
});
|
|
1225
|
-
}
|
|
1226
|
-
const dedupKey = taskNotificationDedupKey({
|
|
1227
|
-
taskId,
|
|
1228
|
-
status: rawStatus,
|
|
1229
|
-
...(typeof n.seq === 'number' || typeof n.seq === 'string' ? { seq: n.seq } : {}),
|
|
1230
|
-
...(typeof n.task_type === 'string' ? { taskType: n.task_type } : {}),
|
|
1231
|
-
});
|
|
1232
|
-
if (this.renderedNotifications.has(dedupKey))
|
|
1233
|
-
break;
|
|
1234
|
-
this.renderedNotifications.add(dedupKey);
|
|
1235
|
-
// B1 + #6 —— 必须在 cardEnqueued 早退**之前**:probe 合成先入队的场景恰是双投主形。
|
|
1236
|
-
if (n.task_type !== 'external' && terminal) {
|
|
1237
|
-
yield chrome({
|
|
1238
|
-
kind: 'notification_terminal',
|
|
1239
|
-
laneProof: MAIN,
|
|
1240
|
-
taskId,
|
|
1241
|
-
status: rawStatus,
|
|
1242
|
-
...(typeof n.task_type === 'string' ? { taskType: n.task_type } : {}),
|
|
1243
|
-
});
|
|
1244
|
-
// B1 —— 这帧 = 引擎 server-side steer-inject 的回声,模型已在引擎侧收到一遍;把还躺在
|
|
1245
|
-
// 队列里的同 run Path B 条目(workflow_complete/probe/bg feeder)在 turn 收口 drain 前
|
|
1246
|
-
// 丢弃,免得 idle auto-submit 再喂模型一遍(跨进程双投的客户端半场)。
|
|
1247
|
-
// 🔴 B4 起由**适配器自持**(B2 已把队列端口搬进本包):此前挂在 `notification_terminal`
|
|
1248
|
-
// 的宿主义务①,是「说得通但没人做」的典型 —— 宿主没接线就静默双投。
|
|
1249
|
-
dropQueuedNotificationsForRun(taskId);
|
|
1250
|
-
if (n.task_type !== 'background_bash') {
|
|
1251
|
-
// #6 通知-settle 边(推送半场):session 常驻的 bg 子代行不再被 turn sweep 假结,
|
|
1252
|
-
// 真终态在此落行(failed/killed ⇒ isError 真归因)。
|
|
1253
|
-
clearEnginePanelTaskResident(taskId);
|
|
1254
|
-
yield chrome({
|
|
1255
|
-
kind: 'panel_task',
|
|
1256
|
-
laneProof: MAIN,
|
|
1257
|
-
event: { kind: 'end', taskId, isError: rawStatus !== 'completed' },
|
|
1258
|
-
});
|
|
1259
|
-
if (!endedPanelTasks.has(taskId)) {
|
|
1260
|
-
endedPanelTasks.add(taskId);
|
|
1261
|
-
// SubagentStop 只对**真 fire 过 Start** 的子代(workflow runId / 外来 id 不配对乱 fire)。
|
|
1262
|
-
// cli 同门 `firedSubagentStartHookTaskIds.has(taskId)` —— B4 前本臂无条件发 stop,
|
|
1263
|
-
// 宿主若照 guard 注释自建台账才不会乱 fire;台账在库里就该库来判。
|
|
1264
|
-
if (this.firedSubagentStartHookTaskIds.has(taskId)) {
|
|
1265
|
-
yield chrome({
|
|
1266
|
-
kind: 'subagent_lifecycle',
|
|
1267
|
-
laneProof: MAIN,
|
|
1268
|
-
phase: 'stop',
|
|
1269
|
-
taskId,
|
|
1270
|
-
guard: 'if-started',
|
|
1271
|
-
});
|
|
1272
|
-
}
|
|
1273
|
-
}
|
|
1274
|
-
}
|
|
1275
|
-
}
|
|
1276
|
-
// 双通道反向去重:probe 合成的完成卡已入队 ⇒ 后到的推送帧只做上面的记账,不渲第二条。
|
|
1277
|
-
// 🔴 B5 ①:读的是 **module 台账**(cli `isWorkflowCompletionCardEnqueued` 逐字)——
|
|
1278
|
-
// 记账方(两个 enqueue + idle watcher)与清账方(上面的 drop)都在那儿。
|
|
1279
|
-
if (n.task_type !== 'external' && isWorkflowCompletionCardEnqueued(taskId))
|
|
1280
|
-
break;
|
|
1281
|
-
// 跨通道记账(cli markEngineWorkflowNotified 同位、同条件:非 external,不看终态):
|
|
1282
|
-
// 这帧到达 = 引擎已 server-side steer-inject 过,补发通道(workflow_complete)不得再注入。
|
|
1283
|
-
// external 豁免([769] 对抗复审收紧:external 从不经补发通道,mark 只会缴械 probe 兜底)。
|
|
1284
|
-
if (n.task_type !== 'external')
|
|
1285
|
-
markEngineWorkflowNotified(taskId);
|
|
1286
|
-
yield transcript({
|
|
1287
|
-
type: 'user',
|
|
1288
|
-
message: { role: 'user', content: [{ type: 'text', text: renderTaskNotificationXml(fields) }] },
|
|
1289
|
-
uuid: idOf(m),
|
|
1290
|
-
session_id: ctx.sessionId,
|
|
1291
|
-
});
|
|
1292
|
-
break;
|
|
1293
|
-
}
|
|
1294
|
-
case 'workflow_complete': {
|
|
1295
|
-
const runId = typeof m.runId === 'string' ? m.runId : undefined;
|
|
1296
|
-
if (runId === undefined)
|
|
1297
|
-
break;
|
|
1298
|
-
// 跨通道去重(cli enqueueEngineWorkflowNotification 首行):该 run 已由推送帧记账过
|
|
1299
|
-
// (引擎 server-side 注入过)⇒ Path B 绝不再喂模型一遍。
|
|
1300
|
-
// 🔴 B5 ①:门与记账都在 module 台账 —— cli 里这一整段就是 enqueueEngineWorkflowNotification
|
|
1301
|
-
// 的前三行,probe/idle watcher 合成的那两条链写的也是它。
|
|
1302
|
-
// 🔴 B6 契约钉死(见 seam.ts 本臂注释「门归包、投递归宿主」):门 + 下面两条记号
|
|
1303
|
-
// (markEngineWorkflowNotified / noteWorkflowCompletionCardEnqueued)**都在库内**,
|
|
1304
|
-
// 宿主拿到臂只管裸入队。宿主若借自带同一道门的老函数投递 ⇒ 恒早退、一条都进不了队列。
|
|
1305
|
-
if (isEngineWorkflowNotified(runId))
|
|
1306
|
-
break;
|
|
1307
|
-
markEngineWorkflowNotified(runId);
|
|
1308
|
-
const wfStatus = m.status === 'failed' ? 'failed' : 'completed';
|
|
1309
|
-
const wfSummary = typeof m.summary === 'string' ? m.summary : '';
|
|
1310
|
-
// 🔴 B6 时序修(先红后绿,pure 门「早退 ⇒ cardEnqueued 仍落」):这一行原本在 `yield`
|
|
1311
|
-
// **之后** —— 生成器在 yield 处挂起,后面的语句只有消费者**再要下一个值**时才跑;
|
|
1312
|
-
// 消费者早退(turn 中止 / `it.return()`)时它**永不执行**。而同臂的
|
|
1313
|
-
// `markEngineWorkflowNotified` 在 yield 之前恒执行 ⇒ 两条记号一条恒落一条条件落,
|
|
1314
|
-
// 且方向正好是坏的那个:notified 落了 ⇒ idle watcher 不补发;cardEnqueued 没落 ⇒
|
|
1315
|
-
// 推送帧到达时判不出卡的所有权 ⇒ **渲第二张完成卡**(B1c 反钉②)。
|
|
1316
|
-
// 现存驱动全是走完整流的 `for await`,所以这条路径此前从未被断言过([1521]F1 同形)。
|
|
1317
|
-
// 两条记号必须同在 yield 之前落 —— 「臂已发出」这件事在 yield 那一刻就已成立。
|
|
1318
|
-
noteWorkflowCompletionCardEnqueued(runId);
|
|
1319
|
-
yield chrome({
|
|
1320
|
-
kind: 'workflow_notification_enqueue',
|
|
1321
|
-
laneProof: MAIN,
|
|
1322
|
-
runId,
|
|
1323
|
-
status: wfStatus,
|
|
1324
|
-
summary: wfSummary,
|
|
1325
|
-
// 模型面正文照铸(宿主直接入队即可,不必各自拼 XML;与 core 的 tag/段序同形)
|
|
1326
|
-
message: renderTaskNotificationXml({ taskId: runId, status: wfStatus, summary: wfSummary }),
|
|
1327
|
-
});
|
|
1328
|
-
break;
|
|
1329
|
-
}
|
|
1330
|
-
case 'diagnostics': {
|
|
1331
|
-
if (typeof m.parentToolCallId === 'string')
|
|
1332
|
-
break; // 子流帧不上 leader 面
|
|
1333
|
-
const att = projectDiagnosticsFrame(m);
|
|
1334
|
-
if (att === null)
|
|
1335
|
-
break;
|
|
1336
|
-
yield chrome({ kind: 'attachment', laneProof: MAIN, id: idOf(m), attachment: att });
|
|
1337
|
-
break;
|
|
1338
|
-
}
|
|
1339
|
-
case 'steering_injected': {
|
|
1340
|
-
if (typeof m.parentToolCallId === 'string')
|
|
1341
|
-
break;
|
|
1342
|
-
const source = typeof m.source === 'string' ? m.source : '';
|
|
1343
|
-
if (source.length === 0)
|
|
1344
|
-
break;
|
|
1345
|
-
const preview = typeof m.preview === 'string' ? m.preview : '';
|
|
1346
|
-
let i = 0;
|
|
1347
|
-
for (const att of steeringInjectedToAttachments(source, preview)) {
|
|
1348
|
-
yield chrome({
|
|
1349
|
-
kind: 'attachment',
|
|
1350
|
-
laneProof: MAIN,
|
|
1351
|
-
id: idOf(m, `steering-${i++}`),
|
|
1352
|
-
attachment: att,
|
|
1353
|
-
});
|
|
1354
|
-
}
|
|
1355
|
-
break;
|
|
1356
|
-
}
|
|
1357
|
-
case 'workspace_changed': {
|
|
1358
|
-
if (typeof m.parentToolCallId === 'string')
|
|
1359
|
-
break;
|
|
1360
|
-
const cwd = typeof m.cwd === 'string' ? m.cwd : '';
|
|
1361
|
-
if (cwd.length === 0)
|
|
1362
|
-
break;
|
|
1363
|
-
yield chrome({
|
|
1364
|
-
kind: 'attachment',
|
|
1365
|
-
laneProof: MAIN,
|
|
1366
|
-
id: idOf(m),
|
|
1367
|
-
attachment: { type: 'workspace_changed', cwd },
|
|
1368
|
-
});
|
|
1369
|
-
break;
|
|
1370
|
-
}
|
|
1371
|
-
case 'prompt_suggestions': {
|
|
1372
|
-
// T57 第五处断闸(#47 矩阵 #5 同族):子流(parentToolCallId 标)的建议绝不骑主 composer。
|
|
1373
|
-
if (typeof m.parentToolCallId === 'string')
|
|
1374
|
-
break;
|
|
1375
|
-
const list = Array.isArray(m.suggestions)
|
|
1376
|
-
? m.suggestions.filter((s) => typeof s === 'string')
|
|
1377
|
-
: [];
|
|
1378
|
-
// 空批不发(cli 同门:`if (list.length > 0)`)—— 作废由 turn 开场那一发负责,
|
|
1379
|
-
// 这里再发一次 null 会把本轮真产出的建议在极端交错下擦掉。
|
|
1380
|
-
if (list.length > 0) {
|
|
1381
|
-
yield chrome({ kind: 'prompt_suggestions', laneProof: MAIN, suggestions: list });
|
|
1382
|
-
}
|
|
1383
|
-
break;
|
|
1384
|
-
}
|
|
1385
|
-
case 'retry_status': {
|
|
1386
|
-
// #47 矩阵 #5:子代 lane 帧绝不骑主 spinner(无判别守卫的无条件 store 写=默认可疑)。
|
|
1387
|
-
if (typeof m.parentToolCallId === 'string')
|
|
1388
|
-
break;
|
|
1389
|
-
yield chrome({
|
|
1390
|
-
kind: 'retry_status',
|
|
1391
|
-
laneProof: MAIN,
|
|
1392
|
-
status: mapBrainStatusToRetry({
|
|
1393
|
-
phase: typeof m.phase === 'string' ? m.phase : '',
|
|
1394
|
-
...(typeof m.detail === 'string' ? { detail: m.detail } : {}),
|
|
1395
|
-
...(typeof m.retryInSec === 'number' ? { retryInSec: m.retryInSec } : {}),
|
|
1396
|
-
}, ctx.now()),
|
|
1397
|
-
});
|
|
1398
|
-
retryOverlayActive = true;
|
|
1399
|
-
break;
|
|
1400
|
-
}
|
|
1401
|
-
default:
|
|
1402
|
-
break;
|
|
1403
|
-
}
|
|
1404
|
-
// COALESCER 节拍:一个间隔至多一次 flush(同步 SSE 突发先攒着,过闸再一次泄干)。
|
|
1405
|
-
if (m.type === 'stream_event') {
|
|
1406
|
-
const now = ctx.now();
|
|
1407
|
-
if ((textPending.length > 0 || thinkingPending.length > 0) && now - lastFlushAt >= flushEveryMs) {
|
|
1408
|
-
yield* drainLive();
|
|
1409
|
-
lastFlushAt = now;
|
|
1410
|
-
}
|
|
1411
|
-
}
|
|
272
|
+
yield* text.drainLive();
|
|
273
|
+
// ── 分派(拆分前是一个 600 行的 switch;`default:` 空臂 = 表里查不到,行为同)────────────
|
|
274
|
+
// 单读快照(codex 复审 #1 收货修):旧 switch 对 `m.type` 恰一次属性读,`typeof m.type
|
|
275
|
+
// ? m.type` 是两读 —— 对 accessor 形帧可观测,降回一读。
|
|
276
|
+
const rawType = m.type;
|
|
277
|
+
const armKey = typeof rawType === 'string' ? rawType : undefined;
|
|
278
|
+
const arm = armKey !== undefined ? ARMS.get(armKey) : undefined;
|
|
279
|
+
if (arm !== undefined)
|
|
280
|
+
yield* arm(m, deps);
|
|
281
|
+
// COALESCER 节拍尾(判据 + lastFlushAt 已收进 M1;`ctx.now()` 仍**只在 stream_event 那一拍**
|
|
282
|
+
// 取,与拆分前逐字同一处时钟读点)。
|
|
283
|
+
if (m.type === 'stream_event')
|
|
284
|
+
yield* text.maybeFlushOnBeat(ctx.now());
|
|
1412
285
|
}
|
|
1413
286
|
}
|
|
1414
287
|
finally {
|
|
@@ -1430,23 +303,22 @@ class WireToCcAdapterImpl {
|
|
|
1430
303
|
yield chrome({ kind: 'retry_status', laneProof: MAIN, status: null });
|
|
1431
304
|
yield chrome({ kind: 'thinking_activity', laneProof: MAIN, active: false });
|
|
1432
305
|
// 复审修 #2a:被打断的 turn 也**必须**结掉自己的面板行,否则 Esc 之后 footer 药丸永远
|
|
1433
|
-
// 显示 "1 local agent"(sweep 点
|
|
1434
|
-
yield*
|
|
306
|
+
// 显示 "1 local agent"(sweep 点 3/4;四点各有正控,见 pure 门 SPLIT 段钉①)。
|
|
307
|
+
yield* panel.settle(null, false);
|
|
1435
308
|
ctx.log?.('debug', 'turn aborted (Esc) — no terminal answer emitted');
|
|
1436
309
|
return;
|
|
1437
310
|
}
|
|
1438
311
|
// T60 —— turn 收口(cli 尾部同序,覆盖"流结束了但没有 result 臂"的路径):
|
|
1439
312
|
// 泄尾增量 → 清 liveness → 提交尾思考 → 提交尾文本段。
|
|
1440
|
-
yield* drainLive();
|
|
313
|
+
yield* text.drainLive();
|
|
1441
314
|
yield chrome({ kind: 'retry_status', laneProof: MAIN, status: null });
|
|
1442
|
-
// 复审修 #2a 孪生:没有 `result` 臂的收尾路径同样 sweep(sweep 点
|
|
1443
|
-
yield*
|
|
315
|
+
// 复审修 #2a 孪生:没有 `result` 臂的收尾路径同样 sweep(sweep 点 4/4)。
|
|
316
|
+
yield* panel.settle(null, false);
|
|
1444
317
|
// B5:无 `result` 收尾腿同样关掉还开着的卡(cli 同位;abort 路径**不做**,见上)。
|
|
1445
|
-
yield*
|
|
1446
|
-
yield* takeThinking();
|
|
1447
|
-
yield* takeAnswerSegment();
|
|
1448
|
-
ctx.log?.('debug', `turn done · emittedAssistantText=${emittedAssistantText} answerLen=${
|
|
1449
|
-
void answer; // 全流无 text 臂时的兜底源(result 臂已消费);保留供调试/后续批
|
|
318
|
+
yield* cards.sweep();
|
|
319
|
+
yield* text.takeThinking();
|
|
320
|
+
yield* text.takeAnswerSegment();
|
|
321
|
+
ctx.log?.('debug', `turn done · emittedAssistantText=${text.emittedAssistantText} answerLen=${text.answerLength}`);
|
|
1450
322
|
}
|
|
1451
323
|
}
|
|
1452
324
|
/** 造一个带台账的适配器实例(**一个 session 一个**——跨 turn 去重靠实例存活,重启靠 import/export)。 */
|