@botbotgo/agent-harness 0.0.306 → 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 +2 -0
- package/README.zh.md +2 -0
- package/dist/package-version.d.ts +1 -1
- package/dist/package-version.js +1 -1
- package/dist/procedural/index.d.ts +1 -0
- package/dist/procedural/index.js +1 -0
- package/dist/procedural/manager.d.ts +59 -0
- package/dist/procedural/manager.js +345 -0
- package/dist/resources/prompts/runtime/procedural-memory-manager.md +40 -0
- package/dist/runtime/harness.d.ts +6 -0
- package/dist/runtime/harness.js +144 -14
- package/dist/runtime/support/runtime-prompts.d.ts +7 -0
- package/dist/runtime/support/runtime-prompts.js +14 -0
- package/package.json +1 -1
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.
|
|
1
|
+
export declare const AGENT_HARNESS_VERSION = "0.0.306";
|
package/dist/package-version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const AGENT_HARNESS_VERSION = "0.0.
|
|
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";
|
package/dist/procedural/index.js
CHANGED
|
@@ -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;
|
package/dist/runtime/harness.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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:
|
|
1052
|
-
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
|
+
}
|