@matthewfl/pi-contemplator 0.0.3 → 0.0.5
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 +96 -18
- package/package.json +1 -1
- package/src/agents/contemplator/agent.ts +77 -7
- package/src/agents/reviewer/agent.ts +28 -10
- package/src/commands/settings.ts +9 -2
- package/src/commands/status.ts +13 -0
- package/src/config.ts +4 -0
- package/src/runtime.ts +2 -2
- package/src/session-ledger/progress.ts +12 -0
- package/src/session-ledger/types.ts +2 -0
package/README.md
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
# pi-contemplator
|
|
2
2
|
|
|
3
|
-
`pi-contemplator` is a [Pi](https://pi.dev/) plugin
|
|
3
|
+
`pi-contemplator` is a [Pi](https://pi.dev/) plugin for long-running, largely unsupervised agentic sessions. It does two things:
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
1. **Keeps memory alive across compaction.** A background **observer**, **reflector**, and **dropper** distill the conversation into a durable, branch-local memory ledger. When Pi compacts the context window, the summary is rendered deterministically from that ledger — fast, model-free, and lossless enough that important facts survive.
|
|
6
|
+
2. **Gives the primary agent a second set of eyes.** A background **contemplator** reads the accumulated memories and watches for reasoning that is going wrong. When it finds a genuine problem, it can inject a focused, memory-cited question. For deeper, recurring structural issues, it can commission a short-lived **reviewer** that produces a durable, advisory design proposal.
|
|
7
|
+
|
|
8
|
+
The result: a long session is less likely to drift off course, get stuck in an unproductive loop, or silently compound a wrong conclusion — and when it does, it can often catch itself before the mistake poisons the rest of the work.
|
|
6
9
|
|
|
7
10
|
> [!WARNING]
|
|
8
11
|
> **Alpha-quality software:** `pi-contemplator` is under active development. Expect bugs, behavioral changes, and breaking configuration or memory-format changes.
|
|
@@ -15,7 +18,11 @@ It adds a background **contemplator** agent that examines accumulated session me
|
|
|
15
18
|
|
|
16
19
|
## Why use it?
|
|
17
20
|
|
|
18
|
-
A long-running agent
|
|
21
|
+
A long-running agent faces three compounding risks:
|
|
22
|
+
|
|
23
|
+
- **Compaction amnesia.** Pi compacts older messages to fit the context window. Details that weren't captured deliberately are simply gone.
|
|
24
|
+
- **Reasoning entrenchment.** An agent can draw the wrong conclusion from valid evidence and then repeatedly reinforce it. As the mistake accumulates in context, the agent becomes *less* likely to question it and *less* able to discover the real problem.
|
|
25
|
+
- **Unproductive loops.** The agent keeps bumping into the same obstacle through slightly different approaches, burning tokens without gaining information.
|
|
19
26
|
|
|
20
27
|
Without a contemplator, a session can look like this:
|
|
21
28
|
|
|
@@ -66,17 +73,86 @@ A or C is true
|
|
|
66
73
|
|
|
67
74
|
The contemplator does not take over the primary agent's work. It provides occasional, evidence-grounded challenges when reconsidering the current reasoning is more valuable than allowing the session to continue along the same path.
|
|
68
75
|
|
|
76
|
+
## What it does for you
|
|
77
|
+
|
|
78
|
+
### It catches reasoning problems before they settle
|
|
79
|
+
|
|
80
|
+
The contemplator is a "System 2" thinker: slower and more deliberative, focused on the larger shape of the problem while the primary agent does the hands-on work. It watches the memory ledger (not the live transcript) and looks for:
|
|
81
|
+
|
|
82
|
+
- unsupported assumptions — a conclusion that depends on something never established;
|
|
83
|
+
- contradictions — a recent claim that conflicts with earlier recorded evidence;
|
|
84
|
+
- drift from the user's intent, priorities, or constraints;
|
|
85
|
+
- overlooked alternatives — treating one explanation as settled when others are still plausible;
|
|
86
|
+
- unproductive loops — several memories showing the agent returning to the same obstacle;
|
|
87
|
+
- connections — relevant facts from separate memories that the primary agent never joined up.
|
|
88
|
+
|
|
89
|
+
When it finds something worth challenging, it sends a concise, memory-cited question as an asynchronous probe. Probes appear as purple cards in the chat and are delivered to the agent's next turn — they never interrupt mid-work or force a new turn:
|
|
90
|
+
|
|
91
|
+
```text
|
|
92
|
+
◆ CONTEMPLATOR PROBE
|
|
93
|
+
|
|
94
|
+
You concluded that fix Y resolved the failure, citing memory [a1b2c3d4e5f6].
|
|
95
|
+
Earlier, memory [d4e5f6a1b2c3] recorded that A, B, and C were also
|
|
96
|
+
possible explanations. What observation would distinguish Y from A, B, and C
|
|
97
|
+
before you build on that conclusion?
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### It commissions deeper structural reviews
|
|
101
|
+
|
|
102
|
+
Some patterns are bigger than one question. When several memories point at a *recurring* structural problem, the contemplator can request a review instead of a probe. The reviewer is a separate, short-lived, strictly-scoped agent that independently investigates the concern and, if the evidence supports it, records a durable conceptual proposal.
|
|
103
|
+
|
|
104
|
+
Two scopes are handled separately:
|
|
105
|
+
|
|
106
|
+
- **Workflow** — problems in *how* the work is performed: repeatedly reconstructing the same information, reasoning at length about something that could just be observed or computed, failing to preserve a reusable result, weak feedback loops.
|
|
107
|
+
- **Software** — problems in *the software being produced*: repeated special cases, several fixes touching the same missing invariant, duplicated concepts, unclear responsibility boundaries, local workarounds that hint at a missing abstraction.
|
|
108
|
+
|
|
109
|
+
The reviewer isn't just fed the concern and asked to design a fix. It independently searches the memory ledger *and* the primary agent's recorded chat history (including regex search over the transcript), looks for supporting and contrary evidence, and reaches exactly one terminal outcome: either a durable proposal with a memory id, or a conclusion that no proposal is currently justified. The primary agent receives only a compact notice pointing at the proposal:
|
|
110
|
+
|
|
111
|
+
```text
|
|
112
|
+
◆ CONTEMPLATOR REVIEW
|
|
113
|
+
|
|
114
|
+
BACKGROUND WORKFLOW REVIEW PROPOSAL [f7a8b9c0d1e2]
|
|
115
|
+
|
|
116
|
+
Several memories ([1111], [2222], [3333]) show the same multi-step lookup being
|
|
117
|
+
rebuilt from scratch each time. The proposal suggests a reusable, indexable
|
|
118
|
+
trace that would make the relationship reproducible in seconds instead of minutes.
|
|
119
|
+
|
|
120
|
+
Recall memory [f7a8b9c0d1e2] to read the full conceptual proposal when it is
|
|
121
|
+
relevant. This is advisory.
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Proposals are deliberately conceptual — the reviewer cannot write code, specify exact APIs, name files, or hand over an implementation plan. The primary agent remains the only agent grounded in the actual codebase and environment, and it decides whether, when, and how to use a proposal.
|
|
125
|
+
|
|
126
|
+
### It keeps memory useful and accountable
|
|
127
|
+
|
|
128
|
+
The memory system is built to be trustworthy:
|
|
129
|
+
|
|
130
|
+
- **Every memory has provenance.** Observations cite the exact source entries that support them; reflections cite the observations they're based on. Everything gets a deterministic 12-character id computed in code, not guessed by a model.
|
|
131
|
+
- **Nothing important is silently lost.** The `recall` tool recovers the exact source evidence behind any memory id. The `search_memories` tool finds candidate memories by topic. Even observations the dropper later prunes remain recallable from ledger history.
|
|
132
|
+
- **Compaction is deterministic and model-free.** The summary the agent sees is folded from the ledger by code, not rewritten by a model. That makes compaction fast and cheap, and it means the same session state always produces the same summary.
|
|
133
|
+
|
|
134
|
+
## Why you can trust it in the background
|
|
135
|
+
|
|
136
|
+
Running extra agents in the background can be worrying — what if they take over, spam the user, or go off the rails? `pi-contemplator` is explicitly designed against that:
|
|
137
|
+
|
|
138
|
+
- **Advisory only.** The contemplator and reviewer never modify the primary agent's work, never implement anything, and never inject instructions into the user's session. They produce questions and proposals; the primary agent decides what to do.
|
|
139
|
+
- **One intervention per update.** The contemplator can send at most one probe *or* one review request per turn — never a barrage. Most turns it sends nothing at all.
|
|
140
|
+
- **Grounded in evidence, not noise.** Loops must be supported by a clear pattern across *multiple* memories. The contemplator is explicitly forbidden from inferring a stall from silence, elapsed time, or a missing recent result. It may consult activity signals (cumulative tokens, tool-call count, active time) as *advisory* context, but never as proof.
|
|
141
|
+
- **Sees memory, not secrets.** The contemplator reads only the memory ledger, not the live transcript, so it can't react to or interfere with in-flight work.
|
|
142
|
+
- **Epistemic humility.** The contemplator is instructed not to behave as though it already knows the answer, not to praise or criticize, and not to manage the primary agent step by step. It asks questions that help *both* agents discover what's missing.
|
|
143
|
+
- **Bounded and de-duplicated.** Reviews are serialized (one at a time), coalesced (no duplicate review for the same evidence and concern), and capped by a lifetime output-token budget — a review that runs out of budget records an honest "no proposal" rather than a half-baked design.
|
|
144
|
+
|
|
69
145
|
## Background agents
|
|
70
146
|
|
|
71
|
-
`pi-contemplator` uses five specialized background agents:
|
|
147
|
+
`pi-contemplator` uses five specialized background agents, all enabled by default:
|
|
72
148
|
|
|
73
|
-
| Agent |
|
|
149
|
+
| Agent | What it does | When it runs |
|
|
74
150
|
|---|---|---|
|
|
75
|
-
| **Observer** | Extracts concrete,
|
|
76
|
-
| **Reflector** |
|
|
77
|
-
| **Dropper** |
|
|
78
|
-
| **Contemplator** | Watches accumulated memories for reasoning gaps, contradictions, overlooked alternatives, and recurring structural concerns;
|
|
79
|
-
| **Reviewer** | Performs a
|
|
151
|
+
| **Observer** | Extracts concrete, timestamped observations from the primary session, citing source entries. | In the background after turns, once enough new source text accumulates. |
|
|
152
|
+
| **Reflector** | Distills durable conclusions (user intent, decisions, constraints) from observations, citing supporting observations. | Periodically, after the observer is up to date. |
|
|
153
|
+
| **Dropper** | Prunes observations that are obsolete, redundant, or safely represented elsewhere, keeping the active memory pool bounded. | Only after a successful reflection, when the memory pool is over target. |
|
|
154
|
+
| **Contemplator** | Watches accumulated memories for reasoning gaps, contradictions, overlooked alternatives, and recurring structural concerns; can send a focused probe or request a review. | Asynchronously after enough new memories accumulate. |
|
|
155
|
+
| **Reviewer** | Performs a deep, scoped (workflow or software) structural investigation and records a durable proposal or a no-proposal conclusion. | Only when the contemplator commissions a review, and only one at a time. |
|
|
80
156
|
|
|
81
157
|
The observer, reflector, and dropper provide the durable memory substrate. The contemplator reasons over that substrate, while the reviewer is launched only when a concern warrants a deeper structural investigation.
|
|
82
158
|
|
|
@@ -94,18 +170,20 @@ Or run it directly from a local checkout:
|
|
|
94
170
|
pi -e ./src/index.ts
|
|
95
171
|
```
|
|
96
172
|
|
|
97
|
-
##
|
|
173
|
+
## Commands and configuration
|
|
98
174
|
|
|
99
|
-
The plugin works with its defaults, including the contemplator and reviewer.
|
|
175
|
+
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.
|
|
100
176
|
|
|
101
|
-
Useful commands
|
|
177
|
+
Useful commands:
|
|
102
178
|
|
|
103
|
-
- `/om:status` — show memory and background-agent status.
|
|
104
|
-
- `/om:view
|
|
179
|
+
- `/om:status` — show memory and background-agent status, including how much recorded memory is still visible vs. pending.
|
|
180
|
+
- `/om:view` — inspect visible memory (and attempt to copy it).
|
|
181
|
+
- `/om:view full` — inspect the full memory ledger, including everything not yet folded into a compaction.
|
|
182
|
+
- `/om:view contemplator` — inspect the contemplator's private transcript and probes.
|
|
105
183
|
- `/om:view reviewer` — inspect structural reviewer transcripts and outcomes.
|
|
106
|
-
- `/om:settings` — inspect or change session-level settings.
|
|
184
|
+
- `/om:settings` — inspect or change session-level settings (including `messages on|off`, `reviewer on|off`, `compaction on|off`).
|
|
107
185
|
|
|
108
|
-
See [docs/how-it-works.md](docs/how-it-works.md) for the memory lifecycle and
|
|
186
|
+
Model selection, trigger thresholds, passive mode (which stops all background work), compaction behavior, and other tuning are documented in [docs/configuration.md](docs/configuration.md). See [docs/how-it-works.md](docs/how-it-works.md) for the memory lifecycle and [docs/concepts.md](docs/concepts.md) for the mental model.
|
|
109
187
|
|
|
110
188
|
## Development
|
|
111
189
|
|
|
@@ -117,4 +195,4 @@ npm run typecheck
|
|
|
117
195
|
|
|
118
196
|
## License
|
|
119
197
|
|
|
120
|
-
MIT. See [LICENSE](LICENSE).
|
|
198
|
+
MIT. See [LICENSE](LICENSE).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@matthewfl/pi-contemplator",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.5",
|
|
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,7 +4,8 @@ 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 {
|
|
7
|
+
import { Box, Text } from "@earendil-works/pi-tui";
|
|
8
|
+
import { agentActiveTimeMs, assistantOutputTokens, assistantToolCallCount, fullProjection, isReviewRequestEntry, isReviewResultEntry, OM_AGENT_ACTIVITY, 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";
|
|
10
11
|
import { createRecallAgentTool } from "../../tools/recall-observation.js";
|
|
@@ -21,6 +22,7 @@ interface PendingUpdate {
|
|
|
21
22
|
reviews: string[];
|
|
22
23
|
mainAgentOutputTokens: number;
|
|
23
24
|
mainAgentToolCalls: number;
|
|
25
|
+
mainAgentActiveTimeMs: number;
|
|
24
26
|
}
|
|
25
27
|
|
|
26
28
|
type Intervention =
|
|
@@ -80,9 +82,32 @@ function reviewSummaryLine(review: ReviewResult): string {
|
|
|
80
82
|
function reviewRequestKey(request: RequestReviewArgs): string {
|
|
81
83
|
return `${request.scope}:${hashId(`${request.evidence}\n${request.concern}`)}`;
|
|
82
84
|
}
|
|
85
|
+
|
|
86
|
+
function customMessageText(content: unknown): string {
|
|
87
|
+
if (typeof content === "string") return content;
|
|
88
|
+
if (!Array.isArray(content)) return "";
|
|
89
|
+
return content
|
|
90
|
+
.map((block) => block && typeof block === "object" && "text" in block && typeof block.text === "string" ? block.text : "")
|
|
91
|
+
.filter(Boolean)
|
|
92
|
+
.join("\n");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const AGENT_TIME_BUCKET_MINUTES = 5;
|
|
96
|
+
|
|
97
|
+
function coarseAgentTime(durationMs: number): string {
|
|
98
|
+
const totalMinutes = Math.floor(durationMs / 60_000);
|
|
99
|
+
const bucketMinutes = Math.floor(totalMinutes / AGENT_TIME_BUCKET_MINUTES) * AGENT_TIME_BUCKET_MINUTES;
|
|
100
|
+
if (bucketMinutes < AGENT_TIME_BUCKET_MINUTES) return `less than ${AGENT_TIME_BUCKET_MINUTES} minutes`;
|
|
101
|
+
const hours = Math.floor(bucketMinutes / 60);
|
|
102
|
+
const minutes = bucketMinutes % 60;
|
|
103
|
+
if (hours === 0) return `about ${minutes} minutes`;
|
|
104
|
+
const hourLabel = `${hours} hour${hours === 1 ? "" : "s"}`;
|
|
105
|
+
return minutes === 0 ? `about ${hourLabel}` : `about ${hourLabel} ${minutes} minutes`;
|
|
106
|
+
}
|
|
83
107
|
const CONTEMPLATOR_MESSAGE = "om.contemplator.message";
|
|
84
108
|
const CONTEMPLATOR_STATE = "om.contemplator.state";
|
|
85
109
|
const CONTEMPLATOR_SUGGESTION = "om.contemplator.suggestion";
|
|
110
|
+
const REVIEW_PROPOSAL_MESSAGE = "om.review.proposal";
|
|
86
111
|
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
112
|
const ReviewScopeSchema = Type.Union([Type.Literal("workflow"), Type.Literal("software")]);
|
|
88
113
|
export const RequestReviewSchema = Type.Object({
|
|
@@ -149,13 +174,34 @@ export class Contemplator {
|
|
|
149
174
|
private latestCtx: MemoryUpdateCtx | undefined;
|
|
150
175
|
private turnsSinceRun = 0;
|
|
151
176
|
private restoredTipId: string | undefined;
|
|
177
|
+
/** Start of the unpersisted portion of the current main-agent run. */
|
|
178
|
+
private agentActiveSince: number | undefined;
|
|
152
179
|
|
|
153
180
|
constructor(private readonly pi: ExtensionAPI, private readonly runtime: Runtime) {}
|
|
154
181
|
|
|
155
182
|
register(): void {
|
|
183
|
+
this.pi.registerMessageRenderer(CONTEMPLATOR_SUGGESTION, (message, _options, theme) => {
|
|
184
|
+
const content = customMessageText(message.content).replace(/^Background contemplator probe \(advisory\):\n?/, "");
|
|
185
|
+
const box = new Box(1, 1, (text) => theme.bg("customMessageBg", text));
|
|
186
|
+
box.addChild(new Text(theme.fg("thinkingHigh", `${theme.bold("◆ CONTEMPLATOR PROBE")}\n${content}`), 0, 0));
|
|
187
|
+
return box;
|
|
188
|
+
});
|
|
189
|
+
this.pi.registerMessageRenderer(REVIEW_PROPOSAL_MESSAGE, (message, _options, theme) => {
|
|
190
|
+
const box = new Box(1, 1, (text) => theme.bg("customMessageBg", text));
|
|
191
|
+
box.addChild(new Text(theme.fg("thinkingHigh", `${theme.bold("◆ CONTEMPLATOR REVIEW")}\n${customMessageText(message.content)}`), 0, 0));
|
|
192
|
+
return box;
|
|
193
|
+
});
|
|
156
194
|
this.runtime.setMemoryUpdateListener((ctx) => this.withDebugContext(ctx, () => this.observeTurn(ctx)));
|
|
195
|
+
this.pi.on("agent_start", () => {
|
|
196
|
+
this.agentActiveSince = Date.now();
|
|
197
|
+
});
|
|
198
|
+
this.pi.on("agent_end", (_event: unknown, ctx: ExtensionContext) => {
|
|
199
|
+
this.persistAgentActivity(ctx);
|
|
200
|
+
this.agentActiveSince = undefined;
|
|
201
|
+
});
|
|
157
202
|
this.pi.on("session_start", (event: any, ctx: ExtensionContext) => {
|
|
158
203
|
this.sessionGeneration++;
|
|
204
|
+
this.agentActiveSince = undefined;
|
|
159
205
|
// AgentSession preserves its steering queue across extension reloads. An
|
|
160
206
|
// undelivered tracking entry therefore still has a live queued message;
|
|
161
207
|
// restoring it here would enqueue the same probe a second time.
|
|
@@ -164,11 +210,13 @@ export class Contemplator {
|
|
|
164
210
|
});
|
|
165
211
|
this.pi.on("session_tree", (_event: any, ctx: ExtensionContext) => {
|
|
166
212
|
this.sessionGeneration++;
|
|
213
|
+
this.agentActiveSince = undefined;
|
|
167
214
|
// Pending steering messages remain queued while navigating the tree.
|
|
168
215
|
this.restore(ctx, true, true);
|
|
169
216
|
});
|
|
170
217
|
this.pi.on("session_shutdown", () => {
|
|
171
218
|
this.sessionGeneration++;
|
|
219
|
+
this.agentActiveSince = undefined;
|
|
172
220
|
this.history = [];
|
|
173
221
|
this.pending = undefined;
|
|
174
222
|
this.seenObservationIds.clear();
|
|
@@ -212,11 +260,23 @@ export class Contemplator {
|
|
|
212
260
|
}
|
|
213
261
|
});
|
|
214
262
|
this.pi.on("turn_end", (_event: any, ctx: ExtensionContext) => {
|
|
263
|
+
this.persistAgentActivity(ctx);
|
|
215
264
|
this.turnsSinceRun++;
|
|
216
265
|
this.withDebugContext(ctx, () => this.observeTurn(ctx));
|
|
217
266
|
});
|
|
218
267
|
}
|
|
219
268
|
|
|
269
|
+
private persistAgentActivity(ctx: MemoryUpdateCtx): void {
|
|
270
|
+
const startedAt = this.agentActiveSince;
|
|
271
|
+
if (startedAt === undefined) return;
|
|
272
|
+
const endedAt = Date.now();
|
|
273
|
+
this.agentActiveSince = endedAt;
|
|
274
|
+
const durationMs = Math.max(0, endedAt - startedAt);
|
|
275
|
+
if (durationMs === 0) return;
|
|
276
|
+
this.pi.appendEntry(OM_AGENT_ACTIVITY, { version: 1, durationMs, endedAt });
|
|
277
|
+
debugLog("agent.activity_recorded", { durationMs });
|
|
278
|
+
}
|
|
279
|
+
|
|
220
280
|
private withDebugContext<T>(ctx: MemoryUpdateCtx, fn: () => T): T {
|
|
221
281
|
this.runtime.ensureConfig(ctx.cwd);
|
|
222
282
|
const sessionManager = ctx.sessionManager as { getSessionId?: () => string; getSessionFile?: () => string };
|
|
@@ -337,7 +397,8 @@ export class Contemplator {
|
|
|
337
397
|
debugLog("contemplator.skipped", { reason: "passive" });
|
|
338
398
|
return;
|
|
339
399
|
}
|
|
340
|
-
const
|
|
400
|
+
const branchEntries = ctx.sessionManager.getBranch() as Entry[];
|
|
401
|
+
const projection = fullProjection(branchEntries);
|
|
341
402
|
const observations = projection.observations.map((item) => `[${item.id}] ${item.content}`);
|
|
342
403
|
const reflections = projection.reflections.map((item) => `[${item.id}] ${item.content}`);
|
|
343
404
|
const reviews = projection.reviews ?? [];
|
|
@@ -365,11 +426,18 @@ export class Contemplator {
|
|
|
365
426
|
observations: mergeMemoryLines(this.pending?.observations ?? [], newObservations),
|
|
366
427
|
reflections: mergeMemoryLines(this.pending?.reflections ?? [], newReflections),
|
|
367
428
|
reviews: mergeMemoryLines(this.pending?.reviews ?? [], newReviews),
|
|
368
|
-
mainAgentOutputTokens: assistantOutputTokens(
|
|
369
|
-
mainAgentToolCalls: assistantToolCallCount(
|
|
429
|
+
mainAgentOutputTokens: assistantOutputTokens(branchEntries),
|
|
430
|
+
mainAgentToolCalls: assistantToolCallCount(branchEntries),
|
|
431
|
+
mainAgentActiveTimeMs: agentActiveTimeMs(branchEntries),
|
|
370
432
|
};
|
|
371
433
|
}
|
|
372
434
|
if (!this.pending) return;
|
|
435
|
+
// Activity values are cumulative send-time snapshots, not values frozen when
|
|
436
|
+
// the first memory entered a pending batch. This includes work performed
|
|
437
|
+
// while that batch waits for its memory/turn thresholds.
|
|
438
|
+
this.pending.mainAgentOutputTokens = assistantOutputTokens(branchEntries);
|
|
439
|
+
this.pending.mainAgentToolCalls = assistantToolCallCount(branchEntries);
|
|
440
|
+
this.pending.mainAgentActiveTimeMs = agentActiveTimeMs(branchEntries);
|
|
373
441
|
const enoughMemories = this.pending.reviews.length > 0 || this.pending.observations.length >= this.runtime.config.contemplatorMinNewObservations || this.pending.reflections.length >= this.runtime.config.contemplatorMinNewReflections;
|
|
374
442
|
if (!enoughMemories || this.turnsSinceRun < this.runtime.config.contemplatorMinTurns) {
|
|
375
443
|
debugLog("contemplator.waiting", {
|
|
@@ -430,6 +498,7 @@ export class Contemplator {
|
|
|
430
498
|
reviews: mergeMemoryLines(pending?.reviews ?? [], update.reviews),
|
|
431
499
|
mainAgentOutputTokens: update.mainAgentOutputTokens,
|
|
432
500
|
mainAgentToolCalls: update.mainAgentToolCalls,
|
|
501
|
+
mainAgentActiveTimeMs: update.mainAgentActiveTimeMs,
|
|
433
502
|
};
|
|
434
503
|
this.turnsSinceRun = turnsBeforeRun;
|
|
435
504
|
}
|
|
@@ -454,7 +523,7 @@ export class Contemplator {
|
|
|
454
523
|
const interventionInstruction = reviewerEnabled
|
|
455
524
|
? "Use send_probe for one focused question, or request_review only when a deeper workflow or software review is justified. Use no more than one intervention."
|
|
456
525
|
: "Use send_probe only when one focused question is materially useful. Use no more than one intervention.";
|
|
457
|
-
const prompt: Message = { role: "user", content: [{ type: "text", text: `NEW MEMORY UPDATE\n\n${updateBody}\n\
|
|
526
|
+
const prompt: Message = { role: "user", content: [{ type: "text", text: `NEW MEMORY UPDATE\n\n${updateBody}\n\nCUMULATIVE ACTIVITY: ${update.mainAgentOutputTokens} generated tokens; ${update.mainAgentToolCalls} tool calls; ${coarseAgentTime(update.mainAgentActiveTimeMs)} active.\n\nConsider these updates in the context of the accumulated memories. Prioritize reasoning gaps, contradictions, user-intent alignment, relevant overlooked alternatives, well-supported loops, and recurring structural patterns. ${interventionInstruction}` }], timestamp: Date.now() };
|
|
458
527
|
promptMessage = prompt;
|
|
459
528
|
this.history.push(prompt);
|
|
460
529
|
let intervention: Intervention | undefined;
|
|
@@ -556,6 +625,7 @@ export class Contemplator {
|
|
|
556
625
|
reviews: mergeMemoryLines(pending?.reviews ?? [], update.reviews),
|
|
557
626
|
mainAgentOutputTokens: update.mainAgentOutputTokens,
|
|
558
627
|
mainAgentToolCalls: update.mainAgentToolCalls,
|
|
628
|
+
mainAgentActiveTimeMs: update.mainAgentActiveTimeMs,
|
|
559
629
|
};
|
|
560
630
|
this.turnsSinceRun = turnsBeforeRun;
|
|
561
631
|
}
|
|
@@ -575,7 +645,7 @@ export class Contemplator {
|
|
|
575
645
|
this.pi.sendMessage({
|
|
576
646
|
customType: CONTEMPLATOR_SUGGESTION,
|
|
577
647
|
content: `Background contemplator probe (advisory):\n${question}`,
|
|
578
|
-
display:
|
|
648
|
+
display: this.runtime.config.showContemplatorMessages,
|
|
579
649
|
details: { version: 1, question, source, probeId },
|
|
580
650
|
}, { deliverAs: "steer", triggerTurn: false });
|
|
581
651
|
// sendMessage queues synchronously. Mark every source (not only restore)
|
|
@@ -657,7 +727,7 @@ export class Contemplator {
|
|
|
657
727
|
debugLog(result.outcome === "proposal" ? "reviewer.proposal_created" : "reviewer.no_proposal", { reviewRequestId: request.id, reviewMemoryId: result.id, scope: result.scope });
|
|
658
728
|
if (result.outcome === "proposal") {
|
|
659
729
|
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:
|
|
730
|
+
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
731
|
this.pi.appendEntry(OM_REVIEWER_NOTICE, { version: 1, reviewRequestId: request.id, reviewMemoryId: result.id, scope: result.scope, content: notice });
|
|
662
732
|
this.markTipPersisted(ctx);
|
|
663
733
|
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
|
@@ -3,6 +3,7 @@ import { observationPoolMetrics } from "../agents/dropper/pool.js";
|
|
|
3
3
|
import { resolveCompactAfterTokens } from "../config.js";
|
|
4
4
|
import type { Runtime } from "../runtime.js";
|
|
5
5
|
import {
|
|
6
|
+
agentActiveTimeMs,
|
|
6
7
|
diffProjection,
|
|
7
8
|
foldLedger,
|
|
8
9
|
fullProjection,
|
|
@@ -26,6 +27,16 @@ function formatTokens(n: number): string {
|
|
|
26
27
|
return String(n);
|
|
27
28
|
}
|
|
28
29
|
|
|
30
|
+
function formatDuration(durationMs: number): string {
|
|
31
|
+
const totalSeconds = Math.floor(durationMs / 1_000);
|
|
32
|
+
const hours = Math.floor(totalSeconds / 3_600);
|
|
33
|
+
const minutes = Math.floor((totalSeconds % 3_600) / 60);
|
|
34
|
+
const seconds = totalSeconds % 60;
|
|
35
|
+
if (hours > 0) return `${hours}h ${minutes}m ${seconds}s`;
|
|
36
|
+
if (minutes > 0) return `${minutes}m ${seconds}s`;
|
|
37
|
+
return `${seconds}s`;
|
|
38
|
+
}
|
|
39
|
+
|
|
29
40
|
function truncateStatusText(value: string, limit = 1_000): string {
|
|
30
41
|
return value.length <= limit ? value : `${value.slice(0, limit - 1)}…`;
|
|
31
42
|
}
|
|
@@ -99,9 +110,11 @@ export function registerStatusCommand(pi: ExtensionAPI, runtime: Runtime): void
|
|
|
99
110
|
`Visible observation pool: ~${visibleObservationTokens.toLocaleString()} / ${runtime.config.observationsPoolMaxTokens.toLocaleString()} tokens (${pct(visibleObservationTokens, runtime.config.observationsPoolMaxTokens)}%)`,
|
|
100
111
|
`Active observation pool: ~${activeObservationPool.observationTokens.toLocaleString()} / ${runtime.config.observationsPoolTargetTokens.toLocaleString()} target tokens (${pct(activeObservationPool.observationTokens, runtime.config.observationsPoolTargetTokens)}%)`,
|
|
101
112
|
`Reflection pool: ~${visibleReflectionTokens.toLocaleString()} tokens`,
|
|
113
|
+
`Cumulative agent time: ${formatDuration(agentActiveTimeMs(entries))}`,
|
|
102
114
|
`Compaction observer: ${runtime.config.compactionObserverEnabled === false ? "disabled" : "enabled"}`,
|
|
103
115
|
`Contemplator: ${runtime.config.contemplatorEnabled ? "enabled" : "disabled"}`,
|
|
104
116
|
`Contemplator model: ${runtime.config.contemplatorModel ? `${runtime.config.contemplatorModel.provider}/${runtime.config.contemplatorModel.id}` : "current session model"}`,
|
|
117
|
+
`Contemplator messages: ${runtime.config.showContemplatorMessages ? "visible" : "hidden"}`,
|
|
105
118
|
`Structural reviewer: ${runtime.config.reviewerEnabled === false ? "disabled" : "enabled"}`,
|
|
106
119
|
`Reviewer model: ${runtime.config.reviewerModel ? `${runtime.config.reviewerModel.provider}/${runtime.config.reviewerModel.id}` : "current session model"}`,
|
|
107
120
|
];
|
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",
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { estimateEntryTokens } from "../tokens.js";
|
|
2
2
|
import {
|
|
3
|
+
OM_AGENT_ACTIVITY,
|
|
3
4
|
OM_OBSERVATIONS_DROPPED,
|
|
4
5
|
OM_OBSERVATIONS_RECORDED,
|
|
5
6
|
OM_REFLECTIONS_RECORDED,
|
|
@@ -52,6 +53,17 @@ export function assistantToolCallCount(entries: Entry[]): number {
|
|
|
52
53
|
return total;
|
|
53
54
|
}
|
|
54
55
|
|
|
56
|
+
/** Sum persisted main-agent active wall-clock time on the current branch. */
|
|
57
|
+
export function agentActiveTimeMs(entries: Entry[]): number {
|
|
58
|
+
let total = 0;
|
|
59
|
+
for (const entry of entries) {
|
|
60
|
+
if (entry.type !== "custom" || entry.customType !== OM_AGENT_ACTIVITY || !isObject(entry.data)) continue;
|
|
61
|
+
const durationMs = entry.data.durationMs;
|
|
62
|
+
if (typeof durationMs === "number" && Number.isFinite(durationMs) && durationMs >= 0) total += durationMs;
|
|
63
|
+
}
|
|
64
|
+
return total;
|
|
65
|
+
}
|
|
66
|
+
|
|
55
67
|
function isObject(value: unknown): value is Record<string, unknown> {
|
|
56
68
|
return typeof value === "object" && value !== null;
|
|
57
69
|
}
|
|
@@ -9,6 +9,8 @@ export const OM_REVIEWER_MESSAGE = "om.reviewer.message";
|
|
|
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";
|
|
12
|
+
/** Main-agent active wall-clock time, excluding idle waits for user input. */
|
|
13
|
+
export const OM_AGENT_ACTIVITY = "om.agent.activity";
|
|
12
14
|
export const OM_FOLDED = "om.folded";
|
|
13
15
|
|
|
14
16
|
export const RELEVANCE_VALUES = ["low", "medium", "high", "critical"] as const;
|