@sema-agent/client-core 0.62.1 → 0.63.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 +135 -0
- package/README.md +9 -2
- package/dist/autoModeUnavailable.d.ts +77 -0
- package/dist/autoModeUnavailable.js +101 -0
- package/dist/engineIdentity.d.ts +94 -0
- package/dist/engineIdentity.js +143 -0
- package/dist/engineNoticeCodes.d.ts +91 -0
- package/dist/engineNoticeCodes.js +215 -0
- package/dist/gateVocabulary.d.ts +67 -0
- package/dist/gateVocabulary.js +134 -0
- package/dist/hitl/persistedRulesWire.d.ts +63 -1
- package/dist/hitl/persistedRulesWire.js +86 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +21 -0
- package/dist/liveInitToolFace.js +13 -0
- package/dist/permissionRuleIssue.d.ts +33 -0
- package/dist/permissionRuleIssue.js +121 -0
- package/dist/postureKnob.d.ts +90 -0
- package/dist/postureKnob.js +174 -0
- package/dist/steering.d.ts +6 -1
- package/dist/steering.js +10 -2
- package/dist/subagentContentStore.d.ts +5 -1
- package/dist/subagentContentStore.js +230 -90
- package/dist/toolRoster.d.ts +182 -0
- package/dist/toolRoster.js +236 -0
- package/docs/INTEGRATION-CLIENTS.md +503 -24
- package/package.json +4 -4
|
@@ -196,6 +196,22 @@ function appendBytes(prev, chunk) {
|
|
|
196
196
|
const joinsPair = lastPrev >= 0xd800 && lastPrev <= 0xdbff && firstNew >= 0xdc00 && firstNew <= 0xdfff;
|
|
197
197
|
return joinsPair ? add - 2 : add;
|
|
198
198
|
}
|
|
199
|
+
/**
|
|
200
|
+
* 工具参数(开集 `unknown`)的**规范序列化** —— 字节账({@link itemBytesOf})与「这一发到底改了
|
|
201
|
+
* 没有」({@link startContentKey})共用这**一口**,两处对同一份参数的看法因此不可能分叉。
|
|
202
|
+
* 不可序列化(循环引用等)⇒ 空串:它进不了展示面,也就占不了这本账的预算、也不算内容变化。
|
|
203
|
+
*/
|
|
204
|
+
function serializeToolInput(input) {
|
|
205
|
+
if (input === undefined)
|
|
206
|
+
return '';
|
|
207
|
+
try {
|
|
208
|
+
return JSON.stringify(input) ?? '';
|
|
209
|
+
}
|
|
210
|
+
catch {
|
|
211
|
+
// 循环引用/不可序列化 ⇒ 这一位按空串计(见本函数头注)
|
|
212
|
+
return '';
|
|
213
|
+
}
|
|
214
|
+
}
|
|
199
215
|
/** 一条 item 的字节账(`truncated` 是关于预算的元信息,自身不计)。 */
|
|
200
216
|
function itemBytesOf(it) {
|
|
201
217
|
switch (it.kind) {
|
|
@@ -203,18 +219,9 @@ function itemBytesOf(it) {
|
|
|
203
219
|
case 'thinking':
|
|
204
220
|
case 'echo':
|
|
205
221
|
return utf8Len(it.text);
|
|
206
|
-
case 'tool':
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
try {
|
|
210
|
-
if (it.input !== undefined)
|
|
211
|
-
n += utf8Len(JSON.stringify(it.input) ?? '');
|
|
212
|
-
}
|
|
213
|
-
catch {
|
|
214
|
-
// 循环引用/不可序列化 ⇒ 这一位按 0 计(它进不了展示面,也就占不了这本账的预算)
|
|
215
|
-
}
|
|
216
|
-
return n;
|
|
217
|
-
}
|
|
222
|
+
case 'tool':
|
|
223
|
+
// 工具参数序列化一次算体量(每张卡只算一次,不在增量热路径上)。
|
|
224
|
+
return utf8Len(it.name) + utf8Len(it.output ?? '') + utf8Len(serializeToolInput(it.input));
|
|
218
225
|
case 'truncated':
|
|
219
226
|
return 0;
|
|
220
227
|
}
|
|
@@ -256,27 +263,39 @@ function noteDropped(s, dropped) {
|
|
|
256
263
|
}
|
|
257
264
|
s.items.unshift({ kind: 'truncated', droppedBytes: s.droppedBytes, keptFrom: s.droppedBytes });
|
|
258
265
|
s.itemBytes.unshift(0);
|
|
259
|
-
|
|
266
|
+
pruneCardPhases(s);
|
|
260
267
|
}
|
|
261
|
-
/**
|
|
262
|
-
*
|
|
263
|
-
*
|
|
264
|
-
*
|
|
265
|
-
|
|
266
|
-
|
|
268
|
+
/**
|
|
269
|
+
* items 被增删之后,把**卡已不在 items 上**的阶段账清掉。
|
|
270
|
+
*
|
|
271
|
+
* 🔴 这里此前还维护第二张表(`toolCallId` → 开着的卡下标)。删掉它是本批「按类修」的一部分:
|
|
272
|
+
* 一次工具调用的卡**在不在、在哪里**只有 `items` 说了算(本文件 {@link toolCardIndexOf} 头注
|
|
273
|
+
* 逐字写过这条),而下标表是同一件事的第二处真源 —— 它与 items 脱节的方式是可预期的
|
|
274
|
+
* (splice/unshift 之后下标整体漂移),而本批四条的病根正是「同一件事记了两本各自记的账」。
|
|
275
|
+
* 收口那一支改成每次按 `toolCallId` 现查(至多 200 条,一次线性扫),两处真源就只剩一处。
|
|
276
|
+
*/
|
|
277
|
+
function pruneCardPhases(s) {
|
|
278
|
+
// 🔴 只清**卡的**阶段账。段边界不在这里 —— 它压在缓冲上(TaskContentState.pendingCuts),卡被丢掉
|
|
279
|
+
// 时缓冲里那截正文一个字都没少,边界跟着卡消失就等于把「这里有过一次工具」这件事丢了。
|
|
267
280
|
const live = new Set();
|
|
268
|
-
s.items
|
|
269
|
-
if (it.kind === 'tool')
|
|
281
|
+
for (const it of s.items) {
|
|
282
|
+
if (it.kind === 'tool')
|
|
270
283
|
live.add(it.id);
|
|
271
|
-
|
|
272
|
-
s.openTools.set(it.id, i);
|
|
273
|
-
}
|
|
274
|
-
});
|
|
284
|
+
}
|
|
275
285
|
for (const id of s.toolPhase.keys()) {
|
|
276
286
|
if (!live.has(id))
|
|
277
287
|
s.toolPhase.delete(id);
|
|
278
288
|
}
|
|
279
289
|
}
|
|
290
|
+
/** 取(必要时新建)一张卡的阶段账 —— 卡与账同生:建卡/改卡的每一支都经这一口。 */
|
|
291
|
+
function phaseSlot(s, id) {
|
|
292
|
+
const cur = s.toolPhase.get(id);
|
|
293
|
+
if (cur !== undefined)
|
|
294
|
+
return cur;
|
|
295
|
+
const next = { boundary: false };
|
|
296
|
+
s.toolPhase.set(id, next);
|
|
297
|
+
return next;
|
|
298
|
+
}
|
|
280
299
|
/**
|
|
281
300
|
* 每子代帽:超出就**从最早的内容开始丢**,丢不动了再从缓冲头部裁。
|
|
282
301
|
*
|
|
@@ -289,6 +308,13 @@ function enforceTaskCap(s) {
|
|
|
289
308
|
if (s.bytes <= cap)
|
|
290
309
|
return false;
|
|
291
310
|
let dropped = 0;
|
|
311
|
+
// 「有没有卡离开 items」按**条数**量,不拿 `dropped`(字节)代答。
|
|
312
|
+
// 📋 如实(变异自证):这一改**今天零行为差异** —— 每子代帽恒 ≥ 1(configureSubagentContentStore
|
|
313
|
+
// 的 fail-loud 挡住 0 与负数),而循环只在「还装不下」时继续,所以有卡离开就一定有字节离开,
|
|
314
|
+
// 两个条件当下等价;门对它没有判别力是**正常**的,别拿门当它的证明。这样写的理由是:
|
|
315
|
+
// 「卡走了、它的阶段账同批清」是一条**行为面**不变量(陈旧的 `end` 会把重建那一轮的收口整帧
|
|
316
|
+
// 拦下 —— 卡建回来却永远显示在飞,G9 那一格量的就是它),它不该靠一条关于**字节**的推论维持。
|
|
317
|
+
let removedItems = 0;
|
|
292
318
|
const first = () => (s.items[0]?.kind === 'truncated' ? 1 : 0);
|
|
293
319
|
while (s.bytes > cap && s.items.length > first()) {
|
|
294
320
|
const idx = first();
|
|
@@ -297,25 +323,32 @@ function enforceTaskCap(s) {
|
|
|
297
323
|
s.itemBytes.splice(idx, 1);
|
|
298
324
|
addBytes(s, -b);
|
|
299
325
|
dropped += b;
|
|
326
|
+
removedItems += 1;
|
|
300
327
|
}
|
|
328
|
+
// 🔴 裁的是缓冲**头部** ⇒ 压在这条缓冲上的段边界同拍左移(边界是位置,前缀短了它就得跟着短;
|
|
329
|
+
// 不移的话那一刀会切在错的字上)。段边界活得比卡长,所以这一步是真会走到的:上面那个循环
|
|
330
|
+
// 可以只丢掉卡而缓冲一个字没少,反过来这里裁了缓冲而边界还压着。
|
|
301
331
|
if (s.bytes > cap) {
|
|
302
332
|
const cut = trimHeadBytes(s.thinkBuf, s.bytes - cap);
|
|
333
|
+
shiftPendingCuts(s, 'think', s.thinkBuf.length - cut.rest.length);
|
|
303
334
|
s.thinkBuf = cut.rest;
|
|
304
335
|
addBytes(s, -cut.removed);
|
|
305
336
|
dropped += cut.removed;
|
|
306
337
|
}
|
|
307
338
|
if (s.bytes > cap) {
|
|
308
339
|
const cut = trimHeadBytes(s.textBuf, s.bytes - cap);
|
|
340
|
+
shiftPendingCuts(s, 'text', s.textBuf.length - cut.rest.length);
|
|
309
341
|
s.textBuf = cut.rest;
|
|
310
342
|
addBytes(s, -cut.removed);
|
|
311
343
|
dropped += cut.removed;
|
|
312
344
|
}
|
|
345
|
+
if (removedItems > 0)
|
|
346
|
+
pruneCardPhases(s);
|
|
313
347
|
if (dropped > 0) {
|
|
314
|
-
rebuildOpenTools(s);
|
|
315
348
|
noteDropped(s, dropped);
|
|
316
349
|
return true;
|
|
317
350
|
}
|
|
318
|
-
return
|
|
351
|
+
return removedItems > 0;
|
|
319
352
|
}
|
|
320
353
|
/**
|
|
321
354
|
* 总帽:整条**最久未用**的子代账本被清掉(`tasks` 的迭代序就是 LRU 序,活跃项每次访问都会重插)。
|
|
@@ -375,12 +408,12 @@ function stateFor(taskId, parentToolCallId) {
|
|
|
375
408
|
itemBytes: [],
|
|
376
409
|
textBuf: '',
|
|
377
410
|
thinkBuf: '',
|
|
378
|
-
openTools: new Map(),
|
|
379
411
|
parentToolCallId,
|
|
380
412
|
bytes: 0,
|
|
381
413
|
droppedBytes: 0,
|
|
382
414
|
seenAggregateIds: new Set(),
|
|
383
415
|
recordedSegments: new Set(),
|
|
416
|
+
pendingCuts: [],
|
|
384
417
|
toolPhase: new Map(),
|
|
385
418
|
};
|
|
386
419
|
tasks.set(taskId, s);
|
|
@@ -405,29 +438,82 @@ function pushItem(s, item) {
|
|
|
405
438
|
addBytes(s, -freed);
|
|
406
439
|
// 条数帽丢掉的同样是**内容**,同样要留痕(此前这条路是无声的)。
|
|
407
440
|
noteDropped(s, freed);
|
|
408
|
-
//
|
|
409
|
-
|
|
441
|
+
// 卡被裁掉了 ⇒ 它的阶段账同批作废(卡没了,那本账就没有指涉对象)
|
|
442
|
+
pruneCardPhases(s);
|
|
410
443
|
}
|
|
411
444
|
}
|
|
412
|
-
/**
|
|
445
|
+
/**
|
|
446
|
+
* 段闭合:把两条缓冲收成 item(段边界 = 工具起头,或这一轮 settle)。
|
|
447
|
+
*
|
|
448
|
+
* 🔴 **在所有还没兑现的段边界处一起切**({@link TaskContentState.pendingCuts};收口先于开始帧
|
|
449
|
+
* 到达时记下的位置)。
|
|
450
|
+
* 为什么不是「只切触发这一次闭合的那张卡的那一条」:边界记的是**缓冲上的位置**,不是某张卡的
|
|
451
|
+
* 私产 —— 一条缓冲上可以同时压着好几条(两张 orphan 卡交错),而闭合一旦发生缓冲就整只清空,
|
|
452
|
+
* 没被兑现的那些位置从此无处可切。异源复审 R5 的两个反例都是这一形:①兑现者是 `settle` 或
|
|
453
|
+
* 别的工具的 `tool_start`(它们此前根本不看这些位置)⇒ 两段黏成一段;②两张 orphan 交错时第一发
|
|
454
|
+
* 迟到的 start 只兑现自己那条、把另一条丢掉 ⇒ 后两段黏成一段。黏成一段的代价是整轮重放时那一段
|
|
455
|
+
* 的内容闸不命中,历史与缓冲各留一份。
|
|
456
|
+
*/
|
|
413
457
|
function flushBuffers(s) {
|
|
458
|
+
const asc = (a, b) => a - b;
|
|
459
|
+
const textCuts = [...new Set(s.pendingCuts.map((c) => c.text))].sort(asc);
|
|
460
|
+
const thinkCuts = [...new Set(s.pendingCuts.map((c) => c.think))].sort(asc);
|
|
414
461
|
// 🔴 缓冲的字节此前已经计过账;搬进 item 时先扣掉缓冲那一份,再由 pushItem 计 item 那一份
|
|
415
462
|
// (纯空白段被丢弃时同样要扣 —— 少扣一次,总账就会一路虚高到把别人的账本淘汰掉)。
|
|
416
463
|
addBytes(s, -utf8Len(s.thinkBuf));
|
|
417
464
|
const think = s.thinkBuf;
|
|
418
465
|
s.thinkBuf = '';
|
|
419
|
-
|
|
420
|
-
pushItem(s, { kind: 'thinking', text: think });
|
|
421
|
-
rememberSegment(s, think);
|
|
422
|
-
}
|
|
466
|
+
closeSegments(s, 'thinking', think, thinkCuts);
|
|
423
467
|
addBytes(s, -utf8Len(s.textBuf));
|
|
424
468
|
const text = s.textBuf;
|
|
425
469
|
s.textBuf = '';
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
470
|
+
closeSegments(s, 'text', text, textCuts);
|
|
471
|
+
// 缓冲已空 ⇒ 压在它上面的位置一条不剩地兑现完了(指涉对象没有了)
|
|
472
|
+
s.pendingCuts.length = 0;
|
|
473
|
+
// 🔴 段闭合发生过了 ⇒ 在场每张卡都不再欠一次段闭合。这一位问的是「自这张卡出现以来发生过段闭合
|
|
474
|
+
// 没有」而不是「它自己切过没有」:兑现的可以是别人,而那一发迟到的 `tool_start` 若还当自己欠着,
|
|
475
|
+
// 就会在别人切完之后**再切一刀** —— 切掉的是当前那条与它无关的活体尾段。
|
|
476
|
+
for (const p of s.toolPhase.values())
|
|
477
|
+
p.boundary = true;
|
|
478
|
+
}
|
|
479
|
+
/** 一条缓冲收成 item:在每一条**落在中间**的边界上切一刀(空白段照旧丢弃、不铸空 item)。 */
|
|
480
|
+
function closeSegments(s, kind, buf, cuts) {
|
|
481
|
+
const push = (part) => {
|
|
482
|
+
if (!part.trim())
|
|
483
|
+
return;
|
|
484
|
+
pushItem(s, kind === 'text' ? { kind: 'text', text: part } : { kind: 'thinking', text: part });
|
|
485
|
+
rememberSegment(s, part);
|
|
486
|
+
};
|
|
487
|
+
let from = 0;
|
|
488
|
+
for (const at of cuts) {
|
|
489
|
+
const n = safeCut(buf, at);
|
|
490
|
+
if (n <= from || n >= buf.length)
|
|
491
|
+
continue;
|
|
492
|
+
push(buf.slice(from, n));
|
|
493
|
+
from = n;
|
|
429
494
|
}
|
|
495
|
+
push(buf.slice(from));
|
|
496
|
+
}
|
|
497
|
+
/**
|
|
498
|
+
* 把段边界夹进 `[0, buf.length]`,并且**绝不劈开合法代理对**。
|
|
499
|
+
*
|
|
500
|
+
* 🔴 为什么会劈到:边界是「记下它那一刻的缓冲长度」,而上游真的会把一对代理对拆成**两帧**送
|
|
501
|
+
* (本文件 {@link appendBytes} 头注就是为这一形写的)—— 记边界那一刻缓冲尾部可能正停在高位
|
|
502
|
+
* 代理项上。从那里切会切出两个孤代理项:屏上两段各挂半个字符,两段的字节账合计也比整条多 2。
|
|
503
|
+
* ⇒ 撞上就把边界**退**到那一对之前(宁可把这半个字符归给后一段)。
|
|
504
|
+
*/
|
|
505
|
+
function safeCut(buf, at) {
|
|
506
|
+
let n = at < 0 ? 0 : at > buf.length ? buf.length : at;
|
|
507
|
+
if (n > 0 && n < buf.length) {
|
|
508
|
+
const prev = buf.charCodeAt(n - 1);
|
|
509
|
+
const here = buf.charCodeAt(n);
|
|
510
|
+
if (prev >= 0xd800 && prev <= 0xdbff && here >= 0xdc00 && here <= 0xdfff)
|
|
511
|
+
n -= 1;
|
|
512
|
+
}
|
|
513
|
+
return n;
|
|
430
514
|
}
|
|
515
|
+
/** 两次段闭合之间最多记多少条段边界(见 {@link TaskContentState.pendingCuts} 的有界那一笔)。 */
|
|
516
|
+
const MAX_PENDING_CUTS = 64;
|
|
431
517
|
/** 有界 FIFO 记号(两本小账共用;只用来做幂等判定,不参与预算)。 */
|
|
432
518
|
const MAX_REPLAY_KEYS = 64;
|
|
433
519
|
function remember(set, key) {
|
|
@@ -555,20 +641,66 @@ function notNewer(eventId, mark) {
|
|
|
555
641
|
return false;
|
|
556
642
|
return eventId <= mark;
|
|
557
643
|
}
|
|
558
|
-
/**
|
|
644
|
+
/** 读一张卡的阶段账(没有 ⇒ 这张卡还没落过任何一半的帧)。 */
|
|
559
645
|
function phaseOf(s, id) {
|
|
560
646
|
return id === undefined ? undefined : s.toolPhase.get(id);
|
|
561
647
|
}
|
|
562
|
-
/**
|
|
563
|
-
|
|
564
|
-
|
|
648
|
+
/**
|
|
649
|
+
* 内容落账之后给这张卡盖章:**水位与卡上现在这份内容同源**。三形:
|
|
650
|
+
* · 身份在场 ⇒ 抬到这一发;
|
|
651
|
+
* · 身份缺席**且这一发真的改了卡上那一半的内容** ⇒ 把这一半**清掉**。此前缺席时一律「不抬」,
|
|
652
|
+
* 于是无身份的那一发把内容换掉了、水位还停在**换掉之前**那一阶段上 —— 随后正确的那一发原样
|
|
653
|
+
* 重放被自己的水位 `notNewer` 整帧拦掉,卡**永久**停在旧内容上;
|
|
654
|
+
* · 身份缺席**而内容一个字节没变**(重放送来的是同一份值)⇒ **原样保留**。水位仍然如实描述卡上
|
|
655
|
+
* 这份内容,清掉它等于顺手把「旧一阶段不许盖新一阶段」那道闸一起卸了 —— 异源复审 R5 的反例:
|
|
656
|
+
* 一发无身份的**同值**重放之后,旧一阶段的 start 就能把改后参数盖回原参,而更新的结果还留在
|
|
657
|
+
* 卡上,屏上成了「这个参数产生了那个结果」。喂点真的会混来源(tail 腿的工具帧不带 `eventId`)。
|
|
658
|
+
*
|
|
659
|
+
* 一句话:**这一半的水位,永远描述卡上这一半现在这份内容**——内容没动它就没理由动。
|
|
660
|
+
*/
|
|
661
|
+
function stampPhase(s, id, half, eventId, changed) {
|
|
662
|
+
if (id === undefined)
|
|
565
663
|
return;
|
|
566
|
-
const
|
|
567
|
-
if (
|
|
568
|
-
|
|
664
|
+
const p = phaseSlot(s, id);
|
|
665
|
+
if (typeof eventId === 'string' && eventId.length > 0)
|
|
666
|
+
p[half] = eventId;
|
|
667
|
+
else if (changed)
|
|
668
|
+
delete p[half];
|
|
669
|
+
}
|
|
670
|
+
/** 卡上「开始那一半」的内容指纹(名 + 参数的规范序列化)—— 只用来回答「这一发真的改了吗」。 */
|
|
671
|
+
function startContentKey(it) {
|
|
672
|
+
return `${it.name}\u0000${serializeToolInput(it.input)}`;
|
|
673
|
+
}
|
|
674
|
+
/** 这张卡不欠段闭合了(建卡那一支刚刚无条件闭合过一次)。 */
|
|
675
|
+
function markBoundary(s, id) {
|
|
676
|
+
phaseSlot(s, id).boundary = true;
|
|
677
|
+
}
|
|
678
|
+
/** 收口先到 ⇒ 把**此刻**两条缓冲的长度记成这条缓冲上的一个段边界(见 TaskContentState.pendingCuts)。 */
|
|
679
|
+
function recordCut(s) {
|
|
680
|
+
if (s.pendingCuts.length >= MAX_PENDING_CUTS)
|
|
569
681
|
return;
|
|
682
|
+
s.pendingCuts.push({ text: s.textBuf.length, think: s.thinkBuf.length });
|
|
683
|
+
}
|
|
684
|
+
/** 缓冲头部被裁掉 `removed` 个 UTF-16 单元 ⇒ 压在这条缓冲上的段边界一律左移(夹到 0)。 */
|
|
685
|
+
function shiftPendingCuts(s, half, removed) {
|
|
686
|
+
if (removed <= 0)
|
|
687
|
+
return;
|
|
688
|
+
for (const c of s.pendingCuts) {
|
|
689
|
+
const next = c[half] - removed;
|
|
690
|
+
c[half] = next > 0 ? next : 0;
|
|
570
691
|
}
|
|
571
|
-
|
|
692
|
+
}
|
|
693
|
+
/**
|
|
694
|
+
* 这一发**收口**比卡上现在承载的那一阶段旧吗(旧 ⇒ 整帧丢弃)。
|
|
695
|
+
*
|
|
696
|
+
* 🔴 **两半都要问**,这是 0.62.2 修的第二条:只问 `end` 的话,两条独立的流交错时(恢复阶段的
|
|
697
|
+
* `start` 已经到、旧一阶段的 `end` 才姗姗来迟)旧收口会落到新阶段那张开着的卡上 —— 新参数
|
|
698
|
+
* 配上旧阶段的失败结果,新流若中断这个错误状态就一直留在屏上。更新的 `start` 已经**建立了
|
|
699
|
+
* 新的一阶段**,旧收口不属于它;「避免永远在飞」不构成接受一份已知过期结果的理由(那张卡
|
|
700
|
+
* 确实还在飞,显示在飞是**如实**的)。
|
|
701
|
+
*/
|
|
702
|
+
function staleEnd(phase, eventId) {
|
|
703
|
+
return notNewer(eventId, phase?.start) || notNewer(eventId, phase?.end);
|
|
572
704
|
}
|
|
573
705
|
/**
|
|
574
706
|
* 这条 `toolCallId` 在本账本里已经有卡了吗(有 ⇒ 返回下标,没有 ⇒ `-1`)。
|
|
@@ -665,32 +797,41 @@ export function publishSubagentContentEvent(ev) {
|
|
|
665
797
|
// 刻意**不写** `seenAggregateIds`:那本记号账是有界 FIFO,额度按「聚合帧只在重放腿上来」定的
|
|
666
798
|
// (活体走 `*_delta`,一个字都不写);工具帧**活体也来**,跟着写会把还在屏上的散文身份整窗
|
|
667
799
|
// 挤掉 —— 修一个臂的重放去重、弄坏另一个臂的,不是修。
|
|
668
|
-
// 🔴
|
|
669
|
-
//
|
|
800
|
+
// 🔴 收口那一支**不按「卡开着没开着」分岔**(0.62.2 合并):卡在不在只按 `toolCallId` 现查
|
|
801
|
+
// `items`,在账就过同一道闸、走同一条落账。此前开着那一支另有一张下标表、且无条件落结果
|
|
802
|
+
// (理由写的是「在飞的卡被拦掉收口就永远在飞」)—— 那条理由在**旧一阶段的收口迟到**这一形上
|
|
803
|
+
// 是错的:更新的 `start` 已经建立了新的一阶段,那张卡确实还在跑,显示在飞是**如实**的,而
|
|
804
|
+
// 把旧阶段的失败结果配到新参数上并关掉卡才是用户看得见的损坏。
|
|
670
805
|
case 'tool_start': {
|
|
671
806
|
const startId = ev.toolCallId;
|
|
672
807
|
const seen = startId !== undefined && toolCardIndexOf(s, startId) >= 0;
|
|
673
|
-
|
|
808
|
+
const known = phaseOf(s, startId);
|
|
809
|
+
if (seen && notNewer(ev.eventId, known?.start))
|
|
674
810
|
break;
|
|
675
811
|
// 段闭合可能触发条数裁剪(pushItem 从头 splice + unshift 留痕)⇒ 裁剪**前**取的下标与引用一律
|
|
676
812
|
// 作废:flush 之后按 toolCallId **重新**找卡。找不到 = 这张卡刚被裁掉 ⇒ 落到下面「新的一次」
|
|
677
813
|
// 建卡那一支(§26b 留白①),绝不对着已删的引用改名补参、也绝不把差额记到顶上那条留痕上。
|
|
678
|
-
|
|
814
|
+
// 这张卡还欠一次段闭合 ⇒ 现在补做(段闭合会在**所有**还没兑现的段边界上一起切,并把在场
|
|
815
|
+
// 每张卡的这一位置上 —— 兑现者不限于「欠着的那一张」)。
|
|
816
|
+
if (seen && known?.boundary !== true)
|
|
679
817
|
flushBuffers(s);
|
|
680
818
|
const at = startId !== undefined ? toolCardIndexOf(s, startId) : -1;
|
|
681
819
|
if (at >= 0) {
|
|
682
|
-
// 这次调用的卡已在账 ⇒
|
|
820
|
+
// 这次调用的卡已在账 ⇒ **不铸第二张**。剩下的两件按这张卡的**阶段账**判:
|
|
683
821
|
// 🔴 ① 这一发比卡上现在承载的那一阶段旧(或就是同一条帧又来一遍)⇒ 整帧丢弃。少了这一条,
|
|
684
822
|
// 门 park 那一阶段的 start 重放会把批复时**改过的执行参数**盖回原参数 —— 屏上就成了
|
|
685
823
|
// 「这个参数产生了那个结果」,而那个结果根本不是它跑出来的。
|
|
686
|
-
// 🔴 ②
|
|
687
|
-
// 参数 `{}
|
|
688
|
-
//
|
|
689
|
-
//
|
|
690
|
-
//
|
|
824
|
+
// 🔴 ② 这张卡的**首个段边界**给过没有(收口先到时下面那支会合成一张占位卡:名字 `Tool`、
|
|
825
|
+
// 参数 `{}`、边界还没给)—— 没给过 ⇒ 迟到的这一发就是它的第一个段边界,要补做一次段闭合,
|
|
826
|
+
// 并且按合成那一刻记下的边界把「工具之前 / 之后」两段**分开**收(不分开的话两段黏成
|
|
827
|
+
// 一段、只进一条记号,随后整轮重放时两段的内容闸都不命中)。给过了 ⇒ 这一发是重放或
|
|
828
|
+
// 下一阶段,**不**段闭合:边界在活体那一轮已经给过,在这里切会把还在流的那一段提前截断。
|
|
829
|
+
// 🔴 判据是卡上那一位,**不是**「start 水位在不在」:水位是可选位,上游不给 `eventId` 时
|
|
830
|
+
// 它永远抬不起来 ⇒ 每一次重放都会再切一刀(0.62.2 修的第三条)。
|
|
691
831
|
// 名字与参数就地补齐/更新(重放送来的是同一份值,补齐是空操作;字节按**差额**记账)。
|
|
692
832
|
const it = s.items[at];
|
|
693
833
|
const before = itemBytesOf(it);
|
|
834
|
+
const keyBefore = startContentKey(it);
|
|
694
835
|
if (ev.toolName !== undefined)
|
|
695
836
|
it.name = ev.toolName;
|
|
696
837
|
if (ev.args !== undefined)
|
|
@@ -698,54 +839,47 @@ export function publishSubagentContentEvent(ev) {
|
|
|
698
839
|
const delta = itemBytesOf(it) - before;
|
|
699
840
|
s.itemBytes[at] = (s.itemBytes[at] ?? 0) + delta;
|
|
700
841
|
addBytes(s, delta);
|
|
701
|
-
|
|
842
|
+
stampPhase(s, it.id, 'start', ev.eventId, startContentKey(it) !== keyBefore);
|
|
702
843
|
break;
|
|
703
844
|
}
|
|
704
845
|
flushBuffers(s);
|
|
705
846
|
const id = startId ?? `${ev.taskId}-tool-${s.items.length}`;
|
|
706
847
|
pushItem(s, { kind: 'tool', id, name: ev.toolName ?? 'Tool', input: ev.args ?? {} });
|
|
707
|
-
|
|
708
|
-
|
|
848
|
+
// 建卡这一支刚刚无条件段闭合过 ⇒ 边界就是此刻,同拍记在卡上
|
|
849
|
+
markBoundary(s, id);
|
|
850
|
+
// 卡是这一刻新铸的 ⇒ 这一半的内容当然是「变了」(此前没有任何一份)
|
|
851
|
+
stampPhase(s, id, 'start', ev.eventId, true);
|
|
709
852
|
break;
|
|
710
853
|
}
|
|
711
854
|
case 'tool_end': {
|
|
712
855
|
const id = ev.toolCallId;
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
//
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
}
|
|
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))
|
|
856
|
+
// 卡在不在、在哪里,只按 `toolCallId` 现查 items —— **开着**与**已收口**两支从此共用同一条
|
|
857
|
+
// 定位与同一道闸(0.62.2 合并):此前开着那一支走的是另一张「开着的卡 → 下标」表、且**无条件**
|
|
858
|
+
// 落结果,于是同一件事有两处真源、两套规矩,而两条独立的流交错时旧收口正是从那一支进来的。
|
|
859
|
+
const at = id !== undefined ? toolCardIndexOf(s, id) : -1;
|
|
860
|
+
if (at >= 0) {
|
|
861
|
+
// 🔴 一道闸管两支:比卡上现在承载的那一阶段**旧、或就是同一条帧** ⇒ 整帧丢弃。
|
|
862
|
+
// · 同一条帧重放 ⇒ 再落一次会把结果字节重复计进预算,走到下面那支还会另铸一张已完成的卡;
|
|
863
|
+
// · 旧一阶段的收口(park 的 end 在恢复阶段之后才姗姗来迟)⇒ 会把屏上的真结果盖回去,
|
|
864
|
+
// 卡还开着时更会**关掉**一张属于新阶段、其实还在跑的卡(见 {@link staleEnd} 头注);
|
|
865
|
+
// · 反过来,**更新的一发**收口(门 park 掉这次调用、批复后恢复执行,上游按同一个
|
|
866
|
+
// `toolCallId` 再发一对 start/end)⇒ 后到的才是真结果,就地替换/落账。把「已 settled」
|
|
867
|
+
// 当成永久完成会把真结果吞掉,屏上停在那句「被门拦下」上。
|
|
868
|
+
// 🔴 身份缺席(判不出新旧)⇒ 闸不拦,照落(重放送的是同一份值,落账是空操作),同拍把
|
|
869
|
+
// `end` 那一半的水位清掉(见 {@link stampPhase})。
|
|
870
|
+
if (staleEnd(phaseOf(s, id), ev.eventId))
|
|
740
871
|
break;
|
|
741
|
-
const it = s.items[
|
|
872
|
+
const it = s.items[at];
|
|
742
873
|
const before = itemBytesOf(it);
|
|
874
|
+
const outBefore = it.output;
|
|
875
|
+
const errBefore = it.isError;
|
|
743
876
|
it.output = ev.output ?? '';
|
|
744
877
|
it.isError = ev.isError === true;
|
|
878
|
+
// 结果正文可能是**这一刻**才落到已在账的那条 item 上的 ⇒ 按差额补记(替换时差额可正可负)。
|
|
745
879
|
const delta = itemBytesOf(it) - before;
|
|
746
|
-
s.itemBytes[
|
|
880
|
+
s.itemBytes[at] = (s.itemBytes[at] ?? 0) + delta;
|
|
747
881
|
addBytes(s, delta);
|
|
748
|
-
|
|
882
|
+
stampPhase(s, it.id, 'end', ev.eventId, it.output !== outBefore || it.isError !== errBefore);
|
|
749
883
|
break;
|
|
750
884
|
}
|
|
751
885
|
// orphan close (start lost to the ring / arrived first) — synthesize a resolved card.
|
|
@@ -761,7 +895,13 @@ export function publishSubagentContentEvent(ev) {
|
|
|
761
895
|
output: ev.output ?? '',
|
|
762
896
|
isError: ev.isError === true,
|
|
763
897
|
});
|
|
764
|
-
|
|
898
|
+
// 🔴 边界虽然不在这里切,**位置**要在这里记下来:此刻缓冲里的那一截是「工具之前」那一段,
|
|
899
|
+
// 之后再流进来的是「工具之后」那一段。迟到的 `tool_start` 按这个位置切开两段(见
|
|
900
|
+
// {@link TaskContentState.pendingCuts})—— 不记的话它只能把整条缓冲当成一段闭合,
|
|
901
|
+
// 两段就此黏死。"
|
|
902
|
+
recordCut(s);
|
|
903
|
+
// 卡是这一刻新铸的 ⇒ 结果那一半当然是「变了」
|
|
904
|
+
stampPhase(s, synthId, 'end', ev.eventId, true);
|
|
765
905
|
break;
|
|
766
906
|
}
|
|
767
907
|
}
|