@ilya-lesikov/pi-pi 0.6.0 → 0.8.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 +1 -1
- package/3p/pi-plannotator/apps/pi-extension/README.md +6 -5
- package/3p/pi-plannotator/apps/pi-extension/plannotator-browser.ts +25 -10
- package/3p/pi-plannotator/apps/pi-extension/plannotator-events.code-review.test.ts +172 -0
- package/3p/pi-plannotator/apps/pi-extension/plannotator-events.ts +50 -12
- package/3p/pi-plannotator/apps/pi-extension/server/project.test.ts +45 -0
- package/3p/pi-plannotator/apps/pi-extension/server/project.ts +7 -6
- package/3p/pi-plannotator/apps/pi-extension/server/serverReview.ts +41 -25
- package/3p/pi-subagents/src/agent-manager.ts +34 -1
- package/3p/pi-subagents/src/agent-runner.ts +66 -33
- package/3p/pi-subagents/src/index.ts +3 -38
- package/3p/pi-subagents/src/types.ts +4 -0
- package/extensions/orchestrator/agents/advisor.ts +35 -0
- package/extensions/orchestrator/agents/brainstorm-reviewer.ts +7 -7
- package/extensions/orchestrator/agents/code-reviewer.ts +7 -7
- package/extensions/orchestrator/agents/constraints.test.ts +44 -0
- package/extensions/orchestrator/agents/constraints.ts +3 -0
- package/extensions/orchestrator/agents/deep-debugger.ts +35 -0
- package/extensions/orchestrator/agents/plan-reviewer.ts +7 -7
- package/extensions/orchestrator/agents/planner.ts +9 -8
- package/extensions/orchestrator/agents/prompts.test.ts +120 -0
- package/extensions/orchestrator/agents/reviewer.ts +48 -0
- package/extensions/orchestrator/agents/task.ts +6 -20
- package/extensions/orchestrator/agents/tool-routing.ts +23 -1
- package/extensions/orchestrator/command-handlers.ts +1 -1
- package/extensions/orchestrator/config.ts +8 -4
- package/extensions/orchestrator/context.test.ts +54 -0
- package/extensions/orchestrator/context.ts +65 -2
- package/extensions/orchestrator/custom-footer.ts +5 -2
- package/extensions/orchestrator/doctor.test.ts +3 -1
- package/extensions/orchestrator/doctor.ts +40 -2
- package/extensions/orchestrator/event-handlers.test.ts +97 -1
- package/extensions/orchestrator/event-handlers.ts +222 -48
- package/extensions/orchestrator/flant-infra.test.ts +312 -0
- package/extensions/orchestrator/flant-infra.ts +407 -44
- package/extensions/orchestrator/index.ts +1 -1
- package/extensions/orchestrator/integration.test.ts +312 -18
- package/extensions/orchestrator/messages.test.ts +30 -0
- package/extensions/orchestrator/messages.ts +6 -0
- package/extensions/orchestrator/model-registry.test.ts +124 -13
- package/extensions/orchestrator/model-registry.ts +91 -33
- package/extensions/orchestrator/orchestrator.test.ts +113 -0
- package/extensions/orchestrator/orchestrator.ts +163 -3
- package/extensions/orchestrator/phases/brainstorm.ts +9 -20
- package/extensions/orchestrator/phases/implementation.test.ts +11 -0
- package/extensions/orchestrator/phases/implementation.ts +4 -6
- package/extensions/orchestrator/phases/planning.test.ts +16 -0
- package/extensions/orchestrator/phases/planning.ts +11 -4
- package/extensions/orchestrator/phases/review-task.ts +1 -4
- package/extensions/orchestrator/phases/review.test.ts +62 -0
- package/extensions/orchestrator/phases/review.ts +58 -15
- package/extensions/orchestrator/plannotator.ts +9 -6
- package/extensions/orchestrator/pp-menu.test.ts +74 -1
- package/extensions/orchestrator/pp-menu.ts +366 -94
- package/extensions/orchestrator/pp-state-tools.test.ts +192 -0
- package/extensions/orchestrator/pp-state-tools.ts +249 -0
- package/extensions/orchestrator/rate-limit-fallback-flow.test.ts +128 -0
- package/extensions/orchestrator/rate-limit-fallback.test.ts +98 -0
- package/extensions/orchestrator/rate-limit-fallback.ts +243 -0
- package/extensions/orchestrator/state.ts +41 -20
- package/extensions/orchestrator/test-helpers.ts +18 -3
- package/extensions/orchestrator/usage-tracker.test.ts +131 -3
- package/extensions/orchestrator/usage-tracker.ts +78 -11
- package/extensions/orchestrator/validate-artifacts.test.ts +20 -0
- package/extensions/orchestrator/validate-artifacts.ts +2 -2
- package/package.json +1 -1
|
@@ -14,17 +14,20 @@ import {
|
|
|
14
14
|
loadAllContextFiles,
|
|
15
15
|
getPhaseArtifacts,
|
|
16
16
|
getLatestSynthesizedPlan,
|
|
17
|
+
getArtifactManifest,
|
|
17
18
|
loadBrainstormReviewOutputs,
|
|
18
19
|
loadCodeReviewOutputs,
|
|
19
20
|
loadPlanReviewOutputs,
|
|
20
21
|
} from "./context.js";
|
|
21
|
-
import { PRINCIPLES_BLOCK, TOOLS_BLOCK } from "./agents/tool-routing.js";
|
|
22
|
+
import { PRINCIPLES_BLOCK, TOOLS_BLOCK, DELEGATION_BLOCK } from "./agents/tool-routing.js";
|
|
22
23
|
import { constraintsBlock, phaseConstraint } from "./agents/constraints.js";
|
|
23
24
|
import { registerCbmTools } from "./cbm.js";
|
|
24
25
|
import { registerExaTools } from "./exa.js";
|
|
25
26
|
import { registerAstSearchTool } from "./ast-search.js";
|
|
26
27
|
import { SUBAGENT_SESSION_KEY } from "./index.js";
|
|
27
28
|
import { registerCommandHandlers } from "./command-handlers.js";
|
|
29
|
+
import { registerStateFileTools } from "./pp-state-tools.js";
|
|
30
|
+
import { handleMainRateLimit, handleSubagentRateLimit, isRateLimitError, isSdkRetryableError } from "./rate-limit-fallback.js";
|
|
28
31
|
import { setExtensionOnlyMode, unregisterAgentDefinitions } from "./agents/registry.js";
|
|
29
32
|
import { resolveModel, getModelInfo, updateRegistryFromAvailableModels } from "./model-registry.js";
|
|
30
33
|
import { spawnPlanners, spawnPlanReviewers } from "./phases/planning.js";
|
|
@@ -33,9 +36,10 @@ import { spawnBrainstormReviewers } from "./phases/brainstorm.js";
|
|
|
33
36
|
import { reviewPassUnanimousApprove } from "./phases/verdict.js";
|
|
34
37
|
import { validateExitCriteria } from "./phases/machine.js";
|
|
35
38
|
import { openPlannotator, waitForPlannotatorResult, cancelPendingPlannotatorWait } from "./plannotator.js";
|
|
39
|
+
import { advanceBanner } from "./messages.js";
|
|
36
40
|
import { Orchestrator, type ActiveTask } from "./orchestrator.js";
|
|
37
41
|
import { createCustomFooter, setFooterContext, setFooterTracker } from "./custom-footer.js";
|
|
38
|
-
import { createUsageTracker, dumpUsageSummary, loadUsageSummary, type UsageTracker } from "./usage-tracker.js";
|
|
42
|
+
import { createUsageTracker, dumpUsageSummary, loadUsageSummary, isSubscriptionRouted, type UsageTracker } from "./usage-tracker.js";
|
|
39
43
|
import { askUser, isCancel } from "../../3p/pi-ask-user/index.js";
|
|
40
44
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
41
45
|
import { findRootRepo, normalizeRepoPath, resolveRepoForFile, type RepoInfo } from "./repo-utils.js";
|
|
@@ -51,6 +55,34 @@ function isPathInside(basePath: string, targetPath: string): boolean {
|
|
|
51
55
|
return rel === "" || (!rel.startsWith(`..${sep}`) && rel !== ".." && !isAbsolute(rel));
|
|
52
56
|
}
|
|
53
57
|
|
|
58
|
+
// Builds the spawn-time context block appended to a free agent's spawn prompt:
|
|
59
|
+
// inlines USER_REQUEST + RESEARCH (always present by spawn time) and appends a
|
|
60
|
+
// path-aware manifest of additional on-demand documents (artifacts + plan).
|
|
61
|
+
function buildSpawnContextBlock(taskDir: string): string {
|
|
62
|
+
const parts: string[] = [];
|
|
63
|
+
|
|
64
|
+
const userRequestPath = join(taskDir, "USER_REQUEST.md");
|
|
65
|
+
if (existsSync(userRequestPath)) {
|
|
66
|
+
parts.push("=== USER REQUEST ===\n" + readFileSync(userRequestPath, "utf-8").trimEnd());
|
|
67
|
+
}
|
|
68
|
+
const researchPath = join(taskDir, "RESEARCH.md");
|
|
69
|
+
if (existsSync(researchPath)) {
|
|
70
|
+
parts.push("=== RESEARCH ===\n" + readFileSync(researchPath, "utf-8").trimEnd());
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const manifest = getArtifactManifest(taskDir);
|
|
74
|
+
if (manifest.length > 0) {
|
|
75
|
+
const lines = manifest.map((m) => `- ${m.path} — ${m.title}`);
|
|
76
|
+
parts.push(
|
|
77
|
+
"=== ADDITIONAL DOCUMENTS (read from disk if relevant) ===\n" +
|
|
78
|
+
lines.join("\n") +
|
|
79
|
+
"\nDo NOT re-read USER_REQUEST/RESEARCH from disk (already above).",
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return parts.join("\n\n");
|
|
84
|
+
}
|
|
85
|
+
|
|
54
86
|
export async function detectDefaultBranch(orchestrator: Orchestrator, repos: RepoInfo[], repoPath: string): Promise<string> {
|
|
55
87
|
const normalizedPath = normalizeRepoPath(repoPath);
|
|
56
88
|
const repo = repos.find((r) => r.path === normalizedPath);
|
|
@@ -206,14 +238,21 @@ function tryCompleteReviewCycle(orchestrator: Orchestrator, spawnedReviewers?: n
|
|
|
206
238
|
},
|
|
207
239
|
"instruction",
|
|
208
240
|
);
|
|
209
|
-
orchestrator.safeSendUserMessage(reviewReadyMessage(phase));
|
|
241
|
+
orchestrator.safeSendUserMessage(advanceBanner(reviewReadyMessage(phase, getEffectivePhaseMode(orchestrator.active.state))));
|
|
210
242
|
}
|
|
211
243
|
|
|
212
|
-
function reviewReadyMessage(phase: string): string {
|
|
244
|
+
function reviewReadyMessage(phase: string, mode: TaskMode): string {
|
|
213
245
|
if (phase === "brainstorm") {
|
|
214
246
|
return "[PI-PI] Review cycle is ready for apply_feedback. The reviewers assessed your artifacts (USER_REQUEST.md, RESEARCH.md, and artifacts/), not a code diff. Read their outputs and update those artifacts as needed.";
|
|
215
247
|
}
|
|
216
|
-
|
|
248
|
+
// Only autonomous plan/implement auto-advance: those must re-call
|
|
249
|
+
// pp_phase_complete to finalize the pass and transition. Guided phases
|
|
250
|
+
// (including debug and the interactive review phase) stay user-driven, so
|
|
251
|
+
// they get neutral wording with no re-call/auto-advance directive.
|
|
252
|
+
if (mode === "autonomous") {
|
|
253
|
+
return "[PI-PI] Review cycle is ready for apply_feedback. Read the reviewer outputs, apply any required changes, then call pp_phase_complete again to finalize this review pass and advance the phase. Do NOT stop or wait for the user — the phase is NOT complete until you re-call pp_phase_complete.";
|
|
254
|
+
}
|
|
255
|
+
return "[PI-PI] Review cycle is ready for apply_feedback. Read the reviewer outputs and apply any required changes.";
|
|
217
256
|
}
|
|
218
257
|
|
|
219
258
|
export async function enterReviewCycle(
|
|
@@ -414,6 +453,7 @@ function registerOrchestratorTools(orchestrator: Orchestrator): void {
|
|
|
414
453
|
registerPhaseCompleteTool(orchestrator);
|
|
415
454
|
registerCommitTool(orchestrator);
|
|
416
455
|
registerSpecifyReviewsTool(orchestrator);
|
|
456
|
+
registerStateFileTools(orchestrator);
|
|
417
457
|
}
|
|
418
458
|
|
|
419
459
|
function registerRepoTool(orchestrator: Orchestrator): void {
|
|
@@ -555,24 +595,24 @@ function registerRepoTool(orchestrator: Orchestrator): void {
|
|
|
555
595
|
});
|
|
556
596
|
}
|
|
557
597
|
|
|
558
|
-
function openCodeReviewDirect(
|
|
559
|
-
|
|
598
|
+
async function openCodeReviewDirect(
|
|
599
|
+
orchestrator: Orchestrator,
|
|
560
600
|
payload: Record<string, unknown>,
|
|
561
601
|
): Promise<{ approved: boolean; feedback?: string } | { error: string }> {
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
}
|
|
602
|
+
const { opened, reviewId } = await openPlannotator(orchestrator.pi, "code-review", payload);
|
|
603
|
+
if (!opened) {
|
|
604
|
+
return { error: "Plannotator not available" };
|
|
605
|
+
}
|
|
606
|
+
let result: { approved: boolean; feedback?: string; error?: string };
|
|
607
|
+
try {
|
|
608
|
+
result = await waitForPlannotatorResult(orchestrator, reviewId, null);
|
|
609
|
+
} catch (err) {
|
|
610
|
+
return { error: err instanceof Error ? err.message : "Plannotator review failed" };
|
|
611
|
+
}
|
|
612
|
+
if (result.error) {
|
|
613
|
+
return { error: result.error };
|
|
614
|
+
}
|
|
615
|
+
return { approved: result.approved, feedback: result.feedback };
|
|
576
616
|
}
|
|
577
617
|
|
|
578
618
|
function registerSpecifyReviewsTool(orchestrator: Orchestrator): void {
|
|
@@ -664,7 +704,7 @@ function registerSpecifyReviewsTool(orchestrator: Orchestrator): void {
|
|
|
664
704
|
continue;
|
|
665
705
|
}
|
|
666
706
|
|
|
667
|
-
const result = await openCodeReviewDirect(
|
|
707
|
+
const result = await openCodeReviewDirect(orchestrator, {
|
|
668
708
|
cwd: reviewCwd,
|
|
669
709
|
diffType: "branch",
|
|
670
710
|
defaultBranch: compareBase,
|
|
@@ -713,8 +753,14 @@ function registerSpecifyReviewsTool(orchestrator: Orchestrator): void {
|
|
|
713
753
|
|
|
714
754
|
ctx.ui?.setWorkingMessage?.("Waiting for user input…");
|
|
715
755
|
try {
|
|
716
|
-
const { showActiveTaskMenu } = await import("./pp-menu.js");
|
|
756
|
+
const { showActiveTaskMenu, USER_CANCELLED } = await import("./pp-menu.js");
|
|
717
757
|
const text = await showActiveTaskMenu(orchestrator, ctx, `Plannotator review complete.\n\n${summary}`, "tool");
|
|
758
|
+
// Deliberate user ESC: stop the turn cleanly (mirror ask_user), no
|
|
759
|
+
// reminder text that would start a new LLM turn.
|
|
760
|
+
if (text === USER_CANCELLED) {
|
|
761
|
+
ctx.abort?.();
|
|
762
|
+
return { content: [{ type: "text" as const, text: "" }], details: {} };
|
|
763
|
+
}
|
|
718
764
|
// A transition may have started while the menu was open. The controller
|
|
719
765
|
// is the source of truth; abort the agent's pending turn so it doesn't
|
|
720
766
|
// race the transition. (Interactive-UX abort — stays local, not routed.)
|
|
@@ -840,7 +886,10 @@ function registerPhaseCompleteTool(orchestrator: Orchestrator): void {
|
|
|
840
886
|
const count = orchestrator.spawnedAgentIds.size + orchestrator.pendingSubagentSpawns;
|
|
841
887
|
return { content: [{ type: "text" as const, text: `${count} subagent(s) still running. Wait for them to complete before calling pp_phase_complete.` }], isError: true as const, details: {} };
|
|
842
888
|
}
|
|
843
|
-
|
|
889
|
+
// Gate on the effective PHASE mode, not the task mode: brainstorm/debug/
|
|
890
|
+
// review are always user-driven even for an autonomous task, so they must
|
|
891
|
+
// fall through to the guided menu path below rather than auto-advancing.
|
|
892
|
+
const effectiveMode = getEffectivePhaseMode(orchestrator.active.state);
|
|
844
893
|
if (effectiveMode === "autonomous") {
|
|
845
894
|
const phase = orchestrator.active.state.phase;
|
|
846
895
|
let justFinalizedReviewCycle = false;
|
|
@@ -908,8 +957,16 @@ function registerPhaseCompleteTool(orchestrator: Orchestrator): void {
|
|
|
908
957
|
}
|
|
909
958
|
ctx.ui.setWorkingMessage?.("Waiting for user approval…");
|
|
910
959
|
try {
|
|
911
|
-
const { showActiveTaskMenu } = await import("./pp-menu.js");
|
|
960
|
+
const { showActiveTaskMenu, USER_CANCELLED } = await import("./pp-menu.js");
|
|
912
961
|
const text = await showActiveTaskMenu(orchestrator, ctx, params.summary, "tool");
|
|
962
|
+
// A deliberate user ESC on the menu means "stop cleanly, let me type" —
|
|
963
|
+
// mirror ask_user: abort the turn and return nothing so no new LLM turn
|
|
964
|
+
// starts. This takes precedence over the reminder path below, in ALL
|
|
965
|
+
// interactive cases (including while a transition is mid-flight).
|
|
966
|
+
if (text === USER_CANCELLED) {
|
|
967
|
+
ctx.abort?.();
|
|
968
|
+
return { content: [{ type: "text" as const, text: "" }], details: {} };
|
|
969
|
+
}
|
|
913
970
|
// A transition or await may have started while the menu was open. The
|
|
914
971
|
// controller is the source of truth; abort the pending turn so it can't
|
|
915
972
|
// race. (Interactive-UX abort — stays local, not routed.)
|
|
@@ -923,6 +980,8 @@ function registerPhaseCompleteTool(orchestrator: Orchestrator): void {
|
|
|
923
980
|
return { content: [{ type: "text" as const, text: "" }], details: {} };
|
|
924
981
|
}
|
|
925
982
|
if (!text) {
|
|
983
|
+
// Non-ESC dismissal (explicit "Back") while a transition is running:
|
|
984
|
+
// keep the intentional artifact-update reminder (commit 10e7021).
|
|
926
985
|
return { content: [{ type: "text" as const, text: "User dismissed the menu. Wait for the user's next message. When you resume work, update USER_REQUEST.md and RESEARCH.md with any new findings before calling pp_phase_complete." }], details: {} };
|
|
927
986
|
}
|
|
928
987
|
return { content: [{ type: "text" as const, text }], details: {} };
|
|
@@ -993,6 +1052,21 @@ export function registerEventHandlers(orchestrator: Orchestrator): void {
|
|
|
993
1052
|
|
|
994
1053
|
registerMainTraceHooks(orchestrator);
|
|
995
1054
|
|
|
1055
|
+
// Personal-subscription Claude routing registers the sub provider with a
|
|
1056
|
+
// literal OAuth token (a static snapshot). That token expires within a few
|
|
1057
|
+
// hours, so a long-lived session would keep sending a stale token and the
|
|
1058
|
+
// gateway would return 401. Refresh (and re-register on change) before each
|
|
1059
|
+
// turn's LLM call. Cheap no-op when subscription routing is inactive or the
|
|
1060
|
+
// token is unchanged. registerEventHandlers runs only on the root session.
|
|
1061
|
+
pi.on("turn_start", async () => {
|
|
1062
|
+
try {
|
|
1063
|
+
const { refreshSubProvider } = await import("./flant-infra.js");
|
|
1064
|
+
await refreshSubProvider(pi);
|
|
1065
|
+
} catch (err: any) {
|
|
1066
|
+
getLogger().debug({ s: "flant", err: err?.message }, "sub provider refresh failed");
|
|
1067
|
+
}
|
|
1068
|
+
});
|
|
1069
|
+
|
|
996
1070
|
// TransitionController drivers. These are the reliable idle/completion signals:
|
|
997
1071
|
// - agent_end: the main loop went idle. If a transition is pending, this is
|
|
998
1072
|
// when the controller fires compaction.
|
|
@@ -1127,6 +1201,7 @@ export function registerEventHandlers(orchestrator: Orchestrator): void {
|
|
|
1127
1201
|
if (data.description) {
|
|
1128
1202
|
orchestrator.agentDescriptions.set(data.id, data.description);
|
|
1129
1203
|
}
|
|
1204
|
+
|
|
1130
1205
|
trackSubagentEvent(data, "created");
|
|
1131
1206
|
startStaleAgentWatchdog();
|
|
1132
1207
|
const mgr = (globalThis as any)[Symbol.for("pi-subagents:manager")];
|
|
@@ -1173,7 +1248,14 @@ export function registerEventHandlers(orchestrator: Orchestrator): void {
|
|
|
1173
1248
|
|
|
1174
1249
|
const failedPlannerVariants = [...orchestrator.failedPlannerVariants];
|
|
1175
1250
|
const effectiveMode = getEffectiveMode(orchestrator.active.state);
|
|
1176
|
-
|
|
1251
|
+
// Do NOT auto-retry failed variants while a subscription-429 fallback decision
|
|
1252
|
+
// is in flight: re-spawning now would use the still-sub-routed model and
|
|
1253
|
+
// re-hit the limit. Once the user decides, the fallback nudge re-drives this.
|
|
1254
|
+
if (
|
|
1255
|
+
effectiveMode === "autonomous" &&
|
|
1256
|
+
failedPlannerVariants.length > 0 &&
|
|
1257
|
+
!orchestrator.subFallbackPendingDecision
|
|
1258
|
+
) {
|
|
1177
1259
|
const alreadyRetried = orchestrator.active.state.plannerFailureAutoRetried === true;
|
|
1178
1260
|
if (!alreadyRetried) {
|
|
1179
1261
|
const failedSet = new Set(failedPlannerVariants);
|
|
@@ -1345,7 +1427,13 @@ export function registerEventHandlers(orchestrator: Orchestrator): void {
|
|
|
1345
1427
|
|
|
1346
1428
|
const failedReviewerVariants = [...orchestrator.failedReviewerVariants];
|
|
1347
1429
|
const effectiveMode = getEffectiveMode(orchestrator.active.state);
|
|
1348
|
-
|
|
1430
|
+
// See the planner path: suppress auto-retry while a sub-429 fallback decision
|
|
1431
|
+
// is pending so we don't re-spawn on the still-sub-routed model.
|
|
1432
|
+
if (
|
|
1433
|
+
effectiveMode === "autonomous" &&
|
|
1434
|
+
failedReviewerVariants.length > 0 &&
|
|
1435
|
+
!orchestrator.subFallbackPendingDecision
|
|
1436
|
+
) {
|
|
1349
1437
|
const cycle = orchestrator.active.state.reviewCycle;
|
|
1350
1438
|
const phase = orchestrator.active.state.phase;
|
|
1351
1439
|
const outputs = loadPhaseReviewOutputs(orchestrator.active.dir, phase, cycle.pass);
|
|
@@ -1600,6 +1688,23 @@ export function registerEventHandlers(orchestrator: Orchestrator): void {
|
|
|
1600
1688
|
orchestrator.abortAllSubagents();
|
|
1601
1689
|
}
|
|
1602
1690
|
|
|
1691
|
+
// Subscription rate-limit (429) on a sub-routed subagent: offer the ONE
|
|
1692
|
+
// global switch-to-non-sub dialogue (never per-subagent). subagents:failed
|
|
1693
|
+
// carries the subagent's resolved model id.
|
|
1694
|
+
const failedModelId = typeof data.modelId === "string" ? data.modelId : undefined;
|
|
1695
|
+
const subRateLimited =
|
|
1696
|
+
isRateLimitError(data.error) &&
|
|
1697
|
+
isSubscriptionRouted(failedModelId) &&
|
|
1698
|
+
!orchestrator.subFallbackActive;
|
|
1699
|
+
if (subRateLimited) {
|
|
1700
|
+
// Set the decision-pending flag SYNCHRONOUSLY (before the completion checks
|
|
1701
|
+
// below) so the autonomous planner/reviewer auto-retry does NOT re-spawn
|
|
1702
|
+
// the failed variant on the still-sub-routed model while the fallback
|
|
1703
|
+
// dialogue is in flight.
|
|
1704
|
+
orchestrator.subFallbackPendingDecision = true;
|
|
1705
|
+
void handleSubagentRateLimit(orchestrator, orchestrator.lastCtx, failedModelId);
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1603
1708
|
orchestrator.transitionController.sendCustom(
|
|
1604
1709
|
{
|
|
1605
1710
|
customType: "pp-subagent-error",
|
|
@@ -1780,6 +1885,10 @@ export function registerEventHandlers(orchestrator: Orchestrator): void {
|
|
|
1780
1885
|
if (!isControllerInjected) {
|
|
1781
1886
|
orchestrator.nudgeHalted = false;
|
|
1782
1887
|
orchestrator.consecutiveNudges = 0;
|
|
1888
|
+
// Genuine user re-engagement also clears the API-error retry halt, so a
|
|
1889
|
+
// fresh request can auto-retry again from a clean counter.
|
|
1890
|
+
orchestrator.errorNudgeHalted = false;
|
|
1891
|
+
orchestrator.errorRetryCount = 0;
|
|
1783
1892
|
}
|
|
1784
1893
|
orchestrator.updateStatus(ctx);
|
|
1785
1894
|
|
|
@@ -1814,6 +1923,7 @@ export function registerEventHandlers(orchestrator: Orchestrator): void {
|
|
|
1814
1923
|
constraintsBlock(phase as Phase, effectiveMode),
|
|
1815
1924
|
PRINCIPLES_BLOCK,
|
|
1816
1925
|
TOOLS_BLOCK,
|
|
1926
|
+
DELEGATION_BLOCK,
|
|
1817
1927
|
projectContext,
|
|
1818
1928
|
taskBlock,
|
|
1819
1929
|
]
|
|
@@ -1836,24 +1946,29 @@ export function registerEventHandlers(orchestrator: Orchestrator): void {
|
|
|
1836
1946
|
if (event.toolName === "Agent" && orchestrator.active) {
|
|
1837
1947
|
const input = event.input as Record<string, unknown>;
|
|
1838
1948
|
const requestedType = ((input.subagent_type as string) || "").toLowerCase();
|
|
1949
|
+
const validTypes = ["explore", "librarian", "task", "advisor", "deep-debugger", "reviewer"];
|
|
1839
1950
|
if (!requestedType) {
|
|
1840
|
-
return { block: true, reason: "subagent_type is required.
|
|
1951
|
+
return { block: true, reason: "subagent_type is required. Valid types: explore (codebase research), librarian (external docs), task (implementation subtask), advisor (design/'why is this broken' judgment), deep-debugger (hard persistent failures), reviewer (code review — only when the user explicitly asks)." };
|
|
1841
1952
|
}
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1953
|
+
if (!validTypes.includes(requestedType)) {
|
|
1954
|
+
return { block: true, reason: `Unknown subagent_type "${requestedType}". Valid types: ${validTypes.join(", ")}.` };
|
|
1955
|
+
}
|
|
1956
|
+
|
|
1957
|
+
const simple = orchestrator.config.agents.subagents.simple;
|
|
1958
|
+
const role = requestedType as keyof typeof simple;
|
|
1959
|
+
input.subagent_type = requestedType;
|
|
1960
|
+
input.model = resolveModel(simple[role].model);
|
|
1961
|
+
input.thinking = simple[role].thinking;
|
|
1962
|
+
|
|
1963
|
+
// Spawn-time context injection: append USER_REQUEST + RESEARCH content and a
|
|
1964
|
+
// path-aware manifest of on-demand documents to the LLM-supplied spawn prompt.
|
|
1965
|
+
// explore/librarian intentionally get nothing (fast, scoped retrieval).
|
|
1966
|
+
if (["task", "advisor", "deep-debugger", "reviewer"].includes(requestedType)) {
|
|
1967
|
+
const contextBlock = buildSpawnContextBlock(orchestrator.active.dir);
|
|
1968
|
+
if (contextBlock) {
|
|
1969
|
+
const existingPrompt = typeof input.prompt === "string" ? input.prompt : "";
|
|
1970
|
+
input.prompt = existingPrompt ? `${existingPrompt}\n\n${contextBlock}` : contextBlock;
|
|
1971
|
+
}
|
|
1857
1972
|
}
|
|
1858
1973
|
}
|
|
1859
1974
|
|
|
@@ -2105,6 +2220,39 @@ export function registerEventHandlers(orchestrator: Orchestrator): void {
|
|
|
2105
2220
|
contentBlocks: msg.content?.length ?? 0,
|
|
2106
2221
|
contentSummary,
|
|
2107
2222
|
}, "turn ended with error");
|
|
2223
|
+
// Subscription rate-limit (429) on a sub-routed main turn: retrying the
|
|
2224
|
+
// same sub model is futile against an account-level limit. Offer a
|
|
2225
|
+
// user-gated switch to non-sub Claude instead of the generic backoff.
|
|
2226
|
+
const activeModelId = (typeof msg.model === "string" && msg.model) || ctx.model?.id;
|
|
2227
|
+
const activeProvider = (typeof msg.provider === "string" && msg.provider) || ctx.model?.provider;
|
|
2228
|
+
if (
|
|
2229
|
+
isRateLimitError(errorMsg) &&
|
|
2230
|
+
isSubscriptionRouted(activeModelId, activeProvider) &&
|
|
2231
|
+
!orchestrator.subFallbackActive
|
|
2232
|
+
) {
|
|
2233
|
+
void handleMainRateLimit(orchestrator, ctx, activeModelId, activeProvider);
|
|
2234
|
+
return;
|
|
2235
|
+
}
|
|
2236
|
+
// The SDK already auto-retries this class of error itself (abortable
|
|
2237
|
+
// backoff bound to ESC, continuing the SAME turn). Running pi-pi's OWN
|
|
2238
|
+
// independent retry on top would double-fire: its followUp races the SDK's
|
|
2239
|
+
// continue() into "Agent is already processing", and it re-nudges a still-
|
|
2240
|
+
// failing model. So for SDK-retryable errors we defer entirely to the SDK
|
|
2241
|
+
// and do nothing here. pi-pi's own idle-gated retry remains only as the
|
|
2242
|
+
// fallback for errors the SDK does NOT retry.
|
|
2243
|
+
if (isSdkRetryableError(errorMsg)) {
|
|
2244
|
+
getLogger().debug({ s: "turn", err: errorMsg }, "deferring to SDK auto-retry; pi-pi retry skipped");
|
|
2245
|
+
return;
|
|
2246
|
+
}
|
|
2247
|
+
// Halt guard: once the consecutive-error cap is exceeded we stop auto-
|
|
2248
|
+
// retrying until the user re-engages. errorRetryCount is NO LONGER reset on
|
|
2249
|
+
// benign intervening turns (see below) — otherwise a retried turn that ends
|
|
2250
|
+
// as a harmless text-only reply would reset the counter and the cap could
|
|
2251
|
+
// never accumulate, letting transient errors nudge unbounded.
|
|
2252
|
+
if (orchestrator.errorNudgeHalted) {
|
|
2253
|
+
getLogger().debug({ s: "turn", err: errorMsg }, "error auto-retry halted; awaiting user re-engagement");
|
|
2254
|
+
return;
|
|
2255
|
+
}
|
|
2108
2256
|
orchestrator.errorRetryCount = (orchestrator.errorRetryCount ?? 0) + 1;
|
|
2109
2257
|
const maxRetries = 5;
|
|
2110
2258
|
if (orchestrator.errorRetryCount <= maxRetries) {
|
|
@@ -2112,18 +2260,44 @@ export function registerEventHandlers(orchestrator: Orchestrator): void {
|
|
|
2112
2260
|
ctx.ui.notify(`API error (attempt ${orchestrator.errorRetryCount}/${maxRetries}): ${errorMsg}. Retrying in ${delay / 1000}s...`, "warning");
|
|
2113
2261
|
const taskToken = orchestrator.activeTaskToken;
|
|
2114
2262
|
if (orchestrator.pendingRetryTimer) clearTimeout(orchestrator.pendingRetryTimer);
|
|
2263
|
+
// Arm a direct ESC interrupt for this retry window — no SDK/interactive
|
|
2264
|
+
// binding covers pi-pi's own timer (the turn already ended in error).
|
|
2265
|
+
orchestrator.armRetryEscInterrupt(ctx);
|
|
2115
2266
|
orchestrator.pendingRetryTimer = setTimeout(() => {
|
|
2116
2267
|
orchestrator.pendingRetryTimer = null;
|
|
2117
|
-
if (orchestrator.activeTaskToken !== taskToken || !orchestrator.active)
|
|
2118
|
-
|
|
2268
|
+
if (orchestrator.activeTaskToken !== taskToken || !orchestrator.active) {
|
|
2269
|
+
orchestrator.disarmRetryEscInterrupt();
|
|
2270
|
+
return;
|
|
2271
|
+
}
|
|
2272
|
+
// Defer until the main session is idle: sending a followUp while the SDK
|
|
2273
|
+
// still has an active run throws an async, runtime-swallowed "Agent is
|
|
2274
|
+
// already processing" error. sendUserMessageWhenIdle polls (reusing
|
|
2275
|
+
// pendingRetryTimer so ESC/abort still cancels) and disarms the ESC hook
|
|
2276
|
+
// once delivered.
|
|
2277
|
+
orchestrator.sendUserMessageWhenIdle(
|
|
2278
|
+
`[PI-PI] Previous request failed due to an API error. Continue working on the current phase (${phase}).`,
|
|
2279
|
+
taskToken,
|
|
2280
|
+
);
|
|
2119
2281
|
}, delay);
|
|
2120
2282
|
} else {
|
|
2121
|
-
ctx.ui.notify(`API error persisted after ${maxRetries} retries: ${errorMsg}. Stopping auto-retry.`, "error");
|
|
2122
|
-
|
|
2283
|
+
ctx.ui.notify(`API error persisted after ${maxRetries} retries: ${errorMsg}. Stopping auto-retry — send any message to resume.`, "error");
|
|
2284
|
+
// Halt (do NOT reset the counter) so no further error turn re-arms a retry
|
|
2285
|
+
// until the user re-engages. cancelPendingRetry would reset the count and
|
|
2286
|
+
// re-open the floodgate, so only clear the live timer/ESC hook here.
|
|
2287
|
+
orchestrator.errorNudgeHalted = true;
|
|
2288
|
+
if (orchestrator.pendingRetryTimer) {
|
|
2289
|
+
clearTimeout(orchestrator.pendingRetryTimer);
|
|
2290
|
+
orchestrator.pendingRetryTimer = null;
|
|
2291
|
+
}
|
|
2292
|
+
orchestrator.disarmRetryEscInterrupt();
|
|
2123
2293
|
}
|
|
2124
2294
|
return;
|
|
2125
2295
|
}
|
|
2126
|
-
|
|
2296
|
+
// NOTE: errorRetryCount is intentionally NOT reset here. Resetting on every
|
|
2297
|
+
// non-error turn let a benign nudge-induced turn zero the counter, defeating
|
|
2298
|
+
// the maxRetries cap and allowing unbounded error nudges. The counter is
|
|
2299
|
+
// reset only on genuine user re-engagement (before_agent_start) and on task
|
|
2300
|
+
// reset / cancelPendingRetry.
|
|
2127
2301
|
|
|
2128
2302
|
if ((globalThis as any)[SUBAGENT_SESSION_KEY]) {
|
|
2129
2303
|
return;
|