@ilya-lesikov/pi-pi 0.4.0 → 0.6.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-ask-user/index.ts +65 -49
- package/3p/pi-subagents/src/agent-manager.ts +8 -0
- package/3p/pi-subagents/src/agent-runner.ts +112 -19
- package/3p/pi-subagents/src/index.ts +3 -0
- package/extensions/orchestrator/agents/brainstorm-reviewer.ts +32 -19
- package/extensions/orchestrator/agents/code-reviewer.ts +31 -17
- package/extensions/orchestrator/agents/constraints.ts +55 -0
- package/extensions/orchestrator/agents/explore.ts +13 -13
- package/extensions/orchestrator/agents/librarian.ts +12 -9
- package/extensions/orchestrator/agents/plan-reviewer.ts +31 -19
- package/extensions/orchestrator/agents/planner.ts +28 -23
- package/extensions/orchestrator/agents/registry.ts +2 -1
- package/extensions/orchestrator/agents/repo-context.ts +11 -0
- package/extensions/orchestrator/agents/task.ts +14 -11
- package/extensions/orchestrator/agents/tool-routing.ts +17 -32
- package/extensions/orchestrator/ast-search.ts +2 -1
- package/extensions/orchestrator/cbm.test.ts +35 -0
- package/extensions/orchestrator/cbm.ts +43 -13
- package/extensions/orchestrator/command-handlers.test.ts +390 -19
- package/extensions/orchestrator/command-handlers.ts +73 -31
- package/extensions/orchestrator/commands.test.ts +255 -2
- package/extensions/orchestrator/commands.ts +108 -10
- package/extensions/orchestrator/config.test.ts +289 -68
- package/extensions/orchestrator/config.ts +630 -121
- package/extensions/orchestrator/context.test.ts +177 -10
- package/extensions/orchestrator/context.ts +115 -14
- package/extensions/orchestrator/custom-footer.ts +2 -1
- package/extensions/orchestrator/doctor.test.ts +559 -0
- package/extensions/orchestrator/doctor.ts +664 -0
- package/extensions/orchestrator/event-handlers.test.ts +84 -22
- package/extensions/orchestrator/event-handlers.ts +1191 -360
- package/extensions/orchestrator/exa.test.ts +46 -0
- package/extensions/orchestrator/exa.ts +16 -10
- package/extensions/orchestrator/flant-infra.test.ts +224 -0
- package/extensions/orchestrator/flant-infra.ts +110 -41
- package/extensions/orchestrator/index.ts +13 -2
- package/extensions/orchestrator/integration.test.ts +2866 -118
- package/extensions/orchestrator/log.test.ts +219 -0
- package/extensions/orchestrator/log.ts +153 -0
- package/extensions/orchestrator/model-registry.test.ts +238 -0
- package/extensions/orchestrator/model-registry.ts +282 -0
- package/extensions/orchestrator/model-version.test.ts +27 -0
- package/extensions/orchestrator/model-version.ts +19 -0
- package/extensions/orchestrator/orchestrator.test.ts +206 -56
- package/extensions/orchestrator/orchestrator.ts +295 -148
- package/extensions/orchestrator/phases/brainstorm.test.ts +10 -7
- package/extensions/orchestrator/phases/brainstorm.ts +41 -35
- package/extensions/orchestrator/phases/implementation.ts +7 -11
- package/extensions/orchestrator/phases/machine.test.ts +27 -8
- package/extensions/orchestrator/phases/machine.ts +13 -0
- package/extensions/orchestrator/phases/planning.ts +57 -29
- package/extensions/orchestrator/phases/review-task.ts +3 -3
- package/extensions/orchestrator/phases/review.ts +38 -39
- package/extensions/orchestrator/phases/spawn-blocking.test.ts +69 -0
- package/extensions/orchestrator/phases/verdict.test.ts +139 -0
- package/extensions/orchestrator/phases/verdict.ts +82 -0
- package/extensions/orchestrator/plannotator.test.ts +85 -0
- package/extensions/orchestrator/plannotator.ts +24 -6
- package/extensions/orchestrator/pp-menu.test.ts +134 -0
- package/extensions/orchestrator/pp-menu.ts +2631 -392
- package/extensions/orchestrator/repo-utils.test.ts +151 -0
- package/extensions/orchestrator/repo-utils.ts +67 -0
- package/extensions/orchestrator/spawn-cleanup.test.ts +57 -0
- package/extensions/orchestrator/spawn-cleanup.ts +35 -0
- package/extensions/orchestrator/state.test.ts +76 -6
- package/extensions/orchestrator/state.ts +89 -26
- package/extensions/orchestrator/subagent-session-marker.test.ts +36 -0
- package/extensions/orchestrator/test-helpers.ts +217 -0
- package/extensions/orchestrator/tracer.test.ts +132 -0
- package/extensions/orchestrator/tracer.ts +127 -0
- package/extensions/orchestrator/transition-controller.test.ts +207 -0
- package/extensions/orchestrator/transition-controller.ts +259 -0
- package/extensions/orchestrator/usage-tracker.test.ts +435 -0
- package/extensions/orchestrator/usage-tracker.ts +23 -2
- package/extensions/orchestrator/validate-artifacts.test.ts +83 -1
- package/package.json +2 -1
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
import { existsSync, copyFileSync, mkdirSync, writeFileSync, readFileSync, readdirSync, rmSync } from "fs";
|
|
2
2
|
import { join, basename, relative } from "path";
|
|
3
3
|
import type { ExtensionAPI, ExtensionContext, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
4
|
-
import { loadConfig,
|
|
4
|
+
import { loadConfig, resolvePreset, type NormalizedPiPiConfig } from "./config.js";
|
|
5
5
|
import {
|
|
6
6
|
createTask,
|
|
7
7
|
loadTask,
|
|
8
8
|
saveTask,
|
|
9
9
|
lockTask,
|
|
10
|
+
getEffectivePhaseMode,
|
|
10
11
|
type TaskType,
|
|
12
|
+
type TaskMode,
|
|
11
13
|
type TaskState,
|
|
12
14
|
type Phase,
|
|
13
15
|
} from "./state.js";
|
|
14
16
|
import { phasePipeline } from "./phases/machine.js";
|
|
15
|
-
import {
|
|
17
|
+
import { getContextDirs, loadAllContextFiles, getPhaseArtifacts, getLatestSynthesizedPlan } from "./context.js";
|
|
16
18
|
import { brainstormSystemPrompt } from "./phases/brainstorm.js";
|
|
17
19
|
import { planningSystemPrompt, spawnPlanners } from "./phases/planning.js";
|
|
18
20
|
import { implementationSystemPrompt } from "./phases/implementation.js";
|
|
@@ -22,6 +24,15 @@ import { registerAgentDefinitions, unregisterAgentDefinitions } from "./agents/r
|
|
|
22
24
|
import { createExploreAgent } from "./agents/explore.js";
|
|
23
25
|
import { createLibrarianAgent } from "./agents/librarian.js";
|
|
24
26
|
import { createTaskAgent } from "./agents/task.js";
|
|
27
|
+
import { resolveModel, getModelInfo } from "./model-registry.js";
|
|
28
|
+
import { buildRepoContext } from "./agents/repo-context.js";
|
|
29
|
+
import { getLogger, addTaskDestination, removeTaskDestination, setLogLevel } from "./log.js";
|
|
30
|
+
import { handleSpawnResult } from "./spawn-cleanup.js";
|
|
31
|
+
import { TransitionController, type TransitionHost } from "./transition-controller.js";
|
|
32
|
+
|
|
33
|
+
function isEnabled(value: { enabled?: boolean } | undefined): boolean {
|
|
34
|
+
return value?.enabled !== false;
|
|
35
|
+
}
|
|
25
36
|
|
|
26
37
|
const BUNDLED_TOOLS = new Set([
|
|
27
38
|
"Agent", "get_subagent_result", "steer_subagent",
|
|
@@ -42,7 +53,7 @@ export interface ActiveTask {
|
|
|
42
53
|
|
|
43
54
|
export class Orchestrator {
|
|
44
55
|
active: ActiveTask | null = null;
|
|
45
|
-
config!:
|
|
56
|
+
config!: NormalizedPiPiConfig;
|
|
46
57
|
cwd = "";
|
|
47
58
|
spawnedAgentIds = new Set<string>();
|
|
48
59
|
agentDescriptions = new Map<string, string>();
|
|
@@ -59,22 +70,18 @@ export class Orchestrator {
|
|
|
59
70
|
step?: string;
|
|
60
71
|
}>();
|
|
61
72
|
staleAgentTimer: ReturnType<typeof setInterval> | null = null;
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
nudgeTimestamps: number[] = [];
|
|
67
|
-
cooldownHits: number[] = [];
|
|
73
|
+
// Single consecutive-nudge guard (replaces the old multi-tier throttle). Reset
|
|
74
|
+
// to 0 on any productive turn; once it reaches the cap the nudges halt with one
|
|
75
|
+
// user notification.
|
|
76
|
+
consecutiveNudges = 0;
|
|
68
77
|
nudgeHalted = false;
|
|
69
78
|
pendingSubagentSpawns = 0;
|
|
70
79
|
errorRetryCount = 0;
|
|
71
80
|
commitReminderSent = false;
|
|
72
81
|
phaseStartTime = 0;
|
|
73
|
-
awaitPollTimer: ReturnType<typeof setInterval> | null = null;
|
|
74
82
|
pendingRetryTimer: ReturnType<typeof setTimeout> | null = null;
|
|
75
83
|
activeTaskToken = 0;
|
|
76
84
|
userGatePending = false;
|
|
77
|
-
reviewTransitionToken = -1;
|
|
78
85
|
lastCtx: any = null;
|
|
79
86
|
failedPlannerVariants: string[] = [];
|
|
80
87
|
failedReviewerVariants: string[] = [];
|
|
@@ -82,9 +89,65 @@ export class Orchestrator {
|
|
|
82
89
|
reviewerFailureDialogPending = false;
|
|
83
90
|
plannotatorReject: ((reason: Error) => void) | null = null;
|
|
84
91
|
plannotatorUnsub: (() => void) | null = null;
|
|
85
|
-
|
|
92
|
+
plannotatorTimer: ReturnType<typeof setTimeout> | null = null;
|
|
93
|
+
transitionToNextPhase: (ctx: any, plannerPreset?: string) => Promise<{ ok: boolean; error?: string }> = async () => ({ ok: false, error: "not initialized" });
|
|
94
|
+
// Assigned by registerEventHandlers. Wired into planner spawn onSettled as the
|
|
95
|
+
// safety net the deleted 5s poller used to provide: when a spawn settles having
|
|
96
|
+
// produced ZERO agents (zero enabled planners, or all spawns failed before any
|
|
97
|
+
// subagents:completed/failed event), nothing else would advance await_planners.
|
|
98
|
+
// For spawned>0 the lifecycle events drive completion, so onSettled passes the
|
|
99
|
+
// spawned count and this only force-checks the zero case.
|
|
100
|
+
checkPlannerCompletion: () => void = () => {};
|
|
101
|
+
readonly transitionController: TransitionController;
|
|
102
|
+
|
|
103
|
+
constructor(readonly pi: ExtensionAPI) {
|
|
104
|
+
// The controller calls pi (the main session) directly for sends, and uses the
|
|
105
|
+
// host only for live-ctx-dependent bits (compact/isIdle/currentStep).
|
|
106
|
+
this.transitionController = new TransitionController(this.makeTransitionHost(), this.pi);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Live-session host the TransitionController uses for compaction/idle/step.
|
|
110
|
+
private makeTransitionHost(): TransitionHost {
|
|
111
|
+
return {
|
|
112
|
+
compact: (options) => {
|
|
113
|
+
const compact = this.lastCtx?.compact;
|
|
114
|
+
if (!compact) return false;
|
|
115
|
+
compact(options);
|
|
116
|
+
return true;
|
|
117
|
+
},
|
|
118
|
+
isIdle: () => {
|
|
119
|
+
const idle = this.lastCtx?.isIdle;
|
|
120
|
+
return typeof idle === "function" ? !!idle.call(this.lastCtx) : false;
|
|
121
|
+
},
|
|
122
|
+
currentStep: () => this.active?.state.step ?? null,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
86
125
|
|
|
87
|
-
|
|
126
|
+
safeSendUserMessage(text: string): void {
|
|
127
|
+
const log = getLogger();
|
|
128
|
+
const attempt = (retries: number) => {
|
|
129
|
+
try {
|
|
130
|
+
// Route through the controller's single send path. "instruction" maps to
|
|
131
|
+
// followUp: queues the message and triggers a turn once the current one
|
|
132
|
+
// settles (or runs immediately when idle), so it never throws "Agent is
|
|
133
|
+
// already processing" when called mid-tool (e.g. during a transition).
|
|
134
|
+
this.transitionController.send(text, "instruction");
|
|
135
|
+
log.debug({ s: "orchestrator", retries, text: text.slice(0, 200) }, "safeSend sent");
|
|
136
|
+
} catch (err: any) {
|
|
137
|
+
if (retries < 30) {
|
|
138
|
+
log.debug({ s: "orchestrator", retries, err: err?.message }, "safeSend retry");
|
|
139
|
+
setTimeout(() => attempt(retries + 1), 1000);
|
|
140
|
+
} else {
|
|
141
|
+
log.error({ s: "orchestrator", retries, err: err?.message ?? String(err), text: text.slice(0, 200) }, "safeSend failed after max retries");
|
|
142
|
+
this.lastCtx?.ui?.notify?.(
|
|
143
|
+
"pi-pi could not deliver a message to the agent; the task may be stalled. See logs.",
|
|
144
|
+
"error",
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
attempt(0);
|
|
150
|
+
}
|
|
88
151
|
|
|
89
152
|
truncateResult(result: string): string {
|
|
90
153
|
const trimmed = result.trim();
|
|
@@ -96,34 +159,53 @@ export class Orchestrator {
|
|
|
96
159
|
}
|
|
97
160
|
|
|
98
161
|
async switchModel(ctx: ExtensionContext, modelSpec: string, thinking: string): Promise<boolean> {
|
|
162
|
+
const log = getLogger();
|
|
99
163
|
const registry = ctx.modelRegistry;
|
|
100
164
|
const allModels = registry.getAvailable();
|
|
101
165
|
|
|
102
|
-
const
|
|
166
|
+
const requestedSpecs = [resolveModel(modelSpec), modelSpec].filter((value, index, arr) => arr.indexOf(value) === index);
|
|
167
|
+
log.debug({ s: "model", requestedSpecs, thinking, availableCount: allModels.length }, "switchModel");
|
|
103
168
|
let resolved;
|
|
104
|
-
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
169
|
+
for (const spec of requestedSpecs) {
|
|
170
|
+
const slashIdx = spec.indexOf("/");
|
|
171
|
+
if (slashIdx !== -1) {
|
|
172
|
+
const provider = spec.substring(0, slashIdx).trim().toLowerCase();
|
|
173
|
+
const modelId = spec.substring(slashIdx + 1).trim().toLowerCase();
|
|
174
|
+
resolved = allModels.find(
|
|
175
|
+
(m) => m.provider.toLowerCase() === provider && m.id.toLowerCase() === modelId,
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
if (!resolved) {
|
|
179
|
+
const pattern = spec.toLowerCase();
|
|
180
|
+
const matches = allModels.filter(
|
|
181
|
+
(m) => m.id.toLowerCase() === pattern || m.id.toLowerCase().includes(pattern),
|
|
182
|
+
);
|
|
183
|
+
if (matches.length === 1) resolved = matches[0];
|
|
184
|
+
}
|
|
185
|
+
if (resolved) break;
|
|
110
186
|
}
|
|
187
|
+
|
|
111
188
|
if (!resolved) {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
(m) => m.id.toLowerCase() === pattern || m.id.toLowerCase().includes(pattern),
|
|
115
|
-
);
|
|
116
|
-
if (matches.length === 1) resolved = matches[0];
|
|
189
|
+
log.warn({ s: "model", requestedSpecs }, "model not found");
|
|
190
|
+
return false;
|
|
117
191
|
}
|
|
118
192
|
|
|
119
|
-
if (!resolved) return false;
|
|
120
|
-
|
|
121
193
|
const ok = await this.pi.setModel(resolved);
|
|
122
|
-
if (!ok)
|
|
194
|
+
if (!ok) {
|
|
195
|
+
log.warn({ s: "model", resolved: `${resolved.provider}/${resolved.id}` }, "setModel returned false");
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
123
198
|
|
|
124
|
-
const VALID_THINKING = new Set(["off", "low", "medium", "high"]);
|
|
125
|
-
const thinkingLevel = (VALID_THINKING.has(thinking) ? thinking : "high") as
|
|
199
|
+
const VALID_THINKING = new Set(["off", "minimal", "low", "medium", "high", "xhigh"]);
|
|
200
|
+
const thinkingLevel = (VALID_THINKING.has(thinking) ? thinking : "high") as
|
|
201
|
+
| "off"
|
|
202
|
+
| "minimal"
|
|
203
|
+
| "low"
|
|
204
|
+
| "medium"
|
|
205
|
+
| "high"
|
|
206
|
+
| "xhigh";
|
|
126
207
|
this.pi.setThinkingLevel(thinkingLevel);
|
|
208
|
+
log.debug({ s: "model", model: `${resolved.provider}/${resolved.id}`, thinking: thinkingLevel }, "model switched");
|
|
127
209
|
return true;
|
|
128
210
|
}
|
|
129
211
|
|
|
@@ -137,11 +219,13 @@ export class Orchestrator {
|
|
|
137
219
|
const phase = this.active.state.phase;
|
|
138
220
|
const step = this.active.state.step;
|
|
139
221
|
const reviewCycle = this.active.state.reviewCycle;
|
|
222
|
+
const effectiveMode = this.active.state.effectiveMode ?? this.active.state.mode;
|
|
223
|
+
const modeLabel = effectiveMode === "autonomous" ? " [autonomous]" : "";
|
|
140
224
|
|
|
141
|
-
if (type === "debug" || type === "brainstorm") {
|
|
225
|
+
if (type === "debug" || type === "brainstorm" || type === "quick") {
|
|
142
226
|
const elapsed = this.phaseStartTime > 0 ? this.formatElapsed(this.phaseStartTime) : "";
|
|
143
227
|
const suffix = elapsed ? ` (${elapsed})` : "";
|
|
144
|
-
ctx.ui.setStatus("pp-phase", `pp: ${type}${suffix}`);
|
|
228
|
+
ctx.ui.setStatus("pp-phase", `pp: ${type}${modeLabel}${suffix}`);
|
|
145
229
|
return;
|
|
146
230
|
}
|
|
147
231
|
|
|
@@ -162,7 +246,7 @@ export class Orchestrator {
|
|
|
162
246
|
else if (step === "user_gate") detail = "review";
|
|
163
247
|
|
|
164
248
|
if (reviewCycle) {
|
|
165
|
-
const kind = reviewCycle.kind === "plannotator" ? "plannotator" :
|
|
249
|
+
const kind = reviewCycle.kind === "plannotator" ? "plannotator" : "review";
|
|
166
250
|
detail = `${kind} #${reviewCycle.pass}`;
|
|
167
251
|
}
|
|
168
252
|
|
|
@@ -174,7 +258,7 @@ export class Orchestrator {
|
|
|
174
258
|
}
|
|
175
259
|
}
|
|
176
260
|
|
|
177
|
-
ctx.ui.setStatus("pp-phase", `pp: ${parts.join(" → ")}`);
|
|
261
|
+
ctx.ui.setStatus("pp-phase", `pp: ${parts.join(" → ")}${modeLabel}`);
|
|
178
262
|
}
|
|
179
263
|
|
|
180
264
|
private formatElapsed(startTime: number): string {
|
|
@@ -188,14 +272,22 @@ export class Orchestrator {
|
|
|
188
272
|
return remMin > 0 ? `${hr}h ${remMin}m` : `${hr}h`;
|
|
189
273
|
}
|
|
190
274
|
|
|
191
|
-
getPlanStartState(taskDir: string): { step: string; shouldSpawnPlanners: boolean } {
|
|
275
|
+
getPlanStartState(taskDir: string, plannerPresetName?: string): { step: string; shouldSpawnPlanners: boolean } {
|
|
192
276
|
const plansDir = join(taskDir, "plans");
|
|
193
|
-
const
|
|
277
|
+
const presetName = plannerPresetName ?? this.config.agents.subagents.presetGroups.planners.default;
|
|
278
|
+
const plannerVariants = resolvePreset(this.config, "planners", presetName);
|
|
279
|
+
const enabledPlannerVariants = Object.entries(plannerVariants)
|
|
280
|
+
.filter(([, v]) => isEnabled(v))
|
|
281
|
+
.map(([name]) => name);
|
|
194
282
|
const plannerOutputs = existsSync(plansDir)
|
|
195
283
|
? readdirSync(plansDir).filter((f) => f.endsWith(".md") && !f.includes("synthesized") && !f.includes("review_"))
|
|
196
284
|
: [];
|
|
285
|
+
const completedVariants = new Set(
|
|
286
|
+
plannerOutputs.map((f) => f.replace(/^\d+_/, "").replace(/\.md$/, "")),
|
|
287
|
+
);
|
|
288
|
+
const hasAllEnabledVariants = enabledPlannerVariants.every((name) => completedVariants.has(name));
|
|
197
289
|
|
|
198
|
-
if (
|
|
290
|
+
if (enabledPlannerVariants.length === 0 || hasAllEnabledVariants || getLatestSynthesizedPlan(taskDir)) {
|
|
199
291
|
return { step: "synthesize", shouldSpawnPlanners: false };
|
|
200
292
|
}
|
|
201
293
|
|
|
@@ -205,23 +297,26 @@ export class Orchestrator {
|
|
|
205
297
|
getPhasePrompt(_ctx: ExtensionContext): string {
|
|
206
298
|
if (!this.active) return "";
|
|
207
299
|
|
|
300
|
+
const mode: TaskMode = getEffectivePhaseMode(this.active.state);
|
|
301
|
+
|
|
208
302
|
if (this.active.state.reviewCycle?.step === "apply_feedback") {
|
|
209
303
|
const pass = this.active.state.reviewCycle.pass;
|
|
210
|
-
|
|
211
|
-
return reviewCycleSystemPrompt(this.active.dir, pass, manualReview, this.active.state.phase);
|
|
304
|
+
return reviewCycleSystemPrompt(this.active.dir, pass, this.active.state.phase);
|
|
212
305
|
}
|
|
213
306
|
|
|
214
307
|
switch (this.active.state.phase) {
|
|
215
308
|
case "brainstorm":
|
|
216
|
-
return brainstormSystemPrompt(this.active.type, this.active.description, this.active.dir);
|
|
309
|
+
return brainstormSystemPrompt(this.active.type, this.active.description, this.active.dir, this.cwd);
|
|
217
310
|
case "debug":
|
|
218
|
-
return brainstormSystemPrompt(this.active.type, this.active.description, this.active.dir);
|
|
311
|
+
return brainstormSystemPrompt(this.active.type, this.active.description, this.active.dir, this.cwd);
|
|
219
312
|
case "plan":
|
|
220
|
-
return planningSystemPrompt(this.active.dir);
|
|
313
|
+
return planningSystemPrompt(this.active.dir, mode);
|
|
221
314
|
case "implement":
|
|
222
|
-
return implementationSystemPrompt(this.active.dir);
|
|
315
|
+
return implementationSystemPrompt(this.active.dir, this.cwd);
|
|
223
316
|
case "review":
|
|
224
|
-
return reviewTaskSystemPrompt(this.active.dir);
|
|
317
|
+
return reviewTaskSystemPrompt(this.active.dir, this.cwd);
|
|
318
|
+
case "quick":
|
|
319
|
+
return "Work on the user's request directly. There are no phases, planning, or reviews.";
|
|
225
320
|
default:
|
|
226
321
|
return "";
|
|
227
322
|
}
|
|
@@ -244,19 +339,31 @@ export class Orchestrator {
|
|
|
244
339
|
description: string,
|
|
245
340
|
fromTaskDir?: string,
|
|
246
341
|
skipBrainstorm?: boolean,
|
|
342
|
+
mode?: TaskMode,
|
|
247
343
|
): Promise<void> {
|
|
344
|
+
const log = getLogger();
|
|
345
|
+
log.info({ s: "task", type, description, fromTaskDir: fromTaskDir ?? null, skipBrainstorm: skipBrainstorm ?? false, mode: mode ?? null }, "startTask");
|
|
346
|
+
const hadActive = !!this.active;
|
|
248
347
|
if (this.active) {
|
|
249
348
|
ctx.ui.notify(
|
|
250
|
-
`
|
|
349
|
+
`Pausing previous task "${this.active.description}" (phase: ${this.active.state.phase})…`,
|
|
251
350
|
"info",
|
|
252
351
|
);
|
|
253
352
|
this.abortAllSubagents();
|
|
254
|
-
this.active.state.phase = "done";
|
|
255
353
|
saveTask(this.active.dir, this.active.state);
|
|
256
354
|
unregisterAgentDefinitions(this.pi);
|
|
257
355
|
await this.cleanupActive();
|
|
258
356
|
}
|
|
259
357
|
|
|
358
|
+
if (hadActive) {
|
|
359
|
+
// Route new-task compaction through the controller as a "done" target.
|
|
360
|
+
this.lastCtx = ctx;
|
|
361
|
+
await this.transitionController.requestTransition({
|
|
362
|
+
kind: "done",
|
|
363
|
+
summary: `Starting new ${type} task. Previous conversation discarded.`,
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
|
|
260
367
|
try {
|
|
261
368
|
this.config = loadConfig(this.cwd);
|
|
262
369
|
} catch (err: any) {
|
|
@@ -264,16 +371,28 @@ export class Orchestrator {
|
|
|
264
371
|
return;
|
|
265
372
|
}
|
|
266
373
|
|
|
374
|
+
setLogLevel(this.config.general.logLevel);
|
|
267
375
|
ensureGitignore(this.cwd);
|
|
268
376
|
|
|
269
|
-
const dir = createTask(this.cwd, type, description);
|
|
377
|
+
const dir = createTask(this.cwd, type, description, mode);
|
|
270
378
|
const state = loadTask(dir);
|
|
271
379
|
|
|
272
380
|
if (fromTaskDir) {
|
|
273
381
|
const srcUr = join(fromTaskDir, "USER_REQUEST.md");
|
|
274
382
|
const srcRes = join(fromTaskDir, "RESEARCH.md");
|
|
275
383
|
const srcArtifacts = join(fromTaskDir, "artifacts");
|
|
276
|
-
if (existsSync(srcUr))
|
|
384
|
+
if (existsSync(srcUr)) {
|
|
385
|
+
const originalUr = readFileSync(srcUr, "utf-8");
|
|
386
|
+
const implNote =
|
|
387
|
+
"# IMPLEMENTATION TASK\n\n" +
|
|
388
|
+
"This is now an **implement** task — the previous brainstorm/debug/review task is over.\n" +
|
|
389
|
+
"The user request, research, and artifacts below are carried over as context for implementation.\n" +
|
|
390
|
+
"Your job is to plan and implement actual code changes based on this research.\n" +
|
|
391
|
+
"Any prior instructions in the text below saying \"brainstorm only\", \"review only\",\n" +
|
|
392
|
+
"\"do not implement\", \"no code changes\", or similar DO NOT APPLY — they were for the previous task.\n\n" +
|
|
393
|
+
"---\n\n";
|
|
394
|
+
writeFileSync(join(dir, "USER_REQUEST.md"), implNote + originalUr, "utf-8");
|
|
395
|
+
}
|
|
277
396
|
if (existsSync(srcRes)) copyFileSync(srcRes, join(dir, "RESEARCH.md"));
|
|
278
397
|
if (existsSync(srcArtifacts)) {
|
|
279
398
|
const destArtifacts = join(dir, "artifacts");
|
|
@@ -285,19 +404,21 @@ export class Orchestrator {
|
|
|
285
404
|
state.from = relative(join(this.cwd, ".pp", "state"), fromTaskDir);
|
|
286
405
|
if (skipBrainstorm && type === "implement") {
|
|
287
406
|
state.phase = "plan";
|
|
288
|
-
state.
|
|
407
|
+
state.initialPhase = "plan";
|
|
408
|
+
state.activePlannerPreset = this.config.agents.subagents.presetGroups.planners.default;
|
|
409
|
+
state.step = this.getPlanStartState(dir, state.activePlannerPreset).step;
|
|
289
410
|
}
|
|
290
411
|
saveTask(dir, state);
|
|
291
412
|
}
|
|
292
413
|
|
|
293
414
|
let release: (() => Promise<void>) | null = null;
|
|
294
415
|
try {
|
|
295
|
-
release = await lockTask(dir, this.config.
|
|
416
|
+
release = await lockTask(dir, this.config.performance.internals);
|
|
296
417
|
} catch (err: any) {
|
|
297
418
|
try {
|
|
298
419
|
rmSync(dir, { recursive: true, force: true });
|
|
299
420
|
} catch {
|
|
300
|
-
|
|
421
|
+
log.warn({ s: "task", dir }, "failed to clean up orphaned task dir");
|
|
301
422
|
}
|
|
302
423
|
ctx.ui.notify(`Failed to lock task: ${err.message}`, "error");
|
|
303
424
|
return;
|
|
@@ -317,7 +438,10 @@ export class Orchestrator {
|
|
|
317
438
|
description: state.description,
|
|
318
439
|
};
|
|
319
440
|
|
|
320
|
-
|
|
441
|
+
addTaskDestination(dir);
|
|
442
|
+
log.info({ s: "task", dir, taskId: this.active.taskId, phase: state.phase, step: state.step }, "task activated");
|
|
443
|
+
|
|
444
|
+
const modelConfig = this.config.agents.orchestrators[
|
|
321
445
|
type === "debug" ? "debug"
|
|
322
446
|
: type === "brainstorm" ? "brainstorm"
|
|
323
447
|
: type === "review" ? "review"
|
|
@@ -337,30 +461,52 @@ export class Orchestrator {
|
|
|
337
461
|
|
|
338
462
|
this.phaseStartTime = Date.now();
|
|
339
463
|
const isGenericDescription = ["implement", "debug", "brainstorm", "review"].includes(this.active.description);
|
|
464
|
+
const isGenericQuickDescription = this.active.description === "quick";
|
|
340
465
|
const hasInheritedTaskContext = Boolean(fromTaskDir && type === "implement");
|
|
341
466
|
const isWaitingForPlanners = this.active.state.phase === "plan" && this.active.state.step === "await_planners";
|
|
342
|
-
if (isGenericDescription && !hasInheritedTaskContext) {
|
|
467
|
+
if ((isGenericDescription || isGenericQuickDescription) && !hasInheritedTaskContext) {
|
|
343
468
|
ctx.ui.notify("Task created. Describe what you'd like to do.", "info");
|
|
344
469
|
} else if (isWaitingForPlanners) {
|
|
345
470
|
ctx.ui.notify("Entered plan phase. Waiting for planners to complete before synthesis.", "info");
|
|
346
471
|
} else {
|
|
347
|
-
|
|
472
|
+
const desc = this.active.description;
|
|
473
|
+
const descSuffix = !isGenericDescription ? `\n\nTask: ${desc}` : "";
|
|
474
|
+
this.safeSendUserMessage(`[PI-PI] Entered ${this.active.state.phase} phase. Begin working.${descSuffix}`);
|
|
348
475
|
}
|
|
349
476
|
|
|
350
477
|
if (this.active.state.phase === "plan" && this.active.state.step === "await_planners") {
|
|
351
|
-
|
|
478
|
+
const requestedPlannerPresetName = this.active.state.activePlannerPreset ?? this.config.agents.subagents.presetGroups.planners.default;
|
|
479
|
+
const plannerPresetExists = Object.prototype.hasOwnProperty.call(this.config.agents.subagents.presetGroups.planners.presets ?? {}, requestedPlannerPresetName);
|
|
480
|
+
const plannerPresetName = plannerPresetExists
|
|
481
|
+
? requestedPlannerPresetName
|
|
482
|
+
: (Object.keys(this.config.agents.subagents.presetGroups.planners.presets ?? {})[0] ?? requestedPlannerPresetName);
|
|
483
|
+
if (this.active.state.activePlannerPreset !== plannerPresetName) {
|
|
484
|
+
this.active.state.activePlannerPreset = plannerPresetName;
|
|
485
|
+
saveTask(this.active.dir, this.active.state);
|
|
486
|
+
}
|
|
487
|
+
if (!plannerPresetExists && plannerPresetName !== requestedPlannerPresetName) {
|
|
488
|
+
ctx.ui.notify(
|
|
489
|
+
`Planner preset "${requestedPlannerPresetName}" not found. Falling back to "${plannerPresetName}".`,
|
|
490
|
+
"warning",
|
|
491
|
+
);
|
|
492
|
+
}
|
|
493
|
+
const plannerVariants = resolvePreset(this.config, "planners", plannerPresetName);
|
|
494
|
+
this.pendingSubagentSpawns = Object.values(plannerVariants).filter((v) => isEnabled(v)).length;
|
|
352
495
|
this.failedPlannerVariants = [];
|
|
353
|
-
|
|
354
|
-
this
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
this.
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
496
|
+
handleSpawnResult(
|
|
497
|
+
this,
|
|
498
|
+
spawnPlanners(
|
|
499
|
+
this.pi,
|
|
500
|
+
this.cwd,
|
|
501
|
+
this.active.dir,
|
|
502
|
+
this.active.taskId,
|
|
503
|
+
this.config,
|
|
504
|
+
this.transitionController.phaseSend,
|
|
505
|
+
plannerVariants,
|
|
506
|
+
this.active?.state.repos ?? [],
|
|
507
|
+
),
|
|
508
|
+
{ kind: "planner", logScope: "planner", logMessage: "spawnPlanners failed", onSettled: (result) => { if (!result?.spawned) this.checkPlannerCompletion(); } },
|
|
509
|
+
);
|
|
364
510
|
}
|
|
365
511
|
}
|
|
366
512
|
|
|
@@ -383,22 +529,14 @@ export class Orchestrator {
|
|
|
383
529
|
this.pendingSubagentSpawns = 0;
|
|
384
530
|
this.errorRetryCount = 0;
|
|
385
531
|
this.commitReminderSent = false;
|
|
386
|
-
this.
|
|
387
|
-
this.cooldownHits = [];
|
|
532
|
+
this.consecutiveNudges = 0;
|
|
388
533
|
this.nudgeHalted = false;
|
|
389
|
-
this.phaseCompactionPending = false;
|
|
390
|
-
this.phaseCompactionResolve = null;
|
|
391
534
|
this.phaseStartTime = 0;
|
|
392
535
|
this.userGatePending = false;
|
|
393
|
-
this.reviewTransitionToken = -1;
|
|
394
536
|
this.failedPlannerVariants = [];
|
|
395
537
|
this.failedReviewerVariants = [];
|
|
396
538
|
this.plannerFailureDialogPending = false;
|
|
397
539
|
this.reviewerFailureDialogPending = false;
|
|
398
|
-
if (this.awaitPollTimer) {
|
|
399
|
-
clearInterval(this.awaitPollTimer);
|
|
400
|
-
this.awaitPollTimer = null;
|
|
401
|
-
}
|
|
402
540
|
if (this.pendingRetryTimer) {
|
|
403
541
|
clearTimeout(this.pendingRetryTimer);
|
|
404
542
|
this.pendingRetryTimer = null;
|
|
@@ -411,101 +549,127 @@ export class Orchestrator {
|
|
|
411
549
|
|
|
412
550
|
async cleanupActive(): Promise<void> {
|
|
413
551
|
if (!this.active) return;
|
|
552
|
+
const dir = this.active.dir;
|
|
553
|
+
getLogger().info({ s: "task", dir }, "cleaning up active task");
|
|
554
|
+
removeTaskDestination();
|
|
414
555
|
this.resetTaskScopedState();
|
|
415
556
|
if (this.active.release) {
|
|
416
557
|
try {
|
|
417
558
|
await this.active.release();
|
|
418
559
|
} catch (err: any) {
|
|
419
|
-
|
|
560
|
+
getLogger().error({ s: "task", dir, err: err.message }, "failed to release lock");
|
|
420
561
|
}
|
|
421
562
|
}
|
|
422
563
|
this.active = null;
|
|
423
564
|
}
|
|
424
565
|
|
|
425
566
|
registerAgents(): void {
|
|
567
|
+
const log = getLogger();
|
|
426
568
|
const explore = createExploreAgent(this.config);
|
|
427
569
|
const librarian = createLibrarianAgent(this.config);
|
|
428
570
|
const taskAgent = createTaskAgent(this.config, "{{subtask}}", { userRequest: "", synthesizedPlan: "" });
|
|
429
|
-
|
|
430
|
-
const
|
|
431
|
-
|
|
432
|
-
|
|
571
|
+
const phase = this.active?.state.phase;
|
|
572
|
+
const repos = this.active?.state.repos ?? [];
|
|
573
|
+
log.debug({ s: "agents", phase, repoCount: repos.length }, "registering agent definitions");
|
|
574
|
+
const contextDirs = getContextDirs(this.cwd, repos, this.config.general.loadExtraRepoConfigs);
|
|
575
|
+
const repoContext = buildRepoContext(repos);
|
|
576
|
+
|
|
577
|
+
const appendContext = (agentType: string, prompt: string, modelInfo: { vendor: string; family: string; tier: string }): string => {
|
|
578
|
+
const contextFiles = loadAllContextFiles(contextDirs, agentType as any, "system", phase, modelInfo);
|
|
579
|
+
if (contextFiles.length === 0 && !repoContext) return prompt;
|
|
580
|
+
const parts = [prompt];
|
|
581
|
+
if (repoContext) parts.push(repoContext.trimEnd());
|
|
582
|
+
if (contextFiles.length === 0) return parts.join("\n\n");
|
|
433
583
|
const contextBlock = contextFiles.map((f) => f.content).join("\n\n");
|
|
434
|
-
|
|
584
|
+
parts.push("# Project Context\n\n" + contextBlock);
|
|
585
|
+
return parts.join("\n\n");
|
|
435
586
|
};
|
|
436
587
|
|
|
437
588
|
registerAgentDefinitions(this.pi, [
|
|
438
|
-
{
|
|
439
|
-
|
|
440
|
-
|
|
589
|
+
{
|
|
590
|
+
type: "explore",
|
|
591
|
+
variant: null,
|
|
592
|
+
...explore,
|
|
593
|
+
prompt: appendContext("explore", explore.prompt, getModelInfo(resolveModel(this.config.agents.subagents.simple.explore.model))),
|
|
594
|
+
},
|
|
595
|
+
{
|
|
596
|
+
type: "librarian",
|
|
597
|
+
variant: null,
|
|
598
|
+
...librarian,
|
|
599
|
+
prompt: appendContext("librarian", librarian.prompt, getModelInfo(resolveModel(this.config.agents.subagents.simple.librarian.model))),
|
|
600
|
+
},
|
|
601
|
+
{
|
|
602
|
+
type: "task",
|
|
603
|
+
variant: null,
|
|
604
|
+
...taskAgent,
|
|
605
|
+
prompt: appendContext("task", taskAgent.prompt, getModelInfo(resolveModel(this.config.agents.subagents.simple.task.model))),
|
|
606
|
+
},
|
|
441
607
|
]);
|
|
442
608
|
}
|
|
443
609
|
|
|
444
610
|
injectContextAndArtifacts(taskDir: string, phase: Phase): void {
|
|
445
|
-
const
|
|
611
|
+
const log = getLogger();
|
|
612
|
+
log.debug({ s: "context", taskDir, phase }, "injecting context and artifacts");
|
|
613
|
+
const modelSpec =
|
|
614
|
+
phase === "debug" && this.active?.type === "debug"
|
|
615
|
+
? this.config.agents.orchestrators.debug.model
|
|
616
|
+
: phase === "brainstorm" && this.active?.type === "brainstorm"
|
|
617
|
+
? this.config.agents.orchestrators.brainstorm.model
|
|
618
|
+
: phase === "review" && this.active?.type === "review"
|
|
619
|
+
? this.config.agents.orchestrators.review.model
|
|
620
|
+
: phase === "plan"
|
|
621
|
+
? this.config.agents.orchestrators.plan.model
|
|
622
|
+
: this.config.agents.orchestrators.implement.model;
|
|
623
|
+
const activeModelSpec = this.lastCtx?.model
|
|
624
|
+
? `${this.lastCtx.model.provider}/${this.lastCtx.model.id}`
|
|
625
|
+
: modelSpec;
|
|
626
|
+
const repos = this.active?.state.repos ?? [];
|
|
627
|
+
const contextDirs = getContextDirs(this.cwd, repos, this.config.general.loadExtraRepoConfigs);
|
|
628
|
+
const contextFiles = loadAllContextFiles(
|
|
629
|
+
contextDirs,
|
|
630
|
+
"main",
|
|
631
|
+
"context",
|
|
632
|
+
phase,
|
|
633
|
+
getModelInfo(activeModelSpec),
|
|
634
|
+
);
|
|
446
635
|
for (const cf of contextFiles) {
|
|
447
|
-
this.
|
|
636
|
+
this.transitionController.sendCustom(
|
|
448
637
|
{ customType: "pp-context", content: cf.content, display: false },
|
|
449
|
-
|
|
638
|
+
"context",
|
|
450
639
|
);
|
|
451
640
|
}
|
|
452
641
|
const artifacts = getPhaseArtifacts(taskDir, phase);
|
|
453
642
|
for (const artifact of artifacts) {
|
|
454
|
-
this.
|
|
643
|
+
this.transitionController.sendCustom(
|
|
455
644
|
{ customType: "pp-artifact", content: `=== ${artifact.name} ===\n${artifact.content}`, display: false },
|
|
456
|
-
|
|
645
|
+
"context",
|
|
457
646
|
);
|
|
458
647
|
}
|
|
459
648
|
}
|
|
460
649
|
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
onComplete: () => {
|
|
473
|
-
this.phaseCompactionPending = false;
|
|
474
|
-
if (this.phaseCompactionResolve) {
|
|
475
|
-
this.phaseCompactionResolve();
|
|
476
|
-
this.phaseCompactionResolve = null;
|
|
477
|
-
}
|
|
478
|
-
this.phaseStartTime = Date.now();
|
|
479
|
-
if (this.active && (phase === "plan" || phase === "implement")) {
|
|
480
|
-
const modelConfig = this.config.mainModel.implement;
|
|
481
|
-
this.switchModel(ctx, modelConfig.model, modelConfig.thinking).catch(() => {});
|
|
482
|
-
}
|
|
483
|
-
this.injectContextAndArtifacts(taskDir, phase);
|
|
484
|
-
if (this.active?.state.phase === "plan" && this.active.state.step === "await_planners") {
|
|
485
|
-
ctx.ui.notify("Entered plan phase. Waiting for planners to complete before synthesis.", "info");
|
|
486
|
-
} else {
|
|
487
|
-
this.pi.sendUserMessage(`[PI-PI] Entered ${phase} phase. Begin working.`, { deliverAs: "followUp" });
|
|
488
|
-
}
|
|
489
|
-
},
|
|
490
|
-
onError: (err) => {
|
|
491
|
-
console.error(`[pi-pi] Phase compaction failed: ${err.message}`);
|
|
492
|
-
this.phaseCompactionPending = false;
|
|
493
|
-
if (this.phaseCompactionResolve) {
|
|
494
|
-
this.phaseCompactionResolve();
|
|
495
|
-
this.phaseCompactionResolve = null;
|
|
496
|
-
}
|
|
650
|
+
compactAndTransition(ctx: ExtensionContext, taskDir: string, phase: Phase, onReady?: () => void, summary?: string): void {
|
|
651
|
+
getLogger().info({ s: "phase", taskDir, phase }, "compact and transition");
|
|
652
|
+
// Ensure the controller's host can reach this live ctx for compact/isIdle.
|
|
653
|
+
this.lastCtx = ctx;
|
|
654
|
+
// Notify-only case: entering plan and immediately awaiting planners. No
|
|
655
|
+
// "Begin working" instruction is sent — the agent waits for onSubagentsDone.
|
|
656
|
+
const notifyOnly = this.active?.state.phase === "plan" && this.active.state.step === "await_planners";
|
|
657
|
+
void this.transitionController.requestTransition({
|
|
658
|
+
kind: "phase",
|
|
659
|
+
summary: summary || "Phase transition — previous phase completed.",
|
|
660
|
+
onResume: async () => {
|
|
497
661
|
this.phaseStartTime = Date.now();
|
|
498
662
|
if (this.active && (phase === "plan" || phase === "implement")) {
|
|
499
|
-
const modelConfig = this.config.
|
|
500
|
-
this.switchModel(ctx, modelConfig.model, modelConfig.thinking)
|
|
663
|
+
const modelConfig = phase === "plan" ? this.config.agents.orchestrators.plan : this.config.agents.orchestrators.implement;
|
|
664
|
+
await this.switchModel(ctx, modelConfig.model, modelConfig.thinking);
|
|
501
665
|
}
|
|
502
666
|
this.injectContextAndArtifacts(taskDir, phase);
|
|
503
|
-
|
|
667
|
+
onReady?.();
|
|
668
|
+
if (notifyOnly) {
|
|
504
669
|
ctx.ui.notify("Entered plan phase. Waiting for planners to complete before synthesis.", "info");
|
|
505
|
-
} else {
|
|
506
|
-
this.pi.sendUserMessage(`[PI-PI] Entered ${phase} phase. Begin working.`, { deliverAs: "followUp" });
|
|
507
670
|
}
|
|
508
671
|
},
|
|
672
|
+
instruction: notifyOnly ? undefined : `[PI-PI] Entered ${phase} phase. Begin working.`,
|
|
509
673
|
});
|
|
510
674
|
}
|
|
511
675
|
|
|
@@ -521,23 +685,6 @@ export class Orchestrator {
|
|
|
521
685
|
}
|
|
522
686
|
}
|
|
523
687
|
|
|
524
|
-
export function deepReviewConfig(config: PiPiConfig): PiPiConfig {
|
|
525
|
-
const THINKING_UPGRADE: Record<string, string> = { low: "medium", medium: "high", high: "xhigh" };
|
|
526
|
-
const upgrade = (reviewers: Record<string, VariantConfig>) => {
|
|
527
|
-
const upgraded: Record<string, VariantConfig> = {};
|
|
528
|
-
for (const [name, variant] of Object.entries(reviewers)) {
|
|
529
|
-
upgraded[name] = { ...variant, thinking: THINKING_UPGRADE[variant.thinking] ?? "high" };
|
|
530
|
-
}
|
|
531
|
-
return upgraded;
|
|
532
|
-
};
|
|
533
|
-
return {
|
|
534
|
-
...config,
|
|
535
|
-
codeReviewers: upgrade(config.codeReviewers),
|
|
536
|
-
planReviewers: upgrade(config.planReviewers),
|
|
537
|
-
brainstormReviewers: upgrade(config.brainstormReviewers),
|
|
538
|
-
};
|
|
539
|
-
}
|
|
540
|
-
|
|
541
688
|
export function ensureGitignore(cwd: string): void {
|
|
542
689
|
const ppDir = join(cwd, ".pp");
|
|
543
690
|
if (!existsSync(ppDir)) {
|
|
@@ -545,7 +692,7 @@ export function ensureGitignore(cwd: string): void {
|
|
|
545
692
|
}
|
|
546
693
|
|
|
547
694
|
const gitignorePath = join(ppDir, ".gitignore");
|
|
548
|
-
const requiredEntries = ["state/", "config.json"];
|
|
695
|
+
const requiredEntries = ["state/", "config.json", "logs/"];
|
|
549
696
|
|
|
550
697
|
if (!existsSync(gitignorePath)) {
|
|
551
698
|
writeFileSync(gitignorePath, requiredEntries.join("\n") + "\n", "utf-8");
|