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 singlerun_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. Betweenrun 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”. Thefingerprint function computes a stable SHA-256 over the tool name and canonicalized arguments:
resume, each verdict is bound to the exact (tool, args) it was granted for.
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
DurableApproverused outside aDurableAgentrun (with no run scope bound) raisesRuntimeErrorrather 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 newPendingApproval. Keep resuming until status is "completed".
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.