Skip to content

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.

StatusRunStatusMeaningFinal
queuedQUEUEDStored, waiting for a free slot, a free concurrency key and a claimno
runningRUNNINGClaimed by one engine, which drives the model and writes steps and checkpointsno
waitingWAITINGPaused on a tool call that needs approval; holds no claim and no slotno
interruptedINTERRUPTEDAborted by drain(); claim released, waits for recover()no
completedCOMPLETEDThe model finished; output holds the text of its last answeryes
failedFAILEDA budget, a timeout, a FatalToolError or an infrastructure error ended it; error holds { code, message }yes
cancelledCANCELLEDcancel() ended ityes
dispatch() stores a queued run, deduplicated by idempotency key. The engine claims it and runs it: every LLM or tool step is followed by a checkpoint in the store. A run ends completed, failed or cancelled. A tool call that needs approval pauses the run in waiting with a signed resume token; approving or denying it requeues the run. drain() or a crashed process leaves a run interrupted or with an expired claim; recover() requeues it, and the next execution continues from the latest checkpoint.

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() ----------------> cancelled

Every 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:

KindWritten whennameinput / output
llmafter every model callthe model idoutput: text, tool calls, finish reason, token usage
toolfor every tool result or tool error of that stepthe tool namethe tool input and its output, or error: { code, message }
waitwhen an approval is answeredthe tool namethe 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) and step.appended. It returns an unsubscribe function. A listener that throws is reported to onError and does not affect the run.

ts
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.