@botbotgo/agent-harness 0.0.152 → 0.0.154
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -1
- package/README.zh.md +31 -1
- package/dist/api.d.ts +4 -1
- package/dist/api.js +6 -0
- package/dist/contracts/runtime.d.ts +77 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- 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 +21 -0
- package/dist/runtime/harness/system/runtime-memory-records.js +405 -0
- package/dist/runtime/harness.d.ts +9 -1
- package/dist/runtime/harness.js +208 -33
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -118,7 +118,7 @@ That means:
|
|
|
118
118
|
|
|
119
119
|
The runtime provides:
|
|
120
120
|
|
|
121
|
-
- `createAgentHarness(workspaceRoot)`, `run(...)`, `resolveApproval(...)`, `subscribe(...)`, inspection methods, and `stop(...)`
|
|
121
|
+
- `createAgentHarness(workspaceRoot)`, `run(...)`, `memorize(...)`, `recall(...)`, `resolveApproval(...)`, `subscribe(...)`, inspection methods, and `stop(...)`
|
|
122
122
|
- YAML-defined workspace assembly for routing, models, tools, stores, backends, MCP, recovery, and maintenance
|
|
123
123
|
- backend-adapted execution with current LangChain v1 and DeepAgents adapters
|
|
124
124
|
- local `resources/tools/` `tool({...})` modules and `resources/skills/` discovery
|
|
@@ -389,6 +389,36 @@ const result = await run(
|
|
|
389
389
|
|
|
390
390
|
Use `normalizeUserChatInput(...)` when a product already has chat-style user messages and wants to project one user turn onto the stable `run(..., { input, invocation })` surface without introducing a separate harness-owned chat API.
|
|
391
391
|
|
|
392
|
+
### Store And Recall Durable Runtime Memory
|
|
393
|
+
|
|
394
|
+
```ts
|
|
395
|
+
import { memorize, recall } from "@botbotgo/agent-harness";
|
|
396
|
+
|
|
397
|
+
await memorize(runtime, {
|
|
398
|
+
threadId: "thread-123",
|
|
399
|
+
records: [
|
|
400
|
+
{
|
|
401
|
+
content: "The release checklist requires a smoke test before publish.",
|
|
402
|
+
summary: "Run a smoke test before publish",
|
|
403
|
+
scope: "workspace",
|
|
404
|
+
kind: "procedural",
|
|
405
|
+
sourceRef: "docs/release-checklist.md",
|
|
406
|
+
},
|
|
407
|
+
],
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
const recalled = await recall(runtime, {
|
|
411
|
+
query: "What does the release checklist require?",
|
|
412
|
+
scopes: ["workspace"],
|
|
413
|
+
});
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
Use `memorize(...)` and `recall(...)` when an application needs a stable public runtime memory surface without importing internal `runtime/harness/system/*` modules.
|
|
417
|
+
|
|
418
|
+
- `memorize(...)` returns stable `MemoryRecord` and `MemoryDecision` results while leaving merge, review, archive, and storage layout runtime-managed
|
|
419
|
+
- `recall(...)` returns ranked `MemoryRecord` items filtered by runtime memory scope and kind
|
|
420
|
+
- app-specific knowledge taxonomy, review UI, and admin surfaces still belong in the application layer
|
|
421
|
+
|
|
392
422
|
### Let The Runtime Route
|
|
393
423
|
|
|
394
424
|
```ts
|
package/README.zh.md
CHANGED
|
@@ -118,7 +118,7 @@ AI 让 agent 逻辑、工具调用和工作流代码更容易生成,真正更
|
|
|
118
118
|
|
|
119
119
|
运行时提供:
|
|
120
120
|
|
|
121
|
-
- `createAgentHarness(workspaceRoot)`、`run(...)`、`resolveApproval(...)`、`subscribe(...)`、各类查询方法,以及 `stop(...)`
|
|
121
|
+
- `createAgentHarness(workspaceRoot)`、`run(...)`、`memorize(...)`、`recall(...)`、`resolveApproval(...)`、`subscribe(...)`、各类查询方法,以及 `stop(...)`
|
|
122
122
|
- 以 YAML 描述的工作区装配:路由、模型、工具、存储、后端、MCP、恢复与维护等
|
|
123
123
|
- 通过适配器对接当前的 LangChain v1 与 DeepAgents 执行
|
|
124
124
|
- 本地 `resources/tools/` 中 `tool({...})` 工具模块与 `resources/skills/` 的发现
|
|
@@ -360,6 +360,36 @@ const result = await run(runtime, {
|
|
|
360
360
|
- `invocation.inputs`:结构化运行时输入
|
|
361
361
|
- `invocation.attachments`:当前后端可解释的类附件负载
|
|
362
362
|
|
|
363
|
+
### 写入与召回 durable runtime memory
|
|
364
|
+
|
|
365
|
+
```ts
|
|
366
|
+
import { memorize, recall } from "@botbotgo/agent-harness";
|
|
367
|
+
|
|
368
|
+
await memorize(runtime, {
|
|
369
|
+
threadId: "thread-123",
|
|
370
|
+
records: [
|
|
371
|
+
{
|
|
372
|
+
content: "The release checklist requires a smoke test before publish.",
|
|
373
|
+
summary: "Run a smoke test before publish",
|
|
374
|
+
scope: "workspace",
|
|
375
|
+
kind: "procedural",
|
|
376
|
+
sourceRef: "docs/release-checklist.md",
|
|
377
|
+
},
|
|
378
|
+
],
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
const recalled = await recall(runtime, {
|
|
382
|
+
query: "What does the release checklist require?",
|
|
383
|
+
scopes: ["workspace"],
|
|
384
|
+
});
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
当应用需要稳定的公开 runtime memory 接口,而不想依赖内部 `runtime/harness/system/*` 模块时,使用 `memorize(...)` 与 `recall(...)`。
|
|
388
|
+
|
|
389
|
+
- `memorize(...)` 返回稳定的 `MemoryRecord` 与 `MemoryDecision` 结果,而 merge、review、archive 与存储布局仍由 runtime 内部托管
|
|
390
|
+
- `recall(...)` 返回按相关性排序、并按 scope / kind 过滤后的 `MemoryRecord`
|
|
391
|
+
- 业务知识分类、review UI 与管理后台仍应留在应用层
|
|
392
|
+
|
|
363
393
|
### 由运行时路由
|
|
364
394
|
|
|
365
395
|
```ts
|
package/dist/api.d.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import type { CancelOptions, InvocationEnvelope, MessageContent, RequestRecord, RequestSummary, ResumeOptions, RunDecisionOptions, RunResult, RunStartOptions, RuntimeHealthSnapshot, RuntimeAdapterOptions, SessionRecord, SessionSummary, WorkspaceLoadOptions } from "./contracts/types.js";
|
|
1
|
+
import type { CancelOptions, InvocationEnvelope, MemorizeInput, MemorizeResult, MessageContent, RecallInput, RecallResult, RequestRecord, RequestSummary, ResumeOptions, RunDecisionOptions, RunResult, RunStartOptions, RuntimeHealthSnapshot, RuntimeAdapterOptions, SessionRecord, SessionSummary, WorkspaceLoadOptions } from "./contracts/types.js";
|
|
2
2
|
import { AgentHarnessRuntime } from "./runtime/harness.js";
|
|
3
3
|
import type { InventoryAgentRecord, InventorySkillRecord } from "./runtime/harness/system/inventory.js";
|
|
4
4
|
import type { RequirementAssessmentOptions } from "./runtime/harness/system/skill-requirements.js";
|
|
5
5
|
import type { ToolMcpServerOptions } from "./mcp.js";
|
|
6
6
|
export { AgentHarnessRuntime } from "./runtime/harness.js";
|
|
7
7
|
export { createUpstreamTimelineReducer } from "./upstream-events.js";
|
|
8
|
+
export type { MemoryDecision, MemoryKind, MemoryRecord, MemoryScope, MemorizeInput, MemorizeResult, RecallInput, RecallResult, } from "./contracts/types.js";
|
|
8
9
|
type PublicApprovalRecord = {
|
|
9
10
|
approvalId: string;
|
|
10
11
|
pendingActionId: string;
|
|
@@ -54,6 +55,8 @@ export declare function createAgentHarness(): Promise<AgentHarnessRuntime>;
|
|
|
54
55
|
export declare function createAgentHarness(workspaceRoot: string, options?: CreateAgentHarnessOptions): Promise<AgentHarnessRuntime>;
|
|
55
56
|
export declare function normalizeUserChatInput(input: UserChatInput, options?: NormalizeUserChatInputOptions): Pick<RunStartOptions, "input" | "invocation">;
|
|
56
57
|
export declare function run(runtime: AgentHarnessRuntime, options: PublicRunOptions): Promise<PublicRunResult>;
|
|
58
|
+
export declare function memorize(runtime: AgentHarnessRuntime, input: MemorizeInput): Promise<MemorizeResult>;
|
|
59
|
+
export declare function recall(runtime: AgentHarnessRuntime, input: RecallInput): Promise<RecallResult>;
|
|
57
60
|
export declare function subscribe(runtime: AgentHarnessRuntime, listener: Parameters<AgentHarnessRuntime["subscribe"]>[0]): () => void;
|
|
58
61
|
export declare function listSessions(runtime: AgentHarnessRuntime, filter?: Parameters<AgentHarnessRuntime["listThreads"]>[0]): Promise<SessionSummary[]>;
|
|
59
62
|
export declare function listRequests(runtime: AgentHarnessRuntime, filter?: {
|
package/dist/api.js
CHANGED
|
@@ -128,6 +128,12 @@ export function normalizeUserChatInput(input, options = {}) {
|
|
|
128
128
|
export async function run(runtime, options) {
|
|
129
129
|
return toPublicRunResult(await runtime.run(toInternalRunOptions(options)));
|
|
130
130
|
}
|
|
131
|
+
export async function memorize(runtime, input) {
|
|
132
|
+
return runtime.memorize(input);
|
|
133
|
+
}
|
|
134
|
+
export async function recall(runtime, input) {
|
|
135
|
+
return runtime.recall(input);
|
|
136
|
+
}
|
|
131
137
|
export function subscribe(runtime, listener) {
|
|
132
138
|
return runtime.subscribe(listener);
|
|
133
139
|
}
|
|
@@ -118,6 +118,83 @@ export type MemoryCandidate = {
|
|
|
118
118
|
noStore?: boolean;
|
|
119
119
|
provenance?: Record<string, unknown>;
|
|
120
120
|
};
|
|
121
|
+
export type MemoryKind = "semantic" | "episodic" | "procedural";
|
|
122
|
+
export type MemoryScope = "thread" | "agent" | "workspace" | "user" | "project";
|
|
123
|
+
export type MemoryRecordStatus = "active" | "stale" | "conflicted" | "archived" | "pending_review";
|
|
124
|
+
export type MemoryDecisionAction = "reject" | "store" | "merge" | "refresh" | "supersede" | "archive" | "review";
|
|
125
|
+
export type MemoryRecord = {
|
|
126
|
+
id: string;
|
|
127
|
+
canonicalKey: string;
|
|
128
|
+
kind: MemoryKind;
|
|
129
|
+
scope: MemoryScope;
|
|
130
|
+
content: string;
|
|
131
|
+
summary: string;
|
|
132
|
+
status: MemoryRecordStatus;
|
|
133
|
+
confidence: number;
|
|
134
|
+
createdAt: string;
|
|
135
|
+
observedAt: string;
|
|
136
|
+
lastConfirmedAt: string;
|
|
137
|
+
expiresAt?: string;
|
|
138
|
+
sourceType: string;
|
|
139
|
+
sourceRefs: string[];
|
|
140
|
+
tags: string[];
|
|
141
|
+
provenance: Record<string, unknown>;
|
|
142
|
+
revision: number;
|
|
143
|
+
supersedes: string[];
|
|
144
|
+
conflictsWith: string[];
|
|
145
|
+
};
|
|
146
|
+
export type MemoryDecision = {
|
|
147
|
+
action: MemoryDecisionAction;
|
|
148
|
+
reason: string;
|
|
149
|
+
recordId?: string;
|
|
150
|
+
kind?: MemoryRecord["kind"];
|
|
151
|
+
scope?: MemoryScope;
|
|
152
|
+
confidence?: number;
|
|
153
|
+
maintenance?: "none" | "dedupe" | "merge" | "review";
|
|
154
|
+
reviewRequired?: boolean;
|
|
155
|
+
};
|
|
156
|
+
export type MemorizeInputRecord = {
|
|
157
|
+
content: string;
|
|
158
|
+
summary?: string;
|
|
159
|
+
kind?: MemoryKind;
|
|
160
|
+
scope?: MemoryScope;
|
|
161
|
+
confidence?: number;
|
|
162
|
+
tags?: string[];
|
|
163
|
+
sourceType?: string;
|
|
164
|
+
sourceRef?: string;
|
|
165
|
+
observedAt?: string;
|
|
166
|
+
sensitivity?: string;
|
|
167
|
+
provenance?: Record<string, unknown>;
|
|
168
|
+
noStore?: boolean;
|
|
169
|
+
};
|
|
170
|
+
export type MemorizeInput = {
|
|
171
|
+
records: MemorizeInputRecord[];
|
|
172
|
+
threadId?: string;
|
|
173
|
+
runId?: string;
|
|
174
|
+
agentId?: string;
|
|
175
|
+
userId?: string;
|
|
176
|
+
projectId?: string;
|
|
177
|
+
recordedAt?: string;
|
|
178
|
+
};
|
|
179
|
+
export type MemorizeResult = {
|
|
180
|
+
records: MemoryRecord[];
|
|
181
|
+
decisions: MemoryDecision[];
|
|
182
|
+
};
|
|
183
|
+
export type RecallInput = {
|
|
184
|
+
query: string;
|
|
185
|
+
scopes?: MemoryScope[];
|
|
186
|
+
kinds?: MemoryKind[];
|
|
187
|
+
topK?: number;
|
|
188
|
+
includeStale?: boolean;
|
|
189
|
+
threadId?: string;
|
|
190
|
+
agentId?: string;
|
|
191
|
+
workspaceId?: string;
|
|
192
|
+
userId?: string;
|
|
193
|
+
projectId?: string;
|
|
194
|
+
};
|
|
195
|
+
export type RecallResult = {
|
|
196
|
+
items: MemoryRecord[];
|
|
197
|
+
};
|
|
121
198
|
/**
|
|
122
199
|
* Operator-facing projection of tool execution policy already compiled into a binding.
|
|
123
200
|
* This summarizes existing timeout, retry, validation, and retry-safety hints without
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export { AgentHarnessRuntime, cancelRun, createAgentHarness, createUpstreamTimelineReducer, createToolMcpServer, deleteSession, describeInventory, getAgent, getApproval, getRequest, getHealth, getSession, listAgentSkills, listApprovals, listRequests, listSessions, normalizeUserChatInput, resolveApproval, run, serveToolsOverStdio, subscribe, stop, } from "./api.js";
|
|
2
|
-
export type { NormalizeUserChatInputOptions, UserChatInput, UserChatMessage } from "./api.js";
|
|
1
|
+
export { AgentHarnessRuntime, cancelRun, createAgentHarness, createUpstreamTimelineReducer, createToolMcpServer, deleteSession, describeInventory, getAgent, getApproval, getRequest, getHealth, getSession, listAgentSkills, listApprovals, listRequests, listSessions, memorize, normalizeUserChatInput, recall, resolveApproval, run, serveToolsOverStdio, subscribe, stop, } from "./api.js";
|
|
2
|
+
export type { MemoryDecision, MemoryKind, MemoryRecord, MemoryScope, MemorizeInput, MemorizeResult, NormalizeUserChatInputOptions, RecallInput, RecallResult, UserChatInput, UserChatMessage, } from "./api.js";
|
|
3
3
|
export type { ToolMcpServerOptions } from "./mcp.js";
|
|
4
4
|
export { tool } from "./tools.js";
|
|
5
5
|
export type { UpstreamTimelineProjection, UpstreamTimelineReducer } from "./upstream-events.js";
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { AgentHarnessRuntime, cancelRun, createAgentHarness, createUpstreamTimelineReducer, createToolMcpServer, deleteSession, describeInventory, getAgent, getApproval, getRequest, getHealth, getSession, listAgentSkills, listApprovals, listRequests, listSessions, normalizeUserChatInput, resolveApproval, run, serveToolsOverStdio, subscribe, stop, } from "./api.js";
|
|
1
|
+
export { AgentHarnessRuntime, cancelRun, createAgentHarness, createUpstreamTimelineReducer, createToolMcpServer, deleteSession, describeInventory, getAgent, getApproval, getRequest, getHealth, getSession, listAgentSkills, listApprovals, listRequests, listSessions, memorize, normalizeUserChatInput, recall, resolveApproval, run, serveToolsOverStdio, subscribe, stop, } from "./api.js";
|
|
2
2
|
export { tool } from "./tools.js";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const AGENT_HARNESS_VERSION = "0.0.
|
|
1
|
+
export declare const AGENT_HARNESS_VERSION = "0.0.153";
|
package/dist/package-version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const AGENT_HARNESS_VERSION = "0.0.
|
|
1
|
+
export const AGENT_HARNESS_VERSION = "0.0.153";
|
|
@@ -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,21 @@
|
|
|
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
|
+
workspaceId: string;
|
|
10
|
+
userId: string;
|
|
11
|
+
projectId: string;
|
|
12
|
+
recordedAt: string;
|
|
13
|
+
};
|
|
14
|
+
export declare function renderMemoryRecordsMarkdown(title: string, records: MemoryRecord[]): string;
|
|
15
|
+
export declare function rebuildStructuredMemoryProjections(store: StoreLike, namespace: string[], title: string, scope: MemoryScope, maxEntries: number): Promise<void>;
|
|
16
|
+
export declare function listMemoryRecordsForScopes(store: StoreLike, scopes: MemoryScope[]): Promise<MemoryRecord[]>;
|
|
17
|
+
export declare function persistStructuredMemoryRecords(options: PersistMemoryRecordsOptions): Promise<{
|
|
18
|
+
records: MemoryRecord[];
|
|
19
|
+
decisions: MemoryDecision[];
|
|
20
|
+
}>;
|
|
21
|
+
export {};
|
|
@@ -0,0 +1,405 @@
|
|
|
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
|
+
workspaceId: options.workspaceId,
|
|
122
|
+
userId: options.userId,
|
|
123
|
+
projectId: options.projectId,
|
|
124
|
+
...(candidate.provenance ?? {}),
|
|
125
|
+
},
|
|
126
|
+
revision: 1,
|
|
127
|
+
supersedes: [],
|
|
128
|
+
conflictsWith: [],
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
function isSameRecordContent(left, right) {
|
|
132
|
+
return normalizeTextForCompare(left.content) === normalizeTextForCompare(right.content);
|
|
133
|
+
}
|
|
134
|
+
function refreshRecord(existing, incoming, recordedAt) {
|
|
135
|
+
return {
|
|
136
|
+
...existing,
|
|
137
|
+
summary: incoming.summary.length > existing.summary.length ? incoming.summary : existing.summary,
|
|
138
|
+
confidence: Math.max(existing.confidence, incoming.confidence),
|
|
139
|
+
observedAt: incoming.observedAt,
|
|
140
|
+
lastConfirmedAt: recordedAt,
|
|
141
|
+
sourceRefs: mergeUniqueStrings(existing.sourceRefs, incoming.sourceRefs),
|
|
142
|
+
tags: mergeUniqueStrings(existing.tags, incoming.tags),
|
|
143
|
+
provenance: {
|
|
144
|
+
...existing.provenance,
|
|
145
|
+
refreshedFrom: incoming.provenance,
|
|
146
|
+
lastRefreshRunId: incoming.provenance.runId,
|
|
147
|
+
lastRefreshThreadId: incoming.provenance.threadId,
|
|
148
|
+
},
|
|
149
|
+
revision: existing.revision + 1,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
function mergeRecord(existing, incoming, recordedAt) {
|
|
153
|
+
return {
|
|
154
|
+
...existing,
|
|
155
|
+
content: mergeRecordContent(existing.content, incoming.content),
|
|
156
|
+
summary: incoming.summary.length >= existing.summary.length ? incoming.summary : existing.summary,
|
|
157
|
+
confidence: Math.max(existing.confidence, incoming.confidence),
|
|
158
|
+
observedAt: incoming.observedAt,
|
|
159
|
+
lastConfirmedAt: recordedAt,
|
|
160
|
+
sourceRefs: mergeUniqueStrings(existing.sourceRefs, incoming.sourceRefs),
|
|
161
|
+
tags: mergeUniqueStrings(existing.tags, incoming.tags),
|
|
162
|
+
provenance: {
|
|
163
|
+
...existing.provenance,
|
|
164
|
+
mergedFrom: incoming.provenance,
|
|
165
|
+
lastMergeRunId: incoming.provenance.runId,
|
|
166
|
+
lastMergeThreadId: incoming.provenance.threadId,
|
|
167
|
+
},
|
|
168
|
+
revision: existing.revision + 1,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
function markConflicted(record, conflictingRecordId, recordedAt) {
|
|
172
|
+
return {
|
|
173
|
+
...record,
|
|
174
|
+
status: "conflicted",
|
|
175
|
+
lastConfirmedAt: recordedAt,
|
|
176
|
+
conflictsWith: mergeUniqueStrings(record.conflictsWith, [conflictingRecordId]),
|
|
177
|
+
revision: record.revision + 1,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
function createReviewRecord(existing, incoming, recordedAt) {
|
|
181
|
+
return {
|
|
182
|
+
...incoming,
|
|
183
|
+
status: "pending_review",
|
|
184
|
+
conflictsWith: mergeUniqueStrings(incoming.conflictsWith, [existing.id]),
|
|
185
|
+
lastConfirmedAt: recordedAt,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
async function listStoredRecords(store, scope) {
|
|
189
|
+
const scopes = scope ? [scope] : MEMORY_SCOPES;
|
|
190
|
+
const docs = await Promise.all(scopes.map((currentScope) => store.search(buildScopeNamespace(currentScope))));
|
|
191
|
+
return docs
|
|
192
|
+
.flat()
|
|
193
|
+
.map((entry) => entry.value)
|
|
194
|
+
.filter((record) => typeof record?.id === "string" && typeof record.content === "string");
|
|
195
|
+
}
|
|
196
|
+
function findMatchingRecord(existingRecords, incoming) {
|
|
197
|
+
const exact = existingRecords.find((record) => record.scope === incoming.scope && isSameRecordContent(record, incoming));
|
|
198
|
+
if (exact) {
|
|
199
|
+
return exact;
|
|
200
|
+
}
|
|
201
|
+
const sourceRefMatch = existingRecords.find((record) => record.scope === incoming.scope &&
|
|
202
|
+
incoming.sourceRefs.some((sourceRef) => record.sourceRefs.includes(sourceRef)));
|
|
203
|
+
if (sourceRefMatch) {
|
|
204
|
+
return sourceRefMatch;
|
|
205
|
+
}
|
|
206
|
+
return existingRecords.find((record) => record.scope === incoming.scope && record.canonicalKey === incoming.canonicalKey);
|
|
207
|
+
}
|
|
208
|
+
function evaluateDecision(existing, incoming, recordedAt) {
|
|
209
|
+
if (!existing) {
|
|
210
|
+
return {
|
|
211
|
+
decision: {
|
|
212
|
+
action: "store",
|
|
213
|
+
reason: "Stored as a new durable memory record.",
|
|
214
|
+
recordId: incoming.id,
|
|
215
|
+
kind: incoming.kind,
|
|
216
|
+
scope: incoming.scope,
|
|
217
|
+
confidence: incoming.confidence,
|
|
218
|
+
maintenance: "dedupe",
|
|
219
|
+
reviewRequired: false,
|
|
220
|
+
},
|
|
221
|
+
primaryRecord: incoming,
|
|
222
|
+
additionalRecords: [],
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
if (isSameRecordContent(existing, incoming)) {
|
|
226
|
+
const refreshed = refreshRecord(existing, incoming, recordedAt);
|
|
227
|
+
return {
|
|
228
|
+
decision: {
|
|
229
|
+
action: "refresh",
|
|
230
|
+
reason: "Refreshed an exact durable memory match.",
|
|
231
|
+
recordId: refreshed.id,
|
|
232
|
+
kind: refreshed.kind,
|
|
233
|
+
scope: refreshed.scope,
|
|
234
|
+
confidence: refreshed.confidence,
|
|
235
|
+
maintenance: "none",
|
|
236
|
+
reviewRequired: false,
|
|
237
|
+
},
|
|
238
|
+
primaryRecord: refreshed,
|
|
239
|
+
additionalRecords: [],
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
if (existing.canonicalKey === incoming.canonicalKey ||
|
|
243
|
+
incoming.sourceRefs.some((sourceRef) => existing.sourceRefs.includes(sourceRef))) {
|
|
244
|
+
if (hasNegationSignal(existing.content) !== hasNegationSignal(incoming.content)) {
|
|
245
|
+
const conflictedExisting = markConflicted(existing, incoming.id, recordedAt);
|
|
246
|
+
const pendingReview = createReviewRecord(conflictedExisting, incoming, recordedAt);
|
|
247
|
+
return {
|
|
248
|
+
decision: {
|
|
249
|
+
action: "review",
|
|
250
|
+
reason: "Detected conflicting durable memory with the same canonical identity.",
|
|
251
|
+
recordId: pendingReview.id,
|
|
252
|
+
kind: pendingReview.kind,
|
|
253
|
+
scope: pendingReview.scope,
|
|
254
|
+
confidence: pendingReview.confidence,
|
|
255
|
+
maintenance: "review",
|
|
256
|
+
reviewRequired: true,
|
|
257
|
+
},
|
|
258
|
+
primaryRecord: conflictedExisting,
|
|
259
|
+
additionalRecords: [pendingReview],
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
const similarity = jaccardSimilarity(existing.summary, incoming.summary);
|
|
263
|
+
if (similarity >= 0.45) {
|
|
264
|
+
const merged = mergeRecord(existing, incoming, recordedAt);
|
|
265
|
+
return {
|
|
266
|
+
decision: {
|
|
267
|
+
action: "merge",
|
|
268
|
+
reason: "Merged a matching durable memory record with compatible additions.",
|
|
269
|
+
recordId: merged.id,
|
|
270
|
+
kind: merged.kind,
|
|
271
|
+
scope: merged.scope,
|
|
272
|
+
confidence: merged.confidence,
|
|
273
|
+
maintenance: "merge",
|
|
274
|
+
reviewRequired: false,
|
|
275
|
+
},
|
|
276
|
+
primaryRecord: merged,
|
|
277
|
+
additionalRecords: [],
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
const conflictedExisting = markConflicted(existing, incoming.id, recordedAt);
|
|
281
|
+
const pendingReview = createReviewRecord(conflictedExisting, incoming, recordedAt);
|
|
282
|
+
return {
|
|
283
|
+
decision: {
|
|
284
|
+
action: "review",
|
|
285
|
+
reason: "Detected conflicting durable memory with the same canonical identity.",
|
|
286
|
+
recordId: pendingReview.id,
|
|
287
|
+
kind: pendingReview.kind,
|
|
288
|
+
scope: pendingReview.scope,
|
|
289
|
+
confidence: pendingReview.confidence,
|
|
290
|
+
maintenance: "review",
|
|
291
|
+
reviewRequired: true,
|
|
292
|
+
},
|
|
293
|
+
primaryRecord: conflictedExisting,
|
|
294
|
+
additionalRecords: [pendingReview],
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
return {
|
|
298
|
+
decision: {
|
|
299
|
+
action: "store",
|
|
300
|
+
reason: "Stored as a new durable memory record.",
|
|
301
|
+
recordId: incoming.id,
|
|
302
|
+
kind: incoming.kind,
|
|
303
|
+
scope: incoming.scope,
|
|
304
|
+
confidence: incoming.confidence,
|
|
305
|
+
maintenance: "dedupe",
|
|
306
|
+
reviewRequired: false,
|
|
307
|
+
},
|
|
308
|
+
primaryRecord: incoming,
|
|
309
|
+
additionalRecords: [],
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
async function putRecordWithIndexes(store, record, updatedAt) {
|
|
313
|
+
const canonicalKeyId = createFingerprint(record.canonicalKey);
|
|
314
|
+
await store.put(buildScopeNamespace(record.scope), `${record.id}.json`, record);
|
|
315
|
+
await store.put(["memories", "indexes", "canonical", record.scope], `${canonicalKeyId}-${record.id}.json`, {
|
|
316
|
+
canonicalKey: record.canonicalKey,
|
|
317
|
+
recordId: record.id,
|
|
318
|
+
scope: record.scope,
|
|
319
|
+
kind: record.kind,
|
|
320
|
+
updatedAt,
|
|
321
|
+
});
|
|
322
|
+
await Promise.all(record.sourceRefs.map((sourceRef) => {
|
|
323
|
+
const sourceRefId = createFingerprint(sourceRef);
|
|
324
|
+
return store.put(["memories", "indexes", "source-ref", record.scope], `${sourceRefId}-${record.id}.json`, {
|
|
325
|
+
sourceRef,
|
|
326
|
+
recordId: record.id,
|
|
327
|
+
scope: record.scope,
|
|
328
|
+
kind: record.kind,
|
|
329
|
+
updatedAt,
|
|
330
|
+
});
|
|
331
|
+
}));
|
|
332
|
+
}
|
|
333
|
+
export function renderMemoryRecordsMarkdown(title, records) {
|
|
334
|
+
const lines = [`# ${title}`, ""];
|
|
335
|
+
if (records.length === 0) {
|
|
336
|
+
lines.push("(none)", "");
|
|
337
|
+
return lines.join("\n");
|
|
338
|
+
}
|
|
339
|
+
for (const record of records) {
|
|
340
|
+
lines.push(`## ${record.summary}`);
|
|
341
|
+
lines.push(`- kind: ${record.kind}`);
|
|
342
|
+
lines.push(`- scope: ${record.scope}`);
|
|
343
|
+
lines.push(`- status: ${record.status}`);
|
|
344
|
+
lines.push(`- confidence: ${record.confidence.toFixed(2)}`);
|
|
345
|
+
if (record.tags.length > 0) {
|
|
346
|
+
lines.push(`- tags: ${record.tags.join(", ")}`);
|
|
347
|
+
}
|
|
348
|
+
lines.push("");
|
|
349
|
+
lines.push(record.content);
|
|
350
|
+
lines.push("");
|
|
351
|
+
}
|
|
352
|
+
return lines.join("\n");
|
|
353
|
+
}
|
|
354
|
+
export async function rebuildStructuredMemoryProjections(store, namespace, title, scope, maxEntries) {
|
|
355
|
+
const records = (await listStoredRecords(store, scope))
|
|
356
|
+
.filter((record) => record.status === "active")
|
|
357
|
+
.sort((left, right) => left.lastConfirmedAt.localeCompare(right.lastConfirmedAt))
|
|
358
|
+
.slice(-maxEntries);
|
|
359
|
+
await store.put(namespace, "structured-memory.md", {
|
|
360
|
+
content: `${renderMemoryRecordsMarkdown(title, records)}\n`,
|
|
361
|
+
items: records,
|
|
362
|
+
});
|
|
363
|
+
const grouped = new Map();
|
|
364
|
+
for (const record of records) {
|
|
365
|
+
const current = grouped.get(record.kind) ?? [];
|
|
366
|
+
current.push(record);
|
|
367
|
+
grouped.set(record.kind, current);
|
|
368
|
+
}
|
|
369
|
+
await Promise.all(Array.from(grouped.entries()).map(([kind, items]) => store.put(namespace, `${kind}.md`, {
|
|
370
|
+
content: `${renderMemoryRecordsMarkdown(`${title} (${kind})`, items)}\n`,
|
|
371
|
+
items,
|
|
372
|
+
})));
|
|
373
|
+
}
|
|
374
|
+
export async function listMemoryRecordsForScopes(store, scopes) {
|
|
375
|
+
const all = await Promise.all(scopes.map((scope) => listStoredRecords(store, scope)));
|
|
376
|
+
return all.flat();
|
|
377
|
+
}
|
|
378
|
+
export async function persistStructuredMemoryRecords(options) {
|
|
379
|
+
const existingRecords = await listStoredRecords(options.store);
|
|
380
|
+
const persistedRecords = [];
|
|
381
|
+
const decisions = [];
|
|
382
|
+
for (const candidate of options.candidates) {
|
|
383
|
+
const incoming = createMemoryRecord(candidate, options);
|
|
384
|
+
const existing = findMatchingRecord(existingRecords, incoming);
|
|
385
|
+
const evaluated = evaluateDecision(existing, incoming, options.recordedAt);
|
|
386
|
+
const recordsToWrite = [evaluated.primaryRecord, ...evaluated.additionalRecords];
|
|
387
|
+
for (const record of recordsToWrite) {
|
|
388
|
+
await putRecordWithIndexes(options.store, record, options.recordedAt);
|
|
389
|
+
const existingIndex = existingRecords.findIndex((item) => item.id === record.id);
|
|
390
|
+
if (existingIndex >= 0) {
|
|
391
|
+
existingRecords[existingIndex] = record;
|
|
392
|
+
}
|
|
393
|
+
else {
|
|
394
|
+
existingRecords.push(record);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
await options.store.put(["memories", "decisions", options.threadId, options.runId], `${evaluated.decision.recordId ?? incoming.id}.json`, evaluated.decision);
|
|
398
|
+
decisions.push(evaluated.decision);
|
|
399
|
+
persistedRecords.push(...recordsToWrite);
|
|
400
|
+
}
|
|
401
|
+
return {
|
|
402
|
+
records: persistedRecords,
|
|
403
|
+
decisions,
|
|
404
|
+
};
|
|
405
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ApprovalRecord, CancelOptions, HarnessEvent, HarnessStreamItem, RuntimeHealthSnapshot, MessageContent, RunRecord, RunStartOptions, RestartConversationOptions, RuntimeAdapterOptions, ResumeOptions, RunOptions, RunResult, RunSummary, ThreadSummary, ThreadRecord, WorkspaceBundle } from "../contracts/types.js";
|
|
1
|
+
import type { ApprovalRecord, CancelOptions, HarnessEvent, HarnessStreamItem, RuntimeHealthSnapshot, MessageContent, RunRecord, RunStartOptions, RestartConversationOptions, RuntimeAdapterOptions, ResumeOptions, RunOptions, RunResult, RunSummary, MemorizeInput, MemorizeResult, RecallInput, RecallResult, ThreadSummary, ThreadRecord, WorkspaceBundle } from "../contracts/types.js";
|
|
2
2
|
import { type ToolMcpServerOptions } from "../mcp.js";
|
|
3
3
|
import { type InventoryAgentRecord, type InventorySkillRecord } from "./harness/system/inventory.js";
|
|
4
4
|
import type { RequirementAssessmentOptions } from "./harness/system/skill-requirements.js";
|
|
@@ -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;
|
|
@@ -61,6 +62,8 @@ export declare class AgentHarnessRuntime {
|
|
|
61
62
|
threadId?: string;
|
|
62
63
|
state?: RunSummary["state"];
|
|
63
64
|
}): Promise<RunSummary[]>;
|
|
65
|
+
memorize(input: MemorizeInput): Promise<MemorizeResult>;
|
|
66
|
+
recall(input: RecallInput): Promise<RecallResult>;
|
|
64
67
|
getRun(runId: string): Promise<RunRecord | null>;
|
|
65
68
|
private getSession;
|
|
66
69
|
getThread(threadId: string): Promise<ThreadRecord | null>;
|
|
@@ -92,8 +95,13 @@ export declare class AgentHarnessRuntime {
|
|
|
92
95
|
private finalizeCancelledRun;
|
|
93
96
|
private invokeWithHistory;
|
|
94
97
|
private resolveMemoryNamespace;
|
|
98
|
+
private getWorkspaceId;
|
|
99
|
+
private resolveRecallScopes;
|
|
100
|
+
private matchesRecallScope;
|
|
101
|
+
private getMemoryScopeBoost;
|
|
95
102
|
private buildRuntimeMemoryContext;
|
|
96
103
|
private persistRuntimeMemoryCandidates;
|
|
104
|
+
private persistStructuredMemoryCandidates;
|
|
97
105
|
private appendMemoryDigest;
|
|
98
106
|
private resolvePersistedRunPriority;
|
|
99
107
|
private enqueuePendingRunSlot;
|
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,
|
|
@@ -238,6 +243,78 @@ export class AgentHarnessRuntime {
|
|
|
238
243
|
async listRuns(filter) {
|
|
239
244
|
return this.persistence.listRuns(filter);
|
|
240
245
|
}
|
|
246
|
+
async memorize(input) {
|
|
247
|
+
const binding = this.defaultRuntimeEntryBinding;
|
|
248
|
+
if (!binding) {
|
|
249
|
+
throw new Error("memorize requires a runtime entry binding.");
|
|
250
|
+
}
|
|
251
|
+
if (!Array.isArray(input.records)) {
|
|
252
|
+
throw new Error("memorize requires input.records to be an array.");
|
|
253
|
+
}
|
|
254
|
+
const candidates = input.records
|
|
255
|
+
.filter((record) => typeof record === "object" && record !== null)
|
|
256
|
+
.filter((record) => record.noStore !== true);
|
|
257
|
+
if (candidates.length === 0) {
|
|
258
|
+
return { records: [], decisions: [] };
|
|
259
|
+
}
|
|
260
|
+
if (candidates.some((record) => typeof record.content !== "string" || record.content.trim().length === 0)) {
|
|
261
|
+
throw new Error("memorize requires every record to include non-empty content.");
|
|
262
|
+
}
|
|
263
|
+
if (candidates.some((record) => (record.scope ?? "thread") === "thread") && !input.threadId) {
|
|
264
|
+
throw new Error("memorize requires threadId when storing thread-scoped memory.");
|
|
265
|
+
}
|
|
266
|
+
const recordedAt = input.recordedAt ?? new Date().toISOString();
|
|
267
|
+
const runId = input.runId ?? createPersistentId(new Date(recordedAt));
|
|
268
|
+
const threadId = input.threadId ?? `memory-api-${runId}`;
|
|
269
|
+
return this.persistStructuredMemoryCandidates(binding, {
|
|
270
|
+
candidates: candidates.map((record) => ({ ...record, content: record.content.trim() })),
|
|
271
|
+
threadId,
|
|
272
|
+
runId,
|
|
273
|
+
agentId: input.agentId ?? binding.agent.id,
|
|
274
|
+
userId: input.userId,
|
|
275
|
+
projectId: input.projectId,
|
|
276
|
+
recordedAt,
|
|
277
|
+
storeCandidateLog: true,
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
async recall(input) {
|
|
281
|
+
const binding = this.defaultRuntimeEntryBinding;
|
|
282
|
+
if (!binding) {
|
|
283
|
+
throw new Error("recall requires a runtime entry binding.");
|
|
284
|
+
}
|
|
285
|
+
if (typeof input.query !== "string" || input.query.trim().length === 0) {
|
|
286
|
+
throw new Error("recall requires a non-empty query.");
|
|
287
|
+
}
|
|
288
|
+
const workspaceId = this.getWorkspaceId(binding);
|
|
289
|
+
const agentId = input.agentId ?? binding.agent.id;
|
|
290
|
+
const userId = input.userId ?? "default";
|
|
291
|
+
const projectId = input.projectId ?? workspaceId;
|
|
292
|
+
const scopes = this.resolveRecallScopes(input);
|
|
293
|
+
const topK = typeof input.topK === "number" && Number.isInteger(input.topK) && input.topK > 0
|
|
294
|
+
? input.topK
|
|
295
|
+
: (this.runtimeMemoryPolicy?.retrieval.defaultTopK ?? 5);
|
|
296
|
+
const kinds = input.kinds?.length ? new Set(input.kinds) : null;
|
|
297
|
+
const items = (await listMemoryRecordsForScopes(this.runtimeMemoryStore, scopes))
|
|
298
|
+
.filter((record) => this.matchesRecallScope(record, {
|
|
299
|
+
threadId: input.threadId,
|
|
300
|
+
agentId,
|
|
301
|
+
workspaceId: input.workspaceId ?? workspaceId,
|
|
302
|
+
userId,
|
|
303
|
+
projectId,
|
|
304
|
+
}))
|
|
305
|
+
.filter((record) => (input.includeStale ? record.status === "active" || record.status === "stale" : record.status === "active"))
|
|
306
|
+
.filter((record) => (kinds ? kinds.has(record.kind) : true))
|
|
307
|
+
.map((record) => ({
|
|
308
|
+
record,
|
|
309
|
+
score: scoreMemoryText(input.query.trim(), `${record.summary}\n${record.content}`, this.getMemoryScopeBoost(record.scope)) +
|
|
310
|
+
Math.max(0, 1 - ((Date.now() - Date.parse(record.lastConfirmedAt)) / (1000 * 60 * 60 * 24 * 365))) +
|
|
311
|
+
record.confidence,
|
|
312
|
+
}))
|
|
313
|
+
.sort((left, right) => right.score - left.score)
|
|
314
|
+
.slice(0, topK)
|
|
315
|
+
.map(({ record }) => record);
|
|
316
|
+
return { items };
|
|
317
|
+
}
|
|
241
318
|
async getRun(runId) {
|
|
242
319
|
return this.persistence.getRun(runId);
|
|
243
320
|
}
|
|
@@ -387,8 +464,7 @@ export class AgentHarnessRuntime {
|
|
|
387
464
|
}
|
|
388
465
|
}
|
|
389
466
|
resolveMemoryNamespace(scope, binding, options = {}) {
|
|
390
|
-
const
|
|
391
|
-
const workspaceId = path.basename(workspaceRoot) || "default";
|
|
467
|
+
const workspaceId = this.getWorkspaceId(binding);
|
|
392
468
|
const template = this.runtimeMemoryPolicy?.namespaces[scope] ?? `memories/${scope}s/{${scope}Id}`;
|
|
393
469
|
return resolveMemoryNamespace(template, {
|
|
394
470
|
threadId: options.threadId,
|
|
@@ -398,34 +474,82 @@ export class AgentHarnessRuntime {
|
|
|
398
474
|
projectId: options.projectId ?? workspaceId,
|
|
399
475
|
});
|
|
400
476
|
}
|
|
477
|
+
getWorkspaceId(binding) {
|
|
478
|
+
const workspaceRoot = binding.harnessRuntime.workspaceRoot ?? this.workspace.workspaceRoot;
|
|
479
|
+
return path.basename(workspaceRoot) || "default";
|
|
480
|
+
}
|
|
481
|
+
resolveRecallScopes(input) {
|
|
482
|
+
if (Array.isArray(input.scopes) && input.scopes.length > 0) {
|
|
483
|
+
return Array.from(new Set(input.scopes));
|
|
484
|
+
}
|
|
485
|
+
const scopes = new Set(["thread", "agent", "workspace"]);
|
|
486
|
+
if (input.userId) {
|
|
487
|
+
scopes.add("user");
|
|
488
|
+
}
|
|
489
|
+
if (input.projectId) {
|
|
490
|
+
scopes.add("project");
|
|
491
|
+
}
|
|
492
|
+
return Array.from(scopes);
|
|
493
|
+
}
|
|
494
|
+
matchesRecallScope(record, filters) {
|
|
495
|
+
if (record.scope === "thread") {
|
|
496
|
+
return typeof filters.threadId === "string" && String(record.provenance.threadId ?? "") === filters.threadId;
|
|
497
|
+
}
|
|
498
|
+
if (record.scope === "agent") {
|
|
499
|
+
return String(record.provenance.agentId ?? "") === filters.agentId;
|
|
500
|
+
}
|
|
501
|
+
if (record.scope === "workspace") {
|
|
502
|
+
return String(record.provenance.workspaceId ?? filters.workspaceId) === filters.workspaceId;
|
|
503
|
+
}
|
|
504
|
+
if (record.scope === "user") {
|
|
505
|
+
return String(record.provenance.userId ?? "default") === filters.userId;
|
|
506
|
+
}
|
|
507
|
+
return String(record.provenance.projectId ?? filters.workspaceId) === filters.projectId;
|
|
508
|
+
}
|
|
509
|
+
getMemoryScopeBoost(scope) {
|
|
510
|
+
if (scope === "thread") {
|
|
511
|
+
return 4;
|
|
512
|
+
}
|
|
513
|
+
if (scope === "agent") {
|
|
514
|
+
return 2;
|
|
515
|
+
}
|
|
516
|
+
if (scope === "workspace") {
|
|
517
|
+
return 1;
|
|
518
|
+
}
|
|
519
|
+
return 0;
|
|
520
|
+
}
|
|
401
521
|
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
522
|
const query = typeof input === "string" ? input : JSON.stringify(input ?? "");
|
|
416
|
-
const ranked =
|
|
417
|
-
.
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
523
|
+
const ranked = (await listMemoryRecordsForScopes(this.runtimeMemoryStore, ["thread", "agent", "workspace"]))
|
|
524
|
+
.filter((record) => {
|
|
525
|
+
if (record.status !== "active") {
|
|
526
|
+
return false;
|
|
527
|
+
}
|
|
528
|
+
if (record.scope === "thread") {
|
|
529
|
+
return String(record.provenance.threadId ?? "") === threadId;
|
|
530
|
+
}
|
|
531
|
+
if (record.scope === "agent") {
|
|
532
|
+
return String(record.provenance.agentId ?? "") === binding.agent.id;
|
|
421
533
|
}
|
|
422
|
-
|
|
534
|
+
return true;
|
|
535
|
+
})
|
|
536
|
+
.map((record) => {
|
|
537
|
+
const scopeBoost = record.scope === "thread" ? 4 : record.scope === "agent" ? 2 : 1;
|
|
538
|
+
const freshnessBoost = Math.max(0, 1 - ((Date.now() - Date.parse(record.lastConfirmedAt)) / (1000 * 60 * 60 * 24 * 365)));
|
|
423
539
|
return {
|
|
424
|
-
content:
|
|
425
|
-
|
|
540
|
+
content: [
|
|
541
|
+
`# Durable ${record.scope[0].toUpperCase()}${record.scope.slice(1)} Memory`,
|
|
542
|
+
"",
|
|
543
|
+
`- summary: ${record.summary}`,
|
|
544
|
+
`- kind: ${record.kind}`,
|
|
545
|
+
`- scope: ${record.scope}`,
|
|
546
|
+
`- confidence: ${record.confidence.toFixed(2)}`,
|
|
547
|
+
"",
|
|
548
|
+
record.content,
|
|
549
|
+
].join("\n"),
|
|
550
|
+
score: scoreMemoryText(query, `${record.summary}\n${record.content}`, scopeBoost) + freshnessBoost + record.confidence,
|
|
426
551
|
};
|
|
427
552
|
})
|
|
428
|
-
.filter((value) => Boolean(value))
|
|
429
553
|
.sort((left, right) => right.score - left.score)
|
|
430
554
|
.slice(0, this.runtimeMemoryPolicy?.retrieval.maxPromptMemories ?? 8);
|
|
431
555
|
if (ranked.length === 0) {
|
|
@@ -443,26 +567,77 @@ export class AgentHarnessRuntime {
|
|
|
443
567
|
if (candidates.length === 0) {
|
|
444
568
|
return;
|
|
445
569
|
}
|
|
446
|
-
|
|
447
|
-
const workspaceCandidates = candidates.filter((candidate) => (candidate.scope ?? "thread") === "workspace");
|
|
448
|
-
const agentCandidates = candidates.filter((candidate) => (candidate.scope ?? "thread") === "agent");
|
|
449
|
-
await this.runtimeMemoryStore.put(["memories", "candidates", threadId], `${runId}.json`, {
|
|
450
|
-
runId,
|
|
451
|
-
threadId,
|
|
452
|
-
storedAt: new Date().toISOString(),
|
|
570
|
+
await this.persistStructuredMemoryCandidates(binding, {
|
|
453
571
|
candidates,
|
|
572
|
+
threadId,
|
|
573
|
+
runId,
|
|
574
|
+
agentId: binding.agent.id,
|
|
575
|
+
recordedAt: new Date().toISOString(),
|
|
576
|
+
storeCandidateLog: true,
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
async persistStructuredMemoryCandidates(binding, input) {
|
|
580
|
+
const workspaceId = this.getWorkspaceId(binding);
|
|
581
|
+
const userId = input.userId ?? "default";
|
|
582
|
+
const projectId = input.projectId ?? workspaceId;
|
|
583
|
+
const threadCandidates = input.candidates.filter((candidate) => (candidate.scope ?? "thread") === "thread");
|
|
584
|
+
const workspaceCandidates = input.candidates.filter((candidate) => (candidate.scope ?? "thread") === "workspace");
|
|
585
|
+
const agentCandidates = input.candidates.filter((candidate) => (candidate.scope ?? "thread") === "agent");
|
|
586
|
+
if (input.storeCandidateLog) {
|
|
587
|
+
await this.runtimeMemoryStore.put(["memories", "candidates", input.threadId], `${input.runId}.json`, {
|
|
588
|
+
runId: input.runId,
|
|
589
|
+
threadId: input.threadId,
|
|
590
|
+
storedAt: input.recordedAt,
|
|
591
|
+
candidates: input.candidates,
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
const persisted = await persistStructuredMemoryRecords({
|
|
595
|
+
store: this.runtimeMemoryStore,
|
|
596
|
+
candidates: input.candidates,
|
|
597
|
+
threadId: input.threadId,
|
|
598
|
+
runId: input.runId,
|
|
599
|
+
agentId: input.agentId,
|
|
600
|
+
workspaceId,
|
|
601
|
+
userId,
|
|
602
|
+
projectId,
|
|
603
|
+
recordedAt: input.recordedAt,
|
|
454
604
|
});
|
|
455
605
|
const writes = [];
|
|
456
606
|
if (threadCandidates.length > 0) {
|
|
457
|
-
writes.push(this.appendMemoryDigest(this.resolveMemoryNamespace("thread", binding, { threadId }), "tool-memory.md", threadCandidates, 12, "Thread Tool Memory"));
|
|
607
|
+
writes.push(this.appendMemoryDigest(this.resolveMemoryNamespace("thread", binding, { threadId: input.threadId }), "tool-memory.md", threadCandidates, 12, "Thread Tool Memory"));
|
|
608
|
+
writes.push(consolidateStructuredMemoryScope({
|
|
609
|
+
store: this.runtimeMemoryStore,
|
|
610
|
+
namespace: this.resolveMemoryNamespace("thread", binding, { threadId: input.threadId }),
|
|
611
|
+
title: "Thread Structured Memory",
|
|
612
|
+
scope: "thread",
|
|
613
|
+
maxEntries: 12,
|
|
614
|
+
config: this.runtimeMemoryMaintenanceConfig ?? undefined,
|
|
615
|
+
}));
|
|
458
616
|
}
|
|
459
617
|
if (workspaceCandidates.length > 0) {
|
|
460
618
|
writes.push(this.appendMemoryDigest(this.resolveMemoryNamespace("workspace", binding), "tool-memory.md", workspaceCandidates, 20, "Workspace Tool Memory"));
|
|
619
|
+
writes.push(consolidateStructuredMemoryScope({
|
|
620
|
+
store: this.runtimeMemoryStore,
|
|
621
|
+
namespace: this.resolveMemoryNamespace("workspace", binding),
|
|
622
|
+
title: "Workspace Structured Memory",
|
|
623
|
+
scope: "workspace",
|
|
624
|
+
maxEntries: 20,
|
|
625
|
+
config: this.runtimeMemoryMaintenanceConfig ?? undefined,
|
|
626
|
+
}));
|
|
461
627
|
}
|
|
462
628
|
if (agentCandidates.length > 0) {
|
|
463
629
|
writes.push(this.appendMemoryDigest(this.resolveMemoryNamespace("agent", binding), "tool-memory.md", agentCandidates, 20, "Agent Tool Memory"));
|
|
630
|
+
writes.push(consolidateStructuredMemoryScope({
|
|
631
|
+
store: this.runtimeMemoryStore,
|
|
632
|
+
namespace: this.resolveMemoryNamespace("agent", binding),
|
|
633
|
+
title: "Agent Structured Memory",
|
|
634
|
+
scope: "agent",
|
|
635
|
+
maxEntries: 20,
|
|
636
|
+
config: this.runtimeMemoryMaintenanceConfig ?? undefined,
|
|
637
|
+
}));
|
|
464
638
|
}
|
|
465
639
|
await Promise.all(writes);
|
|
640
|
+
return persisted;
|
|
466
641
|
}
|
|
467
642
|
async appendMemoryDigest(namespace, key, candidates, maxEntries, title) {
|
|
468
643
|
const existing = await this.runtimeMemoryStore.get(namespace, key);
|