Approvals
Put a human in front of a tool call, and settle it from your own UI.
Some tool calls should not happen because a model decided they should — issuing a refund, sending an email, deleting a record. For those, the run pauses and asks.
An approval is a pair of events on the session log, so it works the same whether the person deciding is watching a live stream or opening the session an hour later.
The two events
When a gated call comes up, the run appends approval_requested and waits:
| Field | What it holds |
|---|---|
call_id |
The call being decided |
server |
Which server the tool belongs to |
tool |
The tool name |
arguments |
What the model wants to call it with |
args_hash |
The identity a re-fire is checked against |
arguments is carried on the request event on purpose: whoever decides can see what they are deciding without reading back through the log to find the tool_call it follows.
Settling appends approval_resolved, carrying the decision (allow or deny), an optional deny_message explaining the refusal to the model, and resolved_by.
Building the UI
Watch for the request
A client already tailing the session stream sees approval_requested
like any other event. Render it from the event’s own fields.
Show what is actually being asked
Render arguments verbatim. The whole value of an approval gate is that
a person sees the real call, not a summary of it.
Settle it
Post the decision to the call_id the request named.
POST /v1/sessions/{session_id}/approvals/{call_id}{ "decision": "deny", "message": "Refunds over $500 need a manager." }On allow, the call fires and its tool_result follows. On deny, the
model receives the denial — with your message if you set one — and the
turn continues from there.
message belongs to deny alone. Sending one alongside {"decision": "allow"} is rejected rather than ignored, so an allow cannot quietly carry steering text the model never sees.
Decide promptly. The broker holding the call reads your decision on its next tick and answers the engine in line; past that hold, the run requeues and re-fires the call instead — which is what args_hash exists to check.
Denial is not failure
A denied call is a normal outcome, not an error. The model gets a tool result saying so and carries on, which is why deny_message is worth writing: “not approved” leaves the model guessing, while “refunds over $500 need a manager” tells it what to do next.