@matthewfl/pi-contemplator 0.0.6 → 0.0.8
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@matthewfl/pi-contemplator",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.8",
|
|
4
4
|
"description": "A Pi extension that keeps long-running agentic sessions on track with background memory, contemplation, and structural review.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -244,11 +244,24 @@ export class Contemplator {
|
|
|
244
244
|
}
|
|
245
245
|
this.persistReviewerStates(ctx);
|
|
246
246
|
});
|
|
247
|
+
this.pi.on("message_end", (event: any) => {
|
|
248
|
+
const message = event?.message;
|
|
249
|
+
if (message?.role !== "custom" || message.customType !== CONTEMPLATOR_SUGGESTION) return;
|
|
250
|
+
if (typeof message.details?.probeId !== "string") return;
|
|
251
|
+
// message_end means Pi has drained the steer into the conversation
|
|
252
|
+
// stream. It is no longer protected by an in-memory queue, so a later
|
|
253
|
+
// tree restore must be allowed to requeue it until context acknowledges it.
|
|
254
|
+
this.queuedProbeIds.delete(message.details.probeId);
|
|
255
|
+
});
|
|
247
256
|
this.pi.on("context", (event: any, ctx: ExtensionContext) => {
|
|
248
257
|
const deliveredMessages = event.messages?.filter((message: any) => message?.role === "custom" && message.customType === CONTEMPLATOR_SUGGESTION && typeof message.details?.probeId === "string") ?? [];
|
|
249
258
|
for (const delivered of deliveredMessages) {
|
|
250
259
|
if (this.deliveredProbeIds.has(delivered.details.probeId)) continue;
|
|
251
260
|
this.deliveredProbeIds.add(delivered.details.probeId);
|
|
261
|
+
// Once Pi includes the probe in a provider context it is no longer in
|
|
262
|
+
// either in-memory delivery queue. Keeping this id indefinitely caused
|
|
263
|
+
// later tree restores to suppress a genuinely needed requeue.
|
|
264
|
+
this.queuedProbeIds.delete(delivered.details.probeId);
|
|
252
265
|
this.pi.appendEntry(CONTEMPLATOR_SUGGESTION, {
|
|
253
266
|
version: 1,
|
|
254
267
|
suggestion: typeof delivered.details.question === "string" ? delivered.details.question : String(delivered.content ?? ""),
|
|
@@ -310,12 +323,7 @@ export class Contemplator {
|
|
|
310
323
|
this.turnsSinceRun = 0;
|
|
311
324
|
}
|
|
312
325
|
const undeliveredSuggestions = new Map<string, string>();
|
|
313
|
-
const queuedProbeIds = new Set<string>();
|
|
314
326
|
for (const entry of entries) {
|
|
315
|
-
if (entry.customType === CONTEMPLATOR_SUGGESTION && entry.type === "custom_message") {
|
|
316
|
-
const details = entry.details as { probeId?: unknown } | undefined;
|
|
317
|
-
if (typeof details?.probeId === "string") queuedProbeIds.add(details.probeId);
|
|
318
|
-
}
|
|
319
327
|
if (entry.customType === CONTEMPLATOR_STATE && entry.data && typeof entry.data === "object") {
|
|
320
328
|
const state = entry.data as { history?: unknown };
|
|
321
329
|
if (Array.isArray(state.history)) this.history = state.history.filter((message): message is AgentMessage => !!message && typeof message === "object");
|
|
@@ -380,7 +388,11 @@ export class Contemplator {
|
|
|
380
388
|
}
|
|
381
389
|
this.restoredTipId = tipId;
|
|
382
390
|
for (const [probeId, question] of undeliveredSuggestions) {
|
|
383
|
-
|
|
391
|
+
// A durable custom_message proves only that Pi inserted the probe at some
|
|
392
|
+
// point; it does not prove an in-memory queue still owns it, and compaction
|
|
393
|
+
// may have removed it from active model context. Suppress requeue only for
|
|
394
|
+
// ids this live extension instance still knows are queued.
|
|
395
|
+
if (skipUndeliveredRestore || this.queuedProbeIds.has(probeId)) continue;
|
|
384
396
|
this.queueProbe(ctx, question, "restore", probeId);
|
|
385
397
|
}
|
|
386
398
|
if (resetTracking) void this.resumePendingReviews(ctx);
|
|
@@ -650,18 +662,22 @@ export class Contemplator {
|
|
|
650
662
|
|
|
651
663
|
private queueProbe(ctx: MemoryUpdateCtx, question: string, source: "send_probe" | "restore", existingProbeId?: string): void {
|
|
652
664
|
const probeId = existingProbeId ?? `${Date.now().toString(36)}-${Math.random().toString(16).slice(2, 8)}`;
|
|
665
|
+
// Persist intent before touching Pi's in-memory queue. A crash in between
|
|
666
|
+
// leaves a recoverable pending probe rather than an invisible lost one.
|
|
667
|
+
this.pi.appendEntry(CONTEMPLATOR_SUGGESTION, { version: 1, suggestion: question, delivered: false, source, probeId });
|
|
668
|
+
this.markTipPersisted(ctx);
|
|
669
|
+
// DELIVERY INVARIANT: probes must always use steer. The contemplator is
|
|
670
|
+
// designed for agents that run for hours; a probe must be injected after
|
|
671
|
+
// the current tool-call batch, before the very next model request. Never
|
|
672
|
+
// change this to nextTurn: that can postpone delivery until a user prompt.
|
|
673
|
+
// triggerTurn stays false so a probe never starts an extra agent turn.
|
|
674
|
+
if (this.agentActiveSince !== undefined) this.queuedProbeIds.add(probeId);
|
|
653
675
|
this.pi.sendMessage({
|
|
654
676
|
customType: CONTEMPLATOR_SUGGESTION,
|
|
655
677
|
content: `Background contemplator probe (advisory):\n${question}`,
|
|
656
678
|
display: this.runtime.config.showContemplatorMessages,
|
|
657
679
|
details: { version: 1, question, source, probeId },
|
|
658
680
|
}, { deliverAs: "steer", triggerTurn: false });
|
|
659
|
-
// sendMessage queues synchronously. Mark every source (not only restore)
|
|
660
|
-
// before a later turn_end can rebuild state from the still-undelivered
|
|
661
|
-
// tracking entry and enqueue this probe again.
|
|
662
|
-
this.queuedProbeIds.add(probeId);
|
|
663
|
-
this.pi.appendEntry(CONTEMPLATOR_SUGGESTION, { version: 1, suggestion: question, delivered: false, source, probeId });
|
|
664
|
-
this.markTipPersisted(ctx);
|
|
665
681
|
debugLog("contemplator.suggestion_queued", {
|
|
666
682
|
probeId,
|
|
667
683
|
suggestionLength: question.length,
|
|
@@ -30,13 +30,22 @@ function isCurrentWatch(runtime: Runtime, generation: number): boolean {
|
|
|
30
30
|
return runtime.compactionResumePending && runtime.compactionResumeGeneration === generation;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
function
|
|
33
|
+
function continuationMessage(afterFailure: boolean, shortContinuationPrompt?: string): string {
|
|
34
|
+
if (shortContinuationPrompt) {
|
|
35
|
+
return afterFailure
|
|
36
|
+
? `Context compaction failed. Continue with these instructions:\n\n${shortContinuationPrompt}`
|
|
37
|
+
: shortContinuationPrompt;
|
|
38
|
+
}
|
|
39
|
+
return afterFailure
|
|
40
|
+
? "Context compaction failed. Continue the current task without waiting for another user message."
|
|
41
|
+
: "Continue the current task from the compacted context without waiting for another user message.";
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function sendResumeMessage(pi: ExtensionAPI, ctx: ResumeCtx, afterFailure: boolean, shortContinuationPrompt?: string): void {
|
|
34
45
|
try {
|
|
35
46
|
pi.sendMessage({
|
|
36
47
|
customType: "om.compaction.resume",
|
|
37
|
-
content: afterFailure
|
|
38
|
-
? "Context compaction failed. Continue the current task without waiting for another user message."
|
|
39
|
-
: "Continue the current task from the compacted context without waiting for another user message.",
|
|
48
|
+
content: continuationMessage(afterFailure, shortContinuationPrompt),
|
|
40
49
|
display: false,
|
|
41
50
|
}, {
|
|
42
51
|
deliverAs: "followUp",
|
|
@@ -54,6 +63,7 @@ function scheduleResumeRetries(
|
|
|
54
63
|
ctx: ResumeCtx,
|
|
55
64
|
generation: number,
|
|
56
65
|
afterFailure: boolean,
|
|
66
|
+
shortContinuationPrompt: string | undefined,
|
|
57
67
|
retryIndex = 0,
|
|
58
68
|
): void {
|
|
59
69
|
if (!isCurrentWatch(runtime, generation)) return;
|
|
@@ -75,8 +85,8 @@ function scheduleResumeRetries(
|
|
|
75
85
|
`Observational memory: continuation did not start; retrying (${retryIndex + 1}/${RESUME_RETRY_DELAYS_MS.length})`,
|
|
76
86
|
"warning",
|
|
77
87
|
);
|
|
78
|
-
sendResumeMessage(pi, ctx, afterFailure);
|
|
79
|
-
scheduleResumeRetries(pi, runtime, ctx, generation, afterFailure, retryIndex + 1);
|
|
88
|
+
sendResumeMessage(pi, ctx, afterFailure, shortContinuationPrompt);
|
|
89
|
+
scheduleResumeRetries(pi, runtime, ctx, generation, afterFailure, shortContinuationPrompt, retryIndex + 1);
|
|
80
90
|
}, RESUME_RETRY_DELAYS_MS[retryIndex]);
|
|
81
91
|
}
|
|
82
92
|
|
|
@@ -86,10 +96,11 @@ export function resumeAfterCompaction(
|
|
|
86
96
|
runtime: Runtime,
|
|
87
97
|
ctx: ResumeCtx,
|
|
88
98
|
afterFailure = false,
|
|
99
|
+
shortContinuationPrompt?: string,
|
|
89
100
|
): void {
|
|
90
101
|
const generation = beginResumeWatch(runtime);
|
|
91
|
-
sendResumeMessage(pi, ctx, afterFailure);
|
|
92
|
-
scheduleResumeRetries(pi, runtime, ctx, generation, afterFailure);
|
|
102
|
+
sendResumeMessage(pi, ctx, afterFailure, shortContinuationPrompt);
|
|
103
|
+
scheduleResumeRetries(pi, runtime, ctx, generation, afterFailure, shortContinuationPrompt);
|
|
93
104
|
}
|
|
94
105
|
|
|
95
106
|
/**
|
|
@@ -111,7 +122,7 @@ export function watchForNativeCompactionResume(
|
|
|
111
122
|
"warning",
|
|
112
123
|
);
|
|
113
124
|
sendResumeMessage(pi, ctx, false);
|
|
114
|
-
scheduleResumeRetries(pi, runtime, ctx, generation, false);
|
|
125
|
+
scheduleResumeRetries(pi, runtime, ctx, generation, false, undefined);
|
|
115
126
|
}, NATIVE_RESUME_GRACE_MS);
|
|
116
127
|
}
|
|
117
128
|
|
|
@@ -17,6 +17,7 @@ export function registerCompactionTrigger(pi: ExtensionAPI, runtime: Runtime): v
|
|
|
17
17
|
if (runtime.compactInFlight) return;
|
|
18
18
|
|
|
19
19
|
const agentRequested = runtime.compactRequested;
|
|
20
|
+
const shortContinuationPrompt = agentRequested ? runtime.compactContinuationPrompt : undefined;
|
|
20
21
|
if (agentRequested) runtime.compactRequested = false;
|
|
21
22
|
else if (runtime.config.passive === true) return;
|
|
22
23
|
|
|
@@ -88,6 +89,7 @@ export function registerCompactionTrigger(pi: ExtensionAPI, runtime: Runtime): v
|
|
|
88
89
|
"info",
|
|
89
90
|
);
|
|
90
91
|
}
|
|
92
|
+
if (agentRequested) runtime.compactContinuationPrompt = undefined;
|
|
91
93
|
ctx.compact({
|
|
92
94
|
onComplete: () => {
|
|
93
95
|
runtime.compactInFlight = false;
|
|
@@ -95,7 +97,7 @@ export function registerCompactionTrigger(pi: ExtensionAPI, runtime: Runtime): v
|
|
|
95
97
|
// Both explicit and proactive OM compactions are manual from Pi's
|
|
96
98
|
// perspective (willRetry=false), so Pi will not continue either one.
|
|
97
99
|
// Always enqueue a hidden continuation after OM finishes compacting.
|
|
98
|
-
resumeAfterCompaction(pi, runtime, { hasUI, ui });
|
|
100
|
+
resumeAfterCompaction(pi, runtime, { hasUI, ui }, false, shortContinuationPrompt);
|
|
99
101
|
},
|
|
100
102
|
onError: (error: { message: string }) => {
|
|
101
103
|
runtime.compactInFlight = false;
|
|
@@ -104,18 +106,19 @@ export function registerCompactionTrigger(pi: ExtensionAPI, runtime: Runtime): v
|
|
|
104
106
|
if (error.message !== "Compaction cancelled" && hasUI) {
|
|
105
107
|
ui?.notify(`Observational memory: ${error.message}`, "error");
|
|
106
108
|
}
|
|
107
|
-
resumeAfterCompaction(pi, runtime, { hasUI, ui }, true);
|
|
109
|
+
resumeAfterCompaction(pi, runtime, { hasUI, ui }, true, shortContinuationPrompt);
|
|
108
110
|
},
|
|
109
111
|
});
|
|
110
112
|
} catch (error) {
|
|
111
113
|
runtime.compactInFlight = false;
|
|
114
|
+
if (agentRequested) runtime.compactContinuationPrompt = undefined;
|
|
112
115
|
runtime.compactOrigin = undefined;
|
|
113
116
|
const msg = error instanceof Error ? error.message : String(error);
|
|
114
117
|
if (hasUI) {
|
|
115
118
|
ui?.setStatus?.(COMPACTION_STATUS_KEY, undefined);
|
|
116
119
|
ui?.notify(`Observational memory: compact threw: ${msg}`, "error");
|
|
117
120
|
}
|
|
118
|
-
resumeAfterCompaction(pi, runtime, { hasUI, ui }, true);
|
|
121
|
+
resumeAfterCompaction(pi, runtime, { hasUI, ui }, true, shortContinuationPrompt);
|
|
119
122
|
}
|
|
120
123
|
}, 0);
|
|
121
124
|
});
|
package/src/runtime.ts
CHANGED
|
@@ -145,6 +145,8 @@ export class Runtime {
|
|
|
145
145
|
consolidationPhase: ConsolidationPhase | undefined;
|
|
146
146
|
compactInFlight = false;
|
|
147
147
|
compactRequested = false;
|
|
148
|
+
/** Agent-authored instructions to deliver after an explicit compact_context request. */
|
|
149
|
+
compactContinuationPrompt: string | undefined;
|
|
148
150
|
compactOrigin: "proactive" | "agent-requested" | undefined;
|
|
149
151
|
compactHookInFlight = false;
|
|
150
152
|
compactionResumePending = false;
|
|
@@ -212,6 +214,7 @@ export class Runtime {
|
|
|
212
214
|
// never-cleared compactInFlight bricking all future compactions).
|
|
213
215
|
this.compactInFlight = false;
|
|
214
216
|
this.compactRequested = false;
|
|
217
|
+
this.compactContinuationPrompt = undefined;
|
|
215
218
|
this.compactOrigin = undefined;
|
|
216
219
|
this.compactionResumePending = false;
|
|
217
220
|
this.compactionResumeGeneration += 1;
|
|
@@ -4,7 +4,7 @@ import type { Runtime } from "../runtime.js";
|
|
|
4
4
|
|
|
5
5
|
export const COMPACT_CONTEXT_TOOL_NAME = "compact_context";
|
|
6
6
|
export const COMPACT_CONTEXT_DESCRIPTION =
|
|
7
|
-
"Force manual compaction. Use sparingly when substantial work remains but the remaining context is insufficient, or when accumulated context has become noisy or stale enough to impair focus and reliable reasoning.";
|
|
7
|
+
"Force manual compaction and resume with agent-authored next-step instructions. Use sparingly when substantial work remains but the remaining context is insufficient, or when accumulated context has become noisy or stale enough to impair focus and reliable reasoning.";
|
|
8
8
|
|
|
9
9
|
export type CompactContextDetails = {
|
|
10
10
|
status: "scheduled" | "already_pending" | "in_progress";
|
|
@@ -20,10 +20,17 @@ export function createCompactContextTool(runtime: Runtime) {
|
|
|
20
20
|
promptGuidelines: [
|
|
21
21
|
"Use compact_context sparingly when either substantial additional work remains and there is not enough context left to complete it, or accumulated past context has become noisy, stale, or distracting enough that you are struggling to focus on the current task or reason about it reliably.",
|
|
22
22
|
"Do not use compact_context routinely, for short tasks, or merely because the conversation is long; use it for genuine context-capacity pressure or context degradation that is interfering with the work.",
|
|
23
|
-
"Call compact_context by itself and stop the current turn; observational memory will compact the context and automatically resume
|
|
23
|
+
"Call compact_context by itself, provide short_continuation_prompt with concrete instructions for the next agent step, and stop the current turn; observational memory will compact the context and automatically resume with those instructions.",
|
|
24
24
|
],
|
|
25
|
-
parameters: Type.Object({
|
|
26
|
-
|
|
25
|
+
parameters: Type.Object({
|
|
26
|
+
short_continuation_prompt: Type.String({
|
|
27
|
+
minLength: 1,
|
|
28
|
+
maxLength: 1_000,
|
|
29
|
+
pattern: "\\S",
|
|
30
|
+
description: "Short, concrete instructions to your post-compaction self describing the next action and any critical immediate constraint. Do not summarize the whole conversation.",
|
|
31
|
+
}),
|
|
32
|
+
}),
|
|
33
|
+
async execute(_toolCallId, params) {
|
|
27
34
|
if (runtime.compactInFlight) {
|
|
28
35
|
return {
|
|
29
36
|
content: [{ type: "text" as const, text: "Context compaction is already in progress. Stop this turn and wait for automatic resume." }],
|
|
@@ -40,6 +47,7 @@ export function createCompactContextTool(runtime: Runtime) {
|
|
|
40
47
|
}
|
|
41
48
|
|
|
42
49
|
runtime.compactRequested = true;
|
|
50
|
+
runtime.compactContinuationPrompt = params.short_continuation_prompt.trim();
|
|
43
51
|
return {
|
|
44
52
|
content: [{ type: "text" as const, text: "Context compaction scheduled. Stop this turn; the task will resume automatically after compaction." }],
|
|
45
53
|
details: { status: "scheduled" } as CompactContextDetails,
|