@memory-river/core 0.2.2 → 0.2.3
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.
|
@@ -88,6 +88,7 @@ export declare function findProbeTextMismatchDetail(summarizePairs: ComparableTr
|
|
|
88
88
|
export declare function logProbeTextMismatchDetail(detail: ProbeTextMismatchDetail): void;
|
|
89
89
|
export declare function buildDualTrackPrompt(conversationLog: string, capsuleLanguage?: string): string;
|
|
90
90
|
export declare function buildGeneralConversationPrompt(conversationLog: string, capsuleLanguage?: string): string;
|
|
91
|
+
export declare function buildImportanceScoringPrompt(noteTexts: string[]): string;
|
|
91
92
|
export declare function buildSimplePrompt(conversationLog: string, capsuleLanguage?: string): string;
|
|
92
93
|
export type CodexReasoningEffort = 'none' | 'low' | 'medium' | 'high' | 'xhigh' | 'max';
|
|
93
94
|
export interface CodexCliConfig {
|
|
@@ -129,6 +130,7 @@ export interface ConcentratorConfig {
|
|
|
129
130
|
transcriptArchive: TranscriptArchive;
|
|
130
131
|
sessionSummaryDir: string;
|
|
131
132
|
llm?: LlmClient;
|
|
133
|
+
importanceScorer?: (prompt: string) => Promise<string>;
|
|
132
134
|
}
|
|
133
135
|
export interface CapsuleOutput {
|
|
134
136
|
messages: ContextMessage[];
|
|
@@ -142,6 +144,7 @@ export declare class ConcentratorAdapter implements LlmClient {
|
|
|
142
144
|
private statsStore?;
|
|
143
145
|
private transcriptArchive;
|
|
144
146
|
private llm?;
|
|
147
|
+
private importanceScorer?;
|
|
145
148
|
private readonly MAX_CONTEXT_WINDOW;
|
|
146
149
|
private static readonly WATERLINE_CODE;
|
|
147
150
|
private static readonly WATERLINE_DEFAULT;
|
|
@@ -153,6 +156,7 @@ export declare class ConcentratorAdapter implements LlmClient {
|
|
|
153
156
|
*/
|
|
154
157
|
private detectConversationMode;
|
|
155
158
|
buildFallbackCapsule(messages: ContextMessage[]): string;
|
|
159
|
+
private scoreNoteImportance;
|
|
156
160
|
concentrate(rawMessages: ContextMessage[], dryRun?: boolean, force?: boolean, context?: ConcentrateContext): Promise<CapsuleOutput>;
|
|
157
161
|
estimateTokens(messages: ContextMessage[]): number;
|
|
158
162
|
estimateTokenBreakdown(messages: ContextMessage[]): {
|
|
@@ -796,7 +796,7 @@ ${conversationLog}
|
|
|
796
796
|
【時間正規化】對話每個 turn 前綴有 [at=<ISO>] 絕對時間戳。當事實含「昨天/今天/明天/上週/下個月/剛才/之後」等相對時間,必須以該事實所在 turn 的 at 為錨,在 text 內寫出絕對日期(例:「在 2023-05-07(原文稱昨天)參加了…」),並填 when 欄位。若 at=unknown 或語意不足以唯一解析,保留原文相對詞、不可猜,when.precision 與 when.source 設為 "unknown"。明確日期、月份、年份、區間、期限、事件先後順序都要保留,不要只寫「最近」「之前」。
|
|
797
797
|
|
|
798
798
|
每筆格式:
|
|
799
|
-
{ "text": "...", "category": "fact|decision|entity|preference|constraint|identity|knowledge|history|business|other", "
|
|
799
|
+
{ "text": "...", "category": "fact|decision|entity|preference|constraint|identity|knowledge|history|business|other", "tags": ["..."], "subject": "主體(選配)", "predicate": "關係/屬性/動作(選配)", "value": "值,字串/數字/布林(選配)", "unit": "數值單位(選配)", "when": { "start": "YYYY-MM-DD 或 ISO(選配)", "end": "(選配)", "precision": "datetime|date|month|year|range|unknown", "sourceText": "原文時間詞", "source": "explicit|relative_anchored|contextual|unknown", "anchor": "錨定用的 turn ISO 時間" } }
|
|
800
800
|
(subject/predicate/value/unit/entities/when 全為選配:有對應資訊才填,沒有就整個省略,不可填空字串或編造。text 仍是主要可讀、可檢索內容;日期、數值、人名、地名、檔案名、產品名、事件名也必須同時寫進 text,能正規化的日期同時寫入 text 與 when。)
|
|
801
801
|
|
|
802
802
|
收錄:
|
|
@@ -816,7 +816,6 @@ ${conversationLog}
|
|
|
816
816
|
- 一筆只表達一個可獨立更新的主張,必須自足:寫明主體、內容、作用域,以及必要的時間、狀態、條件或理由;不得使用「這個」「上述方案」「已處理」等脫離原文便無法理解的指涉。
|
|
817
817
|
- 保留足以語意檢索與回查原始 transcript 的專案、元件、人物或事件名稱,但不要複製整段對話、長篇推理、日誌或操作過程(原文可由 rehydrate 取回)。
|
|
818
818
|
- category 選最能代表該主張長期用途者;tags 用少量具辨識力的實體/專案/領域/狀態詞。
|
|
819
|
-
- importance 依「跨時間耐久性 × 未來決策效用」評分,不依篇幅、情緒強度或主題是否熱門;較低分代表耐久性或決策效用較低,但不得因此省略可獨立檢索的具體事實。
|
|
820
819
|
|
|
821
820
|
CRITICAL INSTRUCTION: Output ONLY valid, raw JSON. Do NOT wrap in markdown code blocks. Start with '{' and end with '}'.
|
|
822
821
|
|
|
@@ -843,7 +842,6 @@ confidence 評分標準(0.0–1.0):
|
|
|
843
842
|
{
|
|
844
843
|
"text": "精確的顆粒化記憶(含足夠上下文,獨立可理解;日期/數值/人名要寫進 text)",
|
|
845
844
|
"category": "fact|decision|entity|preference|constraint|identity|knowledge|history|business|other",
|
|
846
|
-
"importance": 0.0-1.0,
|
|
847
845
|
"tags": [],
|
|
848
846
|
"subject": "主體(選配,省略則整個不要出現)",
|
|
849
847
|
"predicate": "關係/屬性/動作(選配)",
|
|
@@ -898,7 +896,7 @@ ${capsuleLanguageInstruction}寫一段「前情提要」(600–900 字),
|
|
|
898
896
|
【時間正規化】對話每個 turn 前綴有 [at=<ISO>] 絕對時間戳。當事實含「昨天/今天/明天/上週/下個月/剛才/之後」等相對時間,必須以該事實所在 turn 的 at 為錨,在 text 內寫出絕對日期(例:「在 2023-05-07(原文稱昨天)參加了…」),並填 when 欄位。若 at=unknown 或語意不足以唯一解析,保留原文相對詞、不可猜,when.precision 與 when.source 設為 "unknown"。明確日期、月份、年份、區間、期限、事件先後順序都要保留,不要只寫「最近」「之前」。
|
|
899
897
|
|
|
900
898
|
每筆格式:
|
|
901
|
-
{ "text": "...", "category": "fact|decision|entity|preference|constraint|identity|knowledge|history|business|other", "
|
|
899
|
+
{ "text": "...", "category": "fact|decision|entity|preference|constraint|identity|knowledge|history|business|other", "tags": ["..."], "subject": "主體(選配)", "predicate": "關係/屬性/動作(選配)", "value": "值,字串/數字/布林(選配)", "unit": "數值單位(選配)", "when": { "start": "YYYY-MM-DD 或 ISO(選配)", "end": "(選配)", "precision": "datetime|date|month|year|range|unknown", "sourceText": "原文時間詞", "source": "explicit|relative_anchored|contextual|unknown", "anchor": "錨定用的 turn ISO 時間" } }
|
|
902
900
|
(subject/predicate/value/unit/entities/when 全為選配:有對應資訊才填,沒有就整個省略,不可填空字串或編造。text 仍是主要可讀、可檢索內容;日期、數值、人名、地名、檔案名、產品名、事件名也必須同時寫進 text,能正規化的日期同時寫入 text 與 when。)
|
|
903
901
|
|
|
904
902
|
收錄:
|
|
@@ -918,7 +916,6 @@ ${capsuleLanguageInstruction}寫一段「前情提要」(600–900 字),
|
|
|
918
916
|
- 一筆只表達一個可獨立更新的主張,必須自足:寫明主體、內容、作用域,以及必要的時間、狀態、條件或理由;不得使用「這個」「上述方案」「已處理」等脫離原文便無法理解的指涉。
|
|
919
917
|
- 保留足以語意檢索與回查原始 transcript 的專案、元件、人物或事件名稱,但不要複製整段對話、長篇推理、日誌或操作過程(原文可由 rehydrate 取回)。
|
|
920
918
|
- category 選最能代表該主張長期用途者;tags 用少量具辨識力的實體/專案/領域/狀態詞。
|
|
921
|
-
- importance 依「跨時間耐久性 × 未來決策效用」評分,不依篇幅、情緒強度或主題是否熱門;較低分代表耐久性或決策效用較低,但不得因此省略可獨立檢索的具體事實。
|
|
922
919
|
|
|
923
920
|
CRITICAL INSTRUCTION: Output ONLY valid, raw JSON. Do NOT wrap in markdown code blocks. Start with '{' and end with '}'.
|
|
924
921
|
|
|
@@ -932,7 +929,7 @@ confidence 評分標準(0.0–1.0):
|
|
|
932
929
|
"confidence": 0.85,
|
|
933
930
|
"capsule": "任務B的自然語言前情提要...",
|
|
934
931
|
"notes": [
|
|
935
|
-
{ "text": "精確的顆粒化記憶(含足夠上下文,獨立可理解;日期/數值/人名要寫進 text)", "category": "fact|decision|entity|preference|constraint|identity|knowledge|history|business|other", "
|
|
932
|
+
{ "text": "精確的顆粒化記憶(含足夠上下文,獨立可理解;日期/數值/人名要寫進 text)", "category": "fact|decision|entity|preference|constraint|identity|knowledge|history|business|other", "tags": [], "subject": "(選配,省略則不要出現)", "predicate": "(選配)", "value": "(選配)", "unit": "(選配)", "when": { "start": "YYYY-MM-DD(選配)", "precision": "date", "sourceText": "原文時間詞", "source": "relative_anchored", "anchor": "turn ISO 時間" } }
|
|
936
933
|
]
|
|
937
934
|
}
|
|
938
935
|
|
|
@@ -942,6 +939,70 @@ confidence 評分標準(0.0–1.0):
|
|
|
942
939
|
- 必須回傳可完整 JSON.parse 的有效 JSON,不可輸出半截 JSON`;
|
|
943
940
|
return isSourceLanguage ? `${sourceLanguageRequirementTop}\n\n${prompt}\n\n${sourceLanguageRequirementBottom}` : prompt;
|
|
944
941
|
}
|
|
942
|
+
export function buildImportanceScoringPrompt(noteTexts) {
|
|
943
|
+
const numberedNotes = noteTexts
|
|
944
|
+
.map((text, index) => `${index + 1}. ${text}`)
|
|
945
|
+
.join('\n');
|
|
946
|
+
return `非互動模式,立即回答,不要提問、不要使用任何工具、不要讀檔。
|
|
947
|
+
|
|
948
|
+
下面是即將存進 AI 長期記憶庫的記憶。請為每一則評 importance(0.0-1.0)。
|
|
949
|
+
|
|
950
|
+
評分標準(照這個定義,不要自行更改):
|
|
951
|
+
importance 依「跨時間耐久性 × 未來決策效用」評分,不依篇幅、情緒強度或主題是否熱門;
|
|
952
|
+
較低分代表耐久性或決策效用較低。
|
|
953
|
+
|
|
954
|
+
只輸出 JSON,不要任何其他文字、不要 markdown 圍欄。必須包含全部 <${noteTexts.length}> 筆:
|
|
955
|
+
{"r":[{"n":<編號>,"i":<0.0-1.0>}]}
|
|
956
|
+
|
|
957
|
+
${numberedNotes}`;
|
|
958
|
+
}
|
|
959
|
+
class ImportanceScoreParseError extends Error {
|
|
960
|
+
scores;
|
|
961
|
+
constructor(message, scores) {
|
|
962
|
+
super(message);
|
|
963
|
+
this.scores = scores;
|
|
964
|
+
this.name = 'ImportanceScoreParseError';
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
function parseImportanceScores(raw, expectedCount) {
|
|
968
|
+
const lines = raw.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
969
|
+
let response = null;
|
|
970
|
+
for (let index = lines.length - 1; index >= 0; index--) {
|
|
971
|
+
try {
|
|
972
|
+
const candidate = JSON.parse(lines[index]);
|
|
973
|
+
if (candidate && typeof candidate === 'object' && Array.isArray(candidate.r)) {
|
|
974
|
+
response = candidate;
|
|
975
|
+
break;
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
catch {
|
|
979
|
+
// Codex progress lines and other non-JSON output are expected here.
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
if (!response) {
|
|
983
|
+
throw new ImportanceScoreParseError('importance score response parse failed', new Map());
|
|
984
|
+
}
|
|
985
|
+
const scores = new Map();
|
|
986
|
+
for (const entry of response.r) {
|
|
987
|
+
if (!entry || typeof entry !== 'object')
|
|
988
|
+
continue;
|
|
989
|
+
const item = entry;
|
|
990
|
+
if (typeof item.n === 'number'
|
|
991
|
+
&& Number.isInteger(item.n)
|
|
992
|
+
&& item.n >= 1
|
|
993
|
+
&& item.n <= expectedCount
|
|
994
|
+
&& typeof item.i === 'number'
|
|
995
|
+
&& Number.isFinite(item.i)) {
|
|
996
|
+
scores.set(item.n, Math.max(0, Math.min(1, item.i)));
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
const missing = Array.from({ length: expectedCount }, (_, index) => index + 1)
|
|
1000
|
+
.filter((number) => !scores.has(number));
|
|
1001
|
+
if (missing.length > 0) {
|
|
1002
|
+
throw new ImportanceScoreParseError(`importance score response missing notes: ${missing.join(',')}`, scores);
|
|
1003
|
+
}
|
|
1004
|
+
return scores;
|
|
1005
|
+
}
|
|
945
1006
|
// ─── 降級版 Prompt (專供本地小模型使用) ───
|
|
946
1007
|
export function buildSimplePrompt(conversationLog, capsuleLanguage = '繁體中文') {
|
|
947
1008
|
if (capsuleLanguage === 'source') {
|
|
@@ -1396,6 +1457,7 @@ export class ConcentratorAdapter {
|
|
|
1396
1457
|
statsStore;
|
|
1397
1458
|
transcriptArchive;
|
|
1398
1459
|
llm;
|
|
1460
|
+
importanceScorer;
|
|
1399
1461
|
MAX_CONTEXT_WINDOW = 200000;
|
|
1400
1462
|
// ── 動態水位線常數 ──────────────────────────────────────
|
|
1401
1463
|
static WATERLINE_CODE = 0.40; // 代碼/技術密集:40%
|
|
@@ -1422,6 +1484,7 @@ export class ConcentratorAdapter {
|
|
|
1422
1484
|
this.statsStore = config.statsStore;
|
|
1423
1485
|
this.transcriptArchive = config.transcriptArchive;
|
|
1424
1486
|
this.llm = config.llm;
|
|
1487
|
+
this.importanceScorer = config.importanceScorer;
|
|
1425
1488
|
this.capsuleBridge = new CapsuleBridge(this.config.inboxPath);
|
|
1426
1489
|
}
|
|
1427
1490
|
/**
|
|
@@ -1491,6 +1554,41 @@ export class ConcentratorAdapter {
|
|
|
1491
1554
|
`## 9. Optional Next Step\n${latestAssistant === '無' ? '無' : latestAssistant}`,
|
|
1492
1555
|
].join('\n\n');
|
|
1493
1556
|
}
|
|
1557
|
+
async scoreNoteImportance(notes, metricContext) {
|
|
1558
|
+
if (notes.length === 0)
|
|
1559
|
+
return;
|
|
1560
|
+
const prompt = buildImportanceScoringPrompt(notes.map((note) => typeof note.text === 'string' ? note.text : ''));
|
|
1561
|
+
let scores = new Map();
|
|
1562
|
+
try {
|
|
1563
|
+
const raw = this.importanceScorer
|
|
1564
|
+
? await this.importanceScorer(prompt)
|
|
1565
|
+
: await this.callWithFallback(prompt, 'scoreImportance', undefined, { sessionIdentity: metricContext?.sessionIdentity, inputTokens: estimatePromptTokens(prompt) }, 2048);
|
|
1566
|
+
scores = parseImportanceScores(raw, notes.length);
|
|
1567
|
+
}
|
|
1568
|
+
catch (error) {
|
|
1569
|
+
if (error instanceof ImportanceScoreParseError) {
|
|
1570
|
+
scores = error.scores;
|
|
1571
|
+
}
|
|
1572
|
+
const message = String(error?.message ?? error).toLowerCase();
|
|
1573
|
+
const reason = error instanceof ImportanceScoreParseError
|
|
1574
|
+
? (message.includes('missing') ? 'partial missing' : 'parse failed')
|
|
1575
|
+
: (message.includes('timeout') || message.includes('timed out') || message.includes('abort')
|
|
1576
|
+
? 'timeout'
|
|
1577
|
+
: 'call failed');
|
|
1578
|
+
console.warn(`[ConcentratorAdapter] Importance scoring fallback: ${reason}; missing scores use 0.5`);
|
|
1579
|
+
}
|
|
1580
|
+
notes.forEach((note, index) => {
|
|
1581
|
+
const importance = scores.get(index + 1);
|
|
1582
|
+
if (importance === undefined) {
|
|
1583
|
+
note.importance = 0.5;
|
|
1584
|
+
note.importanceFallback = true;
|
|
1585
|
+
}
|
|
1586
|
+
else {
|
|
1587
|
+
note.importance = importance;
|
|
1588
|
+
delete note.importanceFallback;
|
|
1589
|
+
}
|
|
1590
|
+
});
|
|
1591
|
+
}
|
|
1494
1592
|
async concentrate(rawMessages, dryRun = false, force = false, context = {}) {
|
|
1495
1593
|
const messages = rawMessages.map(msg => truncateGiantStrings(msg));
|
|
1496
1594
|
const currentTokens = estimateTotalTokens(messages);
|
|
@@ -1692,7 +1790,9 @@ export class ConcentratorAdapter {
|
|
|
1692
1790
|
const originalTokens = estimatePromptTokens(conversationLog);
|
|
1693
1791
|
const summaryTokens = estimatePromptTokens(capsuleText);
|
|
1694
1792
|
const compressionRatio = originalTokens / Math.max(1, summaryTokens);
|
|
1695
|
-
const notes = parsedData.notes
|
|
1793
|
+
const notes = Array.isArray(parsedData.notes)
|
|
1794
|
+
? parsedData.notes.map((item) => item && typeof item === 'object' ? item : {})
|
|
1795
|
+
: [];
|
|
1696
1796
|
const sourceEntryIds = sourceEntryIdsProbe?.matched ? sourceEntryIdsProbe.sourceEntryIds : [];
|
|
1697
1797
|
const sourceEntryRange = sourceEntryIds.length > 0
|
|
1698
1798
|
? {
|
|
@@ -1720,6 +1820,9 @@ export class ConcentratorAdapter {
|
|
|
1720
1820
|
console.log(`[ConcentratorAdapter] sourceEntryIds metadata: length=${sourceEntryIds.length} firstEntryId=${sourceEntryRange?.firstEntryId ?? 'none'} lastEntryId=${sourceEntryRange?.lastEntryId ?? 'none'}`);
|
|
1721
1821
|
console.log(`[ConcentratorAdapter] Short-term capsule written (health: 30, confidence: ${confidence.toFixed(2)})`);
|
|
1722
1822
|
}
|
|
1823
|
+
await this.scoreNoteImportance(notes, {
|
|
1824
|
+
sessionIdentity: context.sessionIdentity,
|
|
1825
|
+
});
|
|
1723
1826
|
const acceptedNoteTexts = [];
|
|
1724
1827
|
for (const item of notes) {
|
|
1725
1828
|
const text = typeof item.text === 'string' ? item.text.trim() : '';
|
|
@@ -1744,6 +1847,7 @@ export class ConcentratorAdapter {
|
|
|
1744
1847
|
confidence,
|
|
1745
1848
|
firstTimestamp,
|
|
1746
1849
|
lastTimestamp,
|
|
1850
|
+
...(item.importanceFallback === true ? { importanceFallback: true } : {}),
|
|
1747
1851
|
...(sourceEntryIds.length > 0 ? { sourceEntryIds } : {}),
|
|
1748
1852
|
...(sourceEntryRange ? { sourceEntryRange } : {}),
|
|
1749
1853
|
// Optional structured enrichment — additive, stored as-is (no schema change),
|
|
@@ -1770,7 +1874,7 @@ export class ConcentratorAdapter {
|
|
|
1770
1874
|
|| rawMessages[0]?.sessionId
|
|
1771
1875
|
|| 'unknown';
|
|
1772
1876
|
const concentratedAt = Date.now();
|
|
1773
|
-
await this.writeSessionSummary({ sessionId, concentratedAt, capsule: capsuleText, notes:
|
|
1877
|
+
await this.writeSessionSummary({ sessionId, concentratedAt, capsule: capsuleText, notes: notes, primaryRequest: summary?.primaryRequest || '', pendingTasks: summary?.pendingTasks || '', nextStep: summary?.nextStep || '' });
|
|
1774
1878
|
}
|
|
1775
1879
|
catch (err) {
|
|
1776
1880
|
console.error("[ConcentratorAdapter] Compaction failed:", err);
|