@sema-agent/client-core 0.62.0 → 0.62.1
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 +33 -0
- package/README.md +1 -1
- package/dist/adapter/runStream.js +1 -0
- package/dist/subagentContentStore.d.ts +7 -1
- package/dist/subagentContentStore.js +153 -15
- package/docs/INTEGRATION-CLIENTS.md +74 -11
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -49,6 +49,39 @@
|
|
|
49
49
|
> 挡住 ⇒ 本批把它机械化——④a0 对 `pending` 行**要求段头已是日期形**(`(未发布)` 直接红),阶段一
|
|
50
50
|
> commit 漏转在发布前就红,不再靠人记。
|
|
51
51
|
|
|
52
|
+
## 0.62.1(2026-09-09)
|
|
53
|
+
|
|
54
|
+
> patch 内容批(两件)。**本段只记内容**:`package.json.version` 未动、README `Version` 行未动、
|
|
55
|
+
> `FROZEN` 未动 —— bump 与段头转日期形归**发包批**(阶段一义务见本档头注)。两件都**零新导出、
|
|
56
|
+
> 零型面变化**,老宿主一个字节不改照常跑;`peerDependencies` 未动(sdk 地板仍 `>=8.5.0`)。
|
|
57
|
+
|
|
58
|
+
- 🔴 **子代内容账本:工具两臂补上重放幂等闸**(行为**修复**)。0.62.0 只给聚合两臂
|
|
59
|
+
(`text` / `reasoning`)上了两道闸,`tool_start` / `tool_end` **一道都没有** —— 断线重连后
|
|
60
|
+
durable 腿整轮重放时,同一次工具调用在子代查看态里排出**两张卡**、结果正文被记两遍
|
|
61
|
+
(内容字节预算跟着虚高)。现形:判据形与散文两臂相同(先问身份、再问账本),身份 = `toolCallId`、
|
|
62
|
+
账本 = 查看态 `items` 上那张卡 —— **刻意不共用聚合帧那本有界身份记号**(它的额度按「聚合帧只在
|
|
63
|
+
重放腿上来」定,而工具帧活体也来,跟着写会把还在屏上的散文身份挤掉)。
|
|
64
|
+
🔴 判据不是「见过没见过」而是**阶段的先后**:同一个 `toolCallId` 真的会有**第二对** start/end ——
|
|
65
|
+
门把调用 park 掉、批复后恢复执行时引擎复用同一个 id 再发一对,第二对带的才是真结果(批复还可能
|
|
66
|
+
改过参数)。库为每张卡记一条**阶段水位**(已落账的 start / end 身份),按 `eventId` 的先后判新旧:
|
|
67
|
+
比水位旧或就是同一条帧 ⇒ 整帧丢弃;更新 ⇒ 后到的才算数、就地替换(按差额记账)。可比性的前提是
|
|
68
|
+
`eventId` **时间可排序**(上游铸的 uuidv7);缺席 ⇒ 判不出 ⇒ 退回「后到的落账」(空操作)。
|
|
69
|
+
水位与卡**同生共死**。四条细则:①`tool_start` 不铸第二张卡;这张卡**还没落过 start**(收口先到时
|
|
70
|
+
合成的占位卡)⇒ 先段闭合再补齐名参,水位已在 ⇒ 不段闭合;②旧一阶段的 start 不许把改后参数盖回;
|
|
71
|
+
③`tool_end` **先问卡还开着吗** —— 开着就落结果(在飞的卡被拦掉收口 = 永远在飞);④收口找不到卡时
|
|
72
|
+
合成一张已完成的卡、**不**在那里段闭合(边界由迟到的 start 补做;在收口处闭合会把还在流的一段
|
|
73
|
+
提前切成历史 item,紧帽下随即被丢)。去重射程严格是**同一次调用** —— 另一个 `toolCallId` 的卡照常进。
|
|
74
|
+
判据见 `docs/INTEGRATION-CLIENTS.md` §26b、§26j **G-14**;常驻门
|
|
75
|
+
`scripts/run-subagent-durable-divert-test.mjs` 新增 G8 / G9 / G10 / G11 四段。
|
|
76
|
+
- 📋 **§26j 黑盒判据 G-12 改写**(判据面订正,**产品字节零变化**)。原文「诊断行显示长度**不超过**
|
|
77
|
+
同长度良性值那一行」超出实现的承诺:呈前封长是**先转义、后按转义结果封长**,一个不可见字符转义后
|
|
78
|
+
占 6 个显示字符 —— 转义后仍在帽内时一枚都不用截,那一行必然**长于**同长度的良性行(良性行 1:1)。
|
|
79
|
+
改写后的 G-12 判的是真正承诺的量:**≤ 帽、行尾无半截转义序列、转义后仍在帽内的值不截**,且判据量
|
|
80
|
+
一律是**转义后的显示长度**(原文短于帽但转义后超帽 ⇒ 照样截)。常驻门
|
|
81
|
+
`scripts/run-display-cap-order-test.mjs` 同批补三档(转义后在帽内不截且逐序列完整 / 原文短而转义
|
|
82
|
+
超帽照截在序列边界 / 恰好抵帽时行尾不是半截)。
|
|
83
|
+
- 子代内容账本:迟到的 `tool_start` 触发条数裁剪后不再按裁剪前的下标/引用改名补参、写差额(异源复审第四轮 [high]);flush 之后按 `toolCallId` 重新找卡,卡已被裁掉 ⇒ 按「新的一次」重建(§26b 留白①);门 G12 三格。
|
|
84
|
+
|
|
52
85
|
## 0.62.0(2026-09-09)
|
|
53
86
|
|
|
54
87
|
> 内容批(隔离树交付,七件)。**本段只记内容**(0.60.0/0.61.0 先例逐字):`package.json.version`
|
package/README.md
CHANGED
|
@@ -35,7 +35,7 @@ Renamed from **`@sema-agent/wire-cc-adapter`** (0.1.x, deprecated — see *Migra
|
|
|
35
35
|
|
|
36
36
|
## Scope
|
|
37
37
|
|
|
38
|
-
**Version:** 0.62.
|
|
38
|
+
**Version:** 0.62.1
|
|
39
39
|
|
|
40
40
|
- **Today** — the adapter seam, the whole `adapt()` pipeline (all 14 A-layer arms plus the
|
|
41
41
|
B/D/E tool-card layers), the notification/caps/model families, the adapter kernel (stream driver
|
|
@@ -331,6 +331,7 @@ async function* runStreamInner(events, ctx, handle = {}) {
|
|
|
331
331
|
text: sub.text,
|
|
332
332
|
// 聚合帧的**幂等键**:durable 重放会把整轮的段再送一遍,而缝前的事件序号去重只在**同一条**
|
|
333
333
|
// 流内有效(重连后是新的一条流)。把身份交给 store,让跨流的重放也只收一次。
|
|
334
|
+
// (工具两臂不读这一位 —— store 侧按 `toolCallId` 与账本上那张卡判,见 subagentContentStore。)
|
|
334
335
|
eventId: sub.eventId,
|
|
335
336
|
toolCallId: sub.toolCallId,
|
|
336
337
|
toolName: sub.toolName,
|
|
@@ -13,6 +13,11 @@
|
|
|
13
13
|
* —— 🔴 **durable 腿(重放)送的是同一段的聚合形** `text` / `reasoning`(整段全文,同样带
|
|
14
14
|
* EventIdentity)。两类都要收:只收增量的话,续听重放尾的宿主整条子代转录都是空的
|
|
15
15
|
* (0.62.0 补;两类的处置**不同**,见 {@link publishSubagentContentEvent} 的吸收规则)
|
|
16
|
+
* 🔴 **重放的幂等闸覆盖四个臂**(`text` / `reasoning` / `tool_start` / `tool_end`):重连 = 新的一条
|
|
17
|
+
* 流,缝前那道事件序号去重只在同一条流内有效,所以「整轮重放」拦得住的只能是这本账。判据形相同
|
|
18
|
+
* (先问身份、再问账本),身份与账本各按臂取:散文两臂 = 聚合帧 `eventId` + 已上屏的整段正文;
|
|
19
|
+
* 工具两臂 = `toolCallId` + `items` 上那张卡,收口另带一本**自己额度**的收口身份账(同一次调用
|
|
20
|
+
* 会有**第二发**真收口 —— 门 park 之后恢复执行,见 {@link publishSubagentContentEvent})。
|
|
16
21
|
* (§E1 redact upstream; §E2 identity). 🔴 EventIdentity 的键是 `eventId` / `parentToolCallId`
|
|
17
22
|
* (+ LIVE 白名单四臂上的 `sourceTaskId` / `bgAgentId`)—— **没有 `taskId`**(sdk `events.d.ts` 的
|
|
18
23
|
* `interface EventIdentity` 直证;`taskId` 只长在 `meta` 首帧上)。所以内容帧到不了「自带引擎
|
|
@@ -91,7 +96,8 @@ export interface SubagentContentEvent {
|
|
|
91
96
|
/** 聚合两臂(`text` / `reasoning`)的**整段全文**。刻意不复用 `delta` 位:同名不同义会让
|
|
92
97
|
* 「这一位是增量还是全文」变成读者要靠 `type` 去反推的事,而反推错的代价是内容重复上屏。 */
|
|
93
98
|
text?: string | undefined;
|
|
94
|
-
/** wire 上那一帧的**事件身份**(聚合两臂的幂等键;缺席 ⇒ 退回内容判据,见 publish 的两道闸)。
|
|
99
|
+
/** wire 上那一帧的**事件身份**(聚合两臂的幂等键;缺席 ⇒ 退回内容判据,见 publish 的两道闸)。
|
|
100
|
+
* 🔴 工具两臂**不读这一位**:那次调用的身份是 `toolCallId`,账本是 `items` 上那张卡。 */
|
|
95
101
|
eventId?: string | undefined;
|
|
96
102
|
toolCallId?: string | undefined;
|
|
97
103
|
toolName?: string | undefined;
|
|
@@ -13,6 +13,11 @@
|
|
|
13
13
|
* —— 🔴 **durable 腿(重放)送的是同一段的聚合形** `text` / `reasoning`(整段全文,同样带
|
|
14
14
|
* EventIdentity)。两类都要收:只收增量的话,续听重放尾的宿主整条子代转录都是空的
|
|
15
15
|
* (0.62.0 补;两类的处置**不同**,见 {@link publishSubagentContentEvent} 的吸收规则)
|
|
16
|
+
* 🔴 **重放的幂等闸覆盖四个臂**(`text` / `reasoning` / `tool_start` / `tool_end`):重连 = 新的一条
|
|
17
|
+
* 流,缝前那道事件序号去重只在同一条流内有效,所以「整轮重放」拦得住的只能是这本账。判据形相同
|
|
18
|
+
* (先问身份、再问账本),身份与账本各按臂取:散文两臂 = 聚合帧 `eventId` + 已上屏的整段正文;
|
|
19
|
+
* 工具两臂 = `toolCallId` + `items` 上那张卡,收口另带一本**自己额度**的收口身份账(同一次调用
|
|
20
|
+
* 会有**第二发**真收口 —— 门 park 之后恢复执行,见 {@link publishSubagentContentEvent})。
|
|
16
21
|
* (§E1 redact upstream; §E2 identity). 🔴 EventIdentity 的键是 `eventId` / `parentToolCallId`
|
|
17
22
|
* (+ LIVE 白名单四臂上的 `sourceTaskId` / `bgAgentId`)—— **没有 `taskId`**(sdk `events.d.ts` 的
|
|
18
23
|
* `interface EventIdentity` 直证;`taskId` 只长在 `meta` 首帧上)。所以内容帧到不了「自带引擎
|
|
@@ -253,13 +258,24 @@ function noteDropped(s, dropped) {
|
|
|
253
258
|
s.itemBytes.unshift(0);
|
|
254
259
|
rebuildOpenTools(s);
|
|
255
260
|
}
|
|
256
|
-
/** items 被增删之后重建「开着的工具卡 → 下标」表(下标随 splice/unshift 整体漂移)。
|
|
261
|
+
/** items 被增删之后重建「开着的工具卡 → 下标」表(下标随 splice/unshift 整体漂移)。
|
|
262
|
+
* 🔴 同批把**卡已不在 items 上**的阶段水位清掉:水位是「这张卡现在承载的是哪一阶段」,卡没了它
|
|
263
|
+
* 就没有指涉对象,而这张表按 `toolCallId` 一条一条长 —— 不跟着卡清 = 一条长 turn 上的无界增长
|
|
264
|
+
* (本账本的立身之本就是有界)。守的是内存面,不是行为面,理由见 `toolPhase` 头注的那一笔。 */
|
|
257
265
|
function rebuildOpenTools(s) {
|
|
258
266
|
s.openTools.clear();
|
|
267
|
+
const live = new Set();
|
|
259
268
|
s.items.forEach((it, i) => {
|
|
260
|
-
if (it.kind === 'tool'
|
|
261
|
-
|
|
269
|
+
if (it.kind === 'tool') {
|
|
270
|
+
live.add(it.id);
|
|
271
|
+
if (it.output === undefined)
|
|
272
|
+
s.openTools.set(it.id, i);
|
|
273
|
+
}
|
|
262
274
|
});
|
|
275
|
+
for (const id of s.toolPhase.keys()) {
|
|
276
|
+
if (!live.has(id))
|
|
277
|
+
s.toolPhase.delete(id);
|
|
278
|
+
}
|
|
263
279
|
}
|
|
264
280
|
/**
|
|
265
281
|
* 每子代帽:超出就**从最早的内容开始丢**,丢不动了再从缓冲头部裁。
|
|
@@ -365,6 +381,7 @@ function stateFor(taskId, parentToolCallId) {
|
|
|
365
381
|
droppedBytes: 0,
|
|
366
382
|
seenAggregateIds: new Set(),
|
|
367
383
|
recordedSegments: new Set(),
|
|
384
|
+
toolPhase: new Map(),
|
|
368
385
|
};
|
|
369
386
|
tasks.set(taskId, s);
|
|
370
387
|
}
|
|
@@ -497,7 +514,10 @@ export function registerSubagentAlias(parentToolCallId, taskId) {
|
|
|
497
514
|
/**
|
|
498
515
|
* 这一条**聚合帧**是不是「已经收下过的那一条」(durable 重放的两道闸;命中 ⇒ 整帧丢弃)。
|
|
499
516
|
*
|
|
500
|
-
*
|
|
517
|
+
* 🔴 工具两臂**同一条判据形、不同的身份**:它们不进这一口(见 publish 的工具两臂),因为
|
|
518
|
+
* `toolCallId` 就是那次调用的身份、账本就是 `items` 自己 —— 比一本有界 FIFO 强,也不占它的额度。
|
|
519
|
+
*
|
|
520
|
+
* 为什么光靠缓冲前缀吸收不够(异源复审逼出的真病):吸收规则只认**当前缓冲**那一段。一条
|
|
501
521
|
* 跨过工具边界的 turn,早先那些段早就段闭合成 item、缓冲已空 —— 重放时它们的聚合帧再来一遍,
|
|
502
522
|
* `''` 是任何串的前缀,于是整段被当成新内容塞回缓冲,而且塞在**后面那段**之后(顺序也是错的)。
|
|
503
523
|
*
|
|
@@ -521,6 +541,53 @@ function replaySeen(s, ev) {
|
|
|
521
541
|
return true;
|
|
522
542
|
return false;
|
|
523
543
|
}
|
|
544
|
+
/**
|
|
545
|
+
* 这一发工具帧比卡上现在承载的那一阶段**旧或一样**吗(旧 ⇒ 整帧丢弃)。
|
|
546
|
+
*
|
|
547
|
+
* 判据是 `eventId` 的字典序 —— 前提是它**时间可排序**(core 铸的 uuidv7,sdk `EventIdentity` 逐字)。
|
|
548
|
+
* 两侧任一缺席 ⇒ 判不出新旧,答 `false`(退回「后到的落账」:重放送的是同一份值,落账是空操作)。
|
|
549
|
+
* 逐字相等 ⇒ 同一条帧又来一遍 ⇒ 同样答 `true`(整帧丢弃,一次也不重复落账)。
|
|
550
|
+
*/
|
|
551
|
+
function notNewer(eventId, mark) {
|
|
552
|
+
if (typeof eventId !== 'string' || eventId.length === 0)
|
|
553
|
+
return false;
|
|
554
|
+
if (typeof mark !== 'string' || mark.length === 0)
|
|
555
|
+
return false;
|
|
556
|
+
return eventId <= mark;
|
|
557
|
+
}
|
|
558
|
+
/** 读一张卡的阶段水位(没有 ⇒ 这张卡还没落过对应那一半的帧)。 */
|
|
559
|
+
function phaseOf(s, id) {
|
|
560
|
+
return id === undefined ? undefined : s.toolPhase.get(id);
|
|
561
|
+
}
|
|
562
|
+
/** 抬一张卡的阶段水位(身份缺席 ⇒ 不抬:一个抬不动的水位比一个抬错的水位诚实)。 */
|
|
563
|
+
function markPhase(s, id, half, eventId) {
|
|
564
|
+
if (id === undefined || typeof eventId !== 'string' || eventId.length === 0)
|
|
565
|
+
return;
|
|
566
|
+
const cur = s.toolPhase.get(id);
|
|
567
|
+
if (cur === undefined) {
|
|
568
|
+
s.toolPhase.set(id, { [half]: eventId });
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
cur[half] = eventId;
|
|
572
|
+
}
|
|
573
|
+
/**
|
|
574
|
+
* 这条 `toolCallId` 在本账本里已经有卡了吗(有 ⇒ 返回下标,没有 ⇒ `-1`)。
|
|
575
|
+
*
|
|
576
|
+
* 🔴 为什么账本就是 `items` 自己、不另铸第三本小账:一次工具调用的卡**在不在屏上**只有 `items`
|
|
577
|
+
* 说了算。另开一本记号账就会有两处真源,而它们脱节的方式是可预期的 —— 条数帽/字节帽把卡丢掉
|
|
578
|
+
* 之后记号还在,重放于是被永久挡住,屏上那一格从此空着且无人知道。这里逐条问 `items`,
|
|
579
|
+
* 答案与屏上看到的恒等(上限 200 条,每帧一次线性扫,量级可忽略)。
|
|
580
|
+
* 🔴 从尾往前找:同一 id 的卡至多一张(本闸的作用就是维持这条),从尾找只是让常见形(刚落的那张)
|
|
581
|
+
* 第一下命中。
|
|
582
|
+
*/
|
|
583
|
+
function toolCardIndexOf(s, id) {
|
|
584
|
+
for (let i = s.items.length - 1; i >= 0; i--) {
|
|
585
|
+
const it = s.items[i];
|
|
586
|
+
if (it?.kind === 'tool' && it.id === id)
|
|
587
|
+
return i;
|
|
588
|
+
}
|
|
589
|
+
return -1;
|
|
590
|
+
}
|
|
524
591
|
/**
|
|
525
592
|
* 聚合帧对当前缓冲的**吸收判定**(两个聚合臂共用一份;`null` = 这一帧整只丢弃)。
|
|
526
593
|
*
|
|
@@ -592,17 +659,61 @@ export function publishSubagentContentEvent(ev) {
|
|
|
592
659
|
}
|
|
593
660
|
}
|
|
594
661
|
break;
|
|
662
|
+
// ── 工具两臂(活体与 durable 重放送的是同一形)────────────────────────────────────────
|
|
663
|
+
// 🔴 与聚合两臂**同一条判据形**(先问身份、再问账本),但身份与账本都换成工具自己的那一套:
|
|
664
|
+
// 身份 = `toolCallId`(wire 给的那次调用的 id),账本 = `items` 自己(见 toolCardIndexOf)。
|
|
665
|
+
// 刻意**不写** `seenAggregateIds`:那本记号账是有界 FIFO,额度按「聚合帧只在重放腿上来」定的
|
|
666
|
+
// (活体走 `*_delta`,一个字都不写);工具帧**活体也来**,跟着写会把还在屏上的散文身份整窗
|
|
667
|
+
// 挤掉 —— 修一个臂的重放去重、弄坏另一个臂的,不是修。
|
|
668
|
+
// 🔴 顺序上「开着的卡恒收口」排在最前:一张在飞的卡如果被任何一道闸拦掉收口,它就**永远在飞**
|
|
669
|
+
// (屏上显示成还在跑),而那比多渲一次结果坏得多。
|
|
595
670
|
case 'tool_start': {
|
|
671
|
+
const startId = ev.toolCallId;
|
|
672
|
+
const seen = startId !== undefined && toolCardIndexOf(s, startId) >= 0;
|
|
673
|
+
if (seen && notNewer(ev.eventId, phaseOf(s, startId)?.start))
|
|
674
|
+
break;
|
|
675
|
+
// 段闭合可能触发条数裁剪(pushItem 从头 splice + unshift 留痕)⇒ 裁剪**前**取的下标与引用一律
|
|
676
|
+
// 作废:flush 之后按 toolCallId **重新**找卡。找不到 = 这张卡刚被裁掉 ⇒ 落到下面「新的一次」
|
|
677
|
+
// 建卡那一支(§26b 留白①),绝不对着已删的引用改名补参、也绝不把差额记到顶上那条留痕上。
|
|
678
|
+
if (seen && phaseOf(s, startId)?.start === undefined)
|
|
679
|
+
flushBuffers(s);
|
|
680
|
+
const at = startId !== undefined ? toolCardIndexOf(s, startId) : -1;
|
|
681
|
+
if (at >= 0) {
|
|
682
|
+
// 这次调用的卡已在账 ⇒ **不铸第二张**。剩下的两件按阶段水位判:
|
|
683
|
+
// 🔴 ① 这一发比卡上现在承载的那一阶段旧(或就是同一条帧又来一遍)⇒ 整帧丢弃。少了这一条,
|
|
684
|
+
// 门 park 那一阶段的 start 重放会把批复时**改过的执行参数**盖回原参数 —— 屏上就成了
|
|
685
|
+
// 「这个参数产生了那个结果」,而那个结果根本不是它跑出来的。
|
|
686
|
+
// 🔴 ② 这张卡有没有落过 `tool_start`:收口先到时下面那支会合成一张占位卡(名字 `Tool`、
|
|
687
|
+
// 参数 `{}`),那张卡**没有** start 水位 —— 迟到的这一发是它的**第一个段边界**,要补做
|
|
688
|
+
// 一次段闭合(不补的话工具前后两段散文会黏成一段、且一段都进不了已上屏正文那本记号账,
|
|
689
|
+
// 随后整轮重放时两段都拦不住)。反过来,水位已在 = 这一发是重放或下一阶段,**不**段闭合:
|
|
690
|
+
// 边界在活体那一轮已经给过,在这里切开会把还在流的那一段提前截断。
|
|
691
|
+
// 名字与参数就地补齐/更新(重放送来的是同一份值,补齐是空操作;字节按**差额**记账)。
|
|
692
|
+
const it = s.items[at];
|
|
693
|
+
const before = itemBytesOf(it);
|
|
694
|
+
if (ev.toolName !== undefined)
|
|
695
|
+
it.name = ev.toolName;
|
|
696
|
+
if (ev.args !== undefined)
|
|
697
|
+
it.input = ev.args;
|
|
698
|
+
const delta = itemBytesOf(it) - before;
|
|
699
|
+
s.itemBytes[at] = (s.itemBytes[at] ?? 0) + delta;
|
|
700
|
+
addBytes(s, delta);
|
|
701
|
+
markPhase(s, startId, 'start', ev.eventId);
|
|
702
|
+
break;
|
|
703
|
+
}
|
|
596
704
|
flushBuffers(s);
|
|
597
|
-
const id =
|
|
705
|
+
const id = startId ?? `${ev.taskId}-tool-${s.items.length}`;
|
|
598
706
|
pushItem(s, { kind: 'tool', id, name: ev.toolName ?? 'Tool', input: ev.args ?? {} });
|
|
599
707
|
s.openTools.set(id, s.items.length - 1);
|
|
708
|
+
markPhase(s, id, 'start', ev.eventId);
|
|
600
709
|
break;
|
|
601
710
|
}
|
|
602
711
|
case 'tool_end': {
|
|
603
712
|
const id = ev.toolCallId;
|
|
604
713
|
const idx = id !== undefined ? s.openTools.get(id) : undefined;
|
|
605
714
|
if (idx !== undefined && s.items[idx]?.kind === 'tool') {
|
|
715
|
+
// 卡还开着 ⇒ 结果照落。这一支**排在所有闸之前**:一张在飞的卡被任何一道闸拦掉收口,
|
|
716
|
+
// 它就永远显示成还在跑,那比多渲一次结果坏得多。
|
|
606
717
|
const it = s.items[idx];
|
|
607
718
|
it.output = ev.output ?? '';
|
|
608
719
|
it.isError = ev.isError === true;
|
|
@@ -612,18 +723,45 @@ export function publishSubagentContentEvent(ev) {
|
|
|
612
723
|
addBytes(s, add);
|
|
613
724
|
if (id !== undefined)
|
|
614
725
|
s.openTools.delete(id);
|
|
726
|
+
markPhase(s, id, 'end', ev.eventId);
|
|
727
|
+
break;
|
|
615
728
|
}
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
729
|
+
const settledAt = id !== undefined ? toolCardIndexOf(s, id) : -1;
|
|
730
|
+
if (settledAt >= 0) {
|
|
731
|
+
// 卡在账、却已不在「开着」表上 = 这次调用的结果**早就落过**。两种可能,判据是阶段水位:
|
|
732
|
+
// · 比卡上现在那一份**旧或就是同一条帧**(durable 重放 / 旧一阶段迟到)⇒ 整帧丢弃 ——
|
|
733
|
+
// 再落一次会把结果字节重复计进预算,走到下面那支还会另铸一张已完成的卡;更坏的是
|
|
734
|
+
// 旧一阶段会把屏上的真结果盖回去;
|
|
735
|
+
// · **更新的一发**收口(门 park 掉这次调用、批复后恢复执行,上游按同一个 `toolCallId`
|
|
736
|
+
// 再发一对 start/end)⇒ **后到的才是真结果**,就地替换。把「已 settled」当成永久完成
|
|
737
|
+
// 会把真结果吞掉,屏上停在那句「被门拦下」上。
|
|
738
|
+
// 🔴 身份缺席(判不出新旧)时走替换那一支:重放送的是同一份值,替换是空操作(差额恒 0)。
|
|
739
|
+
if (notNewer(ev.eventId, phaseOf(s, id)?.end))
|
|
740
|
+
break;
|
|
741
|
+
const it = s.items[settledAt];
|
|
742
|
+
const before = itemBytesOf(it);
|
|
743
|
+
it.output = ev.output ?? '';
|
|
744
|
+
it.isError = ev.isError === true;
|
|
745
|
+
const delta = itemBytesOf(it) - before;
|
|
746
|
+
s.itemBytes[settledAt] = (s.itemBytes[settledAt] ?? 0) + delta;
|
|
747
|
+
addBytes(s, delta);
|
|
748
|
+
markPhase(s, id, 'end', ev.eventId);
|
|
749
|
+
break;
|
|
626
750
|
}
|
|
751
|
+
// orphan close (start lost to the ring / arrived first) — synthesize a resolved card.
|
|
752
|
+
// 🔴 这里**不**段闭合:段边界由 `tool_start` 给(收口先到时那一发迟到的 start 会补做,见上)。
|
|
753
|
+
// 在这里闭合会把**还在流的**那一段提前切成 item —— 每子代帽配得很紧时,那一段随即变成
|
|
754
|
+
// 「最早的内容」而被帽整只丢掉,而它其实是用户眼前正在看的字。
|
|
755
|
+
const synthId = id ?? `${ev.taskId}-tool-${s.items.length}`;
|
|
756
|
+
pushItem(s, {
|
|
757
|
+
kind: 'tool',
|
|
758
|
+
id: synthId,
|
|
759
|
+
name: ev.toolName ?? 'Tool',
|
|
760
|
+
input: ev.args ?? {},
|
|
761
|
+
output: ev.output ?? '',
|
|
762
|
+
isError: ev.isError === true,
|
|
763
|
+
});
|
|
764
|
+
markPhase(s, synthId, 'end', ev.eventId);
|
|
627
765
|
break;
|
|
628
766
|
}
|
|
629
767
|
}
|
|
@@ -3861,7 +3861,7 @@ readFaceDisagreement(capFace: unknown, posture: ReadFacePostureView | undefined)
|
|
|
3861
3861
|
|
|
3862
3862
|
| # | 件 | 面 | 破坏性 | 端要做什么 |
|
|
3863
3863
|
|---|---|---|---|---|
|
|
3864
|
-
| ① | 子代内容分流补 durable 聚合形 | 行为**修复** | 无 | 什么都不用做(自动生效);web 端自守的同款分流**可以退役**,判据见 26b |
|
|
3864
|
+
| ① | 子代内容分流补 durable 聚合形(0.62.1 再补工具两臂的幂等闸) | 行为**修复** | 无 | 什么都不用做(自动生效);web 端自守的同款分流**可以退役**,判据见 26b |
|
|
3865
3865
|
| ② | 子代内容账本字节预算 | additive 三导出 + 一测试钩 | 无 | 可选:启动时配值;渲染端**建议**接新的 `truncated` 槽位 |
|
|
3866
3866
|
| ③ | 座位 lane 子代两位 + 逐键出身表 | additive 两键 + 一导出 | 无 | 想要子代正文/子代续跑的端在 `sendMessage` 里带这两位 |
|
|
3867
3867
|
| ④ | 出站凭证位放宽成「串或取值函数」 | additive 型放宽 | 无 | 可选:凭证会变的端把串换成取值函数,不必再重建 client |
|
|
@@ -3896,11 +3896,56 @@ text?: string // 聚合两臂的整段全文(刻意不复用 delta 位)
|
|
|
3896
3896
|
库的处置是**吸收**:全文以当前缓冲为前缀 ⇒ 换掉而不是追加(同一条聚合帧重复送达因此**幂等**);
|
|
3897
3897
|
前缀对不上 ⇒ 退回追加、两段都留下(少渲一段比替引擎判定「哪一段才算数」更坏)。段闭合仍由工具帧/收口给。
|
|
3898
3898
|
|
|
3899
|
-
**durable 重放的两道幂等闸**(
|
|
3899
|
+
**durable 重放的两道幂等闸**(异源复审跟修;端若自己也在做同款分流,这两条一起才够):
|
|
3900
3900
|
①**事件身份** —— 聚合帧的 `eventId` 是精确的幂等键,**跨流也管用**(适配器内那道事件序号去重只在
|
|
3901
|
-
**同一条流**内有效,重连后是新的一条流);②**整段内容** —— 已段闭合上屏过的整段(
|
|
3902
|
-
|
|
3903
|
-
|
|
3901
|
+
**同一条流**内有效,重连后是新的一条流);②**整段内容** —— 已段闭合上屏过的整段(兜底:上游没给
|
|
3902
|
+
身份,或本进程这条流是重连后新起的)。另有第三形:被预算**裁过头部**的那一段,缓冲此时只剩尾巴,
|
|
3903
|
+
单看前缀会判成新内容整只追加回来 ⇒ 判据是「全文以缓冲结尾 ⇒ 同一段、整帧丢弃」。
|
|
3904
|
+
|
|
3905
|
+
🔴 **工具两臂同样有闸,身份换成 `toolCallId`**(**0.62.1 补**;端若自己也在做同款分流,这一条同样要):
|
|
3906
|
+
0.62.0 只给聚合两臂上了闸,`tool_start` / `tool_end` **一道都没有** —— 断线重连、durable 腿整轮重放时,
|
|
3907
|
+
同一次工具调用在子代查看态里排出**两张卡**,结果正文也被记两遍(内容字节预算跟着虚高)。0.62.1 起判据
|
|
3908
|
+
形与散文两臂相同(先问身份、再问账本),但**身份是 `toolCallId`、账本是查看态 `items` 上那张卡**,
|
|
3909
|
+
**刻意不共用聚合帧那本有界身份记号**:那本账的额度是按「聚合帧只在重放腿上来」定的,而工具帧**活体
|
|
3910
|
+
也来**,跟着写会把还在屏上的散文身份整窗挤掉 —— 修一个臂的重放去重、弄坏另一个臂的,不算修。
|
|
3911
|
+
逐条:
|
|
3912
|
+
- `tool_start` 见到「这个 `toolCallId` 的卡已在账」⇒ **不铸第二张**;
|
|
3913
|
+
- 但「卡在账」**不等于**「这一发 `tool_start` 处理过」—— 收口先到时库会合成一张占位卡(名 `Tool`、
|
|
3914
|
+
参数 `{}`),迟到/重放的 `tool_start` 带的才是真的 ⇒ **就地补齐名字与参数**(不覆盖已落的结果,
|
|
3915
|
+
字节按差额记账,重放多少次都不虚涨);段边界怎么给见下面那一条;
|
|
3916
|
+
- `tool_end` **先问「卡还开着吗」**:开着就落结果 —— 一张在飞的卡若被任何一道闸拦掉收口,它就
|
|
3917
|
+
**永远在飞**(屏上一直显示在跑),那比多渲一次结果坏得多;
|
|
3918
|
+
- 卡在账、却已不在开着表上 = 结果**早就落过**。这一档**不是**「整帧丢弃」那么简单,因为同一次调用
|
|
3919
|
+
真的会有**第二对** `tool_start`/`tool_end`:门把调用 park 掉、批复之后恢复执行时,引擎复用
|
|
3920
|
+
**同一个** `toolCallId` 再发一对,第二对带的才是真结果(批复还可能**改过参数**,两次 start 的
|
|
3921
|
+
参数因此可以不同)。所以判据不是「见过没见过」而是**阶段的先后**;
|
|
3922
|
+
- **阶段水位**:库为每张卡记住它当下承载的那一阶段(已落账的 `tool_start` / `tool_end` 身份),
|
|
3923
|
+
按 `eventId` 的先后判新旧 —— 比水位**旧或就是同一条帧** ⇒ 整帧丢弃(重放不重复计字节、旧一阶段
|
|
3924
|
+
不许把屏上的真结果或改后参数盖回去);**更新** ⇒ 后到的才算数,就地替换/更新(按差额记账)。
|
|
3925
|
+
🔴 **可比性的前提**:`eventId` 是上游铸的**时间可排序**句柄(uuidv7);端若自己造内容帧喂进来,
|
|
3926
|
+
这一位必须同样时间可排序,否则新旧判不出。**缺席** ⇒ 判不出 ⇒ 退回「后到的落账」(重放送的是
|
|
3927
|
+
同一份值,落账是空操作)。水位与卡**同生共死**(卡被帽丢掉时一并清掉);
|
|
3928
|
+
- `tool_start` 除了不铸第二张卡,还负责**段边界**:这张卡**还没落过** `tool_start`(= 它是收口先到时
|
|
3929
|
+
合成的占位卡)时,这一发是它的第一个段边界 ⇒ **先段闭合再补齐名参**;水位已在(重放或下一阶段)
|
|
3930
|
+
⇒ **不**段闭合,边界在活体那一轮已经给过;
|
|
3931
|
+
- `tool_end` 找不到卡(收口先于开始帧到达)⇒ 合成一张已完成的卡,**不**在那里段闭合 —— 边界由那一发
|
|
3932
|
+
迟到的 `tool_start` 补做。在收口这一支闭合会把**还在流的**那一段提前切成一条历史 item,每子代帽
|
|
3933
|
+
配得很紧时它随即变成「最早的内容」被帽整只丢掉,而那正是用户眼前在看的字;
|
|
3934
|
+
- 去重射程严格是**同一次调用**:另一个 `toolCallId` 的卡照常进(子代真的调了两次工具时两张卡都要在)。
|
|
3935
|
+
- 迟到的 `tool_start` 先段闭合、再找卡:段闭合本身可能触发条数裁剪(从头删卡 + 顶上留痕),所以库在 flush
|
|
3936
|
+
**之后**才按 `toolCallId` 定位那张卡;找不到 = 刚被裁掉 ⇒ 按「新的一次」重建(留白①),绝不对着裁剪前的
|
|
3937
|
+
下标/引用改名补参(否则差额记到留痕槽位上、字节账从此虚高)。
|
|
3938
|
+
|
|
3939
|
+
📋 **如实留白(工具臂)**:
|
|
3940
|
+
- ①卡在不在账的判据就是 `items` 自己(不另铸卡的记号账)—— 一张卡若已被条数帽/字节帽丢掉,同 id
|
|
3941
|
+
的帧按**新的一次**来处理:`tool_start` 与 `tool_end` **都还会再来**(整轮重放)时那张卡连同
|
|
3942
|
+
**最新那一阶段**的结果一起补回来;只剩收口帧的话补回来的卡缺参数,**只剩开始帧的话补回来的卡
|
|
3943
|
+
会显示成在飞**(结果那一发已经过去了,库不留它的内容)。这是刻意的:另开一本卡的记号账会在
|
|
3944
|
+
帽丢卡之后把重放永久挡住,屏上那一格从此空着且无人知道。
|
|
3945
|
+
- ②`toolCallId` 缺席的工具帧无从去重(库会为它合成一个位置相关的 id)—— wire 上的工具帧恒带这一位,
|
|
3946
|
+
这一格只对自造帧的宿主成立。
|
|
3947
|
+
- ③收口**先于**开始帧到达的那种乱序里,卡的位置是收口到达的位置(库不把它挪到开始帧那一刻),
|
|
3948
|
+
但它前后的散文段各自完整、各留一份。
|
|
3904
3949
|
📋 **如实留白**:内容闸的代价是「一条 turn 里出现两段**逐字节相同**的正文时,durable 腿上只留一段」。
|
|
3905
3950
|
取舍明写:每次重连把整轮正文再渲一遍是**必然**发生的用户可见损坏,而同一条 turn 说两遍一模一样的话
|
|
3906
3951
|
是罕见形,且活体腿上两段都在。
|
|
@@ -4152,25 +4197,43 @@ function capForDisplay(raw: string, max: number): string
|
|
|
4152
4197
|
(不缺尾、也没有重复的前半段)。
|
|
4153
4198
|
- **G-11**:构造一个终帧与流式内容**从中途分岔**的场景 —— 屏上**不出现**拼接出来的第三种正文,且转录行
|
|
4154
4199
|
**改口**(不是一句「已完成」)。
|
|
4155
|
-
- **G-12
|
|
4156
|
-
|
|
4200
|
+
- **G-12**(0.62.1 判据订正,产品字节未动):让引擎/数据库回一个含控制字符的姿态值 —— 诊断行的
|
|
4201
|
+
显示长度**不超过该处的显示帽**,**行尾没有半截转义序列**,且**转义后仍在帽内的值一个字符都不截**
|
|
4202
|
+
(原样渲成完整的转义序列)。🔴 判据量一律是**转义后的显示长度**,不是原始字符数。
|
|
4203
|
+
🔴 原文写的是「不超过**同长度良性值**那一行」,那句话在**转义后仍在帽内**的那一档不成立、而且
|
|
4204
|
+
不成立是**设计**:呈前处理是**先转义、后按转义结果封长**,一个不可见字符渲成 6 个显示字符 ——
|
|
4205
|
+
那一档一枚都不用截,那一行因此**必然长于**同长度的良性行(良性 1:1)。照原文去实现的唯一办法是
|
|
4206
|
+
把展示行截到「原始字符数」那么长,而那会把整行诊断截没(实测:5 个控制字符 + 帽 40 ⇒ 空串)。
|
|
4207
|
+
「不比同长度良性行长」只在**两条输入都长到真的被封长**时成立,那是相对判据的射程,不是承诺。
|
|
4208
|
+
📐 三档一起验才不漏(帽 40 为例):5 个控制字符 ⇒ 转义后 30,**不截**;7 个控制字符 ⇒ 转义后 42
|
|
4209
|
+
**超帽**,截到 36(6 枚完整转义序列,原文只有 7 个字符也照截);6 个控制字符 + 33 个良性字符
|
|
4210
|
+
⇒ 恰 40,行尾是完整序列后的良性字符。
|
|
4157
4211
|
- **G-13**:一条**正常的多步回答**(散文 → 工具 → 散文)—— 转录里**不出现**任何「最终答复与显示的
|
|
4158
4212
|
不一致」类披露(拿整轮拼接去比会在这里假报)。
|
|
4159
4213
|
- **G-14**:活体跑完一整轮(两段散文 + 中间一张工具卡)之后**断线重连**、按 durable 腿整轮重放 ——
|
|
4160
|
-
|
|
4161
|
-
|
|
4214
|
+
子代查看态里每段只出现一次,**那张工具卡也只出现一次、结果只显示一次**,且顺序与活体时相同。
|
|
4215
|
+
🔴 重连是**新的一条流**,所以这一格必须真的断开重连来做:在同一条流里重放测不到它。
|
|
4216
|
+
反向一格(去重射程):同一轮里子代真的调了**两次不同**的工具 —— 两张卡都要在,不许被折成一张。
|
|
4217
|
+
四条边界一起验:①让某张工具卡被字节预算丢掉,再断线重连**整轮**重放(开始帧与收口帧都再来)——
|
|
4218
|
+
那张卡**连同最新那一阶段的结果**回来(不是空着,不是一张永远显示「在跑」的卡,也不是停在更早
|
|
4219
|
+
那一阶段的结果上);②让**结果先到、开始帧后到** —— 屏上只有一张卡,且它显示的是真的工具名与
|
|
4220
|
+
参数(不是占位),结果没被盖掉,这一次工具前后的两段散文各自完整、各留一份(重放之后也还是
|
|
4221
|
+
一份),而**还在流的**那一段不因为收口帧到达就被提前切成历史;③让一次调用**先被门拦下、批复后
|
|
4222
|
+
恢复执行**(引擎按同一个调用 id 再发一对开始/收口,批复时**改一下参数**)—— 屏上仍只有一张卡,
|
|
4223
|
+
显示的是**恢复后**的真结果与**改后**的参数;④随后把被拦下那一阶段的帧原样重放、以及让它**首次
|
|
4224
|
+
迟到**(乱序)—— 两种都**不许**把真结果或改后参数盖回去(「没见过」不等于「更新」)。
|
|
4162
4225
|
- **G-15**:把每子代帽配得比总帽还大 —— **响亮失败**(一个单条子代能整只越过的总帽不是预算)。
|
|
4163
4226
|
|
|
4164
4227
|
### 26k. 常驻门
|
|
4165
4228
|
|
|
4166
4229
|
| 门 | 守什么 |
|
|
4167
4230
|
|---|---|
|
|
4168
|
-
| 🆕 `scripts/run-subagent-durable-divert-test.mjs` | ① 聚合形分流:正控/负控/不双份三格/段边界/增量恒追加/**跨工具边界重放两道闸**(事件身份 + 整段内容) |
|
|
4231
|
+
| 🆕 `scripts/run-subagent-durable-divert-test.mjs` | ① 聚合形分流:正控/负控/不双份三格/段边界/增量恒追加/**跨工具边界重放两道闸**(事件身份 + 整段内容)/**工具两臂幂等闸**(跨流整轮重放卡恰一张 + 结果恰一次 + 异 `toolCallId` 负控 + 缺事件身份正控 + **帽淘汰后重放补回卡与结果** + **收口先到时迟来的开始帧补齐名参**) |
|
|
4169
4232
|
| 🆕 `scripts/run-subagent-content-budget-test.mjs` | ② 字节预算:每子代帽/总帽/截尾留痕(含进渲染计划)/缺省值可触到/配置面 fail-loud 八形/未越限零行为差/**拼接边界代理对不虚账**/**每子代帽 ≤ 总帽**/**被裁过的段重放不重算统计** |
|
|
4170
4233
|
| 🆕 `scripts/run-seat-task-request-origin-test.mjs` | ③ 座位载荷逐键出身对 sdk 型**直读**对账 + 子代两位回归钉 + 保留窗口内层两键 |
|
|
4171
4234
|
| 🆕 `scripts/run-wire-auth-source-test.mjs` | ④ 取值时机:构造期零读 / 读取次数 = 请求次数 / 改值即生效 / 三态逐格 / 凭证只在 Authorization 头 / 抛错 fail-soft |
|
|
4172
4235
|
| 🆕 `scripts/run-result-text-backfill-test.mjs` | ⑥ 补差四格 + 事实位形 + 跨工具边界格 + **正常多步回答零分岔** + **首尾空白零分岔**(真链路驱动,终帧按引擎真形造) |
|
|
4173
|
-
| 🆕 `scripts/run-display-cap-order-test.mjs` | ⑦ 封长顺序四铸点 + 共用铸点两条避让 + 反向参照 |
|
|
4236
|
+
| 🆕 `scripts/run-display-cap-order-test.mjs` | ⑦ 封长顺序四铸点 + 共用铸点两条避让 + 反向参照 + **转义后在帽内不截 / 原文短而转义超帽照截 / 恰好抵帽**三档(G-12 订正后的判据) |
|
|
4174
4237
|
| `scripts/run-esc-halt-plan-test.mjs` | ⑤ 新增 G6/G6b 两段(detach 格 + 缺席零差全格穷举) |
|
|
4175
4238
|
| `scripts/run-public-surface-test.mjs` | 公面导出基线 **864 → 869**(测试钩 43 → 44) |
|
|
4176
4239
|
| `scripts/run-integration-doc-freshness-test.mjs` | 本节新增坐标在盘 + 入库;§0a/§2/§2b 计数与基线等值 |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sema-agent/client-core",
|
|
3
|
-
"version": "0.62.
|
|
3
|
+
"version": "0.62.1",
|
|
4
4
|
"description": "Client-side session runtime shared by every sema human client (TUI / web / desktop): sema wire frames (AgentEvent) -> CC session vocabulary (SDKMessage) with dual-plane output (transcript/chrome), deterministic transcript ids, lane discipline as a type, and the notification/dedup ledgers. Every CC-skin shape is collected here so the wire itself stays neutral. Renamed from @sema-agent/wire-cc-adapter (0.1.x).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|