@sema-agent/client-core 0.68.1 → 0.69.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/CHANGELOG.md +129 -0
- package/README.md +8 -5
- package/dist/adapt/arms.js +30 -0
- package/dist/adapt/textSegmentAuthority.d.ts +157 -0
- package/dist/adapt/textSegmentAuthority.js +108 -0
- package/dist/adapt/textStream.d.ts +63 -0
- package/dist/adapt/textStream.js +122 -12
- package/dist/adapt.js +14 -0
- package/dist/adapter/downstream/eventToSdkMessage.d.ts +19 -0
- package/dist/adapter/downstream/eventToSdkMessage.js +42 -14
- package/dist/coreValuePorts.d.ts +161 -0
- package/dist/coreValuePorts.js +98 -0
- package/dist/engineHttpTools.d.ts +31 -0
- package/dist/engineHttpTools.js +30 -0
- package/dist/engineNoticeCodes.d.ts +1 -1
- package/dist/engineNoticeCodes.js +12 -1
- package/dist/gateOutcome.d.ts +27 -13
- package/dist/gateOutcome.js +29 -2
- package/dist/index.d.ts +6 -0
- package/dist/index.js +22 -0
- package/dist/mcpPanel.d.ts +104 -0
- package/dist/mcpPanel.js +129 -0
- package/dist/request/taskRequest.d.ts +54 -0
- package/dist/request/taskRequest.js +30 -0
- package/dist/sdkWireTransit.d.ts +41 -0
- package/dist/sdkWireTransit.js +32 -0
- package/dist/seam.d.ts +49 -1
- package/dist/seam.js +9 -1
- package/dist/workflowClient.d.ts +20 -0
- package/dist/workflowClient.js +19 -1
- package/docs/INTEGRATION-CLIENTS.md +321 -12
- package/package.json +5 -5
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* adapt/textSegmentAuthority.ts — `text_end.content` **权威段替换**的判决单源(L-318,0.68.2)。
|
|
3
|
+
*
|
|
4
|
+
* ── 为什么有这个文件(病形一句话)────────────────────────────────────────────────────────────
|
|
5
|
+
* 0.68.1 把权威替换落在 {@link import('./textStream.js').TextStream.replaceAnswerSegment} 里,
|
|
6
|
+
* 那一只服务的是**交互车道**(transcript / chrome 双平面)。而 `-p` 车道走的是另一条链
|
|
7
|
+
* (`eventToSdkMessage` 的内部臂 → 宿主自己的 CC stdout 投影器),它拿不到那只段缓冲 ⇒ cli 1.0.114
|
|
8
|
+
* 在壳里**又实现了一遍同样的六形**(B-122 热修)。同一条语义两份实现 = 上游改口时两边各走各的:
|
|
9
|
+
* 这正是「壳持有第二份真相」那一族病。
|
|
10
|
+
*
|
|
11
|
+
* ⇒ 本模块把**判决**(六形里真正决定结果的那三件事实 + 尾段)抽成一只**纯函数**,两条车道同吃:
|
|
12
|
+
* · 交互车道:`textStream.replaceAnswerSegment` 调它,再按本包的双平面语义落账;
|
|
13
|
+
* · print 车道:宿主(cli `PrintStreamProjector`)调它 + {@link createTextSegmentLedger},
|
|
14
|
+
* 只剩「把尾段写进 CC content block」这一步装配动作留在端上(多端改造设计稿 §8-5:
|
|
15
|
+
* stream-json 帧序/SSE 重铸属端,判定属包)。
|
|
16
|
+
*
|
|
17
|
+
* 🔴 **本模块零宿主形**:进出全是 `string` / `number` / `boolean` —— 没有 CC content block、没有
|
|
18
|
+
* transcript 消息、没有帧。可移植门(`--platform=browser` 真打包)因此一个字节都不受影响。
|
|
19
|
+
*
|
|
20
|
+
* ── 判决与落账刻意分家(读这一段再改本文件)──────────────────────────────────────────────────
|
|
21
|
+
* 两条车道的**账本形状不同**,而且那个差别是有理由的,不许在这里归一:
|
|
22
|
+
* · 交互车道的已提交前缀分两本(`previousSegmentCommitted` / `segmentCommitted`),因为它按
|
|
23
|
+
* **transcript 消息**划界;段跨过消息边界、且已提交那截自己也过期时它**只发信号不动字节**
|
|
24
|
+
* (`handOffOnly`,0.68.1 轮五 finding② 的基线对照结论);
|
|
25
|
+
* · print 车道只有一本(工具卡边界带走的字节跨卡累加),没有那一形 —— 它的 stdout 是只进不退
|
|
26
|
+
* 的流,`(b2)` 恒走「一个字节都不再交」。
|
|
27
|
+
* ⇒ 本模块只交**判决**;「判决出来之后各自怎么落账」仍归各自那一层。把落账也归一 = 改掉其中一条
|
|
28
|
+
* 车道的既有行为,那是行为面改动,不是归层。
|
|
29
|
+
*/
|
|
30
|
+
/**
|
|
31
|
+
* 权威段替换的**六形判决**(两条车道共用的那一份;`content` 是引擎报的段权威全文)。
|
|
32
|
+
*
|
|
33
|
+
* 六形与它们各自的判据锚:
|
|
34
|
+
* (a) 常态:整段还押在调用层手里 ⇒ `replace_tail`,`committedPrefixLen === 0`;
|
|
35
|
+
* (b1) 半段已出门、且 `content.startsWith(前缀)` ⇒ `replace_tail`,只换未出门那一截;
|
|
36
|
+
* (b2) 半段已出门、**前缀自己也过期** ⇒ `withhold`。🔴 判据锚 = `startsWith`,**不是**「有没有
|
|
37
|
+
* flush/提交过」—— 后者是前置条件,前者才是真正决定结果的那个量;
|
|
38
|
+
* (c) 一 turn 多段(anthropic 车道逐 `content_block_stop` 发 `text_end`):本函数只交尾段,
|
|
39
|
+
* 「换完即封存、下一段另起」由调用层落账 —— 不封存的话第二帧会把第一段正文一起覆盖;
|
|
40
|
+
* (d) 子流(`parentToolCallId` 在场):**根本不该走到这里**,断闸在调用层的臂上(拿子代的段边界
|
|
41
|
+
* 去改 leader 的缓冲 = 跨 lane 状态破坏)。本函数不认识 lane 位,也刻意不去认识它;
|
|
42
|
+
* (e) 迟到的段边界(openai 车道:工具卡先执行、`text_end` 在 `finalize()` 才到)⇒ 由调用层把
|
|
43
|
+
* 两本账相加后交进 {@link TextSegmentAuthorityInput.committedPrefix},照 (b1)/(b2) 分流;
|
|
44
|
+
* (f) 调用层一个字节都没经手这一段(两截皆空)⇒ `untouched`。替换的语义是「换掉本层自己缝合
|
|
45
|
+
* 出来的那一段」,不是「凭空铸一段」:durable 整块 `text` 腿之后再来一帧 `text_end`,塞进
|
|
46
|
+
* 空缓冲就会在收口**再铸一条一模一样的**(0.68.1 `#323-g` 正控守的就是这一形)。
|
|
47
|
+
*
|
|
48
|
+
* @param content 该段的权威全文(UNTRUSTED、仅展示,与 `text_delta` 同契约 —— 渲染,绝不回喂模型)。
|
|
49
|
+
*/
|
|
50
|
+
export function resolveTextSegmentAuthority(content, input) {
|
|
51
|
+
const committedPrefix = input.committedPrefix;
|
|
52
|
+
const openSegment = input.openSegment;
|
|
53
|
+
// (f) 两截皆空 ⇒ 什么都不做。三位全报否(never-false 律:缺席 = 否定)。
|
|
54
|
+
if (committedPrefix.length === 0 && openSegment.length === 0) {
|
|
55
|
+
return {
|
|
56
|
+
form: 'untouched',
|
|
57
|
+
diverged: false,
|
|
58
|
+
committedPrefixLen: 0,
|
|
59
|
+
committedPrefixDiverged: false,
|
|
60
|
+
tail: '',
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
const committedPrefixLen = committedPrefix.length;
|
|
64
|
+
const diverged = committedPrefix + openSegment !== content;
|
|
65
|
+
const committedPrefixDiverged = committedPrefixLen > 0 && !content.startsWith(committedPrefix);
|
|
66
|
+
if (committedPrefixDiverged) {
|
|
67
|
+
// (b2):已出门那截撤不回,而切片拼不出 `content` —— 再补一条就是同一段话上屏两遍。
|
|
68
|
+
return { form: 'withhold', diverged, committedPrefixLen, committedPrefixDiverged: true, tail: '' };
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
form: 'replace_tail',
|
|
72
|
+
diverged,
|
|
73
|
+
committedPrefixLen,
|
|
74
|
+
committedPrefixDiverged: false,
|
|
75
|
+
tail: content.slice(committedPrefixLen),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
/** 建一本 print 形段账(一条流一本;跨流复用会把上一条流的已发前缀算进这一条)。 */
|
|
79
|
+
export function createTextSegmentLedger() {
|
|
80
|
+
let committedPrefix = '';
|
|
81
|
+
const withheld = [];
|
|
82
|
+
return {
|
|
83
|
+
noteFlushed(text) {
|
|
84
|
+
committedPrefix += text;
|
|
85
|
+
},
|
|
86
|
+
closeSegmentAccount() {
|
|
87
|
+
committedPrefix = '';
|
|
88
|
+
},
|
|
89
|
+
applyTextEnd(content, openSegment) {
|
|
90
|
+
const verdict = resolveTextSegmentAuthority(content, { committedPrefix, openSegment });
|
|
91
|
+
if (verdict.form === 'withhold')
|
|
92
|
+
withheld.push(content);
|
|
93
|
+
// 🔴 `untouched` 形也清账:两截皆空本来就没有账可留,清它是幂等的;而漏清会把一条
|
|
94
|
+
// 「本层零经手」的段边界留成下一段的前缀(那正是 (b2) 误判的来源)。
|
|
95
|
+
committedPrefix = '';
|
|
96
|
+
return verdict;
|
|
97
|
+
},
|
|
98
|
+
repeatsWithheldSegment(candidate) {
|
|
99
|
+
const norm = candidate.trim();
|
|
100
|
+
if (norm.length === 0)
|
|
101
|
+
return false;
|
|
102
|
+
return withheld.some(w => w.trim() === norm);
|
|
103
|
+
},
|
|
104
|
+
get committedPrefixLength() {
|
|
105
|
+
return committedPrefix.length;
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
}
|
|
@@ -28,6 +28,31 @@
|
|
|
28
28
|
*/
|
|
29
29
|
import type { AdapterContext, AdapterOutput } from '../seam.js';
|
|
30
30
|
import { type Frame, type IdOf } from './ids.js';
|
|
31
|
+
/**
|
|
32
|
+
* **段身份键**(CC-01,0.68.2)—— 打在 committed assistant **文本**转录行**顶层**的 `_sema_` 超集键。
|
|
33
|
+
*
|
|
34
|
+
* ── 为什么有它(归层修,不是新功能)──────────────────────────────────────────────────────────
|
|
35
|
+
* `text_segment_end` 到达时若已提交前缀自己也过期(`committedPrefixDiverged`),宿主要在**自己持有的**
|
|
36
|
+
* 转录里找出「这一段交出去的那一行」把它换掉。按字节相等去找会被**同后缀的独立行**冒充(实撞过);
|
|
37
|
+
* 按 uuid 找只证明「是这一行」,不证明「是这一段的」。⇒ 归属要两把钥匙:行身份(`uuid`)∧ **段身份**。
|
|
38
|
+
* 段身份此前由壳在自己的过境口铸(先壳后包);段的边界(开场 / 收口 / 轮换)本来就只有本模块知道,
|
|
39
|
+
* 铸点归这里,壳只读。
|
|
40
|
+
*
|
|
41
|
+
* ── 语义(一句话)────────────────────────────────────────────────────────────────────────────
|
|
42
|
+
* **一个身份覆盖「上一次段收口 → 本次段收口」之间过境的全部 committed 文本行**:流开场一个初值;
|
|
43
|
+
* idle-flush 提交的半段与终态提交的同段共享一个身份(归属正要这个);`text_segment_end` 臂处理完之后
|
|
44
|
+
* 轮换(帧上带的是轮换**前**的那一个,与它盖过的行同值);思考→回答边界**不**轮换;子流(带
|
|
45
|
+
* `parentToolCallId`)的段边界不轮换(它们不产 leader 事件,见臂头注)。
|
|
46
|
+
*
|
|
47
|
+
* ── 铸法与形 ──────────────────────────────────────────────────────────────────────────────
|
|
48
|
+
* 派生自本窗口的**第一帧**锚 + 窗口序号(`idOf(anchor, 'segment<n>')`)⇒ **同流重放同身份**
|
|
49
|
+
* (与 committed 行的 uuid 同一条确定性纪律;宿主的双跑对拍门靠它)。🔴 它是**转录格式**上的键,
|
|
50
|
+
* 不是 wire 键:只出现在宿主落盘的转录行上,不进 provider 请求(顶层自铸键不外溢,与
|
|
51
|
+
* `_sema_degraded` 同律)。🔴 **只盖尾块是 `text` 的 assistant 行**:只带 `tool_use` 的行永远不可能是
|
|
52
|
+
* 替换目标,盖了只是往转录里加噪;思考块同理。缺席 = 「这条不属于任何引擎段」,不是「属于某个未知段」。
|
|
53
|
+
* 🔴 **只加不减**:非本形消息一个字节不动。
|
|
54
|
+
*/
|
|
55
|
+
export declare const SEMA_SEGMENT_ID_KEY = "_sema_segment_id";
|
|
31
56
|
/**
|
|
32
57
|
* {@link TextStream.replaceAnswerSegment} 的回执 —— **三件事实**,一个字节的正文都不复述。
|
|
33
58
|
*
|
|
@@ -72,6 +97,17 @@ export interface TextSegmentReplacement {
|
|
|
72
97
|
*/
|
|
73
98
|
committedPrefixDiverged: boolean;
|
|
74
99
|
}
|
|
100
|
+
/** {@link TextStream.replaceThinkingSegment} 的回执 —— 与 {@link TextSegmentReplacement} 同名同律,多一把 uuid。 */
|
|
101
|
+
export interface ThinkingSegmentReplacement {
|
|
102
|
+
/** 本包缝出来的那一块(已提交的 ∪ 还押着的)与权威全文不逐字节相等。 */
|
|
103
|
+
diverged: boolean;
|
|
104
|
+
/** 已 committed 思考行的全文长度(整块;0 = 没提交过)。 */
|
|
105
|
+
committedPrefixLen: number;
|
|
106
|
+
/** 已 committed 的那条思考行与权威全文不相等 ⇒ 本包不再交字节,宿主整块换。 */
|
|
107
|
+
committedPrefixDiverged: boolean;
|
|
108
|
+
/** `committedPrefixDiverged` 为真时在场:那条行的 `uuid`。 */
|
|
109
|
+
committedUuid?: string;
|
|
110
|
+
}
|
|
75
111
|
/** M1 对外的七个动作 + 两个读位(矩阵 §3.2 的 #2/#3 两条跨模块接口就是最后那三件)。 */
|
|
76
112
|
export interface TextStream {
|
|
77
113
|
/** A5 `text_delta` 半场:MOD-1 思考→回答边界(思考在场就先 committed 上屏)+ 段锚 + 三缓冲累加。 */
|
|
@@ -207,6 +243,33 @@ export interface TextStream {
|
|
|
207
243
|
* 🔴 它**不动** `emittedAssistantText`:那一位问的是「这一轮产没产过正文」,与消息边界无关。
|
|
208
244
|
*/
|
|
209
245
|
beginAssistantMessage(): void;
|
|
246
|
+
/**
|
|
247
|
+
* CC-02(0.68.3;server ≥7.77.0 `reasoning_end`):思考块的**权威段替换** —— `text_end` 那一套
|
|
248
|
+
* ({@link TextStream.replaceAnswerSegment})在推理面的孪生,但**只有两形**:
|
|
249
|
+
* (a) 思考还押在缓冲里(没到任何提交边界)⇒ 缓冲整块换成 `content`,活体尾巴按已泄前缀重算;
|
|
250
|
+
* (b) 思考已在边界**整块**提交(思考→回答边界最常见:`text_delta` 先到、server 的 `reasoning_end`
|
|
251
|
+
* 后到)⇒ 撤不回,一个字节都不再交;立 `committedPrefixDiverged` + 那条行的 `uuid`,由宿主整块换。
|
|
252
|
+
* (f) 本包一个字节都没经手(durable 整块思考臂 / 宿主只喂 end 不喂 delta)⇒ 什么都不做。
|
|
253
|
+
* 没有 (b1)/(c)/(e):思考块无 idle-flush、无封存、无跨工具卡累加 —— 提交是整块的,边界一到就清账。
|
|
254
|
+
* 🔴 `committedUuid` 只在 (b) 形在场;它是本包在 {@link TextStream.takeThinking} 铸的那个 uuid。
|
|
255
|
+
*/
|
|
256
|
+
replaceThinkingSegment(content: string, anchor?: Frame): ThinkingSegmentReplacement;
|
|
257
|
+
/**
|
|
258
|
+
* 当前**段身份**(语义与铸法见 {@link SEMA_SEGMENT_ID_KEY} 头注)。同一窗口内多次读**同值**;
|
|
259
|
+
* 首次读时窗口还没有帧锚(durable 整条消息先于任何增量到达)⇒ 退到 `ctx.uuid()` 并缓存。
|
|
260
|
+
*/
|
|
261
|
+
segmentId(): string;
|
|
262
|
+
/**
|
|
263
|
+
* **段界到此为止**:换新段身份(下一窗口以 `anchor` 为帧锚)。调用点恰一处 = `text_segment_end`
|
|
264
|
+
* 臂处理完之后(帧上带的是轮换前的那一个)。🔴 不在这里之外的任何地方轮换。
|
|
265
|
+
*/
|
|
266
|
+
rotateSegmentIdentity(anchor: Frame): void;
|
|
267
|
+
/**
|
|
268
|
+
* **出口单点**:committed assistant **文本**行盖当前段身份({@link SEMA_SEGMENT_ID_KEY});
|
|
269
|
+
* 非本形(chrome 事件 / 思考块 / 只带 tool_use 的行 / 非 assistant)**原样返回,一个字节不动**。
|
|
270
|
+
* 调用点恰一处 = `adapt()` 的出口(durable 整条与流式分段两条腿都从那里过)。
|
|
271
|
+
*/
|
|
272
|
+
stampSegmentIdentity(out: AdapterOutput): AdapterOutput;
|
|
210
273
|
/** D8 收口日志读位(`answer` 本体不出模块)。 */
|
|
211
274
|
readonly answerLength: number;
|
|
212
275
|
}
|
package/dist/adapt/textStream.js
CHANGED
|
@@ -1,5 +1,32 @@
|
|
|
1
1
|
import { chrome, MAIN, transcript, messageIdentityOf } from './ids.js';
|
|
2
2
|
import { estimateCjkTokens, FLUSH_INTERVAL_MS } from './wireShapes.js';
|
|
3
|
+
// L-318:六形判决单源(两条车道同吃;落账各归各层,见 textSegmentAuthority.ts 头注)。
|
|
4
|
+
import { resolveTextSegmentAuthority } from './textSegmentAuthority.js';
|
|
5
|
+
/**
|
|
6
|
+
* **段身份键**(CC-01,0.68.2)—— 打在 committed assistant **文本**转录行**顶层**的 `_sema_` 超集键。
|
|
7
|
+
*
|
|
8
|
+
* ── 为什么有它(归层修,不是新功能)──────────────────────────────────────────────────────────
|
|
9
|
+
* `text_segment_end` 到达时若已提交前缀自己也过期(`committedPrefixDiverged`),宿主要在**自己持有的**
|
|
10
|
+
* 转录里找出「这一段交出去的那一行」把它换掉。按字节相等去找会被**同后缀的独立行**冒充(实撞过);
|
|
11
|
+
* 按 uuid 找只证明「是这一行」,不证明「是这一段的」。⇒ 归属要两把钥匙:行身份(`uuid`)∧ **段身份**。
|
|
12
|
+
* 段身份此前由壳在自己的过境口铸(先壳后包);段的边界(开场 / 收口 / 轮换)本来就只有本模块知道,
|
|
13
|
+
* 铸点归这里,壳只读。
|
|
14
|
+
*
|
|
15
|
+
* ── 语义(一句话)────────────────────────────────────────────────────────────────────────────
|
|
16
|
+
* **一个身份覆盖「上一次段收口 → 本次段收口」之间过境的全部 committed 文本行**:流开场一个初值;
|
|
17
|
+
* idle-flush 提交的半段与终态提交的同段共享一个身份(归属正要这个);`text_segment_end` 臂处理完之后
|
|
18
|
+
* 轮换(帧上带的是轮换**前**的那一个,与它盖过的行同值);思考→回答边界**不**轮换;子流(带
|
|
19
|
+
* `parentToolCallId`)的段边界不轮换(它们不产 leader 事件,见臂头注)。
|
|
20
|
+
*
|
|
21
|
+
* ── 铸法与形 ──────────────────────────────────────────────────────────────────────────────
|
|
22
|
+
* 派生自本窗口的**第一帧**锚 + 窗口序号(`idOf(anchor, 'segment<n>')`)⇒ **同流重放同身份**
|
|
23
|
+
* (与 committed 行的 uuid 同一条确定性纪律;宿主的双跑对拍门靠它)。🔴 它是**转录格式**上的键,
|
|
24
|
+
* 不是 wire 键:只出现在宿主落盘的转录行上,不进 provider 请求(顶层自铸键不外溢,与
|
|
25
|
+
* `_sema_degraded` 同律)。🔴 **只盖尾块是 `text` 的 assistant 行**:只带 `tool_use` 的行永远不可能是
|
|
26
|
+
* 替换目标,盖了只是往转录里加噪;思考块同理。缺席 = 「这条不属于任何引擎段」,不是「属于某个未知段」。
|
|
27
|
+
* 🔴 **只加不减**:非本形消息一个字节不动。
|
|
28
|
+
*/
|
|
29
|
+
export const SEMA_SEGMENT_ID_KEY = '_sema_segment_id';
|
|
3
30
|
/**
|
|
4
31
|
* 件 A(#323 症状①)判据 (b) 的句末字符集 —— 中英文各一套,**闭集**(开集匹配会把「代码里的点」
|
|
5
32
|
* 之外的一切标点都当句末,等于没收紧)。换行单独在 {@link endsAtSentenceBoundary} 里判(段末)。
|
|
@@ -109,6 +136,25 @@ export function createTextStream(ctx, idOf) {
|
|
|
109
136
|
/** committed 消息的 id 锚 —— 该段/该思考块的**第一帧**(重放确定性:同流同 id)。 */
|
|
110
137
|
let segmentAnchor = null;
|
|
111
138
|
let thinkingAnchor = null;
|
|
139
|
+
/**
|
|
140
|
+
* CC-02:**当前窗口里最近一次整块提交的思考行**(正文 + 本包铸的 uuid)—— `reasoning_end` 迟到时
|
|
141
|
+
* (`text_delta` 先触发了思考→回答边界的提交)唯一能拿来算分歧、指认行的对照物。
|
|
142
|
+
* 写于 `takeThinking` 提交那一拍;清于 `replaceThinkingSegment` 用过之后、以及下一块思考开始
|
|
143
|
+
* (`feedThinking` 首条)—— 上一块的提交不是这一块的对照物。
|
|
144
|
+
*/
|
|
145
|
+
let committedThinking = null;
|
|
146
|
+
/**
|
|
147
|
+
* 段身份窗口(见 {@link SEMA_SEGMENT_ID_KEY} 头注):窗口的第一帧锚 + 序号 + 派生结果缓存。
|
|
148
|
+
* 🔴 缓存不是优化:锚缺席那一形派生落到 `ctx.uuid()`,不缓存的话同一窗口两次读会得到两个身份。
|
|
149
|
+
*/
|
|
150
|
+
let segmentWindowAnchor = null;
|
|
151
|
+
let segmentWindowSerial = 0;
|
|
152
|
+
let segmentIdCache = null;
|
|
153
|
+
const segmentIdNow = () => {
|
|
154
|
+
if (segmentIdCache === null)
|
|
155
|
+
segmentIdCache = idOf(segmentWindowAnchor ?? {}, `segment${segmentWindowSerial}`);
|
|
156
|
+
return segmentIdCache;
|
|
157
|
+
};
|
|
112
158
|
/** 提交累积的思考块(cli takeThinking:committed 形 + 关活体块 + 收活动行)。 */
|
|
113
159
|
function* takeThinking() {
|
|
114
160
|
if (thinking.length === 0)
|
|
@@ -123,6 +169,7 @@ export function createTextStream(ctx, idOf) {
|
|
|
123
169
|
session_id: ctx.sessionId,
|
|
124
170
|
parent_tool_use_id: null,
|
|
125
171
|
};
|
|
172
|
+
committedThinking = { text: thinking, uuid: msg.uuid };
|
|
126
173
|
thinking = '';
|
|
127
174
|
thinkingAnchor = null;
|
|
128
175
|
thinkingBlockOpen = false;
|
|
@@ -225,13 +272,19 @@ export function createTextStream(ctx, idOf) {
|
|
|
225
272
|
// L-310:已封存的定稿段也算「这条消息已经开了头」⇒ 锚不重置(同流重放同 id)。
|
|
226
273
|
if (answerSegment.length === 0 && segmentSealed.length === 0)
|
|
227
274
|
segmentAnchor = frame;
|
|
275
|
+
if (segmentWindowAnchor === null)
|
|
276
|
+
segmentWindowAnchor = frame;
|
|
228
277
|
answer += delta;
|
|
229
278
|
answerSegment += delta;
|
|
230
279
|
textPending += delta;
|
|
231
280
|
},
|
|
232
281
|
*feedThinking(delta, frame) {
|
|
282
|
+
if (segmentWindowAnchor === null)
|
|
283
|
+
segmentWindowAnchor = frame;
|
|
233
284
|
if (thinking.length === 0) {
|
|
234
285
|
thinkingAnchor = frame;
|
|
286
|
+
// CC-02:新一块思考开始 ⇒ 上一块的提交不再是对照物(迟到的 reasoning_end 只认当前块)。
|
|
287
|
+
committedThinking = null;
|
|
235
288
|
// P2d:elapsed 锚在**首条** leader 推理增量(token 累计由 stream_delta.estimatedTokens
|
|
236
289
|
// 承载,宿主自加;>30s 无增量的 stall 提示同样由宿主按增量时间戳判)。
|
|
237
290
|
yield chrome({ kind: 'thinking_activity', laneProof: MAIN, active: true });
|
|
@@ -255,20 +308,29 @@ export function createTextStream(ctx, idOf) {
|
|
|
255
308
|
* 而权威全文又被完整提交一次(同一段话上屏两遍 + 明文没人清)。实测复现过。
|
|
256
309
|
*/
|
|
257
310
|
const livePrefix = previousSegmentCommitted + segmentCommitted;
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
311
|
+
/**
|
|
312
|
+
* 🔴 **判决单源**(L-318,0.68.2):六形判据整只让位给 `adapt/textSegmentAuthority.ts` 的
|
|
313
|
+
* {@link resolveTextSegmentAuthority} —— 与 `-p` 形车道(宿主的 CC stdout 投影器)吃的是
|
|
314
|
+
* **同一只**。修前这里是本包这一份,壳里另有逐形对照的第二份(cli 1.0.114 B-122 热修),
|
|
315
|
+
* 上游改口时两边各走各的。判据语义**逐字未变**:
|
|
316
|
+
* · 形 (f)「本层一个字节都没经手」⇒ `form: 'untouched'`,三位全报否;
|
|
317
|
+
* · `diverged` = 缝出来的那一段 ≠ 权威全文;
|
|
318
|
+
* · `committedPrefixLen` = 已撤不回那一截的 UTF-16 长度;
|
|
319
|
+
* · `committedPrefixDiverged` 锚 `content.startsWith(前缀)`(不锚「有没有 flush 过」)。
|
|
320
|
+
* **落账仍归本模块**:下面的 handOffOnly / 活体尾巴重算 / 封存 / 补差账同步,一行未动
|
|
321
|
+
* (两条车道的账本形状不同,理由见 textSegmentAuthority.ts 头注「判决与落账刻意分家」)。
|
|
322
|
+
*/
|
|
323
|
+
const verdict = resolveTextSegmentAuthority(content, {
|
|
324
|
+
committedPrefix: livePrefix,
|
|
325
|
+
openSegment: prevSegment,
|
|
326
|
+
});
|
|
327
|
+
if (verdict.form === 'untouched') {
|
|
266
328
|
return { diverged: false, committedPrefixLen: 0, committedPrefixDiverged: false };
|
|
267
329
|
}
|
|
268
330
|
const liveSegment = livePrefix + prevSegment;
|
|
269
|
-
const diverged =
|
|
270
|
-
const committedPrefixLen =
|
|
271
|
-
const committedPrefixDiverged =
|
|
331
|
+
const diverged = verdict.diverged;
|
|
332
|
+
const committedPrefixLen = verdict.committedPrefixLen;
|
|
333
|
+
const committedPrefixDiverged = verdict.committedPrefixDiverged;
|
|
272
334
|
/**
|
|
273
335
|
* 🔴 **段跨过包侧边界、而且已提交那截自己也过期** ⇒ 本模块**一个字节都不动**(只发信号)。
|
|
274
336
|
*
|
|
@@ -326,7 +388,9 @@ export function createTextStream(ctx, idOf) {
|
|
|
326
388
|
}
|
|
327
389
|
}
|
|
328
390
|
else {
|
|
329
|
-
|
|
391
|
+
// L-318:尾段切片也走判决那一份(`content.slice(committedPrefixLen)` 逐字等价),
|
|
392
|
+
// 免得「怎么切」这件事在两条车道上各留一个算式。
|
|
393
|
+
segmentSealed += verdict.tail;
|
|
330
394
|
}
|
|
331
395
|
answerSegment = '';
|
|
332
396
|
// ── ③ `answer`(D8 调试行的长度读位)跟着换尾 —— 只在尾巴真对得上时换 ──────────────────
|
|
@@ -366,6 +430,52 @@ export function createTextStream(ctx, idOf) {
|
|
|
366
430
|
if (typeof committed === 'string')
|
|
367
431
|
committedText += committed;
|
|
368
432
|
},
|
|
433
|
+
replaceThinkingSegment: (content) => {
|
|
434
|
+
// (b) 已整块提交:撤不回 ⇒ 只算分歧、指认行,一个字节都不再交。
|
|
435
|
+
if (thinking.length === 0) {
|
|
436
|
+
const c = committedThinking;
|
|
437
|
+
committedThinking = null;
|
|
438
|
+
if (c === null || c.text.length === 0) {
|
|
439
|
+
// (f):本包没经手过这一块(durable 整块臂铸过 transcript 行 / 宿主只喂 end)。
|
|
440
|
+
return { diverged: false, committedPrefixLen: 0, committedPrefixDiverged: false };
|
|
441
|
+
}
|
|
442
|
+
const diverged = c.text !== content;
|
|
443
|
+
return diverged
|
|
444
|
+
? { diverged: true, committedPrefixLen: c.text.length, committedPrefixDiverged: true, committedUuid: c.uuid }
|
|
445
|
+
: { diverged: false, committedPrefixLen: c.text.length, committedPrefixDiverged: false };
|
|
446
|
+
}
|
|
447
|
+
// (a) 还押在缓冲里:整块换,活体尾巴按已泄前缀重算(与 replaceAnswerSegment 的活体面同律)。
|
|
448
|
+
const prev = thinking;
|
|
449
|
+
const diverged = prev !== content;
|
|
450
|
+
const emitted = prev.slice(0, prev.length - thinkingPending.length);
|
|
451
|
+
thinkingPending = content.startsWith(emitted) ? content.slice(emitted.length) : '';
|
|
452
|
+
thinking = content;
|
|
453
|
+
committedThinking = null;
|
|
454
|
+
return { diverged, committedPrefixLen: 0, committedPrefixDiverged: false };
|
|
455
|
+
},
|
|
456
|
+
segmentId: segmentIdNow,
|
|
457
|
+
rotateSegmentIdentity: (anchor) => {
|
|
458
|
+
segmentWindowSerial += 1;
|
|
459
|
+
segmentWindowAnchor = anchor;
|
|
460
|
+
segmentIdCache = null;
|
|
461
|
+
},
|
|
462
|
+
stampSegmentIdentity: (out) => {
|
|
463
|
+
if (out.plane !== 'transcript')
|
|
464
|
+
return out;
|
|
465
|
+
// ⚠️ 局部名刻意不叫 `msg`/`content`:message-branching 门把 `msg.message` 读成「错误文案」,
|
|
466
|
+
// 并按名字把污染扩到整个文件 —— 同名的 `content.startsWith` 会被误判成按文案分支。
|
|
467
|
+
const row = out.message;
|
|
468
|
+
if (row.type !== 'assistant')
|
|
469
|
+
return out;
|
|
470
|
+
const blocks = row.message?.content;
|
|
471
|
+
if (!Array.isArray(blocks) || blocks.length === 0)
|
|
472
|
+
return out;
|
|
473
|
+
const last = blocks[blocks.length - 1];
|
|
474
|
+
if (last === undefined || last.type !== 'text' || typeof last.text !== 'string')
|
|
475
|
+
return out;
|
|
476
|
+
out.message[SEMA_SEGMENT_ID_KEY] = segmentIdNow();
|
|
477
|
+
return out;
|
|
478
|
+
},
|
|
369
479
|
beginAssistantMessage: () => {
|
|
370
480
|
if (committedText.length > 0)
|
|
371
481
|
previousCommittedText = committedText;
|
package/dist/adapt.js
CHANGED
|
@@ -203,7 +203,20 @@ class WireToCcAdapterImpl {
|
|
|
203
203
|
// 壳 B4 的队列端口回钩仍可调它 —— 三条入队链本来就写同一个集合,回钩因此变成幂等 no-op。
|
|
204
204
|
noteWorkflowCompletionCardEnqueued(runId);
|
|
205
205
|
}
|
|
206
|
+
/**
|
|
207
|
+
* CC-01(0.68.2):`adapt()` 的**出口单点** —— 每条产出过境时由 M1 盖段身份
|
|
208
|
+
* ({@link TextStream.stampSegmentIdentity};只盖 committed assistant 文本行,其余原样)。
|
|
209
|
+
* 放在出口而不放在各铸点:durable 整条消息(`assistant` 臂)与流式分段(M1 提交口)是两条腿,
|
|
210
|
+
* 归属键必须两条腿都盖,而「过境」是它们唯一的公共点(壳此前也正是在自己的过境口盖的)。
|
|
211
|
+
* 🔴 只包一层转发,不改内层的手动 `next()` 竞速与 abort 语义(T40/T58 全在内层)。
|
|
212
|
+
*/
|
|
206
213
|
async *adapt(frames, ctx) {
|
|
214
|
+
const slot = {};
|
|
215
|
+
for await (const out of this.adaptUnstamped(frames, ctx, slot)) {
|
|
216
|
+
yield slot.text === undefined ? out : slot.text.stampSegmentIdentity(out);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
async *adaptUnstamped(frames, ctx, slot) {
|
|
207
220
|
// FIX-05:实例台账走**唯一一条**显式路径(拆分前是 `self.` / `this.` 两条)。
|
|
208
221
|
const inst = this.ledger;
|
|
209
222
|
const turnStartAt = ctx.now();
|
|
@@ -211,6 +224,7 @@ class WireToCcAdapterImpl {
|
|
|
211
224
|
const idOf = makeIdOf(ctx);
|
|
212
225
|
// ── M1 流合并器(矩阵 §1.1 的 12 行状态整体落 adapt/textStream.ts)────────────────────────
|
|
213
226
|
const text = createTextStream(ctx, idOf);
|
|
227
|
+
slot.text = text;
|
|
214
228
|
// ── M2 工具卡台账(矩阵 §1.2 的 3 行状态整体落 adapt/toolCards.ts)────────────────────────
|
|
215
229
|
const cards = createToolCardLedger(ctx, idOf);
|
|
216
230
|
// ── M3 面板行台账(矩阵 §1.3 的 6 行状态 + 五个闭包整体落 adapt/panelTasks.ts)──────────
|
|
@@ -231,6 +231,25 @@ export interface WiringManifestMcpEntry {
|
|
|
231
231
|
*/
|
|
232
232
|
httpStatus?: number;
|
|
233
233
|
}
|
|
234
|
+
/**
|
|
235
|
+
* `wiring_manifest.mcp` 的形校验(S-124 / core 7.5.0,server ≥7.60.0)。
|
|
236
|
+
*
|
|
237
|
+
* 🔴 **空数组 ≠ 缺席,两者都要能被消费端分辨**(core 顶注逐字:"an empty array is *this leg
|
|
238
|
+
* declared no servers*, absence is an older mint or an external derivation")⇒ **真的**空数组
|
|
239
|
+
* 原样铸成 `[]`。把它折成缺席 = 把「我一台都没申报」这句**正面事实**说成「不知道」。
|
|
240
|
+
* 🔴 **但「过滤后为空」不是「原本就是空」**(异源对抗复审 r1 [medium] 采纳,真病):一份
|
|
241
|
+
* `[{name:'github', errorCode:'http_503'}]`(缺 `status`)进来时,唯一那一行被下面的逐行判据
|
|
242
|
+
* 丢掉 —— 若照样铸出 `[]`,消费端按本臂的义务④读到的是**「这条腿一台都没申报」这句肯定话**,
|
|
243
|
+
* 一份读不懂的回体就此被伪装成一个确定的零申报。⇒ **非空输入而零行幸存 ⇒ 整段缺席**
|
|
244
|
+
* (「我读不出来」不是「我知道是零」)。这与下面「逐条独立」并不矛盾:只要**还有幸存者**,
|
|
245
|
+
* 半张表仍是真读数,坏行照旧只丢自己。
|
|
246
|
+
* 🔴 **逐条独立**:一条坏行只丢自己(与 server 侧 `continue` 同判据),绝不因为一条坏行丢整表。
|
|
247
|
+
* 🔴 **逐键挑,禁 spread**:core 往条目上加新键时必须由人显式处置 —— 而它下一个加的很可能又是
|
|
248
|
+
* 一个像 `error` 那样的自由文本面。(0.60.0 按此显式处置了 S6-B 的 `delivered` / `httpStatus`。)
|
|
249
|
+
* ⚠️ `toolCount` 按**有限数**判(`Number.isFinite`),不按真值判:`0` 是合法读数(连上了、零工具),
|
|
250
|
+
* 折成缺席会让「连上了但没工具」与「没报」在消费端同形。
|
|
251
|
+
*/
|
|
252
|
+
export declare function projectMcpSection(raw: unknown): WiringManifestMcpEntry[] | undefined;
|
|
234
253
|
/**
|
|
235
254
|
* CS-7 §2.7 — turn_end usage → CC `ModelUsage` (pinned name mapping;
|
|
236
255
|
* costMicroUsd/1e6 → costUSD). Surfaced separately because the slice has no
|
|
@@ -49,8 +49,12 @@ export const INTERNAL_SDK_ARM_TYPES = new Set([
|
|
|
49
49
|
'turn_usage',
|
|
50
50
|
// #310 / #318 件①:引擎结构化通告的会话面(raw 预分派铸点,见 eventToSdkMessage 顶部)。
|
|
51
51
|
'engine_notice',
|
|
52
|
-
// #323 / core #447:assistant 流式**散文段边界**(
|
|
52
|
+
// #323 / core #447:assistant 流式**散文段边界**(sdk 9.4.0 起进 union,`case 'text_end'`;见 `textEndProjection` 头注)。
|
|
53
53
|
'text_end',
|
|
54
|
+
// CC-02(0.68.3;server ≥7.77.0 S-310 / sdk ≥9.4.0 `SegmentEndFields`):**推理段边界** + 权威全文,
|
|
55
|
+
// `text_end` 的孪生(形逐字同,空段判据归 server)。消费口 = `adapt/arms.ts` 的 `thinkingSegmentEndArm`。
|
|
56
|
+
// ⚠️ 三端:print 车道对内部臂「要么吞、要么转成真 CC 帧」—— 本臂在 CC stdout 帧全集零同位帧,壳按 `text_end` 同律接。
|
|
57
|
+
'reasoning_end',
|
|
54
58
|
// L-70 / L-108②③(core #524 + core 147③,server ≥7.58.0):`wiring_manifest` 帧上**两个新段**的
|
|
55
59
|
// 超集投影(`_sema_modelGate` / `_sema_autoMode`)。整份 manifest 的其余段仍不在本切片里 ——
|
|
56
60
|
// 射程写在 `case 'wiring_manifest'` 头注,别读成「manifest 接上了」。
|
|
@@ -150,10 +154,27 @@ export function eventToSdkMessage(ev, ctx) {
|
|
|
150
154
|
// 🔴 **到期复核(自退休,不靠人记)**:预分派用 `(ev as {type?:unknown})` 形读判别键,**不收窄** `ev`
|
|
151
155
|
// ⇒ 臂一进 union,switch 的 `default` 仍看得见它,B5 穷举断言 `assertNeverArm` **编译期真红**,
|
|
152
156
|
// 逼下一棒把它搬进 switch。搬进去时行为一字不改(下面的投影函数原样复用)。
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
}
|
|
157
|
+
// 🔴 **到期复核已兑现(sdk 9.4.0 提货,0.68.3 CC-02)**:`text_end` 进了 union(与 `reasoning_end` 共用
|
|
158
|
+
// `SegmentEndFields`),按原定计划搬进 switch;`textEndProjection` 原样复用,行为一字不改。
|
|
156
159
|
switch (ev.type) {
|
|
160
|
+
case 'text_end':
|
|
161
|
+
return textEndProjection(ev, ctx);
|
|
162
|
+
/**
|
|
163
|
+
* CC-02(0.68.3):`reasoning_end` —— `text_end` 的推理面孪生(server ≥7.77.0 自铸,core 不发;sdk 9.4.0
|
|
164
|
+
* 两枚帧一份形 `SegmentEndFields`)。处置逐条同 `text_end`:中性内部臂、绝不铸 transcript 行、绝不
|
|
165
|
+
* `not_in_slice`、`content` 非串/lane 位坏 ⇒ malformed、空串 ⇒ empty_payload、身份两键原样透传。
|
|
166
|
+
* 差别只在消费口:`adapt/arms.ts` 的 `thinkingSegmentEndArm` 做**思考块**的整段替换 + chrome
|
|
167
|
+
* `thinking_segment_end`(两形,见 `TextStream.replaceThinkingSegment`)。
|
|
168
|
+
*/
|
|
169
|
+
case 'reasoning_end':
|
|
170
|
+
return segmentEndProjection(ev, ctx, 'reasoning_end');
|
|
171
|
+
/**
|
|
172
|
+
* L-315(候 0.68.4):`tool_roster_delta` —— 名册增量。本包今天不投 `wiring_manifest.tools`(切片只投
|
|
173
|
+
* 三段),名册增量没有可挂的基线 ⇒ **有痕 dropped**,与 0.65.0 前的 `approval_request` 同形(不静默、
|
|
174
|
+
* 不 not_in_slice);接上 `tools` 段的那一批把它一起接。
|
|
175
|
+
*/
|
|
176
|
+
case 'tool_roster_delta':
|
|
177
|
+
return dropped('unsupported_arm', 'tool_roster_delta');
|
|
157
178
|
// ── `human_input`(core 5.14.0 design/171 / server 7.4.0 SSE,[3017]/[3020])────────────
|
|
158
179
|
// 🔴 **到期复核已兑现(sdk 6.9.0 提货,2026-08-08)**:本臂此前是 switch **之前**的一条 raw
|
|
159
180
|
// 预分派(理由 = 它还没进 SDK 6.3.0 的 `AgentEvent` union,`case 'human_input'` 在 `ev.type`
|
|
@@ -922,9 +943,16 @@ export function eventToSdkMessage(ev, ctx) {
|
|
|
922
943
|
* ⚠️ **UNTRUSTED、仅展示**:`content` 是模型输出,契约与 `text_delta` 同 —— 渲染,绝不回喂模型。
|
|
923
944
|
*/
|
|
924
945
|
function textEndProjection(ev, ctx) {
|
|
946
|
+
return segmentEndProjection(ev, ctx, 'text_end');
|
|
947
|
+
}
|
|
948
|
+
/**
|
|
949
|
+
* 段末帧(`text_end` / `reasoning_end`)的**共用投影**(sdk 9.4.0 `SegmentEndFields` 单源 ⇒ 本侧也只写一份;
|
|
950
|
+
* 判据与留痕见 {@link textEndProjection} 头注,两枚帧逐字同)。`arm` 只决定内部臂名与留痕里的帧名。
|
|
951
|
+
*/
|
|
952
|
+
function segmentEndProjection(ev, ctx, arm) {
|
|
925
953
|
const content = ev.content;
|
|
926
954
|
if (typeof content !== 'string')
|
|
927
|
-
return dropped('malformed',
|
|
955
|
+
return dropped('malformed', arm);
|
|
928
956
|
// 🔴 **lane 身份位坏了就整帧 fail-closed**(异源对抗复审第三轮 [medium] 采纳;与本文件
|
|
929
957
|
// `applyBgNotification` 对 `parentTaskId` 的 B3-DIRTY 处置逐字同族):`parentToolCallId` 是
|
|
930
958
|
// **子流断闸的锚**。键在场却不是串时,若按「不是串就当没有」处理,这条**子代**的段边界会被
|
|
@@ -933,18 +961,18 @@ function textEndProjection(ev, ctx) {
|
|
|
933
961
|
// 三态与 B3-DIRTY 同:键缺席/`undefined` ⇒ 本来就是 leader 帧(照旧);键在场却非串(含 `null`,
|
|
934
962
|
// wire schema 只允许缺席或 string)⇒ `malformed` 丢弃并留痕,坏值不许买路。
|
|
935
963
|
if (ev.parentToolCallId !== undefined && typeof ev.parentToolCallId !== 'string') {
|
|
936
|
-
return dropped('malformed',
|
|
964
|
+
return dropped('malformed', arm);
|
|
937
965
|
}
|
|
938
966
|
if (content.length === 0)
|
|
939
967
|
return nothing('empty_payload');
|
|
940
|
-
|
|
941
|
-
type: 'text_end',
|
|
942
|
-
content,
|
|
968
|
+
const identity = {
|
|
943
969
|
...(typeof ev.eventId === 'string' ? { eventId: ev.eventId } : {}),
|
|
944
|
-
...(typeof ev.parentToolCallId === 'string'
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
970
|
+
...(typeof ev.parentToolCallId === 'string' ? { parentToolCallId: ev.parentToolCallId } : {}),
|
|
971
|
+
};
|
|
972
|
+
// 两处字面量铸点(不是一处参数化):内部臂词表门按 `armBody({ type: '<字面量>'` 抽铸点,参数化会让两臂在账上「多登记」。
|
|
973
|
+
return arm === 'text_end'
|
|
974
|
+
? projected(stamp(ctx, armBody({ type: 'text_end', content, ...identity })))
|
|
975
|
+
: projected(stamp(ctx, armBody({ type: 'reasoning_end', content, ...identity })));
|
|
948
976
|
}
|
|
949
977
|
/**
|
|
950
978
|
* `wiring_manifest.mcp` 的形校验(S-124 / core 7.5.0,server ≥7.60.0)。
|
|
@@ -964,7 +992,7 @@ function textEndProjection(ev, ctx) {
|
|
|
964
992
|
* ⚠️ `toolCount` 按**有限数**判(`Number.isFinite`),不按真值判:`0` 是合法读数(连上了、零工具),
|
|
965
993
|
* 折成缺席会让「连上了但没工具」与「没报」在消费端同形。
|
|
966
994
|
*/
|
|
967
|
-
function projectMcpSection(raw) {
|
|
995
|
+
export function projectMcpSection(raw) {
|
|
968
996
|
if (!Array.isArray(raw))
|
|
969
997
|
return undefined;
|
|
970
998
|
const rows = [];
|