@matthewfl/pi-contemplator 0.0.9 → 0.1.0
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 +17 -11
- package/package.json +8 -6
- package/src/agents/contemplator/agent.ts +325 -91
- package/src/agents/contemplator/prompts.ts +6 -6
- package/src/agents/observer/agent.ts +14 -6
- package/src/agents/observer/prompts.ts +16 -7
- package/src/agents/reviewer/agent.ts +24 -4
- package/src/agents/reviewer/prompts.ts +1 -1
- package/src/agents/reviewer/tools.ts +24 -9
- package/src/agents/stream-errors.ts +1 -1
- package/src/agents/summarizer/agent.ts +597 -0
- package/src/agents/summarizer/prompts.ts +46 -0
- package/src/agents/summarizer/sampling.ts +80 -0
- package/src/commands/contemplator-view.ts +22 -1
- package/src/commands/settings.ts +73 -69
- package/src/commands/status.ts +60 -36
- package/src/commands/summarizer-view.ts +58 -0
- package/src/commands/view.ts +22 -10
- package/src/config.ts +25 -32
- package/src/hooks/compaction-hook.ts +36 -19
- package/src/hooks/compaction-resume.ts +4 -4
- package/src/hooks/compaction-trigger.ts +96 -56
- package/src/hooks/consolidation-trigger.ts +213 -196
- package/src/memory-citations.ts +37 -0
- package/src/required-tool-choice.ts +28 -0
- package/src/runtime.ts +116 -33
- package/src/session-ledger/fold.ts +82 -53
- package/src/session-ledger/index.ts +1 -0
- package/src/session-ledger/pools.ts +77 -0
- package/src/session-ledger/progress.ts +7 -18
- package/src/session-ledger/projection.ts +45 -177
- package/src/session-ledger/recall.ts +129 -127
- package/src/session-ledger/render-summary.ts +20 -19
- package/src/session-ledger/search.ts +99 -115
- package/src/session-ledger/types.ts +102 -75
- package/src/tools/compact-context.ts +1 -1
- package/src/tools/recall-observation.ts +99 -459
- package/src/tools/search-memories.ts +31 -72
- package/src/agents/dropper/agent.ts +0 -291
- package/src/agents/dropper/coverage.ts +0 -128
- package/src/agents/dropper/pool.ts +0 -67
- package/src/agents/dropper/prompts.ts +0 -48
- package/src/agents/reflector/agent.ts +0 -213
- package/src/agents/reflector/prompts.ts +0 -81
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { estimateStringTokens } from "../../tokens.js";
|
|
2
|
+
import type { Observation, Summary } from "../../session-ledger/index.js";
|
|
3
|
+
|
|
4
|
+
export type SummarizerMemory =
|
|
5
|
+
| { kind: "observation"; memory: Observation }
|
|
6
|
+
| { kind: "summary"; memory: Summary };
|
|
7
|
+
|
|
8
|
+
export type SummarizerSample = {
|
|
9
|
+
memories: SummarizerMemory[];
|
|
10
|
+
sampled: boolean;
|
|
11
|
+
eligibleCount: number;
|
|
12
|
+
selectedCount: number;
|
|
13
|
+
eligibleTokens: number;
|
|
14
|
+
selectedTokens: number;
|
|
15
|
+
budgetTokens: number;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type SummarizerSamplingArgs = {
|
|
19
|
+
memories: SummarizerMemory[];
|
|
20
|
+
/** Maximum rendered old-memory input tokens before sampling. */
|
|
21
|
+
samplingThresholdTokens?: number;
|
|
22
|
+
random?: () => number;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
type Candidate = {
|
|
26
|
+
item: SummarizerMemory;
|
|
27
|
+
tokens: number;
|
|
28
|
+
weight: number;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export function renderSummarizerMemory(item: SummarizerMemory): string {
|
|
32
|
+
if (item.kind === "observation") {
|
|
33
|
+
const memory = item.memory;
|
|
34
|
+
return `[${memory.id}] observation ${memory.timestamp} relevance=${memory.relevance}: ${memory.content}`;
|
|
35
|
+
}
|
|
36
|
+
return `[${item.memory.id}] summary ${item.memory.timestamp} sources=[${item.memory.sourceMemoryIds.join(", ")}]: ${item.memory.content}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function tokenCost(item: SummarizerMemory): number {
|
|
40
|
+
return Math.max(1, estimateStringTokens(renderSummarizerMemory(item)) + 4);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Weighted random order without replacement. Weight is exactly inverse length. */
|
|
44
|
+
function weightedOrder(candidates: Candidate[], random: () => number): Candidate[] {
|
|
45
|
+
return candidates
|
|
46
|
+
.map((candidate) => ({ candidate, priority: -Math.log(Math.max(Number.EPSILON, random())) / candidate.weight }))
|
|
47
|
+
.sort((a, b) => a.priority - b.priority || a.candidate.item.memory.id.localeCompare(b.candidate.item.memory.id))
|
|
48
|
+
.map(({ candidate }) => candidate);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function sampleSummarizerMemories(args: SummarizerSamplingArgs): SummarizerSample {
|
|
52
|
+
const random = args.random ?? Math.random;
|
|
53
|
+
const budgetTokens = Math.max(1, Math.floor(args.samplingThresholdTokens ?? 60_000));
|
|
54
|
+
const candidates = args.memories.map((item): Candidate => {
|
|
55
|
+
const tokens = tokenCost(item);
|
|
56
|
+
return { item, tokens, weight: 1 / tokens };
|
|
57
|
+
});
|
|
58
|
+
const eligibleTokens = candidates.reduce((sum, candidate) => sum + candidate.tokens, 0);
|
|
59
|
+
const sampled = eligibleTokens > budgetTokens;
|
|
60
|
+
const selected = sampled ? [] as Candidate[] : candidates;
|
|
61
|
+
if (sampled) {
|
|
62
|
+
let used = 0;
|
|
63
|
+
for (const candidate of weightedOrder(candidates, random)) {
|
|
64
|
+
if (candidate.tokens > budgetTokens - used) continue;
|
|
65
|
+
selected.push(candidate);
|
|
66
|
+
used += candidate.tokens;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const selectedIds = new Set(selected.map((candidate) => candidate.item.memory.id));
|
|
70
|
+
const memories = args.memories.filter((item) => selectedIds.has(item.memory.id));
|
|
71
|
+
return {
|
|
72
|
+
memories,
|
|
73
|
+
sampled,
|
|
74
|
+
eligibleCount: candidates.length,
|
|
75
|
+
selectedCount: selected.length,
|
|
76
|
+
eligibleTokens,
|
|
77
|
+
selectedTokens: selected.reduce((sum, candidate) => sum + candidate.tokens, 0),
|
|
78
|
+
budgetTokens,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ContemplatorRunState } from "../runtime.js";
|
|
1
2
|
import type { Entry } from "../session-ledger/index.js";
|
|
2
3
|
|
|
3
4
|
const CONTEMPLATOR_MESSAGE = "om.contemplator.message";
|
|
@@ -57,7 +58,26 @@ function renderMessage(message: StoredMessage, compacted: boolean): string {
|
|
|
57
58
|
return `${DIM}── ${role}${marker} · ~${tokens} tokens ──${RESET}\n${renderContent(message.content) || `${DIM}(empty message)${RESET}`}`;
|
|
58
59
|
}
|
|
59
60
|
|
|
60
|
-
|
|
61
|
+
function liveStateLine(state: ContemplatorRunState): string {
|
|
62
|
+
const pending = `${state.pendingObservations} observations / ${state.pendingSummaries} summaries / ${state.pendingReviews} reviews pending`;
|
|
63
|
+
const timing = `Last start: ${state.lastStartedAt === undefined ? "not run this launch" : new Date(state.lastStartedAt).toISOString()} · Last end: ${state.lastCompletedAt === undefined ? "not completed this launch" : new Date(state.lastCompletedAt).toISOString()}`;
|
|
64
|
+
const error = state.lastError ? `\nLast error: ${state.lastError}` : "";
|
|
65
|
+
if (state.running) return `LIVE · running for ${Math.max(0, Math.floor((Date.now() - (state.lastStartedAt ?? Date.now())) / 60_000))}m · ${pending}\n${timing}${error}`;
|
|
66
|
+
const reason = state.waitingFor === "memories"
|
|
67
|
+
? "waiting for memory threshold"
|
|
68
|
+
: state.waitingFor === "responses"
|
|
69
|
+
? "waiting for response spacing"
|
|
70
|
+
: state.waitingFor === "ready"
|
|
71
|
+
? "ready to launch"
|
|
72
|
+
: state.waitingFor === "disabled"
|
|
73
|
+
? "disabled"
|
|
74
|
+
: state.waitingFor === "passive"
|
|
75
|
+
? "passive mode"
|
|
76
|
+
: "idle";
|
|
77
|
+
return `LIVE · ${reason} · ${pending} · ${state.responsesSinceRun} primary responses since last run\n${timing}${error}`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function renderContemplator(entries: Entry[], state?: ContemplatorRunState): string {
|
|
61
81
|
const messages: Array<{ message: StoredMessage; compacted: boolean }> = [];
|
|
62
82
|
const suggestions: Array<{ suggestion: string; delivered: boolean }> = [];
|
|
63
83
|
const reviews: Array<{ requestId: string; scope: string; outcome: string; memoryId?: string }> = [];
|
|
@@ -102,6 +122,7 @@ export function renderContemplator(entries: Entry[]): string {
|
|
|
102
122
|
const totalTokens = messages.reduce((total, item) => total + estimateTokens(item.message), 0);
|
|
103
123
|
const lines = [
|
|
104
124
|
`${DIM}CONTEMPLATOR · ${messages.length} messages · ~${totalTokens} estimated tokens${RESET}`,
|
|
125
|
+
...(state ? [`${DIM}${liveStateLine(state)}${RESET}`] : []),
|
|
105
126
|
"",
|
|
106
127
|
];
|
|
107
128
|
if (messages.length === 0) {
|
package/src/commands/settings.ts
CHANGED
|
@@ -9,8 +9,8 @@ type ModelRegistryLike = {
|
|
|
9
9
|
getAvailable(): Array<{ provider: string; id: string }>;
|
|
10
10
|
getAll(): Array<{ provider: string; id: string }>;
|
|
11
11
|
};
|
|
12
|
-
type NumberSetting = "observeAfterTokens" | "
|
|
13
|
-
type BooleanSetting = "contemplatorEnabled" | "showContemplatorMessages" | "reviewerEnabled" | "compactionObserverEnabled" | "showWorkerNotifications" | "passive" | "debugLog";
|
|
12
|
+
type NumberSetting = "observeAfterTokens" | "compactAfterTokens" | "observerChunkMaxTokens" | "newMemoryPoolMaxTokens" | "oldMemoryPoolTargetTokens" | "agentMaxTurns" | "contemplatorMinNewObservations" | "contemplatorMinNewSummaries" | "contemplatorMinTurns" | "summarizerRetriggerTokens" | "summarizerSamplingThresholdTokens";
|
|
13
|
+
type BooleanSetting = "contemplatorEnabled" | "showContemplatorMessages" | "reviewerEnabled" | "summarizerEnabled" | "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";
|
|
@@ -20,9 +20,12 @@ function branch(ctx: ExtensionContext): readonly unknown[] {
|
|
|
20
20
|
return ctx.sessionManager.getBranch() as readonly unknown[];
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
function appendSettings(pi: ExtensionAPI, runtime: Runtime, settings: SessionSettings): void {
|
|
23
|
+
function appendSettings(pi: ExtensionAPI, runtime: Runtime, settings: SessionSettings, ctx?: ExtensionContext): void {
|
|
24
24
|
runtime.setSessionSettings(settings);
|
|
25
25
|
pi.appendEntry(OM_SETTINGS, { version: 1, ...settings });
|
|
26
|
+
// Settings that affect worker eligibility should take effect immediately,
|
|
27
|
+
// rather than waiting for an unrelated observer batch or session restart.
|
|
28
|
+
if (ctx) runtime.notifySettingsUpdate(ctx, settings);
|
|
26
29
|
}
|
|
27
30
|
|
|
28
31
|
function hasOverride(settings: SessionSettings, key: string): boolean {
|
|
@@ -33,7 +36,12 @@ function scalarLabel(runtime: Runtime, key: NumberSetting | BooleanSetting | "co
|
|
|
33
36
|
const current = runtime.config[key];
|
|
34
37
|
const defaultValue = runtime.getDefaultConfig()[key];
|
|
35
38
|
const renderedDefault = defaultValue === undefined ? "derived" : String(defaultValue);
|
|
36
|
-
return hasOverride(runtime.getSessionSettings(), key) ? String(current) :
|
|
39
|
+
return hasOverride(runtime.getSessionSettings(), key) ? String(current) : `${renderedDefault} (default)`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function extensionEnabledLabel(runtime: Runtime): string {
|
|
43
|
+
const enabled = !runtime.config.passive;
|
|
44
|
+
return hasOverride(runtime.getSessionSettings(), "passive") ? String(enabled) : `${enabled} (default)`;
|
|
37
45
|
}
|
|
38
46
|
|
|
39
47
|
interface ModelOption extends SelectItem {
|
|
@@ -124,28 +132,17 @@ async function chooseModel(ctx: ExtensionContext, current: ConfiguredModel | und
|
|
|
124
132
|
}
|
|
125
133
|
|
|
126
134
|
async function editNumber(ctx: ExtensionContext, runtime: Runtime, key: NumberSetting, title: string): Promise<number | undefined> {
|
|
127
|
-
const
|
|
135
|
+
const requirement = "positive integer";
|
|
136
|
+
const value = await ctx.ui.input(`${title} (current: ${scalarLabel(runtime, key)})`, `${requirement}; blank cancels`);
|
|
128
137
|
if (value === undefined || value.trim() === "") return undefined;
|
|
129
138
|
const parsed = Number(value.trim());
|
|
130
139
|
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
131
|
-
ctx.ui.notify(
|
|
140
|
+
ctx.ui.notify(`Value must be a ${requirement}.`, "warning");
|
|
132
141
|
return undefined;
|
|
133
142
|
}
|
|
134
143
|
return parsed;
|
|
135
144
|
}
|
|
136
145
|
|
|
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
146
|
export function registerSettingsCommand(pi: ExtensionAPI, runtime: Runtime): void {
|
|
150
147
|
const restoreSettings = (_event: unknown, ctx: ExtensionContext) => {
|
|
151
148
|
runtime.ensureConfig(ctx.cwd);
|
|
@@ -155,19 +152,24 @@ export function registerSettingsCommand(pi: ExtensionAPI, runtime: Runtime): voi
|
|
|
155
152
|
pi.on("session_tree", restoreSettings);
|
|
156
153
|
|
|
157
154
|
pi.registerCommand("om:settings", {
|
|
158
|
-
description: "Configure
|
|
155
|
+
description: "Configure pi-contemplator for this session",
|
|
159
156
|
handler: async (args, ctx) => {
|
|
160
157
|
runtime.ensureConfig(ctx.cwd);
|
|
161
158
|
runtime.restoreSessionSettings(branch(ctx));
|
|
162
159
|
const argument = typeof args === "string" ? args.trim().toLowerCase() : "";
|
|
163
160
|
if (argument === "on" || argument === "off") {
|
|
164
161
|
appendSettings(pi, runtime, { contemplatorEnabled: argument === "on" });
|
|
165
|
-
ctx.ui.notify(`
|
|
162
|
+
ctx.ui.notify(`Contemplator: ${argument === "on" ? "enabled" : "disabled"} for this session.`, "info");
|
|
166
163
|
return;
|
|
167
164
|
}
|
|
168
165
|
if (argument === "compaction on" || argument === "compaction off") {
|
|
169
166
|
appendSettings(pi, runtime, { compactionObserverEnabled: argument.endsWith("on") });
|
|
170
|
-
ctx.ui.notify(`
|
|
167
|
+
ctx.ui.notify(`Observe source during compaction: ${argument.endsWith("on") ? "enabled" : "disabled"} for this session.`, "info");
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
if (argument === "summarizer on" || argument === "summarizer off") {
|
|
171
|
+
appendSettings(pi, runtime, { summarizerEnabled: argument.endsWith("on") }, ctx);
|
|
172
|
+
ctx.ui.notify(`Summarizer: ${argument.endsWith("on") ? "enabled" : "disabled"} for this session.`, "info");
|
|
171
173
|
return;
|
|
172
174
|
}
|
|
173
175
|
if (argument === "reviewer on" || argument === "reviewer off") {
|
|
@@ -181,81 +183,83 @@ export function registerSettingsCommand(pi: ExtensionAPI, runtime: Runtime): voi
|
|
|
181
183
|
return;
|
|
182
184
|
}
|
|
183
185
|
if (argument) {
|
|
184
|
-
ctx.ui.notify("Usage: /om:settings [on|off|messages on|messages off|reviewer on|reviewer off|compaction on|compaction off]", "info");
|
|
186
|
+
ctx.ui.notify("Usage: /om:settings [on|off|messages on|messages off|summarizer on|summarizer off|reviewer on|reviewer off|compaction on|compaction off]", "info");
|
|
185
187
|
return;
|
|
186
188
|
}
|
|
187
189
|
|
|
188
190
|
while (true) {
|
|
189
191
|
const settings = runtime.getSessionSettings();
|
|
190
|
-
const choice = await ctx.ui.select("
|
|
191
|
-
`
|
|
192
|
-
`
|
|
193
|
-
`Contemplator
|
|
194
|
-
`
|
|
195
|
-
`
|
|
196
|
-
`
|
|
197
|
-
`
|
|
198
|
-
`
|
|
199
|
-
`
|
|
200
|
-
`
|
|
201
|
-
`
|
|
202
|
-
`
|
|
203
|
-
`
|
|
204
|
-
`
|
|
205
|
-
`
|
|
206
|
-
`
|
|
207
|
-
`
|
|
208
|
-
`
|
|
209
|
-
`
|
|
192
|
+
const choice = await ctx.ui.select("pi-contemplator settings (session overrides)", [
|
|
193
|
+
`Pi-contemplator Enabled: ${extensionEnabledLabel(runtime)}`,
|
|
194
|
+
`Contemplator enabled: ${scalarLabel(runtime, "contemplatorEnabled")}`,
|
|
195
|
+
`Contemplator model: ${hasOverride(settings, "contemplatorModel") ? modelLabel(runtime.config.contemplatorModel) : `${modelLabel(runtime.getDefaultConfig().contemplatorModel)} (default)`}`,
|
|
196
|
+
`Show contemplator messages: ${scalarLabel(runtime, "showContemplatorMessages")}`,
|
|
197
|
+
`Contemplator new-observation trigger (count): ${scalarLabel(runtime, "contemplatorMinNewObservations")}`,
|
|
198
|
+
`Contemplator new-summary trigger (count): ${scalarLabel(runtime, "contemplatorMinNewSummaries")}`,
|
|
199
|
+
`Contemplator response spacing (count): ${scalarLabel(runtime, "contemplatorMinTurns")}`,
|
|
200
|
+
`Summarizer enabled: ${scalarLabel(runtime, "summarizerEnabled")}`,
|
|
201
|
+
`New memory pool protection budget (tokens): ${scalarLabel(runtime, "newMemoryPoolMaxTokens")}`,
|
|
202
|
+
`Old memory pool target (tokens, advisory): ${scalarLabel(runtime, "oldMemoryPoolTargetTokens")}`,
|
|
203
|
+
`Summarizer old-pool retrigger growth (tokens): ${scalarLabel(runtime, "summarizerRetriggerTokens")}`,
|
|
204
|
+
`Summarizer input cap before sampling (tokens): ${scalarLabel(runtime, "summarizerSamplingThresholdTokens")}`,
|
|
205
|
+
`Structural reviewer enabled: ${scalarLabel(runtime, "reviewerEnabled")}`,
|
|
206
|
+
`Structural reviewer model: ${hasOverride(settings, "reviewerModel") ? modelLabel(runtime.config.reviewerModel) : `${modelLabel(runtime.getDefaultConfig().reviewerModel)} (default)`}`,
|
|
207
|
+
`Observe source during compaction: ${scalarLabel(runtime, "compactionObserverEnabled")}`,
|
|
208
|
+
`Observer and summarizer model: ${hasOverride(settings, "model") ? modelLabel(runtime.config.model) : `${modelLabel(runtime.getDefaultConfig().model)} (default)`}`,
|
|
209
|
+
`Observer source backlog trigger (tokens): ${scalarLabel(runtime, "observeAfterTokens")}`,
|
|
210
|
+
`Observer input cap (tokens): ${scalarLabel(runtime, "observerChunkMaxTokens")}`,
|
|
211
|
+
`Observer and summarizer max rounds: ${scalarLabel(runtime, "agentMaxTurns")}`,
|
|
212
|
+
`Automatic compaction source backlog trigger (tokens): ${scalarLabel(runtime, "compactAfterTokens")}`,
|
|
213
|
+
`Automatic compaction threshold mode: ${hasOverride(settings, "compactAfterTokensMode") ? runtime.config.compactAfterTokensMode : `${runtime.getDefaultConfig().compactAfterTokensMode} (default)`}`,
|
|
214
|
+
`Automatic compaction source-backlog context ratio: ${hasOverride(settings, "compactAfterTokensRatio") ? runtime.config.compactAfterTokensRatio : `${runtime.getDefaultConfig().compactAfterTokensRatio} (default)`}`,
|
|
210
215
|
`Worker notifications: ${scalarLabel(runtime, "showWorkerNotifications")}`,
|
|
211
|
-
`Passive mode: ${scalarLabel(runtime, "passive")}`,
|
|
212
216
|
`Debug logging: ${scalarLabel(runtime, "debugLog")}`,
|
|
213
217
|
"Done",
|
|
214
218
|
]);
|
|
215
219
|
if (!choice || choice === "Done") return;
|
|
216
|
-
if (choice.startsWith("
|
|
217
|
-
else if (choice.startsWith("Contemplator
|
|
218
|
-
else if (choice.startsWith("
|
|
219
|
-
else if (choice.startsWith("
|
|
220
|
+
if (choice.startsWith("Pi-contemplator Enabled:")) appendSettings(pi, runtime, { passive: !runtime.config.passive });
|
|
221
|
+
else if (choice.startsWith("Contemplator enabled:")) appendSettings(pi, runtime, { contemplatorEnabled: !runtime.config.contemplatorEnabled });
|
|
222
|
+
else if (choice.startsWith("Summarizer enabled:")) appendSettings(pi, runtime, { summarizerEnabled: !runtime.config.summarizerEnabled }, ctx);
|
|
223
|
+
else if (choice.startsWith("Show contemplator messages:")) appendSettings(pi, runtime, { showContemplatorMessages: !runtime.config.showContemplatorMessages });
|
|
224
|
+
else if (choice.startsWith("Structural reviewer enabled:")) appendSettings(pi, runtime, { reviewerEnabled: !runtime.config.reviewerEnabled });
|
|
225
|
+
else if (choice.startsWith("Observe source during compaction:")) appendSettings(pi, runtime, { compactionObserverEnabled: !runtime.config.compactionObserverEnabled });
|
|
220
226
|
else if (choice.startsWith("Worker notifications:")) appendSettings(pi, runtime, { showWorkerNotifications: !runtime.config.showWorkerNotifications });
|
|
221
|
-
else if (choice.startsWith("Passive mode:")) appendSettings(pi, runtime, { passive: !runtime.config.passive });
|
|
222
227
|
else if (choice.startsWith("Debug logging:")) appendSettings(pi, runtime, { debugLog: !runtime.config.debugLog });
|
|
223
|
-
else if (choice.startsWith("
|
|
224
|
-
const model = await chooseModel(ctx, runtime.config.contemplatorModel, "
|
|
228
|
+
else if (choice.startsWith("Contemplator model:")) {
|
|
229
|
+
const model = await chooseModel(ctx, runtime.config.contemplatorModel, "Contemplator model");
|
|
225
230
|
if (model !== undefined) appendSettings(pi, runtime, { contemplatorModel: model });
|
|
226
231
|
} else if (choice.startsWith("Structural reviewer model:")) {
|
|
227
232
|
const model = await chooseModel(ctx, runtime.config.reviewerModel, "Structural reviewer model");
|
|
228
233
|
if (model !== undefined) appendSettings(pi, runtime, { reviewerModel: model });
|
|
229
|
-
} else if (choice.startsWith("
|
|
230
|
-
const model = await chooseModel(ctx, runtime.config.model, "
|
|
234
|
+
} else if (choice.startsWith("Observer and summarizer model:")) {
|
|
235
|
+
const model = await chooseModel(ctx, runtime.config.model, "Observer and summarizer model");
|
|
231
236
|
if (model !== undefined) appendSettings(pi, runtime, { model });
|
|
232
|
-
} else if (choice.startsWith("
|
|
233
|
-
const mode = await ctx.ui.select("
|
|
237
|
+
} else if (choice.startsWith("Automatic compaction threshold mode:")) {
|
|
238
|
+
const mode = await ctx.ui.select("Automatic compaction threshold mode", ["calibrated", "ratio"]);
|
|
234
239
|
if (mode === "calibrated" || mode === "ratio") appendSettings(pi, runtime, { compactAfterTokensMode: mode });
|
|
235
|
-
} else if (choice.startsWith("
|
|
236
|
-
const value = await ctx.ui.input(`
|
|
240
|
+
} else if (choice.startsWith("Automatic compaction context ratio:")) {
|
|
241
|
+
const value = await ctx.ui.input(`Automatic compaction source-backlog context ratio (current: ${scalarLabel(runtime, "compactAfterTokensRatio")})`, "decimal between 0 and 1");
|
|
237
242
|
const ratio = value === undefined ? undefined : Number(value.trim());
|
|
238
243
|
if (ratio !== undefined && Number.isFinite(ratio) && ratio > 0 && ratio < 1) appendSettings(pi, runtime, { compactAfterTokensRatio: ratio });
|
|
239
244
|
else if (value !== undefined) ctx.ui.notify("Ratio must be a number between 0 and 1.", "warning");
|
|
240
245
|
} else {
|
|
241
246
|
const numberChoice: Array<[string, NumberSetting, string]> = [
|
|
242
|
-
["
|
|
243
|
-
["
|
|
244
|
-
["
|
|
245
|
-
["
|
|
246
|
-
["
|
|
247
|
-
["
|
|
248
|
-
["
|
|
249
|
-
["
|
|
250
|
-
["
|
|
251
|
-
["
|
|
247
|
+
["Observer source backlog trigger (tokens):", "observeAfterTokens", "Observer source backlog trigger (tokens)"],
|
|
248
|
+
["Observer input cap (tokens):", "observerChunkMaxTokens", "Observer input cap (tokens)"],
|
|
249
|
+
["Observer and summarizer max rounds:", "agentMaxTurns", "Observer and summarizer max rounds"],
|
|
250
|
+
["Automatic compaction source backlog trigger (tokens):", "compactAfterTokens", "Automatic compaction source backlog trigger (tokens)"],
|
|
251
|
+
["New memory pool protection budget (tokens):", "newMemoryPoolMaxTokens", "New memory pool protection budget (tokens)"],
|
|
252
|
+
["Old memory pool target (tokens, advisory):", "oldMemoryPoolTargetTokens", "Old memory pool target (tokens, advisory)"],
|
|
253
|
+
["Contemplator new-observation trigger (count):", "contemplatorMinNewObservations", "Contemplator new-observation trigger (count)"],
|
|
254
|
+
["Contemplator new-summary trigger (count):", "contemplatorMinNewSummaries", "Contemplator new-summary trigger (count)"],
|
|
255
|
+
["Contemplator response spacing (count):", "contemplatorMinTurns", "Contemplator response spacing (count)"],
|
|
256
|
+
["Summarizer old-pool retrigger growth (tokens):", "summarizerRetriggerTokens", "Summarizer old-pool retrigger growth (tokens)"],
|
|
257
|
+
["Summarizer input cap before sampling (tokens):", "summarizerSamplingThresholdTokens", "Summarizer input cap before sampling (tokens)"],
|
|
252
258
|
];
|
|
253
259
|
const selected = numberChoice.find(([prefix]) => choice.startsWith(prefix));
|
|
254
260
|
if (selected) {
|
|
255
261
|
const value = await editNumber(ctx, runtime, selected[1], selected[2]);
|
|
256
|
-
if (value !== undefined
|
|
257
|
-
appendSettings(pi, runtime, { [selected[1]]: value } as SessionSettings);
|
|
258
|
-
}
|
|
262
|
+
if (value !== undefined) appendSettings(pi, runtime, { [selected[1]]: value } as SessionSettings, ctx);
|
|
259
263
|
}
|
|
260
264
|
}
|
|
261
265
|
}
|
package/src/commands/status.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { observationPoolMetrics } from "../agents/dropper/pool.js";
|
|
3
2
|
import { resolveCompactAfterTokens } from "../config.js";
|
|
4
3
|
import type { Runtime } from "../runtime.js";
|
|
5
4
|
import {
|
|
@@ -7,9 +6,9 @@ import {
|
|
|
7
6
|
diffProjection,
|
|
8
7
|
foldLedger,
|
|
9
8
|
fullProjection,
|
|
9
|
+
partitionMemoryPools,
|
|
10
10
|
rawTokensSinceLastCompaction,
|
|
11
11
|
rawTokensSinceObservationCoverage,
|
|
12
|
-
rawTokensSinceReflectionCoverage,
|
|
13
12
|
visibleProjection,
|
|
14
13
|
type Entry,
|
|
15
14
|
} from "../session-ledger/index.js";
|
|
@@ -37,6 +36,22 @@ function formatDuration(durationMs: number): string {
|
|
|
37
36
|
return `${seconds}s`;
|
|
38
37
|
}
|
|
39
38
|
|
|
39
|
+
function formatRunAge(timestamp: number): string {
|
|
40
|
+
return `${new Date(timestamp).toISOString()} (${formatDuration(Math.max(0, Date.now() - timestamp))} ago)`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function contemplatorWaitingLabel(waitingFor: Runtime["contemplatorState"]["waitingFor"]): string {
|
|
44
|
+
switch (waitingFor) {
|
|
45
|
+
case "memories": return "waiting for memory threshold";
|
|
46
|
+
case "responses": return "waiting for response spacing";
|
|
47
|
+
case "ready": return "ready to launch";
|
|
48
|
+
case "running": return "running";
|
|
49
|
+
case "disabled": return "disabled";
|
|
50
|
+
case "passive": return "passive mode";
|
|
51
|
+
default: return "idle; no pending memories";
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
40
55
|
function truncateStatusText(value: string, limit = 1_000): string {
|
|
41
56
|
return value.length <= limit ? value : `${value.slice(0, limit - 1)}…`;
|
|
42
57
|
}
|
|
@@ -60,7 +75,7 @@ function appendSuffixes(line: string, suffixes: (string | undefined)[]): string
|
|
|
60
75
|
|
|
61
76
|
export function registerStatusCommand(pi: ExtensionAPI, runtime: Runtime): void {
|
|
62
77
|
pi.registerCommand("om:status", {
|
|
63
|
-
description: "Show
|
|
78
|
+
description: "Show pi-contemplator status",
|
|
64
79
|
handler: async (_args, ctx) => {
|
|
65
80
|
runtime.ensureConfig(ctx.cwd);
|
|
66
81
|
const entries = ctx.sessionManager.getBranch() as Entry[];
|
|
@@ -70,21 +85,17 @@ export function registerStatusCommand(pi: ExtensionAPI, runtime: Runtime): void
|
|
|
70
85
|
const drift = diffProjection(visible, full);
|
|
71
86
|
|
|
72
87
|
const visibleObservationTokens = tokenSum(visible.observations);
|
|
73
|
-
const
|
|
74
|
-
const
|
|
88
|
+
const visibleSummaryTokens = tokenSum(visible.summaries);
|
|
89
|
+
const pools = partitionMemoryPools(folded.activeObservations, folded.activeSummaries, runtime.config.newMemoryPoolMaxTokens);
|
|
75
90
|
const observationLine = appendSuffixes(
|
|
76
|
-
`Observations: ${folded.observations.length} recorded / ${folded.
|
|
77
|
-
[
|
|
78
|
-
addedSuffix(drift.observationsOnlyInFull.length),
|
|
79
|
-
removedSuffix(drift.droppedOnlyInFull.length),
|
|
80
|
-
],
|
|
91
|
+
`Observations: ${folded.observations.length} recorded / ${folded.activeObservations.length} active / ${visible.observations.length} visible`,
|
|
92
|
+
[addedSuffix(drift.observationsOnlyInFull.length), removedSuffix(drift.observationsOnlyInVisible.length)],
|
|
81
93
|
);
|
|
82
|
-
const
|
|
83
|
-
`
|
|
84
|
-
[addedSuffix(drift.
|
|
94
|
+
const summaryLine = appendSuffixes(
|
|
95
|
+
`Summaries: ${folded.summaries.length} recorded / ${folded.activeSummaries.length} active / ${visible.summaries.length} visible`,
|
|
96
|
+
[addedSuffix(drift.summariesOnlyInFull.length), removedSuffix(drift.summariesOnlyInVisible.length)],
|
|
85
97
|
);
|
|
86
98
|
const obsProgress = rawTokensSinceObservationCoverage(entries);
|
|
87
|
-
const reflectionProgress = rawTokensSinceReflectionCoverage(entries);
|
|
88
99
|
const compactionProgress = rawTokensSinceLastCompaction(entries);
|
|
89
100
|
const contextWindow = typeof ctx.model?.contextWindow === "number" ? ctx.model.contextWindow : undefined;
|
|
90
101
|
const compactThreshold = resolveCompactAfterTokens(runtime.config, contextWindow);
|
|
@@ -97,22 +108,27 @@ export function registerStatusCommand(pi: ExtensionAPI, runtime: Runtime): void
|
|
|
97
108
|
]
|
|
98
109
|
: [];
|
|
99
110
|
|
|
111
|
+
const summarizerTrigger = runtime.summarizerNextTriggerTokens ?? runtime.config.oldMemoryPoolTargetTokens;
|
|
112
|
+
const summarizerSamplingTokens = runtime.config.summarizerSamplingThresholdTokens;
|
|
100
113
|
const lines = [
|
|
101
114
|
...passiveLines,
|
|
102
115
|
"── Memory ──",
|
|
103
116
|
observationLine,
|
|
104
|
-
|
|
117
|
+
summaryLine,
|
|
105
118
|
"",
|
|
106
119
|
"── Activity ──",
|
|
107
|
-
`
|
|
108
|
-
`
|
|
109
|
-
`
|
|
110
|
-
`Visible observation pool: ~${visibleObservationTokens.toLocaleString()}
|
|
111
|
-
`
|
|
112
|
-
`
|
|
120
|
+
`Observer source backlog: ~${obsProgress.toLocaleString()} / ${runtime.config.observeAfterTokens.toLocaleString()} tokens (${pct(obsProgress, runtime.config.observeAfterTokens)}%)`,
|
|
121
|
+
`Summarizer trigger: old pool ~${pools.oldTokens.toLocaleString()} / ${summarizerTrigger.toLocaleString()} tokens (${pct(pools.oldTokens, summarizerTrigger)}%)`,
|
|
122
|
+
`Automatic compaction source backlog: ~${compactionProgress.toLocaleString()} / ${compactThreshold.toLocaleString()} tokens (${pct(compactionProgress, compactThreshold)}%; injected memory excluded)`,
|
|
123
|
+
`Visible observation pool: ~${visibleObservationTokens.toLocaleString()} tokens`,
|
|
124
|
+
`New memory pool: ~${pools.newTokens.toLocaleString()} / ${runtime.config.newMemoryPoolMaxTokens.toLocaleString()} protection-budget tokens (${pct(pools.newTokens, runtime.config.newMemoryPoolMaxTokens)}%; newest memory always protected whole)`,
|
|
125
|
+
`Old memory pool: ~${pools.oldTokens.toLocaleString()} / ${runtime.config.oldMemoryPoolTargetTokens.toLocaleString()} advisory target tokens (${pct(pools.oldTokens, runtime.config.oldMemoryPoolTargetTokens)}%)`,
|
|
126
|
+
`Summary pool: ~${visibleSummaryTokens.toLocaleString()} visible tokens`,
|
|
127
|
+
`Summarizer: ${runtime.config.summarizerEnabled === false ? "disabled" : "enabled"}; retrigger after +${runtime.config.summarizerRetriggerTokens.toLocaleString()} old-pool tokens / sample above ~${summarizerSamplingTokens.toLocaleString()} tokens`,
|
|
113
128
|
`Cumulative agent time: ${formatDuration(agentActiveTimeMs(entries))}`,
|
|
114
|
-
`
|
|
129
|
+
`Observe source during compaction: ${runtime.config.compactionObserverEnabled === false ? "disabled" : "enabled"}`,
|
|
115
130
|
`Contemplator: ${runtime.config.contemplatorEnabled ? "enabled" : "disabled"}`,
|
|
131
|
+
`Contemplator trigger: ${runtime.contemplatorState.pendingObservations} observations / ${runtime.contemplatorState.pendingSummaries} summaries / ${runtime.contemplatorState.pendingReviews} reviews pending; ${runtime.contemplatorState.responsesSinceRun} / ${runtime.config.contemplatorMinTurns} primary responses; ${contemplatorWaitingLabel(runtime.contemplatorState.waitingFor)}`,
|
|
116
132
|
`Contemplator model: ${runtime.config.contemplatorModel ? `${runtime.config.contemplatorModel.provider}/${runtime.config.contemplatorModel.id}` : "current session model"}`,
|
|
117
133
|
`Contemplator messages: ${runtime.config.showContemplatorMessages ? "visible" : "hidden"}`,
|
|
118
134
|
`Structural reviewer: ${runtime.config.reviewerEnabled === false ? "disabled" : "enabled"}`,
|
|
@@ -124,6 +140,15 @@ export function registerStatusCommand(pi: ExtensionAPI, runtime: Runtime): void
|
|
|
124
140
|
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"})`);
|
|
125
141
|
}
|
|
126
142
|
|
|
143
|
+
lines.push("", "── Last worker runs ──");
|
|
144
|
+
lines.push(`Last observer start: ${runtime.lastObserverStartedAt === undefined ? "not run this launch" : formatRunAge(runtime.lastObserverStartedAt)}`);
|
|
145
|
+
lines.push(`Last observer end: ${runtime.lastObserverCompletedAt === undefined ? "not completed this launch" : formatRunAge(runtime.lastObserverCompletedAt)}`);
|
|
146
|
+
lines.push(`Last summarizer start: ${runtime.lastSummarizerStartedAt === undefined ? "not run this launch" : formatRunAge(runtime.lastSummarizerStartedAt)}`);
|
|
147
|
+
lines.push(`Last summarizer end: ${runtime.lastSummarizerCompletedAt === undefined ? "not completed this launch" : formatRunAge(runtime.lastSummarizerCompletedAt)}`);
|
|
148
|
+
lines.push(`Last contemplator start: ${runtime.contemplatorState.lastStartedAt === undefined ? "not run this launch" : formatRunAge(runtime.contemplatorState.lastStartedAt)}`);
|
|
149
|
+
lines.push(`Last contemplator end: ${runtime.contemplatorState.lastCompletedAt === undefined ? "not completed this launch" : formatRunAge(runtime.contemplatorState.lastCompletedAt)}`);
|
|
150
|
+
|
|
151
|
+
lines.push("", "── Interventions ──");
|
|
127
152
|
// Probe stats come from the branch ledger (like /om:view contemplator):
|
|
128
153
|
// deduped by probeId so restore re-queues don't inflate the count, and
|
|
129
154
|
// entries without a probeId (sent before probe tracking existed) count
|
|
@@ -146,12 +171,12 @@ export function registerStatusCommand(pi: ExtensionAPI, runtime: Runtime): void
|
|
|
146
171
|
probeSuggestions[existingIndex] = { suggestion: data.suggestion };
|
|
147
172
|
}
|
|
148
173
|
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
lines.push(`Last probe: ${probeSuggestions[probeSuggestions.length - 1].suggestion}`);
|
|
152
|
-
}
|
|
174
|
+
lines.push(`Probes sent: ${probeSuggestions.length}`);
|
|
175
|
+
if (probeSuggestions.length > 0) lines.push(`Last probe: ${probeSuggestions[probeSuggestions.length - 1].suggestion}`);
|
|
153
176
|
|
|
154
|
-
const
|
|
177
|
+
const reviews = full.reviews ?? [];
|
|
178
|
+
lines.push(`Reviews completed: ${reviews.length}`);
|
|
179
|
+
const latestReview = reviews.at(-1);
|
|
155
180
|
if (latestReview) {
|
|
156
181
|
lines.push(`Last review: [${latestReview.id}] ${latestReview.scope} ${latestReview.outcome}`);
|
|
157
182
|
if (latestReview.outcome === "proposal") lines.push(`Last review summary: ${truncateStatusText(latestReview.summary)}`);
|
|
@@ -165,22 +190,21 @@ export function registerStatusCommand(pi: ExtensionAPI, runtime: Runtime): void
|
|
|
165
190
|
}
|
|
166
191
|
if (latestNotice) lines.push(`Last reviewer notice: ${truncateStatusText(latestNotice)}`);
|
|
167
192
|
|
|
168
|
-
if (runtime.consolidationInFlight || runtime.compactInFlight || runtime.compactHookInFlight || runtime.reviewInFlight) {
|
|
193
|
+
if (runtime.consolidationInFlight || runtime.summarizerInFlight || runtime.contemplatorState.running || runtime.compactInFlight || runtime.compactHookInFlight || runtime.reviewInFlight) {
|
|
169
194
|
lines.push("", "── In flight ──");
|
|
170
|
-
if (runtime.consolidationInFlight)
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
if (runtime.compactInFlight) lines.push("Auto-compaction: running");
|
|
195
|
+
if (runtime.consolidationInFlight) lines.push("Observer: running");
|
|
196
|
+
if (runtime.summarizerInFlight) lines.push("Summarizer: running");
|
|
197
|
+
if (runtime.contemplatorState.running) lines.push("Contemplator: running");
|
|
198
|
+
if (runtime.compactInFlight) lines.push("Automatic compaction: running");
|
|
175
199
|
if (runtime.compactHookInFlight) lines.push("Compaction hook: running");
|
|
176
200
|
if (runtime.reviewInFlight) lines.push("Structural review: running");
|
|
177
201
|
}
|
|
178
202
|
|
|
179
|
-
if (runtime.lastObserverError || runtime.
|
|
203
|
+
if (runtime.lastObserverError || runtime.lastSummarizerError || runtime.contemplatorState.lastError) {
|
|
180
204
|
lines.push("", "── Last error ──");
|
|
181
205
|
if (runtime.lastObserverError) lines.push(`Observer: ${runtime.lastObserverError}`);
|
|
182
|
-
if (runtime.
|
|
183
|
-
if (runtime.
|
|
206
|
+
if (runtime.lastSummarizerError) lines.push(`Summarizer: ${runtime.lastSummarizerError}`);
|
|
207
|
+
if (runtime.contemplatorState.lastError) lines.push(`Contemplator: ${runtime.contemplatorState.lastError}`);
|
|
184
208
|
}
|
|
185
209
|
|
|
186
210
|
ctx.ui.notify(lines.join("\n"), "info");
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { SummarizerRunView } from "../runtime.js";
|
|
2
|
+
|
|
3
|
+
const DIM = "\x1b[2m";
|
|
4
|
+
const RESET = "\x1b[0m";
|
|
5
|
+
|
|
6
|
+
type StoredMessage = { role?: unknown; content?: unknown };
|
|
7
|
+
type ContentPart = { type?: unknown; text?: unknown; thinking?: unknown; name?: unknown; arguments?: unknown; content?: unknown };
|
|
8
|
+
|
|
9
|
+
function renderValue(value: unknown): string {
|
|
10
|
+
if (typeof value === "string") return value;
|
|
11
|
+
if (value === undefined || value === null) return "";
|
|
12
|
+
return JSON.stringify(value, null, 2);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function renderContent(content: unknown): string {
|
|
16
|
+
if (typeof content === "string") return content;
|
|
17
|
+
if (!Array.isArray(content)) return renderValue(content);
|
|
18
|
+
return content.map((part: ContentPart) => {
|
|
19
|
+
if (part.type === "text") return typeof part.text === "string" ? part.text : "";
|
|
20
|
+
if (part.type === "thinking") {
|
|
21
|
+
const thinking = typeof part.thinking === "string" ? part.thinking : typeof part.text === "string" ? part.text : renderValue(part);
|
|
22
|
+
return `[thinking]\n${thinking}`;
|
|
23
|
+
}
|
|
24
|
+
if (part.type === "toolCall" || part.type === "tool_use" || part.type === "toolUse") {
|
|
25
|
+
const name = typeof part.name === "string" ? part.name : "unknown tool";
|
|
26
|
+
return `[tool call: ${name}${part.arguments === undefined ? "" : ` ${renderValue(part.arguments)}`}]`;
|
|
27
|
+
}
|
|
28
|
+
if (part.type === "toolResult" || part.type === "tool_result") return `[tool result]\n${renderValue(part.content)}`;
|
|
29
|
+
return `[${String(part.type ?? "content")}] ${renderValue(part)}`;
|
|
30
|
+
}).filter(Boolean).join("\n");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function estimateTokens(value: unknown): number {
|
|
34
|
+
return Math.max(1, Math.ceil(JSON.stringify(value).length / 4));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Render the most recent launch-local summarizer transcript. */
|
|
38
|
+
export function renderSummarizer(run: SummarizerRunView | undefined): string {
|
|
39
|
+
if (!run) return `${DIM}SUMMARIZER${RESET}\n\n${DIM}Summarizer has not run yet during this launch.${RESET}`;
|
|
40
|
+
const messages = run.messages.filter((message): message is StoredMessage => !!message && typeof message === "object");
|
|
41
|
+
const totalTokens = messages.reduce((sum, message) => sum + estimateTokens(message), 0);
|
|
42
|
+
const lines = [
|
|
43
|
+
`${DIM}SUMMARIZER · ${run.status} · ${messages.length} messages · ~${totalTokens.toLocaleString()} estimated tokens${RESET}`,
|
|
44
|
+
`${DIM}Started ${new Date(run.startedAt).toISOString()}${RESET}`,
|
|
45
|
+
`${DIM}${run.completedAt === undefined ? "Not completed" : `Ended ${new Date(run.completedAt).toISOString()}`}${RESET}`,
|
|
46
|
+
"",
|
|
47
|
+
];
|
|
48
|
+
if (messages.length === 0) lines.push(`${DIM}(no summarizer messages captured yet)${RESET}`);
|
|
49
|
+
for (const [index, message] of messages.entries()) {
|
|
50
|
+
if (index > 0) lines.push("");
|
|
51
|
+
const role = typeof message.role === "string" ? message.role : "unknown";
|
|
52
|
+
lines.push(`${DIM}── ${role} · ~${estimateTokens(message).toLocaleString()} tokens ──${RESET}`);
|
|
53
|
+
lines.push(renderContent(message.content) || `${DIM}(empty message)${RESET}`);
|
|
54
|
+
}
|
|
55
|
+
if (run.summary) lines.push("", `${DIM}── Completion summary ──${RESET}`, run.summary);
|
|
56
|
+
if (run.error) lines.push("", `${DIM}── Failure ──${RESET}`, run.error);
|
|
57
|
+
return lines.join("\n");
|
|
58
|
+
}
|