recallnest 1.4.1 → 2.2.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/.dockerignore +11 -0
- package/CLAUDE.md +7 -3
- package/Dockerfile +20 -0
- package/README.md +202 -130
- package/README_CN.md +263 -190
- package/ROADMAP.md +128 -81
- package/SKILL.md +250 -0
- package/assets/dashboard.png +0 -0
- package/assets/knowledge-graph.png +0 -0
- package/assets/screenshots/ui-full.png +0 -0
- package/assets/screenshots/ui-overview.png +0 -0
- package/assets/screenshots/ui-search.png +0 -0
- package/assets/ui/app.js +227 -3
- package/assets/ui/index.html +81 -11
- package/assets/ui/styles.css +178 -2
- package/bin/recallnest.mjs +62 -0
- package/bun.lock +337 -0
- package/daily-recall.sh +23 -0
- package/daily-reflections/2026-04-02.md +35 -0
- package/daily-reflections/2026-04-03.md +37 -0
- package/daily-reflections/2026-04-04.md +37 -0
- package/daily-reflections/2026-04-05.md +35 -0
- package/daily-reflections/2026-04-06.md +36 -0
- package/daily-reflections/2026-04-07.md +35 -0
- package/daily-reflections/2026-04-08.md +35 -0
- package/daily-reflections/2026-04-12.md +35 -0
- package/data/alias-map.example.json +22 -0
- package/docs/plans/2026-04-01-recall-boost.md +100 -0
- package/docs/superpowers/plans/2026-04-11-constructive-retrieval-emotion.md +1195 -0
- package/docs/superpowers/specs/2026-04-11-constructive-retrieval-emotion-design.md +398 -0
- package/glama.json +6 -0
- package/integrations/setup-all.sh +31 -0
- package/package.json +10 -3
- package/plan-philosophy-upgrades.md +363 -0
- package/plan-product-upgrades.md +155 -0
- package/plan.md +140 -0
- package/research-memory-evolution.md +440 -0
- package/research-rl-agent-memory.md +330 -0
- package/scripts/backfill-anchors.ts +130 -0
- package/scripts/diagnose-vector-search.ts +132 -0
- package/scripts/migrate-language.ts +75 -0
- package/scripts/verify-emotion-decay.ts +148 -0
- package/src/__tests__/activity-counter.test.ts +96 -0
- package/src/__tests__/admission-control.test.ts +206 -0
- package/src/__tests__/anchor-generator.test.ts +67 -0
- package/src/__tests__/babel-integration.test.ts +139 -0
- package/src/__tests__/capture-engine.test.ts +5 -3
- package/src/__tests__/cascade-forget.test.ts +1 -1
- package/src/__tests__/cc8-cc1-essential-context.test.ts +298 -0
- package/src/__tests__/circuit-breaker.test.ts +99 -0
- package/src/__tests__/cluster-consolidation.test.ts +712 -0
- package/src/__tests__/confidence-metadata.test.ts +125 -0
- package/src/__tests__/confidence-tracker.test.ts +53 -0
- package/src/__tests__/consolidation-engine.test.ts +1 -1
- package/src/__tests__/constructive-retrieval.test.ts +509 -0
- package/src/__tests__/context-collapse-renderer.test.ts +214 -0
- package/src/__tests__/context-reconstructor.test.ts +65 -0
- package/src/__tests__/conversation-importer.test.ts +692 -0
- package/src/__tests__/data-checkup.test.ts +225 -0
- package/src/__tests__/decay-exemption.test.ts +84 -0
- package/src/__tests__/dedup-guard.test.ts +39 -0
- package/src/__tests__/distill-lock.test.ts +176 -0
- package/src/__tests__/dream-pipeline.test.ts +232 -0
- package/src/__tests__/emotion-decay.test.ts +46 -0
- package/src/__tests__/emotion-detector.test.ts +56 -0
- package/src/__tests__/emotion-phase1-integration.test.ts +153 -0
- package/src/__tests__/emotion-retrieval.test.ts +56 -0
- package/src/__tests__/ethics-layer.test.ts +476 -0
- package/src/__tests__/feature-flags-integration.test.ts +54 -0
- package/src/__tests__/frequency-tracker.test.ts +113 -0
- package/src/__tests__/graph-export.test.ts +489 -0
- package/src/__tests__/interference-detector.test.ts +203 -0
- package/src/__tests__/mcp-efficiency.test.ts +288 -0
- package/src/__tests__/memory-evolution-b2-b3-e1.test.ts +243 -0
- package/src/__tests__/memory-evolution.test.ts +271 -0
- package/src/__tests__/memory-lint.test.ts +271 -0
- package/src/__tests__/narrative-architecture.test.ts +456 -0
- package/src/__tests__/pending-review.test.ts +77 -0
- package/src/__tests__/post-retrieval-filter.test.ts +178 -0
- package/src/__tests__/prediction-engine.test.ts +349 -0
- package/src/__tests__/predictive-prospective-memory.test.ts +416 -0
- package/src/__tests__/preference-slots.test.ts +99 -0
- package/src/__tests__/query-expander-alias.test.ts +89 -0
- package/src/__tests__/recall-governor.test.ts +227 -0
- package/src/__tests__/resume-context.test.ts +156 -1
- package/src/__tests__/salience-scoring.test.ts +106 -0
- package/src/__tests__/session-distiller.test.ts +611 -0
- package/src/__tests__/skill-engine.test.ts +546 -0
- package/src/__tests__/skill-promotion.test.ts +349 -0
- package/src/__tests__/supersede-chain.test.ts +154 -0
- package/src/__tests__/temporal-validity.test.ts +160 -0
- package/src/__tests__/topic-tag.test.ts +113 -0
- package/src/__tests__/trusted-memory.test.ts +828 -0
- package/src/__tests__/write-verifier.test.ts +122 -0
- package/src/access-tracker.ts +3 -2
- package/src/activity-counter.ts +109 -0
- package/src/adaptive-retrieval.ts +6 -5
- package/src/admission-control.ts +163 -0
- package/src/anchor-generator.ts +80 -0
- package/src/api-server.ts +10 -0
- package/src/audit-log.ts +122 -0
- package/src/auto-gc.ts +98 -29
- package/src/auto-recall.ts +33 -4
- package/src/capture-engine.ts +372 -23
- package/src/cascade-forget.ts +3 -2
- package/src/cli.ts +107 -5
- package/src/compat.ts +22 -0
- package/src/confidence-tracker.ts +210 -47
- package/src/conflict-store.ts +12 -3
- package/src/consolidation-engine.ts +296 -8
- package/src/context-collapse-renderer.ts +222 -0
- package/src/context-composer-stable-selection.ts +5 -1
- package/src/context-composer.ts +239 -4
- package/src/context-reconstructor.ts +404 -0
- package/src/conversation-importer.ts +441 -0
- package/src/data-checkup.ts +232 -0
- package/src/decay-engine.ts +84 -0
- package/src/distill-lock.ts +122 -0
- package/src/doctor.ts +8 -10
- package/src/dream-pipeline.ts +227 -0
- package/src/emotion-detector.ts +90 -0
- package/src/eval.ts +3 -2
- package/src/forget-engine.ts +269 -0
- package/src/frequency-tracker.ts +175 -0
- package/src/graph-export.ts +644 -0
- package/src/ingest.ts +36 -6
- package/src/interference-detector.ts +187 -0
- package/src/kg-extractor.ts +15 -24
- package/src/kg-store.ts +2 -1
- package/src/language-hook.ts +67 -0
- package/src/llm-client.ts +289 -6
- package/src/mcp-server.ts +707 -80
- package/src/memory-assets.ts +22 -5
- package/src/memory-evolution.ts +365 -0
- package/src/memory-lint.ts +429 -0
- package/src/memory-output.ts +69 -0
- package/src/memory-schema.ts +74 -2
- package/src/narrative-schema.ts +79 -0
- package/src/narrative-tagger.ts +164 -0
- package/src/pii-detector.ts +98 -0
- package/src/post-retrieval-filter.ts +76 -0
- package/src/prediction-engine.ts +253 -0
- package/src/preference-slots.ts +65 -2
- package/src/prospective-memory.ts +215 -0
- package/src/query-expander.ts +67 -0
- package/src/recall-governor.ts +183 -0
- package/src/retention-policy.ts +90 -0
- package/src/retriever.ts +828 -55
- package/src/rif.ts +34 -8
- package/src/runtime-config.ts +23 -8
- package/src/scope-inventory.ts +4 -3
- package/src/scope-policy.ts +4 -0
- package/src/session-distiller.ts +307 -0
- package/src/session-output.ts +40 -0
- package/src/session-schema.ts +37 -0
- package/src/session-store.ts +7 -2
- package/src/skill-engine.ts +208 -0
- package/src/skill-promotion.ts +293 -0
- package/src/skill-schema.ts +39 -0
- package/src/store.ts +96 -10
- package/src/topic-tag.ts +66 -0
- package/src/tracker.ts +3 -1
- package/src/ui-server.ts +80 -8
- package/src/workflow-observation-scope-review.ts +2 -1
- package/src/workflow-observation-store.ts +2 -1
- package/src/write-verifier.ts +126 -0
package/src/llm-client.ts
CHANGED
|
@@ -14,6 +14,89 @@
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
import OpenAI from "openai";
|
|
17
|
+
import { logInfo } from "./stderr-log.js";
|
|
18
|
+
import { isEmotionScoringEnabled } from "./memory-schema.js";
|
|
19
|
+
|
|
20
|
+
// ============================================================================
|
|
21
|
+
// LME-9: Circuit Breaker — LLM 降级策略
|
|
22
|
+
// ============================================================================
|
|
23
|
+
|
|
24
|
+
type CircuitState = "closed" | "open" | "half-open";
|
|
25
|
+
|
|
26
|
+
export interface CircuitBreakerConfig {
|
|
27
|
+
/** Consecutive failures before opening the circuit (default: 3) */
|
|
28
|
+
failureThreshold: number;
|
|
29
|
+
/** Cooldown in ms before trying again (default: 30_000 = 30s) */
|
|
30
|
+
cooldownMs: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const DEFAULT_BREAKER_CONFIG: CircuitBreakerConfig = {
|
|
34
|
+
failureThreshold: 3,
|
|
35
|
+
cooldownMs: 30_000,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export class CircuitBreaker {
|
|
39
|
+
private state: CircuitState = "closed";
|
|
40
|
+
private consecutiveFailures = 0;
|
|
41
|
+
private openedAt = 0;
|
|
42
|
+
private readonly config: CircuitBreakerConfig;
|
|
43
|
+
|
|
44
|
+
constructor(config?: Partial<CircuitBreakerConfig>) {
|
|
45
|
+
this.config = { ...DEFAULT_BREAKER_CONFIG, ...config };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Check if a call should be allowed through. */
|
|
49
|
+
canAttempt(): boolean {
|
|
50
|
+
if (this.state === "closed") return true;
|
|
51
|
+
if (this.state === "open") {
|
|
52
|
+
// Check if cooldown has elapsed → transition to half-open
|
|
53
|
+
if (Date.now() - this.openedAt >= this.config.cooldownMs) {
|
|
54
|
+
this.state = "half-open";
|
|
55
|
+
logInfo("[circuit-breaker] transitioning to half-open, allowing probe");
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
// half-open: allow one probe
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Record a successful call. */
|
|
65
|
+
recordSuccess(): void {
|
|
66
|
+
if (this.state !== "closed") {
|
|
67
|
+
logInfo("[circuit-breaker] probe succeeded, circuit closed");
|
|
68
|
+
}
|
|
69
|
+
this.consecutiveFailures = 0;
|
|
70
|
+
this.state = "closed";
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Record a failed call. */
|
|
74
|
+
recordFailure(): void {
|
|
75
|
+
this.consecutiveFailures++;
|
|
76
|
+
if (this.state === "half-open") {
|
|
77
|
+
// Probe failed → reopen
|
|
78
|
+
this.state = "open";
|
|
79
|
+
this.openedAt = Date.now();
|
|
80
|
+
logInfo("[circuit-breaker] probe failed, circuit re-opened");
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (this.consecutiveFailures >= this.config.failureThreshold) {
|
|
84
|
+
this.state = "open";
|
|
85
|
+
this.openedAt = Date.now();
|
|
86
|
+
logInfo(`[circuit-breaker] ${this.consecutiveFailures} consecutive failures, circuit opened`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Current circuit state (for testing/diagnostics). */
|
|
91
|
+
getState(): CircuitState {
|
|
92
|
+
return this.state;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Current failure count (for testing/diagnostics). */
|
|
96
|
+
getFailureCount(): number {
|
|
97
|
+
return this.consecutiveFailures;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
17
100
|
|
|
18
101
|
// ============================================================================
|
|
19
102
|
// Types
|
|
@@ -53,6 +136,12 @@ export interface SmartExtraction {
|
|
|
53
136
|
l1: string;
|
|
54
137
|
/** Importance score 0-1 (LLM's estimate) */
|
|
55
138
|
importance: number;
|
|
139
|
+
/** Optional emotional tone (only populated when feature flag is on) */
|
|
140
|
+
emotion?: {
|
|
141
|
+
valence: number;
|
|
142
|
+
arousal: number;
|
|
143
|
+
label?: string;
|
|
144
|
+
};
|
|
56
145
|
}
|
|
57
146
|
|
|
58
147
|
/** Category-specific merge strategies */
|
|
@@ -96,8 +185,10 @@ export class LLMClient {
|
|
|
96
185
|
private model: string;
|
|
97
186
|
private temperature: number;
|
|
98
187
|
private timeoutMs: number;
|
|
188
|
+
/** LME-9: Circuit breaker for graceful LLM degradation */
|
|
189
|
+
readonly breaker: CircuitBreaker;
|
|
99
190
|
|
|
100
|
-
constructor(config: LLMConfig) {
|
|
191
|
+
constructor(config: LLMConfig, breakerConfig?: Partial<CircuitBreakerConfig>) {
|
|
101
192
|
const apiKey = resolveEnvVars(config.apiKey);
|
|
102
193
|
this.client = new OpenAI({
|
|
103
194
|
apiKey,
|
|
@@ -106,6 +197,7 @@ export class LLMClient {
|
|
|
106
197
|
this.model = config.model;
|
|
107
198
|
this.temperature = config.temperature ?? 0.1;
|
|
108
199
|
this.timeoutMs = config.timeoutMs ?? 15000;
|
|
200
|
+
this.breaker = new CircuitBreaker(breakerConfig);
|
|
109
201
|
}
|
|
110
202
|
|
|
111
203
|
/**
|
|
@@ -116,7 +208,9 @@ export class LLMClient {
|
|
|
116
208
|
try {
|
|
117
209
|
const response = await this.chat(
|
|
118
210
|
"你是记忆索引助手。给以下对话片段写一句话摘要(不超过80字)," +
|
|
119
|
-
"
|
|
211
|
+
"用于快速检索。只输出摘要本身,不加任何前缀。\n" +
|
|
212
|
+
"保真规则:端口号/IP/URL/文件路径/API名称 → 原样保留;" +
|
|
213
|
+
"函数名/事件名/配置项 → 逐项保留不概括。",
|
|
120
214
|
text.slice(0, 2000), // Cap input to avoid token overflow
|
|
121
215
|
);
|
|
122
216
|
if (!response || response.length < 5) return null;
|
|
@@ -144,7 +238,7 @@ export class LLMClient {
|
|
|
144
238
|
const validCategories: SmartCategory[] = ["profile", "preferences", "entities", "events", "cases", "patterns"];
|
|
145
239
|
if (!validCategories.includes(parsed.category)) return null;
|
|
146
240
|
|
|
147
|
-
|
|
241
|
+
const result: SmartExtraction = {
|
|
148
242
|
category: parsed.category,
|
|
149
243
|
l0: (parsed.l0 || "").slice(0, 150),
|
|
150
244
|
l1: (parsed.l1 || "").slice(0, 500),
|
|
@@ -152,6 +246,16 @@ export class LLMClient {
|
|
|
152
246
|
? Math.max(0, Math.min(1, parsed.importance))
|
|
153
247
|
: CATEGORY_DEFAULT_IMPORTANCE[parsed.category],
|
|
154
248
|
};
|
|
249
|
+
|
|
250
|
+
if (isEmotionScoringEnabled() && parsed.emotion) {
|
|
251
|
+
result.emotion = {
|
|
252
|
+
valence: Math.max(-1, Math.min(1, parsed.emotion.valence ?? 0)),
|
|
253
|
+
arousal: Math.max(0, Math.min(1, parsed.emotion.arousal ?? 0)),
|
|
254
|
+
label: parsed.emotion.label,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return result;
|
|
155
259
|
} catch {
|
|
156
260
|
return null;
|
|
157
261
|
}
|
|
@@ -255,6 +359,87 @@ export class LLMClient {
|
|
|
255
359
|
return results;
|
|
256
360
|
}
|
|
257
361
|
|
|
362
|
+
/**
|
|
363
|
+
* B-3: Assess the long-term importance of a memory (0–1).
|
|
364
|
+
* Called when store_memory importance is at the default value, to let LLM refine it.
|
|
365
|
+
* Returns null on failure (caller should keep the default).
|
|
366
|
+
*/
|
|
367
|
+
async assessImportance(text: string, category: string): Promise<number | null> {
|
|
368
|
+
try {
|
|
369
|
+
const response = await this.chat(
|
|
370
|
+
"你是记忆重要性评估助手。评估以下记忆的长期重要性(0.0-1.0)。\n\n" +
|
|
371
|
+
"锚定标准(每级附具体示例,严格对标):\n" +
|
|
372
|
+
"- 0.85-1.0:用户明确纠正行为(如'不要这样做')、身份声明(职业/角色)、" +
|
|
373
|
+
"情感/行为变化、核心偏好(写作风格/沟通方式)、反复强调的规则\n" +
|
|
374
|
+
"- 0.70-0.84:关键决策(架构选型/技术路线)、项目承诺(deadline/里程碑)、" +
|
|
375
|
+
"可复用案例(踩坑→修复→教训)、持久偏好(喜欢X不喜欢Y)\n" +
|
|
376
|
+
"- 0.55-0.69:实体信息(项目名/工具名/人名)、新学到的背景知识、" +
|
|
377
|
+
"配置参数(API key位置/环境变量)\n" +
|
|
378
|
+
"- 0.40-0.54:普通事件(今天部署了X)、一次性操作步骤、" +
|
|
379
|
+
"已解决的 bug 细节(修完即过时)\n" +
|
|
380
|
+
"- 0.10-0.30:日常寒暄、重复信息(已有更完整版本)、" +
|
|
381
|
+
"临时调试输出、短期上下文(如'刚才那个文件')\n\n" +
|
|
382
|
+
"常见误判提醒:\n" +
|
|
383
|
+
"- 用户说'记住'/'永远不要' → 至少 0.85\n" +
|
|
384
|
+
"- 看起来是小事但反复出现 → 至少 0.70(重复=重要)\n" +
|
|
385
|
+
"- 纯技术细节但没有复用价值 → 不超过 0.50\n\n" +
|
|
386
|
+
"只输出一行 JSON:{\"importance\":0.7,\"reason\":\"简短原因\"}",
|
|
387
|
+
`[类别] ${category}\n[内容] ${text.slice(0, 1000)}`,
|
|
388
|
+
);
|
|
389
|
+
if (!response) return null;
|
|
390
|
+
const parsed = parseJSON<{ importance: number; reason?: string }>(response);
|
|
391
|
+
if (parsed && typeof parsed.importance === "number") {
|
|
392
|
+
return Math.max(0, Math.min(1, parsed.importance));
|
|
393
|
+
}
|
|
394
|
+
return null;
|
|
395
|
+
} catch {
|
|
396
|
+
return null;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* HP-4: Batch re-assess importance for existing memories during distill.
|
|
402
|
+
* Returns a map of id → new importance (only for entries that changed).
|
|
403
|
+
*/
|
|
404
|
+
async reassessImportanceBatch(
|
|
405
|
+
entries: Array<{ id: string; text: string; category: string; currentImportance: number }>,
|
|
406
|
+
): Promise<Map<string, number>> {
|
|
407
|
+
const results = new Map<string, number>();
|
|
408
|
+
if (entries.length === 0) return results;
|
|
409
|
+
|
|
410
|
+
const batch = entries
|
|
411
|
+
.map((e, i) => `[${i}] category=${e.category} importance=${e.currentImportance}\n${e.text.slice(0, 300)}`)
|
|
412
|
+
.join("\n---\n");
|
|
413
|
+
|
|
414
|
+
try {
|
|
415
|
+
const response = await this.chat(
|
|
416
|
+
"批量重评记忆重要性。使用同一套锚定标准(0.85+ 身份/纠正/核心偏好," +
|
|
417
|
+
"0.70-0.84 决策/案例/偏好,0.55-0.69 实体/背景,0.40-0.54 事件/操作," +
|
|
418
|
+
"0.10-0.30 寒暄/重复/临时)。\n" +
|
|
419
|
+
"只输出 JSON 数组,每项 {\"idx\":0,\"importance\":0.7}," +
|
|
420
|
+
"只包含需要调整的项(与当前分数差 ≥ 0.1 才输出)。",
|
|
421
|
+
batch,
|
|
422
|
+
);
|
|
423
|
+
if (!response) return results;
|
|
424
|
+
const parsed = parseJSON<Array<{ idx: number; importance: number }>>(response);
|
|
425
|
+
if (Array.isArray(parsed)) {
|
|
426
|
+
for (const item of parsed) {
|
|
427
|
+
if (
|
|
428
|
+
typeof item.idx === "number" &&
|
|
429
|
+
typeof item.importance === "number" &&
|
|
430
|
+
item.idx >= 0 &&
|
|
431
|
+
item.idx < entries.length
|
|
432
|
+
) {
|
|
433
|
+
results.set(entries[item.idx].id, Math.max(0, Math.min(1, item.importance)));
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
} catch {
|
|
438
|
+
// Batch re-assessment failure is non-fatal
|
|
439
|
+
}
|
|
440
|
+
return results;
|
|
441
|
+
}
|
|
442
|
+
|
|
258
443
|
/**
|
|
259
444
|
* Semantic dedup decision: given a new chunk and an existing similar chunk,
|
|
260
445
|
* decide whether to CREATE (store new), MERGE (combine), or SKIP (discard).
|
|
@@ -363,7 +548,10 @@ export class LLMClient {
|
|
|
363
548
|
try {
|
|
364
549
|
return await this.chat(
|
|
365
550
|
"你是记忆精炼助手。把以下内容提炼为一段核心摘要(不超过200字)。\n" +
|
|
366
|
-
"
|
|
551
|
+
"要求:保留关键事实、人名、数字、时间等;去除冗余修辞;直接输出摘要,不加前缀。\n" +
|
|
552
|
+
"保真规则:端口号/IP/URL/文件路径 → 原样保留;" +
|
|
553
|
+
"命名列表(函数名/事件名/配置项)→ 逐项保留不概括;" +
|
|
554
|
+
"规则表/状态转移 → 完整保留;对比/排序 → 保留顺序和理由。",
|
|
367
555
|
text.slice(0, 2000),
|
|
368
556
|
);
|
|
369
557
|
} catch {
|
|
@@ -454,6 +642,33 @@ export class LLMClient {
|
|
|
454
642
|
}
|
|
455
643
|
}
|
|
456
644
|
|
|
645
|
+
/**
|
|
646
|
+
* HP-5: Extract cross-memory pattern from a cluster of related memories.
|
|
647
|
+
* Asks LLM whether the cluster reveals an implicit recurring theme,
|
|
648
|
+
* preference, or behavior not stated in any single memory.
|
|
649
|
+
*
|
|
650
|
+
* @returns Pattern text if found, null if no pattern or LLM failure.
|
|
651
|
+
*/
|
|
652
|
+
async extractPattern(clusterTexts: string[]): Promise<string | null> {
|
|
653
|
+
if (clusterTexts.length < 3) return null;
|
|
654
|
+
|
|
655
|
+
const numbered = clusterTexts.map((t, i) => `[${i + 1}] ${t}`).join("\n");
|
|
656
|
+
const result = await this.chatJson<{ hasPattern: boolean; pattern?: string }>(
|
|
657
|
+
"你是记忆模式发现助手。分析以下一组相关记忆,判断它们是否暗示了某个跨条目的隐含模式——" +
|
|
658
|
+
"例如反复出现的偏好、行为倾向、价值观或循环主题。\n\n" +
|
|
659
|
+
"规则:\n" +
|
|
660
|
+
"- 只发现真正跨条目的模式,而非单条记忆已经明确表达的内容\n" +
|
|
661
|
+
"- 模式必须有至少 2 条记忆作为证据支撑\n" +
|
|
662
|
+
"- 如果没有发现有意义的模式,返回 hasPattern: false\n" +
|
|
663
|
+
"- 模式描述不超过 150 字,简洁且可操作\n" +
|
|
664
|
+
'- 输出 JSON:{"hasPattern": true, "pattern": "描述..."} 或 {"hasPattern": false}',
|
|
665
|
+
`记忆条目(共${clusterTexts.length}条):\n${numbered}`,
|
|
666
|
+
);
|
|
667
|
+
|
|
668
|
+
if (!result || !result.hasPattern || !result.pattern) return null;
|
|
669
|
+
return result.pattern;
|
|
670
|
+
}
|
|
671
|
+
|
|
457
672
|
/**
|
|
458
673
|
* Chat + JSON parse helper for structured extraction.
|
|
459
674
|
* Returns null on failure (LLM error or invalid JSON).
|
|
@@ -481,11 +696,63 @@ export class LLMClient {
|
|
|
481
696
|
}
|
|
482
697
|
}
|
|
483
698
|
|
|
699
|
+
async chatLong(system: string, user: string, maxTokens = 2000): Promise<string | null> {
|
|
700
|
+
if (!this.breaker.canAttempt()) {
|
|
701
|
+
return null;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
const controller = new AbortController();
|
|
705
|
+
const timeout = setTimeout(() => controller.abort(), this.timeoutMs * 2);
|
|
706
|
+
|
|
707
|
+
try {
|
|
708
|
+
const response = await this.client.chat.completions.create(
|
|
709
|
+
{
|
|
710
|
+
model: this.model,
|
|
711
|
+
messages: [
|
|
712
|
+
{ role: "system", content: system },
|
|
713
|
+
{ role: "user", content: user },
|
|
714
|
+
],
|
|
715
|
+
temperature: this.temperature,
|
|
716
|
+
max_tokens: maxTokens,
|
|
717
|
+
},
|
|
718
|
+
{ signal: controller.signal },
|
|
719
|
+
);
|
|
720
|
+
|
|
721
|
+
const result = response.choices[0]?.message?.content?.trim() || null;
|
|
722
|
+
this.breaker.recordSuccess();
|
|
723
|
+
return result;
|
|
724
|
+
} catch (err) {
|
|
725
|
+
this.breaker.recordFailure();
|
|
726
|
+
throw err;
|
|
727
|
+
} finally {
|
|
728
|
+
clearTimeout(timeout);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// --------------------------------------------------------------------------
|
|
733
|
+
// Constructive Retrieval — public wrappers for reconstruction pipeline
|
|
734
|
+
// --------------------------------------------------------------------------
|
|
735
|
+
|
|
736
|
+
/** Public wrapper for reconstruction pipeline */
|
|
737
|
+
async generateReconstruction(system: string, user: string): Promise<string | null> {
|
|
738
|
+
return this.chat(system, user);
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
/** Check if LLM circuit breaker allows requests */
|
|
742
|
+
isAvailable(): boolean {
|
|
743
|
+
return this.breaker.canAttempt();
|
|
744
|
+
}
|
|
745
|
+
|
|
484
746
|
// --------------------------------------------------------------------------
|
|
485
747
|
// Internals
|
|
486
748
|
// --------------------------------------------------------------------------
|
|
487
749
|
|
|
488
750
|
private async chat(system: string, user: string): Promise<string | null> {
|
|
751
|
+
// LME-9: Circuit breaker — short-circuit when LLM is down
|
|
752
|
+
if (!this.breaker.canAttempt()) {
|
|
753
|
+
return null;
|
|
754
|
+
}
|
|
755
|
+
|
|
489
756
|
const controller = new AbortController();
|
|
490
757
|
const timeout = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
491
758
|
|
|
@@ -503,7 +770,12 @@ export class LLMClient {
|
|
|
503
770
|
{ signal: controller.signal },
|
|
504
771
|
);
|
|
505
772
|
|
|
506
|
-
|
|
773
|
+
const result = response.choices[0]?.message?.content?.trim() || null;
|
|
774
|
+
this.breaker.recordSuccess();
|
|
775
|
+
return result;
|
|
776
|
+
} catch (err) {
|
|
777
|
+
this.breaker.recordFailure();
|
|
778
|
+
throw err;
|
|
507
779
|
} finally {
|
|
508
780
|
clearTimeout(timeout);
|
|
509
781
|
}
|
|
@@ -538,11 +810,22 @@ const SMART_EXTRACT_SYSTEM_PROMPT = `你是 AI 记忆分类助手。把对话片
|
|
|
538
810
|
| 报错→修复、问题→方案 | cases |
|
|
539
811
|
| "每次…"、"一般…"、规律总结 | patterns |
|
|
540
812
|
|
|
813
|
+
## 保真规则(l0/l1 必须遵守)
|
|
814
|
+
- 端口号/IP/URL/文件路径/API endpoint → 原样保留
|
|
815
|
+
- 命名列表(函数名/事件名/配置项)→ 逐项保留,不概括为"等"
|
|
816
|
+
- 规则表/状态转移 → 完整保留
|
|
817
|
+
- 对比/排序 → 保留顺序和理由
|
|
818
|
+
|
|
541
819
|
## 输出格式
|
|
542
820
|
只输出一个 JSON 对象:
|
|
543
821
|
{"category":"类别","l0":"一句话摘要(≤80字)","l1":"结构化概述(2-5行markdown)","importance":0.7}
|
|
544
822
|
|
|
545
|
-
importance 评分标准:身份/模式 0.8+、偏好/案例 0.7、实体 0.65、普通事件 0.5
|
|
823
|
+
importance 评分标准:身份/模式 0.8+、偏好/案例 0.7、实体 0.65、普通事件 0.5
|
|
824
|
+
|
|
825
|
+
Also rate emotional tone:
|
|
826
|
+
- emotion.valence: [-1, 1] (negative to positive)
|
|
827
|
+
- emotion.arousal: [0, 1] (calm to excited)
|
|
828
|
+
- emotion.label: one word (e.g., "frustration", "neutral")`;
|
|
546
829
|
|
|
547
830
|
const SMART_EXTRACT_BATCH_PROMPT = `你是 AI 记忆分类助手。把每段对话分类到 6 种记忆类型之一。
|
|
548
831
|
|