Run Lifecycle
A run has exactly one of seven statuses. RunStatus is an enum of them, and RunStatusLiteral is the union of its string values, which is the type of run.status: compare against the enum, or against the plain string when that reads better.
| Status | RunStatus | Meaning | Final |
|---|---|---|---|
queued | QUEUED | Stored, waiting for a free slot, a free concurrency key and a claim | no |
running | RUNNING | Claimed by one engine, which drives the model and writes steps and checkpoints | no |
waiting | WAITING | Paused on a tool call that needs approval; holds no claim and no slot | no |
interrupted | INTERRUPTED | Aborted by drain(); claim released, waits for recover() | no |
completed | COMPLETED | The model finished; output holds the text of its last answer, or the parsed value when the agent defines structured output | yes |
failed | FAILED | A budget, a timeout, a FatalToolError, a failed or incomplete model call, an output that does not parse, or an infrastructure error ended it; error holds { code, message } | yes |
cancelled | CANCELLED | cancel() ended it | yes |
Transitions
dispatch ----------------------------> queued
queued -- slot, key and claim -----> running
running -- model finished ----------> completed
running -- budget, timeout, fatal --> failed
running -- cancel() ----------------> cancelled
running -- tool call needs approval > waiting
waiting -- resume() or timeout -----> queued (then running again)
waiting -- cancel() ----------------> cancelled
running -- drain() -----------------> interrupted
interrupted -- recover() ------------> queued (then running again)
running, claim expired (crash) -- recover() --> queued
queued -- cancel() ----------------> cancelledEvery non-final status can come back to queued. The next execution always starts from the latest checkpoint, so work that completed before the pause, the drain or the crash is not repeated.
Steps
While a run executes, the engine appends Step records to the store:
| Kind | Written when | name | input / output |
|---|---|---|---|
llm | after every model call, a failed one included (finish reason error) | the model id | output: text, tool calls, finish reason, token usage |
tool | for every tool result or tool error of that step | the tool name | the tool input and its output, or error: { code, message } (tool_not_allowed for a call to a tool the toolPolicy refuses) |
wait | when an approval is answered | the tool name | the tool input; output { approvalId, approved, reason } |
Steps are append-only and numbered per run (index). They are the run's audit log; the checkpoint, not the steps, is what a resumed run continues from.
Watching runs
engine.on() subscribes to run.updated (any status, usage or wait point change), step.appended and run.delta (model output while it streams, see Streaming). It returns an unsubscribe function. A listener that throws is reported to onError and does not affect the run.
const offRun = engine.on('run.updated', (event) => {
if (event.type === 'run.updated') {
console.log(event.run.id, event.run.status);
}
});
const offStep = engine.on('step.appended', (event) => {
if (event.type === 'step.appended') {
console.log(event.step.runId, event.step.index, event.step.kind);
}
});engine.getRun(id) reads the current state from the store. engine.idle() resolves once nothing is running in this process and nothing more can be started, which is useful in tests and scripts.