@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
package/src/commands/view.ts
CHANGED
|
@@ -3,11 +3,13 @@ import type { Runtime } from "../runtime.js";
|
|
|
3
3
|
import { copyTextToClipboard } from "../clipboard.js";
|
|
4
4
|
import { renderContemplator, stripAnsi } from "./contemplator-view.js";
|
|
5
5
|
import { renderReviewer } from "./reviewer-view.js";
|
|
6
|
+
import { renderSummarizer } from "./summarizer-view.js";
|
|
6
7
|
import { executeRecall, formatRecallResultForTui } from "../tools/recall-observation.js";
|
|
7
8
|
import {
|
|
9
|
+
chronologicalMemories,
|
|
8
10
|
fullProjection,
|
|
9
11
|
observationToSummaryLine,
|
|
10
|
-
|
|
12
|
+
summaryToSummaryLine,
|
|
11
13
|
visibleProjection,
|
|
12
14
|
type Entry,
|
|
13
15
|
type Projection,
|
|
@@ -44,12 +46,12 @@ function renderContentOnlyProjection(
|
|
|
44
46
|
projection: Projection,
|
|
45
47
|
emptyScope: "visible" | "recorded",
|
|
46
48
|
): string {
|
|
49
|
+
const memories = chronologicalMemories(projection.observations, projection.summaries);
|
|
47
50
|
const lines = [
|
|
48
|
-
"──
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
renderList(projection.observations, observationToSummaryLine, `No ${emptyScope} observations.`),
|
|
51
|
+
"── Memories (chronological) ──",
|
|
52
|
+
memories.length > 0
|
|
53
|
+
? memories.map((item) => item.kind === "observation" ? observationToSummaryLine(item.memory) : summaryToSummaryLine(item.memory)).join("\n")
|
|
54
|
+
: `No ${emptyScope} memories.`,
|
|
53
55
|
];
|
|
54
56
|
if (projection.reviews?.length) lines.push("", "── Advisory reviews ──", ...projection.reviews.map(reviewSummaryLine));
|
|
55
57
|
return lines.join("\n");
|
|
@@ -57,7 +59,7 @@ function renderContentOnlyProjection(
|
|
|
57
59
|
|
|
58
60
|
function hasMemory(projection: Projection): boolean {
|
|
59
61
|
return (
|
|
60
|
-
projection.
|
|
62
|
+
projection.summaries.length > 0 || projection.observations.length > 0 || (projection.reviews?.length ?? 0) > 0
|
|
61
63
|
);
|
|
62
64
|
}
|
|
63
65
|
|
|
@@ -74,7 +76,7 @@ export function registerViewCommand(
|
|
|
74
76
|
|
|
75
77
|
pi.registerCommand("om:view", {
|
|
76
78
|
description:
|
|
77
|
-
"Print and copy
|
|
79
|
+
"Print and copy pi-contemplator memory content (visible, full, memory, contemplator, summarizer, reviewer, or reviews)",
|
|
78
80
|
handler: async (args, ctx) => {
|
|
79
81
|
runtime.ensureConfig(ctx.cwd);
|
|
80
82
|
const entries = ctx.sessionManager.getBranch() as Entry[];
|
|
@@ -105,7 +107,7 @@ export function registerViewCommand(
|
|
|
105
107
|
}
|
|
106
108
|
|
|
107
109
|
if (mode === "contemplator") {
|
|
108
|
-
const output = renderContemplator(entries);
|
|
110
|
+
const output = renderContemplator(entries, runtime.contemplatorState);
|
|
109
111
|
const copied = await copyToClipboard(stripAnsi(output)).catch(() => false);
|
|
110
112
|
ctx.ui.notify(
|
|
111
113
|
`${output}\n\n${copied ? "Copied /om:view contemplator output to clipboard." : "Warning: failed to copy /om:view contemplator output to clipboard."}`,
|
|
@@ -114,6 +116,16 @@ export function registerViewCommand(
|
|
|
114
116
|
return;
|
|
115
117
|
}
|
|
116
118
|
|
|
119
|
+
if (mode === "summarizer") {
|
|
120
|
+
const output = renderSummarizer(runtime.lastSummarizerRun);
|
|
121
|
+
const copied = await copyToClipboard(stripAnsi(output)).catch(() => false);
|
|
122
|
+
ctx.ui.notify(
|
|
123
|
+
`${output}\n\n${copied ? "Copied /om:view summarizer output to clipboard." : "Warning: failed to copy /om:view summarizer output to clipboard."}`,
|
|
124
|
+
"info",
|
|
125
|
+
);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
117
129
|
if (mode === "reviewer") {
|
|
118
130
|
const output = renderReviewer(entries);
|
|
119
131
|
const copied = await copyToClipboard(stripAnsi(output)).catch(() => false);
|
|
@@ -142,7 +154,7 @@ export function registerViewCommand(
|
|
|
142
154
|
}
|
|
143
155
|
|
|
144
156
|
if (mode && mode !== "visible") {
|
|
145
|
-
ctx.ui.notify("Usage: /om:view [visible|full|memory <id>|contemplator|reviewer|reviews]", "info");
|
|
157
|
+
ctx.ui.notify("Usage: /om:view [visible|full|memory <id>|contemplator|summarizer|reviewer|reviews]", "info");
|
|
146
158
|
return;
|
|
147
159
|
}
|
|
148
160
|
|
package/src/config.ts
CHANGED
|
@@ -32,7 +32,6 @@ export type CompactAfterTokensMode = "calibrated" | "ratio";
|
|
|
32
32
|
|
|
33
33
|
export interface Config {
|
|
34
34
|
observeAfterTokens: number;
|
|
35
|
-
reflectAfterTokens: number;
|
|
36
35
|
/**
|
|
37
36
|
* Maximum estimated source tokens serialized into a single observer chunk.
|
|
38
37
|
* Unset (default) derives the cap from the resolved memory model's context
|
|
@@ -42,8 +41,10 @@ export interface Config {
|
|
|
42
41
|
compactAfterTokens: number;
|
|
43
42
|
compactAfterTokensMode: CompactAfterTokensMode;
|
|
44
43
|
compactAfterTokensRatio: number;
|
|
45
|
-
|
|
46
|
-
|
|
44
|
+
/** Token budget for the protected newest-memory suffix; newest record always fits whole. */
|
|
45
|
+
newMemoryPoolMaxTokens: number;
|
|
46
|
+
/** Advisory token target for older summarizer-eligible memory. */
|
|
47
|
+
oldMemoryPoolTargetTokens: number;
|
|
47
48
|
agentMaxTurns: number;
|
|
48
49
|
model?: ConfiguredModel;
|
|
49
50
|
showWorkerNotifications: boolean;
|
|
@@ -59,19 +60,25 @@ export interface Config {
|
|
|
59
60
|
/** Optional model override used only by short-lived structural reviewers. */
|
|
60
61
|
reviewerModel?: ConfiguredModel;
|
|
61
62
|
contemplatorMinNewObservations: number;
|
|
62
|
-
|
|
63
|
+
contemplatorMinNewSummaries: number;
|
|
64
|
+
/** Minimum completed primary-model responses between contemplator runs. */
|
|
63
65
|
contemplatorMinTurns: number;
|
|
66
|
+
/** Stateless loss-aware summarizer for the old memory pool. */
|
|
67
|
+
summarizerEnabled: boolean;
|
|
68
|
+
/** Additional old-pool tokens required before retrying an above-target pool. */
|
|
69
|
+
summarizerRetriggerTokens: number;
|
|
70
|
+
/** Rendered old-memory tokens available before pressure-valve sampling. */
|
|
71
|
+
summarizerSamplingThresholdTokens: number;
|
|
64
72
|
debugLog: boolean;
|
|
65
73
|
}
|
|
66
74
|
|
|
67
75
|
export const DEFAULTS: Config = {
|
|
68
76
|
observeAfterTokens: 10_000,
|
|
69
|
-
reflectAfterTokens: 20_000,
|
|
70
77
|
compactAfterTokens: 81_000,
|
|
71
78
|
compactAfterTokensMode: "calibrated",
|
|
72
79
|
compactAfterTokensRatio: 0.68,
|
|
73
|
-
|
|
74
|
-
|
|
80
|
+
newMemoryPoolMaxTokens: 40_000,
|
|
81
|
+
oldMemoryPoolTargetTokens: 40_000,
|
|
75
82
|
agentMaxTurns: 16,
|
|
76
83
|
showWorkerNotifications: true,
|
|
77
84
|
passive: false,
|
|
@@ -80,8 +87,11 @@ export const DEFAULTS: Config = {
|
|
|
80
87
|
showContemplatorMessages: true,
|
|
81
88
|
reviewerEnabled: true,
|
|
82
89
|
contemplatorMinNewObservations: 8,
|
|
83
|
-
|
|
90
|
+
contemplatorMinNewSummaries: 1,
|
|
84
91
|
contemplatorMinTurns: 10,
|
|
92
|
+
summarizerEnabled: true,
|
|
93
|
+
summarizerRetriggerTokens: 2_000,
|
|
94
|
+
summarizerSamplingThresholdTokens: 60_000,
|
|
85
95
|
debugLog: false,
|
|
86
96
|
};
|
|
87
97
|
|
|
@@ -155,15 +165,6 @@ function positiveIntegerOrUndefined(value: unknown): number | undefined {
|
|
|
155
165
|
return Number.isInteger(value) && typeof value === "number" && value > 0 ? value : undefined;
|
|
156
166
|
}
|
|
157
167
|
|
|
158
|
-
function validTargetOrUndefined(value: unknown, maxTokens: number): number | undefined {
|
|
159
|
-
const target = positiveIntegerOrUndefined(value);
|
|
160
|
-
return target !== undefined && target < maxTokens ? target : undefined;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
function derivedObservationPoolTarget(maxTokens: number): number {
|
|
164
|
-
return Math.floor(maxTokens / 2);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
168
|
function isThinkingLevel(value: unknown): value is ModelThinkingLevel {
|
|
168
169
|
return typeof value === "string" && (THINKING_LEVEL_VALUES as readonly string[]).includes(value);
|
|
169
170
|
}
|
|
@@ -203,15 +204,16 @@ function normalizeSettingsConfig(value: Record<string, unknown>): Partial<Config
|
|
|
203
204
|
const normalized: Partial<Config> = {};
|
|
204
205
|
const numberKeys = [
|
|
205
206
|
"observeAfterTokens",
|
|
206
|
-
"reflectAfterTokens",
|
|
207
207
|
"observerChunkMaxTokens",
|
|
208
208
|
"compactAfterTokens",
|
|
209
|
-
"
|
|
210
|
-
"
|
|
209
|
+
"newMemoryPoolMaxTokens",
|
|
210
|
+
"oldMemoryPoolTargetTokens",
|
|
211
211
|
"agentMaxTurns",
|
|
212
212
|
"contemplatorMinNewObservations",
|
|
213
|
-
"
|
|
213
|
+
"contemplatorMinNewSummaries",
|
|
214
214
|
"contemplatorMinTurns",
|
|
215
|
+
"summarizerRetriggerTokens",
|
|
216
|
+
"summarizerSamplingThresholdTokens",
|
|
215
217
|
] as const;
|
|
216
218
|
for (const key of numberKeys) {
|
|
217
219
|
const normalizedValue = positiveIntegerOrUndefined(value[key]);
|
|
@@ -228,6 +230,7 @@ function normalizeSettingsConfig(value: Record<string, unknown>): Partial<Config
|
|
|
228
230
|
if (typeof value.contemplatorEnabled === "boolean") normalized.contemplatorEnabled = value.contemplatorEnabled;
|
|
229
231
|
if (typeof value.showContemplatorMessages === "boolean") normalized.showContemplatorMessages = value.showContemplatorMessages;
|
|
230
232
|
if (typeof value.reviewerEnabled === "boolean") normalized.reviewerEnabled = value.reviewerEnabled;
|
|
233
|
+
if (typeof value.summarizerEnabled === "boolean") normalized.summarizerEnabled = value.summarizerEnabled;
|
|
231
234
|
if (typeof value.debugLog === "boolean") normalized.debugLog = value.debugLog;
|
|
232
235
|
const model = normalizeModel(value.model);
|
|
233
236
|
if (model) normalized.model = model;
|
|
@@ -269,20 +272,10 @@ export function loadConfig(cwd: string, env: NodeJS.ProcessEnv = process.env): C
|
|
|
269
272
|
const globalConfig = readNamespacedConfig(globalPath);
|
|
270
273
|
const projectConfig = readNamespacedConfig(projectPath);
|
|
271
274
|
const envConfig = readEnvConfig(env);
|
|
272
|
-
|
|
275
|
+
return {
|
|
273
276
|
...DEFAULTS,
|
|
274
|
-
observationsPoolTargetTokens: undefined,
|
|
275
277
|
...globalConfig,
|
|
276
278
|
...projectConfig,
|
|
277
279
|
...envConfig,
|
|
278
280
|
};
|
|
279
|
-
const target = validTargetOrUndefined(
|
|
280
|
-
merged.observationsPoolTargetTokens,
|
|
281
|
-
merged.observationsPoolMaxTokens,
|
|
282
|
-
) ?? derivedObservationPoolTarget(merged.observationsPoolMaxTokens);
|
|
283
|
-
|
|
284
|
-
return {
|
|
285
|
-
...merged,
|
|
286
|
-
observationsPoolTargetTokens: target,
|
|
287
|
-
};
|
|
288
281
|
}
|
|
@@ -1,26 +1,19 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
|
|
3
|
+
import { debugLog } from "../debug-log.js";
|
|
3
4
|
import { computeSessionSettings, type Runtime } from "../runtime.js";
|
|
4
5
|
import { launchCompactionObserver, type ConsolidationCtx } from "./consolidation-trigger.js";
|
|
5
6
|
import { buildCompactionProjection, renderSummary, type Entry } from "../session-ledger/index.js";
|
|
6
7
|
import { watchForNativeCompactionResume } from "./compaction-resume.js";
|
|
7
8
|
|
|
8
|
-
const DEFAULT_OBSERVATIONS_POOL_MAX_TOKENS = 20_000;
|
|
9
9
|
const COMPACTION_STATUS_KEY = "observational-memory-compaction";
|
|
10
10
|
|
|
11
|
-
function observationsPoolMaxTokens(runtime: Runtime): number {
|
|
12
|
-
const value = (runtime.config as { observationsPoolMaxTokens?: unknown }).observationsPoolMaxTokens;
|
|
13
|
-
return typeof value === "number" && Number.isFinite(value) && value > 0
|
|
14
|
-
? value
|
|
15
|
-
: DEFAULT_OBSERVATIONS_POOL_MAX_TOKENS;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
11
|
export function registerCompactionHook(pi: ExtensionAPI, runtime: Runtime): void {
|
|
19
12
|
pi.on("session_before_compact", async (event: any, ctx: any) => {
|
|
20
13
|
if (runtime.compactHookInFlight) {
|
|
21
14
|
if (ctx.hasUI) {
|
|
22
15
|
ctx.ui.notify(
|
|
23
|
-
"
|
|
16
|
+
"pi-contemplator: another compaction is already in progress; cancelling duplicate",
|
|
24
17
|
"warning",
|
|
25
18
|
);
|
|
26
19
|
}
|
|
@@ -29,14 +22,15 @@ export function registerCompactionHook(pi: ExtensionAPI, runtime: Runtime): void
|
|
|
29
22
|
|
|
30
23
|
const initiatedByOm = runtime.compactInFlight && event.reason === "manual";
|
|
31
24
|
const reason = initiatedByOm ? (runtime.compactOrigin ?? "proactive") : event.reason;
|
|
25
|
+
const omWillResume = initiatedByOm && (runtime.compactOrigin === "agent-requested" || runtime.compactOrigin === "length-stop");
|
|
32
26
|
if (ctx.hasUI) {
|
|
33
27
|
let pending = "";
|
|
34
28
|
if (event.willRetry) pending = ", retry pending";
|
|
35
|
-
else if (
|
|
29
|
+
else if (omWillResume) pending = ", resume pending";
|
|
36
30
|
ctx.ui.setStatus?.(COMPACTION_STATUS_KEY, `OM compaction: running (${reason}${pending})`);
|
|
37
31
|
if (!initiatedByOm) {
|
|
38
32
|
const continuation = event.willRetry ? "; the interrupted agent run will resume automatically" : "";
|
|
39
|
-
ctx.ui.notify(`
|
|
33
|
+
ctx.ui.notify(`pi-contemplator: compaction started (${reason})${continuation}`, "info");
|
|
40
34
|
}
|
|
41
35
|
}
|
|
42
36
|
event.signal?.addEventListener?.("abort", () => {
|
|
@@ -54,12 +48,8 @@ export function registerCompactionHook(pi: ExtensionAPI, runtime: Runtime): void
|
|
|
54
48
|
launchCompactionObserver(pi, runtime, ctx as ConsolidationCtx, branch);
|
|
55
49
|
}
|
|
56
50
|
const { firstKeptEntryId, tokensBefore } = preparation;
|
|
57
|
-
const projection = buildCompactionProjection(
|
|
58
|
-
|
|
59
|
-
firstKeptEntryId,
|
|
60
|
-
{ observationsPoolMaxTokens: observationsPoolMaxTokens(runtime) },
|
|
61
|
-
);
|
|
62
|
-
const summary = renderSummary(projection.reflections, projection.observations);
|
|
51
|
+
const projection = buildCompactionProjection(branch, firstKeptEntryId);
|
|
52
|
+
const summary = renderSummary(projection.summaries, projection.observations);
|
|
63
53
|
// Compaction removes older custom entries from the active branch. Keep
|
|
64
54
|
// session-scoped overrides in the compaction details so they can be
|
|
65
55
|
// restored after a reload from the surviving branch. Bake the merged
|
|
@@ -88,12 +78,39 @@ export function registerCompactionHook(pi: ExtensionAPI, runtime: Runtime): void
|
|
|
88
78
|
pi.on("session_compact", (event: any, ctx: any) => {
|
|
89
79
|
const initiatedByOm = runtime.compactInFlight && event.reason === "manual";
|
|
90
80
|
const reason = initiatedByOm ? (runtime.compactOrigin ?? "proactive") : event.reason;
|
|
81
|
+
const omWillResume = initiatedByOm && (runtime.compactOrigin === "agent-requested" || runtime.compactOrigin === "length-stop");
|
|
91
82
|
if (event.willRetry) watchForNativeCompactionResume(pi, runtime, ctx);
|
|
92
83
|
if (!ctx.hasUI) return;
|
|
93
84
|
ctx.ui.setStatus?.(COMPACTION_STATUS_KEY, undefined);
|
|
94
85
|
let continuation = "";
|
|
95
86
|
if (event.willRetry) continuation = "; resuming the interrupted agent run";
|
|
96
|
-
else if (
|
|
97
|
-
ctx.ui.notify(`
|
|
87
|
+
else if (omWillResume) continuation = "; resuming the agent run";
|
|
88
|
+
ctx.ui.notify(`pi-contemplator: compaction complete (${reason})${continuation}`, "info");
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
pi.on("session_compact_failed", (event, ctx) => {
|
|
92
|
+
const initiatedByOm = runtime.compactInFlight && event.reason === "manual";
|
|
93
|
+
const reason = initiatedByOm ? (runtime.compactOrigin ?? "proactive") : event.reason;
|
|
94
|
+
debugLog("compaction.failed", {
|
|
95
|
+
reason,
|
|
96
|
+
piReason: event.reason,
|
|
97
|
+
errorMessage: event.errorMessage,
|
|
98
|
+
aborted: event.aborted,
|
|
99
|
+
willRetry: event.willRetry,
|
|
100
|
+
fromExtension: event.fromExtension,
|
|
101
|
+
initiatedByOm,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// OM-initiated ctx.compact() calls already have an onError callback that
|
|
105
|
+
// clears UI state and applies the origin-specific continuation policy. Do
|
|
106
|
+
// not duplicate that work here; Pi emits this event before invoking it.
|
|
107
|
+
if (initiatedByOm) return;
|
|
108
|
+
if (ctx.hasUI) ctx.ui.setStatus?.(COMPACTION_STATUS_KEY, undefined);
|
|
109
|
+
if (!event.aborted && ctx.hasUI) {
|
|
110
|
+
ctx.ui.notify(
|
|
111
|
+
`pi-contemplator: compaction failed (${reason}): ${event.errorMessage ?? "unknown error"}`,
|
|
112
|
+
"error",
|
|
113
|
+
);
|
|
114
|
+
}
|
|
98
115
|
});
|
|
99
116
|
}
|
|
@@ -53,7 +53,7 @@ function sendResumeMessage(pi: ExtensionAPI, ctx: ResumeCtx, afterFailure: boole
|
|
|
53
53
|
});
|
|
54
54
|
} catch (error) {
|
|
55
55
|
const message = error instanceof Error ? error.message : String(error);
|
|
56
|
-
ctx.ui?.notify?.(`
|
|
56
|
+
ctx.ui?.notify?.(`pi-contemplator: failed to request continuation: ${message}`, "error");
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
59
|
|
|
@@ -72,7 +72,7 @@ function scheduleResumeRetries(
|
|
|
72
72
|
if (!isCurrentWatch(runtime, generation)) return;
|
|
73
73
|
clearResumeWatch(runtime);
|
|
74
74
|
ctx.ui?.notify?.(
|
|
75
|
-
"
|
|
75
|
+
"pi-contemplator: the agent did not acknowledge continuation after compaction",
|
|
76
76
|
"error",
|
|
77
77
|
);
|
|
78
78
|
}, RESUME_RETRY_DELAYS_MS.at(-1));
|
|
@@ -82,7 +82,7 @@ function scheduleResumeRetries(
|
|
|
82
82
|
runtime.compactionResumeTimer = setTimeout(() => {
|
|
83
83
|
if (!isCurrentWatch(runtime, generation)) return;
|
|
84
84
|
ctx.ui?.notify?.(
|
|
85
|
-
`
|
|
85
|
+
`pi-contemplator: continuation did not start; retrying (${retryIndex + 1}/${RESUME_RETRY_DELAYS_MS.length})`,
|
|
86
86
|
"warning",
|
|
87
87
|
);
|
|
88
88
|
sendResumeMessage(pi, ctx, afterFailure, shortContinuationPrompt);
|
|
@@ -118,7 +118,7 @@ export function watchForNativeCompactionResume(
|
|
|
118
118
|
runtime.compactionResumeTimer = setTimeout(() => {
|
|
119
119
|
if (!isCurrentWatch(runtime, generation)) return;
|
|
120
120
|
ctx.ui?.notify?.(
|
|
121
|
-
"
|
|
121
|
+
"pi-contemplator: native compaction did not resume the agent; sending fallback continuation",
|
|
122
122
|
"warning",
|
|
123
123
|
);
|
|
124
124
|
sendResumeMessage(pi, ctx, false);
|
|
@@ -9,52 +9,34 @@ import {
|
|
|
9
9
|
} from "./compaction-resume.js";
|
|
10
10
|
|
|
11
11
|
const COMPACTION_STATUS_KEY = "observational-memory-compaction";
|
|
12
|
+
type CompactionOrigin = "agent-requested" | "length-stop" | "proactive";
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const agentRequested = runtime.compactRequested;
|
|
20
|
-
const shortContinuationPrompt = agentRequested ? runtime.compactContinuationPrompt : undefined;
|
|
21
|
-
if (agentRequested) runtime.compactRequested = false;
|
|
22
|
-
else if (runtime.config.passive === true) return;
|
|
14
|
+
type TriggerOptions = {
|
|
15
|
+
origin: CompactionOrigin;
|
|
16
|
+
resume: boolean;
|
|
17
|
+
threshold?: number;
|
|
18
|
+
shortContinuationPrompt?: string;
|
|
19
|
+
};
|
|
23
20
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
);
|
|
35
|
-
if (
|
|
36
|
-
lastAssistant
|
|
37
|
-
&& (
|
|
38
|
-
lastAssistant.stopReason === "error"
|
|
39
|
-
|| lastAssistant.stopReason === "aborted"
|
|
40
|
-
|| isContextOverflow(lastAssistant, contextWindow)
|
|
41
|
-
)
|
|
42
|
-
) return;
|
|
21
|
+
/** A stop with thinking but no text or tool call did not produce a usable turn. */
|
|
22
|
+
function isEmptyNormalStop(message: any): boolean {
|
|
23
|
+
if (!message || message.role !== "assistant" || message.stopReason !== "stop") return false;
|
|
24
|
+
if (typeof message.content === "string") return message.content.trim().length === 0;
|
|
25
|
+
if (!Array.isArray(message.content)) return true;
|
|
26
|
+
return !message.content.some((part: any) =>
|
|
27
|
+
part?.type === "toolCall"
|
|
28
|
+
|| (part?.type === "text" && typeof part.text === "string" && part.text.trim().length > 0),
|
|
29
|
+
);
|
|
30
|
+
}
|
|
43
31
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
// window when ratio mode is configured. ctx.model is the current session model
|
|
48
|
-
// (Model<any> | undefined per ExtensionContext).
|
|
49
|
-
threshold = resolveCompactAfterTokens(runtime.config, contextWindow > 0 ? contextWindow : undefined);
|
|
50
|
-
if (tokens < threshold) return;
|
|
51
|
-
}
|
|
32
|
+
export function registerCompactionTrigger(pi: ExtensionAPI, runtime: Runtime): void {
|
|
33
|
+
registerCompactionResumeAcknowledgement(pi, runtime);
|
|
34
|
+
let resumeEmptyStopAfterProactiveCompaction = false;
|
|
52
35
|
|
|
53
|
-
|
|
54
|
-
|
|
36
|
+
const triggerCompaction = (ctx: any, options: TriggerOptions): void => {
|
|
37
|
+
const { origin, resume, threshold, shortContinuationPrompt } = options;
|
|
55
38
|
const hasUI = ctx.hasUI;
|
|
56
39
|
const ui = ctx.ui;
|
|
57
|
-
const origin = agentRequested ? "agent-requested" : "proactive";
|
|
58
40
|
|
|
59
41
|
runtime.compactInFlight = true;
|
|
60
42
|
runtime.compactOrigin = origin;
|
|
@@ -63,9 +45,9 @@ export function registerCompactionTrigger(pi: ExtensionAPI, runtime: Runtime): v
|
|
|
63
45
|
if (!ctx.isIdle()) {
|
|
64
46
|
runtime.compactInFlight = false;
|
|
65
47
|
runtime.compactOrigin = undefined;
|
|
66
|
-
if (
|
|
48
|
+
if (origin === "agent-requested") runtime.compactRequested = true;
|
|
67
49
|
if (hasUI) ui?.notify(
|
|
68
|
-
"
|
|
50
|
+
"pi-contemplator: compaction deferred — agent became busy before compaction",
|
|
69
51
|
"info",
|
|
70
52
|
);
|
|
71
53
|
return;
|
|
@@ -76,50 +58,108 @@ export function registerCompactionTrigger(pi: ExtensionAPI, runtime: Runtime): v
|
|
|
76
58
|
runtime.compactInFlight = false;
|
|
77
59
|
runtime.compactOrigin = undefined;
|
|
78
60
|
if (hasUI) ui?.notify(
|
|
79
|
-
"
|
|
61
|
+
"pi-contemplator: compaction skipped — another compaction already ran before deferred compaction",
|
|
80
62
|
"info",
|
|
81
63
|
);
|
|
82
64
|
return;
|
|
83
65
|
}
|
|
84
66
|
if (hasUI) {
|
|
85
|
-
|
|
86
|
-
|
|
67
|
+
const pending = resume ? ", resume pending" : "";
|
|
68
|
+
ui?.setStatus?.(COMPACTION_STATUS_KEY, `OM compaction: running (${origin}${pending})`);
|
|
69
|
+
const reason = origin === "agent-requested" ? "agent-requested, " : origin === "length-stop" ? "length-stop, " : "";
|
|
70
|
+
const continuation = resume ? "; the interrupted agent run will resume automatically" : "";
|
|
87
71
|
ui?.notify(
|
|
88
|
-
`
|
|
72
|
+
`pi-contemplator: compaction started (${reason}~${currentTokens.toLocaleString()} tokens)${continuation}`,
|
|
89
73
|
"info",
|
|
90
74
|
);
|
|
91
75
|
}
|
|
92
|
-
if (
|
|
76
|
+
if (origin === "agent-requested") runtime.compactContinuationPrompt = undefined;
|
|
93
77
|
ctx.compact({
|
|
94
78
|
onComplete: () => {
|
|
95
79
|
runtime.compactInFlight = false;
|
|
96
80
|
runtime.compactOrigin = undefined;
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
// Always enqueue a hidden continuation after OM finishes compacting.
|
|
100
|
-
resumeAfterCompaction(pi, runtime, { hasUI, ui }, false, shortContinuationPrompt);
|
|
81
|
+
if (hasUI && !resume) ui?.setStatus?.(COMPACTION_STATUS_KEY, undefined);
|
|
82
|
+
if (resume) resumeAfterCompaction(pi, runtime, { hasUI, ui }, false, shortContinuationPrompt);
|
|
101
83
|
},
|
|
102
84
|
onError: (error: { message: string }) => {
|
|
103
85
|
runtime.compactInFlight = false;
|
|
104
86
|
runtime.compactOrigin = undefined;
|
|
105
87
|
if (hasUI) ui?.setStatus?.(COMPACTION_STATUS_KEY, undefined);
|
|
106
88
|
if (error.message !== "Compaction cancelled" && hasUI) {
|
|
107
|
-
ui?.notify(`
|
|
89
|
+
ui?.notify(`pi-contemplator: ${error.message}`, "error");
|
|
108
90
|
}
|
|
109
|
-
resumeAfterCompaction(pi, runtime, { hasUI, ui }, true, shortContinuationPrompt);
|
|
91
|
+
if (resume) resumeAfterCompaction(pi, runtime, { hasUI, ui }, true, shortContinuationPrompt);
|
|
110
92
|
},
|
|
111
93
|
});
|
|
112
94
|
} catch (error) {
|
|
113
95
|
runtime.compactInFlight = false;
|
|
114
|
-
if (
|
|
96
|
+
if (origin === "agent-requested") runtime.compactContinuationPrompt = undefined;
|
|
115
97
|
runtime.compactOrigin = undefined;
|
|
116
98
|
const msg = error instanceof Error ? error.message : String(error);
|
|
117
99
|
if (hasUI) {
|
|
118
100
|
ui?.setStatus?.(COMPACTION_STATUS_KEY, undefined);
|
|
119
|
-
ui?.notify(`
|
|
101
|
+
ui?.notify(`pi-contemplator: compact threw: ${msg}`, "error");
|
|
120
102
|
}
|
|
121
|
-
resumeAfterCompaction(pi, runtime, { hasUI, ui }, true, shortContinuationPrompt);
|
|
103
|
+
if (resume) resumeAfterCompaction(pi, runtime, { hasUI, ui }, true, shortContinuationPrompt);
|
|
122
104
|
}
|
|
123
105
|
}, 0);
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
pi.on("agent_end", (event: any, ctx: any) => {
|
|
109
|
+
runtime.ensureConfig(ctx.cwd);
|
|
110
|
+
if (runtime.compactInFlight) return;
|
|
111
|
+
|
|
112
|
+
const lastAssistant = [...event.messages].reverse().find(
|
|
113
|
+
(m): m is Extract<typeof m, { role: "assistant" }> => m.role === "assistant",
|
|
114
|
+
);
|
|
115
|
+
// Some providers occasionally return stop after spending output tokens but
|
|
116
|
+
// emit no text or tool call. Pi regards that as settled, yet it plainly is
|
|
117
|
+
// not a completed autonomous turn. Remember this only until agent_settled so
|
|
118
|
+
// threshold compaction can continue it; never resume an ordinary text stop.
|
|
119
|
+
resumeEmptyStopAfterProactiveCompaction = isEmptyNormalStop(lastAssistant);
|
|
120
|
+
|
|
121
|
+
const agentRequested = runtime.compactRequested;
|
|
122
|
+
if (agentRequested) {
|
|
123
|
+
const shortContinuationPrompt = runtime.compactContinuationPrompt;
|
|
124
|
+
runtime.compactRequested = false;
|
|
125
|
+
triggerCompaction(ctx, { origin: "agent-requested", resume: true, shortContinuationPrompt });
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
if (runtime.config.passive === true) return;
|
|
129
|
+
|
|
130
|
+
// Pi owns error, abort, and overflow retry policy. OM's session hook still
|
|
131
|
+
// supplies the compaction contents when Pi performs a native retry.
|
|
132
|
+
const contextWindow = typeof ctx.model?.contextWindow === "number" ? ctx.model.contextWindow : 0;
|
|
133
|
+
if (
|
|
134
|
+
!lastAssistant
|
|
135
|
+
|| lastAssistant.stopReason === "error"
|
|
136
|
+
|| lastAssistant.stopReason === "aborted"
|
|
137
|
+
|| isContextOverflow(lastAssistant, contextWindow)
|
|
138
|
+
) return;
|
|
139
|
+
|
|
140
|
+
// A non-overflow length stop is interrupted work, not a normal completed
|
|
141
|
+
// turn. Preserve the older compact-and-resume behavior only for this case.
|
|
142
|
+
if (lastAssistant.stopReason !== "length") return;
|
|
143
|
+
const entries = ctx.sessionManager.getBranch() as Entry[];
|
|
144
|
+
const threshold = resolveCompactAfterTokens(runtime.config, contextWindow > 0 ? contextWindow : undefined);
|
|
145
|
+
if (rawTokensSinceLastCompaction(entries) < threshold) return;
|
|
146
|
+
triggerCompaction(ctx, { origin: "length-stop", resume: true, threshold });
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// Proactive threshold compaction is maintenance after Pi has fully settled.
|
|
150
|
+
// It must not manufacture another agent turn after an ordinary completed
|
|
151
|
+
// response. The narrow exception is a provider's empty normal stop: there was
|
|
152
|
+
// no usable response, so compaction must preserve the autonomous run rather
|
|
153
|
+
// than making that provider failure look like successful completion.
|
|
154
|
+
pi.on("agent_settled", (_event: any, ctx: any) => {
|
|
155
|
+
const resume = resumeEmptyStopAfterProactiveCompaction;
|
|
156
|
+
resumeEmptyStopAfterProactiveCompaction = false;
|
|
157
|
+
runtime.ensureConfig(ctx.cwd);
|
|
158
|
+
if (runtime.config.passive === true || runtime.compactInFlight || runtime.compactRequested) return;
|
|
159
|
+
const entries = ctx.sessionManager.getBranch() as Entry[];
|
|
160
|
+
const contextWindow = typeof ctx.model?.contextWindow === "number" ? ctx.model.contextWindow : undefined;
|
|
161
|
+
const threshold = resolveCompactAfterTokens(runtime.config, contextWindow);
|
|
162
|
+
if (rawTokensSinceLastCompaction(entries) < threshold) return;
|
|
163
|
+
triggerCompaction(ctx, { origin: "proactive", resume, threshold });
|
|
124
164
|
});
|
|
125
165
|
}
|