Multi-agent composition
Multi-agent systems are ordinary XState actor composition.
Child machines
Register a child machine under actors, invoke it from a state, and handle its typed output with onDone. runToQuiescence recursively binds Agent request actors in registered child machines.
Long-lived actors
Invoke an agent machine at a parent state when it must survive transitions among nested substates. Send it events with XState's sendTo; receive outputs or emitted events through normal XState channels.
Parallel teams
Use parallel states when the branch topology is known in the artifact. Use spawned actors when it is dynamic. Generic fan-out, child collection, actor identity, and persistence belong to XState rather than an Agent-specific abstraction.
Collecting dynamic children
Spawned children are named at runtime, so the machine's event union cannot name their done events. Handle the canonical xstate.done.actor event and branch on actorId. DoneActorEventOf<TLogic, TId?> types that handler against the child's own output, which is XState's DoneActorEvent applied to the logic's OutputFrom.
"xstate.done.actor": ({ context, event }) => {
const { actorId, output } = event as DoneActorEventOf<typeof researchLogic>;
if (!actorId.startsWith("research-")) return undefined;
return { context: { findings: { ...context.findings, [actorId]: output.finding } } };
}Barriers
Spawned children that must all reach a checkpoint before any proceeds use two events: a child→parent status event and a parent→child release event.
- Child. On entry to its checkpoint state, it sends the parent a status event, then waits for
RELEASE. - Parent. It counts status events. When every child has reported (or finished), it sends each held child
RELEASE.
// Child: report, then wait.
ready: {
entry: ({ context, parent }, enq) => {
if (parent) enq.sendTo(parent, { type: "CHILD_READY", id: context.id });
},
on: { RELEASE: { target: "working" } },
},
// Parent: hold until everyone reported, then release them all.
collecting: {
on: {
CHILD_READY: ({ context, event, children }, enq) => {
const ready = [...context.ready, event.id];
if (ready.length + context.finished.length < context.total) {
return { context: { ready } };
}
for (const id of ready) enq.sendTo(children[`worker:${id}`], { type: "RELEASE" });
return { context: { ready: [] } };
},
},
},- Release per child instead. Send
RELEASEin theCHILD_READYhandler itself; the barrier disappears and each child proceeds on its own. - Safety lives in the child. The child takes
RELEASEonly in its checkpoint state, so a stray or forged release cannot skip a step. Check it withcanReachon the child machine. - Preset.
createApprovalBatchMachineis this pattern for tool calls: one child per call, the checkpoint is approval, andexecuteWhenpicks barrier or per-child release. See Preset machines.
Events from one model turn
agent.act sends several events from one tool loop, each applied as it arrives. Use it when every event the model calls is safe to apply; use the approval batch when each needs a verdict first.
See hierarchical teams, swarm handoff, deep research, and game loop agent.