Skip to main content
Turn on durability with one flag: the agent journals every model and tool boundary, so a crash mid-loop resumes from the exact step it left off — recorded tool results replay instead of re-executing.

Quick Start

1

Enable durable execution

Set durable=True on ExecutionConfig, then capture agent.last_durable_run_id after the run and persist it alongside your business record.
2

Resume after a crash

Set resume_run_id to the saved id and call start() with the same prompt. Recorded tool results replay from the journal — side-effecting tools do not fire again.
The prompt must match on resume. A different prompt raises ValueError("Resume prompt does not match ..."), an unknown id raises ValueError("Cannot resume unknown durable run ..."), and a terminal run raises ValueError("Cannot resume terminal durable run ... (status=...)").
3

Enable in the gateway

Gateway agents journal their turns by default whenever the session store is durable (the shipped default) — no flag needed. Set the flag explicitly only when you want to pin the choice in gateway.yaml:
See Gateway Durable Runs for the full operator surface, including the durable_runs: false and reliability: "off" opt-outs.

How It Works

The agent records one event per boundary. On resume the loop is re-driven from the top; journalled steps return their recorded payload instantly, and only un-journalled steps do real work.

Run Outcomes

Each terminal outcome maps to an auto-finalize state and controls whether resume_run_id clears itself. A retryable failure leaves the run resumable. A non-retryable failure marks it failed and non-resumable. A per-tool NotSafelyResumable outcome does not by itself change the run status; the loop continues and the run finalizes normally on the next iteration.

Configuration Options

Three fields on ExecutionConfig control durability. Agent.last_durable_run_id is the read-side companion.

Idempotency Contract for Tools

Any tool whose signature accepts idempotency_key (or **kwargs) automatically receives a stable per-call key on every invocation. Non-declaring tools work unchanged.
The key shape is "{run_id}:{seq}:{function_name}" — the same on resume, so a tool that dedups on it stays safe even before journal replay records its result. restart_safe and idempotency_key are complementary: restart_safe=True says the tool may be replayed at all; the auto-injected idempotency_key gives it a stable per-call key to dedupe on when it is. See Restart-Safety Contract for Tools below.

Restart-Safety Contract for Tools

A tool can declare whether it is safe to re-run after a crash — on resume, an in-flight tool is only re-executed when this declaration says so, otherwise it surfaces a typed outcome for the operator to reconcile. Declare it on the decorator: True for read-only or idempotent tools, False for anything that sends, writes, charges, or deletes.
Pick the value by asking whether the tool has an external side effect that is not safe to repeat. The class attribute form BaseTool.restart_safe is the equivalent for subclass-style tools. The same declaration also gates the ordinary tool-retry loops: a restart_safe=False tool is executed exactly once even when a transient error would normally be retried. See Tool Retry & Backoff → Retrying Non-Idempotent Tools.

When resume returns NotSafelyResumable

An effectful or undeclared tool that was in-flight at the crash is not re-run — the resumed turn records a typed outcome instead, so the operator can reconcile the external action.
Reconcile the external action out of band (or with an idempotency check) and re-drive the loop, or mark the tool restart_safe=True if it is genuinely idempotent.

Discovering Resumable Runs

When your process restarts and you don’t have the run_id handy, ask the journal directly for runs still running.

Advanced

Framework and tool authors integrating a custom executor can reach the durable context directly through praisonaiagents.agent.durable.
The module also exposes begin_durable_run, abegin_durable_run, end_durable_run, get_durable_run, and DurableRunContext for wiring the journal into a bespoke tool loop.

Common Patterns

Long-running scheduled job that survives a container restart:
Batch of paid API calls that must never double-charge — the recorded receipt replays instead of re-calling:
Pause-for-approval flows survive a restart because the approval decision is journalled — see Durable Approvals.

Best Practices

Capture agent.last_durable_run_id right after start()/chat() and store it next to the order id or ticket id. That’s the value you set into resume_run_id later.
The default ~/.praisonai/runs/journal.db is lost when a container is recreated. Pass an explicit path on a mounted volume so the journal survives restarts.
Opt into the auto-injected idempotency_key so a tool that crashes before recording its result still dedups on retry.
True for read-only / idempotent tools; False for anything that sends, writes, charges, or deletes. Undeclared tools fall back to a name heuristic and fail closed on uncertainty — safe, but noisier than an explicit declaration.
If a resumed turn returns error_type: "NotSafelyResumable", the in-flight side effect was not re-driven. Reconcile the external action, then either mark the tool restart_safe=True (if truly idempotent) or continue the turn out of band.
ToolExecutionError(is_retryable=True) leaves the run resumable; is_retryable=False marks it failed and non-resumable.
Calling stream.close() finalizes the run as cancelled, so aborting a stream never leaves a ghost running row.

Run-State Journal

The SQLite layer under durable runs.

Durable Approvals

Pause-for-approval that survives a restart.

Execution

Agent execution limits and configuration.

Gateway Session Persistence

Persist gateway sessions across restarts.
Enable durable runs for every agent in the WebSocket gateway.

Async Jobs

Persist async jobs and idempotency keys with SqliteJobStore.