@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.
- package/dist/contracts/runtime.d.ts +34 -0
- package/dist/package-version.d.ts +1 -1
- package/dist/package-version.js +1 -1
- package/dist/runtime/harness/system/runtime-memory-consolidation.d.ts +12 -0
- package/dist/runtime/harness/system/runtime-memory-consolidation.js +51 -0
- package/dist/runtime/harness/system/runtime-memory-policy.d.ts +6 -0
- package/dist/runtime/harness/system/runtime-memory-policy.js +15 -0
- package/dist/runtime/harness/system/runtime-memory-records.d.ts +18 -0
- package/dist/runtime/harness/system/runtime-memory-records.js +402 -0
- package/dist/runtime/harness.d.ts +1 -0
- package/dist/runtime/harness.js +66 -24
- package/package.json +1 -1
|
@@ -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.
|
|
1
|
+
export declare const AGENT_HARNESS_VERSION = "0.0.152";
|
package/dist/package-version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const AGENT_HARNESS_VERSION = "0.0.
|
|
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;
|
package/dist/runtime/harness.js
CHANGED
|
@@ -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 {
|
|
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 =
|
|
417
|
-
.
|
|
418
|
-
|
|
419
|
-
|
|
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
|
-
|
|
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:
|
|
425
|
-
|
|
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:
|
|
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
|
}
|