Agentic AIPHPverifiedVerified
Plan-and-Execute Pattern in PHP
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 PHP
1Step 1: Define the plan and step structures
class PlanStep
{
public function __construct(
public readonly string $description,
public readonly string $tool,
public string $status = 'pending',
public ?string $result = null,
) {}
}
class Plan
{
/** @var PlanStep[] */
public array $steps = [];
public function addStep(string $description, string $tool): void
{
$this->steps[] = new PlanStep($description, $tool);
}
public function isComplete(): bool
{
return empty(array_filter($this->steps, fn(PlanStep $s) => $s->status === 'pending'));
}
}2Step 2: Implement the planner that generates a plan from a task
function createPlan(string $task, callable $llm): Plan
{
$response = $llm("Break this task into steps: {$task}");
$plan = new Plan();
// Parse LLM response into steps
foreach (explode("\n", $response) as $line) {
if (trim($line) !== '') {
$plan->addStep(trim($line), 'default');
}
}
return $plan;
}3Step 3: Execute each step in the plan sequentially
function executePlan(Plan $plan, callable $executor): string
{
foreach ($plan->steps as $step) {
$step->result = $executor($step->description, $step->tool);
$step->status = 'completed';
}
$results = array_map(fn(PlanStep $s) => $s->result, $plan->steps);
return implode("\n", array_filter($results));
}<?php
declare(strict_types=1);
// [step] Define enums, value objects, and interfaces for plan execution
enum StepStatus: string
{
case Pending = 'pending';
case Running = 'running';
case Completed = 'completed';
case Failed = 'failed';
case Skipped = 'skipped';
}
final class PlanStep
{
public StepStatus $status = StepStatus::Pending;
public ?string $result = null;
public ?string $error = null;
public float $durationMs = 0;
/** @param string[] $dependencies */
public function __construct(
public readonly int $id,
public readonly string $description,
public readonly string $tool,
public readonly array $dependencies = [],
) {}
}
final class ExecutionPlan
{
/** @var PlanStep[] */
private array $steps = [];
private int $nextId = 1;
/** @param string[] $dependencies */
public function addStep(string $description, string $tool, array $dependencies = []): int
{
$id = $this->nextId++;
$this->steps[$id] = new PlanStep(
id: $id,
description: $description,
tool: $tool,
dependencies: $dependencies,
);
return $id;
}
/** @return PlanStep[] */
public function getReadySteps(): array
{
return array_filter($this->steps, function (PlanStep $step): bool {
if ($step->status !== StepStatus::Pending) return false;
foreach ($step->dependencies as $depId) {
if (($this->steps[(int) $depId]->status ?? null) !== StepStatus::Completed) {
return false;
}
}
return true;
});
}
public function isComplete(): bool
{
return empty(array_filter(
$this->steps,
fn(PlanStep $s) => $s->status === StepStatus::Pending || $s->status === StepStatus::Running,
));
}
/** @return PlanStep[] */
public function getSteps(): array
{
return $this->steps;
}
}
interface PlannerInterface
{
public function createPlan(string $task): ExecutionPlan;
public function replan(string $task, ExecutionPlan $currentPlan, string $error): ExecutionPlan;
}
interface ExecutorInterface
{
public function executeStep(PlanStep $step): string;
}
interface LoggerInterface
{
public function info(string $message, array $context = []): void;
public function error(string $message, array $context = []): void;
}
// [step] Implement the plan-and-execute engine with error recovery
final class PlanAndExecuteEngine
{
public function __construct(
private readonly PlannerInterface $planner,
private readonly ExecutorInterface $executor,
private readonly LoggerInterface $logger,
private readonly int $maxReplans = 3,
) {}
/** @return array{plan: ExecutionPlan, results: string[]} */
public function run(string $task): array
{
$plan = $this->planner->createPlan($task);
$replans = 0;
$results = [];
while (!$plan->isComplete()) {
$readySteps = $plan->getReadySteps();
if (empty($readySteps) && !$plan->isComplete()) {
$this->logger->error('Deadlock detected: no ready steps');
break;
}
foreach ($readySteps as $step) {
$step->status = StepStatus::Running;
$start = hrtime(true);
try {
$step->result = $this->executor->executeStep($step);
$step->status = StepStatus::Completed;
$step->durationMs = (hrtime(true) - $start) / 1e6;
$results[] = $step->result;
$this->logger->info("Step completed", ['id' => $step->id]);
} catch (\Throwable $e) {
$step->status = StepStatus::Failed;
$step->error = $e->getMessage();
$step->durationMs = (hrtime(true) - $start) / 1e6;
$this->logger->error("Step failed", [
'id' => $step->id,
'error' => $e->getMessage(),
]);
if ($replans < $this->maxReplans) {
$plan = $this->planner->replan($task, $plan, $e->getMessage());
$replans++;
}
}
}
}
return ['plan' => $plan, 'results' => $results];
}
}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.”