@matthewfl/pi-contemplator 0.0.10 → 0.1.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 +12 -12
- package/package.json +6 -6
- package/src/agents/contemplator/agent.ts +325 -91
- package/src/agents/contemplator/prompts.ts +6 -6
- package/src/agents/observer/agent.ts +96 -39
- package/src/agents/observer/prompts.ts +19 -10
- package/src/agents/reviewer/agent.ts +24 -4
- package/src/agents/reviewer/prompts.ts +1 -1
- package/src/agents/reviewer/tools.ts +24 -9
- package/src/agents/stream-errors.ts +1 -1
- package/src/agents/summarizer/agent.ts +597 -0
- package/src/agents/summarizer/prompts.ts +46 -0
- package/src/agents/summarizer/sampling.ts +80 -0
- package/src/commands/contemplator-view.ts +22 -1
- package/src/commands/settings.ts +95 -70
- package/src/commands/status.ts +60 -36
- package/src/commands/summarizer-view.ts +58 -0
- package/src/commands/view.ts +22 -10
- package/src/config.ts +30 -37
- package/src/hooks/compaction-hook.ts +32 -17
- package/src/hooks/compaction-resume.ts +4 -4
- package/src/hooks/compaction-trigger.ts +33 -11
- package/src/hooks/consolidation-trigger.ts +245 -215
- package/src/memory-citations.ts +37 -0
- package/src/required-tool-choice.ts +28 -0
- package/src/runtime.ts +115 -32
- package/src/session-ledger/fold.ts +82 -53
- package/src/session-ledger/index.ts +1 -0
- package/src/session-ledger/pools.ts +77 -0
- package/src/session-ledger/progress.ts +8 -19
- package/src/session-ledger/projection.ts +45 -177
- package/src/session-ledger/recall.ts +129 -127
- package/src/session-ledger/render-summary.ts +20 -19
- package/src/session-ledger/search.ts +99 -115
- package/src/session-ledger/types.ts +103 -77
- package/src/tools/compact-context.ts +1 -1
- package/src/tools/recall-observation.ts +99 -459
- package/src/tools/search-memories.ts +31 -72
- package/src/agents/dropper/agent.ts +0 -291
- package/src/agents/dropper/coverage.ts +0 -128
- package/src/agents/dropper/pool.ts +0 -67
- package/src/agents/dropper/prompts.ts +0 -48
- package/src/agents/reflector/agent.ts +0 -213
- package/src/agents/reflector/prompts.ts +0 -81
|
@@ -1,31 +1,23 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import {
|
|
3
|
-
import { observationPoolMetrics } from "../agents/dropper/pool.js";
|
|
2
|
+
import { runSummarizer } from "../agents/summarizer/agent.js";
|
|
4
3
|
import { runObserver } from "../agents/observer/agent.js";
|
|
5
|
-
import { runReflector } from "../agents/reflector/agent.js";
|
|
6
4
|
import { debugLog, withDebugLogContext } from "../debug-log.js";
|
|
7
5
|
import { resolveObserverChunkMaxTokens } from "../config.js";
|
|
8
6
|
import type { ResolveResult, Runtime } from "../runtime.js";
|
|
9
7
|
import { serializeSourceAddressedBranchEntries } from "../serialize.js";
|
|
10
8
|
import {
|
|
11
|
-
|
|
9
|
+
OM_SUMMARIZER_COMMIT,
|
|
12
10
|
OM_OBSERVATIONS_RECORDED,
|
|
13
|
-
OM_REFLECTIONS_RECORDED,
|
|
14
|
-
buildObservationsDroppedData,
|
|
15
11
|
buildObservationsRecordedData,
|
|
16
|
-
buildReflectionsRecordedData,
|
|
17
|
-
earlierCoverageMarkerId,
|
|
18
12
|
foldLedger,
|
|
19
13
|
fullProjection,
|
|
20
14
|
isSourceEntry,
|
|
21
15
|
latestCoverageIndex,
|
|
22
|
-
latestCoverageMarkerId,
|
|
23
16
|
observationToSummaryLine,
|
|
17
|
+
partitionMemoryPools,
|
|
24
18
|
rawTokensSinceObservationCoverage,
|
|
25
|
-
|
|
26
|
-
reflectionToSummaryLine,
|
|
19
|
+
summaryToSummaryLine,
|
|
27
20
|
type Entry,
|
|
28
|
-
type Reflection,
|
|
29
21
|
} from "../session-ledger/index.js";
|
|
30
22
|
|
|
31
23
|
type ResolvedModel = Extract<ResolveResult, { ok: true }>;
|
|
@@ -45,11 +37,35 @@ export type ConsolidationCtx = {
|
|
|
45
37
|
|
|
46
38
|
type StageOutcome = "continue" | "abort";
|
|
47
39
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
40
|
+
/** Abort only when a summarizer produces no stream/message progress for this long. */
|
|
41
|
+
export const SUMMARIZER_STALL_TIMEOUT_MS = 15 * 60_000;
|
|
42
|
+
|
|
43
|
+
export function createSummarizerStallWatchdog(
|
|
44
|
+
timeoutMs: number,
|
|
45
|
+
onStall: (signal: AbortSignal) => void,
|
|
46
|
+
): { signal: AbortSignal; progress: () => void; dispose: () => void } {
|
|
47
|
+
const controller = new AbortController();
|
|
48
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
49
|
+
const progress = () => {
|
|
50
|
+
if (controller.signal.aborted) return;
|
|
51
|
+
if (timer !== undefined) clearTimeout(timer);
|
|
52
|
+
timer = setTimeout(() => {
|
|
53
|
+
timer = undefined;
|
|
54
|
+
controller.abort(new Error(`summarizer produced no progress for ${Math.round(timeoutMs / 60_000)} minutes`));
|
|
55
|
+
onStall(controller.signal);
|
|
56
|
+
}, timeoutMs);
|
|
57
|
+
(timer as ReturnType<typeof setTimeout> & { unref?: () => void }).unref?.();
|
|
58
|
+
};
|
|
59
|
+
progress();
|
|
60
|
+
return {
|
|
61
|
+
signal: controller.signal,
|
|
62
|
+
progress,
|
|
63
|
+
dispose: () => {
|
|
64
|
+
if (timer !== undefined) clearTimeout(timer);
|
|
65
|
+
timer = undefined;
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
}
|
|
53
69
|
|
|
54
70
|
function sourceEntriesAfter(entries: Entry[], index: number): Entry[] {
|
|
55
71
|
return entries.slice(index + 1).filter(isSourceEntry);
|
|
@@ -59,27 +75,15 @@ function appendEntry(pi: ExtensionAPI, customType: string, data: unknown): void
|
|
|
59
75
|
pi.appendEntry(customType, data);
|
|
60
76
|
}
|
|
61
77
|
|
|
62
|
-
function mergeReflections(existing: Reflection[], additional: Reflection[]): Reflection[] {
|
|
63
|
-
const seen = new Set(existing.map((reflection) => reflection.id));
|
|
64
|
-
const merged = [...existing];
|
|
65
|
-
for (const reflection of additional) {
|
|
66
|
-
if (seen.has(reflection.id)) continue;
|
|
67
|
-
seen.add(reflection.id);
|
|
68
|
-
merged.push(reflection);
|
|
69
|
-
}
|
|
70
|
-
return merged;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
78
|
function anyStageDue(entries: Entry[], runtime: Runtime): boolean {
|
|
74
|
-
return rawTokensSinceObservationCoverage(entries) >= runtime.config.observeAfterTokens
|
|
75
|
-
|| rawTokensSinceReflectionCoverage(entries) >= runtime.config.reflectAfterTokens;
|
|
79
|
+
return rawTokensSinceObservationCoverage(entries) >= runtime.config.observeAfterTokens;
|
|
76
80
|
}
|
|
77
81
|
|
|
78
82
|
function shouldNotifyWorker(runtime: Runtime, ctx: ConsolidationCtx): boolean {
|
|
79
83
|
return runtime.config.showWorkerNotifications && ctx.hasUI;
|
|
80
84
|
}
|
|
81
85
|
|
|
82
|
-
function makeModelResolver(runtime: Runtime, ctx: ConsolidationCtx): (stage: "observer"
|
|
86
|
+
function makeModelResolver(runtime: Runtime, ctx: ConsolidationCtx): (stage: "observer") => Promise<ResolvedModel | undefined> {
|
|
83
87
|
let cached: ResolveResult | undefined;
|
|
84
88
|
return async (stage) => {
|
|
85
89
|
cached ??= await runtime.resolveModel({
|
|
@@ -94,7 +98,7 @@ function makeModelResolver(runtime: Runtime, ctx: ConsolidationCtx): (stage: "ob
|
|
|
94
98
|
}
|
|
95
99
|
debugLog(`${stage}.model_unavailable`, { reason: cached.reason });
|
|
96
100
|
if (!runtime.resolveFailureNotified && ctx.hasUI && ctx.ui) {
|
|
97
|
-
ctx.ui.notify(`
|
|
101
|
+
ctx.ui.notify(`pi-contemplator: ${stage} skipped — ${cached.reason}`, "warning");
|
|
98
102
|
runtime.resolveFailureNotified = true;
|
|
99
103
|
}
|
|
100
104
|
return undefined;
|
|
@@ -105,8 +109,31 @@ export function registerConsolidationTrigger(pi: ExtensionAPI, runtime: Runtime)
|
|
|
105
109
|
const launch = (_event: unknown, ctx: ConsolidationCtx) => {
|
|
106
110
|
maybeLaunchConsolidation(pi, runtime, ctx);
|
|
107
111
|
};
|
|
108
|
-
pi.on("agent_start",
|
|
109
|
-
|
|
112
|
+
pi.on("agent_start", (event, ctx) => {
|
|
113
|
+
launch(event, ctx);
|
|
114
|
+
syncAndScheduleSummarizer(pi, runtime, ctx as ConsolidationCtx);
|
|
115
|
+
});
|
|
116
|
+
pi.on("turn_end", (event, ctx) => {
|
|
117
|
+
launch(event, ctx);
|
|
118
|
+
syncAndScheduleSummarizer(pi, runtime, ctx as ConsolidationCtx);
|
|
119
|
+
});
|
|
120
|
+
runtime.setAgentActivityListener((ctx) => {
|
|
121
|
+
runtime.ensureConfig(ctx.cwd);
|
|
122
|
+
// Token pools are re-evaluated at every primary-agent progress checkpoint;
|
|
123
|
+
// idle wall-clock time has no scheduling meaning.
|
|
124
|
+
scheduleSummarizer(pi, runtime, ctx as ConsolidationCtx);
|
|
125
|
+
});
|
|
126
|
+
runtime.setSettingsUpdateListener((ctx, settings) => {
|
|
127
|
+
const affectsScheduling = settings.summarizerEnabled !== undefined ||
|
|
128
|
+
settings.newMemoryPoolMaxTokens !== undefined ||
|
|
129
|
+
settings.oldMemoryPoolTargetTokens !== undefined ||
|
|
130
|
+
settings.summarizerRetriggerTokens !== undefined;
|
|
131
|
+
if (!affectsScheduling) return;
|
|
132
|
+
runtime.ensureConfig(ctx.cwd);
|
|
133
|
+
// A changed pool boundary starts a fresh threshold cycle.
|
|
134
|
+
runtime.summarizerNextTriggerTokens = undefined;
|
|
135
|
+
scheduleSummarizer(pi, runtime, ctx as ConsolidationCtx);
|
|
136
|
+
});
|
|
110
137
|
}
|
|
111
138
|
|
|
112
139
|
function debugSessionMetadata(ctx: ConsolidationCtx): { sessionId?: string; sessionFile?: string } {
|
|
@@ -183,6 +210,10 @@ export type ConsolidationPipelineOptions = {
|
|
|
183
210
|
observerOnly?: boolean;
|
|
184
211
|
};
|
|
185
212
|
|
|
213
|
+
export function shouldScheduleSummarizerFromObserver(options: ConsolidationPipelineOptions): boolean {
|
|
214
|
+
return options.observerOnly !== true;
|
|
215
|
+
}
|
|
216
|
+
|
|
186
217
|
export async function runConsolidationPipeline(
|
|
187
218
|
pi: ExtensionAPI,
|
|
188
219
|
runtime: Runtime,
|
|
@@ -192,41 +223,177 @@ export async function runConsolidationPipeline(
|
|
|
192
223
|
const resolveModel = makeModelResolver(runtime, ctx);
|
|
193
224
|
const contextGeneration = runtime.getContextGeneration();
|
|
194
225
|
|
|
226
|
+
const beforeFold = foldLedger(ctx.sessionManager.getBranch() as Entry[]);
|
|
195
227
|
runtime.consolidationPhase = "observer";
|
|
228
|
+
runtime.lastObserverStartedAt = Date.now();
|
|
196
229
|
try {
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
230
|
+
// A large backlog is drained in bounded, oldest-first chunks. The normal
|
|
231
|
+
// trigger threshold controls when the batch stops; a static compaction
|
|
232
|
+
// snapshot is intentionally processed only once. Coverage must advance on
|
|
233
|
+
// every iteration, otherwise stop rather than spin on a failed chunk.
|
|
234
|
+
while (true) {
|
|
235
|
+
const beforeEntries = ctx.sessionManager.getBranch() as Entry[];
|
|
236
|
+
const beforeCoverage = latestCoverageIndex(beforeEntries, OM_OBSERVATIONS_RECORDED);
|
|
237
|
+
const observerOutcome = await runObserverStage(pi, runtime, ctx, resolveModel, {
|
|
238
|
+
force: options.forceObserver === true,
|
|
239
|
+
entries: options.observerEntries,
|
|
240
|
+
contextGeneration,
|
|
241
|
+
});
|
|
242
|
+
if (observerOutcome === "abort") return;
|
|
243
|
+
if (options.observerEntries) break;
|
|
244
|
+
|
|
245
|
+
const afterEntries = ctx.sessionManager.getBranch() as Entry[];
|
|
246
|
+
const afterCoverage = latestCoverageIndex(afterEntries, OM_OBSERVATIONS_RECORDED);
|
|
247
|
+
const remainingTokens = rawTokensSinceObservationCoverage(afterEntries);
|
|
248
|
+
if (afterCoverage <= beforeCoverage || remainingTokens < runtime.config.observeAfterTokens) break;
|
|
249
|
+
debugLog("observer.backlog_continue", { remainingTokens, afterCoverage });
|
|
250
|
+
}
|
|
203
251
|
} catch (error) {
|
|
204
252
|
debugLog("observer.error", { errorMessage: runtime.recordConsolidationStageError(ctx, "observer", error) });
|
|
205
253
|
return;
|
|
254
|
+
} finally {
|
|
255
|
+
runtime.lastObserverCompletedAt = Date.now();
|
|
206
256
|
}
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
257
|
+
const afterFold = foldLedger(ctx.sessionManager.getBranch() as Entry[]);
|
|
258
|
+
const beforeIds = new Set(beforeFold.observations.map((item) => item.id));
|
|
259
|
+
const added = afterFold.observations.filter((item) => !beforeIds.has(item.id));
|
|
260
|
+
if (added.length > 0 && shouldScheduleSummarizerFromObserver(options)) scheduleSummarizer(pi, runtime, ctx);
|
|
261
|
+
if (contextGeneration === runtime.getContextGeneration()) runtime.notifyMemoryUpdate?.(ctx);
|
|
262
|
+
}
|
|
211
263
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
}
|
|
264
|
+
export function currentMemoryPools(runtime: Runtime, entries: Entry[]) {
|
|
265
|
+
const folded = foldLedger(entries);
|
|
266
|
+
return partitionMemoryPools(
|
|
267
|
+
folded.activeObservations,
|
|
268
|
+
folded.activeSummaries,
|
|
269
|
+
runtime.config.newMemoryPoolMaxTokens,
|
|
270
|
+
);
|
|
271
|
+
}
|
|
221
272
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
273
|
+
export function summarizerTriggerTokens(runtime: Runtime): number {
|
|
274
|
+
return runtime.summarizerNextTriggerTokens ?? runtime.config.oldMemoryPoolTargetTokens;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export function nextSummarizerTriggerTokens(targetTokens: number, postRunOldTokens: number, retriggerTokens: number): number {
|
|
278
|
+
return postRunOldTokens <= targetTokens
|
|
279
|
+
? targetTokens
|
|
280
|
+
: Math.max(targetTokens, postRunOldTokens + retriggerTokens);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** Failed/incomplete launches must remain eligible at the prior threshold. */
|
|
284
|
+
export function summarizerTriggerAfterRun(
|
|
285
|
+
successfullyCompleted: boolean,
|
|
286
|
+
currentTriggerTokens: number | undefined,
|
|
287
|
+
targetTokens: number,
|
|
288
|
+
postRunOldTokens: number,
|
|
289
|
+
retriggerTokens: number,
|
|
290
|
+
): number | undefined {
|
|
291
|
+
return successfullyCompleted
|
|
292
|
+
? nextSummarizerTriggerTokens(targetTokens, postRunOldTokens, retriggerTokens)
|
|
293
|
+
: currentTriggerTokens;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function syncAndScheduleSummarizer(pi: ExtensionAPI, runtime: Runtime, ctx: ConsolidationCtx): void {
|
|
297
|
+
runtime.ensureConfig(ctx.cwd);
|
|
298
|
+
scheduleSummarizer(pi, runtime, ctx);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export function scheduleSummarizer(pi: ExtensionAPI, runtime: Runtime, ctx: ConsolidationCtx): void {
|
|
302
|
+
if (runtime.config.passive || !runtime.config.summarizerEnabled || runtime.summarizerInFlight) return;
|
|
303
|
+
const entries = ctx.sessionManager.getBranch() as Entry[];
|
|
304
|
+
const pools = currentMemoryPools(runtime, entries);
|
|
305
|
+
if (pools.oldTokens <= summarizerTriggerTokens(runtime)) return;
|
|
306
|
+
const generation = runtime.getContextGeneration();
|
|
307
|
+
const runId = `summarizer-${Date.now().toString(36)}-${Math.random().toString(16).slice(2, 8)}`;
|
|
308
|
+
const sessionMetadata = debugSessionMetadata(ctx);
|
|
309
|
+
void runtime.launchSummarizerTask(ctx, async () => withDebugLogContext({
|
|
310
|
+
enabled: runtime.config.debugLog === true,
|
|
311
|
+
cwd: ctx.cwd,
|
|
312
|
+
...sessionMetadata,
|
|
313
|
+
runId,
|
|
314
|
+
}, async () => {
|
|
315
|
+
let stalled = false;
|
|
316
|
+
let successfullyCompleted = false;
|
|
317
|
+
let disposeStallWatchdog = () => {};
|
|
318
|
+
const startedAt = Date.now();
|
|
319
|
+
runtime.lastSummarizerStartedAt = startedAt;
|
|
320
|
+
runtime.lastSummarizerRun = { startedAt, status: "running", messages: [] };
|
|
321
|
+
try {
|
|
322
|
+
const resolved = await runtime.resolveModel({ model: ctx.model, modelRegistry: ctx.modelRegistry, hasUI: ctx.hasUI, ui: ctx.ui });
|
|
323
|
+
if (!resolved.ok) {
|
|
324
|
+
debugLog("summarizer.model_unavailable", { reason: resolved.reason });
|
|
325
|
+
runtime.lastSummarizerRun = { startedAt, status: "failed", messages: [], error: resolved.reason };
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
if (generation !== runtime.getContextGeneration()) return;
|
|
329
|
+
if (shouldNotifyWorker(runtime, ctx)) ctx.ui?.notify("pi-contemplator: summarizer running", "info");
|
|
330
|
+
const watchdog = createSummarizerStallWatchdog(SUMMARIZER_STALL_TIMEOUT_MS, (signal) => {
|
|
331
|
+
stalled = true;
|
|
332
|
+
const reason = signal.reason instanceof Error ? signal.reason.message : "summarizer stalled";
|
|
333
|
+
debugLog("summarizer.stalled", { reason });
|
|
334
|
+
if (shouldNotifyWorker(runtime, ctx)) ctx.ui?.notify(`pi-contemplator: ${reason}; cancelling and leaving the backlog eligible for retry`, "warning");
|
|
335
|
+
});
|
|
336
|
+
disposeStallWatchdog = watchdog.dispose;
|
|
337
|
+
const result = await runSummarizer({
|
|
338
|
+
signal: watchdog.signal,
|
|
339
|
+
model: resolved.model as any,
|
|
340
|
+
apiKey: resolved.apiKey,
|
|
341
|
+
headers: resolved.headers,
|
|
342
|
+
getBranch: () => ctx.sessionManager.getBranch() as Entry[],
|
|
343
|
+
targetTokens: runtime.config.oldMemoryPoolTargetTokens,
|
|
344
|
+
newPoolMaxTokens: runtime.config.newMemoryPoolMaxTokens,
|
|
345
|
+
samplingThresholdTokens: runtime.config.summarizerSamplingThresholdTokens,
|
|
346
|
+
maxTurns: runtime.config.agentMaxTurns,
|
|
347
|
+
thinkingLevel: runtime.config.model?.thinking ?? "minimal",
|
|
348
|
+
recordUsage: (usage) => runtime.recordAgentUsage(usage),
|
|
349
|
+
onMessages: (messages) => {
|
|
350
|
+
watchdog.progress();
|
|
351
|
+
if (generation === runtime.getContextGeneration()) runtime.lastSummarizerRun = { startedAt, status: "running", messages: messages.slice() };
|
|
352
|
+
},
|
|
353
|
+
});
|
|
354
|
+
if (generation !== runtime.getContextGeneration()) return;
|
|
355
|
+
if (!result.completed) {
|
|
356
|
+
runtime.lastSummarizerRun = stalled
|
|
357
|
+
? { ...runtime.lastSummarizerRun!, status: "failed", error: "Summarizer stalled and was cancelled; memory remains eligible for retry." }
|
|
358
|
+
: { ...runtime.lastSummarizerRun!, status: "incomplete" };
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
if (result.commit) {
|
|
362
|
+
pi.appendEntry(OM_SUMMARIZER_COMMIT, result.commit);
|
|
363
|
+
successfullyCompleted = true;
|
|
364
|
+
const summary = `${result.commit.summaries.length} summaries consumed ${result.commit.metrics.consumedMemoryCount} memories, reducing visible memory by ~${result.commit.metrics.estimatedTokenReduction.toLocaleString()} tokens.`;
|
|
365
|
+
runtime.lastSummarizerRun = { ...runtime.lastSummarizerRun!, status: "completed", summary };
|
|
366
|
+
debugLog("summarizer.appended", { summaries: result.commit.summaries.length, consumed: result.commit.metrics.consumedMemoryCount, sampled: result.sample?.sampled ?? false });
|
|
367
|
+
if (shouldNotifyWorker(runtime, ctx)) ctx.ui?.notify(`pi-contemplator: summarizer completed — ${summary}`, "info");
|
|
368
|
+
runtime.notifyMemoryUpdate(ctx);
|
|
369
|
+
} else {
|
|
370
|
+
successfullyCompleted = true;
|
|
371
|
+
runtime.lastSummarizerRun = { ...runtime.lastSummarizerRun!, status: "completed", summary: "No safe summaries were created." };
|
|
372
|
+
if (shouldNotifyWorker(runtime, ctx)) ctx.ui?.notify("pi-contemplator: summarizer completed — no safe summaries", "info");
|
|
373
|
+
}
|
|
374
|
+
} catch (error) {
|
|
375
|
+
if (generation === runtime.getContextGeneration() && runtime.lastSummarizerRun) {
|
|
376
|
+
runtime.lastSummarizerRun = { ...runtime.lastSummarizerRun, status: "failed", error: error instanceof Error ? error.message : String(error) };
|
|
377
|
+
}
|
|
378
|
+
throw error;
|
|
379
|
+
} finally {
|
|
380
|
+
disposeStallWatchdog();
|
|
381
|
+
if (generation === runtime.getContextGeneration()) {
|
|
382
|
+
const completedAt = Date.now();
|
|
383
|
+
runtime.lastSummarizerCompletedAt = completedAt;
|
|
384
|
+
if (runtime.lastSummarizerRun) runtime.lastSummarizerRun = { ...runtime.lastSummarizerRun, completedAt };
|
|
385
|
+
const postRunPools = currentMemoryPools(runtime, ctx.sessionManager.getBranch() as Entry[]);
|
|
386
|
+
const target = runtime.config.oldMemoryPoolTargetTokens;
|
|
387
|
+
runtime.summarizerNextTriggerTokens = summarizerTriggerAfterRun(
|
|
388
|
+
successfullyCompleted,
|
|
389
|
+
runtime.summarizerNextTriggerTokens,
|
|
390
|
+
target,
|
|
391
|
+
postRunPools.oldTokens,
|
|
392
|
+
runtime.config.summarizerRetriggerTokens,
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}));
|
|
230
397
|
}
|
|
231
398
|
|
|
232
399
|
async function runObserverStage(
|
|
@@ -280,11 +447,11 @@ async function runObserverStage(
|
|
|
280
447
|
}
|
|
281
448
|
|
|
282
449
|
const memory = fullProjection(entries);
|
|
283
|
-
const
|
|
450
|
+
const priorSummaries = memory.summaries.map(summaryToSummaryLine);
|
|
284
451
|
const priorObservations = memory.observations.map(observationToSummaryLine);
|
|
285
452
|
|
|
286
453
|
if (shouldNotifyWorker(runtime, ctx)) ctx.ui?.notify(
|
|
287
|
-
`
|
|
454
|
+
`pi-contemplator: observer running on ~${chunkTokens.toLocaleString()}-token chunk`,
|
|
288
455
|
"info",
|
|
289
456
|
);
|
|
290
457
|
debugLog("observer.start", {
|
|
@@ -293,7 +460,7 @@ async function runObserverStage(
|
|
|
293
460
|
coversUpToId,
|
|
294
461
|
sourceEntryIds,
|
|
295
462
|
sourceEntryCount: sourceEntryIds.length,
|
|
296
|
-
|
|
463
|
+
priorSummaries: priorSummaries.length,
|
|
297
464
|
priorObservations: priorObservations.length,
|
|
298
465
|
});
|
|
299
466
|
|
|
@@ -301,7 +468,7 @@ async function runObserverStage(
|
|
|
301
468
|
model: resolved.model as any,
|
|
302
469
|
apiKey: resolved.apiKey,
|
|
303
470
|
headers: resolved.headers,
|
|
304
|
-
|
|
471
|
+
priorSummaries,
|
|
305
472
|
priorObservations,
|
|
306
473
|
chunk,
|
|
307
474
|
allowedSourceEntryIds: sourceEntryIds,
|
|
@@ -313,15 +480,6 @@ async function runObserverStage(
|
|
|
313
480
|
debugLog("observer.stale", { reason: "session_or_branch_changed" });
|
|
314
481
|
return "abort";
|
|
315
482
|
}
|
|
316
|
-
if (!observations || observations.length === 0) {
|
|
317
|
-
debugLog("observer.empty", { coversUpToId });
|
|
318
|
-
if (ctx.hasUI) ctx.ui?.notify(
|
|
319
|
-
"Observational memory: observer returned no observations",
|
|
320
|
-
"warning",
|
|
321
|
-
);
|
|
322
|
-
return "continue";
|
|
323
|
-
}
|
|
324
|
-
|
|
325
483
|
const currentEntries = ctx.sessionManager.getBranch() as Entry[];
|
|
326
484
|
let effectiveCoversUpToId = coversUpToId;
|
|
327
485
|
if (!currentEntries.some((entry) => entry.id === coversUpToId)) {
|
|
@@ -336,153 +494,25 @@ async function runObserverStage(
|
|
|
336
494
|
compactionId: compaction?.id,
|
|
337
495
|
});
|
|
338
496
|
}
|
|
339
|
-
const
|
|
497
|
+
const accepted = observations ?? [];
|
|
498
|
+
const data = buildObservationsRecordedData(accepted, effectiveCoversUpToId);
|
|
340
499
|
if (!data) return "continue";
|
|
341
|
-
debugLog("observer.records", {
|
|
342
|
-
count:
|
|
343
|
-
observationTokens:
|
|
500
|
+
debugLog(accepted.length > 0 ? "observer.records" : "observer.coverage_only", {
|
|
501
|
+
count: accepted.length,
|
|
502
|
+
observationTokens: accepted.reduce((sum, observation) => sum + observation.tokenCount, 0),
|
|
344
503
|
coversUpToId: effectiveCoversUpToId,
|
|
345
504
|
});
|
|
505
|
+
// A clean zero-observation verdict is still successful coverage. Persist an
|
|
506
|
+
// empty batch so the next bounded pass starts after this chunk instead of
|
|
507
|
+
// retrying the same low-information source forever. Failures throw above and
|
|
508
|
+
// therefore never reach this coverage commit.
|
|
346
509
|
appendEntry(pi, OM_OBSERVATIONS_RECORDED, data);
|
|
347
|
-
debugLog("observer.appended", { count:
|
|
348
|
-
if (shouldNotifyWorker(runtime, ctx)) ctx.ui?.notify(
|
|
349
|
-
`Observational memory: ${observations.length} observation${observations.length === 1 ? "" : "s"} recorded`,
|
|
350
|
-
"info",
|
|
351
|
-
);
|
|
352
|
-
return "continue";
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
async function runReflectorStage(
|
|
356
|
-
pi: ExtensionAPI,
|
|
357
|
-
runtime: Runtime,
|
|
358
|
-
ctx: ConsolidationCtx,
|
|
359
|
-
resolveModel: (stage: "reflector") => Promise<ResolvedModel | undefined>,
|
|
360
|
-
contextGeneration: number,
|
|
361
|
-
): Promise<ReflectorStageResult> {
|
|
362
|
-
const entries = ctx.sessionManager.getBranch() as Entry[];
|
|
363
|
-
const reflectionTokens = rawTokensSinceReflectionCoverage(entries);
|
|
364
|
-
if (reflectionTokens < runtime.config.reflectAfterTokens) return { outcome: "continue", sameRunReflections: [] };
|
|
365
|
-
|
|
366
|
-
const observationCoverageId = latestCoverageMarkerId(entries, OM_OBSERVATIONS_RECORDED);
|
|
367
|
-
if (!observationCoverageId) return { outcome: "continue", sameRunReflections: [] };
|
|
368
|
-
|
|
510
|
+
debugLog("observer.appended", { count: accepted.length, coversUpToId: effectiveCoversUpToId });
|
|
369
511
|
if (shouldNotifyWorker(runtime, ctx)) ctx.ui?.notify(
|
|
370
|
-
|
|
512
|
+
accepted.length > 0
|
|
513
|
+
? `pi-contemplator: ${accepted.length} observation${accepted.length === 1 ? "" : "s"} recorded`
|
|
514
|
+
: "pi-contemplator: observer found no new information; processed chunk marked covered",
|
|
371
515
|
"info",
|
|
372
516
|
);
|
|
373
|
-
const resolved = await resolveModel("reflector");
|
|
374
|
-
if (!resolved) return { outcome: "abort", sameRunReflections: [] };
|
|
375
|
-
if (contextGeneration !== runtime.getContextGeneration()) {
|
|
376
|
-
debugLog("reflector.stale", { reason: "session_or_branch_changed" });
|
|
377
|
-
return { outcome: "abort", sameRunReflections: [] };
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
const folded = foldLedger(entries);
|
|
381
|
-
const reflections = await runReflector({
|
|
382
|
-
model: resolved.model as any,
|
|
383
|
-
apiKey: resolved.apiKey,
|
|
384
|
-
headers: resolved.headers,
|
|
385
|
-
reflections: folded.reflections,
|
|
386
|
-
observations: folded.activeObservations,
|
|
387
|
-
maxTurns: runtime.config.agentMaxTurns,
|
|
388
|
-
thinkingLevel: runtime.config.model?.thinking ?? "low",
|
|
389
|
-
recordUsage: (usage) => runtime.recordAgentUsage(usage),
|
|
390
|
-
});
|
|
391
|
-
if (contextGeneration !== runtime.getContextGeneration()) {
|
|
392
|
-
debugLog("reflector.stale", { reason: "session_or_branch_changed" });
|
|
393
|
-
return { outcome: "abort", sameRunReflections: [] };
|
|
394
|
-
}
|
|
395
|
-
if (!reflections) return { outcome: "continue", sameRunReflections: [] };
|
|
396
|
-
|
|
397
|
-
const data = buildReflectionsRecordedData(reflections, observationCoverageId);
|
|
398
|
-
if (!data) return { outcome: "continue", sameRunReflections: [] };
|
|
399
|
-
appendEntry(pi, OM_REFLECTIONS_RECORDED, data);
|
|
400
|
-
return {
|
|
401
|
-
outcome: "continue",
|
|
402
|
-
sameRunReflections: reflections,
|
|
403
|
-
effectiveReflectionCoverageId: data.coversUpToId,
|
|
404
|
-
};
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
async function runDropperStage(
|
|
408
|
-
pi: ExtensionAPI,
|
|
409
|
-
runtime: Runtime,
|
|
410
|
-
ctx: ConsolidationCtx,
|
|
411
|
-
resolveModel: (stage: "dropper") => Promise<ResolvedModel | undefined>,
|
|
412
|
-
sameRunReflections: Reflection[],
|
|
413
|
-
sameRunReflectionCoverageId: string | undefined,
|
|
414
|
-
contextGeneration: number,
|
|
415
|
-
): Promise<StageOutcome> {
|
|
416
|
-
if (!sameRunReflectionCoverageId || sameRunReflections.length === 0) {
|
|
417
|
-
debugLog("dropper.waiting_for_reflection", { sameRunReflections: sameRunReflections.length });
|
|
418
|
-
return "continue";
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
const entries = ctx.sessionManager.getBranch() as Entry[];
|
|
422
|
-
const observationCoverageId = latestCoverageMarkerId(entries, OM_OBSERVATIONS_RECORDED);
|
|
423
|
-
if (!observationCoverageId) return "continue";
|
|
424
|
-
|
|
425
|
-
const folded = foldLedger(entries);
|
|
426
|
-
const metrics = observationPoolMetrics(folded.activeObservations, runtime.config.observationsPoolTargetTokens);
|
|
427
|
-
if (!metrics.ready) {
|
|
428
|
-
debugLog("dropper.not_ready", {
|
|
429
|
-
observationTokens: metrics.observationTokens,
|
|
430
|
-
targetTokens: metrics.targetTokens,
|
|
431
|
-
tokensOverTarget: metrics.tokensOverTarget,
|
|
432
|
-
fullness: metrics.fullness,
|
|
433
|
-
activeObservationCount: metrics.activeObservationCount,
|
|
434
|
-
droppableCount: metrics.droppableCount,
|
|
435
|
-
maxDropsAllowed: metrics.maxDropsAllowed,
|
|
436
|
-
});
|
|
437
|
-
return "continue";
|
|
438
|
-
}
|
|
439
|
-
debugLog("dropper.stage_start", {
|
|
440
|
-
observationCoverageId,
|
|
441
|
-
sameRunReflectionCoverageId,
|
|
442
|
-
sameRunReflectionCount: sameRunReflections.length,
|
|
443
|
-
activeObservationCount: metrics.activeObservationCount,
|
|
444
|
-
observationTokens: metrics.observationTokens,
|
|
445
|
-
targetTokens: metrics.targetTokens,
|
|
446
|
-
tokensOverTarget: metrics.tokensOverTarget,
|
|
447
|
-
fullness: metrics.fullness,
|
|
448
|
-
maxDropsAllowed: metrics.maxDropsAllowed,
|
|
449
|
-
});
|
|
450
|
-
|
|
451
|
-
if (shouldNotifyWorker(runtime, ctx)) ctx.ui?.notify(
|
|
452
|
-
`Observational memory: dropper running after reflection — active observation pool ~${metrics.observationTokens.toLocaleString()} / ${metrics.targetTokens.toLocaleString()} target tokens (${Math.round(metrics.fullness * 100).toLocaleString()}%)`,
|
|
453
|
-
"info",
|
|
454
|
-
);
|
|
455
|
-
const resolved = await resolveModel("dropper");
|
|
456
|
-
if (!resolved) return "abort";
|
|
457
|
-
if (contextGeneration !== runtime.getContextGeneration()) {
|
|
458
|
-
debugLog("dropper.stale", { reason: "session_or_branch_changed" });
|
|
459
|
-
return "abort";
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
const reflectionsForDropper = mergeReflections(folded.reflections, sameRunReflections);
|
|
463
|
-
const droppedIds = await runDropper({
|
|
464
|
-
model: resolved.model as any,
|
|
465
|
-
apiKey: resolved.apiKey,
|
|
466
|
-
headers: resolved.headers,
|
|
467
|
-
reflections: reflectionsForDropper,
|
|
468
|
-
observations: folded.activeObservations,
|
|
469
|
-
targetTokens: runtime.config.observationsPoolTargetTokens,
|
|
470
|
-
maxTurns: runtime.config.agentMaxTurns,
|
|
471
|
-
thinkingLevel: runtime.config.model?.thinking ?? "low",
|
|
472
|
-
recordUsage: (usage) => runtime.recordAgentUsage(usage),
|
|
473
|
-
});
|
|
474
|
-
if (contextGeneration !== runtime.getContextGeneration()) {
|
|
475
|
-
debugLog("dropper.stale", { reason: "session_or_branch_changed" });
|
|
476
|
-
return "abort";
|
|
477
|
-
}
|
|
478
|
-
const coversUpToId = earlierCoverageMarkerId(entries, observationCoverageId, sameRunReflectionCoverageId);
|
|
479
|
-
const data = coversUpToId && droppedIds ? buildObservationsDroppedData(droppedIds, coversUpToId) : undefined;
|
|
480
|
-
debugLog("dropper.append", {
|
|
481
|
-
droppedIdsCount: droppedIds?.length ?? 0,
|
|
482
|
-
coversUpToId,
|
|
483
|
-
dataBuilt: data !== undefined,
|
|
484
|
-
appended: data !== undefined,
|
|
485
|
-
});
|
|
486
|
-
if (data) appendEntry(pi, OM_OBSERVATIONS_DROPPED, data);
|
|
487
517
|
return "continue";
|
|
488
518
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
const MEMORY_ID_RE = /(?<![0-9A-Za-z])([0-9a-f]{7,16})(?![0-9A-Za-z])/g;
|
|
2
|
+
const DELIMITED_LIST_RES = [/\[([^\]]*)\]/g, /\(([^)]*)\)/g, /\{([^}]*)\}/g];
|
|
3
|
+
const MEMORY_ID_LIST_RE = /^\s*[0-9a-f]{7,16}(?:\s*,\s*[0-9a-f]{7,16})*\s*$/;
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Extract likely memory or entry references in textual order.
|
|
7
|
+
*
|
|
8
|
+
* IDs inside [], (), or {} may be comma-separated. A bare candidate must
|
|
9
|
+
* contain both a digit and a letter so ordinary words made only from a-f and
|
|
10
|
+
* decimal numbers are not mistaken for references. Delimited IDs do not need
|
|
11
|
+
* that mix because the delimiters provide an explicit citation signal.
|
|
12
|
+
*/
|
|
13
|
+
export function memoryReferenceIds(text: string): string[] {
|
|
14
|
+
const delimitedRanges: Array<[number, number]> = [];
|
|
15
|
+
for (const expression of DELIMITED_LIST_RES) {
|
|
16
|
+
for (const match of text.matchAll(expression)) {
|
|
17
|
+
const contents = match[1];
|
|
18
|
+
if (contents !== undefined && MEMORY_ID_LIST_RE.test(contents)) {
|
|
19
|
+
const start = (match.index ?? 0) + 1;
|
|
20
|
+
delimitedRanges.push([start, start + contents.length]);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const ids: string[] = [];
|
|
26
|
+
const seen = new Set<string>();
|
|
27
|
+
for (const match of text.matchAll(MEMORY_ID_RE)) {
|
|
28
|
+
const id = match[1];
|
|
29
|
+
const index = match.index ?? -1;
|
|
30
|
+
const explicitlyDelimited = delimitedRanges.some(([start, end]) => index >= start && index < end);
|
|
31
|
+
const hashLike = /[0-9]/.test(id) && /[a-f]/.test(id);
|
|
32
|
+
if ((!explicitlyDelimited && !hashLike) || seen.has(id)) continue;
|
|
33
|
+
seen.add(id);
|
|
34
|
+
ids.push(id);
|
|
35
|
+
}
|
|
36
|
+
return ids;
|
|
37
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** Agent-loop tool-choice value accepted by each provider family. */
|
|
2
|
+
export function requiredToolChoice(api: string | undefined): "any" | "required" {
|
|
3
|
+
if (api === "anthropic-messages" || api === "google-generative-ai" || api === "google-vertex" || api === "bedrock-converse-stream") return "any";
|
|
4
|
+
return "required";
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/** Apply provider-native required-tool controls at the final API payload layer. */
|
|
8
|
+
export function forceRequiredToolPayload(payload: unknown, api: string | undefined): unknown {
|
|
9
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) return payload;
|
|
10
|
+
const record = payload as Record<string, unknown>;
|
|
11
|
+
if (api === "anthropic-messages") return { ...record, tool_choice: { type: "any" } };
|
|
12
|
+
if (api === "google-generative-ai" || api === "google-vertex") {
|
|
13
|
+
const config = record.config && typeof record.config === "object" && !Array.isArray(record.config) ? record.config as Record<string, unknown> : {};
|
|
14
|
+
const toolConfig = config.toolConfig && typeof config.toolConfig === "object" && !Array.isArray(config.toolConfig) ? config.toolConfig as Record<string, unknown> : {};
|
|
15
|
+
const functionCallingConfig = toolConfig.functionCallingConfig && typeof toolConfig.functionCallingConfig === "object" && !Array.isArray(toolConfig.functionCallingConfig) ? toolConfig.functionCallingConfig as Record<string, unknown> : {};
|
|
16
|
+
return { ...record, config: { ...config, toolConfig: { ...toolConfig, functionCallingConfig: { ...functionCallingConfig, mode: "ANY" } } } };
|
|
17
|
+
}
|
|
18
|
+
if (api === "bedrock-converse-stream") {
|
|
19
|
+
const toolConfig = record.toolConfig && typeof record.toolConfig === "object" && !Array.isArray(record.toolConfig) ? record.toolConfig as Record<string, unknown> : {};
|
|
20
|
+
return { ...record, toolConfig: { ...toolConfig, toolChoice: { any: {} } } };
|
|
21
|
+
}
|
|
22
|
+
if (api === "mistral-conversations") return { ...record, toolChoice: "required" };
|
|
23
|
+
if (api === "pi-messages") {
|
|
24
|
+
const options = record.options && typeof record.options === "object" && !Array.isArray(record.options) ? record.options as Record<string, unknown> : {};
|
|
25
|
+
return { ...record, options: { ...options, toolChoice: "required" } };
|
|
26
|
+
}
|
|
27
|
+
return { ...record, tool_choice: "required" };
|
|
28
|
+
}
|