@matthewfl/pi-contemplator 0.0.9 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -11
- package/package.json +8 -6
- package/src/agents/contemplator/agent.ts +325 -91
- package/src/agents/contemplator/prompts.ts +6 -6
- package/src/agents/observer/agent.ts +14 -6
- package/src/agents/observer/prompts.ts +16 -7
- 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 +73 -69
- 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 +25 -32
- package/src/hooks/compaction-hook.ts +36 -19
- package/src/hooks/compaction-resume.ts +4 -4
- package/src/hooks/compaction-trigger.ts +96 -56
- package/src/hooks/consolidation-trigger.ts +213 -196
- package/src/memory-citations.ts +37 -0
- package/src/required-tool-choice.ts +28 -0
- package/src/runtime.ts +116 -33
- 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 +7 -18
- 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 +102 -75
- 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,7 +223,9 @@ 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
230
|
const observerOutcome = await runObserverStage(pi, runtime, ctx, resolveModel, {
|
|
198
231
|
force: options.forceObserver === true,
|
|
@@ -203,30 +236,149 @@ export async function runConsolidationPipeline(
|
|
|
203
236
|
} catch (error) {
|
|
204
237
|
debugLog("observer.error", { errorMessage: runtime.recordConsolidationStageError(ctx, "observer", error) });
|
|
205
238
|
return;
|
|
239
|
+
} finally {
|
|
240
|
+
runtime.lastObserverCompletedAt = Date.now();
|
|
206
241
|
}
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
242
|
+
const afterFold = foldLedger(ctx.sessionManager.getBranch() as Entry[]);
|
|
243
|
+
const beforeIds = new Set(beforeFold.observations.map((item) => item.id));
|
|
244
|
+
const added = afterFold.observations.filter((item) => !beforeIds.has(item.id));
|
|
245
|
+
if (added.length > 0 && shouldScheduleSummarizerFromObserver(options)) scheduleSummarizer(pi, runtime, ctx);
|
|
246
|
+
if (contextGeneration === runtime.getContextGeneration()) runtime.notifyMemoryUpdate?.(ctx);
|
|
247
|
+
}
|
|
211
248
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
}
|
|
249
|
+
export function currentMemoryPools(runtime: Runtime, entries: Entry[]) {
|
|
250
|
+
const folded = foldLedger(entries);
|
|
251
|
+
return partitionMemoryPools(
|
|
252
|
+
folded.activeObservations,
|
|
253
|
+
folded.activeSummaries,
|
|
254
|
+
runtime.config.newMemoryPoolMaxTokens,
|
|
255
|
+
);
|
|
256
|
+
}
|
|
221
257
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
258
|
+
export function summarizerTriggerTokens(runtime: Runtime): number {
|
|
259
|
+
return runtime.summarizerNextTriggerTokens ?? runtime.config.oldMemoryPoolTargetTokens;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export function nextSummarizerTriggerTokens(targetTokens: number, postRunOldTokens: number, retriggerTokens: number): number {
|
|
263
|
+
return postRunOldTokens <= targetTokens
|
|
264
|
+
? targetTokens
|
|
265
|
+
: Math.max(targetTokens, postRunOldTokens + retriggerTokens);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/** Failed/incomplete launches must remain eligible at the prior threshold. */
|
|
269
|
+
export function summarizerTriggerAfterRun(
|
|
270
|
+
successfullyCompleted: boolean,
|
|
271
|
+
currentTriggerTokens: number | undefined,
|
|
272
|
+
targetTokens: number,
|
|
273
|
+
postRunOldTokens: number,
|
|
274
|
+
retriggerTokens: number,
|
|
275
|
+
): number | undefined {
|
|
276
|
+
return successfullyCompleted
|
|
277
|
+
? nextSummarizerTriggerTokens(targetTokens, postRunOldTokens, retriggerTokens)
|
|
278
|
+
: currentTriggerTokens;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function syncAndScheduleSummarizer(pi: ExtensionAPI, runtime: Runtime, ctx: ConsolidationCtx): void {
|
|
282
|
+
runtime.ensureConfig(ctx.cwd);
|
|
283
|
+
scheduleSummarizer(pi, runtime, ctx);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
export function scheduleSummarizer(pi: ExtensionAPI, runtime: Runtime, ctx: ConsolidationCtx): void {
|
|
287
|
+
if (runtime.config.passive || !runtime.config.summarizerEnabled || runtime.summarizerInFlight) return;
|
|
288
|
+
const entries = ctx.sessionManager.getBranch() as Entry[];
|
|
289
|
+
const pools = currentMemoryPools(runtime, entries);
|
|
290
|
+
if (pools.oldTokens <= summarizerTriggerTokens(runtime)) return;
|
|
291
|
+
const generation = runtime.getContextGeneration();
|
|
292
|
+
const runId = `summarizer-${Date.now().toString(36)}-${Math.random().toString(16).slice(2, 8)}`;
|
|
293
|
+
const sessionMetadata = debugSessionMetadata(ctx);
|
|
294
|
+
void runtime.launchSummarizerTask(ctx, async () => withDebugLogContext({
|
|
295
|
+
enabled: runtime.config.debugLog === true,
|
|
296
|
+
cwd: ctx.cwd,
|
|
297
|
+
...sessionMetadata,
|
|
298
|
+
runId,
|
|
299
|
+
}, async () => {
|
|
300
|
+
let stalled = false;
|
|
301
|
+
let successfullyCompleted = false;
|
|
302
|
+
let disposeStallWatchdog = () => {};
|
|
303
|
+
const startedAt = Date.now();
|
|
304
|
+
runtime.lastSummarizerStartedAt = startedAt;
|
|
305
|
+
runtime.lastSummarizerRun = { startedAt, status: "running", messages: [] };
|
|
306
|
+
try {
|
|
307
|
+
const resolved = await runtime.resolveModel({ model: ctx.model, modelRegistry: ctx.modelRegistry, hasUI: ctx.hasUI, ui: ctx.ui });
|
|
308
|
+
if (!resolved.ok) {
|
|
309
|
+
debugLog("summarizer.model_unavailable", { reason: resolved.reason });
|
|
310
|
+
runtime.lastSummarizerRun = { startedAt, status: "failed", messages: [], error: resolved.reason };
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
if (generation !== runtime.getContextGeneration()) return;
|
|
314
|
+
if (shouldNotifyWorker(runtime, ctx)) ctx.ui?.notify("pi-contemplator: summarizer running", "info");
|
|
315
|
+
const watchdog = createSummarizerStallWatchdog(SUMMARIZER_STALL_TIMEOUT_MS, (signal) => {
|
|
316
|
+
stalled = true;
|
|
317
|
+
const reason = signal.reason instanceof Error ? signal.reason.message : "summarizer stalled";
|
|
318
|
+
debugLog("summarizer.stalled", { reason });
|
|
319
|
+
if (shouldNotifyWorker(runtime, ctx)) ctx.ui?.notify(`pi-contemplator: ${reason}; cancelling and leaving the backlog eligible for retry`, "warning");
|
|
320
|
+
});
|
|
321
|
+
disposeStallWatchdog = watchdog.dispose;
|
|
322
|
+
const result = await runSummarizer({
|
|
323
|
+
signal: watchdog.signal,
|
|
324
|
+
model: resolved.model as any,
|
|
325
|
+
apiKey: resolved.apiKey,
|
|
326
|
+
headers: resolved.headers,
|
|
327
|
+
getBranch: () => ctx.sessionManager.getBranch() as Entry[],
|
|
328
|
+
targetTokens: runtime.config.oldMemoryPoolTargetTokens,
|
|
329
|
+
newPoolMaxTokens: runtime.config.newMemoryPoolMaxTokens,
|
|
330
|
+
samplingThresholdTokens: runtime.config.summarizerSamplingThresholdTokens,
|
|
331
|
+
maxTurns: runtime.config.agentMaxTurns,
|
|
332
|
+
thinkingLevel: runtime.config.model?.thinking ?? "minimal",
|
|
333
|
+
recordUsage: (usage) => runtime.recordAgentUsage(usage),
|
|
334
|
+
onMessages: (messages) => {
|
|
335
|
+
watchdog.progress();
|
|
336
|
+
if (generation === runtime.getContextGeneration()) runtime.lastSummarizerRun = { startedAt, status: "running", messages: messages.slice() };
|
|
337
|
+
},
|
|
338
|
+
});
|
|
339
|
+
if (generation !== runtime.getContextGeneration()) return;
|
|
340
|
+
if (!result.completed) {
|
|
341
|
+
runtime.lastSummarizerRun = stalled
|
|
342
|
+
? { ...runtime.lastSummarizerRun!, status: "failed", error: "Summarizer stalled and was cancelled; memory remains eligible for retry." }
|
|
343
|
+
: { ...runtime.lastSummarizerRun!, status: "incomplete" };
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
if (result.commit) {
|
|
347
|
+
pi.appendEntry(OM_SUMMARIZER_COMMIT, result.commit);
|
|
348
|
+
successfullyCompleted = true;
|
|
349
|
+
const summary = `${result.commit.summaries.length} summaries consumed ${result.commit.metrics.consumedMemoryCount} memories, reducing visible memory by ~${result.commit.metrics.estimatedTokenReduction.toLocaleString()} tokens.`;
|
|
350
|
+
runtime.lastSummarizerRun = { ...runtime.lastSummarizerRun!, status: "completed", summary };
|
|
351
|
+
debugLog("summarizer.appended", { summaries: result.commit.summaries.length, consumed: result.commit.metrics.consumedMemoryCount, sampled: result.sample?.sampled ?? false });
|
|
352
|
+
if (shouldNotifyWorker(runtime, ctx)) ctx.ui?.notify(`pi-contemplator: summarizer completed — ${summary}`, "info");
|
|
353
|
+
runtime.notifyMemoryUpdate(ctx);
|
|
354
|
+
} else {
|
|
355
|
+
successfullyCompleted = true;
|
|
356
|
+
runtime.lastSummarizerRun = { ...runtime.lastSummarizerRun!, status: "completed", summary: "No safe summaries were created." };
|
|
357
|
+
if (shouldNotifyWorker(runtime, ctx)) ctx.ui?.notify("pi-contemplator: summarizer completed — no safe summaries", "info");
|
|
358
|
+
}
|
|
359
|
+
} catch (error) {
|
|
360
|
+
if (generation === runtime.getContextGeneration() && runtime.lastSummarizerRun) {
|
|
361
|
+
runtime.lastSummarizerRun = { ...runtime.lastSummarizerRun, status: "failed", error: error instanceof Error ? error.message : String(error) };
|
|
362
|
+
}
|
|
363
|
+
throw error;
|
|
364
|
+
} finally {
|
|
365
|
+
disposeStallWatchdog();
|
|
366
|
+
if (generation === runtime.getContextGeneration()) {
|
|
367
|
+
const completedAt = Date.now();
|
|
368
|
+
runtime.lastSummarizerCompletedAt = completedAt;
|
|
369
|
+
if (runtime.lastSummarizerRun) runtime.lastSummarizerRun = { ...runtime.lastSummarizerRun, completedAt };
|
|
370
|
+
const postRunPools = currentMemoryPools(runtime, ctx.sessionManager.getBranch() as Entry[]);
|
|
371
|
+
const target = runtime.config.oldMemoryPoolTargetTokens;
|
|
372
|
+
runtime.summarizerNextTriggerTokens = summarizerTriggerAfterRun(
|
|
373
|
+
successfullyCompleted,
|
|
374
|
+
runtime.summarizerNextTriggerTokens,
|
|
375
|
+
target,
|
|
376
|
+
postRunPools.oldTokens,
|
|
377
|
+
runtime.config.summarizerRetriggerTokens,
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}));
|
|
230
382
|
}
|
|
231
383
|
|
|
232
384
|
async function runObserverStage(
|
|
@@ -280,11 +432,11 @@ async function runObserverStage(
|
|
|
280
432
|
}
|
|
281
433
|
|
|
282
434
|
const memory = fullProjection(entries);
|
|
283
|
-
const
|
|
435
|
+
const priorSummaries = memory.summaries.map(summaryToSummaryLine);
|
|
284
436
|
const priorObservations = memory.observations.map(observationToSummaryLine);
|
|
285
437
|
|
|
286
438
|
if (shouldNotifyWorker(runtime, ctx)) ctx.ui?.notify(
|
|
287
|
-
`
|
|
439
|
+
`pi-contemplator: observer running on ~${chunkTokens.toLocaleString()}-token chunk`,
|
|
288
440
|
"info",
|
|
289
441
|
);
|
|
290
442
|
debugLog("observer.start", {
|
|
@@ -293,7 +445,7 @@ async function runObserverStage(
|
|
|
293
445
|
coversUpToId,
|
|
294
446
|
sourceEntryIds,
|
|
295
447
|
sourceEntryCount: sourceEntryIds.length,
|
|
296
|
-
|
|
448
|
+
priorSummaries: priorSummaries.length,
|
|
297
449
|
priorObservations: priorObservations.length,
|
|
298
450
|
});
|
|
299
451
|
|
|
@@ -301,7 +453,7 @@ async function runObserverStage(
|
|
|
301
453
|
model: resolved.model as any,
|
|
302
454
|
apiKey: resolved.apiKey,
|
|
303
455
|
headers: resolved.headers,
|
|
304
|
-
|
|
456
|
+
priorSummaries,
|
|
305
457
|
priorObservations,
|
|
306
458
|
chunk,
|
|
307
459
|
allowedSourceEntryIds: sourceEntryIds,
|
|
@@ -316,7 +468,7 @@ async function runObserverStage(
|
|
|
316
468
|
if (!observations || observations.length === 0) {
|
|
317
469
|
debugLog("observer.empty", { coversUpToId });
|
|
318
470
|
if (ctx.hasUI) ctx.ui?.notify(
|
|
319
|
-
"
|
|
471
|
+
"pi-contemplator: observer returned no observations",
|
|
320
472
|
"warning",
|
|
321
473
|
);
|
|
322
474
|
return "continue";
|
|
@@ -346,143 +498,8 @@ async function runObserverStage(
|
|
|
346
498
|
appendEntry(pi, OM_OBSERVATIONS_RECORDED, data);
|
|
347
499
|
debugLog("observer.appended", { count: observations.length, coversUpToId: effectiveCoversUpToId });
|
|
348
500
|
if (shouldNotifyWorker(runtime, ctx)) ctx.ui?.notify(
|
|
349
|
-
`
|
|
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
|
-
|
|
369
|
-
if (shouldNotifyWorker(runtime, ctx)) ctx.ui?.notify(
|
|
370
|
-
`Observational memory: reflector running (~${reflectionTokens.toLocaleString()} tokens)`,
|
|
371
|
-
"info",
|
|
372
|
-
);
|
|
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()}%)`,
|
|
501
|
+
`pi-contemplator: ${observations.length} observation${observations.length === 1 ? "" : "s"} recorded`,
|
|
453
502
|
"info",
|
|
454
503
|
);
|
|
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
504
|
return "continue";
|
|
488
505
|
}
|
|
@@ -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
|
+
}
|