@matthewfl/pi-contemplator 0.0.10 → 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 +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 +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 +32 -17
- package/src/hooks/compaction-resume.ts +4 -4
- package/src/hooks/compaction-trigger.ts +33 -11
- 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 +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 +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
|
}
|
|
@@ -37,7 +30,7 @@ export function registerCompactionHook(pi: ExtensionAPI, runtime: Runtime): void
|
|
|
37
30
|
ctx.ui.setStatus?.(COMPACTION_STATUS_KEY, `OM compaction: running (${reason}${pending})`);
|
|
38
31
|
if (!initiatedByOm) {
|
|
39
32
|
const continuation = event.willRetry ? "; the interrupted agent run will resume automatically" : "";
|
|
40
|
-
ctx.ui.notify(`
|
|
33
|
+
ctx.ui.notify(`pi-contemplator: compaction started (${reason})${continuation}`, "info");
|
|
41
34
|
}
|
|
42
35
|
}
|
|
43
36
|
event.signal?.addEventListener?.("abort", () => {
|
|
@@ -55,12 +48,8 @@ export function registerCompactionHook(pi: ExtensionAPI, runtime: Runtime): void
|
|
|
55
48
|
launchCompactionObserver(pi, runtime, ctx as ConsolidationCtx, branch);
|
|
56
49
|
}
|
|
57
50
|
const { firstKeptEntryId, tokensBefore } = preparation;
|
|
58
|
-
const projection = buildCompactionProjection(
|
|
59
|
-
|
|
60
|
-
firstKeptEntryId,
|
|
61
|
-
{ observationsPoolMaxTokens: observationsPoolMaxTokens(runtime) },
|
|
62
|
-
);
|
|
63
|
-
const summary = renderSummary(projection.reflections, projection.observations);
|
|
51
|
+
const projection = buildCompactionProjection(branch, firstKeptEntryId);
|
|
52
|
+
const summary = renderSummary(projection.summaries, projection.observations);
|
|
64
53
|
// Compaction removes older custom entries from the active branch. Keep
|
|
65
54
|
// session-scoped overrides in the compaction details so they can be
|
|
66
55
|
// restored after a reload from the surviving branch. Bake the merged
|
|
@@ -96,6 +85,32 @@ export function registerCompactionHook(pi: ExtensionAPI, runtime: Runtime): void
|
|
|
96
85
|
let continuation = "";
|
|
97
86
|
if (event.willRetry) continuation = "; resuming the interrupted agent run";
|
|
98
87
|
else if (omWillResume) continuation = "; resuming the agent run";
|
|
99
|
-
ctx.ui.notify(`
|
|
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
|
+
}
|
|
100
115
|
});
|
|
101
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);
|
|
@@ -18,8 +18,20 @@ type TriggerOptions = {
|
|
|
18
18
|
shortContinuationPrompt?: string;
|
|
19
19
|
};
|
|
20
20
|
|
|
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
|
+
}
|
|
31
|
+
|
|
21
32
|
export function registerCompactionTrigger(pi: ExtensionAPI, runtime: Runtime): void {
|
|
22
33
|
registerCompactionResumeAcknowledgement(pi, runtime);
|
|
34
|
+
let resumeEmptyStopAfterProactiveCompaction = false;
|
|
23
35
|
|
|
24
36
|
const triggerCompaction = (ctx: any, options: TriggerOptions): void => {
|
|
25
37
|
const { origin, resume, threshold, shortContinuationPrompt } = options;
|
|
@@ -35,7 +47,7 @@ export function registerCompactionTrigger(pi: ExtensionAPI, runtime: Runtime): v
|
|
|
35
47
|
runtime.compactOrigin = undefined;
|
|
36
48
|
if (origin === "agent-requested") runtime.compactRequested = true;
|
|
37
49
|
if (hasUI) ui?.notify(
|
|
38
|
-
"
|
|
50
|
+
"pi-contemplator: compaction deferred — agent became busy before compaction",
|
|
39
51
|
"info",
|
|
40
52
|
);
|
|
41
53
|
return;
|
|
@@ -46,7 +58,7 @@ export function registerCompactionTrigger(pi: ExtensionAPI, runtime: Runtime): v
|
|
|
46
58
|
runtime.compactInFlight = false;
|
|
47
59
|
runtime.compactOrigin = undefined;
|
|
48
60
|
if (hasUI) ui?.notify(
|
|
49
|
-
"
|
|
61
|
+
"pi-contemplator: compaction skipped — another compaction already ran before deferred compaction",
|
|
50
62
|
"info",
|
|
51
63
|
);
|
|
52
64
|
return;
|
|
@@ -57,7 +69,7 @@ export function registerCompactionTrigger(pi: ExtensionAPI, runtime: Runtime): v
|
|
|
57
69
|
const reason = origin === "agent-requested" ? "agent-requested, " : origin === "length-stop" ? "length-stop, " : "";
|
|
58
70
|
const continuation = resume ? "; the interrupted agent run will resume automatically" : "";
|
|
59
71
|
ui?.notify(
|
|
60
|
-
`
|
|
72
|
+
`pi-contemplator: compaction started (${reason}~${currentTokens.toLocaleString()} tokens)${continuation}`,
|
|
61
73
|
"info",
|
|
62
74
|
);
|
|
63
75
|
}
|
|
@@ -74,7 +86,7 @@ export function registerCompactionTrigger(pi: ExtensionAPI, runtime: Runtime): v
|
|
|
74
86
|
runtime.compactOrigin = undefined;
|
|
75
87
|
if (hasUI) ui?.setStatus?.(COMPACTION_STATUS_KEY, undefined);
|
|
76
88
|
if (error.message !== "Compaction cancelled" && hasUI) {
|
|
77
|
-
ui?.notify(`
|
|
89
|
+
ui?.notify(`pi-contemplator: ${error.message}`, "error");
|
|
78
90
|
}
|
|
79
91
|
if (resume) resumeAfterCompaction(pi, runtime, { hasUI, ui }, true, shortContinuationPrompt);
|
|
80
92
|
},
|
|
@@ -86,7 +98,7 @@ export function registerCompactionTrigger(pi: ExtensionAPI, runtime: Runtime): v
|
|
|
86
98
|
const msg = error instanceof Error ? error.message : String(error);
|
|
87
99
|
if (hasUI) {
|
|
88
100
|
ui?.setStatus?.(COMPACTION_STATUS_KEY, undefined);
|
|
89
|
-
ui?.notify(`
|
|
101
|
+
ui?.notify(`pi-contemplator: compact threw: ${msg}`, "error");
|
|
90
102
|
}
|
|
91
103
|
if (resume) resumeAfterCompaction(pi, runtime, { hasUI, ui }, true, shortContinuationPrompt);
|
|
92
104
|
}
|
|
@@ -97,6 +109,15 @@ export function registerCompactionTrigger(pi: ExtensionAPI, runtime: Runtime): v
|
|
|
97
109
|
runtime.ensureConfig(ctx.cwd);
|
|
98
110
|
if (runtime.compactInFlight) return;
|
|
99
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
|
+
|
|
100
121
|
const agentRequested = runtime.compactRequested;
|
|
101
122
|
if (agentRequested) {
|
|
102
123
|
const shortContinuationPrompt = runtime.compactContinuationPrompt;
|
|
@@ -108,9 +129,6 @@ export function registerCompactionTrigger(pi: ExtensionAPI, runtime: Runtime): v
|
|
|
108
129
|
|
|
109
130
|
// Pi owns error, abort, and overflow retry policy. OM's session hook still
|
|
110
131
|
// supplies the compaction contents when Pi performs a native retry.
|
|
111
|
-
const lastAssistant = [...event.messages].reverse().find(
|
|
112
|
-
(m): m is Extract<typeof m, { role: "assistant" }> => m.role === "assistant",
|
|
113
|
-
);
|
|
114
132
|
const contextWindow = typeof ctx.model?.contextWindow === "number" ? ctx.model.contextWindow : 0;
|
|
115
133
|
if (
|
|
116
134
|
!lastAssistant
|
|
@@ -129,15 +147,19 @@ export function registerCompactionTrigger(pi: ExtensionAPI, runtime: Runtime): v
|
|
|
129
147
|
});
|
|
130
148
|
|
|
131
149
|
// Proactive threshold compaction is maintenance after Pi has fully settled.
|
|
132
|
-
// It must not manufacture another agent turn
|
|
133
|
-
//
|
|
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.
|
|
134
154
|
pi.on("agent_settled", (_event: any, ctx: any) => {
|
|
155
|
+
const resume = resumeEmptyStopAfterProactiveCompaction;
|
|
156
|
+
resumeEmptyStopAfterProactiveCompaction = false;
|
|
135
157
|
runtime.ensureConfig(ctx.cwd);
|
|
136
158
|
if (runtime.config.passive === true || runtime.compactInFlight || runtime.compactRequested) return;
|
|
137
159
|
const entries = ctx.sessionManager.getBranch() as Entry[];
|
|
138
160
|
const contextWindow = typeof ctx.model?.contextWindow === "number" ? ctx.model.contextWindow : undefined;
|
|
139
161
|
const threshold = resolveCompactAfterTokens(runtime.config, contextWindow);
|
|
140
162
|
if (rawTokensSinceLastCompaction(entries) < threshold) return;
|
|
141
|
-
triggerCompaction(ctx, { origin: "proactive", resume
|
|
163
|
+
triggerCompaction(ctx, { origin: "proactive", resume, threshold });
|
|
142
164
|
});
|
|
143
165
|
}
|