@cup319/mmpl 2.8.1 → 3.0.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 +30 -268
- package/install.js +90 -0
- package/mmpl-v3.0.0-linux-x86_64.tar.gz +0 -0
- package/mmpl-v3.0.0-windows-x86_64.zip +0 -0
- package/package.json +20 -30
- package/run.js +15 -0
- package/dist/auto-context-NUQFETSY.js +0 -248
- package/dist/auto-context-NUQFETSY.js.map +0 -1
- package/dist/checkpoint-ABRNCBCA.js +0 -68
- package/dist/checkpoint-ABRNCBCA.js.map +0 -1
- package/dist/chunk-2H7UOFLK.js +0 -11
- package/dist/chunk-2H7UOFLK.js.map +0 -1
- package/dist/chunk-CHVFXR24.js +0 -19
- package/dist/chunk-CHVFXR24.js.map +0 -1
- package/dist/chunk-JL66S7P7.js +0 -151
- package/dist/chunk-JL66S7P7.js.map +0 -1
- package/dist/chunk-SSDURI3I.js +0 -46
- package/dist/chunk-SSDURI3I.js.map +0 -1
- package/dist/index.js +0 -21994
- package/dist/index.js.map +0 -1
- package/dist/mermaid-injector-ROQAAM7B.js +0 -171
- package/dist/mermaid-injector-ROQAAM7B.js.map +0 -1
- package/dist/observability-6AMQYVCF.js +0 -100
- package/dist/observability-6AMQYVCF.js.map +0 -1
- package/dist/offload-engine-BF5IXXBH.js +0 -10
- package/dist/offload-engine-BF5IXXBH.js.map +0 -1
- package/dist/persona-generator-NZQVIVDA.js +0 -228
- package/dist/persona-generator-NZQVIVDA.js.map +0 -1
- package/dist/pipeline-manager-IJ2JLUMK.js +0 -485
- package/dist/pipeline-manager-IJ2JLUMK.js.map +0 -1
- package/dist/scene-builder-TLVG23BJ.js +0 -296
- package/dist/scene-builder-TLVG23BJ.js.map +0 -1
- package/dist/scene-index-SU6GMVDL.js +0 -178
- package/dist/scene-index-SU6GMVDL.js.map +0 -1
- package/dist/symbolic-A4XAGBBB.js +0 -502
- package/dist/symbolic-A4XAGBBB.js.map +0 -1
|
@@ -1,248 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
OffloadEngine
|
|
4
|
-
} from "./chunk-JL66S7P7.js";
|
|
5
|
-
import {
|
|
6
|
-
estimateTokens
|
|
7
|
-
} from "./chunk-CHVFXR24.js";
|
|
8
|
-
import "./chunk-2H7UOFLK.js";
|
|
9
|
-
|
|
10
|
-
// src/auto-context.ts
|
|
11
|
-
var EMPTY_RESULT = {
|
|
12
|
-
contextText: "",
|
|
13
|
-
tokenCount: 0,
|
|
14
|
-
memoryCount: 0,
|
|
15
|
-
hasPersona: false,
|
|
16
|
-
sceneCount: 0,
|
|
17
|
-
offloadStrategy: "none",
|
|
18
|
-
pendingL0: 0
|
|
19
|
-
};
|
|
20
|
-
var AutoContextGenerator = class {
|
|
21
|
-
opts;
|
|
22
|
-
offloadEngine;
|
|
23
|
-
constructor(opts) {
|
|
24
|
-
this.opts = opts;
|
|
25
|
-
this.offloadEngine = opts.offloadEngine ?? new OffloadEngine({ tokenBudget: opts.tokenBudget ?? 2e3 });
|
|
26
|
-
}
|
|
27
|
-
/**
|
|
28
|
-
* 根据用户查询生成完整的记忆上下文。
|
|
29
|
-
*
|
|
30
|
-
* 全程 try-catch,任何子步骤失败都会被吞掉并跳过;整体失败返回空上下文。
|
|
31
|
-
*/
|
|
32
|
-
async generate(query) {
|
|
33
|
-
if (!query || !query.trim()) {
|
|
34
|
-
return { ...EMPTY_RESULT };
|
|
35
|
-
}
|
|
36
|
-
try {
|
|
37
|
-
const topK = this.opts.topK ?? 5;
|
|
38
|
-
const relevanceThreshold = this.opts.relevanceThreshold ?? 0.5;
|
|
39
|
-
const scanLimit = this.opts.scanLimit ?? 50;
|
|
40
|
-
const personaTokenLimit = this.opts.personaTokenLimit ?? 500;
|
|
41
|
-
const snippetLen = this.opts.memorySnippetLength ?? 120;
|
|
42
|
-
const entries = [];
|
|
43
|
-
await this.collectVectorHits(
|
|
44
|
-
query,
|
|
45
|
-
entries,
|
|
46
|
-
relevanceThreshold,
|
|
47
|
-
scanLimit
|
|
48
|
-
);
|
|
49
|
-
await this.collectKeywordHits(query, entries, topK);
|
|
50
|
-
await this.collectSceneHits(query, entries);
|
|
51
|
-
const offloadResult = this.offloadEngine.compress(entries);
|
|
52
|
-
let contextText = "";
|
|
53
|
-
if (offloadResult.retained.length > 0) {
|
|
54
|
-
contextText += "## \u76F8\u5173\u8BB0\u5FC6\n";
|
|
55
|
-
for (const e of offloadResult.retained) {
|
|
56
|
-
const snippet = e.content.length > snippetLen ? e.content.substring(0, snippetLen) + "\u2026" : e.content;
|
|
57
|
-
contextText += `- ${snippet}
|
|
58
|
-
`;
|
|
59
|
-
}
|
|
60
|
-
if (offloadResult.summary) {
|
|
61
|
-
contextText += `
|
|
62
|
-
> ${offloadResult.summary}
|
|
63
|
-
`;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
const personaOut = await this.injectPersona(
|
|
67
|
-
contextText,
|
|
68
|
-
personaTokenLimit
|
|
69
|
-
);
|
|
70
|
-
contextText = personaOut.text;
|
|
71
|
-
const hasPersona = personaOut.injected;
|
|
72
|
-
let hasMermaid = false;
|
|
73
|
-
if (this.opts.mermaidInjector && this.opts.sessionId) {
|
|
74
|
-
try {
|
|
75
|
-
const mmdResult = await this.opts.mermaidInjector.injectExisting(this.opts.sessionId);
|
|
76
|
-
if (mmdResult.injectText) {
|
|
77
|
-
contextText = mmdResult.injectText + "\n\n" + contextText;
|
|
78
|
-
hasMermaid = true;
|
|
79
|
-
}
|
|
80
|
-
} catch {
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
let pendingL0 = 0;
|
|
84
|
-
try {
|
|
85
|
-
const raw = await this.opts.storage.getUnconsumedRaw(100);
|
|
86
|
-
pendingL0 = Array.isArray(raw) ? raw.length : 0;
|
|
87
|
-
} catch {
|
|
88
|
-
}
|
|
89
|
-
const tokenCount = estimateTokens(contextText);
|
|
90
|
-
return {
|
|
91
|
-
contextText,
|
|
92
|
-
tokenCount,
|
|
93
|
-
memoryCount: offloadResult.retained.filter((e) => e.layer === 1).length,
|
|
94
|
-
hasPersona,
|
|
95
|
-
sceneCount: offloadResult.retained.filter((e) => e.layer === 2).length,
|
|
96
|
-
offloadStrategy: offloadResult.strategy,
|
|
97
|
-
pendingL0
|
|
98
|
-
};
|
|
99
|
-
} catch (err) {
|
|
100
|
-
console.error(
|
|
101
|
-
"[auto-context] generate failed:",
|
|
102
|
-
err instanceof Error ? err.message : err
|
|
103
|
-
);
|
|
104
|
-
return { ...EMPTY_RESULT };
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
/**
|
|
108
|
-
* 步骤 1: 向量检索 L1 active memories。
|
|
109
|
-
* 用 cosine 相似度过滤,命中阈值则加入 entries。
|
|
110
|
-
*/
|
|
111
|
-
async collectVectorHits(query, entries, relevanceThreshold, scanLimit) {
|
|
112
|
-
try {
|
|
113
|
-
if (!this.opts.embeddingService?.available) return;
|
|
114
|
-
const emb = await this.opts.embeddingService.embed(query);
|
|
115
|
-
if (!emb || emb.length === 0) return;
|
|
116
|
-
const memories = await this.opts.storage.getActiveMemories();
|
|
117
|
-
if (!Array.isArray(memories)) return;
|
|
118
|
-
for (const m of memories.slice(0, scanLimit)) {
|
|
119
|
-
let mEmb = m.embedding;
|
|
120
|
-
if (typeof mEmb === "string") {
|
|
121
|
-
try {
|
|
122
|
-
mEmb = JSON.parse(mEmb);
|
|
123
|
-
} catch {
|
|
124
|
-
mEmb = null;
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
if (!Array.isArray(mEmb) || mEmb.length === 0) continue;
|
|
128
|
-
const sim = this.cosineSim(emb, mEmb);
|
|
129
|
-
if (sim >= relevanceThreshold) {
|
|
130
|
-
entries.push({
|
|
131
|
-
id: m.id,
|
|
132
|
-
content: m.content,
|
|
133
|
-
score: sim,
|
|
134
|
-
layer: 1,
|
|
135
|
-
importance: typeof m.importance === "number" ? m.importance : 0.5,
|
|
136
|
-
strength: typeof m.strength === "number" ? m.strength : 0.5,
|
|
137
|
-
source: "vector"
|
|
138
|
-
});
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
} catch {
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
/**
|
|
145
|
-
* 步骤 2: BM25 关键词检索补充 L1。
|
|
146
|
-
* 只加入尚未被向量召回命中的记忆。
|
|
147
|
-
*/
|
|
148
|
-
async collectKeywordHits(query, entries, topK) {
|
|
149
|
-
try {
|
|
150
|
-
const results = await this.opts.storage.searchByKeywords(
|
|
151
|
-
query,
|
|
152
|
-
topK,
|
|
153
|
-
"active"
|
|
154
|
-
);
|
|
155
|
-
if (!Array.isArray(results)) return;
|
|
156
|
-
for (const m of results) {
|
|
157
|
-
if (entries.find((e) => e.id === m.id)) continue;
|
|
158
|
-
entries.push({
|
|
159
|
-
id: m.id,
|
|
160
|
-
content: m.content,
|
|
161
|
-
score: 0.7,
|
|
162
|
-
layer: 1,
|
|
163
|
-
importance: typeof m.importance === "number" ? m.importance : 0.5,
|
|
164
|
-
strength: typeof m.strength === "number" ? m.strength : 0.5,
|
|
165
|
-
source: "bm25"
|
|
166
|
-
});
|
|
167
|
-
}
|
|
168
|
-
} catch {
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
/**
|
|
172
|
-
* 步骤 3: 场景匹配 L2。
|
|
173
|
-
* 用 label / summary 子串匹配(大小写不敏感)。
|
|
174
|
-
*/
|
|
175
|
-
async collectSceneHits(query, entries) {
|
|
176
|
-
try {
|
|
177
|
-
const scenes = await this.opts.storage.getAllScenes();
|
|
178
|
-
if (!Array.isArray(scenes)) return;
|
|
179
|
-
const qLower = query.toLowerCase();
|
|
180
|
-
for (const s of scenes) {
|
|
181
|
-
const label = (s.label ?? "").toLowerCase();
|
|
182
|
-
const summary = (s.summary ?? "").toLowerCase();
|
|
183
|
-
if (label.includes(qLower) || summary.includes(qLower)) {
|
|
184
|
-
entries.push({
|
|
185
|
-
id: s.id,
|
|
186
|
-
content: `[\u573A\u666F] ${s.label ?? s.id}: ${(s.summary ?? "").substring(
|
|
187
|
-
0,
|
|
188
|
-
80
|
|
189
|
-
)}`,
|
|
190
|
-
score: 0.6,
|
|
191
|
-
layer: 2,
|
|
192
|
-
importance: 0.7,
|
|
193
|
-
strength: 1,
|
|
194
|
-
source: "scene"
|
|
195
|
-
});
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
} catch {
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
/**
|
|
202
|
-
* 步骤 6: 注入 L3 用户画像。
|
|
203
|
-
* 画像内容不参与 offload 压缩,但有自己的 token 上限保护。
|
|
204
|
-
* 返回更新后的文本和是否注入标志。
|
|
205
|
-
*/
|
|
206
|
-
async injectPersona(contextText, personaTokenLimit) {
|
|
207
|
-
try {
|
|
208
|
-
const persona = await this.opts.storage.getLatestPersona();
|
|
209
|
-
if (!persona || !persona.content) {
|
|
210
|
-
return { text: contextText, injected: false };
|
|
211
|
-
}
|
|
212
|
-
const personaTokens = estimateTokens(persona.content);
|
|
213
|
-
if (personaTokens >= personaTokenLimit) {
|
|
214
|
-
return { text: contextText, injected: false };
|
|
215
|
-
}
|
|
216
|
-
const text = contextText + `
|
|
217
|
-
## \u7528\u6237\u753B\u50CF
|
|
218
|
-
${persona.content.substring(0, 300)}
|
|
219
|
-
`;
|
|
220
|
-
return { text, injected: true };
|
|
221
|
-
} catch {
|
|
222
|
-
return { text: contextText, injected: false };
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
/**
|
|
226
|
-
* 余弦相似度(内联实现,不依赖 retrieval.ts)
|
|
227
|
-
* 不等长或空向量返回 0。
|
|
228
|
-
*/
|
|
229
|
-
cosineSim(a, b) {
|
|
230
|
-
if (!a || !b || a.length === 0 || b.length === 0 || a.length !== b.length) {
|
|
231
|
-
return 0;
|
|
232
|
-
}
|
|
233
|
-
let dot = 0;
|
|
234
|
-
let magA = 0;
|
|
235
|
-
let magB = 0;
|
|
236
|
-
for (let i = 0; i < a.length; i++) {
|
|
237
|
-
dot += a[i] * b[i];
|
|
238
|
-
magA += a[i] * a[i];
|
|
239
|
-
magB += b[i] * b[i];
|
|
240
|
-
}
|
|
241
|
-
const denom = Math.sqrt(magA) * Math.sqrt(magB);
|
|
242
|
-
return denom > 0 ? dot / denom : 0;
|
|
243
|
-
}
|
|
244
|
-
};
|
|
245
|
-
export {
|
|
246
|
-
AutoContextGenerator
|
|
247
|
-
};
|
|
248
|
-
//# sourceMappingURL=auto-context-NUQFETSY.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/auto-context.ts"],"sourcesContent":["/**\n * 自动召回上下文生成器 (Auto-Context Generator)\n *\n * 背景:\n * MCP Server 无法 Hook agent 的生命周期(不像 OpenClaw 这类插件框架可以\n * 拦截 before_prompt_build),因此这里实现一个\"一站式\"工具: agent 在每轮\n * 对话开始时调用一次 generate(query),即可拿到该轮所需的全部记忆上下文。\n *\n * 这相当于 TencentDB AgentMemory 的 before_prompt_build Hook 的 MCP 版本。\n *\n * 流程:\n * 1. 向量检索 L1 active memories(语义相似)\n * 2. BM25 关键词检索 L1 补充(覆盖向量召回遗漏)\n * 3. 场景匹配 L2(label / summary 子串匹配)\n * 4. L3 用户画像固定注入(不参与压缩)\n * 5. 检查 L0 待处理计数(提示 agent 是否需要 consolidate)\n * 6. 用 OffloadEngine 把 L1+L2 压缩到 token 预算内\n * 7. 格式化为可读 Markdown 文本\n *\n * 全程 try-catch,任何子步骤失败都不会抛出,最坏返回空上下文。\n * 不依赖任何外部库。\n */\n\nimport { OffloadEngine, OffloadEntry } from \"./offload-engine.js\";\nimport { estimateTokens } from \"./token-counter.js\";\n\n/** AutoContextGenerator 配置选项 */\nexport interface AutoContextOptions {\n /** 存储层(鸭子类型,提供 getActiveMemories / getAllScenes / getLatestPersona / searchByKeywords / getUnconsumedRaw 等) */\n storage: any;\n /** Embedding 服务(提供 available / embed) */\n embeddingService: any;\n /** 可选的 OffloadEngine,不传则按 tokenBudget 自动创建 */\n offloadEngine?: OffloadEngine;\n /** 可选的 MermaidInjector,提供时自动注入任务符号图 */\n mermaidInjector?: any;\n /** 可选的 session_id,用于注入已有 Mermaid 会话图 */\n sessionId?: string;\n /** 上下文 token 预算,默认 2000 */\n tokenBudget?: number;\n /** 每层取 top-k,默认 5 */\n topK?: number;\n /** 相关性阈值,默认 0.5 */\n relevanceThreshold?: number;\n /** 向量扫描上限(性能保护),默认 50 */\n scanLimit?: number;\n /** 画像内容 token 上限,超过则不注入,默认 500 */\n personaTokenLimit?: number;\n /** 单条记忆输出截断长度,默认 120 */\n memorySnippetLength?: number;\n}\n\n/** generate() 返回结果 */\nexport interface AutoContextResult {\n /** 注入到 prompt 前面的记忆上下文文本 */\n contextText: string;\n /** 上下文 token 数 */\n tokenCount: number;\n /** 命中的记忆条数(L1 retained) */\n memoryCount: number;\n /** 是否包含用户画像 */\n hasPersona: boolean;\n /** 命中的场景数(L2 retained) */\n sceneCount: number;\n /** 压缩策略名称 */\n offloadStrategy: string;\n /** 未消费的 L0 计数 */\n pendingL0: number;\n}\n\n/** 空结果常量,失败时统一返回 */\nconst EMPTY_RESULT: AutoContextResult = {\n contextText: \"\",\n tokenCount: 0,\n memoryCount: 0,\n hasPersona: false,\n sceneCount: 0,\n offloadStrategy: \"none\",\n pendingL0: 0,\n};\n\n/**\n * 自动召回上下文生成器\n */\nexport class AutoContextGenerator {\n private opts: AutoContextOptions;\n private offloadEngine: OffloadEngine;\n\n constructor(opts: AutoContextOptions) {\n this.opts = opts;\n // 若未提供 offloadEngine,按 tokenBudget 自动创建一个\n this.offloadEngine =\n opts.offloadEngine ??\n new OffloadEngine({ tokenBudget: opts.tokenBudget ?? 2000 });\n }\n\n /**\n * 根据用户查询生成完整的记忆上下文。\n *\n * 全程 try-catch,任何子步骤失败都会被吞掉并跳过;整体失败返回空上下文。\n */\n async generate(query: string): Promise<AutoContextResult> {\n // 防御:空 query 直接返回空上下文\n if (!query || !query.trim()) {\n return { ...EMPTY_RESULT };\n }\n\n try {\n const topK = this.opts.topK ?? 5;\n const relevanceThreshold = this.opts.relevanceThreshold ?? 0.5;\n const scanLimit = this.opts.scanLimit ?? 50;\n const personaTokenLimit = this.opts.personaTokenLimit ?? 500;\n const snippetLen = this.opts.memorySnippetLength ?? 120;\n\n const entries: OffloadEntry[] = [];\n\n // ===== 1. 向量检索 L1 active memories =====\n await this.collectVectorHits(\n query,\n entries,\n relevanceThreshold,\n scanLimit\n );\n\n // ===== 2. BM25 关键词检索补充 L1 =====\n await this.collectKeywordHits(query, entries, topK);\n\n // ===== 3. 场景匹配 L2 =====\n await this.collectSceneHits(query, entries);\n\n // ===== 4. 用 OffloadEngine 压缩到 token 预算 =====\n const offloadResult = this.offloadEngine.compress(entries);\n\n // ===== 5. 格式化记忆 + 场景文本 =====\n let contextText = \"\";\n if (offloadResult.retained.length > 0) {\n contextText += \"## 相关记忆\\n\";\n for (const e of offloadResult.retained) {\n const snippet =\n e.content.length > snippetLen\n ? e.content.substring(0, snippetLen) + \"…\"\n : e.content;\n contextText += `- ${snippet}\\n`;\n }\n if (offloadResult.summary) {\n contextText += `\\n> ${offloadResult.summary}\\n`;\n }\n }\n\n // ===== 6. L3 用户画像(固定注入,不参与压缩) =====\n const personaOut = await this.injectPersona(\n contextText,\n personaTokenLimit\n );\n contextText = personaOut.text;\n const hasPersona = personaOut.injected;\n\n // ===== 6.5 Mermaid 任务符号图注入 =====\n let hasMermaid = false;\n if (this.opts.mermaidInjector && this.opts.sessionId) {\n try {\n const mmdResult = await this.opts.mermaidInjector.injectExisting(this.opts.sessionId);\n if (mmdResult.injectText) {\n contextText = mmdResult.injectText + \"\\n\\n\" + contextText;\n hasMermaid = true;\n }\n } catch {\n /* ignore mermaid injection failure */\n }\n }\n\n // ===== 7. L0 待处理计数 =====\n let pendingL0 = 0;\n try {\n const raw = await this.opts.storage.getUnconsumedRaw(100);\n pendingL0 = Array.isArray(raw) ? raw.length : 0;\n } catch {\n /* ignore */\n }\n\n const tokenCount = estimateTokens(contextText);\n\n return {\n contextText,\n tokenCount,\n memoryCount: offloadResult.retained.filter((e) => e.layer === 1).length,\n hasPersona,\n sceneCount: offloadResult.retained.filter((e) => e.layer === 2).length,\n offloadStrategy: offloadResult.strategy,\n pendingL0,\n };\n } catch (err) {\n // 任何意外错误都返回空上下文,绝不抛出\n console.error(\n \"[auto-context] generate failed:\",\n err instanceof Error ? err.message : err\n );\n return { ...EMPTY_RESULT };\n }\n }\n\n /**\n * 步骤 1: 向量检索 L1 active memories。\n * 用 cosine 相似度过滤,命中阈值则加入 entries。\n */\n private async collectVectorHits(\n query: string,\n entries: OffloadEntry[],\n relevanceThreshold: number,\n scanLimit: number\n ): Promise<void> {\n try {\n if (!this.opts.embeddingService?.available) return;\n const emb = await this.opts.embeddingService.embed(query);\n if (!emb || emb.length === 0) return;\n\n const memories = await this.opts.storage.getActiveMemories();\n if (!Array.isArray(memories)) return;\n\n for (const m of memories.slice(0, scanLimit)) {\n // embedding 可能存成 JSON 字符串\n let mEmb: any = m.embedding;\n if (typeof mEmb === \"string\") {\n try {\n mEmb = JSON.parse(mEmb);\n } catch {\n mEmb = null;\n }\n }\n if (!Array.isArray(mEmb) || mEmb.length === 0) continue;\n\n const sim = this.cosineSim(emb, mEmb);\n if (sim >= relevanceThreshold) {\n entries.push({\n id: m.id,\n content: m.content,\n score: sim,\n layer: 1,\n importance: typeof m.importance === \"number\" ? m.importance : 0.5,\n strength: typeof m.strength === \"number\" ? m.strength : 0.5,\n source: \"vector\",\n });\n }\n }\n } catch {\n /* ignore — 向量检索失败不影响后续步骤 */\n }\n }\n\n /**\n * 步骤 2: BM25 关键词检索补充 L1。\n * 只加入尚未被向量召回命中的记忆。\n */\n private async collectKeywordHits(\n query: string,\n entries: OffloadEntry[],\n topK: number\n ): Promise<void> {\n try {\n const results = await this.opts.storage.searchByKeywords(\n query,\n topK,\n \"active\"\n );\n if (!Array.isArray(results)) return;\n for (const m of results) {\n if (entries.find((e) => e.id === m.id)) continue;\n entries.push({\n id: m.id,\n content: m.content,\n score: 0.7,\n layer: 1,\n importance: typeof m.importance === \"number\" ? m.importance : 0.5,\n strength: typeof m.strength === \"number\" ? m.strength : 0.5,\n source: \"bm25\",\n });\n }\n } catch {\n /* ignore */\n }\n }\n\n /**\n * 步骤 3: 场景匹配 L2。\n * 用 label / summary 子串匹配(大小写不敏感)。\n */\n private async collectSceneHits(\n query: string,\n entries: OffloadEntry[]\n ): Promise<void> {\n try {\n const scenes = await this.opts.storage.getAllScenes();\n if (!Array.isArray(scenes)) return;\n const qLower = query.toLowerCase();\n for (const s of scenes) {\n const label = (s.label ?? \"\").toLowerCase();\n const summary = (s.summary ?? \"\").toLowerCase();\n if (label.includes(qLower) || summary.includes(qLower)) {\n entries.push({\n id: s.id,\n content: `[场景] ${s.label ?? s.id}: ${(s.summary ?? \"\").substring(\n 0,\n 80\n )}`,\n score: 0.6,\n layer: 2,\n importance: 0.7,\n strength: 1.0,\n source: \"scene\",\n });\n }\n }\n } catch {\n /* ignore */\n }\n }\n\n /**\n * 步骤 6: 注入 L3 用户画像。\n * 画像内容不参与 offload 压缩,但有自己的 token 上限保护。\n * 返回更新后的文本和是否注入标志。\n */\n private async injectPersona(\n contextText: string,\n personaTokenLimit: number\n ): Promise<{ text: string; injected: boolean }> {\n try {\n const persona = await this.opts.storage.getLatestPersona();\n if (!persona || !persona.content) {\n return { text: contextText, injected: false };\n }\n const personaTokens = estimateTokens(persona.content);\n if (personaTokens >= personaTokenLimit) {\n return { text: contextText, injected: false };\n }\n const text =\n contextText +\n `\\n## 用户画像\\n${persona.content.substring(0, 300)}\\n`;\n return { text, injected: true };\n } catch {\n return { text: contextText, injected: false };\n }\n }\n\n /**\n * 余弦相似度(内联实现,不依赖 retrieval.ts)\n * 不等长或空向量返回 0。\n */\n private cosineSim(a: number[], b: number[]): number {\n if (\n !a ||\n !b ||\n a.length === 0 ||\n b.length === 0 ||\n a.length !== b.length\n ) {\n return 0;\n }\n let dot = 0;\n let magA = 0;\n let magB = 0;\n for (let i = 0; i < a.length; i++) {\n dot += a[i] * b[i];\n magA += a[i] * a[i];\n magB += b[i] * b[i];\n }\n const denom = Math.sqrt(magA) * Math.sqrt(magB);\n return denom > 0 ? dot / denom : 0;\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import "./chunk-2H7UOFLK.js";
|
|
3
|
-
|
|
4
|
-
// src/checkpoint.ts
|
|
5
|
-
import { promises as fs } from "fs";
|
|
6
|
-
import path from "path";
|
|
7
|
-
var CheckpointManager = class {
|
|
8
|
-
checkpointPath;
|
|
9
|
-
checkpoint;
|
|
10
|
-
dirty = false;
|
|
11
|
-
constructor(dataDir) {
|
|
12
|
-
this.checkpointPath = path.join(dataDir, ".metadata", "pipeline_checkpoint.json");
|
|
13
|
-
this.checkpoint = {
|
|
14
|
-
lastL1ProcessedRawId: null,
|
|
15
|
-
lastL1Time: 0,
|
|
16
|
-
lastL2Time: 0,
|
|
17
|
-
lastL3Time: 0,
|
|
18
|
-
totalL0Processed: 0,
|
|
19
|
-
totalL1Extracted: 0,
|
|
20
|
-
totalL2Built: 0,
|
|
21
|
-
totalL3Versions: 0,
|
|
22
|
-
updatedAt: 0
|
|
23
|
-
};
|
|
24
|
-
}
|
|
25
|
-
async init() {
|
|
26
|
-
try {
|
|
27
|
-
const raw = await fs.readFile(this.checkpointPath, "utf-8");
|
|
28
|
-
const parsed = JSON.parse(raw);
|
|
29
|
-
this.checkpoint = { ...this.checkpoint, ...parsed };
|
|
30
|
-
} catch {
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
recordL1(rawId, extracted) {
|
|
34
|
-
this.checkpoint.lastL1ProcessedRawId = rawId;
|
|
35
|
-
this.checkpoint.lastL1Time = Date.now();
|
|
36
|
-
this.checkpoint.totalL0Processed++;
|
|
37
|
-
this.checkpoint.totalL1Extracted += extracted;
|
|
38
|
-
this.dirty = true;
|
|
39
|
-
}
|
|
40
|
-
recordL2(sceneCount) {
|
|
41
|
-
this.checkpoint.lastL2Time = Date.now();
|
|
42
|
-
this.checkpoint.totalL2Built += sceneCount;
|
|
43
|
-
this.dirty = true;
|
|
44
|
-
}
|
|
45
|
-
recordL3() {
|
|
46
|
-
this.checkpoint.lastL3Time = Date.now();
|
|
47
|
-
this.checkpoint.totalL3Versions++;
|
|
48
|
-
this.dirty = true;
|
|
49
|
-
}
|
|
50
|
-
getCheckpoint() {
|
|
51
|
-
return { ...this.checkpoint };
|
|
52
|
-
}
|
|
53
|
-
/** 持久化到磁盘(原子写入:tmp + rename) */
|
|
54
|
-
async flush() {
|
|
55
|
-
if (!this.dirty) return;
|
|
56
|
-
this.checkpoint.updatedAt = Date.now();
|
|
57
|
-
const dir = path.dirname(this.checkpointPath);
|
|
58
|
-
await fs.mkdir(dir, { recursive: true });
|
|
59
|
-
const tmpPath = this.checkpointPath + ".tmp";
|
|
60
|
-
await fs.writeFile(tmpPath, JSON.stringify(this.checkpoint, null, 2), "utf-8");
|
|
61
|
-
await fs.rename(tmpPath, this.checkpointPath);
|
|
62
|
-
this.dirty = false;
|
|
63
|
-
}
|
|
64
|
-
};
|
|
65
|
-
export {
|
|
66
|
-
CheckpointManager
|
|
67
|
-
};
|
|
68
|
-
//# sourceMappingURL=checkpoint-ABRNCBCA.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/checkpoint.ts"],"sourcesContent":["import { promises as fs } from \"node:fs\";\nimport path from \"node:path\";\n\n/**\n * 管道检查点 — 追踪记忆处理进度,支持崩溃恢复\n *\n * 灵感: TencentDB Agent Memory 的 checkpoint.ts\n */\n\nexport interface PipelineCheckpoint {\n /** 最后一次 L1 提取处理的 L0 记录 ID */\n lastL1ProcessedRawId: string | null;\n /** 最后一次 L1 提取时间 */\n lastL1Time: number;\n /** 最后一次 L2 场景构建时间 */\n lastL2Time: number;\n /** 最后一次 L3 画像生成时间 */\n lastL3Time: number;\n /** 已处理的 L0 记录总数 */\n totalL0Processed: number;\n /** 已提取的 L1 记忆总数 */\n totalL1Extracted: number;\n /** 已构建的 L2 场景总数 */\n totalL2Built: number;\n /** 已生成的 L3 画像版本数 */\n totalL3Versions: number;\n /** 检查点最后更新时间 */\n updatedAt: number;\n}\n\nexport class CheckpointManager {\n private checkpointPath: string;\n private checkpoint: PipelineCheckpoint;\n private dirty: boolean = false;\n\n constructor(dataDir: string) {\n this.checkpointPath = path.join(dataDir, \".metadata\", \"pipeline_checkpoint.json\");\n this.checkpoint = {\n lastL1ProcessedRawId: null,\n lastL1Time: 0, lastL2Time: 0, lastL3Time: 0,\n totalL0Processed: 0, totalL1Extracted: 0, totalL2Built: 0, totalL3Versions: 0,\n updatedAt: 0,\n };\n }\n\n async init(): Promise<void> {\n try {\n const raw = await fs.readFile(this.checkpointPath, \"utf-8\");\n const parsed = JSON.parse(raw);\n this.checkpoint = { ...this.checkpoint, ...parsed };\n } catch { /* first run */ }\n }\n\n recordL1(rawId: string, extracted: number): void {\n this.checkpoint.lastL1ProcessedRawId = rawId;\n this.checkpoint.lastL1Time = Date.now();\n this.checkpoint.totalL0Processed++;\n this.checkpoint.totalL1Extracted += extracted;\n this.dirty = true;\n }\n\n recordL2(sceneCount: number): void {\n this.checkpoint.lastL2Time = Date.now();\n this.checkpoint.totalL2Built += sceneCount;\n this.dirty = true;\n }\n\n recordL3(): void {\n this.checkpoint.lastL3Time = Date.now();\n this.checkpoint.totalL3Versions++;\n this.dirty = true;\n }\n\n getCheckpoint(): PipelineCheckpoint { return { ...this.checkpoint }; }\n\n /** 持久化到磁盘(原子写入:tmp + rename) */\n async flush(): Promise<void> {\n if (!this.dirty) return;\n this.checkpoint.updatedAt = Date.now();\n const dir = path.dirname(this.checkpointPath);\n await fs.mkdir(dir, { recursive: true });\n const tmpPath = this.checkpointPath + \".tmp\";\n await fs.writeFile(tmpPath, JSON.stringify(this.checkpoint, null, 2), \"utf-8\");\n await fs.rename(tmpPath, this.checkpointPath);\n this.dirty = false;\n }\n}\n"],"mappings":";;;;AAAA,SAAS,YAAY,UAAU;AAC/B,OAAO,UAAU;AA6BV,IAAM,oBAAN,MAAwB;AAAA,EACrB;AAAA,EACA;AAAA,EACA,QAAiB;AAAA,EAEzB,YAAY,SAAiB;AAC3B,SAAK,iBAAiB,KAAK,KAAK,SAAS,aAAa,0BAA0B;AAChF,SAAK,aAAa;AAAA,MAChB,sBAAsB;AAAA,MACtB,YAAY;AAAA,MAAG,YAAY;AAAA,MAAG,YAAY;AAAA,MAC1C,kBAAkB;AAAA,MAAG,kBAAkB;AAAA,MAAG,cAAc;AAAA,MAAG,iBAAiB;AAAA,MAC5E,WAAW;AAAA,IACb;AAAA,EACF;AAAA,EAEA,MAAM,OAAsB;AAC1B,QAAI;AACF,YAAM,MAAM,MAAM,GAAG,SAAS,KAAK,gBAAgB,OAAO;AAC1D,YAAM,SAAS,KAAK,MAAM,GAAG;AAC7B,WAAK,aAAa,EAAE,GAAG,KAAK,YAAY,GAAG,OAAO;AAAA,IACpD,QAAQ;AAAA,IAAkB;AAAA,EAC5B;AAAA,EAEA,SAAS,OAAe,WAAyB;AAC/C,SAAK,WAAW,uBAAuB;AACvC,SAAK,WAAW,aAAa,KAAK,IAAI;AACtC,SAAK,WAAW;AAChB,SAAK,WAAW,oBAAoB;AACpC,SAAK,QAAQ;AAAA,EACf;AAAA,EAEA,SAAS,YAA0B;AACjC,SAAK,WAAW,aAAa,KAAK,IAAI;AACtC,SAAK,WAAW,gBAAgB;AAChC,SAAK,QAAQ;AAAA,EACf;AAAA,EAEA,WAAiB;AACf,SAAK,WAAW,aAAa,KAAK,IAAI;AACtC,SAAK,WAAW;AAChB,SAAK,QAAQ;AAAA,EACf;AAAA,EAEA,gBAAoC;AAAE,WAAO,EAAE,GAAG,KAAK,WAAW;AAAA,EAAG;AAAA;AAAA,EAGrE,MAAM,QAAuB;AAC3B,QAAI,CAAC,KAAK,MAAO;AACjB,SAAK,WAAW,YAAY,KAAK,IAAI;AACrC,UAAM,MAAM,KAAK,QAAQ,KAAK,cAAc;AAC5C,UAAM,GAAG,MAAM,KAAK,EAAE,WAAW,KAAK,CAAC;AACvC,UAAM,UAAU,KAAK,iBAAiB;AACtC,UAAM,GAAG,UAAU,SAAS,KAAK,UAAU,KAAK,YAAY,MAAM,CAAC,GAAG,OAAO;AAC7E,UAAM,GAAG,OAAO,SAAS,KAAK,cAAc;AAC5C,SAAK,QAAQ;AAAA,EACf;AACF;","names":[]}
|
package/dist/chunk-2H7UOFLK.js
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
var __defProp = Object.defineProperty;
|
|
3
|
-
var __export = (target, all) => {
|
|
4
|
-
for (var name in all)
|
|
5
|
-
__defProp(target, name, { get: all[name], enumerable: true });
|
|
6
|
-
};
|
|
7
|
-
|
|
8
|
-
export {
|
|
9
|
-
__export
|
|
10
|
-
};
|
|
11
|
-
//# sourceMappingURL=chunk-2H7UOFLK.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
package/dist/chunk-CHVFXR24.js
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
// src/token-counter.ts
|
|
4
|
-
function estimateTokens(text) {
|
|
5
|
-
if (!text) return 0;
|
|
6
|
-
let count = 0;
|
|
7
|
-
for (const char of text) {
|
|
8
|
-
if (/[\u4e00-\u9fff\u3040-\u30ff\uac00-\ud7af]/.test(char)) count += 1;
|
|
9
|
-
else if (/[a-zA-Z0-9]/.test(char)) count += 0.25;
|
|
10
|
-
else if (/\s/.test(char)) count += 0.1;
|
|
11
|
-
else count += 0.5;
|
|
12
|
-
}
|
|
13
|
-
return Math.ceil(count);
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export {
|
|
17
|
-
estimateTokens
|
|
18
|
-
};
|
|
19
|
-
//# sourceMappingURL=chunk-CHVFXR24.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/token-counter.ts"],"sourcesContent":["/**\n * 轻量 token 估算器 — 无需 tiktoken 依赖\n *\n * 估算公式:\n * 中文/CJK ~1 token/字\n * 英文字母数字 ~0.25 token/字符 (≈0.75 token/单词)\n * 空格 ~0.1 token\n * 标点符号 ~0.5 token\n *\n * 用于 Context Offload 压缩引擎的 token 预算计算。\n * 纯字符级估算,无外部依赖,开销极低。\n */\n\n/**\n * 估算文本的 token 数\n *\n * @param text 待估算的文本\n * @returns 估算的 token 数(向上取整)\n */\nexport function estimateTokens(text: string): number {\n if (!text) return 0;\n let count = 0;\n for (const char of text) {\n // CJK 字符(中日韩): ~1 token each\n if (/[\\u4e00-\\u9fff\\u3040-\\u30ff\\uac00-\\ud7af]/.test(char)) count += 1;\n // ASCII 字母数字: ~0.25 token (4 个字符约 1 token)\n else if (/[a-zA-Z0-9]/.test(char)) count += 0.25;\n // 空白字符: 0.1 token\n else if (/\\s/.test(char)) count += 0.1;\n // 标点及其他符号: 0.5 token\n else count += 0.5;\n }\n return Math.ceil(count);\n}\n\n/**\n * 估算消息数组的总 token 数(含结构开销)\n *\n * @param messages 消息列表,每条含 role 和 content\n * @returns 估算的总 token 数\n */\nexport function estimateMessagesTokens(\n messages: Array<{ role: string; content: string }>\n): number {\n let total = 0;\n for (const msg of messages) {\n total += estimateTokens(msg.content);\n total += 4; // role + structural overhead per message\n }\n return total + 2; // primer overhead\n}\n"],"mappings":";;;AAmBO,SAAS,eAAe,MAAsB;AACnD,MAAI,CAAC,KAAM,QAAO;AAClB,MAAI,QAAQ;AACZ,aAAW,QAAQ,MAAM;AAEvB,QAAI,4CAA4C,KAAK,IAAI,EAAG,UAAS;AAAA,aAE5D,cAAc,KAAK,IAAI,EAAG,UAAS;AAAA,aAEnC,KAAK,KAAK,IAAI,EAAG,UAAS;AAAA,QAE9B,UAAS;AAAA,EAChB;AACA,SAAO,KAAK,KAAK,KAAK;AACxB;","names":[]}
|
package/dist/chunk-JL66S7P7.js
DELETED
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
estimateTokens
|
|
4
|
-
} from "./chunk-CHVFXR24.js";
|
|
5
|
-
|
|
6
|
-
// src/offload-engine.ts
|
|
7
|
-
var DEFAULT_OPTIONS = {
|
|
8
|
-
tokenBudget: 2e3,
|
|
9
|
-
cascadeThreshold: 0.8,
|
|
10
|
-
aggressiveThreshold: 0.95,
|
|
11
|
-
emergencyMinKeep: 3
|
|
12
|
-
};
|
|
13
|
-
var OffloadEngine = class {
|
|
14
|
-
opts;
|
|
15
|
-
constructor(opts) {
|
|
16
|
-
this.opts = { ...DEFAULT_OPTIONS, ...opts };
|
|
17
|
-
}
|
|
18
|
-
/**
|
|
19
|
-
* 压缩记忆列表到 token 预算内
|
|
20
|
-
*
|
|
21
|
-
* 策略(逐级升级):
|
|
22
|
-
* 1. 正常: 总 token < cascade 阈值 → 不压缩
|
|
23
|
-
* 2. 级联: cascade ≤ 总 token < aggressive → 按最低 score 逐条卸载
|
|
24
|
-
* 3. 激进: aggressive ≤ 总 token < budget → 按最低 importance×strength 批量卸载
|
|
25
|
-
* 4. 紧急: 总 token ≥ budget → 只保留 emergencyMinKeep 条最高分条目,其余浓缩为摘要
|
|
26
|
-
*
|
|
27
|
-
* @param entries 待压缩的记忆条目列表(不会被修改)
|
|
28
|
-
* @returns 压缩结果
|
|
29
|
-
*/
|
|
30
|
-
compress(entries) {
|
|
31
|
-
const tokensBefore = entries.reduce(
|
|
32
|
-
(sum, e) => sum + estimateTokens(e.content),
|
|
33
|
-
0
|
|
34
|
-
);
|
|
35
|
-
const cascade = this.opts.tokenBudget * this.opts.cascadeThreshold;
|
|
36
|
-
const aggressive = this.opts.tokenBudget * this.opts.aggressiveThreshold;
|
|
37
|
-
if (tokensBefore <= cascade) {
|
|
38
|
-
return {
|
|
39
|
-
retained: entries,
|
|
40
|
-
offloaded: [],
|
|
41
|
-
tokensBefore,
|
|
42
|
-
tokensAfter: tokensBefore,
|
|
43
|
-
strategy: "none",
|
|
44
|
-
summary: null
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
if (tokensBefore <= aggressive) {
|
|
48
|
-
return this.cascadeCompress(entries, cascade, tokensBefore);
|
|
49
|
-
}
|
|
50
|
-
if (tokensBefore <= this.opts.tokenBudget) {
|
|
51
|
-
return this.aggressiveCompress(entries, cascade, tokensBefore);
|
|
52
|
-
}
|
|
53
|
-
return this.emergencyCompress(entries, tokensBefore);
|
|
54
|
-
}
|
|
55
|
-
/**
|
|
56
|
-
* 级联压缩: 按 score 升序排列,从最低分开始逐条卸载,直到降到目标 token 数
|
|
57
|
-
*/
|
|
58
|
-
cascadeCompress(entries, targetTokens, tokensBefore) {
|
|
59
|
-
const sorted = [...entries].sort((a, b) => a.score - b.score);
|
|
60
|
-
const offloaded = [];
|
|
61
|
-
let currentTokens = tokensBefore;
|
|
62
|
-
for (const entry of sorted) {
|
|
63
|
-
if (currentTokens <= targetTokens) break;
|
|
64
|
-
offloaded.push(entry.id);
|
|
65
|
-
currentTokens -= estimateTokens(entry.content);
|
|
66
|
-
}
|
|
67
|
-
const offloadedSet = new Set(offloaded);
|
|
68
|
-
const retained = entries.filter((e) => !offloadedSet.has(e.id));
|
|
69
|
-
return {
|
|
70
|
-
retained,
|
|
71
|
-
offloaded,
|
|
72
|
-
tokensBefore,
|
|
73
|
-
tokensAfter: currentTokens,
|
|
74
|
-
strategy: "cascade",
|
|
75
|
-
summary: null
|
|
76
|
-
};
|
|
77
|
-
}
|
|
78
|
-
/**
|
|
79
|
-
* 激进压缩: 按 importance × strength 升序批量卸载,直到降到目标 token 数;
|
|
80
|
-
* 对被卸载条目生成简短摘要。
|
|
81
|
-
*/
|
|
82
|
-
aggressiveCompress(entries, targetTokens, tokensBefore) {
|
|
83
|
-
const sorted = [...entries].sort(
|
|
84
|
-
(a, b) => a.importance * a.strength - b.importance * b.strength
|
|
85
|
-
);
|
|
86
|
-
const offloaded = [];
|
|
87
|
-
let currentTokens = tokensBefore;
|
|
88
|
-
for (const entry of sorted) {
|
|
89
|
-
if (currentTokens <= targetTokens) break;
|
|
90
|
-
offloaded.push(entry.id);
|
|
91
|
-
currentTokens -= estimateTokens(entry.content);
|
|
92
|
-
}
|
|
93
|
-
const offloadedSet = new Set(offloaded);
|
|
94
|
-
const retained = entries.filter((e) => !offloadedSet.has(e.id));
|
|
95
|
-
const offloadedEntries = entries.filter((e) => offloadedSet.has(e.id));
|
|
96
|
-
const summary = this.generateSummary(offloadedEntries);
|
|
97
|
-
return {
|
|
98
|
-
retained,
|
|
99
|
-
offloaded,
|
|
100
|
-
tokensBefore,
|
|
101
|
-
tokensAfter: currentTokens,
|
|
102
|
-
strategy: "aggressive",
|
|
103
|
-
summary
|
|
104
|
-
};
|
|
105
|
-
}
|
|
106
|
-
/**
|
|
107
|
-
* 紧急压缩: 按 score 降序,只保留 top emergencyMinKeep 条;
|
|
108
|
-
* 其余条目浓缩为摘要。保证最少保留 emergencyMinKeep 条。
|
|
109
|
-
*/
|
|
110
|
-
emergencyCompress(entries, tokensBefore) {
|
|
111
|
-
const sorted = [...entries].sort((a, b) => b.score - a.score);
|
|
112
|
-
const keepCount = Math.min(this.opts.emergencyMinKeep, sorted.length);
|
|
113
|
-
const retained = sorted.slice(0, keepCount);
|
|
114
|
-
const retainedIds = new Set(retained.map((e) => e.id));
|
|
115
|
-
const offloadedEntries = entries.filter((e) => !retainedIds.has(e.id));
|
|
116
|
-
const offloaded = offloadedEntries.map((e) => e.id);
|
|
117
|
-
const tokensAfter = retained.reduce(
|
|
118
|
-
(sum, e) => sum + estimateTokens(e.content),
|
|
119
|
-
0
|
|
120
|
-
);
|
|
121
|
-
const summary = this.generateSummary(offloadedEntries);
|
|
122
|
-
return {
|
|
123
|
-
retained,
|
|
124
|
-
offloaded,
|
|
125
|
-
tokensBefore,
|
|
126
|
-
tokensAfter,
|
|
127
|
-
strategy: "emergency",
|
|
128
|
-
summary
|
|
129
|
-
};
|
|
130
|
-
}
|
|
131
|
-
/**
|
|
132
|
-
* 把被卸载的条目浓缩为一句话摘要
|
|
133
|
-
*
|
|
134
|
-
* @param entries 被卸载的条目
|
|
135
|
-
* @returns 浓缩摘要字符串(无条目时返回空字符串)
|
|
136
|
-
*/
|
|
137
|
-
generateSummary(entries) {
|
|
138
|
-
if (!entries || entries.length === 0) return "";
|
|
139
|
-
const labels = entries.map((e) => e.content.substring(0, 30));
|
|
140
|
-
return `[\u5DF2\u538B\u7F29 ${entries.length} \u6761\u4F4E\u4F18\u5148\u8BB0\u5FC6: ${labels.join("; ")}...]`;
|
|
141
|
-
}
|
|
142
|
-
/** 获取引擎当前配置(返回副本,避免外部篡改) */
|
|
143
|
-
getOptions() {
|
|
144
|
-
return { ...this.opts };
|
|
145
|
-
}
|
|
146
|
-
};
|
|
147
|
-
|
|
148
|
-
export {
|
|
149
|
-
OffloadEngine
|
|
150
|
-
};
|
|
151
|
-
//# sourceMappingURL=chunk-JL66S7P7.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/offload-engine.ts"],"sourcesContent":["/**\n * Context Offload 压缩引擎\n *\n * 灵感来自 TencentDB Agent Memory 的多级压缩策略:\n * - score-cascade (级联压缩)\n * - aggressive compress (激进压缩)\n * - emergency compress (紧急压缩)\n *\n * 将大量记忆条目压缩到 token 预算内,按以下四级策略逐级升级:\n *\n * 1. none — 总 token < cascade 阈值 → 不压缩\n * 2. cascade — cascade ≤ token < aggressive → 按最低 score 逐条卸载\n * 3. aggressive— aggressive ≤ token < budget → 按最低 importance×strength 批量卸载\n * 4. emergency — token ≥ budget → 只保留 top-N 最高分条目,其余浓缩为摘要\n *\n * compress() 是纯函数式的,不修改输入数组。\n */\n\nimport { estimateTokens } from \"./token-counter.js\";\n\n// ===== 数据类型 =====\n\n/** 待压缩的记忆条目 */\nexport interface OffloadEntry {\n /** 记忆 ID */\n id: string;\n /** 记忆内容文本 */\n content: string;\n /** 相关性分数 (0-1) */\n score: number;\n /** 记忆层级 L0-L3 */\n layer: number;\n /** 重要性 (0-1) */\n importance: number;\n /** 强度 (0-1) */\n strength: number;\n /** 来源标识 */\n source: string;\n}\n\n/** 压缩策略类型 */\nexport type OffloadStrategy = \"none\" | \"cascade\" | \"aggressive\" | \"emergency\";\n\n/** 压缩结果 */\nexport interface OffloadResult {\n /** 压缩后保留的条目 */\n retained: OffloadEntry[];\n /** 被卸载的条目(ID 列表) */\n offloaded: string[];\n /** 压缩前的 token 数 */\n tokensBefore: number;\n /** 压缩后的 token 数 */\n tokensAfter: number;\n /** 压缩策略 */\n strategy: OffloadStrategy;\n /** 压缩摘要(被卸载条目的浓缩),无卸载时为 null */\n summary: string | null;\n}\n\n/** 引擎配置选项 */\nexport interface OffloadEngineOptions {\n /** token 预算上限(默认 2000) */\n tokenBudget: number;\n /** 级联压缩触发阈值,相对 budget 的比例(默认 0.8) */\n cascadeThreshold: number;\n /** 激进压缩触发阈值,相对 budget 的比例(默认 0.95) */\n aggressiveThreshold: number;\n /** 紧急压缩最少保留条目数(默认 3) */\n emergencyMinKeep: number;\n}\n\n/** 默认配置 */\nconst DEFAULT_OPTIONS: OffloadEngineOptions = {\n tokenBudget: 2000,\n cascadeThreshold: 0.8,\n aggressiveThreshold: 0.95,\n emergencyMinKeep: 3,\n};\n\n/**\n * Context Offload 压缩引擎\n */\nexport class OffloadEngine {\n private opts: OffloadEngineOptions;\n\n constructor(opts?: Partial<OffloadEngineOptions>) {\n this.opts = { ...DEFAULT_OPTIONS, ...opts };\n }\n\n /**\n * 压缩记忆列表到 token 预算内\n *\n * 策略(逐级升级):\n * 1. 正常: 总 token < cascade 阈值 → 不压缩\n * 2. 级联: cascade ≤ 总 token < aggressive → 按最低 score 逐条卸载\n * 3. 激进: aggressive ≤ 总 token < budget → 按最低 importance×strength 批量卸载\n * 4. 紧急: 总 token ≥ budget → 只保留 emergencyMinKeep 条最高分条目,其余浓缩为摘要\n *\n * @param entries 待压缩的记忆条目列表(不会被修改)\n * @returns 压缩结果\n */\n compress(entries: OffloadEntry[]): OffloadResult {\n const tokensBefore = entries.reduce(\n (sum, e) => sum + estimateTokens(e.content),\n 0\n );\n const cascade = this.opts.tokenBudget * this.opts.cascadeThreshold;\n const aggressive = this.opts.tokenBudget * this.opts.aggressiveThreshold;\n\n // 1. 正常 — 无需压缩\n if (tokensBefore <= cascade) {\n return {\n retained: entries,\n offloaded: [],\n tokensBefore,\n tokensAfter: tokensBefore,\n strategy: \"none\",\n summary: null,\n };\n }\n\n // 2. 级联压缩 — 按 score 从低到高逐条卸载\n if (tokensBefore <= aggressive) {\n return this.cascadeCompress(entries, cascade, tokensBefore);\n }\n\n // 3. 激进压缩 — 按 importance×strength 批量卸载\n if (tokensBefore <= this.opts.tokenBudget) {\n return this.aggressiveCompress(entries, cascade, tokensBefore);\n }\n\n // 4. 紧急压缩 — 只保留 top-N,其余浓缩\n return this.emergencyCompress(entries, tokensBefore);\n }\n\n /**\n * 级联压缩: 按 score 升序排列,从最低分开始逐条卸载,直到降到目标 token 数\n */\n private cascadeCompress(\n entries: OffloadEntry[],\n targetTokens: number,\n tokensBefore: number\n ): OffloadResult {\n // 按 score 升序排列,从最低分开始卸载\n const sorted = [...entries].sort((a, b) => a.score - b.score);\n const offloaded: string[] = [];\n let currentTokens = tokensBefore;\n\n for (const entry of sorted) {\n if (currentTokens <= targetTokens) break;\n offloaded.push(entry.id);\n currentTokens -= estimateTokens(entry.content);\n }\n\n const offloadedSet = new Set(offloaded);\n const retained = entries.filter((e) => !offloadedSet.has(e.id));\n return {\n retained,\n offloaded,\n tokensBefore,\n tokensAfter: currentTokens,\n strategy: \"cascade\",\n summary: null,\n };\n }\n\n /**\n * 激进压缩: 按 importance × strength 升序批量卸载,直到降到目标 token 数;\n * 对被卸载条目生成简短摘要。\n */\n private aggressiveCompress(\n entries: OffloadEntry[],\n targetTokens: number,\n tokensBefore: number\n ): OffloadResult {\n // 按 importance × strength 升序排列\n const sorted = [...entries].sort(\n (a, b) => a.importance * a.strength - b.importance * b.strength\n );\n const offloaded: string[] = [];\n let currentTokens = tokensBefore;\n\n for (const entry of sorted) {\n if (currentTokens <= targetTokens) break;\n offloaded.push(entry.id);\n currentTokens -= estimateTokens(entry.content);\n }\n\n const offloadedSet = new Set(offloaded);\n const retained = entries.filter((e) => !offloadedSet.has(e.id));\n // 生成简短摘要\n const offloadedEntries = entries.filter((e) => offloadedSet.has(e.id));\n const summary = this.generateSummary(offloadedEntries);\n return {\n retained,\n offloaded,\n tokensBefore,\n tokensAfter: currentTokens,\n strategy: \"aggressive\",\n summary,\n };\n }\n\n /**\n * 紧急压缩: 按 score 降序,只保留 top emergencyMinKeep 条;\n * 其余条目浓缩为摘要。保证最少保留 emergencyMinKeep 条。\n */\n private emergencyCompress(\n entries: OffloadEntry[],\n tokensBefore: number\n ): OffloadResult {\n // 按 score 降序,只保留 top emergencyMinKeep\n const sorted = [...entries].sort((a, b) => b.score - a.score);\n const keepCount = Math.min(this.opts.emergencyMinKeep, sorted.length);\n const retained = sorted.slice(0, keepCount);\n const retainedIds = new Set(retained.map((e) => e.id));\n const offloadedEntries = entries.filter((e) => !retainedIds.has(e.id));\n const offloaded = offloadedEntries.map((e) => e.id);\n const tokensAfter = retained.reduce(\n (sum, e) => sum + estimateTokens(e.content),\n 0\n );\n const summary = this.generateSummary(offloadedEntries);\n return {\n retained,\n offloaded,\n tokensBefore,\n tokensAfter,\n strategy: \"emergency\",\n summary,\n };\n }\n\n /**\n * 把被卸载的条目浓缩为一句话摘要\n *\n * @param entries 被卸载的条目\n * @returns 浓缩摘要字符串(无条目时返回空字符串)\n */\n private generateSummary(entries: OffloadEntry[]): string {\n if (!entries || entries.length === 0) return \"\";\n const labels = entries.map((e) => e.content.substring(0, 30));\n return `[已压缩 ${entries.length} 条低优先记忆: ${labels.join(\"; \")}...]`;\n }\n\n /** 获取引擎当前配置(返回副本,避免外部篡改) */\n getOptions(): OffloadEngineOptions {\n return { ...this.opts };\n }\n}\n"],"mappings":"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","names":[]}
|
package/dist/chunk-SSDURI3I.js
DELETED
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
// src/llm-lock.ts
|
|
4
|
-
var _chain = Promise.resolve();
|
|
5
|
-
function withLlmLock(fn) {
|
|
6
|
-
const run = _chain.then(fn, fn);
|
|
7
|
-
_chain = run.then(() => void 0, () => void 0);
|
|
8
|
-
return run;
|
|
9
|
-
}
|
|
10
|
-
async function fetchWithRetry(url, options, maxRetries = 3) {
|
|
11
|
-
let lastError = null;
|
|
12
|
-
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
13
|
-
try {
|
|
14
|
-
const response = await fetch(url, options);
|
|
15
|
-
if (response.status === 429 || response.status >= 500) {
|
|
16
|
-
if (attempt < maxRetries) {
|
|
17
|
-
const delay = Math.pow(2, attempt + 1) * 1e3;
|
|
18
|
-
console.warn(
|
|
19
|
-
`[llm-lock] ${response.status} \u91CD\u8BD5 ${attempt + 1}/${maxRetries}\uFF0C\u7B49\u5F85 ${delay}ms`
|
|
20
|
-
);
|
|
21
|
-
await new Promise((r) => setTimeout(r, delay));
|
|
22
|
-
continue;
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
return response;
|
|
26
|
-
} catch (err) {
|
|
27
|
-
lastError = err;
|
|
28
|
-
if (attempt < maxRetries) {
|
|
29
|
-
const delay = Math.pow(2, attempt + 1) * 1e3;
|
|
30
|
-
console.warn(
|
|
31
|
-
`[llm-lock] \u7F51\u7EDC\u9519\u8BEF\u91CD\u8BD5 ${attempt + 1}/${maxRetries}\uFF0C\u7B49\u5F85 ${delay}ms:`,
|
|
32
|
-
err.message
|
|
33
|
-
);
|
|
34
|
-
await new Promise((r) => setTimeout(r, delay));
|
|
35
|
-
continue;
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
throw lastError ?? new Error("fetchWithRetry exhausted");
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export {
|
|
43
|
-
withLlmLock,
|
|
44
|
-
fetchWithRetry
|
|
45
|
-
};
|
|
46
|
-
//# sourceMappingURL=chunk-SSDURI3I.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/llm-lock.ts"],"sourcesContent":["/**\n * LLM 并发控制器 — 确保同时只有一个 LLM API 请求在执行\n *\n * GLM-4.7-Flash 免费模型并发数为 1,多个模块共享此锁。\n */\n\n// 全局二值信号量:同一进程内所有 LLM 调用串行\nlet _chain: Promise<any> = Promise.resolve();\n\n/**\n * 用信号量包装一个异步 LLM 调用,确保串行执行\n */\nexport function withLlmLock<T>(fn: () => Promise<T>): Promise<T> {\n const run = _chain.then(fn, fn); // 无论前一个成功或失败都继续\n // 链尾推进,但不让后续 catch 影响调用者\n _chain = run.then(() => undefined, () => undefined);\n return run;\n}\n\n/**\n * 带重试的 fetch(用于 429 限流和 5xx 临时错误)\n */\nexport async function fetchWithRetry(\n url: string,\n options: RequestInit,\n maxRetries = 3\n): Promise<Response> {\n let lastError: Error | null = null;\n\n for (let attempt = 0; attempt <= maxRetries; attempt++) {\n try {\n const response = await fetch(url, options);\n\n // 429 或 5xx → 重试\n if (response.status === 429 || response.status >= 500) {\n if (attempt < maxRetries) {\n // 指数退避:2s, 4s, 8s\n const delay = Math.pow(2, attempt + 1) * 1000;\n console.warn(\n `[llm-lock] ${response.status} 重试 ${attempt + 1}/${maxRetries},等待 ${delay}ms`\n );\n await new Promise((r) => setTimeout(r, delay));\n continue;\n }\n }\n\n return response;\n } catch (err) {\n lastError = err as Error;\n if (attempt < maxRetries) {\n const delay = Math.pow(2, attempt + 1) * 1000;\n console.warn(\n `[llm-lock] 网络错误重试 ${attempt + 1}/${maxRetries},等待 ${delay}ms:`,\n (err as Error).message\n );\n await new Promise((r) => setTimeout(r, delay));\n continue;\n }\n }\n }\n\n throw lastError ?? new Error(\"fetchWithRetry exhausted\");\n}\n"],"mappings":";;;AAOA,IAAI,SAAuB,QAAQ,QAAQ;AAKpC,SAAS,YAAe,IAAkC;AAC/D,QAAM,MAAM,OAAO,KAAK,IAAI,EAAE;AAE9B,WAAS,IAAI,KAAK,MAAM,QAAW,MAAM,MAAS;AAClD,SAAO;AACT;AAKA,eAAsB,eACpB,KACA,SACA,aAAa,GACM;AACnB,MAAI,YAA0B;AAE9B,WAAS,UAAU,GAAG,WAAW,YAAY,WAAW;AACtD,QAAI;AACF,YAAM,WAAW,MAAM,MAAM,KAAK,OAAO;AAGzC,UAAI,SAAS,WAAW,OAAO,SAAS,UAAU,KAAK;AACrD,YAAI,UAAU,YAAY;AAExB,gBAAM,QAAQ,KAAK,IAAI,GAAG,UAAU,CAAC,IAAI;AACzC,kBAAQ;AAAA,YACN,cAAc,SAAS,MAAM,iBAAO,UAAU,CAAC,IAAI,UAAU,sBAAO,KAAK;AAAA,UAC3E;AACA,gBAAM,IAAI,QAAQ,CAAC,MAAM,WAAW,GAAG,KAAK,CAAC;AAC7C;AAAA,QACF;AAAA,MACF;AAEA,aAAO;AAAA,IACT,SAAS,KAAK;AACZ,kBAAY;AACZ,UAAI,UAAU,YAAY;AACxB,cAAM,QAAQ,KAAK,IAAI,GAAG,UAAU,CAAC,IAAI;AACzC,gBAAQ;AAAA,UACN,mDAAqB,UAAU,CAAC,IAAI,UAAU,sBAAO,KAAK;AAAA,UACzD,IAAc;AAAA,QACjB;AACA,cAAM,IAAI,QAAQ,CAAC,MAAM,WAAW,GAAG,KAAK,CAAC;AAC7C;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,QAAM,aAAa,IAAI,MAAM,0BAA0B;AACzD;","names":[]}
|