@botbotgo/agent-harness 0.0.152 → 0.0.153

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.
@@ -118,6 +118,40 @@ export type MemoryCandidate = {
118
118
  noStore?: boolean;
119
119
  provenance?: Record<string, unknown>;
120
120
  };
121
+ export type MemoryScope = "thread" | "agent" | "workspace" | "user" | "project";
122
+ export type MemoryRecordStatus = "active" | "stale" | "conflicted" | "archived" | "pending_review";
123
+ export type MemoryDecisionAction = "reject" | "store" | "merge" | "refresh" | "supersede" | "archive" | "review";
124
+ export type MemoryRecord = {
125
+ id: string;
126
+ canonicalKey: string;
127
+ kind: "semantic" | "episodic" | "procedural";
128
+ scope: MemoryScope;
129
+ content: string;
130
+ summary: string;
131
+ status: MemoryRecordStatus;
132
+ confidence: number;
133
+ createdAt: string;
134
+ observedAt: string;
135
+ lastConfirmedAt: string;
136
+ expiresAt?: string;
137
+ sourceType: string;
138
+ sourceRefs: string[];
139
+ tags: string[];
140
+ provenance: Record<string, unknown>;
141
+ revision: number;
142
+ supersedes: string[];
143
+ conflictsWith: string[];
144
+ };
145
+ export type MemoryDecision = {
146
+ action: MemoryDecisionAction;
147
+ reason: string;
148
+ recordId?: string;
149
+ kind?: MemoryRecord["kind"];
150
+ scope?: MemoryScope;
151
+ confidence?: number;
152
+ maintenance?: "none" | "dedupe" | "merge" | "review";
153
+ reviewRequired?: boolean;
154
+ };
121
155
  /**
122
156
  * Operator-facing projection of tool execution policy already compiled into a binding.
123
157
  * This summarizes existing timeout, retry, validation, and retry-safety hints without
@@ -1 +1 @@
1
- export declare const AGENT_HARNESS_VERSION = "0.0.151";
1
+ export declare const AGENT_HARNESS_VERSION = "0.0.152";
@@ -1 +1 @@
1
- export const AGENT_HARNESS_VERSION = "0.0.151";
1
+ export const AGENT_HARNESS_VERSION = "0.0.152";
@@ -0,0 +1,12 @@
1
+ import type { MemoryScope } from "../../../contracts/types.js";
2
+ import type { ResolvedRuntimeMemoryMaintenanceConfig } from "./runtime-memory-policy.js";
3
+ import type { StoreLike } from "./store.js";
4
+ export declare function consolidateStructuredMemoryScope(input: {
5
+ store: StoreLike;
6
+ namespace: string[];
7
+ scope: MemoryScope;
8
+ title: string;
9
+ maxEntries: number;
10
+ config: ResolvedRuntimeMemoryMaintenanceConfig | undefined;
11
+ now?: string;
12
+ }): Promise<void>;
@@ -0,0 +1,51 @@
1
+ import { listMemoryRecordsForScopes, rebuildStructuredMemoryProjections } from "./runtime-memory-records.js";
2
+ function normalizeText(value) {
3
+ return value.toLowerCase().replace(/\s+/g, " ").trim();
4
+ }
5
+ function sortByLastConfirmed(records) {
6
+ return [...records].sort((left, right) => left.lastConfirmedAt.localeCompare(right.lastConfirmedAt));
7
+ }
8
+ export async function consolidateStructuredMemoryScope(input) {
9
+ const now = input.now ?? new Date().toISOString();
10
+ const records = sortByLastConfirmed(await listMemoryRecordsForScopes(input.store, [input.scope]));
11
+ const updates = new Map();
12
+ if (input.config?.dedupe !== false) {
13
+ const activeRecords = records.filter((record) => record.status === "active");
14
+ const seen = new Map();
15
+ for (const record of activeRecords) {
16
+ const identity = `${record.canonicalKey}:${normalizeText(record.content)}`;
17
+ const prior = seen.get(identity);
18
+ if (!prior) {
19
+ seen.set(identity, record);
20
+ continue;
21
+ }
22
+ const archived = {
23
+ ...prior,
24
+ status: "archived",
25
+ lastConfirmedAt: now,
26
+ supersedes: Array.from(new Set([...prior.supersedes, record.id])),
27
+ revision: prior.revision + 1,
28
+ };
29
+ updates.set(archived.id, archived);
30
+ seen.set(identity, record);
31
+ }
32
+ }
33
+ if (typeof input.config?.maxAgeDays === "number" && input.config.maxAgeDays > 0) {
34
+ const threshold = Date.parse(now) - input.config.maxAgeDays * 24 * 60 * 60 * 1000;
35
+ for (const record of records) {
36
+ if (record.status !== "active") {
37
+ continue;
38
+ }
39
+ const updated = Date.parse(record.lastConfirmedAt);
40
+ if (Number.isFinite(updated) && updated < threshold) {
41
+ updates.set(record.id, {
42
+ ...record,
43
+ status: "stale",
44
+ revision: record.revision + 1,
45
+ });
46
+ }
47
+ }
48
+ }
49
+ await Promise.all(Array.from(updates.values()).map((record) => input.store.put(["memories", "records", input.scope], `${record.id}.json`, record)));
50
+ await rebuildStructuredMemoryProjections(input.store, input.namespace, input.title, input.scope, input.maxEntries);
51
+ }
@@ -12,7 +12,13 @@ export type ResolvedRuntimeMemoryPolicyConfig = {
12
12
  project: string;
13
13
  };
14
14
  };
15
+ export type ResolvedRuntimeMemoryMaintenanceConfig = {
16
+ enabled: true;
17
+ dedupe: boolean;
18
+ maxAgeDays?: number;
19
+ };
15
20
  export declare function normalizeLangMemMemoryKind(kind: string | undefined): "semantic" | "episodic" | "procedural";
16
21
  export declare function readRuntimeMemoryPolicyConfig(runtimeMemory: Record<string, unknown> | undefined, workspaceRoot: string): ResolvedRuntimeMemoryPolicyConfig | undefined;
22
+ export declare function readRuntimeMemoryMaintenanceConfig(runtimeMemory: Record<string, unknown> | undefined): ResolvedRuntimeMemoryMaintenanceConfig | undefined;
17
23
  export declare function resolveMemoryNamespace(template: string, values: Record<string, string | undefined>): string[];
18
24
  export declare function scoreMemoryText(query: string, content: string, scopeBoost?: number): number;
@@ -43,6 +43,21 @@ export function readRuntimeMemoryPolicyConfig(runtimeMemory, workspaceRoot) {
43
43
  },
44
44
  };
45
45
  }
46
+ export function readRuntimeMemoryMaintenanceConfig(runtimeMemory) {
47
+ if (runtimeMemory?.enabled !== true) {
48
+ return undefined;
49
+ }
50
+ const consolidation = asRecord(runtimeMemory.consolidation);
51
+ const decay = asRecord(consolidation?.decay);
52
+ return {
53
+ enabled: true,
54
+ dedupe: consolidation?.dedupe !== false,
55
+ ...(asBoolean(decay?.enabled) !== false && asPositiveInteger(decay?.maxAgeDays) ? { maxAgeDays: asPositiveInteger(decay?.maxAgeDays) } : {}),
56
+ };
57
+ }
58
+ function asBoolean(value) {
59
+ return typeof value === "boolean" ? value : undefined;
60
+ }
46
61
  export function resolveMemoryNamespace(template, values) {
47
62
  const rendered = template.replace(/\{([a-zA-Z0-9_]+)\}/g, (_match, key) => values[key] ?? key);
48
63
  return rendered
@@ -0,0 +1,18 @@
1
+ import type { MemoryCandidate, MemoryDecision, MemoryRecord, MemoryScope } from "../../../contracts/types.js";
2
+ import type { StoreLike } from "./store.js";
3
+ type PersistMemoryRecordsOptions = {
4
+ store: StoreLike;
5
+ candidates: MemoryCandidate[];
6
+ threadId: string;
7
+ runId: string;
8
+ agentId: string;
9
+ recordedAt: string;
10
+ };
11
+ export declare function renderMemoryRecordsMarkdown(title: string, records: MemoryRecord[]): string;
12
+ export declare function rebuildStructuredMemoryProjections(store: StoreLike, namespace: string[], title: string, scope: MemoryScope, maxEntries: number): Promise<void>;
13
+ export declare function listMemoryRecordsForScopes(store: StoreLike, scopes: MemoryScope[]): Promise<MemoryRecord[]>;
14
+ export declare function persistStructuredMemoryRecords(options: PersistMemoryRecordsOptions): Promise<{
15
+ records: MemoryRecord[];
16
+ decisions: MemoryDecision[];
17
+ }>;
18
+ export {};
@@ -0,0 +1,402 @@
1
+ import { createHash } from "node:crypto";
2
+ import { normalizeLangMemMemoryKind } from "./runtime-memory-policy.js";
3
+ const MEMORY_SCOPES = ["thread", "agent", "workspace", "user", "project"];
4
+ function normalizeScope(scope) {
5
+ if (scope === "agent" || scope === "workspace" || scope === "user" || scope === "project") {
6
+ return scope;
7
+ }
8
+ return "thread";
9
+ }
10
+ function normalizeSummary(candidate) {
11
+ const value = candidate.summary?.trim() || candidate.content.trim().split("\n")[0] || candidate.content.trim();
12
+ return value.slice(0, 240);
13
+ }
14
+ function createFingerprint(input) {
15
+ return createHash("sha256").update(input).digest("hex");
16
+ }
17
+ function normalizeConfidence(value) {
18
+ if (typeof value !== "number" || Number.isNaN(value)) {
19
+ return 0.5;
20
+ }
21
+ return Math.min(1, Math.max(0, value));
22
+ }
23
+ function normalizeSourceRef(value) {
24
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
25
+ }
26
+ function createCanonicalKey(candidate, kind, scope) {
27
+ const sourceRef = normalizeSourceRef(candidate.sourceRef);
28
+ if (sourceRef) {
29
+ return `${kind}:${scope}:source:${createFingerprint(sourceRef)}`;
30
+ }
31
+ const base = normalizeSummary(candidate).toLowerCase().replace(/\s+/g, " ").trim();
32
+ return `${kind}:${scope}:summary:${createFingerprint(base)}`;
33
+ }
34
+ function buildScopeNamespace(scope) {
35
+ return ["memories", "records", scope];
36
+ }
37
+ function normalizeTextForCompare(value) {
38
+ return value.toLowerCase().replace(/\s+/g, " ").trim();
39
+ }
40
+ function tokenize(value) {
41
+ return new Set(value
42
+ .toLowerCase()
43
+ .split(/[^a-z0-9_]+/i)
44
+ .map((token) => token.trim())
45
+ .filter((token) => token.length > 2));
46
+ }
47
+ function jaccardSimilarity(left, right) {
48
+ const leftTokens = tokenize(left);
49
+ const rightTokens = tokenize(right);
50
+ if (leftTokens.size === 0 || rightTokens.size === 0) {
51
+ return 0;
52
+ }
53
+ let intersection = 0;
54
+ for (const token of leftTokens) {
55
+ if (rightTokens.has(token)) {
56
+ intersection += 1;
57
+ }
58
+ }
59
+ const union = new Set([...leftTokens, ...rightTokens]).size;
60
+ return union === 0 ? 0 : intersection / union;
61
+ }
62
+ function hasNegationSignal(value) {
63
+ const normalized = ` ${normalizeTextForCompare(value)} `;
64
+ return (normalized.includes(" not ") ||
65
+ normalized.includes(" no ") ||
66
+ normalized.includes(" never ") ||
67
+ normalized.includes(" without ") ||
68
+ normalized.includes(" do not ") ||
69
+ normalized.includes(" does not ") ||
70
+ normalized.includes(" did not "));
71
+ }
72
+ function mergeUniqueStrings(left, right) {
73
+ return Array.from(new Set([...left, ...right].filter((item) => item.trim().length > 0)));
74
+ }
75
+ function mergeRecordContent(left, right) {
76
+ const normalizedLeft = normalizeTextForCompare(left);
77
+ const normalizedRight = normalizeTextForCompare(right);
78
+ if (normalizedLeft === normalizedRight) {
79
+ return left;
80
+ }
81
+ if (normalizedLeft.includes(normalizedRight)) {
82
+ return left;
83
+ }
84
+ if (normalizedRight.includes(normalizedLeft)) {
85
+ return right;
86
+ }
87
+ return `${left.trim()}\n\n${right.trim()}`;
88
+ }
89
+ function createMemoryRecord(candidate, options) {
90
+ const kind = normalizeLangMemMemoryKind(candidate.kind);
91
+ const scope = normalizeScope(candidate.scope);
92
+ const canonicalKey = createCanonicalKey(candidate, kind, scope);
93
+ const id = createFingerprint([
94
+ canonicalKey,
95
+ candidate.content.trim(),
96
+ options.threadId,
97
+ options.runId,
98
+ options.agentId,
99
+ options.recordedAt,
100
+ ].join("\n"));
101
+ const sourceRef = normalizeSourceRef(candidate.sourceRef);
102
+ return {
103
+ id,
104
+ canonicalKey,
105
+ kind,
106
+ scope,
107
+ content: candidate.content.trim(),
108
+ summary: normalizeSummary(candidate),
109
+ status: "active",
110
+ confidence: normalizeConfidence(candidate.confidence),
111
+ createdAt: options.recordedAt,
112
+ observedAt: candidate.observedAt ?? options.recordedAt,
113
+ lastConfirmedAt: options.recordedAt,
114
+ sourceType: candidate.sourceType?.trim() || "tool-output",
115
+ sourceRefs: sourceRef ? [sourceRef] : [],
116
+ tags: candidate.tags ?? [],
117
+ provenance: {
118
+ threadId: options.threadId,
119
+ runId: options.runId,
120
+ agentId: options.agentId,
121
+ ...(candidate.provenance ?? {}),
122
+ },
123
+ revision: 1,
124
+ supersedes: [],
125
+ conflictsWith: [],
126
+ };
127
+ }
128
+ function isSameRecordContent(left, right) {
129
+ return normalizeTextForCompare(left.content) === normalizeTextForCompare(right.content);
130
+ }
131
+ function refreshRecord(existing, incoming, recordedAt) {
132
+ return {
133
+ ...existing,
134
+ summary: incoming.summary.length > existing.summary.length ? incoming.summary : existing.summary,
135
+ confidence: Math.max(existing.confidence, incoming.confidence),
136
+ observedAt: incoming.observedAt,
137
+ lastConfirmedAt: recordedAt,
138
+ sourceRefs: mergeUniqueStrings(existing.sourceRefs, incoming.sourceRefs),
139
+ tags: mergeUniqueStrings(existing.tags, incoming.tags),
140
+ provenance: {
141
+ ...existing.provenance,
142
+ refreshedFrom: incoming.provenance,
143
+ lastRefreshRunId: incoming.provenance.runId,
144
+ lastRefreshThreadId: incoming.provenance.threadId,
145
+ },
146
+ revision: existing.revision + 1,
147
+ };
148
+ }
149
+ function mergeRecord(existing, incoming, recordedAt) {
150
+ return {
151
+ ...existing,
152
+ content: mergeRecordContent(existing.content, incoming.content),
153
+ summary: incoming.summary.length >= existing.summary.length ? incoming.summary : existing.summary,
154
+ confidence: Math.max(existing.confidence, incoming.confidence),
155
+ observedAt: incoming.observedAt,
156
+ lastConfirmedAt: recordedAt,
157
+ sourceRefs: mergeUniqueStrings(existing.sourceRefs, incoming.sourceRefs),
158
+ tags: mergeUniqueStrings(existing.tags, incoming.tags),
159
+ provenance: {
160
+ ...existing.provenance,
161
+ mergedFrom: incoming.provenance,
162
+ lastMergeRunId: incoming.provenance.runId,
163
+ lastMergeThreadId: incoming.provenance.threadId,
164
+ },
165
+ revision: existing.revision + 1,
166
+ };
167
+ }
168
+ function markConflicted(record, conflictingRecordId, recordedAt) {
169
+ return {
170
+ ...record,
171
+ status: "conflicted",
172
+ lastConfirmedAt: recordedAt,
173
+ conflictsWith: mergeUniqueStrings(record.conflictsWith, [conflictingRecordId]),
174
+ revision: record.revision + 1,
175
+ };
176
+ }
177
+ function createReviewRecord(existing, incoming, recordedAt) {
178
+ return {
179
+ ...incoming,
180
+ status: "pending_review",
181
+ conflictsWith: mergeUniqueStrings(incoming.conflictsWith, [existing.id]),
182
+ lastConfirmedAt: recordedAt,
183
+ };
184
+ }
185
+ async function listStoredRecords(store, scope) {
186
+ const scopes = scope ? [scope] : MEMORY_SCOPES;
187
+ const docs = await Promise.all(scopes.map((currentScope) => store.search(buildScopeNamespace(currentScope))));
188
+ return docs
189
+ .flat()
190
+ .map((entry) => entry.value)
191
+ .filter((record) => typeof record?.id === "string" && typeof record.content === "string");
192
+ }
193
+ function findMatchingRecord(existingRecords, incoming) {
194
+ const exact = existingRecords.find((record) => record.scope === incoming.scope && isSameRecordContent(record, incoming));
195
+ if (exact) {
196
+ return exact;
197
+ }
198
+ const sourceRefMatch = existingRecords.find((record) => record.scope === incoming.scope &&
199
+ incoming.sourceRefs.some((sourceRef) => record.sourceRefs.includes(sourceRef)));
200
+ if (sourceRefMatch) {
201
+ return sourceRefMatch;
202
+ }
203
+ return existingRecords.find((record) => record.scope === incoming.scope && record.canonicalKey === incoming.canonicalKey);
204
+ }
205
+ function evaluateDecision(existing, incoming, recordedAt) {
206
+ if (!existing) {
207
+ return {
208
+ decision: {
209
+ action: "store",
210
+ reason: "Stored as a new durable memory record.",
211
+ recordId: incoming.id,
212
+ kind: incoming.kind,
213
+ scope: incoming.scope,
214
+ confidence: incoming.confidence,
215
+ maintenance: "dedupe",
216
+ reviewRequired: false,
217
+ },
218
+ primaryRecord: incoming,
219
+ additionalRecords: [],
220
+ };
221
+ }
222
+ if (isSameRecordContent(existing, incoming)) {
223
+ const refreshed = refreshRecord(existing, incoming, recordedAt);
224
+ return {
225
+ decision: {
226
+ action: "refresh",
227
+ reason: "Refreshed an exact durable memory match.",
228
+ recordId: refreshed.id,
229
+ kind: refreshed.kind,
230
+ scope: refreshed.scope,
231
+ confidence: refreshed.confidence,
232
+ maintenance: "none",
233
+ reviewRequired: false,
234
+ },
235
+ primaryRecord: refreshed,
236
+ additionalRecords: [],
237
+ };
238
+ }
239
+ if (existing.canonicalKey === incoming.canonicalKey ||
240
+ incoming.sourceRefs.some((sourceRef) => existing.sourceRefs.includes(sourceRef))) {
241
+ if (hasNegationSignal(existing.content) !== hasNegationSignal(incoming.content)) {
242
+ const conflictedExisting = markConflicted(existing, incoming.id, recordedAt);
243
+ const pendingReview = createReviewRecord(conflictedExisting, incoming, recordedAt);
244
+ return {
245
+ decision: {
246
+ action: "review",
247
+ reason: "Detected conflicting durable memory with the same canonical identity.",
248
+ recordId: pendingReview.id,
249
+ kind: pendingReview.kind,
250
+ scope: pendingReview.scope,
251
+ confidence: pendingReview.confidence,
252
+ maintenance: "review",
253
+ reviewRequired: true,
254
+ },
255
+ primaryRecord: conflictedExisting,
256
+ additionalRecords: [pendingReview],
257
+ };
258
+ }
259
+ const similarity = jaccardSimilarity(existing.summary, incoming.summary);
260
+ if (similarity >= 0.45) {
261
+ const merged = mergeRecord(existing, incoming, recordedAt);
262
+ return {
263
+ decision: {
264
+ action: "merge",
265
+ reason: "Merged a matching durable memory record with compatible additions.",
266
+ recordId: merged.id,
267
+ kind: merged.kind,
268
+ scope: merged.scope,
269
+ confidence: merged.confidence,
270
+ maintenance: "merge",
271
+ reviewRequired: false,
272
+ },
273
+ primaryRecord: merged,
274
+ additionalRecords: [],
275
+ };
276
+ }
277
+ const conflictedExisting = markConflicted(existing, incoming.id, recordedAt);
278
+ const pendingReview = createReviewRecord(conflictedExisting, incoming, recordedAt);
279
+ return {
280
+ decision: {
281
+ action: "review",
282
+ reason: "Detected conflicting durable memory with the same canonical identity.",
283
+ recordId: pendingReview.id,
284
+ kind: pendingReview.kind,
285
+ scope: pendingReview.scope,
286
+ confidence: pendingReview.confidence,
287
+ maintenance: "review",
288
+ reviewRequired: true,
289
+ },
290
+ primaryRecord: conflictedExisting,
291
+ additionalRecords: [pendingReview],
292
+ };
293
+ }
294
+ return {
295
+ decision: {
296
+ action: "store",
297
+ reason: "Stored as a new durable memory record.",
298
+ recordId: incoming.id,
299
+ kind: incoming.kind,
300
+ scope: incoming.scope,
301
+ confidence: incoming.confidence,
302
+ maintenance: "dedupe",
303
+ reviewRequired: false,
304
+ },
305
+ primaryRecord: incoming,
306
+ additionalRecords: [],
307
+ };
308
+ }
309
+ async function putRecordWithIndexes(store, record, updatedAt) {
310
+ const canonicalKeyId = createFingerprint(record.canonicalKey);
311
+ await store.put(buildScopeNamespace(record.scope), `${record.id}.json`, record);
312
+ await store.put(["memories", "indexes", "canonical", record.scope], `${canonicalKeyId}-${record.id}.json`, {
313
+ canonicalKey: record.canonicalKey,
314
+ recordId: record.id,
315
+ scope: record.scope,
316
+ kind: record.kind,
317
+ updatedAt,
318
+ });
319
+ await Promise.all(record.sourceRefs.map((sourceRef) => {
320
+ const sourceRefId = createFingerprint(sourceRef);
321
+ return store.put(["memories", "indexes", "source-ref", record.scope], `${sourceRefId}-${record.id}.json`, {
322
+ sourceRef,
323
+ recordId: record.id,
324
+ scope: record.scope,
325
+ kind: record.kind,
326
+ updatedAt,
327
+ });
328
+ }));
329
+ }
330
+ export function renderMemoryRecordsMarkdown(title, records) {
331
+ const lines = [`# ${title}`, ""];
332
+ if (records.length === 0) {
333
+ lines.push("(none)", "");
334
+ return lines.join("\n");
335
+ }
336
+ for (const record of records) {
337
+ lines.push(`## ${record.summary}`);
338
+ lines.push(`- kind: ${record.kind}`);
339
+ lines.push(`- scope: ${record.scope}`);
340
+ lines.push(`- status: ${record.status}`);
341
+ lines.push(`- confidence: ${record.confidence.toFixed(2)}`);
342
+ if (record.tags.length > 0) {
343
+ lines.push(`- tags: ${record.tags.join(", ")}`);
344
+ }
345
+ lines.push("");
346
+ lines.push(record.content);
347
+ lines.push("");
348
+ }
349
+ return lines.join("\n");
350
+ }
351
+ export async function rebuildStructuredMemoryProjections(store, namespace, title, scope, maxEntries) {
352
+ const records = (await listStoredRecords(store, scope))
353
+ .filter((record) => record.status === "active")
354
+ .sort((left, right) => left.lastConfirmedAt.localeCompare(right.lastConfirmedAt))
355
+ .slice(-maxEntries);
356
+ await store.put(namespace, "structured-memory.md", {
357
+ content: `${renderMemoryRecordsMarkdown(title, records)}\n`,
358
+ items: records,
359
+ });
360
+ const grouped = new Map();
361
+ for (const record of records) {
362
+ const current = grouped.get(record.kind) ?? [];
363
+ current.push(record);
364
+ grouped.set(record.kind, current);
365
+ }
366
+ await Promise.all(Array.from(grouped.entries()).map(([kind, items]) => store.put(namespace, `${kind}.md`, {
367
+ content: `${renderMemoryRecordsMarkdown(`${title} (${kind})`, items)}\n`,
368
+ items,
369
+ })));
370
+ }
371
+ export async function listMemoryRecordsForScopes(store, scopes) {
372
+ const all = await Promise.all(scopes.map((scope) => listStoredRecords(store, scope)));
373
+ return all.flat();
374
+ }
375
+ export async function persistStructuredMemoryRecords(options) {
376
+ const existingRecords = await listStoredRecords(options.store);
377
+ const persistedRecords = [];
378
+ const decisions = [];
379
+ for (const candidate of options.candidates) {
380
+ const incoming = createMemoryRecord(candidate, options);
381
+ const existing = findMatchingRecord(existingRecords, incoming);
382
+ const evaluated = evaluateDecision(existing, incoming, options.recordedAt);
383
+ const recordsToWrite = [evaluated.primaryRecord, ...evaluated.additionalRecords];
384
+ for (const record of recordsToWrite) {
385
+ await putRecordWithIndexes(options.store, record, options.recordedAt);
386
+ const existingIndex = existingRecords.findIndex((item) => item.id === record.id);
387
+ if (existingIndex >= 0) {
388
+ existingRecords[existingIndex] = record;
389
+ }
390
+ else {
391
+ existingRecords.push(record);
392
+ }
393
+ }
394
+ await options.store.put(["memories", "decisions", options.threadId, options.runId], `${evaluated.decision.recordId ?? incoming.id}.json`, evaluated.decision);
395
+ decisions.push(evaluated.decision);
396
+ persistedRecords.push(...recordsToWrite);
397
+ }
398
+ return {
399
+ records: persistedRecords,
400
+ decisions,
401
+ };
402
+ }
@@ -20,6 +20,7 @@ export declare class AgentHarnessRuntime {
20
20
  private readonly defaultStore;
21
21
  private readonly runtimeMemoryStore;
22
22
  private readonly runtimeMemoryPolicy;
23
+ private readonly runtimeMemoryMaintenanceConfig;
23
24
  private readonly routingRules;
24
25
  private readonly routingDefaultAgentId?;
25
26
  private readonly threadMemorySync;
@@ -30,7 +30,9 @@ import { describeWorkspaceInventory, getAgentInventoryRecord, listAgentSkills as
30
30
  import { createDefaultHealthSnapshot, isInventoryEnabled, isThreadMemorySyncEnabled, } from "./harness/runtime-defaults.js";
31
31
  import { Mem0IngestionSync, readMem0RuntimeConfig } from "./harness/system/mem0-ingestion-sync.js";
32
32
  import { renderMemoryCandidatesMarkdown } from "./harness/system/runtime-memory-candidates.js";
33
- import { normalizeLangMemMemoryKind, readRuntimeMemoryPolicyConfig, resolveMemoryNamespace, scoreMemoryText, } from "./harness/system/runtime-memory-policy.js";
33
+ import { listMemoryRecordsForScopes, persistStructuredMemoryRecords, } from "./harness/system/runtime-memory-records.js";
34
+ import { consolidateStructuredMemoryScope } from "./harness/system/runtime-memory-consolidation.js";
35
+ import { normalizeLangMemMemoryKind, readRuntimeMemoryMaintenanceConfig, readRuntimeMemoryPolicyConfig, resolveMemoryNamespace, scoreMemoryText, } from "./harness/system/runtime-memory-policy.js";
34
36
  import { resolveRuntimeAdapterOptions } from "./support/runtime-adapter-options.js";
35
37
  import { initializeHarnessRuntime, reclaimExpiredClaimedRuns as reclaimHarnessExpiredClaimedRuns, recoverStartupRuns as recoverHarnessStartupRuns, isStaleRunningRun as isHarnessStaleRunningRun, } from "./harness/run/startup-runtime.js";
36
38
  import { streamHarnessRun } from "./harness/run/stream-run.js";
@@ -59,6 +61,7 @@ export class AgentHarnessRuntime {
59
61
  defaultStore;
60
62
  runtimeMemoryStore;
61
63
  runtimeMemoryPolicy;
64
+ runtimeMemoryMaintenanceConfig;
62
65
  routingRules;
63
66
  routingDefaultAgentId;
64
67
  threadMemorySync;
@@ -123,6 +126,8 @@ export class AgentHarnessRuntime {
123
126
  : undefined;
124
127
  this.runtimeMemoryStore = resolveStoreFromConfig(this.stores, runtimeMemoryStoreConfig, runRoot) ?? this.defaultStore;
125
128
  this.runtimeMemoryPolicy = readRuntimeMemoryPolicyConfig(this.defaultRuntimeEntryBinding?.harnessRuntime.runtimeMemory, this.workspace.workspaceRoot) ?? null;
129
+ this.runtimeMemoryMaintenanceConfig =
130
+ readRuntimeMemoryMaintenanceConfig(this.defaultRuntimeEntryBinding?.harnessRuntime.runtimeMemory) ?? null;
126
131
  this.resolvedRuntimeAdapterOptions = resolveRuntimeAdapterOptions({
127
132
  workspace,
128
133
  runtimeAdapterOptions,
@@ -399,33 +404,37 @@ export class AgentHarnessRuntime {
399
404
  });
400
405
  }
401
406
  async buildRuntimeMemoryContext(binding, threadId, input) {
402
- const threadNamespace = this.resolveMemoryNamespace("thread", binding, { threadId });
403
- const agentNamespace = this.resolveMemoryNamespace("agent", binding);
404
- const workspaceNamespace = this.resolveMemoryNamespace("workspace", binding);
405
- const docs = await Promise.all([
406
- this.runtimeMemoryStore.get(threadNamespace, "durable-summary.md"),
407
- this.runtimeMemoryStore.get(threadNamespace, "tool-memory.md"),
408
- this.runtimeMemoryStore.get(threadNamespace, "semantic.md"),
409
- this.runtimeMemoryStore.get(threadNamespace, "episodic.md"),
410
- this.runtimeMemoryStore.get(threadNamespace, "procedural.md"),
411
- this.runtimeMemoryStore.get(agentNamespace, "procedural.md"),
412
- this.runtimeMemoryStore.get(workspaceNamespace, "semantic.md"),
413
- this.runtimeMemoryStore.get(workspaceNamespace, "procedural.md"),
414
- ]);
415
407
  const query = typeof input === "string" ? input : JSON.stringify(input ?? "");
416
- const ranked = docs
417
- .map((doc, index) => {
418
- const content = doc?.value?.content;
419
- if (typeof content !== "string" || content.trim().length === 0) {
420
- return null;
408
+ const ranked = (await listMemoryRecordsForScopes(this.runtimeMemoryStore, ["thread", "agent", "workspace"]))
409
+ .filter((record) => {
410
+ if (record.status !== "active") {
411
+ return false;
421
412
  }
422
- const scopeBoost = index < 5 ? 4 : index === 5 ? 2 : 1;
413
+ if (record.scope === "thread") {
414
+ return String(record.provenance.threadId ?? "") === threadId;
415
+ }
416
+ if (record.scope === "agent") {
417
+ return String(record.provenance.agentId ?? "") === binding.agent.id;
418
+ }
419
+ return true;
420
+ })
421
+ .map((record) => {
422
+ const scopeBoost = record.scope === "thread" ? 4 : record.scope === "agent" ? 2 : 1;
423
+ const freshnessBoost = Math.max(0, 1 - ((Date.now() - Date.parse(record.lastConfirmedAt)) / (1000 * 60 * 60 * 24 * 365)));
423
424
  return {
424
- content: content.trim(),
425
- score: scoreMemoryText(query, content, scopeBoost),
425
+ content: [
426
+ `# Durable ${record.scope[0].toUpperCase()}${record.scope.slice(1)} Memory`,
427
+ "",
428
+ `- summary: ${record.summary}`,
429
+ `- kind: ${record.kind}`,
430
+ `- scope: ${record.scope}`,
431
+ `- confidence: ${record.confidence.toFixed(2)}`,
432
+ "",
433
+ record.content,
434
+ ].join("\n"),
435
+ score: scoreMemoryText(query, `${record.summary}\n${record.content}`, scopeBoost) + freshnessBoost + record.confidence,
426
436
  };
427
437
  })
428
- .filter((value) => Boolean(value))
429
438
  .sort((left, right) => right.score - left.score)
430
439
  .slice(0, this.runtimeMemoryPolicy?.retrieval.maxPromptMemories ?? 8);
431
440
  if (ranked.length === 0) {
@@ -446,21 +455,54 @@ export class AgentHarnessRuntime {
446
455
  const threadCandidates = candidates.filter((candidate) => (candidate.scope ?? "thread") === "thread");
447
456
  const workspaceCandidates = candidates.filter((candidate) => (candidate.scope ?? "thread") === "workspace");
448
457
  const agentCandidates = candidates.filter((candidate) => (candidate.scope ?? "thread") === "agent");
458
+ const recordedAt = new Date().toISOString();
449
459
  await this.runtimeMemoryStore.put(["memories", "candidates", threadId], `${runId}.json`, {
450
460
  runId,
451
461
  threadId,
452
- storedAt: new Date().toISOString(),
462
+ storedAt: recordedAt,
463
+ candidates,
464
+ });
465
+ await persistStructuredMemoryRecords({
466
+ store: this.runtimeMemoryStore,
453
467
  candidates,
468
+ threadId,
469
+ runId,
470
+ agentId: binding.agent.id,
471
+ recordedAt,
454
472
  });
455
473
  const writes = [];
456
474
  if (threadCandidates.length > 0) {
457
475
  writes.push(this.appendMemoryDigest(this.resolveMemoryNamespace("thread", binding, { threadId }), "tool-memory.md", threadCandidates, 12, "Thread Tool Memory"));
476
+ writes.push(consolidateStructuredMemoryScope({
477
+ store: this.runtimeMemoryStore,
478
+ namespace: this.resolveMemoryNamespace("thread", binding, { threadId }),
479
+ title: "Thread Structured Memory",
480
+ scope: "thread",
481
+ maxEntries: 12,
482
+ config: this.runtimeMemoryMaintenanceConfig ?? undefined,
483
+ }));
458
484
  }
459
485
  if (workspaceCandidates.length > 0) {
460
486
  writes.push(this.appendMemoryDigest(this.resolveMemoryNamespace("workspace", binding), "tool-memory.md", workspaceCandidates, 20, "Workspace Tool Memory"));
487
+ writes.push(consolidateStructuredMemoryScope({
488
+ store: this.runtimeMemoryStore,
489
+ namespace: this.resolveMemoryNamespace("workspace", binding),
490
+ title: "Workspace Structured Memory",
491
+ scope: "workspace",
492
+ maxEntries: 20,
493
+ config: this.runtimeMemoryMaintenanceConfig ?? undefined,
494
+ }));
461
495
  }
462
496
  if (agentCandidates.length > 0) {
463
497
  writes.push(this.appendMemoryDigest(this.resolveMemoryNamespace("agent", binding), "tool-memory.md", agentCandidates, 20, "Agent Tool Memory"));
498
+ writes.push(consolidateStructuredMemoryScope({
499
+ store: this.runtimeMemoryStore,
500
+ namespace: this.resolveMemoryNamespace("agent", binding),
501
+ title: "Agent Structured Memory",
502
+ scope: "agent",
503
+ maxEntries: 20,
504
+ config: this.runtimeMemoryMaintenanceConfig ?? undefined,
505
+ }));
464
506
  }
465
507
  await Promise.all(writes);
466
508
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@botbotgo/agent-harness",
3
- "version": "0.0.152",
3
+ "version": "0.0.153",
4
4
  "description": "Workspace runtime for multi-agent applications",
5
5
  "type": "module",
6
6
  "packageManager": "npm@10.9.2",