@openanonymity/nanomem 0.1.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 +194 -0
- package/package.json +85 -0
- package/src/backends/BaseStorage.js +177 -0
- package/src/backends/filesystem.js +177 -0
- package/src/backends/indexeddb.js +208 -0
- package/src/backends/ram.js +113 -0
- package/src/backends/schema.js +42 -0
- package/src/bullets/bulletIndex.js +125 -0
- package/src/bullets/compaction.js +109 -0
- package/src/bullets/index.js +16 -0
- package/src/bullets/normalize.js +241 -0
- package/src/bullets/parser.js +199 -0
- package/src/bullets/scoring.js +53 -0
- package/src/cli/auth.js +323 -0
- package/src/cli/commands.js +411 -0
- package/src/cli/config.js +120 -0
- package/src/cli/diff.js +68 -0
- package/src/cli/help.js +84 -0
- package/src/cli/output.js +269 -0
- package/src/cli/spinner.js +54 -0
- package/src/cli.js +178 -0
- package/src/engine/compactor.js +247 -0
- package/src/engine/executors.js +152 -0
- package/src/engine/ingester.js +229 -0
- package/src/engine/retriever.js +414 -0
- package/src/engine/toolLoop.js +176 -0
- package/src/imports/chatgpt.js +160 -0
- package/src/imports/index.js +14 -0
- package/src/imports/markdown.js +104 -0
- package/src/imports/oaFastchat.js +124 -0
- package/src/index.js +199 -0
- package/src/llm/anthropic.js +264 -0
- package/src/llm/openai.js +179 -0
- package/src/prompt_sets/conversation/ingestion.js +51 -0
- package/src/prompt_sets/document/ingestion.js +43 -0
- package/src/prompt_sets/index.js +31 -0
- package/src/types.js +382 -0
- package/src/utils/portability.js +174 -0
- package/types/backends/BaseStorage.d.ts +42 -0
- package/types/backends/filesystem.d.ts +11 -0
- package/types/backends/indexeddb.d.ts +12 -0
- package/types/backends/ram.d.ts +8 -0
- package/types/backends/schema.d.ts +14 -0
- package/types/bullets/bulletIndex.d.ts +47 -0
- package/types/bullets/compaction.d.ts +10 -0
- package/types/bullets/index.d.ts +36 -0
- package/types/bullets/normalize.d.ts +95 -0
- package/types/bullets/parser.d.ts +31 -0
- package/types/bullets/scoring.d.ts +12 -0
- package/types/engine/compactor.d.ts +27 -0
- package/types/engine/executors.d.ts +46 -0
- package/types/engine/ingester.d.ts +29 -0
- package/types/engine/retriever.d.ts +50 -0
- package/types/engine/toolLoop.d.ts +9 -0
- package/types/imports/chatgpt.d.ts +14 -0
- package/types/imports/index.d.ts +3 -0
- package/types/imports/markdown.d.ts +31 -0
- package/types/imports/oaFastchat.d.ts +30 -0
- package/types/index.d.ts +21 -0
- package/types/llm/anthropic.d.ts +16 -0
- package/types/llm/openai.d.ts +16 -0
- package/types/prompt_sets/conversation/ingestion.d.ts +7 -0
- package/types/prompt_sets/document/ingestion.d.ts +7 -0
- package/types/prompt_sets/index.d.ts +11 -0
- package/types/types.d.ts +293 -0
- package/types/utils/portability.d.ts +33 -0
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalization utilities for memory bullet metadata.
|
|
3
|
+
* @import { Tier, Status, Source, Confidence, Bullet, EnsureBulletMetadataOptions } from '../types.js'
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @param {string | number | null | undefined} value
|
|
8
|
+
* @returns {string | null}
|
|
9
|
+
*/
|
|
10
|
+
export function safeDateIso(value) {
|
|
11
|
+
if (!value) return null;
|
|
12
|
+
const date = new Date(value);
|
|
13
|
+
if (Number.isNaN(date.getTime())) return null;
|
|
14
|
+
return date.toISOString().slice(0, 10);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** @returns {string} */
|
|
18
|
+
export function todayIsoDate() {
|
|
19
|
+
return new Date().toISOString().slice(0, 10);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @param {string} path
|
|
24
|
+
* @returns {string}
|
|
25
|
+
*/
|
|
26
|
+
export function inferTopicFromPath(path) {
|
|
27
|
+
if (!path || typeof path !== 'string') return 'general';
|
|
28
|
+
const first = path.split('/')[0]?.trim().toLowerCase();
|
|
29
|
+
return first || 'general';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @param {string} value
|
|
34
|
+
* @param {string} [fallback]
|
|
35
|
+
* @returns {string}
|
|
36
|
+
*/
|
|
37
|
+
export function normalizeTopic(value, fallback = 'general') {
|
|
38
|
+
const source = String(value || '').trim().toLowerCase();
|
|
39
|
+
const normalized = source
|
|
40
|
+
.replace(/[^a-z0-9/_-]+/g, '-')
|
|
41
|
+
.replace(/-+/g, '-')
|
|
42
|
+
.replace(/^[-/]+|[-/]+$/g, '');
|
|
43
|
+
return normalized || fallback;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* @param {string} value
|
|
48
|
+
* @returns {string}
|
|
49
|
+
*/
|
|
50
|
+
export function normalizeFactText(value) {
|
|
51
|
+
return String(value || '')
|
|
52
|
+
.toLowerCase()
|
|
53
|
+
.replace(/[^\w\s]/g, ' ')
|
|
54
|
+
.replace(/\s+/g, ' ')
|
|
55
|
+
.trim();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* @param {string | null | undefined} value
|
|
60
|
+
* @param {Tier} [fallback]
|
|
61
|
+
* @returns {Tier}
|
|
62
|
+
*/
|
|
63
|
+
export function normalizeTier(value, fallback = 'long_term') {
|
|
64
|
+
const source = String(value || '').trim().toLowerCase();
|
|
65
|
+
if (['working', 'short_term', 'short-term'].includes(source)) return 'working';
|
|
66
|
+
if (['long_term', 'long-term', 'longterm', 'active'].includes(source)) return 'long_term';
|
|
67
|
+
if (['history', 'archive', 'archived'].includes(source)) return 'history';
|
|
68
|
+
return fallback;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* @param {string | null | undefined} value
|
|
73
|
+
* @param {Status} [fallback]
|
|
74
|
+
* @returns {Status}
|
|
75
|
+
*/
|
|
76
|
+
export function normalizeStatus(value, fallback = 'active') {
|
|
77
|
+
const source = String(value || '').trim().toLowerCase();
|
|
78
|
+
if (['active', 'current'].includes(source)) return 'active';
|
|
79
|
+
if (['superseded', 'replaced', 'resolved'].includes(source)) return 'superseded';
|
|
80
|
+
if (['expired', 'stale'].includes(source)) return 'expired';
|
|
81
|
+
if (['uncertain', 'tentative'].includes(source)) return 'uncertain';
|
|
82
|
+
return fallback;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* @param {string | null | undefined} value
|
|
87
|
+
* @param {Source} [fallback]
|
|
88
|
+
* @returns {Source}
|
|
89
|
+
*/
|
|
90
|
+
export function normalizeSource(value, fallback = 'user_statement') {
|
|
91
|
+
const source = String(value || '').trim().toLowerCase();
|
|
92
|
+
if (['user_statement', 'user', 'explicit_user'].includes(source)) return 'user_statement';
|
|
93
|
+
if (['assistant_summary', 'assistant', 'summary'].includes(source)) return 'assistant_summary';
|
|
94
|
+
if (['inference', 'inferred'].includes(source)) return 'inference';
|
|
95
|
+
if (['system', 'system_note'].includes(source)) return 'system';
|
|
96
|
+
return fallback;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* @param {string | null | undefined} value
|
|
101
|
+
* @param {Confidence} [fallback]
|
|
102
|
+
* @returns {Confidence}
|
|
103
|
+
*/
|
|
104
|
+
export function normalizeConfidence(value, fallback = 'medium') {
|
|
105
|
+
const source = String(value || '').trim().toLowerCase();
|
|
106
|
+
if (['high', 'strong'].includes(source)) return 'high';
|
|
107
|
+
if (['medium', 'med', 'moderate'].includes(source)) return 'medium';
|
|
108
|
+
if (['low', 'weak'].includes(source)) return 'low';
|
|
109
|
+
return fallback;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* @param {Source | string | null | undefined} source
|
|
114
|
+
* @returns {Confidence}
|
|
115
|
+
*/
|
|
116
|
+
export function defaultConfidenceForSource(source) {
|
|
117
|
+
if (source === 'user_statement') return 'high';
|
|
118
|
+
if (source === 'assistant_summary') return 'medium';
|
|
119
|
+
if (source === 'system') return 'medium';
|
|
120
|
+
return 'low';
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* @param {string} section
|
|
125
|
+
* @returns {Tier}
|
|
126
|
+
*/
|
|
127
|
+
export function inferTierFromSection(section) {
|
|
128
|
+
if (section === 'working') return 'working';
|
|
129
|
+
if (section === 'history') return 'history';
|
|
130
|
+
return 'long_term';
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* @param {string} section
|
|
135
|
+
* @returns {Status}
|
|
136
|
+
*/
|
|
137
|
+
export function inferStatusFromSection(section) {
|
|
138
|
+
return section === 'history' ? 'superseded' : 'active';
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* @param {string} value
|
|
143
|
+
* @returns {Tier}
|
|
144
|
+
*/
|
|
145
|
+
export function normalizeTierToSection(value) {
|
|
146
|
+
const tier = normalizeTier(value);
|
|
147
|
+
if (tier === 'working') return 'working';
|
|
148
|
+
if (tier === 'history') return 'history';
|
|
149
|
+
return 'long_term';
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* @param {Partial<Bullet>} bullet
|
|
154
|
+
* @param {Tier} [fallback]
|
|
155
|
+
* @returns {Tier}
|
|
156
|
+
*/
|
|
157
|
+
export function inferTierFromBullet(bullet, fallback = 'long_term') {
|
|
158
|
+
if (bullet?.reviewAt || bullet?.expiresAt) return 'working';
|
|
159
|
+
|
|
160
|
+
const text = String(bullet?.text || '').toLowerCase();
|
|
161
|
+
if (!text) return fallback;
|
|
162
|
+
|
|
163
|
+
const workingPatterns = [
|
|
164
|
+
/\bcurrently\b/,
|
|
165
|
+
/\bright now\b/,
|
|
166
|
+
/\bthis (week|month|quarter|year)\b/,
|
|
167
|
+
/\bnext (week|month|quarter|year)\b/,
|
|
168
|
+
/\bplanning\b/,
|
|
169
|
+
/\bevaluating\b/,
|
|
170
|
+
/\bconsidering\b/,
|
|
171
|
+
/\btrying to\b/,
|
|
172
|
+
/\bworking on\b/,
|
|
173
|
+
/\bdebugging\b/,
|
|
174
|
+
/\bpreparing\b/,
|
|
175
|
+
/\binterviewing\b/,
|
|
176
|
+
/\bin progress\b/,
|
|
177
|
+
/\btemporary\b/,
|
|
178
|
+
/\bfor now\b/,
|
|
179
|
+
/\bas of \d{4}-\d{2}-\d{2}\b/
|
|
180
|
+
];
|
|
181
|
+
|
|
182
|
+
return workingPatterns.some((pattern) => pattern.test(text)) ? 'working' : fallback;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* @param {Partial<Bullet>} bullet
|
|
187
|
+
* @param {EnsureBulletMetadataOptions} [options]
|
|
188
|
+
* @returns {Bullet}
|
|
189
|
+
*/
|
|
190
|
+
export function ensureBulletMetadata(bullet, options = {}) {
|
|
191
|
+
const fallbackTopic = normalizeTopic(options.defaultTopic || 'general');
|
|
192
|
+
const fallbackUpdatedAt = options.updatedAt || todayIsoDate();
|
|
193
|
+
const inferredTier = inferTierFromBullet(bullet, options.defaultTier || 'long_term');
|
|
194
|
+
const preferredTier = bullet?.explicitTier ? bullet?.tier : inferredTier;
|
|
195
|
+
const fallbackTier = normalizeTier(options.defaultTier || preferredTier || bullet?.tier || 'long_term');
|
|
196
|
+
const fallbackStatus = normalizeStatus(
|
|
197
|
+
options.defaultStatus
|
|
198
|
+
|| (bullet?.explicitStatus ? bullet?.status : null)
|
|
199
|
+
|| inferStatusFromSection(normalizeTierToSection(fallbackTier))
|
|
200
|
+
);
|
|
201
|
+
return {
|
|
202
|
+
text: String(bullet?.text || '').trim(),
|
|
203
|
+
topic: normalizeTopic(bullet?.topic || fallbackTopic, fallbackTopic),
|
|
204
|
+
updatedAt: safeDateIso(bullet?.updatedAt) || fallbackUpdatedAt,
|
|
205
|
+
expiresAt: safeDateIso(bullet?.expiresAt),
|
|
206
|
+
reviewAt: safeDateIso(bullet?.reviewAt),
|
|
207
|
+
tier: normalizeTier(preferredTier, fallbackTier),
|
|
208
|
+
status: normalizeStatus(bullet?.status, fallbackStatus),
|
|
209
|
+
source: normalizeSource(
|
|
210
|
+
bullet?.source,
|
|
211
|
+
normalizeSource(options.defaultSource, 'user_statement')
|
|
212
|
+
),
|
|
213
|
+
confidence: normalizeConfidence(
|
|
214
|
+
bullet?.confidence,
|
|
215
|
+
normalizeConfidence(
|
|
216
|
+
options.defaultConfidence,
|
|
217
|
+
defaultConfidenceForSource(normalizeSource(
|
|
218
|
+
bullet?.source,
|
|
219
|
+
normalizeSource(options.defaultSource, 'user_statement')
|
|
220
|
+
))
|
|
221
|
+
)
|
|
222
|
+
),
|
|
223
|
+
explicitTier: Boolean(bullet?.explicitTier || bullet?.tier),
|
|
224
|
+
explicitStatus: Boolean(bullet?.explicitStatus || bullet?.status),
|
|
225
|
+
explicitSource: Boolean(bullet?.explicitSource || bullet?.source),
|
|
226
|
+
explicitConfidence: Boolean(bullet?.explicitConfidence || bullet?.confidence),
|
|
227
|
+
heading: String(bullet?.heading || 'General'),
|
|
228
|
+
section: normalizeTierToSection(preferredTier || fallbackTier),
|
|
229
|
+
lineIndex: Number.isFinite(bullet?.lineIndex) ? /** @type {number} */ (bullet.lineIndex) : 0
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* @param {Bullet} bullet
|
|
235
|
+
* @param {string} [today]
|
|
236
|
+
* @returns {boolean}
|
|
237
|
+
*/
|
|
238
|
+
export function isExpiredBullet(bullet, today = todayIsoDate()) {
|
|
239
|
+
if (!bullet?.expiresAt) return false;
|
|
240
|
+
return String(bullet.expiresAt) < String(today);
|
|
241
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parsing and rendering for memory bullets.
|
|
3
|
+
*/
|
|
4
|
+
/** @import { Bullet } from '../types.js' */
|
|
5
|
+
import {
|
|
6
|
+
safeDateIso,
|
|
7
|
+
normalizeTier,
|
|
8
|
+
normalizeStatus,
|
|
9
|
+
normalizeSource,
|
|
10
|
+
normalizeConfidence,
|
|
11
|
+
normalizeTopic,
|
|
12
|
+
normalizeTierToSection,
|
|
13
|
+
inferTierFromSection,
|
|
14
|
+
inferStatusFromSection,
|
|
15
|
+
ensureBulletMetadata,
|
|
16
|
+
} from './normalize.js';
|
|
17
|
+
|
|
18
|
+
const BULLET_REGEX = /^\s*-\s+(.*)$/;
|
|
19
|
+
const HEADING_REGEX = /^\s{0,3}#{1,6}\s+(.*)$/;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @param {string} content
|
|
23
|
+
* @returns {Bullet[]}
|
|
24
|
+
*/
|
|
25
|
+
export function parseBullets(content) {
|
|
26
|
+
const lines = String(content || '').split('\n');
|
|
27
|
+
const bullets = [];
|
|
28
|
+
let currentHeading = 'General';
|
|
29
|
+
let section = 'long_term';
|
|
30
|
+
|
|
31
|
+
for (let i = 0; i < lines.length; i++) {
|
|
32
|
+
const line = lines[i];
|
|
33
|
+
const headingMatch = line.match(HEADING_REGEX);
|
|
34
|
+
if (headingMatch) {
|
|
35
|
+
currentHeading = headingMatch[1].trim() || currentHeading;
|
|
36
|
+
if (/^(working)$/i.test(currentHeading)) {
|
|
37
|
+
section = 'working';
|
|
38
|
+
} else if (/^(long[- ]?term|active)$/i.test(currentHeading)) {
|
|
39
|
+
section = 'long_term';
|
|
40
|
+
} else if (/^(history|archive)$/i.test(currentHeading)) {
|
|
41
|
+
section = 'history';
|
|
42
|
+
}
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const bulletMatch = line.match(BULLET_REGEX);
|
|
47
|
+
if (!bulletMatch) continue;
|
|
48
|
+
|
|
49
|
+
const raw = bulletMatch[1].trim();
|
|
50
|
+
const parts = raw.split('|').map((part) => part.trim()).filter(Boolean);
|
|
51
|
+
if (parts.length === 0) continue;
|
|
52
|
+
|
|
53
|
+
const text = parts.shift() || '';
|
|
54
|
+
let topic = null;
|
|
55
|
+
let updatedAt = null;
|
|
56
|
+
let expiresAt = null;
|
|
57
|
+
let reviewAt = null;
|
|
58
|
+
let tier = null;
|
|
59
|
+
let status = null;
|
|
60
|
+
let source = null;
|
|
61
|
+
let confidence = null;
|
|
62
|
+
|
|
63
|
+
for (const part of parts) {
|
|
64
|
+
const kv = part.match(/^([a-z_]+)\s*=\s*(.+)$/i);
|
|
65
|
+
if (!kv) continue;
|
|
66
|
+
const key = kv[1].toLowerCase();
|
|
67
|
+
const value = kv[2].trim();
|
|
68
|
+
if (key === 'topic') topic = value;
|
|
69
|
+
if (key === 'updated_at') updatedAt = safeDateIso(value);
|
|
70
|
+
if (key === 'expires_at') expiresAt = safeDateIso(value);
|
|
71
|
+
if (key === 'review_at') reviewAt = safeDateIso(value);
|
|
72
|
+
if (key === 'tier') tier = normalizeTier(value);
|
|
73
|
+
if (key === 'status') status = normalizeStatus(value);
|
|
74
|
+
if (key === 'source') source = normalizeSource(value);
|
|
75
|
+
if (key === 'confidence') confidence = normalizeConfidence(value);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
bullets.push({
|
|
79
|
+
text,
|
|
80
|
+
topic: topic ? normalizeTopic(topic) : null,
|
|
81
|
+
updatedAt,
|
|
82
|
+
expiresAt,
|
|
83
|
+
reviewAt,
|
|
84
|
+
tier: tier || inferTierFromSection(section),
|
|
85
|
+
status: status || inferStatusFromSection(section),
|
|
86
|
+
source: source || null,
|
|
87
|
+
confidence: confidence || null,
|
|
88
|
+
explicitTier: Boolean(tier),
|
|
89
|
+
explicitStatus: Boolean(status),
|
|
90
|
+
explicitSource: Boolean(source),
|
|
91
|
+
explicitConfidence: Boolean(confidence),
|
|
92
|
+
heading: currentHeading,
|
|
93
|
+
section,
|
|
94
|
+
lineIndex: i
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return bullets;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* @param {string} content
|
|
103
|
+
* @returns {number}
|
|
104
|
+
*/
|
|
105
|
+
export function countBullets(content) {
|
|
106
|
+
return parseBullets(content).length;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* @param {string} content
|
|
111
|
+
* @returns {string[]}
|
|
112
|
+
*/
|
|
113
|
+
export function extractTitles(content) {
|
|
114
|
+
const lines = String(content || '').split('\n');
|
|
115
|
+
const titles = [];
|
|
116
|
+
|
|
117
|
+
for (const line of lines) {
|
|
118
|
+
const headingMatch = line.match(HEADING_REGEX);
|
|
119
|
+
if (!headingMatch) continue;
|
|
120
|
+
|
|
121
|
+
const title = headingMatch[1].trim();
|
|
122
|
+
if (!title) continue;
|
|
123
|
+
if (/^(working|long[- ]?term|history|active|archive|current context|stable facts|no longer current)$/i.test(title)) continue;
|
|
124
|
+
titles.push(title);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return titles;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* @param {Partial<Bullet>} bullet
|
|
132
|
+
* @returns {string}
|
|
133
|
+
*/
|
|
134
|
+
export function renderBullet(bullet) {
|
|
135
|
+
const clean = ensureBulletMetadata(bullet);
|
|
136
|
+
const metadata = [
|
|
137
|
+
`topic=${clean.topic}`,
|
|
138
|
+
`tier=${clean.tier}`,
|
|
139
|
+
`status=${clean.status}`,
|
|
140
|
+
`source=${clean.source}`,
|
|
141
|
+
`confidence=${clean.confidence}`,
|
|
142
|
+
`updated_at=${clean.updatedAt}`
|
|
143
|
+
];
|
|
144
|
+
if (clean.reviewAt) metadata.push(`review_at=${clean.reviewAt}`);
|
|
145
|
+
if (clean.expiresAt) metadata.push(`expires_at=${clean.expiresAt}`);
|
|
146
|
+
return `- ${clean.text} | ${metadata.join(' | ')}`;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function topicHeading(topic) {
|
|
150
|
+
const clean = String(topic || 'general').trim();
|
|
151
|
+
if (!clean) return 'General';
|
|
152
|
+
return clean
|
|
153
|
+
.split(/[\/_-]+/)
|
|
154
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
155
|
+
.join(' ');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function inferDocumentTopic(bullets, fallback = 'general') {
|
|
159
|
+
const firstTopic = (bullets || []).find((bullet) => bullet?.topic)?.topic;
|
|
160
|
+
return firstTopic || fallback;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function renderSection(lines, title, subsectionTitle, bullets, forceHistory = false) {
|
|
164
|
+
lines.push(`## ${title}`);
|
|
165
|
+
lines.push(`### ${subsectionTitle}`);
|
|
166
|
+
|
|
167
|
+
if (!bullets || bullets.length === 0) {
|
|
168
|
+
lines.push('_No entries yet._');
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
for (const bullet of bullets) {
|
|
173
|
+
const nextBullet = forceHistory
|
|
174
|
+
? { ...bullet, tier: 'history', status: bullet.status === 'active' ? 'superseded' : bullet.status, section: 'history' }
|
|
175
|
+
: bullet;
|
|
176
|
+
lines.push(renderBullet(nextBullet));
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* @param {Bullet[]} working
|
|
182
|
+
* @param {Bullet[]} longTerm
|
|
183
|
+
* @param {Bullet[]} history
|
|
184
|
+
* @param {{ titleTopic?: string }} [options]
|
|
185
|
+
* @returns {string}
|
|
186
|
+
*/
|
|
187
|
+
export function renderCompactedDocument(working, longTerm, history, options = {}) {
|
|
188
|
+
const lines = [];
|
|
189
|
+
const docTopic = normalizeTopic(options.titleTopic || inferDocumentTopic([...working, ...longTerm, ...history], 'general'));
|
|
190
|
+
lines.push(`# Memory: ${topicHeading(docTopic)}`);
|
|
191
|
+
lines.push('');
|
|
192
|
+
renderSection(lines, 'Working', 'Current context', working);
|
|
193
|
+
lines.push('');
|
|
194
|
+
renderSection(lines, 'Long-Term', 'Stable facts', longTerm);
|
|
195
|
+
lines.push('');
|
|
196
|
+
renderSection(lines, 'History', 'No longer current', history, true);
|
|
197
|
+
|
|
198
|
+
return lines.join('\n').trim();
|
|
199
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Relevance scoring for memory bullet retrieval.
|
|
3
|
+
*/
|
|
4
|
+
/** @import { Bullet } from '../types.js' */
|
|
5
|
+
import {
|
|
6
|
+
normalizeTier,
|
|
7
|
+
normalizeStatus,
|
|
8
|
+
normalizeSource,
|
|
9
|
+
normalizeTierToSection,
|
|
10
|
+
inferStatusFromSection,
|
|
11
|
+
} from './normalize.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @param {Partial<Bullet>} bullet
|
|
15
|
+
* @param {string[]} [queryTerms]
|
|
16
|
+
* @returns {number}
|
|
17
|
+
*/
|
|
18
|
+
export function scoreBullet(bullet, queryTerms = []) {
|
|
19
|
+
const text = String(bullet?.text || '').toLowerCase();
|
|
20
|
+
const topic = String(bullet?.topic || '').toLowerCase();
|
|
21
|
+
const tier = normalizeTier(bullet?.tier || bullet?.section || 'long_term');
|
|
22
|
+
const status = normalizeStatus(bullet?.status || inferStatusFromSection(normalizeTierToSection(tier)));
|
|
23
|
+
if (!text) return 0;
|
|
24
|
+
|
|
25
|
+
let score = 0;
|
|
26
|
+
for (const term of queryTerms) {
|
|
27
|
+
if (!term) continue;
|
|
28
|
+
if (text.includes(term)) score += 2;
|
|
29
|
+
if (topic.includes(term)) score += 1;
|
|
30
|
+
}
|
|
31
|
+
if (tier === 'working') score += 2;
|
|
32
|
+
if (tier === 'long_term') score += 1;
|
|
33
|
+
if (status === 'active') score += 2;
|
|
34
|
+
if (status === 'uncertain') score -= 1;
|
|
35
|
+
if (status === 'expired' || status === 'superseded' || tier === 'history') score -= 3;
|
|
36
|
+
if (bullet?.source === 'user_statement') score += 2;
|
|
37
|
+
if (bullet?.source === 'inference') score -= 1;
|
|
38
|
+
if (bullet?.confidence === 'high') score += 1;
|
|
39
|
+
if (bullet?.confidence === 'low') score -= 1;
|
|
40
|
+
return score;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @param {string} query
|
|
45
|
+
* @returns {string[]}
|
|
46
|
+
*/
|
|
47
|
+
export function tokenizeQuery(query) {
|
|
48
|
+
return String(query || '')
|
|
49
|
+
.toLowerCase()
|
|
50
|
+
.split(/[^a-z0-9]+/)
|
|
51
|
+
.map((t) => t.trim())
|
|
52
|
+
.filter((t) => t.length >= 3);
|
|
53
|
+
}
|