pi-observational-memory 2.4.1 → 2.4.3
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 -2
- package/package.json +6 -5
- package/src/commands/status.ts +4 -3
- package/src/commands/view.ts +5 -3
- package/src/compaction.ts +448 -35
- package/src/config.ts +56 -0
- package/src/debug-log.ts +53 -0
- package/src/hooks/compaction-hook.ts +197 -8
- package/src/hooks/compaction-trigger.ts +24 -1
- package/src/hooks/observer-trigger.ts +70 -37
- package/src/model-budget.ts +9 -0
- package/src/observer.ts +24 -6
- package/src/progress.ts +155 -0
- package/src/prompts.ts +23 -22
package/src/compaction.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
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 "
|
|
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";
|
|
4
5
|
import { hashId } from "./ids.js";
|
|
6
|
+
import { AGENT_LOOP_MAX_TOKENS, boundedMaxTokens } from "./model-budget.js";
|
|
5
7
|
import { observationsToPromptLines } from "./observer.js";
|
|
6
8
|
import { buildPrunerPassGuidance, buildReflectorPassGuidance, CONTEXT_USAGE_INSTRUCTIONS, PRUNER_SYSTEM, REFLECTOR_SYSTEM } from "./prompts.js";
|
|
7
9
|
import { truncateRecordContent } from "./serialize.js";
|
|
@@ -9,12 +11,12 @@ import { estimateStringTokens } from "./tokens.js";
|
|
|
9
11
|
import { reflectionContent, reflectionToPromptLine } from "./types.js";
|
|
10
12
|
import type { MemoryReflection, ObservationRecord, ReflectionRecord } from "./types.js";
|
|
11
13
|
|
|
12
|
-
const REFLECTOR_MAX_PASSES =
|
|
13
|
-
const PRUNER_MAX_PASSES =
|
|
14
|
+
export const REFLECTOR_MAX_PASSES = 2;
|
|
15
|
+
export const PRUNER_MAX_PASSES = 2;
|
|
14
16
|
const PRUNER_TARGET_RATIO = 0.8;
|
|
15
17
|
|
|
16
|
-
function observationPoolTokens(observations: ObservationRecord[]): number {
|
|
17
|
-
return
|
|
18
|
+
export function observationPoolTokens(observations: ObservationRecord[]): number {
|
|
19
|
+
return estimateStringTokens(observationsToPromptLines(observations).join("\n"));
|
|
18
20
|
}
|
|
19
21
|
|
|
20
22
|
interface LlmArgs {
|
|
@@ -23,6 +25,9 @@ interface LlmArgs {
|
|
|
23
25
|
headers?: Record<string, string>;
|
|
24
26
|
signal?: AbortSignal;
|
|
25
27
|
agentLoop?: typeof agentLoop;
|
|
28
|
+
onEvent?: (event: import("@mariozechner/pi-agent-core").AgentEvent) => void;
|
|
29
|
+
maxTurns?: number;
|
|
30
|
+
thinkingLevel?: ModelThinkingLevel;
|
|
26
31
|
}
|
|
27
32
|
|
|
28
33
|
function joinReflectionsOrEmpty(items: MemoryReflection[]): string {
|
|
@@ -33,8 +38,123 @@ function joinObservationsOrEmpty(items: ObservationRecord[]): string {
|
|
|
33
38
|
return items.length ? observationsToPromptLines(items).join("\n") : "(none yet)";
|
|
34
39
|
}
|
|
35
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
|
+
|
|
36
150
|
export type ObservationCoverageTag = "uncited" | "cited" | "reinforced";
|
|
37
151
|
|
|
152
|
+
export interface CoverageTagCounts {
|
|
153
|
+
uncited: number;
|
|
154
|
+
cited: number;
|
|
155
|
+
reinforced: number;
|
|
156
|
+
}
|
|
157
|
+
|
|
38
158
|
export function deriveObservationCoverageTags(
|
|
39
159
|
reflections: MemoryReflection[],
|
|
40
160
|
observations: ObservationRecord[],
|
|
@@ -57,6 +177,18 @@ export function deriveObservationCoverageTags(
|
|
|
57
177
|
return tags;
|
|
58
178
|
}
|
|
59
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
|
+
|
|
60
192
|
export function renderObservationsForPrunerPrompt(
|
|
61
193
|
observations: ObservationRecord[],
|
|
62
194
|
coverageTags: ReadonlyMap<string, ObservationCoverageTag>,
|
|
@@ -120,13 +252,13 @@ const RecordReflectionsSchema = Type.Object({
|
|
|
120
252
|
supportingObservationIds: Type.Array(
|
|
121
253
|
Type.String({
|
|
122
254
|
pattern: "^[a-f0-9]{12}$",
|
|
123
|
-
description: "Exact observation id from the current-observations list
|
|
255
|
+
description: "Exact observation id from the current-observations list whose durable meaning is captured by this reflection.",
|
|
124
256
|
}),
|
|
125
257
|
{
|
|
126
258
|
minItems: 1,
|
|
127
259
|
description:
|
|
128
|
-
"
|
|
129
|
-
"Use only ids shown in the current observations list; never invent ids.",
|
|
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.",
|
|
130
262
|
},
|
|
131
263
|
),
|
|
132
264
|
}),
|
|
@@ -179,6 +311,83 @@ export interface ApplyReflectionProposalsResult {
|
|
|
179
311
|
unsupported: number;
|
|
180
312
|
}
|
|
181
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
|
+
|
|
182
391
|
function reflectorPassContext(pass: number): ReflectorPassContext {
|
|
183
392
|
return {
|
|
184
393
|
pass,
|
|
@@ -307,16 +516,26 @@ async function runReflectorPass(
|
|
|
307
516
|
reflections: MemoryReflection[],
|
|
308
517
|
observations: ObservationRecord[],
|
|
309
518
|
passContext: ReflectorPassContext,
|
|
310
|
-
): Promise<{ reflections: MemoryReflection[];
|
|
519
|
+
): Promise<{ reflections: MemoryReflection[]; stats: ReflectorPassStats }> {
|
|
311
520
|
const allowedObservationIds = observations.map((o) => o.id);
|
|
312
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
|
+
});
|
|
313
532
|
|
|
314
533
|
const recordTool: AgentTool<typeof RecordReflectionsSchema> = {
|
|
315
534
|
name: "record_reflections",
|
|
316
535
|
label: "Record reflections",
|
|
317
536
|
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, " +
|
|
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, " +
|
|
320
539
|
"then emit a short plain-text confirmation.",
|
|
321
540
|
parameters: RecordReflectionsSchema,
|
|
322
541
|
execute: async (_id, params: RecordReflectionsArgs) => {
|
|
@@ -327,6 +546,12 @@ async function runReflectorPass(
|
|
|
327
546
|
passContext,
|
|
328
547
|
);
|
|
329
548
|
currentReflections = result.reflections;
|
|
549
|
+
addReflectionProposalStats(stats, result);
|
|
550
|
+
if (result.accepted === 0) {
|
|
551
|
+
consecutiveEmptyCalls++;
|
|
552
|
+
} else {
|
|
553
|
+
consecutiveEmptyCalls = 0;
|
|
554
|
+
}
|
|
330
555
|
const parts: string[] = [];
|
|
331
556
|
parts.push(`Accepted ${result.accepted} reflection proposal${result.accepted === 1 ? "" : "s"}.`);
|
|
332
557
|
if (result.added) parts.push(`${result.added} new.`);
|
|
@@ -339,6 +564,20 @@ async function runReflectorPass(
|
|
|
339
564
|
);
|
|
340
565
|
}
|
|
341
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
|
+
});
|
|
342
581
|
return {
|
|
343
582
|
content: [{ type: "text", text: parts.join(" ") }],
|
|
344
583
|
details: result,
|
|
@@ -356,7 +595,7 @@ ${joinObservationsOrEmpty(observations)}
|
|
|
356
595
|
REFLECTOR PASS GUIDANCE:
|
|
357
596
|
${passGuidance}
|
|
358
597
|
|
|
359
|
-
Crystallize long-lived reflections from the full observation pool for this pass. Call record_reflections with batches of reflection proposals, each with
|
|
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.`;
|
|
360
599
|
|
|
361
600
|
const prompts: Message[] = [
|
|
362
601
|
{
|
|
@@ -373,50 +612,114 @@ Crystallize long-lived reflections from the full observation pool for this pass.
|
|
|
373
612
|
};
|
|
374
613
|
|
|
375
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
|
+
|
|
376
619
|
const config: AgentLoopConfig = {
|
|
377
620
|
model: args.model as any,
|
|
378
621
|
apiKey: args.apiKey,
|
|
379
622
|
headers: args.headers,
|
|
380
|
-
maxTokens:
|
|
623
|
+
maxTokens: boundedMaxTokens(args.model, AGENT_LOOP_MAX_TOKENS),
|
|
381
624
|
convertToLlm: (msgs) => msgs as Message[],
|
|
382
625
|
toolExecution: "sequential",
|
|
383
|
-
...(reasoning ? { reasoning:
|
|
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
|
+
},
|
|
384
633
|
};
|
|
385
634
|
|
|
635
|
+
let firstEventSeen = false;
|
|
386
636
|
try {
|
|
637
|
+
debugLog("reflector.agent_loop.before_call", { pass: passContext.pass });
|
|
387
638
|
const loop = args.agentLoop ?? agentLoop;
|
|
388
639
|
const stream = loop(prompts, context, config, args.signal);
|
|
389
|
-
|
|
390
|
-
|
|
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);
|
|
391
648
|
}
|
|
392
649
|
await stream.result();
|
|
393
|
-
|
|
394
|
-
|
|
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 };
|
|
395
659
|
}
|
|
396
660
|
|
|
397
|
-
return { reflections: currentReflections,
|
|
661
|
+
return { reflections: currentReflections, stats };
|
|
398
662
|
}
|
|
399
663
|
|
|
400
664
|
export async function runReflector(
|
|
401
665
|
args: LlmArgs,
|
|
402
666
|
reflections: MemoryReflection[],
|
|
403
667
|
observations: ObservationRecord[],
|
|
404
|
-
|
|
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
|
+
});
|
|
405
683
|
let currentReflections = reflections;
|
|
684
|
+
const passes: ReflectorPassStats[] = [];
|
|
406
685
|
|
|
407
686
|
for (let pass = 1; pass <= REFLECTOR_MAX_PASSES; pass++) {
|
|
687
|
+
onPassStart?.(pass, REFLECTOR_MAX_PASSES);
|
|
408
688
|
const result = await runReflectorPass(args, currentReflections, observations, reflectorPassContext(pass));
|
|
409
689
|
currentReflections = result.reflections;
|
|
410
|
-
|
|
690
|
+
passes.push(result.stats);
|
|
691
|
+
if (result.stats.failed) break;
|
|
411
692
|
}
|
|
412
693
|
|
|
413
|
-
|
|
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;
|
|
414
715
|
}
|
|
415
716
|
|
|
416
717
|
export interface PrunerResult {
|
|
417
718
|
observations: ObservationRecord[];
|
|
418
719
|
droppedIds: string[];
|
|
419
720
|
fellBack: boolean;
|
|
721
|
+
passes: PrunerPassStats[];
|
|
722
|
+
stopReason: PrunerStopReason;
|
|
420
723
|
}
|
|
421
724
|
|
|
422
725
|
const DropObservationsSchema = Type.Object({
|
|
@@ -460,6 +763,17 @@ async function runPrunerPass(
|
|
|
460
763
|
): Promise<PrunerPassResult> {
|
|
461
764
|
const idSet = new Set(observations.map((o) => o.id));
|
|
462
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
|
+
});
|
|
463
777
|
|
|
464
778
|
const dropTool: AgentTool<typeof DropObservationsSchema> = {
|
|
465
779
|
name: "drop_observations",
|
|
@@ -484,6 +798,11 @@ async function runPrunerPass(
|
|
|
484
798
|
dropped.add(id);
|
|
485
799
|
valid.push(id);
|
|
486
800
|
}
|
|
801
|
+
if (valid.length === 0) {
|
|
802
|
+
consecutiveEmptyCalls++;
|
|
803
|
+
} else {
|
|
804
|
+
consecutiveEmptyCalls = 0;
|
|
805
|
+
}
|
|
487
806
|
const remaining = idSet.size - dropped.size;
|
|
488
807
|
const parts: string[] = [];
|
|
489
808
|
parts.push(`Dropped ${valid.length} observation${valid.length === 1 ? "" : "s"}.`);
|
|
@@ -491,6 +810,15 @@ async function runPrunerPass(
|
|
|
491
810
|
if (already.length) parts.push(`Already dropped: ${already.join(", ")}.`);
|
|
492
811
|
parts.push(`Remaining kept: ${remaining} of ${idSet.size}.`);
|
|
493
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
|
+
});
|
|
494
822
|
return {
|
|
495
823
|
content: [{ type: "text", text: parts.join(" ") }],
|
|
496
824
|
details: { dropped: valid, unknown, already, remaining },
|
|
@@ -532,29 +860,68 @@ Decide which observations to remove from the kept set. Call drop_observations wi
|
|
|
532
860
|
};
|
|
533
861
|
|
|
534
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
|
+
|
|
535
867
|
const config: AgentLoopConfig = {
|
|
536
868
|
model: args.model as any,
|
|
537
869
|
apiKey: args.apiKey,
|
|
538
870
|
headers: args.headers,
|
|
539
|
-
maxTokens:
|
|
871
|
+
maxTokens: boundedMaxTokens(args.model, AGENT_LOOP_MAX_TOKENS),
|
|
540
872
|
convertToLlm: (msgs) => msgs as Message[],
|
|
541
873
|
toolExecution: "sequential",
|
|
542
|
-
...(reasoning ? { reasoning:
|
|
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
|
+
},
|
|
543
881
|
};
|
|
544
882
|
|
|
883
|
+
let agentLoopCalled = false;
|
|
884
|
+
let streamCreated = false;
|
|
885
|
+
let firstEventSeen = false;
|
|
545
886
|
try {
|
|
887
|
+
debugLog("pruner.agent_loop.before_call", { pass: passContext.pass });
|
|
546
888
|
const loop = args.agentLoop ?? agentLoop;
|
|
889
|
+
agentLoopCalled = true;
|
|
547
890
|
const stream = loop(prompts, context, config, args.signal);
|
|
548
|
-
|
|
549
|
-
|
|
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);
|
|
550
900
|
}
|
|
551
901
|
await stream.result();
|
|
552
|
-
} catch {
|
|
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
|
+
});
|
|
553
910
|
return { kept: observations, droppedIds: [], fellBack: true };
|
|
554
911
|
}
|
|
555
912
|
|
|
556
913
|
const kept = observations.filter((o) => !dropped.has(o.id));
|
|
557
|
-
|
|
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 };
|
|
558
925
|
}
|
|
559
926
|
|
|
560
927
|
export async function runPruner(
|
|
@@ -562,21 +929,44 @@ export async function runPruner(
|
|
|
562
929
|
reflections: MemoryReflection[],
|
|
563
930
|
observations: ObservationRecord[],
|
|
564
931
|
budgetTokens: number,
|
|
932
|
+
onPassStart?: (pass: number, maxPasses: number) => void,
|
|
565
933
|
): Promise<PrunerResult> {
|
|
934
|
+
debugLog("pruner.start", {
|
|
935
|
+
reflectionCount: reflections.length,
|
|
936
|
+
observationCount: observations.length,
|
|
937
|
+
budgetTokens,
|
|
938
|
+
});
|
|
566
939
|
if (observations.length === 0) {
|
|
567
|
-
|
|
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;
|
|
568
950
|
}
|
|
569
951
|
|
|
570
952
|
const target = Math.max(1, Math.floor(budgetTokens * PRUNER_TARGET_RATIO));
|
|
571
|
-
let pool = observations;
|
|
572
953
|
const coverageTags = deriveObservationCoverageTags(reflections, observations);
|
|
954
|
+
let pool = observations;
|
|
955
|
+
|
|
573
956
|
const allDropped: string[] = [];
|
|
957
|
+
const passes: PrunerPassStats[] = [];
|
|
574
958
|
let fellBack = false;
|
|
959
|
+
let stopReason: PrunerStopReason | undefined;
|
|
575
960
|
|
|
576
961
|
for (let pass = 1; pass <= PRUNER_MAX_PASSES; pass++) {
|
|
577
962
|
const poolTokens = observationPoolTokens(pool);
|
|
578
|
-
if (poolTokens <= target)
|
|
963
|
+
if (poolTokens <= target) {
|
|
964
|
+
stopReason = "under_target";
|
|
965
|
+
debugLog("pruner.under_target", { pass, poolTokens, targetTokens: target, observationCount: pool.length });
|
|
966
|
+
break;
|
|
967
|
+
}
|
|
579
968
|
|
|
969
|
+
onPassStart?.(pass, PRUNER_MAX_PASSES);
|
|
580
970
|
const deltaTokens = poolTokens - target;
|
|
581
971
|
const result = await runPrunerPass(args, reflections, pool, {
|
|
582
972
|
poolTokens,
|
|
@@ -586,18 +976,41 @@ export async function runPruner(
|
|
|
586
976
|
maxPasses: PRUNER_MAX_PASSES,
|
|
587
977
|
coverageTags,
|
|
588
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
|
+
});
|
|
589
988
|
|
|
590
989
|
if (result.fellBack) {
|
|
591
990
|
fellBack = true;
|
|
991
|
+
stopReason = "fell_back";
|
|
992
|
+
break;
|
|
993
|
+
}
|
|
994
|
+
if (result.droppedIds.length === 0) {
|
|
995
|
+
stopReason = "zero_drops";
|
|
592
996
|
break;
|
|
593
997
|
}
|
|
594
|
-
if (result.droppedIds.length === 0) break;
|
|
595
998
|
|
|
596
999
|
pool = result.kept;
|
|
597
1000
|
allDropped.push(...result.droppedIds);
|
|
598
1001
|
}
|
|
599
1002
|
|
|
600
|
-
|
|
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;
|
|
601
1014
|
}
|
|
602
1015
|
|
|
603
1016
|
export function renderSummary(reflections: MemoryReflection[], observations: ObservationRecord[]): string {
|