@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
|
@@ -5,7 +5,7 @@ import { streamSimple } from "@earendil-works/pi-ai/compat";
|
|
|
5
5
|
import { generateSummaryWithUsage } from "@earendil-works/pi-coding-agent";
|
|
6
6
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
7
7
|
import { Box, Text } from "@earendil-works/pi-tui";
|
|
8
|
-
import { agentActiveTimeMs, assistantOutputTokens, assistantToolCallCount, fullProjection, isReviewRequestEntry, isReviewResultEntry, OM_AGENT_ACTIVITY, OM_REVIEWER_MESSAGE, OM_REVIEWER_NOTICE, OM_REVIEWER_STATE, OM_REVIEW_REQUEST, OM_REVIEW_RESULT, type Entry, type ReviewResult, type StructuralReviewRequest } from "../../session-ledger/index.js";
|
|
8
|
+
import { agentActiveTimeMs, assistantOutputTokens, assistantToolCallCount, fullProjection, isReviewRequestEntry, isReviewResultEntry, OM_AGENT_ACTIVITY, OM_REVIEWER_MESSAGE, OM_REVIEWER_NOTICE, OM_REVIEWER_STATE, OM_REVIEW_REQUEST, OM_REVIEW_RESULT, recallMemorySources, type Entry, type ReviewResult, type StructuralReviewRequest } from "../../session-ledger/index.js";
|
|
9
9
|
import { hashId } from "../../ids.js";
|
|
10
10
|
import { createSearchMemoriesAgentTool } from "../../tools/search-memories.js";
|
|
11
11
|
import { createRecallAgentTool } from "../../tools/recall-observation.js";
|
|
@@ -13,12 +13,14 @@ import type { MemoryUpdateCtx, Runtime } from "../../runtime.js";
|
|
|
13
13
|
import { logAgentStreamError } from "../stream-errors.js";
|
|
14
14
|
import { debugLog, withDebugLogContext } from "../../debug-log.js";
|
|
15
15
|
import { boundedMaxTokens, AGENT_LOOP_MAX_TOKENS } from "../../model-budget.js";
|
|
16
|
+
import { forceRequiredToolPayload, requiredToolChoice } from "../../required-tool-choice.js";
|
|
17
|
+
import { memoryReferenceIds } from "../../memory-citations.js";
|
|
16
18
|
import { buildContemplatorSystemPrompt } from "./prompts.js";
|
|
17
19
|
import { runStructuralReview } from "../reviewer/agent.js";
|
|
18
20
|
|
|
19
21
|
interface PendingUpdate {
|
|
20
22
|
observations: string[];
|
|
21
|
-
|
|
23
|
+
summaries: string[];
|
|
22
24
|
reviews: string[];
|
|
23
25
|
mainAgentOutputTokens: number;
|
|
24
26
|
mainAgentToolCalls: number;
|
|
@@ -27,7 +29,8 @@ interface PendingUpdate {
|
|
|
27
29
|
|
|
28
30
|
type Intervention =
|
|
29
31
|
| { kind: "probe"; question: string }
|
|
30
|
-
| { kind: "review"; request: Omit<StructuralReviewRequest, "createdAt" | "requestedBy"> }
|
|
32
|
+
| { kind: "review"; request: Omit<StructuralReviewRequest, "createdAt" | "requestedBy"> }
|
|
33
|
+
| { kind: "none" };
|
|
31
34
|
|
|
32
35
|
type ReviewerSession = {
|
|
33
36
|
scope: StructuralReviewRequest["scope"];
|
|
@@ -93,6 +96,7 @@ function customMessageText(content: unknown): string {
|
|
|
93
96
|
}
|
|
94
97
|
|
|
95
98
|
const AGENT_TIME_BUCKET_MINUTES = 5;
|
|
99
|
+
export const CONTEMPLATOR_MAX_INVOCATIONS = 3;
|
|
96
100
|
|
|
97
101
|
function coarseAgentTime(durationMs: number): string {
|
|
98
102
|
const totalMinutes = Math.floor(durationMs / 60_000);
|
|
@@ -109,6 +113,7 @@ const CONTEMPLATOR_STATE = "om.contemplator.state";
|
|
|
109
113
|
const CONTEMPLATOR_SUGGESTION = "om.contemplator.suggestion";
|
|
110
114
|
const REVIEW_PROPOSAL_MESSAGE = "om.review.proposal";
|
|
111
115
|
const SendProbeSchema = Type.Object({ question: Type.String({ minLength: 1, description: "One concise, memory-grounded probing question, optionally preceded by one short sentence of context. Cite relevant memory identifiers." }) });
|
|
116
|
+
const NoInterventionSchema = Type.Object({});
|
|
112
117
|
const ReviewScopeSchema = Type.Union([Type.Literal("workflow"), Type.Literal("software")]);
|
|
113
118
|
export const RequestReviewSchema = Type.Object({
|
|
114
119
|
scope: ReviewScopeSchema,
|
|
@@ -120,37 +125,86 @@ export const RequestReviewSchema = Type.Object({
|
|
|
120
125
|
type SendProbeArgs = Static<typeof SendProbeSchema>;
|
|
121
126
|
export type RequestReviewArgs = Static<typeof RequestReviewSchema>;
|
|
122
127
|
|
|
123
|
-
|
|
128
|
+
type InterventionWrite = { overwritten: boolean };
|
|
129
|
+
type ReviewWrite = InterventionWrite & { reviewRequestId: string };
|
|
130
|
+
|
|
131
|
+
function interventionResultText(options: {
|
|
132
|
+
kind: "probe" | "review";
|
|
133
|
+
memoryIds: string[];
|
|
134
|
+
memoryExists: (id: string) => boolean;
|
|
135
|
+
overwritten: boolean;
|
|
136
|
+
queuedText: string;
|
|
137
|
+
}): string {
|
|
138
|
+
const replacementTool = options.kind === "probe" ? "send_probe" : "request_review";
|
|
139
|
+
const warnings = options.memoryIds
|
|
140
|
+
.filter((id) => !options.memoryExists(id))
|
|
141
|
+
.map((id) => `WARNING: memory ${id} not found; use search_memories and recall to find the correct memory, then call ${replacementTool} again to replace the ${options.kind} before it is sent.`);
|
|
142
|
+
if (options.overwritten) warnings.push("WARNING: overwriting prior probe/review tool call; only one action may be taken per turn.");
|
|
143
|
+
warnings.push(options.queuedText);
|
|
144
|
+
return warnings.join("\n");
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function createSendProbeTool(
|
|
148
|
+
onProbe: (question: string) => InterventionWrite,
|
|
149
|
+
memoryExists: (id: string) => boolean = () => true,
|
|
150
|
+
): AgentTool<typeof SendProbeSchema> {
|
|
124
151
|
return {
|
|
125
152
|
name: "send_probe",
|
|
126
153
|
label: "Send probe",
|
|
127
|
-
description: "Send one concise, high-level probing question to the primary agent asynchronously. The message must contain one focused question, optionally preceded by one short sentence of context, and cite relevant memory identifiers. Do not use it for routine reminders, status updates, generic advice, direct task management, or a structural design deserving review.",
|
|
154
|
+
description: "Send one concise, high-level probing question to the primary agent asynchronously. The message must contain one focused question, optionally preceded by one short sentence of context, and cite relevant memory identifiers. Do not use it for routine reminders, status updates, generic advice, direct task management, or a structural design deserving review. This is a terminal tool when all cited memory ids are valid; citation warnings leave the turn open so the action can be replaced. A later intervention call in the same turn replaces this one.",
|
|
128
155
|
parameters: SendProbeSchema,
|
|
129
156
|
execute: async (_toolCallId, params: SendProbeArgs) => {
|
|
130
157
|
const question = params.question.trim();
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
|
|
158
|
+
const write = onProbe(question);
|
|
159
|
+
const memoryIds = memoryReferenceIds(question);
|
|
160
|
+
debugLog("contemplator.tool_call", { tool: "send_probe", suggestionLength: question.length, memoryIds, overwritten: write.overwritten });
|
|
161
|
+
return {
|
|
162
|
+
content: [{ type: "text", text: interventionResultText({ kind: "probe", memoryIds, memoryExists, overwritten: write.overwritten, queuedText: "Probe will be delivered at the end of your turn." }) }],
|
|
163
|
+
details: { queued: true, overwritten: write.overwritten, memoryIds },
|
|
164
|
+
};
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function createNoInterventionTool(
|
|
170
|
+
onNoIntervention: () => InterventionWrite,
|
|
171
|
+
): AgentTool<typeof NoInterventionSchema> {
|
|
172
|
+
return {
|
|
173
|
+
name: "no_intervention",
|
|
174
|
+
label: "No intervention",
|
|
175
|
+
description: "Terminally end this contemplator update without sending anything to the primary agent. This argument-free tool is the preferred default whenever no specific, grounded, materially useful intervention is clearly warranted or usefulness is uncertain. Never send a probe merely to avoid choosing no_intervention. A later final-action call in the same turn replaces an earlier warned action.",
|
|
176
|
+
parameters: NoInterventionSchema,
|
|
177
|
+
execute: async () => {
|
|
178
|
+
const write = onNoIntervention();
|
|
179
|
+
debugLog("contemplator.no_intervention", { overwritten: write.overwritten });
|
|
180
|
+
const warning = write.overwritten ? "WARNING: overwriting prior probe/review/no_intervention tool call; only one final action may be taken per turn.\n" : "";
|
|
181
|
+
return {
|
|
182
|
+
content: [{ type: "text", text: `${warning}No intervention will be sent.` }],
|
|
183
|
+
details: { selected: true, overwritten: write.overwritten },
|
|
184
|
+
};
|
|
136
185
|
},
|
|
137
186
|
};
|
|
138
187
|
}
|
|
139
188
|
|
|
140
|
-
export function createRequestReviewTool(
|
|
189
|
+
export function createRequestReviewTool(
|
|
190
|
+
onReview: (request: RequestReviewArgs) => ReviewWrite,
|
|
191
|
+
memoryExists: (id: string) => boolean = () => true,
|
|
192
|
+
): AgentTool<typeof RequestReviewSchema> {
|
|
141
193
|
return {
|
|
142
194
|
name: "request_review",
|
|
143
195
|
label: "Request structural review",
|
|
144
|
-
description: "Request a short-lived structural review grounded in cited memories. Use workflow for recurring problems in how work is performed and software for recurring problems in the product structure. Identify evidence, the suspected concern, review focus, and constraints without designing the solution.",
|
|
196
|
+
description: "Request a short-lived structural review grounded in cited memories. Use workflow for recurring problems in how work is performed and software for recurring problems in the product structure. Identify evidence, the suspected concern, review focus, and constraints without designing the solution. This is a terminal tool when all cited memory ids are valid; citation warnings leave the turn open so the action can be replaced. A later intervention call in the same turn replaces this one.",
|
|
145
197
|
parameters: RequestReviewSchema,
|
|
146
198
|
execute: async (_toolCallId, params: RequestReviewArgs) => {
|
|
147
199
|
const request = { ...params, evidence: params.evidence.trim(), concern: params.concern.trim(), review_focus: params.review_focus.trim(), constraints: params.constraints?.trim() || undefined };
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
|
|
200
|
+
const write = onReview(request);
|
|
201
|
+
const memoryIds = memoryReferenceIds([request.evidence, request.concern, request.review_focus, request.constraints].filter((value): value is string => Boolean(value)).join("\n"));
|
|
202
|
+
debugLog("contemplator.review_requested", { reviewRequestId: write.reviewRequestId, scope: request.scope, evidenceLength: request.evidence.length, concernLength: request.concern.length, memoryIds, overwritten: write.overwritten });
|
|
203
|
+
const queuedText = `${request.scope === "workflow" ? "Workflow" : "Software"} review [${write.reviewRequestId}] will be started at the end of your turn.`;
|
|
204
|
+
return {
|
|
205
|
+
content: [{ type: "text", text: interventionResultText({ kind: "review", memoryIds, memoryExists, overwritten: write.overwritten, queuedText }) }],
|
|
206
|
+
details: { queued: true, overwritten: write.overwritten, scope: request.scope, reviewRequestId: write.reviewRequestId, memoryIds },
|
|
207
|
+
};
|
|
154
208
|
},
|
|
155
209
|
};
|
|
156
210
|
}
|
|
@@ -160,7 +214,7 @@ export class Contemplator {
|
|
|
160
214
|
private pending: PendingUpdate | undefined;
|
|
161
215
|
private running = false;
|
|
162
216
|
private seenObservationIds = new Set<string>();
|
|
163
|
-
private
|
|
217
|
+
private seenSummaryIds = new Set<string>();
|
|
164
218
|
private seenReviewIds = new Set<string>();
|
|
165
219
|
private inFlightReviewKeys = new Set<string>();
|
|
166
220
|
private inFlightReviewIds = new Set<string>();
|
|
@@ -172,7 +226,10 @@ export class Contemplator {
|
|
|
172
226
|
private queuedProbeIds = new Set<string>();
|
|
173
227
|
private sessionGeneration = 0;
|
|
174
228
|
private latestCtx: MemoryUpdateCtx | undefined;
|
|
229
|
+
/** Completed primary-model responses since the previous contemplator run. */
|
|
175
230
|
private turnsSinceRun = 0;
|
|
231
|
+
/** Used to avoid counting the final turn_end after its assistant message_end. */
|
|
232
|
+
private assistantResponsesInCurrentTurn = 0;
|
|
176
233
|
private restoredTipId: string | undefined;
|
|
177
234
|
/** Start of the unpersisted portion of the current main-agent run. */
|
|
178
235
|
private agentActiveSince: number | undefined;
|
|
@@ -181,7 +238,12 @@ export class Contemplator {
|
|
|
181
238
|
|
|
182
239
|
register(): void {
|
|
183
240
|
this.pi.registerMessageRenderer(CONTEMPLATOR_SUGGESTION, (message, _options, theme) => {
|
|
184
|
-
const
|
|
241
|
+
const details = message.details as { question?: unknown } | undefined;
|
|
242
|
+
const content = typeof details?.question === "string"
|
|
243
|
+
? details.question
|
|
244
|
+
: customMessageText(message.content)
|
|
245
|
+
.replace(/^Background contemplator probe \(advisory\):\n?/, "")
|
|
246
|
+
.replace(/\n\nReferenced memories can be reviewed using the recall tool\.\s*$/, "");
|
|
185
247
|
const box = new Box(1, 1, (text) => theme.bg("customMessageBg", text));
|
|
186
248
|
box.addChild(new Text(theme.fg("thinkingHigh", `${theme.bold("◆ CONTEMPLATOR PROBE")}\n${content}`), 0, 0));
|
|
187
249
|
return box;
|
|
@@ -201,12 +263,18 @@ export class Contemplator {
|
|
|
201
263
|
});
|
|
202
264
|
this.pi.on("session_start", (event: any, ctx: ExtensionContext) => {
|
|
203
265
|
this.sessionGeneration++;
|
|
266
|
+
const generation = this.sessionGeneration;
|
|
204
267
|
this.agentActiveSince = undefined;
|
|
205
268
|
// AgentSession preserves its steering queue across extension reloads. An
|
|
206
269
|
// undelivered tracking entry therefore still has a live queued message;
|
|
207
270
|
// restoring it here would enqueue the same probe a second time.
|
|
208
271
|
const reload = event?.reason === "reload";
|
|
209
272
|
this.restore(ctx, true, reload, reload);
|
|
273
|
+
// Reconstruct and schedule durable memory immediately. In particular, a
|
|
274
|
+
// reload after a failed run must not silently mark its pending backlog seen.
|
|
275
|
+
queueMicrotask(() => {
|
|
276
|
+
if (generation === this.sessionGeneration) this.withDebugContext(ctx, () => this.observeTurn(ctx));
|
|
277
|
+
});
|
|
210
278
|
});
|
|
211
279
|
this.pi.on("session_tree", (_event: any, ctx: ExtensionContext) => {
|
|
212
280
|
this.sessionGeneration++;
|
|
@@ -220,7 +288,7 @@ export class Contemplator {
|
|
|
220
288
|
this.history = [];
|
|
221
289
|
this.pending = undefined;
|
|
222
290
|
this.seenObservationIds.clear();
|
|
223
|
-
this.
|
|
291
|
+
this.seenSummaryIds.clear();
|
|
224
292
|
this.seenReviewIds.clear();
|
|
225
293
|
this.inFlightReviewKeys.clear();
|
|
226
294
|
this.inFlightReviewIds.clear();
|
|
@@ -231,21 +299,38 @@ export class Contemplator {
|
|
|
231
299
|
this.queuedProbeIds.clear();
|
|
232
300
|
this.latestCtx = undefined;
|
|
233
301
|
this.turnsSinceRun = 0;
|
|
302
|
+
this.assistantResponsesInCurrentTurn = 0;
|
|
234
303
|
this.restoredTipId = undefined;
|
|
304
|
+
this.runtime.contemplatorState = {
|
|
305
|
+
running: false,
|
|
306
|
+
pendingObservations: 0,
|
|
307
|
+
pendingSummaries: 0,
|
|
308
|
+
pendingReviews: 0,
|
|
309
|
+
responsesSinceRun: 0,
|
|
310
|
+
waitingFor: "idle",
|
|
311
|
+
};
|
|
235
312
|
});
|
|
236
313
|
this.pi.on("session_compact", (_event: any, ctx: ExtensionContext) => {
|
|
237
|
-
//
|
|
238
|
-
//
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
this.pi.appendEntry(CONTEMPLATOR_STATE, { version: 1, history });
|
|
314
|
+
// In-flight invocation messages remain local to flush until its required
|
|
315
|
+
// final action is selected, so history always contains only completed work.
|
|
316
|
+
if (this.history.length > 0) {
|
|
317
|
+
this.pi.appendEntry(CONTEMPLATOR_STATE, { version: 1, history: this.history });
|
|
242
318
|
this.markTipPersisted(ctx);
|
|
243
|
-
debugLog("contemplator.state_persisted", { historyMessageCount: history.length, running: this.running });
|
|
319
|
+
debugLog("contemplator.state_persisted", { historyMessageCount: this.history.length, running: this.running });
|
|
244
320
|
}
|
|
245
321
|
this.persistReviewerStates(ctx);
|
|
246
322
|
});
|
|
247
|
-
this.pi.on("message_end", (event: any) => {
|
|
323
|
+
this.pi.on("message_end", (event: any, ctx: ExtensionContext) => {
|
|
248
324
|
const message = event?.message;
|
|
325
|
+
// A Pi turn can contain hours of assistant/tool/model rounds. Count each
|
|
326
|
+
// completed primary-model response, not only the eventual turn_end, or the
|
|
327
|
+
// contemplator can remain throttled forever during a long autonomous run.
|
|
328
|
+
if (message?.role === "assistant") {
|
|
329
|
+
this.persistAgentActivity(ctx);
|
|
330
|
+
this.turnsSinceRun++;
|
|
331
|
+
this.assistantResponsesInCurrentTurn++;
|
|
332
|
+
this.withDebugContext(ctx, () => this.observeTurn(ctx));
|
|
333
|
+
}
|
|
249
334
|
if (message?.role !== "custom" || message.customType !== CONTEMPLATOR_SUGGESTION) return;
|
|
250
335
|
if (typeof message.details?.probeId !== "string") return;
|
|
251
336
|
// message_end means Pi has drained the steer into the conversation
|
|
@@ -253,6 +338,11 @@ export class Contemplator {
|
|
|
253
338
|
// tree restore must be allowed to requeue it until context acknowledges it.
|
|
254
339
|
this.queuedProbeIds.delete(message.details.probeId);
|
|
255
340
|
});
|
|
341
|
+
this.pi.on("tool_execution_end", (_event: unknown, ctx: ExtensionContext) => {
|
|
342
|
+
// This records one wall-clock interval regardless of how many tools were
|
|
343
|
+
// running concurrently; persistAgentActivity restarts the shared clock.
|
|
344
|
+
this.persistAgentActivity(ctx);
|
|
345
|
+
});
|
|
256
346
|
this.pi.on("context", (event: any, ctx: ExtensionContext) => {
|
|
257
347
|
const deliveredMessages = event.messages?.filter((message: any) => message?.role === "custom" && message.customType === CONTEMPLATOR_SUGGESTION && typeof message.details?.probeId === "string") ?? [];
|
|
258
348
|
for (const delivered of deliveredMessages) {
|
|
@@ -274,7 +364,10 @@ export class Contemplator {
|
|
|
274
364
|
});
|
|
275
365
|
this.pi.on("turn_end", (_event: any, ctx: ExtensionContext) => {
|
|
276
366
|
this.persistAgentActivity(ctx);
|
|
277
|
-
|
|
367
|
+
// Normally message_end already counted the final assistant response. Keep a
|
|
368
|
+
// one-response fallback for hosts/tests that emit turn_end without it.
|
|
369
|
+
if (this.assistantResponsesInCurrentTurn === 0) this.turnsSinceRun++;
|
|
370
|
+
this.assistantResponsesInCurrentTurn = 0;
|
|
278
371
|
this.withDebugContext(ctx, () => this.observeTurn(ctx));
|
|
279
372
|
});
|
|
280
373
|
}
|
|
@@ -287,6 +380,9 @@ export class Contemplator {
|
|
|
287
380
|
const durationMs = Math.max(0, endedAt - startedAt);
|
|
288
381
|
if (durationMs === 0) return;
|
|
289
382
|
this.pi.appendEntry(OM_AGENT_ACTIVITY, { version: 1, durationMs, endedAt });
|
|
383
|
+
// Notify only after appendEntry so active-time schedulers always observe the
|
|
384
|
+
// checkpoint, regardless of Pi's ordering between independent event handlers.
|
|
385
|
+
this.runtime.notifyAgentActivity(ctx);
|
|
290
386
|
debugLog("agent.activity_recorded", { durationMs });
|
|
291
387
|
}
|
|
292
388
|
|
|
@@ -301,6 +397,22 @@ export class Contemplator {
|
|
|
301
397
|
}, fn);
|
|
302
398
|
}
|
|
303
399
|
|
|
400
|
+
private publishState(
|
|
401
|
+
waitingFor: typeof this.runtime.contemplatorState.waitingFor,
|
|
402
|
+
overrides: Partial<typeof this.runtime.contemplatorState> = {},
|
|
403
|
+
): void {
|
|
404
|
+
this.runtime.contemplatorState = {
|
|
405
|
+
...this.runtime.contemplatorState,
|
|
406
|
+
running: this.running,
|
|
407
|
+
pendingObservations: this.pending?.observations.length ?? 0,
|
|
408
|
+
pendingSummaries: this.pending?.summaries.length ?? 0,
|
|
409
|
+
pendingReviews: this.pending?.reviews.length ?? 0,
|
|
410
|
+
responsesSinceRun: this.turnsSinceRun,
|
|
411
|
+
waitingFor,
|
|
412
|
+
...overrides,
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
|
|
304
416
|
private restore(ctx: MemoryUpdateCtx, resetTracking = false, retainQueuedIds = false, skipUndeliveredRestore = false): void {
|
|
305
417
|
this.latestCtx = ctx;
|
|
306
418
|
const entries = ctx.sessionManager.getBranch() as Entry[];
|
|
@@ -308,6 +420,7 @@ export class Contemplator {
|
|
|
308
420
|
if (this.running && !resetTracking) return;
|
|
309
421
|
if (tipId === this.restoredTipId && !resetTracking) return;
|
|
310
422
|
this.history = [];
|
|
423
|
+
let resetProjection: ReturnType<typeof fullProjection> | undefined;
|
|
311
424
|
if (resetTracking) {
|
|
312
425
|
this.deliveredProbeIds.clear();
|
|
313
426
|
if (!retainQueuedIds) this.queuedProbeIds.clear();
|
|
@@ -315,12 +428,21 @@ export class Contemplator {
|
|
|
315
428
|
this.resolvingReviewIds.clear();
|
|
316
429
|
this.resumedReviewIds.clear();
|
|
317
430
|
this.reviewerSessions.clear();
|
|
318
|
-
|
|
319
|
-
this.seenObservationIds
|
|
320
|
-
this.
|
|
321
|
-
this.seenReviewIds
|
|
431
|
+
resetProjection = fullProjection(entries);
|
|
432
|
+
this.seenObservationIds.clear();
|
|
433
|
+
this.seenSummaryIds.clear();
|
|
434
|
+
this.seenReviewIds.clear();
|
|
322
435
|
this.pending = undefined;
|
|
323
436
|
this.turnsSinceRun = 0;
|
|
437
|
+
this.assistantResponsesInCurrentTurn = 0;
|
|
438
|
+
this.runtime.contemplatorState = {
|
|
439
|
+
running: false,
|
|
440
|
+
pendingObservations: 0,
|
|
441
|
+
pendingSummaries: 0,
|
|
442
|
+
pendingReviews: 0,
|
|
443
|
+
responsesSinceRun: 0,
|
|
444
|
+
waitingFor: "idle",
|
|
445
|
+
};
|
|
324
446
|
}
|
|
325
447
|
const undeliveredSuggestions = new Map<string, string>();
|
|
326
448
|
for (const entry of entries) {
|
|
@@ -386,6 +508,27 @@ export class Contemplator {
|
|
|
386
508
|
}
|
|
387
509
|
}
|
|
388
510
|
}
|
|
511
|
+
if (resetTracking && resetProjection) {
|
|
512
|
+
// Successful contemplator update prompts are the durable coverage record.
|
|
513
|
+
// Only memories present in those prompts are considered seen after reload;
|
|
514
|
+
// memories from a failed, unpersisted run remain pending and retryable.
|
|
515
|
+
const coveredIds = new Set<string>();
|
|
516
|
+
for (const message of this.history) {
|
|
517
|
+
if (message.role !== "user") continue;
|
|
518
|
+
const text = customMessageText(message.content);
|
|
519
|
+
if (!text.includes("NEW MEMORY UPDATE")) continue;
|
|
520
|
+
for (const id of memoryReferenceIds(text)) coveredIds.add(id);
|
|
521
|
+
}
|
|
522
|
+
this.seenObservationIds = new Set(resetProjection.observations.filter((item) => coveredIds.has(item.id)).map((item) => item.id));
|
|
523
|
+
this.seenSummaryIds = new Set(resetProjection.summaries.filter((item) => coveredIds.has(item.id)).map((item) => item.id));
|
|
524
|
+
this.seenReviewIds = new Set((resetProjection.reviews ?? []).filter((item) => coveredIds.has(item.id)).map((item) => item.id));
|
|
525
|
+
const unprocessedObservations = resetProjection.observations.length - this.seenObservationIds.size;
|
|
526
|
+
const unprocessedSummaries = resetProjection.summaries.length - this.seenSummaryIds.size;
|
|
527
|
+
const unprocessedReviews = (resetProjection.reviews?.length ?? 0) - this.seenReviewIds.size;
|
|
528
|
+
if (unprocessedReviews > 0 || unprocessedObservations >= this.runtime.config.contemplatorMinNewObservations || unprocessedSummaries >= this.runtime.config.contemplatorMinNewSummaries) {
|
|
529
|
+
this.turnsSinceRun = this.runtime.config.contemplatorMinTurns;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
389
532
|
this.restoredTipId = tipId;
|
|
390
533
|
for (const [probeId, question] of undeliveredSuggestions) {
|
|
391
534
|
// A durable custom_message proves only that Pi inserted the probe at some
|
|
@@ -402,68 +545,75 @@ export class Contemplator {
|
|
|
402
545
|
this.restore(ctx);
|
|
403
546
|
this.runtime.ensureConfig(ctx.cwd);
|
|
404
547
|
if (!this.runtime.config.contemplatorEnabled) {
|
|
548
|
+
this.publishState("disabled");
|
|
405
549
|
debugLog("contemplator.skipped", { reason: "disabled" });
|
|
406
550
|
return;
|
|
407
551
|
}
|
|
408
552
|
if (this.runtime.config.passive) {
|
|
553
|
+
this.publishState("passive");
|
|
409
554
|
debugLog("contemplator.skipped", { reason: "passive" });
|
|
410
555
|
return;
|
|
411
556
|
}
|
|
412
557
|
const branchEntries = ctx.sessionManager.getBranch() as Entry[];
|
|
413
558
|
const projection = fullProjection(branchEntries);
|
|
414
559
|
const observations = projection.observations.map((item) => `[${item.id}] ${item.content}`);
|
|
415
|
-
const
|
|
560
|
+
const summaries = projection.summaries.map((item) => `[${item.id}] ${item.content}`);
|
|
416
561
|
const reviews = projection.reviews ?? [];
|
|
417
562
|
const newObservationItems = projection.observations.filter((item) => !this.seenObservationIds.has(item.id));
|
|
418
|
-
const
|
|
563
|
+
const newSummaryItems = projection.summaries.filter((item) => !this.seenSummaryIds.has(item.id));
|
|
419
564
|
const newReviewItems = reviews.filter((item) => !this.seenReviewIds.has(item.id));
|
|
420
565
|
const newObservations = newObservationItems.map((item) => `[${item.id}] ${item.content}`);
|
|
421
|
-
const
|
|
566
|
+
const newSummaries = newSummaryItems.map((item) => `[${item.id}] ${item.content}`);
|
|
422
567
|
const newReviews = newReviewItems.map(reviewSummaryLine);
|
|
423
568
|
for (const item of newObservationItems) this.seenObservationIds.add(item.id);
|
|
424
|
-
for (const item of
|
|
569
|
+
for (const item of newSummaryItems) this.seenSummaryIds.add(item.id);
|
|
425
570
|
for (const item of newReviewItems) this.seenReviewIds.add(item.id);
|
|
426
571
|
debugLog("contemplator.update", {
|
|
427
572
|
observationCount: observations.length,
|
|
428
|
-
|
|
573
|
+
summaryCount: summaries.length,
|
|
429
574
|
newObservationCount: newObservations.length,
|
|
430
|
-
|
|
575
|
+
newSummaryCount: newSummaries.length,
|
|
431
576
|
newReviewCount: newReviews.length,
|
|
432
577
|
turnsSinceRun: this.turnsSinceRun,
|
|
433
578
|
pending: this.pending !== undefined,
|
|
434
579
|
running: this.running,
|
|
435
580
|
});
|
|
436
|
-
if (newObservations.length > 0 ||
|
|
581
|
+
if (newObservations.length > 0 || newSummaries.length > 0 || newReviews.length > 0) {
|
|
437
582
|
this.pending = {
|
|
438
583
|
observations: mergeMemoryLines(this.pending?.observations ?? [], newObservations),
|
|
439
|
-
|
|
584
|
+
summaries: mergeMemoryLines(this.pending?.summaries ?? [], newSummaries),
|
|
440
585
|
reviews: mergeMemoryLines(this.pending?.reviews ?? [], newReviews),
|
|
441
586
|
mainAgentOutputTokens: assistantOutputTokens(branchEntries),
|
|
442
587
|
mainAgentToolCalls: assistantToolCallCount(branchEntries),
|
|
443
588
|
mainAgentActiveTimeMs: agentActiveTimeMs(branchEntries),
|
|
444
589
|
};
|
|
445
590
|
}
|
|
446
|
-
if (!this.pending)
|
|
591
|
+
if (!this.pending) {
|
|
592
|
+
this.publishState(this.running ? "running" : "idle");
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
447
595
|
// Activity values are cumulative send-time snapshots, not values frozen when
|
|
448
596
|
// the first memory entered a pending batch. This includes work performed
|
|
449
597
|
// while that batch waits for its memory/turn thresholds.
|
|
450
598
|
this.pending.mainAgentOutputTokens = assistantOutputTokens(branchEntries);
|
|
451
599
|
this.pending.mainAgentToolCalls = assistantToolCallCount(branchEntries);
|
|
452
600
|
this.pending.mainAgentActiveTimeMs = agentActiveTimeMs(branchEntries);
|
|
453
|
-
const enoughMemories = this.pending.reviews.length > 0 || this.pending.observations.length >= this.runtime.config.contemplatorMinNewObservations || this.pending.
|
|
601
|
+
const enoughMemories = this.pending.reviews.length > 0 || this.pending.observations.length >= this.runtime.config.contemplatorMinNewObservations || this.pending.summaries.length >= this.runtime.config.contemplatorMinNewSummaries;
|
|
454
602
|
if (!enoughMemories || this.turnsSinceRun < this.runtime.config.contemplatorMinTurns) {
|
|
603
|
+
this.publishState(!enoughMemories ? "memories" : "responses");
|
|
455
604
|
debugLog("contemplator.waiting", {
|
|
456
605
|
enoughMemories,
|
|
457
606
|
turnsSinceRun: this.turnsSinceRun,
|
|
458
607
|
minTurns: this.runtime.config.contemplatorMinTurns,
|
|
459
608
|
minNewObservations: this.runtime.config.contemplatorMinNewObservations,
|
|
460
|
-
|
|
609
|
+
minNewSummaries: this.runtime.config.contemplatorMinNewSummaries,
|
|
461
610
|
});
|
|
462
611
|
return;
|
|
463
612
|
}
|
|
613
|
+
this.publishState(this.running ? "running" : "ready");
|
|
464
614
|
debugLog("contemplator.triggered", {
|
|
465
615
|
pendingObservationCount: this.pending.observations.length,
|
|
466
|
-
|
|
616
|
+
pendingSummaryCount: this.pending.summaries.length,
|
|
467
617
|
pendingReviewCount: this.pending.reviews.length,
|
|
468
618
|
turnsSinceRun: this.turnsSinceRun,
|
|
469
619
|
});
|
|
@@ -491,11 +641,13 @@ export class Contemplator {
|
|
|
491
641
|
this.turnsSinceRun = 0;
|
|
492
642
|
const startedAt = Date.now();
|
|
493
643
|
let failed = false;
|
|
644
|
+
let failureMessage: string | undefined;
|
|
645
|
+
let workerNotified = false;
|
|
494
646
|
let promptPersisted = false;
|
|
495
|
-
|
|
647
|
+
this.publishState("running", { lastStartedAt: startedAt, lastError: undefined });
|
|
496
648
|
debugLog("contemplator.start", {
|
|
497
649
|
newObservationCount: update.observations.length,
|
|
498
|
-
|
|
650
|
+
newSummaryCount: update.summaries.length,
|
|
499
651
|
newReviewCount: update.reviews.length,
|
|
500
652
|
historyMessageCount: this.history.length,
|
|
501
653
|
});
|
|
@@ -509,12 +661,13 @@ export class Contemplator {
|
|
|
509
661
|
});
|
|
510
662
|
if (!resolved.ok) {
|
|
511
663
|
failed = true;
|
|
664
|
+
failureMessage = resolved.reason;
|
|
512
665
|
debugLog("contemplator.model_unavailable", { reason: resolved.reason });
|
|
513
666
|
if (sessionGeneration === this.sessionGeneration) {
|
|
514
667
|
const pending = this.pending as PendingUpdate | undefined;
|
|
515
668
|
this.pending = {
|
|
516
669
|
observations: mergeMemoryLines(pending?.observations ?? [], update.observations),
|
|
517
|
-
|
|
670
|
+
summaries: mergeMemoryLines(pending?.summaries ?? [], update.summaries),
|
|
518
671
|
reviews: mergeMemoryLines(pending?.reviews ?? [], update.reviews),
|
|
519
672
|
mainAgentOutputTokens: update.mainAgentOutputTokens,
|
|
520
673
|
mainAgentToolCalls: update.mainAgentToolCalls,
|
|
@@ -534,80 +687,135 @@ export class Contemplator {
|
|
|
534
687
|
modelId: selectedModel.id,
|
|
535
688
|
contextWindow: selectedModel.contextWindow,
|
|
536
689
|
});
|
|
690
|
+
if (this.runtime.config.showWorkerNotifications && ctx.hasUI) {
|
|
691
|
+
ctx.ui?.notify("pi-contemplator: contemplator running", "info");
|
|
692
|
+
workerNotified = true;
|
|
693
|
+
}
|
|
537
694
|
const reviewerEnabled = this.runtime.config.reviewerEnabled;
|
|
538
695
|
const updateSections: string[] = [];
|
|
539
696
|
if (update.observations.length > 0) updateSections.push(`OBSERVATIONS:\n${update.observations.join("\n")}`);
|
|
540
|
-
if (update.
|
|
697
|
+
if (update.summaries.length > 0) updateSections.push(`SUMMARIES:\n${update.summaries.join("\n")}`);
|
|
541
698
|
if (update.reviews.length > 0) updateSections.push(`REVIEWS:\n${update.reviews.join("\n")}`);
|
|
542
699
|
const updateBody = updateSections.length > 0 ? updateSections.join("\n\n") : "(no new memories)";
|
|
700
|
+
const finalActionNames = reviewerEnabled
|
|
701
|
+
? "send_probe, request_review, or no_intervention"
|
|
702
|
+
: "send_probe or no_intervention";
|
|
543
703
|
const interventionInstruction = reviewerEnabled
|
|
544
|
-
?
|
|
545
|
-
:
|
|
704
|
+
? `You must end this update by calling exactly one final-action tool: ${finalActionNames}. The tool requirement is bookkeeping, not a reason to intervene. Prefer the argument-free no_intervention whenever no specific, grounded, materially useful intervention is clearly warranted or usefulness is uncertain. Use send_probe only for one unusually useful focused question, and request_review only when a deeper workflow or software review is justified. Never send a probe merely to satisfy the final-action requirement. If a tool warns about a bad memory citation, use search_memories and recall, then call a final-action tool again to replace it.`
|
|
705
|
+
: `You must end this update by calling exactly one final-action tool: ${finalActionNames}. The tool requirement is bookkeeping, not a reason to intervene. Prefer the argument-free no_intervention whenever no specific, grounded, materially useful probe is clearly warranted or usefulness is uncertain. Use send_probe only for one unusually useful focused question. Never send a probe merely to satisfy the final-action requirement. If send_probe warns about a bad memory citation, use search_memories and recall, then call a final-action tool again to replace it.`;
|
|
546
706
|
const prompt: Message = { role: "user", content: [{ type: "text", text: `NEW MEMORY UPDATE\n\n${updateBody}\n\nCUMULATIVE ACTIVITY: ${update.mainAgentOutputTokens} generated tokens; ${update.mainAgentToolCalls} tool calls; ${coarseAgentTime(update.mainAgentActiveTimeMs)} active.\n\nConsider these updates in the context of the accumulated memories. Prioritize reasoning gaps, contradictions, user-intent alignment, relevant overlooked alternatives, well-supported loops, and recurring structural patterns. ${interventionInstruction}` }], timestamp: Date.now() };
|
|
547
|
-
promptMessage = prompt;
|
|
548
|
-
this.history.push(prompt);
|
|
549
707
|
let intervention: Intervention | undefined;
|
|
708
|
+
let finalActionWarned = false;
|
|
550
709
|
const branchEntries = ctx.sessionManager.getBranch() as Entry[];
|
|
551
710
|
const getBranch = () => branchEntries;
|
|
552
711
|
const searchMemoriesTool = createSearchMemoriesAgentTool(getBranch);
|
|
553
712
|
const recallTool = createRecallAgentTool(getBranch);
|
|
713
|
+
const memoryExists = (id: string) => {
|
|
714
|
+
const exists = recallMemorySources(branchEntries, id).status === "found";
|
|
715
|
+
if (!exists) finalActionWarned = true;
|
|
716
|
+
return exists;
|
|
717
|
+
};
|
|
554
718
|
const sendProbe = createSendProbeTool((question) => {
|
|
555
|
-
|
|
719
|
+
const overwritten = intervention !== undefined;
|
|
720
|
+
finalActionWarned = false;
|
|
556
721
|
intervention = { kind: "probe", question };
|
|
557
|
-
return
|
|
722
|
+
return { overwritten };
|
|
723
|
+
}, memoryExists);
|
|
724
|
+
const noIntervention = createNoInterventionTool(() => {
|
|
725
|
+
const overwritten = intervention !== undefined;
|
|
726
|
+
finalActionWarned = false;
|
|
727
|
+
intervention = { kind: "none" };
|
|
728
|
+
return { overwritten };
|
|
558
729
|
});
|
|
559
|
-
const tools: AgentTool<any>[] = [searchMemoriesTool as AgentTool<any>, recallTool as AgentTool<any>, sendProbe as AgentTool<any>];
|
|
730
|
+
const tools: AgentTool<any>[] = [searchMemoriesTool as AgentTool<any>, recallTool as AgentTool<any>, sendProbe as AgentTool<any>, noIntervention as AgentTool<any>];
|
|
560
731
|
if (reviewerEnabled) {
|
|
561
732
|
const requestReview = createRequestReviewTool((request) => {
|
|
562
|
-
|
|
563
|
-
|
|
733
|
+
const overwritten = intervention !== undefined;
|
|
734
|
+
finalActionWarned = false;
|
|
735
|
+
const reviewRequestId = `review-${Date.now().toString(36)}-${Math.random().toString(16).slice(2, 8)}`;
|
|
564
736
|
intervention = { kind: "review", request: {
|
|
565
|
-
id,
|
|
737
|
+
id: reviewRequestId,
|
|
566
738
|
scope: request.scope,
|
|
567
739
|
evidence: request.evidence,
|
|
568
740
|
concern: request.concern,
|
|
569
741
|
reviewFocus: request.review_focus,
|
|
570
742
|
constraints: request.constraints,
|
|
571
743
|
} };
|
|
572
|
-
return
|
|
573
|
-
});
|
|
744
|
+
return { reviewRequestId, overwritten };
|
|
745
|
+
}, memoryExists);
|
|
574
746
|
tools.push(requestReview as AgentTool<any>);
|
|
575
747
|
}
|
|
576
|
-
const
|
|
577
|
-
const
|
|
748
|
+
const selectedThinkingLevel = this.runtime.config.contemplatorModel?.thinking ?? this.runtime.config.model?.thinking ?? "medium";
|
|
749
|
+
const supportsReasoning = (resolved.model as { reasoning?: unknown }).reasoning === true;
|
|
750
|
+
const config: AgentLoopConfig & { onPayload?: (payload: unknown) => unknown } = {
|
|
578
751
|
model: resolved.model as Model<any>,
|
|
579
752
|
apiKey: resolved.apiKey,
|
|
580
753
|
headers: resolved.headers,
|
|
581
754
|
maxTokens: boundedMaxTokens(resolved.model as Model<any>, AGENT_LOOP_MAX_TOKENS),
|
|
582
755
|
convertToLlm: (messages) => messages as Message[],
|
|
583
756
|
toolExecution: "sequential",
|
|
757
|
+
// A clean final-action call is the end of the contemplator turn. Do not
|
|
758
|
+
// spend another model request asking it to narrate after its decision.
|
|
759
|
+
// Citation warnings leave the loop open so it can correct the action.
|
|
760
|
+
shouldStopAfterTurn: () => intervention !== undefined && !finalActionWarned,
|
|
761
|
+
...(supportsReasoning && selectedThinkingLevel !== "off" ? { reasoning: selectedThinkingLevel } : {}),
|
|
584
762
|
};
|
|
585
|
-
const
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
763
|
+
const runMessages: AgentMessage[] = [];
|
|
764
|
+
let nextPrompt = prompt;
|
|
765
|
+
for (let invocation = 1; invocation <= CONTEMPLATOR_MAX_INVOCATIONS && !intervention; invocation++) {
|
|
766
|
+
runMessages.push(nextPrompt);
|
|
767
|
+
const context: AgentContext = { systemPrompt: buildContemplatorSystemPrompt(reviewerEnabled), messages: [...this.history, ...runMessages.slice(0, -1)], tools };
|
|
768
|
+
const api = (resolved.model as Model<any>).api;
|
|
769
|
+
const invocationConfig: AgentLoopConfig & { onPayload?: (payload: unknown) => unknown } = invocation === 1 ? config : {
|
|
770
|
+
...config,
|
|
771
|
+
onPayload: (payload) => forceRequiredToolPayload(payload, api),
|
|
772
|
+
};
|
|
773
|
+
// SimpleStreamOptions 0.84.3 types provider-neutral choice as auto/none,
|
|
774
|
+
// while individual provider APIs also support required/any. Preserve the
|
|
775
|
+
// runtime hint and final-payload enforcement without weakening base types.
|
|
776
|
+
if (invocation > 1) (invocationConfig as any).toolChoice = requiredToolChoice(api);
|
|
777
|
+
const stream = agentLoop([nextPrompt], context, invocationConfig, undefined, streamSimple);
|
|
778
|
+
for await (const event of stream) logAgentStreamError("contemplator", event);
|
|
779
|
+
const result = await stream.result();
|
|
780
|
+
// agentLoop returns its input prompt as the first new message. We already
|
|
781
|
+
// added nextPrompt above, so do not duplicate each update in the durable
|
|
782
|
+
// contemplator history or in a subsequent retry's context.
|
|
783
|
+
const returnedMessages = result[0] === nextPrompt ? result.slice(1) : result;
|
|
784
|
+
runMessages.push(...returnedMessages);
|
|
785
|
+
// The LLM call happened and was billed regardless of what we do next.
|
|
786
|
+
for (const message of result) {
|
|
787
|
+
if (message.role === "assistant" && message.usage) this.runtime.recordAgentUsage(message.usage);
|
|
788
|
+
}
|
|
789
|
+
const assistant = [...result].reverse().find((message) => message.role === "assistant");
|
|
790
|
+
debugLog("contemplator.result", {
|
|
791
|
+
invocation,
|
|
792
|
+
messageCount: result.length,
|
|
793
|
+
assistantFound: assistant !== undefined,
|
|
794
|
+
assistantStopReason: assistant && "stopReason" in assistant ? assistant.stopReason : undefined,
|
|
795
|
+
intervention: (intervention as Intervention | undefined)?.kind,
|
|
796
|
+
});
|
|
797
|
+
if (assistant && "stopReason" in assistant && (assistant.stopReason === "error" || assistant.stopReason === "aborted")) {
|
|
798
|
+
const errorMessage = "errorMessage" in assistant && typeof assistant.errorMessage === "string"
|
|
799
|
+
? assistant.errorMessage
|
|
800
|
+
: `Contemplator model ${assistant.stopReason}`;
|
|
801
|
+
throw new Error(errorMessage);
|
|
802
|
+
}
|
|
803
|
+
if (!intervention && invocation < CONTEMPLATOR_MAX_INVOCATIONS) {
|
|
804
|
+
nextPrompt = { role: "user", content: [{ type: "text", text: `You stopped without selecting a final action. If stopping meant that no intervention was clearly warranted, call the argument-free no_intervention tool now; that is the preferred default, and no explanation is required. Do not invent or send a probe merely to satisfy the tool requirement. Use send_probe only for a specific, memory-grounded question that is materially likely to improve the primary agent's reasoning${reviewerEnabled ? ", and request_review only for a well-supported recurring structural concern" : ""}. Call one final-action tool now: ${finalActionNames}. search_memories and recall do not satisfy this requirement.` }], timestamp: Date.now() };
|
|
593
805
|
}
|
|
594
806
|
}
|
|
595
|
-
|
|
596
|
-
debugLog("contemplator.result", {
|
|
597
|
-
messageCount: result.length,
|
|
598
|
-
assistantFound: assistant !== undefined,
|
|
599
|
-
assistantStopReason: assistant && "stopReason" in assistant ? assistant.stopReason : undefined,
|
|
600
|
-
intervention: intervention?.kind,
|
|
601
|
-
});
|
|
807
|
+
if (!intervention) throw new Error(`Contemplator stopped ${CONTEMPLATOR_MAX_INVOCATIONS} times without calling a final-action tool`);
|
|
602
808
|
if (sessionGeneration === this.sessionGeneration) {
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
this.
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
809
|
+
// Keep the durable contemplator history compact: prompts and assistant
|
|
810
|
+
// decisions are sufficient to resume its reasoning. Tool-result bodies
|
|
811
|
+
// are available within this run but are not copied into the ledger.
|
|
812
|
+
for (const message of runMessages) {
|
|
813
|
+
if (message.role !== "user" && message.role !== "assistant") continue;
|
|
814
|
+
this.history.push(message);
|
|
815
|
+
this.pi.appendEntry(CONTEMPLATOR_MESSAGE, { version: 1, message });
|
|
816
|
+
promptPersisted = true;
|
|
817
|
+
this.markTipPersisted(ctx);
|
|
818
|
+
}
|
|
611
819
|
}
|
|
612
820
|
if (intervention?.kind === "probe" && sessionGeneration === this.sessionGeneration) this.queueProbe(ctx, intervention.question, "send_probe");
|
|
613
821
|
if (intervention?.kind === "review" && this.runtime.config.reviewerEnabled && sessionGeneration === this.sessionGeneration) {
|
|
@@ -635,13 +843,13 @@ export class Contemplator {
|
|
|
635
843
|
if (sessionGeneration === this.sessionGeneration) await this.compactHistory(resolved.model as Model<any>, resolved.apiKey, resolved.headers, sessionGeneration);
|
|
636
844
|
} catch (error) {
|
|
637
845
|
failed = true;
|
|
638
|
-
|
|
846
|
+
failureMessage = error instanceof Error ? error.message : String(error);
|
|
847
|
+
debugLog("contemplator.error", { errorMessage: failureMessage });
|
|
639
848
|
if (sessionGeneration === this.sessionGeneration && !promptPersisted) {
|
|
640
|
-
if (promptMessage && this.history.at(-1) === promptMessage) this.history.pop();
|
|
641
849
|
const pending = this.pending as PendingUpdate | undefined;
|
|
642
850
|
this.pending = {
|
|
643
851
|
observations: mergeMemoryLines(pending?.observations ?? [], update.observations),
|
|
644
|
-
|
|
852
|
+
summaries: mergeMemoryLines(pending?.summaries ?? [], update.summaries),
|
|
645
853
|
reviews: mergeMemoryLines(pending?.reviews ?? [], update.reviews),
|
|
646
854
|
mainAgentOutputTokens: update.mainAgentOutputTokens,
|
|
647
855
|
mainAgentToolCalls: update.mainAgentToolCalls,
|
|
@@ -651,11 +859,33 @@ export class Contemplator {
|
|
|
651
859
|
}
|
|
652
860
|
} finally {
|
|
653
861
|
this.running = false;
|
|
862
|
+
const pendingHasEnoughMemories = this.pending !== undefined && (
|
|
863
|
+
this.pending.reviews.length > 0 ||
|
|
864
|
+
this.pending.observations.length >= this.runtime.config.contemplatorMinNewObservations ||
|
|
865
|
+
this.pending.summaries.length >= this.runtime.config.contemplatorMinNewSummaries
|
|
866
|
+
);
|
|
867
|
+
const waitingFor = !this.pending
|
|
868
|
+
? "idle"
|
|
869
|
+
: !pendingHasEnoughMemories
|
|
870
|
+
? "memories"
|
|
871
|
+
: this.turnsSinceRun < this.runtime.config.contemplatorMinTurns
|
|
872
|
+
? "responses"
|
|
873
|
+
: "ready";
|
|
874
|
+
this.publishState(waitingFor, {
|
|
875
|
+
lastCompletedAt: Date.now(),
|
|
876
|
+
lastError: failureMessage,
|
|
877
|
+
});
|
|
654
878
|
debugLog("contemplator.complete", {
|
|
655
879
|
durationMs: Date.now() - startedAt,
|
|
656
880
|
historyMessageCount: this.history.length,
|
|
657
881
|
pendingUpdate: this.pending !== undefined,
|
|
658
882
|
});
|
|
883
|
+
if (workerNotified && sessionGeneration === this.sessionGeneration) {
|
|
884
|
+
ctx.ui?.notify(
|
|
885
|
+
failed ? `pi-contemplator: contemplator failed — ${failureMessage ?? "unknown error"}` : "pi-contemplator: contemplator completed",
|
|
886
|
+
failed ? "warning" : "info",
|
|
887
|
+
);
|
|
888
|
+
}
|
|
659
889
|
if (!failed && sessionGeneration === this.sessionGeneration && this.pending) this.observeTurn(ctx);
|
|
660
890
|
}
|
|
661
891
|
}
|
|
@@ -674,10 +904,14 @@ export class Contemplator {
|
|
|
674
904
|
// triggerTurn:false as "do not queue while streaming" and inserts directly
|
|
675
905
|
// into agent.state, outside the active run's context snapshot. Omitting it
|
|
676
906
|
// still does not start a turn while idle, but allows steer to work in-run.
|
|
677
|
-
|
|
907
|
+
// Whether Pi is currently running or idle, sendMessage owns this probe in an
|
|
908
|
+
// in-memory steer queue until message_end drains it. Track both cases so an
|
|
909
|
+
// unrelated observer update or compaction callback cannot restore and enqueue
|
|
910
|
+
// a duplicate while the original idle steer is still pending.
|
|
911
|
+
this.queuedProbeIds.add(probeId);
|
|
678
912
|
this.pi.sendMessage({
|
|
679
913
|
customType: CONTEMPLATOR_SUGGESTION,
|
|
680
|
-
content: `Background contemplator probe (advisory):\n${question}
|
|
914
|
+
content: `Background contemplator probe (advisory):\n${question}\n\nReferenced memories can be reviewed using the recall tool.`,
|
|
681
915
|
display: this.runtime.config.showContemplatorMessages,
|
|
682
916
|
details: { version: 1, question, source, probeId },
|
|
683
917
|
}, { deliverAs: "steer" });
|