| title | Choosing a run mode |
|---|---|
| description | Four ways to execute the same agent machine, what each one owns, and which to reach for. |
Alpha:
@statelyai/agent2.0 is in alpha. APIs can change between releases; pin an exact version. Feedback: github.com/statelyai/agent.
This page describes the four ways to execute an agent machine and how to choose between them.
An agent machine declares states, requests, and decisions. It does not run itself and does not call a model. A host drives it. There are four hosts to choose from:
runAgent(controlled). The library owns the actor and the run loop.provideExecutorswithcreateActor(uncontrolled). You own the actor and XState drives it.runDurableAgent(durable runtime). The library owns an event-sourced durable loop.- The step path (
getAgentEffects,executeAgentRequest,replay). You own the loop and the persistence.
The machine is the same in all four modes. The same file runs under each one, and the same tests cover it. Only the host changes.
| Mode | You own | The library owns | Durability | Reach for it when |
|---|---|---|---|---|
runAgent |
The call site and the executors | The actor, the run loop, request retries, usage aggregation, traces | Snapshot per settle, plus a replayable result.events log |
Scripts, HTTP handlers, workers, and anything with a request/response boundary |
provideExecutors |
createActor, the actor lifecycle, subscriptions |
Executor binding for agent sources only | Whatever you persist off the actor (getPersistedSnapshot) |
A host that already owns an actor lifecycle, such as React, a Durable Object, or a long-lived process |
runDurableAgent |
Journal persistence and the external event boundary | The durable runtime loop, executor binding, and replay | Event-sourced. Persist entries; resume with an event |
Durable request/response turns without writing the host loop |
| Step path | The loop, the event log, when to persist, the clock | Pure effect derivation, request execution, replay, verification | Event-sourced. Append before continue, resume by replay |
Custom durable engines and crash recovery that cannot re-bill completed model calls |
Each mode reaches the point where the machine waits for an outside event differently:
runAgentsettles with statusidleand returns a snapshot to resume from.provideExecutorsdoes not settle. The actor stays alive in its current state until you send it an event.runDurableAgentsettles with statusidle; persistentriesand resume with them plus an externalevent.- On the step path, you detect idle yourself. When no asynchronous effect is owed, persist and return.
runAgent is the default mode. It binds executors, drives the machine to a settle point, and returns { status, output?, snapshot, events, usage }.
Choose another mode when one of these is true:
- The host already owns an actor and a render loop. Use the uncontrolled mode instead of running a second loop inside it.
- Each turn is a separate process invocation and the standard durable loop fits. Use
runDurableAgent. - You need custom persistence, scheduling, or effect execution. Use the step path.
Everything else uses runAgent with a persisted snapshot, including human-in-the-loop pauses that last days.
import { runAgent } from "@statelyai/agent";
const result = await runAgent(machine, { input, executors });
if (result.status === "idle") {
// resume later, on any process, from result.snapshot
}runAgentsettles with statusdone,idle, orerror. It stops its actor on every settle path, so you always resume from a snapshot.- It descends into invoked child machines and rebinds executors as it goes.
- It aggregates token usage into
result.usageand emits the trace stream throughonTrace. - It returns
result.events, a JSON-safeAgentLogEntry[]that you can pass toreplay.
Two entry points run the same engine as runAgent with a different call shape. They are variants of the controlled mode, not separate modes.
generateResult(machine, options)resolves with the done result and throwsAgentIdleErrorif the machine pauses. Use it when an idle settle is a failure for the caller.createAgentActor(machine, options)returns an actor that survives idle settles. Use it for long-lived sessions such as chat turns, sockets, or device events, where the event log, budgets, and traces persist across turns. Callsession.actor.send(event)to re-open the cycle, andawait session.settled()to resolve at the next quiescence.
Read more about Hosts and executors.
import { createActor } from "xstate";
import { provideExecutors } from "@statelyai/agent";
const actor = createActor(provideExecutors(machine, executors), { input });
actor.subscribe((snapshot) => snapshot.status === "done" && console.log(snapshot.output));
actor.start();provideExecutorsreturns a machine with every agent source bound. The result is a plain XState actor.- There is no run loop and no idle settling. The actor waits in its current state until you send it an event.
provideExecutorsdescends into registered child machines, at any depth, and binds each with its own schemas. A child invoked as a direct object is not bound, the same as underrunAgent.agent.userInputis left unbound. Supply it through the third argument,{ actors }.- To trace an uncontrolled run, pass
onTracetoprovideExecutorsand passtraceTransitions(onTrace)to the actor'sinspectoption. The two produce one merged stream.
Read more about Use in any stack.
import { runDurableAgent } from "@statelyai/agent";
const first = await runDurableAgent(machine, { input, executors });
await store.save(first.entries);
const next = await runDurableAgent(machine, {
entries: await store.load(),
event: { type: "APPROVE" },
executors,
});- The result is
donewithoutput, oridleawaiting an external event. entriesis the complete journal. Persist it after each call and pass it back on resume.- Recorded invoke completions replay without re-running; work still in flight at a crash runs again.
- Use
onEntrywhen the store should persist each append incrementally. - This mode is experimental because it uses XState's experimental durable runtime.
import { getAgentEffects, executeAgentRequest, createReplayEntry, replay } from "@statelyai/agent";
// resume: rebuild the frontier from the persisted log alone
const { snapshot, effects } = replay(machine, entries);
// execute one owed effect, append its completion, then fold it in
const { output } = await executeAgentRequest(effects[0], executors);
const entry = createReplayEntry(machine, entries, effects[0].toDoneEvent(output));
await store.append({ threadId, expectedIndex: entries.length, entries: [entry] });- There is no actor.
getAgentEffectslowers the current frontier into an orderedAgentEffect[]. You resolve one effect, append its completion, and fold it back in. - The event log is the source of truth.
replay(machine, entries)reconstructs the snapshot and the still-owed effects without executing anything. - Append before you continue. An optimistic
expectedIndexappend is the commit point, so two workers on one thread resolve to exactly one winner. - Every owed effect carries a replay-stable
requestIdthat you can use as an idempotency key. - The host owns the clock for
delayeffects and the runtime fortaskeffects. replay(machine, events, { verify: 'strict' })re-checks recorded hashes, so a tampered or diverged log fails with an error.
Read more about The step path, including per-effect handling and known limits.
- No part of the machine is mode-specific. A machine written for
runAgentruns on the step path without changes, and the reverse is also true. - Tests do not depend on the mode.
simulateAgentwalks the step path from a script keyed bysrc, andcreateScriptedExecutorsworks with any host that takes executors. The same assertions cover the machine in every mode. See Testing and verification. - One deployment can use several modes. An HTTP route can use
runAgent, long-running jobs for the same machine can use the step path, and a React view can run it uncontrolled. - Moving between modes changes only the host code around the machine.
- Hosts and executors: the executor contract and the shipped AI SDK adapter.
- Use in any stack: the same machine behind Express, a Durable Object, or React.
- The step path: the per-model-call loop for durable hosts.
- Where state lives: which artifact survives what, in every mode.
- Human in the loop: idle states and resuming with an event.