Klein Kit

Workflows

Cross-agent pipelines with DAG dependencies, reviewer loops, and verify/repair

@klein-kit/orchestrate chains agents across providers: Claude plans, Codex implements, tests gate the result, an independent reviewer can send it back.

pnpm add @klein-kit/orchestrate
import { workflow, runWorkflow } from "@klein-kit/orchestrate";

const def = workflow("dark-mode", "Add a dark-mode toggle to settings")
  .plan("plan", { agent: { provider: "claude" } })        // Claude plans
  .implement("impl", { agent: { provider: "codex" },      // Codex implements
                       dependsOn: ["plan"] })
  .verify("tests", "pnpm test", { retries: 2 })           // failing tests → repair agent → re-run
  .review("review", "impl", { agent: "auto", retries: 1 }) // independent reviewer; REVISE loops back
  .build();                                               // plain JSON — serialize it, ship it

const result = await runWorkflow(def, { onEvent: (e) => render(e) });
// { status: "completed" | "failed", taskState, results }

Builder

workflow(name, goal): WorkflowBuilder

.session(options)                  // Partial<SessionOptions> shared by every step
.plan(id, input?)                  // default planning prompt
.implement(id, input?)             // default implementation prompt
.review(id, target, input?)        // reviews step `target` (auto-added to dependsOn)
.verify(id, command, input?)       // runs a shell command as a gate
.step(id, role, input)             // fully custom
.build(): WorkflowDefinition       // validated, JSON-safe

StepInput:

interface StepInput {
  agent?: AgentSpec;      // "auto" | { provider, model?, reasoningEffort?, session? }
  prompt?: PromptTemplate;
  dependsOn?: string[];
  retries?: number;       // default 1
}

Prompts are templates with {{goal}}, {{taskState}}, {{output:<stepId>}}, and {{feedback}} placeholders. .plan/.implement/.review ship sensible default prompts; supplying your own overrides them.

build() validates: unique ids, review targets exist, verify steps have a command, and no dependency cycles. Serialize with JSON.stringify, load back with loadWorkflow(json).

Running

runWorkflow(def, {
  onEvent?: (event: WorkflowEvent) => void,
  autoProviders?: ProviderId[],   // restrict "auto" selection
  signal?: AbortSignal,           // aborts interrupt + close live sessions
});
  • Ready steps run concurrently (it's a DAG, not a list).
  • Each attempt is a fresh session with one prompt, closed afterward.
  • A step whose dependency failed is skipped with reason: "dependency failed".
  • Verify loop: command fails → a repair agent runs (configurable via repairAgent, default "auto") → command re-runs, up to retries.
  • Review loop: the reviewer answers VERDICT: APPROVE or VERDICT: REVISE; on revise, the target step re-runs with the reviewer's notes injected as {{feedback}}, then gets re-reviewed.
  • Conflict reconciliation: if unrelated steps touched the same paths, a conflict.detected event fires and a synthetic __reconcile__ step merges them.

Workflow events

workflow.started · workflow.completed · workflow.failed · step.started · step.completed · step.failed · step.skipped · review.verdict · verify.result · handoff · conflict.detected · agent (wraps every underlying AgentEvent with its stepId — full streaming fidelity inside workflows).

Auto selection

agent: "auto" picks from installed, authenticated providers with role-based preferences (planning prefers Claude, implementation prefers Codex, …). Reviewers avoid the provider that produced the target. Restrict the pool with autoProviders.

Subagents

One-shot delegation without a full workflow:

import { runSubagent } from "@klein-kit/orchestrate";

const { result, taskState } = await runSubagent({
  provider: "codex",
  task: "Write unit tests for src/utils/dates.ts",
  parentTaskState: session.taskState,  // optional context injection
  onEvent: (e) => render(e),
});

On this page