A minimal workflow runner. Define state handlers and wire them together into workflows. Transitions can be static or triggered by signals emitted from handlers.
Define a ContextInterface that represents the state of your system and passes through your workflow.
Each state has a StateHandlerInterface, which processes the context and returns a HandlerResult, possibly with transition signals.
The WorkflowRunner coordinates:
- Calling handlers in a loop
- Transitioning based on signal or static mapping
$context = new MyContext(state: 'start');
$registry = new StateHandlerRegistry();
$registry->register('start', new StartHandler());
$registry->register('next', new NextHandler());
$workflow = new WorkflowDefinition(
staticTransitions: ['start' => 'next'],
signalTransitions: ['done' => 'final']
);
$workflows = new WorkflowRegistry();
$workflows->add('default', $workflow);
$runner = new WorkflowRunner($registry, new TransitionResolver(), $workflows);
$finalContext = $runner->run($context);If a handler "emits" a signal (via HandlerResult::$signals), that takes precedence over the static transition.
return new HandlerResult(
context: $context,
signals: ['done' => true] // will override static transition
);If no signals match, the runner falls back to the static transition based on current state.
In a real project:
final class Agent
{
public function __construct(
private WorkflowRunner $workflowRunner,
) {}
public function run(string $agentId, string $input): LLMResponse
{
$context = new AgentContext(agentId: $agentId, input: $input);
$result = $this->workflowRunner->run($context, $context->agentId);
return $result->getFinalResponse()
?? throw new RuntimeException('Agent completed but returned no response.');
}
}Implement:
ContextInterface: your mutable data object, immutable in use.StateHandlerInterface: logic per step/state.
- Make
StateHandlerRegistrycontainer aware (allow passing a PSR-11 compliant DI container)
MIT License