@botbotgo/agent-harness 0.0.305 → 0.0.307

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
@@ -942,6 +942,8 @@ The default repository shape uses:
942
942
  - `KnowledgeRuntime`: hot path + background formation + long-term maintenance
943
943
  - `ProceduralMemoryRuntime`: background formation + scheduled or idle maintenance
944
944
 
945
+ In the shipped runtime, explicit durable facts such as “remember I moved to the United States” still go to `KnowledgeRuntime` and land in `knowledge/knowledge.sqlite`. Background procedural learning writes its own store and state files under the same data root, such as `knowledge/procedural-memory.sqlite` and `knowledge/procedural-memory-state.json`.
946
+
945
947
  For DeepAgents-backed workspaces, keep upstream context compaction upstream-owned and use procedural memory only as a background learning layer.
946
948
 
947
949
  ### `config/catalogs/backends.yaml`
package/README.zh.md CHANGED
@@ -910,6 +910,8 @@ spec:
910
910
  - `KnowledgeRuntime`:hot path + 后台形成 + 长期养护
911
911
  - `ProceduralMemoryRuntime`:后台形成 + 定时或空闲整理
912
912
 
913
+ 在当前已发布的 runtime 里,像“请记住我最近搬到了美国”这种显式 durable fact 仍然走 `KnowledgeRuntime`,并写入 `knowledge/knowledge.sqlite`。后台 procedural learning 会在同一个 data root 下单独写自己的 store 与 state 文件,例如 `knowledge/procedural-memory.sqlite` 和 `knowledge/procedural-memory-state.json`。
914
+
913
915
  对于 `backend: deepagent` 的工作区,应继续把 context compaction 留给上游 DeepAgents,只把 procedural memory 当作后台学习层使用。
914
916
 
915
917
  ### `config/catalogs/backends.yaml`
@@ -1 +1 @@
1
- export declare const AGENT_HARNESS_VERSION = "0.0.304";
1
+ export declare const AGENT_HARNESS_VERSION = "0.0.306";
@@ -1 +1 @@
1
- export const AGENT_HARNESS_VERSION = "0.0.304";
1
+ export const AGENT_HARNESS_VERSION = "0.0.306";
@@ -1,2 +1,3 @@
1
1
  export { readProceduralMemoryRuntimeConfig } from "./config.js";
2
+ export { createBackgroundProceduralCandidates, createProceduralMemoryManager, ProceduralMemoryFormationSync, } from "./manager.js";
2
3
  export type { ProceduralMemoryBackgroundConfig, ProceduralMemoryFormationConfig, ProceduralMemoryMaintenanceConfig, ProceduralMemoryMaintenanceIdleConfig, ProceduralMemoryMaintenanceScheduleConfig, ProceduralMemoryProviderConfig, ProceduralMemoryRetrievalConfig, ProceduralMemoryRuntimeConfig, } from "./config.js";
@@ -1 +1,2 @@
1
1
  export { readProceduralMemoryRuntimeConfig } from "./config.js";
2
+ export { createBackgroundProceduralCandidates, createProceduralMemoryManager, ProceduralMemoryFormationSync, } from "./manager.js";
@@ -0,0 +1,59 @@
1
+ import type { CompiledAgentBinding, HarnessEvent, HarnessEventProjection, InternalApprovalRecord, MemoryCandidate, MemoryRecord, SessionSummary, TranscriptMessage, WorkspaceBundle } from "../contracts/types.js";
2
+ import type { RuntimePersistence } from "../persistence/types.js";
3
+ import { type StoreLike } from "../runtime/harness/system/store.js";
4
+ import type { ProceduralMemoryRuntimeConfig } from "./config.js";
5
+ type ProceduralMemoryWriter = (input: {
6
+ candidates: MemoryCandidate[];
7
+ sessionId: string;
8
+ requestId: string;
9
+ agentId: string;
10
+ userId?: string;
11
+ projectId?: string;
12
+ recordedAt: string;
13
+ }) => Promise<void>;
14
+ type ProceduralMemoryFormationOptions = {
15
+ userId?: string;
16
+ projectId?: string;
17
+ };
18
+ type ProceduralMemoryManagerLike = {
19
+ transform(input: {
20
+ candidates: MemoryCandidate[];
21
+ binding: CompiledAgentBinding;
22
+ sessionId: string;
23
+ requestId: string;
24
+ recordedAt: string;
25
+ existingRecords: MemoryRecord[];
26
+ }): Promise<MemoryCandidate[]>;
27
+ };
28
+ export declare function createBackgroundProceduralCandidates(input: {
29
+ session: SessionSummary;
30
+ requestId: string;
31
+ agentId: string;
32
+ trigger: "approval.resolved" | "request.completed";
33
+ recordedAt: string;
34
+ messages: TranscriptMessage[];
35
+ approvals: InternalApprovalRecord[];
36
+ focus: string[];
37
+ }): MemoryCandidate[];
38
+ export declare function createProceduralMemoryManager(input: {
39
+ workspace: WorkspaceBundle;
40
+ binding: CompiledAgentBinding;
41
+ config: ProceduralMemoryRuntimeConfig;
42
+ modelResolver?: (modelId: string) => unknown;
43
+ }): ProceduralMemoryManagerLike;
44
+ export declare class ProceduralMemoryFormationSync implements HarnessEventProjection {
45
+ private readonly persistence;
46
+ private readonly config;
47
+ private readonly writer;
48
+ private readonly stateStore;
49
+ private readonly options;
50
+ private readonly pending;
51
+ private syncChain;
52
+ readonly name = "procedural-memory-formation-sync";
53
+ constructor(persistence: RuntimePersistence, config: ProceduralMemoryRuntimeConfig, writer: ProceduralMemoryWriter, runtimeRoot: string, stateStore?: StoreLike, options?: ProceduralMemoryFormationOptions);
54
+ shouldHandle(event: HarnessEvent): boolean;
55
+ handleEvent(event: HarnessEvent): Promise<void>;
56
+ private reflectRun;
57
+ close(): Promise<void>;
58
+ }
59
+ export {};
@@ -0,0 +1,345 @@
1
+ import path from "node:path";
2
+ import { extractMessageText } from "../utils/message-content.js";
3
+ import { createResolvedModel } from "../runtime/adapter/model/model-providers.js";
4
+ import { FileBackedStore } from "../runtime/harness/system/store.js";
5
+ import { compileModel } from "../workspace/resource-compilers.js";
6
+ import { resolveRefId } from "../workspace/support/workspace-ref-utils.js";
7
+ import { renderProceduralMemoryManagerPrompt } from "../runtime/support/runtime-prompts.js";
8
+ const FORMATION_EVENT_TYPES = new Set([
9
+ "request.state.changed",
10
+ "approval.resolved",
11
+ ]);
12
+ function excerpt(message) {
13
+ if (!message?.content) {
14
+ return undefined;
15
+ }
16
+ const normalized = extractMessageText(message.content).replace(/\s+/g, " ").trim();
17
+ if (!normalized) {
18
+ return undefined;
19
+ }
20
+ return normalized.length > 240 ? `${normalized.slice(0, 237)}...` : normalized;
21
+ }
22
+ function summarizeApprovals(approvals) {
23
+ if (approvals.length === 0) {
24
+ return "No approvals were recorded in this run.";
25
+ }
26
+ return approvals
27
+ .map((approval) => `${approval.toolName} (${approval.status})`)
28
+ .join(", ");
29
+ }
30
+ function fingerprintMessages(messages, focus) {
31
+ const serialized = messages
32
+ .map((message) => `${message.role}\n${message.createdAt}\n${extractMessageText(message.content)}`)
33
+ .join("\n---\n");
34
+ return `${focus.join(",")}::${serialized}`;
35
+ }
36
+ function selectRelatedContextRecords(candidate, existingRecords, maxRecords) {
37
+ const candidateText = `${candidate.summary ?? ""}\n${candidate.content}`.toLowerCase().trim();
38
+ if (!candidateText) {
39
+ return existingRecords.slice(0, maxRecords);
40
+ }
41
+ return existingRecords
42
+ .filter((record) => record.status === "active")
43
+ .map((record) => {
44
+ let score = 0;
45
+ const recordText = `${record.summary}\n${record.content}`.toLowerCase();
46
+ if (candidate.scope && record.scope === candidate.scope) {
47
+ score += 2;
48
+ }
49
+ if (recordText.includes(candidateText) || candidateText.includes(recordText)) {
50
+ score += 4;
51
+ }
52
+ const candidateTokens = new Set(candidateText.split(/[^a-z0-9_\u4e00-\u9fff]+/iu).filter((token) => token.length > 1));
53
+ const recordTokens = new Set(recordText.split(/[^a-z0-9_\u4e00-\u9fff]+/iu).filter((token) => token.length > 1));
54
+ let shared = 0;
55
+ for (const token of candidateTokens) {
56
+ if (recordTokens.has(token)) {
57
+ shared += 1;
58
+ }
59
+ }
60
+ score += shared;
61
+ return { record, score };
62
+ })
63
+ .filter((item) => item.score > 0)
64
+ .sort((left, right) => right.score - left.score || right.record.lastConfirmedAt.localeCompare(left.record.lastConfirmedAt))
65
+ .slice(0, maxRecords)
66
+ .map((item) => item.record);
67
+ }
68
+ function extractText(value) {
69
+ if (typeof value === "string") {
70
+ return value;
71
+ }
72
+ if (typeof value !== "object" || value === null) {
73
+ return undefined;
74
+ }
75
+ if ("content" in value && typeof value.content === "string") {
76
+ return String(value.content);
77
+ }
78
+ const kwargs = "kwargs" in value && typeof value.kwargs === "object" && value.kwargs !== null
79
+ ? value.kwargs
80
+ : undefined;
81
+ if (typeof kwargs?.content === "string") {
82
+ return kwargs.content;
83
+ }
84
+ return undefined;
85
+ }
86
+ function tryParseJsonObject(text) {
87
+ try {
88
+ const parsed = JSON.parse(text);
89
+ return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? parsed : null;
90
+ }
91
+ catch {
92
+ const match = text.match(/\{[\s\S]*\}/);
93
+ if (!match) {
94
+ return null;
95
+ }
96
+ try {
97
+ const parsed = JSON.parse(match[0]);
98
+ return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? parsed : null;
99
+ }
100
+ catch {
101
+ return null;
102
+ }
103
+ }
104
+ }
105
+ function asString(value) {
106
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
107
+ }
108
+ function asNumber(value) {
109
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
110
+ }
111
+ function asStringArray(value) {
112
+ if (!Array.isArray(value)) {
113
+ return undefined;
114
+ }
115
+ const items = value
116
+ .filter((item) => typeof item === "string")
117
+ .map((item) => item.trim())
118
+ .filter((item) => item.length > 0);
119
+ return items.length > 0 ? Array.from(new Set(items)) : undefined;
120
+ }
121
+ function normalizeScope(value) {
122
+ return value === "session" || value === "agent" || value === "workspace" || value === "user" || value === "project"
123
+ ? value
124
+ : undefined;
125
+ }
126
+ function normalizeCandidateOutputs(parsed, candidate) {
127
+ const rawOutputs = Array.isArray(parsed.mutations)
128
+ ? parsed.mutations.filter((item) => typeof item === "object" && item !== null && !Array.isArray(item))
129
+ : [parsed];
130
+ return rawOutputs.map((output) => ({
131
+ ...candidate,
132
+ kind: "procedural",
133
+ scope: normalizeScope(output.scope) ?? candidate.scope ?? "workspace",
134
+ summary: asString(output.summary) ?? candidate.summary,
135
+ content: asString(output.content) ?? candidate.content,
136
+ confidence: asNumber(output.confidence) ?? candidate.confidence ?? 0.72,
137
+ tags: asStringArray(output.tags) ?? candidate.tags,
138
+ })).filter((item) => typeof item.content === "string" && item.content.trim().length > 0);
139
+ }
140
+ export function createBackgroundProceduralCandidates(input) {
141
+ const latestUser = excerpt(input.messages.filter((message) => message.role === "user").at(-1));
142
+ const latestAssistant = excerpt(input.messages.filter((message) => message.role === "assistant").at(-1));
143
+ const transcriptPreview = input.messages
144
+ .slice(-6)
145
+ .map((message) => `${message.role}: ${excerpt(message) ?? "(empty)"}`)
146
+ .join("\n");
147
+ const approvals = summarizeApprovals(input.approvals);
148
+ const sourceRef = `runtime://sessions/${input.session.sessionId}/requests/${input.requestId}/procedural-reflection`;
149
+ return [{
150
+ kind: "procedural",
151
+ scope: "workspace",
152
+ sourceType: "runtime-transcript",
153
+ sourceRef,
154
+ summary: latestUser ?? latestAssistant ?? `Procedural reflection for ${input.requestId}`,
155
+ content: [
156
+ "Completed run transcript evidence for procedural memory extraction.",
157
+ `Trigger: ${input.trigger}`,
158
+ `Focus: ${input.focus.join(", ")}`,
159
+ "",
160
+ "Latest user message:",
161
+ latestUser ?? "(none)",
162
+ "",
163
+ "Latest assistant response:",
164
+ latestAssistant ?? "(none)",
165
+ "",
166
+ "Recent transcript excerpt:",
167
+ transcriptPreview || "(none)",
168
+ "",
169
+ "Approval snapshot:",
170
+ approvals,
171
+ ].join("\n"),
172
+ confidence: 0.64,
173
+ observedAt: input.recordedAt,
174
+ tags: ["procedural-background-extraction", input.trigger, ...input.focus],
175
+ }];
176
+ }
177
+ async function runProceduralMemoryManager(input) {
178
+ let resolvedModel;
179
+ try {
180
+ resolvedModel = await createResolvedModel(input.model, input.modelResolver);
181
+ }
182
+ catch {
183
+ return input.candidates;
184
+ }
185
+ const invoker = resolvedModel;
186
+ if (typeof invoker.invoke !== "function") {
187
+ return input.candidates;
188
+ }
189
+ const transformed = [];
190
+ for (const candidate of input.candidates) {
191
+ const prompt = renderProceduralMemoryManagerPrompt({
192
+ candidate,
193
+ sessionId: input.sessionId,
194
+ requestId: input.requestId,
195
+ focus: input.focus,
196
+ existingRecords: selectRelatedContextRecords(candidate, input.existingRecords, input.maxContextRecords ?? input.existingRecords.length),
197
+ });
198
+ let response;
199
+ try {
200
+ response = await invoker.invoke(prompt, {});
201
+ }
202
+ catch {
203
+ continue;
204
+ }
205
+ const parsed = tryParseJsonObject(extractText(response) ?? "");
206
+ if (!parsed || parsed.store === false) {
207
+ continue;
208
+ }
209
+ transformed.push(...normalizeCandidateOutputs(parsed, candidate));
210
+ }
211
+ return transformed;
212
+ }
213
+ export function createProceduralMemoryManager(input) {
214
+ return {
215
+ async transform({ candidates, binding, sessionId, requestId, recordedAt, existingRecords }) {
216
+ if (input.config.enabled !== true || candidates.length === 0) {
217
+ return candidates;
218
+ }
219
+ if (!binding.langchainAgentParams?.model) {
220
+ return candidates;
221
+ }
222
+ const providerModelRef = asString(input.config.provider?.options?.modelRef);
223
+ const primaryModel = (() => {
224
+ if (!providerModelRef) {
225
+ return binding.langchainAgentParams.model;
226
+ }
227
+ const configured = input.workspace.models.get(resolveRefId(providerModelRef));
228
+ return configured ? compileModel(configured) : binding.langchainAgentParams.model;
229
+ })();
230
+ return runProceduralMemoryManager({
231
+ workspace: input.workspace,
232
+ binding,
233
+ model: primaryModel,
234
+ candidates,
235
+ sessionId,
236
+ requestId,
237
+ recordedAt,
238
+ existingRecords: existingRecords.filter((record) => record.status === "active"),
239
+ focus: input.config.formation?.background?.scopeHints ?? ["workflow_patterns", "debugging_lessons", "reusable_procedures"],
240
+ maxContextRecords: 8,
241
+ modelResolver: input.modelResolver,
242
+ });
243
+ },
244
+ };
245
+ }
246
+ export class ProceduralMemoryFormationSync {
247
+ persistence;
248
+ config;
249
+ writer;
250
+ stateStore;
251
+ options;
252
+ pending = new Set();
253
+ syncChain = Promise.resolve();
254
+ name = "procedural-memory-formation-sync";
255
+ constructor(persistence, config, writer, runtimeRoot, stateStore = new FileBackedStore(path.join(runtimeRoot, config.formation?.background?.stateStorePath ?? "knowledge/procedural-memory-state.json")), options = {}) {
256
+ this.persistence = persistence;
257
+ this.config = config;
258
+ this.writer = writer;
259
+ this.stateStore = stateStore;
260
+ this.options = options;
261
+ }
262
+ shouldHandle(event) {
263
+ const background = this.config.formation?.background;
264
+ if (!background?.enabled || !FORMATION_EVENT_TYPES.has(event.eventType)) {
265
+ return false;
266
+ }
267
+ if (event.eventType === "approval.resolved") {
268
+ return background.writeOnApprovalResolution;
269
+ }
270
+ return background.writeOnRequestCompletion && event.payload.state === "completed";
271
+ }
272
+ async handleEvent(event) {
273
+ if (!this.shouldHandle(event)) {
274
+ return;
275
+ }
276
+ const trigger = event.eventType === "approval.resolved" ? "approval.resolved" : "request.completed";
277
+ const task = this.syncChain
278
+ .then(() => this.reflectRun(event.sessionId, event.requestId, trigger, event.timestamp))
279
+ .catch(() => {
280
+ // Fail open: procedural reflection should not block runtime progress.
281
+ });
282
+ this.syncChain = task
283
+ .catch(() => {
284
+ // Fail open: procedural reflection should not block runtime progress.
285
+ })
286
+ .finally(() => {
287
+ this.pending.delete(task);
288
+ });
289
+ this.pending.add(task);
290
+ }
291
+ async reflectRun(sessionId, requestId, trigger, recordedAt) {
292
+ const background = this.config.formation?.background;
293
+ if (!background) {
294
+ return;
295
+ }
296
+ const [session, run, allMessages, approvals] = await Promise.all([
297
+ this.persistence.getSession(sessionId),
298
+ this.persistence.getRequest(requestId),
299
+ this.persistence.listSessionMessages(sessionId, background.maxMessagesPerRequest),
300
+ this.persistence.getRequestApprovals(sessionId, requestId),
301
+ ]);
302
+ if (!session || !run) {
303
+ return;
304
+ }
305
+ const messages = allMessages.filter((message) => message.requestId === requestId);
306
+ if (messages.length === 0) {
307
+ return;
308
+ }
309
+ const fingerprint = fingerprintMessages(messages, background.scopeHints);
310
+ const namespace = ["memories", "formation", "sessions", sessionId, "requests"];
311
+ const cursor = await this.stateStore.get(namespace, requestId);
312
+ const existing = cursor?.value;
313
+ if (existing?.fingerprint === fingerprint && existing.trigger === trigger) {
314
+ return;
315
+ }
316
+ const candidates = createBackgroundProceduralCandidates({
317
+ session,
318
+ requestId,
319
+ agentId: run.agentId ?? session.agentId,
320
+ trigger,
321
+ recordedAt,
322
+ messages,
323
+ approvals,
324
+ focus: background.scopeHints,
325
+ });
326
+ await this.writer({
327
+ candidates,
328
+ sessionId,
329
+ requestId,
330
+ agentId: run.agentId ?? session.agentId,
331
+ userId: this.options.userId,
332
+ projectId: this.options.projectId,
333
+ recordedAt,
334
+ });
335
+ await this.stateStore.put(namespace, requestId, {
336
+ fingerprint,
337
+ candidateCount: candidates.length,
338
+ syncedAt: new Date().toISOString(),
339
+ trigger,
340
+ });
341
+ }
342
+ async close() {
343
+ await Promise.allSettled(Array.from(this.pending));
344
+ }
345
+ }
@@ -0,0 +1,40 @@
1
+ You are deciding whether a completed run should produce reusable procedural memory.
2
+
3
+ Focus areas:
4
+ {{focus}}
5
+
6
+ Session:
7
+ {{sessionId}}
8
+
9
+ Request:
10
+ {{requestId}}
11
+
12
+ Candidate:
13
+ {{candidateJson}}
14
+
15
+ Existing procedural memory:
16
+ {{existingRecords}}
17
+
18
+ Return exactly one JSON object.
19
+
20
+ Rules:
21
+ - Store only reusable experience, tactics, workflows, debugging lessons, or failure-prevention guidance.
22
+ - Ignore pure personal facts, profile facts, location changes, and one-off durable knowledge facts unless they imply a reusable procedure.
23
+ - Prefer concise, imperative procedural guidance that can help a future run act better.
24
+ - Avoid duplicating existing procedural memory.
25
+ - Default kind must be "procedural".
26
+ - Use scope "workspace" for repo/workspace habits, "project" for broader project rules, "agent" for agent-specific operating habits, and "user" only when the reusable procedure is clearly personal to the user.
27
+
28
+ If there is no reusable procedural lesson, return:
29
+ {"store":false}
30
+
31
+ If there is a reusable lesson, return:
32
+ {
33
+ "store": true,
34
+ "summary": "short summary",
35
+ "content": "clear reusable procedure or lesson",
36
+ "kind": "procedural",
37
+ "scope": "workspace",
38
+ "tags": ["workflow_patterns"],
39
+ "confidence": 0.78
40
+ }
@@ -36,6 +36,12 @@ export declare class AgentHarnessRuntime {
36
36
  private readonly knowledgeModule;
37
37
  private readonly runtimeMemoryFormationSync;
38
38
  private readonly unregisterRuntimeMemoryFormationSync;
39
+ private readonly proceduralMemoryConfig;
40
+ private readonly proceduralMemoryStore;
41
+ private readonly proceduralMemoryManager;
42
+ private readonly proceduralMemoryModule;
43
+ private readonly proceduralMemoryFormationSync;
44
+ private readonly unregisterProceduralMemoryFormationSync;
39
45
  private readonly resolvedRuntimeAdapterOptions;
40
46
  private readonly scheduleManager;
41
47
  private readonly healthMonitor;
@@ -35,7 +35,7 @@ import { Mem0IngestionSync, Mem0SemanticRecall, readMem0RuntimeConfig, } from ".
35
35
  import { createRuntimeMemoryManager, RuntimeMemoryFormationSync, readRuntimeMemoryFormationConfig, } from "./harness/system/runtime-memory-manager.js";
36
36
  import { readRuntimeMemoryMaintenanceConfig, readRuntimeMemoryPolicyConfig, resolveMemoryNamespace, } from "./harness/system/runtime-memory-policy.js";
37
37
  import { resolveRuntimeAdapterOptions } from "./support/runtime-adapter-options.js";
38
- import { resolveKnowledgeStorePath } from "./support/runtime-layout.js";
38
+ import { resolveKnowledgeStorePath, resolveProceduralMemoryStorePath } from "./support/runtime-layout.js";
39
39
  import { SystemScheduleManager } from "./scheduling/system-schedule-manager.js";
40
40
  import { initializeHarnessRuntime, reclaimExpiredClaimedRequests as reclaimHarnessExpiredClaimedRequests, recoverStartupRequests as recoverHarnessStartupRequests, isStaleRunningRequest as isHarnessStaleRunningRequest, } from "./harness/run/startup-runtime.js";
41
41
  import { traceStartupStage } from "./startup-tracing.js";
@@ -44,6 +44,7 @@ import { streamHarnessRun } from "./harness/run/stream-run.js";
44
44
  import { defaultRequestedAgentId, prepareRunStart } from "./harness/run/start-run.js";
45
45
  import { buildRequestInspectionRecord, buildSessionInspectionRecord, deleteSessionRecord, getPublicApproval, listPublicApprovals, } from "./harness/run/session-records.js";
46
46
  import { createKnowledgeModule } from "../knowledge/index.js";
47
+ import { createProceduralMemoryManager, ProceduralMemoryFormationSync, readProceduralMemoryRuntimeConfig, } from "../procedural/index.js";
47
48
  const ACTIVE_REQUEST_STATES = [
48
49
  "queued",
49
50
  "claimed",
@@ -132,6 +133,17 @@ function toPublicHarnessStreamItem(item) {
132
133
  return item;
133
134
  }
134
135
  }
136
+ function mergeMemoryItems(...groups) {
137
+ const merged = new Map();
138
+ for (const group of groups) {
139
+ for (const item of group) {
140
+ if (!merged.has(item.id)) {
141
+ merged.set(item.id, item);
142
+ }
143
+ }
144
+ }
145
+ return Array.from(merged.values());
146
+ }
135
147
  function normalizeSessionListText(content, limit) {
136
148
  if (!content) {
137
149
  return undefined;
@@ -216,6 +228,12 @@ export class AgentHarnessRuntime {
216
228
  knowledgeModule;
217
229
  runtimeMemoryFormationSync;
218
230
  unregisterRuntimeMemoryFormationSync;
231
+ proceduralMemoryConfig;
232
+ proceduralMemoryStore;
233
+ proceduralMemoryManager;
234
+ proceduralMemoryModule;
235
+ proceduralMemoryFormationSync;
236
+ unregisterProceduralMemoryFormationSync;
219
237
  resolvedRuntimeAdapterOptions;
220
238
  scheduleManager;
221
239
  healthMonitor;
@@ -461,6 +479,82 @@ export class AgentHarnessRuntime {
461
479
  this.runtimeMemoryFormationSync = null;
462
480
  this.unregisterRuntimeMemoryFormationSync = () => { };
463
481
  }
482
+ const proceduralMemoryConfig = readProceduralMemoryRuntimeConfig(this.defaultRuntimeEntryBinding?.harnessRuntime.proceduralMemory);
483
+ this.proceduralMemoryConfig = proceduralMemoryConfig?.enabled ? proceduralMemoryConfig : null;
484
+ const proceduralStoreConfig = this.proceduralMemoryConfig?.store && Object.keys(this.proceduralMemoryConfig.store).length > 0
485
+ ? this.proceduralMemoryConfig.store
486
+ : {
487
+ kind: "SqliteStore",
488
+ path: resolveProceduralMemoryStorePath(runtimeRoot),
489
+ };
490
+ this.proceduralMemoryStore = this.proceduralMemoryConfig
491
+ ? resolveStoreFromConfig(this.stores, proceduralStoreConfig, runtimeRoot) ?? null
492
+ : null;
493
+ this.proceduralMemoryManager =
494
+ this.defaultRuntimeEntryBinding && this.proceduralMemoryConfig
495
+ ? createProceduralMemoryManager({
496
+ workspace: this.workspace,
497
+ binding: this.defaultRuntimeEntryBinding,
498
+ config: this.proceduralMemoryConfig,
499
+ modelResolver: this.resolvedRuntimeAdapterOptions.modelResolver,
500
+ })
501
+ : null;
502
+ this.proceduralMemoryModule =
503
+ this.proceduralMemoryConfig && this.proceduralMemoryStore
504
+ ? createKnowledgeModule({
505
+ store: this.proceduralMemoryStore,
506
+ policy: null,
507
+ maintenanceConfig: null,
508
+ resolveNamespace: (scope, context) => {
509
+ const binding = this.defaultRuntimeEntryBinding;
510
+ if (!binding) {
511
+ const identifier = scope === "session"
512
+ ? context.sessionId
513
+ : scope === "agent"
514
+ ? context.agentId
515
+ : scope === "workspace"
516
+ ? context.workspaceId
517
+ : scope === "user"
518
+ ? context.userId ?? "default"
519
+ : context.projectId ?? context.workspaceId;
520
+ return ["procedural", `${scope}s`, identifier ?? "default"];
521
+ }
522
+ return this.resolveMemoryNamespace(scope, binding, {
523
+ sessionId: context.sessionId,
524
+ agentId: context.agentId,
525
+ userId: context.userId,
526
+ projectId: context.projectId,
527
+ });
528
+ },
529
+ resolveVectorStore: async () => null,
530
+ transformCandidates: this.proceduralMemoryManager && this.defaultRuntimeEntryBinding
531
+ ? ({ candidates, context, existingRecords }) => this.proceduralMemoryManager.transform({
532
+ candidates,
533
+ binding: this.defaultRuntimeEntryBinding,
534
+ sessionId: context.sessionId ?? `procedural-api-${context.requestId ?? "unknown"}`,
535
+ requestId: context.requestId ?? createPersistentId(new Date(context.recordedAt ?? new Date().toISOString())),
536
+ recordedAt: context.recordedAt ?? new Date().toISOString(),
537
+ existingRecords,
538
+ })
539
+ : undefined,
540
+ })
541
+ : null;
542
+ if (this.proceduralMemoryConfig && this.proceduralMemoryModule) {
543
+ this.proceduralMemoryFormationSync = new ProceduralMemoryFormationSync(this.persistence, this.proceduralMemoryConfig, (input) => this.proceduralMemoryModule.memorizeCandidates(input.candidates, {
544
+ sessionId: input.sessionId,
545
+ requestId: input.requestId,
546
+ agentId: input.agentId,
547
+ workspaceId: this.getWorkspaceId(this.defaultRuntimeEntryBinding),
548
+ userId: input.userId,
549
+ projectId: input.projectId,
550
+ recordedAt: input.recordedAt,
551
+ }, { storeCandidateLog: false }).then(() => undefined), runtimeRoot);
552
+ this.unregisterProceduralMemoryFormationSync = this.eventBus.registerProjection(this.proceduralMemoryFormationSync);
553
+ }
554
+ else {
555
+ this.proceduralMemoryFormationSync = null;
556
+ this.unregisterProceduralMemoryFormationSync = () => { };
557
+ }
464
558
  this.recoveryConfig = getRecoveryConfig(workspace.refs);
465
559
  this.concurrencyConfig = getConcurrencyConfig(workspace.refs);
466
560
  const healthConfig = readHealthMonitorConfig(workspace);
@@ -514,6 +608,9 @@ export class AgentHarnessRuntime {
514
608
  scheduleBackgroundTask: (task) => this.scheduleBackgroundStartupTask(task),
515
609
  });
516
610
  this.scheduleBackgroundStartupTask(traceStartupStage("runtime.initialize.startupRecovery", () => this.recoverStartupRequests()));
611
+ if (this.proceduralMemoryStore) {
612
+ await this.proceduralMemoryStore.listNamespaces();
613
+ }
517
614
  this.initialized = true;
518
615
  }
519
616
  subscribe(listener) {
@@ -604,26 +701,44 @@ export class AgentHarnessRuntime {
604
701
  if (!binding) {
605
702
  throw new Error("recall requires a runtime entry binding.");
606
703
  }
607
- return this.knowledgeModule.recall(input, {
704
+ const context = {
608
705
  sessionId: input.sessionId,
609
706
  agentId: input.agentId ?? binding.agent.id,
610
707
  workspaceId: input.workspaceId ?? this.getWorkspaceId(binding),
611
708
  userId: input.userId,
612
709
  projectId: input.projectId,
613
- });
710
+ };
711
+ const [knowledge, procedural] = await Promise.all([
712
+ this.knowledgeModule.recall(input, context),
713
+ this.proceduralMemoryModule && this.proceduralMemoryConfig?.retrieval?.enabled !== false
714
+ ? this.proceduralMemoryModule.recall(input, context)
715
+ : Promise.resolve({ items: [] }),
716
+ ]);
717
+ return {
718
+ items: mergeMemoryItems(knowledge.items, procedural.items),
719
+ };
614
720
  }
615
721
  async listMemories(input = {}) {
616
722
  const binding = this.defaultRuntimeEntryBinding;
617
723
  if (!binding) {
618
724
  throw new Error("listMemories requires a runtime entry binding.");
619
725
  }
620
- return this.knowledgeModule.list(input, {
726
+ const context = {
621
727
  sessionId: input.sessionId,
622
728
  agentId: input.agentId,
623
729
  workspaceId: input.workspaceId ?? this.getWorkspaceId(binding),
624
730
  userId: input.userId,
625
731
  projectId: input.projectId,
626
- });
732
+ };
733
+ const [knowledge, procedural] = await Promise.all([
734
+ this.knowledgeModule.list(input, context),
735
+ this.proceduralMemoryModule
736
+ ? this.proceduralMemoryModule.list(input, context)
737
+ : Promise.resolve({ items: [] }),
738
+ ]);
739
+ return {
740
+ items: mergeMemoryItems(knowledge.items, procedural.items),
741
+ };
627
742
  }
628
743
  async updateMemory(input) {
629
744
  const binding = this.defaultRuntimeEntryBinding;
@@ -1029,27 +1144,40 @@ export class AgentHarnessRuntime {
1029
1144
  return path.basename(workspaceRoot) || "default";
1030
1145
  }
1031
1146
  async buildRuntimeMemoryContext(binding, sessionId, input) {
1032
- if (!this.runtimeMemoryPolicy) {
1147
+ if (!this.runtimeMemoryPolicy && !this.proceduralMemoryModule) {
1033
1148
  return undefined;
1034
1149
  }
1035
1150
  const query = extractMessageText(input ?? "").trim();
1036
1151
  if (!query) {
1037
1152
  return undefined;
1038
1153
  }
1039
- const promptRecall = await this.knowledgeModule.buildPromptRecall({
1040
- query,
1041
- topK: this.runtimeMemoryPolicy?.retrieval.maxPromptMemories ?? 8,
1042
- }, {
1154
+ const context = {
1043
1155
  sessionId,
1044
1156
  agentId: binding.agent.id,
1045
1157
  workspaceId: this.getWorkspaceId(binding),
1046
- });
1047
- if (!promptRecall.context?.trim()) {
1158
+ };
1159
+ const proceduralTopK = this.proceduralMemoryConfig?.retrieval?.maxPromptItems ?? 4;
1160
+ const [knowledgeRecall, proceduralRecall] = await Promise.all([
1161
+ this.runtimeMemoryPolicy
1162
+ ? this.knowledgeModule.buildPromptRecall({
1163
+ query,
1164
+ topK: this.runtimeMemoryPolicy?.retrieval.maxPromptMemories ?? 8,
1165
+ }, context)
1166
+ : Promise.resolve({ items: [], context: undefined }),
1167
+ this.proceduralMemoryModule && this.proceduralMemoryConfig?.retrieval?.enabled !== false
1168
+ ? this.proceduralMemoryModule.buildPromptRecall({
1169
+ query,
1170
+ topK: proceduralTopK,
1171
+ }, context)
1172
+ : Promise.resolve({ items: [], context: undefined }),
1173
+ ]);
1174
+ const promptParts = [knowledgeRecall.context?.trim(), proceduralRecall.context?.trim()].filter((value) => !!value);
1175
+ if (promptParts.length === 0) {
1048
1176
  return undefined;
1049
1177
  }
1050
1178
  return {
1051
- prompt: promptRecall.context,
1052
- items: promptRecall.items,
1179
+ prompt: promptParts.join("\n\n"),
1180
+ items: mergeMemoryItems(knowledgeRecall.items, proceduralRecall.items),
1053
1181
  };
1054
1182
  }
1055
1183
  async persistRuntimeMemoryCandidates(binding, sessionId, requestId, value) {
@@ -1374,11 +1502,13 @@ export class AgentHarnessRuntime {
1374
1502
  this.unregisterRuntimeMemorySync();
1375
1503
  this.unregisterMem0IngestionSync();
1376
1504
  this.unregisterRuntimeMemoryFormationSync();
1505
+ this.unregisterProceduralMemoryFormationSync();
1377
1506
  await Promise.allSettled(Array.from(this.backgroundTasks));
1378
1507
  await this.sessionMemorySync?.close();
1379
1508
  await this.runtimeMemorySync?.close();
1380
1509
  await this.mem0IngestionSync?.close();
1381
1510
  await this.runtimeMemoryFormationSync?.close();
1511
+ await this.proceduralMemoryFormationSync?.close();
1382
1512
  await closeMcpClientsForWorkspace(this.workspace);
1383
1513
  this.initialized = false;
1384
1514
  }
@@ -28,3 +28,10 @@ export declare function renderRuntimeMemoryMutationReconciliationPrompt(input: {
28
28
  candidate: MemoryCandidate;
29
29
  existingRecords: MemoryRecord[];
30
30
  }): string;
31
+ export declare function renderProceduralMemoryManagerPrompt(input: {
32
+ candidate: MemoryCandidate;
33
+ sessionId: string;
34
+ requestId: string;
35
+ focus: string[];
36
+ existingRecords: MemoryRecord[];
37
+ }): string;
@@ -53,3 +53,17 @@ export function renderRuntimeMemoryMutationReconciliationPrompt(input) {
53
53
  existingRecords: existing,
54
54
  });
55
55
  }
56
+ export function renderProceduralMemoryManagerPrompt(input) {
57
+ const existing = input.existingRecords.length === 0
58
+ ? "(none)"
59
+ : input.existingRecords
60
+ .map((record) => `- scope=${record.scope}; kind=${record.kind}; status=${record.status}; summary=${record.summary}; content=${record.content.replace(/\s+/g, " ").slice(0, 220)}`)
61
+ .join("\n");
62
+ return renderBundledTemplate("prompts/runtime/procedural-memory-manager.md", {
63
+ sessionId: input.sessionId,
64
+ requestId: input.requestId,
65
+ focus: input.focus.join(", "),
66
+ candidateJson: JSON.stringify(input.candidate, null, 2),
67
+ existingRecords: existing,
68
+ });
69
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@botbotgo/agent-harness",
3
- "version": "0.0.305",
3
+ "version": "0.0.307",
4
4
  "description": "Workspace runtime for multi-agent applications",
5
5
  "license": "MIT",
6
6
  "type": "module",