1. Go to this page and download the library: Download milpa/orchestrator library. Choose the download type require.
2. Extract the ZIP file and open the index.php.
3. Add this code to the index.php.
<?php
require_once('vendor/autoload.php');
/* Start to develop here. Best regards https://php-download.com/ */
milpa / orchestrator example snippets
use Milpa\Command\Declaration\{Because, Mutates, Operation, Reads, Target};
use Milpa\Command\Effect\{Externality, Mutation, Reversibility, Subject};
use Milpa\Orchestrator\Declaration\{Appends, Ask, Edge, Graph, Route, Start};
/** The grader's verdict. Its cases ARE the outgoing edges of the node that returns it. */
enum Verdict: string
{
case Passed = 'PASSED';
case Failed = 'FAILED';
}
/** The editor's options. Its cases ARE the options of the gate that offers them. */
enum EditorCall: string
{
case PublishAsIs = 'publish_as_is';
case OneMoreRound = 'one_more_round';
case Abandon = 'abandon';
}
/** A node's return type is its output schema; each property name is the channel it lands in. */
final readonly class GradeReport
{
public function __construct(public string $feedback, public Verdict $verdict)
{
}
}
#[Operation(name: 'essay:grade', description: 'Judge the draft against the rubric.')]
#[Reads(externality: Externality::ThirdParty)] // a read is not automatically harmless: the draft leaves the machine
final readonly class Grade
{
public function __construct(
#[Because('The rubric to judge against')] public string $rubric,
#[Because('The draft under review')] public string $draft,
) {
}
public function run(Model $model): GradeReport
{
return $model->judge($this->rubric, $this->draft);
}
}
#[Graph(name: 'essay:review', description: 'Draft an essay against a rubric until it passes, then publish it.')]
#[Start(Write::class)]
#[Edge(from: Write::class, to: Grade::class)]
#[Route(when: Verdict::Passed, to: Publish::class)]
#[Route(when: Verdict::Failed, to: Write::class, atMost: 3, thenAsk: EditorCall::class)]
#[Ask(
EditorCall::class,
of: 'editor',
because: 'Three drafts in a row failed the rubric. A human decides whether to publish as is, pay for one more round, or close it.',
waivable: false,
)]
#[Route(when: EditorCall::PublishAsIs, to: Publish::class)]
#[Route(when: EditorCall::OneMoreRound, to: Write::class)]
#[Route(when: EditorCall::Abandon, to: Abandon::class)]
final readonly class EssayReview
{
public function __construct(
#[Target] #[Because('The essay being written — the human names it')] public string $title,
#[Because('The rubric every draft is graded against')] public string $rubric,
public string $draft = '',
public string $feedback = '',
#[Appends('draft')] public array $drafts = [],
public ?string $verdict = null,
) {
}
}
use Milpa\Orchestrator\ProcessDefinition;
use Milpa\Workflow\Entities\GateDefinition;
use Milpa\Workflow\Entities\StateDefinition;
use Milpa\Workflow\Entities\TransitionDefinition;
use Milpa\Workflow\Enums\ApprovalPolicy;
$draft = (new StateDefinition())->setDomain('publish_post')->setCode('draft')->setLabel('Draft')->setIsInitial(true);
$review = (new StateDefinition())->setDomain('publish_post')->setCode('review')->setLabel('In review');
$published = (new StateDefinition())->setDomain('publish_post')->setCode('published')->setLabel('Published')->setIsTerminal(true);
$gate = (new GateDefinition())
->setDomain('publish_post')->setCode('review_gate')->setName('Editorial review')
->setRequesterRole('author')->setApproverRole('editor')
->setApprovalPolicy(ApprovalPolicy::SINGLE);
$submit = (new TransitionDefinition())->setDomain('publish_post')->setCode('submit')->setFromState($draft)->setToState($review);
$approve = (new TransitionDefinition())->setDomain('publish_post')->setCode('approve')->setFromState($review)->setToState($published);
$reject = (new TransitionDefinition())->setDomain('publish_post')->setCode('reject')->setFromState($review)->setToState($draft);
$approve->addGateDefinition($gate); // both outcomes share the SAME gate: one checkpoint,
$reject->addGateDefinition($gate); // two options (approve | reject)
$definition = new ProcessDefinition([$draft, $review, $published], [$submit, $approve, $reject]);
use Milpa\EventStore\FileEventStore;
use Milpa\Eventing\EventDispatcher;
use Milpa\Orchestrator\HumanGate;
use Milpa\Orchestrator\ProcessDefinitionRegistry;
use Milpa\Orchestrator\ProcessRunner;
use Milpa\Orchestrator\Tools\ProcessInstantiateTool;
use Milpa\Orchestrator\Tools\ProcessListPendingApprovalsTool;
use Milpa\Orchestrator\Tools\ProcessSubmitDecisionTool;
use Milpa\ToolRuntime\Contracts\ToolContext;
use Psr\Log\NullLogger;
$store = new FileEventStore('/tmp/posts.jsonl'); // the append-only log
$dispatcher = new EventDispatcher(new NullLogger()); // milpa/events
$dispatcher->subscribe('process.terminal', function (string $name, array $payload): void {
// Reaching `published` runs the real domain effect HERE — the engine itself touches no
// domain entity. $payload = {instance_id, final_state, context}.
});
$registry = new ProcessDefinitionRegistry();
$registry->register('publish_post', $definition);
$gate = new HumanGate(new YourDecisionSurfaceFactory()); // a milpa/live surface, consumer-supplied
$runner = new ProcessRunner($dispatcher);
$instantiate = new ProcessInstantiateTool($store, $gate, $runner, $registry);
$instantiate->setCurrentContext(ToolContext::mcp('req-1', 'agent:author', ['*']));
$list = new ProcessListPendingApprovalsTool($store, $gate, $registry);
$submit = new ProcessSubmitDecisionTool($store, $gate, $runner, $registry);
use Milpa\Orchestrator\ProcessInstance;
// 1. Instantiate — auto-advances draft --submit--> review and PARKS at the human gate.
$started = $instantiate->instantiate('publish_post', ['post_id' => 42]);
$instanceId = $started->data['instance_id'];
$started->data['current_state']; // 'review' — the runner stopped at the gate, not past it
// 2. The gate is pending; its options are projected 1:1 from the process's OWN transitions.
$pending = $list->list()->data['pending'][0];
$pending['assignee']; // 'editor'
$pending['options']; // ['approve', 'reject']
$gateId = $pending['gate_id'];
// 3. An editor — NOT the author — resolves it. Who decides is the AUTHENTICATED caller the host
// puts in the ToolContext, never an argument: the tool takes no `principal`, so the author
// cannot answer by naming someone else. Submitting under the author's own context returns
// SELF_APPROVAL_FORBIDDEN; submitting with no context returns UNAUTHENTICATED; and a transport's
// placeholder (`stdio`, `local-shell`, a nameless `mcp`) names nobody, so it returns UNVERIFIED_APPROVER.
$submit->setCurrentContext(ToolContext::mcp('req-2', 'human:editor', ['*']));
$done = $submit->submit($instanceId, $gateId, 'approve');
$done->data['current_state']; // 'published' — auto-advanced past the gate to terminal;
// `process.terminal` fired exactly once.
// 4. State is a projection: a FRESH store + handle over the SAME log reconstructs it, no cache.
$replayed = new ProcessInstance($instanceId, $definition);
$replayed->currentState(new FileEventStore('/tmp/posts.jsonl')); // 'published'
Loading please wait ...
Before you can download the PHP files, the dependencies should be resolved. This can take some minutes. Please be patient.