@botbotgo/agent-harness 0.0.154 → 0.0.155
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 +9 -2
- package/README.zh.md +16 -2
- package/dist/config/catalogs/stores.yaml +3 -3
- package/dist/config/catalogs/vector-stores.yaml +8 -1
- package/dist/config/runtime/runtime-memory.yaml +17 -0
- package/dist/contracts/workspace.d.ts +6 -0
- package/dist/init-project.js +18 -0
- package/dist/package-version.d.ts +1 -1
- package/dist/package-version.js +1 -1
- package/dist/runtime/harness/system/mem0-ingestion-sync.d.ts +28 -2
- package/dist/runtime/harness/system/mem0-ingestion-sync.js +112 -1
- package/dist/runtime/harness/system/runtime-memory-manager.d.ts +90 -0
- package/dist/runtime/harness/system/runtime-memory-manager.js +371 -0
- package/dist/runtime/harness/system/runtime-memory-records.d.ts +1 -0
- package/dist/runtime/harness/system/runtime-memory-records.js +9 -0
- package/dist/runtime/harness/system/store.d.ts +27 -0
- package/dist/runtime/harness/system/store.js +96 -0
- package/dist/runtime/harness.d.ts +14 -0
- package/dist/runtime/harness.js +332 -45
- package/dist/runtime/support/runtime-factories.js +5 -1
- package/dist/runtime/support/vector-stores.js +97 -0
- package/dist/workspace/resource-compilers.js +19 -0
- package/dist/workspace/yaml-object-reader.js +2 -0
- package/package.json +1 -1
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { extractMessageText } from "../../../utils/message-content.js";
|
|
3
|
+
import { createResolvedModel } from "../../adapter/model/model-providers.js";
|
|
4
|
+
import { FileBackedStore } from "./store.js";
|
|
5
|
+
import { compileModel } from "../../../workspace/resource-compilers.js";
|
|
6
|
+
import { resolveRefId } from "../../../workspace/support/workspace-ref-utils.js";
|
|
7
|
+
const FORMATION_EVENT_TYPES = new Set(["run.state.changed", "approval.resolved"]);
|
|
8
|
+
function asRecord(value) {
|
|
9
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : undefined;
|
|
10
|
+
}
|
|
11
|
+
function asBoolean(value) {
|
|
12
|
+
return typeof value === "boolean" ? value : undefined;
|
|
13
|
+
}
|
|
14
|
+
function asPositiveInteger(value) {
|
|
15
|
+
return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : undefined;
|
|
16
|
+
}
|
|
17
|
+
function asNonEmptyString(value) {
|
|
18
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
|
|
19
|
+
}
|
|
20
|
+
function asMemoryScopes(value) {
|
|
21
|
+
if (!Array.isArray(value)) {
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
const scopes = value
|
|
25
|
+
.filter((item) => typeof item === "string")
|
|
26
|
+
.filter((item) => ["thread", "agent", "workspace", "user", "project"].includes(item));
|
|
27
|
+
return scopes.length > 0 ? Array.from(new Set(scopes)) : undefined;
|
|
28
|
+
}
|
|
29
|
+
function normalizeScopeList(scopes) {
|
|
30
|
+
return Array.from(new Set(scopes));
|
|
31
|
+
}
|
|
32
|
+
function asStrategy(value) {
|
|
33
|
+
return value === "rules" || value === "model" ? value : undefined;
|
|
34
|
+
}
|
|
35
|
+
export function readRuntimeMemoryFormationConfig(runtimeMemory, workspaceRoot) {
|
|
36
|
+
if (runtimeMemory?.enabled !== true) {
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
const formation = asRecord(runtimeMemory.formation);
|
|
40
|
+
const hotPath = asRecord(formation?.hotPath);
|
|
41
|
+
const manager = asRecord(formation?.manager);
|
|
42
|
+
const background = asRecord(formation?.background);
|
|
43
|
+
const ingestion = asRecord(runtimeMemory.ingestion);
|
|
44
|
+
const workspaceBaseName = path.basename(workspaceRoot) || "agent-harness";
|
|
45
|
+
const resolved = {
|
|
46
|
+
enabled: true,
|
|
47
|
+
hotPath: {
|
|
48
|
+
enabled: asBoolean(hotPath?.enabled) ?? true,
|
|
49
|
+
},
|
|
50
|
+
manager: {
|
|
51
|
+
enabled: asBoolean(manager?.enabled) ?? true,
|
|
52
|
+
strategy: asStrategy(manager?.strategy) ?? "rules",
|
|
53
|
+
modelRef: asNonEmptyString(manager?.modelRef),
|
|
54
|
+
maxContextRecords: asPositiveInteger(manager?.maxContextRecords) ?? 12,
|
|
55
|
+
},
|
|
56
|
+
background: {
|
|
57
|
+
enabled: asBoolean(background?.enabled) ?? true,
|
|
58
|
+
maxMessagesPerRun: asPositiveInteger(background?.maxMessagesPerRun) ?? asPositiveInteger(ingestion?.maxMessagesPerRun) ?? 40,
|
|
59
|
+
scopes: normalizeScopeList(asMemoryScopes(background?.scopes) ?? ["thread"]),
|
|
60
|
+
stateStorePath: asNonEmptyString(background?.stateStorePath) ?? `${workspaceBaseName}-memory-formation-state.json`,
|
|
61
|
+
writeOnApprovalResolution: asBoolean(background?.writeOnApprovalResolution) ?? asBoolean(ingestion?.writeOnApprovalResolution) ?? true,
|
|
62
|
+
writeOnRunCompletion: asBoolean(background?.writeOnRunCompletion) ?? asBoolean(ingestion?.writeOnRunCompletion) ?? true,
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
if (!resolved.hotPath.enabled && !resolved.background.enabled) {
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
return resolved;
|
|
69
|
+
}
|
|
70
|
+
function excerpt(message) {
|
|
71
|
+
if (!message?.content) {
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
const normalized = extractMessageText(message.content).replace(/\s+/g, " ").trim();
|
|
75
|
+
if (!normalized) {
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
return normalized.length > 240 ? `${normalized.slice(0, 237)}...` : normalized;
|
|
79
|
+
}
|
|
80
|
+
function summarizeApprovals(approvals) {
|
|
81
|
+
if (approvals.length === 0) {
|
|
82
|
+
return "No approvals were recorded in this run.";
|
|
83
|
+
}
|
|
84
|
+
return approvals
|
|
85
|
+
.map((approval) => `${approval.toolName} (${approval.status})`)
|
|
86
|
+
.join(", ");
|
|
87
|
+
}
|
|
88
|
+
export function createBackgroundMemoryCandidates(input) {
|
|
89
|
+
const latestUser = excerpt(input.messages.filter((message) => message.role === "user").at(-1));
|
|
90
|
+
const latestAssistant = excerpt(input.messages.filter((message) => message.role === "assistant").at(-1));
|
|
91
|
+
const userContext = latestUser ?? "No durable user context captured.";
|
|
92
|
+
const assistantContext = latestAssistant ?? "No durable assistant response captured.";
|
|
93
|
+
const approvals = summarizeApprovals(input.approvals);
|
|
94
|
+
const summarySeed = latestUser ?? latestAssistant ?? `Run ${input.runId}`;
|
|
95
|
+
const baseSummary = summarySeed.length > 120 ? `${summarySeed.slice(0, 117)}...` : summarySeed;
|
|
96
|
+
const sourceRefBase = `runtime://threads/${input.thread.threadId}/runs/${input.runId}/background-reflection`;
|
|
97
|
+
return input.scopes.map((scope) => ({
|
|
98
|
+
kind: "episodic",
|
|
99
|
+
scope,
|
|
100
|
+
sourceType: "runtime-reflection",
|
|
101
|
+
sourceRef: `${sourceRefBase}#${scope}`,
|
|
102
|
+
summary: `${baseSummary} (${scope})`,
|
|
103
|
+
content: [
|
|
104
|
+
`Run ${input.runId} completed for thread ${input.thread.threadId}.`,
|
|
105
|
+
`Trigger: ${input.trigger}.`,
|
|
106
|
+
`Latest user context: ${userContext}`,
|
|
107
|
+
`Latest assistant context: ${assistantContext}`,
|
|
108
|
+
`Approvals: ${approvals}`,
|
|
109
|
+
].join("\n"),
|
|
110
|
+
confidence: 0.72,
|
|
111
|
+
observedAt: input.recordedAt,
|
|
112
|
+
tags: ["background-reflection", "langmem-aligned", scope, input.trigger],
|
|
113
|
+
}));
|
|
114
|
+
}
|
|
115
|
+
function normalizeKind(value, fallback) {
|
|
116
|
+
if (value === "semantic" || value === "episodic" || value === "procedural") {
|
|
117
|
+
return value;
|
|
118
|
+
}
|
|
119
|
+
return fallback;
|
|
120
|
+
}
|
|
121
|
+
function normalizeScope(value, fallback) {
|
|
122
|
+
if (value === "thread" || value === "agent" || value === "workspace" || value === "user" || value === "project") {
|
|
123
|
+
return value;
|
|
124
|
+
}
|
|
125
|
+
return fallback;
|
|
126
|
+
}
|
|
127
|
+
function normalizeTags(value, fallback) {
|
|
128
|
+
if (!Array.isArray(value)) {
|
|
129
|
+
return fallback;
|
|
130
|
+
}
|
|
131
|
+
const tags = value.filter((item) => typeof item === "string" && item.trim().length > 0);
|
|
132
|
+
return tags.length > 0 ? tags : fallback;
|
|
133
|
+
}
|
|
134
|
+
function normalizeConfidence(value, fallback) {
|
|
135
|
+
if (typeof value !== "number" || Number.isNaN(value)) {
|
|
136
|
+
return fallback;
|
|
137
|
+
}
|
|
138
|
+
return Math.min(1, Math.max(0, value));
|
|
139
|
+
}
|
|
140
|
+
function extractText(value) {
|
|
141
|
+
if (typeof value === "string") {
|
|
142
|
+
return value;
|
|
143
|
+
}
|
|
144
|
+
if (typeof value === "object" && value !== null && "content" in value && typeof value.content === "string") {
|
|
145
|
+
return String(value.content);
|
|
146
|
+
}
|
|
147
|
+
return undefined;
|
|
148
|
+
}
|
|
149
|
+
function tryParseJsonObject(text) {
|
|
150
|
+
try {
|
|
151
|
+
const parsed = JSON.parse(text);
|
|
152
|
+
return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? parsed : null;
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
const match = text.match(/\{[\s\S]*\}/);
|
|
156
|
+
if (!match) {
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
try {
|
|
160
|
+
const parsed = JSON.parse(match[0]);
|
|
161
|
+
return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? parsed : null;
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
function renderManagerPrompt(input) {
|
|
169
|
+
const existing = input.existingRecords.length === 0
|
|
170
|
+
? "(none)"
|
|
171
|
+
: input.existingRecords
|
|
172
|
+
.map((record) => `- scope=${record.scope}; kind=${record.kind}; summary=${record.summary}; status=${record.status}; content=${record.content.replace(/\s+/g, " ").slice(0, 220)}`)
|
|
173
|
+
.join("\n");
|
|
174
|
+
return [
|
|
175
|
+
"You are the runtime memory manager.",
|
|
176
|
+
"Decide whether a candidate should be stored as durable memory and refine it if appropriate.",
|
|
177
|
+
"Return JSON only.",
|
|
178
|
+
"",
|
|
179
|
+
"Rules:",
|
|
180
|
+
'- Store only durable reusable knowledge. Reject transient chatter, scratchpad, or duplication without added value.',
|
|
181
|
+
'- Prefer semantic/episodic/procedural kinds only.',
|
|
182
|
+
'- Prefer scopes thread/agent/workspace/user/project only.',
|
|
183
|
+
'- If the candidate should not be stored, return {"store": false, "reason": "..."}',
|
|
184
|
+
'- If it should be stored, return {"store": true, "content": "...", "summary": "...", "kind": "...", "scope": "...", "tags": ["..."], "confidence": 0.0}',
|
|
185
|
+
"",
|
|
186
|
+
`threadId=${input.threadId}`,
|
|
187
|
+
`runId=${input.runId}`,
|
|
188
|
+
"",
|
|
189
|
+
"Candidate:",
|
|
190
|
+
JSON.stringify(input.candidate, null, 2),
|
|
191
|
+
"",
|
|
192
|
+
"Existing relevant records:",
|
|
193
|
+
existing,
|
|
194
|
+
].join("\n");
|
|
195
|
+
}
|
|
196
|
+
export async function runModelMemoryManager(input) {
|
|
197
|
+
const resolvedModel = await createResolvedModel(input.model, input.modelResolver);
|
|
198
|
+
const invoker = resolvedModel;
|
|
199
|
+
if (typeof invoker.invoke !== "function") {
|
|
200
|
+
return input.candidates;
|
|
201
|
+
}
|
|
202
|
+
const transformed = [];
|
|
203
|
+
for (const candidate of input.candidates) {
|
|
204
|
+
const prompt = renderManagerPrompt({
|
|
205
|
+
candidate,
|
|
206
|
+
threadId: input.threadId,
|
|
207
|
+
runId: input.runId,
|
|
208
|
+
existingRecords: input.existingRecords,
|
|
209
|
+
});
|
|
210
|
+
const response = await invoker.invoke(prompt, {});
|
|
211
|
+
const parsed = tryParseJsonObject(extractText(response) ?? "");
|
|
212
|
+
if (!parsed) {
|
|
213
|
+
transformed.push(candidate);
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
if (parsed.store === false) {
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
transformed.push({
|
|
220
|
+
...candidate,
|
|
221
|
+
content: typeof parsed.content === "string" && parsed.content.trim().length > 0 ? parsed.content.trim() : candidate.content,
|
|
222
|
+
summary: typeof parsed.summary === "string" && parsed.summary.trim().length > 0 ? parsed.summary.trim() : candidate.summary,
|
|
223
|
+
kind: normalizeKind(parsed.kind, normalizeKind(candidate.kind, undefined)),
|
|
224
|
+
scope: normalizeScope(parsed.scope, normalizeScope(candidate.scope, undefined)),
|
|
225
|
+
tags: normalizeTags(parsed.tags, candidate.tags),
|
|
226
|
+
confidence: normalizeConfidence(parsed.confidence, candidate.confidence),
|
|
227
|
+
observedAt: candidate.observedAt ?? input.recordedAt,
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
return transformed;
|
|
231
|
+
}
|
|
232
|
+
export function createRuntimeMemoryManager(input) {
|
|
233
|
+
return {
|
|
234
|
+
async transform({ candidates, binding, threadId, runId, recordedAt, existingRecords }) {
|
|
235
|
+
if (input.config.manager.enabled !== true || candidates.length === 0) {
|
|
236
|
+
return candidates;
|
|
237
|
+
}
|
|
238
|
+
const contextRecords = existingRecords
|
|
239
|
+
.filter((record) => record.status === "active")
|
|
240
|
+
.slice(-input.config.manager.maxContextRecords);
|
|
241
|
+
if (input.config.manager.strategy !== "model") {
|
|
242
|
+
return candidates;
|
|
243
|
+
}
|
|
244
|
+
if (!binding.langchainAgentParams?.model) {
|
|
245
|
+
return candidates;
|
|
246
|
+
}
|
|
247
|
+
const primaryModel = (() => {
|
|
248
|
+
if (!input.config.manager.modelRef) {
|
|
249
|
+
return binding.langchainAgentParams.model;
|
|
250
|
+
}
|
|
251
|
+
const configured = input.workspace.models.get(resolveRefId(input.config.manager.modelRef));
|
|
252
|
+
return configured ? compileModel(configured) : binding.langchainAgentParams.model;
|
|
253
|
+
})();
|
|
254
|
+
return runModelMemoryManager({
|
|
255
|
+
workspace: input.workspace,
|
|
256
|
+
binding,
|
|
257
|
+
model: primaryModel,
|
|
258
|
+
candidates,
|
|
259
|
+
threadId,
|
|
260
|
+
runId,
|
|
261
|
+
recordedAt,
|
|
262
|
+
existingRecords: contextRecords,
|
|
263
|
+
modelResolver: input.modelResolver,
|
|
264
|
+
});
|
|
265
|
+
},
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
function fingerprintMessages(messages, scopes) {
|
|
269
|
+
const serialized = messages
|
|
270
|
+
.map((message) => `${message.role}\n${message.createdAt}\n${extractMessageText(message.content)}`)
|
|
271
|
+
.join("\n---\n");
|
|
272
|
+
return `${scopes.join(",")}::${serialized}`;
|
|
273
|
+
}
|
|
274
|
+
export class RuntimeMemoryFormationSync {
|
|
275
|
+
persistence;
|
|
276
|
+
config;
|
|
277
|
+
writer;
|
|
278
|
+
stateStore;
|
|
279
|
+
options;
|
|
280
|
+
pending = new Set();
|
|
281
|
+
syncChain = Promise.resolve();
|
|
282
|
+
name = "runtime-memory-formation-sync";
|
|
283
|
+
constructor(persistence, config, writer, runRoot, stateStore = new FileBackedStore(path.join(runRoot, config.background.stateStorePath)), options = {}) {
|
|
284
|
+
this.persistence = persistence;
|
|
285
|
+
this.config = config;
|
|
286
|
+
this.writer = writer;
|
|
287
|
+
this.stateStore = stateStore;
|
|
288
|
+
this.options = options;
|
|
289
|
+
}
|
|
290
|
+
shouldHandle(event) {
|
|
291
|
+
if (!this.config.background.enabled || !FORMATION_EVENT_TYPES.has(event.eventType)) {
|
|
292
|
+
return false;
|
|
293
|
+
}
|
|
294
|
+
if (event.eventType === "approval.resolved") {
|
|
295
|
+
return this.config.background.writeOnApprovalResolution;
|
|
296
|
+
}
|
|
297
|
+
return this.config.background.writeOnRunCompletion && event.payload.state === "completed";
|
|
298
|
+
}
|
|
299
|
+
async handleEvent(event) {
|
|
300
|
+
if (!this.shouldHandle(event)) {
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
const trigger = event.eventType === "approval.resolved" ? "approval.resolved" : "run.completed";
|
|
304
|
+
const task = this.syncChain
|
|
305
|
+
.then(() => this.reflectRun(event.threadId, event.runId, trigger, event.timestamp))
|
|
306
|
+
.catch(() => {
|
|
307
|
+
// Fail open: reflection should not block runtime progress.
|
|
308
|
+
});
|
|
309
|
+
this.syncChain = task
|
|
310
|
+
.catch(() => {
|
|
311
|
+
// Fail open: reflection should not block runtime progress.
|
|
312
|
+
})
|
|
313
|
+
.finally(() => {
|
|
314
|
+
this.pending.delete(task);
|
|
315
|
+
});
|
|
316
|
+
this.pending.add(task);
|
|
317
|
+
}
|
|
318
|
+
async reflectRun(threadId, runId, trigger, recordedAt) {
|
|
319
|
+
const [thread, run, allMessages, approvals] = await Promise.all([
|
|
320
|
+
this.persistence.getSession(threadId),
|
|
321
|
+
this.persistence.getRun(runId),
|
|
322
|
+
this.persistence.listThreadMessages(threadId, this.config.background.maxMessagesPerRun),
|
|
323
|
+
this.persistence.getRunApprovals(threadId, runId),
|
|
324
|
+
]);
|
|
325
|
+
if (!thread || !run) {
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
const messages = allMessages.filter((message) => message.runId === runId);
|
|
329
|
+
if (messages.length === 0) {
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
const fingerprint = fingerprintMessages(messages, this.config.background.scopes);
|
|
333
|
+
const namespace = ["memories", "formation", "threads", threadId, "runs"];
|
|
334
|
+
const cursor = await this.stateStore.get(namespace, runId);
|
|
335
|
+
const existing = cursor?.value;
|
|
336
|
+
if (existing?.fingerprint === fingerprint && existing.trigger === trigger) {
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
const candidates = createBackgroundMemoryCandidates({
|
|
340
|
+
thread,
|
|
341
|
+
runId,
|
|
342
|
+
agentId: run.agentId ?? thread.agentId,
|
|
343
|
+
trigger,
|
|
344
|
+
recordedAt,
|
|
345
|
+
messages,
|
|
346
|
+
approvals,
|
|
347
|
+
scopes: this.config.background.scopes,
|
|
348
|
+
});
|
|
349
|
+
if (candidates.length === 0) {
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
await this.writer({
|
|
353
|
+
candidates,
|
|
354
|
+
threadId,
|
|
355
|
+
runId,
|
|
356
|
+
agentId: run.agentId ?? thread.agentId,
|
|
357
|
+
userId: this.options.userId,
|
|
358
|
+
projectId: this.options.projectId,
|
|
359
|
+
recordedAt,
|
|
360
|
+
});
|
|
361
|
+
await this.stateStore.put(namespace, runId, {
|
|
362
|
+
fingerprint,
|
|
363
|
+
candidateCount: candidates.length,
|
|
364
|
+
syncedAt: new Date().toISOString(),
|
|
365
|
+
trigger,
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
async close() {
|
|
369
|
+
await Promise.allSettled(Array.from(this.pending));
|
|
370
|
+
}
|
|
371
|
+
}
|
|
@@ -14,6 +14,7 @@ type PersistMemoryRecordsOptions = {
|
|
|
14
14
|
export declare function renderMemoryRecordsMarkdown(title: string, records: MemoryRecord[]): string;
|
|
15
15
|
export declare function rebuildStructuredMemoryProjections(store: StoreLike, namespace: string[], title: string, scope: MemoryScope, maxEntries: number): Promise<void>;
|
|
16
16
|
export declare function listMemoryRecordsForScopes(store: StoreLike, scopes: MemoryScope[]): Promise<MemoryRecord[]>;
|
|
17
|
+
export declare function getMemoryRecord(store: StoreLike, scope: MemoryScope, recordId: string): Promise<MemoryRecord | null>;
|
|
17
18
|
export declare function persistStructuredMemoryRecords(options: PersistMemoryRecordsOptions): Promise<{
|
|
18
19
|
records: MemoryRecord[];
|
|
19
20
|
decisions: MemoryDecision[];
|
|
@@ -375,6 +375,15 @@ export async function listMemoryRecordsForScopes(store, scopes) {
|
|
|
375
375
|
const all = await Promise.all(scopes.map((scope) => listStoredRecords(store, scope)));
|
|
376
376
|
return all.flat();
|
|
377
377
|
}
|
|
378
|
+
export async function getMemoryRecord(store, scope, recordId) {
|
|
379
|
+
const entry = await store.get(buildScopeNamespace(scope), `${recordId}.json`);
|
|
380
|
+
const record = entry?.value;
|
|
381
|
+
if (typeof record !== "object" || !record || Array.isArray(record)) {
|
|
382
|
+
return null;
|
|
383
|
+
}
|
|
384
|
+
const candidate = record;
|
|
385
|
+
return typeof candidate.id === "string" && typeof candidate.content === "string" ? candidate : null;
|
|
386
|
+
}
|
|
378
387
|
export async function persistStructuredMemoryRecords(options) {
|
|
379
388
|
const existingRecords = await listStoredRecords(options.store);
|
|
380
389
|
const persistedRecords = [];
|
|
@@ -49,4 +49,31 @@ export declare class FileBackedStore {
|
|
|
49
49
|
delete(namespace: string[], key: string): Promise<void>;
|
|
50
50
|
listNamespaces(options?: Parameters<InMemoryStore["listNamespaces"]>[0]): Promise<string[][]>;
|
|
51
51
|
}
|
|
52
|
+
export declare class SqliteBackedStore {
|
|
53
|
+
readonly filePath: string;
|
|
54
|
+
private readonly db;
|
|
55
|
+
constructor(filePath: string);
|
|
56
|
+
batch(operations: Array<Record<string, unknown>>): Promise<readonly unknown[]>;
|
|
57
|
+
get(namespace: string[], key: string): Promise<{
|
|
58
|
+
value: unknown;
|
|
59
|
+
key: string;
|
|
60
|
+
namespace: string[];
|
|
61
|
+
createdAt: Date;
|
|
62
|
+
updatedAt: Date;
|
|
63
|
+
} | null>;
|
|
64
|
+
private getSync;
|
|
65
|
+
search(namespacePrefix: string[]): Promise<Array<{
|
|
66
|
+
value: unknown;
|
|
67
|
+
key: string;
|
|
68
|
+
namespace: string[];
|
|
69
|
+
createdAt: Date;
|
|
70
|
+
updatedAt: Date;
|
|
71
|
+
score?: number;
|
|
72
|
+
}>>;
|
|
73
|
+
put(namespace: string[], key: string, value: Record<string, any>, _index?: false | string[]): Promise<void>;
|
|
74
|
+
private putSync;
|
|
75
|
+
delete(namespace: string[], key: string): Promise<void>;
|
|
76
|
+
private deleteSync;
|
|
77
|
+
listNamespaces(): Promise<string[][]>;
|
|
78
|
+
}
|
|
52
79
|
export declare function createInMemoryStore(): StoreLike;
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { mkdirSync } from "node:fs";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
import { InMemoryStore } from "@langchain/langgraph";
|
|
5
|
+
import Database from "better-sqlite3";
|
|
6
|
+
const NAMESPACE_SEPARATOR = "\u001f";
|
|
4
7
|
function encodeValue(value) {
|
|
5
8
|
if (value instanceof Date) {
|
|
6
9
|
return {
|
|
@@ -50,6 +53,18 @@ function decodeValue(value) {
|
|
|
50
53
|
}
|
|
51
54
|
return value;
|
|
52
55
|
}
|
|
56
|
+
function serializeNamespace(namespace) {
|
|
57
|
+
return namespace.join(NAMESPACE_SEPARATOR);
|
|
58
|
+
}
|
|
59
|
+
function parseRow(row) {
|
|
60
|
+
return {
|
|
61
|
+
value: decodeValue(JSON.parse(row.value_json)),
|
|
62
|
+
key: row.key,
|
|
63
|
+
namespace: JSON.parse(row.namespace_json),
|
|
64
|
+
createdAt: new Date(row.created_at),
|
|
65
|
+
updatedAt: new Date(row.updated_at),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
53
68
|
export class FileBackedStore {
|
|
54
69
|
filePath;
|
|
55
70
|
delegate = new InMemoryStore();
|
|
@@ -131,6 +146,87 @@ export class FileBackedStore {
|
|
|
131
146
|
});
|
|
132
147
|
}
|
|
133
148
|
}
|
|
149
|
+
export class SqliteBackedStore {
|
|
150
|
+
filePath;
|
|
151
|
+
db;
|
|
152
|
+
constructor(filePath) {
|
|
153
|
+
this.filePath = filePath;
|
|
154
|
+
mkdirSync(path.dirname(filePath), { recursive: true });
|
|
155
|
+
this.db = new Database(filePath);
|
|
156
|
+
this.db.pragma("journal_mode = WAL");
|
|
157
|
+
this.db.exec(`
|
|
158
|
+
CREATE TABLE IF NOT EXISTS store_entries (
|
|
159
|
+
namespace TEXT NOT NULL,
|
|
160
|
+
namespace_json TEXT NOT NULL,
|
|
161
|
+
key TEXT NOT NULL,
|
|
162
|
+
value_json TEXT NOT NULL,
|
|
163
|
+
created_at TEXT NOT NULL,
|
|
164
|
+
updated_at TEXT NOT NULL,
|
|
165
|
+
PRIMARY KEY (namespace, key)
|
|
166
|
+
);
|
|
167
|
+
CREATE INDEX IF NOT EXISTS idx_store_entries_namespace ON store_entries(namespace);
|
|
168
|
+
`);
|
|
169
|
+
}
|
|
170
|
+
async batch(operations) {
|
|
171
|
+
const results = this.db.transaction((items) => items.map((operation) => {
|
|
172
|
+
const namespace = Array.isArray(operation.namespace) ? operation.namespace.filter((item) => typeof item === "string") : [];
|
|
173
|
+
const key = typeof operation.key === "string" ? operation.key : "";
|
|
174
|
+
if ("value" in operation) {
|
|
175
|
+
this.putSync(namespace, key, operation.value);
|
|
176
|
+
return undefined;
|
|
177
|
+
}
|
|
178
|
+
if (operation.delete === true) {
|
|
179
|
+
this.deleteSync(namespace, key);
|
|
180
|
+
return undefined;
|
|
181
|
+
}
|
|
182
|
+
return this.getSync(namespace, key);
|
|
183
|
+
}))(operations);
|
|
184
|
+
return results;
|
|
185
|
+
}
|
|
186
|
+
async get(namespace, key) {
|
|
187
|
+
return this.getSync(namespace, key);
|
|
188
|
+
}
|
|
189
|
+
getSync(namespace, key) {
|
|
190
|
+
const row = this.db.prepare(`SELECT namespace, namespace_json, key, value_json, created_at, updated_at
|
|
191
|
+
FROM store_entries
|
|
192
|
+
WHERE namespace = ? AND key = ?`).get(serializeNamespace(namespace), key);
|
|
193
|
+
return row ? parseRow(row) : null;
|
|
194
|
+
}
|
|
195
|
+
async search(namespacePrefix) {
|
|
196
|
+
const prefix = serializeNamespace(namespacePrefix);
|
|
197
|
+
const rows = this.db.prepare(`SELECT namespace, namespace_json, key, value_json, created_at, updated_at
|
|
198
|
+
FROM store_entries
|
|
199
|
+
WHERE namespace = ? OR namespace LIKE ?
|
|
200
|
+
ORDER BY namespace ASC, key ASC`).all(prefix, `${prefix}${NAMESPACE_SEPARATOR}%`);
|
|
201
|
+
return rows.map((row) => parseRow(row));
|
|
202
|
+
}
|
|
203
|
+
async put(namespace, key, value, _index) {
|
|
204
|
+
this.putSync(namespace, key, value);
|
|
205
|
+
}
|
|
206
|
+
putSync(namespace, key, value) {
|
|
207
|
+
const now = new Date().toISOString();
|
|
208
|
+
const serializedNamespace = serializeNamespace(namespace);
|
|
209
|
+
const encodedValue = JSON.stringify(encodeValue(value));
|
|
210
|
+
this.db.prepare(`INSERT INTO store_entries (namespace, namespace_json, key, value_json, created_at, updated_at)
|
|
211
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
212
|
+
ON CONFLICT(namespace, key) DO UPDATE SET
|
|
213
|
+
namespace_json = excluded.namespace_json,
|
|
214
|
+
value_json = excluded.value_json,
|
|
215
|
+
updated_at = excluded.updated_at`).run(serializedNamespace, JSON.stringify(namespace), key, encodedValue, now, now);
|
|
216
|
+
}
|
|
217
|
+
async delete(namespace, key) {
|
|
218
|
+
this.deleteSync(namespace, key);
|
|
219
|
+
}
|
|
220
|
+
deleteSync(namespace, key) {
|
|
221
|
+
this.db.prepare(`DELETE FROM store_entries WHERE namespace = ? AND key = ?`).run(serializeNamespace(namespace), key);
|
|
222
|
+
}
|
|
223
|
+
async listNamespaces() {
|
|
224
|
+
const rows = this.db.prepare(`SELECT DISTINCT namespace_json
|
|
225
|
+
FROM store_entries
|
|
226
|
+
ORDER BY namespace ASC`).all();
|
|
227
|
+
return rows.map((row) => JSON.parse(row.namespace_json));
|
|
228
|
+
}
|
|
229
|
+
}
|
|
134
230
|
export function createInMemoryStore() {
|
|
135
231
|
return new InMemoryStore();
|
|
136
232
|
}
|
|
@@ -29,6 +29,11 @@ export declare class AgentHarnessRuntime {
|
|
|
29
29
|
private readonly unregisterRuntimeMemorySync;
|
|
30
30
|
private readonly mem0IngestionSync;
|
|
31
31
|
private readonly unregisterMem0IngestionSync;
|
|
32
|
+
private readonly mem0SemanticRecall;
|
|
33
|
+
private readonly runtimeMemoryFormationConfig;
|
|
34
|
+
private readonly runtimeMemoryManager;
|
|
35
|
+
private readonly runtimeMemoryFormationSync;
|
|
36
|
+
private readonly unregisterRuntimeMemoryFormationSync;
|
|
32
37
|
private readonly resolvedRuntimeAdapterOptions;
|
|
33
38
|
private readonly healthMonitor;
|
|
34
39
|
private readonly recoveryConfig;
|
|
@@ -43,6 +48,7 @@ export declare class AgentHarnessRuntime {
|
|
|
43
48
|
private closed;
|
|
44
49
|
private readonly backgroundEventRuntime;
|
|
45
50
|
private readonly runtimeEventOperations;
|
|
51
|
+
private resolveRuntimeMemoryVectorStore;
|
|
46
52
|
private defaultRunRoot;
|
|
47
53
|
private getDefaultRuntimeEntryAgentId;
|
|
48
54
|
private resolveSelectedAgentId;
|
|
@@ -99,6 +105,14 @@ export declare class AgentHarnessRuntime {
|
|
|
99
105
|
private resolveRecallScopes;
|
|
100
106
|
private matchesRecallScope;
|
|
101
107
|
private getMemoryScopeBoost;
|
|
108
|
+
private memoryFreshnessBoost;
|
|
109
|
+
private scoreStructuredRecord;
|
|
110
|
+
private normalizeMemoryDedupKey;
|
|
111
|
+
private inferMem0MemoryKind;
|
|
112
|
+
private inferMem0MemoryScope;
|
|
113
|
+
private createMem0MemoryRecord;
|
|
114
|
+
private rankRecallCandidates;
|
|
115
|
+
private rebuildRuntimeMemoryVectorIndex;
|
|
102
116
|
private buildRuntimeMemoryContext;
|
|
103
117
|
private persistRuntimeMemoryCandidates;
|
|
104
118
|
private persistStructuredMemoryCandidates;
|