import * as path from "node:path"; import { getAgentDir, type ExtensionAPI, type ExtensionContext, } from "@earendil-works/pi-coding-agent"; import { loadWorkflowConfig } from "./config.ts"; import type { WorkflowRun } from "./domain.ts"; import { openWorkflowDashboard } from "./dashboard.ts"; import { registerWorkflowContextTransitions, type ContextTransitionRuntime, } from "./context-transition.ts"; import { WorkflowEngine } from "./engine.ts"; import { PiAssignmentExecutor, type ProcessAssignmentScheduler, } from "./execution.ts"; import { loadAgentProfiles } from "./profiles.ts"; import { reconcileWorkflowDeliveries, registerWorkflowNotifications, } from "./notifications.ts"; import { runScriptSandbox } from "./script-runtime.ts"; import { WorkflowStore } from "./store.ts"; import { registerWorkflowTool } from "./tool.ts"; import { refreshWorkflowProgress, registerWorkflowProgress, } from "./progress.ts"; import { ApprovedHostCommandExecutor, FilesystemPrototypeValidator, GitDiffProvider, GitWorkspaceManager, } from "./workspace.ts"; import type { ThinkingLevel, WorkflowModel } from "../_workflows/runner.ts"; export interface WorkflowExtensionOptions { stateRoot?: string; scheduler?: ProcessAssignmentScheduler; afterRunPersist?: ( ctx: ExtensionContext, run: WorkflowRun, ) => void | Promise; } export function createWorkflowRuntime( options: WorkflowExtensionOptions = {}, ): ContextTransitionRuntime { const store = new WorkflowStore( options.stateRoot ?? path.join(getAgentDir(), "workflows"), ); return { store, async engineForContext(ctx: ExtensionContext) { const config = await loadWorkflowConfig({ cwd: ctx.cwd, projectTrusted: ctx.isProjectTrusted(), }); const catalog = await loadAgentProfiles({ cwd: ctx.cwd, projectTrusted: ctx.isProjectTrusted(), standardAgents: config.standardAgents, }); const executor = new PiAssignmentExecutor({ modelRegistry: ctx.modelRegistry, parentCwd: ctx.cwd, parentProjectTrusted: ctx.isProjectTrusted(), parentModel: ctx.model as WorkflowModel | undefined, parentThinkingLevel: (ctx.thinkingLevel ?? "off") as ThinkingLevel, concurrencyCeiling: config.concurrency.ceiling, runConcurrency: config.concurrency.default, ...(options.scheduler ? { scheduler: options.scheduler } : {}), }); return new WorkflowEngine({ store, workspace: new GitWorkspaceManager(), executor, hostCommands: new ApprovedHostCommandExecutor(), reviewDiff: new GitDiffProvider(), scriptRuntime: { run: runScriptSandbox }, prototypeValidator: new FilesystemPrototypeValidator(), sourceRoot: ctx.cwd, parentSessionId: ctx.sessionManager.getSessionId(), effectiveConfig: config, profiles: catalog.profiles, ...(options.afterRunPersist ? { afterRunPersist: (run) => options.afterRunPersist!(ctx, run), } : {}), }); }, }; } export function registerWorkflowRecovery( pi: ExtensionAPI, runtime: ContextTransitionRuntime, ) { pi.on("session_start", async (_event, ctx) => { try { const engine = await runtime.engineForContext(ctx); await engine.recover(); } catch (error) { ctx.ui.notify( `workflow startup recovery failed: ${error instanceof Error ? error.message : String(error)}`, "error", ); } }); } export default function workflowExtension(pi: ExtensionAPI) { let runtime: ContextTransitionRuntime; runtime = createWorkflowRuntime({ afterRunPersist: async (ctx) => { await Promise.all([ reconcileWorkflowDeliveries(pi, runtime, ctx), refreshWorkflowProgress(runtime, ctx), ]); }, }); registerWorkflowContextTransitions(pi, runtime, (args, ctx) => openWorkflowDashboard(pi, runtime, ctx, args || undefined), ); registerWorkflowTool(pi, runtime); registerWorkflowRecovery(pi, runtime); registerWorkflowNotifications(pi, runtime); registerWorkflowProgress(pi, runtime); }