audrey 0.3.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/LICENSE +21 -0
- package/README.md +493 -0
- package/mcp-server/index.js +155 -0
- package/mcp-server/register.sh +30 -0
- package/package.json +74 -0
- package/src/audrey.js +179 -0
- package/src/causal.js +77 -0
- package/src/confidence.js +75 -0
- package/src/consolidate.js +219 -0
- package/src/db.js +234 -0
- package/src/decay.js +72 -0
- package/src/embedding.js +88 -0
- package/src/encode.js +46 -0
- package/src/index.js +12 -0
- package/src/introspect.js +44 -0
- package/src/llm.js +132 -0
- package/src/prompts.js +134 -0
- package/src/recall.js +260 -0
- package/src/rollback.js +33 -0
- package/src/ulid.js +11 -0
- package/src/utils.js +22 -0
- package/src/validate.js +150 -0
package/src/prompts.js
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { safeJsonParse } from './utils.js';
|
|
2
|
+
|
|
3
|
+
export function buildPrincipleExtractionPrompt(episodes) {
|
|
4
|
+
const episodeList = episodes.map((ep, i) => {
|
|
5
|
+
const tags = safeJsonParse(ep.tags, []);
|
|
6
|
+
return `Episode ${i + 1}:
|
|
7
|
+
- Content: ${ep.content}
|
|
8
|
+
- Source: ${ep.source}
|
|
9
|
+
- Date: ${ep.created_at}
|
|
10
|
+
- Tags: ${tags.length > 0 ? tags.join(', ') : 'none'}`;
|
|
11
|
+
}).join('\n\n');
|
|
12
|
+
|
|
13
|
+
return [
|
|
14
|
+
{
|
|
15
|
+
role: 'system',
|
|
16
|
+
content: `You are performing principleExtraction for a memory system. Given a cluster of related episodic memories, extract a generalized principle or procedure.
|
|
17
|
+
|
|
18
|
+
Respond with ONLY valid JSON in this exact format:
|
|
19
|
+
{
|
|
20
|
+
"content": "The generalized principle expressed as a clear, actionable statement",
|
|
21
|
+
"type": "semantic or procedural — semantic for factual principles, procedural for how-to/workflow knowledge",
|
|
22
|
+
"conditions": ["boundary condition 1", "boundary condition 2"] or null if universally applicable
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
Rules:
|
|
26
|
+
- GENERALIZE, do not merely summarize or concatenate the episodes
|
|
27
|
+
- Identify boundary conditions: when does this principle NOT apply?
|
|
28
|
+
- Classify as "semantic" (facts, rules, patterns) or "procedural" (steps, workflows, strategies)
|
|
29
|
+
- Consider source diversity — principles from diverse sources are stronger
|
|
30
|
+
- Be concise but precise`,
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
role: 'user',
|
|
34
|
+
content: `Extract a principle from these ${episodes.length} related episodes:\n\n${episodeList}`,
|
|
35
|
+
},
|
|
36
|
+
];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function buildContradictionDetectionPrompt(newContent, existingContent) {
|
|
40
|
+
return [
|
|
41
|
+
{
|
|
42
|
+
role: 'system',
|
|
43
|
+
content: `You are performing contradictionDetection for a memory system. Given two claims, determine if they contradict each other.
|
|
44
|
+
|
|
45
|
+
Respond with ONLY valid JSON in this exact format:
|
|
46
|
+
{
|
|
47
|
+
"contradicts": true or false,
|
|
48
|
+
"explanation": "Brief explanation of why these do or do not contradict",
|
|
49
|
+
"resolution": "new_wins" or "existing_wins" or "context_dependent" or null if no contradiction,
|
|
50
|
+
"conditions": { "new": "context where new claim is true", "existing": "context where existing claim is true" } or null
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
Rules:
|
|
54
|
+
- Two claims contradict if they cannot both be true in the same context
|
|
55
|
+
- If both can be true under different conditions, set resolution to "context_dependent" and specify conditions
|
|
56
|
+
- If one clearly supersedes the other, indicate which wins
|
|
57
|
+
- If unclear, set resolution to null (leave as open contradiction)`,
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
role: 'user',
|
|
61
|
+
content: `Compare these two claims for contradiction:
|
|
62
|
+
|
|
63
|
+
NEW CLAIM: ${newContent}
|
|
64
|
+
|
|
65
|
+
EXISTING CLAIM: ${existingContent}`,
|
|
66
|
+
},
|
|
67
|
+
];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function buildCausalArticulationPrompt(cause, effect) {
|
|
71
|
+
return [
|
|
72
|
+
{
|
|
73
|
+
role: 'system',
|
|
74
|
+
content: `You are performing causalArticulation for a memory system. Given a cause and effect, articulate the mechanism that connects them.
|
|
75
|
+
|
|
76
|
+
Respond with ONLY valid JSON in this exact format:
|
|
77
|
+
{
|
|
78
|
+
"mechanism": "A clear explanation of WHY the cause leads to the effect",
|
|
79
|
+
"linkType": "causal" or "correlational" or "temporal",
|
|
80
|
+
"confidence": 0.0 to 1.0,
|
|
81
|
+
"spurious": true or false
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
Rules:
|
|
85
|
+
- "causal": there is a clear mechanistic explanation for why A causes B
|
|
86
|
+
- "correlational": A and B co-occur but no clear mechanism (may share a common cause)
|
|
87
|
+
- "temporal": A happens before B but that may be coincidence
|
|
88
|
+
- If you cannot articulate a mechanism, classify as "correlational" or "temporal", NOT "causal"
|
|
89
|
+
- Set "spurious" to true if the correlation is likely coincidental
|
|
90
|
+
- Confidence reflects how certain you are about the link type classification`,
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
role: 'user',
|
|
94
|
+
content: `Analyze the causal relationship:
|
|
95
|
+
|
|
96
|
+
CAUSE: ${cause.content} (source: ${cause.source})
|
|
97
|
+
|
|
98
|
+
EFFECT: ${effect.content} (source: ${effect.source})`,
|
|
99
|
+
},
|
|
100
|
+
];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function buildContextResolutionPrompt(claimA, claimB, context) {
|
|
104
|
+
const contextSection = context
|
|
105
|
+
? `\n\nADDITIONAL CONTEXT: ${context}`
|
|
106
|
+
: '';
|
|
107
|
+
|
|
108
|
+
return [
|
|
109
|
+
{
|
|
110
|
+
role: 'system',
|
|
111
|
+
content: `You are performing contextResolution for a memory system. Given two contradicting claims, determine how to resolve the contradiction.
|
|
112
|
+
|
|
113
|
+
Respond with ONLY valid JSON in this exact format:
|
|
114
|
+
{
|
|
115
|
+
"resolution": "a_wins" or "b_wins" or "context_dependent",
|
|
116
|
+
"conditions": { "a": "context where claim A is true", "b": "context where claim B is true" } or null,
|
|
117
|
+
"explanation": "Brief explanation of the resolution"
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
Rules:
|
|
121
|
+
- If one claim is clearly more accurate/recent/well-sourced, it wins
|
|
122
|
+
- If both can be true in different contexts, mark as "context_dependent" and specify conditions
|
|
123
|
+
- Provide clear conditions that an agent could evaluate at retrieval time`,
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
role: 'user',
|
|
127
|
+
content: `Resolve this contradiction:
|
|
128
|
+
|
|
129
|
+
CLAIM A: ${claimA}
|
|
130
|
+
|
|
131
|
+
CLAIM B: ${claimB}${contextSection}`,
|
|
132
|
+
},
|
|
133
|
+
];
|
|
134
|
+
}
|
package/src/recall.js
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import { computeConfidence, DEFAULT_HALF_LIVES } from './confidence.js';
|
|
2
|
+
import { daysBetween, safeJsonParse } from './utils.js';
|
|
3
|
+
|
|
4
|
+
function computeEpisodicConfidence(ep, now) {
|
|
5
|
+
const ageDays = daysBetween(ep.created_at, now);
|
|
6
|
+
return computeConfidence({
|
|
7
|
+
sourceType: ep.source,
|
|
8
|
+
supportingCount: 1,
|
|
9
|
+
contradictingCount: 0,
|
|
10
|
+
ageDays,
|
|
11
|
+
halfLifeDays: DEFAULT_HALF_LIVES.episodic,
|
|
12
|
+
retrievalCount: 0,
|
|
13
|
+
daysSinceRetrieval: ageDays,
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function computeSemanticConfidence(sem, now) {
|
|
18
|
+
const ageDays = daysBetween(sem.created_at, now);
|
|
19
|
+
const daysSinceRetrieval = sem.last_reinforced_at
|
|
20
|
+
? daysBetween(sem.last_reinforced_at, now)
|
|
21
|
+
: ageDays;
|
|
22
|
+
return computeConfidence({
|
|
23
|
+
sourceType: 'tool-result',
|
|
24
|
+
supportingCount: sem.supporting_count || 0,
|
|
25
|
+
contradictingCount: sem.contradicting_count || 0,
|
|
26
|
+
ageDays,
|
|
27
|
+
halfLifeDays: DEFAULT_HALF_LIVES.semantic,
|
|
28
|
+
retrievalCount: sem.retrieval_count || 0,
|
|
29
|
+
daysSinceRetrieval,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function computeProceduralConfidence(proc, now) {
|
|
34
|
+
const ageDays = daysBetween(proc.created_at, now);
|
|
35
|
+
const daysSinceRetrieval = proc.last_reinforced_at
|
|
36
|
+
? daysBetween(proc.last_reinforced_at, now)
|
|
37
|
+
: ageDays;
|
|
38
|
+
return computeConfidence({
|
|
39
|
+
sourceType: 'tool-result',
|
|
40
|
+
supportingCount: proc.success_count || 0,
|
|
41
|
+
contradictingCount: proc.failure_count || 0,
|
|
42
|
+
ageDays,
|
|
43
|
+
halfLifeDays: DEFAULT_HALF_LIVES.procedural,
|
|
44
|
+
retrievalCount: proc.retrieval_count || 0,
|
|
45
|
+
daysSinceRetrieval,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function buildEpisodicEntry(ep, confidence, score, includeProvenance) {
|
|
50
|
+
const entry = {
|
|
51
|
+
id: ep.id,
|
|
52
|
+
content: ep.content,
|
|
53
|
+
type: 'episodic',
|
|
54
|
+
confidence,
|
|
55
|
+
score,
|
|
56
|
+
source: ep.source,
|
|
57
|
+
createdAt: ep.created_at,
|
|
58
|
+
};
|
|
59
|
+
if (includeProvenance) {
|
|
60
|
+
entry.provenance = {
|
|
61
|
+
source: ep.source,
|
|
62
|
+
sourceReliability: ep.source_reliability,
|
|
63
|
+
createdAt: ep.created_at,
|
|
64
|
+
supersedes: ep.supersedes || null,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
return entry;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function buildSemanticEntry(sem, confidence, score, includeProvenance) {
|
|
71
|
+
const entry = {
|
|
72
|
+
id: sem.id,
|
|
73
|
+
content: sem.content,
|
|
74
|
+
type: 'semantic',
|
|
75
|
+
confidence,
|
|
76
|
+
score,
|
|
77
|
+
source: 'consolidation',
|
|
78
|
+
state: sem.state,
|
|
79
|
+
createdAt: sem.created_at,
|
|
80
|
+
};
|
|
81
|
+
if (includeProvenance) {
|
|
82
|
+
entry.provenance = {
|
|
83
|
+
evidenceEpisodeIds: safeJsonParse(sem.evidence_episode_ids, []),
|
|
84
|
+
evidenceCount: sem.evidence_count || 0,
|
|
85
|
+
supportingCount: sem.supporting_count || 0,
|
|
86
|
+
contradictingCount: sem.contradicting_count || 0,
|
|
87
|
+
consolidationCheckpoint: sem.consolidation_checkpoint || null,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
return entry;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function buildProceduralEntry(proc, confidence, score, includeProvenance) {
|
|
94
|
+
const entry = {
|
|
95
|
+
id: proc.id,
|
|
96
|
+
content: proc.content,
|
|
97
|
+
type: 'procedural',
|
|
98
|
+
confidence,
|
|
99
|
+
score,
|
|
100
|
+
source: 'consolidation',
|
|
101
|
+
state: proc.state,
|
|
102
|
+
createdAt: proc.created_at,
|
|
103
|
+
};
|
|
104
|
+
if (includeProvenance) {
|
|
105
|
+
entry.provenance = {
|
|
106
|
+
evidenceEpisodeIds: safeJsonParse(proc.evidence_episode_ids, []),
|
|
107
|
+
successCount: proc.success_count || 0,
|
|
108
|
+
failureCount: proc.failure_count || 0,
|
|
109
|
+
triggerConditions: proc.trigger_conditions || null,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
return entry;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function knnEpisodic(db, queryBuffer, candidateK, now, minConfidence, includeProvenance) {
|
|
116
|
+
const rows = db.prepare(`
|
|
117
|
+
SELECT e.*, (1.0 - v.distance) AS similarity
|
|
118
|
+
FROM vec_episodes v
|
|
119
|
+
JOIN episodes e ON e.id = v.id
|
|
120
|
+
WHERE v.embedding MATCH ?
|
|
121
|
+
AND k = ?
|
|
122
|
+
AND e.superseded_by IS NULL
|
|
123
|
+
`).all(queryBuffer, candidateK);
|
|
124
|
+
|
|
125
|
+
const results = [];
|
|
126
|
+
for (const row of rows) {
|
|
127
|
+
const confidence = computeEpisodicConfidence(row, now);
|
|
128
|
+
if (confidence < minConfidence) continue;
|
|
129
|
+
const score = row.similarity * confidence;
|
|
130
|
+
results.push(buildEpisodicEntry(row, confidence, score, includeProvenance));
|
|
131
|
+
}
|
|
132
|
+
return results;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function knnSemantic(db, queryBuffer, candidateK, now, minConfidence, includeProvenance, includeDormant) {
|
|
136
|
+
let stateFilter;
|
|
137
|
+
if (includeDormant) {
|
|
138
|
+
stateFilter = "AND (v.state = 'active' OR v.state = 'context_dependent' OR v.state = 'dormant')";
|
|
139
|
+
} else {
|
|
140
|
+
stateFilter = "AND (v.state = 'active' OR v.state = 'context_dependent')";
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const rows = db.prepare(`
|
|
144
|
+
SELECT s.*, (1.0 - v.distance) AS similarity
|
|
145
|
+
FROM vec_semantics v
|
|
146
|
+
JOIN semantics s ON s.id = v.id
|
|
147
|
+
WHERE v.embedding MATCH ?
|
|
148
|
+
AND k = ?
|
|
149
|
+
${stateFilter}
|
|
150
|
+
`).all(queryBuffer, candidateK);
|
|
151
|
+
|
|
152
|
+
const results = [];
|
|
153
|
+
const matchedIds = [];
|
|
154
|
+
for (const row of rows) {
|
|
155
|
+
const confidence = computeSemanticConfidence(row, now);
|
|
156
|
+
if (confidence < minConfidence) continue;
|
|
157
|
+
const score = row.similarity * confidence;
|
|
158
|
+
matchedIds.push(row.id);
|
|
159
|
+
results.push(buildSemanticEntry(row, confidence, score, includeProvenance));
|
|
160
|
+
}
|
|
161
|
+
return { results, matchedIds };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function knnProcedural(db, queryBuffer, candidateK, now, minConfidence, includeProvenance, includeDormant) {
|
|
165
|
+
let stateFilter;
|
|
166
|
+
if (includeDormant) {
|
|
167
|
+
stateFilter = "AND (v.state = 'active' OR v.state = 'context_dependent' OR v.state = 'dormant')";
|
|
168
|
+
} else {
|
|
169
|
+
stateFilter = "AND (v.state = 'active' OR v.state = 'context_dependent')";
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const rows = db.prepare(`
|
|
173
|
+
SELECT p.*, (1.0 - v.distance) AS similarity
|
|
174
|
+
FROM vec_procedures v
|
|
175
|
+
JOIN procedures p ON p.id = v.id
|
|
176
|
+
WHERE v.embedding MATCH ?
|
|
177
|
+
AND k = ?
|
|
178
|
+
${stateFilter}
|
|
179
|
+
`).all(queryBuffer, candidateK);
|
|
180
|
+
|
|
181
|
+
const results = [];
|
|
182
|
+
const matchedIds = [];
|
|
183
|
+
for (const row of rows) {
|
|
184
|
+
const confidence = computeProceduralConfidence(row, now);
|
|
185
|
+
if (confidence < minConfidence) continue;
|
|
186
|
+
const score = row.similarity * confidence;
|
|
187
|
+
matchedIds.push(row.id);
|
|
188
|
+
results.push(buildProceduralEntry(row, confidence, score, includeProvenance));
|
|
189
|
+
}
|
|
190
|
+
return { results, matchedIds };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export async function* recallStream(db, embeddingProvider, query, options = {}) {
|
|
194
|
+
const {
|
|
195
|
+
minConfidence = 0,
|
|
196
|
+
types,
|
|
197
|
+
limit = 10,
|
|
198
|
+
includeProvenance = false,
|
|
199
|
+
includeDormant = false,
|
|
200
|
+
} = options;
|
|
201
|
+
|
|
202
|
+
const queryVector = await embeddingProvider.embed(query);
|
|
203
|
+
const queryBuffer = embeddingProvider.vectorToBuffer(queryVector);
|
|
204
|
+
const searchTypes = types || ['episodic', 'semantic', 'procedural'];
|
|
205
|
+
const now = new Date();
|
|
206
|
+
const candidateK = limit * 3;
|
|
207
|
+
|
|
208
|
+
const allResults = [];
|
|
209
|
+
|
|
210
|
+
if (searchTypes.includes('episodic')) {
|
|
211
|
+
const episodic = knnEpisodic(db, queryBuffer, candidateK, now, minConfidence, includeProvenance);
|
|
212
|
+
allResults.push(...episodic);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (searchTypes.includes('semantic')) {
|
|
216
|
+
const { results: semResults, matchedIds: semIds } =
|
|
217
|
+
knnSemantic(db, queryBuffer, candidateK, now, minConfidence, includeProvenance, includeDormant);
|
|
218
|
+
allResults.push(...semResults);
|
|
219
|
+
|
|
220
|
+
if (semIds.length > 0) {
|
|
221
|
+
const updateStmt = db.prepare(
|
|
222
|
+
'UPDATE semantics SET retrieval_count = retrieval_count + 1, last_reinforced_at = ? WHERE id = ?'
|
|
223
|
+
);
|
|
224
|
+
const nowISO = now.toISOString();
|
|
225
|
+
for (const id of semIds) {
|
|
226
|
+
updateStmt.run(nowISO, id);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (searchTypes.includes('procedural')) {
|
|
232
|
+
const { results: procResults, matchedIds: procIds } =
|
|
233
|
+
knnProcedural(db, queryBuffer, candidateK, now, minConfidence, includeProvenance, includeDormant);
|
|
234
|
+
allResults.push(...procResults);
|
|
235
|
+
|
|
236
|
+
if (procIds.length > 0) {
|
|
237
|
+
const updateStmt = db.prepare(
|
|
238
|
+
'UPDATE procedures SET retrieval_count = retrieval_count + 1, last_reinforced_at = ? WHERE id = ?'
|
|
239
|
+
);
|
|
240
|
+
const nowISO = now.toISOString();
|
|
241
|
+
for (const id of procIds) {
|
|
242
|
+
updateStmt.run(nowISO, id);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
allResults.sort((a, b) => b.score - a.score);
|
|
248
|
+
const top = allResults.slice(0, limit);
|
|
249
|
+
for (const entry of top) {
|
|
250
|
+
yield entry;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export async function recall(db, embeddingProvider, query, options = {}) {
|
|
255
|
+
const results = [];
|
|
256
|
+
for await (const entry of recallStream(db, embeddingProvider, query, options)) {
|
|
257
|
+
results.push(entry);
|
|
258
|
+
}
|
|
259
|
+
return results;
|
|
260
|
+
}
|
package/src/rollback.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { safeJsonParse } from './utils.js';
|
|
2
|
+
|
|
3
|
+
export function getConsolidationHistory(db) {
|
|
4
|
+
return db.prepare(`
|
|
5
|
+
SELECT id, checkpoint_cursor, input_episode_ids, output_memory_ids,
|
|
6
|
+
started_at, completed_at, status
|
|
7
|
+
FROM consolidation_runs ORDER BY started_at DESC
|
|
8
|
+
`).all();
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function rollbackConsolidation(db, runId) {
|
|
12
|
+
const run = db.prepare('SELECT * FROM consolidation_runs WHERE id = ?').get(runId);
|
|
13
|
+
if (!run) throw new Error(`Consolidation run not found: ${runId}`);
|
|
14
|
+
if (run.status === 'rolled_back') throw new Error(`Run already rolled back: ${runId}`);
|
|
15
|
+
|
|
16
|
+
const outputIds = safeJsonParse(run.output_memory_ids, []);
|
|
17
|
+
const inputIds = safeJsonParse(run.input_episode_ids, []);
|
|
18
|
+
|
|
19
|
+
const doRollback = db.transaction(() => {
|
|
20
|
+
const markSemantics = db.prepare('UPDATE semantics SET state = ? WHERE id = ?');
|
|
21
|
+
const markProcedures = db.prepare('UPDATE procedures SET state = ? WHERE id = ?');
|
|
22
|
+
for (const id of outputIds) {
|
|
23
|
+
markSemantics.run('rolled_back', id);
|
|
24
|
+
markProcedures.run('rolled_back', id);
|
|
25
|
+
}
|
|
26
|
+
const unmark = db.prepare('UPDATE episodes SET consolidated = 0 WHERE id = ?');
|
|
27
|
+
for (const id of inputIds) { unmark.run(id); }
|
|
28
|
+
db.prepare('UPDATE consolidation_runs SET status = ? WHERE id = ?').run('rolled_back', runId);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
doRollback();
|
|
32
|
+
return { rolledBackMemories: outputIds.length, restoredEpisodes: inputIds.length };
|
|
33
|
+
}
|
package/src/ulid.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { ulid } from 'ulid';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
|
|
4
|
+
export function generateId() {
|
|
5
|
+
return ulid();
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function generateDeterministicId(...parts) {
|
|
9
|
+
const input = JSON.stringify(parts);
|
|
10
|
+
return createHash('sha256').update(input).digest('hex').slice(0, 26);
|
|
11
|
+
}
|
package/src/utils.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export function cosineSimilarity(bufA, bufB, provider) {
|
|
2
|
+
const a = provider.bufferToVector(bufA);
|
|
3
|
+
const b = provider.bufferToVector(bufB);
|
|
4
|
+
let dot = 0, magA = 0, magB = 0;
|
|
5
|
+
for (let i = 0; i < a.length; i++) {
|
|
6
|
+
dot += a[i] * b[i];
|
|
7
|
+
magA += a[i] * a[i];
|
|
8
|
+
magB += b[i] * b[i];
|
|
9
|
+
}
|
|
10
|
+
const mag = Math.sqrt(magA) * Math.sqrt(magB);
|
|
11
|
+
return mag === 0 ? 0 : dot / mag;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function daysBetween(dateStr, now) {
|
|
15
|
+
return Math.max(0, (now.getTime() - new Date(dateStr).getTime()) / (1000 * 60 * 60 * 24));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function safeJsonParse(str, fallback = null) {
|
|
19
|
+
if (!str) return fallback;
|
|
20
|
+
try { return JSON.parse(str); }
|
|
21
|
+
catch { return fallback; }
|
|
22
|
+
}
|
package/src/validate.js
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { generateId } from './ulid.js';
|
|
2
|
+
import { safeJsonParse } from './utils.js';
|
|
3
|
+
import { buildContradictionDetectionPrompt } from './prompts.js';
|
|
4
|
+
|
|
5
|
+
const REINFORCEMENT_THRESHOLD = 0.85;
|
|
6
|
+
const CONTRADICTION_THRESHOLD = 0.60;
|
|
7
|
+
|
|
8
|
+
export async function validateMemory(db, embeddingProvider, episode, options = {}) {
|
|
9
|
+
const {
|
|
10
|
+
threshold = REINFORCEMENT_THRESHOLD,
|
|
11
|
+
contradictionThreshold = CONTRADICTION_THRESHOLD,
|
|
12
|
+
llmProvider,
|
|
13
|
+
} = options;
|
|
14
|
+
|
|
15
|
+
const episodeVector = await embeddingProvider.embed(episode.content);
|
|
16
|
+
const episodeBuffer = embeddingProvider.vectorToBuffer(episodeVector);
|
|
17
|
+
|
|
18
|
+
const nearestSemantic = db.prepare(`
|
|
19
|
+
SELECT s.*, (1.0 - v.distance) AS similarity
|
|
20
|
+
FROM vec_semantics v
|
|
21
|
+
JOIN semantics s ON s.id = v.id
|
|
22
|
+
WHERE v.embedding MATCH ?
|
|
23
|
+
AND k = 1
|
|
24
|
+
AND (v.state = 'active' OR v.state = 'context_dependent')
|
|
25
|
+
`).get(episodeBuffer);
|
|
26
|
+
|
|
27
|
+
let bestMatch = null;
|
|
28
|
+
let bestSimilarity = 0;
|
|
29
|
+
|
|
30
|
+
if (nearestSemantic) {
|
|
31
|
+
bestMatch = nearestSemantic;
|
|
32
|
+
bestSimilarity = nearestSemantic.similarity;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (bestMatch && bestSimilarity >= threshold) {
|
|
36
|
+
const evidenceIds = safeJsonParse(bestMatch.evidence_episode_ids, []);
|
|
37
|
+
if (!evidenceIds.includes(episode.id)) {
|
|
38
|
+
evidenceIds.push(episode.id);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const diversity = computeSourceDiversity(db, evidenceIds, episode);
|
|
42
|
+
|
|
43
|
+
const now = new Date().toISOString();
|
|
44
|
+
db.prepare(`
|
|
45
|
+
UPDATE semantics SET
|
|
46
|
+
supporting_count = supporting_count + 1,
|
|
47
|
+
evidence_episode_ids = ?,
|
|
48
|
+
evidence_count = ?,
|
|
49
|
+
source_type_diversity = ?,
|
|
50
|
+
last_reinforced_at = ?
|
|
51
|
+
WHERE id = ?
|
|
52
|
+
`).run(
|
|
53
|
+
JSON.stringify(evidenceIds),
|
|
54
|
+
evidenceIds.length,
|
|
55
|
+
diversity,
|
|
56
|
+
now,
|
|
57
|
+
bestMatch.id,
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
action: 'reinforced',
|
|
62
|
+
semanticId: bestMatch.id,
|
|
63
|
+
similarity: bestSimilarity,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (bestMatch && bestSimilarity >= contradictionThreshold && llmProvider) {
|
|
68
|
+
const messages = buildContradictionDetectionPrompt(episode.content, bestMatch.content);
|
|
69
|
+
const verdict = await llmProvider.json(messages);
|
|
70
|
+
|
|
71
|
+
if (verdict.contradicts) {
|
|
72
|
+
const resolution = verdict.resolution === 'context_dependent'
|
|
73
|
+
? { type: 'context_dependent', conditions: verdict.conditions, explanation: verdict.explanation }
|
|
74
|
+
: verdict.resolution
|
|
75
|
+
? { type: verdict.resolution, explanation: verdict.explanation }
|
|
76
|
+
: null;
|
|
77
|
+
|
|
78
|
+
const contradictionId = createContradiction(
|
|
79
|
+
db,
|
|
80
|
+
bestMatch.id,
|
|
81
|
+
'semantic',
|
|
82
|
+
episode.id,
|
|
83
|
+
'episodic',
|
|
84
|
+
resolution,
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
if (verdict.resolution === 'new_wins') {
|
|
88
|
+
db.prepare("UPDATE semantics SET state = 'disputed' WHERE id = ?").run(bestMatch.id);
|
|
89
|
+
} else if (verdict.resolution === 'context_dependent' && verdict.conditions) {
|
|
90
|
+
db.prepare("UPDATE semantics SET state = 'context_dependent', conditions = ? WHERE id = ?")
|
|
91
|
+
.run(JSON.stringify(verdict.conditions), bestMatch.id);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
action: 'contradiction',
|
|
96
|
+
contradictionId,
|
|
97
|
+
semanticId: bestMatch.id,
|
|
98
|
+
similarity: bestSimilarity,
|
|
99
|
+
resolution: verdict.resolution || null,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return { action: 'none' };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function computeSourceDiversity(db, evidenceIds, currentEpisode) {
|
|
108
|
+
const sourceTypes = new Set();
|
|
109
|
+
sourceTypes.add(currentEpisode.source);
|
|
110
|
+
|
|
111
|
+
if (evidenceIds.length > 0) {
|
|
112
|
+
const placeholders = evidenceIds.map(() => '?').join(',');
|
|
113
|
+
const rows = db.prepare(
|
|
114
|
+
`SELECT DISTINCT source FROM episodes WHERE id IN (${placeholders})`
|
|
115
|
+
).all(...evidenceIds);
|
|
116
|
+
for (const row of rows) {
|
|
117
|
+
sourceTypes.add(row.source);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return sourceTypes.size;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function createContradiction(db, claimAId, claimAType, claimBId, claimBType, resolution) {
|
|
125
|
+
const id = generateId();
|
|
126
|
+
const now = new Date().toISOString();
|
|
127
|
+
|
|
128
|
+
const state = resolution ? 'resolved' : 'open';
|
|
129
|
+
const resolvedAt = resolution ? now : null;
|
|
130
|
+
const resolutionJson = resolution ? JSON.stringify(resolution) : null;
|
|
131
|
+
|
|
132
|
+
db.prepare(`
|
|
133
|
+
INSERT INTO contradictions (id, claim_a_id, claim_a_type, claim_b_id, claim_b_type,
|
|
134
|
+
state, resolution, resolved_at, created_at)
|
|
135
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
136
|
+
`).run(id, claimAId, claimAType, claimBId, claimBType, state, resolutionJson, resolvedAt, now);
|
|
137
|
+
|
|
138
|
+
return id;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function reopenContradiction(db, contradictionId, newEvidenceId) {
|
|
142
|
+
const now = new Date().toISOString();
|
|
143
|
+
db.prepare(`
|
|
144
|
+
UPDATE contradictions SET
|
|
145
|
+
state = 'reopened',
|
|
146
|
+
reopen_evidence_id = ?,
|
|
147
|
+
reopened_at = ?
|
|
148
|
+
WHERE id = ?
|
|
149
|
+
`).run(newEvidenceId, now, contradictionId);
|
|
150
|
+
}
|