Skip to main content
A synchronous approver has to answer immediately. A real human sign-off can take minutes or hours, and no thread can wait that long. Durable approval lets a governed run suspend at the approval point, persist itself, and resume later once a human has decided, without blocking anything.
Durable approval lives in infy.governance, not the core agent loop. The default create_agent stays synchronous and untouched. This is the opt-in durable path.

How it works

The pieces fit together around a single run_id:
1

A run reaches a gated tool

DurableApprover.review() looks for a prior human decision in an ApprovalStore. If none exists, it records the pending request and raises ApprovalRequired.
2

The run suspends and persists

DurableAgent catches ApprovalRequired, saves the run’s messages to the store keyed by run_id, and returns an AgentResult with status="suspended".
3

A human decides, later

You surface the pending approval to an inbox. Hours later, a human approves or denies it.
4

The run resumes

agent.resume(run_id, decisions) records the verdicts, reloads the persisted messages, and continues from exactly where it suspended.

Run, suspend, resume

The suspend and resume boundary is a plain function call return. Between run and resume, your process can restart, and the run waits in the store.
DurableAgent takes the same arguments as create_agent (model, tools, system_prompt, max_iterations) plus a required governance and store. Its governance must use a DurableApprover, and both must share the same store.

The suspended AgentResult

run() and resume() both return an AgentResult. Durable approval adds three fields that describe a suspended run. A PendingApproval carries the ApprovalRequest (its tool, args, risk_tier, and reason) so you can render exactly what a human is being asked to authorize, and the fingerprint you key the decision by.

The fingerprint binds an approval to the exact action

A verdict is not “approve the run”. It is “approve this tool with these arguments”. The fingerprint function computes a stable SHA-256 over the tool name and canonicalized arguments:
A recorded decision only applies to a call with the same fingerprint. When you call resume, each verdict is bound to the exact (tool, args) it was granted for.
This defeats a TOCTOU (time-of-check to time-of-use) swap. If the arguments change between when a human approves and when the action runs, the recorded verdict no longer matches, so the run stays suspended and the action does not execute. A verdict recorded against a different fingerprint never releases the action.

Fail-closed guarantee

An action that is never approved never runs. There is no default that lets a gated call through.
  • When a gated tool has no recorded decision, the run suspends. The side effect does not run, and no tool result is produced.
  • In a batch that mixes a safe read with a gated payment, every call in the batch is decided before any call executes. A pending approval suspends the whole batch, so the read does not run ahead of the payment’s approval.
  • A denial resumes the run with a blocked tool result the model can see, and the denied action still never executes.
  • A DurableApprover used outside a DurableAgent run (with no run scope bound) raises RuntimeError rather than silently approving.

The ApprovalStore

ApprovalStore is a Protocol. It holds three things per run_id: recorded decisions, pending requests, and the suspended run’s messages.
InMemoryApprovalStore ships for development and tests. It is not durable across processes, so implement the protocol against a real backend (for example Postgres) for production.
When a run completes, DurableAgent calls store.clear(run_id), which drops the persisted state and pending records. InMemoryApprovalStore keeps recorded decisions as a small record of what was decided.

Resuming an unknown run

resume raises KeyError if there is no suspended run for the run_id, for example because it already completed and its state was cleared.

Multi-step runs

A single run can gate more than one action. If a resumed run reaches a second gated tool, it suspends again with a new PendingApproval. Keep resuming until status is "completed".
Every approve and deny is written to the audit chain, so audit.verify() still holds across a suspend and resume.

Governance overview

Policy, risk tiering, approval, and audit on the agent loop.

Human approval

The synchronous approvers: CallbackApprover, AutoApprove, DenyAll.

Audit chain

The tamper-evident hash chain with verify().

Policy

Deny-by-default allowlists and approval requirements.