@matthewfl/pi-contemplator 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +120 -0
- package/package.json +60 -0
- package/src/agents/contemplator/agent.ts +718 -0
- package/src/agents/contemplator/prompts.ts +212 -0
- package/src/agents/dropper/agent.ts +291 -0
- package/src/agents/dropper/coverage.ts +128 -0
- package/src/agents/dropper/pool.ts +67 -0
- package/src/agents/dropper/prompts.ts +48 -0
- package/src/agents/observer/agent.ts +207 -0
- package/src/agents/observer/prompts.ts +119 -0
- package/src/agents/reflector/agent.ts +213 -0
- package/src/agents/reflector/prompts.ts +81 -0
- package/src/agents/reviewer/agent.ts +187 -0
- package/src/agents/reviewer/history-tools.ts +337 -0
- package/src/agents/reviewer/prompts.ts +135 -0
- package/src/agents/reviewer/tools.ts +84 -0
- package/src/agents/stream-errors.ts +22 -0
- package/src/clipboard.ts +63 -0
- package/src/commands/contemplator-view.ts +128 -0
- package/src/commands/reviewer-view.ts +89 -0
- package/src/commands/settings.ts +257 -0
- package/src/commands/status.ts +176 -0
- package/src/commands/view.ts +171 -0
- package/src/config.ts +284 -0
- package/src/debug-log.ts +72 -0
- package/src/hooks/compaction-hook.ts +99 -0
- package/src/hooks/compaction-resume.ts +124 -0
- package/src/hooks/compaction-trigger.ts +122 -0
- package/src/hooks/consolidation-trigger.ts +488 -0
- package/src/ids.ts +5 -0
- package/src/index.ts +32 -0
- package/src/model-budget.ts +16 -0
- package/src/runtime.ts +316 -0
- package/src/serialize.ts +274 -0
- package/src/session-ledger/fold.ts +115 -0
- package/src/session-ledger/index.ts +7 -0
- package/src/session-ledger/progress.ts +156 -0
- package/src/session-ledger/projection.ts +243 -0
- package/src/session-ledger/recall.ts +258 -0
- package/src/session-ledger/render-summary.ts +31 -0
- package/src/session-ledger/search.ts +184 -0
- package/src/session-ledger/types.ts +329 -0
- package/src/tokens.ts +27 -0
- package/src/tools/compact-context.ts +54 -0
- package/src/tools/recall-observation.ts +532 -0
- package/src/tools/search-memories.ts +131 -0
|
@@ -0,0 +1,718 @@
|
|
|
1
|
+
import { agentLoop, type AgentContext, type AgentLoopConfig, type AgentMessage, type AgentTool } from "@earendil-works/pi-agent-core";
|
|
2
|
+
import { Type, type Message, type Model } from "@earendil-works/pi-ai";
|
|
3
|
+
import type { Static } from "typebox";
|
|
4
|
+
import { streamSimple } from "@earendil-works/pi-ai/compat";
|
|
5
|
+
import { generateSummaryWithUsage } from "@earendil-works/pi-coding-agent";
|
|
6
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
7
|
+
import { assistantOutputTokens, assistantToolCallCount, fullProjection, isReviewRequestEntry, isReviewResultEntry, 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 { hashId } from "../../ids.js";
|
|
9
|
+
import { createSearchMemoriesAgentTool } from "../../tools/search-memories.js";
|
|
10
|
+
import { createRecallAgentTool } from "../../tools/recall-observation.js";
|
|
11
|
+
import type { MemoryUpdateCtx, Runtime } from "../../runtime.js";
|
|
12
|
+
import { logAgentStreamError } from "../stream-errors.js";
|
|
13
|
+
import { debugLog, withDebugLogContext } from "../../debug-log.js";
|
|
14
|
+
import { boundedMaxTokens, AGENT_LOOP_MAX_TOKENS } from "../../model-budget.js";
|
|
15
|
+
import { buildContemplatorSystemPrompt } from "./prompts.js";
|
|
16
|
+
import { runStructuralReview } from "../reviewer/agent.js";
|
|
17
|
+
|
|
18
|
+
interface PendingUpdate {
|
|
19
|
+
observations: string[];
|
|
20
|
+
reflections: string[];
|
|
21
|
+
reviews: string[];
|
|
22
|
+
mainAgentOutputTokens: number;
|
|
23
|
+
mainAgentToolCalls: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
type Intervention =
|
|
27
|
+
| { kind: "probe"; question: string }
|
|
28
|
+
| { kind: "review"; request: Omit<StructuralReviewRequest, "createdAt" | "requestedBy"> };
|
|
29
|
+
|
|
30
|
+
type ReviewerSession = { scope: StructuralReviewRequest["scope"]; history: AgentMessage[] };
|
|
31
|
+
|
|
32
|
+
type QueueStructuralReviewOptions = {
|
|
33
|
+
ctx: MemoryUpdateCtx;
|
|
34
|
+
requestArgs: Extract<Intervention, { kind: "review" }>["request"];
|
|
35
|
+
branchEntries: Entry[];
|
|
36
|
+
model: Model<any>;
|
|
37
|
+
apiKey: string;
|
|
38
|
+
headers: Record<string, string> | undefined;
|
|
39
|
+
sessionGeneration: number;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
type LaunchStructuralReviewOptions = {
|
|
43
|
+
ctx: MemoryUpdateCtx;
|
|
44
|
+
request: StructuralReviewRequest;
|
|
45
|
+
model: Model<any>;
|
|
46
|
+
apiKey: string;
|
|
47
|
+
headers: Record<string, string> | undefined;
|
|
48
|
+
sessionGeneration: number;
|
|
49
|
+
key?: string;
|
|
50
|
+
history?: AgentMessage[];
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
function mergeMemoryLines(existing: string[], incoming: string[]): string[] {
|
|
54
|
+
const merged = [...existing];
|
|
55
|
+
const seen = new Set(existing.map((line) => line.match(/^\[([^\]]+)\]/)?.[1] ?? line));
|
|
56
|
+
for (const line of incoming) {
|
|
57
|
+
const key = line.match(/^\[([^\]]+)\]/)?.[1] ?? line;
|
|
58
|
+
if (seen.has(key)) continue;
|
|
59
|
+
seen.add(key);
|
|
60
|
+
merged.push(line);
|
|
61
|
+
}
|
|
62
|
+
return merged;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function reviewSummaryLine(review: ReviewResult): string {
|
|
66
|
+
return review.outcome === "proposal"
|
|
67
|
+
? `[${review.id}] ${review.scope} proposal: ${review.title} — ${review.summary}`
|
|
68
|
+
: `[${review.id}] ${review.scope} review concluded with no proposal — ${review.reason}`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function reviewRequestKey(request: RequestReviewArgs): string {
|
|
72
|
+
return `${request.scope}:${hashId(`${request.evidence}\n${request.concern}`)}`;
|
|
73
|
+
}
|
|
74
|
+
const CONTEMPLATOR_MESSAGE = "om.contemplator.message";
|
|
75
|
+
const CONTEMPLATOR_STATE = "om.contemplator.state";
|
|
76
|
+
const CONTEMPLATOR_SUGGESTION = "om.contemplator.suggestion";
|
|
77
|
+
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." }) });
|
|
78
|
+
const ReviewScopeSchema = Type.Union([Type.Literal("workflow"), Type.Literal("software")]);
|
|
79
|
+
export const RequestReviewSchema = Type.Object({
|
|
80
|
+
scope: ReviewScopeSchema,
|
|
81
|
+
evidence: Type.String({ minLength: 1, description: "Memory-grounded evidence for the suspected recurring pattern." }),
|
|
82
|
+
concern: Type.String({ minLength: 1, description: "Suspected structural concern stated as a possibility." }),
|
|
83
|
+
review_focus: Type.String({ minLength: 1, description: "What the reviewer should determine without prescribing a solution." }),
|
|
84
|
+
constraints: Type.Optional(Type.String({ minLength: 1, description: "Relevant user requirements, boundaries, or uncertainties." })),
|
|
85
|
+
});
|
|
86
|
+
type SendProbeArgs = Static<typeof SendProbeSchema>;
|
|
87
|
+
export type RequestReviewArgs = Static<typeof RequestReviewSchema>;
|
|
88
|
+
|
|
89
|
+
export function createSendProbeTool(onProbe: (question: string) => boolean): AgentTool<typeof SendProbeSchema> {
|
|
90
|
+
return {
|
|
91
|
+
name: "send_probe",
|
|
92
|
+
label: "Send probe",
|
|
93
|
+
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.",
|
|
94
|
+
parameters: SendProbeSchema,
|
|
95
|
+
execute: async (_toolCallId, params: SendProbeArgs) => {
|
|
96
|
+
const question = params.question.trim();
|
|
97
|
+
if (!onProbe(question)) {
|
|
98
|
+
return { content: [{ type: "text", text: "No probe was queued because this update already has an intervention. Continue without calling another intervention tool." }], details: { queued: false } };
|
|
99
|
+
}
|
|
100
|
+
debugLog("contemplator.tool_call", { tool: "send_probe", suggestionLength: question.length });
|
|
101
|
+
return { content: [{ type: "text", text: "Probe queued for the primary agent's next context." }], details: { queued: true } };
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function createRequestReviewTool(onReview: (request: RequestReviewArgs) => string | undefined): AgentTool<typeof RequestReviewSchema> {
|
|
107
|
+
return {
|
|
108
|
+
name: "request_review",
|
|
109
|
+
label: "Request structural review",
|
|
110
|
+
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.",
|
|
111
|
+
parameters: RequestReviewSchema,
|
|
112
|
+
execute: async (_toolCallId, params: RequestReviewArgs) => {
|
|
113
|
+
const request = { ...params, evidence: params.evidence.trim(), concern: params.concern.trim(), review_focus: params.review_focus.trim(), constraints: params.constraints?.trim() || undefined };
|
|
114
|
+
const reviewRequestId = onReview(request);
|
|
115
|
+
if (!reviewRequestId) {
|
|
116
|
+
return { content: [{ type: "text", text: "No review was queued because this update already has an intervention. Continue without calling another intervention tool." }], details: { queued: false, scope: request.scope } };
|
|
117
|
+
}
|
|
118
|
+
debugLog("contemplator.review_requested", { reviewRequestId, scope: request.scope, evidenceLength: request.evidence.length, concernLength: request.concern.length });
|
|
119
|
+
return { content: [{ type: "text", text: `${request.scope === "workflow" ? "Workflow" : "Software"} review queued as [${reviewRequestId}].` }], details: { queued: true, scope: request.scope, reviewRequestId } };
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export class Contemplator {
|
|
125
|
+
private history: AgentMessage[] = [];
|
|
126
|
+
private pending: PendingUpdate | undefined;
|
|
127
|
+
private running = false;
|
|
128
|
+
private seenObservationIds = new Set<string>();
|
|
129
|
+
private seenReflectionIds = new Set<string>();
|
|
130
|
+
private seenReviewIds = new Set<string>();
|
|
131
|
+
private inFlightReviewKeys = new Set<string>();
|
|
132
|
+
private inFlightReviewIds = new Set<string>();
|
|
133
|
+
private resolvingReviewIds = new Set<string>();
|
|
134
|
+
private resumedReviewIds = new Set<string>();
|
|
135
|
+
private reviewerSessions = new Map<string, ReviewerSession>();
|
|
136
|
+
private deliveredProbeIds = new Set<string>();
|
|
137
|
+
private requeuedProbeIds = new Set<string>();
|
|
138
|
+
private sessionGeneration = 0;
|
|
139
|
+
private latestCtx: MemoryUpdateCtx | undefined;
|
|
140
|
+
private turnsSinceRun = 0;
|
|
141
|
+
private restoredTipId: string | undefined;
|
|
142
|
+
|
|
143
|
+
constructor(private readonly pi: ExtensionAPI, private readonly runtime: Runtime) {}
|
|
144
|
+
|
|
145
|
+
register(): void {
|
|
146
|
+
this.runtime.setMemoryUpdateListener((ctx) => this.withDebugContext(ctx, () => this.observeTurn(ctx)));
|
|
147
|
+
const restoreSessionBranch = (_event: any, ctx: ExtensionContext) => {
|
|
148
|
+
this.sessionGeneration++;
|
|
149
|
+
this.restore(ctx, true);
|
|
150
|
+
};
|
|
151
|
+
this.pi.on("session_start", restoreSessionBranch);
|
|
152
|
+
this.pi.on("session_tree", restoreSessionBranch);
|
|
153
|
+
this.pi.on("session_shutdown", () => {
|
|
154
|
+
this.sessionGeneration++;
|
|
155
|
+
this.history = [];
|
|
156
|
+
this.pending = undefined;
|
|
157
|
+
this.seenObservationIds.clear();
|
|
158
|
+
this.seenReflectionIds.clear();
|
|
159
|
+
this.seenReviewIds.clear();
|
|
160
|
+
this.inFlightReviewKeys.clear();
|
|
161
|
+
this.inFlightReviewIds.clear();
|
|
162
|
+
this.resolvingReviewIds.clear();
|
|
163
|
+
this.resumedReviewIds.clear();
|
|
164
|
+
this.reviewerSessions.clear();
|
|
165
|
+
this.deliveredProbeIds.clear();
|
|
166
|
+
this.requeuedProbeIds.clear();
|
|
167
|
+
this.latestCtx = undefined;
|
|
168
|
+
this.turnsSinceRun = 0;
|
|
169
|
+
this.restoredTipId = undefined;
|
|
170
|
+
});
|
|
171
|
+
this.pi.on("session_compact", (_event: any, ctx: ExtensionContext) => {
|
|
172
|
+
// The in-flight prompt is persisted by flush after its agent loop. Do not
|
|
173
|
+
// snapshot it here or compaction would make restore replay it twice.
|
|
174
|
+
const history = this.running ? this.history.slice(0, -1) : this.history;
|
|
175
|
+
if (history.length > 0) {
|
|
176
|
+
this.pi.appendEntry(CONTEMPLATOR_STATE, { version: 1, history });
|
|
177
|
+
this.markTipPersisted(ctx);
|
|
178
|
+
debugLog("contemplator.state_persisted", { historyMessageCount: history.length, running: this.running });
|
|
179
|
+
}
|
|
180
|
+
this.persistReviewerStates(ctx);
|
|
181
|
+
});
|
|
182
|
+
this.pi.on("context", (event: any, ctx: ExtensionContext) => {
|
|
183
|
+
const deliveredMessages = event.messages?.filter((message: any) => message?.role === "custom" && message.customType === CONTEMPLATOR_SUGGESTION && typeof message.details?.probeId === "string") ?? [];
|
|
184
|
+
for (const delivered of deliveredMessages) {
|
|
185
|
+
if (this.deliveredProbeIds.has(delivered.details.probeId)) continue;
|
|
186
|
+
this.deliveredProbeIds.add(delivered.details.probeId);
|
|
187
|
+
this.pi.appendEntry(CONTEMPLATOR_SUGGESTION, {
|
|
188
|
+
version: 1,
|
|
189
|
+
suggestion: typeof delivered.details.question === "string" ? delivered.details.question : String(delivered.content ?? ""),
|
|
190
|
+
probeId: delivered.details.probeId,
|
|
191
|
+
delivered: true,
|
|
192
|
+
});
|
|
193
|
+
this.markTipPersisted(ctx);
|
|
194
|
+
debugLog("contemplator.suggestion_delivered", { probeId: delivered.details.probeId });
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
this.pi.on("turn_end", (_event: any, ctx: ExtensionContext) => {
|
|
198
|
+
this.turnsSinceRun++;
|
|
199
|
+
this.withDebugContext(ctx, () => this.observeTurn(ctx));
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
private withDebugContext<T>(ctx: MemoryUpdateCtx, fn: () => T): T {
|
|
204
|
+
this.runtime.ensureConfig(ctx.cwd);
|
|
205
|
+
const sessionManager = ctx.sessionManager as { getSessionId?: () => string; getSessionFile?: () => string };
|
|
206
|
+
return withDebugLogContext({
|
|
207
|
+
enabled: this.runtime.config.debugLog === true,
|
|
208
|
+
cwd: ctx.cwd,
|
|
209
|
+
sessionId: sessionManager.getSessionId?.(),
|
|
210
|
+
sessionFile: sessionManager.getSessionFile?.(),
|
|
211
|
+
}, fn);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
private restore(ctx: MemoryUpdateCtx, resetTracking = false): void {
|
|
215
|
+
this.latestCtx = ctx;
|
|
216
|
+
const entries = ctx.sessionManager.getBranch() as Entry[];
|
|
217
|
+
const tipId = entries.at(-1)?.id;
|
|
218
|
+
if (this.running && !resetTracking) return;
|
|
219
|
+
if (tipId === this.restoredTipId && !resetTracking) return;
|
|
220
|
+
this.history = [];
|
|
221
|
+
if (resetTracking) {
|
|
222
|
+
this.deliveredProbeIds.clear();
|
|
223
|
+
this.requeuedProbeIds.clear();
|
|
224
|
+
this.inFlightReviewIds.clear();
|
|
225
|
+
this.resolvingReviewIds.clear();
|
|
226
|
+
this.resumedReviewIds.clear();
|
|
227
|
+
this.reviewerSessions.clear();
|
|
228
|
+
const projection = fullProjection(entries);
|
|
229
|
+
this.seenObservationIds = new Set(projection.observations.map((item) => item.id));
|
|
230
|
+
this.seenReflectionIds = new Set(projection.reflections.map((item) => item.id));
|
|
231
|
+
this.seenReviewIds = new Set((projection.reviews ?? []).map((item) => item.id));
|
|
232
|
+
this.pending = undefined;
|
|
233
|
+
this.turnsSinceRun = 0;
|
|
234
|
+
}
|
|
235
|
+
const undeliveredSuggestions = new Map<string, string>();
|
|
236
|
+
const queuedProbeIds = new Set<string>();
|
|
237
|
+
for (const entry of entries) {
|
|
238
|
+
if (entry.customType === CONTEMPLATOR_SUGGESTION && entry.type === "custom_message") {
|
|
239
|
+
const details = entry.details as { probeId?: unknown } | undefined;
|
|
240
|
+
if (typeof details?.probeId === "string") queuedProbeIds.add(details.probeId);
|
|
241
|
+
}
|
|
242
|
+
if (entry.customType === CONTEMPLATOR_STATE && entry.data && typeof entry.data === "object") {
|
|
243
|
+
const state = entry.data as { history?: unknown };
|
|
244
|
+
if (Array.isArray(state.history)) this.history = state.history.filter((message): message is AgentMessage => !!message && typeof message === "object");
|
|
245
|
+
}
|
|
246
|
+
if (entry.customType === CONTEMPLATOR_MESSAGE && entry.data && typeof entry.data === "object") {
|
|
247
|
+
const data = entry.data as { message?: unknown; compacted?: unknown };
|
|
248
|
+
const message = data.message;
|
|
249
|
+
if (message && typeof message === "object") {
|
|
250
|
+
if (data.compacted === true) this.history = [message as AgentMessage];
|
|
251
|
+
else this.history.push(message as AgentMessage);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
if (entry.customType === OM_REVIEWER_STATE && entry.data && typeof entry.data === "object") {
|
|
255
|
+
const state = entry.data as { reviewRequestId?: unknown; scope?: unknown; history?: unknown };
|
|
256
|
+
if (typeof state.reviewRequestId === "string" && (state.scope === "workflow" || state.scope === "software") && Array.isArray(state.history)) {
|
|
257
|
+
this.reviewerSessions.set(state.reviewRequestId, { scope: state.scope, history: state.history.filter((message): message is AgentMessage => !!message && typeof message === "object") });
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
if (entry.customType === OM_REVIEWER_MESSAGE && entry.data && typeof entry.data === "object") {
|
|
261
|
+
const data = entry.data as { reviewRequestId?: unknown; scope?: unknown; message?: unknown };
|
|
262
|
+
if (typeof data.reviewRequestId === "string" && (data.scope === "workflow" || data.scope === "software") && data.message && typeof data.message === "object") {
|
|
263
|
+
const session = this.reviewerSessions.get(data.reviewRequestId) ?? { scope: data.scope, history: [] };
|
|
264
|
+
session.history.push(data.message as AgentMessage);
|
|
265
|
+
this.reviewerSessions.set(data.reviewRequestId, session);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
if (entry.customType === CONTEMPLATOR_SUGGESTION && entry.data && typeof entry.data === "object") {
|
|
269
|
+
const data = entry.data as { suggestion?: unknown; delivered?: unknown; probeId?: unknown };
|
|
270
|
+
if (typeof data.probeId !== "string") continue;
|
|
271
|
+
if (data.delivered === true) {
|
|
272
|
+
this.deliveredProbeIds.add(data.probeId);
|
|
273
|
+
undeliveredSuggestions.delete(data.probeId);
|
|
274
|
+
} else if (typeof data.suggestion === "string") {
|
|
275
|
+
undeliveredSuggestions.set(data.probeId, data.suggestion);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
this.restoredTipId = tipId;
|
|
280
|
+
for (const [probeId, question] of undeliveredSuggestions) {
|
|
281
|
+
if (queuedProbeIds.has(probeId) || this.requeuedProbeIds.has(probeId)) continue;
|
|
282
|
+
this.requeuedProbeIds.add(probeId);
|
|
283
|
+
this.queueProbe(ctx, question, "restore", probeId);
|
|
284
|
+
}
|
|
285
|
+
if (resetTracking) void this.resumePendingReviews(ctx);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
private observeTurn(ctx: MemoryUpdateCtx): void {
|
|
289
|
+
this.restore(ctx);
|
|
290
|
+
this.runtime.ensureConfig(ctx.cwd);
|
|
291
|
+
if (!this.runtime.config.contemplatorEnabled) {
|
|
292
|
+
debugLog("contemplator.skipped", { reason: "disabled" });
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
if (this.runtime.config.passive) {
|
|
296
|
+
debugLog("contemplator.skipped", { reason: "passive" });
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
const projection = fullProjection(ctx.sessionManager.getBranch() as Entry[]);
|
|
300
|
+
const observations = projection.observations.map((item) => `[${item.id}] ${item.content}`);
|
|
301
|
+
const reflections = projection.reflections.map((item) => `[${item.id}] ${item.content}`);
|
|
302
|
+
const reviews = projection.reviews ?? [];
|
|
303
|
+
const newObservationItems = projection.observations.filter((item) => !this.seenObservationIds.has(item.id));
|
|
304
|
+
const newReflectionItems = projection.reflections.filter((item) => !this.seenReflectionIds.has(item.id));
|
|
305
|
+
const newReviewItems = reviews.filter((item) => !this.seenReviewIds.has(item.id));
|
|
306
|
+
const newObservations = newObservationItems.map((item) => `[${item.id}] ${item.content}`);
|
|
307
|
+
const newReflections = newReflectionItems.map((item) => `[${item.id}] ${item.content}`);
|
|
308
|
+
const newReviews = newReviewItems.map(reviewSummaryLine);
|
|
309
|
+
for (const item of newObservationItems) this.seenObservationIds.add(item.id);
|
|
310
|
+
for (const item of newReflectionItems) this.seenReflectionIds.add(item.id);
|
|
311
|
+
for (const item of newReviewItems) this.seenReviewIds.add(item.id);
|
|
312
|
+
debugLog("contemplator.update", {
|
|
313
|
+
observationCount: observations.length,
|
|
314
|
+
reflectionCount: reflections.length,
|
|
315
|
+
newObservationCount: newObservations.length,
|
|
316
|
+
newReflectionCount: newReflections.length,
|
|
317
|
+
newReviewCount: newReviews.length,
|
|
318
|
+
turnsSinceRun: this.turnsSinceRun,
|
|
319
|
+
pending: this.pending !== undefined,
|
|
320
|
+
running: this.running,
|
|
321
|
+
});
|
|
322
|
+
if (newObservations.length > 0 || newReflections.length > 0 || newReviews.length > 0) {
|
|
323
|
+
this.pending = {
|
|
324
|
+
observations: mergeMemoryLines(this.pending?.observations ?? [], newObservations),
|
|
325
|
+
reflections: mergeMemoryLines(this.pending?.reflections ?? [], newReflections),
|
|
326
|
+
reviews: mergeMemoryLines(this.pending?.reviews ?? [], newReviews),
|
|
327
|
+
mainAgentOutputTokens: assistantOutputTokens(ctx.sessionManager.getBranch() as Entry[]),
|
|
328
|
+
mainAgentToolCalls: assistantToolCallCount(ctx.sessionManager.getBranch() as Entry[]),
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
if (!this.pending) return;
|
|
332
|
+
const enoughMemories = this.pending.reviews.length > 0 || this.pending.observations.length >= this.runtime.config.contemplatorMinNewObservations || this.pending.reflections.length >= this.runtime.config.contemplatorMinNewReflections;
|
|
333
|
+
if (!enoughMemories || this.turnsSinceRun < this.runtime.config.contemplatorMinTurns) {
|
|
334
|
+
debugLog("contemplator.waiting", {
|
|
335
|
+
enoughMemories,
|
|
336
|
+
turnsSinceRun: this.turnsSinceRun,
|
|
337
|
+
minTurns: this.runtime.config.contemplatorMinTurns,
|
|
338
|
+
minNewObservations: this.runtime.config.contemplatorMinNewObservations,
|
|
339
|
+
minNewReflections: this.runtime.config.contemplatorMinNewReflections,
|
|
340
|
+
});
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
debugLog("contemplator.triggered", {
|
|
344
|
+
pendingObservationCount: this.pending.observations.length,
|
|
345
|
+
pendingReflectionCount: this.pending.reflections.length,
|
|
346
|
+
pendingReviewCount: this.pending.reviews.length,
|
|
347
|
+
turnsSinceRun: this.turnsSinceRun,
|
|
348
|
+
});
|
|
349
|
+
void this.flush(ctx);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
private async flush(ctx: MemoryUpdateCtx): Promise<void> {
|
|
353
|
+
if (this.running || !this.pending) {
|
|
354
|
+
debugLog("contemplator.flush_skipped", { reason: this.running ? "already_running" : "no_pending_update" });
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
const update = this.pending;
|
|
358
|
+
this.pending = undefined;
|
|
359
|
+
const turnsBeforeRun = this.turnsSinceRun;
|
|
360
|
+
const sessionGeneration = this.sessionGeneration;
|
|
361
|
+
this.running = true;
|
|
362
|
+
this.turnsSinceRun = 0;
|
|
363
|
+
const startedAt = Date.now();
|
|
364
|
+
let failed = false;
|
|
365
|
+
let promptPersisted = false;
|
|
366
|
+
let promptMessage: Message | undefined;
|
|
367
|
+
debugLog("contemplator.start", {
|
|
368
|
+
newObservationCount: update.observations.length,
|
|
369
|
+
newReflectionCount: update.reflections.length,
|
|
370
|
+
newReviewCount: update.reviews.length,
|
|
371
|
+
historyMessageCount: this.history.length,
|
|
372
|
+
});
|
|
373
|
+
try {
|
|
374
|
+
const resolved = await this.runtime.resolveModel({
|
|
375
|
+
model: ctx.model,
|
|
376
|
+
modelRegistry: ctx.modelRegistry,
|
|
377
|
+
hasUI: ctx.hasUI,
|
|
378
|
+
ui: ctx.ui,
|
|
379
|
+
configuredModel: this.runtime.config.contemplatorModel ?? null,
|
|
380
|
+
});
|
|
381
|
+
if (!resolved.ok) {
|
|
382
|
+
failed = true;
|
|
383
|
+
debugLog("contemplator.model_unavailable", { reason: resolved.reason });
|
|
384
|
+
if (sessionGeneration === this.sessionGeneration) {
|
|
385
|
+
const pending = this.pending as PendingUpdate | undefined;
|
|
386
|
+
this.pending = {
|
|
387
|
+
observations: mergeMemoryLines(pending?.observations ?? [], update.observations),
|
|
388
|
+
reflections: mergeMemoryLines(pending?.reflections ?? [], update.reflections),
|
|
389
|
+
reviews: mergeMemoryLines(pending?.reviews ?? [], update.reviews),
|
|
390
|
+
mainAgentOutputTokens: update.mainAgentOutputTokens,
|
|
391
|
+
mainAgentToolCalls: update.mainAgentToolCalls,
|
|
392
|
+
};
|
|
393
|
+
this.turnsSinceRun = turnsBeforeRun;
|
|
394
|
+
}
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
if (sessionGeneration !== this.sessionGeneration) {
|
|
398
|
+
debugLog("contemplator.flush_stale", { reason: "session_changed" });
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
const selectedModel = resolved.model as { provider?: unknown; id?: unknown; contextWindow?: unknown };
|
|
402
|
+
debugLog("contemplator.model_resolved", {
|
|
403
|
+
provider: selectedModel.provider,
|
|
404
|
+
modelId: selectedModel.id,
|
|
405
|
+
contextWindow: selectedModel.contextWindow,
|
|
406
|
+
});
|
|
407
|
+
const reviewerEnabled = this.runtime.config.reviewerEnabled;
|
|
408
|
+
const updateSections: string[] = [];
|
|
409
|
+
if (update.observations.length > 0) updateSections.push(`OBSERVATIONS:\n${update.observations.join("\n")}`);
|
|
410
|
+
if (update.reflections.length > 0) updateSections.push(`REFLECTIONS:\n${update.reflections.join("\n")}`);
|
|
411
|
+
if (update.reviews.length > 0) updateSections.push(`REVIEWS:\n${update.reviews.join("\n")}`);
|
|
412
|
+
const updateBody = updateSections.length > 0 ? updateSections.join("\n\n") : "(no new memories)";
|
|
413
|
+
const interventionInstruction = reviewerEnabled
|
|
414
|
+
? "Use send_probe for one focused question, or request_review only when a deeper workflow or software review is justified. Use no more than one intervention."
|
|
415
|
+
: "Use send_probe only when one focused question is materially useful. Use no more than one intervention.";
|
|
416
|
+
const prompt: Message = { role: "user", content: [{ type: "text", text: `NEW MEMORY UPDATE\n\n${updateBody}\n\nACTIVITY SIGNAL cumulative primary-agent generated tokens: ${update.mainAgentOutputTokens}; cumulative primary-agent tool calls: ${update.mainAgentToolCalls}\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() };
|
|
417
|
+
promptMessage = prompt;
|
|
418
|
+
this.history.push(prompt);
|
|
419
|
+
let intervention: Intervention | undefined;
|
|
420
|
+
const branchEntries = ctx.sessionManager.getBranch() as Entry[];
|
|
421
|
+
const getBranch = () => branchEntries;
|
|
422
|
+
const searchMemoriesTool = createSearchMemoriesAgentTool(getBranch);
|
|
423
|
+
const recallTool = createRecallAgentTool(getBranch);
|
|
424
|
+
const sendProbe = createSendProbeTool((question) => {
|
|
425
|
+
if (intervention) return false;
|
|
426
|
+
intervention = { kind: "probe", question };
|
|
427
|
+
return true;
|
|
428
|
+
});
|
|
429
|
+
const tools: AgentTool<any>[] = [searchMemoriesTool as AgentTool<any>, recallTool as AgentTool<any>, sendProbe as AgentTool<any>];
|
|
430
|
+
if (reviewerEnabled) {
|
|
431
|
+
const requestReview = createRequestReviewTool((request) => {
|
|
432
|
+
if (intervention) return undefined;
|
|
433
|
+
const id = `review-${Date.now().toString(36)}-${Math.random().toString(16).slice(2, 8)}`;
|
|
434
|
+
intervention = { kind: "review", request: {
|
|
435
|
+
id,
|
|
436
|
+
scope: request.scope,
|
|
437
|
+
evidence: request.evidence,
|
|
438
|
+
concern: request.concern,
|
|
439
|
+
reviewFocus: request.review_focus,
|
|
440
|
+
constraints: request.constraints,
|
|
441
|
+
} };
|
|
442
|
+
return id;
|
|
443
|
+
});
|
|
444
|
+
tools.push(requestReview as AgentTool<any>);
|
|
445
|
+
}
|
|
446
|
+
const context: AgentContext = { systemPrompt: buildContemplatorSystemPrompt(reviewerEnabled), messages: this.history.slice(0, -1), tools };
|
|
447
|
+
const config: AgentLoopConfig = {
|
|
448
|
+
model: resolved.model as Model<any>,
|
|
449
|
+
apiKey: resolved.apiKey,
|
|
450
|
+
headers: resolved.headers,
|
|
451
|
+
maxTokens: boundedMaxTokens(resolved.model as Model<any>, AGENT_LOOP_MAX_TOKENS),
|
|
452
|
+
convertToLlm: (messages) => messages as Message[],
|
|
453
|
+
toolExecution: "sequential",
|
|
454
|
+
};
|
|
455
|
+
const stream = agentLoop([prompt], context, config, undefined, streamSimple);
|
|
456
|
+
for await (const event of stream) logAgentStreamError("contemplator", event);
|
|
457
|
+
const result = await stream.result();
|
|
458
|
+
// The LLM call happened and was billed regardless of what we do next, so
|
|
459
|
+
// record its usage even if the session generation changed mid-run.
|
|
460
|
+
for (const message of result) {
|
|
461
|
+
if (message.role === "assistant" && message.usage) {
|
|
462
|
+
this.runtime.recordAgentUsage(message.usage);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
const assistant = [...result].reverse().find((message) => message.role === "assistant");
|
|
466
|
+
debugLog("contemplator.result", {
|
|
467
|
+
messageCount: result.length,
|
|
468
|
+
assistantFound: assistant !== undefined,
|
|
469
|
+
assistantStopReason: assistant && "stopReason" in assistant ? assistant.stopReason : undefined,
|
|
470
|
+
intervention: intervention?.kind,
|
|
471
|
+
});
|
|
472
|
+
if (sessionGeneration === this.sessionGeneration) {
|
|
473
|
+
this.pi.appendEntry(CONTEMPLATOR_MESSAGE, { version: 1, message: prompt });
|
|
474
|
+
promptPersisted = true;
|
|
475
|
+
this.markTipPersisted(ctx);
|
|
476
|
+
}
|
|
477
|
+
if (assistant && sessionGeneration === this.sessionGeneration) {
|
|
478
|
+
this.history.push(assistant);
|
|
479
|
+
this.pi.appendEntry(CONTEMPLATOR_MESSAGE, { version: 1, message: assistant });
|
|
480
|
+
this.markTipPersisted(ctx);
|
|
481
|
+
}
|
|
482
|
+
if (intervention?.kind === "probe" && sessionGeneration === this.sessionGeneration) this.queueProbe(ctx, intervention.question, "send_probe");
|
|
483
|
+
if (intervention?.kind === "review" && this.runtime.config.reviewerEnabled && sessionGeneration === this.sessionGeneration) {
|
|
484
|
+
const reviewerModel = await this.runtime.resolveModel({
|
|
485
|
+
model: ctx.model,
|
|
486
|
+
modelRegistry: ctx.modelRegistry,
|
|
487
|
+
hasUI: ctx.hasUI,
|
|
488
|
+
ui: ctx.ui,
|
|
489
|
+
configuredModel: this.runtime.config.reviewerModel ?? null,
|
|
490
|
+
});
|
|
491
|
+
if (!reviewerModel.ok) {
|
|
492
|
+
debugLog("reviewer.model_unavailable", { reason: reviewerModel.reason });
|
|
493
|
+
} else {
|
|
494
|
+
this.queueStructuralReview({
|
|
495
|
+
ctx,
|
|
496
|
+
requestArgs: intervention.request,
|
|
497
|
+
branchEntries,
|
|
498
|
+
model: reviewerModel.model as Model<any>,
|
|
499
|
+
apiKey: reviewerModel.apiKey,
|
|
500
|
+
headers: reviewerModel.headers,
|
|
501
|
+
sessionGeneration,
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
if (sessionGeneration === this.sessionGeneration) await this.compactHistory(resolved.model as Model<any>, resolved.apiKey, resolved.headers, sessionGeneration);
|
|
506
|
+
} catch (error) {
|
|
507
|
+
failed = true;
|
|
508
|
+
debugLog("contemplator.error", { errorMessage: error instanceof Error ? error.message : String(error) });
|
|
509
|
+
if (sessionGeneration === this.sessionGeneration && !promptPersisted) {
|
|
510
|
+
if (promptMessage && this.history.at(-1) === promptMessage) this.history.pop();
|
|
511
|
+
const pending = this.pending as PendingUpdate | undefined;
|
|
512
|
+
this.pending = {
|
|
513
|
+
observations: mergeMemoryLines(pending?.observations ?? [], update.observations),
|
|
514
|
+
reflections: mergeMemoryLines(pending?.reflections ?? [], update.reflections),
|
|
515
|
+
reviews: mergeMemoryLines(pending?.reviews ?? [], update.reviews),
|
|
516
|
+
mainAgentOutputTokens: update.mainAgentOutputTokens,
|
|
517
|
+
mainAgentToolCalls: update.mainAgentToolCalls,
|
|
518
|
+
};
|
|
519
|
+
this.turnsSinceRun = turnsBeforeRun;
|
|
520
|
+
}
|
|
521
|
+
} finally {
|
|
522
|
+
this.running = false;
|
|
523
|
+
debugLog("contemplator.complete", {
|
|
524
|
+
durationMs: Date.now() - startedAt,
|
|
525
|
+
historyMessageCount: this.history.length,
|
|
526
|
+
pendingUpdate: this.pending !== undefined,
|
|
527
|
+
});
|
|
528
|
+
if (!failed && sessionGeneration === this.sessionGeneration && this.pending) this.observeTurn(ctx);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
private queueProbe(ctx: MemoryUpdateCtx, question: string, source: "send_probe" | "restore", existingProbeId?: string): void {
|
|
533
|
+
const probeId = existingProbeId ?? `${Date.now().toString(36)}-${Math.random().toString(16).slice(2, 8)}`;
|
|
534
|
+
this.pi.sendMessage({
|
|
535
|
+
customType: CONTEMPLATOR_SUGGESTION,
|
|
536
|
+
content: `Background contemplator probe (advisory):\n${question}`,
|
|
537
|
+
display: false,
|
|
538
|
+
details: { version: 1, question, source, probeId },
|
|
539
|
+
}, { deliverAs: "steer", triggerTurn: false });
|
|
540
|
+
this.pi.appendEntry(CONTEMPLATOR_SUGGESTION, { version: 1, suggestion: question, delivered: false, source, probeId });
|
|
541
|
+
this.markTipPersisted(ctx);
|
|
542
|
+
debugLog("contemplator.suggestion_queued", {
|
|
543
|
+
probeId,
|
|
544
|
+
suggestionLength: question.length,
|
|
545
|
+
delivery: "pi.sendMessage",
|
|
546
|
+
deliverAs: "steer",
|
|
547
|
+
triggerTurn: false,
|
|
548
|
+
source,
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
private queueStructuralReview(options: QueueStructuralReviewOptions): void {
|
|
553
|
+
const { ctx, requestArgs, branchEntries, model, apiKey, headers, sessionGeneration } = options;
|
|
554
|
+
const requestForKey: RequestReviewArgs = { scope: requestArgs.scope, evidence: requestArgs.evidence, concern: requestArgs.concern, review_focus: requestArgs.reviewFocus, constraints: requestArgs.constraints };
|
|
555
|
+
const key = reviewRequestKey(requestForKey);
|
|
556
|
+
const duplicateRequest = branchEntries.some((entry) => isReviewRequestEntry(entry) && reviewRequestKey({ scope: entry.data.request.scope, evidence: entry.data.request.evidence, concern: entry.data.request.concern, review_focus: entry.data.request.reviewFocus, constraints: entry.data.request.constraints }) === key);
|
|
557
|
+
if (this.inFlightReviewKeys.has(key) || duplicateRequest) {
|
|
558
|
+
debugLog("contemplator.review_coalesced", { scope: requestArgs.scope, key });
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
const request: StructuralReviewRequest = { ...requestArgs, id: requestArgs.id, createdAt: Date.now(), requestedBy: "contemplator" };
|
|
562
|
+
this.pi.appendEntry(OM_REVIEW_REQUEST, { version: 1, request });
|
|
563
|
+
this.markTipPersisted(ctx);
|
|
564
|
+
this.launchStructuralReview({ ctx, request, model, apiKey, headers, sessionGeneration, key });
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
private launchStructuralReview(options: LaunchStructuralReviewOptions): boolean {
|
|
568
|
+
const { ctx, request, model, apiKey, headers, sessionGeneration, key } = options;
|
|
569
|
+
if (this.runtime.reviewInFlight || this.inFlightReviewIds.has(request.id)) return false;
|
|
570
|
+
// Do not spin a no-progress reviewer repeatedly in one live session. The
|
|
571
|
+
// request stays pending and a later session/tree restoration resumes it.
|
|
572
|
+
this.resumedReviewIds.add(request.id);
|
|
573
|
+
this.inFlightReviewIds.add(request.id);
|
|
574
|
+
if (key) this.inFlightReviewKeys.add(key);
|
|
575
|
+
const session = this.reviewerSessions.get(request.id) ?? { scope: request.scope, history: options.history ?? [] };
|
|
576
|
+
this.reviewerSessions.set(request.id, session);
|
|
577
|
+
const task = this.runtime.launchReviewTask(ctx, async () => {
|
|
578
|
+
try {
|
|
579
|
+
debugLog("reviewer.started", { reviewRequestId: request.id, scope: request.scope, resumed: session.history.length > 0 });
|
|
580
|
+
const result = await runStructuralReview({
|
|
581
|
+
request, model, apiKey, headers,
|
|
582
|
+
getBranch: () => ctx.sessionManager.getBranch() as Entry[],
|
|
583
|
+
recordUsage: (usage) => this.runtime.recordAgentUsage(usage),
|
|
584
|
+
history: session.history,
|
|
585
|
+
onMessages: (messages) => {
|
|
586
|
+
if (sessionGeneration !== this.sessionGeneration || !this.reviewIsPending(ctx, request.id)) return;
|
|
587
|
+
for (const message of messages) {
|
|
588
|
+
session.history.push(message);
|
|
589
|
+
this.pi.appendEntry(OM_REVIEWER_MESSAGE, { version: 1, reviewRequestId: request.id, scope: request.scope, message });
|
|
590
|
+
}
|
|
591
|
+
if (messages.length > 0) this.markTipPersisted(ctx);
|
|
592
|
+
},
|
|
593
|
+
});
|
|
594
|
+
if (sessionGeneration !== this.sessionGeneration) {
|
|
595
|
+
debugLog("reviewer.failed", { reviewRequestId: request.id, reason: "session_changed" });
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
if (!this.reviewIsPending(ctx, request.id)) {
|
|
599
|
+
debugLog("reviewer.failed", { reviewRequestId: request.id, reason: "request_no_longer_pending" });
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
if (!result) {
|
|
603
|
+
debugLog("reviewer.incomplete", { reviewRequestId: request.id, reason: "no_terminal_tool_call" });
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
this.pi.appendEntry(OM_REVIEW_RESULT, { result });
|
|
607
|
+
this.reviewerSessions.delete(request.id);
|
|
608
|
+
this.markTipPersisted(ctx);
|
|
609
|
+
debugLog(result.outcome === "proposal" ? "reviewer.proposal_created" : "reviewer.no_proposal", { reviewRequestId: request.id, reviewMemoryId: result.id, scope: result.scope });
|
|
610
|
+
if (result.outcome === "proposal") {
|
|
611
|
+
const notice = `BACKGROUND ${result.scope.toUpperCase()} REVIEW PROPOSAL [${result.id}]\n\n${result.summary}\n\nRecall memory [${result.id}] to read the full conceptual proposal when it is relevant.\n\nThis is advisory. Evaluate it against the actual environment and current work.`;
|
|
612
|
+
this.pi.sendMessage({ customType: "om.review.proposal", content: notice, display: false, details: { version: 1, reviewRequestId: request.id, reviewMemoryId: result.id, scope: result.scope } }, { deliverAs: "steer", triggerTurn: false });
|
|
613
|
+
this.pi.appendEntry(OM_REVIEWER_NOTICE, { version: 1, reviewRequestId: request.id, reviewMemoryId: result.id, scope: result.scope, content: notice });
|
|
614
|
+
this.markTipPersisted(ctx);
|
|
615
|
+
debugLog("reviewer.primary_notice_queued", { reviewRequestId: request.id, reviewMemoryId: result.id });
|
|
616
|
+
}
|
|
617
|
+
this.runtime.notifyMemoryUpdate(ctx);
|
|
618
|
+
} finally {
|
|
619
|
+
this.inFlightReviewIds.delete(request.id);
|
|
620
|
+
if (key) this.inFlightReviewKeys.delete(key);
|
|
621
|
+
}
|
|
622
|
+
});
|
|
623
|
+
if (!task) {
|
|
624
|
+
this.inFlightReviewIds.delete(request.id);
|
|
625
|
+
this.resumedReviewIds.delete(request.id);
|
|
626
|
+
if (key) this.inFlightReviewKeys.delete(key);
|
|
627
|
+
return false;
|
|
628
|
+
}
|
|
629
|
+
// Runtime clears reviewInFlight in its own finally before this continuation,
|
|
630
|
+
// so the next persisted pending request can start without overlap.
|
|
631
|
+
void task.then(() => {
|
|
632
|
+
const resumeCtx = this.latestCtx;
|
|
633
|
+
if (resumeCtx) void this.resumePendingReviews(resumeCtx);
|
|
634
|
+
});
|
|
635
|
+
return true;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
private reviewIsPending(ctx: MemoryUpdateCtx, reviewRequestId: string): boolean {
|
|
639
|
+
const entries = ctx.sessionManager.getBranch() as Entry[];
|
|
640
|
+
return entries.some((entry) => isReviewRequestEntry(entry) && entry.data.request.id === reviewRequestId)
|
|
641
|
+
&& !entries.some((entry) => isReviewResultEntry(entry) && entry.data.result.reviewRequestId === reviewRequestId);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
private persistReviewerStates(ctx: MemoryUpdateCtx): void {
|
|
645
|
+
for (const [reviewRequestId, session] of this.reviewerSessions) {
|
|
646
|
+
if (session.history.length === 0) continue;
|
|
647
|
+
this.pi.appendEntry(OM_REVIEWER_STATE, { version: 1, reviewRequestId, scope: session.scope, history: session.history });
|
|
648
|
+
}
|
|
649
|
+
if (this.reviewerSessions.size > 0) this.markTipPersisted(ctx);
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
private async resumePendingReviews(ctx: MemoryUpdateCtx): Promise<void> {
|
|
653
|
+
if (!this.runtime.config.reviewerEnabled || this.runtime.config.passive || this.runtime.reviewInFlight) return;
|
|
654
|
+
const entries = ctx.sessionManager.getBranch() as Entry[];
|
|
655
|
+
const completed = new Set(entries.filter(isReviewResultEntry).map((entry) => entry.data.result.reviewRequestId));
|
|
656
|
+
const request = entries.filter(isReviewRequestEntry).map((entry) => entry.data.request).find((item) => !completed.has(item.id) && !this.resumedReviewIds.has(item.id) && !this.inFlightReviewIds.has(item.id) && !this.resolvingReviewIds.has(item.id));
|
|
657
|
+
if (!request) return;
|
|
658
|
+
this.resolvingReviewIds.add(request.id);
|
|
659
|
+
const generation = this.sessionGeneration;
|
|
660
|
+
try {
|
|
661
|
+
const resolved = await this.runtime.resolveModel({ model: ctx.model, modelRegistry: ctx.modelRegistry, hasUI: ctx.hasUI, ui: ctx.ui, configuredModel: this.runtime.config.reviewerModel ?? null });
|
|
662
|
+
if (!resolved.ok || generation !== this.sessionGeneration) {
|
|
663
|
+
debugLog("reviewer.resume_skipped", { reviewRequestId: request.id, reason: resolved.ok ? "session_changed" : resolved.reason });
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
this.launchStructuralReview({ ctx, request, model: resolved.model as Model<any>, apiKey: resolved.apiKey, headers: resolved.headers, sessionGeneration: generation, history: this.reviewerSessions.get(request.id)?.history ?? [] });
|
|
667
|
+
} finally {
|
|
668
|
+
this.resolvingReviewIds.delete(request.id);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
private markTipPersisted(ctx: MemoryUpdateCtx): void {
|
|
673
|
+
this.restoredTipId = (ctx.sessionManager.getBranch() as Entry[]).at(-1)?.id;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
private async compactHistory(model: Model<any>, apiKey: string, headers: Record<string, string> | undefined, sessionGeneration: number): Promise<void> {
|
|
677
|
+
const serializedLength = this.history.reduce((total, message) => total + JSON.stringify(message).length, 0);
|
|
678
|
+
if (this.history.length < 12 || serializedLength < 60_000) return;
|
|
679
|
+
const previousMessageCount = this.history.length;
|
|
680
|
+
debugLog("contemplator.compaction_start", {
|
|
681
|
+
historyMessageCount: previousMessageCount,
|
|
682
|
+
serializedLength,
|
|
683
|
+
});
|
|
684
|
+
const history = this.history.slice();
|
|
685
|
+
const summaryWithUsage = await generateSummaryWithUsage(history as AgentMessage[], model, 4_000, apiKey, headers);
|
|
686
|
+
this.runtime.recordAgentUsage(summaryWithUsage.usage);
|
|
687
|
+
if (sessionGeneration !== this.sessionGeneration) {
|
|
688
|
+
debugLog("contemplator.compaction_stale", { reason: "session_or_branch_changed" });
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
const summary = summaryWithUsage.text;
|
|
692
|
+
const summaryModel = model as Model<any> & { api?: unknown; provider?: string; id?: string };
|
|
693
|
+
const summaryUsage = summaryWithUsage.usage;
|
|
694
|
+
this.history = [{
|
|
695
|
+
role: "assistant",
|
|
696
|
+
content: [{ type: "text", text: `Previous contemplator context summary:\n${summary}` }],
|
|
697
|
+
api: summaryModel.api,
|
|
698
|
+
provider: summaryModel.provider ?? "unknown",
|
|
699
|
+
model: summaryModel.id ?? "contemplator",
|
|
700
|
+
usage: {
|
|
701
|
+
input: summaryUsage.input,
|
|
702
|
+
output: summaryUsage.output,
|
|
703
|
+
cacheRead: summaryUsage.cacheRead,
|
|
704
|
+
cacheWrite: summaryUsage.cacheWrite,
|
|
705
|
+
totalTokens: summaryUsage.input + summaryUsage.output + summaryUsage.cacheRead + summaryUsage.cacheWrite,
|
|
706
|
+
cost: summaryUsage.cost,
|
|
707
|
+
},
|
|
708
|
+
stopReason: "stop",
|
|
709
|
+
timestamp: Date.now(),
|
|
710
|
+
} as AgentMessage];
|
|
711
|
+
this.pi.appendEntry(CONTEMPLATOR_MESSAGE, { version: 1, compacted: true, message: this.history[0] });
|
|
712
|
+
debugLog("contemplator.compaction_complete", {
|
|
713
|
+
previousMessageCount,
|
|
714
|
+
newMessageCount: this.history.length,
|
|
715
|
+
summaryLength: summary.length,
|
|
716
|
+
});
|
|
717
|
+
}
|
|
718
|
+
}
|