/** * Declarative workflow engine over the subagent runtime. * * A workflow is a list of stages with explicit (`needs`) or implicit (linear * chain) dependencies. The engine schedules stages over a SubagentRuntime, * honors the platform's two-channel handoff, and never rejects — per-stage * failures are typed outcomes, not exceptions. * * Semantics baked in: * - Two-channel handoff: (1) typed results flow to dependents via * `StageContext.results`; (2) when a stage that declares `sharesTree` * completes ok in a git repo, its `git diff HEAD` snapshot (bounded) flows * to dependents via `StageContext.treeDiffs`. * - Resource exclusion: `sharesTree` stages never run concurrently with ANY * other stage — they wait for the running set to drain and block new * starts while queued/running. Non-sharesTree stages overlap freely. * - Failure containment: a failed dependency transitively skips its * dependents (kind 'skipped'); an exhausted token budget skips unstarted * stages (kind 'budget_exceeded'); gates that never pass fail the stage * (kind 'gate_failed'). Runtime 'empty' outcomes are failures — use a * `gate` to enforce content contracts and avoid vacuous passes. * - Control artifacts: every run persists status.json + stages/.json * under its runDir. Each stage artifact carries a per-stage content key * (Merkle-style: the stage's own content plus its upstream needs' keys), * and status.json carries a `stageKeys` map plus a whole-spec `specHash` * (back-compat). `resumeFrom` replays a previously-ok stage ONLY if its * content key still matches — an unchanged prefix replays free; a changed * stage re-runs itself and (via upstream chaining) everything downstream. * Old runDirs without `stageKeys` fall back to the whole-spec `specHash`. * * The engine depends only on the SubagentRuntime TYPE (never pi runtime * modules), so tests drive it with a fake runtime. */ import type { TSchema } from 'typebox'; import type { SpawnFailureKind, SpawnUsage, SubagentRuntime } from './subagents.js'; export type StageOutcome = { ok: true; output: string; data?: unknown; usage: SpawnUsage; durationMs: number; attempts: number; } | { ok: false; kind: SpawnFailureKind | 'gate_failed' | 'skipped' | 'budget_exceeded'; error: string; durationMs: number; attempts: number; }; export type StageOk = Extract; export interface StageContext { /** Outcomes of all completed stages so far, by stage id. */ results: Record; /** Bounded `git diff HEAD` snapshots from completed sharesTree stages. */ treeDiffs: Record; cwd: string; runDir: string; } export interface WorkflowStage { id: string; /** Label, e.g. "reviewer". */ agent?: string; /** Static prompt or function of dependency context (item/index set for foreach jobs). */ prompt: string | ((ctx: StageContext, item?: unknown, index?: number) => string); model?: string; /** * Built-in tool allowlist for the stage's spawns. Default read-only * ['read', 'grep', 'find', 'ls'] — a stage that edits files or runs bash * must declare its tools explicitly. [] = no tools. */ tools?: string[]; systemPrompt?: string; /** Typebox schema, passed through to the runtime; validated per spawn (per item for foreach). */ outputSchema?: TSchema; /** Explicit dependencies. Default: the previously declared stage (linear chain). Explicit [] = no deps. */ needs?: string[]; /** Declares the stage edits/reads the shared working tree. Default false. See header for exclusion semantics. */ sharesTree?: boolean; /** * Run this stage's spawns in isolated git worktrees instead of the shared tree. * The change set (incl. untracked files) lands as a .patch beside the run artifact; * integration is the caller's decision. Mutually exclusive with sharesTree. */ worktree?: boolean; /** Fan out one spawn per item: static array, or items picked from a dependency's ok outcome. */ foreach?: unknown[] | { from: string; pick?: (outcome: StageOk) => unknown[]; }; /** * Validation gate on an ok outcome. Return true to pass, or { revise: feedback } * to re-spawn with the feedback appended to the prompt (up to maxGateAttempts * total attempts). Gate exceptions fail the stage as 'gate_failed'. */ gate?: (outcome: StageOk, ctx: StageContext) => true | { revise: string; }; /** Total gate-evaluated attempts before failing as 'gate_failed'. Default 2. */ maxGateAttempts?: number; /** Re-spawn on crashed/empty outcomes, up to this many times. Default 0. */ retries?: number; maxTurns?: number; maxToolCalls?: number; timeoutMs?: number; } export interface WorkflowSpec { name: string; stages: WorkflowStage[]; /** Scheduler cap on concurrent spawns. Default 4. */ concurrency?: number; /** Stop starting new stages once summed usage exceeds this many total tokens. */ tokenBudget?: number; } export interface WorkflowResult { ok: boolean; outcomes: Record; usage: SpawnUsage; runDir: string; } export interface WorkflowEvent { type: 'stage_start' | 'stage_complete' | 'stage_failed' | 'stage_skipped' | 'workflow_complete' | 'resume_summary'; stageId?: string; outcome?: StageOutcome; /** `resume_summary` only: stages reused from the prior run. */ replayed?: number; /** `resume_summary` only: stages still pending (to execute) this run. */ rerun?: number; } export interface RunWorkflowOptions { cwd: string; /** Override the run directory (default: /workflow-runs/-). */ runDir?: string; /** A prior run's directory; stages recorded ok there are skipped and their outcomes loaded. */ resumeFrom?: string; /** Aborts every stage spawn (threaded into each runtime.spawn call). */ signal?: AbortSignal; onProgress?: (event: WorkflowEvent) => void; } export declare function runWorkflow(spec: WorkflowSpec, runtime: SubagentRuntime, opts: RunWorkflowOptions): Promise;