@sema-agent/client-core 0.75.1 → 0.76.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.
@@ -0,0 +1,175 @@
1
+ /**
2
+ * memorySpecWire.ts — `agents[].memory`(单个 agent 定义上的**记忆 spec**)的三端共用**读口**与
3
+ * **闭白名单判官**(CC-96)。
4
+ *
5
+ * ── 为什么这一件必须在包里 ────────────────────────────────────────────────────────────────────
6
+ * 这一键上有两处判定,三端各写一遍必然各错一遍:
7
+ * ① **三态**。`scopes` / `writeScope` 各有三个互不相同的读数,而它们的**中间那一态最容易被折掉**:
8
+ * · `scopes` 缺席 = 「这份 spec 没有指定挂哪些层」,**不是**「挂 0 层」。折成空集 = 替声明人说了一句
9
+ * 他没说的话(引擎侧的 spec 归一化对两者的处置不同);
10
+ * · `writeScope` 的**显式 `null`** = 「这一次 run 对记忆面**只读**」——没有 remember 工具、不做
11
+ * consolidation 写,**recall 仍然可以**。把它折成「未指定」会让 UI 说「会写」,折成「关掉记忆」
12
+ * 会让 UI 说「读不到」,两个方向都是假话。
13
+ * ② **闭白名单**。上游对这一键的校验是**闭合白名单**(`{ scopes?, writeScope?, enabled?, scopeContract? }`):
14
+ * 未列键**不是被丢掉那一个键**,而是**整只 agent 定义**被拒(400)。单数 `scope` 这个拼法自引擎 5.0.0 起
15
+ * 已退役 —— 还在发它的调用方丢的是整只 agent。这条后果差别(丢一个键 vs 丢一只 agent)是判定,不是文案,
16
+ * 所以判官({@link unknownMemorySpecKeys})与它那句话({@link unknownMemorySpecKeysNote})都归包。
17
+ *
18
+ * ── 🔴 读法(与兄弟读口同律)───────────────────────────────────────────────────────────────────
19
+ * · **整体坏形 ⇒ `undefined`,永不抛**:item 非对象 / 数组、`memory` 非对象 / 数组 / `null`、取值的 getter 抛 ——
20
+ * 一律 `undefined` =「这份 spec 读不出」,与 `{kind:'absent'}`(= 这只 agent 没有记忆 spec)**分开**。
21
+ * · **逐键各自防御读**:一个键读不懂只落自己那一格 `unreadable`,不牵连兄弟、更不静默塌进 `unspecified` ——
22
+ * 「未指定」与「在场但读不懂」是两句话,后者在上游那里还会让整只 agent 被拒。
23
+ * · 🔴 **判据 = 真序列化快照,不是 `Object.keys`**(异源对抗复审 R1 [high] + [medium] 采修)。本件要回答的是
24
+ * 「**引擎会收到什么**」,而 `Object.hasOwn` / `Object.keys` 回答的是「这个进程内对象长什么样」——两者**真的会分叉**:
25
+ * · **不可枚举**的 `writeScope: null`(或不可枚举的 `memory` 自己):`hasOwn` 说在场,而 `JSON.stringify` 把它丢掉
26
+ * ⇒ 修前读口会宣布「这一次 run 记忆只读」,而引擎收到的是**未指定**、照缺省可能照写;
27
+ * · **值为 `undefined`** 的键(`{ scope: undefined }`):`Object.keys` 报得出,而字节上根本没有这一键
28
+ * ⇒ 修前判官会拿一个不存在的违规去断言「整只 agent 会被 400」;
29
+ * · **`toJSON`**(哪怕在原型上):`Object.keys` 看不见它造出来的键,而字节上**真有** ⇒ 修前判官漏报退役键;
30
+ * · **抛出的 getter**:`Object.keys` 不碰值,所以判官会说「看过了,零未列键」,而这份 spec 连序列化都做不到。
31
+ * ⇒ 两个口一律先取 `JSON.parse(JSON.stringify(memory))` 这**一份**快照再判:序列化丢掉的(不可枚举 / Symbol /
32
+ * 值 `undefined` / 函数)本端就当它不在;序列化抛(抛 getter / 循环 / BigInt)⇒ 答「读不出」。
33
+ * · **一次读取**顺带落实:快照只取一次,每个 getter 被调用**恰一次**(带 getter / Proxy 的入参再也没有
34
+ * 「第一次过校验、第二次落进另一格」的缝)。
35
+ * 快照按**属性上下文**取(`JSON.stringify({ memory })` 而不是把它当根值)—— 否则 `toJSON(key)` 收到的 key 会是空串
36
+ * 而不是真请求里的 `'memory'`,按 key 分支的 `toJSON` 就能让我们判的那份与引擎收的那份分叉。
37
+ * · ⚠️ 前提:宿主按**普通 JSON** 发这份请求。若宿主自带 `replacer` 或发前再改一手,判定必须挪到那一步**之后**,
38
+ * 否则本件判的仍是另一份字节(登记在 `.car-notes.md` 缺口里,不在本件射程)。
39
+ * · ⚠️ **两个口各取一次快照**(各自自洽)。同一个带**可变 getter / 可变 `toJSON`** 的入参上分别调两个口,getter 会跑两次、
40
+ * 两个答案可以不一致。要一份**自洽**的答案就只读 {@link readAgentMemorySpec} 的 `spec.unknownKeys}`(它与那四格出自同一份快照),
41
+ * 并且**发出去的也应当是同一份已物化的数据**,而不是那个还会变的对象。
42
+ * · 值域**不替上游收紧**:`writeScope` 的字符串取值域、`scopeContract` 的词(已知 `"v2"`)都是**开集**,原词透传,
43
+ * 不折、不映射、不按前缀猜。
44
+ *
45
+ * ── 留位不留码 ────────────────────────────────────────────────────────────────────────────────
46
+ * 上游预告将来会给这份 spec **加一个键** `projectKey`(引擎铸,形如 `{ scope }` 或 `{ absent: … }`)。它**今天不在白名单上**
47
+ * ⇒ 判官照报({@link PENDING_MEMORY_SPEC_KEYS} 只是登记「我们知道它要来」,**不是**放行):今天发它,整只 agent 照样 400。
48
+ * 白名单放行是**那一版**的事,届时把它从 pending 挪进 {@link KNOWN_AGENT_MEMORY_SPEC_KEYS} 并同批补读数格。
49
+ */
50
+ /**
51
+ * 上游**闭白名单**的四个键(排序;判官的「已列」集就是这一份,别在消费点再抄一张)。
52
+ * 🔴 它是**闭集**(与本仓绝大多数「识别表」相反):未列键不是「本端不认得」,是「上游会拒整只 agent」。
53
+ * 加成员的唯一理由 = 上游真放行了那个键,且必须同批补一格读数与判据。
54
+ */
55
+ export declare const KNOWN_AGENT_MEMORY_SPEC_KEYS: readonly string[];
56
+ export declare const PENDING_MEMORY_SPEC_KEYS: readonly string[];
57
+ /** 已退役的**单数**拼法(引擎 5.0.0 起永不再认)。发它 = 丢整只 agent,所以诊断句对它单独点名。 */
58
+ export declare const RETIRED_MEMORY_SPEC_SINGULAR_KEY = "scope";
59
+ /** 判官清单的**上限**(有界:键名来自不可信入参,列表要上屏)。达上限 ⇒ 诊断句自报「可能还有」。 */
60
+ export declare const MAX_UNKNOWN_MEMORY_SPEC_KEYS = 32;
61
+ /**
62
+ * `scopes` 读数。
63
+ * · `unspecified` —— 键缺席 =「没指定挂哪些层」,🔴 **不是空集**;
64
+ * · `empty` —— 键在场而一层都没有(显式 `[]`,或成员全被滤掉 ⇒ 带 `dropped`);
65
+ * · `listed` —— 至少一层可用(`values` 原词;非串 / 空串成员逐个滤掉并计入 `dropped`);
66
+ * · `unreadable` —— 键在场但不是数组(替调用方猜一个数组 = 编)。
67
+ */
68
+ export type MemoryScopesReading = {
69
+ readonly state: 'unspecified';
70
+ } | {
71
+ readonly state: 'empty';
72
+ readonly dropped?: number;
73
+ } | {
74
+ readonly state: 'listed';
75
+ readonly values: readonly string[];
76
+ readonly dropped?: number;
77
+ } | {
78
+ readonly state: 'unreadable';
79
+ };
80
+ /**
81
+ * `writeScope` 读数。🔴 **三态绝不折成两态**:
82
+ * · `unspecified` —— 键缺席 =「没指定写到哪一层」(引擎侧有自己的缺省,本端不替它算);
83
+ * · `read_only` —— **显式 `null`** =「这一次 run 对记忆面只读」:没有 remember 工具、不做 consolidation 写,
84
+ * **recall 仍然可以**。它是一句肯定事实,与「没指定」是两件事;
85
+ * · `scope` —— 一个非空串层名(**开集**原词;不校拼法、不按前缀猜层);
86
+ * · `unreadable` —— 键在场却既不是非空串也不是 `null`(空串同落此格:上游拿它当不合法值,本端不替它折)。
87
+ */
88
+ export type MemoryWriteScopeReading = {
89
+ readonly state: 'unspecified';
90
+ } | {
91
+ readonly state: 'read_only';
92
+ } | {
93
+ readonly state: 'scope';
94
+ readonly value: string;
95
+ } | {
96
+ readonly state: 'unreadable';
97
+ };
98
+ /** `enabled` 读数(**严格布尔**:`0` / `'true'` / `null` 一律 `unreadable`,绝不当真也绝不当假)。 */
99
+ export type MemoryEnabledReading = {
100
+ readonly state: 'unspecified';
101
+ } | {
102
+ readonly state: 'set';
103
+ readonly value: boolean;
104
+ } | {
105
+ readonly state: 'unreadable';
106
+ };
107
+ /** `scopeContract` 读数(**开集**非空串,已知 `"v2"`;原词不折、不与版本号比较)。 */
108
+ export type MemoryScopeContractReading = {
109
+ readonly state: 'unspecified';
110
+ } | {
111
+ readonly state: 'set';
112
+ readonly value: string;
113
+ } | {
114
+ readonly state: 'unreadable';
115
+ };
116
+ /** 一份记忆 spec 的四格读数 + 判官答案(同一次读取的产物,消费点不必再问一遍)。 */
117
+ export interface AgentMemorySpecView {
118
+ readonly scopes: MemoryScopesReading;
119
+ readonly writeScope: MemoryWriteScopeReading;
120
+ readonly enabled: MemoryEnabledReading;
121
+ readonly scopeContract: MemoryScopeContractReading;
122
+ /** 未列键(= {@link unknownMemorySpecKeys} 的同一份答案;非空 ⇒ 上游会拒**整只 agent**)。 */
123
+ readonly unknownKeys: readonly string[];
124
+ }
125
+ /** `absent` = 这只 agent 没有记忆 spec(键缺席);`present` = 读出了一份。读不出 ⇒ 读口答 `undefined`。 */
126
+ export type AgentMemorySpecReading = {
127
+ readonly kind: 'absent';
128
+ } | {
129
+ readonly kind: 'present';
130
+ readonly spec: AgentMemorySpecView;
131
+ };
132
+ /**
133
+ * 单个 agent wire 定义(`TaskAgentWireItem` 形)→ 记忆 spec 读数。
134
+ *
135
+ * 🔴 三个出口刻意分开:`undefined` = 读不出;`{kind:'absent'}` = 这只 agent 没有记忆 spec;
136
+ * `{kind:'present'}` = 读出了一份(四格各自可以是 `unspecified` / `unreadable`)。
137
+ * 🔴 要读一份**裸 spec**,把它包成 `{ memory: spec }` 再进来 —— 读口刻意不去猜「入参是 item 还是 spec」:
138
+ * 猜错的那一形上,「memory 缺席」与「这就是 spec」在结构上无法区分。
139
+ */
140
+ export declare function readAgentMemorySpec(item: unknown): AgentMemorySpecReading | undefined;
141
+ /**
142
+ * **白名单判官**:这份 spec 上有哪些键不在上游白名单里(排序、有界 {@link MAX_UNKNOWN_MEMORY_SPEC_KEYS})。
143
+ *
144
+ * 🔴 只看**真序列化快照上的键** —— 那正是上游会收到的那一集(见文件头「判据 = 真序列化快照」):原型链上的同名键、
145
+ * 不可枚举键、Symbol 键、值为 `undefined` 的键都到不了上游,报它们等于凭空给调用方派一个它没犯的错;
146
+ * 反过来,`JSON.parse('{"__proto__":{}}')` 造出的 `__proto__`、以及 `toJSON`(哪怕在原型上)造出来的键
147
+ * **真的会**上 wire ⇒ 判官必须报它们。
148
+ * 🔴 **三个答案,刻意分开**(与 {@link readAgentMemorySpec} 的三出口同一条纪律):
149
+ * · `[]` —— **肯定断言**:看过了,一个未列键都没有(含「这只 agent 压根没写 `memory`」那一形:没有键就没有未列键);
150
+ * · 非空数组 —— 看到了这些未列键(原词、排序、有界);
151
+ * · `undefined` —— **读不出**:item 非对象 / 数组、`memory` 非对象 / 数组 / `null`、取值 getter 抛。
152
+ * 🔴 读不出**绝不冒充空集**:空集是「我看过了」,`undefined` 是「我没看见」——「`[]` 与 `undefined` 的区别」
153
+ * 就是本包对「不知道」的一贯处置。把后者渲成前者 = 替调用方担保一份根本没读过的 spec 能过,
154
+ * 而它会连**整只 agent** 一起被拒。
155
+ * 🔴 **数组形 `memory` 不报下标键**:`"0"` / `"1"` 是 JS 的记账不是调用方写的键,报它们只是噪音;
156
+ * 数组本身就不是一份 spec ⇒ 答 `undefined`,与读口同口径。
157
+ */
158
+ export declare function unknownMemorySpecKeys(item: unknown): readonly string[] | undefined;
159
+ /**
160
+ * 未列键 → 一句可渲的诊断。空表 ⇒ `undefined`;判官的 `undefined`(读不出)⇒ 同样 `undefined`。
161
+ * 🔴 **两种缺席都不铸句子,但理由不同**:空表是「没什么要说的」,读不出是「没资格说」——
162
+ * 本口一句「一切正常」都不铸(那句话本包证不出:上游的校验规则不在这一侧)。
163
+ *
164
+ * 句子由三段拼成:读到了哪些键 + 🔴 后果是整只 agent 被拒 + 单数 `scope` / 预告键 `projectKey` 各自的一句。
165
+ * 清单达上限时自报「可能还有」(判官有界 ⇒ 句子不许把一份被截断的清单说成全部)。
166
+ */
167
+ export declare function unknownMemorySpecKeysNote(keys: readonly string[] | undefined): string | undefined;
168
+ /**
169
+ * 记忆面**构造期拒码**的一句人话(码常量与词表在 `engineErrorCodes.ts`,本处只铸句子)。
170
+ *
171
+ * 🔴 两码**分诊归引擎**,本包一个字都不自判:拼写合不合法、范围算不算不匹配,判据都在引擎侧 ——
172
+ * 本端预铸第二判官 = 第二份会漂的规则。认不得的码 ⇒ `undefined`(开集纪律:不认得就不给句子)。
173
+ * 🔴 两句都点明「拒在受理之前」= 这一次运行**什么都没跑**,所以出路是改请求再发,不是等、也不是查运行结果。
174
+ */
175
+ export declare function memoryConfigRefusalNoteOf(code: unknown): string | undefined;
@@ -0,0 +1,320 @@
1
+ /**
2
+ * memorySpecWire.ts — `agents[].memory`(单个 agent 定义上的**记忆 spec**)的三端共用**读口**与
3
+ * **闭白名单判官**(CC-96)。
4
+ *
5
+ * ── 为什么这一件必须在包里 ────────────────────────────────────────────────────────────────────
6
+ * 这一键上有两处判定,三端各写一遍必然各错一遍:
7
+ * ① **三态**。`scopes` / `writeScope` 各有三个互不相同的读数,而它们的**中间那一态最容易被折掉**:
8
+ * · `scopes` 缺席 = 「这份 spec 没有指定挂哪些层」,**不是**「挂 0 层」。折成空集 = 替声明人说了一句
9
+ * 他没说的话(引擎侧的 spec 归一化对两者的处置不同);
10
+ * · `writeScope` 的**显式 `null`** = 「这一次 run 对记忆面**只读**」——没有 remember 工具、不做
11
+ * consolidation 写,**recall 仍然可以**。把它折成「未指定」会让 UI 说「会写」,折成「关掉记忆」
12
+ * 会让 UI 说「读不到」,两个方向都是假话。
13
+ * ② **闭白名单**。上游对这一键的校验是**闭合白名单**(`{ scopes?, writeScope?, enabled?, scopeContract? }`):
14
+ * 未列键**不是被丢掉那一个键**,而是**整只 agent 定义**被拒(400)。单数 `scope` 这个拼法自引擎 5.0.0 起
15
+ * 已退役 —— 还在发它的调用方丢的是整只 agent。这条后果差别(丢一个键 vs 丢一只 agent)是判定,不是文案,
16
+ * 所以判官({@link unknownMemorySpecKeys})与它那句话({@link unknownMemorySpecKeysNote})都归包。
17
+ *
18
+ * ── 🔴 读法(与兄弟读口同律)───────────────────────────────────────────────────────────────────
19
+ * · **整体坏形 ⇒ `undefined`,永不抛**:item 非对象 / 数组、`memory` 非对象 / 数组 / `null`、取值的 getter 抛 ——
20
+ * 一律 `undefined` =「这份 spec 读不出」,与 `{kind:'absent'}`(= 这只 agent 没有记忆 spec)**分开**。
21
+ * · **逐键各自防御读**:一个键读不懂只落自己那一格 `unreadable`,不牵连兄弟、更不静默塌进 `unspecified` ——
22
+ * 「未指定」与「在场但读不懂」是两句话,后者在上游那里还会让整只 agent 被拒。
23
+ * · 🔴 **判据 = 真序列化快照,不是 `Object.keys`**(异源对抗复审 R1 [high] + [medium] 采修)。本件要回答的是
24
+ * 「**引擎会收到什么**」,而 `Object.hasOwn` / `Object.keys` 回答的是「这个进程内对象长什么样」——两者**真的会分叉**:
25
+ * · **不可枚举**的 `writeScope: null`(或不可枚举的 `memory` 自己):`hasOwn` 说在场,而 `JSON.stringify` 把它丢掉
26
+ * ⇒ 修前读口会宣布「这一次 run 记忆只读」,而引擎收到的是**未指定**、照缺省可能照写;
27
+ * · **值为 `undefined`** 的键(`{ scope: undefined }`):`Object.keys` 报得出,而字节上根本没有这一键
28
+ * ⇒ 修前判官会拿一个不存在的违规去断言「整只 agent 会被 400」;
29
+ * · **`toJSON`**(哪怕在原型上):`Object.keys` 看不见它造出来的键,而字节上**真有** ⇒ 修前判官漏报退役键;
30
+ * · **抛出的 getter**:`Object.keys` 不碰值,所以判官会说「看过了,零未列键」,而这份 spec 连序列化都做不到。
31
+ * ⇒ 两个口一律先取 `JSON.parse(JSON.stringify(memory))` 这**一份**快照再判:序列化丢掉的(不可枚举 / Symbol /
32
+ * 值 `undefined` / 函数)本端就当它不在;序列化抛(抛 getter / 循环 / BigInt)⇒ 答「读不出」。
33
+ * · **一次读取**顺带落实:快照只取一次,每个 getter 被调用**恰一次**(带 getter / Proxy 的入参再也没有
34
+ * 「第一次过校验、第二次落进另一格」的缝)。
35
+ * 快照按**属性上下文**取(`JSON.stringify({ memory })` 而不是把它当根值)—— 否则 `toJSON(key)` 收到的 key 会是空串
36
+ * 而不是真请求里的 `'memory'`,按 key 分支的 `toJSON` 就能让我们判的那份与引擎收的那份分叉。
37
+ * · ⚠️ 前提:宿主按**普通 JSON** 发这份请求。若宿主自带 `replacer` 或发前再改一手,判定必须挪到那一步**之后**,
38
+ * 否则本件判的仍是另一份字节(登记在 `.car-notes.md` 缺口里,不在本件射程)。
39
+ * · ⚠️ **两个口各取一次快照**(各自自洽)。同一个带**可变 getter / 可变 `toJSON`** 的入参上分别调两个口,getter 会跑两次、
40
+ * 两个答案可以不一致。要一份**自洽**的答案就只读 {@link readAgentMemorySpec} 的 `spec.unknownKeys}`(它与那四格出自同一份快照),
41
+ * 并且**发出去的也应当是同一份已物化的数据**,而不是那个还会变的对象。
42
+ * · 值域**不替上游收紧**:`writeScope` 的字符串取值域、`scopeContract` 的词(已知 `"v2"`)都是**开集**,原词透传,
43
+ * 不折、不映射、不按前缀猜。
44
+ *
45
+ * ── 留位不留码 ────────────────────────────────────────────────────────────────────────────────
46
+ * 上游预告将来会给这份 spec **加一个键** `projectKey`(引擎铸,形如 `{ scope }` 或 `{ absent: … }`)。它**今天不在白名单上**
47
+ * ⇒ 判官照报({@link PENDING_MEMORY_SPEC_KEYS} 只是登记「我们知道它要来」,**不是**放行):今天发它,整只 agent 照样 400。
48
+ * 白名单放行是**那一版**的事,届时把它从 pending 挪进 {@link KNOWN_AGENT_MEMORY_SPEC_KEYS} 并同批补读数格。
49
+ */
50
+ import { CONFIG_MEMORY_PROJECT_KEY_SPELLING, CONFIG_MEMORY_WRITE_SCOPE_MISMATCH } from './engineErrorCodes.js';
51
+ import { capForDisplay } from './fleetTaskDesc.js';
52
+ // ── 白名单 / 留位 / 退役词 ─────────────────────────────────────────────────────────────────────
53
+ /**
54
+ * 上游**闭白名单**的四个键(排序;判官的「已列」集就是这一份,别在消费点再抄一张)。
55
+ * 🔴 它是**闭集**(与本仓绝大多数「识别表」相反):未列键不是「本端不认得」,是「上游会拒整只 agent」。
56
+ * 加成员的唯一理由 = 上游真放行了那个键,且必须同批补一格读数与判据。
57
+ */
58
+ export const KNOWN_AGENT_MEMORY_SPEC_KEYS = Object.freeze([
59
+ 'enabled',
60
+ 'scopeContract',
61
+ 'scopes',
62
+ 'writeScope',
63
+ ]);
64
+ /**
65
+ * **预告键**登记(今天仍是未列键 ⇒ 判官照报)。留这一行的用处只有两个:让诊断句能对这一个键多说一句
66
+ * 「它是预告中的键,不是打字错」,以及让门钉住「预告 ∩ 已列 = ∅」——哪天有人手滑把它当成已放行,门当场红。
67
+ */
68
+ const PENDING_PROJECT_KEY = 'projectKey';
69
+ export const PENDING_MEMORY_SPEC_KEYS = Object.freeze([PENDING_PROJECT_KEY]);
70
+ /** 已退役的**单数**拼法(引擎 5.0.0 起永不再认)。发它 = 丢整只 agent,所以诊断句对它单独点名。 */
71
+ export const RETIRED_MEMORY_SPEC_SINGULAR_KEY = 'scope';
72
+ /** 判官清单的**上限**(有界:键名来自不可信入参,列表要上屏)。达上限 ⇒ 诊断句自报「可能还有」。 */
73
+ export const MAX_UNKNOWN_MEMORY_SPEC_KEYS = 32;
74
+ // ── 一次读取:item → memory 值 ─────────────────────────────────────────────────────────────────
75
+ /**
76
+ * 冻结空表 = 判官的**肯定断言**:「我看过这份 spec 了,一个未列键都没有」。
77
+ * 🔴 它与判官的 `undefined`(读不出)**刻意分开** —— 这是本包对「不知道」的一贯处置:
78
+ * 空集是一句断言,读不出不许冒充它(同 {@link readAgentMemorySpec} 的 `undefined` 与 `{kind:'absent'}` 分家)。
79
+ * 把「我没看见」渲成「没有」是替调用方下一个证不出的结论。
80
+ */
81
+ const NO_UNLISTED_KEYS = Object.freeze([]);
82
+ const isPlainish = (v) => v !== null && typeof v === 'object' && !Array.isArray(v);
83
+ /** 自有**且可枚举** —— 这一对才是「会被 `JSON.stringify` 送出去」的条件(单独的 `hasOwn` 不是)。 */
84
+ function ownEnumerable(o, key) {
85
+ return Object.hasOwn(o, key) && Object.prototype.propertyIsEnumerable.call(o, key);
86
+ }
87
+ /**
88
+ * item 上 `memory` 的**上 wire 快照**(全件唯一的一次读取)。
89
+ * · `{ value: <快照> }` —— 这一键会出现在字节上,`value` 就是引擎将收到的那一份(纯数据:无 getter、无原型、无 Symbol);
90
+ * · `{ value: undefined }` —— 这一键**不会**出现在字节上(键不在 / 不可枚举 / 值 `undefined` / 值序列化不出 JSON 值);
91
+ * · `undefined` —— **读不出**:item 非对象 / 数组,或序列化过程抛(抛出的 getter / 循环引用 / BigInt)。
92
+ * 🔴 异常路径答的是**诚实缺席**,不是健康值。
93
+ */
94
+ function memoryWireSnapshot(item) {
95
+ if (!isPlainish(item))
96
+ return undefined;
97
+ try {
98
+ if (!ownEnumerable(item, 'memory'))
99
+ return { value: undefined };
100
+ // 🔴 **必须带属性上下文序列化**(异源对抗复审 R2 [high] 采修):`JSON.stringify(item.memory)` 是把它当
101
+ // **根值**序列化,于是 `toJSON(key)` 收到的 key 是**空串**;而真请求里这一位是 `memory` 属性,`toJSON`
102
+ // 收到的是 `'memory'`。一个按 key 分支的 `toJSON` 会让「我们判的那份」与「引擎收的那份」分叉 ——
103
+ // 实测过的形:按 key 分支时可以一边让快照显示 `writeScope: null`(只读),一边让真字节送出
104
+ // `{scope:'user', writeScope:'user'}`(退役键 + 可写)。包一层同名属性即恢复真上下文。
105
+ const text = JSON.stringify({ memory: item.memory });
106
+ const parsed = JSON.parse(text);
107
+ if (!isPlainish(parsed))
108
+ return undefined;
109
+ // 键不在解析结果上 = 序列化把它丢了(值 `undefined` / 函数 / symbol / `toJSON` 返回 `undefined`)⇒ 上游收不到这一键。
110
+ return { value: Object.hasOwn(parsed, 'memory') ? parsed.memory : undefined };
111
+ }
112
+ catch {
113
+ return undefined;
114
+ }
115
+ }
116
+ /** 快照上一个键的读取。快照是纯数据 ⇒ `hasOwn` 在这里就等于「字节上有这一键」(没有不可枚举 / getter 的缝)。 */
117
+ function snapKey(spec, key) {
118
+ return Object.hasOwn(spec, key) ? spec[key] : undefined;
119
+ }
120
+ function readScopes(raw) {
121
+ if (raw === undefined)
122
+ return { state: 'unspecified' };
123
+ if (!Array.isArray(raw))
124
+ return { state: 'unreadable' };
125
+ const values = [];
126
+ let dropped = 0;
127
+ for (const v of raw) {
128
+ if (typeof v === 'string' && v.length > 0)
129
+ values.push(v);
130
+ else
131
+ dropped += 1;
132
+ }
133
+ if (values.length === 0)
134
+ return { state: 'empty', ...(dropped > 0 ? { dropped } : {}) };
135
+ return { state: 'listed', values: Object.freeze(values), ...(dropped > 0 ? { dropped } : {}) };
136
+ }
137
+ function readWriteScope(raw) {
138
+ if (raw === undefined)
139
+ return { state: 'unspecified' };
140
+ if (raw === null)
141
+ return { state: 'read_only' };
142
+ if (typeof raw === 'string' && raw.length > 0)
143
+ return { state: 'scope', value: raw };
144
+ return { state: 'unreadable' };
145
+ }
146
+ function readEnabled(raw) {
147
+ if (raw === undefined)
148
+ return { state: 'unspecified' };
149
+ if (raw === true || raw === false)
150
+ return { state: 'set', value: raw };
151
+ return { state: 'unreadable' };
152
+ }
153
+ function readScopeContract(raw) {
154
+ if (raw === undefined)
155
+ return { state: 'unspecified' };
156
+ if (typeof raw === 'string' && raw.length > 0)
157
+ return { state: 'set', value: raw };
158
+ return { state: 'unreadable' };
159
+ }
160
+ /** 未列键计算(已读到的 spec 对象上;排序 + 有界)。 */
161
+ function unlistedKeysOf(spec) {
162
+ const out = [];
163
+ for (const k of Object.keys(spec)) {
164
+ if (!KNOWN_AGENT_MEMORY_SPEC_KEYS.includes(k))
165
+ out.push(k);
166
+ }
167
+ if (out.length === 0)
168
+ return NO_UNLISTED_KEYS;
169
+ out.sort();
170
+ return Object.freeze(out.slice(0, MAX_UNKNOWN_MEMORY_SPEC_KEYS));
171
+ }
172
+ /**
173
+ * 单个 agent wire 定义(`TaskAgentWireItem` 形)→ 记忆 spec 读数。
174
+ *
175
+ * 🔴 三个出口刻意分开:`undefined` = 读不出;`{kind:'absent'}` = 这只 agent 没有记忆 spec;
176
+ * `{kind:'present'}` = 读出了一份(四格各自可以是 `unspecified` / `unreadable`)。
177
+ * 🔴 要读一份**裸 spec**,把它包成 `{ memory: spec }` 再进来 —— 读口刻意不去猜「入参是 item 还是 spec」:
178
+ * 猜错的那一形上,「memory 缺席」与「这就是 spec」在结构上无法区分。
179
+ */
180
+ export function readAgentMemorySpec(item) {
181
+ const held = memoryWireSnapshot(item);
182
+ if (held === undefined)
183
+ return undefined;
184
+ if (held.value === undefined)
185
+ return { kind: 'absent' };
186
+ if (!isPlainish(held.value))
187
+ return undefined;
188
+ const spec = held.value;
189
+ try {
190
+ const scopes = readScopes(snapKey(spec, 'scopes'));
191
+ const writeScope = readWriteScope(snapKey(spec, 'writeScope'));
192
+ const enabled = readEnabled(snapKey(spec, 'enabled'));
193
+ const scopeContract = readScopeContract(snapKey(spec, 'scopeContract'));
194
+ const unknownKeys = unlistedKeysOf(spec);
195
+ return { kind: 'present', spec: { scopes, writeScope, enabled, scopeContract, unknownKeys } };
196
+ }
197
+ catch {
198
+ return undefined;
199
+ }
200
+ }
201
+ /**
202
+ * **白名单判官**:这份 spec 上有哪些键不在上游白名单里(排序、有界 {@link MAX_UNKNOWN_MEMORY_SPEC_KEYS})。
203
+ *
204
+ * 🔴 只看**真序列化快照上的键** —— 那正是上游会收到的那一集(见文件头「判据 = 真序列化快照」):原型链上的同名键、
205
+ * 不可枚举键、Symbol 键、值为 `undefined` 的键都到不了上游,报它们等于凭空给调用方派一个它没犯的错;
206
+ * 反过来,`JSON.parse('{"__proto__":{}}')` 造出的 `__proto__`、以及 `toJSON`(哪怕在原型上)造出来的键
207
+ * **真的会**上 wire ⇒ 判官必须报它们。
208
+ * 🔴 **三个答案,刻意分开**(与 {@link readAgentMemorySpec} 的三出口同一条纪律):
209
+ * · `[]` —— **肯定断言**:看过了,一个未列键都没有(含「这只 agent 压根没写 `memory`」那一形:没有键就没有未列键);
210
+ * · 非空数组 —— 看到了这些未列键(原词、排序、有界);
211
+ * · `undefined` —— **读不出**:item 非对象 / 数组、`memory` 非对象 / 数组 / `null`、取值 getter 抛。
212
+ * 🔴 读不出**绝不冒充空集**:空集是「我看过了」,`undefined` 是「我没看见」——「`[]` 与 `undefined` 的区别」
213
+ * 就是本包对「不知道」的一贯处置。把后者渲成前者 = 替调用方担保一份根本没读过的 spec 能过,
214
+ * 而它会连**整只 agent** 一起被拒。
215
+ * 🔴 **数组形 `memory` 不报下标键**:`"0"` / `"1"` 是 JS 的记账不是调用方写的键,报它们只是噪音;
216
+ * 数组本身就不是一份 spec ⇒ 答 `undefined`,与读口同口径。
217
+ */
218
+ export function unknownMemorySpecKeys(item) {
219
+ const held = memoryWireSnapshot(item);
220
+ if (held === undefined)
221
+ return undefined;
222
+ // 键缺席 = 这只 agent 没有记忆 spec ⇒「看过了,零未列键」是真话(不是读不出)。
223
+ if (held.value === undefined)
224
+ return NO_UNLISTED_KEYS;
225
+ if (!isPlainish(held.value))
226
+ return undefined;
227
+ try {
228
+ return unlistedKeysOf(held.value);
229
+ }
230
+ catch {
231
+ return undefined;
232
+ }
233
+ }
234
+ // ── 诊断句(单铸;端只渲)──────────────────────────────────────────────────────────────────────
235
+ /** 诊断句里单个键名的显示上限(键名来自不可信入参 ⇒ 呈前**消毒 + 封长**,与本包其余远端文本铸点同律)。
236
+ * 🔴 消毒只进文案,**不进判据**:{@link unknownMemorySpecKeys} 交的是**原词**(机器比对面),句子里才是消毒过的那一份。 */
237
+ const MEMORY_SPEC_KEY_DISPLAY_MAX = 64;
238
+ /** 键名被截短时挂在**引号外**的标记(挂在引号外 ⇒ 一个字面叫 `(truncated)` 的键与「被截短」永不同形)。 */
239
+ const TRUNCATED_KEY_MARK = '(truncated)';
240
+ /** 句尾指路:被截短的键名去哪儿看原词。 */
241
+ const TRUNCATED_KEY_POINTER = 'key names marked (truncated) are shortened for display only — read the verbatim names from the judge\'s list';
242
+ /**
243
+ * 键名 → 句子里的**带引号转义形** + 是否被截短(异源对抗复审 R2 / R3 next-step 与 [medium] 采修)。
244
+ *
245
+ * 🔴 为什么一律加引号而不是只给空串特殊待遇:不加引号时 `''` 与一个**字面叫 `""`** 的键在句子里长得一模一样 ——
246
+ * 两个不同的违规读成同一句,人照着改会改错那一个。引号 + 转义反斜杠与引号之后三者互不同形。
247
+ * 🔴 **按码点逐 token 累加**(R3 [medium] 采修):修前是「先整串转义、再按长度切」,两个后果都真出现过 ——
248
+ * ① 切点落在一枚转义中间:留下**单个**反斜杠,紧跟的收尾引号看起来像被转义了(`…\"`),整句读不出边界;
249
+ * ② 前缀相同的两个长键渲成**同一句**,而句子没有任何「这是截短的」痕迹 ⇒ 人把前缀当完整违规键去改,改错对象。
250
+ * 现在每个码点先各自变成一枚完整 token(`\\\\` / `\\"` / 控制字符与孤代理项的 `\uXXXX` / 原样字符),
251
+ * 累加到装不下就停 —— **token 绝不被切断**,合法代理对因为按码点迭代也不会被拆。
252
+ * 🔴 **截短要说出来**:装不下时 `truncated` 置位,句子在引号外挂 {@link TRUNCATED_KEY_MARK} 并在句尾指向判官的原词表。
253
+ * ⚠️ 仍有一条**说清了的**残留:共享同一段前缀的两个长键,展示形相同(有界显示换不来任意长键的可分性)——
254
+ * 所以句子不再把前缀当完整键名,机器面要原词就读 {@link unknownMemorySpecKeys} 的返回值。
255
+ */
256
+ function quotedKeyForDisplay(key) {
257
+ let out = '';
258
+ let truncated = false;
259
+ // 按**码点**迭代(`for...of`)⇒ 合法代理对是一个 `ch`,不会被拆成两个孤代理项。
260
+ for (const ch of key) {
261
+ // 一个码点 → 一枚完整 token。`capForDisplay` 在这里只干消毒(单码点最长 `\uXXXX` = 6,给 8 的余量 ⇒ 它自己不会截)。
262
+ const tok = ch === '\\' ? '\\\\' : ch === '"' ? '\\"' : capForDisplay(ch, 8);
263
+ if (out.length + tok.length > MEMORY_SPEC_KEY_DISPLAY_MAX) {
264
+ truncated = true;
265
+ break;
266
+ }
267
+ out += tok;
268
+ }
269
+ return { text: `"${out}"`, truncated };
270
+ }
271
+ const KEYS_HEAD = 'this memory spec carries keys the engine does not accept';
272
+ /** 🔴 本诊断的**全部价值**:说清后果的量级 —— 丢的是整只 agent,不是这一个键。 */
273
+ const WHOLE_AGENT = 'the engine checks this spec against a closed list, so an unlisted key is refused (400) together with the WHOLE agent definition — the agent is lost, not just the key';
274
+ const SINGULAR_HINT = `"${RETIRED_MEMORY_SPEC_SINGULAR_KEY}" is the retired singular spelling — use "scopes" (plural)`;
275
+ const PENDING_HINT = '"' + PENDING_PROJECT_KEY + '" is an announced future key, not a typo: it is still not on the accepted list, so sending it today loses the agent all the same';
276
+ /**
277
+ * 未列键 → 一句可渲的诊断。空表 ⇒ `undefined`;判官的 `undefined`(读不出)⇒ 同样 `undefined`。
278
+ * 🔴 **两种缺席都不铸句子,但理由不同**:空表是「没什么要说的」,读不出是「没资格说」——
279
+ * 本口一句「一切正常」都不铸(那句话本包证不出:上游的校验规则不在这一侧)。
280
+ *
281
+ * 句子由三段拼成:读到了哪些键 + 🔴 后果是整只 agent 被拒 + 单数 `scope` / 预告键 `projectKey` 各自的一句。
282
+ * 清单达上限时自报「可能还有」(判官有界 ⇒ 句子不许把一份被截断的清单说成全部)。
283
+ */
284
+ export function unknownMemorySpecKeysNote(keys) {
285
+ if (!Array.isArray(keys))
286
+ return undefined;
287
+ // 🔴 只滤**非串**(异源对抗复审 R1 [medium] 采修):空串 `''` 是一个**真的会上 wire** 的未列键
288
+ // (`{"":true}` 序列化得出来),把它滤掉会让一条非空违规清单**一句后果都不说** —— 而后果是整只 agent 被拒。
289
+ const shown = keys.filter((k) => typeof k === 'string');
290
+ if (shown.length === 0)
291
+ return undefined;
292
+ const more = shown.length >= MAX_UNKNOWN_MEMORY_SPEC_KEYS ? ', and possibly more (list capped)' : '';
293
+ const rendered = shown.map(quotedKeyForDisplay);
294
+ const safe = rendered.map((r) => (r.truncated ? `${r.text}${TRUNCATED_KEY_MARK}` : r.text));
295
+ const parts = [`${KEYS_HEAD}: ${safe.join(', ')}${more}`, WHOLE_AGENT];
296
+ // 有键被截短 ⇒ 句子必须自报,并指向拿得到原词的那一个口(前缀不是键名)。
297
+ if (rendered.some((r) => r.truncated))
298
+ parts.push(TRUNCATED_KEY_POINTER);
299
+ if (shown.includes(RETIRED_MEMORY_SPEC_SINGULAR_KEY))
300
+ parts.push(SINGULAR_HINT);
301
+ if (shown.includes(PENDING_PROJECT_KEY))
302
+ parts.push(PENDING_HINT);
303
+ return `${parts.join('. ')}.`;
304
+ }
305
+ /**
306
+ * 记忆面**构造期拒码**的一句人话(码常量与词表在 `engineErrorCodes.ts`,本处只铸句子)。
307
+ *
308
+ * 🔴 两码**分诊归引擎**,本包一个字都不自判:拼写合不合法、范围算不算不匹配,判据都在引擎侧 ——
309
+ * 本端预铸第二判官 = 第二份会漂的规则。认不得的码 ⇒ `undefined`(开集纪律:不认得就不给句子)。
310
+ * 🔴 两句都点明「拒在受理之前」= 这一次运行**什么都没跑**,所以出路是改请求再发,不是等、也不是查运行结果。
311
+ */
312
+ export function memoryConfigRefusalNoteOf(code) {
313
+ if (code === CONFIG_MEMORY_PROJECT_KEY_SPELLING) {
314
+ return 'the memory project key in this request is not spelled the way the engine requires (refused with 400, before the run started — nothing ran). Fix the spelling and send again; the engine owns this check.';
315
+ }
316
+ if (code === CONFIG_MEMORY_WRITE_SCOPE_MISMATCH) {
317
+ return 'the memory write scope in this request does not match the one already in force (refused with 409, before the run started — nothing ran). This is a conflict, not a typo: line the write scope up with the one in force, or leave it out, and send again; the engine owns this check.';
318
+ }
319
+ return undefined;
320
+ }
package/dist/seam.d.ts CHANGED
@@ -9,7 +9,7 @@
9
9
  import type { ModelUsage, SDKMessage } from '@sema-agent/agent-types';
10
10
  import type { EngineTurnUsage } from './adapter/downstream/turnUsageToModelUsage.js';
11
11
  import type { RunCostReconcile } from './adapter/downstream/terminalToSdkResult.js';
12
- import type { WiringManifestAutoMode, WiringManifestModelGate, WiringManifestMcpEntryView, WiringManifestHookEntryView, WiringManifestLspSeamView, WiringManifestWriteProtectionView, WiringManifestAutoConsolidationView, WiringManifestReadDenyView, ContextUsageSectionView } from './adapter/downstream/eventToSdkMessage.js';
12
+ import type { WiringManifestAutoMode, WiringManifestModelGate, WiringManifestMcpEntryView, WiringManifestHookEntryView, WiringManifestLspSeamView, WiringManifestWriteProtectionView, WiringManifestAutoConsolidationView, WiringManifestReadDenyView, ContextUsageSectionView, PromptManifestSectionView, PromptManifestBlockView, PromptManifestToolView } from './adapter/downstream/eventToSdkMessage.js';
13
13
  import type { ToolRosterView, ToolRosterDeltaView } from './toolRoster.js';
14
14
  /**
15
15
  * `AbortSignal` 的结构型(B3 扩容,SEAM-GAP-4)。
@@ -153,9 +153,6 @@ export type ChromeEvent = {
153
153
  laneProof: LaneProof;
154
154
  taskId: string;
155
155
  registration: unknown;
156
- } | {
157
- kind: 'tasks_expand';
158
- laneProof: LaneProof;
159
156
  } | {
160
157
  kind: 'thinking_activity';
161
158
  laneProof: LaneProof;
@@ -545,7 +542,7 @@ export type ChromeEvent = {
545
542
  * ⚠️ **只有 live 腿有本帧**(server 7.69.0 亲读:durable/bg 腿把它喂给 rewind 锚而不入账本)
546
543
  * ⇒ replay/resume 上映射表是空的;那时正解是**不画分割线**,不是回退成「画在最后一条」。
547
544
  */
548
- | MessageCommittedChromeEvent | EngineNoticeChromeEvent | TextSegmentEndChromeEvent | ThinkingSegmentEndChromeEvent | ToolDisclosureChromeEvent | ToolProgressChromeEvent | ToolRosterDeltaChromeEvent | ContextUsageChromeEvent | CompactionOutcomeChromeEvent | WiringManifestChromeEvent | ResultTextDivergedChromeEvent
545
+ | MessageCommittedChromeEvent | EngineNoticeChromeEvent | TextSegmentEndChromeEvent | ThinkingSegmentEndChromeEvent | ToolDisclosureChromeEvent | ToolProgressChromeEvent | ToolRosterDeltaChromeEvent | ContextUsageChromeEvent | CompactionOutcomeChromeEvent | PromptAssembledChromeEvent | WiringManifestChromeEvent | ResultTextDivergedChromeEvent
549
546
  /**
550
547
  * B-078 / L-208(0.65.0):design/172 流内审批协议的**两条帧**。修前它们在标准管线上落
551
548
  * `dropped('unsupported_arm')`,壳自己另接一份 ⇒ 只走包管线的宿主(desktop/web)拿不到
@@ -820,6 +817,57 @@ export interface CompactionOutcomeChromeEvent {
820
817
  reason?: string;
821
818
  eventId?: string;
822
819
  }
820
+ /**
821
+ * CC-91(0.76.0;sdk `prompt_assembled` = `{type} & PromptManifest`):**这一次 prepare 的提示词装配清单** ——
822
+ * 系统提示词由哪些段 / 块组成、每段多大、这一腿挂了哪些工具。durable 腿的台账帧。
823
+ *
824
+ * 🔴 **没有 `tokens`,只有 `chars`** —— 上游真字节里就没有 token 位(core `prompt.assembled` 的 sections 是
825
+ * `chars`,server 的白名单投影逐字段同形)。本包**刻意不铸** `tokens`:`chars / 系数` 是编的,不是真值。
826
+ * 要引擎自己的 token 估算去读 {@link ContextUsageChromeEvent} 的 `sections[].tokens`(引擎就是拿同一份
827
+ * `chars` 估的)。🔴 **这条 join 只在两面的 `sections` 都在场时成立**(同一张 v2 段 id 词表):`context_usage`
828
+ * 那一面的段视图也是从同一份 manifest 段列投影的,manifest 没有段列时它同样缺席 ⇒ **v1-only 腿上没有这条 join**,
829
+ * 那时别拿 `blocks` 当 `sections` 的同名替代(见下,两张词表不同)。
830
+ * 🔴 **`chars` 与 `context_usage.usedTokens` 禁相减**(两个量纲不是一条账,且 chars ≠ tokens 更差一层);
831
+ * `Σ chars` 也不构成「提示词占了窗口多少」——那句话只能由 `context_usage` 那一帧回答。
832
+ * 🔴 **一次任务可能多条**(cascade / verify 的每一 rung、resume 腿各自 re-prepare,server 逐条保序落账)⇒
833
+ * 本包每一条都交出去、**不去重**;要「当前组成」的宿主按**最后一条**取(与 `context_usage` 同律)。
834
+ * 🔴 `sections[].id` 与 `blocks[].id` 是**两张不同的词表**,不要混着分组:
835
+ * · `sections[].id` = core pack 的**段** id(`core/<slot>.<name>` 命名空间,例 `core/role.base` / `core/harness.head`);
836
+ * · `blocks[].id` = v1 的**带标签块** id(`role.base` / `harness.context` / `env.context` / `memory.tail` …),
837
+ * 而且是**多对一**:多个段可以折进同一个块 ⇒ 块不是段的另一种写法,两面的行数与粒度都可以不同。
838
+ * ⚠️ 两张词表**没有命名空间隔离的保证**(块 id 不带前缀、operator 声明的段 id 是自由串,理论上能撞同名)⇒
839
+ * **按面各自分组**,别靠 id 形状判断自己在哪一面,更别拿块 id 去 join `context_usage` 的段面。
840
+ * 两张词表都是引擎**开集**原词:按成员渲染必须带 default 臂,认不得的原词照显示;**别**假定任何固定的分类闭集,
841
+ * 也别把某一类的缺席读成「那一类是零」(见下)。
842
+ * ⚠️ **这一面没有工具尺寸**:`tools[]` 只有名字,「工具 schema 占了多少」在这条 wire 上读不出来。
843
+ * 🔴 **缺席只说「本事件没有可读的这一面」,不是一句关于引擎的诊断**:上游整键缺席、发了空数组、逐行滤完只剩空
844
+ * 三种情形在本臂上**同形**(都折成键缺席)⇒ 看到 `sections` / `tools` / `blocks` 不在场,**不许**据此渲
845
+ * 「这个 provider 交的是不透明提示词」「这一腿没挂工具」之类的结论 —— 那需要别的独立证据。
846
+ * ⚠️ `sections[].slot` 是**语义槽**(`identity` / `security` / `harness` / `discovery` / `memory` …,开集),逐字沿用上游键名 ——
847
+ * 同一件事在 {@link ContextUsageChromeEvent} 的 `sections[].kind` 上叫 `kind`,两面各跟自己的上游,别指望同名。
848
+ * ⚠️ **skills 清单不在这一面**:core 已把它移出稳定前缀(观测口是 `steering_injected{source:"skills_listing"}`)⇒
849
+ * 清单里找不到 skills **不是**「skills 是 0」。
850
+ * ⚠️ `hash` / `contentHash` 等对账锚、以及帧上的 `constitution`(谁拥有宪法层)本包**不过境** —— 前者进程加盐跨重启不可比,后者今天零消费场景
851
+ * (每个上公面的键都是一条要长期兑现的契约:additive 键晚加不破坏、早加删不掉)。要它们按表态制另提需求。
852
+ * 🔴 本臂**不是终态**、也不是一条转录消息(转录面零行);`laneProof.lane === 'subagent'` = 某只子代这一腿的清单
853
+ * (server 今天的登记闸只登记 top-level run,所以这一支是防御性的,不是常态),别渲成主会话的。
854
+ */
855
+ export interface PromptAssembledChromeEvent {
856
+ kind: 'prompt_assembled';
857
+ laneProof: LaneProof;
858
+ /** 整条提示词的渲染字符数。缺席 = 没报,**不是 0**。 */
859
+ totalChars?: number;
860
+ /** v1 面:带标签块(`role.base` / `harness.context` / …;与 `sections[].id` **不是同一张词表**,且段→块可多对一)。
861
+ * 缺席 = 本事件没有可读的块视图(上游缺席 / 空数组 / 逐行滤光三者同形),**不是**「没有块」。 */
862
+ blocks?: readonly PromptManifestBlockView[];
863
+ /** v2 面:编译 IR 的逐段视图。🔴 整键缺席 = **本事件没有可读的段视图**(上游整键缺席 / 空数组 / 逐行滤光同形),
864
+ * **不是**各段为零、也**不能**反推「引擎没装配出段 IR」(那是上游缺席那一支的成因之一,本面分不出是哪一支)。 */
865
+ sections?: readonly PromptManifestSectionView[];
866
+ /** v2 面:这一腿挂载的工具名(MCP 类按名字前缀分组渲染靠它)。缺席 = 本事件没有可读的工具视图(三支同形),
867
+ * **不是**「没挂工具」、更不是「工具占 0」(这一面本来就没有尺寸位)。 */
868
+ tools?: readonly PromptManifestToolView[];
869
+ eventId?: string;
870
+ }
823
871
  export interface ToolDisclosureChromeEvent {
824
872
  kind: 'tool_disclosure';
825
873
  laneProof: LaneProof;
package/dist/seam.js CHANGED
@@ -11,7 +11,6 @@ const CHROME_ARM_TABLE = {
11
11
  panel_task: { required: true, duty: 'publishEngineAgentPanelEvent(event) 原样转发' },
12
12
  bgshell_settle: { required: true, duty: '后台 shell 面板行落终态(含 outputPath)' },
13
13
  bgshell_register: { required: true, duty: '后台 shell 面板行建行 + 登记「句柄→宿主 run」' },
14
- tasks_expand: { required: false, duty: '展开 ctrl+t 任务面板' },
15
14
  thinking_activity: { required: false, duty: '开/收活体 "∴ Thinking…" 行' },
16
15
  request_start: { required: false, duty: 'spinner 置 requesting 态' },
17
16
  stream_delta: { required: false, duty: '渲活体预览 + 推进 responseLength' },
@@ -93,6 +92,16 @@ const CHROME_ARM_TABLE = {
93
92
  duty: 'CC-89(0.75.0):一次压缩的非 compacted 结局(作废 / 失败等),给用户一条可见提示。`outcome` / `trigger` 是引擎开集原词,按成员渲染必带 default 臂、认不得照原词显示;' +
94
93
  '🔴 不是终态 —— 任务还在跑,别据它收 spinner;`laneProof.lane` 为 subagent = 子代的压缩结局,别渲成主会话的;成功压缩不走本臂(权威是转录的 compact_boundary)。不接 = 压缩失败对用户不可见。',
95
94
  },
95
+ prompt_assembled: {
96
+ required: false,
97
+ duty: 'CC-91(0.76.0):这一次 prepare 的提示词装配清单(每段 / 块多大、挂了哪些工具),/context 类面板的引擎侧真值来源。🔴 一次任务可能多条(cascade / verify / resume 各自 re-prepare)' +
98
+ ' ⇒ 包不去重,要「当前组成」的宿主按**最后一条**取(与 context_usage 同律);🔴 本面只有 chars **没有 tokens**(上游真字节无此位)—— 要 token 估算去读 context_usage.sections[].tokens,' +
99
+ '但那条 join **只在两面的 sections 都在场时成立**(同一张 v2 段 id 词表;v1-only 腿两面都没有段视图 ⇒ 没有这条 join);chars 与 usedTokens **禁相减**(量纲不是一条账,chars ≠ tokens 更差一层)、禁拿 chars/系数 自己编 token 数;' +
100
+ '🔴 sections[].id(core pack 段 id,core/<slot>.<name>)与 blocks[].id(v1 带标签块)是**两张不同词表**且段→块可多对一,别混着分组;两张都是引擎开集原词,按成员渲染必带 default 臂,别假定任何固定分类闭集;' +
101
+ '🔴 sections / blocks / tools 整键缺席只说「本事件没有可读的这一面」(上游缺席 / 空数组 / 逐行滤光三支同形)—— 不是各段为零、不许据此渲「provider 交的是不透明提示词」或「没挂工具」这类诊断;tools 只有名字没有尺寸;' +
102
+ 'skills 清单不在本面(观测口是 steering_injected{source:"skills_listing"})⇒ 找不到 skills 不等于 skills 是 0;' +
103
+ '转录面零行,本臂不是终态;laneProof.lane 为 subagent = 子代那一腿的清单,别渲成主会话的。不接 = 同旧(端只能按本地镜像自己估)。',
104
+ },
96
105
  tool_disclosure: {
97
106
  required: false,
98
107
  duty: 'CC-08(0.71.0):这条腿的工具名字占位普查。`deferred` / `activated` 都是**全量快照,整只替换**(不做增量合并);' +