Part of Advanced topics. Start with Durable agent runs for the runtime API, session identity, run handles, and lifecycle.
Recovery must distinguish a recorded result from a tool whose external effect is uncertain. Persisted state cannot by itself prove whether an external system accepted a request.
Declare the replay contract
| Policy | Interrupted operation | Application responsibility |
|---|---|---|
| manual (default) | Wait for explicit reconciliation | Verify the outcome, accept a retry, or abort |
| safe | May be invoked again | The operation must be safe to repeat |
| idempotent | May be invoked again with its execution key | The external adapter/service must enforce idempotency |
The runtime persists dispatch intent before invoking a tool, and persists its raw result before result middleware. A recorded result can be reused. A dispatch record without a result leaves an ambiguity window. replay_policy accepts the ReplayPolicy enum (ReplayPolicy.IDEMPOTENT) or the plain strings; the definition stores the string.
Use an execution key
from harnessx import PermissionLevel, current_tool_context
def install_orders(agent, orders):
@agent.tools.register(
permission=PermissionLevel.ASK,
replay_policy="idempotent",
)
async def create_order(customer_id: str) -> str:
"""Create an order for an approved customer."""
key = current_tool_context().execution_key
return await orders.create(customer_id, idempotency_key=key)
Here orders is your application adapter. The service must reject or deduplicate repeated writes with that key. Merely accepting a parameter called idempotency_key is insufficient.
Persist and answer approvals
In AgentRuntime, ASK tools yield persisted pending requests: the run returns needs_input, and result.pending lists PendingTool objects with status == "approval". Present the exact tool and arguments to the user, then record the decision. With resume=True the run continues and the finished RunResult is returned.
async def answer_approval(runtime, result, allow):
pending = result.pending[0]
print(pending.call.name, pending.call.input)
if allow:
return await runtime.approve(pending, resume=True)
return await runtime.decline(pending, resume=True)
approve() and decline() accept a PendingTool or its execution key. A run with several pending tools takes one decision per tool without resume, then a single await runtime.resume(session_id). A declined tool receives the result "Permission denied". Workers never implicitly read stdin. A web or terminal application supplies the approval interaction.
Resolve an uncertain outcome
A PendingTool with status == "uncertain" might already have produced an external effect. resolve_tool() accepts the pending entry or its execution key:
from harnessx import ToolResult
async def record_verified_result(runtime, pending, outcome):
await runtime.resolve_tool(
pending,
result=ToolResult(pending.call.id, outcome),
)
Alternatively, use retry=True to explicitly accept possible repetition, or abort=True to stop the operation. Choose exactly one resolution, then resume the session. Inspect the external system before supplying a verified outcome. A key the run does not have raises UnknownExecutionKey; a decision that does not fit the tool's state raises ResolutionError.
Pause, cancellation, and failures
Pause stops new steps and lets active tools settle. Cancellation is cooperative; Python threads or remote requests may outlive it. Neither operation rolls back completed effects.
Interrupted model calls can be invoked again with a new attempt. The result may differ and unknown charges can mark usage incomplete. The run state remembers how long it has already waited on rate-limit retries and how slow the server has been to start replying, so a resumed run picks up with the same retry budget and first-event deadline. See Retries & fallback models. Storage commit failure is not proof that an external tool failed.
Try the approval workflow
From the SDK checkout:
uv run python examples/06-durability/tool_approvals_and_resume.py
This scripted example exercises SQLite persistence without model credentials. See Flight recorder to capture the boundaries needed for incident inspection.