@matthewfl/pi-contemplator 0.1.2 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@matthewfl/pi-contemplator",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "A Pi extension that keeps long-running agentic sessions on track with background memory, contemplation, and structural review.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"typecheck": "tsc --noEmit",
|
|
42
42
|
"test": "npm run test:unit && npm run test:e2e",
|
|
43
43
|
"test:unit": "vitest run",
|
|
44
|
-
"test:e2e": "node tests/e2e/rpc-contemplator.mjs && node tests/e2e/rpc-summarizer.mjs && node tests/e2e/rpc-delivery.mjs && node tests/e2e/rpc-restore-review.mjs && node tests/e2e/rpc-compaction.mjs && node tests/e2e/rpc-compaction-resilience.mjs && node tests/e2e/rpc-memory-edges.mjs && node tests/e2e/rpc-routing-isolation.mjs"
|
|
44
|
+
"test:e2e": "node tests/e2e/rpc-contemplator.mjs && node tests/e2e/rpc-summarizer.mjs && node tests/e2e/rpc-delivery.mjs && node tests/e2e/rpc-restore-review.mjs && node tests/e2e/rpc-compaction.mjs && node tests/e2e/rpc-compaction-resilience.mjs && node tests/e2e/rpc-memory-edges.mjs && node tests/e2e/rpc-observer-length.mjs && node tests/e2e/rpc-routing-isolation.mjs"
|
|
45
45
|
},
|
|
46
46
|
"peerDependencies": {
|
|
47
47
|
"@earendil-works/pi-agent-core": "*",
|
|
@@ -26,6 +26,7 @@ interface RunObserverArgs {
|
|
|
26
26
|
thinkingLevel?: ModelThinkingLevel;
|
|
27
27
|
recordUsage?: (usage: LlmUsageInput) => void;
|
|
28
28
|
onProgress?: () => void;
|
|
29
|
+
onMessages?: (messages: readonly AgentMessage[]) => void;
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
const RelevanceSchema = Type.Union([
|
|
@@ -216,17 +217,35 @@ IMPORTANT: Now call record_observations to record the useful new observations fr
|
|
|
216
217
|
const loop = args.agentLoop ?? agentLoop;
|
|
217
218
|
const history: AgentMessage[] = [];
|
|
218
219
|
let terminalFailure: { stopReason: string; errorMessage?: string } | undefined;
|
|
219
|
-
|
|
220
|
+
let lengthRetryAttempted = false;
|
|
221
|
+
const runInvocation = async (prompt: Message, afterLength = false): Promise<void> => {
|
|
220
222
|
const context: AgentContext = {
|
|
221
223
|
systemPrompt: OBSERVER_SYSTEM,
|
|
222
224
|
messages: history.slice(),
|
|
223
225
|
tools: [recordObservations as AgentTool<any>, doneTool],
|
|
224
226
|
};
|
|
225
|
-
|
|
227
|
+
// Publish a live launch-local transcript for /om:view observer. Keep an
|
|
228
|
+
// invocation-local list because agentLoop owns its internal context copy.
|
|
229
|
+
let liveMessages: AgentMessage[] = [...history, prompt];
|
|
230
|
+
args.onMessages?.(liveMessages.slice());
|
|
231
|
+
const invocationConfig: AgentLoopConfig = afterLength && reasoning
|
|
232
|
+
? { ...baseConfig, reasoning: "minimal" }
|
|
233
|
+
: baseConfig;
|
|
234
|
+
const stream = loop([prompt], context, invocationConfig, signal, streamSimple);
|
|
226
235
|
for await (const event of stream) {
|
|
227
236
|
args.onProgress?.();
|
|
228
237
|
logAgentStreamError("observer", event);
|
|
229
|
-
const
|
|
238
|
+
const typedEvent = event as { type?: string; message?: AgentMessage & { role?: string; stopReason?: string; errorMessage?: string } };
|
|
239
|
+
const message = typedEvent.message;
|
|
240
|
+
if (message && message !== prompt && message.role !== "user") {
|
|
241
|
+
if (typedEvent.type === "message_start") liveMessages.push(message);
|
|
242
|
+
else if (typedEvent.type === "message_update" || typedEvent.type === "message_end") {
|
|
243
|
+
const index = liveMessages.map((item) => item.role).lastIndexOf(message.role);
|
|
244
|
+
if (index >= 0) liveMessages[index] = message;
|
|
245
|
+
else liveMessages.push(message);
|
|
246
|
+
}
|
|
247
|
+
args.onMessages?.(liveMessages.slice());
|
|
248
|
+
}
|
|
230
249
|
if (message?.role === "assistant" && ["error", "aborted", "length"].includes(message.stopReason ?? "")) {
|
|
231
250
|
terminalFailure = { stopReason: message.stopReason!, errorMessage: message.errorMessage };
|
|
232
251
|
}
|
|
@@ -234,6 +253,8 @@ IMPORTANT: Now call record_observations to record the useful new observations fr
|
|
|
234
253
|
const result = await stream.result();
|
|
235
254
|
if (!Array.isArray(result)) return;
|
|
236
255
|
history.push(...result);
|
|
256
|
+
liveMessages = history.slice();
|
|
257
|
+
args.onMessages?.(liveMessages);
|
|
237
258
|
for (const message of result) {
|
|
238
259
|
if (message.role === "assistant" && ["error", "aborted", "length"].includes(message.stopReason ?? "")) {
|
|
239
260
|
terminalFailure = { stopReason: message.stopReason, errorMessage: message.errorMessage };
|
|
@@ -243,6 +264,23 @@ IMPORTANT: Now call record_observations to record the useful new observations fr
|
|
|
243
264
|
};
|
|
244
265
|
|
|
245
266
|
await runInvocation(initialPrompt);
|
|
267
|
+
if (accumulated.size === 0 && terminalFailure?.stopReason === "length") {
|
|
268
|
+
lengthRetryAttempted = true;
|
|
269
|
+
// A provider can impose a lower output ceiling than the advertised model
|
|
270
|
+
// maximum. agentLoop stops on `length` when no tool call was completed; it
|
|
271
|
+
// does not automatically send a continuation request. Preserve the partial
|
|
272
|
+
// response so the model can continue from work it already performed rather
|
|
273
|
+
// than paying to reproduce it, then append a short tool-focused instruction
|
|
274
|
+
// and reduce reasoning to minimal. A second length stop fails forward at the
|
|
275
|
+
// bounded-chunk level.
|
|
276
|
+
terminalFailure = undefined;
|
|
277
|
+
const retryPrompt: Message = {
|
|
278
|
+
role: "user",
|
|
279
|
+
content: [{ type: "text", text: "IMPORTANT: The previous response reached the provider output limit before recording anything. Continue from the work already above and call record_observations now instead of spending another response budget analyzing." }],
|
|
280
|
+
timestamp: Date.now(),
|
|
281
|
+
};
|
|
282
|
+
await runInvocation(retryPrompt, true);
|
|
283
|
+
}
|
|
246
284
|
if (accumulated.size === 0 && !doneCalled && !terminalFailure && rejectedTotal === 0) {
|
|
247
285
|
const reminder: Message = {
|
|
248
286
|
role: "user",
|
|
@@ -257,7 +295,10 @@ IMPORTANT: Now call record_observations to record the useful new observations fr
|
|
|
257
295
|
// zero-observation stop is also a valid empty result after the reminder;
|
|
258
296
|
// actual stream failures, truncation, and malformed records still throw.
|
|
259
297
|
if (accumulated.size === 0 && terminalFailure) {
|
|
260
|
-
|
|
298
|
+
const detail = terminalFailure.stopReason === "length" && lengthRetryAttempted
|
|
299
|
+
? `provider reached the output limit twice without recording an observation (effective max output request: ${baseConfig.maxTokens} tokens)`
|
|
300
|
+
: terminalFailure.errorMessage;
|
|
301
|
+
throw new ObserverStreamError(terminalFailure.stopReason, detail);
|
|
261
302
|
}
|
|
262
303
|
if (accumulated.size === 0 && rejectedTotal > 0) {
|
|
263
304
|
throw new ObserverStreamError("invalid_observations", `${rejectedTotal} proposed observation${rejectedTotal === 1 ? " was" : "s were"} rejected`);
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { ObserverRunView } 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 currently active observer chunk, or the most recently completed chunk. */
|
|
38
|
+
export function renderObserver(run: ObserverRunView | undefined, now = Date.now()): string {
|
|
39
|
+
if (!run) return `${DIM}OBSERVER${RESET}\n\n${DIM}Observer 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 elapsedMs = Math.max(0, (run.completedAt ?? now) - run.startedAt);
|
|
43
|
+
const lines = [
|
|
44
|
+
`${DIM}OBSERVER · ${run.status} · ${messages.length} messages · ~${totalTokens.toLocaleString()} estimated transcript tokens${RESET}`,
|
|
45
|
+
`${DIM}Chunk ~${run.chunkTokens.toLocaleString()} tokens · backlog at start ~${run.backlogTokens.toLocaleString()} tokens · ${run.sourceEntryIds.length} source entr${run.sourceEntryIds.length === 1 ? "y" : "ies"}${RESET}`,
|
|
46
|
+
`${DIM}Started ${new Date(run.startedAt).toISOString()} · ${run.completedAt === undefined ? `running for ${Math.floor(elapsedMs / 1000)}s` : `ended ${new Date(run.completedAt).toISOString()} after ${Math.floor(elapsedMs / 1000)}s`}${RESET}`,
|
|
47
|
+
"",
|
|
48
|
+
];
|
|
49
|
+
if (messages.length === 0) lines.push(`${DIM}(no observer messages captured yet)${RESET}`);
|
|
50
|
+
for (const [index, message] of messages.entries()) {
|
|
51
|
+
if (index > 0) lines.push("");
|
|
52
|
+
const role = typeof message.role === "string" ? message.role : "unknown";
|
|
53
|
+
lines.push(`${DIM}── ${role} · ~${estimateTokens(message).toLocaleString()} tokens ──${RESET}`);
|
|
54
|
+
lines.push(renderContent(message.content) || `${DIM}(empty message)${RESET}`);
|
|
55
|
+
}
|
|
56
|
+
if (run.summary) lines.push("", `${DIM}── Completion summary ──${RESET}`, run.summary);
|
|
57
|
+
if (run.error) lines.push("", `${DIM}── Failure ──${RESET}`, run.error);
|
|
58
|
+
return lines.join("\n");
|
|
59
|
+
}
|
package/src/commands/status.ts
CHANGED
|
@@ -142,8 +142,8 @@ export function registerStatusCommand(pi: ExtensionAPI, runtime: Runtime): void
|
|
|
142
142
|
}
|
|
143
143
|
|
|
144
144
|
lines.push("", "── Last worker runs ──");
|
|
145
|
-
lines.push(`
|
|
146
|
-
lines.push(`
|
|
145
|
+
lines.push(`Observer chunk start: ${runtime.lastObserverStartedAt === undefined ? "not run this launch" : formatRunAge(runtime.lastObserverStartedAt)}`);
|
|
146
|
+
lines.push(`Observer chunk end: ${runtime.lastObserverCompletedAt === undefined ? runtime.lastObserverRun?.status === "running" ? "running" : "not completed this launch" : formatRunAge(runtime.lastObserverCompletedAt)}`);
|
|
147
147
|
lines.push(`Last summarizer start: ${runtime.lastSummarizerStartedAt === undefined ? "not run this launch" : formatRunAge(runtime.lastSummarizerStartedAt)}`);
|
|
148
148
|
lines.push(`Last summarizer end: ${runtime.lastSummarizerCompletedAt === undefined ? "not completed this launch" : formatRunAge(runtime.lastSummarizerCompletedAt)}`);
|
|
149
149
|
lines.push(`Last contemplator start: ${runtime.contemplatorState.lastStartedAt === undefined ? "not run this launch" : formatRunAge(runtime.contemplatorState.lastStartedAt)}`);
|
package/src/commands/view.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { copyTextToClipboard } from "../clipboard.js";
|
|
|
4
4
|
import { renderContemplator, stripAnsi } from "./contemplator-view.js";
|
|
5
5
|
import { renderReviewer } from "./reviewer-view.js";
|
|
6
6
|
import { renderSummarizer } from "./summarizer-view.js";
|
|
7
|
+
import { renderObserver } from "./observer-view.js";
|
|
7
8
|
import { executeRecall, formatRecallResultForTui } from "../tools/recall-observation.js";
|
|
8
9
|
import {
|
|
9
10
|
chronologicalMemories,
|
|
@@ -76,7 +77,7 @@ export function registerViewCommand(
|
|
|
76
77
|
|
|
77
78
|
pi.registerCommand("om:view", {
|
|
78
79
|
description:
|
|
79
|
-
"Print and copy pi-contemplator memory content (visible, full, memory, contemplator, summarizer, reviewer, or reviews)",
|
|
80
|
+
"Print and copy pi-contemplator memory content (visible, full, memory, contemplator, observer, summarizer, reviewer, or reviews)",
|
|
80
81
|
handler: async (args, ctx) => {
|
|
81
82
|
runtime.ensureConfig(ctx.cwd);
|
|
82
83
|
const entries = ctx.sessionManager.getBranch() as Entry[];
|
|
@@ -116,6 +117,18 @@ export function registerViewCommand(
|
|
|
116
117
|
return;
|
|
117
118
|
}
|
|
118
119
|
|
|
120
|
+
if (mode === "observer") {
|
|
121
|
+
const output = renderObserver(runtime.lastObserverRun);
|
|
122
|
+
const copied = await copyToClipboard(stripAnsi(output)).catch(() => false);
|
|
123
|
+
ctx.ui.notify(
|
|
124
|
+
`${output}
|
|
125
|
+
|
|
126
|
+
${copied ? "Copied /om:view observer output to clipboard." : "Warning: failed to copy /om:view observer output to clipboard."}`,
|
|
127
|
+
"info",
|
|
128
|
+
);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
119
132
|
if (mode === "summarizer") {
|
|
120
133
|
const output = renderSummarizer(runtime.lastSummarizerRun);
|
|
121
134
|
const copied = await copyToClipboard(stripAnsi(output)).catch(() => false);
|
|
@@ -154,7 +167,7 @@ export function registerViewCommand(
|
|
|
154
167
|
}
|
|
155
168
|
|
|
156
169
|
if (mode && mode !== "visible") {
|
|
157
|
-
ctx.ui.notify("Usage: /om:view [visible|full|memory <id>|contemplator|summarizer|reviewer|reviews]", "info");
|
|
170
|
+
ctx.ui.notify("Usage: /om:view [visible|full|memory <id>|contemplator|observer|summarizer|reviewer|reviews]", "info");
|
|
158
171
|
return;
|
|
159
172
|
}
|
|
160
173
|
|
|
@@ -5,6 +5,7 @@ import { debugLog, withDebugLogContext } from "../debug-log.js";
|
|
|
5
5
|
import { resolveObserverChunkMaxTokens } from "../config.js";
|
|
6
6
|
import type { ResolveResult, Runtime } from "../runtime.js";
|
|
7
7
|
import { createWorkerStallWatchdog } from "../worker-watchdog.js";
|
|
8
|
+
import { boundedMaxTokens, OBSERVER_AGENT_LOOP_MAX_TOKENS } from "../model-budget.js";
|
|
8
9
|
import { serializeSourceAddressedBranchEntries } from "../serialize.js";
|
|
9
10
|
import {
|
|
10
11
|
OM_SUMMARIZER_COMMIT,
|
|
@@ -226,7 +227,6 @@ export async function runConsolidationPipeline(
|
|
|
226
227
|
|
|
227
228
|
const beforeFold = foldLedger(ctx.sessionManager.getBranch() as Entry[]);
|
|
228
229
|
runtime.consolidationPhase = "observer";
|
|
229
|
-
runtime.lastObserverStartedAt = Date.now();
|
|
230
230
|
try {
|
|
231
231
|
// A large backlog is drained in bounded, oldest-first chunks. The normal
|
|
232
232
|
// trigger threshold controls when the batch stops; a static compaction
|
|
@@ -252,8 +252,6 @@ export async function runConsolidationPipeline(
|
|
|
252
252
|
} catch (error) {
|
|
253
253
|
debugLog("observer.error", { errorMessage: runtime.recordConsolidationStageError(ctx, "observer", error) });
|
|
254
254
|
return;
|
|
255
|
-
} finally {
|
|
256
|
-
runtime.lastObserverCompletedAt = Date.now();
|
|
257
255
|
}
|
|
258
256
|
const afterFold = foldLedger(ctx.sessionManager.getBranch() as Entry[]);
|
|
259
257
|
const beforeIds = new Set(beforeFold.observations.map((item) => item.id));
|
|
@@ -466,6 +464,9 @@ async function runObserverStage(
|
|
|
466
464
|
debugLog("observer.start", {
|
|
467
465
|
tokens,
|
|
468
466
|
chunkTokens,
|
|
467
|
+
requestedMaxOutputTokens: OBSERVER_AGENT_LOOP_MAX_TOKENS,
|
|
468
|
+
effectiveMaxOutputTokens: boundedMaxTokens(resolved.model as any, OBSERVER_AGENT_LOOP_MAX_TOKENS),
|
|
469
|
+
advertisedModelMaxTokens: (resolved.model as { maxTokens?: number }).maxTokens,
|
|
469
470
|
coversUpToId,
|
|
470
471
|
sourceEntryIds,
|
|
471
472
|
sourceEntryCount: sourceEntryIds.length,
|
|
@@ -473,6 +474,20 @@ async function runObserverStage(
|
|
|
473
474
|
priorObservations: priorObservations.length,
|
|
474
475
|
});
|
|
475
476
|
|
|
477
|
+
const observerStartedAt = Date.now();
|
|
478
|
+
runtime.lastObserverStartedAt = observerStartedAt;
|
|
479
|
+
// Clear the previous end when a new chunk starts so /om:status never pairs
|
|
480
|
+
// this chunk's start with the preceding chunk's completion.
|
|
481
|
+
runtime.lastObserverCompletedAt = undefined;
|
|
482
|
+
runtime.lastObserverRun = {
|
|
483
|
+
startedAt: observerStartedAt,
|
|
484
|
+
status: "running",
|
|
485
|
+
messages: [],
|
|
486
|
+
chunkTokens,
|
|
487
|
+
backlogTokens: tokens,
|
|
488
|
+
sourceEntryIds: sourceEntryIds.slice(),
|
|
489
|
+
};
|
|
490
|
+
let acceptsObserverMessages = true;
|
|
476
491
|
let observations;
|
|
477
492
|
let failedMessage: string | undefined;
|
|
478
493
|
const observerWatchdog = createWorkerStallWatchdog("observer");
|
|
@@ -489,6 +504,17 @@ async function runObserverStage(
|
|
|
489
504
|
thinkingLevel: runtime.config.model?.thinking ?? "low",
|
|
490
505
|
recordUsage: (usage) => runtime.recordAgentUsage(usage),
|
|
491
506
|
onProgress: observerWatchdog.progress,
|
|
507
|
+
onMessages: (messages) => {
|
|
508
|
+
if (!acceptsObserverMessages || options.contextGeneration !== undefined && options.contextGeneration !== runtime.getContextGeneration()) return;
|
|
509
|
+
runtime.lastObserverRun = {
|
|
510
|
+
startedAt: observerStartedAt,
|
|
511
|
+
status: "running",
|
|
512
|
+
messages: messages.slice(),
|
|
513
|
+
chunkTokens,
|
|
514
|
+
backlogTokens: tokens,
|
|
515
|
+
sourceEntryIds: sourceEntryIds.slice(),
|
|
516
|
+
};
|
|
517
|
+
},
|
|
492
518
|
signal: observerWatchdog.signal,
|
|
493
519
|
}));
|
|
494
520
|
} catch (error) {
|
|
@@ -505,8 +531,19 @@ async function runObserverStage(
|
|
|
505
531
|
chunkTokens,
|
|
506
532
|
});
|
|
507
533
|
observations = undefined;
|
|
534
|
+
if (runtime.lastObserverRun?.startedAt === observerStartedAt) {
|
|
535
|
+
runtime.lastObserverRun = { ...runtime.lastObserverRun, status: "failed", error: failedMessage };
|
|
536
|
+
}
|
|
508
537
|
} finally {
|
|
538
|
+
acceptsObserverMessages = false;
|
|
509
539
|
observerWatchdog.dispose();
|
|
540
|
+
if (options.contextGeneration === undefined || options.contextGeneration === runtime.getContextGeneration()) {
|
|
541
|
+
const completedAt = Date.now();
|
|
542
|
+
runtime.lastObserverCompletedAt = completedAt;
|
|
543
|
+
if (runtime.lastObserverRun?.startedAt === observerStartedAt) {
|
|
544
|
+
runtime.lastObserverRun = { ...runtime.lastObserverRun, completedAt };
|
|
545
|
+
}
|
|
546
|
+
}
|
|
510
547
|
}
|
|
511
548
|
if (options.contextGeneration !== undefined && options.contextGeneration !== runtime.getContextGeneration()) {
|
|
512
549
|
debugLog("observer.stale", { reason: "session_or_branch_changed" });
|
|
@@ -527,6 +564,15 @@ async function runObserverStage(
|
|
|
527
564
|
});
|
|
528
565
|
}
|
|
529
566
|
const accepted = observations ?? [];
|
|
567
|
+
if (!failedMessage && runtime.lastObserverRun?.startedAt === observerStartedAt) {
|
|
568
|
+
runtime.lastObserverRun = {
|
|
569
|
+
...runtime.lastObserverRun,
|
|
570
|
+
status: "completed",
|
|
571
|
+
summary: accepted.length > 0
|
|
572
|
+
? `${accepted.length} observation${accepted.length === 1 ? "" : "s"} recorded; chunk covered through ${effectiveCoversUpToId}.`
|
|
573
|
+
: `No observations recorded; chunk covered through ${effectiveCoversUpToId}.`,
|
|
574
|
+
};
|
|
575
|
+
}
|
|
530
576
|
const data = buildObservationsRecordedData(accepted, effectiveCoversUpToId);
|
|
531
577
|
if (!data) return "continue";
|
|
532
578
|
debugLog(failedMessage ? "observer.failed_coverage" : accepted.length > 0 ? "observer.records" : "observer.coverage_only", {
|
package/src/runtime.ts
CHANGED
|
@@ -56,6 +56,18 @@ export interface MemoryUpdateCtx extends LaunchCtx {
|
|
|
56
56
|
|
|
57
57
|
export type SettingsUpdate = Partial<SessionSettings>;
|
|
58
58
|
|
|
59
|
+
export interface ObserverRunView {
|
|
60
|
+
startedAt: number;
|
|
61
|
+
completedAt?: number;
|
|
62
|
+
status: "running" | "completed" | "failed";
|
|
63
|
+
messages: readonly unknown[];
|
|
64
|
+
chunkTokens: number;
|
|
65
|
+
backlogTokens: number;
|
|
66
|
+
sourceEntryIds: readonly string[];
|
|
67
|
+
summary?: string;
|
|
68
|
+
error?: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
59
71
|
export interface SummarizerRunView {
|
|
60
72
|
startedAt: number;
|
|
61
73
|
completedAt?: number;
|
|
@@ -187,6 +199,8 @@ export class Runtime {
|
|
|
187
199
|
lastObserverCompletedAt: number | undefined;
|
|
188
200
|
lastSummarizerStartedAt: number | undefined;
|
|
189
201
|
lastSummarizerCompletedAt: number | undefined;
|
|
202
|
+
/** Current or most recent observer chunk transcript in this launch/session context. */
|
|
203
|
+
lastObserverRun: ObserverRunView | undefined;
|
|
190
204
|
/** Most recent summarizer transcript in this extension launch/session context. */
|
|
191
205
|
lastSummarizerRun: SummarizerRunView | undefined;
|
|
192
206
|
/** Launch-local liveness and trigger diagnostics published by the contemplator. */
|
|
@@ -277,6 +291,7 @@ export class Runtime {
|
|
|
277
291
|
this.lastObserverCompletedAt = undefined;
|
|
278
292
|
this.lastSummarizerStartedAt = undefined;
|
|
279
293
|
this.lastSummarizerCompletedAt = undefined;
|
|
294
|
+
this.lastObserverRun = undefined;
|
|
280
295
|
this.lastSummarizerRun = undefined;
|
|
281
296
|
this.contemplatorState = {
|
|
282
297
|
running: false,
|