@matthewfl/pi-contemplator 0.0.1
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/LICENSE +21 -0
- package/README.md +120 -0
- package/package.json +60 -0
- package/src/agents/contemplator/agent.ts +718 -0
- package/src/agents/contemplator/prompts.ts +212 -0
- package/src/agents/dropper/agent.ts +291 -0
- package/src/agents/dropper/coverage.ts +128 -0
- package/src/agents/dropper/pool.ts +67 -0
- package/src/agents/dropper/prompts.ts +48 -0
- package/src/agents/observer/agent.ts +207 -0
- package/src/agents/observer/prompts.ts +119 -0
- package/src/agents/reflector/agent.ts +213 -0
- package/src/agents/reflector/prompts.ts +81 -0
- package/src/agents/reviewer/agent.ts +187 -0
- package/src/agents/reviewer/history-tools.ts +337 -0
- package/src/agents/reviewer/prompts.ts +135 -0
- package/src/agents/reviewer/tools.ts +84 -0
- package/src/agents/stream-errors.ts +22 -0
- package/src/clipboard.ts +63 -0
- package/src/commands/contemplator-view.ts +128 -0
- package/src/commands/reviewer-view.ts +89 -0
- package/src/commands/settings.ts +257 -0
- package/src/commands/status.ts +176 -0
- package/src/commands/view.ts +171 -0
- package/src/config.ts +284 -0
- package/src/debug-log.ts +72 -0
- package/src/hooks/compaction-hook.ts +99 -0
- package/src/hooks/compaction-resume.ts +124 -0
- package/src/hooks/compaction-trigger.ts +122 -0
- package/src/hooks/consolidation-trigger.ts +488 -0
- package/src/ids.ts +5 -0
- package/src/index.ts +32 -0
- package/src/model-budget.ts +16 -0
- package/src/runtime.ts +316 -0
- package/src/serialize.ts +274 -0
- package/src/session-ledger/fold.ts +115 -0
- package/src/session-ledger/index.ts +7 -0
- package/src/session-ledger/progress.ts +156 -0
- package/src/session-ledger/projection.ts +243 -0
- package/src/session-ledger/recall.ts +258 -0
- package/src/session-ledger/render-summary.ts +31 -0
- package/src/session-ledger/search.ts +184 -0
- package/src/session-ledger/types.ts +329 -0
- package/src/tokens.ts +27 -0
- package/src/tools/compact-context.ts +54 -0
- package/src/tools/recall-observation.ts +532 -0
- package/src/tools/search-memories.ts +131 -0
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import { DynamicBorder, getSelectListTheme } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { Container, getKeybindings, Input, SelectList, Spacer, Text, fuzzyFilter, type Focusable, type SelectItem } from "@earendil-works/pi-tui";
|
|
4
|
+
import type { ConfiguredModel } from "../config.js";
|
|
5
|
+
import { OM_SETTINGS, type Runtime, type SessionSettings } from "../runtime.js";
|
|
6
|
+
|
|
7
|
+
type ModelRegistryLike = {
|
|
8
|
+
refresh?(): Promise<void>;
|
|
9
|
+
getAvailable(): Array<{ provider: string; id: string }>;
|
|
10
|
+
getAll(): Array<{ provider: string; id: string }>;
|
|
11
|
+
};
|
|
12
|
+
type NumberSetting = "observeAfterTokens" | "reflectAfterTokens" | "compactAfterTokens" | "observerChunkMaxTokens" | "observationsPoolMaxTokens" | "observationsPoolTargetTokens" | "agentMaxTurns" | "contemplatorMinNewObservations" | "contemplatorMinNewReflections" | "contemplatorMinTurns";
|
|
13
|
+
type BooleanSetting = "contemplatorEnabled" | "reviewerEnabled" | "compactionObserverEnabled" | "showWorkerNotifications" | "passive" | "debugLog";
|
|
14
|
+
|
|
15
|
+
function modelLabel(model: ConfiguredModel | undefined): string {
|
|
16
|
+
return model ? `${model.provider}/${model.id}` : "current session model";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function branch(ctx: ExtensionContext): readonly unknown[] {
|
|
20
|
+
return ctx.sessionManager.getBranch() as readonly unknown[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function appendSettings(pi: ExtensionAPI, runtime: Runtime, settings: SessionSettings): void {
|
|
24
|
+
runtime.setSessionSettings(settings);
|
|
25
|
+
pi.appendEntry(OM_SETTINGS, { version: 1, ...settings });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function hasOverride(settings: SessionSettings, key: string): boolean {
|
|
29
|
+
return Object.hasOwn(settings, key);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function scalarLabel(runtime: Runtime, key: NumberSetting | BooleanSetting | "compactAfterTokensRatio"): string {
|
|
33
|
+
const current = runtime.config[key];
|
|
34
|
+
const defaultValue = runtime.getDefaultConfig()[key];
|
|
35
|
+
const renderedDefault = defaultValue === undefined ? "derived" : String(defaultValue);
|
|
36
|
+
return hasOverride(runtime.getSessionSettings(), key) ? String(current) : `default (${renderedDefault})`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface ModelOption extends SelectItem {
|
|
40
|
+
configuredModel: ConfiguredModel | null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
class FilterableModelSelector extends Container implements Focusable {
|
|
44
|
+
private readonly searchInput = new Input();
|
|
45
|
+
private readonly listContainer = new Container();
|
|
46
|
+
private readonly allItems: ModelOption[];
|
|
47
|
+
private readonly done: (value: ConfiguredModel | null | undefined) => void;
|
|
48
|
+
private list!: SelectList;
|
|
49
|
+
private _focused = false;
|
|
50
|
+
|
|
51
|
+
get focused(): boolean {
|
|
52
|
+
return this._focused;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
set focused(value: boolean) {
|
|
56
|
+
this._focused = value;
|
|
57
|
+
this.searchInput.focused = value;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
constructor(
|
|
61
|
+
title: string,
|
|
62
|
+
models: ConfiguredModel[],
|
|
63
|
+
current: ConfiguredModel | undefined,
|
|
64
|
+
done: (value: ConfiguredModel | null | undefined) => void,
|
|
65
|
+
) {
|
|
66
|
+
super();
|
|
67
|
+
this.done = done;
|
|
68
|
+
this.allItems = [
|
|
69
|
+
{ value: "", label: "Use current session model", description: current ? `(current: ${modelLabel(current)})` : "(current session model)", configuredModel: null },
|
|
70
|
+
...models.map((model) => ({
|
|
71
|
+
value: `${model.provider}/${model.id}`,
|
|
72
|
+
label: `${model.provider}/${model.id}`,
|
|
73
|
+
description: current && model.provider === current.provider && model.id === current.id ? "(current)" : undefined,
|
|
74
|
+
configuredModel: model,
|
|
75
|
+
})),
|
|
76
|
+
];
|
|
77
|
+
this.addChild(new DynamicBorder());
|
|
78
|
+
this.addChild(new Spacer(1));
|
|
79
|
+
this.addChild(new Text(`${title} — type to filter; ↑/↓ to scroll; Enter to select`, 0, 0));
|
|
80
|
+
this.addChild(new Spacer(1));
|
|
81
|
+
this.addChild(this.searchInput);
|
|
82
|
+
this.addChild(new Spacer(1));
|
|
83
|
+
this.addChild(this.listContainer);
|
|
84
|
+
this.addChild(new Spacer(1));
|
|
85
|
+
this.addChild(new DynamicBorder());
|
|
86
|
+
this.searchInput.onSubmit = () => {
|
|
87
|
+
const selected = this.list.getSelectedItem();
|
|
88
|
+
if (selected) done((selected as ModelOption).configuredModel);
|
|
89
|
+
};
|
|
90
|
+
this.updateList();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
private updateList(): void {
|
|
94
|
+
const query = this.searchInput.getValue();
|
|
95
|
+
const filtered = query ? fuzzyFilter(this.allItems, query, (item) => item.value || item.label) : this.allItems;
|
|
96
|
+
this.list = new SelectList(filtered, 5, getSelectListTheme());
|
|
97
|
+
this.list.onSelect = (item) => this.done((item as ModelOption).configuredModel);
|
|
98
|
+
this.list.onCancel = () => this.done(undefined);
|
|
99
|
+
this.listContainer.clear();
|
|
100
|
+
this.listContainer.addChild(this.list);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
handleInput(keyData: string): void {
|
|
104
|
+
const kb = getKeybindings();
|
|
105
|
+
if (kb.matches(keyData, "tui.select.up") || kb.matches(keyData, "tui.select.down") || kb.matches(keyData, "tui.select.confirm") || kb.matches(keyData, "tui.select.cancel")) {
|
|
106
|
+
this.list.handleInput(keyData);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
this.searchInput.handleInput(keyData);
|
|
110
|
+
this.updateList();
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function chooseModel(ctx: ExtensionContext, current: ConfiguredModel | undefined, title: string): Promise<ConfiguredModel | null | undefined> {
|
|
115
|
+
const registry = ctx.modelRegistry as unknown as ModelRegistryLike;
|
|
116
|
+
await registry.refresh?.();
|
|
117
|
+
const available = registry.getAvailable();
|
|
118
|
+
const models = available.length > 0 ? available : registry.getAll();
|
|
119
|
+
if (models.length === 0) {
|
|
120
|
+
ctx.ui.notify("No configured models are available.", "warning");
|
|
121
|
+
return undefined;
|
|
122
|
+
}
|
|
123
|
+
return ctx.ui.custom((_tui, _theme, _keybindings, done) => new FilterableModelSelector(title, models, current, done));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function editNumber(ctx: ExtensionContext, runtime: Runtime, key: NumberSetting, title: string): Promise<number | undefined> {
|
|
127
|
+
const value = await ctx.ui.input(`${title} (current: ${scalarLabel(runtime, key)})`, "positive integer; blank cancels");
|
|
128
|
+
if (value === undefined || value.trim() === "") return undefined;
|
|
129
|
+
const parsed = Number(value.trim());
|
|
130
|
+
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
131
|
+
ctx.ui.notify("Value must be a positive integer.", "warning");
|
|
132
|
+
return undefined;
|
|
133
|
+
}
|
|
134
|
+
return parsed;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function validObservationPoolOverride(ctx: ExtensionContext, runtime: Runtime, key: NumberSetting, value: number): boolean {
|
|
138
|
+
if (key === "observationsPoolMaxTokens" && value <= runtime.config.observationsPoolTargetTokens) {
|
|
139
|
+
ctx.ui.notify("Observation pool max must be greater than the current target. Lower the target first.", "warning");
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
if (key === "observationsPoolTargetTokens" && value >= runtime.config.observationsPoolMaxTokens) {
|
|
143
|
+
ctx.ui.notify("Observation pool target must be less than the current maximum.", "warning");
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function registerSettingsCommand(pi: ExtensionAPI, runtime: Runtime): void {
|
|
150
|
+
const restoreSettings = (_event: unknown, ctx: ExtensionContext) => {
|
|
151
|
+
runtime.ensureConfig(ctx.cwd);
|
|
152
|
+
runtime.restoreSessionSettings(branch(ctx));
|
|
153
|
+
};
|
|
154
|
+
pi.on("session_start", restoreSettings);
|
|
155
|
+
pi.on("session_tree", restoreSettings);
|
|
156
|
+
|
|
157
|
+
pi.registerCommand("om:settings", {
|
|
158
|
+
description: "Configure observational memory for this session",
|
|
159
|
+
handler: async (args, ctx) => {
|
|
160
|
+
runtime.ensureConfig(ctx.cwd);
|
|
161
|
+
runtime.restoreSessionSettings(branch(ctx));
|
|
162
|
+
const argument = typeof args === "string" ? args.trim().toLowerCase() : "";
|
|
163
|
+
if (argument === "on" || argument === "off") {
|
|
164
|
+
appendSettings(pi, runtime, { contemplatorEnabled: argument === "on" });
|
|
165
|
+
ctx.ui.notify(`Contemplation: ${argument === "on" ? "enabled" : "disabled"} for this session.`, "info");
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
if (argument === "compaction on" || argument === "compaction off") {
|
|
169
|
+
appendSettings(pi, runtime, { compactionObserverEnabled: argument.endsWith("on") });
|
|
170
|
+
ctx.ui.notify(`Compaction observer: ${argument.endsWith("on") ? "enabled" : "disabled"} for this session.`, "info");
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
if (argument === "reviewer on" || argument === "reviewer off") {
|
|
174
|
+
appendSettings(pi, runtime, { reviewerEnabled: argument.endsWith("on") });
|
|
175
|
+
ctx.ui.notify(`Structural reviewer: ${argument.endsWith("on") ? "enabled" : "disabled"} for this session.`, "info");
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
if (argument) {
|
|
179
|
+
ctx.ui.notify("Usage: /om:settings [on|off|reviewer on|reviewer off|compaction on|compaction off]", "info");
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
while (true) {
|
|
184
|
+
const settings = runtime.getSessionSettings();
|
|
185
|
+
const choice = await ctx.ui.select("Observational memory settings (session overrides)", [
|
|
186
|
+
`Contemplation: ${scalarLabel(runtime, "contemplatorEnabled")}`,
|
|
187
|
+
`Contemplation model: ${hasOverride(settings, "contemplatorModel") ? modelLabel(runtime.config.contemplatorModel) : `default (${modelLabel(runtime.getDefaultConfig().contemplatorModel)})`}`,
|
|
188
|
+
`Structural reviewer: ${scalarLabel(runtime, "reviewerEnabled")}`,
|
|
189
|
+
`Structural reviewer model: ${hasOverride(settings, "reviewerModel") ? modelLabel(runtime.config.reviewerModel) : `default (${modelLabel(runtime.getDefaultConfig().reviewerModel)})`}`,
|
|
190
|
+
`Compaction observer: ${scalarLabel(runtime, "compactionObserverEnabled")}`,
|
|
191
|
+
`Memory worker model: ${hasOverride(settings, "model") ? modelLabel(runtime.config.model) : `default (${modelLabel(runtime.getDefaultConfig().model)})`}`,
|
|
192
|
+
`Observation threshold: ${scalarLabel(runtime, "observeAfterTokens")}`,
|
|
193
|
+
`Reflection threshold: ${scalarLabel(runtime, "reflectAfterTokens")}`,
|
|
194
|
+
`Compaction threshold: ${scalarLabel(runtime, "compactAfterTokens")}`,
|
|
195
|
+
`Compaction mode: ${hasOverride(settings, "compactAfterTokensMode") ? runtime.config.compactAfterTokensMode : `default (${runtime.getDefaultConfig().compactAfterTokensMode})`}`,
|
|
196
|
+
`Compaction ratio: ${hasOverride(settings, "compactAfterTokensRatio") ? runtime.config.compactAfterTokensRatio : `default (${runtime.getDefaultConfig().compactAfterTokensRatio})`}`,
|
|
197
|
+
`Observer chunk limit: ${scalarLabel(runtime, "observerChunkMaxTokens")}`,
|
|
198
|
+
`Observation pool max: ${scalarLabel(runtime, "observationsPoolMaxTokens")}`,
|
|
199
|
+
`Observation pool target: ${scalarLabel(runtime, "observationsPoolTargetTokens")}`,
|
|
200
|
+
`Worker max turns: ${scalarLabel(runtime, "agentMaxTurns")}`,
|
|
201
|
+
`Contemplation observation trigger: ${scalarLabel(runtime, "contemplatorMinNewObservations")}`,
|
|
202
|
+
`Contemplation reflection trigger: ${scalarLabel(runtime, "contemplatorMinNewReflections")}`,
|
|
203
|
+
`Contemplation turn interval: ${scalarLabel(runtime, "contemplatorMinTurns")}`,
|
|
204
|
+
`Worker notifications: ${scalarLabel(runtime, "showWorkerNotifications")}`,
|
|
205
|
+
`Passive mode: ${scalarLabel(runtime, "passive")}`,
|
|
206
|
+
`Debug logging: ${scalarLabel(runtime, "debugLog")}`,
|
|
207
|
+
"Done",
|
|
208
|
+
]);
|
|
209
|
+
if (!choice || choice === "Done") return;
|
|
210
|
+
if (choice.startsWith("Contemplation:")) appendSettings(pi, runtime, { contemplatorEnabled: !runtime.config.contemplatorEnabled });
|
|
211
|
+
else if (choice.startsWith("Structural reviewer:")) appendSettings(pi, runtime, { reviewerEnabled: !runtime.config.reviewerEnabled });
|
|
212
|
+
else if (choice.startsWith("Compaction observer:")) appendSettings(pi, runtime, { compactionObserverEnabled: !runtime.config.compactionObserverEnabled });
|
|
213
|
+
else if (choice.startsWith("Worker notifications:")) appendSettings(pi, runtime, { showWorkerNotifications: !runtime.config.showWorkerNotifications });
|
|
214
|
+
else if (choice.startsWith("Passive mode:")) appendSettings(pi, runtime, { passive: !runtime.config.passive });
|
|
215
|
+
else if (choice.startsWith("Debug logging:")) appendSettings(pi, runtime, { debugLog: !runtime.config.debugLog });
|
|
216
|
+
else if (choice.startsWith("Contemplation model:")) {
|
|
217
|
+
const model = await chooseModel(ctx, runtime.config.contemplatorModel, "Contemplation model");
|
|
218
|
+
if (model !== undefined) appendSettings(pi, runtime, { contemplatorModel: model });
|
|
219
|
+
} else if (choice.startsWith("Structural reviewer model:")) {
|
|
220
|
+
const model = await chooseModel(ctx, runtime.config.reviewerModel, "Structural reviewer model");
|
|
221
|
+
if (model !== undefined) appendSettings(pi, runtime, { reviewerModel: model });
|
|
222
|
+
} else if (choice.startsWith("Memory worker model:")) {
|
|
223
|
+
const model = await chooseModel(ctx, runtime.config.model, "Memory worker model");
|
|
224
|
+
if (model !== undefined) appendSettings(pi, runtime, { model });
|
|
225
|
+
} else if (choice.startsWith("Compaction mode:")) {
|
|
226
|
+
const mode = await ctx.ui.select("Compaction threshold mode", ["calibrated", "ratio"]);
|
|
227
|
+
if (mode === "calibrated" || mode === "ratio") appendSettings(pi, runtime, { compactAfterTokensMode: mode });
|
|
228
|
+
} else if (choice.startsWith("Compaction ratio:")) {
|
|
229
|
+
const value = await ctx.ui.input(`Compaction ratio (current: ${scalarLabel(runtime, "compactAfterTokensRatio")})`, "decimal between 0 and 1");
|
|
230
|
+
const ratio = value === undefined ? undefined : Number(value.trim());
|
|
231
|
+
if (ratio !== undefined && Number.isFinite(ratio) && ratio > 0 && ratio < 1) appendSettings(pi, runtime, { compactAfterTokensRatio: ratio });
|
|
232
|
+
else if (value !== undefined) ctx.ui.notify("Ratio must be a number between 0 and 1.", "warning");
|
|
233
|
+
} else {
|
|
234
|
+
const numberChoice: Array<[string, NumberSetting, string]> = [
|
|
235
|
+
["Observation threshold:", "observeAfterTokens", "Observation threshold"],
|
|
236
|
+
["Reflection threshold:", "reflectAfterTokens", "Reflection threshold"],
|
|
237
|
+
["Compaction threshold:", "compactAfterTokens", "Compaction threshold"],
|
|
238
|
+
["Observer chunk limit:", "observerChunkMaxTokens", "Observer chunk limit"],
|
|
239
|
+
["Observation pool max:", "observationsPoolMaxTokens", "Observation pool max"],
|
|
240
|
+
["Observation pool target:", "observationsPoolTargetTokens", "Observation pool target"],
|
|
241
|
+
["Worker max turns:", "agentMaxTurns", "Worker max turns"],
|
|
242
|
+
["Contemplation observation trigger:", "contemplatorMinNewObservations", "Contemplation observation trigger"],
|
|
243
|
+
["Contemplation reflection trigger:", "contemplatorMinNewReflections", "Contemplation reflection trigger"],
|
|
244
|
+
["Contemplation turn interval:", "contemplatorMinTurns", "Contemplation turn interval"],
|
|
245
|
+
];
|
|
246
|
+
const selected = numberChoice.find(([prefix]) => choice.startsWith(prefix));
|
|
247
|
+
if (selected) {
|
|
248
|
+
const value = await editNumber(ctx, runtime, selected[1], selected[2]);
|
|
249
|
+
if (value !== undefined && validObservationPoolOverride(ctx, runtime, selected[1], value)) {
|
|
250
|
+
appendSettings(pi, runtime, { [selected[1]]: value } as SessionSettings);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
},
|
|
256
|
+
});
|
|
257
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { observationPoolMetrics } from "../agents/dropper/pool.js";
|
|
3
|
+
import { resolveCompactAfterTokens } from "../config.js";
|
|
4
|
+
import type { Runtime } from "../runtime.js";
|
|
5
|
+
import {
|
|
6
|
+
diffProjection,
|
|
7
|
+
foldLedger,
|
|
8
|
+
fullProjection,
|
|
9
|
+
rawTokensSinceLastCompaction,
|
|
10
|
+
rawTokensSinceObservationCoverage,
|
|
11
|
+
rawTokensSinceReflectionCoverage,
|
|
12
|
+
visibleProjection,
|
|
13
|
+
type Entry,
|
|
14
|
+
} from "../session-ledger/index.js";
|
|
15
|
+
|
|
16
|
+
const CONTEMPLATOR_SUGGESTION = "om.contemplator.suggestion";
|
|
17
|
+
const REVIEWER_NOTICE = "om.reviewer.notice";
|
|
18
|
+
|
|
19
|
+
function pct(current: number, total: number): number {
|
|
20
|
+
return total > 0 ? Math.round((current / total) * 100) : 0;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function formatTokens(n: number): string {
|
|
24
|
+
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1).replace(/\.0$/, "")}M`;
|
|
25
|
+
if (n >= 1_000) return `${(n / 1_000).toFixed(1).replace(/\.0$/, "")}k`;
|
|
26
|
+
return String(n);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function truncateStatusText(value: string, limit = 1_000): string {
|
|
30
|
+
return value.length <= limit ? value : `${value.slice(0, limit - 1)}…`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function tokenSum(items: { tokenCount: number }[]): number {
|
|
34
|
+
return items.reduce((sum, item) => sum + item.tokenCount, 0);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function addedSuffix(count: number): string | undefined {
|
|
38
|
+
return count > 0 ? `+${count.toLocaleString()}` : undefined;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function removedSuffix(count: number): string | undefined {
|
|
42
|
+
return count > 0 ? `-${count.toLocaleString()}` : undefined;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function appendSuffixes(line: string, suffixes: (string | undefined)[]): string {
|
|
46
|
+
const rendered = suffixes.filter((suffix): suffix is string => suffix !== undefined);
|
|
47
|
+
return rendered.length > 0 ? `${line} ${rendered.join(" ")}` : line;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function registerStatusCommand(pi: ExtensionAPI, runtime: Runtime): void {
|
|
51
|
+
pi.registerCommand("om:status", {
|
|
52
|
+
description: "Show observational memory status",
|
|
53
|
+
handler: async (_args, ctx) => {
|
|
54
|
+
runtime.ensureConfig(ctx.cwd);
|
|
55
|
+
const entries = ctx.sessionManager.getBranch() as Entry[];
|
|
56
|
+
const folded = foldLedger(entries);
|
|
57
|
+
const visible = visibleProjection(entries);
|
|
58
|
+
const full = fullProjection(entries);
|
|
59
|
+
const drift = diffProjection(visible, full);
|
|
60
|
+
|
|
61
|
+
const visibleObservationTokens = tokenSum(visible.observations);
|
|
62
|
+
const visibleReflectionTokens = tokenSum(visible.reflections);
|
|
63
|
+
const activeObservationPool = observationPoolMetrics(folded.activeObservations, runtime.config.observationsPoolTargetTokens);
|
|
64
|
+
const observationLine = appendSuffixes(
|
|
65
|
+
`Observations: ${folded.observations.length} recorded / ${folded.droppedObservationIds.size} dropped / ${folded.activeObservations.length} active / ${visible.observations.length} visible`,
|
|
66
|
+
[
|
|
67
|
+
addedSuffix(drift.observationsOnlyInFull.length),
|
|
68
|
+
removedSuffix(drift.droppedOnlyInFull.length),
|
|
69
|
+
],
|
|
70
|
+
);
|
|
71
|
+
const reflectionLine = appendSuffixes(
|
|
72
|
+
`Reflections: ${folded.reflections.length} recorded / ${visible.reflections.length} visible`,
|
|
73
|
+
[addedSuffix(drift.reflectionsOnlyInFull.length)],
|
|
74
|
+
);
|
|
75
|
+
const obsProgress = rawTokensSinceObservationCoverage(entries);
|
|
76
|
+
const reflectionProgress = rawTokensSinceReflectionCoverage(entries);
|
|
77
|
+
const compactionProgress = rawTokensSinceLastCompaction(entries);
|
|
78
|
+
const contextWindow = typeof ctx.model?.contextWindow === "number" ? ctx.model.contextWindow : undefined;
|
|
79
|
+
const compactThreshold = resolveCompactAfterTokens(runtime.config, contextWindow);
|
|
80
|
+
|
|
81
|
+
const passiveLines = runtime.config.passive === true
|
|
82
|
+
? [
|
|
83
|
+
"── Mode ──",
|
|
84
|
+
"Passive: automatic memory workers and auto-compaction disabled; manual/Pi compaction, commands, and recall remain active",
|
|
85
|
+
"",
|
|
86
|
+
]
|
|
87
|
+
: [];
|
|
88
|
+
|
|
89
|
+
const lines = [
|
|
90
|
+
...passiveLines,
|
|
91
|
+
"── Memory ──",
|
|
92
|
+
observationLine,
|
|
93
|
+
reflectionLine,
|
|
94
|
+
"",
|
|
95
|
+
"── Activity ──",
|
|
96
|
+
`Next observation: ~${obsProgress.toLocaleString()} / ${runtime.config.observeAfterTokens.toLocaleString()} tokens (${pct(obsProgress, runtime.config.observeAfterTokens)}%)`,
|
|
97
|
+
`Next reflection: ~${reflectionProgress.toLocaleString()} / ${runtime.config.reflectAfterTokens.toLocaleString()} tokens (${pct(reflectionProgress, runtime.config.reflectAfterTokens)}%)`,
|
|
98
|
+
`Next compaction: ~${compactionProgress.toLocaleString()} / ${compactThreshold.toLocaleString()} tokens (${pct(compactionProgress, compactThreshold)}%)`,
|
|
99
|
+
`Visible observation pool: ~${visibleObservationTokens.toLocaleString()} / ${runtime.config.observationsPoolMaxTokens.toLocaleString()} tokens (${pct(visibleObservationTokens, runtime.config.observationsPoolMaxTokens)}%)`,
|
|
100
|
+
`Active observation pool: ~${activeObservationPool.observationTokens.toLocaleString()} / ${runtime.config.observationsPoolTargetTokens.toLocaleString()} target tokens (${pct(activeObservationPool.observationTokens, runtime.config.observationsPoolTargetTokens)}%)`,
|
|
101
|
+
`Reflection pool: ~${visibleReflectionTokens.toLocaleString()} tokens`,
|
|
102
|
+
`Compaction observer: ${runtime.config.compactionObserverEnabled === false ? "disabled" : "enabled"}`,
|
|
103
|
+
`Contemplator: ${runtime.config.contemplatorEnabled ? "enabled" : "disabled"}`,
|
|
104
|
+
`Contemplator model: ${runtime.config.contemplatorModel ? `${runtime.config.contemplatorModel.provider}/${runtime.config.contemplatorModel.id}` : "current session model"}`,
|
|
105
|
+
`Structural reviewer: ${runtime.config.reviewerEnabled === false ? "disabled" : "enabled"}`,
|
|
106
|
+
`Reviewer model: ${runtime.config.reviewerModel ? `${runtime.config.reviewerModel.provider}/${runtime.config.reviewerModel.id}` : "current session model"}`,
|
|
107
|
+
];
|
|
108
|
+
|
|
109
|
+
if (runtime.agentUsage.runs > 0) {
|
|
110
|
+
const u = runtime.agentUsage;
|
|
111
|
+
lines.push(`Token usage: ↑${formatTokens(u.input)} ↓${formatTokens(u.output)}${u.cacheRead ? ` R${formatTokens(u.cacheRead)}` : ""}${u.cacheWrite ? ` W${formatTokens(u.cacheWrite)}` : ""} $${u.cost.toFixed(3)} (${u.runs} call${u.runs === 1 ? "" : "s"})`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Probe stats come from the branch ledger (like /om:view contemplator):
|
|
115
|
+
// deduped by probeId so restore re-queues don't inflate the count, and
|
|
116
|
+
// entries without a probeId (sent before probe tracking existed) count
|
|
117
|
+
// individually. Survives reloads, unlike an in-memory counter.
|
|
118
|
+
const probeSuggestions: { suggestion: string }[] = [];
|
|
119
|
+
const probeIndexByProbeId = new Map<string, number>();
|
|
120
|
+
for (const entry of entries) {
|
|
121
|
+
if (entry.customType !== CONTEMPLATOR_SUGGESTION) continue;
|
|
122
|
+
const data = (entry.data ?? {}) as { suggestion?: unknown; probeId?: unknown };
|
|
123
|
+
if (typeof data.suggestion !== "string") continue;
|
|
124
|
+
if (typeof data.probeId !== "string") {
|
|
125
|
+
probeSuggestions.push({ suggestion: data.suggestion });
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
const existingIndex = probeIndexByProbeId.get(data.probeId);
|
|
129
|
+
if (existingIndex === undefined) {
|
|
130
|
+
probeIndexByProbeId.set(data.probeId, probeSuggestions.length);
|
|
131
|
+
probeSuggestions.push({ suggestion: data.suggestion });
|
|
132
|
+
} else {
|
|
133
|
+
probeSuggestions[existingIndex] = { suggestion: data.suggestion };
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (probeSuggestions.length > 0) {
|
|
137
|
+
lines.push(`Probes sent: ${probeSuggestions.length}`);
|
|
138
|
+
lines.push(`Last probe: ${probeSuggestions[probeSuggestions.length - 1].suggestion}`);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const latestReview = full.reviews?.at(-1);
|
|
142
|
+
if (latestReview) {
|
|
143
|
+
lines.push(`Last review: [${latestReview.id}] ${latestReview.scope} ${latestReview.outcome}`);
|
|
144
|
+
if (latestReview.outcome === "proposal") lines.push(`Last review summary: ${truncateStatusText(latestReview.summary)}`);
|
|
145
|
+
else lines.push(`Last review reason: ${truncateStatusText(latestReview.reason)}`);
|
|
146
|
+
}
|
|
147
|
+
let latestNotice: string | undefined;
|
|
148
|
+
for (const entry of entries) {
|
|
149
|
+
if (entry.customType !== REVIEWER_NOTICE || !entry.data || typeof entry.data !== "object") continue;
|
|
150
|
+
const content = (entry.data as { content?: unknown }).content;
|
|
151
|
+
if (typeof content === "string") latestNotice = content;
|
|
152
|
+
}
|
|
153
|
+
if (latestNotice) lines.push(`Last reviewer notice: ${truncateStatusText(latestNotice)}`);
|
|
154
|
+
|
|
155
|
+
if (runtime.consolidationInFlight || runtime.compactInFlight || runtime.compactHookInFlight || runtime.reviewInFlight) {
|
|
156
|
+
lines.push("", "── In flight ──");
|
|
157
|
+
if (runtime.consolidationInFlight) {
|
|
158
|
+
const phase = runtime.consolidationPhase ? ` (${runtime.consolidationPhase})` : "";
|
|
159
|
+
lines.push(`Consolidation: running${phase}`);
|
|
160
|
+
}
|
|
161
|
+
if (runtime.compactInFlight) lines.push("Auto-compaction: running");
|
|
162
|
+
if (runtime.compactHookInFlight) lines.push("Compaction hook: running");
|
|
163
|
+
if (runtime.reviewInFlight) lines.push("Structural review: running");
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (runtime.lastObserverError || runtime.lastReflectorError || runtime.lastDropperError) {
|
|
167
|
+
lines.push("", "── Last error ──");
|
|
168
|
+
if (runtime.lastObserverError) lines.push(`Observer: ${runtime.lastObserverError}`);
|
|
169
|
+
if (runtime.lastReflectorError) lines.push(`Reflector: ${runtime.lastReflectorError}`);
|
|
170
|
+
if (runtime.lastDropperError) lines.push(`Dropper: ${runtime.lastDropperError}`);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
ctx.ui.notify(lines.join("\n"), "info");
|
|
174
|
+
},
|
|
175
|
+
});
|
|
176
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { Runtime } from "../runtime.js";
|
|
3
|
+
import { copyTextToClipboard } from "../clipboard.js";
|
|
4
|
+
import { renderContemplator, stripAnsi } from "./contemplator-view.js";
|
|
5
|
+
import { renderReviewer } from "./reviewer-view.js";
|
|
6
|
+
import { executeRecall, formatRecallResultForTui } from "../tools/recall-observation.js";
|
|
7
|
+
import {
|
|
8
|
+
fullProjection,
|
|
9
|
+
observationToSummaryLine,
|
|
10
|
+
reflectionToSummaryLine,
|
|
11
|
+
visibleProjection,
|
|
12
|
+
type Entry,
|
|
13
|
+
type Projection,
|
|
14
|
+
} from "../session-ledger/index.js";
|
|
15
|
+
|
|
16
|
+
function argAt(args: unknown, index: number): string | undefined {
|
|
17
|
+
if (Array.isArray(args)) return typeof args[index] === "string" ? args[index] : undefined;
|
|
18
|
+
if (typeof args === "string") return args.trim().split(/\s+/)[index];
|
|
19
|
+
if (args && typeof args === "object" && "mode" in args && index === 0) {
|
|
20
|
+
const mode = (args as { mode?: unknown }).mode;
|
|
21
|
+
return typeof mode === "string" ? mode : undefined;
|
|
22
|
+
}
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function firstArg(args: unknown): string | undefined {
|
|
27
|
+
return argAt(args, 0);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function renderList<T>(
|
|
31
|
+
items: T[],
|
|
32
|
+
render: (item: T) => string,
|
|
33
|
+
empty: string,
|
|
34
|
+
): string {
|
|
35
|
+
return items.length > 0 ? items.map(render).join("\n") : empty;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function reviewSummaryLine(review: NonNullable<Projection["reviews"]>[number]): string {
|
|
39
|
+
if (review.outcome === "proposal") return `[${review.id}] ${review.scope} proposal: ${review.title} — ${review.summary}`;
|
|
40
|
+
return `[${review.id}] ${review.scope} review concluded with no proposal — ${review.reason}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function renderContentOnlyProjection(
|
|
44
|
+
projection: Projection,
|
|
45
|
+
emptyScope: "visible" | "recorded",
|
|
46
|
+
): string {
|
|
47
|
+
const lines = [
|
|
48
|
+
"── Reflections ──",
|
|
49
|
+
renderList(projection.reflections, reflectionToSummaryLine, `No ${emptyScope} reflections.`),
|
|
50
|
+
"",
|
|
51
|
+
"── Observations ──",
|
|
52
|
+
renderList(projection.observations, observationToSummaryLine, `No ${emptyScope} observations.`),
|
|
53
|
+
];
|
|
54
|
+
if (projection.reviews?.length) lines.push("", "── Advisory reviews ──", ...projection.reviews.map(reviewSummaryLine));
|
|
55
|
+
return lines.join("\n");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function hasMemory(projection: Projection): boolean {
|
|
59
|
+
return (
|
|
60
|
+
projection.reflections.length > 0 || projection.observations.length > 0 || (projection.reviews?.length ?? 0) > 0
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
interface ViewCommandOptions {
|
|
65
|
+
copyToClipboard?: (text: string) => Promise<boolean>;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function registerViewCommand(
|
|
69
|
+
pi: ExtensionAPI,
|
|
70
|
+
runtime: Runtime,
|
|
71
|
+
options: ViewCommandOptions = {},
|
|
72
|
+
): void {
|
|
73
|
+
const copyToClipboard = options.copyToClipboard ?? copyTextToClipboard;
|
|
74
|
+
|
|
75
|
+
pi.registerCommand("om:view", {
|
|
76
|
+
description:
|
|
77
|
+
"Print and copy observational memory content (visible, full, memory, contemplator, reviewer, or reviews)",
|
|
78
|
+
handler: async (args, ctx) => {
|
|
79
|
+
runtime.ensureConfig(ctx.cwd);
|
|
80
|
+
const entries = ctx.sessionManager.getBranch() as Entry[];
|
|
81
|
+
const mode = firstArg(args);
|
|
82
|
+
|
|
83
|
+
const notifyWithCopy = async (output: string) => {
|
|
84
|
+
const copied = await copyToClipboard(output).catch(() => false);
|
|
85
|
+
ctx.ui.notify(
|
|
86
|
+
copied
|
|
87
|
+
? `${output}\n\nCopied /om:view output to clipboard.`
|
|
88
|
+
: `${output}\n\nWarning: failed to copy /om:view output to clipboard.`,
|
|
89
|
+
"info",
|
|
90
|
+
);
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
if (mode === "memory") {
|
|
94
|
+
const memoryId = argAt(args, 1);
|
|
95
|
+
if (!memoryId) {
|
|
96
|
+
ctx.ui.notify("Usage: /om:view memory <12-character-memory-id>", "info");
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const recalled = executeRecall({ id: memoryId }, () => entries);
|
|
100
|
+
const output = recalled.details?.reviews.length
|
|
101
|
+
? recalled.content.filter((part): part is { type: "text"; text: string } => part.type === "text").map((part) => part.text).join("\n")
|
|
102
|
+
: formatRecallResultForTui(recalled, false);
|
|
103
|
+
await notifyWithCopy(output);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (mode === "contemplator") {
|
|
108
|
+
const output = renderContemplator(entries);
|
|
109
|
+
const copied = await copyToClipboard(stripAnsi(output)).catch(() => false);
|
|
110
|
+
ctx.ui.notify(
|
|
111
|
+
`${output}\n\n${copied ? "Copied /om:view contemplator output to clipboard." : "Warning: failed to copy /om:view contemplator output to clipboard."}`,
|
|
112
|
+
"info",
|
|
113
|
+
);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (mode === "reviewer") {
|
|
118
|
+
const output = renderReviewer(entries);
|
|
119
|
+
const copied = await copyToClipboard(stripAnsi(output)).catch(() => false);
|
|
120
|
+
ctx.ui.notify(
|
|
121
|
+
`${output}\n\n${copied ? "Copied /om:view reviewer output to clipboard." : "Warning: failed to copy /om:view reviewer output to clipboard."}`,
|
|
122
|
+
"info",
|
|
123
|
+
);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (mode === "reviews") {
|
|
128
|
+
const output = renderList(
|
|
129
|
+
fullProjection(entries).reviews ?? [],
|
|
130
|
+
reviewSummaryLine,
|
|
131
|
+
"No advisory reviews recorded.",
|
|
132
|
+
);
|
|
133
|
+
await notifyWithCopy(output);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (mode === "full") {
|
|
138
|
+
await notifyWithCopy(
|
|
139
|
+
renderContentOnlyProjection(fullProjection(entries), "recorded"),
|
|
140
|
+
);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (mode && mode !== "visible") {
|
|
145
|
+
ctx.ui.notify("Usage: /om:view [visible|full|memory <id>|contemplator|reviewer|reviews]", "info");
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const visible = visibleProjection(entries);
|
|
150
|
+
if (hasMemory(visible)) {
|
|
151
|
+
await notifyWithCopy(renderContentOnlyProjection(visible, "visible"));
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (mode === "visible") {
|
|
156
|
+
await notifyWithCopy(renderContentOnlyProjection(visible, "visible"));
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const recorded = fullProjection(entries);
|
|
161
|
+
if (hasMemory(recorded)) {
|
|
162
|
+
await notifyWithCopy(
|
|
163
|
+
`No visible memory has been folded into a compaction yet; showing recorded memory.\n\n${renderContentOnlyProjection(recorded, "recorded")}`,
|
|
164
|
+
);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
await notifyWithCopy(renderContentOnlyProjection(visible, "visible"));
|
|
169
|
+
},
|
|
170
|
+
});
|
|
171
|
+
}
|