@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.
- package/dist/config/runtime/runtime-memory.yaml +4 -0
- package/dist/contracts/runtime.d.ts +34 -0
- package/dist/init-project.js +2 -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/system/runtime-memory-sync.d.ts +9 -1
- package/dist/runtime/harness/system/runtime-memory-sync.js +12 -4
- package/dist/runtime/harness/system/thread-memory-sync.d.ts +5 -1
- package/dist/runtime/harness/system/thread-memory-sync.js +18 -12
- package/dist/runtime/harness.d.ts +1 -0
- package/dist/runtime/harness.js +84 -26
- package/package.json +1 -1
|
@@ -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
|
package/dist/init-project.js
CHANGED
|
@@ -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
|
+
}
|
|
@@ -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(
|
|
165
|
-
this.store.put(
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
30
|
+
function formatOpenApprovalsSection(approvals) {
|
|
31
|
+
const lines = ["## Open Approvals", ""];
|
|
30
32
|
if (approvals.length === 0) {
|
|
31
|
-
|
|
33
|
+
lines.push("(none)", "");
|
|
34
|
+
return lines;
|
|
32
35
|
}
|
|
33
|
-
const lines = ["# Open Approvals", ""];
|
|
34
36
|
for (const approval of approvals) {
|
|
35
|
-
lines.push(
|
|
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
|
|
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
|
|
97
|
-
|
|
98
|
-
|
|
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;
|
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,
|
|
@@ -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 =
|
|
401
|
-
.
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
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
|
-
|
|
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:
|
|
409
|
-
|
|
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:
|
|
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
|
}
|