pi-observational-memory 2.4.2 → 3.0.0
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 +316 -79
- package/package.json +10 -9
- package/src/agents/dropper/agent.ts +152 -0
- package/src/agents/dropper/pool.ts +67 -0
- package/src/agents/dropper/prompts.ts +43 -0
- package/src/{observer.ts → agents/observer/agent.ts} +30 -16
- package/src/agents/observer/prompts.ts +119 -0
- package/src/agents/reflector/agent.ts +134 -0
- package/src/agents/reflector/prompts.ts +77 -0
- package/src/clipboard.ts +63 -0
- package/src/commands/status.ts +79 -78
- package/src/commands/view.ts +58 -66
- package/src/config.ts +95 -16
- package/src/debug-log.ts +49 -0
- package/src/hooks/compaction-hook.ts +28 -184
- package/src/hooks/compaction-trigger.ts +35 -21
- package/src/hooks/consolidation-trigger.ts +331 -0
- package/src/index.ts +3 -3
- package/src/model-budget.ts +9 -0
- package/src/runtime.ts +46 -19
- package/src/serialize.ts +1 -1
- package/src/session-ledger/fold.ts +100 -0
- package/src/session-ledger/index.ts +6 -0
- package/src/session-ledger/progress.ts +129 -0
- package/src/session-ledger/projection.ts +220 -0
- package/src/session-ledger/recall.ts +237 -0
- package/src/session-ledger/render-summary.ts +31 -0
- package/src/session-ledger/types.ts +200 -0
- package/src/tokens.ts +1 -1
- package/src/tools/recall-observation.ts +84 -214
- package/src/branch.ts +0 -577
- package/src/compaction.ts +0 -617
- package/src/hooks/observer-trigger.ts +0 -96
- package/src/prompts.ts +0 -301
- package/src/relevance.ts +0 -15
- package/src/types.ts +0 -155
package/src/compaction.ts
DELETED
|
@@ -1,617 +0,0 @@
|
|
|
1
|
-
import { agentLoop, type AgentContext, type AgentLoopConfig, type AgentTool } from "@mariozechner/pi-agent-core";
|
|
2
|
-
import { Type, type Message, type Model } from "@mariozechner/pi-ai";
|
|
3
|
-
import type { Static } from "@sinclair/typebox";
|
|
4
|
-
import { hashId } from "./ids.js";
|
|
5
|
-
import { observationsToPromptLines } from "./observer.js";
|
|
6
|
-
import { buildPrunerPassGuidance, buildReflectorPassGuidance, CONTEXT_USAGE_INSTRUCTIONS, PRUNER_SYSTEM, REFLECTOR_SYSTEM } from "./prompts.js";
|
|
7
|
-
import { truncateRecordContent } from "./serialize.js";
|
|
8
|
-
import { estimateStringTokens } from "./tokens.js";
|
|
9
|
-
import { reflectionContent, reflectionToPromptLine } from "./types.js";
|
|
10
|
-
import type { MemoryReflection, ObservationRecord, ReflectionRecord } from "./types.js";
|
|
11
|
-
|
|
12
|
-
const REFLECTOR_MAX_PASSES = 3;
|
|
13
|
-
const PRUNER_MAX_PASSES = 5;
|
|
14
|
-
const PRUNER_TARGET_RATIO = 0.8;
|
|
15
|
-
|
|
16
|
-
export function observationPoolTokens(observations: ObservationRecord[]): number {
|
|
17
|
-
return estimateStringTokens(observationsToPromptLines(observations).join("\n"));
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
interface LlmArgs {
|
|
21
|
-
model: Model<any>;
|
|
22
|
-
apiKey: string;
|
|
23
|
-
headers?: Record<string, string>;
|
|
24
|
-
signal?: AbortSignal;
|
|
25
|
-
agentLoop?: typeof agentLoop;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function joinReflectionsOrEmpty(items: MemoryReflection[]): string {
|
|
29
|
-
return items.length ? items.map(reflectionToPromptLine).join("\n") : "(none yet)";
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function joinObservationsOrEmpty(items: ObservationRecord[]): string {
|
|
33
|
-
return items.length ? observationsToPromptLines(items).join("\n") : "(none yet)";
|
|
34
|
-
}
|
|
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
|
-
|
|
113
|
-
const RecordReflectionsSchema = Type.Object({
|
|
114
|
-
reflections: Type.Array(
|
|
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
|
-
),
|
|
132
|
-
}),
|
|
133
|
-
{
|
|
134
|
-
minItems: 1,
|
|
135
|
-
description: "Batch of new reflection proposals with their supporting observation ids.",
|
|
136
|
-
},
|
|
137
|
-
),
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
type RecordReflectionsArgs = Static<typeof RecordReflectionsSchema>;
|
|
141
|
-
|
|
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(
|
|
306
|
-
args: LlmArgs,
|
|
307
|
-
reflections: MemoryReflection[],
|
|
308
|
-
observations: ObservationRecord[],
|
|
309
|
-
passContext: ReflectorPassContext,
|
|
310
|
-
): Promise<{ reflections: MemoryReflection[]; failed: boolean }> {
|
|
311
|
-
const allowedObservationIds = observations.map((o) => o.id);
|
|
312
|
-
let currentReflections = reflections;
|
|
313
|
-
|
|
314
|
-
const recordTool: AgentTool<typeof RecordReflectionsSchema> = {
|
|
315
|
-
name: "record_reflections",
|
|
316
|
-
label: "Record reflections",
|
|
317
|
-
description:
|
|
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, " +
|
|
320
|
-
"then emit a short plain-text confirmation.",
|
|
321
|
-
parameters: RecordReflectionsSchema,
|
|
322
|
-
execute: async (_id, params: RecordReflectionsArgs) => {
|
|
323
|
-
const result = applyReflectionProposals(
|
|
324
|
-
currentReflections,
|
|
325
|
-
params.reflections,
|
|
326
|
-
allowedObservationIds,
|
|
327
|
-
passContext,
|
|
328
|
-
);
|
|
329
|
-
currentReflections = result.reflections;
|
|
330
|
-
const parts: string[] = [];
|
|
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.");
|
|
342
|
-
return {
|
|
343
|
-
content: [{ type: "text", text: parts.join(" ") }],
|
|
344
|
-
details: result,
|
|
345
|
-
};
|
|
346
|
-
},
|
|
347
|
-
};
|
|
348
|
-
|
|
349
|
-
const passGuidance = buildReflectorPassGuidance(passContext.pass, passContext.maxPasses);
|
|
350
|
-
const userText = `CURRENT REFLECTIONS:
|
|
351
|
-
${renderReflectionsForReflectorPrompt(reflections)}
|
|
352
|
-
|
|
353
|
-
CURRENT OBSERVATIONS:
|
|
354
|
-
${joinObservationsOrEmpty(observations)}
|
|
355
|
-
|
|
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.`;
|
|
360
|
-
|
|
361
|
-
const prompts: Message[] = [
|
|
362
|
-
{
|
|
363
|
-
role: "user",
|
|
364
|
-
content: [{ type: "text", text: userText }],
|
|
365
|
-
timestamp: Date.now(),
|
|
366
|
-
},
|
|
367
|
-
];
|
|
368
|
-
|
|
369
|
-
const context: AgentContext = {
|
|
370
|
-
systemPrompt: REFLECTOR_SYSTEM,
|
|
371
|
-
messages: [],
|
|
372
|
-
tools: [recordTool as AgentTool<any>],
|
|
373
|
-
};
|
|
374
|
-
|
|
375
|
-
const reasoning = (args.model as { reasoning?: unknown }).reasoning;
|
|
376
|
-
const config: AgentLoopConfig = {
|
|
377
|
-
model: args.model as any,
|
|
378
|
-
apiKey: args.apiKey,
|
|
379
|
-
headers: args.headers,
|
|
380
|
-
maxTokens: 4096,
|
|
381
|
-
convertToLlm: (msgs) => msgs as Message[],
|
|
382
|
-
toolExecution: "sequential",
|
|
383
|
-
...(reasoning ? { reasoning: "high" as const } : {}),
|
|
384
|
-
};
|
|
385
|
-
|
|
386
|
-
try {
|
|
387
|
-
const loop = args.agentLoop ?? agentLoop;
|
|
388
|
-
const stream = loop(prompts, context, config, args.signal);
|
|
389
|
-
for await (const _event of stream) {
|
|
390
|
-
// Drain events; the tool's execute already updates reflections.
|
|
391
|
-
}
|
|
392
|
-
await stream.result();
|
|
393
|
-
} catch {
|
|
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;
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
return currentReflections;
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
export interface PrunerResult {
|
|
417
|
-
observations: ObservationRecord[];
|
|
418
|
-
droppedIds: string[];
|
|
419
|
-
fellBack: boolean;
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
const DropObservationsSchema = Type.Object({
|
|
423
|
-
ids: Type.Array(
|
|
424
|
-
Type.String({
|
|
425
|
-
pattern: "^[a-f0-9]{12}$",
|
|
426
|
-
description: "12-character hex observation id from the current-observations list.",
|
|
427
|
-
}),
|
|
428
|
-
{
|
|
429
|
-
minItems: 1,
|
|
430
|
-
description: "Ids of observations to remove from the kept set.",
|
|
431
|
-
},
|
|
432
|
-
),
|
|
433
|
-
reason: Type.Optional(
|
|
434
|
-
Type.String({ description: "Optional short note explaining why these observations were dropped." }),
|
|
435
|
-
),
|
|
436
|
-
});
|
|
437
|
-
|
|
438
|
-
type DropObservationsArgs = Static<typeof DropObservationsSchema>;
|
|
439
|
-
|
|
440
|
-
interface PrunerPassContext {
|
|
441
|
-
poolTokens: number;
|
|
442
|
-
targetTokens: number;
|
|
443
|
-
deltaTokens: number;
|
|
444
|
-
pass: number;
|
|
445
|
-
maxPasses: number;
|
|
446
|
-
coverageTags: ReadonlyMap<string, ObservationCoverageTag>;
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
interface PrunerPassResult {
|
|
450
|
-
kept: ObservationRecord[];
|
|
451
|
-
droppedIds: string[];
|
|
452
|
-
fellBack: boolean;
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
async function runPrunerPass(
|
|
456
|
-
args: LlmArgs,
|
|
457
|
-
reflections: MemoryReflection[],
|
|
458
|
-
observations: ObservationRecord[],
|
|
459
|
-
passContext: PrunerPassContext,
|
|
460
|
-
): Promise<PrunerPassResult> {
|
|
461
|
-
const idSet = new Set(observations.map((o) => o.id));
|
|
462
|
-
const dropped = new Set<string>();
|
|
463
|
-
|
|
464
|
-
const dropTool: AgentTool<typeof DropObservationsSchema> = {
|
|
465
|
-
name: "drop_observations",
|
|
466
|
-
label: "Drop observations",
|
|
467
|
-
description:
|
|
468
|
-
"Remove one or more observations from the kept set by id. May be called multiple times. " +
|
|
469
|
-
"Stop calling when no further drops are warranted, then emit a short plain-text confirmation.",
|
|
470
|
-
parameters: DropObservationsSchema,
|
|
471
|
-
execute: async (_id, params: DropObservationsArgs) => {
|
|
472
|
-
const valid: string[] = [];
|
|
473
|
-
const unknown: string[] = [];
|
|
474
|
-
const already: string[] = [];
|
|
475
|
-
for (const id of params.ids) {
|
|
476
|
-
if (!idSet.has(id)) {
|
|
477
|
-
unknown.push(id);
|
|
478
|
-
continue;
|
|
479
|
-
}
|
|
480
|
-
if (dropped.has(id)) {
|
|
481
|
-
already.push(id);
|
|
482
|
-
continue;
|
|
483
|
-
}
|
|
484
|
-
dropped.add(id);
|
|
485
|
-
valid.push(id);
|
|
486
|
-
}
|
|
487
|
-
const remaining = idSet.size - dropped.size;
|
|
488
|
-
const parts: string[] = [];
|
|
489
|
-
parts.push(`Dropped ${valid.length} observation${valid.length === 1 ? "" : "s"}.`);
|
|
490
|
-
if (unknown.length) parts.push(`Unknown ids ignored: ${unknown.join(", ")}.`);
|
|
491
|
-
if (already.length) parts.push(`Already dropped: ${already.join(", ")}.`);
|
|
492
|
-
parts.push(`Remaining kept: ${remaining} of ${idSet.size}.`);
|
|
493
|
-
parts.push("Call drop_observations again if more should be removed; otherwise stop and emit a short plain-text confirmation.");
|
|
494
|
-
return {
|
|
495
|
-
content: [{ type: "text", text: parts.join(" ") }],
|
|
496
|
-
details: { dropped: valid, unknown, already, remaining },
|
|
497
|
-
};
|
|
498
|
-
},
|
|
499
|
-
};
|
|
500
|
-
|
|
501
|
-
const pressureLine =
|
|
502
|
-
passContext.deltaTokens > 0
|
|
503
|
-
? `Pool ~${passContext.poolTokens.toLocaleString()} tokens, target ~${passContext.targetTokens.toLocaleString()} tokens, still need to cut at least ~${passContext.deltaTokens.toLocaleString()} tokens.`
|
|
504
|
-
: `Pool ~${passContext.poolTokens.toLocaleString()} tokens, target ~${passContext.targetTokens.toLocaleString()} tokens (already under budget) — drop only clear redundancies.`;
|
|
505
|
-
|
|
506
|
-
const passGuidance = buildPrunerPassGuidance(passContext.pass, passContext.maxPasses);
|
|
507
|
-
|
|
508
|
-
const userText = `CURRENT REFLECTIONS:
|
|
509
|
-
${joinReflectionsOrEmpty(reflections)}
|
|
510
|
-
|
|
511
|
-
CURRENT OBSERVATIONS:
|
|
512
|
-
${renderObservationsForPrunerPrompt(observations, passContext.coverageTags)}
|
|
513
|
-
|
|
514
|
-
${pressureLine}
|
|
515
|
-
|
|
516
|
-
${passGuidance}
|
|
517
|
-
|
|
518
|
-
Decide which observations to remove from the kept set. Call drop_observations with the ids you want to drop. You may call the tool multiple times as you reason through the pool. When satisfied, stop calling the tool and emit a short plain-text confirmation to end the run.`;
|
|
519
|
-
|
|
520
|
-
const prompts: Message[] = [
|
|
521
|
-
{
|
|
522
|
-
role: "user",
|
|
523
|
-
content: [{ type: "text", text: userText }],
|
|
524
|
-
timestamp: Date.now(),
|
|
525
|
-
},
|
|
526
|
-
];
|
|
527
|
-
|
|
528
|
-
const context: AgentContext = {
|
|
529
|
-
systemPrompt: PRUNER_SYSTEM,
|
|
530
|
-
messages: [],
|
|
531
|
-
tools: [dropTool as AgentTool<any>],
|
|
532
|
-
};
|
|
533
|
-
|
|
534
|
-
const reasoning = (args.model as { reasoning?: unknown }).reasoning;
|
|
535
|
-
const config: AgentLoopConfig = {
|
|
536
|
-
model: args.model as any,
|
|
537
|
-
apiKey: args.apiKey,
|
|
538
|
-
headers: args.headers,
|
|
539
|
-
maxTokens: 2048,
|
|
540
|
-
convertToLlm: (msgs) => msgs as Message[],
|
|
541
|
-
toolExecution: "sequential",
|
|
542
|
-
...(reasoning ? { reasoning: "high" as const } : {}),
|
|
543
|
-
};
|
|
544
|
-
|
|
545
|
-
try {
|
|
546
|
-
const loop = args.agentLoop ?? agentLoop;
|
|
547
|
-
const stream = loop(prompts, context, config, args.signal);
|
|
548
|
-
for await (const _event of stream) {
|
|
549
|
-
// Drain events; the tool's execute already records drops.
|
|
550
|
-
}
|
|
551
|
-
await stream.result();
|
|
552
|
-
} catch {
|
|
553
|
-
return { kept: observations, droppedIds: [], fellBack: true };
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
const kept = observations.filter((o) => !dropped.has(o.id));
|
|
557
|
-
return { kept, droppedIds: Array.from(dropped), fellBack: false };
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
export async function runPruner(
|
|
561
|
-
args: LlmArgs,
|
|
562
|
-
reflections: MemoryReflection[],
|
|
563
|
-
observations: ObservationRecord[],
|
|
564
|
-
budgetTokens: number,
|
|
565
|
-
): Promise<PrunerResult> {
|
|
566
|
-
if (observations.length === 0) {
|
|
567
|
-
return { observations: [], droppedIds: [], fellBack: false };
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
const target = Math.max(1, Math.floor(budgetTokens * PRUNER_TARGET_RATIO));
|
|
571
|
-
let pool = observations;
|
|
572
|
-
const coverageTags = deriveObservationCoverageTags(reflections, observations);
|
|
573
|
-
const allDropped: string[] = [];
|
|
574
|
-
let fellBack = false;
|
|
575
|
-
|
|
576
|
-
for (let pass = 1; pass <= PRUNER_MAX_PASSES; pass++) {
|
|
577
|
-
const poolTokens = observationPoolTokens(pool);
|
|
578
|
-
if (poolTokens <= target) break;
|
|
579
|
-
|
|
580
|
-
const deltaTokens = poolTokens - target;
|
|
581
|
-
const result = await runPrunerPass(args, reflections, pool, {
|
|
582
|
-
poolTokens,
|
|
583
|
-
targetTokens: target,
|
|
584
|
-
deltaTokens,
|
|
585
|
-
pass,
|
|
586
|
-
maxPasses: PRUNER_MAX_PASSES,
|
|
587
|
-
coverageTags,
|
|
588
|
-
});
|
|
589
|
-
|
|
590
|
-
if (result.fellBack) {
|
|
591
|
-
fellBack = true;
|
|
592
|
-
break;
|
|
593
|
-
}
|
|
594
|
-
if (result.droppedIds.length === 0) break;
|
|
595
|
-
|
|
596
|
-
pool = result.kept;
|
|
597
|
-
allDropped.push(...result.droppedIds);
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
return { observations: pool, droppedIds: allDropped, fellBack };
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
export function renderSummary(reflections: MemoryReflection[], observations: ObservationRecord[]): string {
|
|
604
|
-
if (reflections.length === 0 && observations.length === 0) return "";
|
|
605
|
-
|
|
606
|
-
const parts: string[] = [CONTEXT_USAGE_INSTRUCTIONS];
|
|
607
|
-
|
|
608
|
-
if (reflections.length > 0) {
|
|
609
|
-
parts.push(`## Reflections\n${reflections.map(reflectionToPromptLine).join("\n")}`);
|
|
610
|
-
}
|
|
611
|
-
if (observations.length > 0) {
|
|
612
|
-
const body = observationsToPromptLines(observations).join("\n");
|
|
613
|
-
parts.push(`## Observations\n${body}`);
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
return parts.join("\n\n");
|
|
617
|
-
}
|
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
2
|
-
import {
|
|
3
|
-
firstRawIdAfter,
|
|
4
|
-
getMemoryState,
|
|
5
|
-
lastObservationCoverEndIdx,
|
|
6
|
-
rawTailEntriesBetween,
|
|
7
|
-
rawTokensSinceLastBound,
|
|
8
|
-
} from "../branch.js";
|
|
9
|
-
import { observationsToPromptLines, runObserver } from "../observer.js";
|
|
10
|
-
import type { Runtime } from "../runtime.js";
|
|
11
|
-
import { serializeSourceAddressedBranchEntries } from "../serialize.js";
|
|
12
|
-
import { estimateStringTokens } from "../tokens.js";
|
|
13
|
-
import { OBSERVATION_CUSTOM_TYPE, reflectionToPromptLine, type ObservationEntryData } from "../types.js";
|
|
14
|
-
|
|
15
|
-
export function registerObserverTrigger(pi: ExtensionAPI, runtime: Runtime): void {
|
|
16
|
-
pi.on("turn_end", (_event, ctx) => {
|
|
17
|
-
runtime.ensureConfig(ctx.cwd);
|
|
18
|
-
if (runtime.config.passive === true) return;
|
|
19
|
-
if (runtime.observerInFlight) return;
|
|
20
|
-
|
|
21
|
-
const entries = ctx.sessionManager.getBranch() as Parameters<typeof rawTokensSinceLastBound>[0];
|
|
22
|
-
const tokens = rawTokensSinceLastBound(entries);
|
|
23
|
-
if (tokens < runtime.config.observationThresholdTokens) return;
|
|
24
|
-
|
|
25
|
-
const lastBoundIdx = lastObservationCoverEndIdx(entries);
|
|
26
|
-
const coversFromId = firstRawIdAfter(entries, lastBoundIdx);
|
|
27
|
-
if (!coversFromId) return;
|
|
28
|
-
|
|
29
|
-
const leafId = ctx.sessionManager.getLeafId();
|
|
30
|
-
if (!leafId) return;
|
|
31
|
-
const coversUpToId = leafId;
|
|
32
|
-
|
|
33
|
-
const { reflections, committedObs, pendingObs } = getMemoryState(entries);
|
|
34
|
-
const priorObservationLines = observationsToPromptLines([...committedObs, ...pendingObs]);
|
|
35
|
-
|
|
36
|
-
const chunkEntries = rawTailEntriesBetween(entries, coversFromId, coversUpToId);
|
|
37
|
-
if (chunkEntries.length === 0) return;
|
|
38
|
-
const { text: chunk, sourceEntryIds } = serializeSourceAddressedBranchEntries(chunkEntries);
|
|
39
|
-
if (!chunk.trim() || sourceEntryIds.length === 0) return;
|
|
40
|
-
|
|
41
|
-
if (ctx.hasUI) ctx.ui.notify(
|
|
42
|
-
`Observational memory: observer running on ~${tokens.toLocaleString()}-token chunk`,
|
|
43
|
-
"info",
|
|
44
|
-
);
|
|
45
|
-
|
|
46
|
-
// Capture ctx properties synchronously — the async work below may outlive
|
|
47
|
-
// the extension ctx (stale after session replacement/reload).
|
|
48
|
-
const hasUI = ctx.hasUI;
|
|
49
|
-
const ui = ctx.ui;
|
|
50
|
-
|
|
51
|
-
void runtime.launchObserverTask(ctx, "observer", async () => {
|
|
52
|
-
const resolved = await runtime.resolveModel(ctx as any);
|
|
53
|
-
if (!resolved.ok) {
|
|
54
|
-
if (!runtime.resolveFailureNotified && hasUI && ui) {
|
|
55
|
-
ui.notify(
|
|
56
|
-
`Observational memory: observer skipped — ${resolved.reason}`,
|
|
57
|
-
"warning",
|
|
58
|
-
);
|
|
59
|
-
runtime.resolveFailureNotified = true;
|
|
60
|
-
}
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
|
-
runtime.resolveFailureNotified = false;
|
|
64
|
-
|
|
65
|
-
const records = await runObserver({
|
|
66
|
-
model: resolved.model as any,
|
|
67
|
-
apiKey: resolved.apiKey,
|
|
68
|
-
headers: resolved.headers,
|
|
69
|
-
priorReflections: reflections.map(reflectionToPromptLine),
|
|
70
|
-
priorObservations: priorObservationLines,
|
|
71
|
-
chunk,
|
|
72
|
-
allowedSourceEntryIds: sourceEntryIds,
|
|
73
|
-
});
|
|
74
|
-
if (!records || records.length === 0) {
|
|
75
|
-
if (hasUI && ui) ui.notify(
|
|
76
|
-
"Observational memory: observer returned no observations",
|
|
77
|
-
"warning",
|
|
78
|
-
);
|
|
79
|
-
return;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
const observationTokens = records.reduce((sum, r) => sum + estimateStringTokens(r.content), 0);
|
|
83
|
-
const data: ObservationEntryData = {
|
|
84
|
-
records,
|
|
85
|
-
coversFromId,
|
|
86
|
-
coversUpToId,
|
|
87
|
-
tokenCount: observationTokens,
|
|
88
|
-
};
|
|
89
|
-
pi.appendEntry(OBSERVATION_CUSTOM_TYPE, data);
|
|
90
|
-
if (hasUI && ui) ui.notify(
|
|
91
|
-
`Observational memory: ${records.length} observation${records.length === 1 ? "" : "s"} recorded (~${observationTokens.toLocaleString()} tokens)`,
|
|
92
|
-
"info",
|
|
93
|
-
);
|
|
94
|
-
});
|
|
95
|
-
});
|
|
96
|
-
}
|