@matthewfl/pi-contemplator 0.0.7 → 0.0.9
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 +1 -1
- package/src/agents/contemplator/agent.ts +36 -15
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@matthewfl/pi-contemplator",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.9",
|
|
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,24 +662,31 @@ 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
|
+
// IMPORTANT: omit triggerTurn entirely. Pi 0.84+ interprets an explicit
|
|
674
|
+
// triggerTurn:false as "do not queue while streaming" and inserts directly
|
|
675
|
+
// into agent.state, outside the active run's context snapshot. Omitting it
|
|
676
|
+
// still does not start a turn while idle, but allows steer to work in-run.
|
|
677
|
+
if (this.agentActiveSince !== undefined) this.queuedProbeIds.add(probeId);
|
|
653
678
|
this.pi.sendMessage({
|
|
654
679
|
customType: CONTEMPLATOR_SUGGESTION,
|
|
655
680
|
content: `Background contemplator probe (advisory):\n${question}`,
|
|
656
681
|
display: this.runtime.config.showContemplatorMessages,
|
|
657
682
|
details: { version: 1, question, source, probeId },
|
|
658
|
-
}, { deliverAs: "steer"
|
|
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);
|
|
683
|
+
}, { deliverAs: "steer" });
|
|
665
684
|
debugLog("contemplator.suggestion_queued", {
|
|
666
685
|
probeId,
|
|
667
686
|
suggestionLength: question.length,
|
|
668
687
|
delivery: "pi.sendMessage",
|
|
669
688
|
deliverAs: "steer",
|
|
670
|
-
triggerTurn:
|
|
689
|
+
triggerTurn: "omitted",
|
|
671
690
|
source,
|
|
672
691
|
});
|
|
673
692
|
}
|
|
@@ -735,7 +754,9 @@ export class Contemplator {
|
|
|
735
754
|
debugLog(result.outcome === "proposal" ? "reviewer.proposal_created" : "reviewer.no_proposal", { reviewRequestId: request.id, reviewMemoryId: result.id, scope: result.scope });
|
|
736
755
|
if (result.outcome === "proposal") {
|
|
737
756
|
const notice = `BACKGROUND ${result.scope.toUpperCase()} REVIEW PROPOSAL [${result.id}]\n\n${result.summary}\n\nRecall memory [${result.id}] to read the full conceptual proposal when it is relevant.\n\nThis is advisory. Evaluate it against the actual environment and current work.`;
|
|
738
|
-
|
|
757
|
+
// As with probes, triggerTurn must be omitted or Pi 0.84+ bypasses
|
|
758
|
+
// the streaming steer queue and the active run never sees this message.
|
|
759
|
+
this.pi.sendMessage({ customType: REVIEW_PROPOSAL_MESSAGE, content: notice, display: this.runtime.config.showContemplatorMessages, details: { version: 1, reviewRequestId: request.id, reviewMemoryId: result.id, scope: result.scope } }, { deliverAs: "steer" });
|
|
739
760
|
this.pi.appendEntry(OM_REVIEWER_NOTICE, { version: 1, reviewRequestId: request.id, reviewMemoryId: result.id, scope: result.scope, content: notice });
|
|
740
761
|
this.markTipPersisted(ctx);
|
|
741
762
|
debugLog("reviewer.primary_notice_queued", { reviewRequestId: request.id, reviewMemoryId: result.id });
|