@botbotgo/agent-harness 0.0.151 → 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.
@@ -66,6 +66,10 @@ spec:
66
66
  writeOnApprovalResolution: true
67
67
  writeOnRunCompletion: true
68
68
 
69
+ # agent-harness feature: optional thread snapshot projection for operational state and pending approvals.
70
+ threadMemorySync:
71
+ enabled: true
72
+
69
73
  # agent-harness feature: optional Mem0 OSS ingestion engine for automatic long-term knowledge extraction.
70
74
  mem0:
71
75
  enabled: false
@@ -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
@@ -162,6 +162,8 @@ spec:
162
162
  backgroundConsolidation: true
163
163
  writeOnApprovalResolution: true
164
164
  writeOnRunCompletion: true
165
+ threadMemorySync:
166
+ enabled: true
165
167
  mem0:
166
168
  enabled: false
167
169
  apiKeyEnv: MEM0_API_KEY
@@ -1 +1 @@
1
- export declare const AGENT_HARNESS_VERSION = "0.0.150";
1
+ export declare const AGENT_HARNESS_VERSION = "0.0.152";
@@ -1 +1 @@
1
- export const AGENT_HARNESS_VERSION = "0.0.150";
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
+ }
@@ -8,17 +8,25 @@ export type ResolvedRuntimeMemorySyncConfig = {
8
8
  backgroundConsolidation: boolean;
9
9
  maxMessagesPerRun: number;
10
10
  };
11
+ type RuntimeMemorySyncOptions = {
12
+ resolveThreadNamespace?: (threadId: string) => string[];
13
+ resolveRunNamespace?: (threadId: string) => string[];
14
+ };
11
15
  export declare function readRuntimeMemorySyncConfig(runtimeMemory: Record<string, unknown> | undefined): ResolvedRuntimeMemorySyncConfig | undefined;
12
16
  export declare class RuntimeMemorySync implements HarnessEventProjection {
13
17
  private readonly persistence;
14
18
  readonly store: StoreLike;
15
19
  private readonly config;
20
+ private readonly options;
16
21
  private readonly pending;
17
22
  private syncChain;
18
23
  readonly name = "runtime-memory-sync";
19
- constructor(persistence: RuntimePersistence, store: StoreLike, config: ResolvedRuntimeMemorySyncConfig);
24
+ constructor(persistence: RuntimePersistence, store: StoreLike, config: ResolvedRuntimeMemorySyncConfig, options?: RuntimeMemorySyncOptions);
20
25
  shouldHandle(event: HarnessEvent): boolean;
21
26
  handleEvent(event: HarnessEvent): Promise<void>;
22
27
  private syncRun;
28
+ private resolveThreadNamespace;
29
+ private resolveRunNamespace;
23
30
  close(): Promise<void>;
24
31
  }
32
+ export {};
@@ -100,13 +100,15 @@ export class RuntimeMemorySync {
100
100
  persistence;
101
101
  store;
102
102
  config;
103
+ options;
103
104
  pending = new Set();
104
105
  syncChain = Promise.resolve();
105
106
  name = "runtime-memory-sync";
106
- constructor(persistence, store, config) {
107
+ constructor(persistence, store, config, options = {}) {
107
108
  this.persistence = persistence;
108
109
  this.store = store;
109
110
  this.config = config;
111
+ this.options = options;
110
112
  }
111
113
  shouldHandle(event) {
112
114
  if (!RUNTIME_MEMORY_EVENT_TYPES.has(event.eventType)) {
@@ -161,8 +163,8 @@ export class RuntimeMemorySync {
161
163
  approvals,
162
164
  });
163
165
  await Promise.all([
164
- this.store.put(["memories", "runs", threadId], `${runId}.summary.md`, { content: `${summaryMarkdown}\n` }),
165
- this.store.put(["memories", "runs", threadId], `${runId}.record.json`, {
166
+ this.store.put(this.resolveRunNamespace(threadId), `${runId}.summary.md`, { content: `${summaryMarkdown}\n` }),
167
+ this.store.put(this.resolveRunNamespace(threadId), `${runId}.record.json`, {
166
168
  kind: "summary",
167
169
  scope: "thread",
168
170
  threadId,
@@ -185,7 +187,13 @@ export class RuntimeMemorySync {
185
187
  capturedAt,
186
188
  messages,
187
189
  });
188
- await this.store.put(["memories", "threads", threadId], "durable-summary.md", { content: `${digestMarkdown}\n` });
190
+ await this.store.put(this.resolveThreadNamespace(threadId), "durable-summary.md", { content: `${digestMarkdown}\n` });
191
+ }
192
+ resolveThreadNamespace(threadId) {
193
+ return this.options.resolveThreadNamespace ? this.options.resolveThreadNamespace(threadId) : ["memories", "threads", threadId];
194
+ }
195
+ resolveRunNamespace(threadId) {
196
+ return this.options.resolveRunNamespace ? this.options.resolveRunNamespace(threadId) : ["memories", "runs", threadId];
189
197
  }
190
198
  async close() {
191
199
  await Promise.allSettled(Array.from(this.pending));
@@ -1,14 +1,18 @@
1
1
  import type { HarnessEvent, HarnessEventProjection } from "../../../contracts/types.js";
2
2
  import type { RuntimePersistence } from "../../../persistence/types.js";
3
+ type ThreadMemorySyncOptions = {
4
+ resolveThreadNamespace?: (threadId: string) => string[];
5
+ };
3
6
  export declare class ThreadMemorySync implements HarnessEventProjection {
4
7
  private readonly persistence;
5
8
  private readonly store?;
9
+ private readonly options;
6
10
  private readonly pending;
7
11
  private syncChain;
8
12
  readonly name = "thread-memory-sync";
9
13
  constructor(persistence: RuntimePersistence, store?: {
10
14
  put: (namespace: string[], key: string, value: Record<string, any>) => Promise<void>;
11
- } | undefined);
15
+ } | undefined, options?: ThreadMemorySyncOptions);
12
16
  shouldHandle(event: HarnessEvent): boolean;
13
17
  handleEvent(event: HarnessEvent): Promise<void>;
14
18
  private syncThread;
@@ -6,11 +6,11 @@ function excerpt(message) {
6
6
  const normalized = extractMessageText(message.content).replace(/\s+/g, " ").trim();
7
7
  return normalized.length > 240 ? `${normalized.slice(0, 237)}...` : normalized;
8
8
  }
9
- function renderStatusMarkdown(thread, messages) {
9
+ function renderThreadSnapshotMarkdown(thread, messages, approvals) {
10
10
  const userMessages = messages.filter((message) => message.role === "user");
11
11
  const assistantMessages = messages.filter((message) => message.role === "assistant");
12
12
  return [
13
- "# Thread Status",
13
+ "# Thread Snapshot",
14
14
  "",
15
15
  `- thread_id: ${thread.threadId}`,
16
16
  `- latest_run_id: ${thread.latestRunId}`,
@@ -24,15 +24,17 @@ function renderStatusMarkdown(thread, messages) {
24
24
  "## Recent Assistant Message",
25
25
  excerpt(assistantMessages.at(-1)),
26
26
  "",
27
+ ...formatOpenApprovalsSection(approvals),
27
28
  ].join("\n");
28
29
  }
29
- function renderOpenApprovalsMarkdown(approvals) {
30
+ function formatOpenApprovalsSection(approvals) {
31
+ const lines = ["## Open Approvals", ""];
30
32
  if (approvals.length === 0) {
31
- return ["# Open Approvals", "", "(none)", ""].join("\n");
33
+ lines.push("(none)", "");
34
+ return lines;
32
35
  }
33
- const lines = ["# Open Approvals", ""];
34
36
  for (const approval of approvals) {
35
- lines.push(`## ${approval.approvalId}`);
37
+ lines.push(`### ${approval.approvalId}`);
36
38
  lines.push(`- pending_action_id: ${approval.pendingActionId}`);
37
39
  lines.push(`- tool: ${approval.toolName}`);
38
40
  lines.push(`- run_id: ${approval.runId}`);
@@ -40,7 +42,10 @@ function renderOpenApprovalsMarkdown(approvals) {
40
42
  lines.push(`- allowed: ${approval.allowedDecisions.join(", ")}`);
41
43
  lines.push("");
42
44
  }
43
- return lines.join("\n");
45
+ return lines;
46
+ }
47
+ function resolveThreadNamespace(threadId, resolver) {
48
+ return resolver ? resolver(threadId) : ["memories", "threads", threadId];
44
49
  }
45
50
  const THREAD_MEMORY_EVENT_TYPES = new Set([
46
51
  "run.state.changed",
@@ -52,12 +57,14 @@ const THREAD_MEMORY_EVENT_TYPES = new Set([
52
57
  export class ThreadMemorySync {
53
58
  persistence;
54
59
  store;
60
+ options;
55
61
  pending = new Set();
56
62
  syncChain = Promise.resolve();
57
63
  name = "thread-memory-sync";
58
- constructor(persistence, store) {
64
+ constructor(persistence, store, options = {}) {
59
65
  this.persistence = persistence;
60
66
  this.store = store;
67
+ this.options = options;
61
68
  }
62
69
  shouldHandle(event) {
63
70
  return THREAD_MEMORY_EVENT_TYPES.has(event.eventType);
@@ -93,10 +100,9 @@ export class ThreadMemorySync {
93
100
  if (!this.store) {
94
101
  return;
95
102
  }
96
- await Promise.all([
97
- this.store.put(["memories", "threads", threadId], "status.md", { content: `${renderStatusMarkdown(thread, messages)}\n` }),
98
- this.store.put(["memories", "threads", threadId], "open-approvals.md", { content: `${renderOpenApprovalsMarkdown(pendingApprovals)}\n` }),
99
- ]);
103
+ await this.store.put(resolveThreadNamespace(threadId, this.options.resolveThreadNamespace), "snapshot.md", {
104
+ content: `${renderThreadSnapshotMarkdown(thread, messages, pendingApprovals)}\n`,
105
+ });
100
106
  }
101
107
  async close() {
102
108
  await Promise.allSettled(Array.from(this.pending));
@@ -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,
@@ -144,7 +149,15 @@ export class AgentHarnessRuntime {
144
149
  this.routingRules = getRoutingRules(workspace.refs);
145
150
  this.routingDefaultAgentId = getRoutingDefaultAgentId(workspace.refs);
146
151
  if (isThreadMemorySyncEnabled(workspace)) {
147
- this.threadMemorySync = new ThreadMemorySync(this.persistence, this.runtimeMemoryStore);
152
+ this.threadMemorySync = new ThreadMemorySync(this.persistence, this.runtimeMemoryStore, {
153
+ resolveThreadNamespace: (threadId) => {
154
+ const binding = this.defaultRuntimeEntryBinding;
155
+ if (!binding) {
156
+ return ["memories", "threads", threadId];
157
+ }
158
+ return this.resolveMemoryNamespace("thread", binding, { threadId });
159
+ },
160
+ });
148
161
  this.unregisterThreadMemorySync = this.eventBus.registerProjection(this.threadMemorySync);
149
162
  }
150
163
  else {
@@ -153,7 +166,15 @@ export class AgentHarnessRuntime {
153
166
  }
154
167
  const runtimeMemorySyncConfig = readRuntimeMemorySyncConfig(this.defaultRuntimeEntryBinding?.harnessRuntime.runtimeMemory);
155
168
  if (runtimeMemorySyncConfig) {
156
- this.runtimeMemorySync = new RuntimeMemorySync(this.persistence, this.runtimeMemoryStore, runtimeMemorySyncConfig);
169
+ this.runtimeMemorySync = new RuntimeMemorySync(this.persistence, this.runtimeMemoryStore, runtimeMemorySyncConfig, {
170
+ resolveThreadNamespace: (threadId) => {
171
+ const binding = this.defaultRuntimeEntryBinding;
172
+ if (!binding) {
173
+ return ["memories", "threads", threadId];
174
+ }
175
+ return this.resolveMemoryNamespace("thread", binding, { threadId });
176
+ },
177
+ });
157
178
  this.unregisterRuntimeMemorySync = this.eventBus.registerProjection(this.runtimeMemorySync);
158
179
  }
159
180
  else {
@@ -383,33 +404,37 @@ export class AgentHarnessRuntime {
383
404
  });
384
405
  }
385
406
  async buildRuntimeMemoryContext(binding, threadId, input) {
386
- const threadNamespace = this.resolveMemoryNamespace("thread", binding, { threadId });
387
- const agentNamespace = this.resolveMemoryNamespace("agent", binding);
388
- const workspaceNamespace = this.resolveMemoryNamespace("workspace", binding);
389
- const docs = await Promise.all([
390
- this.runtimeMemoryStore.get(threadNamespace, "durable-summary.md"),
391
- this.runtimeMemoryStore.get(threadNamespace, "tool-memory.md"),
392
- this.runtimeMemoryStore.get(threadNamespace, "semantic.md"),
393
- this.runtimeMemoryStore.get(threadNamespace, "episodic.md"),
394
- this.runtimeMemoryStore.get(threadNamespace, "procedural.md"),
395
- this.runtimeMemoryStore.get(agentNamespace, "procedural.md"),
396
- this.runtimeMemoryStore.get(workspaceNamespace, "semantic.md"),
397
- this.runtimeMemoryStore.get(workspaceNamespace, "procedural.md"),
398
- ]);
399
407
  const query = typeof input === "string" ? input : JSON.stringify(input ?? "");
400
- const ranked = docs
401
- .map((doc, index) => {
402
- const content = doc?.value?.content;
403
- if (typeof content !== "string" || content.trim().length === 0) {
404
- return null;
408
+ const ranked = (await listMemoryRecordsForScopes(this.runtimeMemoryStore, ["thread", "agent", "workspace"]))
409
+ .filter((record) => {
410
+ if (record.status !== "active") {
411
+ return false;
412
+ }
413
+ if (record.scope === "thread") {
414
+ return String(record.provenance.threadId ?? "") === threadId;
405
415
  }
406
- const scopeBoost = index < 5 ? 4 : index === 5 ? 2 : 1;
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)));
407
424
  return {
408
- content: content.trim(),
409
- 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,
410
436
  };
411
437
  })
412
- .filter((value) => Boolean(value))
413
438
  .sort((left, right) => right.score - left.score)
414
439
  .slice(0, this.runtimeMemoryPolicy?.retrieval.maxPromptMemories ?? 8);
415
440
  if (ranked.length === 0) {
@@ -430,21 +455,54 @@ export class AgentHarnessRuntime {
430
455
  const threadCandidates = candidates.filter((candidate) => (candidate.scope ?? "thread") === "thread");
431
456
  const workspaceCandidates = candidates.filter((candidate) => (candidate.scope ?? "thread") === "workspace");
432
457
  const agentCandidates = candidates.filter((candidate) => (candidate.scope ?? "thread") === "agent");
458
+ const recordedAt = new Date().toISOString();
433
459
  await this.runtimeMemoryStore.put(["memories", "candidates", threadId], `${runId}.json`, {
434
460
  runId,
435
461
  threadId,
436
- storedAt: new Date().toISOString(),
462
+ storedAt: recordedAt,
437
463
  candidates,
438
464
  });
465
+ await persistStructuredMemoryRecords({
466
+ store: this.runtimeMemoryStore,
467
+ candidates,
468
+ threadId,
469
+ runId,
470
+ agentId: binding.agent.id,
471
+ recordedAt,
472
+ });
439
473
  const writes = [];
440
474
  if (threadCandidates.length > 0) {
441
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
+ }));
442
484
  }
443
485
  if (workspaceCandidates.length > 0) {
444
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
+ }));
445
495
  }
446
496
  if (agentCandidates.length > 0) {
447
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
+ }));
448
506
  }
449
507
  await Promise.all(writes);
450
508
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@botbotgo/agent-harness",
3
- "version": "0.0.151",
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",