@x-otto/memory 0.0.1-alpha.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/README.md +161 -0
- package/dist/index.d.ts +1117 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +201 -0
- package/dist/index.js.map +1 -0
- package/package.json +36 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,1117 @@
|
|
|
1
|
+
import { Message } from "@x-otto/ai";
|
|
2
|
+
|
|
3
|
+
//#region src/types.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* 触发阈值的三种表达。
|
|
6
|
+
* - `['tokens', n]` 绝对 token 数。
|
|
7
|
+
* - `['fraction', f]` 占窗口比例(显式覆盖逃生口;不再是默认压缩触发)。
|
|
8
|
+
* - `['headroom', h]` 绝对留白:阈值 = max(安全下界, window − h)。默认压缩触发用此——
|
|
9
|
+
* 自适应窗口大小(小窗早压防溢出、大窗晚压省成本)。
|
|
10
|
+
*/
|
|
11
|
+
type ContextSize = ['tokens', number] | ['fraction', number] | ['headroom', number];
|
|
12
|
+
type StorageType = 'file' | 'http' | 'memory';
|
|
13
|
+
interface MemorySource {
|
|
14
|
+
path: string;
|
|
15
|
+
writable: boolean;
|
|
16
|
+
}
|
|
17
|
+
interface MemoryStore {
|
|
18
|
+
load(sources: MemorySource[]): Promise<Map<string, string>>;
|
|
19
|
+
/**
|
|
20
|
+
* 写入一份记忆文件。**远端同步的延迟语义按实现分化**(终局审查 2026-07-18 显式声明):
|
|
21
|
+
* `HttpMemoryStore` 对 user-scope 路径是本地写完即 resolve(远端 fire-and-forget,
|
|
22
|
+
* 不阻塞、静默降级),对 workspace-scope 路径则 await 远端写入(D6 冲突检测的
|
|
23
|
+
* onConflict 回调需要与 write() resolve 同步可见)——调用方不应假设 resolve 时
|
|
24
|
+
* 远端一定已同步。纯本地实现(FileSystemMemoryStore)resolve 即落盘完成。
|
|
25
|
+
*/
|
|
26
|
+
write(path: string, content: string): Promise<void>;
|
|
27
|
+
watch?(sources: MemorySource[], onChange: (path: string) => void): () => void;
|
|
28
|
+
}
|
|
29
|
+
interface PruneConfig {
|
|
30
|
+
trigger: ContextSize;
|
|
31
|
+
protect: ContextSize;
|
|
32
|
+
minimum: number;
|
|
33
|
+
/**
|
|
34
|
+
* 可再生工具允许列表(fail-closed)。**仅**裁此列表内工具的 tool_result;
|
|
35
|
+
* 列表外(未知/MCP/agent 产物/一次性快照如 capture_file_state)默认保留——其输出不可廉价重生,
|
|
36
|
+
* 裁掉=真实信息损失。空列表 = 不裁任何 tool_result。
|
|
37
|
+
*/
|
|
38
|
+
regenerableTools: string[];
|
|
39
|
+
/** 二次保护:即便在 regenerableTools 内,列入此处的工具仍不裁(覆盖)。 */
|
|
40
|
+
protectedTools: string[];
|
|
41
|
+
truncateTools: string[];
|
|
42
|
+
truncateMaxLength: number;
|
|
43
|
+
}
|
|
44
|
+
interface PruneResult {
|
|
45
|
+
messages: Message[];
|
|
46
|
+
prunedCount: number;
|
|
47
|
+
tokensSaved: number;
|
|
48
|
+
changed: boolean;
|
|
49
|
+
}
|
|
50
|
+
interface CompactionConfig {
|
|
51
|
+
enabled: boolean;
|
|
52
|
+
trigger: ContextSize;
|
|
53
|
+
keepRecent: ContextSize;
|
|
54
|
+
/**
|
|
55
|
+
* RFC-321 M6:生效上下文窗口(token),供 `compact()` 做**压缩输入窗口预检**——
|
|
56
|
+
* 待摘要文本超 `窗口 × MEMORY_COMPACTION_INPUT_WINDOW_FRACTION` 时改走分批归约
|
|
57
|
+
* (map-reduce),避免单个超长 prompt 直接 413 导致摘要产不出(底线2 机制保证)。
|
|
58
|
+
* 由 `MemoryManager.compact()` 从 `effectiveContextWindow()` 注入;未提供时退回
|
|
59
|
+
* 单批行为(向后兼容)。
|
|
60
|
+
*/
|
|
61
|
+
contextWindow?: number;
|
|
62
|
+
reserveTokens: number;
|
|
63
|
+
/**
|
|
64
|
+
* 压缩 total 口径硬顶占窗口比例(RFC-066 增量修复,事故 f9bab4c0)。不设则用
|
|
65
|
+
* MEMORY_COMPACTION_HARD_CEILING_FRACTION 默认(0.90,可经 env 覆盖)。config/api option
|
|
66
|
+
* 层显式覆盖点——宿主可按模型/场景定制硬顶余量。
|
|
67
|
+
*/
|
|
68
|
+
hardCeilingFraction?: number;
|
|
69
|
+
/**
|
|
70
|
+
* RFC-324 D3:摘要覆盖率 warning 阈值。coverage 低于此值时,下次压缩调大批次上限
|
|
71
|
+
* (每批更小源内容 → 提高覆盖率)。缺省 `MEMORY_COVERAGE_WARN_THRESHOLD`(0.7)。
|
|
72
|
+
* 由宿主从 `.otto/config.json` 的 `residency.coverage_warn_threshold` 注入。
|
|
73
|
+
*/
|
|
74
|
+
coverageWarnThreshold?: number;
|
|
75
|
+
/**
|
|
76
|
+
* RFC-324 D3:摘要覆盖率 error 阈值。coverage 低于此值时记 warn 日志并**双重**调大
|
|
77
|
+
* 批次上限(更激进的质量保护)。缺省 `MEMORY_COVERAGE_ERROR_THRESHOLD`(0.5)。
|
|
78
|
+
* 必须 ≤ warn 阈值,否则 error 分支永不可达(接线层已做钳制)。
|
|
79
|
+
*/
|
|
80
|
+
coverageErrorThreshold?: number;
|
|
81
|
+
/**
|
|
82
|
+
* RFC-340 D1:摘要覆盖率**硬下限**。低于此值时 `MemoryManager.process()` 不建立
|
|
83
|
+
* 压缩边界(视同本轮压缩未发生),阻断"摘要未覆盖的历史被 `capStoredHistory`
|
|
84
|
+
* 当作死历史物理删除"这条不可逆损失路径。缺省 `MEMORY_COVERAGE_FLOOR_THRESHOLD`(0.3)。
|
|
85
|
+
*
|
|
86
|
+
* 不变式 `floor ≤ error ≤ warn`——floor 是叠加在既有 warn/error 递推之上的**新档位**,
|
|
87
|
+
* 不替换它们(RFC-340 规则 4)。接线层负责钳制,防配置写反导致档位失效。
|
|
88
|
+
*/
|
|
89
|
+
coverageFloorThreshold?: number;
|
|
90
|
+
customInstructions?: string;
|
|
91
|
+
}
|
|
92
|
+
interface CutPoint {
|
|
93
|
+
firstKeptIndex: number;
|
|
94
|
+
turnStartIndex: number;
|
|
95
|
+
isSplitTurn: boolean;
|
|
96
|
+
}
|
|
97
|
+
interface CompactionPreparation {
|
|
98
|
+
messagesToSummarize: Message[];
|
|
99
|
+
turnPrefixMessages: Message[];
|
|
100
|
+
preservedMessages: Message[];
|
|
101
|
+
isSplitTurn: boolean;
|
|
102
|
+
tokensBefore: number;
|
|
103
|
+
previousSummary?: string;
|
|
104
|
+
fileOps: {
|
|
105
|
+
read: string[];
|
|
106
|
+
modified: string[];
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
interface CompactionResult {
|
|
110
|
+
summary: string;
|
|
111
|
+
firstKeptIndex: number;
|
|
112
|
+
tokensBefore: number;
|
|
113
|
+
archivePath?: string;
|
|
114
|
+
/**
|
|
115
|
+
* RFC-340 D1:摘要覆盖率 = 真实喂入摘要器的字符量 ÷ 源字符量。单批路径恒为 1
|
|
116
|
+
* (全量喂入);map-reduce 路径为实算值(截断导致 <1)。
|
|
117
|
+
*
|
|
118
|
+
* **纯上报,compaction 层不据此决策**——判定归 `MemoryManager.compact()`(策略与
|
|
119
|
+
* 执行分离):低于 floor 时给摘要正文前置低保真标注(`buildLowCoverageNotice`),
|
|
120
|
+
* 边界照常建立。标注本身进摘要正文(对模型可见),故不需要平行的布尔标志位——
|
|
121
|
+
* 终局复审(2026-08-10)删除了首版的 `coverageDegraded` 字段:零消费方的 write-only
|
|
122
|
+
* 状态位是形式化包装,可观测性已由 warn 日志 + 摘要文本承载。
|
|
123
|
+
*/
|
|
124
|
+
coverage: number;
|
|
125
|
+
}
|
|
126
|
+
interface ArchiveStorage {
|
|
127
|
+
readonly type: StorageType;
|
|
128
|
+
archive(sessionId: string, messages: Message[], summary: string): Promise<string>;
|
|
129
|
+
read(archivePath: string): Promise<string>;
|
|
130
|
+
list(sessionId: string): Promise<ArchiveEntry[]>;
|
|
131
|
+
/**
|
|
132
|
+
* 会话销毁时驱逐其归档数据(可选——仅内存态实现如 InMemoryArchiveStorage 需要,
|
|
133
|
+
* 落盘/远程实现天然无需随进程内 session 生命周期清理)。MemoryManager.forgetSession
|
|
134
|
+
* 无条件调用(缺省时 no-op),防内存态实现在长跑进程中无界增长。
|
|
135
|
+
*/
|
|
136
|
+
forgetSession?(sessionId: string): void;
|
|
137
|
+
}
|
|
138
|
+
interface ArchiveEntry {
|
|
139
|
+
path: string;
|
|
140
|
+
timestamp: number;
|
|
141
|
+
messageCount: number;
|
|
142
|
+
summary: string;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* MemoryManager 读取会话条目的最小契约——避免 @x-otto/memory 依赖
|
|
146
|
+
* @x-otto/session。实现侧(RelationalSessionPersistence / SqliteSessionRepository)满足此形。
|
|
147
|
+
*/
|
|
148
|
+
interface MinimalEntry {
|
|
149
|
+
readonly type: 'message' | 'compaction' | 'clear';
|
|
150
|
+
/** 插入序(seq),用于排序;缺省时按负载时间戳回落。 */
|
|
151
|
+
readonly seq?: number;
|
|
152
|
+
readonly data: unknown;
|
|
153
|
+
}
|
|
154
|
+
/** 从会话条目中提取 Message(供 formatArchive 重建归档)。 */
|
|
155
|
+
type MessageFromEntry = (entry: MinimalEntry) => Message | null;
|
|
156
|
+
interface ContextTokenEstimate {
|
|
157
|
+
total: number;
|
|
158
|
+
fromUsage: number;
|
|
159
|
+
fromEstimate: number;
|
|
160
|
+
lastUsageIndex: number;
|
|
161
|
+
/**
|
|
162
|
+
* 稳定系统前缀的派生 token 数(system prompt + tool schemas + memory 注入)。
|
|
163
|
+
* 这些不在 `messages` 数组里(provider 层单独注入),但被 provider 的 usage 计入——故
|
|
164
|
+
* = `fromUsage − (usage 覆盖区间内消息的纯估算)`。无 usage 时为 0。
|
|
165
|
+
*/
|
|
166
|
+
derivedPrefix: number;
|
|
167
|
+
/**
|
|
168
|
+
* BodyAfterPrefix 触发口径 = `total − derivedPrefix`。压缩/prune 软触发以此为准,
|
|
169
|
+
* 使大的稳定缓存前缀不把上下文管理提前顶过阈值(减压缩频次 = provider 无关的缓存收益)。
|
|
170
|
+
*/
|
|
171
|
+
bodyAfterPrefix: number;
|
|
172
|
+
}
|
|
173
|
+
interface MemoryManagerConfig {
|
|
174
|
+
sources?: MemorySource[];
|
|
175
|
+
memoryStore?: MemoryStore;
|
|
176
|
+
compaction?: Partial<CompactionConfig>;
|
|
177
|
+
prune?: Partial<PruneConfig>;
|
|
178
|
+
/** @deprecated archive = session 派生视图;此字段仅作 InMemory 回退(无 session entries 时)。 */
|
|
179
|
+
archiveStorage?: ArchiveStorage;
|
|
180
|
+
/**
|
|
181
|
+
* 从 session entries 按 sessionId 加载条目的委托。
|
|
182
|
+
* MemoryManager 用此按需重建 archive(compact 间 messages 还原),替代独立 archive persistence。
|
|
183
|
+
* 未注入时回退到 archiveStorage(InMemory)。实现侧在 App 接线,惰性绕开「session persistence 在 memory 之后构建」时序。
|
|
184
|
+
*/
|
|
185
|
+
loadSessionEntries?: (sessionId: string) => Promise<MinimalEntry[]>;
|
|
186
|
+
/**
|
|
187
|
+
* 从 MinimalEntry.data 中提取 Message(供 formatArchive)。
|
|
188
|
+
* 实现侧简单:message entries 的 data = Message。注入以避免 @x-otto/memory 依赖 @x-otto/ai Message 类型。
|
|
189
|
+
*/
|
|
190
|
+
messageFromEntry?: MessageFromEntry;
|
|
191
|
+
summarize: (prompt: string, options?: {
|
|
192
|
+
maxTokens?: number;
|
|
193
|
+
signal?: AbortSignal;
|
|
194
|
+
}) => Promise<string>;
|
|
195
|
+
estimateTokens?: (text: string) => number;
|
|
196
|
+
model?: {
|
|
197
|
+
contextWindow: number;
|
|
198
|
+
maxOutput: number;
|
|
199
|
+
};
|
|
200
|
+
/**
|
|
201
|
+
* **惰性**上下文窗口解析器,每次触发判定时调用,使 prune/compaction 阈值
|
|
202
|
+
* 始终跟随当前生效模型。解决"构造期 model 未知(惰性解析)→ contextWindow 落 200K 默认 +
|
|
203
|
+
* reconfigure 未接线 → DeepSeek 1M 被当 200K、compaction 在真实容量 ~17% 就过早触发"的 bug。
|
|
204
|
+
* 返回 undefined(模型仍未定)时回退构造期 contextWindow。优先级高于 reconfigure 写入值。
|
|
205
|
+
*/
|
|
206
|
+
getContextWindow?: () => number | undefined;
|
|
207
|
+
/**
|
|
208
|
+
* 会话消息内容字节估算预算(RFC-178 触发器 B 软段,同 `@x-otto/session-contract` 的
|
|
209
|
+
* `SESSION_MAX_CONTENT_BYTES`,调用方注入——本包不直接依赖 session-contract,对齐
|
|
210
|
+
* `messageFromEntry`/`getContextWindow` 等既有"实现侧注入避免跨包依赖"模式)。
|
|
211
|
+
* 用于 `needsCompaction` 的字节触发因子:估算字节逼近预算(×0.9)时提前强制压缩,
|
|
212
|
+
* 确保字节维度的存储层收紧(`capStoredHistoryByBytes`)触发时压缩边界必然已存在。
|
|
213
|
+
* 未注入或 `getEstimatedBytes` 缺席时不启用该因子(向后兼容)。
|
|
214
|
+
*/
|
|
215
|
+
maxContentBytes?: number;
|
|
216
|
+
/**
|
|
217
|
+
* RFC-321 M3:**生效**字节预算的惰性读数(覆盖 `maxContentBytes` 静态值)。
|
|
218
|
+
*
|
|
219
|
+
* 存在理由:`ResidencyGovernor`(RFC-178 触发器 C)在内存压力 warning/critical 时把
|
|
220
|
+
* 驻留层的字节 cap 除以 2/4,但 memory 侧的压缩触发阈值此前是**构造期注入的静态值**,
|
|
221
|
+
* 无 reconfigure 通路——实测 critical 档下驻留层 cap 已收紧、而 `needsCompaction` 仍
|
|
222
|
+
* 按全值判定,导致「cap 触发时压缩边界必然已存在」的不变式在压力档结构性失效
|
|
223
|
+
* (而压力档恰是最需要它成立的时刻)。
|
|
224
|
+
*
|
|
225
|
+
* 注入后两侧读同一个有效值,收紧同步生效。缺省时回退 `maxContentBytes`(向后兼容)。
|
|
226
|
+
* 模式对齐同文件的 `getContextWindow`/`getEstimatedBytes`(实现侧注入避免跨包依赖)。
|
|
227
|
+
*/
|
|
228
|
+
getMaxContentBytes?: () => number | undefined;
|
|
229
|
+
/**
|
|
230
|
+
* RFC-324 D3:摘要覆盖率阈值的惰性读数。
|
|
231
|
+
*
|
|
232
|
+
* 必须是 getter 而非静态值——`compactionConfig` 在构造期/`reconfigure` 时快照,
|
|
233
|
+
* 而宿主 settings 在 App 构造**之后**才 load,静态传值会永远读到 undefined
|
|
234
|
+
* (这正是此前 `coverage_*` 配置零消费的成因之一)。模式对齐同文件的
|
|
235
|
+
* `getContextWindow`/`getMaxContentBytes`。
|
|
236
|
+
*/
|
|
237
|
+
getCoverageThresholds?: () => {
|
|
238
|
+
warn?: number;
|
|
239
|
+
error?: number;
|
|
240
|
+
floor?: number;
|
|
241
|
+
} | undefined;
|
|
242
|
+
/**
|
|
243
|
+
* 当前会话消息字节估算读数(RFC-178:`InMemorySession.estimatedContentBytes` 的惰性
|
|
244
|
+
* 读取器,跟随活跃会话)。与 `maxContentBytes` 配对注入,缺一不启用字节因子。
|
|
245
|
+
*/
|
|
246
|
+
getEstimatedBytes?: () => number | undefined;
|
|
247
|
+
}
|
|
248
|
+
interface MemoryDefaults {
|
|
249
|
+
compaction: CompactionConfig;
|
|
250
|
+
prune: PruneConfig;
|
|
251
|
+
}
|
|
252
|
+
//#endregion
|
|
253
|
+
//#region src/memory-manager.d.ts
|
|
254
|
+
declare class MemoryManager {
|
|
255
|
+
private readonly persistent;
|
|
256
|
+
/**
|
|
257
|
+
* archive = session 派生视图——loadSessionEntries 就位时走 session 条目的
|
|
258
|
+
* compaction 边界间重建;未就位(无持久化/V 形态)时回退到 archiveStorage(InMemory)。
|
|
259
|
+
*/
|
|
260
|
+
private readonly archiveStorage;
|
|
261
|
+
private readonly loadSessionEntries?;
|
|
262
|
+
private readonly messageFromEntry?;
|
|
263
|
+
private readonly summarize;
|
|
264
|
+
private readonly estimateTokens;
|
|
265
|
+
/** 构造时保留原始配置副本,供 reconfigure 用新窗口重算阈值。 */
|
|
266
|
+
private readonly rawConfig;
|
|
267
|
+
private contextWindow;
|
|
268
|
+
/** 惰性窗口解析器(跟随生效模型),优先于 this.contextWindow。 */
|
|
269
|
+
private readonly getContextWindow?;
|
|
270
|
+
private compactionConfig;
|
|
271
|
+
private pruneConfig;
|
|
272
|
+
/** 会话消息字节估算预算(RFC-178 字节触发因子;RFC-321 M2 后是唯一的非 token 触发因子)。 */
|
|
273
|
+
private readonly maxContentBytes?;
|
|
274
|
+
/** RFC-321 M3:生效字节预算惰性读数(跟随 ResidencyGovernor 收紧档位)。 */
|
|
275
|
+
private readonly getMaxContentBytes?;
|
|
276
|
+
private readonly getEstimatedBytes?;
|
|
277
|
+
private readonly previousSummaries;
|
|
278
|
+
/** archive Path 单调发号——每会话内独立(archive:{sessionId}:{1..N})。 */
|
|
279
|
+
private readonly archiveIdCounter;
|
|
280
|
+
/**
|
|
281
|
+
* 终局架构 review 建议优化项:并发 process() 门闩(per-session)。
|
|
282
|
+
*
|
|
283
|
+
* 单进程事件循环下 JS 无 TOCTOU(check-then-act 之间不会被抢占),但若同一 sessionId
|
|
284
|
+
* 的两次 process() 调用在 await LLM 压缩(compact())期间交错(如同一会话被两个不同
|
|
285
|
+
* 入口几乎同时触发 process,例如 work-loop 的常规压缩检查与用户手动 /compact 命令
|
|
286
|
+
* 撞车),后调用者会基于前调用者压缩前的旧快照重复生成摘要,产生冗余/冲突的
|
|
287
|
+
* previousSummaries 写入。此 flag 在 compact() 的 await 期间挡住同 session 的重入,
|
|
288
|
+
* 后来者直接跳过本轮压缩(下一次 needsCompaction 判定仍会触发,不丢失压缩机会,只是
|
|
289
|
+
* 延后一轮),finally 保证异常路径也会复位。
|
|
290
|
+
*/
|
|
291
|
+
private readonly compactionInFlight;
|
|
292
|
+
constructor(config: MemoryManagerConfig);
|
|
293
|
+
/**
|
|
294
|
+
* `/clear`——驱逐本会话的滚动压缩摘要桶(L2 工作记忆)。L1 持久知识(persistent)
|
|
295
|
+
* 与 L3 归档(archiveStorage)不动。无 sessionId 走 __default__ 桶(单会话场景)。
|
|
296
|
+
*/
|
|
297
|
+
clearSession(sessionId?: string): void;
|
|
298
|
+
/**
|
|
299
|
+
* 审计修复:会话销毁/驱逐时遗忘其全部 per-session 状态——防三个 Map 在长跑进程中无界增长。
|
|
300
|
+
* 与 clearSession 不同:此处清 archiveIdCounter(会话终态,无再压缩可能)。
|
|
301
|
+
*/
|
|
302
|
+
forgetSession(sessionId: string): void;
|
|
303
|
+
/**
|
|
304
|
+
* 模型切换时更新上下文窗口 + 重算 compaction/prune 触发阈值。
|
|
305
|
+
* 调用后 needsCompaction/needsPrune 基于新窗口判定。
|
|
306
|
+
*/
|
|
307
|
+
reconfigure(model: {
|
|
308
|
+
contextWindow: number;
|
|
309
|
+
maxOutput: number;
|
|
310
|
+
}): void;
|
|
311
|
+
/**
|
|
312
|
+
* 生效上下文窗口——优先惰性解析器(跟随当前模型),回退构造期/ reconfigure 值。
|
|
313
|
+
* prune/compaction 一切触发线均按此计算,避免构造期 model 未知导致的 200K 默认锁死。
|
|
314
|
+
*/
|
|
315
|
+
private effectiveContextWindow;
|
|
316
|
+
/**
|
|
317
|
+
* 主动检查当前消息量是否超过压缩触发线(基于 reconfigure 后的新窗口);
|
|
318
|
+
* 超过则 force-compact。用于模型切换时预防下一轮 413。
|
|
319
|
+
*/
|
|
320
|
+
ensureFitsWindow(messages: readonly Message[], sessionId?: string, signal?: AbortSignal): Promise<{
|
|
321
|
+
messages: Message[];
|
|
322
|
+
compacted: boolean;
|
|
323
|
+
summary?: string;
|
|
324
|
+
replacement?: Message[];
|
|
325
|
+
}>;
|
|
326
|
+
loadMemory(): Promise<void>;
|
|
327
|
+
getMemoryPrompt(): string;
|
|
328
|
+
reloadMemory(): Promise<void>;
|
|
329
|
+
getMemories(): ReadonlyMap<string, string>;
|
|
330
|
+
needsPrune(messages: readonly Message[]): boolean;
|
|
331
|
+
/** 估算复用变体(终局审查 2026-07-18:process() 一次估算供判定+执行两处消费,
|
|
332
|
+
* 消除同批消息的重复全量遍历——WeakMap 缓存对 spread 新建的消息数组会穿透)。 */
|
|
333
|
+
private needsPruneWith;
|
|
334
|
+
prune(messages: readonly Message[], force?: boolean): PruneResult;
|
|
335
|
+
private pruneWith;
|
|
336
|
+
needsCompaction(messages: readonly Message[]): boolean;
|
|
337
|
+
/**
|
|
338
|
+
* 驻留(字节)维度是否已逼近预算——`needsCompaction` 的非 token 触发因子。
|
|
339
|
+
*
|
|
340
|
+
* 单独抽出的原因(RFC-321 R10,二轮实证):字节因子与已删除的条数因子**同型死区**——
|
|
341
|
+
* 判定为真不代表切得动。`findCutPoint` 要求总 token ≥ keepRecent(窗口×0.1),大窗口下
|
|
342
|
+
* 「字节超预算但总 token 偏小」的形态(如少量大附件)会让 `prepareCompaction` 返回 null,
|
|
343
|
+
* 压缩静默不发生(实证:1M 窗口、200 条 × 1000 字符、字节因子触发 → compacted=false、
|
|
344
|
+
* LLM 调用 0)。故 `process()` 在**由驻留压力驱动**时同样启用 keepRecent 钳制,与
|
|
345
|
+
* 逃生阀 force 路径共用同一修法。
|
|
346
|
+
*/
|
|
347
|
+
private residencyPressure;
|
|
348
|
+
isEligibleForManualCompact(messages: readonly Message[]): boolean;
|
|
349
|
+
/**
|
|
350
|
+
* @param options.force RFC-321 R10:强制压缩语义下 keepRecent 钳制到当前总量一半,
|
|
351
|
+
* 使"总 token < keepRecent"的超短会话也能切出边界(否则 force 仍被切点约束挡住、
|
|
352
|
+
* 压缩静默不发生)。详见 `compaction.ts` 的 prepareCompaction 内注释。
|
|
353
|
+
*/
|
|
354
|
+
prepareCompaction(messages: readonly Message[], sessionId?: string, options?: {
|
|
355
|
+
force?: boolean;
|
|
356
|
+
}): CompactionPreparation | null;
|
|
357
|
+
compact(preparation: CompactionPreparation, sessionId?: string, signal?: AbortSignal): Promise<CompactionResult>;
|
|
358
|
+
/**
|
|
359
|
+
* 压缩归档单源(reused B1 缓存命中 / fresh LLM 压缩两路共用,memory-1 去重):递增 per-session
|
|
360
|
+
* 归档序号 → 赋逻辑 `archive:<sessionId>:<idx>` → 仅当无 loadSessionEntries 重建器时落盘
|
|
361
|
+
* (有重建器则归档按需从会话条目还原,archivePath 只作逻辑引用)。
|
|
362
|
+
*/
|
|
363
|
+
private recordCompactionArchive;
|
|
364
|
+
process(messages: readonly Message[], sessionId?: string, signal?: AbortSignal, options?: {
|
|
365
|
+
force?: boolean;
|
|
366
|
+
skipPrune?: boolean;
|
|
367
|
+
}): Promise<{
|
|
368
|
+
messages: Message[];
|
|
369
|
+
summary?: string;
|
|
370
|
+
archivePath?: string;
|
|
371
|
+
pruned: boolean;
|
|
372
|
+
compacted: boolean;
|
|
373
|
+
replacement?: Message[];
|
|
374
|
+
pruneMetrics?: {
|
|
375
|
+
prunedCount: number;
|
|
376
|
+
tokensSaved: number;
|
|
377
|
+
messagesBefore: number;
|
|
378
|
+
messagesAfter: number;
|
|
379
|
+
};
|
|
380
|
+
}>;
|
|
381
|
+
listArchives(sessionId: string): Promise<ArchiveEntry[]>;
|
|
382
|
+
/** 从 session entries 的 compaction 节点枚举 ArchiveEntry 列表。 */
|
|
383
|
+
private listArchivesFromEntries;
|
|
384
|
+
readArchive(archivePath: string): Promise<string>;
|
|
385
|
+
/** 从 session entries 的 compaction 边界间重建归档全文。 */
|
|
386
|
+
private readArchiveFromEntries;
|
|
387
|
+
dispose(): void;
|
|
388
|
+
}
|
|
389
|
+
declare function createMemoryManager(config: MemoryManagerConfig): MemoryManager;
|
|
390
|
+
//#endregion
|
|
391
|
+
//#region src/persistent-memory.d.ts
|
|
392
|
+
declare const DEFAULT_MEMORY_SOURCES: MemorySource[];
|
|
393
|
+
declare class PersistentMemory {
|
|
394
|
+
private readonly store;
|
|
395
|
+
private readonly sources;
|
|
396
|
+
private memories;
|
|
397
|
+
private unwatch?;
|
|
398
|
+
constructor(store: MemoryStore, sources?: MemorySource[]);
|
|
399
|
+
load(): Promise<void>;
|
|
400
|
+
reload(): Promise<void>;
|
|
401
|
+
getInjection(): string;
|
|
402
|
+
getMemories(): ReadonlyMap<string, string>;
|
|
403
|
+
/**
|
|
404
|
+
* 写入可写源(M13-03):让 MemorySource.writable 标记成真。
|
|
405
|
+
* 仅 sources 中声明且 writable: true 的路径可写;写后同步内存缓存(无需全量 reload)。
|
|
406
|
+
*/
|
|
407
|
+
write(sourcePath: string, content: string): Promise<void>;
|
|
408
|
+
watching(): void;
|
|
409
|
+
stop(): void;
|
|
410
|
+
dispose(): void;
|
|
411
|
+
}
|
|
412
|
+
declare class InMemoryMemoryStore implements MemoryStore {
|
|
413
|
+
private data;
|
|
414
|
+
set(path: string, content: string): void;
|
|
415
|
+
load(sources: MemorySource[]): Promise<Map<string, string>>;
|
|
416
|
+
write(path: string, content: string): Promise<void>;
|
|
417
|
+
}
|
|
418
|
+
declare class FileSystemMemoryStore implements MemoryStore {
|
|
419
|
+
private cwd;
|
|
420
|
+
/**
|
|
421
|
+
* `~/` 前缀路径展开的用户主目录 override(缺省调用真实 `os.homedir()`)。
|
|
422
|
+
*
|
|
423
|
+
* **测试隔离缺陷修复**(终局 memory 审计发现的持续污染源):此前 `resolvePath()` 硬编码
|
|
424
|
+
* 调用 `homedir()`,完全忽略构造函数传入的 `cwd`——测试传 `localDir: TEST_DIR` 只对不带
|
|
425
|
+
* `~/` 前缀的相对路径生效,但 `DEFAULT_MEMORY_SOURCES`/测试用例大量使用 `~/.otto/AGENTS.md`
|
|
426
|
+
* 这类带 `~/` 前缀的路径,一律写穿到真实用户主目录(`http-memory-store.test.ts` 的
|
|
427
|
+
* "remote user"/"memory index content" 等 fixture 字符串因此持续覆盖真实 `~/.otto/AGENTS.md`)。
|
|
428
|
+
* 生产环境不传时行为完全不变(仍用真实 `homedir()`),仅测试可注入隔离目录。
|
|
429
|
+
*/
|
|
430
|
+
private readonly homeDirOverride?;
|
|
431
|
+
constructor(cwd?: string, homeDirOverride?: string);
|
|
432
|
+
load(sources: MemorySource[]): Promise<Map<string, string>>;
|
|
433
|
+
write(path: string, content: string): Promise<void>;
|
|
434
|
+
private resolvePath;
|
|
435
|
+
}
|
|
436
|
+
//#endregion
|
|
437
|
+
//#region src/http-memory-store.d.ts
|
|
438
|
+
/**
|
|
439
|
+
* M71c + RFC-164 M164-2: 远端 user 级 / workspace 级 memory 的 HTTP MemoryStore。
|
|
440
|
+
*
|
|
441
|
+
* 对 user 源(~/.otto/AGENTS.md 等)走远端 HTTP 读/写,project 源走本地文件系统;
|
|
442
|
+
* R 形态 opt-in 后 workspace 源(./.otto/MEMORY.md 等)同样走远端(M164-2 新增)。
|
|
443
|
+
* - 读:HTTP GET → 缓存本地 → 返回;远端不可达时回退本地缓存(R5 降级)
|
|
444
|
+
* - 写:本地缓存先写,再 fire-and-forget 推远端(R5 降级:远端不可达不阻塞)
|
|
445
|
+
*
|
|
446
|
+
* 远端 key:
|
|
447
|
+
* - user-scope = `user:<userId>:<相对 ~/.otto/ 的文件路径>`(如 AGENTS.md/MEMORY.md/memory/foo.md)。
|
|
448
|
+
* - workspace-scope(M164-2)= `workspace:<userId>:<workspaceKey>:<相对 workspace 根的文件路径>`
|
|
449
|
+
* (`userId` 仍是安全边界,`workspaceKey` 是同用户下的项目分区,见 `entity-route.ts` 设计记录)。
|
|
450
|
+
*
|
|
451
|
+
* **惰性 getter 而非构造期快照**(RFC-164 M164-2 摸底阶段发现的时序断层修正):
|
|
452
|
+
* `workspaceKey`/`isWorkspaceSyncEnabled` 均为函数,每次 `load()`/`write()` 才实时求值——
|
|
453
|
+
* `HttpMemoryStore` 从始至终只构造一次(App 构造期),但是否真的走远端由每次 I/O 时的
|
|
454
|
+
* 当前状态决定,不在构造期"决定死"。`workspaceRef`(携带这两个字段的来源)要到 `start()`
|
|
455
|
+
* 才异步解析完成,构造期读不到;惰性 getter 与本仓 `getWorkspaceKey: () => this.workspaceRef?.key`
|
|
456
|
+
* (`app.ts:718`)同一模式。
|
|
457
|
+
*/
|
|
458
|
+
declare class HttpMemoryStore implements MemoryStore {
|
|
459
|
+
private readonly baseUrl;
|
|
460
|
+
/** user-scope 路径的用户身份——M164-2 改为可选:R 形态可能没有 userIdentity(未登录),
|
|
461
|
+
* 此时 user-scope 路径优雅降级为本地(不抛错),workspace-scope 路径不受影响。 */
|
|
462
|
+
private readonly userId?;
|
|
463
|
+
private readonly workspaceKeyGetter?;
|
|
464
|
+
private readonly isWorkspaceSyncEnabled;
|
|
465
|
+
private readonly getAuth;
|
|
466
|
+
private readonly fetchImpl;
|
|
467
|
+
private readonly timeoutMs;
|
|
468
|
+
private readonly local;
|
|
469
|
+
/** 冲突发生时的可选通知回调(RFC-164 M164-2 子agent评审F3)——app 层接线 TUI toast。 */
|
|
470
|
+
private readonly onConflict?;
|
|
471
|
+
/** 每个 path 的冲突环形缓冲计数器(进程内存,不持久化——仅用于槽位轮转,不影响正确性)。 */
|
|
472
|
+
private readonly conflictCounters;
|
|
473
|
+
/** 每个远端 path 的已知版本号(简单单调计数器,进程内存)——PUT 时携带,供服务端判定冲突。 */
|
|
474
|
+
private readonly knownVersions;
|
|
475
|
+
constructor(options: {
|
|
476
|
+
/** 远端服务器 base URL(不尾斜线)。 */baseUrl: string; /** 获取 Bearer token 的工厂。 */
|
|
477
|
+
getAuth: () => Promise<{
|
|
478
|
+
token: string;
|
|
479
|
+
}>; /** fetch 实现。 */
|
|
480
|
+
fetch: typeof globalThis.fetch; /** 请求超时 (ms)。 */
|
|
481
|
+
timeoutMs: number; /** 当前用户 identity id(可选——R 形态可能未登录)。 */
|
|
482
|
+
userId?: string;
|
|
483
|
+
/**
|
|
484
|
+
* workspace key 的惰性 getter(R 形态 opt-in 时使用)。缺省时 workspace-scope 路径
|
|
485
|
+
* 恒判定为不可用(降级本地),与 `isWorkspaceSyncEnabled` 缺省行为一致。
|
|
486
|
+
*/
|
|
487
|
+
workspaceKey?: () => string | undefined;
|
|
488
|
+
/**
|
|
489
|
+
* workspace 记忆远端同步是否已 opt-in 的惰性 getter(RFC-164 D2)。每次 I/O 时才求值——
|
|
490
|
+
* 不在构造期决定,而是随 `workspaceRef.memorySyncEnabled` 的当前值实时判定。
|
|
491
|
+
* 缺省 = 恒 `false`(向后兼容,user-only 场景不受影响)。
|
|
492
|
+
*/
|
|
493
|
+
isWorkspaceSyncEnabled?: () => boolean; /** 本地缓存根目录(传给 FileSystemMemoryStore 的 cwd);缺省 process.cwd()。 */
|
|
494
|
+
localDir?: string;
|
|
495
|
+
/**
|
|
496
|
+
* `~/` 前缀路径展开的用户主目录 override(透传给内部 `FileSystemMemoryStore`,仅测试
|
|
497
|
+
* 使用——见 `FileSystemMemoryStore.homeDirOverride` 注释:修复持续写污染真实
|
|
498
|
+
* `~/.otto/` 的测试隔离缺陷)。生产环境不传,行为不变。
|
|
499
|
+
*/
|
|
500
|
+
homeDirOverride?: string; /** 冲突检测到时的通知回调(可选)。 */
|
|
501
|
+
onConflict?: (path: string, conflictPath: string) => void;
|
|
502
|
+
});
|
|
503
|
+
load(sources: MemorySource[]): Promise<Map<string, string>>;
|
|
504
|
+
write(path: string, content: string): Promise<void>;
|
|
505
|
+
watch?(_sources: MemorySource[], _onChange: (path: string) => void): () => void;
|
|
506
|
+
/** workspace-scope 判定:路径形如 workspace 根下的 `.otto/` 文件(非 `~/.otto/`)且 opt-in 已启用。 */
|
|
507
|
+
private isWorkspaceSourceEnabled;
|
|
508
|
+
private remoteKey;
|
|
509
|
+
/**
|
|
510
|
+
* 新路由 `/memory/user/:resourceId` 的 resourceId——文件相对路径(如 `AGENTS.md`/
|
|
511
|
+
* `MEMORY.md`/`memory/foo.md`)本身。多个不同 path 各自映射独立 resourceId,
|
|
512
|
+
* 避免固定单一 key 导致的跨文件覆盖(RFC-108 D1 修正)。
|
|
513
|
+
*/
|
|
514
|
+
private remoteResourceId;
|
|
515
|
+
/** workspace-scope resourceId——相对 workspace 根的路径(如 `.otto/MEMORY.md` → `MEMORY.md`)。 */
|
|
516
|
+
private workspaceResourceId;
|
|
517
|
+
/** 远端 GET(user-scope) → { content: string } | null (网络/404均返回 null) */
|
|
518
|
+
private loadRemoteUser;
|
|
519
|
+
/** 远端 PUT(user-scope)(best-effort, 失败静默——R5) */
|
|
520
|
+
private writeRemoteUser;
|
|
521
|
+
/** 远端 GET(workspace-scope) → { content: string } | null (网络/404均返回 null) */
|
|
522
|
+
private loadRemoteWorkspace;
|
|
523
|
+
/**
|
|
524
|
+
* 远端 PUT(workspace-scope)(best-effort, 失败静默——R5)。
|
|
525
|
+
* D6 冲突检测:携带客户端已知版本号;服务端返回 409(版本不匹配)时,不覆盖,
|
|
526
|
+
* 转存为环形缓冲冲突旁路文件。
|
|
527
|
+
*/
|
|
528
|
+
private writeRemoteWorkspace;
|
|
529
|
+
/** 环形缓冲冲突旁路文件——`<path>.conflict-<slot>`,slot 循环覆盖(M164-2 D6 实现简化)。 */
|
|
530
|
+
private writeConflictSidecar;
|
|
531
|
+
}
|
|
532
|
+
//#endregion
|
|
533
|
+
//#region src/overlay-memory-store.d.ts
|
|
534
|
+
/**
|
|
535
|
+
* RFC-345 影子模式:MemoryStore 的 overlay 包装(读穿透 base、写只进内存)。
|
|
536
|
+
*
|
|
537
|
+
* 语义(对齐 Chromium OverlayUserPrefStore + MCP token store 的 overlay):
|
|
538
|
+
* - write:只写内存 Map,绝不落盘/不触发远端同步;
|
|
539
|
+
* - load:内存 overlay 命中优先(保证"本会话内 record 后 memory_read 读得到",
|
|
540
|
+
* 即写后读正确性),未命中回退 base(继承磁盘/远端既有记忆,可注入 prompt)。
|
|
541
|
+
*
|
|
542
|
+
* 为何是 overlay 而非"write no-op":AutoMemory 的索引(this.index)确实是内存真源,
|
|
543
|
+
* 但 memory_read → readEntry → store.load(entryPath) 读的是**条目文件内容**。若 write
|
|
544
|
+
* 纯 no-op,条目文件从未进任何存储,同会话内 record 后 read 该条目会落空。overlay 让
|
|
545
|
+
* 写入的条目留在内存 Map 里,load 优先命中,写后读因此成立(RFC-345 §D3)。
|
|
546
|
+
*
|
|
547
|
+
* 由 §D1 白名单装配点(storage-wiring.buildMemoryStore)在影子态包装 base store。
|
|
548
|
+
* watch 不转发——影子会话不监听外部记忆变更(与 AuthStore overlay 关 watch 同理)。
|
|
549
|
+
*/
|
|
550
|
+
declare function createOverlayMemoryStore(base: MemoryStore): MemoryStore;
|
|
551
|
+
//#endregion
|
|
552
|
+
//#region src/agents-discovery.d.ts
|
|
553
|
+
interface AgentsDiscoveryOptions {
|
|
554
|
+
/** 文件名,默认 'AGENTS.md'。 */
|
|
555
|
+
fileName?: string;
|
|
556
|
+
/** 项目根标记,遇到即停(含该层),默认 ['.git']。 */
|
|
557
|
+
rootMarkers?: string[];
|
|
558
|
+
/** 向上层数硬上限(防符号链接环 / 异常深路径),默认 25。 */
|
|
559
|
+
maxDepth?: number;
|
|
560
|
+
/** 收集文件数上限,超出保留最靠近 cwd 的(更相关),默认 10。 */
|
|
561
|
+
maxFiles?: number;
|
|
562
|
+
/** 单文件字节上限,超出跳过(防巨型文件撑爆上下文),默认 64KiB。 */
|
|
563
|
+
maxFileBytes?: number;
|
|
564
|
+
}
|
|
565
|
+
/**
|
|
566
|
+
* 从 startDir 向上发现 AGENTS.md,返回 root→cwd 顺序的只读 MemorySource(绝对路径)。
|
|
567
|
+
*/
|
|
568
|
+
declare function discoverAgentsSources(startDir: string, opts?: AgentsDiscoveryOptions): MemorySource[];
|
|
569
|
+
//#endregion
|
|
570
|
+
//#region src/auto-memory.d.ts
|
|
571
|
+
/**
|
|
572
|
+
* AutoMemory(M13-03):可写的跨会话记忆。
|
|
573
|
+
*
|
|
574
|
+
* 约定(progressive disclosure):
|
|
575
|
+
* - `<baseDir>/MEMORY.md` —— 索引,一行一条目指针(`- [name](memory/<slug>.md) — hook`)。
|
|
576
|
+
* 每会话仅注入索引,控制 token 占用。
|
|
577
|
+
* - `<baseDir>/memory/<slug>.md` —— 单事实条目全文,按需经 `read(name)` 读取。
|
|
578
|
+
*
|
|
579
|
+
* 存储经 `MemoryStore` 抽象(FileSystemMemoryStore / HttpMemoryStore / InMemory 同构),
|
|
580
|
+
* 即本地与 remote 双后端天然同源(M13 目标)。
|
|
581
|
+
*
|
|
582
|
+
* 并发边界(已知设计):索引为单文件 last-write-wins,单写者假设
|
|
583
|
+
* (app 级单实例持有);多实例并发 record 同名索引可能丢行。
|
|
584
|
+
*/
|
|
585
|
+
interface AutoMemoryOptions {
|
|
586
|
+
store: MemoryStore;
|
|
587
|
+
/** 记忆根目录(store 语义下的路径前缀),缺省 `./.otto`。 */
|
|
588
|
+
baseDir?: string;
|
|
589
|
+
/**
|
|
590
|
+
* 机器特定记忆根目录(RFC-164 D3/M164-1),缺省 `${baseDir}/local`。
|
|
591
|
+
* `scope:'local'` 的条目落此目录——物理隔离于 `baseDir` 之外,天然不参与
|
|
592
|
+
* 未来 workspace 远端同步判定(M164-2 的 `HttpMemoryStore.isWorkspaceSource()`
|
|
593
|
+
* 只识别 `baseDir` 下路径,不识别 `localBaseDir`)。
|
|
594
|
+
*/
|
|
595
|
+
localBaseDir?: string;
|
|
596
|
+
}
|
|
597
|
+
interface AutoMemoryEntry {
|
|
598
|
+
name: string;
|
|
599
|
+
description: string;
|
|
600
|
+
content: string;
|
|
601
|
+
}
|
|
602
|
+
/**
|
|
603
|
+
* 记忆归属域:`workspace`(RFC-164 D3)= 可移植项目知识(默认,向后兼容,结构化
|
|
604
|
+
* index+entry 存储);`local`(RFC-164 D3)= 机器特定事实,永不跨设备同步;
|
|
605
|
+
* `project-shared`(RFC-228 D2)= 团队可见的项目级约定,直接写 `./.otto/AGENTS.md`
|
|
606
|
+
* (marker 块格式,不接入 remote workspace sync——见 `recordProjectShared` 注释)。
|
|
607
|
+
*/
|
|
608
|
+
type MemoryScope = 'workspace' | 'local' | 'project-shared';
|
|
609
|
+
declare class AutoMemory {
|
|
610
|
+
private readonly store;
|
|
611
|
+
private readonly baseDir;
|
|
612
|
+
private readonly localBaseDir;
|
|
613
|
+
private index;
|
|
614
|
+
/** local scope 的独立索引(不与 workspace 索引合并,读取侧透明查找)。 */
|
|
615
|
+
private localIndex;
|
|
616
|
+
private loaded;
|
|
617
|
+
private localLoaded;
|
|
618
|
+
constructor(options: AutoMemoryOptions);
|
|
619
|
+
get indexPath(): string;
|
|
620
|
+
entryPathOf(slug: string): string;
|
|
621
|
+
get localIndexPath(): string;
|
|
622
|
+
localEntryPathOf(slug: string): string;
|
|
623
|
+
/** PersistentMemory sources 接入点:索引作为一个可写 L1 源注入。 */
|
|
624
|
+
asSource(): MemorySource;
|
|
625
|
+
/** local 索引同样作为一个可写 L1 源注入(RFC-164 grill 已确认:常驻注入,不做按需裁剪)。 */
|
|
626
|
+
asLocalSource(): MemorySource;
|
|
627
|
+
load(): Promise<void>;
|
|
628
|
+
loadLocal(): Promise<void>;
|
|
629
|
+
/**
|
|
630
|
+
* 注入文本 = 索引原文 + 读取导语(空索引 → 空串,不占 token)。
|
|
631
|
+
* 导语说明条目的真实存放位置(可能 ≠ workspace 相对路径,review F4)。
|
|
632
|
+
*/
|
|
633
|
+
getInjection(): string;
|
|
634
|
+
/** local 索引的注入文本(同结构,指向 localBaseDir,供组合注入到独立来源段落)。 */
|
|
635
|
+
getLocalInjection(): string;
|
|
636
|
+
record(entry: AutoMemoryEntry, scope?: MemoryScope): Promise<void>;
|
|
637
|
+
private recordLocal;
|
|
638
|
+
/**
|
|
639
|
+
* RFC-228 D2:`project-shared` scope——直接写 `./.otto/AGENTS.md`(marker 块格式),
|
|
640
|
+
* 收编此前"模型直接 edit/write AGENTS.md"的旁路(见 `prompts.ts` 的 `<memory_guidelines>`
|
|
641
|
+
* 文案变更,D3)。**不接入 remote workspace sync**——`HttpMemoryStore.isWorkspaceSource()`
|
|
642
|
+
* 不识别 `./.otto/AGENTS.md`,AGENTS.md 继续走 git 单一权威协作模型,与 lessons.json/
|
|
643
|
+
* MEMORY.md 的"多机自动同步"模型不同(RFC-228 D2 决策)。
|
|
644
|
+
*
|
|
645
|
+
* **写前 load-latest**(不依赖内存缓存的旧快照):每次调用都重新 `store.load()` 取当前磁盘
|
|
646
|
+
* 最新内容,避免同会话内先前的注入快照覆盖并发写入(人工编辑或另一进程)的内容。
|
|
647
|
+
*
|
|
648
|
+
* slug 复用 `resolveSlug`(同名复用、异名消歧),marker 块按 slug 精确定位替换,不依赖
|
|
649
|
+
* heading 文本本身(避免 `entry.content` 内嵌 `##`/换行破坏边界)。
|
|
650
|
+
*/
|
|
651
|
+
private recordProjectShared;
|
|
652
|
+
/**
|
|
653
|
+
* 读取侧透明合并:先查 workspace 索引,未命中再查 local 索引(RFC-164 §4.2)。
|
|
654
|
+
* RFC-164 M164-2 D6:读到的条目若存在冲突旁路文件(`<path>.conflict-0..4`),
|
|
655
|
+
* 前缀 `[CONFLICT]` 标签——避免 agent 把冲突副本当作等同权威记忆使用(子agent评审 F3)。
|
|
656
|
+
*/
|
|
657
|
+
read(name: string): Promise<string | null>;
|
|
658
|
+
/** 探测 `<path>.conflict-0..4` 旁路文件是否存在,存在则前缀 `[CONFLICT]` 标签。 */
|
|
659
|
+
private withConflictLabel;
|
|
660
|
+
private readEntry;
|
|
661
|
+
forget(name: string): Promise<void>;
|
|
662
|
+
/** record/forget 前确保索引已加载(review F1:防未 load 的 record 清空既有索引)。 */
|
|
663
|
+
private ensureLoaded;
|
|
664
|
+
private ensureLocalLoaded;
|
|
665
|
+
private indexLines;
|
|
666
|
+
private localIndexLines;
|
|
667
|
+
/** 索引中 name → slug 的既有映射(精确名匹配)。传入 lines 使 workspace/local 两套索引共用同一实现。 */
|
|
668
|
+
private slugByName;
|
|
669
|
+
/** slug 解析:同名复用既有 slug;异名撞 slug 追加序号消歧(review F2)。传入 lines 使 workspace/local 两套索引共用同一实现。 */
|
|
670
|
+
private resolveSlug;
|
|
671
|
+
}
|
|
672
|
+
//#endregion
|
|
673
|
+
//#region src/token-estimator.d.ts
|
|
674
|
+
/**
|
|
675
|
+
* 确保 tokenizer 已加载(异步)。多次调用安全。
|
|
676
|
+
* 可在应用启动时调用一次,避免首次估算时的加载延迟。
|
|
677
|
+
*/
|
|
678
|
+
declare function ensureTokenizer(): Promise<void>;
|
|
679
|
+
/**
|
|
680
|
+
* 图片 block 的固定 token 估算值(非精确计算,刻意保守)。
|
|
681
|
+
*
|
|
682
|
+
* Anthropic 官方公式:tokens = (width px * height px) / 750,图片被 resize 到最大
|
|
683
|
+
* 2000x2000px 上限时对应约 5333 tokens。本估算刻意采用远低于该上限的保守值,
|
|
684
|
+
* 宁可估算偏大触发提前压缩,也不要估算偏小导致压缩滞后(沿用
|
|
685
|
+
* IMAGE_MAX_TOKEN_SIZE=2000 的保守设计)。
|
|
686
|
+
*
|
|
687
|
+
* 注意:该常量上调会通过 estimateMessageTokens → needsCompaction 双阈值判断
|
|
688
|
+
* 使含图会话的压缩触发时机提前,这是预期效果而非副作用。
|
|
689
|
+
*/
|
|
690
|
+
declare const IMAGE_BLOCK_TOKEN_ESTIMATE = 2000;
|
|
691
|
+
declare function estimateTokensHeuristic(text: string): number;
|
|
692
|
+
/**
|
|
693
|
+
* 估算文本的 token 数。
|
|
694
|
+
* 优先使用 js-tiktoken(cl100k_base),未加载时回落启发式。
|
|
695
|
+
* 病理输入(同字符长 run / 超长文本)直接走启发式,避免 O(n²) BPE 阻塞主线程。
|
|
696
|
+
*/
|
|
697
|
+
declare function estimateTokens(text: string): number;
|
|
698
|
+
declare function messageToText(message: Message): string;
|
|
699
|
+
declare function estimateMessageTokens(message: Message, estimator?: typeof estimateTokens): number;
|
|
700
|
+
declare function estimateMessagesTokens(messages: readonly Message[], estimator?: typeof estimateTokens): number;
|
|
701
|
+
//#endregion
|
|
702
|
+
//#region src/prune.d.ts
|
|
703
|
+
declare function prune(messages: readonly Message[], contextWindow: number, config?: Partial<PruneConfig>, estimator?: typeof estimateTokens, opts?: {
|
|
704
|
+
currentTokens?: number;
|
|
705
|
+
force?: boolean;
|
|
706
|
+
}): PruneResult;
|
|
707
|
+
//#endregion
|
|
708
|
+
//#region src/archive.d.ts
|
|
709
|
+
declare class InMemoryArchiveStorage implements ArchiveStorage {
|
|
710
|
+
readonly type: StorageType;
|
|
711
|
+
private archives;
|
|
712
|
+
private sessionIndex;
|
|
713
|
+
archive(sessionId: string, messages: Message[], summary: string): Promise<string>;
|
|
714
|
+
read(archivePath: string): Promise<string>;
|
|
715
|
+
list(sessionId: string): Promise<ArchiveEntry[]>;
|
|
716
|
+
/** 会话销毁时驱逐其全部归档条目——防长跑进程中 archives/sessionIndex 无界增长。 */
|
|
717
|
+
forgetSession(sessionId: string): void;
|
|
718
|
+
}
|
|
719
|
+
/**
|
|
720
|
+
* file/http archive persistence 整栈已删。
|
|
721
|
+
* archive = session 派生视图——仅 `InMemoryArchiveStorage` 为 V 形态/无持久化 session tree 回退。
|
|
722
|
+
* createArchiveStorage() 工厂亦退场。
|
|
723
|
+
*/
|
|
724
|
+
declare function formatArchive(messages: Message[], summary: string, timestamp: number): string;
|
|
725
|
+
//#endregion
|
|
726
|
+
//#region src/defaults.d.ts
|
|
727
|
+
declare function computeMemoryDefaults(model: {
|
|
728
|
+
contextWindow: number;
|
|
729
|
+
maxOutput: number;
|
|
730
|
+
}): MemoryDefaults;
|
|
731
|
+
declare const DEFAULT_COMPACTION_CONFIG: CompactionConfig;
|
|
732
|
+
declare const DEFAULT_PRUNE_CONFIG: PruneConfig;
|
|
733
|
+
declare function resolveContextSize(size: ContextSize, contextWindow: number): number;
|
|
734
|
+
//#endregion
|
|
735
|
+
//#region src/prompts.d.ts
|
|
736
|
+
declare const SUMMARIZATION_PROMPT = "You are a conversation summarizer for an AI coding assistant. Your task is to create a structured summary of the conversation that preserves all information needed to continue the work.\n\nCreate the summary in the following format:\n\n## Goal\n[What is the user trying to accomplish?]\n\n## Constraints & Preferences\n- [Any constraints, preferences, or requirements the user mentioned]\n\n## All User Requests\n- [List EVERY distinct request/instruction the user made, in order, that is not a tool result. This is the authoritative record of user intent \u2014 quote the user's own wording; do not paraphrase away specifics.]\n\n## Progress\n### Done\n- [x] [Completed work items]\n### In Progress\n- [ ] [Work in progress]\n\n## Key Decisions\n- **[Decision]**: [Brief rationale]\n\n## Next Steps\n1. [Planned next steps. For the immediate next action, include a VERBATIM quote of the most recent user message or your own last stated intent it derives from \u2014 anchors the task, prevents drift.]\n\n## File Operations\n### Read\n- [Files that were read]\n### Modified\n- [Files that were modified]\n\n## Critical Context\n- [Any critical context needed to continue the work]\n\nIMPORTANT:\n- Preserve exact file paths, function names, error messages, and code snippets\n- Keep technical details precise \u2014 do not generalize\n- Include all tool call results that affect the current state\n- Note any pending or failed operations";
|
|
737
|
+
declare const UPDATE_SUMMARIZATION_PROMPT = "You are updating an existing conversation summary with new information. The existing summary and new messages are provided below.\n\nProduce a single UPDATED summary that stays BOUNDED \u2014 a summary that grows without limit across rounds eventually gets hard-truncated by the output cap, silently losing whatever fell past the limit. Keep it tight by actively triaging, not by appending.\n\nKeep (never drop):\n- Every distinct user request/instruction (the \"All User Requests\" record is authoritative)\n- Unfinished, in-progress, or blocked work\n- Decisions not yet verified, and anything the user corrected you on\n- Exact file paths, function names, error messages, code snippets still in play\n\nDrop / compress (make room):\n- Work that is both completed AND verified \u2014 collapse to a one-line outcome\n- Superseded intermediate attempts and abandoned approaches\n- Redundant restatements already captured elsewhere in the summary\n\nRules:\n- Move completed \"In Progress\" items to \"Done\", then compress \"Done\" per the above\n- Merge new work items, decisions, and context; do not simply append\n- Keep the same structured format (including \"All User Requests\")\n- If any information conflicts, use the newer version";
|
|
738
|
+
declare const TURN_PREFIX_SUMMARIZATION_PROMPT = "You are summarizing the FIRST PART of an assistant turn that was split during conversation compaction. The remaining part of this turn is still in the conversation.\n\nCreate a brief summary in this format:\n\n## Original Request\n[What did the user ask for in this turn?]\n\n## Early Progress\n- [Key actions completed in the prefix portion]\n\n## Context for Remaining Messages\n- [Information needed to understand the remaining suffix messages]\n\nKeep it concise \u2014 this will be prepended to the remaining messages of this turn.";
|
|
739
|
+
declare const WITH_PRESERVED_TAIL_PROMPT = "You are a conversation summarizer for an AI coding assistant. You are summarizing the EARLIER portion of a conversation. The most recent messages are NOT shown to you here \u2014 they are kept verbatim and will follow your summary unchanged.\n\nYour summary and those preserved recent messages together become the assistant's full context going forward.\n\nCreate the summary in the following format:\n\n## Goal\n[What is the user trying to accomplish?]\n\n## Constraints & Preferences\n- [Any constraints, preferences, or requirements the user mentioned]\n\n## All User Requests\n- [List EVERY distinct request/instruction the user made, in order, that is not a tool result. This is the authoritative record of user intent \u2014 multi-round compaction must never let an earlier request silently drop. Quote the user's own wording for each; do not paraphrase away specifics.]\n\n## Progress\n### Done\n- [x] [Completed work items]\n### In Progress\n- [ ] [Work in progress]\n\n## Key Decisions\n- **[Decision]**: [Brief rationale]\n\n## Next Steps\n1. [Planned next steps. For the immediate next action, include a VERBATIM quote of the most recent user message or your own last stated intent that it derives from \u2014 this anchors the task and prevents interpretation drift across compactions.]\n\n## File Operations\n### Read\n- [Files that were read]\n### Modified\n- [Files that were modified]\n\n## Critical Context\n- [Any critical context needed to continue the work]\n\n## Context the Preserved Tail Depends On\n- [Facts a reader MUST know to correctly understand the preserved recent messages that follow: decisions already made, file paths and their current state, conventions/constraints agreed earlier, mistakes already corrected, and any partially-done work the recent messages continue. This section is the whole point of this summary \u2014 be specific.]\n\nIMPORTANT:\n- Preserve exact file paths, function names, error messages, and code snippets\n- Keep technical details precise \u2014 do not generalize\n- Include all tool call results that affect the current state\n- Note any pending or failed operations\n- Do NOT re-summarize the preserved recent messages themselves \u2014 they are kept verbatim after your summary. Summarize only the EARLIER conversation and surface what the tail depends on.";
|
|
740
|
+
declare const SEGMENT_SUMMARIZATION_PROMPT = "You are summarizing ONE SEGMENT of a longer conversation for an AI coding assistant. This is a partial view \u2014 you are NOT seeing the whole conversation, and other segments are being summarized separately. Your segment summary will later be merged with the others.\n\nCreate a concise summary of THIS SEGMENT in the following format:\n\n## Goal\n[What is the user trying to accomplish?]\n\n## Constraints & Preferences\n- [Any constraints, preferences, or requirements the user mentioned]\n\n## All User Requests\n- [List EVERY distinct request/instruction the user made, in order, that is not a tool result. This is the authoritative record of user intent \u2014 multi-round compaction must never let an earlier request silently drop. Quote the user's own wording for each; do not paraphrase away specifics.]\n\n## Progress\n### Done\n- [x] [Completed work items]\n### In Progress\n- [ ] [Work in progress]\n\n## Key Decisions\n- **[Decision]**: [Brief rationale]\n\n## Next Steps\n1. [Planned next steps. For the immediate next action, include a VERBATIM quote of the most recent user message or your own last stated intent that it derives from \u2014 this anchors the task and prevents interpretation drift across compactions.]\n\n## File Operations\n### Read\n- [Files that were read]\n### Modified\n- [Files that were modified]\n\n## Critical Context\n- [Any critical context needed to continue the work]\n\nIMPORTANT:\n- Preserve exact file paths, function names, error messages, and code snippets\n- Keep technical details precise \u2014 do not generalize\n- Include all tool call results that affect the current state\n- Note any pending or failed operations\n- Do NOT emit conclusions that require seeing the whole conversation (e.g. a single global \"next step\"). Report only what THIS segment establishes; the merge step will reconcile across segments.";
|
|
741
|
+
declare const SEGMENT_MERGE_PROMPT = "You are merging several segment summaries of one conversation into a single coherent summary for an AI coding assistant. Each input below is a summary of a different (chronological) segment of the same conversation.\n\nReconcile them into one summary in the following format:\n\n## Goal\n[What is the user trying to accomplish?]\n\n## Constraints & Preferences\n- [Any constraints, preferences, or requirements the user mentioned]\n\n## All User Requests\n- [List EVERY distinct request/instruction the user made, in order, that is not a tool result. This is the authoritative record of user intent \u2014 multi-round compaction must never let an earlier request silently drop. Quote the user's own wording for each; do not paraphrase away specifics.]\n\n## Progress\n### Done\n- [x] [Completed work items]\n### In Progress\n- [ ] [Work in progress]\n\n## Key Decisions\n- **[Decision]**: [Brief rationale]\n\n## Next Steps\n1. [Planned next steps. For the immediate next action, include a VERBATIM quote of the most recent user message or your own last stated intent that it derives from \u2014 this anchors the task and prevents interpretation drift across compactions.]\n\n## File Operations\n### Read\n- [Files that were read]\n### Modified\n- [Files that were modified]\n\n## Critical Context\n- [Any critical context needed to continue the work]\n\nIMPORTANT:\n- Preserve exact file paths, function names, error messages, and code snippets\n- Keep technical details precise \u2014 do not generalize\n- Include all tool call results that affect the current state\n- Note any pending or failed operations\n- Resolve conflicts in favour of the LATER segment.\n- Merge duplicate items; do not simply concatenate.";
|
|
742
|
+
/**
|
|
743
|
+
* RFC-339 D3:摘要提示词变体标识。调用方据压缩形态选择(见 `compaction.ts`)。
|
|
744
|
+
* - `full`:无保留区的全量折叠(或有旧摘要走 UPDATE 路径,见 buildSummarizationPrompt)。
|
|
745
|
+
* - `preserved-tail`:主路径,切点之后有原样保留的近期消息。
|
|
746
|
+
* - `segment`:map-reduce 的单段摘要。
|
|
747
|
+
* - `merge`:map-reduce 的合并阶段(输入是段摘要,非原始对话)。
|
|
748
|
+
*/
|
|
749
|
+
type SummarizationVariant = 'full' | 'preserved-tail' | 'segment' | 'merge';
|
|
750
|
+
/**
|
|
751
|
+
* RFC-339 D3:切点上下文——让提示词感知「保留区」的存在。
|
|
752
|
+
* `preservedCount` = 切点之后原样保留的消息条数(0 = 全量折叠)。
|
|
753
|
+
*/
|
|
754
|
+
interface SummarizationContext {
|
|
755
|
+
variant: SummarizationVariant;
|
|
756
|
+
preservedCount?: number;
|
|
757
|
+
}
|
|
758
|
+
/**
|
|
759
|
+
* 构造摘要提示词。RFC-339 D3:新增可选 `context` 参数使提示词感知切点/批次形态;
|
|
760
|
+
* **不传时行为与改造前逐字节等价**(向后兼容——现有测试与未接线调用点不受影响)。
|
|
761
|
+
*
|
|
762
|
+
* 变体优先级:
|
|
763
|
+
* - `context.variant === 'segment'` → SEGMENT_SUMMARIZATION_PROMPT(map 阶段,忽略 previousSummary)
|
|
764
|
+
* - 有 `previousSummary` → UPDATE_SUMMARIZATION_PROMPT(增量更新,最高优先,兼容旧行为)
|
|
765
|
+
* - `context.variant === 'preserved-tail'` → WITH_PRESERVED_TAIL_PROMPT(主路径)
|
|
766
|
+
* - 否则 → SUMMARIZATION_PROMPT(FULL,全量折叠)
|
|
767
|
+
*
|
|
768
|
+
* 注:UPDATE 优先于 preserved-tail 是刻意的——增量补摘(有旧摘要)时,模型的任务是
|
|
769
|
+
* 把新增消息并入旧摘要,UPDATE 模板的「保留旧摘要全部信息」语义比 preserved-tail 更贴切;
|
|
770
|
+
* 保留区提示由 `preservedCount` 仍通过尾注补充(见下)。
|
|
771
|
+
*/
|
|
772
|
+
declare function buildSummarizationPrompt(messages: string, previousSummary?: string, customInstructions?: string, context?: SummarizationContext): string;
|
|
773
|
+
/**
|
|
774
|
+
* RFC-339 D3:map-reduce 合并阶段的提示词。输入是各段摘要(非原始对话),
|
|
775
|
+
* 包在 `<segment_summaries>` 标签内——修正改造前 reduce 阶段误用 `<conversation>`
|
|
776
|
+
* 标签(内容类型与标签语义不符)的问题。
|
|
777
|
+
*/
|
|
778
|
+
declare function buildSegmentMergePrompt(segmentSummaries: string, previousSummary?: string, customInstructions?: string): string;
|
|
779
|
+
declare function buildTurnPrefixPrompt(turnPrefixMessages: string): string;
|
|
780
|
+
/**
|
|
781
|
+
* `<agent_memory>` 段的默认总预算(tokens,粗估)。
|
|
782
|
+
*
|
|
783
|
+
* 本仓正常场景(AGENTS.md 三源 + AutoMemory 索引)实测 ~3.4k tokens;monorepo
|
|
784
|
+
* `discoverAgentsSources` 理论上限是 10 文件 × 64KB = 640KB,若真的堆到那个量级会把
|
|
785
|
+
* system prompt 撑爆。这里的预算不是替代 discoverAgentsSources 的单文件/文件数上限
|
|
786
|
+
* (那层继续管),而是在全部来源(含它管不到的 writable AGENTS.md / AutoMemory 索引)
|
|
787
|
+
* 汇总之后再把一道总闸——`buildMemoryInjection` 是唯一收口点,别处管不到的这里管。
|
|
788
|
+
*
|
|
789
|
+
* 取 3.4k 实测值的 ~2.3 倍留余量,同时把极端场景压掉 95%+。
|
|
790
|
+
*/
|
|
791
|
+
declare const DEFAULT_MEMORY_INJECTION_BUDGET_TOKENS = 8000;
|
|
792
|
+
declare function buildMemoryInjection(memories: Map<string, string>, budgetTokens?: number): string;
|
|
793
|
+
/**
|
|
794
|
+
* 跨会话经验抽取提示词。要求模型从一段会话切片里提炼可复用的「经验」
|
|
795
|
+
* (trigger=何时适用 / insight=怎么做 / tags=召回关键词),严格输出 JSON 数组。
|
|
796
|
+
* 内容边界(D7/R3)对齐 `buildMemoryInjection` 的 should/should-NOT remember——不存可从
|
|
797
|
+
* repo/git/AGENTS.md 推导的事实、不存 ephemeral 任务态、不存密钥。无可存则输出 []。
|
|
798
|
+
*/
|
|
799
|
+
declare const LESSON_EXTRACTION_PROMPT = "You are a learning extractor for an AI coding assistant. Read the conversation below and extract durable, cross-session \"lessons\" \u2014 operational knowledge that will help the assistant do better on FUTURE, unrelated sessions in this project.\n\nA lesson captures a reusable cause\u2192action:\n- \"trigger\": the situation/context that should recall this lesson (when it applies)\n- \"insight\": the actionable guidance learned (what to do)\n- \"tags\": 1-5 lowercase keywords for retrieval\n\nSAVE these (durable, reusable):\n- A user correction that reveals a lasting preference (\"user prefers X over Y\")\n- A non-obvious gotcha discovered the hard way (\"running Z without flag W silently fails\")\n- A project-specific workflow or command that isn't written down elsewhere\n- A constraint or convention the user insisted on\n\nDO NOT save these (they pollute recall):\n- Anything derivable from the repo, git history, or AGENTS.md/README \u2014 do not restate the obvious\n- Ephemeral task state (\"currently editing foo.ts\") \u2014 lessons must outlive this session\n- One-off Q&A, casual conversation, or a summary of what happened\n- Secrets, API keys, passwords, tokens, or any sensitive value\n\nOutput ONLY a JSON array, nothing else. Each element: {\"trigger\": string, \"insight\": string, \"tags\": string[]}.\nIf there is nothing durable worth saving, output exactly: []";
|
|
800
|
+
declare function buildExtractionPrompt(conversationText: string): string;
|
|
801
|
+
/**
|
|
802
|
+
* RFC-340 D2 方案 B(M2 兑现):压缩摘要的上下文引用。
|
|
803
|
+
*
|
|
804
|
+
* 演进史(两步,勿回退):
|
|
805
|
+
* - **原始文案**承诺 "can be accessed via the **read** tool"——断头路:通用 read 工具
|
|
806
|
+
* 不认识 `archive:` 前缀,`MemoryManager.readArchive()` 零工具暴露,模型照做只会
|
|
807
|
+
* 拿到"文件不存在"。
|
|
808
|
+
* - **M1** 据"不做无法兑现的承诺"删掉可读表述,只留溯源标识。
|
|
809
|
+
* - **M2(本版)** 把召回能力真正做出来(`memory_archive_read`),故把承诺加回——
|
|
810
|
+
* 但指向**具体工具名**而非泛泛的 "read tool",这正是原文案出错的地方。
|
|
811
|
+
*
|
|
812
|
+
* 工具可能未注册(`archiveRecall` 能力未注入,如 V 形态/无持久化会话)。此时文案仍
|
|
813
|
+
* 提到该工具是可接受的:模型调用不存在的工具会得到明确的 unknown-tool 反馈,
|
|
814
|
+
* 比"以为取不回而不去尝试"损失更小;且未注册场景本身就是少数派配置。
|
|
815
|
+
*/
|
|
816
|
+
declare function buildArchiveReference(archivePath: string, summary: string): string;
|
|
817
|
+
/**
|
|
818
|
+
* RFC-340 D1:低覆盖摘要的前置标注文本。
|
|
819
|
+
*
|
|
820
|
+
* 目的是**如实告知而非静默降级**:模型据此知道这段摘要并未覆盖全部历史,遇到摘要里
|
|
821
|
+
* 没有的细节应当向用户确认,而不是当作"历史里就没发生过"。
|
|
822
|
+
*/
|
|
823
|
+
declare function buildLowCoverageNotice(coverage: number): string;
|
|
824
|
+
//#endregion
|
|
825
|
+
//#region src/operational-learning.d.ts
|
|
826
|
+
/**
|
|
827
|
+
* RFC-284 D2:经验的语义类别,与 `state` 生命周期正交。
|
|
828
|
+
*
|
|
829
|
+
* 判别基准是「换一个技术栈后是否还成立」:
|
|
830
|
+
* - `norm` 在任何项目都成立(如「禁用 git stash」)→ 常驻 Core Pack。
|
|
831
|
+
* - `fact` 只在碰到该技术栈时才需要(如「undici 默认 keep-alive 会让第二个请求挂起」)→ 按需检索。
|
|
832
|
+
* - `case` 是某次落地/排障的纪要 → 只供 explain/审计。
|
|
833
|
+
* - `ephemeral` 绑定某台机器/某次基线 → TTL 到期归档。
|
|
834
|
+
*/
|
|
835
|
+
type LessonClass = 'norm' | 'fact' | 'case' | 'ephemeral';
|
|
836
|
+
/**
|
|
837
|
+
* RFC-284 D2:经验的生命周期状态。
|
|
838
|
+
*
|
|
839
|
+
* `live` 表示当前会话的 stable snapshot 仍引用它——内存压力也不得淘汰,
|
|
840
|
+
* 只能从下一个会话起生效(对齐 Flutter ImageCache 对 live image 的处理)。
|
|
841
|
+
*/
|
|
842
|
+
type LessonState = 'active' | 'live' | 'archived' | 'tombstone';
|
|
843
|
+
interface Lesson {
|
|
844
|
+
id: string;
|
|
845
|
+
tags: string[];
|
|
846
|
+
trigger: string;
|
|
847
|
+
insight: string;
|
|
848
|
+
sourceSessionId: string;
|
|
849
|
+
createdAt: number;
|
|
850
|
+
/**
|
|
851
|
+
* @deprecated RFC-284 D8:改用 `relevantHitCount`。
|
|
852
|
+
*
|
|
853
|
+
* 该字段唯一自增点是 `search()`,而注入路径走 `list()` 且 `search()` 零生产调用方,
|
|
854
|
+
* 故它在历史数据中结构性恒为 0(详见 RFC-036 §8.9 勘误)。保留仅为向后兼容:
|
|
855
|
+
* 读取时与 `relevantHitCount` 双向同步,不再作为新的判据来源。
|
|
856
|
+
*/
|
|
857
|
+
appliedCount: number;
|
|
858
|
+
/** 语义类别。缺省视为未归类,由 M4 审计填充。 */
|
|
859
|
+
class?: LessonClass;
|
|
860
|
+
/** 生命周期状态。缺省视为 `active`。 */
|
|
861
|
+
state?: LessonState;
|
|
862
|
+
/** 作用域。由所属 store 类型决定,不从 v1 正文推断。 */
|
|
863
|
+
scope?: 'workspace' | 'local';
|
|
864
|
+
/** 注入文本的内容哈希——只由 content 字段决定,统计字段变化不得影响它。 */
|
|
865
|
+
contentHash?: string;
|
|
866
|
+
/** 注入成本估算(CJK 加权)。用于预算核算,不参与 `contentHash`。 */
|
|
867
|
+
estimatedTokens?: number;
|
|
868
|
+
/** 被 Relevant Hints 检索命中的次数(承接旧 `appliedCount` 语义)。 */
|
|
869
|
+
relevantHitCount?: number;
|
|
870
|
+
/** 进入 stable Core Pack 的次数。 */
|
|
871
|
+
coreInjectedCount?: number;
|
|
872
|
+
/** 任意 selector 命中总数。 */
|
|
873
|
+
hitCountTotal?: number;
|
|
874
|
+
/** 最近一次被 selector 命中的时间。 */
|
|
875
|
+
lastAccessedAt?: number;
|
|
876
|
+
/** 用户或系统固定,不得自动归档。 */
|
|
877
|
+
pinned?: boolean;
|
|
878
|
+
/** 被哪条经验取代(同主题合并后指向合并条款)。 */
|
|
879
|
+
supersededBy?: string;
|
|
880
|
+
/** 合并来源——规范条款保留原始 lesson id,维持 AGENTS.md 引用可追溯性。 */
|
|
881
|
+
mergedFrom?: string[];
|
|
882
|
+
}
|
|
883
|
+
type LessonInput = Omit<Lesson, 'id' | 'createdAt' | 'appliedCount'>;
|
|
884
|
+
interface LessonFilter {
|
|
885
|
+
tags?: string[];
|
|
886
|
+
query?: string;
|
|
887
|
+
}
|
|
888
|
+
interface SimilarLesson {
|
|
889
|
+
lesson: Lesson;
|
|
890
|
+
/** Jaccard 词集相似度 [0,1]。 */
|
|
891
|
+
similarity: number;
|
|
892
|
+
}
|
|
893
|
+
/** 默认去重阈值——trigger+insight 词集 Jaccard ≥ 此值即视作近似重复。 */
|
|
894
|
+
declare const DEFAULT_SIMILARITY_THRESHOLD = 0.6;
|
|
895
|
+
interface LearningStoreOptions {
|
|
896
|
+
/** 本地文件持久化路径(与 `store` 互斥;两者都缺省时纯内存,不持久化)。 */
|
|
897
|
+
path?: string;
|
|
898
|
+
/**
|
|
899
|
+
* RFC-164 M164-2 D5:可选 `MemoryStore` 抽象接入——提供时,读写改走
|
|
900
|
+
* `store.load()`/`store.write()`(序列化为 JSON 字符串),使 lessons.json 与
|
|
901
|
+
* auto-memory 走同一远端同步管道(不再是碎片化的"auto-memory 同步了但 lessons 没同步")。
|
|
902
|
+
* 与 `path` 二选一:`store` 优先;两者都提供时 `store` 生效、`path` 被忽略(`store` 场景下
|
|
903
|
+
* 仍用 `path` 派生 `storeKey`,即 store 语义下的路径,通常与 `path` 同值)。
|
|
904
|
+
*/
|
|
905
|
+
store?: MemoryStore;
|
|
906
|
+
/** `store` 模式下的存储路径(store 语义下的 key/path)。仅 `store` 提供时使用,缺省复用 `path`。 */
|
|
907
|
+
storePath?: string;
|
|
908
|
+
/**
|
|
909
|
+
* 可选 logger——提供时用于诊断加载/持久化错误(warn 级别)。
|
|
910
|
+
* 默认不打印,避免影响现有调用方。
|
|
911
|
+
*/
|
|
912
|
+
logger?: {
|
|
913
|
+
warn: (...args: unknown[]) => void;
|
|
914
|
+
};
|
|
915
|
+
/**
|
|
916
|
+
* RFC-284 D11:本 store 承载的作用域。由装配方按 store 类型传入
|
|
917
|
+
* (workspace store 传 'workspace',local store 传 'local'),
|
|
918
|
+
* 不从 lesson 正文推断——scope 是存储边界属性,不是内容属性。
|
|
919
|
+
*/
|
|
920
|
+
scope?: 'workspace' | 'local';
|
|
921
|
+
/**
|
|
922
|
+
* RFC-345 §D3:影子模式——persist() 整体短路(唯一写盘出口)。
|
|
923
|
+
*
|
|
924
|
+
* 教训子系统是内存优先架构(this.lessons Map 为真源,list/get/search 全读 Map),
|
|
925
|
+
* persist() 只是把 Map 刷盘(§E2 已实证)。短路 persist 后写后读天然正确(写进 Map、
|
|
926
|
+
* 读从 Map),同会话内行为与普通模式一致,仅退出后不留痕。覆盖 store 与 persistence
|
|
927
|
+
* 两个写盘分支于单点。由 §D1 白名单点 agent-services-setup 从 isShadowModeEnabled 派生。
|
|
928
|
+
*/
|
|
929
|
+
ephemeral?: boolean;
|
|
930
|
+
}
|
|
931
|
+
/** 估算注入 token(CJK≈1/字,ASCII≈4 字符/token)。与 RFC-284 探针脚本同口径。 */
|
|
932
|
+
declare function estimateLessonTokens(lesson: Pick<Lesson, 'tags' | 'trigger' | 'insight'>): number;
|
|
933
|
+
/**
|
|
934
|
+
* 内容哈希——**只覆盖进入注入文本的字段**(RFC-284 D6)。
|
|
935
|
+
*
|
|
936
|
+
* 统计字段(hitCount/lastAccessedAt/…)绝不参与,否则每次 selector 命中都会
|
|
937
|
+
* 改变 packId、逐轮击穿 prompt cache 的 stable 前缀。
|
|
938
|
+
*/
|
|
939
|
+
declare function computeLessonContentHash(lesson: Pick<Lesson, 'tags' | 'trigger' | 'insight'>): string;
|
|
940
|
+
declare class OperationalLearningStore {
|
|
941
|
+
private lessons;
|
|
942
|
+
private nextId;
|
|
943
|
+
private loaded;
|
|
944
|
+
private readonly snapshotId;
|
|
945
|
+
private readonly persistence;
|
|
946
|
+
/** RFC-164 M164-2 D5:MemoryStore 接入(可选)——提供时优先于 `persistence` 路径。 */
|
|
947
|
+
private readonly store;
|
|
948
|
+
private readonly storePath;
|
|
949
|
+
private readonly logger;
|
|
950
|
+
/** RFC-284 D11:本 store 的作用域,装配期注入,写入时打到每条 lesson 上。 */
|
|
951
|
+
private readonly scope;
|
|
952
|
+
/** RFC-345 §D3:影子态——persist() 短路(内存优先架构,写后读经 this.lessons Map 保持)。 */
|
|
953
|
+
private readonly ephemeral;
|
|
954
|
+
constructor(options?: LearningStoreOptions);
|
|
955
|
+
save(input: LessonInput): Promise<Lesson>;
|
|
956
|
+
search(query: string, limit?: number): Promise<Lesson[]>;
|
|
957
|
+
/**
|
|
958
|
+
* RFC-036 M61(R4 去重前置):在写入前查找与 `(trigger, insight)` 近似重复的既有经验。
|
|
959
|
+
* 返回相似度最高且 ≥ `threshold` 的一条,没有则 `undefined`。**只读,不自增 appliedCount**
|
|
960
|
+
* (区别于 `search`——`search` 是召回侧、计数被引用;`findSimilar` 是写前去重、不应计数)。
|
|
961
|
+
*/
|
|
962
|
+
findSimilar(trigger: string, insight: string, threshold?: number): Promise<SimilarLesson | undefined>;
|
|
963
|
+
/**
|
|
964
|
+
* RFC-036 M62-03(R4):去重写入——写前 `findSimilar`,命中近似既有经验则**复用**(不新增),
|
|
965
|
+
* 否则 `save`。`learn` 工具与 `saveExtractedLessons` 共用此原语,保证所有写入口都过去重。
|
|
966
|
+
*/
|
|
967
|
+
saveDeduped(input: LessonInput, threshold?: number): Promise<{
|
|
968
|
+
lesson: Lesson;
|
|
969
|
+
deduped: boolean;
|
|
970
|
+
}>;
|
|
971
|
+
list(filter?: LessonFilter): Promise<Lesson[]>;
|
|
972
|
+
get(id: string): Promise<Lesson | undefined>;
|
|
973
|
+
/** RFC-284 T3b:记录 Relevant Hints 命中;只更新统计字段,不改变 contentHash/estimatedTokens。 */
|
|
974
|
+
recordRelevantHits(ids: readonly string[], now?: number): Promise<void>;
|
|
975
|
+
delete(id: string): Promise<boolean>;
|
|
976
|
+
get size(): number;
|
|
977
|
+
private ensureLoaded;
|
|
978
|
+
/**
|
|
979
|
+
* RFC-164 M164-2 D4b:强制重新从 store 拉取一次("最终同步"场景——R↔L 切换 disable 前
|
|
980
|
+
* 拉取远端最新内容覆盖本地缓存,避免用户看到 stale 数据)。仅 store 模式生效;
|
|
981
|
+
* 本地文件模式(无 store)为 no-op(无远端可拉取)。
|
|
982
|
+
*/
|
|
983
|
+
refresh(): Promise<void>;
|
|
984
|
+
/** RFC-164 M164-2 D5:MemoryStore 路径的加载——store.load 返回原始 JSON 字符串,本地解析。 */
|
|
985
|
+
private loadFromStore;
|
|
986
|
+
private persist;
|
|
987
|
+
}
|
|
988
|
+
//#endregion
|
|
989
|
+
//#region src/memory-extractor.d.ts
|
|
990
|
+
/**
|
|
991
|
+
* 抽取器调用 LLM 的接缝——与 compaction 的 `summarize` 同形。memory 包保持纯净:运行时
|
|
992
|
+
* 再决定注入 forked-agent 还是复用主 summarize。
|
|
993
|
+
*/
|
|
994
|
+
type ExtractLLM = (prompt: string, options?: {
|
|
995
|
+
maxTokens?: number;
|
|
996
|
+
signal?: AbortSignal;
|
|
997
|
+
}) => Promise<string>;
|
|
998
|
+
interface ExtractionOptions {
|
|
999
|
+
/** 抽取调用的 token 上限。默认 DEFAULT_MAX_TOKENS=2048。 */
|
|
1000
|
+
maxTokens?: number;
|
|
1001
|
+
signal?: AbortSignal;
|
|
1002
|
+
/** 单次抽取最多产出多少条 lesson(防爆 / 防召回污染)。默认 5。 */
|
|
1003
|
+
maxLessons?: number;
|
|
1004
|
+
}
|
|
1005
|
+
/**
|
|
1006
|
+
* 从会话切片抽取跨会话经验(Lesson)。
|
|
1007
|
+
*
|
|
1008
|
+
* **fail-closed**:LLM 失败/超时/解析失败一律返回 `[]`,绝不抛——抽取永不阻塞、
|
|
1009
|
+
* 不污染主回合。本函数是纯库逻辑(经 `ExtractLLM` 注入模型调用),不依赖 agent/ForkRunner;
|
|
1010
|
+
* 触发节流、不与主 agent 抢、去重写入由上层负责。
|
|
1011
|
+
*
|
|
1012
|
+
* @param messages 会话切片(通常是上次抽取后的新增消息)
|
|
1013
|
+
* @param sourceSessionId 产出 lesson 的来源会话 id(写入 `LessonInput.sourceSessionId`)
|
|
1014
|
+
* @param llm 模型调用接缝
|
|
1015
|
+
* @returns 规整后的 `LessonInput[]`(可直接喂 `OperationalLearningStore.save`),失败为空数组
|
|
1016
|
+
*/
|
|
1017
|
+
declare function extractLessons(messages: readonly Message[], sourceSessionId: string, llm: ExtractLLM, options?: ExtractionOptions): Promise<LessonInput[]>;
|
|
1018
|
+
/** `saveExtractedLessons` 依赖的最小 store 接口——便于单测注入 in-memory store。 */
|
|
1019
|
+
interface LessonWriteStore {
|
|
1020
|
+
saveDeduped(input: LessonInput, threshold?: number): Promise<{
|
|
1021
|
+
lesson: Lesson;
|
|
1022
|
+
deduped: boolean;
|
|
1023
|
+
}>;
|
|
1024
|
+
}
|
|
1025
|
+
interface SaveExtractedResult {
|
|
1026
|
+
/** 真正写入的新经验。 */
|
|
1027
|
+
saved: Lesson[];
|
|
1028
|
+
/** 因命中近似既有经验而跳过的数量(去重)。 */
|
|
1029
|
+
skippedSimilar: number;
|
|
1030
|
+
}
|
|
1031
|
+
interface SaveExtractedOptions {
|
|
1032
|
+
/** 去重阈值,透传 `findSimilar`。省略用 store 默认值。 */
|
|
1033
|
+
similarityThreshold?: number;
|
|
1034
|
+
}
|
|
1035
|
+
/**
|
|
1036
|
+
* 去重前置写入:把抽取出的候选 `LessonInput[]` 逐条写入 store——**写前必 `findSimilar`**,
|
|
1037
|
+
* 命中近似既有经验则跳过,否则 `save`。严禁无去重批量追加(否则 lesson 爆炸→召回退化+持久化膨胀)。
|
|
1038
|
+
*
|
|
1039
|
+
* 批内去重天然成立:顺序写入 + `findSimilar` 查实时 store,故后续候选会命中刚写入的同批经验而跳过。
|
|
1040
|
+
*/
|
|
1041
|
+
declare function saveExtractedLessons(store: LessonWriteStore, candidates: readonly LessonInput[], options?: SaveExtractedOptions): Promise<SaveExtractedResult>;
|
|
1042
|
+
//#endregion
|
|
1043
|
+
//#region src/auto-extract.d.ts
|
|
1044
|
+
/** 编排器配置(camelCase 运行时形态;App 层从 snake_case settings 映射)。 */
|
|
1045
|
+
interface MemoryAutoExtractConfig {
|
|
1046
|
+
/** 总开关。默认关——灰度验证信噪比后再放开(R6)。 */
|
|
1047
|
+
enabled: boolean;
|
|
1048
|
+
/** 节流:每 N 次 idle 才抽取一次(累积切片)。默认 1(每次 idle 都抽)。 */
|
|
1049
|
+
everyNTurns?: number;
|
|
1050
|
+
/** 单次抽取上限,透传 extractLessons。 */
|
|
1051
|
+
maxLessons?: number;
|
|
1052
|
+
/** 去重阈值,透传 saveExtractedLessons。 */
|
|
1053
|
+
similarityThreshold?: number;
|
|
1054
|
+
/** 抽取调用 token 上限,透传 extractLessons。 */
|
|
1055
|
+
maxTokens?: number;
|
|
1056
|
+
}
|
|
1057
|
+
interface MemoryAutoExtractorDeps {
|
|
1058
|
+
llm: ExtractLLM;
|
|
1059
|
+
store: LessonWriteStore;
|
|
1060
|
+
config: MemoryAutoExtractConfig;
|
|
1061
|
+
/**
|
|
1062
|
+
* R5 不与主 agent 抢:判定某工具调用是否算"记忆写入"——若本批切片里主 agent 已写过记忆,
|
|
1063
|
+
* 跳过本次抽取。默认认 `learn` 工具与名字含 `memory` 的工具。
|
|
1064
|
+
*/
|
|
1065
|
+
isMemoryWriteTool?: (toolName: string) => boolean;
|
|
1066
|
+
}
|
|
1067
|
+
type AutoExtractReason = 'disabled' | 'no-new-messages' | 'throttled' | 'main-agent-wrote' | 'error';
|
|
1068
|
+
interface AutoExtractOutcome {
|
|
1069
|
+
ran: boolean;
|
|
1070
|
+
reason?: AutoExtractReason;
|
|
1071
|
+
saved: number;
|
|
1072
|
+
skippedSimilar: number;
|
|
1073
|
+
}
|
|
1074
|
+
/**
|
|
1075
|
+
* 会话经验自动抽取编排器(纯库 + 全注入)。
|
|
1076
|
+
*
|
|
1077
|
+
* 在每次会话 idle(由 runtime 订阅 `session.idle`/`turn.end` 触发)调 `onIdle`——按
|
|
1078
|
+
* gate(R6 默认关)→ 节流(R7)→ 不抢(R5)→ 抽取(fail-closed)→ 去重写入(R4)流程跑。
|
|
1079
|
+
* **永不抛**(R2):任何失败返回 `{ran:false, reason:'error'}`,绝不阻塞或污染主回合。
|
|
1080
|
+
* runtime 层只需提供 `llm`(复用 summarize 接缝)、`store`、`config`,并在每次 idle 把会话
|
|
1081
|
+
* 全量 messages 传进来——编排器自己按 `lastExtractedIndex` 切增量。
|
|
1082
|
+
*/
|
|
1083
|
+
declare function createMemoryAutoExtractor(deps: MemoryAutoExtractorDeps): {
|
|
1084
|
+
onIdle: (sessionId: string, messages: readonly Message[], opts?: {
|
|
1085
|
+
signal?: AbortSignal;
|
|
1086
|
+
}) => Promise<AutoExtractOutcome>;
|
|
1087
|
+
reset: (sessionId: string) => void;
|
|
1088
|
+
};
|
|
1089
|
+
//#endregion
|
|
1090
|
+
//#region src/file-state-snapshot.d.ts
|
|
1091
|
+
interface FileStateSnapshot {
|
|
1092
|
+
timestamp: number;
|
|
1093
|
+
directoryTree: string;
|
|
1094
|
+
modifiedFiles: Array<{
|
|
1095
|
+
path: string;
|
|
1096
|
+
action: 'created' | 'modified' | 'deleted';
|
|
1097
|
+
summary: string;
|
|
1098
|
+
}>;
|
|
1099
|
+
planStatus?: string;
|
|
1100
|
+
findings: string[];
|
|
1101
|
+
}
|
|
1102
|
+
interface FileStateCapture {
|
|
1103
|
+
workspaceDir: string;
|
|
1104
|
+
recentFiles?: string[];
|
|
1105
|
+
planStatus?: string;
|
|
1106
|
+
}
|
|
1107
|
+
declare class FileStateManager {
|
|
1108
|
+
private lastSnapshot;
|
|
1109
|
+
capture(input: FileStateCapture): Promise<FileStateSnapshot>;
|
|
1110
|
+
getLastSnapshot(): FileStateSnapshot | null;
|
|
1111
|
+
formatSnapshot(snapshot: FileStateSnapshot): string;
|
|
1112
|
+
private buildDirectoryTree;
|
|
1113
|
+
private checkRecentFiles;
|
|
1114
|
+
}
|
|
1115
|
+
//#endregion
|
|
1116
|
+
export { type AgentsDiscoveryOptions, type ArchiveEntry, type ArchiveStorage, type AutoExtractOutcome, type AutoExtractReason, AutoMemory, type AutoMemoryEntry, type AutoMemoryOptions, type CompactionConfig, type CompactionPreparation, type CompactionResult, type ContextSize, type ContextTokenEstimate, type CutPoint, DEFAULT_COMPACTION_CONFIG, DEFAULT_MEMORY_INJECTION_BUDGET_TOKENS, DEFAULT_MEMORY_SOURCES, DEFAULT_PRUNE_CONFIG, DEFAULT_SIMILARITY_THRESHOLD, type ExtractLLM, type ExtractionOptions, type FileStateCapture, FileStateManager, type FileStateSnapshot, FileSystemMemoryStore, HttpMemoryStore, IMAGE_BLOCK_TOKEN_ESTIMATE, InMemoryArchiveStorage, InMemoryMemoryStore, LESSON_EXTRACTION_PROMPT, type LearningStoreOptions, type Lesson, type LessonClass, type LessonFilter, type LessonInput, type LessonState, type LessonWriteStore, type MemoryAutoExtractConfig, type MemoryAutoExtractorDeps, type MemoryDefaults, MemoryManager, type MemoryManagerConfig, type MemorySource, type MemoryStore, type MessageFromEntry, type MinimalEntry, OperationalLearningStore, PersistentMemory, type PruneConfig, type PruneResult, SEGMENT_MERGE_PROMPT, SEGMENT_SUMMARIZATION_PROMPT, SUMMARIZATION_PROMPT, type SaveExtractedOptions, type SaveExtractedResult, type SimilarLesson, type StorageType, type SummarizationContext, type SummarizationVariant, TURN_PREFIX_SUMMARIZATION_PROMPT, UPDATE_SUMMARIZATION_PROMPT, WITH_PRESERVED_TAIL_PROMPT, buildArchiveReference, buildExtractionPrompt, buildLowCoverageNotice, buildMemoryInjection, buildSegmentMergePrompt, buildSummarizationPrompt, buildTurnPrefixPrompt, computeLessonContentHash, computeMemoryDefaults, createMemoryAutoExtractor, createMemoryManager, createOverlayMemoryStore, discoverAgentsSources, ensureTokenizer, estimateLessonTokens, estimateMessageTokens, estimateMessagesTokens, estimateTokens, estimateTokensHeuristic, extractLessons, formatArchive, messageToText, prune, resolveContextSize, saveExtractedLessons };
|
|
1117
|
+
//# sourceMappingURL=index.d.ts.map
|