@sema-agent/client-core 0.75.0 → 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.
Files changed (36) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/README.md +7 -2
  3. package/dist/adapt/arms.js +120 -6
  4. package/dist/adapt/panelTasks.d.ts +6 -2
  5. package/dist/adapt/panelTasks.js +6 -3
  6. package/dist/adapter/downstream/eventToSdkMessage.d.ts +45 -0
  7. package/dist/adapter/downstream/eventToSdkMessage.js +121 -6
  8. package/dist/agentSession/backgroundView.js +2 -1
  9. package/dist/agentsWireCaps.d.ts +15 -2
  10. package/dist/engineAgentPanelStore.d.ts +6 -0
  11. package/dist/engineAgentPanelStore.js +113 -10
  12. package/dist/engineErrorCodes.d.ts +4 -0
  13. package/dist/engineErrorCodes.js +14 -0
  14. package/dist/fleet/fleetLedger.js +6 -0
  15. package/dist/fleet/fleetProjection.js +7 -2
  16. package/dist/fleet/fleetRowAgentType.d.ts +9 -0
  17. package/dist/fleet/fleetRowAgentType.js +59 -0
  18. package/dist/fleetAgentPanelProjection.js +24 -3
  19. package/dist/hitl/approvalOutcomeNote.d.ts +0 -10
  20. package/dist/hitl/approvalOutcomeNote.js +35 -8
  21. package/dist/hitl/approvalResolution.d.ts +148 -0
  22. package/dist/hitl/approvalResolution.js +199 -0
  23. package/dist/hitl/approvalsFeed.d.ts +100 -2
  24. package/dist/hitl/approvalsFeed.js +234 -18
  25. package/dist/hitl/livePendingAsk.d.ts +26 -6
  26. package/dist/hitl/livePendingAsk.js +52 -12
  27. package/dist/index.d.ts +2 -0
  28. package/dist/index.js +3 -0
  29. package/dist/memorySpecWire.d.ts +175 -0
  30. package/dist/memorySpecWire.js +320 -0
  31. package/dist/panelRunningHistory.d.ts +21 -0
  32. package/dist/panelRunningHistory.js +25 -0
  33. package/dist/seam.d.ts +64 -5
  34. package/dist/seam.js +10 -1
  35. package/docs/INTEGRATION-CLIENTS.md +112 -9
  36. package/package.json +1 -1
@@ -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
+ }
@@ -19,6 +19,27 @@ export declare function holdNewbornTerminalRow(taskId: string, ev: object): void
19
19
  export declare function peekHeldNewbornTerminalRow<T extends object>(taskId: string): T | undefined;
20
20
  export declare function takeHeldNewbornTerminalRow<T extends object>(taskId: string): T | undefined;
21
21
  export declare function dropHeldNewbornTerminalRow(taskId: string): void;
22
+ /** 一条缓行**自带**的 wire 侧身份键(它是一条 fleet 行帧,两把键都可缺席)。 */
23
+ export interface HeldNewbornTerminalRef {
24
+ /** 缓行在本表里的键 = fleet 行 id 尾段。 */
25
+ readonly taskId: string;
26
+ readonly transcriptId?: string;
27
+ readonly parentToolCallId?: string;
28
+ }
29
+ /**
30
+ * 0.75.1(实测反例):`end` 归一的三把可能的键里,**后两把只在行帧真发布过时才登记**
31
+ * (`transcriptId → 尾段` 钥匙表与 `尾段 → UUID` 反向所有权都由 `publishEngineAgentPanelEvent`
32
+ * 的 fleet-row 臂写),而缓行的定义就是「还没发布」⇒ `tick{UUID}` 先上屏、终态行 `{尾段,
33
+ * transcriptId: UUID}` 被缓住、`end{UUID}` 到达时三把键全查不到,缓行永远放不出、短命子代的
34
+ * 最终用量丢。所以漏斗还需要第四条腿:按缓行**自己带着的** wire 侧键反查。
35
+ *
36
+ * 🔴 **为什么是遍历、不是第六本索引**:面板这一族已经有五本账(running 历史 / ended 账 / 缓行 /
37
+ * 所有权 / 周期水位),再加一张 `transcriptId → 缓行键` 的索引就要在 hold / take / drop / 容量淘汰
38
+ * 四个写口上各维护一次同步 —— 那是本族出过真回归的形(一本账写了另一本没写 ⇒ 缓行放两次或永不放)。
39
+ * 缓行表有界 4096 且只在 **`end` 递达**时被扫(不是每拍;一条子代一生至多几次),所以遍历的成本
40
+ * 上限是一条 end 读 4096 个小对象,换来的是「缓行表自己就是唯一真源」。
41
+ */
42
+ export declare function heldNewbornTerminalRefs(): HeldNewbornTerminalRef[];
22
43
  /**
23
44
  * 测试钩。清四样:① 「发布过 running 且还没结」的行 id 集合 ② 溢出标(溢出后读口一律答保守)③ 「递达过 end」的行 id 集合 ④ 缓着的首见终态行。
24
45
  * 🔴 连续场景中途别调:清掉之后,「首见即终态」的判定会把此前见过 running / 结过的行当成从没见过。
@@ -84,6 +84,31 @@ export function takeHeldNewbornTerminalRow(taskId) {
84
84
  export function dropHeldNewbornTerminalRow(taskId) {
85
85
  heldNewborn.delete(taskId);
86
86
  }
87
+ /**
88
+ * 0.75.1(实测反例):`end` 归一的三把可能的键里,**后两把只在行帧真发布过时才登记**
89
+ * (`transcriptId → 尾段` 钥匙表与 `尾段 → UUID` 反向所有权都由 `publishEngineAgentPanelEvent`
90
+ * 的 fleet-row 臂写),而缓行的定义就是「还没发布」⇒ `tick{UUID}` 先上屏、终态行 `{尾段,
91
+ * transcriptId: UUID}` 被缓住、`end{UUID}` 到达时三把键全查不到,缓行永远放不出、短命子代的
92
+ * 最终用量丢。所以漏斗还需要第四条腿:按缓行**自己带着的** wire 侧键反查。
93
+ *
94
+ * 🔴 **为什么是遍历、不是第六本索引**:面板这一族已经有五本账(running 历史 / ended 账 / 缓行 /
95
+ * 所有权 / 周期水位),再加一张 `transcriptId → 缓行键` 的索引就要在 hold / take / drop / 容量淘汰
96
+ * 四个写口上各维护一次同步 —— 那是本族出过真回归的形(一本账写了另一本没写 ⇒ 缓行放两次或永不放)。
97
+ * 缓行表有界 4096 且只在 **`end` 递达**时被扫(不是每拍;一条子代一生至多几次),所以遍历的成本
98
+ * 上限是一条 end 读 4096 个小对象,换来的是「缓行表自己就是唯一真源」。
99
+ */
100
+ export function heldNewbornTerminalRefs() {
101
+ const out = [];
102
+ for (const [taskId, ev] of heldNewborn) {
103
+ const row = ev;
104
+ out.push({
105
+ taskId,
106
+ ...(typeof row.transcriptId === 'string' && row.transcriptId.length > 0 ? { transcriptId: row.transcriptId } : {}),
107
+ ...(typeof row.parentToolCallId === 'string' && row.parentToolCallId.length > 0 ? { parentToolCallId: row.parentToolCallId } : {}),
108
+ });
109
+ }
110
+ return out;
111
+ }
87
112
  /**
88
113
  * 测试钩。清四样:① 「发布过 running 且还没结」的行 id 集合 ② 溢出标(溢出后读口一律答保守)③ 「递达过 end」的行 id 集合 ④ 缓着的首见终态行。
89
114
  * 🔴 连续场景中途别调:清掉之后,「首见即终态」的判定会把此前见过 running / 结过的行当成从没见过。