@ilya-lesikov/pi-pi 0.1.0 → 0.2.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/3p/pi-plannotator/apps/pi-extension/plannotator-browser.ts +6 -3
- package/3p/pi-plannotator/packages/shared/review-core.ts +22 -3
- package/3p/pi-plannotator/packages/shared/vcs-core.ts +1 -1
- package/3p/pi-subagents/src/index.ts +9 -1
- package/extensions/orchestrator/agents/brainstorm-reviewer.ts +1 -0
- package/extensions/orchestrator/agents/code-reviewer.ts +1 -0
- package/extensions/orchestrator/agents/plan-reviewer.ts +4 -1
- package/extensions/orchestrator/agents/planner.ts +13 -2
- package/extensions/orchestrator/agents/tool-routing.ts +9 -0
- package/extensions/orchestrator/command-handlers.test.ts +22 -25
- package/extensions/orchestrator/config.test.ts +1 -0
- package/extensions/orchestrator/config.ts +3 -1
- package/extensions/orchestrator/custom-footer.ts +146 -0
- package/extensions/orchestrator/event-handlers.test.ts +1 -0
- package/extensions/orchestrator/event-handlers.ts +73 -146
- package/extensions/orchestrator/flant-infra.ts +7 -2
- package/extensions/orchestrator/integration.test.ts +3 -1
- package/extensions/orchestrator/orchestrator.test.ts +1 -0
- package/extensions/orchestrator/orchestrator.ts +12 -4
- package/extensions/orchestrator/phases/machine.ts +38 -0
- package/extensions/orchestrator/phases/review-task.ts +32 -0
- package/extensions/orchestrator/pp-menu.ts +405 -49
- package/extensions/orchestrator/state.ts +8 -9
- package/extensions/orchestrator/usage-tracker.ts +306 -0
- package/package.json +1 -1
|
@@ -24,9 +24,13 @@ import { spawnCodeReviewers } from "./phases/review.js";
|
|
|
24
24
|
import { spawnBrainstormReviewers } from "./phases/brainstorm.js";
|
|
25
25
|
import { openPlannotator, waitForPlannotatorResult, cancelPendingPlannotatorWait } from "./plannotator.js";
|
|
26
26
|
import { Orchestrator, deepReviewConfig, type ActiveTask } from "./orchestrator.js";
|
|
27
|
+
import { createCustomFooter, setFooterContext, setFooterTracker } from "./custom-footer.js";
|
|
28
|
+
import { createUsageTracker, dumpUsageSummary, loadUsageSummary, type UsageTracker } from "./usage-tracker.js";
|
|
27
29
|
import { askUser } from "../../3p/pi-ask-user/index.js";
|
|
28
30
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
29
31
|
|
|
32
|
+
const USAGE_TRACKER_KEY = Symbol.for("pi-pi:usage-tracker");
|
|
33
|
+
|
|
30
34
|
export async function detectDefaultBranch(pi: ExtensionAPI, cwd: string, config?: { diffBaseBranch?: string }): Promise<string> {
|
|
31
35
|
if (config?.diffBaseBranch) return config.diffBaseBranch;
|
|
32
36
|
try {
|
|
@@ -46,17 +50,6 @@ export async function detectDefaultBranch(pi: ExtensionAPI, cwd: string, config?
|
|
|
46
50
|
return "main";
|
|
47
51
|
}
|
|
48
52
|
|
|
49
|
-
export async function detectRepoCwd(pi: ExtensionAPI, modifiedFiles: Set<string>, fallbackCwd: string): Promise<string> {
|
|
50
|
-
if (modifiedFiles.size === 0) return fallbackCwd;
|
|
51
|
-
const firstFile = [...modifiedFiles][0];
|
|
52
|
-
const dir = resolve(firstFile, "..");
|
|
53
|
-
try {
|
|
54
|
-
const result = await pi.exec("git", ["rev-parse", "--show-toplevel"], { cwd: dir, timeout: 5000 });
|
|
55
|
-
if (result.code === 0 && result.stdout.trim()) return result.stdout.trim();
|
|
56
|
-
} catch {}
|
|
57
|
-
return fallbackCwd;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
53
|
export async function selectOption(ctx: any, question: string, options: string[]): Promise<string | undefined> {
|
|
61
54
|
const result = await askUser(ctx, {
|
|
62
55
|
question,
|
|
@@ -69,12 +62,6 @@ export async function selectOption(ctx: any, question: string, options: string[]
|
|
|
69
62
|
return result.selections[0];
|
|
70
63
|
}
|
|
71
64
|
|
|
72
|
-
function setStep(orchestrator: Orchestrator, step: string): void {
|
|
73
|
-
if (!orchestrator.active) return;
|
|
74
|
-
orchestrator.active.state.step = step;
|
|
75
|
-
saveTask(orchestrator.active.dir, orchestrator.active.state);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
65
|
function tryCompleteReviewCycle(orchestrator: Orchestrator): void {
|
|
79
66
|
if (
|
|
80
67
|
!orchestrator.active?.state.reviewCycle ||
|
|
@@ -214,13 +201,13 @@ export async function enterReviewCycle(orchestrator: Orchestrator, ctx: any, kin
|
|
|
214
201
|
return `Started review cycle pass ${pass} (${kind}). Awaiting reviewers.`;
|
|
215
202
|
}
|
|
216
203
|
|
|
217
|
-
export function stopTask(orchestrator: Orchestrator): string {
|
|
204
|
+
export async function stopTask(orchestrator: Orchestrator): Promise<string> {
|
|
218
205
|
if (!orchestrator.active) return "No active task.";
|
|
219
206
|
orchestrator.abortAllSubagents();
|
|
220
207
|
orchestrator.active.state.phase = "done";
|
|
221
208
|
saveTask(orchestrator.active.dir, orchestrator.active.state);
|
|
222
209
|
const desc = orchestrator.active.description;
|
|
223
|
-
orchestrator.cleanupActive();
|
|
210
|
+
await orchestrator.cleanupActive();
|
|
224
211
|
const taskStore = (globalThis as any)[Symbol.for("pi-tasks:store")];
|
|
225
212
|
taskStore?.clearAll?.();
|
|
226
213
|
return `Task "${desc}" stopped.`;
|
|
@@ -238,128 +225,6 @@ export function finalizeReviewCycle(task: ActiveTask): void {
|
|
|
238
225
|
saveTask(task.dir, task.state);
|
|
239
226
|
}
|
|
240
227
|
|
|
241
|
-
export async function runUserGateDialog(orchestrator: Orchestrator, ctx: any, summary: string): Promise<string> {
|
|
242
|
-
if (!orchestrator.active) return "No active task.";
|
|
243
|
-
if (orchestrator.userGatePending) return "A user-gate dialog is already in progress.";
|
|
244
|
-
orchestrator.userGatePending = true;
|
|
245
|
-
|
|
246
|
-
try {
|
|
247
|
-
return await runUserGateDialogInner(orchestrator, ctx, summary);
|
|
248
|
-
} finally {
|
|
249
|
-
orchestrator.userGatePending = false;
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
async function runUserGateDialogInner(orchestrator: Orchestrator, ctx: any, summary: string): Promise<string> {
|
|
254
|
-
if (!orchestrator.active) return "No active task.";
|
|
255
|
-
const pi = orchestrator.pi;
|
|
256
|
-
const phase = orchestrator.active.state.phase;
|
|
257
|
-
const task = orchestrator.active;
|
|
258
|
-
|
|
259
|
-
if (orchestrator.spawnedAgentIds.size > 0 || orchestrator.pendingSubagentSpawns > 0) {
|
|
260
|
-
const count = orchestrator.spawnedAgentIds.size + orchestrator.pendingSubagentSpawns;
|
|
261
|
-
return `${count} subagent(s) still running or spawning.`;
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
const byKind = task.state.reviewPassByKind ?? {};
|
|
265
|
-
const autoCount = byKind["auto"] ?? 0;
|
|
266
|
-
const deepCount = byKind["auto-deep"] ?? 0;
|
|
267
|
-
const autoLabel = autoCount > 0 ? `Review (pass ${autoCount + 1})` : "Review";
|
|
268
|
-
const deepLabel = deepCount > 0 ? `Deep review (pass ${deepCount + 1})` : "Deep review";
|
|
269
|
-
|
|
270
|
-
const continueMessage = "User wants to continue. Run /pp when ready to advance.";
|
|
271
|
-
|
|
272
|
-
if (phase === "brainstorm" && task.type === "implement") {
|
|
273
|
-
const choice = await selectOption(ctx, summary, ["Approve brainstorm", autoLabel, deepLabel, "Continue brainstorming", "Stop task"]);
|
|
274
|
-
finalizeReviewCycle(task);
|
|
275
|
-
if (choice === "Approve brainstorm") {
|
|
276
|
-
const result = await orchestrator.transitionToNextPhase(ctx);
|
|
277
|
-
return result.ok ? "Brainstorm approved. Transitioned to plan." : `Transition blocked: ${result.error}`;
|
|
278
|
-
}
|
|
279
|
-
if (choice === autoLabel) return enterReviewCycle(orchestrator, ctx, "auto");
|
|
280
|
-
if (choice === deepLabel) return enterReviewCycle(orchestrator, ctx, "auto-deep");
|
|
281
|
-
if (choice === "Stop task") return stopTask(orchestrator);
|
|
282
|
-
setStep(orchestrator, "llm_work");
|
|
283
|
-
return continueMessage;
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
if (phase === "brainstorm" && task.type === "brainstorm") {
|
|
287
|
-
const choice = await selectOption(ctx, summary, ["Approve brainstorm", autoLabel, deepLabel, "Continue brainstorming", "Finish brainstorming", "Stop task"]);
|
|
288
|
-
finalizeReviewCycle(task);
|
|
289
|
-
if (choice === "Approve brainstorm") {
|
|
290
|
-
const result = await orchestrator.transitionToNextPhase(ctx);
|
|
291
|
-
return result.ok ? "Brainstorm approved. Transitioned to plan." : `Transition blocked: ${result.error}`;
|
|
292
|
-
}
|
|
293
|
-
if (choice === autoLabel) return enterReviewCycle(orchestrator, ctx, "auto");
|
|
294
|
-
if (choice === deepLabel) return enterReviewCycle(orchestrator, ctx, "auto-deep");
|
|
295
|
-
if (choice === "Finish brainstorming" || choice === "Stop task") return stopTask(orchestrator);
|
|
296
|
-
setStep(orchestrator, "llm_work");
|
|
297
|
-
return continueMessage;
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
if (phase === "debug") {
|
|
301
|
-
const choice = await selectOption(ctx, summary, ["Approve debug", autoLabel, deepLabel, "Continue debugging", "Finish debugging", "Stop task"]);
|
|
302
|
-
finalizeReviewCycle(task);
|
|
303
|
-
if (choice === "Approve debug") {
|
|
304
|
-
const result = await orchestrator.transitionToNextPhase(ctx);
|
|
305
|
-
return result.ok ? "Debug approved. Transitioned to plan." : `Transition blocked: ${result.error}`;
|
|
306
|
-
}
|
|
307
|
-
if (choice === autoLabel) return enterReviewCycle(orchestrator, ctx, "auto");
|
|
308
|
-
if (choice === deepLabel) return enterReviewCycle(orchestrator, ctx, "auto-deep");
|
|
309
|
-
if (choice === "Finish debugging" || choice === "Stop task") return stopTask(orchestrator);
|
|
310
|
-
setStep(orchestrator, "llm_work");
|
|
311
|
-
return continueMessage;
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
if (phase === "plan") {
|
|
315
|
-
const choice = await selectOption(ctx, summary, [
|
|
316
|
-
"Approve plan",
|
|
317
|
-
autoLabel,
|
|
318
|
-
deepLabel,
|
|
319
|
-
"Review in Plannotator",
|
|
320
|
-
"Review on my own",
|
|
321
|
-
"Continue planning",
|
|
322
|
-
"Stop task",
|
|
323
|
-
]);
|
|
324
|
-
finalizeReviewCycle(task);
|
|
325
|
-
if (choice === "Approve plan") {
|
|
326
|
-
const result = await orchestrator.transitionToNextPhase(ctx);
|
|
327
|
-
return result.ok ? "Plan approved. Transitioned to implement." : `Transition blocked: ${result.error}`;
|
|
328
|
-
}
|
|
329
|
-
if (choice === autoLabel) return enterReviewCycle(orchestrator, ctx, "auto");
|
|
330
|
-
if (choice === deepLabel) return enterReviewCycle(orchestrator, ctx, "auto-deep");
|
|
331
|
-
if (choice === "Review in Plannotator") return enterReviewCycle(orchestrator, ctx, "plannotator");
|
|
332
|
-
if (choice === "Stop task") return stopTask(orchestrator);
|
|
333
|
-
setStep(orchestrator, "synthesize");
|
|
334
|
-
return continueMessage;
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
if (phase === "implement") {
|
|
338
|
-
const choice = await selectOption(ctx, summary, [
|
|
339
|
-
"Approve implementation",
|
|
340
|
-
autoLabel,
|
|
341
|
-
deepLabel,
|
|
342
|
-
"Review in Plannotator",
|
|
343
|
-
"Review on my own",
|
|
344
|
-
"Continue implementation",
|
|
345
|
-
"Stop task",
|
|
346
|
-
]);
|
|
347
|
-
finalizeReviewCycle(task);
|
|
348
|
-
if (choice === "Approve implementation") {
|
|
349
|
-
const result = await orchestrator.transitionToNextPhase(ctx);
|
|
350
|
-
return result.ok ? "Implementation approved. Task completed." : `Transition blocked: ${result.error}`;
|
|
351
|
-
}
|
|
352
|
-
if (choice === autoLabel) return enterReviewCycle(orchestrator, ctx, "auto");
|
|
353
|
-
if (choice === deepLabel) return enterReviewCycle(orchestrator, ctx, "auto-deep");
|
|
354
|
-
if (choice === "Review in Plannotator") return enterReviewCycle(orchestrator, ctx, "plannotator");
|
|
355
|
-
if (choice === "Stop task") return stopTask(orchestrator);
|
|
356
|
-
setStep(orchestrator, "llm_work");
|
|
357
|
-
return continueMessage;
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
return "No dialog available for this phase.";
|
|
361
|
-
}
|
|
362
|
-
|
|
363
228
|
function registerOrchestratorTools(orchestrator: Orchestrator): void {
|
|
364
229
|
registerPhaseCompleteTool(orchestrator);
|
|
365
230
|
registerCommitTool(orchestrator);
|
|
@@ -441,8 +306,15 @@ function registerSpecifyReviewsTool(orchestrator: Orchestrator): void {
|
|
|
441
306
|
|
|
442
307
|
ctx.ui?.setWorkingMessage?.("Waiting for user input…");
|
|
443
308
|
try {
|
|
444
|
-
const
|
|
445
|
-
|
|
309
|
+
const { showActiveTaskMenu } = await import("./pp-menu.js");
|
|
310
|
+
const text = await showActiveTaskMenu(orchestrator, ctx, `Plannotator review complete.\n\n${summary}`, "tool");
|
|
311
|
+
if (orchestrator.phaseCompactionPending || orchestrator.taskDoneCompactionPending) {
|
|
312
|
+
return { content: [{ type: "text" as const, text: "Phase transition in progress." }], details: {} };
|
|
313
|
+
}
|
|
314
|
+
if (!text) {
|
|
315
|
+
return { content: [{ type: "text" as const, text: "No action selected." }], details: {} };
|
|
316
|
+
}
|
|
317
|
+
return { content: [{ type: "text" as const, text }], details: {} };
|
|
446
318
|
} finally {
|
|
447
319
|
ctx.ui?.setWorkingMessage?.();
|
|
448
320
|
}
|
|
@@ -539,6 +411,10 @@ function registerPhaseCompleteTool(orchestrator: Orchestrator): void {
|
|
|
539
411
|
export function registerEventHandlers(orchestrator: Orchestrator): void {
|
|
540
412
|
const pi = orchestrator.pi;
|
|
541
413
|
|
|
414
|
+
function getUsageTracker(): UsageTracker | undefined {
|
|
415
|
+
return (globalThis as any)[USAGE_TRACKER_KEY] as UsageTracker | undefined;
|
|
416
|
+
}
|
|
417
|
+
|
|
542
418
|
function trackSubagentEvent(data: any, event: "created" | "started" | "first_tool" | "first_turn" | "completed" | "failed"): void {
|
|
543
419
|
if (!orchestrator.active || !data?.id) return;
|
|
544
420
|
const now = Date.now();
|
|
@@ -709,7 +585,7 @@ export function registerEventHandlers(orchestrator: Orchestrator): void {
|
|
|
709
585
|
}
|
|
710
586
|
|
|
711
587
|
if (choice === "Stop task") {
|
|
712
|
-
pi.sendUserMessage(`[PI-PI] ${stopTask(orchestrator)}`, { deliverAs: "followUp" });
|
|
588
|
+
pi.sendUserMessage(`[PI-PI] ${await stopTask(orchestrator)}`, { deliverAs: "followUp" });
|
|
713
589
|
return;
|
|
714
590
|
}
|
|
715
591
|
|
|
@@ -821,7 +697,7 @@ export function registerEventHandlers(orchestrator: Orchestrator): void {
|
|
|
821
697
|
}
|
|
822
698
|
|
|
823
699
|
if (choice === "Stop task") {
|
|
824
|
-
pi.sendUserMessage(`[PI-PI] ${stopTask(orchestrator)}`, { deliverAs: "followUp" });
|
|
700
|
+
pi.sendUserMessage(`[PI-PI] ${await stopTask(orchestrator)}`, { deliverAs: "followUp" });
|
|
825
701
|
return;
|
|
826
702
|
}
|
|
827
703
|
|
|
@@ -848,6 +724,17 @@ export function registerEventHandlers(orchestrator: Orchestrator): void {
|
|
|
848
724
|
}
|
|
849
725
|
|
|
850
726
|
pi.events.on("subagents:completed", (data: any) => {
|
|
727
|
+
const usageTracker = getUsageTracker();
|
|
728
|
+
if (usageTracker && data?.tokens) {
|
|
729
|
+
usageTracker.recordSubagentCompletion(data.tokens, undefined, {
|
|
730
|
+
description: data.description || data.type || data.id || "unknown",
|
|
731
|
+
modelId: data.modelId || "unknown",
|
|
732
|
+
durationMs: data.durationMs,
|
|
733
|
+
toolUses: data.toolUses,
|
|
734
|
+
});
|
|
735
|
+
(orchestrator.lastCtx?.ui as any)?.requestRender?.();
|
|
736
|
+
}
|
|
737
|
+
|
|
851
738
|
if (!orchestrator.active || !data?.id) return;
|
|
852
739
|
trackSubagentEvent(data, "completed");
|
|
853
740
|
orchestrator.spawnedAgentIds.delete(data.id);
|
|
@@ -916,10 +803,36 @@ export function registerEventHandlers(orchestrator: Orchestrator): void {
|
|
|
916
803
|
await orchestrator.cleanupActive();
|
|
917
804
|
});
|
|
918
805
|
|
|
806
|
+
pi.on("session_shutdown", async (_event, ctx) => {
|
|
807
|
+
if ((globalThis as any)[SUBAGENT_SESSION_KEY]) return;
|
|
808
|
+
const tracker = getUsageTracker();
|
|
809
|
+
if (!tracker) return;
|
|
810
|
+
const sessionId = ctx.sessionManager?.getSessionId?.() || `session-${Date.now()}`;
|
|
811
|
+
try {
|
|
812
|
+
dumpUsageSummary(tracker, sessionId);
|
|
813
|
+
} catch (err: any) {
|
|
814
|
+
console.error(`[pi-pi] Failed to dump usage summary: ${err.message}`);
|
|
815
|
+
}
|
|
816
|
+
delete (globalThis as any)[USAGE_TRACKER_KEY];
|
|
817
|
+
});
|
|
818
|
+
|
|
919
819
|
pi.on("session_start", async (_event, ctx) => {
|
|
920
820
|
orchestrator.lastCtx = ctx;
|
|
921
821
|
orchestrator.cwd = ctx.cwd;
|
|
922
822
|
|
|
823
|
+
if (!(globalThis as any)[SUBAGENT_SESSION_KEY]) {
|
|
824
|
+
const tracker = createUsageTracker();
|
|
825
|
+
const sessionId = ctx.sessionManager?.getSessionId?.() || "";
|
|
826
|
+
if (sessionId) {
|
|
827
|
+
const previous = loadUsageSummary(sessionId);
|
|
828
|
+
if (previous) tracker.loadFromSummary(previous);
|
|
829
|
+
}
|
|
830
|
+
(globalThis as any)[USAGE_TRACKER_KEY] = tracker;
|
|
831
|
+
setFooterContext(ctx);
|
|
832
|
+
setFooterTracker(tracker);
|
|
833
|
+
ctx.ui.setFooter(createCustomFooter);
|
|
834
|
+
}
|
|
835
|
+
|
|
923
836
|
const subagentsMgr = (globalThis as any)[Symbol.for("pi-subagents:manager")];
|
|
924
837
|
subagentsMgr?.refreshWidget?.(ctx.ui);
|
|
925
838
|
const taskStore = (globalThis as any)[Symbol.for("pi-tasks:store")];
|
|
@@ -1196,6 +1109,20 @@ export function registerEventHandlers(orchestrator: Orchestrator): void {
|
|
|
1196
1109
|
});
|
|
1197
1110
|
|
|
1198
1111
|
pi.on("turn_end", async (event, ctx) => {
|
|
1112
|
+
const msg = event.message as any;
|
|
1113
|
+
const usageTracker = getUsageTracker();
|
|
1114
|
+
if (usageTracker && msg?.usage) {
|
|
1115
|
+
const input = typeof msg.usage.input === "number" ? msg.usage.input : 0;
|
|
1116
|
+
const output = typeof msg.usage.output === "number" ? msg.usage.output : 0;
|
|
1117
|
+
const cacheRead = typeof msg.usage.cacheRead === "number" ? msg.usage.cacheRead : 0;
|
|
1118
|
+
const cacheWrite = typeof msg.usage.cacheWrite === "number" ? msg.usage.cacheWrite : 0;
|
|
1119
|
+
const cost = typeof msg.usage.cost?.total === "number" ? msg.usage.cost.total : 0;
|
|
1120
|
+
const modelId = (typeof msg.model === "string" && msg.model) || ctx.model?.id || "unknown-model";
|
|
1121
|
+
const provider = (typeof msg.provider === "string" && msg.provider) || ctx.model?.provider || "unknown";
|
|
1122
|
+
usageTracker.recordTurn(modelId, provider, input, output, cacheRead, cacheWrite, cost);
|
|
1123
|
+
(ctx.ui as any)?.requestRender?.();
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1199
1126
|
if (!orchestrator.active || orchestrator.active.state.phase === "done") return;
|
|
1200
1127
|
orchestrator.updateStatus(ctx);
|
|
1201
1128
|
|
|
@@ -1218,7 +1145,6 @@ export function registerEventHandlers(orchestrator: Orchestrator): void {
|
|
|
1218
1145
|
|
|
1219
1146
|
const phase = orchestrator.active.state.phase;
|
|
1220
1147
|
|
|
1221
|
-
const msg = event.message as any;
|
|
1222
1148
|
if (msg?.stopReason === "aborted") return;
|
|
1223
1149
|
if (msg?.stopReason === "error") {
|
|
1224
1150
|
const errorMsg = msg.errorMessage || "unknown error";
|
|
@@ -1323,6 +1249,7 @@ export function registerEventHandlers(orchestrator: Orchestrator): void {
|
|
|
1323
1249
|
}
|
|
1324
1250
|
|
|
1325
1251
|
if (orchestrator.active.type === "brainstorm" && phase === "brainstorm") return;
|
|
1252
|
+
if (orchestrator.active.type === "review" && phase === "review") return;
|
|
1326
1253
|
|
|
1327
1254
|
const contentParts = Array.isArray(msg?.content) ? msg.content : [];
|
|
1328
1255
|
const hasText = contentParts.some((c: any) => c.type === "text" && c.text?.trim());
|
|
@@ -11,6 +11,8 @@ export interface OpenRouterModelData {
|
|
|
11
11
|
pricing: {
|
|
12
12
|
prompt: number;
|
|
13
13
|
completion: number;
|
|
14
|
+
cacheRead: number;
|
|
15
|
+
cacheWrite: number;
|
|
14
16
|
};
|
|
15
17
|
modality: string;
|
|
16
18
|
}
|
|
@@ -241,6 +243,8 @@ export async function fetchOpenRouterMetadata(modelIds: string[]): Promise<Recor
|
|
|
241
243
|
pricing: {
|
|
242
244
|
prompt: toNumber(pricing.prompt, 0),
|
|
243
245
|
completion: toNumber(pricing.completion, 0),
|
|
246
|
+
cacheRead: toNumber(pricing.input_cache_read, 0),
|
|
247
|
+
cacheWrite: toNumber(pricing.input_cache_write, 0),
|
|
244
248
|
},
|
|
245
249
|
modality: typeof architecture.modality === "string" ? architecture.modality : "text",
|
|
246
250
|
};
|
|
@@ -267,8 +271,8 @@ function buildProviderModelConfig(
|
|
|
267
271
|
cost: {
|
|
268
272
|
input: toNumber(modelMeta?.pricing.prompt, 0) * 1_000_000,
|
|
269
273
|
output: toNumber(modelMeta?.pricing.completion, 0) * 1_000_000,
|
|
270
|
-
cacheRead: 0,
|
|
271
|
-
cacheWrite: 0,
|
|
274
|
+
cacheRead: toNumber(modelMeta?.pricing.cacheRead, 0) * 1_000_000,
|
|
275
|
+
cacheWrite: toNumber(modelMeta?.pricing.cacheWrite, 0) * 1_000_000,
|
|
272
276
|
},
|
|
273
277
|
contextWindow: toNumber(modelMeta?.context_length, 200000),
|
|
274
278
|
maxTokens: toNumber(modelMeta?.max_completion_tokens, 32000),
|
|
@@ -358,6 +362,7 @@ export function generateFlantConfig(models: string[]): Partial<PiPiConfig> {
|
|
|
358
362
|
implement: { model: modelSpec(implementModel), thinking: "high" },
|
|
359
363
|
debug: { model: modelSpec(debugModel), thinking: "high" },
|
|
360
364
|
brainstorm: { model: modelSpec(brainstormModel), thinking: "high" },
|
|
365
|
+
review: { model: modelSpec(implementModel), thinking: "high" },
|
|
361
366
|
},
|
|
362
367
|
planners: {
|
|
363
368
|
opus: makeVariant(latestOpus, fallback),
|
|
@@ -4,7 +4,7 @@ import { join } from "path";
|
|
|
4
4
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
5
5
|
import { Orchestrator } from "./orchestrator.js";
|
|
6
6
|
import { registerCommandHandlers } from "./command-handlers.js";
|
|
7
|
-
import { registerEventHandlers
|
|
7
|
+
import { registerEventHandlers } from "./event-handlers.js";
|
|
8
8
|
import { createTask, loadTask, saveTask } from "./state.js";
|
|
9
9
|
|
|
10
10
|
vi.mock("./cbm.js", () => ({ registerCbmTools: vi.fn() }));
|
|
@@ -25,6 +25,7 @@ vi.mock("./config.js", async (importOriginal) => {
|
|
|
25
25
|
implement: { model: "test/model", thinking: "high" },
|
|
26
26
|
debug: { model: "test/model", thinking: "high" },
|
|
27
27
|
brainstorm: { model: "test/model", thinking: "high" },
|
|
28
|
+
review: { model: "test/model", thinking: "high" },
|
|
28
29
|
},
|
|
29
30
|
planners: { test: { enabled: true, model: "test/planner", thinking: "low" } },
|
|
30
31
|
planReviewers: {},
|
|
@@ -142,6 +143,7 @@ function makeConfig() {
|
|
|
142
143
|
implement: { model: "test/model", thinking: "high" },
|
|
143
144
|
debug: { model: "test/model", thinking: "high" },
|
|
144
145
|
brainstorm: { model: "test/model", thinking: "high" },
|
|
146
|
+
review: { model: "test/model", thinking: "high" },
|
|
145
147
|
},
|
|
146
148
|
planners: {
|
|
147
149
|
test: { enabled: true, model: "test/planner", thinking: "low" },
|
|
@@ -98,6 +98,7 @@ describe("deepReviewConfig", () => {
|
|
|
98
98
|
implement: { model: "a/impl", thinking: "high" },
|
|
99
99
|
debug: { model: "a/debug", thinking: "high" },
|
|
100
100
|
brainstorm: { model: "a/brain", thinking: "high" },
|
|
101
|
+
review: { model: "a/review", thinking: "high" },
|
|
101
102
|
},
|
|
102
103
|
planners: {},
|
|
103
104
|
planReviewers: {
|
|
@@ -16,7 +16,8 @@ import { loadContextFiles, getPhaseArtifacts, getLatestSynthesizedPlan } from ".
|
|
|
16
16
|
import { brainstormSystemPrompt } from "./phases/brainstorm.js";
|
|
17
17
|
import { planningSystemPrompt, spawnPlanners } from "./phases/planning.js";
|
|
18
18
|
import { implementationSystemPrompt } from "./phases/implementation.js";
|
|
19
|
-
import { reviewSystemPrompt } from "./phases/review.js";
|
|
19
|
+
import { reviewSystemPrompt as reviewCycleSystemPrompt } from "./phases/review.js";
|
|
20
|
+
import { reviewSystemPrompt as reviewTaskSystemPrompt } from "./phases/review-task.js";
|
|
20
21
|
import { registerAgentDefinitions, unregisterAgentDefinitions } from "./agents/registry.js";
|
|
21
22
|
import { createExploreAgent } from "./agents/explore.js";
|
|
22
23
|
import { createLibrarianAgent } from "./agents/librarian.js";
|
|
@@ -207,7 +208,7 @@ export class Orchestrator {
|
|
|
207
208
|
if (this.active.state.reviewCycle?.step === "apply_feedback") {
|
|
208
209
|
const pass = this.active.state.reviewCycle.pass;
|
|
209
210
|
const manualReview = this.active.state.reviewCycle.kind === "manual";
|
|
210
|
-
return
|
|
211
|
+
return reviewCycleSystemPrompt(this.active.dir, pass, manualReview, this.active.state.phase);
|
|
211
212
|
}
|
|
212
213
|
|
|
213
214
|
switch (this.active.state.phase) {
|
|
@@ -219,6 +220,8 @@ export class Orchestrator {
|
|
|
219
220
|
return planningSystemPrompt(this.active.dir);
|
|
220
221
|
case "implement":
|
|
221
222
|
return implementationSystemPrompt(this.active.dir);
|
|
223
|
+
case "review":
|
|
224
|
+
return reviewTaskSystemPrompt(this.active.dir);
|
|
222
225
|
default:
|
|
223
226
|
return "";
|
|
224
227
|
}
|
|
@@ -314,7 +317,12 @@ export class Orchestrator {
|
|
|
314
317
|
description: state.description,
|
|
315
318
|
};
|
|
316
319
|
|
|
317
|
-
const modelConfig = this.config.mainModel[
|
|
320
|
+
const modelConfig = this.config.mainModel[
|
|
321
|
+
type === "debug" ? "debug"
|
|
322
|
+
: type === "brainstorm" ? "brainstorm"
|
|
323
|
+
: type === "review" ? "review"
|
|
324
|
+
: "implement"
|
|
325
|
+
];
|
|
318
326
|
const modelOk = await this.switchModel(ctx, modelConfig.model, modelConfig.thinking);
|
|
319
327
|
if (!modelOk) {
|
|
320
328
|
ctx.ui.notify(`Model "${modelConfig.model}" not found — using current model`, "warning");
|
|
@@ -328,7 +336,7 @@ export class Orchestrator {
|
|
|
328
336
|
this.injectContextAndArtifacts(this.active.dir, this.active.state.phase);
|
|
329
337
|
|
|
330
338
|
this.phaseStartTime = Date.now();
|
|
331
|
-
const isGenericDescription = ["implement", "debug", "brainstorm"].includes(this.active.description);
|
|
339
|
+
const isGenericDescription = ["implement", "debug", "brainstorm", "review"].includes(this.active.description);
|
|
332
340
|
const hasInheritedTaskContext = Boolean(fromTaskDir && type === "implement");
|
|
333
341
|
const isWaitingForPlanners = this.active.state.phase === "plan" && this.active.state.step === "await_planners";
|
|
334
342
|
if (isGenericDescription && !hasInheritedTaskContext) {
|
|
@@ -67,6 +67,11 @@ const TRANSITIONS: Record<TaskType, Record<string, string[]>> = {
|
|
|
67
67
|
plan: ["implement"],
|
|
68
68
|
implement: ["done"],
|
|
69
69
|
},
|
|
70
|
+
review: {
|
|
71
|
+
review: ["plan"],
|
|
72
|
+
plan: ["implement"],
|
|
73
|
+
implement: ["done"],
|
|
74
|
+
},
|
|
70
75
|
};
|
|
71
76
|
|
|
72
77
|
export function canTransition(taskType: TaskType, from: Phase, to: Phase): boolean {
|
|
@@ -116,6 +121,37 @@ export function validateExitCriteria(
|
|
|
116
121
|
return { ok: true };
|
|
117
122
|
}
|
|
118
123
|
|
|
124
|
+
case "review": {
|
|
125
|
+
const ur = join(taskDir, "USER_REQUEST.md");
|
|
126
|
+
const res = join(taskDir, "RESEARCH.md");
|
|
127
|
+
if (isMissingOrEmpty(ur)) {
|
|
128
|
+
return { ok: false, reason: "USER_REQUEST.md does not exist or is empty" };
|
|
129
|
+
}
|
|
130
|
+
if (isMissingOrEmpty(res)) {
|
|
131
|
+
return { ok: false, reason: "RESEARCH.md does not exist or is empty" };
|
|
132
|
+
}
|
|
133
|
+
const urContent = readFileSync(ur, "utf-8");
|
|
134
|
+
const resContent = readFileSync(res, "utf-8");
|
|
135
|
+
|
|
136
|
+
const userRequestValidation = validateUserRequest(urContent);
|
|
137
|
+
if (!userRequestValidation.ok) {
|
|
138
|
+
return {
|
|
139
|
+
ok: false,
|
|
140
|
+
reason: formatValidationErrors("USER_REQUEST.md", userRequestValidation.errors, USER_REQUEST_TEMPLATE),
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const researchValidation = validateResearch(resContent);
|
|
145
|
+
if (!researchValidation.ok) {
|
|
146
|
+
return {
|
|
147
|
+
ok: false,
|
|
148
|
+
reason: formatValidationErrors("RESEARCH.md", researchValidation.errors, RESEARCH_TEMPLATE),
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return { ok: true };
|
|
153
|
+
}
|
|
154
|
+
|
|
119
155
|
case "plan": {
|
|
120
156
|
const plan = getLatestSynthesizedPlan(taskDir);
|
|
121
157
|
if (!plan) {
|
|
@@ -205,5 +241,7 @@ export function phasePipeline(taskType: TaskType): Phase[] {
|
|
|
205
241
|
return ["debug", "plan", "implement", "done"];
|
|
206
242
|
case "brainstorm":
|
|
207
243
|
return ["brainstorm", "plan", "implement", "done"];
|
|
244
|
+
case "review":
|
|
245
|
+
return ["review", "plan", "implement", "done"];
|
|
208
246
|
}
|
|
209
247
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export function reviewSystemPrompt(taskDir: string): string {
|
|
2
|
+
return [
|
|
3
|
+
"[PI-PI — REVIEW PHASE]",
|
|
4
|
+
"",
|
|
5
|
+
"You are reviewing code changes. USER_REQUEST.md describes what to review.",
|
|
6
|
+
"Read it first to understand the scope.",
|
|
7
|
+
"",
|
|
8
|
+
"Use available tools to analyze the changes:",
|
|
9
|
+
"- git diff, git log, git show for examining commits and diffs",
|
|
10
|
+
"- read, lsp, grep, find for understanding the code",
|
|
11
|
+
"- gh pr view for GitHub PR context if a PR URL is mentioned",
|
|
12
|
+
"",
|
|
13
|
+
"Write your findings:",
|
|
14
|
+
`- ${taskDir}/USER_REQUEST.md — update with a clear problem statement of issues found`,
|
|
15
|
+
`- ${taskDir}/RESEARCH.md — detailed technical analysis`,
|
|
16
|
+
"",
|
|
17
|
+
"USER_REQUEST.md format:",
|
|
18
|
+
"- # User Request",
|
|
19
|
+
"- ## Problem",
|
|
20
|
+
"- ## Constraints",
|
|
21
|
+
"",
|
|
22
|
+
"RESEARCH.md format:",
|
|
23
|
+
"- ## Affected Code",
|
|
24
|
+
"- ## Architecture Context",
|
|
25
|
+
"- ## Constraints & Edge Cases",
|
|
26
|
+
"- ## Open Questions (optional)",
|
|
27
|
+
"",
|
|
28
|
+
"Focus on: correctness, edge cases, style consistency, missing tests, potential bugs.",
|
|
29
|
+
"",
|
|
30
|
+
"When complete, call pp_phase_complete with a brief summary of findings.",
|
|
31
|
+
].join("\n");
|
|
32
|
+
}
|