@sema-agent/client-core 0.61.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.
@@ -38,10 +38,10 @@
38
38
  *
39
39
  * ── 🔴 UNTRUSTED-for-display ──────────────────────────────────────────────────────────────
40
40
  * `isolation` 是从**数据库服务器**变量里回读的串(`@@transaction_isolation` 一族),不是引擎铸的
41
- * 闭词;三座一律只渲染、绝不参与任何判定,呈现前过本包的单行消毒单源
42
- * ({@link escapeDisplayControlChars})。
41
+ * 闭词;三座一律只渲染、绝不参与任何判定,呈现前过本包的单行消毒 + 封长单源
42
+ * ({@link capForDisplay}:先转义、后按转义结果封长)。
43
43
  */
44
- import { escapeDisplayControlChars } from './fleetTaskDesc.js';
44
+ import { capForDisplay } from './fleetTaskDesc.js';
45
45
  import { engineWireTarget } from './engineWireTarget.js';
46
46
  import { engineCapsGeneration } from './engineCapsCache.js';
47
47
  /**
@@ -131,21 +131,20 @@ export function observedSqlEngine(baseUrl = engineWireTarget()?.baseUrl) {
131
131
  /** 单行 UNTRUSTED 座的展示上限(UTF-16 单元;与壳侧那份同值)。 */
132
132
  const SQL_DETAIL_MAX = 40;
133
133
  /**
134
- * 单行 UNTRUSTED 座的呈前规整:**先按 {@link SQL_DETAIL_MAX} 截原字节,再过单行消毒**。
134
+ * 单行 UNTRUSTED 座的呈前规整:**先转义、后按转义结果封长**({@link capForDisplay})。
135
135
  *
136
- * 🔴 顺序刻意与壳那份相反,理由是两侧消毒器的**输出形不同**:壳那份用 `.` 占位(1:1,先截后清
137
- * 与先清后截等价),本包的单源 {@link escapeDisplayControlChars} 用**可见转义** `\uXXXX`
138
- * (1:6)—— 先清后截会把一个转义序列拦腰截断,屏上留下 `\u20` 这种既不是字符也不是转义的残片。
139
- * 先截后清则至多让**渲染宽度**超出上限,而语义完整;截点劈开的代理对由消毒器自己收
140
- * (孤代理项在它的字符集里)
136
+ * 🔴 0.62.0 改了顺序,理由记全(此前这里刻意写的是先截后转义):旧注释担心的是「先清后截会把一个
137
+ * 转义序列拦腰截断,屏上留下 `\u20` 这种既不是字符也不是转义的残片」—— 那个顾虑是对的,但它的
138
+ * 解法不是把顺序反过来,而是**截点避让**:共用铸点在截点落进一枚 `\uXXXX` 中间时回退到该 token
139
+ * 之前(合法代理对同理回退一位)。两个毛病只能同时消掉一次:先截后转义留下的是**预算失守**
140
+ * (纯控制字符的 40 字符原文转义后占 240 个显示字符,是承诺列宽的六倍),那是比残片更硬的一条。
141
141
  * ⚠️ **与壳那份的已知呈现差分**(换装时随批裁一次):壳 `sqlEngineDoctorDetail` 走的是
142
- * `cleanUntrustedForDisplay`(`.` 占位形),而壳自己的 `untrustedDisplayText` 头注写着
143
- * 「单行字段(路径/URL/参数键值)**必须**用 cleanUntrustedScalar」(可见转义形)——
144
- * 三座正是单行标量。本包按那条规则实现,所以对**正常读数**(`tidb` / `REPEATABLE-READ` /
145
- * `pessimistic`)两侧逐字节相同,只有含控制符的病态值渲染形不同。
142
+ * `.` 占位形消毒器(1:1,先截后清与先清后截等价),而壳自己的头注写着「单行字段(路径/URL/
143
+ * 参数键值)**必须**用可见转义形」—— 三座正是单行标量。本包按那条规则实现,所以对**正常读数**
144
+ * (`tidb` / `REPEATABLE-READ` / `pessimistic`)两侧逐字节相同,只有含控制符的病态值渲染形不同。
146
145
  */
147
146
  function cleanSqlDetailScalar(v) {
148
- return escapeDisplayControlChars(v.slice(0, SQL_DETAIL_MAX));
147
+ return capForDisplay(v, SQL_DETAIL_MAX);
149
148
  }
150
149
  /**
151
150
  * 四态 → doctor 那一行的 detail 串。**唯一措辞真源**(三端共用一句话;别在各端的行装配里另写
@@ -10,6 +10,14 @@
10
10
  *
11
11
  * WIRE FACTS: with `forwardSubagentEvents: true` on the task request, the live stream carries the
12
12
  * subagent's text_delta / reasoning_delta / tool_start / tool_end stamped with EventIdentity
13
+ * —— 🔴 **durable 腿(重放)送的是同一段的聚合形** `text` / `reasoning`(整段全文,同样带
14
+ * EventIdentity)。两类都要收:只收增量的话,续听重放尾的宿主整条子代转录都是空的
15
+ * (0.62.0 补;两类的处置**不同**,见 {@link publishSubagentContentEvent} 的吸收规则)
16
+ * 🔴 **重放的幂等闸覆盖四个臂**(`text` / `reasoning` / `tool_start` / `tool_end`):重连 = 新的一条
17
+ * 流,缝前那道事件序号去重只在同一条流内有效,所以「整轮重放」拦得住的只能是这本账。判据形相同
18
+ * (先问身份、再问账本),身份与账本各按臂取:散文两臂 = 聚合帧 `eventId` + 已上屏的整段正文;
19
+ * 工具两臂 = `toolCallId` + `items` 上那张卡,收口另带一本**自己额度**的收口身份账(同一次调用
20
+ * 会有**第二发**真收口 —— 门 park 之后恢复执行,见 {@link publishSubagentContentEvent})。
13
21
  * (§E1 redact upstream; §E2 identity). 🔴 EventIdentity 的键是 `eventId` / `parentToolCallId`
14
22
  * (+ LIVE 白名单四臂上的 `sourceTaskId` / `bgAgentId`)—— **没有 `taskId`**(sdk `events.d.ts` 的
15
23
  * `interface EventIdentity` 直证;`taskId` 只长在 `meta` 首帧上)。所以内容帧到不了「自带引擎
@@ -46,6 +54,19 @@ export type SubagentContentItem = {
46
54
  | {
47
55
  kind: 'echo';
48
56
  text: string;
57
+ }
58
+ /**
59
+ * **截尾留痕**(0.62.0):字节预算把这条子代早先的内容丢掉了多少。恒**排在 items 最前**、
60
+ * 每条子代**至多一条**(丢一次加一条会把账本自己撑爆),随后续丢弃**原地更新**。
61
+ *
62
+ * 🔴 它存在的唯一理由是「不许无声消失」:没有它,用户读到的转录比真实发生的少一截而毫无提示,
63
+ * 而那正是本包在别处反复记账的那类静默失败。渲染端该把它渲成一行可见的省略说明。
64
+ * 🔴 本记录**自身不计入预算**(它是关于预算的元信息,不是内容)。
65
+ */
66
+ | {
67
+ kind: 'truncated';
68
+ droppedBytes: number;
69
+ keptFrom: number;
49
70
  };
50
71
  /**
51
72
  * 🔴 [2393] sweep-F15(2026-08-03 全窗复审)**记一笔口径分叉**(裁定:留 `| undefined`,写明为什么):
@@ -61,10 +82,23 @@ export type SubagentContentItem = {
61
82
  * (那时两种口径就不能再共存了)。
62
83
  */
63
84
  export interface SubagentContentEvent {
64
- type: 'text_delta' | 'reasoning_delta' | 'tool_start' | 'tool_end';
85
+ /**
86
+ * 🔴 六臂,两类语义(0.62.0 加 `text` / `reasoning` 两个**聚合**臂):
87
+ * · `*_delta` = **增量**,语义是「往缓冲里追加」;
88
+ * · `text` / `reasoning` = 一段的**权威全文**(durable 重放腿送的就是这一形),语义是
89
+ * 「这一段最终是这样」—— 处置**不是追加**,见 {@link publishSubagentContentEvent} 的吸收规则。
90
+ * 两类必须分得开:把聚合帧折成一发 `*_delta` 会让「活体流过 + 重放聚合」变成**双份**。
91
+ */
92
+ type: 'text_delta' | 'reasoning_delta' | 'text' | 'reasoning' | 'tool_start' | 'tool_end';
65
93
  taskId: string;
66
94
  parentToolCallId: string;
67
95
  delta?: string | undefined;
96
+ /** 聚合两臂(`text` / `reasoning`)的**整段全文**。刻意不复用 `delta` 位:同名不同义会让
97
+ * 「这一位是增量还是全文」变成读者要靠 `type` 去反推的事,而反推错的代价是内容重复上屏。 */
98
+ text?: string | undefined;
99
+ /** wire 上那一帧的**事件身份**(聚合两臂的幂等键;缺席 ⇒ 退回内容判据,见 publish 的两道闸)。
100
+ * 🔴 工具两臂**不读这一位**:那次调用的身份是 `toolCallId`,账本是 `items` 上那张卡。 */
101
+ eventId?: string | undefined;
68
102
  toolCallId?: string | undefined;
69
103
  toolName?: string | undefined;
70
104
  args?: unknown;
@@ -104,6 +138,38 @@ export interface SubagentContentEvent {
104
138
  * 动本函数的块数组分支 ⇒ 同批看那条断言。
105
139
  */
106
140
  export declare function coerceOutput(v: unknown): string | undefined;
141
+ /**
142
+ * 本账本的**字节预算**。此前只有条数帽(每条子代 ≤ 200 条 item、≤ 32 条子代),而条数帽对
143
+ * 「一条工具结果里的一整个 base64 附件」「一段几 MB 的模型输出」**零判别力** —— 200 条的上限在
144
+ * **单条无上限**的前提下根本不是一个预算,实测面上一个子代槽位吃到十几 MB 常驻是可复现的。
145
+ *
146
+ * 🔴 单位是 **UTF-8 字节**(不是 JS 串长度):账本里装的是引擎送来的文本,而「多少内存 / 多少屏」
147
+ * 这两件事都跟编码后的体量走;按串长度算会让一段 CJK 的真实占用是名义值的三倍。
148
+ * 🔴 缺省值的来历(如实):对标产品的语料里**没有**对应的「子代转录字节预算」可对齐(查过),
149
+ * 所以这两个数不假称是 parity —— 它们来自 web 端探针实测的「单槽十几 MB」这一观测:总帽取
150
+ * 16 MiB(把整本账本按住在那个量级),每子代帽取总帽的 1/8 = 2 MiB(一条子代吃满也留得下
151
+ * 另外七条的位置)。端可以按自己的宿主改配,这两个数只是「不配时不至于无界」。
152
+ */
153
+ export interface SubagentContentStoreConfig {
154
+ /** 单条子代**保留**的内容字节上限。超出 ⇒ 从最早的内容开始丢,并留一条 `truncated` 记录。 */
155
+ maxBytesPerTask: number;
156
+ /** 整本账本保留的内容字节上限。超出 ⇒ 整条**最久未用**的子代账本被清掉(不是无差别清空)。 */
157
+ maxBytesTotal: number;
158
+ }
159
+ /** 不配时生效的两位(理由见 {@link SubagentContentStoreConfig} 顶注)。 */
160
+ export declare const SUBAGENT_CONTENT_STORE_DEFAULTS: Readonly<SubagentContentStoreConfig>;
161
+ /**
162
+ * 配字节预算(宿主启动时调一次;两位可分开配,只给一位时另一位保持现值)。
163
+ *
164
+ * 🔴 **fail-loud**:0 / 负数 / 非有限数 / 非整数一律 `throw` —— 一个被**静默忽略**的预算配置正是
165
+ * 这本账本要消灭的那类失败(宿主以为配上了,而账本还在按缺省无声地丢或不丢)。
166
+ * 🔴 拒绝是**原子**的:任何一位不合法就整只拒,既有配置一个字节不动(半套配置比不配更坏)。
167
+ */
168
+ export declare function configureSubagentContentStore(next: Partial<SubagentContentStoreConfig>): void;
169
+ /** 当前生效的两位(**快照**:改返回值不影响账本)。 */
170
+ export declare function subagentContentStoreConfig(): SubagentContentStoreConfig;
171
+ /** 测试钩:把预算复位到缺省。 */
172
+ export declare function __resetSubagentContentStoreConfigForTests(): void;
107
173
  /**
108
174
  * **内容归账那一半**(`parentToolCallId → taskId`,多对一):把停在 parent 键下的内容迁到 canonical
109
175
  * task 键,并让后续内容帧的 `canonicalKey` 解析到 task。**不写** `taskToParent`。
@@ -157,6 +223,13 @@ export type SubagentViewSlot = {
157
223
  kind: 'echo';
158
224
  slot: string;
159
225
  text: string;
226
+ }
227
+ /** 截尾留痕(0.62.0):渲染端渲一行「更早的 N 字节输出已被预算丢弃」类说明。 */
228
+ | {
229
+ kind: 'truncated';
230
+ slot: string;
231
+ droppedBytes: number;
232
+ keptFrom: number;
160
233
  } | {
161
234
  kind: 'tool';
162
235
  slot: string;