@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 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.151";
1
+ export declare const AGENT_HARNESS_VERSION = "0.0.153";
@@ -1 +1 @@
1
- export const AGENT_HARNESS_VERSION = "0.0.151";
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;
@@ -30,7 +30,9 @@ import { describeWorkspaceInventory, getAgentInventoryRecord, listAgentSkills as
30
30
  import { createDefaultHealthSnapshot, isInventoryEnabled, isThreadMemorySyncEnabled, } from "./harness/runtime-defaults.js";
31
31
  import { Mem0IngestionSync, readMem0RuntimeConfig } from "./harness/system/mem0-ingestion-sync.js";
32
32
  import { renderMemoryCandidatesMarkdown } from "./harness/system/runtime-memory-candidates.js";
33
- import { normalizeLangMemMemoryKind, readRuntimeMemoryPolicyConfig, resolveMemoryNamespace, scoreMemoryText, } from "./harness/system/runtime-memory-policy.js";
33
+ import { listMemoryRecordsForScopes, persistStructuredMemoryRecords, } from "./harness/system/runtime-memory-records.js";
34
+ import { consolidateStructuredMemoryScope } from "./harness/system/runtime-memory-consolidation.js";
35
+ import { normalizeLangMemMemoryKind, readRuntimeMemoryMaintenanceConfig, readRuntimeMemoryPolicyConfig, resolveMemoryNamespace, scoreMemoryText, } from "./harness/system/runtime-memory-policy.js";
34
36
  import { resolveRuntimeAdapterOptions } from "./support/runtime-adapter-options.js";
35
37
  import { initializeHarnessRuntime, reclaimExpiredClaimedRuns as reclaimHarnessExpiredClaimedRuns, recoverStartupRuns as recoverHarnessStartupRuns, isStaleRunningRun as isHarnessStaleRunningRun, } from "./harness/run/startup-runtime.js";
36
38
  import { streamHarnessRun } from "./harness/run/stream-run.js";
@@ -59,6 +61,7 @@ export class AgentHarnessRuntime {
59
61
  defaultStore;
60
62
  runtimeMemoryStore;
61
63
  runtimeMemoryPolicy;
64
+ runtimeMemoryMaintenanceConfig;
62
65
  routingRules;
63
66
  routingDefaultAgentId;
64
67
  threadMemorySync;
@@ -123,6 +126,8 @@ export class AgentHarnessRuntime {
123
126
  : undefined;
124
127
  this.runtimeMemoryStore = resolveStoreFromConfig(this.stores, runtimeMemoryStoreConfig, runRoot) ?? this.defaultStore;
125
128
  this.runtimeMemoryPolicy = readRuntimeMemoryPolicyConfig(this.defaultRuntimeEntryBinding?.harnessRuntime.runtimeMemory, this.workspace.workspaceRoot) ?? null;
129
+ this.runtimeMemoryMaintenanceConfig =
130
+ readRuntimeMemoryMaintenanceConfig(this.defaultRuntimeEntryBinding?.harnessRuntime.runtimeMemory) ?? null;
126
131
  this.resolvedRuntimeAdapterOptions = resolveRuntimeAdapterOptions({
127
132
  workspace,
128
133
  runtimeAdapterOptions,
@@ -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 workspaceRoot = binding.harnessRuntime.workspaceRoot ?? this.workspace.workspaceRoot;
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 = docs
417
- .map((doc, index) => {
418
- const content = doc?.value?.content;
419
- if (typeof content !== "string" || content.trim().length === 0) {
420
- return null;
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
- const scopeBoost = index < 5 ? 4 : index === 5 ? 2 : 1;
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: content.trim(),
425
- score: scoreMemoryText(query, content, scopeBoost),
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
- const threadCandidates = candidates.filter((candidate) => (candidate.scope ?? "thread") === "thread");
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@botbotgo/agent-harness",
3
- "version": "0.0.152",
3
+ "version": "0.0.154",
4
4
  "description": "Workspace runtime for multi-agent applications",
5
5
  "type": "module",
6
6
  "packageManager": "npm@10.9.2",