@matthewfl/pi-contemplator 0.0.1 → 0.0.3
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.3",
|
|
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",
|
|
@@ -27,7 +27,16 @@ type Intervention =
|
|
|
27
27
|
| { kind: "probe"; question: string }
|
|
28
28
|
| { kind: "review"; request: Omit<StructuralReviewRequest, "createdAt" | "requestedBy"> };
|
|
29
29
|
|
|
30
|
-
type ReviewerSession = {
|
|
30
|
+
type ReviewerSession = {
|
|
31
|
+
scope: StructuralReviewRequest["scope"];
|
|
32
|
+
history: AgentMessage[];
|
|
33
|
+
/** Latest compact checkpoint; older transcript content stays in referenced append-only entries. */
|
|
34
|
+
checkpointEntryId?: string;
|
|
35
|
+
/** Reviewer message entries appended since checkpointEntryId. */
|
|
36
|
+
messageEntryIds: string[];
|
|
37
|
+
/** Message-entry ids already folded into `history` in this live session (restore re-walk guard). */
|
|
38
|
+
foldedEntryIds: Set<string>;
|
|
39
|
+
};
|
|
31
40
|
|
|
32
41
|
type QueueStructuralReviewOptions = {
|
|
33
42
|
ctx: MemoryUpdateCtx;
|
|
@@ -134,7 +143,8 @@ export class Contemplator {
|
|
|
134
143
|
private resumedReviewIds = new Set<string>();
|
|
135
144
|
private reviewerSessions = new Map<string, ReviewerSession>();
|
|
136
145
|
private deliveredProbeIds = new Set<string>();
|
|
137
|
-
|
|
146
|
+
/** Probe ids passed to pi.sendMessage by this live extension runtime. */
|
|
147
|
+
private queuedProbeIds = new Set<string>();
|
|
138
148
|
private sessionGeneration = 0;
|
|
139
149
|
private latestCtx: MemoryUpdateCtx | undefined;
|
|
140
150
|
private turnsSinceRun = 0;
|
|
@@ -144,12 +154,19 @@ export class Contemplator {
|
|
|
144
154
|
|
|
145
155
|
register(): void {
|
|
146
156
|
this.runtime.setMemoryUpdateListener((ctx) => this.withDebugContext(ctx, () => this.observeTurn(ctx)));
|
|
147
|
-
|
|
157
|
+
this.pi.on("session_start", (event: any, ctx: ExtensionContext) => {
|
|
158
|
+
this.sessionGeneration++;
|
|
159
|
+
// AgentSession preserves its steering queue across extension reloads. An
|
|
160
|
+
// undelivered tracking entry therefore still has a live queued message;
|
|
161
|
+
// restoring it here would enqueue the same probe a second time.
|
|
162
|
+
const reload = event?.reason === "reload";
|
|
163
|
+
this.restore(ctx, true, reload, reload);
|
|
164
|
+
});
|
|
165
|
+
this.pi.on("session_tree", (_event: any, ctx: ExtensionContext) => {
|
|
148
166
|
this.sessionGeneration++;
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
this.pi.on("session_tree", restoreSessionBranch);
|
|
167
|
+
// Pending steering messages remain queued while navigating the tree.
|
|
168
|
+
this.restore(ctx, true, true);
|
|
169
|
+
});
|
|
153
170
|
this.pi.on("session_shutdown", () => {
|
|
154
171
|
this.sessionGeneration++;
|
|
155
172
|
this.history = [];
|
|
@@ -163,7 +180,7 @@ export class Contemplator {
|
|
|
163
180
|
this.resumedReviewIds.clear();
|
|
164
181
|
this.reviewerSessions.clear();
|
|
165
182
|
this.deliveredProbeIds.clear();
|
|
166
|
-
this.
|
|
183
|
+
this.queuedProbeIds.clear();
|
|
167
184
|
this.latestCtx = undefined;
|
|
168
185
|
this.turnsSinceRun = 0;
|
|
169
186
|
this.restoredTipId = undefined;
|
|
@@ -211,7 +228,7 @@ export class Contemplator {
|
|
|
211
228
|
}, fn);
|
|
212
229
|
}
|
|
213
230
|
|
|
214
|
-
private restore(ctx: MemoryUpdateCtx, resetTracking = false): void {
|
|
231
|
+
private restore(ctx: MemoryUpdateCtx, resetTracking = false, retainQueuedIds = false, skipUndeliveredRestore = false): void {
|
|
215
232
|
this.latestCtx = ctx;
|
|
216
233
|
const entries = ctx.sessionManager.getBranch() as Entry[];
|
|
217
234
|
const tipId = entries.at(-1)?.id;
|
|
@@ -220,7 +237,7 @@ export class Contemplator {
|
|
|
220
237
|
this.history = [];
|
|
221
238
|
if (resetTracking) {
|
|
222
239
|
this.deliveredProbeIds.clear();
|
|
223
|
-
this.
|
|
240
|
+
if (!retainQueuedIds) this.queuedProbeIds.clear();
|
|
224
241
|
this.inFlightReviewIds.clear();
|
|
225
242
|
this.resolvingReviewIds.clear();
|
|
226
243
|
this.resumedReviewIds.clear();
|
|
@@ -252,17 +269,42 @@ export class Contemplator {
|
|
|
252
269
|
}
|
|
253
270
|
}
|
|
254
271
|
if (entry.customType === OM_REVIEWER_STATE && entry.data && typeof entry.data === "object") {
|
|
255
|
-
const state = entry.data as { reviewRequestId?: unknown; scope?: unknown; history?: unknown };
|
|
256
|
-
if (typeof state.reviewRequestId === "string" && (state.scope === "workflow" || state.scope === "software")
|
|
257
|
-
|
|
272
|
+
const state = entry.data as { version?: unknown; reviewRequestId?: unknown; scope?: unknown; history?: unknown; messageEntryIds?: unknown };
|
|
273
|
+
if (typeof state.reviewRequestId === "string" && (state.scope === "workflow" || state.scope === "software")) {
|
|
274
|
+
if (state.version === 1 && Array.isArray(state.history)) {
|
|
275
|
+
// Backward compatibility for the old, expensive full-transcript snapshots.
|
|
276
|
+
this.reviewerSessions.set(state.reviewRequestId, {
|
|
277
|
+
scope: state.scope,
|
|
278
|
+
history: state.history.filter((message): message is AgentMessage => !!message && typeof message === "object"),
|
|
279
|
+
checkpointEntryId: entry.id,
|
|
280
|
+
messageEntryIds: [],
|
|
281
|
+
foldedEntryIds: new Set(),
|
|
282
|
+
});
|
|
283
|
+
} else if (state.version === 2 && Array.isArray(state.messageEntryIds) && state.messageEntryIds.every((id) => typeof id === "string")) {
|
|
284
|
+
// V2 checkpoints contain references only. The referenced state/messages
|
|
285
|
+
// have already been folded while walking this append-only branch.
|
|
286
|
+
const session = this.reviewerSessions.get(state.reviewRequestId) ?? { scope: state.scope, history: [], messageEntryIds: [], foldedEntryIds: new Set() };
|
|
287
|
+
session.checkpointEntryId = entry.id;
|
|
288
|
+
session.messageEntryIds = [];
|
|
289
|
+
this.reviewerSessions.set(state.reviewRequestId, session);
|
|
290
|
+
}
|
|
258
291
|
}
|
|
259
292
|
}
|
|
260
293
|
if (entry.customType === OM_REVIEWER_MESSAGE && entry.data && typeof entry.data === "object") {
|
|
261
294
|
const data = entry.data as { reviewRequestId?: unknown; scope?: unknown; message?: unknown };
|
|
262
295
|
if (typeof data.reviewRequestId === "string" && (data.scope === "workflow" || data.scope === "software") && data.message && typeof data.message === "object") {
|
|
263
|
-
|
|
264
|
-
session
|
|
265
|
-
|
|
296
|
+
let session = this.reviewerSessions.get(data.reviewRequestId);
|
|
297
|
+
if (!session) {
|
|
298
|
+
session = { scope: data.scope, history: [], messageEntryIds: [], foldedEntryIds: new Set() };
|
|
299
|
+
this.reviewerSessions.set(data.reviewRequestId, session);
|
|
300
|
+
}
|
|
301
|
+
// restore() re-walks the whole branch whenever the tip moves
|
|
302
|
+
// (turn_end), so fold each message entry at most once per live session.
|
|
303
|
+
if (!session.foldedEntryIds.has(entry.id)) {
|
|
304
|
+
session.foldedEntryIds.add(entry.id);
|
|
305
|
+
session.history.push(data.message as AgentMessage);
|
|
306
|
+
session.messageEntryIds.push(entry.id);
|
|
307
|
+
}
|
|
266
308
|
}
|
|
267
309
|
}
|
|
268
310
|
if (entry.customType === CONTEMPLATOR_SUGGESTION && entry.data && typeof entry.data === "object") {
|
|
@@ -278,8 +320,7 @@ export class Contemplator {
|
|
|
278
320
|
}
|
|
279
321
|
this.restoredTipId = tipId;
|
|
280
322
|
for (const [probeId, question] of undeliveredSuggestions) {
|
|
281
|
-
if (queuedProbeIds.has(probeId) || this.
|
|
282
|
-
this.requeuedProbeIds.add(probeId);
|
|
323
|
+
if (skipUndeliveredRestore || queuedProbeIds.has(probeId) || this.queuedProbeIds.has(probeId)) continue;
|
|
283
324
|
this.queueProbe(ctx, question, "restore", probeId);
|
|
284
325
|
}
|
|
285
326
|
if (resetTracking) void this.resumePendingReviews(ctx);
|
|
@@ -537,6 +578,10 @@ export class Contemplator {
|
|
|
537
578
|
display: false,
|
|
538
579
|
details: { version: 1, question, source, probeId },
|
|
539
580
|
}, { deliverAs: "steer", triggerTurn: false });
|
|
581
|
+
// sendMessage queues synchronously. Mark every source (not only restore)
|
|
582
|
+
// before a later turn_end can rebuild state from the still-undelivered
|
|
583
|
+
// tracking entry and enqueue this probe again.
|
|
584
|
+
this.queuedProbeIds.add(probeId);
|
|
540
585
|
this.pi.appendEntry(CONTEMPLATOR_SUGGESTION, { version: 1, suggestion: question, delivered: false, source, probeId });
|
|
541
586
|
this.markTipPersisted(ctx);
|
|
542
587
|
debugLog("contemplator.suggestion_queued", {
|
|
@@ -572,7 +617,7 @@ export class Contemplator {
|
|
|
572
617
|
this.resumedReviewIds.add(request.id);
|
|
573
618
|
this.inFlightReviewIds.add(request.id);
|
|
574
619
|
if (key) this.inFlightReviewKeys.add(key);
|
|
575
|
-
const session = this.reviewerSessions.get(request.id) ?? { scope: request.scope, history: options.history ?? [] };
|
|
620
|
+
const session = this.reviewerSessions.get(request.id) ?? { scope: request.scope, history: options.history ?? [], messageEntryIds: [], foldedEntryIds: new Set() };
|
|
576
621
|
this.reviewerSessions.set(request.id, session);
|
|
577
622
|
const task = this.runtime.launchReviewTask(ctx, async () => {
|
|
578
623
|
try {
|
|
@@ -586,9 +631,12 @@ export class Contemplator {
|
|
|
586
631
|
if (sessionGeneration !== this.sessionGeneration || !this.reviewIsPending(ctx, request.id)) return;
|
|
587
632
|
for (const message of messages) {
|
|
588
633
|
session.history.push(message);
|
|
589
|
-
this.
|
|
634
|
+
const entryId = this.appendEntryWithId(ctx, OM_REVIEWER_MESSAGE, { version: 1, reviewRequestId: request.id, scope: request.scope, message }, request.id);
|
|
635
|
+
if (entryId) {
|
|
636
|
+
session.foldedEntryIds.add(entryId);
|
|
637
|
+
session.messageEntryIds.push(entryId);
|
|
638
|
+
}
|
|
590
639
|
}
|
|
591
|
-
if (messages.length > 0) this.markTipPersisted(ctx);
|
|
592
640
|
},
|
|
593
641
|
});
|
|
594
642
|
if (sessionGeneration !== this.sessionGeneration) {
|
|
@@ -643,10 +691,17 @@ export class Contemplator {
|
|
|
643
691
|
|
|
644
692
|
private persistReviewerStates(ctx: MemoryUpdateCtx): void {
|
|
645
693
|
for (const [reviewRequestId, session] of this.reviewerSessions) {
|
|
646
|
-
if (session.
|
|
647
|
-
this.
|
|
694
|
+
if (session.messageEntryIds.length === 0) continue;
|
|
695
|
+
const checkpointEntryId = this.appendEntryWithId(ctx, OM_REVIEWER_STATE, {
|
|
696
|
+
version: 2,
|
|
697
|
+
reviewRequestId,
|
|
698
|
+
scope: session.scope,
|
|
699
|
+
previousStateEntryId: session.checkpointEntryId,
|
|
700
|
+
messageEntryIds: session.messageEntryIds,
|
|
701
|
+
}, reviewRequestId);
|
|
702
|
+
if (checkpointEntryId) session.checkpointEntryId = checkpointEntryId;
|
|
703
|
+
session.messageEntryIds = [];
|
|
648
704
|
}
|
|
649
|
-
if (this.reviewerSessions.size > 0) this.markTipPersisted(ctx);
|
|
650
705
|
}
|
|
651
706
|
|
|
652
707
|
private async resumePendingReviews(ctx: MemoryUpdateCtx): Promise<void> {
|
|
@@ -669,8 +724,27 @@ export class Contemplator {
|
|
|
669
724
|
}
|
|
670
725
|
}
|
|
671
726
|
|
|
672
|
-
private markTipPersisted(ctx: MemoryUpdateCtx):
|
|
727
|
+
private markTipPersisted(ctx: MemoryUpdateCtx): string | undefined {
|
|
673
728
|
this.restoredTipId = (ctx.sessionManager.getBranch() as Entry[]).at(-1)?.id;
|
|
729
|
+
return this.restoredTipId;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
/**
|
|
733
|
+
* Append a custom entry and return the id of the entry just written, or
|
|
734
|
+
* undefined when it cannot be attributed. pi.appendEntry() does not return
|
|
735
|
+
* the entry id, so the branch is diffed around the synchronous append and the
|
|
736
|
+
* candidate is matched by customType + reviewRequestId. This never credits a
|
|
737
|
+
* concurrently-appended foreign entry reached via the branch tail. A failed
|
|
738
|
+
* attribution is harmless: restore rebuilds transcripts from the durable
|
|
739
|
+
* om.reviewer.message entries themselves and only skips a checkpoint.
|
|
740
|
+
*/
|
|
741
|
+
private appendEntryWithId(ctx: MemoryUpdateCtx, customType: string, data: Record<string, unknown>, reviewRequestId: string): string | undefined {
|
|
742
|
+
const branch = ctx.sessionManager.getBranch() as readonly Entry[];
|
|
743
|
+
const before = branch.length;
|
|
744
|
+
this.pi.appendEntry(customType, data);
|
|
745
|
+
const added = (ctx.sessionManager.getBranch() as readonly Entry[]).slice(before);
|
|
746
|
+
const own = added.find((entry) => entry.customType === customType && (entry.data as { reviewRequestId?: unknown } | undefined)?.reviewRequestId === reviewRequestId);
|
|
747
|
+
return own?.id;
|
|
674
748
|
}
|
|
675
749
|
|
|
676
750
|
private async compactHistory(model: Model<any>, apiKey: string, headers: Record<string, string> | undefined, sessionGeneration: number): Promise<void> {
|
|
@@ -5,7 +5,7 @@ export const OM_REVIEW_REQUEST = "om.review.request";
|
|
|
5
5
|
export const OM_REVIEW_RESULT = "om.review.result";
|
|
6
6
|
/** Persisted assistant/tool output from a short-lived structural reviewer. */
|
|
7
7
|
export const OM_REVIEWER_MESSAGE = "om.reviewer.message";
|
|
8
|
-
/**
|
|
8
|
+
/** Compact checkpoint referencing reviewer transcript entries across primary-session compaction. */
|
|
9
9
|
export const OM_REVIEWER_STATE = "om.reviewer.state";
|
|
10
10
|
/** Compact proposal notice queued for the primary agent. */
|
|
11
11
|
export const OM_REVIEWER_NOTICE = "om.reviewer.notice";
|