@cup319/mmpl 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,151 @@
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
@@ -0,0 +1 @@
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":[]}
@@ -0,0 +1,46 @@
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
@@ -0,0 +1 @@
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":[]}