Durable Runs
Everyagent.run(...) keeps a record in the memory store you chose (in
memory, SQL, Redis, or MongoDB). Nothing else to configure: with a durable
memory store, the record survives a restart. The in-memory store is for
development.
A run’s record
running, awaiting_approval, completed,
failed, blocked, cancelled), step, token usage, trace IDs, approvals,
and every tool call with its state: started before it runs, then
completed, or interrupted when it was cancelled or timed out (its effect is
unknown). Tool arguments are recorded as a digest.
A record belongs to one request. Requests in the same session keep separate
records, and each run keeps its own working context (the history it started
from and its own messages), so nothing another request does changes it.
Approval: pause and resume
With governance enabled, a policy rule can ask for approval. When nothing answers in the moment, the run pauses: the other calls in that step finish, nothing unapproved runs, andrun() returns the approvals it waits for.
- A decision applies to the exact request it was made for (the capability,
target, tool, and arguments), once. Approving
delete a.txtcannot authorizedelete b.txt. - Approvals expire (the policy’s
approval_expires_seconds, or 24 hours). - Each pause and resume is its own trace segment, linked by the run ID, with
run_suspendedandrun_resumedevents. - To refuse unanswered asks instead of pausing, set
governance_config={"approval_mode": "fail", ...}. An applicationapproval_resolverstill answers asks in the moment, as before.
POST /run/sync (or /run), then
POST /runs/{run_id}/approvals/{approval_id} and POST /runs/{run_id}/resume
(see OmniServe).
Recovering a run after a crash
A live run refreshes a heartbeat on its record. If its process dies (a crash, a deploy, a killed container), the record staysrunning with an old
heartbeat. Once the heartbeat is older than run_lease_seconds (default 60),
any process with the same memory store can continue it:
- Completed tool calls never run again; their results are in the run’s context.
- A call that never started runs.
- A call that started but never finished may or may not have taken effect. It runs again only if its tool is idempotent; otherwise the model is told its outcome is unknown and decides what to do.
- A run whose heartbeat is current is refused: it is still running elsewhere. Only one process can take a run over (saves are versioned).
readOnlyHint or idempotentHint.
Running a finished or failed run ID again starts a new attempt on the same
record; the record keeps a summary of the earlier attempts. For background
agents, keep run_lease_seconds at or below the background lease so a
recovered background run is not refused as still running.
Steering and interrupting a run
Send a running run a message; it arrives as a user message at the run’s next step boundary (never in the middle of a model or tool call):kind: "steering") and trace (run_steered). A message sent to
a run that is waiting for approval or interrupted is delivered when it resumes.
Only callers of your application can steer a run; tool output never can.
Stop a run at its next step boundary, and continue it later:
Sandboxes across a pause
A run that waits for approval, is interrupted, or loses its process does not keep its sandbox container. Workspace files are safe (they are copied back to the workspace after every command). When the run resumes, it gets a fresh sandbox with the workspace files, and the model is told the sandbox was reset: files outside the workspace, installed packages, and running processes are gone.Background runs
A background run whose agent pauses for approval goes toawaiting_approval
(not completed): it keeps no worker lease and still holds its task’s slot.
After the approvals are decided, queue it again; the next attempt continues
the same run:
awaiting_budget the same way (background_run_awaiting_budget); after a
top-up (agent.grant_budget, or POST /runs/{run_id}/budget), queue it
again with resume_run or POST /background/runs/{run_id}/resume. What
every budget covering a run has spent — the run’s own, its session’s, its
agent’s, the application’s — is agent.budget_status(run_id) or
GET /runs/{run_id}/budget.
Over OmniServe: POST /background/runs/{run_id}/resume. Cancelling a waiting
background run works as for a queued one.