Files
harness-tools/extensions/pi-workflow/index.ts
T

124 lines
3.9 KiB
TypeScript

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 { registerPiWorkflowTool } from "./tool.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<void>;
}
export function createWorkflowRuntime(
options: WorkflowExtensionOptions = {},
): ContextTransitionRuntime {
const store = new WorkflowStore(
options.stateRoot ?? path.join(getAgentDir(), "pi-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(),
});
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(
`pi-workflow startup recovery failed: ${error instanceof Error ? error.message : String(error)}`,
"error",
);
}
});
}
export default function piWorkflowExtension(pi: ExtensionAPI) {
let runtime: ContextTransitionRuntime;
runtime = createWorkflowRuntime({
afterRunPersist: async (ctx) => {
await reconcileWorkflowDeliveries(pi, runtime, ctx);
},
});
registerWorkflowContextTransitions(pi, runtime, (args, ctx) =>
openWorkflowDashboard(pi, runtime, ctx, args || undefined),
);
registerPiWorkflowTool(pi, runtime);
registerWorkflowRecovery(pi, runtime);
registerWorkflowNotifications(pi, runtime);
}