Agentic AITypeScriptverifiedVerified
Plan-and-Execute Pattern in TypeScript
Separate high-level planning from step-by-step execution: one LLM call produces a structured plan, then individual executor calls carry out each step, with replanning triggered by unexpected results.
How to Implement the Plan-and-Execute Pattern in TypeScript
1Step 1: Define the Step, Plan, and executor types
interface Step {
id: string;
description: string;
status: "pending" | "running" | "done" | "failed";
result?: string;
}
interface Plan {
goal: string;
steps: Step[];
}
type Planner = (goal: string) => Promise<Step[]>;
type Executor = (step: Step) => Promise<string>;2Step 2: Implement the plan-and-execute loop
async function planAndExecute(
goal: string,
planner: Planner,
executor: Executor
): Promise<Plan> {
// Phase 1: Plan
const rawSteps = await planner(goal);
const plan: Plan = { goal, steps: rawSteps };
// Phase 2: Execute each step sequentially
for (const step of plan.steps) {
step.status = "running";
try {
step.result = await executor(step);
step.status = "done";
} catch (e) {
step.result = String(e);
step.status = "failed";
break; // halt on first failure
}
}
return plan;
}3Step 3: Run the pipeline with a sample goal
// Usage
const plan = await planAndExecute(
"Write and publish a blog post",
async (goal) => [
{ id: "1", description: "Research the topic", status: "pending" },
{ id: "2", description: "Draft the outline", status: "pending" },
{ id: "3", description: "Write the post", status: "pending" },
{ id: "4", description: "Review and publish", status: "pending" },
],
async (step) => {
console.log(`Executing: ${step.description}`);
return `Completed: ${step.description}`;
}
);
console.log(plan.steps.map(s => `[${s.status}] ${s.description}`));// ── Plan-and-Execute with Replanning, Dependencies, Progress Tracking ──
type StepStatus = "pending" | "blocked" | "running" | "done" | "failed" | "skipped";
interface PlanStep {
id: string;
description: string;
dependsOn: string[];
status: StepStatus;
result?: unknown;
error?: string;
startedAt?: number;
completedAt?: number;
attempts: number;
}
interface ExecutionPlan {
id: string;
goal: string;
steps: PlanStep[];
version: number;
createdAt: number;
}
interface ExecutorContext {
goal: string;
completedSteps: Map<string, unknown>;
plan: ExecutionPlan;
}
type StepExecutor = (step: PlanStep, ctx: ExecutorContext) => Promise<unknown>;
type PlannerFn = (goal: string, failedSteps?: PlanStep[]) => Promise<Omit<PlanStep, "status" | "result" | "error" | "startedAt" | "completedAt" | "attempts">[]>;
type ProgressCallback = (step: PlanStep, plan: ExecutionPlan) => void;
class PlanAndExecuteAgent {
private MAX_REPLAN_ATTEMPTS = 2;
constructor(
private planner: PlannerFn,
private executor: StepExecutor,
private onProgress?: ProgressCallback
) {}
async run(goal: string, signal?: AbortSignal): Promise<ExecutionPlan> {
let plan = await this.buildPlan(goal);
let replanCount = 0;
while (true) {
if (signal?.aborted) throw new Error("Execution aborted");
const ready = this.getReadySteps(plan);
if (ready.length === 0) break;
const failed: PlanStep[] = [];
for (const step of ready) {
if (signal?.aborted) throw new Error("Execution aborted");
step.status = "running";
step.startedAt = Date.now();
step.attempts++;
this.onProgress?.(step, plan);
try {
const ctx: ExecutorContext = {
goal,
completedSteps: new Map(
plan.steps
.filter(s => s.status === "done")
.map(s => [s.id, s.result])
),
plan,
};
step.result = await this.executor(step, ctx);
step.status = "done";
step.completedAt = Date.now();
} catch (e) {
step.error = e instanceof Error ? e.message : String(e);
step.status = "failed";
step.completedAt = Date.now();
failed.push(step);
}
this.onProgress?.(step, plan);
}
if (failed.length > 0 && replanCount < this.MAX_REPLAN_ATTEMPTS) {
replanCount++;
console.log(`Replanning (attempt ${replanCount}) due to ${failed.length} failed step(s)`);
plan = await this.replan(plan, failed);
continue;
}
if (failed.length > 0) break;
}
// Mark remaining blocked steps as skipped
plan.steps
.filter(s => s.status === "pending" || s.status === "blocked")
.forEach(s => { s.status = "skipped"; });
return plan;
}
private async buildPlan(goal: string): Promise<ExecutionPlan> {
const rawSteps = await this.planner(goal);
return {
id: crypto.randomUUID(),
goal,
version: 1,
createdAt: Date.now(),
steps: rawSteps.map(s => ({
...s,
status: "pending" as StepStatus,
attempts: 0,
})),
};
}
private async replan(current: ExecutionPlan, failed: PlanStep[]): Promise<ExecutionPlan> {
const rawSteps = await this.planner(current.goal, failed);
const doneSlugs = new Set(current.steps.filter(s => s.status === "done").map(s => s.id));
return {
...current,
version: current.version + 1,
steps: [
...current.steps.filter(s => s.status === "done"),
...rawSteps
.filter(s => !doneSlugs.has(s.id))
.map(s => ({ ...s, status: "pending" as StepStatus, attempts: 0 })),
],
};
}
private getReadySteps(plan: ExecutionPlan): PlanStep[] {
const done = new Set(plan.steps.filter(s => s.status === "done").map(s => s.id));
return plan.steps.filter(s =>
s.status === "pending" &&
s.dependsOn.every(dep => done.has(dep))
);
}
}
export { PlanAndExecuteAgent, type ExecutionPlan, type PlanStep, type StepExecutor, type PlannerFn };Plan-and-Execute Pattern Architecture
hourglass_empty
Rendering diagram...
lightbulb
Plan-and-Execute Pattern in the Real World
“A building contractor (Planner) reviews the architectural blueprints and produces a phased construction schedule: foundation, framing, electrical, finishing. Individual trade crews (Executors) carry out each phase. If an inspection fails (unexpected result), the contractor revises the remaining schedule rather than demolishing the entire building and starting over.”