Resume a parked call¶
A confirm tool behind DeferredApproval pauses with ApprovalPending. To run it
later you need only its approval id: executor.resume(approval_id, ...).
This is examples/hello_approval.py. The test suite runs it.
@aa.tool(confirm=True)
def refund(order_id: str, amount: float) -> str:
"""Refund an order."""
return f"refunded {amount} on {order_id}"
executor = aa.configure(approval=aa.DeferredApproval.sqlite(folder / "approvals.db"))
try:
aa.call(refund, order_id="o-7", amount=500)
except aa.ApprovalPending as pending:
approval_id = pending.approval_id # store it, show it to a reviewer
# ... later, in any process that opens the same file:
result = executor.resume_sync(approval_id, True, decided_by="ana")
print(result.value) # runs once; a second resume is refused
What resume does¶
sequenceDiagram
participant App as your code
participant Ex as executor
participant S as ApprovalStore
App->>Ex: resume(approval_id, decision)
Ex->>S: get(approval_id)
S-->>Ex: record (run_id, call_id, tool, arguments)
Ex->>S: decide(approval_id, decision) (only if pending)
Ex->>Ex: run_call(same call id, same run id, same arguments)
Ex->>S: find, then consume(approval_id)
Ex-->>App: ToolResult
The parked call runs again through the whole pipeline. The port then answers it with the
record's decision, once. The binding is the usual one (Approval): same run,
same call id, same tool, same digest of the original arguments. edited_args in the
decision are what runs.
decision |
Pending record | Decided record |
|---|---|---|
None (default) |
ToolUsageError: resume never approves by itself |
runs with the stored decision |
True, False or an ApprovalDecision |
decided so, then run | runs if it agrees; else ApprovalAlreadyDecided |
A resume that decides must name the reviewer: decided_by= (or the decision's own
decided_by), else ToolUsageError. The store records it with decided_at. reason=
fills in a True or False decision. If another reviewer decides first the same way,
the resume still runs; a different decision raises ApprovalAlreadyDecided. A denial comes back as
a ToolResult with status="error" and kind approval-denied; the tool never runs.
Errors¶
| Case | Raises |
|---|---|
| unknown id | ApprovalNotFound |
| the decision was already used | ApprovalAlreadyDecided(status="consumed"); a new call asks again |
| the store holds another decision | ApprovalAlreadyDecided |
the port has no store (AutoApprove, a callback) |
ToolUsageError naming DeferredApproval(store) |
| a flow-node approval | ToolUsageError: use FlowRunner.resume |
| a record the HTTP tool server parked | ToolUsageError: decide it at POST /approvals/{id}/decide |
a pending record and no decision, or no decided_by |
ToolUsageError; nothing is decided |
ctx= of another run |
ToolUsageError; nothing is decided |
Who may resume what¶
The HTTP tool server marks the records it parks (metadata keys origin, principal
or anonymous). It re-authorizes the invoker before it runs one
(HTTP server security). So
executor.resume, resume_sync and queue envelopes refuse those records, by approval id
and by their run and call ids. DeferredApproval itself never consumes one either, so
run_call with the record's ids is refused too; only the server's own port may. A resume ends the run it opened, so its guard state is
freed.
Shortcuts and mistakes¶
| Write | Means |
|---|---|
DeferredApproval.sqlite(path, *, max_records=10_000, timeout=5.0) |
DeferredApproval(SQLiteApprovalStore(path, ...)): one file, every process on the host |
aa.configure(approval=...) |
make that the default executor's port; returns the executor |
executor.resume_sync(...) |
the sync twin, on the shared loop |
store.decide(id, True) is a ToolUsageError that shows the fix:
store.decide(id, ApprovalDecision(approved=True)), or
executor.resume(id, True, decided_by="ana").
ApprovalPending carries a traceback note on how to resume. Its message, which a model
may see, stays short.
Calling run again on the same tool does not resume: it is a new call with a new call
id, so it asks for a new approval. Use resume, or run_call with the same run id and
call id.