@botbotgo/agent-harness 0.0.154 → 0.0.156
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 +34 -8
- package/README.zh.md +41 -8
- package/dist/api.d.ts +5 -2
- package/dist/api.js +9 -0
- 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/runtime.d.ts +31 -0
- package/dist/contracts/workspace.d.ts +6 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- 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 +4 -0
- package/dist/runtime/harness/system/runtime-memory-records.js +57 -2
- 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 +21 -1
- package/dist/runtime/harness.js +469 -45
- package/dist/runtime/support/runtime-factories.js +5 -1
- package/dist/runtime/support/vector-stores.js +97 -0
- package/dist/workspace/object-loader.js +9 -44
- package/dist/workspace/resource-compilers.js +19 -0
- package/dist/workspace/yaml-object-reader.d.ts +0 -4
- package/dist/workspace/yaml-object-reader.js +6 -32
- package/package.json +1 -1
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import type { CompiledAgentBinding, CompiledModel, HarnessEvent, HarnessEventProjection, InternalApprovalRecord, MemoryCandidate, MemoryRecord, MemoryScope, ThreadSummary, TranscriptMessage, WorkspaceBundle } from "../../../contracts/types.js";
|
|
2
|
+
import type { RuntimePersistence } from "../../../persistence/types.js";
|
|
3
|
+
import { type StoreLike } from "./store.js";
|
|
4
|
+
export type ResolvedRuntimeMemoryFormationConfig = {
|
|
5
|
+
enabled: true;
|
|
6
|
+
hotPath: {
|
|
7
|
+
enabled: boolean;
|
|
8
|
+
};
|
|
9
|
+
manager: {
|
|
10
|
+
enabled: boolean;
|
|
11
|
+
strategy: "rules" | "model";
|
|
12
|
+
modelRef?: string;
|
|
13
|
+
maxContextRecords: number;
|
|
14
|
+
};
|
|
15
|
+
background: {
|
|
16
|
+
enabled: boolean;
|
|
17
|
+
maxMessagesPerRun: number;
|
|
18
|
+
scopes: MemoryScope[];
|
|
19
|
+
stateStorePath: string;
|
|
20
|
+
writeOnApprovalResolution: boolean;
|
|
21
|
+
writeOnRunCompletion: boolean;
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
type RuntimeMemoryFormationWriter = (input: {
|
|
25
|
+
candidates: MemoryCandidate[];
|
|
26
|
+
threadId: string;
|
|
27
|
+
runId: string;
|
|
28
|
+
agentId: string;
|
|
29
|
+
userId?: string;
|
|
30
|
+
projectId?: string;
|
|
31
|
+
recordedAt: string;
|
|
32
|
+
}) => Promise<void>;
|
|
33
|
+
type RuntimeMemoryFormationOptions = {
|
|
34
|
+
userId?: string;
|
|
35
|
+
projectId?: string;
|
|
36
|
+
};
|
|
37
|
+
type RuntimeMemoryManagerLike = {
|
|
38
|
+
transform(input: {
|
|
39
|
+
candidates: MemoryCandidate[];
|
|
40
|
+
binding: CompiledAgentBinding;
|
|
41
|
+
threadId: string;
|
|
42
|
+
runId: string;
|
|
43
|
+
recordedAt: string;
|
|
44
|
+
existingRecords: MemoryRecord[];
|
|
45
|
+
}): Promise<MemoryCandidate[]>;
|
|
46
|
+
};
|
|
47
|
+
export declare function readRuntimeMemoryFormationConfig(runtimeMemory: Record<string, unknown> | undefined, workspaceRoot: string): ResolvedRuntimeMemoryFormationConfig | undefined;
|
|
48
|
+
export declare function createBackgroundMemoryCandidates(input: {
|
|
49
|
+
thread: ThreadSummary;
|
|
50
|
+
runId: string;
|
|
51
|
+
agentId: string;
|
|
52
|
+
trigger: "approval.resolved" | "run.completed";
|
|
53
|
+
recordedAt: string;
|
|
54
|
+
messages: TranscriptMessage[];
|
|
55
|
+
approvals: InternalApprovalRecord[];
|
|
56
|
+
scopes: MemoryScope[];
|
|
57
|
+
}): MemoryCandidate[];
|
|
58
|
+
export declare function runModelMemoryManager(input: {
|
|
59
|
+
workspace: WorkspaceBundle;
|
|
60
|
+
binding: CompiledAgentBinding;
|
|
61
|
+
model: CompiledModel;
|
|
62
|
+
candidates: MemoryCandidate[];
|
|
63
|
+
threadId: string;
|
|
64
|
+
runId: string;
|
|
65
|
+
recordedAt: string;
|
|
66
|
+
existingRecords: MemoryRecord[];
|
|
67
|
+
modelResolver?: (modelId: string) => unknown;
|
|
68
|
+
}): Promise<MemoryCandidate[]>;
|
|
69
|
+
export declare function createRuntimeMemoryManager(input: {
|
|
70
|
+
workspace: WorkspaceBundle;
|
|
71
|
+
binding: CompiledAgentBinding;
|
|
72
|
+
config: ResolvedRuntimeMemoryFormationConfig;
|
|
73
|
+
modelResolver?: (modelId: string) => unknown;
|
|
74
|
+
}): RuntimeMemoryManagerLike;
|
|
75
|
+
export declare class RuntimeMemoryFormationSync implements HarnessEventProjection {
|
|
76
|
+
private readonly persistence;
|
|
77
|
+
private readonly config;
|
|
78
|
+
private readonly writer;
|
|
79
|
+
private readonly stateStore;
|
|
80
|
+
private readonly options;
|
|
81
|
+
private readonly pending;
|
|
82
|
+
private syncChain;
|
|
83
|
+
readonly name = "runtime-memory-formation-sync";
|
|
84
|
+
constructor(persistence: RuntimePersistence, config: ResolvedRuntimeMemoryFormationConfig, writer: RuntimeMemoryFormationWriter, runRoot: string, stateStore?: StoreLike, options?: RuntimeMemoryFormationOptions);
|
|
85
|
+
shouldHandle(event: HarnessEvent): boolean;
|
|
86
|
+
handleEvent(event: HarnessEvent): Promise<void>;
|
|
87
|
+
private reflectRun;
|
|
88
|
+
close(): Promise<void>;
|
|
89
|
+
}
|
|
90
|
+
export {};
|
|
@@ -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,10 @@ 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>;
|
|
18
|
+
export declare function findMemoryRecordById(store: StoreLike, recordId: string): Promise<MemoryRecord | null>;
|
|
19
|
+
export declare function updateMemoryRecord(store: StoreLike, record: MemoryRecord, updatedAt: string): Promise<MemoryRecord>;
|
|
20
|
+
export declare function removeMemoryRecord(store: StoreLike, scope: MemoryScope, recordId: string): Promise<MemoryRecord | null>;
|
|
17
21
|
export declare function persistStructuredMemoryRecords(options: PersistMemoryRecordsOptions): Promise<{
|
|
18
22
|
records: MemoryRecord[];
|
|
19
23
|
decisions: MemoryDecision[];
|
|
@@ -34,6 +34,12 @@ function createCanonicalKey(candidate, kind, scope) {
|
|
|
34
34
|
function buildScopeNamespace(scope) {
|
|
35
35
|
return ["memories", "records", scope];
|
|
36
36
|
}
|
|
37
|
+
function buildCanonicalIndexNamespace(scope) {
|
|
38
|
+
return ["memories", "indexes", "canonical", scope];
|
|
39
|
+
}
|
|
40
|
+
function buildSourceRefIndexNamespace(scope) {
|
|
41
|
+
return ["memories", "indexes", "source-ref", scope];
|
|
42
|
+
}
|
|
37
43
|
function normalizeTextForCompare(value) {
|
|
38
44
|
return value.toLowerCase().replace(/\s+/g, " ").trim();
|
|
39
45
|
}
|
|
@@ -312,7 +318,7 @@ function evaluateDecision(existing, incoming, recordedAt) {
|
|
|
312
318
|
async function putRecordWithIndexes(store, record, updatedAt) {
|
|
313
319
|
const canonicalKeyId = createFingerprint(record.canonicalKey);
|
|
314
320
|
await store.put(buildScopeNamespace(record.scope), `${record.id}.json`, record);
|
|
315
|
-
await store.put(
|
|
321
|
+
await store.put(buildCanonicalIndexNamespace(record.scope), `${canonicalKeyId}-${record.id}.json`, {
|
|
316
322
|
canonicalKey: record.canonicalKey,
|
|
317
323
|
recordId: record.id,
|
|
318
324
|
scope: record.scope,
|
|
@@ -321,7 +327,7 @@ async function putRecordWithIndexes(store, record, updatedAt) {
|
|
|
321
327
|
});
|
|
322
328
|
await Promise.all(record.sourceRefs.map((sourceRef) => {
|
|
323
329
|
const sourceRefId = createFingerprint(sourceRef);
|
|
324
|
-
return store.put(
|
|
330
|
+
return store.put(buildSourceRefIndexNamespace(record.scope), `${sourceRefId}-${record.id}.json`, {
|
|
325
331
|
sourceRef,
|
|
326
332
|
recordId: record.id,
|
|
327
333
|
scope: record.scope,
|
|
@@ -375,6 +381,55 @@ export async function listMemoryRecordsForScopes(store, scopes) {
|
|
|
375
381
|
const all = await Promise.all(scopes.map((scope) => listStoredRecords(store, scope)));
|
|
376
382
|
return all.flat();
|
|
377
383
|
}
|
|
384
|
+
export async function getMemoryRecord(store, scope, recordId) {
|
|
385
|
+
const entry = await store.get(buildScopeNamespace(scope), `${recordId}.json`);
|
|
386
|
+
const record = entry?.value;
|
|
387
|
+
if (typeof record !== "object" || !record || Array.isArray(record)) {
|
|
388
|
+
return null;
|
|
389
|
+
}
|
|
390
|
+
const candidate = record;
|
|
391
|
+
return typeof candidate.id === "string" && typeof candidate.content === "string" ? candidate : null;
|
|
392
|
+
}
|
|
393
|
+
export async function findMemoryRecordById(store, recordId) {
|
|
394
|
+
for (const scope of MEMORY_SCOPES) {
|
|
395
|
+
const record = await getMemoryRecord(store, scope, recordId);
|
|
396
|
+
if (record) {
|
|
397
|
+
return record;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
return null;
|
|
401
|
+
}
|
|
402
|
+
async function deleteRecordIndexes(store, record) {
|
|
403
|
+
const [canonicalEntries, sourceRefEntries] = await Promise.all([
|
|
404
|
+
store.search(buildCanonicalIndexNamespace(record.scope)),
|
|
405
|
+
store.search(buildSourceRefIndexNamespace(record.scope)),
|
|
406
|
+
]);
|
|
407
|
+
await Promise.all([
|
|
408
|
+
...canonicalEntries
|
|
409
|
+
.filter((entry) => entry.value?.recordId === record.id)
|
|
410
|
+
.map((entry) => store.delete(entry.namespace, entry.key)),
|
|
411
|
+
...sourceRefEntries
|
|
412
|
+
.filter((entry) => entry.value?.recordId === record.id)
|
|
413
|
+
.map((entry) => store.delete(entry.namespace, entry.key)),
|
|
414
|
+
]);
|
|
415
|
+
}
|
|
416
|
+
export async function updateMemoryRecord(store, record, updatedAt) {
|
|
417
|
+
const existing = await getMemoryRecord(store, record.scope, record.id);
|
|
418
|
+
if (existing) {
|
|
419
|
+
await deleteRecordIndexes(store, existing);
|
|
420
|
+
}
|
|
421
|
+
await putRecordWithIndexes(store, record, updatedAt);
|
|
422
|
+
return record;
|
|
423
|
+
}
|
|
424
|
+
export async function removeMemoryRecord(store, scope, recordId) {
|
|
425
|
+
const existing = await getMemoryRecord(store, scope, recordId);
|
|
426
|
+
if (!existing) {
|
|
427
|
+
return null;
|
|
428
|
+
}
|
|
429
|
+
await deleteRecordIndexes(store, existing);
|
|
430
|
+
await store.delete(buildScopeNamespace(scope), `${recordId}.json`);
|
|
431
|
+
return existing;
|
|
432
|
+
}
|
|
378
433
|
export async function persistStructuredMemoryRecords(options) {
|
|
379
434
|
const existingRecords = await listStoredRecords(options.store);
|
|
380
435
|
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;
|