zidane/ durable
The durable runtime seam, so an agent run continues after its process crashes.
import { … } from 'zidane/durable'src/durable/index.ts
Use this entry point to type or build a durable runtime. Pass one to createAgent({ durable, sessionId }) and the loop records each model call and tool call as a step, so a run continues after a crash instead of starting over: onInterrupted: 'resume' makes run() continue it first, or call agent.resume(), which resolves null when there is nothing to continue. agent.suspend() stops a run so the next process resumes it. createMemoryRuntime() keeps its steps in memory, for tests. For a file on disk use zidane/durable/sqlite, and for a Restate handler zidane/restate. See crash recovery.
import { createAgent } from 'zidane'
import { createMemoryRuntime } from 'zidane/durable'
import { anthropic } from 'zidane/providers/anthropic'
const runtime = createMemoryRuntime()
const agent = createAgent({ provider: anthropic(), durable: { runtime, onInterrupted: 'resume' }, sessionId: 'job-42' })
await agent.run({ prompt: 'Summarize the README.' })
console.log(runtime.mode)Functions#
createMemoryDurableStateFunction#
An empty state for memoryRuntime.
function createMemoryDurableState(): MemoryDurableStateReturns
createMemoryRuntimeFunction#
A memory runtime over a fresh state: what an agent uses when it is given no runtime.
function createMemoryRuntime(): DurableRuntimeReturns
memoryRuntimeFunction#
A memory runtime over state. Several runtimes may share one state (two
"processes" on one database); the state itself is the durable part.
function memoryRuntime(options: { state: MemoryDurableState }): DurableRuntimeParameters
| Name | Type | Description |
|---|---|---|
| options | { state: MemoryDurableState } | Runtime options. |
Returns
nestedCallFunction#
call of the nth call (from 1) made inside parentCall: a codemode
script's inner calls, in the order the script makes them. Same as the
nested call ids the loop already mints (cm1/1, cm1/2).
function nestedCall(parentCall: string, n: number): stringParameters
| Name | Type | Description |
|---|---|---|
| parentCall | string |
|
| n | number | Position of the nested call, from 1. |
Returns
string<parentCall>/<n>.
parseStepKeyFunction#
Split a key built by stepKey. Throws on anything else.
function parseStepKey(key: string): StepKeyPartsParameters
| Name | Type | Description |
|---|---|---|
| key | string | The key to split. |
Returns
StepKeyPartsIts parts, unescaped.
stepKeyFunction#
Build the key of a step. Throws on an empty run or call, or a
negative or fractional turn or attempt.
function stepKey(parts: StepKeyParts): `${string}:${number}:${string}:${number}`Parameters
| Name | Type | Description |
|---|---|---|
| parts | StepKeyParts | The key's parts. See |
Returns
`${string}:${number}:${string}:${number}`The escaped run:turn:call:attempt key.
Classes#
RunLeaseHeldErrorClass#
A run could not start: another live process holds its session's lease
(claimRunLease). owner: that process, when the lease names it.
class RunLeaseHeldError extends ErrorConstructor
new RunLeaseHeldError(sessionId: string, owner?: RunOwner)Parameters
| Name | Type | Description |
|---|---|---|
| sessionId | string | The session whose lease is held. |
| owner? | RunOwner | The process holding it, when the lease names it. |
Returns
Properties
| Name | Type | Description |
|---|---|---|
| owner?readonly | RunOwner | The process holding it, when the lease names it. |
| sessionIdreadonly | string | The session whose lease is held. |
RunSuspendedErrorClass#
Agent.suspend stopped the run (a host draining before it exits). A
control error: no step records it as failed, the run keeps its resumable
mark, and Agent.resume continues it from its last committed step.
class RunSuspendedError extends ErrorConstructor
new RunSuspendedError()Returns
StaleEpochErrorClass#
A write guarded by a lease epoch matched no row: another process claimed the run since. The run stops at once, with no final save.
class StaleEpochError extends ErrorConstructor
new StaleEpochError(sessionId: string, epoch: number)Parameters
| Name | Type | Description |
|---|---|---|
| sessionId | string | The session whose run moved to another process. |
| epoch | number | The epoch the refused write was guarded by. |
Returns
Properties
| Name | Type | Description |
|---|---|---|
| epochreadonly | number | The epoch the refused write was guarded by. |
| sessionIdreadonly | string | The session whose run moved to another process. |
Constants#
MODEL_CALLConstant#
call of the model request of a turn.
const MODEL_CALL = 'model'RUN_TAKEN_OVER_MESSAGEConstant#
Shown when a run's lease moved to another process and its writes were refused.
const RUN_TAKEN_OVER_MESSAGE = 'this run continued in another process'Interfaces#
AwakeableOptionsInterface#
Options of RunScope.awakeable.
interface AwakeableOptionsProperties
| Name | Type | Description |
|---|---|---|
| key | `${string}:${number}:${string}:${number}` | Step key of the wait. A run resumed after a restart asks again with the same key and gets the same awakeable back, with any resolution that arrived while no process was running it. |
| timeoutMs? | number | Deadline, stored as an absolute time when the awakeable is created. Unset or non-positive: no deadline. Ignored when the key already has an awakeable: the deadline stays the stored one. |
AwakeableRecordInterface#
A stored awakeable (the awakeables table of the SQLite adapter). id is unique; (sessionId, key) finds it again on resume.
interface AwakeableRecordProperties
| Name | Type | Description |
|---|---|---|
| deadline? | number | Absolute deadline (epoch ms), when the awakeable has one. |
| id | string | The id a resumer resolves it with. |
| key | `${string}:${number}:${string}:${number}` | See |
| resolved | boolean | A resumer resolved it. |
| sessionId | string | Session of the run that waits. |
| value? | JsonValue | What the resumer sent, once |
DurableAwakeableInterface#
A wait a resumer ends: its id, and the promise that settles with the outcome.
interface DurableAwakeable<T>Type parameters
| Name | Description |
|---|---|
| T |
Properties
| Name | Type | Description |
|---|---|---|
| id | string | Hand this to the resumer ( |
| promise | Promise<AwakeableOutcome<T>> | Settles when a resumer resolves the awakeable or its deadline passes. |
| settled? | { value: T } | The resolution, when the key's awakeable was already resolved as it was asked for (a resumed run whose answer arrived meanwhile): the caller can skip announcing the wait. Unset where the runtime cannot tell (a journal). |
DurableInspectorInterface#
Read access to what a runtime stored, for tests and diagnostics.
interface DurableInspectorProperties
| Name | Type | Description |
|---|---|---|
| awakeables | () => Promise<AwakeableRecord[]> | Every stored awakeable. |
| partials | (runId?: string) => Promise<StreamPartialRecord[]> | Stream partials, in write order. |
| steps | (runId?: string) => Promise<StepRecord[]> | Step records, of one run or of all. Pruned records are gone. |
DurableLeaseInterface#
Who runs a session's run, fenced by an epoch. The agent claims the lease
when a run starts (run or resume) and passes the epoch to
DurableRuntime.openRun. Every claim bumps the epoch; every turn,
step and queue write of the holder is guarded by it, so the writes of an
owner that lost the lease (a laptop waking from sleep) are rejected
(StaleEpochError) instead of interleaving with the new owner's.
Absent on runtimes whose platform already guarantees a single writer.
interface DurableLeaseProperties
| Name | Type | Description |
|---|---|---|
| claim | (sessionId: string, options: { onStale?: (error: StaleEpochError) => void; owner?: RunOwner; staleAt: number }) => Promise<LeaseClaim> | Take the session's run. Succeeds when no live owner holds it: no lease,
one released, one held by an owner known dead ( |
| epoch | (sessionId: string) => Promise<number | null> | Current epoch of the session, |
| release | (sessionId: string, epoch: number) => Promise<void> | Give the lease up and lift this runtime's fence. A stale |
| renew | (sessionId: string, epoch: number) => Promise<boolean> | Keep the lease alive. |
| resumable | (options: { sessionId?: string; staleAt: number }) => Promise<ResumableRun[]> | Top-level runs to resume: still marked resumable ( |
DurableOptionsInterface#
AgentOptions.durable in full: the runtime, and what run() does with a
run a process death left unfinished in the session.
interface DurableOptionsProperties
| Name | Type | Description |
|---|---|---|
| onInterrupted? | 'resume' | 'heal' |
|
| runtime | DurableRuntime | The runtime every run of the agent goes through. |
DurableQueueEntryInterface#
Minimum shape of a queued prompt; the chat host's QueuedEntry fits it.
interface DurableQueueEntryProperties
| Name | Type | Description |
|---|---|---|
| id | string | The entry's id. |
| text | string | The prompt text. |
DurableQueueStoreInterface#
The type-ahead queue of a session, kept across a crash. Wraps the
session-metadata copy the chat host already keeps (zidane.queue).
interface DurableQueueStore<E extends DurableQueueEntry = DurableQueueEntry>Type parameters
| Name | Type | Default | Description |
|---|---|---|---|
| E | extends DurableQueueEntry | DurableQueueEntry |
Properties
| Name | Type | Description |
|---|---|---|
| read | (sessionId: string) => Promise<E[]> | The stored queue, oldest first; empty when none. |
| write | (sessionId: string, entries: readonly E[]) => Promise<void> | Replace the stored queue. An empty list clears it. |
DurableResumeOptionsInterface#
Options of Agent.resume.
interface DurableResumeOptionsProperties
| Name | Type | Description |
|---|---|---|
| run? | Omit<AgentRunOptions, 'prompt' | 'signal' | 'resumeRunId'> | Per-run options of the resumed run, as its |
| runId? | string | Run to resume. Default: the session's last run that did not finish. |
| signal? | AbortSignal | Aborts the resumed run, as |
DurableRuntimeInterface#
What the agent loop needs from a durable runtime. See the module docs.
interface DurableRuntimeProperties
| Name | Type | Description |
|---|---|---|
| findAwakeable? | (sessionId: string, key: `${string}:${number}:${string}:${number}`) => Promise<AwakeableRecord | undefined> | The stored awakeable of |
| isControlError | (error: unknown) => boolean | Errors the runtime uses to stop or suspend a run: the loop rethrows them untouched instead of reporting a failed call. |
| lease?readonly | DurableLease | Who runs each session's run. Absent where the platform guarantees one writer. |
| modereadonly | DurableMode | How the runtime recovers a run, see |
| openRun | (options: OpenRunOptions) => Promise<RunScope> | Open a run's scope. Handlers are per scope, never shared with another run. |
| resolveAwakeable | (id: string, value: JsonValue) => Promise<boolean> | Resolve an awakeable, also while no process runs its run (the run gets
the value when it resumes). Call it from the resumer's side: another
process or an ingress client, never the run that waits. In
|
| storesreadonly | DurableStores | Where a run persists its session, tool state, queue and submissions. |
DurableStoresInterface#
Everything a run persists, behind one runtime so it commits consistently.
interface DurableStoresProperties
| Name | Type | Description |
|---|---|---|
| queue | DurableQueueStore | The session's type-ahead queue. |
| session | SessionStore | The session store the runtime commits turns to. |
| submissions? | DurableSubmissionStore | Absent where the platform dedupes requests itself (an ingress idempotency key). |
| toolState | (sessionId: string) => ToolStateStore | Tool state of one session. |
DurableSubmissionStoreInterface#
Client request ids of sends (requestId), so a retried send gets the
first one's outcome. Wraps the session store's send-request book
(claimSendRequest / settleSendRequest).
interface DurableSubmissionStoreProperties
| Name | Type | Description |
|---|---|---|
| claim | (sessionId: string, requestId: string, fingerprint: string) => Promise<SendRequestClaim> | Claim |
| settle | (sessionId: string, requestId: string, result: JsonValue) => Promise<void> | Store the send's result; |
MemoryDurableStateInterface#
What the memory runtime stores: sessions, steps, awakeables, partials, queue, submissions and tool state.
interface MemoryDurableStateProperties
| Name | Type | Description |
|---|---|---|
| clone | () => MemoryDurableState | A copy of the state as it is now. Later writes to either copy never reach the other. |
| inspectreadonly | DurableInspector | Read access to the stored steps, partials and awakeables. |
OpenRunOptionsInterface#
Options of DurableRuntime.openRun.
interface OpenRunOptionsProperties
| Name | Type | Description |
|---|---|---|
| epoch? | number | Lease epoch every write of the scope is guarded by. Default: the epoch this runtime's lease claimed for the session (a spawned child runs under its parent's claim), or none when the session is not claimed here. |
| handlers | StepHandlers | The run's handlers. They close over its options (model, tools), so they live and die with the scope. |
| runId | string | The run's id, unique within its session. |
| sessionId | string | Session of the run. |
ResumableRunInterface#
A top-level run another process held and stopped renewing, still marked resumable.
interface ResumableRunProperties
| Name | Type | Description |
|---|---|---|
| epoch | number | The session's lease epoch when the run was listed. |
| runId | string | The run to resume. |
| sessionId | string | The run's session. |
RunScopeInterface#
One run's view of the runtime: its steps, clock and awakeables. Opened by the agent when a run starts or resumes, closed when it ends.
interface RunScopeProperties
| Name | Type | Description |
|---|---|---|
| all | <T extends readonly unknown[]>(values: T) => Promise<{ -readonly [K in string | number | symbol]: Awaited<T[K]> }> |
|
| allSettled | <T>(values: readonly Promise<T>[]) => Promise<PromiseSettledResult<T>[]> |
|
| awakeable | <T = JsonValue>(options: AwakeableOptions) => Promise<DurableAwakeable<T>> | Park until a resumer resolves the awakeable or its deadline passes. Resolves once the awakeable is stored. |
| close | () => Promise<void> | End the scope: its handlers are dropped, later |
| now | () => number | Promise<number> | Wall-clock ms. Journaled in |
| race | <T>(values: readonly Promise<T>[]) => Promise<T> |
|
| runIdreadonly | string | The run's id. |
| sessionIdreadonly | string | Session of the run. |
| sleep | (ms: number, signal?: AbortSignal) => Promise<void> | Wait |
| step | <T = JsonValue>(req: StepRequest, live?: StepLive<unknown>) => Promise<StepOutcome<T>> | Run Timing (the crash cases depend on it):
In What a record found after a restart answers:
|
| storedPartials? | () => Promise<StreamPartialRecord[]> | Checkpoint runtimes that keep stream partials: this run's partials, in
write order. |
| storedSteps? | () => Promise<StepRecord[]> | Checkpoint runtimes: this run's stored step records, in write order.
A resumed run reads them to tell the calls of a batch already done
( |
| storedTurns? | () => Promise<number[]> | Checkpoint runtimes: the loop turns this run's stored step records
cover, ascending (the |
| uuid | () => string | RFC-4122 v4 UUID. Journaled in |
StepCompletedInterface#
A step that has a value: it ran now, or its done record answered.
interface StepCompleted<T>Type parameters
| Name | Description |
|---|---|
| T |
Properties
| Name | Type | Description |
|---|---|---|
| replayed | boolean | Answered from a record, the handler did not run. |
| status | 'completed' | Discriminant of |
| toolState? | readonly ToolStateOp[] | Tool-state ops the step staged, live or from its record. The runtime already committed them with the record; the loop applies them to its hydrated view and never commits them again. |
| value | T | The handler's result, or the form rebuilt from the record when replayed. |
StepErrorRecordInterface#
A step's error as stored. A replayed failure rejects with an Error
rebuilt from it: name, message, and the other fields as own
properties, so the loop classifies it (retryable, context exceeded,
retry-after) the way it classified the live error.
interface StepErrorRecordProperties
| Name | Type | Description |
|---|---|---|
| code? | string | Provider or system code ( |
| data? | JsonValue | Anything else the loop classifies on (a retry-after, a limit), as JSON. |
| decision? | JsonValue | The loop's verdict on this failure (retry, fall back, give up), stored with it in checkpoint mode: the rebuilt error has neither the provider's error class nor the stream's progress, so a resumed run takes this verdict instead of judging the rebuilt error again. |
| message | string | The error's |
| name | string | The error's |
| retryable? | boolean | The error's own retry verdict. |
| status? | number | HTTP status of the response that failed. |
StepHandlerContextInterface#
What the runtime hands a running handler besides the caller's StepLive.
interface StepHandlerContext extends StepLiveProperties
| Name | Type | Description |
|---|---|---|
callbacks?from StepLive | unknown | Callbacks the handler drives (a model step's stream callbacks). |
markStarted?from StepLive | boolean | Write the |
onProgress?from StepLive | (progress: StepProgress) => void | Receives the output the handler streams. The runtime may keep it as stream partials. |
signal?from StepLive | AbortSignal | Aborts the handler's work (the run's abort, a cancelled call). |
| stageToolState | (ops: readonly ToolStateOp[]) => void | Stage tool-state ops to commit with this step's |
StepInterruptedInterface#
An unsafe step that started before a restart and never finished, or a step whose record has another fingerprint.
interface StepInterruptedProperties
| Name | Type | Description |
|---|---|---|
| status | 'interrupted' | Discriminant of |
StepKeyPartsInterface#
The four parts of a StepKey, unescaped.
interface StepKeyPartsProperties
| Name | Type | Description |
|---|---|---|
| attempt | number | The loop's own retry index for the step. |
| call | string | What the step is within the turn: |
| run | string | The run id, unique within its session. |
| turn | number | The loop's turn index within the run. |
StepLiveInterface#
The live half of a step. The object itself is never stored or replayed; a
replayed step gets a fresh one from the resumed loop. What a handler
reports through onProgress may be kept by the runtime as stream
partials (see StreamPartialRecord).
interface StepLive<C = unknown>Type parameters
| Name | Default | Description |
|---|---|---|
| C | unknown |
Properties
| Name | Type | Description |
|---|---|---|
| callbacks? | C | Callbacks the handler drives (a model step's stream callbacks). |
| markStarted? | boolean | Write the |
| onProgress? | (progress: StepProgress) => void | Receives the output the handler streams. The runtime may keep it as stream partials. |
| signal? | AbortSignal | Aborts the handler's work (the run's abort, a cancelled call). |
StepRecordInterface#
One stored step (the steps table of the SQLite adapter). Keyed by
(sessionId, key): run ids are only unique within their session.
interface StepRecordProperties
| Name | Type | Description |
|---|---|---|
| error? | StepErrorRecord | What the handler threw, once |
| fingerprint? | string | See |
| key | `${string}:${number}:${string}:${number}` | See |
| kind | StepKind | See |
| replay | StepReplayPolicy | See |
| result? | JsonValue | The handler's result in its JSON form, once |
| runId | string | Run the step belongs to. |
| sessionId | string | Session the step belongs to. |
| status | StepStatus | Where the step is in its lifecycle. |
| toolState? | ToolStateOp[] | Tool-state ops committed with the |
StepRequestInterface#
What a runtime keeps of a step besides its outcome: its key, policy and a description. Not enough to run it again.
Type parameters
| Name | Type | Default | Description |
|---|---|---|---|
| P | extends JsonValue | JsonValue |
Properties
| Name | Type | Description |
|---|---|---|
| fingerprint? | string | What the step does beyond its key: a codemode inner call's tool name and
input hash, since those calls are keyed by their order in the script. A
record whose fingerprint differs means the code took another path since
the record was written: the step answers |
| key | `${string}:${number}:${string}:${number}` |
|
| kind | StepKind | Which of the run's handlers runs it. |
| payload | P | A description of the step (a model name, a tool name and call id), stored with it. The work itself is the live closure in |
| replay | StepReplayPolicy | What a restart does with the step when it started and never finished. |
StreamPartialRecordInterface#
One stored slice of model output streamed by a step that has not finished (stream_partials), keyed by (sessionId, stepKey, seq).
interface StreamPartialRecordProperties
| Name | Type | Description |
|---|---|---|
| runId | string | Run of the step. |
| seq | number | Write order within the step, from 0. |
| sessionId | string | Session of the step. |
| stepKey | `${string}:${number}:${string}:${number}` | Key of the model step that streamed it. |
| text | string | Text streamed since the previous slice, never the whole answer so far. |
| thinking? | string | Thinking streamed since the previous slice. |
Types#
AwakeableOutcomeType#
Settlement of an awakeable: the deadline passed, or a resumer resolved it
with value (which may be null).
type AwakeableOutcome<T> = { timedOut: true } | { timedOut: false; value: T }Type parameters
| Name | Description |
|---|---|
| T |
DurableBodyType#
See DurableToolDef.durableBody.
type DurableBody = 'composite' | 'promise'DurableModeType#
How a runtime survives a restart. See the module docs.
type DurableMode = 'checkpoint' | 'replay'LeaseClaimType#
Answer of DurableLease.claim: the new epoch, or who still holds the lease. previous: the session's status as the claim found it.
type LeaseClaim =
| { claimed: true; epoch: number; previous?: SessionData['status'] }
| { claimed: false; owner?: RunOwner; previous?: SessionData['status'] }MemoryDurableInspectorType#
Read access to the stored state, for tests and the conformance suite.
type MemoryDurableInspector = DurableInspectorStepHandlerType#
Runs one step of a kind: the loop's handlers run the closure handed in
StepLive.callbacks. A replay gets the result's JSON form (it is stored
or journaled as JSON); in checkpoint mode a live call gets the value
itself, and a value the JSON form loses is logged.
type StepHandler<P extends JsonValue = JsonValue, R = JsonValue> = (payload: P, context: StepHandlerContext) => Promise<R>Type parameters
| Name | Type | Default | Description |
|---|---|---|---|
| P | extends JsonValue | JsonValue | |
| R | JsonValue |
StepHandlersType#
A run's handlers by kind.
type StepHandlers = Readonly<Partial<Record<StepKind, StepHandler<any, unknown>>>>StepKeyType#
A step key as built by stepKey.
type StepKey = `${string}:${number}:${string}:${number}`StepKindType#
Handler family of a step (the loop's are LOOP_STEP_KINDS). A run passes
one handler per kind to DurableRuntime.openRun. The open string
lets an adapter add its own.
type StepKind =
| 'model'
| 'tool'
| 'gate'
| 'decision'
| 'compact'
| 'reconcile'
| (string & {})StepOutcomeType#
Result of RunScope.step. A step whose handler failed rejects with
that error instead (rebuilt from its record when replayed).
type StepOutcome<T> = StepCompleted<T> | StepInterruptedType parameters
| Name | Description |
|---|---|
| T |
StepProgressType#
Incremental output a handler reports while it runs. Streamed model text is what an adapter keeps as a partial.
type StepProgress =
| { delta: string; type: 'text' }
| { delta: string; type: 'thinking' }StepReplayPolicyType#
What a resumed run does with a step that started but never finished.
safe: run it again (reads, pure computations, model calls).unsafe: never run it again; the loop reports it as interrupted (TOOL_USE_INTERRUPTED_MARKER), saying the effect may or may not have happened.
type StepReplayPolicy = 'safe' | 'unsafe'StepStatusType#
Lifecycle of a step record: written pending before the handler runs (an unsafe step, or one marked started), then done or failed.
type StepStatus = 'pending' | 'done' | 'failed'ToolPreflightType#
What a DurableToolDef.preflight decided: run the body with input, or answer the call with output.
type ToolPreflight =
| { input: Record<string, unknown> }
| { output: string | ToolResultContent[] }