@sema-agent/client-core 0.2.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 +112 -0
- package/dist/adapt.d.ts +66 -0
- package/dist/adapt.js +773 -0
- package/dist/diagnostics.d.ts +37 -0
- package/dist/diagnostics.js +29 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +15 -0
- package/dist/notifications.d.ts +55 -0
- package/dist/notifications.js +97 -0
- package/dist/retryStatus.d.ts +36 -0
- package/dist/retryStatus.js +28 -0
- package/dist/seam.d.ts +187 -0
- package/dist/seam.js +13 -0
- package/dist/steering.d.ts +61 -0
- package/dist/steering.js +103 -0
- package/dist/workflow.d.ts +18 -0
- package/dist/workflow.js +60 -0
- package/package.json +30 -0
package/dist/adapt.js
ADDED
|
@@ -0,0 +1,773 @@
|
|
|
1
|
+
import { deriveTranscriptId } from './seam.js';
|
|
2
|
+
import { normalizeTaskNotification, renderTaskNotificationXml, taskNotificationDedupKey } from './notifications.js';
|
|
3
|
+
import { mapBrainStatusToRetry } from './retryStatus.js';
|
|
4
|
+
import { steeringInjectedToAttachments } from './steering.js';
|
|
5
|
+
import { projectDiagnosticsFrame } from './diagnostics.js';
|
|
6
|
+
/** 主车道证明——本批实现的臂全在 leader lane(子代/workflow lane 的 tick 面留 #52b)。 */
|
|
7
|
+
const MAIN = { lane: 'main' };
|
|
8
|
+
/** cli COALESCER 节流(207 口径;187 时代是 16ms)。宿主无关,时钟从 ctx.now() 取。 */
|
|
9
|
+
export const FLUSH_INTERVAL_MS = 100;
|
|
10
|
+
/** cli SUBAGENT_TOOL_NAMES / WORKFLOW_TOOL_NAMES 同源(判别资产,别各自重写)。 */
|
|
11
|
+
export const SUBAGENT_TOOL_NAMES = new Set(['Task', 'Agent', 'Fork']);
|
|
12
|
+
export const WORKFLOW_TOOL_NAMES = new Set(['Workflow', 'RunWorkflow', 'run_workflow']);
|
|
13
|
+
/** 引擎侧 task 族工具(ctrl+t 面板台账同步的侧信道触发名;cli handleEngineTaskToolUse 受理的三名:
|
|
14
|
+
* TodoWrite 全量表 upsert + TaskCreate/TaskUpdate;其余工具该函数直接 return)。 */
|
|
15
|
+
const TASK_TOOL_NAMES = new Set(['TodoWrite', 'TaskCreate', 'TaskUpdate']);
|
|
16
|
+
/**
|
|
17
|
+
* CC 自己的 reject/cancel 哨兵串(**数据**,非 CC 逻辑;cli src/utils/messages.ts:210-212 逐字)。
|
|
18
|
+
* CC 的 UserToolResultMessage 按 content 串前缀路由到 reject/cancel 卡,不看 is_error 位。
|
|
19
|
+
*/
|
|
20
|
+
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.";
|
|
21
|
+
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.";
|
|
22
|
+
/** CJK 加权 token 估算(cli estimateCjkTokens 逐字:CJK≈0.6,其余≈0.25 tok/char)。 */
|
|
23
|
+
export function estimateCjkTokens(s) {
|
|
24
|
+
let t = 0;
|
|
25
|
+
for (const c of s) {
|
|
26
|
+
const cp = c.codePointAt(0) ?? 0;
|
|
27
|
+
t +=
|
|
28
|
+
(cp >= 0x2e80 && cp <= 0x9fff) ||
|
|
29
|
+
(cp >= 0x3000 && cp <= 0x30ff) ||
|
|
30
|
+
(cp >= 0xac00 && cp <= 0xd7a3) ||
|
|
31
|
+
(cp >= 0xf900 && cp <= 0xfaff) ||
|
|
32
|
+
(cp >= 0xff00 && cp <= 0xffef) ||
|
|
33
|
+
(cp >= 0x20000 && cp <= 0x3ffff)
|
|
34
|
+
? 0.6
|
|
35
|
+
: 0.25;
|
|
36
|
+
}
|
|
37
|
+
return Math.round(t);
|
|
38
|
+
}
|
|
39
|
+
/** 引擎 subagent 卡的短标签(cli shortTaskLabel 逐字)。 */
|
|
40
|
+
export function shortTaskLabel(task) {
|
|
41
|
+
const firstLine = (task.split('\n').find(l => l.trim()) ?? task).trim();
|
|
42
|
+
const words = firstLine.split(/\s+/).slice(0, 6).join(' ');
|
|
43
|
+
const label = words.length > 48 ? words.slice(0, 47) + '…' : words;
|
|
44
|
+
return label || 'subagent';
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* tool_use 块的渲染面消毒(cli assistant 臂逐条搬运,**纯函数**,不改入参):
|
|
48
|
+
* ① 摘 §E2 wire 身份(eventId/parentToolCallId)并从渲染块剥掉;
|
|
49
|
+
* ② 剥所有 `_sema_*` 保留哨兵(CC 的 per-tool 渲染器 safeParse strictObject,多一个键就降级成通用卡);
|
|
50
|
+
* ③ 引擎 subagent 参数名 {agent,task,taskName} → CC 名 {subagent_type,description,prompt}
|
|
51
|
+
* (strict schema 拒绝多余键 ⇒ REPLACE 不 merge;taskName 缺席时从 task 派生短标签);
|
|
52
|
+
* ④ 件8 类型标签诚实化:引擎通道兑现不了的 subagent_type 值一律删(只留 fork/general-purpose/worker),
|
|
53
|
+
* 否则卡头会显示 `Plan(…)` 而实跑 general-purpose。
|
|
54
|
+
*/
|
|
55
|
+
export function sanitizeToolUseBlock(block) {
|
|
56
|
+
const out = { ...block };
|
|
57
|
+
const rawInput = out.input;
|
|
58
|
+
const eventId = typeof out.eventId === 'string' ? out.eventId : undefined;
|
|
59
|
+
const parentToolCallId = typeof out.parentToolCallId === 'string' ? out.parentToolCallId : undefined;
|
|
60
|
+
delete out.eventId;
|
|
61
|
+
delete out.parentToolCallId;
|
|
62
|
+
if (out.input && typeof out.input === 'object') {
|
|
63
|
+
const cleaned = {};
|
|
64
|
+
for (const [k, val] of Object.entries(out.input)) {
|
|
65
|
+
if (!k.startsWith('_sema_'))
|
|
66
|
+
cleaned[k] = val;
|
|
67
|
+
}
|
|
68
|
+
out.input = cleaned;
|
|
69
|
+
}
|
|
70
|
+
if (out.input && typeof out.input === 'object') {
|
|
71
|
+
const inp = out.input;
|
|
72
|
+
const looksEngineSubagent = typeof inp.task === 'string' && inp.prompt === undefined && inp.description === undefined;
|
|
73
|
+
if (looksEngineSubagent) {
|
|
74
|
+
const task = inp.task;
|
|
75
|
+
const taskName = typeof inp.taskName === 'string' ? inp.taskName.trim() : '';
|
|
76
|
+
const label = taskName || shortTaskLabel(task);
|
|
77
|
+
const remapped = { description: label, prompt: task };
|
|
78
|
+
if (typeof inp.agent === 'string')
|
|
79
|
+
remapped.subagent_type = inp.agent;
|
|
80
|
+
if (typeof inp.model === 'string')
|
|
81
|
+
remapped.model = inp.model;
|
|
82
|
+
out.input = remapped;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (SUBAGENT_TOOL_NAMES.has(out.name) && out.input && typeof out.input === 'object') {
|
|
86
|
+
const inp = out.input;
|
|
87
|
+
if (typeof inp.subagent_type === 'string' &&
|
|
88
|
+
!['fork', 'general-purpose', 'worker'].includes(inp.subagent_type)) {
|
|
89
|
+
delete inp.subagent_type;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return { block: out, rawInput, ...(eventId !== undefined ? { eventId } : {}), ...(parentToolCallId !== undefined ? { parentToolCallId } : {}) };
|
|
93
|
+
}
|
|
94
|
+
/** seam 的 reserved decision 哨兵(cli decisionOf 逐字)。 */
|
|
95
|
+
function decisionOf(block) {
|
|
96
|
+
const input = block.input;
|
|
97
|
+
const d = input?._sema_decision;
|
|
98
|
+
return d === 'reject' || d === 'cancel' || d === 'error' ? d : undefined;
|
|
99
|
+
}
|
|
100
|
+
/** 本批覆盖清单——差分守卫按它区分「已覆盖臂必须逐字段等价」与「已声明未覆盖臂」。 */
|
|
101
|
+
export const ADAPTER_COVERAGE = {
|
|
102
|
+
/** 已落码的帧臂。 */
|
|
103
|
+
frames: [
|
|
104
|
+
'assistant',
|
|
105
|
+
'user',
|
|
106
|
+
'stream_event',
|
|
107
|
+
'turn_usage',
|
|
108
|
+
'result',
|
|
109
|
+
'system',
|
|
110
|
+
'task_notification',
|
|
111
|
+
'workflow_complete',
|
|
112
|
+
'diagnostics',
|
|
113
|
+
'steering_injected',
|
|
114
|
+
'workspace_changed',
|
|
115
|
+
'retry_status',
|
|
116
|
+
'tool_end_result(仅 response-id 复位 + 开卡台账出栈,不铸 tool_result)',
|
|
117
|
+
],
|
|
118
|
+
/** 已落码臂产出的 transcript 消息类目(差分守卫的比对域)。 */
|
|
119
|
+
transcriptKinds: [
|
|
120
|
+
'assistant_text',
|
|
121
|
+
'assistant_thinking',
|
|
122
|
+
'assistant_tool_use',
|
|
123
|
+
'user_tool_result_decision',
|
|
124
|
+
'user_task_notification',
|
|
125
|
+
'system_passthrough',
|
|
126
|
+
],
|
|
127
|
+
/** 如实留白——写不动/依赖未搬运资产的臂,#52b 起分批接。 */
|
|
128
|
+
todo: [
|
|
129
|
+
'tool_end_result → user tool_result 正常路径(需搬 wireOutputToBody/structuredToToolUseResult/mock 合成器 + TodoWrite/ReportFindings 富卡片,约 600 行;workflow 轮询信封的人话投影已在 workflow.ts 就位,等这条接线)',
|
|
130
|
+
'tool_end_result 的两个侧产物:Remember → memory_saved 系统消息、task/task-list structured → 面板台账全量同步',
|
|
131
|
+
'result/turn 末的开卡兜底关闭(同上,依赖合成器)',
|
|
132
|
+
'task_progress → 面板行绑定(bindTaskCard FIFO/显式父、inline stats、alias、resident 台账、SubagentStart hook、workflow lane 三级门)',
|
|
133
|
+
'settlePanelTasks 三处 sweep(依赖上面的行台账)',
|
|
134
|
+
'#117a bg Bash 回执探测(detectEngineBgShellReceipt 文案锚定探测器)',
|
|
135
|
+
'abort/Esc 半场(AdapterContext 无 signal 位——见 SEAM-GAP-4)',
|
|
136
|
+
'IDLE_FLUSH 1.5s 竞速(件1 大 Write 静默窗;需 ctx 侧定时器能力)',
|
|
137
|
+
'macrotask 让渡(cli 在提交前让出宏任务给 Ink;宿主渲染策略,不进库)',
|
|
138
|
+
],
|
|
139
|
+
};
|
|
140
|
+
/** 本批**已知的、有意的**行为分歧(差分守卫显式登记,绝不当等价) */
|
|
141
|
+
export const ADAPTER_DIVERGENCES = [
|
|
142
|
+
'DIVERGENCE-1:`user` 帧透传——cli 的 switch 无 user 臂(default 丢弃),adapt 原样透传。' +
|
|
143
|
+
'现役 wire 不产 user 帧(eventToSdkMessage 无该出口),故对真流是惰性的;保留是为了 desktop 重放存量转录。',
|
|
144
|
+
'DIVERGENCE-2:transcript uuid 取值形——cli 铸随机 v4 / 原样透传帧 uuid,adapt 一律走 ' +
|
|
145
|
+
'deriveTranscriptId(稳定键前缀形 `wid_*`)。差分守卫按「别名结构」比对(同一侧内的复用/区分关系),不比字面值。',
|
|
146
|
+
'DIVERGENCE-3:合并节流的**分片边界**不比对——cli 用 Date.now() 真时钟,adapt 用 ctx.now()。' +
|
|
147
|
+
'守卫比对的是 chrome 增量按 channel 拼接后的全文与顺序(语义不变量),不是 chunk 切分。',
|
|
148
|
+
];
|
|
149
|
+
const TASK_NOTIFICATION_TAG = 'task-notification';
|
|
150
|
+
const TASK_ID_TAG = 'task-id';
|
|
151
|
+
const STATUS_TAG = 'status';
|
|
152
|
+
/**
|
|
153
|
+
* 适配器实例 —— **台账**(通知去重/跨通道记账)挂实例,`adapt()` 每 turn 调一次,turn 级状态在
|
|
154
|
+
* 调用内。跨 turn 语义(进程级去重)因此靠「同一个实例贯穿 session」保证,重启靠 import/export。
|
|
155
|
+
*/
|
|
156
|
+
class WireToCcAdapterImpl {
|
|
157
|
+
renderedNotifications = new Set();
|
|
158
|
+
notifiedRuns = new Set();
|
|
159
|
+
cardEnqueuedRuns = new Set();
|
|
160
|
+
/** W1 三级判据素材:本壳见过的 Workflow 工具 tool_use id(module 级台账的实例化)。 */
|
|
161
|
+
seenWorkflowToolCallIds = new Set();
|
|
162
|
+
exportLedger() {
|
|
163
|
+
return {
|
|
164
|
+
version: 1,
|
|
165
|
+
renderedNotifications: [...this.renderedNotifications],
|
|
166
|
+
notifiedRuns: [...this.notifiedRuns],
|
|
167
|
+
cardEnqueuedRuns: [...this.cardEnqueuedRuns],
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
importLedger(state) {
|
|
171
|
+
let n = 0;
|
|
172
|
+
const take = (v, into) => {
|
|
173
|
+
if (!Array.isArray(v))
|
|
174
|
+
return;
|
|
175
|
+
for (const k of v) {
|
|
176
|
+
if (typeof k === 'string' && k.length > 0 && !into.has(k)) {
|
|
177
|
+
into.add(k);
|
|
178
|
+
n++;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
const s = state;
|
|
183
|
+
take(s.renderedNotifications, this.renderedNotifications);
|
|
184
|
+
take(s.notifiedRuns, this.notifiedRuns);
|
|
185
|
+
take(s.cardEnqueuedRuns, this.cardEnqueuedRuns);
|
|
186
|
+
return n;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* #63 —— completion inbox 是 at-least-once 而去重面纯内存,重启即蒸发;「通知 user-turn 已落盘、
|
|
190
|
+
* ack 未落」的 crash 窗口重放 = 同一完成二次喂模型。落盘转录里的 `<task-notification>` 块携
|
|
191
|
+
* task-id/status/type="external",恰可重建**无 seq 键形**;Monitor 的 `:seq` 批键形不同,绝不误伤。
|
|
192
|
+
*/
|
|
193
|
+
importLedgerFromTranscript(messages) {
|
|
194
|
+
const unescape = (s) => s
|
|
195
|
+
.replace(/</g, '<')
|
|
196
|
+
.replace(/>/g, '>')
|
|
197
|
+
.replace(/"/g, '"')
|
|
198
|
+
.replace(/'/g, "'")
|
|
199
|
+
.replace(/&/g, '&');
|
|
200
|
+
let seeded = 0;
|
|
201
|
+
for (const m of messages) {
|
|
202
|
+
const rec = m;
|
|
203
|
+
if (rec?.type !== 'user')
|
|
204
|
+
continue;
|
|
205
|
+
const content = rec.message?.content;
|
|
206
|
+
const texts = [];
|
|
207
|
+
if (typeof content === 'string')
|
|
208
|
+
texts.push(content);
|
|
209
|
+
else if (Array.isArray(content)) {
|
|
210
|
+
for (const b of content) {
|
|
211
|
+
const bb = b;
|
|
212
|
+
if (bb?.type === 'text' && typeof bb.text === 'string')
|
|
213
|
+
texts.push(bb.text);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
for (const text of texts) {
|
|
217
|
+
if (!text.includes(`<${TASK_NOTIFICATION_TAG}`))
|
|
218
|
+
continue;
|
|
219
|
+
const blockRe = new RegExp(`<${TASK_NOTIFICATION_TAG}(\\s[^>]*)?>([\\s\\S]*?)</${TASK_NOTIFICATION_TAG}>`, 'g');
|
|
220
|
+
for (const bm of text.matchAll(blockRe)) {
|
|
221
|
+
const attrs = bm[1] ?? '';
|
|
222
|
+
const body = bm[2] ?? '';
|
|
223
|
+
const idM = body.match(new RegExp(`<${TASK_ID_TAG}>([\\s\\S]*?)</${TASK_ID_TAG}>`));
|
|
224
|
+
const stM = body.match(new RegExp(`<${STATUS_TAG}>([\\s\\S]*?)</${STATUS_TAG}>`));
|
|
225
|
+
if (!idM || !stM)
|
|
226
|
+
continue;
|
|
227
|
+
const taskId = unescape(idM[1] ?? '');
|
|
228
|
+
const status = unescape(stM[1] ?? '');
|
|
229
|
+
if (taskId.length === 0 || status.length === 0)
|
|
230
|
+
continue;
|
|
231
|
+
const external = /\btype="external"/.test(attrs);
|
|
232
|
+
this.renderedNotifications.add(`${external ? 'external:' : ''}${taskId}:${status}`);
|
|
233
|
+
if (!external && (status === 'completed' || status === 'failed' || status === 'killed')) {
|
|
234
|
+
this.notifiedRuns.add(taskId);
|
|
235
|
+
}
|
|
236
|
+
seeded++;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return seeded;
|
|
241
|
+
}
|
|
242
|
+
noteCardEnqueued(runId) {
|
|
243
|
+
this.cardEnqueuedRuns.add(runId);
|
|
244
|
+
}
|
|
245
|
+
async *adapt(frames, ctx) {
|
|
246
|
+
const turnStartAt = ctx.now();
|
|
247
|
+
// ── id 派生 ────────────────────────────────────────────────────────────────────────────────
|
|
248
|
+
// [1653]:稳定键优先序 = 帧 id > seq > toolCallId,全缺才 ctx.uuid()。现役 wire 的稳定键落在
|
|
249
|
+
// `uuid` 字段(§E2 core 铸的 eventId / stamp 铸的帧 id),故映到 `id` 位喂进去(见 SEAM-GAP-1)。
|
|
250
|
+
// `suffix` = 同一帧派生多条消息时的去撞位(如 tool_use 卡 + 同帧的 reject 结果)。
|
|
251
|
+
const idOf = (frame, suffix) => {
|
|
252
|
+
const base = typeof frame.id === 'string' && frame.id.length > 0
|
|
253
|
+
? frame.id
|
|
254
|
+
: typeof frame.uuid === 'string' && frame.uuid.length > 0
|
|
255
|
+
? frame.uuid
|
|
256
|
+
: undefined;
|
|
257
|
+
return deriveTranscriptId({
|
|
258
|
+
...(base !== undefined ? { id: suffix !== undefined ? `${base}#${suffix}` : base } : {}),
|
|
259
|
+
...(typeof frame.seq === 'number' ? { seq: frame.seq } : {}),
|
|
260
|
+
...(typeof frame.toolCallId === 'string' ? { toolCallId: frame.toolCallId } : {}),
|
|
261
|
+
}, ctx);
|
|
262
|
+
};
|
|
263
|
+
// ── turn 级状态(cli generator 局部变量的等价) ──────────────────────────────────────────────
|
|
264
|
+
let answer = '';
|
|
265
|
+
let answerSegment = '';
|
|
266
|
+
let thinking = '';
|
|
267
|
+
let emittedAssistantText = false;
|
|
268
|
+
let textPending = '';
|
|
269
|
+
let thinkingPending = '';
|
|
270
|
+
let textBlockOpen = false;
|
|
271
|
+
let thinkingBlockOpen = false;
|
|
272
|
+
let lastFlushAt = 0;
|
|
273
|
+
let messageStartEmitted = false;
|
|
274
|
+
let endEmitted = false;
|
|
275
|
+
let assistantResponseId = null;
|
|
276
|
+
/** committed 消息的 id 锚 —— 该段/该思考块的**第一帧**(重放确定性:同流同 id)。 */
|
|
277
|
+
let segmentAnchor = null;
|
|
278
|
+
let thinkingAnchor = null;
|
|
279
|
+
const pending = new Map();
|
|
280
|
+
const chrome = (event) => ({ plane: 'chrome', event });
|
|
281
|
+
const transcript = (message) => ({
|
|
282
|
+
plane: 'transcript',
|
|
283
|
+
message: message,
|
|
284
|
+
});
|
|
285
|
+
const mintResponseId = (frame) => (assistantResponseId ??= `msg_sema_${idOf(frame, 'resp')}`);
|
|
286
|
+
/** 提交累积的思考块(cli takeThinking:committed 形 + 关活体块 + 收活动行)。 */
|
|
287
|
+
function* takeThinking() {
|
|
288
|
+
if (thinking.length === 0)
|
|
289
|
+
return;
|
|
290
|
+
const anchor = thinkingAnchor ?? {};
|
|
291
|
+
const msg = {
|
|
292
|
+
type: 'assistant',
|
|
293
|
+
message: { role: 'assistant', content: [{ type: 'thinking', thinking }] },
|
|
294
|
+
uuid: idOf(anchor, 'thinking'),
|
|
295
|
+
session_id: ctx.sessionId,
|
|
296
|
+
parent_tool_use_id: null,
|
|
297
|
+
};
|
|
298
|
+
thinking = '';
|
|
299
|
+
thinkingAnchor = null;
|
|
300
|
+
thinkingBlockOpen = false;
|
|
301
|
+
// P2d:活体 "∴ Thinking…" 行在 committed 块落地的**同一刻**收掉(先收行再上消息,cli 同序)
|
|
302
|
+
yield chrome({ kind: 'thinking_activity', laneProof: MAIN, active: false });
|
|
303
|
+
yield transcript(msg);
|
|
304
|
+
}
|
|
305
|
+
/** 提交累积的文本段(cli takeAnswerSegment:#27 段序铁律——工具卡前/turn 末各 flush 一次)。 */
|
|
306
|
+
function* takeAnswerSegment() {
|
|
307
|
+
if (answerSegment.length === 0)
|
|
308
|
+
return;
|
|
309
|
+
const anchor = segmentAnchor ?? {};
|
|
310
|
+
const msg = {
|
|
311
|
+
type: 'assistant',
|
|
312
|
+
message: { role: 'assistant', content: [{ type: 'text', text: answerSegment }] },
|
|
313
|
+
uuid: idOf(anchor, 'text'),
|
|
314
|
+
session_id: ctx.sessionId,
|
|
315
|
+
parent_tool_use_id: null,
|
|
316
|
+
};
|
|
317
|
+
answerSegment = '';
|
|
318
|
+
segmentAnchor = null;
|
|
319
|
+
emittedAssistantText = true;
|
|
320
|
+
textBlockOpen = false;
|
|
321
|
+
yield transcript(msg);
|
|
322
|
+
}
|
|
323
|
+
/** 合并后的活体增量下泄(cli drainLive:思考先于文本 = 持久臂序)。 */
|
|
324
|
+
function* drainLive() {
|
|
325
|
+
if (thinkingPending.length > 0) {
|
|
326
|
+
const opening = !thinkingBlockOpen;
|
|
327
|
+
thinkingBlockOpen = true;
|
|
328
|
+
yield chrome({
|
|
329
|
+
kind: 'stream_delta',
|
|
330
|
+
laneProof: MAIN,
|
|
331
|
+
channel: 'thinking',
|
|
332
|
+
text: thinkingPending,
|
|
333
|
+
estimatedTokens: estimateCjkTokens(thinkingPending),
|
|
334
|
+
opening,
|
|
335
|
+
});
|
|
336
|
+
thinkingPending = '';
|
|
337
|
+
}
|
|
338
|
+
if (textPending.length > 0) {
|
|
339
|
+
const opening = !textBlockOpen;
|
|
340
|
+
textBlockOpen = true;
|
|
341
|
+
yield chrome({
|
|
342
|
+
kind: 'stream_delta',
|
|
343
|
+
laneProof: MAIN,
|
|
344
|
+
channel: 'text',
|
|
345
|
+
text: textPending,
|
|
346
|
+
estimatedTokens: estimateCjkTokens(textPending),
|
|
347
|
+
opening,
|
|
348
|
+
});
|
|
349
|
+
textPending = '';
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
// turn 开场卫生(cli:清上一 turn 的 retry 覆盖层 + 思考活动行,再发 request-start)。
|
|
353
|
+
yield chrome({ kind: 'retry_status', laneProof: MAIN, status: null });
|
|
354
|
+
yield chrome({ kind: 'thinking_activity', laneProof: MAIN, active: false });
|
|
355
|
+
yield chrome({ kind: 'request_start', laneProof: MAIN });
|
|
356
|
+
for await (const rawFrame of frames) {
|
|
357
|
+
const m = rawFrame;
|
|
358
|
+
if (m === null || typeof m !== 'object')
|
|
359
|
+
continue;
|
|
360
|
+
if (m.type === 'stream_event' && !messageStartEmitted) {
|
|
361
|
+
messageStartEmitted = true;
|
|
362
|
+
yield chrome({
|
|
363
|
+
kind: 'response_metrics',
|
|
364
|
+
laneProof: MAIN,
|
|
365
|
+
phase: 'start',
|
|
366
|
+
ttftMs: ctx.now() - turnStartAt,
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
// 持久臂之前先把活体增量泄干净(活体预览恒先于它 settle 成的 committed 消息)。
|
|
370
|
+
if (m.type !== 'stream_event')
|
|
371
|
+
yield* drainLive();
|
|
372
|
+
switch (m.type) {
|
|
373
|
+
case 'assistant': {
|
|
374
|
+
const message = m.message;
|
|
375
|
+
const blocks = Array.isArray(message?.content)
|
|
376
|
+
? message.content
|
|
377
|
+
: [];
|
|
378
|
+
const toolUseIdx = blocks.findIndex(b => b?.type === 'tool_use');
|
|
379
|
+
if (toolUseIdx >= 0) {
|
|
380
|
+
const original = blocks[toolUseIdx];
|
|
381
|
+
const decision = decisionOf(original);
|
|
382
|
+
const san = sanitizeToolUseBlock(original);
|
|
383
|
+
const renderBlocks = blocks.map((b, i) => (i === toolUseIdx ? san.block : b));
|
|
384
|
+
const toolUseId = String(san.block.id);
|
|
385
|
+
const toolName = typeof san.block.name === 'string' ? san.block.name : 'unknown';
|
|
386
|
+
// 引擎模式下壳工具 call() 从不执行 —— ctrl+t 面板台账只能靠这条侧信道(宿主义务)。
|
|
387
|
+
if (TASK_TOOL_NAMES.has(toolName)) {
|
|
388
|
+
yield chrome({
|
|
389
|
+
kind: 'task_ledger_sync',
|
|
390
|
+
laneProof: MAIN,
|
|
391
|
+
source: 'tool_use',
|
|
392
|
+
toolName,
|
|
393
|
+
toolUseId,
|
|
394
|
+
input: san.block.input,
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
// #27 段序:先把这张卡之前的散文提交,再提交它之前的思考(思考恒在卡上方)。
|
|
398
|
+
yield* takeAnswerSegment();
|
|
399
|
+
yield* takeThinking();
|
|
400
|
+
yield transcript({
|
|
401
|
+
type: 'assistant',
|
|
402
|
+
// S1:per-RESPONSE 铸 id —— CC 的 grouped-agent 渲染按 `${message.id}:${toolName}`
|
|
403
|
+
// 分组;id-less 帧会把整个 session 的 Agent 卡塌成一张。tool_use 帧才盖章。
|
|
404
|
+
message: { id: mintResponseId(m), role: 'assistant', content: renderBlocks },
|
|
405
|
+
uuid: idOf(m),
|
|
406
|
+
session_id: ctx.sessionId,
|
|
407
|
+
parent_tool_use_id: null,
|
|
408
|
+
});
|
|
409
|
+
if (decision === 'reject' || decision === 'cancel') {
|
|
410
|
+
yield transcript({
|
|
411
|
+
type: 'user',
|
|
412
|
+
message: {
|
|
413
|
+
role: 'user',
|
|
414
|
+
content: [
|
|
415
|
+
{
|
|
416
|
+
type: 'tool_result',
|
|
417
|
+
tool_use_id: san.block.id,
|
|
418
|
+
content: decision === 'reject' ? REJECT_MESSAGE : CANCEL_MESSAGE,
|
|
419
|
+
is_error: false,
|
|
420
|
+
},
|
|
421
|
+
],
|
|
422
|
+
},
|
|
423
|
+
uuid: idOf(m, decision),
|
|
424
|
+
session_id: ctx.sessionId,
|
|
425
|
+
});
|
|
426
|
+
assistantResponseId = null; // 一个结果关闭该 model response
|
|
427
|
+
break;
|
|
428
|
+
}
|
|
429
|
+
if (decision === 'error') {
|
|
430
|
+
yield transcript({
|
|
431
|
+
type: 'user',
|
|
432
|
+
message: {
|
|
433
|
+
role: 'user',
|
|
434
|
+
content: [
|
|
435
|
+
{
|
|
436
|
+
type: 'tool_result',
|
|
437
|
+
tool_use_id: san.block.id,
|
|
438
|
+
content: 'The tool failed to execute.',
|
|
439
|
+
is_error: true,
|
|
440
|
+
},
|
|
441
|
+
],
|
|
442
|
+
},
|
|
443
|
+
uuid: idOf(m, 'error'),
|
|
444
|
+
session_id: ctx.sessionId,
|
|
445
|
+
});
|
|
446
|
+
assistantResponseId = null;
|
|
447
|
+
break;
|
|
448
|
+
}
|
|
449
|
+
pending.set(toolUseId, {
|
|
450
|
+
id: toolUseId,
|
|
451
|
+
name: toolName,
|
|
452
|
+
rawInput: san.rawInput,
|
|
453
|
+
...(san.eventId !== undefined ? { eventId: san.eventId } : {}),
|
|
454
|
+
...(san.parentToolCallId !== undefined ? { parentToolCallId: san.parentToolCallId } : {}),
|
|
455
|
+
});
|
|
456
|
+
if (WORKFLOW_TOOL_NAMES.has(toolName))
|
|
457
|
+
this.seenWorkflowToolCallIds.add(toolUseId);
|
|
458
|
+
break;
|
|
459
|
+
}
|
|
460
|
+
const textBlock = blocks.find(b => b?.type === 'text');
|
|
461
|
+
if (textBlock && typeof textBlock.text === 'string' && textBlock.text.length > 0) {
|
|
462
|
+
answer += textBlock.text;
|
|
463
|
+
yield* takeThinking();
|
|
464
|
+
yield transcript({
|
|
465
|
+
type: 'assistant',
|
|
466
|
+
message: { role: 'assistant', content: [{ type: 'text', text: textBlock.text }] },
|
|
467
|
+
uuid: idOf(m),
|
|
468
|
+
session_id: ctx.sessionId,
|
|
469
|
+
parent_tool_use_id: null,
|
|
470
|
+
});
|
|
471
|
+
emittedAssistantText = true;
|
|
472
|
+
}
|
|
473
|
+
const thinkingBlock = blocks.find(b => b?.type === 'thinking');
|
|
474
|
+
if (thinkingBlock && typeof thinkingBlock.thinking === 'string') {
|
|
475
|
+
yield transcript({
|
|
476
|
+
type: 'assistant',
|
|
477
|
+
message: { role: 'assistant', content: [thinkingBlock] },
|
|
478
|
+
uuid: idOf(m, 'durable-thinking'),
|
|
479
|
+
session_id: ctx.sessionId,
|
|
480
|
+
parent_tool_use_id: null,
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
break;
|
|
484
|
+
}
|
|
485
|
+
// DIVERGENCE-1 —— 现役 wire 不产 user 帧;透传是给 desktop 重放存量转录留的门。
|
|
486
|
+
case 'user': {
|
|
487
|
+
if (m.message === undefined)
|
|
488
|
+
break;
|
|
489
|
+
yield transcript({ ...m, uuid: idOf(m), session_id: ctx.sessionId });
|
|
490
|
+
break;
|
|
491
|
+
}
|
|
492
|
+
case 'tool_end_result': {
|
|
493
|
+
// 本批只做**卡台账出栈 + response-id 复位**(S1:一个结果关闭该 response,后续 tool_use
|
|
494
|
+
// 必须换新 id 才不会被 CC 分组塌成一张卡)。tool_result 消息本体见 ADAPTER_COVERAGE.todo。
|
|
495
|
+
const callId = typeof m.toolCallId === 'string' ? m.toolCallId : undefined;
|
|
496
|
+
if (callId !== undefined && pending.has(callId)) {
|
|
497
|
+
pending.delete(callId);
|
|
498
|
+
assistantResponseId = null;
|
|
499
|
+
}
|
|
500
|
+
break;
|
|
501
|
+
}
|
|
502
|
+
case 'stream_event': {
|
|
503
|
+
const ev = m.event;
|
|
504
|
+
if (ev && ev.type === 'text_delta' && typeof ev.delta === 'string') {
|
|
505
|
+
// MOD-1 思考→回答边界:回答一起头就把思考块 committed 上屏(它推理的内容之前)。
|
|
506
|
+
if (thinking.length > 0 || thinkingPending.length > 0) {
|
|
507
|
+
yield* drainLive();
|
|
508
|
+
yield* takeThinking();
|
|
509
|
+
}
|
|
510
|
+
if (answerSegment.length === 0)
|
|
511
|
+
segmentAnchor = m;
|
|
512
|
+
answer += ev.delta;
|
|
513
|
+
answerSegment += ev.delta;
|
|
514
|
+
textPending += ev.delta;
|
|
515
|
+
}
|
|
516
|
+
else if (ev && ev.type === 'thinking_delta' && typeof ev.delta === 'string') {
|
|
517
|
+
// 子流(parentToolCallId)的推理绝不喂 leader 的计数与预览(#9 的壳侧半场)。
|
|
518
|
+
if (ev.parentToolCallId !== undefined)
|
|
519
|
+
break;
|
|
520
|
+
if (thinking.length === 0) {
|
|
521
|
+
thinkingAnchor = m;
|
|
522
|
+
// P2d:elapsed 锚在**首条** leader 推理增量(token 累计由 stream_delta.estimatedTokens
|
|
523
|
+
// 承载,宿主自加;>30s 无增量的 stall 提示同样由宿主按增量时间戳判)。
|
|
524
|
+
yield chrome({ kind: 'thinking_activity', laneProof: MAIN, active: true });
|
|
525
|
+
}
|
|
526
|
+
thinking += ev.delta;
|
|
527
|
+
thinkingPending += ev.delta;
|
|
528
|
+
}
|
|
529
|
+
break;
|
|
530
|
+
}
|
|
531
|
+
case 'turn_usage': {
|
|
532
|
+
const ot = typeof m.outputTokens === 'number' ? m.outputTokens : undefined;
|
|
533
|
+
if (ot != null) {
|
|
534
|
+
yield* drainLive();
|
|
535
|
+
yield chrome({ kind: 'response_metrics', laneProof: MAIN, phase: 'end', outputTokens: ot });
|
|
536
|
+
endEmitted = true;
|
|
537
|
+
messageStartEmitted = false; // 下一 model round 重新压基线
|
|
538
|
+
}
|
|
539
|
+
break;
|
|
540
|
+
}
|
|
541
|
+
case 'result': {
|
|
542
|
+
yield chrome({ kind: 'retry_status', laneProof: MAIN, status: null });
|
|
543
|
+
yield* takeThinking();
|
|
544
|
+
if (!endEmitted) {
|
|
545
|
+
const u = m.usage;
|
|
546
|
+
if (typeof u?.outputTokens === 'number') {
|
|
547
|
+
yield chrome({
|
|
548
|
+
kind: 'response_metrics',
|
|
549
|
+
laneProof: MAIN,
|
|
550
|
+
phase: 'end',
|
|
551
|
+
outputTokens: u.outputTokens,
|
|
552
|
+
});
|
|
553
|
+
endEmitted = true;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
yield* takeAnswerSegment();
|
|
557
|
+
if (!emittedAssistantText) {
|
|
558
|
+
const text = typeof m.result === 'string' ? m.result : '';
|
|
559
|
+
if (text.length > 0) {
|
|
560
|
+
yield transcript({
|
|
561
|
+
type: 'assistant',
|
|
562
|
+
message: { role: 'assistant', content: [{ type: 'text', text }] },
|
|
563
|
+
uuid: idOf(m, 'terminal-text'),
|
|
564
|
+
session_id: ctx.sessionId,
|
|
565
|
+
parent_tool_use_id: null,
|
|
566
|
+
});
|
|
567
|
+
emittedAssistantText = true;
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
break;
|
|
571
|
+
}
|
|
572
|
+
case 'system': {
|
|
573
|
+
yield transcript(m);
|
|
574
|
+
// core 1.219 attachWorkingFiles:compact_boundary 可携 attachedFiles → CC 的
|
|
575
|
+
// "Referenced file {path}" 行(post-compact attachment,零新 UI)。
|
|
576
|
+
const attachedFiles = m.attachedFiles;
|
|
577
|
+
if (Array.isArray(attachedFiles)) {
|
|
578
|
+
let i = 0;
|
|
579
|
+
for (const f of attachedFiles) {
|
|
580
|
+
if (typeof f?.path !== 'string' || f.path.length === 0)
|
|
581
|
+
continue;
|
|
582
|
+
yield chrome({
|
|
583
|
+
kind: 'attachment',
|
|
584
|
+
laneProof: MAIN,
|
|
585
|
+
id: idOf(m, `attached-file-${i++}`),
|
|
586
|
+
attachment: {
|
|
587
|
+
type: 'compact_file_reference',
|
|
588
|
+
filename: f.path,
|
|
589
|
+
displayPath: f.path,
|
|
590
|
+
},
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
break;
|
|
595
|
+
}
|
|
596
|
+
case 'task_notification': {
|
|
597
|
+
const n = (m.notification ?? {});
|
|
598
|
+
const fields = normalizeTaskNotification(n);
|
|
599
|
+
if (fields === null)
|
|
600
|
+
break; // 空/缺 task_id:RB-75 同族,丢弃
|
|
601
|
+
const taskId = fields.taskId;
|
|
602
|
+
const rawStatus = fields.status;
|
|
603
|
+
const terminal = rawStatus === 'completed' || rawStatus === 'failed' || rawStatus === 'killed';
|
|
604
|
+
// #117a settle 半场 —— **故意在渲染去重之前**:settle 幂等,重放帧仍须 settle 一条
|
|
605
|
+
// 首投之后才注册的行。#47 矩阵 #7:只有 bash 行(或老引擎无 task_type)能被 settle。
|
|
606
|
+
const settleAllowed = n.task_type === undefined || n.task_type === 'background_bash';
|
|
607
|
+
if (settleAllowed && terminal) {
|
|
608
|
+
yield chrome({
|
|
609
|
+
kind: 'bgshell_settle',
|
|
610
|
+
laneProof: MAIN,
|
|
611
|
+
taskId,
|
|
612
|
+
status: rawStatus,
|
|
613
|
+
...(typeof n.output_file === 'string' && n.output_file.length > 0
|
|
614
|
+
? { outputPath: n.output_file }
|
|
615
|
+
: {}),
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
const dedupKey = taskNotificationDedupKey({
|
|
619
|
+
taskId,
|
|
620
|
+
status: rawStatus,
|
|
621
|
+
...(typeof n.seq === 'number' || typeof n.seq === 'string' ? { seq: n.seq } : {}),
|
|
622
|
+
...(typeof n.task_type === 'string' ? { taskType: n.task_type } : {}),
|
|
623
|
+
});
|
|
624
|
+
if (this.renderedNotifications.has(dedupKey))
|
|
625
|
+
break;
|
|
626
|
+
this.renderedNotifications.add(dedupKey);
|
|
627
|
+
// B1 + #6 —— 必须在 cardEnqueued 早退**之前**:probe 合成先入队的场景恰是双投主形。
|
|
628
|
+
if (n.task_type !== 'external' && terminal) {
|
|
629
|
+
yield chrome({
|
|
630
|
+
kind: 'notification_terminal',
|
|
631
|
+
laneProof: MAIN,
|
|
632
|
+
taskId,
|
|
633
|
+
status: rawStatus,
|
|
634
|
+
...(typeof n.task_type === 'string' ? { taskType: n.task_type } : {}),
|
|
635
|
+
});
|
|
636
|
+
if (n.task_type !== 'background_bash') {
|
|
637
|
+
yield chrome({
|
|
638
|
+
kind: 'panel_task',
|
|
639
|
+
laneProof: MAIN,
|
|
640
|
+
event: { kind: 'end', taskId, isError: rawStatus !== 'completed' },
|
|
641
|
+
});
|
|
642
|
+
yield chrome({
|
|
643
|
+
kind: 'subagent_lifecycle',
|
|
644
|
+
laneProof: MAIN,
|
|
645
|
+
phase: 'stop',
|
|
646
|
+
taskId,
|
|
647
|
+
guard: 'if-started',
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
// 双通道反向去重:probe 合成的完成卡已入队 ⇒ 后到的推送帧只做上面的记账,不渲第二条。
|
|
652
|
+
if (n.task_type !== 'external' && this.cardEnqueuedRuns.has(taskId))
|
|
653
|
+
break;
|
|
654
|
+
// 跨通道记账(cli markEngineWorkflowNotified 同位、同条件:非 external,不看终态):
|
|
655
|
+
// 这帧到达 = 引擎已 server-side steer-inject 过,补发通道(workflow_complete)不得再注入。
|
|
656
|
+
// external 豁免([769] 对抗复审收紧:external 从不经补发通道,mark 只会缴械 probe 兜底)。
|
|
657
|
+
if (n.task_type !== 'external')
|
|
658
|
+
this.notifiedRuns.add(taskId);
|
|
659
|
+
yield transcript({
|
|
660
|
+
type: 'user',
|
|
661
|
+
message: { role: 'user', content: [{ type: 'text', text: renderTaskNotificationXml(fields) }] },
|
|
662
|
+
uuid: idOf(m),
|
|
663
|
+
session_id: ctx.sessionId,
|
|
664
|
+
});
|
|
665
|
+
break;
|
|
666
|
+
}
|
|
667
|
+
case 'workflow_complete': {
|
|
668
|
+
const runId = typeof m.runId === 'string' ? m.runId : undefined;
|
|
669
|
+
if (runId === undefined)
|
|
670
|
+
break;
|
|
671
|
+
// 跨通道去重(cli enqueueEngineWorkflowNotification 首行):该 run 已由推送帧记账过
|
|
672
|
+
// (引擎 server-side 注入过)⇒ Path B 绝不再喂模型一遍。适配器自持,不摊给宿主。
|
|
673
|
+
if (this.notifiedRuns.has(runId))
|
|
674
|
+
break;
|
|
675
|
+
this.notifiedRuns.add(runId);
|
|
676
|
+
const wfStatus = m.status === 'failed' ? 'failed' : 'completed';
|
|
677
|
+
const wfSummary = typeof m.summary === 'string' ? m.summary : '';
|
|
678
|
+
yield chrome({
|
|
679
|
+
kind: 'workflow_notification_enqueue',
|
|
680
|
+
laneProof: MAIN,
|
|
681
|
+
runId,
|
|
682
|
+
status: wfStatus,
|
|
683
|
+
summary: wfSummary,
|
|
684
|
+
// 模型面正文照铸(宿主直接入队即可,不必各自拼 XML;与 core 的 tag/段序同形)
|
|
685
|
+
message: renderTaskNotificationXml({ taskId: runId, status: wfStatus, summary: wfSummary }),
|
|
686
|
+
});
|
|
687
|
+
this.cardEnqueuedRuns.add(runId);
|
|
688
|
+
break;
|
|
689
|
+
}
|
|
690
|
+
case 'diagnostics': {
|
|
691
|
+
if (typeof m.parentToolCallId === 'string')
|
|
692
|
+
break; // 子流帧不上 leader 面
|
|
693
|
+
const att = projectDiagnosticsFrame(m);
|
|
694
|
+
if (att === null)
|
|
695
|
+
break;
|
|
696
|
+
yield chrome({ kind: 'attachment', laneProof: MAIN, id: idOf(m), attachment: att });
|
|
697
|
+
break;
|
|
698
|
+
}
|
|
699
|
+
case 'steering_injected': {
|
|
700
|
+
if (typeof m.parentToolCallId === 'string')
|
|
701
|
+
break;
|
|
702
|
+
const source = typeof m.source === 'string' ? m.source : '';
|
|
703
|
+
if (source.length === 0)
|
|
704
|
+
break;
|
|
705
|
+
const preview = typeof m.preview === 'string' ? m.preview : '';
|
|
706
|
+
let i = 0;
|
|
707
|
+
for (const att of steeringInjectedToAttachments(source, preview)) {
|
|
708
|
+
yield chrome({
|
|
709
|
+
kind: 'attachment',
|
|
710
|
+
laneProof: MAIN,
|
|
711
|
+
id: idOf(m, `steering-${i++}`),
|
|
712
|
+
attachment: att,
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
break;
|
|
716
|
+
}
|
|
717
|
+
case 'workspace_changed': {
|
|
718
|
+
if (typeof m.parentToolCallId === 'string')
|
|
719
|
+
break;
|
|
720
|
+
const cwd = typeof m.cwd === 'string' ? m.cwd : '';
|
|
721
|
+
if (cwd.length === 0)
|
|
722
|
+
break;
|
|
723
|
+
yield chrome({
|
|
724
|
+
kind: 'attachment',
|
|
725
|
+
laneProof: MAIN,
|
|
726
|
+
id: idOf(m),
|
|
727
|
+
attachment: { type: 'workspace_changed', cwd },
|
|
728
|
+
});
|
|
729
|
+
break;
|
|
730
|
+
}
|
|
731
|
+
case 'retry_status': {
|
|
732
|
+
// #47 矩阵 #5:子代 lane 帧绝不骑主 spinner(无判别守卫的无条件 store 写=默认可疑)。
|
|
733
|
+
if (typeof m.parentToolCallId === 'string')
|
|
734
|
+
break;
|
|
735
|
+
yield chrome({
|
|
736
|
+
kind: 'retry_status',
|
|
737
|
+
laneProof: MAIN,
|
|
738
|
+
status: mapBrainStatusToRetry({
|
|
739
|
+
phase: typeof m.phase === 'string' ? m.phase : '',
|
|
740
|
+
...(typeof m.detail === 'string' ? { detail: m.detail } : {}),
|
|
741
|
+
...(typeof m.retryInSec === 'number' ? { retryInSec: m.retryInSec } : {}),
|
|
742
|
+
}, ctx.now()),
|
|
743
|
+
});
|
|
744
|
+
break;
|
|
745
|
+
}
|
|
746
|
+
default:
|
|
747
|
+
break;
|
|
748
|
+
}
|
|
749
|
+
// COALESCER 节拍:一个间隔至多一次 flush(同步 SSE 突发先攒着,过闸再一次泄干)。
|
|
750
|
+
if (m.type === 'stream_event') {
|
|
751
|
+
const now = ctx.now();
|
|
752
|
+
if ((textPending.length > 0 || thinkingPending.length > 0) && now - lastFlushAt >= FLUSH_INTERVAL_MS) {
|
|
753
|
+
yield* drainLive();
|
|
754
|
+
lastFlushAt = now;
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
// turn 收口(cli 尾部同序):泄尾增量 → 清 liveness → 提交尾思考 → 提交尾文本段。
|
|
759
|
+
yield* drainLive();
|
|
760
|
+
yield chrome({ kind: 'retry_status', laneProof: MAIN, status: null });
|
|
761
|
+
yield* takeThinking();
|
|
762
|
+
yield* takeAnswerSegment();
|
|
763
|
+
void answer; // 全流无 text 臂时的兜底源(result 臂已消费);保留供调试/后续批
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
/** 造一个带台账的适配器实例(**一个 session 一个**——跨 turn 去重靠实例存活,重启靠 import/export)。 */
|
|
767
|
+
export function createWireToCcAdapter() {
|
|
768
|
+
return new WireToCcAdapterImpl();
|
|
769
|
+
}
|
|
770
|
+
/** 一次性(无跨 turn 台账)投影:`frames → AdapterOutput`。多 turn 请用 createWireToCcAdapter()。 */
|
|
771
|
+
export function adapt(frames, ctx) {
|
|
772
|
+
return createWireToCcAdapter().adapt(frames, ctx);
|
|
773
|
+
}
|