import { createHash } from "node:crypto"; import { join, resolve } from "node:path"; import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "@earendil-works/pi-coding-agent"; import { StringEnum } from "@earendil-works/pi-ai"; import { Type } from "typebox"; import { DEFAULT_CONFIG, loadConfigWithProvenance, loopConfigFrom, type ConfigProvenance, type OrchestratorConfig, type RoleConfig } from "../src/core/config.js"; import { createPiRoutingPlan } from "../src/adapters/piCapabilityRouting.js"; import { enforceRoutingBudget, type RoutingBudgetSnapshot, type RoutingCostEstimate } from "../src/core/routingBudget.js"; import type { ModelSelectionIdentity, RoutingStage, TaskFeatures } from "../src/core/modelRouting.js"; import { coderPrompt, judgePrompt, plannerPrompt, replanPrompt } from "../src/core/prompts.js"; import { detectTestCommand } from "../src/core/tests.js"; import { createIdleState, nextPhase, type LoopEvent, type OrchestratorState, type Verdict } from "../src/core/loop.js"; import { applyWorkflowTransition, type GraphTransitionTrace } from "../src/adapters/piWorkflow/graphExecution.js"; import { fastWorkflowGraph } from "../src/core/workflowGraphs.js"; import { currentRun, readState } from "../src/lifecycle/artifacts.js"; import { isReadOnlyLifecycleCommand } from "../src/lifecycle/readOnlyPolicy.js"; import { appendRoutingBudgetLedgerEvent, appendUserRoutingEvidenceEvent, readRoutingBudgetLedger, resolveUserEvidenceRoot, } from "../src/lifecycle/routingEvidenceStore.js"; const STATE_TYPE = "ai-orchestrator"; const STATUS_KEY = "ai-orchestrator"; const WIDGET_KEY = "ai-orchestrator"; const READ_ONLY_TOOLS = ["read", "grep", "find", "ls", "bash"]; const JUDGE_TOOLS = [...READ_ONLY_TOOLS, "judge_verdict"]; const MUTATION_TOOLS = new Set(["edit", "write"]); const BUILD_TOOL_ALLOWLIST = new Set(["read", "grep", "find", "ls", "edit", "write"]); const PUBLICATION_COMMAND = /\bgit\b[\s\S]*?\b(?:add|commit|push|tag)\b|\bgh\b[\s\S]*?\bpr\b[\s\S]*?\bcreate\b|\b(?:npm|pnpm|yarn)\b[\s\S]*?\bpublish\b/i; const DESTRUCTIVE_GIT_COMMAND = /\bgit\b[\s\S]*?\b(?:clean|reset|checkout|restore)\b/i; interface JudgeVerdictParams { verdict: Verdict; reasons: string; requiredFixes?: string; } interface RuntimeState extends OrchestratorState { runId?: string; cwd?: string; pendingVerdict?: JudgeVerdictParams; judgeReminderSent?: boolean; plannerReminderSent?: boolean; latestJudgeFeedback?: string; toolsBeforeRun?: string[]; toolsBeforeJudge?: string[]; modelSelections?: Array<{ stage: "plan" | "build" | "fast-judge"; provider: string; model: string; family?: string; thinking: OrchestratorConfig["roles"]["coder"]["thinking"]; reason: string; engine: OrchestratorConfig["routing"]["engine"]; policyVersion: string; taskFeaturesHash: string; fallbackCount: number; estimatedCostUsd?: number; failureCategories?: string[]; attemptedModels: string[]; decisionId: string; profileVersion: string; task: Pick; selectedAt: string; }>; rejectionFingerprints?: string[]; buildEvidenceFingerprints?: string[]; planFingerprint?: string; routingFailures?: Array<{ stage: "plan" | "build" | "fast-judge"; identity: string; category: "not-found" | "unavailable" | "provider-error" }>; lastUsage?: { inputTokens: number; outputTokens: number; cacheReadTokens: number; cacheWriteTokens: number; observedUsd: number; }; graphTrace?: GraphTransitionTrace[]; graphWarnings?: string[]; } interface RuntimeConfig { config: OrchestratorConfig; provenance: ConfigProvenance; } export default function orchestratorExtension(pi: ExtensionAPI): void { let state: RuntimeState = createRuntimeState(); let runtime: RuntimeConfig | undefined; let pendingSettlement: { runId?: string; messages: unknown[] } | undefined; let stopping = false; pi.registerFlag("orchestrate-yolo", { description: "Skip the plan approval gate for /orchestrate runs", type: "boolean", default: false, }); pi.registerTool({ name: "judge_verdict", label: "Judge Verdict", description: "Return the final structured verdict for an orchestrator judge phase.", promptSnippet: "Return an approve/reject verdict for the orchestrator judge phase", promptGuidelines: [ "Use judge_verdict exactly once as the final action during an ai-orchestrator judge phase.", "After calling judge_verdict, do not emit another assistant response in the same turn.", ], parameters: Type.Object({ verdict: StringEnum(["approve", "reject"] as const), reasons: Type.String({ minLength: 1, description: "Concrete review findings supporting the verdict" }), requiredFixes: Type.Optional(Type.String({ minLength: 1, description: "Required changes when rejecting" })), }), async execute(_toolCallId, params) { if (state.phase !== "judging") { throw new Error("judge_verdict is only valid during an ai-orchestrator judging phase"); } if (params.reasons.trim().length === 0) throw new Error("judge_verdict reasons must be non-empty"); if (params.verdict === "reject" && (!params.requiredFixes || params.requiredFixes.trim().length === 0)) { throw new Error("judge_verdict rejection requires non-empty requiredFixes"); } state = { ...state, pendingVerdict: { verdict: params.verdict, reasons: params.reasons.trim(), requiredFixes: params.requiredFixes?.trim(), }, }; persist(); return { content: [{ type: "text", text: `Recorded judge verdict: ${params.verdict}` }], details: state.pendingVerdict, terminate: true, }; }, }); pi.registerCommand("orchestrate", { description: "Run a Plan → Code → Judge orchestration for a task", handler: async (args, ctx) => { await ctx.waitForIdle(); await startRun(args, ctx); }, }); pi.registerCommand("orchestrate-stop", { description: "Stop the current orchestration and restore the original model", handler: async (_args, ctx) => { if (state.phase === "idle" && !isRestorePending(state)) { notifyUser(ctx, "No ai-orchestrator run is active.", "info"); return; } await stopRun(ctx, "ai-orchestrator run stopped by user."); }, }); pi.on("session_start", async (_event, ctx) => { let latest = latestPersistedState(ctx); if (!latest) { if (isActiveRunPhase(state.phase) || isRestorePending(state)) { if (isRestorePending(state)) { state = await restorePendingSessionState(ctx, state); if (isRestorePending(state)) { persist(); notifyUser(ctx, "Previous ai-orchestrator run still needs original-model restoration. Fix model availability and use /orchestrate-stop to retry.", "warning"); updateUi(ctx); return; } } else { deactivateJudgeVerdictTool(); } state = createRuntimeState(); runtime = undefined; persist(); notifyUser(ctx, "Previous ai-orchestrator run belonged to another session; original model restored and state reset.", "warning"); } else { deactivateJudgeVerdictTool(); } updateUi(ctx); return; } if (isRestorePending(latest)) { latest = await restorePendingSessionState(ctx, latest); if (isRestorePending(latest)) { state = latest; runtime = undefined; notifyUser(ctx, "Original-model restoration is still pending. Fix model availability and use /orchestrate-stop to retry.", "warning"); updateUi(ctx); return; } } if (isActiveRunPhase(latest.phase)) { state = createRuntimeState(); persist(); notifyUser(ctx, "Previous ai-orchestrator run was interrupted; state reset.", "warning"); } else { state = latest.phase === "idle" ? latest : createRuntimeState(); } deactivateJudgeVerdictTool(); updateUi(ctx); }); pi.on("session_shutdown", async (_event, ctx) => { if (!isActiveRunPhase(state.phase) && !isRestorePending(state)) { deactivateJudgeVerdictTool(); clearUi(ctx); return; } if (isRestorePending(state)) { state = await restorePendingSessionState(ctx, state); if (isRestorePending(state)) { clearUi(ctx); persist(); return; } } else { deactivateJudgeVerdictTool(); } clearUi(ctx); state = createRuntimeState(); runtime = undefined; persist(); }); pi.on("tool_call", async (event) => { const readOnlyPhase = state.phase === "planning" || state.phase === "replanning" || state.phase === "judging"; if (readOnlyPhase && MUTATION_TOOLS.has(event.toolName)) { return { block: true, reason: `ai-orchestrator ${state.phase} phase is read-only; edit/write are blocked.` }; } if (readOnlyPhase && event.toolName === "bash") { const command = (event.input as { command?: unknown }).command; const testCommand = state.phase === "judging" && state.cwd && (runtime?.config.judge.runTests ?? true) ? detectTestCommand(state.cwd) : undefined; if (typeof command !== "string" || !isReadOnlyLifecycleCommand(command, testCommand)) { return { block: true, reason: `ai-orchestrator ${state.phase} phase blocked a non-read-only bash command.` }; } } if (state.phase === "coding" && MUTATION_TOOLS.has(event.toolName)) { const requested = (event.input as { path?: unknown }).path; const path = typeof requested === "string" && state.cwd ? resolveFastPath(state.cwd, requested) : undefined; const metadataRoot = state.cwd ? join(state.cwd, ".ai-orchestrator") : undefined; if (path && metadataRoot && (path === metadataRoot || path.startsWith(`${metadataRoot}/`))) { return { block: true, reason: "ai-orchestrator coding cannot modify orchestrator metadata." }; } } if (state.phase === "coding" && event.toolName === "bash") { const command = (event.input as { command?: unknown }).command; if (typeof command !== "string" || isBlockedFastBuildCommand(command)) { return { block: true, reason: "ai-orchestrator coding cannot modify orchestrator metadata, perform destructive Git operations, stage, commit, tag, push, open a PR, or publish." }; } } }); pi.on("message_end", async (event) => { if (!runtime || !event.message || event.message.role !== "assistant" || !event.message.usage) return; state.lastUsage = { inputTokens: (state.lastUsage?.inputTokens ?? 0) + event.message.usage.input, outputTokens: (state.lastUsage?.outputTokens ?? 0) + event.message.usage.output, cacheReadTokens: (state.lastUsage?.cacheReadTokens ?? 0) + event.message.usage.cacheRead, cacheWriteTokens: (state.lastUsage?.cacheWriteTokens ?? 0) + event.message.usage.cacheWrite, observedUsd: (state.lastUsage?.observedUsd ?? 0) + event.message.usage.cost.total, }; }); pi.on("agent_end", async (event) => { if (isActiveRunPhase(state.phase)) { pendingSettlement = { runId: state.runId, messages: event.messages }; } }); pi.on("agent_settled", async (_event, ctx) => { if (!pendingSettlement || pendingSettlement.runId !== state.runId) return; const { messages } = pendingSettlement; pendingSettlement = undefined; try { if (isActiveRunPhase(state.phase)) { const stopReason = lastAssistantStopReason(messages); if (stopReason === "aborted") { await stopRun(ctx, "ai-orchestrator run was aborted by user; original model restored."); return; } if (stopReason === "error") { if (await retryAfterProviderError(ctx)) return; await stopRun(ctx, "ai-orchestrator run stopped because all eligible provider fallbacks failed."); return; } } if (state.phase === "planning" || state.phase === "replanning") { await handlePlanProduced(ctx, extractLastAssistantText(messages)); return; } if (state.phase === "coding") { await handleCodeProduced(ctx); return; } if (state.phase === "judging") { await handleJudgingEnded(ctx); } } catch (error) { const message = error instanceof Error ? error.message : String(error); await stopRun(ctx, `ai-orchestrator stopped after an internal error: ${message}`); } }); async function startRun(args: string, ctx: ExtensionCommandContext): Promise { if (isRestorePending(state)) { notifyUser(ctx, "Original-model restoration is pending. Fix model availability and use /orchestrate-stop before starting another run.", "error"); return; } if (state.phase !== "idle" && state.phase !== "done" && state.phase !== "failed") { notifyUser(ctx, "An ai-orchestrator run is already active. Use /orchestrate-stop first.", "error"); return; } const parsed = parseCommandArgs(args); if (parsed.warning) { notifyUser(ctx, parsed.warning, "warning"); } if (!parsed.task) { notifyUser(ctx, "Usage: /orchestrate [--yolo] ", "error"); return; } let config: OrchestratorConfig; let provenance: ConfigProvenance; try { const resolved = loadPiResolvedConfig(ctx.cwd); config = resolved.config; provenance = resolved.provenance; } catch (error) { const message = error instanceof Error ? error.message : String(error); notifyUser(ctx, `Invalid ai-orchestrator config: ${message}`, "error"); return; } const lifecycle = currentRun(ctx.cwd, config.lifecycle.artifactsDir); const lifecycleState = lifecycle && readState(lifecycle.paths); if (lifecycleState && isActiveLifecyclePhase(lifecycleState.phase)) { notifyUser(ctx, `Lifecycle run ${lifecycleState.runId} is active at ${lifecycleState.phase}. Use /lifecycle-stop before /orchestrate.`, "error"); return; } runtime = { config, provenance }; const missingRole = config.routing.engine === "capability" ? undefined : findMissingRole(ctx, config); if (missingRole) { notifyUser(ctx, missingRole, "error"); runtime = undefined; return; } const yolo = parsed.yolo || pi.getFlag("orchestrate-yolo") === true; const currentThinking = pi.getThinkingLevel(); state = { ...(await applyWorkflowTransition({ definition: fastWorkflowGraph(), engine: config.execution.engine, state: createIdleState(), event: { type: "start", task: parsed.task, yolo } as LoopEvent, reduce: (current, event) => nextPhase(current, event, loopConfigFrom(config)), ownsState: () => true, onShadowMismatch: (message) => notifyUser(ctx, message, "warning"), }) as RuntimeState), runId: createRunId(), cwd: ctx.cwd, originalModel: ctx.model ? { provider: String(ctx.model.provider), id: String(ctx.model.id), thinking: currentThinking } : undefined, toolsBeforeRun: pi.getActiveTools().filter((toolName) => toolName !== "judge_verdict"), rejectionFingerprints: [], buildEvidenceFingerprints: [], routingFailures: [], }; persist(); const ok = await switchToRole("planner", ctx); if (!ok) return; activateReadOnlyTools(); updateUi(ctx); pi.sendUserMessage(plannerPrompt(parsed.task)); } async function handlePlanProduced(ctx: ExtensionContext, planText: string): Promise { if (!planText.trim()) { if (!state.plannerReminderSent) { state = { ...state, plannerReminderSent: true }; persist(); pi.sendUserMessage( "You must finish the planning phase by writing a concrete implementation plan. Do not edit files yet.", { deliverAs: "followUp" }, ); return; } await stopRun(ctx, "ai-orchestrator stopped because the planner did not produce a plan after a reminder."); return; } persistFastStageOutcome("plan", "unknown", "unknown"); const wasReplanning = state.phase === "replanning"; const nextPlanFingerprint = fastConvergenceFingerprint(planText); const changedPlan = Boolean(state.planFingerprint && state.planFingerprint !== nextPlanFingerprint); state = { ...(await transitionFast({ type: "plan_produced", plan: planText })), judgeReminderSent: false, plannerReminderSent: false, latestJudgeFeedback: wasReplanning ? undefined : state.latestJudgeFeedback, rejectionFingerprints: wasReplanning && changedPlan ? [] : state.rejectionFingerprints, buildEvidenceFingerprints: wasReplanning && changedPlan ? [] : state.buildEvidenceFingerprints, planFingerprint: nextPlanFingerprint, }; persist(); updateUi(ctx); if (state.phase === "awaiting_approval") { await requestPlanApproval(ctx, wasReplanning); return; } if (state.phase === "coding") { await enterCoding(ctx); } } async function requestPlanApproval(ctx: ExtensionContext, wasReplanning: boolean): Promise { const approvalRunId = state.runId; if (!ctx.hasUI) { await stopRun( ctx, "ai-orchestrator stopped: plan approval is required in non-interactive mode. Re-run with --yolo to skip approval explicitly.", ); return; } const choice = await ctx.ui.select("Plan ready — proceed?", [ "Approve and code", "Revise plan (give feedback)", "Cancel", ]); if (!isSameApprovalRun(approvalRunId)) return; if (choice === "Approve and code") { state = await transitionFast({ type: "plan_approved" }); persist(); if (state.phase === "coding") { await enterCoding(ctx); } return; } if (choice === "Revise plan (give feedback)") { const feedback = await ctx.ui.editor("What should the planner revise?", ""); if (!isSameApprovalRun(approvalRunId)) return; if (!feedback?.trim()) { await stopRun(ctx, "ai-orchestrator cancelled during plan revision."); return; } state = await transitionFast({ type: "plan_rejected_by_user" }); persist(); if (state.phase !== "planning") return; const ok = await switchToRole("planner", ctx); if (!ok) return; updateUi(ctx); pi.sendUserMessage( plannerPrompt(state.task, undefined, feedback.trim()), { deliverAs: "followUp" }, ); return; } await stopRun(ctx, wasReplanning ? "ai-orchestrator cancelled after re-plan." : "ai-orchestrator cancelled."); } async function enterCoding(ctx: ExtensionContext): Promise { if (state.phase !== "coding") return; const breaker = fastConvergenceBreakerReason(); if (breaker) { await stopRun(ctx, `ai-orchestrator stopped by convergence circuit breaker: ${breaker}`); return; } activateBuildTools(); const ok = await switchToRole("coder", ctx); if (!ok) return; updateUi(ctx); pi.sendUserMessage(coderPrompt(state.plan ?? "", state.latestJudgeFeedback), { deliverAs: "followUp" }); } async function handleCodeProduced(ctx: ExtensionContext): Promise { await recordFastBuildFingerprint(ctx); if (state.phase !== "coding") return; persistFastStageOutcome("build", "unknown", "unknown"); state = await transitionFast({ type: "code_produced" }); state.judgeReminderSent = false; state.pendingVerdict = undefined; persist(); if (state.phase !== "judging") return; const ok = await switchToRole("judge", ctx); if (!ok) return; runtime = runtime ?? loadPiResolvedConfig(ctx.cwd); pi.setActiveTools(JUDGE_TOOLS); updateUi(ctx); const command = runtime.config.judge.runTests ? detectTestCommand(ctx.cwd) : undefined; pi.sendUserMessage(judgePrompt(state.task, state.plan ?? "", command), { deliverAs: "followUp" }); } async function handleJudgingEnded(ctx: ExtensionContext): Promise { if (!state.pendingVerdict) { if (!state.judgeReminderSent) { state = { ...state, judgeReminderSent: true }; persist(); pi.sendUserMessage( "You must finish the judge phase by calling the judge_verdict tool exactly once. Do not edit files.", { deliverAs: "followUp" }, ); return; } state.pendingVerdict = { verdict: "reject", reasons: "The judge did not produce a structured verdict; treat this attempt as unverified.", requiredFixes: "Re-verify the implementation against the plan, re-run the project's tests, and fix any failures.", }; } const verdict = state.pendingVerdict; if (!verdict) { throw new Error("judge phase ended without a verdict"); } if (verdict.verdict === "reject") { state.rejectionFingerprints = [...(state.rejectionFingerprints ?? []), fastConvergenceFingerprint(`${verdict.reasons}\n${verdict.requiredFixes ?? ""}`)]; persist(); } persistFastStageOutcome("fast-judge", verdict.verdict, true); const stateForTransition: RuntimeState = { ...state }; state = await transitionFast( { type: "verdict", verdict: verdict.verdict, reasons: verdict.reasons, requiredFixes: verdict.requiredFixes, }, stateForTransition, ); state.pendingVerdict = undefined; state.judgeReminderSent = false; state.latestJudgeFeedback = formatJudgeFeedback(verdict); persist(); restoreToolsAfterJudge(); updateUi(ctx); if (state.phase === "coding") { await enterCoding(ctx); return; } if (state.phase === "replanning") { await enterReplanning(ctx); return; } if (state.phase === "done" || state.phase === "failed") { await finishRun(ctx); } } async function enterReplanning(ctx: ExtensionContext): Promise { activateReadOnlyTools(); const ok = await switchToRole("planner", ctx); if (!ok) return; const diffSummary = await getDiffSummary(ctx); updateUi(ctx); pi.sendUserMessage(replanPrompt(state.task, state.plan ?? "", diffSummary, state.judgeReports), { deliverAs: "followUp", }); } async function getDiffSummary(ctx: ExtensionContext): Promise { try { const [unstaged, staged] = await Promise.all([ pi.exec("git", ["diff", "--stat"], { timeout: 5000, signal: ctx.signal }), pi.exec("git", ["diff", "--staged", "--stat"], { timeout: 5000, signal: ctx.signal }), ]); const combined = [ unstaged.stdout.trim() ? `Unstaged diff stat:\n${unstaged.stdout.trim()}` : undefined, staged.stdout.trim() ? `Staged diff stat:\n${staged.stdout.trim()}` : undefined, unstaged.stderr.trim() ? `git diff stderr:\n${unstaged.stderr.trim()}` : undefined, staged.stderr.trim() ? `git diff --staged stderr:\n${staged.stderr.trim()}` : undefined, ] .filter(Boolean) .join("\n\n"); return combined || "No git diff stat was available."; } catch (error) { const message = error instanceof Error ? error.message : String(error); return `Could not collect git diff summary: ${message}`; } } async function finishRun(ctx: ExtensionContext): Promise { const completedState = state; const summary = finalSummary(completedState); restoreRunTools(completedState); const restored = await restoreOriginalModel(ctx, completedState); clearUi(ctx); pi.sendMessage({ customType: STATE_TYPE, content: summary, display: true, details: completedState }, { triggerTurn: false }); if (!restored) { state = completedState; persist(); deactivateJudgeVerdictTool(); notifyUser(ctx, "Run finished, but original-model restoration is pending. Fix model availability and use /orchestrate-stop to retry.", "warning"); return; } state = createRuntimeState(); runtime = undefined; persist(); deactivateJudgeVerdictTool(); } async function stopRun(ctx: ExtensionContext, message: string): Promise { if (stopping) return; stopping = true; const stoppedState = state; try { if (!ctx.isIdle()) { ctx.abort(); } restoreRunTools(stoppedState); state = await transitionFast({ type: "cancelled" }, stoppedState); state.runId = undefined; persist(); const restored = await restoreOriginalModel(ctx, stoppedState); clearUi(ctx); pi.sendMessage({ customType: STATE_TYPE, content: message, display: true, details: stoppedState }, { triggerTurn: false }); if (!restored) { state = { ...state, originalModel: stoppedState.originalModel, toolsBeforeRun: stoppedState.toolsBeforeRun, toolsBeforeJudge: stoppedState.toolsBeforeJudge, }; persist(); notifyUser(ctx, "Original-model restoration is pending. Fix model availability and run /orchestrate-stop again.", "warning"); return; } state = createRuntimeState(); runtime = undefined; persist(); deactivateJudgeVerdictTool(); } finally { stopping = false; } } async function switchToRole( role: keyof OrchestratorConfig["roles"], ctx: ExtensionContext, excludedIdentities: ReadonlySet = new Set(), ): Promise { const resolved = runtime ?? loadPiResolvedConfig(ctx.cwd); runtime = resolved; const stage = fastRoutingStage(role); const priorSelections: ModelSelectionIdentity[] = (state.modelSelections ?? []).map((selection) => ({ stage: selection.stage, provider: selection.provider, model: selection.model, ...(selection.family ? { family: selection.family } : {}), })); const expectedRunId = state.runId; const expectedPhase = state.phase; const evidence = await fastRoutingEvidence(ctx); if (state.runId !== expectedRunId || state.phase !== expectedPhase) return false; const plan = createPiRoutingPlan({ config: resolved.config, provenance: resolved.provenance, stage, role, available: ctx.modelRegistry.getAvailable(), evidence, priorSelections, }); const failed: string[] = []; for (const candidate of plan.candidates) { const identity = `${candidate.provider}/${candidate.model}`; if (excludedIdentities.has(identity)) { failed.push(`${identity} (provider error)`); recordFastRoutingFailure(stage, identity, "provider-error"); continue; } const estimate: RoutingCostEstimate = candidate.estimatedCostUsd === undefined ? { status: "unknown", reason: "candidate cost metadata unavailable" } : { status: "known", estimatedUsd: candidate.estimatedCostUsd }; const budget = enforceRoutingBudget({ stage, estimate, budgets: resolved.config.routing.budgets, snapshot: fastBudgetSnapshot(state), unattended: state.yolo || !ctx.hasUI, }); if (budget.allowed !== true) { if (budget.allowed === "ask") { const expectedRunId = state.runId; const expectedPhase = state.phase; const confirmed = ctx.hasUI && await ctx.ui.confirm("Routing budget warning", `${budget.reason}\n\nContinue with ${candidate.provider}/${candidate.model}?`); if (state.runId !== expectedRunId || state.phase !== expectedPhase) return false; if (confirmed) { // Explicit interactive approval applies only to this candidate invocation. } else { await abortForModelError(ctx, `${role} stopped by routing budget: ${budget.reason}`); return false; } } else { await abortForModelError(ctx, `${role} stopped by routing budget: ${budget.reason}`); return false; } } const model = ctx.modelRegistry.find(candidate.provider, candidate.model); if (!model) { failed.push(`${candidate.provider}/${candidate.model} (not found)`); recordFastRoutingFailure(stage, identity, "not-found"); continue; } const runId = state.runId; const phase = state.phase; const activated = await pi.setModel(model); if (state.runId !== runId || state.phase !== phase) return false; if (!activated) { failed.push(`${candidate.provider}/${candidate.model} (unavailable)`); recordFastRoutingFailure(stage, identity, "unavailable"); continue; } pi.setThinkingLevel(candidate.thinking); const selection: NonNullable[number] = { stage, provider: candidate.provider, model: candidate.model, ...(candidate.family ? { family: candidate.family } : {}), thinking: candidate.thinking, reason: candidate.reason, engine: plan.engine, policyVersion: plan.policyVersion, taskFeaturesHash: plan.taskFeaturesHash, fallbackCount: failed.length, ...(candidate.estimatedCostUsd === undefined ? {} : { estimatedCostUsd: candidate.estimatedCostUsd }), failureCategories: failed.map(fastFailureCategory), attemptedModels: [...failed, `${candidate.provider}/${candidate.model} (selected)`], decisionId: `${state.runId ?? "unknown"}:${stage}:${(state.modelSelections?.length ?? 0) + 1}`, profileVersion: candidate.profileVersion ?? "legacy-role", task: { workKind: plan.taskFeatures.workKind, risk: plan.taskFeatures.risk, languages: [...plan.taskFeatures.languages], fileCount: plan.taskFeatures.fileCount, }, selectedAt: new Date().toISOString(), }; state = { ...state, modelSelections: [...(state.modelSelections ?? []), selection], lastUsage: undefined }; persist(); persistFastStageStarted(selection); return true; } const exclusions = plan.decision?.excluded.map((item) => `${item.identity.provider}/${item.identity.model}: ${item.code}`) ?? []; await abortForModelError(ctx, `${role} has no eligible model (${[...failed, ...exclusions].join(", ") || "no candidates"})`); return false; } function recordFastRoutingFailure( stage: "plan" | "build" | "fast-judge", identity: string, category: "not-found" | "unavailable" | "provider-error", ): void { if (state.routingFailures?.some((failure) => failure.stage === stage && failure.identity === identity && failure.category === category)) return; state.routingFailures = [...(state.routingFailures ?? []), { stage, identity, category }]; persist(); } function fastFailureCategory(value: string): string { if (value.includes("provider error")) return "provider-error"; if (value.includes("not found")) return "not-found"; return "unavailable"; } async function retryAfterProviderError(ctx: ExtensionContext): Promise { const stage = state.phase === "planning" || state.phase === "replanning" ? "plan" : state.phase === "coding" ? "build" : state.phase === "judging" ? "fast-judge" : undefined; const role = stage === "plan" ? "planner" : stage === "build" ? "coder" : stage === "fast-judge" ? "judge" : undefined; if (!stage || !role) return false; persistFastStageOutcome(stage, "unknown", false); const current = [...(state.modelSelections ?? [])].reverse().find((selection) => selection.stage === stage); if (current) { current.failureCategories = [...(current.failureCategories ?? []), "provider-error"]; persist(); } const failed = new Set((state.modelSelections ?? []) .filter((selection) => selection.stage === stage && selection.failureCategories?.includes("provider-error")) .map((selection) => `${selection.provider}/${selection.model}`)); const ok = await switchToRole(role, ctx, failed); if (!ok) return true; if (state.phase === "planning") { pi.sendUserMessage(plannerPrompt(state.task), { deliverAs: "followUp" }); } else if (state.phase === "replanning") { pi.sendUserMessage(replanPrompt(state.task, state.plan ?? "", await getDiffSummary(ctx), state.judgeReports), { deliverAs: "followUp" }); } else if (state.phase === "coding") { pi.sendUserMessage(coderPrompt(state.plan ?? "", state.latestJudgeFeedback), { deliverAs: "followUp" }); } else if (state.phase === "judging") { const command = runtime?.config.judge.runTests ? detectTestCommand(ctx.cwd) : undefined; pi.sendUserMessage(judgePrompt(state.task, state.plan ?? "", command), { deliverAs: "followUp" }); } return true; } async function recordFastBuildFingerprint(ctx: ExtensionContext): Promise { const runId = state.runId; const phase = state.phase; const [diff, untracked] = await Promise.all([ pi.exec("git", ["diff", "--no-ext-diff", "--binary", "HEAD"], { timeout: 20_000, signal: ctx.signal }), pi.exec("git", ["ls-files", "--others", "--exclude-standard", "-z"], { timeout: 10_000, signal: ctx.signal }), ]); if (state.runId !== runId || state.phase !== phase) return; const evidence = `${diff.code}:${diff.stdout}\n${diff.stderr}\n${untracked.code}:${untracked.stdout}\n${untracked.stderr}`; state.buildEvidenceFingerprints = [...(state.buildEvidenceFingerprints ?? []), fastConvergenceFingerprint(evidence)]; persist(); } function fastConvergenceBreakerReason(): string | undefined { if (!runtime) return undefined; const rejectionCount = fastTrailingEqualCount(state.rejectionFingerprints ?? []); if (rejectionCount >= runtime.config.routing.circuitBreakers.repeatedRejectionFingerprintLimit) { return `${rejectionCount} identical judge rejections reached the configured limit; revise the plan with new evidence.`; } const unchangedBuilds = Math.max(0, fastTrailingEqualCount(state.buildEvidenceFingerprints ?? []) - 1); if (unchangedBuilds >= runtime.config.routing.circuitBreakers.maxBuildPassesWithoutImprovement) { return `${unchangedBuilds} consecutive coding passes produced unchanged evidence; inspect the diff and re-plan.`; } return undefined; } function fastTrailingEqualCount(values: readonly string[]): number { const latest = values.at(-1); if (!latest) return 0; let count = 0; for (let index = values.length - 1; index >= 0 && values[index] === latest; index -= 1) count += 1; return count; } function fastConvergenceFingerprint(value: string): string { return createHash("sha256").update(value.trim().replace(/\s+/g, " ").toLowerCase()).digest("hex").slice(0, 16); } async function fastRoutingEvidence(ctx: ExtensionContext): Promise<{ task: string; plan?: string; verdictCategory?: string; changedPaths: string[]; languages: string[]; testCommand?: string; }> { const [changed, untracked] = await Promise.all([ pi.exec("git", ["diff", "--name-only", "-z", "HEAD"], { timeout: 10_000, signal: ctx.signal }), pi.exec("git", ["ls-files", "--others", "--exclude-standard", "-z"], { timeout: 10_000, signal: ctx.signal }), ]); const changedPaths = [...new Set([...(changed.code === 0 ? changed.stdout.split("\0") : []), ...(untracked.code === 0 ? untracked.stdout.split("\0") : [])] .filter((path) => path.length > 0))]; return { task: state.task, plan: state.plan, verdictCategory: state.latestJudgeFeedback, changedPaths, languages: [...new Set(changedPaths.map(fastLanguageForPath).filter((value): value is string => Boolean(value)))], ...(state.cwd ? { testCommand: detectTestCommand(state.cwd) } : {}), }; } function fastLanguageForPath(path: string): string | undefined { const extension = path.split(".").at(-1)?.toLowerCase(); return ({ ts: "typescript", tsx: "typescript", js: "javascript", jsx: "javascript", py: "python", rb: "ruby", rs: "rust", go: "go", swift: "swift", kt: "kotlin", java: "java", cs: "csharp" } as Record)[extension ?? ""]; } function persistFastStageStarted(selection: NonNullable[number]): void { if (!runtime || !state.runId) return; const userStoreRoot = resolveUserEvidenceRoot(undefined, runtime.config.routing.evidence.userStoreDir); appendRoutingBudgetLedgerEvent(userStoreRoot, { version: 1, eventId: `${selection.decisionId}:budget:stage-started`, runId: state.runId, recordedAt: new Date().toISOString(), outcome: "stage-started", ...(selection.estimatedCostUsd === undefined ? {} : { estimatedUsd: selection.estimatedCostUsd }), }); if (!runtime.config.routing.evidence.enabled) return; appendUserRoutingEvidenceEvent(userStoreRoot, { version: 1, eventId: `${selection.decisionId}:stage-started`, runId: state.runId, decisionId: selection.decisionId, stage: selection.stage, recordedAt: new Date().toISOString(), policyVersion: selection.policyVersion, profileVersion: selection.profileVersion, task: selection.task, selected: { provider: selection.provider, model: selection.model, ...(selection.family ? { family: selection.family } : {}) }, durationMs: "unknown", fallbackCount: selection.fallbackCount, usage: { inputTokens: "unknown", outputTokens: "unknown", cacheReadTokens: "unknown", cacheWriteTokens: "unknown" }, cost: { estimatedUsd: selection.estimatedCostUsd ?? "unknown", observedUsd: "unknown" }, outcome: { type: "stage-started", structuredToolCompliance: "unknown", verdict: "unknown", buildIteration: state.coderIterations }, }); } function persistFastStageOutcome( stage: "plan" | "build" | "fast-judge", verdict: "approve" | "reject" | "unknown", structuredToolCompliance: boolean | "unknown", ): void { if (!runtime || !state.runId) return; const selection = [...(state.modelSelections ?? [])].reverse().find((item) => item.stage === stage); if (!selection) return; const usage = state.lastUsage; const userStoreRoot = resolveUserEvidenceRoot(undefined, runtime.config.routing.evidence.userStoreDir); appendRoutingBudgetLedgerEvent(userStoreRoot, { version: 1, eventId: `${selection.decisionId}:budget:stage-ended`, runId: state.runId, recordedAt: new Date().toISOString(), outcome: "stage-ended", ...(usage ? { observedUsd: usage.observedUsd } : {}), }); if (!runtime.config.routing.evidence.enabled) { state.lastUsage = undefined; return; } appendUserRoutingEvidenceEvent(userStoreRoot, { version: 1, eventId: `${selection.decisionId}:stage-ended`, runId: state.runId, decisionId: selection.decisionId, stage, recordedAt: new Date().toISOString(), policyVersion: selection.policyVersion, profileVersion: selection.profileVersion, task: selection.task, selected: { provider: selection.provider, model: selection.model, ...(selection.family ? { family: selection.family } : {}) }, durationMs: Math.max(0, Date.now() - Date.parse(selection.selectedAt)), fallbackCount: selection.fallbackCount, ...(verdict === "reject" ? { rejectionCategory: `${stage}-reject` } : {}), usage: { inputTokens: usage?.inputTokens ?? "unknown", outputTokens: usage?.outputTokens ?? "unknown", cacheReadTokens: usage?.cacheReadTokens ?? "unknown", cacheWriteTokens: usage?.cacheWriteTokens ?? "unknown", }, cost: { estimatedUsd: selection.estimatedCostUsd ?? "unknown", observedUsd: usage?.observedUsd ?? "unknown" }, outcome: { type: "stage-ended", structuredToolCompliance, verdict, buildIteration: state.coderIterations }, }); state.lastUsage = undefined; } function fastBudgetSnapshot(current: RuntimeState): RoutingBudgetSnapshot { const ledger = runtime ? readRoutingBudgetLedger(join(resolveUserEvidenceRoot(undefined, runtime.config.routing.evidence.userStoreDir), "budget.jsonl")) : []; const runEvents = ledger.filter((event) => event.runId === current.runId); const today = new Date().toISOString().slice(0, 10); const dailyEvents = ledger.filter((event) => event.recordedAt.startsWith(today)); const estimatedSelections = (current.modelSelections ?? []).reduce((sum, selection) => sum + (selection.estimatedCostUsd ?? 0), 0); return { estimatedRunUsd: runEvents.length > 0 ? ledgerCost(runEvents, "stage-started", "estimatedUsd") : estimatedSelections, observedRunUsd: ledgerCost(runEvents, "stage-ended", "observedUsd"), estimatedDayUsd: ledgerCost(dailyEvents, "stage-started", "estimatedUsd"), observedDayUsd: ledgerCost(dailyEvents, "stage-ended", "observedUsd"), paidFallbacks: (current.modelSelections ?? []).reduce((sum, selection) => sum + selection.fallbackCount, 0), attemptsByStage: Object.fromEntries( (current.modelSelections ?? []).map((selection) => [ selection.stage, (current.modelSelections ?? []).filter((item) => item.stage === selection.stage).length, ]), ) as Partial>, }; } function ledgerCost( events: ReturnType, outcome: "stage-started" | "stage-ended", field: "estimatedUsd" | "observedUsd", ): number { return events.reduce((sum, event) => sum + (event.outcome === outcome ? event[field] ?? 0 : 0), 0); } async function abortForModelError(ctx: ExtensionContext, reason: string): Promise { const configHint = `Fix role config in ~/.ai-orchestrator/config.json or ${ctx.cwd}/.ai-orchestrator.json.`; await stopRun(ctx, `ai-orchestrator aborted: ${reason}. ${configHint}`); } async function restoreOriginalModel(ctx: ExtensionContext, source: RuntimeState): Promise { const original = source.originalModel; if (!original) return true; const model = ctx.modelRegistry.find(original.provider, original.id); if (!model) { notifyUser(ctx, `Could not restore original model ${original.provider}/${original.id}: model not found.`, "warning"); return false; } const restored = await pi.setModel(model); if (!restored) { notifyUser(ctx, `Could not restore original model ${original.provider}/${original.id}: API key is unavailable.`, "warning"); return false; } pi.setThinkingLevel(original.thinking); return true; } async function restorePendingSessionState(ctx: ExtensionContext, persistedState: RuntimeState): Promise { state = persistedState; restoreRunTools(persistedState); if (!(await restoreOriginalModel(ctx, persistedState))) { persist(); return state; } state = { ...state, originalModel: undefined, toolsBeforeRun: undefined, toolsBeforeJudge: undefined }; persist(); return state; } function activateReadOnlyTools(): void { pi.setActiveTools(READ_ONLY_TOOLS); } function activateBuildTools(): void { const original = state.toolsBeforeRun ?? pi.getActiveTools().filter((toolName) => toolName !== "judge_verdict"); pi.setActiveTools(original.filter((toolName) => BUILD_TOOL_ALLOWLIST.has(toolName))); } function restoreRunTools(source: RuntimeState): void { if (source.toolsBeforeRun) { pi.setActiveTools(source.toolsBeforeRun.filter((toolName) => toolName !== "judge_verdict")); } else if (source.toolsBeforeJudge) { pi.setActiveTools(source.toolsBeforeJudge.filter((toolName) => toolName !== "judge_verdict")); } else { deactivateJudgeVerdictTool(); } } function restoreToolsAfterJudge(): void { if (state.phase === "coding") activateBuildTools(); else deactivateJudgeVerdictTool(); } function deactivateJudgeVerdictTool(): void { const activeTools = pi.getActiveTools(); if (activeTools.includes("judge_verdict")) { pi.setActiveTools(activeTools.filter((toolName) => toolName !== "judge_verdict")); } } function isSameApprovalRun(runId: string | undefined): boolean { return state.phase === "awaiting_approval" && state.runId === runId; } function persist(): void { pi.appendEntry(STATE_TYPE, state); } function loopConfig() { return loopConfigFrom(runtime?.config ?? DEFAULT_CONFIG); } async function transitionFast(event: LoopEvent, source: RuntimeState = state): Promise { if (!runtime) throw new Error("Fast-path runtime is unavailable during graph transition"); let trace: GraphTransitionTrace | undefined; let graphWarning: string | undefined; const next = await applyWorkflowTransition({ definition: fastWorkflowGraph(), engine: runtime.config.execution.engine, state: source, event, reduce: (current, selectedEvent) => nextPhase(current, selectedEvent, loopConfig()) as RuntimeState, ownsState: (candidate) => candidate.runId === state.runId && candidate.phase === state.phase, onTrace: (value) => { trace = value; }, onShadowMismatch: (message) => { graphWarning = message; }, }); return { ...next, ...(trace ? { graphTrace: [...(source.graphTrace ?? []), trace] } : {}), ...(graphWarning ? { graphWarnings: [...(source.graphWarnings ?? []), graphWarning] } : {}), }; } function updateUi(ctx: ExtensionContext): void { if (!ctx.hasUI) return; if (state.phase === "idle") { clearUi(ctx); return; } const selection = state.modelSelections?.at(-1); const modelLabel = selection ? ` ${selection.provider}/${selection.model}` : ""; ctx.ui.setStatus(STATUS_KEY, `orchestrator ${state.phase}${modelLabel}`); const latestReport = state.judgeReports.at(-1); const lines = [ `AI Orchestrator: ${state.phase}`, `Task: ${truncate(state.task, 80)}`, `Coder iterations: ${state.coderIterations}`, `Consecutive rejections: ${state.consecutiveRejections}`, selection ? `Routing: ${selection.engine}; fallback ${selection.fallbackCount}` : undefined, latestReport ? `Last judge: ${latestReport.verdict} — ${truncate(latestReport.reasons, 80)}` : undefined, ].filter((line): line is string => Boolean(line)); ctx.ui.setWidget(WIDGET_KEY, lines); } function clearUi(ctx: ExtensionContext): void { if (!ctx.hasUI) return; ctx.ui.setStatus(STATUS_KEY, undefined); ctx.ui.setWidget(WIDGET_KEY, undefined); } function notifyUser(ctx: ExtensionContext, message: string, level: "info" | "warning" | "error" = "info"): void { if (ctx.hasUI) { ctx.ui.notify(message, level); return; } pi.sendMessage( { customType: STATE_TYPE, content: `[${level}] ${message}`, display: true, details: { level } }, { triggerTurn: false }, ); } } function isBlockedFastBuildCommand(command: string): boolean { // Normalize quoting and shell escapes so equivalent forms such as // `git -C . push`, `g\\it push`, and `bash -lc 'git push'` remain gated. const normalized = command.replace(/\\(?:\r?\n)?/g, "").replace(/["']/g, " "); return PUBLICATION_COMMAND.test(normalized) || DESTRUCTIVE_GIT_COMMAND.test(normalized) || normalized.includes(".ai-orchestrator") || /\brm\b[\s\S]*?(?:\s\.\/?(?:\s|$)|\s\*|\/\*)/i.test(normalized) || /\bfind\b[\s\S]*?\s-delete\b/i.test(normalized); } function resolveFastPath(cwd: string, requested: string): string { return resolve(cwd, requested.replace(/^@/, "")); } function createRuntimeState(overrides: Partial = {}): RuntimeState { return { ...(createIdleState(overrides) as RuntimeState), modelSelections: [], ...overrides }; } function loadPiResolvedConfig(cwd: string) { return loadConfigWithProvenance(cwd, { ignoreMcpProviders: true }); } function fastRoutingStage(role: keyof OrchestratorConfig["roles"]): Extract { if (role === "planner") return "plan"; if (role === "coder") return "build"; if (role === "judge") return "fast-judge"; throw new Error(`Role ${role} is not part of the fast workflow`); } function createRunId(): string { return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`; } function parseCommandArgs(args: string): { yolo: boolean; task: string; warning?: string } { const parts = args.trim().split(/\s+/).filter(Boolean); let index = 0; let yolo = false; while (parts[index] === "--yolo") { yolo = true; index += 1; } const taskParts = parts.slice(index); return { yolo, task: taskParts.join(" ").trim(), warning: taskParts.includes("--yolo") ? "Ignoring non-leading --yolo token in task text. Put --yolo before the task to skip plan approval." : undefined, }; } function findMissingRole(ctx: ExtensionContext, config: OrchestratorConfig): string | undefined { for (const role of ["planner", "coder", "judge"] as const) { const roleConfig: RoleConfig = config.roles[role]; if (!ctx.modelRegistry.find(roleConfig.provider, roleConfig.model)) { return `${role} model not found: ${roleConfig.provider}/${roleConfig.model}. Fix role config in ~/.ai-orchestrator/config.json or ${ctx.cwd}/.ai-orchestrator.json.`; } } return undefined; } function latestPersistedState(ctx: ExtensionContext): RuntimeState | undefined { const branch = ctx.sessionManager.getBranch() as Array<{ type: string; customType?: string; data?: unknown }>; const entry = branch.filter((item) => item.type === "custom" && item.customType === STATE_TYPE).pop(); return isRuntimeState(entry?.data) ? entry.data : undefined; } function isRuntimeState(value: unknown): value is RuntimeState { if (!value || typeof value !== "object") return false; const candidate = value as Partial; return typeof candidate.phase === "string" && typeof candidate.task === "string"; } function extractLastAssistantText(messages: unknown[]): string { for (const message of [...messages].reverse()) { if (!isRecord(message) || message.role !== "assistant") continue; const text = extractText(message.content); if (text.trim().length > 0) return text; } return ""; } function lastAssistantStopReason(messages: unknown[]): string | undefined { const stopReason = findLastAssistant(messages)?.stopReason; return typeof stopReason === "string" ? stopReason : undefined; } function isActiveLifecyclePhase(phase: string): boolean { return phase !== "idle" && phase !== "done" && phase !== "failed"; } function isActiveRunPhase(phase: OrchestratorState["phase"]): boolean { return phase !== "idle" && phase !== "done" && phase !== "failed"; } function isRestorePending(state: RuntimeState): boolean { return Boolean(state.originalModel || state.toolsBeforeRun || state.toolsBeforeJudge); } function findLastAssistant(messages: unknown[]): Record | undefined { const reversed = [...messages].reverse(); return reversed.find((message) => isRecord(message) && message.role === "assistant") as | Record | undefined; } function extractText(content: unknown): string { if (typeof content === "string") return content; if (!Array.isArray(content)) return ""; return content .map((block) => (isRecord(block) && block.type === "text" && typeof block.text === "string" ? block.text : "")) .filter(Boolean) .join("\n"); } function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null; } function formatJudgeFeedback(verdict: JudgeVerdictParams): string { return [ `Verdict: ${verdict.verdict}`, `Reasons: ${verdict.reasons}`, verdict.requiredFixes ? `Required fixes: ${verdict.requiredFixes}` : undefined, ] .filter(Boolean) .join("\n"); } function finalSummary(state: RuntimeState): string { const verdicts = state.judgeReports .map((report, index) => { const requiredFixes = report.requiredFixes ? `\n Required fixes: ${report.requiredFixes}` : ""; return `${index + 1}. ${report.verdict}: ${report.reasons}${requiredFixes}`; }) .join("\n"); return [ state.phase === "done" ? "AI Orchestrator completed successfully." : "AI Orchestrator failed at the loop cap.", `Task: ${state.task}`, `Coder iterations: ${state.coderIterations}`, verdicts ? `Judge reports:\n${verdicts}` : "Judge reports: none", state.phase === "failed" ? "Working tree was left as-is for human review." : undefined, ] .filter(Boolean) .join("\n\n"); } function truncate(value: string, max: number): string { return value.length <= max ? value : `${value.slice(0, max - 1)}…`; }