pi-observational-memory 2.4.3 → 3.0.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 +318 -103
- package/package.json +9 -9
- package/src/agents/dropper/agent.ts +281 -0
- package/src/agents/dropper/coverage.ts +128 -0
- package/src/agents/dropper/pool.ts +67 -0
- package/src/agents/dropper/prompts.ts +48 -0
- package/src/{observer.ts → agents/observer/agent.ts} +11 -13
- package/src/agents/observer/prompts.ts +119 -0
- package/src/agents/reflector/agent.ts +203 -0
- package/src/agents/reflector/prompts.ts +81 -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 +78 -55
- package/src/debug-log.ts +24 -5
- package/src/hooks/compaction-hook.ts +24 -369
- package/src/hooks/compaction-trigger.ts +12 -21
- package/src/hooks/consolidation-trigger.ts +368 -0
- package/src/index.ts +3 -3
- package/src/model-budget.ts +1 -1
- 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 -1030
- package/src/hooks/observer-trigger.ts +0 -129
- package/src/progress.ts +0 -155
- package/src/prompts.ts +0 -302
- package/src/relevance.ts +0 -15
- package/src/types.ts +0 -155
package/src/compaction.ts
DELETED
|
@@ -1,1030 +0,0 @@
|
|
|
1
|
-
import { agentLoop, type AgentContext, type AgentEvent, type AgentLoopConfig, type AgentTool } from "@mariozechner/pi-agent-core";
|
|
2
|
-
import { Type, type Message, type Model, type ModelThinkingLevel } from "@mariozechner/pi-ai";
|
|
3
|
-
import type { Static } from "typebox";
|
|
4
|
-
import { debugLog, isDebugLogEnabled } from "./debug-log.js";
|
|
5
|
-
import { hashId } from "./ids.js";
|
|
6
|
-
import { AGENT_LOOP_MAX_TOKENS, boundedMaxTokens } from "./model-budget.js";
|
|
7
|
-
import { observationsToPromptLines } from "./observer.js";
|
|
8
|
-
import { buildPrunerPassGuidance, buildReflectorPassGuidance, CONTEXT_USAGE_INSTRUCTIONS, PRUNER_SYSTEM, REFLECTOR_SYSTEM } from "./prompts.js";
|
|
9
|
-
import { truncateRecordContent } from "./serialize.js";
|
|
10
|
-
import { estimateStringTokens } from "./tokens.js";
|
|
11
|
-
import { reflectionContent, reflectionToPromptLine } from "./types.js";
|
|
12
|
-
import type { MemoryReflection, ObservationRecord, ReflectionRecord } from "./types.js";
|
|
13
|
-
|
|
14
|
-
export const REFLECTOR_MAX_PASSES = 2;
|
|
15
|
-
export const PRUNER_MAX_PASSES = 2;
|
|
16
|
-
const PRUNER_TARGET_RATIO = 0.8;
|
|
17
|
-
|
|
18
|
-
export function observationPoolTokens(observations: ObservationRecord[]): number {
|
|
19
|
-
return estimateStringTokens(observationsToPromptLines(observations).join("\n"));
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
interface LlmArgs {
|
|
23
|
-
model: Model<any>;
|
|
24
|
-
apiKey: string;
|
|
25
|
-
headers?: Record<string, string>;
|
|
26
|
-
signal?: AbortSignal;
|
|
27
|
-
agentLoop?: typeof agentLoop;
|
|
28
|
-
onEvent?: (event: import("@mariozechner/pi-agent-core").AgentEvent) => void;
|
|
29
|
-
maxTurns?: number;
|
|
30
|
-
thinkingLevel?: ModelThinkingLevel;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function joinReflectionsOrEmpty(items: MemoryReflection[]): string {
|
|
34
|
-
return items.length ? items.map(reflectionToPromptLine).join("\n") : "(none yet)";
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function joinObservationsOrEmpty(items: ObservationRecord[]): string {
|
|
38
|
-
return items.length ? observationsToPromptLines(items).join("\n") : "(none yet)";
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function summarizeContentTypes(content: unknown): string | string[] {
|
|
42
|
-
if (!Array.isArray(content)) return typeof content;
|
|
43
|
-
return content.map((block) => {
|
|
44
|
-
if (block && typeof block === "object" && "type" in block) {
|
|
45
|
-
const type = (block as { type?: unknown }).type;
|
|
46
|
-
return typeof type === "string" ? type : typeof type;
|
|
47
|
-
}
|
|
48
|
-
return typeof block;
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function summarizeAgentMessage(message: unknown): Record<string, unknown> {
|
|
53
|
-
if (!message || typeof message !== "object") return { type: typeof message };
|
|
54
|
-
const record = message as Record<string, unknown>;
|
|
55
|
-
const summary: Record<string, unknown> = {
|
|
56
|
-
role: typeof record.role === "string" ? record.role : "unknown",
|
|
57
|
-
};
|
|
58
|
-
if ("api" in record && typeof record.api === "string") summary.api = record.api;
|
|
59
|
-
if ("provider" in record && typeof record.provider === "string") summary.provider = record.provider;
|
|
60
|
-
if ("model" in record && typeof record.model === "string") summary.model = record.model;
|
|
61
|
-
if ("stopReason" in record && typeof record.stopReason === "string") summary.stopReason = record.stopReason;
|
|
62
|
-
if ("errorMessage" in record && typeof record.errorMessage === "string") summary.errorMessage = record.errorMessage;
|
|
63
|
-
if ("toolName" in record && typeof record.toolName === "string") summary.toolName = record.toolName;
|
|
64
|
-
if ("isError" in record && typeof record.isError === "boolean") summary.isError = record.isError;
|
|
65
|
-
if ("content" in record) summary.contentTypes = summarizeContentTypes(record.content);
|
|
66
|
-
return summary;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function finalAssistantSummary(messages: unknown): Record<string, unknown> | undefined {
|
|
70
|
-
if (!Array.isArray(messages)) return undefined;
|
|
71
|
-
for (let i = messages.length - 1; i >= 0; i--) {
|
|
72
|
-
const message = messages[i];
|
|
73
|
-
if (message && typeof message === "object" && (message as { role?: unknown }).role === "assistant") {
|
|
74
|
-
return summarizeAgentMessage(message);
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
return undefined;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function summarizeToolResults(toolResults: unknown): Record<string, unknown>[] {
|
|
81
|
-
if (!Array.isArray(toolResults)) return [];
|
|
82
|
-
return toolResults.map(summarizeAgentMessage);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function summarizeObjectKeys(value: unknown): string[] | undefined {
|
|
86
|
-
return value && typeof value === "object" ? Object.keys(value as Record<string, unknown>).sort() : undefined;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function logAgentLoopEvent(scope: "reflector" | "pruner", pass: number, event: AgentEvent): void {
|
|
90
|
-
switch (event.type) {
|
|
91
|
-
case "agent_start":
|
|
92
|
-
case "turn_start":
|
|
93
|
-
debugLog(`${scope}.agent_loop.${event.type}`, { pass });
|
|
94
|
-
return;
|
|
95
|
-
case "message_start":
|
|
96
|
-
case "message_end":
|
|
97
|
-
debugLog(`${scope}.agent_loop.${event.type}`, { pass, message: summarizeAgentMessage(event.message) });
|
|
98
|
-
return;
|
|
99
|
-
case "message_update":
|
|
100
|
-
debugLog(`${scope}.agent_loop.message_update`, {
|
|
101
|
-
pass,
|
|
102
|
-
message: summarizeAgentMessage(event.message),
|
|
103
|
-
assistantEventType: event.assistantMessageEvent.type,
|
|
104
|
-
});
|
|
105
|
-
return;
|
|
106
|
-
case "turn_end":
|
|
107
|
-
debugLog(`${scope}.agent_loop.turn_end`, {
|
|
108
|
-
pass,
|
|
109
|
-
message: summarizeAgentMessage(event.message),
|
|
110
|
-
toolResultCount: event.toolResults.length,
|
|
111
|
-
toolResults: summarizeToolResults(event.toolResults),
|
|
112
|
-
});
|
|
113
|
-
return;
|
|
114
|
-
case "agent_end":
|
|
115
|
-
debugLog(`${scope}.agent_loop.agent_end`, {
|
|
116
|
-
pass,
|
|
117
|
-
messageCount: event.messages.length,
|
|
118
|
-
finalAssistant: finalAssistantSummary(event.messages),
|
|
119
|
-
});
|
|
120
|
-
return;
|
|
121
|
-
case "tool_execution_start":
|
|
122
|
-
debugLog(`${scope}.agent_loop.tool_execution_start`, {
|
|
123
|
-
pass,
|
|
124
|
-
toolCallId: event.toolCallId,
|
|
125
|
-
toolName: event.toolName,
|
|
126
|
-
argsKeys: summarizeObjectKeys(event.args),
|
|
127
|
-
});
|
|
128
|
-
return;
|
|
129
|
-
case "tool_execution_update":
|
|
130
|
-
debugLog(`${scope}.agent_loop.tool_execution_update`, {
|
|
131
|
-
pass,
|
|
132
|
-
toolCallId: event.toolCallId,
|
|
133
|
-
toolName: event.toolName,
|
|
134
|
-
argsKeys: summarizeObjectKeys(event.args),
|
|
135
|
-
partialResultKeys: summarizeObjectKeys(event.partialResult),
|
|
136
|
-
});
|
|
137
|
-
return;
|
|
138
|
-
case "tool_execution_end":
|
|
139
|
-
debugLog(`${scope}.agent_loop.tool_execution_end`, {
|
|
140
|
-
pass,
|
|
141
|
-
toolCallId: event.toolCallId,
|
|
142
|
-
toolName: event.toolName,
|
|
143
|
-
isError: event.isError,
|
|
144
|
-
resultKeys: summarizeObjectKeys(event.result),
|
|
145
|
-
});
|
|
146
|
-
return;
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
export type ObservationCoverageTag = "uncited" | "cited" | "reinforced";
|
|
151
|
-
|
|
152
|
-
export interface CoverageTagCounts {
|
|
153
|
-
uncited: number;
|
|
154
|
-
cited: number;
|
|
155
|
-
reinforced: number;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
export function deriveObservationCoverageTags(
|
|
159
|
-
reflections: MemoryReflection[],
|
|
160
|
-
observations: ObservationRecord[],
|
|
161
|
-
): Map<string, ObservationCoverageTag> {
|
|
162
|
-
const activeIds = new Set(observations.map((o) => o.id));
|
|
163
|
-
const counts = new Map<string, number>();
|
|
164
|
-
for (const observation of observations) counts.set(observation.id, 0);
|
|
165
|
-
|
|
166
|
-
for (const reflection of reflections) {
|
|
167
|
-
if (typeof reflection === "string" || reflection.legacy === true) continue;
|
|
168
|
-
const citedActiveIds = new Set(reflection.supportingObservationIds.filter((id) => activeIds.has(id)));
|
|
169
|
-
for (const id of citedActiveIds) counts.set(id, (counts.get(id) ?? 0) + 1);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
const tags = new Map<string, ObservationCoverageTag>();
|
|
173
|
-
for (const observation of observations) {
|
|
174
|
-
const count = counts.get(observation.id) ?? 0;
|
|
175
|
-
tags.set(observation.id, count === 0 ? "uncited" : count >= 4 ? "reinforced" : "cited");
|
|
176
|
-
}
|
|
177
|
-
return tags;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
export function coverageTagCounts(
|
|
181
|
-
reflections: MemoryReflection[],
|
|
182
|
-
observations: ObservationRecord[],
|
|
183
|
-
): CoverageTagCounts {
|
|
184
|
-
const tags = deriveObservationCoverageTags(reflections, observations);
|
|
185
|
-
const counts: CoverageTagCounts = { uncited: 0, cited: 0, reinforced: 0 };
|
|
186
|
-
for (const observation of observations) {
|
|
187
|
-
counts[tags.get(observation.id) ?? "uncited"]++;
|
|
188
|
-
}
|
|
189
|
-
return counts;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
export function renderObservationsForPrunerPrompt(
|
|
193
|
-
observations: ObservationRecord[],
|
|
194
|
-
coverageTags: ReadonlyMap<string, ObservationCoverageTag>,
|
|
195
|
-
): string {
|
|
196
|
-
if (observations.length === 0) return "(none yet)";
|
|
197
|
-
return observations
|
|
198
|
-
.map((observation) => {
|
|
199
|
-
const tag = coverageTags.get(observation.id) ?? "uncited";
|
|
200
|
-
return `[${observation.id}] ${observation.timestamp} [${observation.relevance}] [coverage: ${tag}] ${observation.content}`;
|
|
201
|
-
})
|
|
202
|
-
.join("\n");
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
export function migrateLegacyReflections(reflections: MemoryReflection[]): MemoryReflection[] {
|
|
206
|
-
const migrated: MemoryReflection[] = [];
|
|
207
|
-
const contentToIndex = new Map<string, number>();
|
|
208
|
-
|
|
209
|
-
for (const reflection of reflections) {
|
|
210
|
-
const rawContent = reflectionContent(reflection).trim();
|
|
211
|
-
const normalizedContent = typeof reflection === "string" ? rawContent.replace(/\s+/g, " ") : rawContent;
|
|
212
|
-
if (!normalizedContent) {
|
|
213
|
-
migrated.push(reflection);
|
|
214
|
-
continue;
|
|
215
|
-
}
|
|
216
|
-
const content = truncateRecordContent(normalizedContent);
|
|
217
|
-
|
|
218
|
-
const existingIndex = contentToIndex.get(content);
|
|
219
|
-
if (existingIndex !== undefined) {
|
|
220
|
-
const existing = migrated[existingIndex];
|
|
221
|
-
if (typeof existing !== "string" && existing.legacy === true && typeof reflection !== "string" && reflection.legacy !== true) {
|
|
222
|
-
migrated[existingIndex] = reflection;
|
|
223
|
-
}
|
|
224
|
-
continue;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
if (typeof reflection !== "string") {
|
|
228
|
-
migrated.push(reflection);
|
|
229
|
-
contentToIndex.set(content, migrated.length - 1);
|
|
230
|
-
continue;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
migrated.push({
|
|
234
|
-
id: hashId(content),
|
|
235
|
-
content,
|
|
236
|
-
supportingObservationIds: [],
|
|
237
|
-
legacy: true,
|
|
238
|
-
});
|
|
239
|
-
contentToIndex.set(content, migrated.length - 1);
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
return migrated;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
const RecordReflectionsSchema = Type.Object({
|
|
246
|
-
reflections: Type.Array(
|
|
247
|
-
Type.Object({
|
|
248
|
-
content: Type.String({
|
|
249
|
-
minLength: 1,
|
|
250
|
-
description: "Single-line plain prose reflection. No markdown, no tags, no timestamp, no bullets.",
|
|
251
|
-
}),
|
|
252
|
-
supportingObservationIds: Type.Array(
|
|
253
|
-
Type.String({
|
|
254
|
-
pattern: "^[a-f0-9]{12}$",
|
|
255
|
-
description: "Exact observation id from the current-observations list whose durable meaning is captured by this reflection.",
|
|
256
|
-
}),
|
|
257
|
-
{
|
|
258
|
-
minItems: 1,
|
|
259
|
-
description:
|
|
260
|
-
"Current observation ids whose durable meaning is captured by this reflection and can be treated as covered active-memory detail. " +
|
|
261
|
-
"Do not include observations whose unique exact detail or current task state is not captured. Use only ids shown in the current observations list; never invent ids.",
|
|
262
|
-
},
|
|
263
|
-
),
|
|
264
|
-
}),
|
|
265
|
-
{
|
|
266
|
-
minItems: 1,
|
|
267
|
-
description: "Batch of new reflection proposals with their supporting observation ids.",
|
|
268
|
-
},
|
|
269
|
-
),
|
|
270
|
-
});
|
|
271
|
-
|
|
272
|
-
type RecordReflectionsArgs = Static<typeof RecordReflectionsSchema>;
|
|
273
|
-
|
|
274
|
-
export function normalizeSupportingObservationIds(
|
|
275
|
-
supportingObservationIds: readonly string[] | undefined,
|
|
276
|
-
allowedObservationIds: readonly string[],
|
|
277
|
-
): string[] | undefined {
|
|
278
|
-
if (!supportingObservationIds || supportingObservationIds.length === 0) return undefined;
|
|
279
|
-
const allowedOrder = new Map<string, number>();
|
|
280
|
-
for (let i = 0; i < allowedObservationIds.length; i++) {
|
|
281
|
-
if (!allowedOrder.has(allowedObservationIds[i])) allowedOrder.set(allowedObservationIds[i], i);
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
const seen = new Set<string>();
|
|
285
|
-
for (const id of supportingObservationIds) {
|
|
286
|
-
if (!allowedOrder.has(id)) return undefined;
|
|
287
|
-
seen.add(id);
|
|
288
|
-
}
|
|
289
|
-
if (seen.size === 0) return undefined;
|
|
290
|
-
return Array.from(seen).sort((a, b) => (allowedOrder.get(a) ?? 0) - (allowedOrder.get(b) ?? 0));
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
export interface ReflectorPassContext {
|
|
294
|
-
pass: number;
|
|
295
|
-
maxPasses: number;
|
|
296
|
-
minSupportingObservationIds: number;
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
export type ReflectionProposal = {
|
|
300
|
-
content: string;
|
|
301
|
-
supportingObservationIds?: readonly string[];
|
|
302
|
-
};
|
|
303
|
-
|
|
304
|
-
export interface ApplyReflectionProposalsResult {
|
|
305
|
-
reflections: MemoryReflection[];
|
|
306
|
-
accepted: number;
|
|
307
|
-
added: number;
|
|
308
|
-
merged: number;
|
|
309
|
-
promoted: number;
|
|
310
|
-
duplicates: number;
|
|
311
|
-
unsupported: number;
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
export interface ReflectorPassStats {
|
|
315
|
-
pass: number;
|
|
316
|
-
toolCalls: number;
|
|
317
|
-
accepted: number;
|
|
318
|
-
added: number;
|
|
319
|
-
merged: number;
|
|
320
|
-
promoted: number;
|
|
321
|
-
duplicates: number;
|
|
322
|
-
unsupported: number;
|
|
323
|
-
failed: boolean;
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
export interface ReflectorStats {
|
|
327
|
-
passes: ReflectorPassStats[];
|
|
328
|
-
toolCalls: number;
|
|
329
|
-
accepted: number;
|
|
330
|
-
added: number;
|
|
331
|
-
merged: number;
|
|
332
|
-
promoted: number;
|
|
333
|
-
duplicates: number;
|
|
334
|
-
unsupported: number;
|
|
335
|
-
failedPass?: number;
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
export interface ReflectorResult {
|
|
339
|
-
reflections: MemoryReflection[];
|
|
340
|
-
stats: ReflectorStats;
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
function emptyReflectorPassStats(pass: number): ReflectorPassStats {
|
|
344
|
-
return {
|
|
345
|
-
pass,
|
|
346
|
-
toolCalls: 0,
|
|
347
|
-
accepted: 0,
|
|
348
|
-
added: 0,
|
|
349
|
-
merged: 0,
|
|
350
|
-
promoted: 0,
|
|
351
|
-
duplicates: 0,
|
|
352
|
-
unsupported: 0,
|
|
353
|
-
failed: false,
|
|
354
|
-
};
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
function addReflectionProposalStats(target: ReflectorPassStats, result: ApplyReflectionProposalsResult): void {
|
|
358
|
-
target.toolCalls++;
|
|
359
|
-
target.accepted += result.accepted;
|
|
360
|
-
target.added += result.added;
|
|
361
|
-
target.merged += result.merged;
|
|
362
|
-
target.promoted += result.promoted;
|
|
363
|
-
target.duplicates += result.duplicates;
|
|
364
|
-
target.unsupported += result.unsupported;
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
function aggregateReflectorStats(passes: ReflectorPassStats[]): ReflectorStats {
|
|
368
|
-
const stats: ReflectorStats = {
|
|
369
|
-
passes,
|
|
370
|
-
toolCalls: 0,
|
|
371
|
-
accepted: 0,
|
|
372
|
-
added: 0,
|
|
373
|
-
merged: 0,
|
|
374
|
-
promoted: 0,
|
|
375
|
-
duplicates: 0,
|
|
376
|
-
unsupported: 0,
|
|
377
|
-
};
|
|
378
|
-
for (const pass of passes) {
|
|
379
|
-
stats.toolCalls += pass.toolCalls;
|
|
380
|
-
stats.accepted += pass.accepted;
|
|
381
|
-
stats.added += pass.added;
|
|
382
|
-
stats.merged += pass.merged;
|
|
383
|
-
stats.promoted += pass.promoted;
|
|
384
|
-
stats.duplicates += pass.duplicates;
|
|
385
|
-
stats.unsupported += pass.unsupported;
|
|
386
|
-
if (pass.failed && stats.failedPass === undefined) stats.failedPass = pass.pass;
|
|
387
|
-
}
|
|
388
|
-
return stats;
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
function reflectorPassContext(pass: number): ReflectorPassContext {
|
|
392
|
-
return {
|
|
393
|
-
pass,
|
|
394
|
-
maxPasses: REFLECTOR_MAX_PASSES,
|
|
395
|
-
minSupportingObservationIds: pass === 1 ? 2 : 1,
|
|
396
|
-
};
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
function reflectionContentKey(reflection: MemoryReflection): string {
|
|
400
|
-
return reflectionContent(reflection).trim();
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
function normalizeReflectionProposalContent(content: string): string | undefined {
|
|
404
|
-
const normalized = truncateRecordContent(content.trim());
|
|
405
|
-
if (!normalized || /[\r\n]/.test(normalized)) return undefined;
|
|
406
|
-
return normalized;
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
function mergeSupportingObservationIds(
|
|
410
|
-
existing: readonly string[],
|
|
411
|
-
incoming: readonly string[],
|
|
412
|
-
allowedObservationIds: readonly string[],
|
|
413
|
-
): string[] | undefined {
|
|
414
|
-
const allowed = new Set(allowedObservationIds);
|
|
415
|
-
const historicalExisting = existing.filter((id) => !allowed.has(id));
|
|
416
|
-
const currentExisting = existing.filter((id) => allowed.has(id));
|
|
417
|
-
const normalizedCurrent = normalizeSupportingObservationIds([...currentExisting, ...incoming], allowedObservationIds);
|
|
418
|
-
if (!normalizedCurrent) return undefined;
|
|
419
|
-
return [...historicalExisting, ...normalizedCurrent];
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
export function renderReflectionsForReflectorPrompt(reflections: MemoryReflection[]): string {
|
|
423
|
-
return joinReflectionsOrEmpty(reflections);
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
export function applyReflectionProposals(
|
|
427
|
-
reflections: MemoryReflection[],
|
|
428
|
-
proposals: readonly ReflectionProposal[],
|
|
429
|
-
allowedObservationIds: readonly string[],
|
|
430
|
-
passContext: Pick<ReflectorPassContext, "minSupportingObservationIds">,
|
|
431
|
-
): ApplyReflectionProposalsResult {
|
|
432
|
-
const next = [...reflections];
|
|
433
|
-
let accepted = 0;
|
|
434
|
-
let added = 0;
|
|
435
|
-
let merged = 0;
|
|
436
|
-
let promoted = 0;
|
|
437
|
-
let duplicates = 0;
|
|
438
|
-
let unsupported = 0;
|
|
439
|
-
|
|
440
|
-
for (const proposal of proposals) {
|
|
441
|
-
const content = normalizeReflectionProposalContent(proposal.content);
|
|
442
|
-
if (!content) {
|
|
443
|
-
unsupported++;
|
|
444
|
-
continue;
|
|
445
|
-
}
|
|
446
|
-
const supportingObservationIds = normalizeSupportingObservationIds(
|
|
447
|
-
proposal.supportingObservationIds,
|
|
448
|
-
allowedObservationIds,
|
|
449
|
-
);
|
|
450
|
-
if (!supportingObservationIds || supportingObservationIds.length < passContext.minSupportingObservationIds) {
|
|
451
|
-
unsupported++;
|
|
452
|
-
continue;
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
const existingIndex = next.findIndex((reflection) => reflectionContentKey(reflection) === content);
|
|
456
|
-
if (existingIndex >= 0) {
|
|
457
|
-
const existing = next[existingIndex];
|
|
458
|
-
if (typeof existing === "string") {
|
|
459
|
-
next[existingIndex] = {
|
|
460
|
-
id: hashId(content),
|
|
461
|
-
content,
|
|
462
|
-
supportingObservationIds,
|
|
463
|
-
};
|
|
464
|
-
accepted++;
|
|
465
|
-
promoted++;
|
|
466
|
-
continue;
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
const mergedSupport = mergeSupportingObservationIds(
|
|
470
|
-
existing.supportingObservationIds,
|
|
471
|
-
supportingObservationIds,
|
|
472
|
-
allowedObservationIds,
|
|
473
|
-
);
|
|
474
|
-
if (!mergedSupport) {
|
|
475
|
-
unsupported++;
|
|
476
|
-
continue;
|
|
477
|
-
}
|
|
478
|
-
const hasNewSupport = mergedSupport.length !== existing.supportingObservationIds.length;
|
|
479
|
-
if (existing.legacy === true) {
|
|
480
|
-
next[existingIndex] = {
|
|
481
|
-
id: existing.id,
|
|
482
|
-
content: existing.content,
|
|
483
|
-
supportingObservationIds: mergedSupport,
|
|
484
|
-
};
|
|
485
|
-
accepted++;
|
|
486
|
-
promoted++;
|
|
487
|
-
continue;
|
|
488
|
-
}
|
|
489
|
-
if (hasNewSupport) {
|
|
490
|
-
next[existingIndex] = {
|
|
491
|
-
...existing,
|
|
492
|
-
supportingObservationIds: mergedSupport,
|
|
493
|
-
};
|
|
494
|
-
accepted++;
|
|
495
|
-
merged++;
|
|
496
|
-
} else {
|
|
497
|
-
duplicates++;
|
|
498
|
-
}
|
|
499
|
-
continue;
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
next.push({
|
|
503
|
-
id: hashId(content),
|
|
504
|
-
content,
|
|
505
|
-
supportingObservationIds,
|
|
506
|
-
});
|
|
507
|
-
accepted++;
|
|
508
|
-
added++;
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
return { reflections: next, accepted, added, merged, promoted, duplicates, unsupported };
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
async function runReflectorPass(
|
|
515
|
-
args: LlmArgs,
|
|
516
|
-
reflections: MemoryReflection[],
|
|
517
|
-
observations: ObservationRecord[],
|
|
518
|
-
passContext: ReflectorPassContext,
|
|
519
|
-
): Promise<{ reflections: MemoryReflection[]; stats: ReflectorPassStats }> {
|
|
520
|
-
const allowedObservationIds = observations.map((o) => o.id);
|
|
521
|
-
let currentReflections = reflections;
|
|
522
|
-
const stats = emptyReflectorPassStats(passContext.pass);
|
|
523
|
-
let consecutiveEmptyCalls = 0;
|
|
524
|
-
debugLog("reflector.pass.start", {
|
|
525
|
-
pass: passContext.pass,
|
|
526
|
-
maxPasses: passContext.maxPasses,
|
|
527
|
-
minSupportingObservationIds: passContext.minSupportingObservationIds,
|
|
528
|
-
reflectionCount: reflections.length,
|
|
529
|
-
observationCount: observations.length,
|
|
530
|
-
observationIds: isDebugLogEnabled() ? allowedObservationIds : undefined,
|
|
531
|
-
});
|
|
532
|
-
|
|
533
|
-
const recordTool: AgentTool<typeof RecordReflectionsSchema> = {
|
|
534
|
-
name: "record_reflections",
|
|
535
|
-
label: "Record reflections",
|
|
536
|
-
description:
|
|
537
|
-
"Record a batch of reflections crystallized from the observation pool, with supporting ids for observations whose durable meaning is captured. " +
|
|
538
|
-
"May be called multiple times. Stop calling when nothing more is stable enough to crystallize or strengthen for this pass, " +
|
|
539
|
-
"then emit a short plain-text confirmation.",
|
|
540
|
-
parameters: RecordReflectionsSchema,
|
|
541
|
-
execute: async (_id, params: RecordReflectionsArgs) => {
|
|
542
|
-
const result = applyReflectionProposals(
|
|
543
|
-
currentReflections,
|
|
544
|
-
params.reflections,
|
|
545
|
-
allowedObservationIds,
|
|
546
|
-
passContext,
|
|
547
|
-
);
|
|
548
|
-
currentReflections = result.reflections;
|
|
549
|
-
addReflectionProposalStats(stats, result);
|
|
550
|
-
if (result.accepted === 0) {
|
|
551
|
-
consecutiveEmptyCalls++;
|
|
552
|
-
} else {
|
|
553
|
-
consecutiveEmptyCalls = 0;
|
|
554
|
-
}
|
|
555
|
-
const parts: string[] = [];
|
|
556
|
-
parts.push(`Accepted ${result.accepted} reflection proposal${result.accepted === 1 ? "" : "s"}.`);
|
|
557
|
-
if (result.added) parts.push(`${result.added} new.`);
|
|
558
|
-
if (result.merged) parts.push(`${result.merged} merged into existing reflections.`);
|
|
559
|
-
if (result.promoted) parts.push(`${result.promoted} promoted from legacy/no-provenance memory.`);
|
|
560
|
-
if (result.duplicates) parts.push(`${result.duplicates} duplicate/no-op proposal${result.duplicates === 1 ? "" : "s"} skipped.`);
|
|
561
|
-
if (result.unsupported) {
|
|
562
|
-
parts.push(
|
|
563
|
-
`${result.unsupported} unsupported proposal${result.unsupported === 1 ? "" : "s"} rejected for invalid supporting observation ids or this pass's minimum support requirement.`,
|
|
564
|
-
);
|
|
565
|
-
}
|
|
566
|
-
parts.push("Call record_reflections again if more should be crystallized for this pass; otherwise stop and emit a short plain-text confirmation.");
|
|
567
|
-
debugLog("reflector.tool_call", {
|
|
568
|
-
pass: passContext.pass,
|
|
569
|
-
accepted: result.accepted,
|
|
570
|
-
added: result.added,
|
|
571
|
-
merged: result.merged,
|
|
572
|
-
promoted: result.promoted,
|
|
573
|
-
duplicates: result.duplicates,
|
|
574
|
-
unsupported: result.unsupported,
|
|
575
|
-
currentReflectionCount: currentReflections.length,
|
|
576
|
-
proposals: params.reflections.map((reflection: ReflectionProposal) => ({
|
|
577
|
-
content: reflection.content,
|
|
578
|
-
supportingObservationIds: reflection.supportingObservationIds,
|
|
579
|
-
})),
|
|
580
|
-
});
|
|
581
|
-
return {
|
|
582
|
-
content: [{ type: "text", text: parts.join(" ") }],
|
|
583
|
-
details: result,
|
|
584
|
-
};
|
|
585
|
-
},
|
|
586
|
-
};
|
|
587
|
-
|
|
588
|
-
const passGuidance = buildReflectorPassGuidance(passContext.pass, passContext.maxPasses);
|
|
589
|
-
const userText = `CURRENT REFLECTIONS:
|
|
590
|
-
${renderReflectionsForReflectorPrompt(reflections)}
|
|
591
|
-
|
|
592
|
-
CURRENT OBSERVATIONS:
|
|
593
|
-
${joinObservationsOrEmpty(observations)}
|
|
594
|
-
|
|
595
|
-
REFLECTOR PASS GUIDANCE:
|
|
596
|
-
${passGuidance}
|
|
597
|
-
|
|
598
|
-
Crystallize long-lived reflections from the full observation pool for this pass. Call record_reflections with batches of reflection proposals, each with supporting observation ids whose durable meaning is captured by that reflection. 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 additional valid supporting observation ids. Do not lightly reword existing reflections. Do not attach observations whose unique exact detail or current task state is not captured with equivalent fidelity. When done, stop calling the tool and emit a short plain-text confirmation.`;
|
|
599
|
-
|
|
600
|
-
const prompts: Message[] = [
|
|
601
|
-
{
|
|
602
|
-
role: "user",
|
|
603
|
-
content: [{ type: "text", text: userText }],
|
|
604
|
-
timestamp: Date.now(),
|
|
605
|
-
},
|
|
606
|
-
];
|
|
607
|
-
|
|
608
|
-
const context: AgentContext = {
|
|
609
|
-
systemPrompt: REFLECTOR_SYSTEM,
|
|
610
|
-
messages: [],
|
|
611
|
-
tools: [recordTool as AgentTool<any>],
|
|
612
|
-
};
|
|
613
|
-
|
|
614
|
-
const reasoning = (args.model as { reasoning?: unknown }).reasoning;
|
|
615
|
-
const thinkingLevel = args.thinkingLevel ?? "low";
|
|
616
|
-
const effectiveMaxTurns = args.maxTurns && args.maxTurns > 0 ? args.maxTurns : undefined;
|
|
617
|
-
let turnCount = 0;
|
|
618
|
-
|
|
619
|
-
const config: AgentLoopConfig = {
|
|
620
|
-
model: args.model as any,
|
|
621
|
-
apiKey: args.apiKey,
|
|
622
|
-
headers: args.headers,
|
|
623
|
-
maxTokens: boundedMaxTokens(args.model, AGENT_LOOP_MAX_TOKENS),
|
|
624
|
-
convertToLlm: (msgs) => msgs as Message[],
|
|
625
|
-
toolExecution: "sequential",
|
|
626
|
-
...(reasoning && thinkingLevel !== "off" ? { reasoning: thinkingLevel } : {}),
|
|
627
|
-
shouldStopAfterTurn: () => {
|
|
628
|
-
turnCount++;
|
|
629
|
-
if (effectiveMaxTurns !== undefined && turnCount >= effectiveMaxTurns) return true;
|
|
630
|
-
if (consecutiveEmptyCalls >= 2) return true;
|
|
631
|
-
return false;
|
|
632
|
-
},
|
|
633
|
-
};
|
|
634
|
-
|
|
635
|
-
let firstEventSeen = false;
|
|
636
|
-
try {
|
|
637
|
-
debugLog("reflector.agent_loop.before_call", { pass: passContext.pass });
|
|
638
|
-
const loop = args.agentLoop ?? agentLoop;
|
|
639
|
-
const stream = loop(prompts, context, config, args.signal);
|
|
640
|
-
debugLog("reflector.agent_loop.stream_created", { pass: passContext.pass });
|
|
641
|
-
for await (const event of stream) {
|
|
642
|
-
if (!firstEventSeen) {
|
|
643
|
-
firstEventSeen = true;
|
|
644
|
-
debugLog("reflector.agent_loop.first_event", { pass: passContext.pass, type: event.type });
|
|
645
|
-
}
|
|
646
|
-
logAgentLoopEvent("reflector", passContext.pass, event);
|
|
647
|
-
args.onEvent?.(event);
|
|
648
|
-
}
|
|
649
|
-
await stream.result();
|
|
650
|
-
debugLog("reflector.pass.result", { pass: passContext.pass, stats, reflectionCount: currentReflections.length });
|
|
651
|
-
} catch (error) {
|
|
652
|
-
stats.failed = true;
|
|
653
|
-
debugLog("reflector.agent_loop.error", {
|
|
654
|
-
pass: passContext.pass,
|
|
655
|
-
firstEventSeen,
|
|
656
|
-
errorMessage: error instanceof Error ? error.message : String(error),
|
|
657
|
-
});
|
|
658
|
-
return { reflections: currentReflections, stats };
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
return { reflections: currentReflections, stats };
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
export async function runReflector(
|
|
665
|
-
args: LlmArgs,
|
|
666
|
-
reflections: MemoryReflection[],
|
|
667
|
-
observations: ObservationRecord[],
|
|
668
|
-
onPassStart?: (pass: number, maxPasses: number) => void,
|
|
669
|
-
): Promise<ReflectorResult> {
|
|
670
|
-
debugLog("reflector.start", {
|
|
671
|
-
reflectionCount: reflections.length,
|
|
672
|
-
observationCount: observations.length,
|
|
673
|
-
observations: isDebugLogEnabled()
|
|
674
|
-
? observations.map((observation) => ({
|
|
675
|
-
id: observation.id,
|
|
676
|
-
timestamp: observation.timestamp,
|
|
677
|
-
relevance: observation.relevance,
|
|
678
|
-
content: observation.content,
|
|
679
|
-
sourceEntryIds: observation.sourceEntryIds,
|
|
680
|
-
}))
|
|
681
|
-
: undefined,
|
|
682
|
-
});
|
|
683
|
-
let currentReflections = reflections;
|
|
684
|
-
const passes: ReflectorPassStats[] = [];
|
|
685
|
-
|
|
686
|
-
for (let pass = 1; pass <= REFLECTOR_MAX_PASSES; pass++) {
|
|
687
|
-
onPassStart?.(pass, REFLECTOR_MAX_PASSES);
|
|
688
|
-
const result = await runReflectorPass(args, currentReflections, observations, reflectorPassContext(pass));
|
|
689
|
-
currentReflections = result.reflections;
|
|
690
|
-
passes.push(result.stats);
|
|
691
|
-
if (result.stats.failed) break;
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
const result = { reflections: currentReflections, stats: aggregateReflectorStats(passes) };
|
|
695
|
-
debugLog("reflector.result", {
|
|
696
|
-
stats: result.stats,
|
|
697
|
-
reflectionCount: result.reflections.length,
|
|
698
|
-
reflections: isDebugLogEnabled()
|
|
699
|
-
? result.reflections.map((reflection) => typeof reflection === "string" ? { legacyString: true, content: reflection } : reflection)
|
|
700
|
-
: undefined,
|
|
701
|
-
});
|
|
702
|
-
return result;
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
export type PrunerStopReason = "empty" | "under_target" | "fell_back" | "zero_drops" | "max_passes";
|
|
706
|
-
|
|
707
|
-
export interface PrunerPassStats {
|
|
708
|
-
pass: number;
|
|
709
|
-
poolTokens: number;
|
|
710
|
-
targetTokens: number;
|
|
711
|
-
deltaTokens: number;
|
|
712
|
-
dropped: number;
|
|
713
|
-
remaining: number;
|
|
714
|
-
fellBack: boolean;
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
export interface PrunerResult {
|
|
718
|
-
observations: ObservationRecord[];
|
|
719
|
-
droppedIds: string[];
|
|
720
|
-
fellBack: boolean;
|
|
721
|
-
passes: PrunerPassStats[];
|
|
722
|
-
stopReason: PrunerStopReason;
|
|
723
|
-
}
|
|
724
|
-
|
|
725
|
-
const DropObservationsSchema = Type.Object({
|
|
726
|
-
ids: Type.Array(
|
|
727
|
-
Type.String({
|
|
728
|
-
pattern: "^[a-f0-9]{12}$",
|
|
729
|
-
description: "12-character hex observation id from the current-observations list.",
|
|
730
|
-
}),
|
|
731
|
-
{
|
|
732
|
-
minItems: 1,
|
|
733
|
-
description: "Ids of observations to remove from the kept set.",
|
|
734
|
-
},
|
|
735
|
-
),
|
|
736
|
-
reason: Type.Optional(
|
|
737
|
-
Type.String({ description: "Optional short note explaining why these observations were dropped." }),
|
|
738
|
-
),
|
|
739
|
-
});
|
|
740
|
-
|
|
741
|
-
type DropObservationsArgs = Static<typeof DropObservationsSchema>;
|
|
742
|
-
|
|
743
|
-
interface PrunerPassContext {
|
|
744
|
-
poolTokens: number;
|
|
745
|
-
targetTokens: number;
|
|
746
|
-
deltaTokens: number;
|
|
747
|
-
pass: number;
|
|
748
|
-
maxPasses: number;
|
|
749
|
-
coverageTags: ReadonlyMap<string, ObservationCoverageTag>;
|
|
750
|
-
}
|
|
751
|
-
|
|
752
|
-
interface PrunerPassResult {
|
|
753
|
-
kept: ObservationRecord[];
|
|
754
|
-
droppedIds: string[];
|
|
755
|
-
fellBack: boolean;
|
|
756
|
-
}
|
|
757
|
-
|
|
758
|
-
async function runPrunerPass(
|
|
759
|
-
args: LlmArgs,
|
|
760
|
-
reflections: MemoryReflection[],
|
|
761
|
-
observations: ObservationRecord[],
|
|
762
|
-
passContext: PrunerPassContext,
|
|
763
|
-
): Promise<PrunerPassResult> {
|
|
764
|
-
const idSet = new Set(observations.map((o) => o.id));
|
|
765
|
-
const dropped = new Set<string>();
|
|
766
|
-
let consecutiveEmptyCalls = 0;
|
|
767
|
-
debugLog("pruner.pass.start", {
|
|
768
|
-
pass: passContext.pass,
|
|
769
|
-
maxPasses: passContext.maxPasses,
|
|
770
|
-
poolTokens: passContext.poolTokens,
|
|
771
|
-
targetTokens: passContext.targetTokens,
|
|
772
|
-
deltaTokens: passContext.deltaTokens,
|
|
773
|
-
observationCount: observations.length,
|
|
774
|
-
reflectionCount: reflections.length,
|
|
775
|
-
observationIds: isDebugLogEnabled() ? observations.map((observation) => observation.id) : undefined,
|
|
776
|
-
});
|
|
777
|
-
|
|
778
|
-
const dropTool: AgentTool<typeof DropObservationsSchema> = {
|
|
779
|
-
name: "drop_observations",
|
|
780
|
-
label: "Drop observations",
|
|
781
|
-
description:
|
|
782
|
-
"Remove one or more observations from the kept set by id. May be called multiple times. " +
|
|
783
|
-
"Stop calling when no further drops are warranted, then emit a short plain-text confirmation.",
|
|
784
|
-
parameters: DropObservationsSchema,
|
|
785
|
-
execute: async (_id, params: DropObservationsArgs) => {
|
|
786
|
-
const valid: string[] = [];
|
|
787
|
-
const unknown: string[] = [];
|
|
788
|
-
const already: string[] = [];
|
|
789
|
-
for (const id of params.ids) {
|
|
790
|
-
if (!idSet.has(id)) {
|
|
791
|
-
unknown.push(id);
|
|
792
|
-
continue;
|
|
793
|
-
}
|
|
794
|
-
if (dropped.has(id)) {
|
|
795
|
-
already.push(id);
|
|
796
|
-
continue;
|
|
797
|
-
}
|
|
798
|
-
dropped.add(id);
|
|
799
|
-
valid.push(id);
|
|
800
|
-
}
|
|
801
|
-
if (valid.length === 0) {
|
|
802
|
-
consecutiveEmptyCalls++;
|
|
803
|
-
} else {
|
|
804
|
-
consecutiveEmptyCalls = 0;
|
|
805
|
-
}
|
|
806
|
-
const remaining = idSet.size - dropped.size;
|
|
807
|
-
const parts: string[] = [];
|
|
808
|
-
parts.push(`Dropped ${valid.length} observation${valid.length === 1 ? "" : "s"}.`);
|
|
809
|
-
if (unknown.length) parts.push(`Unknown ids ignored: ${unknown.join(", ")}.`);
|
|
810
|
-
if (already.length) parts.push(`Already dropped: ${already.join(", ")}.`);
|
|
811
|
-
parts.push(`Remaining kept: ${remaining} of ${idSet.size}.`);
|
|
812
|
-
parts.push("Call drop_observations again if more should be removed; otherwise stop and emit a short plain-text confirmation.");
|
|
813
|
-
debugLog("pruner.tool_call", {
|
|
814
|
-
pass: passContext.pass,
|
|
815
|
-
requestedIds: params.ids,
|
|
816
|
-
dropped: valid,
|
|
817
|
-
unknown,
|
|
818
|
-
already,
|
|
819
|
-
remaining,
|
|
820
|
-
reason: params.reason,
|
|
821
|
-
});
|
|
822
|
-
return {
|
|
823
|
-
content: [{ type: "text", text: parts.join(" ") }],
|
|
824
|
-
details: { dropped: valid, unknown, already, remaining },
|
|
825
|
-
};
|
|
826
|
-
},
|
|
827
|
-
};
|
|
828
|
-
|
|
829
|
-
const pressureLine =
|
|
830
|
-
passContext.deltaTokens > 0
|
|
831
|
-
? `Pool ~${passContext.poolTokens.toLocaleString()} tokens, target ~${passContext.targetTokens.toLocaleString()} tokens, still need to cut at least ~${passContext.deltaTokens.toLocaleString()} tokens.`
|
|
832
|
-
: `Pool ~${passContext.poolTokens.toLocaleString()} tokens, target ~${passContext.targetTokens.toLocaleString()} tokens (already under budget) — drop only clear redundancies.`;
|
|
833
|
-
|
|
834
|
-
const passGuidance = buildPrunerPassGuidance(passContext.pass, passContext.maxPasses);
|
|
835
|
-
|
|
836
|
-
const userText = `CURRENT REFLECTIONS:
|
|
837
|
-
${joinReflectionsOrEmpty(reflections)}
|
|
838
|
-
|
|
839
|
-
CURRENT OBSERVATIONS:
|
|
840
|
-
${renderObservationsForPrunerPrompt(observations, passContext.coverageTags)}
|
|
841
|
-
|
|
842
|
-
${pressureLine}
|
|
843
|
-
|
|
844
|
-
${passGuidance}
|
|
845
|
-
|
|
846
|
-
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.`;
|
|
847
|
-
|
|
848
|
-
const prompts: Message[] = [
|
|
849
|
-
{
|
|
850
|
-
role: "user",
|
|
851
|
-
content: [{ type: "text", text: userText }],
|
|
852
|
-
timestamp: Date.now(),
|
|
853
|
-
},
|
|
854
|
-
];
|
|
855
|
-
|
|
856
|
-
const context: AgentContext = {
|
|
857
|
-
systemPrompt: PRUNER_SYSTEM,
|
|
858
|
-
messages: [],
|
|
859
|
-
tools: [dropTool as AgentTool<any>],
|
|
860
|
-
};
|
|
861
|
-
|
|
862
|
-
const reasoning = (args.model as { reasoning?: unknown }).reasoning;
|
|
863
|
-
const thinkingLevel = args.thinkingLevel ?? "low";
|
|
864
|
-
const effectiveMaxTurns = args.maxTurns && args.maxTurns > 0 ? args.maxTurns : undefined;
|
|
865
|
-
let turnCount = 0;
|
|
866
|
-
|
|
867
|
-
const config: AgentLoopConfig = {
|
|
868
|
-
model: args.model as any,
|
|
869
|
-
apiKey: args.apiKey,
|
|
870
|
-
headers: args.headers,
|
|
871
|
-
maxTokens: boundedMaxTokens(args.model, AGENT_LOOP_MAX_TOKENS),
|
|
872
|
-
convertToLlm: (msgs) => msgs as Message[],
|
|
873
|
-
toolExecution: "sequential",
|
|
874
|
-
...(reasoning && thinkingLevel !== "off" ? { reasoning: thinkingLevel } : {}),
|
|
875
|
-
shouldStopAfterTurn: () => {
|
|
876
|
-
turnCount++;
|
|
877
|
-
if (effectiveMaxTurns !== undefined && turnCount >= effectiveMaxTurns) return true;
|
|
878
|
-
if (consecutiveEmptyCalls >= 2) return true;
|
|
879
|
-
return false;
|
|
880
|
-
},
|
|
881
|
-
};
|
|
882
|
-
|
|
883
|
-
let agentLoopCalled = false;
|
|
884
|
-
let streamCreated = false;
|
|
885
|
-
let firstEventSeen = false;
|
|
886
|
-
try {
|
|
887
|
-
debugLog("pruner.agent_loop.before_call", { pass: passContext.pass });
|
|
888
|
-
const loop = args.agentLoop ?? agentLoop;
|
|
889
|
-
agentLoopCalled = true;
|
|
890
|
-
const stream = loop(prompts, context, config, args.signal);
|
|
891
|
-
streamCreated = true;
|
|
892
|
-
debugLog("pruner.agent_loop.stream_created", { pass: passContext.pass });
|
|
893
|
-
for await (const event of stream) {
|
|
894
|
-
if (!firstEventSeen) {
|
|
895
|
-
firstEventSeen = true;
|
|
896
|
-
debugLog("pruner.agent_loop.first_event", { pass: passContext.pass, type: event.type });
|
|
897
|
-
}
|
|
898
|
-
logAgentLoopEvent("pruner", passContext.pass, event);
|
|
899
|
-
args.onEvent?.(event);
|
|
900
|
-
}
|
|
901
|
-
await stream.result();
|
|
902
|
-
} catch (error) {
|
|
903
|
-
debugLog("pruner.agent_loop.error", {
|
|
904
|
-
pass: passContext.pass,
|
|
905
|
-
agentLoopCalled,
|
|
906
|
-
streamCreated,
|
|
907
|
-
firstEventSeen,
|
|
908
|
-
errorMessage: error instanceof Error ? error.message : String(error),
|
|
909
|
-
});
|
|
910
|
-
return { kept: observations, droppedIds: [], fellBack: true };
|
|
911
|
-
}
|
|
912
|
-
|
|
913
|
-
const kept = observations.filter((o) => !dropped.has(o.id));
|
|
914
|
-
const droppedIds = Array.from(dropped);
|
|
915
|
-
debugLog("pruner.pass.result", {
|
|
916
|
-
pass: passContext.pass,
|
|
917
|
-
droppedIds,
|
|
918
|
-
dropped: droppedIds.length,
|
|
919
|
-
remaining: kept.length,
|
|
920
|
-
agentLoopCalled,
|
|
921
|
-
streamCreated,
|
|
922
|
-
firstEventSeen,
|
|
923
|
-
});
|
|
924
|
-
return { kept, droppedIds, fellBack: false };
|
|
925
|
-
}
|
|
926
|
-
|
|
927
|
-
export async function runPruner(
|
|
928
|
-
args: LlmArgs,
|
|
929
|
-
reflections: MemoryReflection[],
|
|
930
|
-
observations: ObservationRecord[],
|
|
931
|
-
budgetTokens: number,
|
|
932
|
-
onPassStart?: (pass: number, maxPasses: number) => void,
|
|
933
|
-
): Promise<PrunerResult> {
|
|
934
|
-
debugLog("pruner.start", {
|
|
935
|
-
reflectionCount: reflections.length,
|
|
936
|
-
observationCount: observations.length,
|
|
937
|
-
budgetTokens,
|
|
938
|
-
});
|
|
939
|
-
if (observations.length === 0) {
|
|
940
|
-
const result: PrunerResult = { observations: [], droppedIds: [], fellBack: false, passes: [], stopReason: "empty" };
|
|
941
|
-
debugLog("pruner.result", {
|
|
942
|
-
stopReason: result.stopReason,
|
|
943
|
-
fellBack: result.fellBack,
|
|
944
|
-
droppedIds: result.droppedIds,
|
|
945
|
-
dropped: result.droppedIds.length,
|
|
946
|
-
passes: result.passes,
|
|
947
|
-
finalObservationCount: result.observations.length,
|
|
948
|
-
});
|
|
949
|
-
return result;
|
|
950
|
-
}
|
|
951
|
-
|
|
952
|
-
const target = Math.max(1, Math.floor(budgetTokens * PRUNER_TARGET_RATIO));
|
|
953
|
-
const coverageTags = deriveObservationCoverageTags(reflections, observations);
|
|
954
|
-
let pool = observations;
|
|
955
|
-
|
|
956
|
-
const allDropped: string[] = [];
|
|
957
|
-
const passes: PrunerPassStats[] = [];
|
|
958
|
-
let fellBack = false;
|
|
959
|
-
let stopReason: PrunerStopReason | undefined;
|
|
960
|
-
|
|
961
|
-
for (let pass = 1; pass <= PRUNER_MAX_PASSES; pass++) {
|
|
962
|
-
const poolTokens = observationPoolTokens(pool);
|
|
963
|
-
if (poolTokens <= target) {
|
|
964
|
-
stopReason = "under_target";
|
|
965
|
-
debugLog("pruner.under_target", { pass, poolTokens, targetTokens: target, observationCount: pool.length });
|
|
966
|
-
break;
|
|
967
|
-
}
|
|
968
|
-
|
|
969
|
-
onPassStart?.(pass, PRUNER_MAX_PASSES);
|
|
970
|
-
const deltaTokens = poolTokens - target;
|
|
971
|
-
const result = await runPrunerPass(args, reflections, pool, {
|
|
972
|
-
poolTokens,
|
|
973
|
-
targetTokens: target,
|
|
974
|
-
deltaTokens,
|
|
975
|
-
pass,
|
|
976
|
-
maxPasses: PRUNER_MAX_PASSES,
|
|
977
|
-
coverageTags,
|
|
978
|
-
});
|
|
979
|
-
passes.push({
|
|
980
|
-
pass,
|
|
981
|
-
poolTokens,
|
|
982
|
-
targetTokens: target,
|
|
983
|
-
deltaTokens,
|
|
984
|
-
dropped: result.droppedIds.length,
|
|
985
|
-
remaining: result.kept.length,
|
|
986
|
-
fellBack: result.fellBack,
|
|
987
|
-
});
|
|
988
|
-
|
|
989
|
-
if (result.fellBack) {
|
|
990
|
-
fellBack = true;
|
|
991
|
-
stopReason = "fell_back";
|
|
992
|
-
break;
|
|
993
|
-
}
|
|
994
|
-
if (result.droppedIds.length === 0) {
|
|
995
|
-
stopReason = "zero_drops";
|
|
996
|
-
break;
|
|
997
|
-
}
|
|
998
|
-
|
|
999
|
-
pool = result.kept;
|
|
1000
|
-
allDropped.push(...result.droppedIds);
|
|
1001
|
-
}
|
|
1002
|
-
|
|
1003
|
-
stopReason ??= observationPoolTokens(pool) <= target ? "under_target" : "max_passes";
|
|
1004
|
-
const result = { observations: pool, droppedIds: allDropped, fellBack, passes, stopReason };
|
|
1005
|
-
debugLog("pruner.result", {
|
|
1006
|
-
stopReason: result.stopReason,
|
|
1007
|
-
fellBack: result.fellBack,
|
|
1008
|
-
droppedIds: result.droppedIds,
|
|
1009
|
-
dropped: result.droppedIds.length,
|
|
1010
|
-
passes: result.passes,
|
|
1011
|
-
finalObservationCount: result.observations.length,
|
|
1012
|
-
});
|
|
1013
|
-
return result;
|
|
1014
|
-
}
|
|
1015
|
-
|
|
1016
|
-
export function renderSummary(reflections: MemoryReflection[], observations: ObservationRecord[]): string {
|
|
1017
|
-
if (reflections.length === 0 && observations.length === 0) return "";
|
|
1018
|
-
|
|
1019
|
-
const parts: string[] = [CONTEXT_USAGE_INSTRUCTIONS];
|
|
1020
|
-
|
|
1021
|
-
if (reflections.length > 0) {
|
|
1022
|
-
parts.push(`## Reflections\n${reflections.map(reflectionToPromptLine).join("\n")}`);
|
|
1023
|
-
}
|
|
1024
|
-
if (observations.length > 0) {
|
|
1025
|
-
const body = observationsToPromptLines(observations).join("\n");
|
|
1026
|
-
parts.push(`## Observations\n${body}`);
|
|
1027
|
-
}
|
|
1028
|
-
|
|
1029
|
-
return parts.join("\n\n");
|
|
1030
|
-
}
|