pi-observational-memory 2.3.0 → 2.4.1
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 +30 -22
- package/package.json +1 -1
- package/src/branch.ts +275 -6
- package/src/commands/status.ts +37 -11
- package/src/commands/view.ts +6 -5
- package/src/compaction.ts +330 -44
- package/src/config.ts +21 -2
- package/src/hooks/compaction-hook.ts +27 -22
- package/src/hooks/compaction-trigger.ts +31 -23
- package/src/hooks/observer-trigger.ts +13 -7
- package/src/prompts.ts +37 -13
- package/src/runtime.ts +6 -1
- package/src/serialize.ts +3 -1
- package/src/tools/recall-observation.ts +315 -111
- package/src/types.ts +91 -9
package/src/compaction.ts
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { agentLoop, type AgentContext, type AgentLoopConfig, type AgentTool } from "@mariozechner/pi-agent-core";
|
|
2
2
|
import { Type, type Message, type Model } from "@mariozechner/pi-ai";
|
|
3
3
|
import type { Static } from "@sinclair/typebox";
|
|
4
|
+
import { hashId } from "./ids.js";
|
|
4
5
|
import { observationsToPromptLines } from "./observer.js";
|
|
5
|
-
import { buildPrunerPassGuidance, CONTEXT_USAGE_INSTRUCTIONS, PRUNER_SYSTEM, REFLECTOR_SYSTEM } from "./prompts.js";
|
|
6
|
+
import { buildPrunerPassGuidance, buildReflectorPassGuidance, CONTEXT_USAGE_INSTRUCTIONS, PRUNER_SYSTEM, REFLECTOR_SYSTEM } from "./prompts.js";
|
|
6
7
|
import { truncateRecordContent } from "./serialize.js";
|
|
7
8
|
import { estimateStringTokens } from "./tokens.js";
|
|
8
|
-
import
|
|
9
|
+
import { reflectionContent, reflectionToPromptLine } from "./types.js";
|
|
10
|
+
import type { MemoryReflection, ObservationRecord, ReflectionRecord } from "./types.js";
|
|
9
11
|
|
|
12
|
+
const REFLECTOR_MAX_PASSES = 3;
|
|
10
13
|
const PRUNER_MAX_PASSES = 5;
|
|
11
14
|
const PRUNER_TARGET_RATIO = 0.8;
|
|
12
15
|
|
|
@@ -19,79 +22,341 @@ interface LlmArgs {
|
|
|
19
22
|
apiKey: string;
|
|
20
23
|
headers?: Record<string, string>;
|
|
21
24
|
signal?: AbortSignal;
|
|
25
|
+
agentLoop?: typeof agentLoop;
|
|
22
26
|
}
|
|
23
27
|
|
|
24
|
-
function joinReflectionsOrEmpty(items:
|
|
25
|
-
return items.length ? items.join("\n") : "(none yet)";
|
|
28
|
+
function joinReflectionsOrEmpty(items: MemoryReflection[]): string {
|
|
29
|
+
return items.length ? items.map(reflectionToPromptLine).join("\n") : "(none yet)";
|
|
26
30
|
}
|
|
27
31
|
|
|
28
32
|
function joinObservationsOrEmpty(items: ObservationRecord[]): string {
|
|
29
33
|
return items.length ? observationsToPromptLines(items).join("\n") : "(none yet)";
|
|
30
34
|
}
|
|
31
35
|
|
|
36
|
+
export type ObservationCoverageTag = "uncited" | "cited" | "reinforced";
|
|
37
|
+
|
|
38
|
+
export function deriveObservationCoverageTags(
|
|
39
|
+
reflections: MemoryReflection[],
|
|
40
|
+
observations: ObservationRecord[],
|
|
41
|
+
): Map<string, ObservationCoverageTag> {
|
|
42
|
+
const activeIds = new Set(observations.map((o) => o.id));
|
|
43
|
+
const counts = new Map<string, number>();
|
|
44
|
+
for (const observation of observations) counts.set(observation.id, 0);
|
|
45
|
+
|
|
46
|
+
for (const reflection of reflections) {
|
|
47
|
+
if (typeof reflection === "string" || reflection.legacy === true) continue;
|
|
48
|
+
const citedActiveIds = new Set(reflection.supportingObservationIds.filter((id) => activeIds.has(id)));
|
|
49
|
+
for (const id of citedActiveIds) counts.set(id, (counts.get(id) ?? 0) + 1);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const tags = new Map<string, ObservationCoverageTag>();
|
|
53
|
+
for (const observation of observations) {
|
|
54
|
+
const count = counts.get(observation.id) ?? 0;
|
|
55
|
+
tags.set(observation.id, count === 0 ? "uncited" : count >= 4 ? "reinforced" : "cited");
|
|
56
|
+
}
|
|
57
|
+
return tags;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function renderObservationsForPrunerPrompt(
|
|
61
|
+
observations: ObservationRecord[],
|
|
62
|
+
coverageTags: ReadonlyMap<string, ObservationCoverageTag>,
|
|
63
|
+
): string {
|
|
64
|
+
if (observations.length === 0) return "(none yet)";
|
|
65
|
+
return observations
|
|
66
|
+
.map((observation) => {
|
|
67
|
+
const tag = coverageTags.get(observation.id) ?? "uncited";
|
|
68
|
+
return `[${observation.id}] ${observation.timestamp} [${observation.relevance}] [coverage: ${tag}] ${observation.content}`;
|
|
69
|
+
})
|
|
70
|
+
.join("\n");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function migrateLegacyReflections(reflections: MemoryReflection[]): MemoryReflection[] {
|
|
74
|
+
const migrated: MemoryReflection[] = [];
|
|
75
|
+
const contentToIndex = new Map<string, number>();
|
|
76
|
+
|
|
77
|
+
for (const reflection of reflections) {
|
|
78
|
+
const rawContent = reflectionContent(reflection).trim();
|
|
79
|
+
const normalizedContent = typeof reflection === "string" ? rawContent.replace(/\s+/g, " ") : rawContent;
|
|
80
|
+
if (!normalizedContent) {
|
|
81
|
+
migrated.push(reflection);
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
const content = truncateRecordContent(normalizedContent);
|
|
85
|
+
|
|
86
|
+
const existingIndex = contentToIndex.get(content);
|
|
87
|
+
if (existingIndex !== undefined) {
|
|
88
|
+
const existing = migrated[existingIndex];
|
|
89
|
+
if (typeof existing !== "string" && existing.legacy === true && typeof reflection !== "string" && reflection.legacy !== true) {
|
|
90
|
+
migrated[existingIndex] = reflection;
|
|
91
|
+
}
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (typeof reflection !== "string") {
|
|
96
|
+
migrated.push(reflection);
|
|
97
|
+
contentToIndex.set(content, migrated.length - 1);
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
migrated.push({
|
|
102
|
+
id: hashId(content),
|
|
103
|
+
content,
|
|
104
|
+
supportingObservationIds: [],
|
|
105
|
+
legacy: true,
|
|
106
|
+
});
|
|
107
|
+
contentToIndex.set(content, migrated.length - 1);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return migrated;
|
|
111
|
+
}
|
|
112
|
+
|
|
32
113
|
const RecordReflectionsSchema = Type.Object({
|
|
33
114
|
reflections: Type.Array(
|
|
34
|
-
Type.
|
|
35
|
-
|
|
36
|
-
|
|
115
|
+
Type.Object({
|
|
116
|
+
content: Type.String({
|
|
117
|
+
minLength: 1,
|
|
118
|
+
description: "Single-line plain prose reflection. No markdown, no tags, no timestamp, no bullets.",
|
|
119
|
+
}),
|
|
120
|
+
supportingObservationIds: Type.Array(
|
|
121
|
+
Type.String({
|
|
122
|
+
pattern: "^[a-f0-9]{12}$",
|
|
123
|
+
description: "Exact observation id from the current-observations list that supports this reflection.",
|
|
124
|
+
}),
|
|
125
|
+
{
|
|
126
|
+
minItems: 1,
|
|
127
|
+
description:
|
|
128
|
+
"Smallest exact set of current observation ids that directly support this reflection. " +
|
|
129
|
+
"Use only ids shown in the current observations list; never invent ids.",
|
|
130
|
+
},
|
|
131
|
+
),
|
|
37
132
|
}),
|
|
38
133
|
{
|
|
39
134
|
minItems: 1,
|
|
40
|
-
description: "Batch of new
|
|
135
|
+
description: "Batch of new reflection proposals with their supporting observation ids.",
|
|
41
136
|
},
|
|
42
137
|
),
|
|
43
138
|
});
|
|
44
139
|
|
|
45
140
|
type RecordReflectionsArgs = Static<typeof RecordReflectionsSchema>;
|
|
46
141
|
|
|
47
|
-
export
|
|
142
|
+
export function normalizeSupportingObservationIds(
|
|
143
|
+
supportingObservationIds: readonly string[] | undefined,
|
|
144
|
+
allowedObservationIds: readonly string[],
|
|
145
|
+
): string[] | undefined {
|
|
146
|
+
if (!supportingObservationIds || supportingObservationIds.length === 0) return undefined;
|
|
147
|
+
const allowedOrder = new Map<string, number>();
|
|
148
|
+
for (let i = 0; i < allowedObservationIds.length; i++) {
|
|
149
|
+
if (!allowedOrder.has(allowedObservationIds[i])) allowedOrder.set(allowedObservationIds[i], i);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const seen = new Set<string>();
|
|
153
|
+
for (const id of supportingObservationIds) {
|
|
154
|
+
if (!allowedOrder.has(id)) return undefined;
|
|
155
|
+
seen.add(id);
|
|
156
|
+
}
|
|
157
|
+
if (seen.size === 0) return undefined;
|
|
158
|
+
return Array.from(seen).sort((a, b) => (allowedOrder.get(a) ?? 0) - (allowedOrder.get(b) ?? 0));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export interface ReflectorPassContext {
|
|
162
|
+
pass: number;
|
|
163
|
+
maxPasses: number;
|
|
164
|
+
minSupportingObservationIds: number;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export type ReflectionProposal = {
|
|
168
|
+
content: string;
|
|
169
|
+
supportingObservationIds?: readonly string[];
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
export interface ApplyReflectionProposalsResult {
|
|
173
|
+
reflections: MemoryReflection[];
|
|
174
|
+
accepted: number;
|
|
175
|
+
added: number;
|
|
176
|
+
merged: number;
|
|
177
|
+
promoted: number;
|
|
178
|
+
duplicates: number;
|
|
179
|
+
unsupported: number;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function reflectorPassContext(pass: number): ReflectorPassContext {
|
|
183
|
+
return {
|
|
184
|
+
pass,
|
|
185
|
+
maxPasses: REFLECTOR_MAX_PASSES,
|
|
186
|
+
minSupportingObservationIds: pass === 1 ? 2 : 1,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function reflectionContentKey(reflection: MemoryReflection): string {
|
|
191
|
+
return reflectionContent(reflection).trim();
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function normalizeReflectionProposalContent(content: string): string | undefined {
|
|
195
|
+
const normalized = truncateRecordContent(content.trim());
|
|
196
|
+
if (!normalized || /[\r\n]/.test(normalized)) return undefined;
|
|
197
|
+
return normalized;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function mergeSupportingObservationIds(
|
|
201
|
+
existing: readonly string[],
|
|
202
|
+
incoming: readonly string[],
|
|
203
|
+
allowedObservationIds: readonly string[],
|
|
204
|
+
): string[] | undefined {
|
|
205
|
+
const allowed = new Set(allowedObservationIds);
|
|
206
|
+
const historicalExisting = existing.filter((id) => !allowed.has(id));
|
|
207
|
+
const currentExisting = existing.filter((id) => allowed.has(id));
|
|
208
|
+
const normalizedCurrent = normalizeSupportingObservationIds([...currentExisting, ...incoming], allowedObservationIds);
|
|
209
|
+
if (!normalizedCurrent) return undefined;
|
|
210
|
+
return [...historicalExisting, ...normalizedCurrent];
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export function renderReflectionsForReflectorPrompt(reflections: MemoryReflection[]): string {
|
|
214
|
+
return joinReflectionsOrEmpty(reflections);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export function applyReflectionProposals(
|
|
218
|
+
reflections: MemoryReflection[],
|
|
219
|
+
proposals: readonly ReflectionProposal[],
|
|
220
|
+
allowedObservationIds: readonly string[],
|
|
221
|
+
passContext: Pick<ReflectorPassContext, "minSupportingObservationIds">,
|
|
222
|
+
): ApplyReflectionProposalsResult {
|
|
223
|
+
const next = [...reflections];
|
|
224
|
+
let accepted = 0;
|
|
225
|
+
let added = 0;
|
|
226
|
+
let merged = 0;
|
|
227
|
+
let promoted = 0;
|
|
228
|
+
let duplicates = 0;
|
|
229
|
+
let unsupported = 0;
|
|
230
|
+
|
|
231
|
+
for (const proposal of proposals) {
|
|
232
|
+
const content = normalizeReflectionProposalContent(proposal.content);
|
|
233
|
+
if (!content) {
|
|
234
|
+
unsupported++;
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
const supportingObservationIds = normalizeSupportingObservationIds(
|
|
238
|
+
proposal.supportingObservationIds,
|
|
239
|
+
allowedObservationIds,
|
|
240
|
+
);
|
|
241
|
+
if (!supportingObservationIds || supportingObservationIds.length < passContext.minSupportingObservationIds) {
|
|
242
|
+
unsupported++;
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const existingIndex = next.findIndex((reflection) => reflectionContentKey(reflection) === content);
|
|
247
|
+
if (existingIndex >= 0) {
|
|
248
|
+
const existing = next[existingIndex];
|
|
249
|
+
if (typeof existing === "string") {
|
|
250
|
+
next[existingIndex] = {
|
|
251
|
+
id: hashId(content),
|
|
252
|
+
content,
|
|
253
|
+
supportingObservationIds,
|
|
254
|
+
};
|
|
255
|
+
accepted++;
|
|
256
|
+
promoted++;
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const mergedSupport = mergeSupportingObservationIds(
|
|
261
|
+
existing.supportingObservationIds,
|
|
262
|
+
supportingObservationIds,
|
|
263
|
+
allowedObservationIds,
|
|
264
|
+
);
|
|
265
|
+
if (!mergedSupport) {
|
|
266
|
+
unsupported++;
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
const hasNewSupport = mergedSupport.length !== existing.supportingObservationIds.length;
|
|
270
|
+
if (existing.legacy === true) {
|
|
271
|
+
next[existingIndex] = {
|
|
272
|
+
id: existing.id,
|
|
273
|
+
content: existing.content,
|
|
274
|
+
supportingObservationIds: mergedSupport,
|
|
275
|
+
};
|
|
276
|
+
accepted++;
|
|
277
|
+
promoted++;
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
if (hasNewSupport) {
|
|
281
|
+
next[existingIndex] = {
|
|
282
|
+
...existing,
|
|
283
|
+
supportingObservationIds: mergedSupport,
|
|
284
|
+
};
|
|
285
|
+
accepted++;
|
|
286
|
+
merged++;
|
|
287
|
+
} else {
|
|
288
|
+
duplicates++;
|
|
289
|
+
}
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
next.push({
|
|
294
|
+
id: hashId(content),
|
|
295
|
+
content,
|
|
296
|
+
supportingObservationIds,
|
|
297
|
+
});
|
|
298
|
+
accepted++;
|
|
299
|
+
added++;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return { reflections: next, accepted, added, merged, promoted, duplicates, unsupported };
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
async function runReflectorPass(
|
|
48
306
|
args: LlmArgs,
|
|
49
|
-
reflections:
|
|
307
|
+
reflections: MemoryReflection[],
|
|
50
308
|
observations: ObservationRecord[],
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
const
|
|
309
|
+
passContext: ReflectorPassContext,
|
|
310
|
+
): Promise<{ reflections: MemoryReflection[]; failed: boolean }> {
|
|
311
|
+
const allowedObservationIds = observations.map((o) => o.id);
|
|
312
|
+
let currentReflections = reflections;
|
|
54
313
|
|
|
55
314
|
const recordTool: AgentTool<typeof RecordReflectionsSchema> = {
|
|
56
315
|
name: "record_reflections",
|
|
57
316
|
label: "Record reflections",
|
|
58
317
|
description:
|
|
59
|
-
"Record a batch of
|
|
60
|
-
"May be called multiple times. Stop calling when nothing more is stable enough to crystallize, " +
|
|
318
|
+
"Record a batch of reflections crystallized from the observation pool. " +
|
|
319
|
+
"May be called multiple times. Stop calling when nothing more is stable enough to crystallize for this pass, " +
|
|
61
320
|
"then emit a short plain-text confirmation.",
|
|
62
321
|
parameters: RecordReflectionsSchema,
|
|
63
322
|
execute: async (_id, params: RecordReflectionsArgs) => {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
continue;
|
|
72
|
-
}
|
|
73
|
-
added.add(content);
|
|
74
|
-
accepted++;
|
|
75
|
-
}
|
|
323
|
+
const result = applyReflectionProposals(
|
|
324
|
+
currentReflections,
|
|
325
|
+
params.reflections,
|
|
326
|
+
allowedObservationIds,
|
|
327
|
+
passContext,
|
|
328
|
+
);
|
|
329
|
+
currentReflections = result.reflections;
|
|
76
330
|
const parts: string[] = [];
|
|
77
|
-
parts.push(`
|
|
78
|
-
if (
|
|
79
|
-
parts.push(
|
|
80
|
-
parts.push(
|
|
331
|
+
parts.push(`Accepted ${result.accepted} reflection proposal${result.accepted === 1 ? "" : "s"}.`);
|
|
332
|
+
if (result.added) parts.push(`${result.added} new.`);
|
|
333
|
+
if (result.merged) parts.push(`${result.merged} merged into existing reflections.`);
|
|
334
|
+
if (result.promoted) parts.push(`${result.promoted} promoted from legacy/no-provenance memory.`);
|
|
335
|
+
if (result.duplicates) parts.push(`${result.duplicates} duplicate/no-op proposal${result.duplicates === 1 ? "" : "s"} skipped.`);
|
|
336
|
+
if (result.unsupported) {
|
|
337
|
+
parts.push(
|
|
338
|
+
`${result.unsupported} unsupported proposal${result.unsupported === 1 ? "" : "s"} rejected for invalid supporting observation ids or this pass's minimum support requirement.`,
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
parts.push("Call record_reflections again if more should be crystallized for this pass; otherwise stop and emit a short plain-text confirmation.");
|
|
81
342
|
return {
|
|
82
343
|
content: [{ type: "text", text: parts.join(" ") }],
|
|
83
|
-
details:
|
|
344
|
+
details: result,
|
|
84
345
|
};
|
|
85
346
|
},
|
|
86
347
|
};
|
|
87
348
|
|
|
349
|
+
const passGuidance = buildReflectorPassGuidance(passContext.pass, passContext.maxPasses);
|
|
88
350
|
const userText = `CURRENT REFLECTIONS:
|
|
89
|
-
${
|
|
351
|
+
${renderReflectionsForReflectorPrompt(reflections)}
|
|
90
352
|
|
|
91
353
|
CURRENT OBSERVATIONS:
|
|
92
354
|
${joinObservationsOrEmpty(observations)}
|
|
93
355
|
|
|
94
|
-
|
|
356
|
+
REFLECTOR PASS GUIDANCE:
|
|
357
|
+
${passGuidance}
|
|
358
|
+
|
|
359
|
+
Crystallize long-lived reflections from the full observation pool for this pass. Call record_reflections with batches of reflection proposals, each with the exact supporting observation ids. You may call the tool multiple times as you reason through the pool. To strengthen or promote an existing reflection, repeat the exact existing reflection content with valid supporting observation ids. Do not lightly reword existing reflections. When done, stop calling the tool and emit a short plain-text confirmation.`;
|
|
95
360
|
|
|
96
361
|
const prompts: Message[] = [
|
|
97
362
|
{
|
|
@@ -119,16 +384,33 @@ Crystallize new long-lived reflections from the observation pool. Call record_re
|
|
|
119
384
|
};
|
|
120
385
|
|
|
121
386
|
try {
|
|
122
|
-
const
|
|
387
|
+
const loop = args.agentLoop ?? agentLoop;
|
|
388
|
+
const stream = loop(prompts, context, config, args.signal);
|
|
123
389
|
for await (const _event of stream) {
|
|
124
|
-
// Drain events; the tool's execute already
|
|
390
|
+
// Drain events; the tool's execute already updates reflections.
|
|
125
391
|
}
|
|
126
392
|
await stream.result();
|
|
127
393
|
} catch {
|
|
128
|
-
|
|
394
|
+
return { reflections: currentReflections, failed: true };
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
return { reflections: currentReflections, failed: false };
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
export async function runReflector(
|
|
401
|
+
args: LlmArgs,
|
|
402
|
+
reflections: MemoryReflection[],
|
|
403
|
+
observations: ObservationRecord[],
|
|
404
|
+
): Promise<MemoryReflection[]> {
|
|
405
|
+
let currentReflections = reflections;
|
|
406
|
+
|
|
407
|
+
for (let pass = 1; pass <= REFLECTOR_MAX_PASSES; pass++) {
|
|
408
|
+
const result = await runReflectorPass(args, currentReflections, observations, reflectorPassContext(pass));
|
|
409
|
+
currentReflections = result.reflections;
|
|
410
|
+
if (result.failed) break;
|
|
129
411
|
}
|
|
130
412
|
|
|
131
|
-
return
|
|
413
|
+
return currentReflections;
|
|
132
414
|
}
|
|
133
415
|
|
|
134
416
|
export interface PrunerResult {
|
|
@@ -161,6 +443,7 @@ interface PrunerPassContext {
|
|
|
161
443
|
deltaTokens: number;
|
|
162
444
|
pass: number;
|
|
163
445
|
maxPasses: number;
|
|
446
|
+
coverageTags: ReadonlyMap<string, ObservationCoverageTag>;
|
|
164
447
|
}
|
|
165
448
|
|
|
166
449
|
interface PrunerPassResult {
|
|
@@ -171,7 +454,7 @@ interface PrunerPassResult {
|
|
|
171
454
|
|
|
172
455
|
async function runPrunerPass(
|
|
173
456
|
args: LlmArgs,
|
|
174
|
-
reflections:
|
|
457
|
+
reflections: MemoryReflection[],
|
|
175
458
|
observations: ObservationRecord[],
|
|
176
459
|
passContext: PrunerPassContext,
|
|
177
460
|
): Promise<PrunerPassResult> {
|
|
@@ -226,7 +509,7 @@ async function runPrunerPass(
|
|
|
226
509
|
${joinReflectionsOrEmpty(reflections)}
|
|
227
510
|
|
|
228
511
|
CURRENT OBSERVATIONS:
|
|
229
|
-
${
|
|
512
|
+
${renderObservationsForPrunerPrompt(observations, passContext.coverageTags)}
|
|
230
513
|
|
|
231
514
|
${pressureLine}
|
|
232
515
|
|
|
@@ -260,7 +543,8 @@ Decide which observations to remove from the kept set. Call drop_observations wi
|
|
|
260
543
|
};
|
|
261
544
|
|
|
262
545
|
try {
|
|
263
|
-
const
|
|
546
|
+
const loop = args.agentLoop ?? agentLoop;
|
|
547
|
+
const stream = loop(prompts, context, config, args.signal);
|
|
264
548
|
for await (const _event of stream) {
|
|
265
549
|
// Drain events; the tool's execute already records drops.
|
|
266
550
|
}
|
|
@@ -275,7 +559,7 @@ Decide which observations to remove from the kept set. Call drop_observations wi
|
|
|
275
559
|
|
|
276
560
|
export async function runPruner(
|
|
277
561
|
args: LlmArgs,
|
|
278
|
-
reflections:
|
|
562
|
+
reflections: MemoryReflection[],
|
|
279
563
|
observations: ObservationRecord[],
|
|
280
564
|
budgetTokens: number,
|
|
281
565
|
): Promise<PrunerResult> {
|
|
@@ -285,6 +569,7 @@ export async function runPruner(
|
|
|
285
569
|
|
|
286
570
|
const target = Math.max(1, Math.floor(budgetTokens * PRUNER_TARGET_RATIO));
|
|
287
571
|
let pool = observations;
|
|
572
|
+
const coverageTags = deriveObservationCoverageTags(reflections, observations);
|
|
288
573
|
const allDropped: string[] = [];
|
|
289
574
|
let fellBack = false;
|
|
290
575
|
|
|
@@ -299,6 +584,7 @@ export async function runPruner(
|
|
|
299
584
|
deltaTokens,
|
|
300
585
|
pass,
|
|
301
586
|
maxPasses: PRUNER_MAX_PASSES,
|
|
587
|
+
coverageTags,
|
|
302
588
|
});
|
|
303
589
|
|
|
304
590
|
if (result.fellBack) {
|
|
@@ -314,13 +600,13 @@ export async function runPruner(
|
|
|
314
600
|
return { observations: pool, droppedIds: allDropped, fellBack };
|
|
315
601
|
}
|
|
316
602
|
|
|
317
|
-
export function renderSummary(reflections:
|
|
603
|
+
export function renderSummary(reflections: MemoryReflection[], observations: ObservationRecord[]): string {
|
|
318
604
|
if (reflections.length === 0 && observations.length === 0) return "";
|
|
319
605
|
|
|
320
606
|
const parts: string[] = [CONTEXT_USAGE_INSTRUCTIONS];
|
|
321
607
|
|
|
322
608
|
if (reflections.length > 0) {
|
|
323
|
-
parts.push(`## Reflections\n${reflections.join("\n")}`);
|
|
609
|
+
parts.push(`## Reflections\n${reflections.map(reflectionToPromptLine).join("\n")}`);
|
|
324
610
|
}
|
|
325
611
|
if (observations.length > 0) {
|
|
326
612
|
const body = observationsToPromptLines(observations).join("\n");
|
package/src/config.ts
CHANGED
|
@@ -6,6 +6,7 @@ export interface Config {
|
|
|
6
6
|
observationThresholdTokens: number;
|
|
7
7
|
compactionThresholdTokens: number;
|
|
8
8
|
reflectionThresholdTokens: number;
|
|
9
|
+
passive: boolean;
|
|
9
10
|
compactionModel?: { provider: string; id: string };
|
|
10
11
|
}
|
|
11
12
|
|
|
@@ -13,22 +14,39 @@ export const DEFAULTS: Config = {
|
|
|
13
14
|
observationThresholdTokens: 1_000,
|
|
14
15
|
compactionThresholdTokens: 50_000,
|
|
15
16
|
reflectionThresholdTokens: 30_000,
|
|
17
|
+
passive: false,
|
|
16
18
|
};
|
|
17
19
|
|
|
18
20
|
const SETTINGS_KEY = "observational-memory";
|
|
21
|
+
const PASSIVE_ENV = "PI_OBSERVATIONAL_MEMORY_PASSIVE";
|
|
22
|
+
|
|
23
|
+
function normalizeSettingsConfig(value: Partial<Config>): Partial<Config> {
|
|
24
|
+
const normalized = { ...value };
|
|
25
|
+
if ("passive" in normalized && typeof normalized.passive !== "boolean") delete normalized.passive;
|
|
26
|
+
return normalized;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function readEnvConfig(env: NodeJS.ProcessEnv = process.env): Partial<Config> {
|
|
30
|
+
const rawPassive = env[PASSIVE_ENV];
|
|
31
|
+
if (rawPassive === undefined) return {};
|
|
32
|
+
const passive = rawPassive.trim().toLowerCase();
|
|
33
|
+
if (["1", "true", "yes", "on"].includes(passive)) return { passive: true };
|
|
34
|
+
if (["0", "false", "no", "off"].includes(passive)) return { passive: false };
|
|
35
|
+
return {};
|
|
36
|
+
}
|
|
19
37
|
|
|
20
38
|
function readNamespacedConfig(path: string): Partial<Config> {
|
|
21
39
|
if (!existsSync(path)) return {};
|
|
22
40
|
try {
|
|
23
41
|
const raw = JSON.parse(readFileSync(path, "utf-8")) as Record<string, unknown>;
|
|
24
42
|
const nested = raw[SETTINGS_KEY];
|
|
25
|
-
return nested && typeof nested === "object" ? (nested as Partial<Config>) : {};
|
|
43
|
+
return nested && typeof nested === "object" ? normalizeSettingsConfig(nested as Partial<Config>) : {};
|
|
26
44
|
} catch {
|
|
27
45
|
return {};
|
|
28
46
|
}
|
|
29
47
|
}
|
|
30
48
|
|
|
31
|
-
export function loadConfig(cwd: string): Config {
|
|
49
|
+
export function loadConfig(cwd: string, env: NodeJS.ProcessEnv = process.env): Config {
|
|
32
50
|
const globalPath = join(getAgentDir(), "settings.json");
|
|
33
51
|
const projectPath = join(cwd, ".pi", "settings.json");
|
|
34
52
|
|
|
@@ -36,5 +54,6 @@ export function loadConfig(cwd: string): Config {
|
|
|
36
54
|
...DEFAULTS,
|
|
37
55
|
...readNamespacedConfig(globalPath),
|
|
38
56
|
...readNamespacedConfig(projectPath),
|
|
57
|
+
...readEnvConfig(env),
|
|
39
58
|
};
|
|
40
59
|
}
|