@matthewfl/pi-contemplator 0.0.3 → 0.0.4
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 +1 -1
- package/package.json +1 -1
- package/src/agents/contemplator/agent.ts +24 -2
- package/src/agents/reviewer/agent.ts +28 -10
- package/src/commands/settings.ts +9 -2
- package/src/commands/status.ts +1 -0
- package/src/config.ts +4 -0
- package/src/runtime.ts +2 -2
package/README.md
CHANGED
|
@@ -96,7 +96,7 @@ pi -e ./src/index.ts
|
|
|
96
96
|
|
|
97
97
|
## Configuration and commands
|
|
98
98
|
|
|
99
|
-
The plugin works with its defaults, including the contemplator and reviewer. Model selection, trigger thresholds, passive mode, compaction behavior, and other settings are documented in [docs/configuration.md](docs/configuration.md).
|
|
99
|
+
The plugin works with its defaults, including the contemplator and reviewer. Contemplator probes and review notices appear as purple cards in the chat by default; use `/om:settings messages off` to hide newly sent cards without stopping their delivery to the agent. Model selection, trigger thresholds, passive mode, compaction behavior, and other settings are documented in [docs/configuration.md](docs/configuration.md).
|
|
100
100
|
|
|
101
101
|
Useful commands include:
|
|
102
102
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@matthewfl/pi-contemplator",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.4",
|
|
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",
|
|
@@ -4,6 +4,7 @@ import type { Static } from "typebox";
|
|
|
4
4
|
import { streamSimple } from "@earendil-works/pi-ai/compat";
|
|
5
5
|
import { generateSummaryWithUsage } from "@earendil-works/pi-coding-agent";
|
|
6
6
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
7
|
+
import { Box, Text } from "@earendil-works/pi-tui";
|
|
7
8
|
import { assistantOutputTokens, assistantToolCallCount, fullProjection, isReviewRequestEntry, isReviewResultEntry, OM_REVIEWER_MESSAGE, OM_REVIEWER_NOTICE, OM_REVIEWER_STATE, OM_REVIEW_REQUEST, OM_REVIEW_RESULT, type Entry, type ReviewResult, type StructuralReviewRequest } from "../../session-ledger/index.js";
|
|
8
9
|
import { hashId } from "../../ids.js";
|
|
9
10
|
import { createSearchMemoriesAgentTool } from "../../tools/search-memories.js";
|
|
@@ -80,9 +81,19 @@ function reviewSummaryLine(review: ReviewResult): string {
|
|
|
80
81
|
function reviewRequestKey(request: RequestReviewArgs): string {
|
|
81
82
|
return `${request.scope}:${hashId(`${request.evidence}\n${request.concern}`)}`;
|
|
82
83
|
}
|
|
84
|
+
|
|
85
|
+
function customMessageText(content: unknown): string {
|
|
86
|
+
if (typeof content === "string") return content;
|
|
87
|
+
if (!Array.isArray(content)) return "";
|
|
88
|
+
return content
|
|
89
|
+
.map((block) => block && typeof block === "object" && "text" in block && typeof block.text === "string" ? block.text : "")
|
|
90
|
+
.filter(Boolean)
|
|
91
|
+
.join("\n");
|
|
92
|
+
}
|
|
83
93
|
const CONTEMPLATOR_MESSAGE = "om.contemplator.message";
|
|
84
94
|
const CONTEMPLATOR_STATE = "om.contemplator.state";
|
|
85
95
|
const CONTEMPLATOR_SUGGESTION = "om.contemplator.suggestion";
|
|
96
|
+
const REVIEW_PROPOSAL_MESSAGE = "om.review.proposal";
|
|
86
97
|
const SendProbeSchema = Type.Object({ question: Type.String({ minLength: 1, description: "One concise, memory-grounded probing question, optionally preceded by one short sentence of context. Cite relevant memory identifiers." }) });
|
|
87
98
|
const ReviewScopeSchema = Type.Union([Type.Literal("workflow"), Type.Literal("software")]);
|
|
88
99
|
export const RequestReviewSchema = Type.Object({
|
|
@@ -153,6 +164,17 @@ export class Contemplator {
|
|
|
153
164
|
constructor(private readonly pi: ExtensionAPI, private readonly runtime: Runtime) {}
|
|
154
165
|
|
|
155
166
|
register(): void {
|
|
167
|
+
this.pi.registerMessageRenderer(CONTEMPLATOR_SUGGESTION, (message, _options, theme) => {
|
|
168
|
+
const content = customMessageText(message.content).replace(/^Background contemplator probe \(advisory\):\n?/, "");
|
|
169
|
+
const box = new Box(1, 1, (text) => theme.bg("customMessageBg", text));
|
|
170
|
+
box.addChild(new Text(theme.fg("thinkingHigh", `${theme.bold("◆ CONTEMPLATOR PROBE")}\n${content}`), 0, 0));
|
|
171
|
+
return box;
|
|
172
|
+
});
|
|
173
|
+
this.pi.registerMessageRenderer(REVIEW_PROPOSAL_MESSAGE, (message, _options, theme) => {
|
|
174
|
+
const box = new Box(1, 1, (text) => theme.bg("customMessageBg", text));
|
|
175
|
+
box.addChild(new Text(theme.fg("thinkingHigh", `${theme.bold("◆ CONTEMPLATOR REVIEW")}\n${customMessageText(message.content)}`), 0, 0));
|
|
176
|
+
return box;
|
|
177
|
+
});
|
|
156
178
|
this.runtime.setMemoryUpdateListener((ctx) => this.withDebugContext(ctx, () => this.observeTurn(ctx)));
|
|
157
179
|
this.pi.on("session_start", (event: any, ctx: ExtensionContext) => {
|
|
158
180
|
this.sessionGeneration++;
|
|
@@ -575,7 +597,7 @@ export class Contemplator {
|
|
|
575
597
|
this.pi.sendMessage({
|
|
576
598
|
customType: CONTEMPLATOR_SUGGESTION,
|
|
577
599
|
content: `Background contemplator probe (advisory):\n${question}`,
|
|
578
|
-
display:
|
|
600
|
+
display: this.runtime.config.showContemplatorMessages,
|
|
579
601
|
details: { version: 1, question, source, probeId },
|
|
580
602
|
}, { deliverAs: "steer", triggerTurn: false });
|
|
581
603
|
// sendMessage queues synchronously. Mark every source (not only restore)
|
|
@@ -657,7 +679,7 @@ export class Contemplator {
|
|
|
657
679
|
debugLog(result.outcome === "proposal" ? "reviewer.proposal_created" : "reviewer.no_proposal", { reviewRequestId: request.id, reviewMemoryId: result.id, scope: result.scope });
|
|
658
680
|
if (result.outcome === "proposal") {
|
|
659
681
|
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.`;
|
|
660
|
-
this.pi.sendMessage({ customType:
|
|
682
|
+
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", triggerTurn: false });
|
|
661
683
|
this.pi.appendEntry(OM_REVIEWER_NOTICE, { version: 1, reviewRequestId: request.id, reviewMemoryId: result.id, scope: result.scope, content: notice });
|
|
662
684
|
this.markTipPersisted(ctx);
|
|
663
685
|
debugLog("reviewer.primary_notice_queued", { reviewRequestId: request.id, reviewMemoryId: result.id });
|
|
@@ -71,6 +71,27 @@ function assistantOutputTokens(messages: AgentMessage[]): number {
|
|
|
71
71
|
return total;
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
function completeReview(request: StructuralReviewRequest, terminal: ReviewTerminalResult): ReviewResult {
|
|
75
|
+
return {
|
|
76
|
+
...terminal,
|
|
77
|
+
id: hashId(`${request.id}:${JSON.stringify(terminal)}:${Date.now()}`),
|
|
78
|
+
version: 1,
|
|
79
|
+
reviewRequestId: request.id,
|
|
80
|
+
createdAt: Date.now(),
|
|
81
|
+
requestedBy: "contemplator",
|
|
82
|
+
} as ReviewResult;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function budgetExhaustedResult(request: StructuralReviewRequest): ReviewResult {
|
|
86
|
+
return completeReview(request, {
|
|
87
|
+
outcome: "no_proposal",
|
|
88
|
+
scope: request.scope,
|
|
89
|
+
reason: "The reviewer exhausted its lifetime output-token budget before recording a terminal proposal decision.",
|
|
90
|
+
evidenceReviewed: "The persisted reviewer transcript was retained, but no terminal evidence assessment was recorded before the budget was exhausted.",
|
|
91
|
+
reconsiderIf: "A new review can be requested with a narrower scope or a smaller evidence set.",
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
74
95
|
export async function runStructuralReview(args: RunStructuralReviewArgs): Promise<ReviewResult | undefined> {
|
|
75
96
|
let terminal: ReviewTerminalResult | undefined;
|
|
76
97
|
const acceptTerminal = (candidate: ReviewTerminalResult): void => {
|
|
@@ -98,7 +119,7 @@ export async function runStructuralReview(args: RunStructuralReviewArgs): Promis
|
|
|
98
119
|
// Usage on persisted assistant messages makes this a lifetime request budget,
|
|
99
120
|
// rather than a fresh allowance on each session/tree resumption.
|
|
100
121
|
let totalOutputTokens = assistantOutputTokens(history);
|
|
101
|
-
if (totalOutputTokens >= REVIEWER_TOTAL_TOKEN_LIMIT) return
|
|
122
|
+
if (totalOutputTokens >= REVIEWER_TOTAL_TOKEN_LIMIT) return budgetExhaustedResult(args.request);
|
|
102
123
|
|
|
103
124
|
// Persist both the user continuation and the returned messages immediately.
|
|
104
125
|
// This makes the transcript sufficient to resume a review after shutdown.
|
|
@@ -175,13 +196,10 @@ export async function runStructuralReview(args: RunStructuralReviewArgs): Promis
|
|
|
175
196
|
progress = await runOnce(keepGoing);
|
|
176
197
|
invocations++;
|
|
177
198
|
}
|
|
178
|
-
if (!terminal)
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
createdAt: Date.now(),
|
|
185
|
-
requestedBy: "contemplator",
|
|
186
|
-
} as ReviewResult;
|
|
199
|
+
if (!terminal) {
|
|
200
|
+
return totalOutputTokens >= REVIEWER_TOTAL_TOKEN_LIMIT
|
|
201
|
+
? budgetExhaustedResult(args.request)
|
|
202
|
+
: undefined;
|
|
203
|
+
}
|
|
204
|
+
return completeReview(args.request, terminal);
|
|
187
205
|
}
|
package/src/commands/settings.ts
CHANGED
|
@@ -10,7 +10,7 @@ type ModelRegistryLike = {
|
|
|
10
10
|
getAll(): Array<{ provider: string; id: string }>;
|
|
11
11
|
};
|
|
12
12
|
type NumberSetting = "observeAfterTokens" | "reflectAfterTokens" | "compactAfterTokens" | "observerChunkMaxTokens" | "observationsPoolMaxTokens" | "observationsPoolTargetTokens" | "agentMaxTurns" | "contemplatorMinNewObservations" | "contemplatorMinNewReflections" | "contemplatorMinTurns";
|
|
13
|
-
type BooleanSetting = "contemplatorEnabled" | "reviewerEnabled" | "compactionObserverEnabled" | "showWorkerNotifications" | "passive" | "debugLog";
|
|
13
|
+
type BooleanSetting = "contemplatorEnabled" | "showContemplatorMessages" | "reviewerEnabled" | "compactionObserverEnabled" | "showWorkerNotifications" | "passive" | "debugLog";
|
|
14
14
|
|
|
15
15
|
function modelLabel(model: ConfiguredModel | undefined): string {
|
|
16
16
|
return model ? `${model.provider}/${model.id}` : "current session model";
|
|
@@ -175,8 +175,13 @@ export function registerSettingsCommand(pi: ExtensionAPI, runtime: Runtime): voi
|
|
|
175
175
|
ctx.ui.notify(`Structural reviewer: ${argument.endsWith("on") ? "enabled" : "disabled"} for this session.`, "info");
|
|
176
176
|
return;
|
|
177
177
|
}
|
|
178
|
+
if (argument === "messages on" || argument === "messages off") {
|
|
179
|
+
appendSettings(pi, runtime, { showContemplatorMessages: argument.endsWith("on") });
|
|
180
|
+
ctx.ui.notify(`Contemplator messages: ${argument.endsWith("on") ? "visible" : "hidden"} for this session.`, "info");
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
178
183
|
if (argument) {
|
|
179
|
-
ctx.ui.notify("Usage: /om:settings [on|off|reviewer on|reviewer off|compaction on|compaction off]", "info");
|
|
184
|
+
ctx.ui.notify("Usage: /om:settings [on|off|messages on|messages off|reviewer on|reviewer off|compaction on|compaction off]", "info");
|
|
180
185
|
return;
|
|
181
186
|
}
|
|
182
187
|
|
|
@@ -185,6 +190,7 @@ export function registerSettingsCommand(pi: ExtensionAPI, runtime: Runtime): voi
|
|
|
185
190
|
const choice = await ctx.ui.select("Observational memory settings (session overrides)", [
|
|
186
191
|
`Contemplation: ${scalarLabel(runtime, "contemplatorEnabled")}`,
|
|
187
192
|
`Contemplation model: ${hasOverride(settings, "contemplatorModel") ? modelLabel(runtime.config.contemplatorModel) : `default (${modelLabel(runtime.getDefaultConfig().contemplatorModel)})`}`,
|
|
193
|
+
`Contemplator messages visible: ${scalarLabel(runtime, "showContemplatorMessages")}`,
|
|
188
194
|
`Structural reviewer: ${scalarLabel(runtime, "reviewerEnabled")}`,
|
|
189
195
|
`Structural reviewer model: ${hasOverride(settings, "reviewerModel") ? modelLabel(runtime.config.reviewerModel) : `default (${modelLabel(runtime.getDefaultConfig().reviewerModel)})`}`,
|
|
190
196
|
`Compaction observer: ${scalarLabel(runtime, "compactionObserverEnabled")}`,
|
|
@@ -208,6 +214,7 @@ export function registerSettingsCommand(pi: ExtensionAPI, runtime: Runtime): voi
|
|
|
208
214
|
]);
|
|
209
215
|
if (!choice || choice === "Done") return;
|
|
210
216
|
if (choice.startsWith("Contemplation:")) appendSettings(pi, runtime, { contemplatorEnabled: !runtime.config.contemplatorEnabled });
|
|
217
|
+
else if (choice.startsWith("Contemplator messages visible:")) appendSettings(pi, runtime, { showContemplatorMessages: !runtime.config.showContemplatorMessages });
|
|
211
218
|
else if (choice.startsWith("Structural reviewer:")) appendSettings(pi, runtime, { reviewerEnabled: !runtime.config.reviewerEnabled });
|
|
212
219
|
else if (choice.startsWith("Compaction observer:")) appendSettings(pi, runtime, { compactionObserverEnabled: !runtime.config.compactionObserverEnabled });
|
|
213
220
|
else if (choice.startsWith("Worker notifications:")) appendSettings(pi, runtime, { showWorkerNotifications: !runtime.config.showWorkerNotifications });
|
package/src/commands/status.ts
CHANGED
|
@@ -102,6 +102,7 @@ export function registerStatusCommand(pi: ExtensionAPI, runtime: Runtime): void
|
|
|
102
102
|
`Compaction observer: ${runtime.config.compactionObserverEnabled === false ? "disabled" : "enabled"}`,
|
|
103
103
|
`Contemplator: ${runtime.config.contemplatorEnabled ? "enabled" : "disabled"}`,
|
|
104
104
|
`Contemplator model: ${runtime.config.contemplatorModel ? `${runtime.config.contemplatorModel.provider}/${runtime.config.contemplatorModel.id}` : "current session model"}`,
|
|
105
|
+
`Contemplator messages: ${runtime.config.showContemplatorMessages ? "visible" : "hidden"}`,
|
|
105
106
|
`Structural reviewer: ${runtime.config.reviewerEnabled === false ? "disabled" : "enabled"}`,
|
|
106
107
|
`Reviewer model: ${runtime.config.reviewerModel ? `${runtime.config.reviewerModel.provider}/${runtime.config.reviewerModel.id}` : "current session model"}`,
|
|
107
108
|
];
|
package/src/config.ts
CHANGED
|
@@ -52,6 +52,8 @@ export interface Config {
|
|
|
52
52
|
compactionObserverEnabled: boolean;
|
|
53
53
|
contemplatorEnabled: boolean;
|
|
54
54
|
contemplatorModel?: ConfiguredModel;
|
|
55
|
+
/** Show contemplator probes and review notices in the chat transcript. */
|
|
56
|
+
showContemplatorMessages: boolean;
|
|
55
57
|
/** Allow the contemplator to commission scoped structural reviewers. */
|
|
56
58
|
reviewerEnabled: boolean;
|
|
57
59
|
/** Optional model override used only by short-lived structural reviewers. */
|
|
@@ -75,6 +77,7 @@ export const DEFAULTS: Config = {
|
|
|
75
77
|
passive: false,
|
|
76
78
|
compactionObserverEnabled: true,
|
|
77
79
|
contemplatorEnabled: true,
|
|
80
|
+
showContemplatorMessages: true,
|
|
78
81
|
reviewerEnabled: true,
|
|
79
82
|
contemplatorMinNewObservations: 8,
|
|
80
83
|
contemplatorMinNewReflections: 1,
|
|
@@ -223,6 +226,7 @@ function normalizeSettingsConfig(value: Record<string, unknown>): Partial<Config
|
|
|
223
226
|
if (typeof value.passive === "boolean") normalized.passive = value.passive;
|
|
224
227
|
if (typeof value.compactionObserverEnabled === "boolean") normalized.compactionObserverEnabled = value.compactionObserverEnabled;
|
|
225
228
|
if (typeof value.contemplatorEnabled === "boolean") normalized.contemplatorEnabled = value.contemplatorEnabled;
|
|
229
|
+
if (typeof value.showContemplatorMessages === "boolean") normalized.showContemplatorMessages = value.showContemplatorMessages;
|
|
226
230
|
if (typeof value.reviewerEnabled === "boolean") normalized.reviewerEnabled = value.reviewerEnabled;
|
|
227
231
|
if (typeof value.debugLog === "boolean") normalized.debugLog = value.debugLog;
|
|
228
232
|
const model = normalizeModel(value.model);
|
package/src/runtime.ts
CHANGED
|
@@ -35,7 +35,7 @@ export type SessionSettings = Partial<Pick<Config,
|
|
|
35
35
|
| "observeAfterTokens" | "reflectAfterTokens" | "observerChunkMaxTokens" | "compactAfterTokens"
|
|
36
36
|
| "compactAfterTokensMode" | "compactAfterTokensRatio"
|
|
37
37
|
| "observationsPoolMaxTokens" | "observationsPoolTargetTokens" | "agentMaxTurns"
|
|
38
|
-
| "showWorkerNotifications" | "passive" | "compactionObserverEnabled" | "contemplatorEnabled" | "reviewerEnabled"
|
|
38
|
+
| "showWorkerNotifications" | "passive" | "compactionObserverEnabled" | "contemplatorEnabled" | "showContemplatorMessages" | "reviewerEnabled"
|
|
39
39
|
| "contemplatorMinNewObservations" | "contemplatorMinNewReflections" | "contemplatorMinTurns" | "debugLog"
|
|
40
40
|
>> & {
|
|
41
41
|
/** null explicitly means use the configured/session model. */
|
|
@@ -108,7 +108,7 @@ export function computeSessionSettings(entries: readonly unknown[]): SessionSett
|
|
|
108
108
|
if (!source || typeof source !== "object") return;
|
|
109
109
|
const data = source as Record<string, unknown>;
|
|
110
110
|
const booleanKeys = [
|
|
111
|
-
"showWorkerNotifications", "passive", "compactionObserverEnabled", "contemplatorEnabled", "reviewerEnabled", "debugLog",
|
|
111
|
+
"showWorkerNotifications", "passive", "compactionObserverEnabled", "contemplatorEnabled", "showContemplatorMessages", "reviewerEnabled", "debugLog",
|
|
112
112
|
] as const;
|
|
113
113
|
const numberKeys = [
|
|
114
114
|
"observeAfterTokens", "reflectAfterTokens", "observerChunkMaxTokens", "compactAfterTokens",
|