Curriculum/Tools & Function Calling
Human Approval
Irreversible calls enqueue for a human. Timeout is a deny. Silence is not consent.
Do not block the Python process on input() in production. Enqueue a ticket: status needs_approval, plus the action, args, and risk.
A human UI accepts or denies. The next model turn sees denied or approved. Timeouts are denials. Silence is not consent. The loop is a client of that queue. It may wait. It may stop. It must not treat “no click yet” as yes.
Risk labels help humans: read, write, irreversible, exfil. Color them in the UI. Do not make “Approve all” the default. Auto-approve only boring reads. Money, mail, and deletes wait.
Approval is part of the tool runtime, not a Slack DM you hope someone sees. Store the pending action, the policy version, the idempotency key, and the human who clicked. If nobody clicks, deny and hand off. Do not auto-approve because the queue is long. A long queue is a staffing problem. It is not a signature.
Timeout is a deny. Silence is not consent.
Irreversible waitsWhat waits
Irreversible writes: refunds, wires, public posts, deletes without trash, production deploys, computer-use Pay clicks. High-exfil reads: bulk export, other-tenant lookups. You can auto-approve get_ticket for the user’s own id and still queue list_all_invoices.
Argument bounds still run before the human sees the ticket. Do not ask a human to approve a refund of -1 or a path with ... Invalid should never reach the queue. Denied by allowlist should never reach the queue. Humans are for legal-but-dangerous, not for garbage.
Show the human the packed args, redacted, plus a diff of what will change if you have it. Do not show a 2 MB log. Do not show the model’s thought as if it were evidence. Show the tool name, the risk, the tenant, the amount.
Timeout, deny, approve
Three outcomes:
- approve — dispatcher runs the function once, with the stored args and key. It does not re-parse a new model message.
- deny — observation
denied: human. Loop stops or explains. World unchanged. - timeout — same as deny. Function never runs. Log
approval_timeout.
Never re-quote the amount after approve. The queued object is the object. If the model emits a new refund while one is pending, it is a second ticket or a collision on the key, not a quiet mutation of the first.
Idempotency still applies after approve. A double click on Approve returns the same receipt.
Classroom enqueue
Search is read and auto-runs. Refund is irreversible and returns needs_approval without a human flag. With human=approve it runs. With timeout it is denied and never refunds INV-18. The function is the policy. The prompt is not consulted.
Run to execute this in your browser. Nothing is sent to a server.
What printed: search runs at once. Refund without a human is needs_approval. Approve runs INV-17. Timeout on INV-18 is denied. The function never ran for silence. That is fail closed.
What goes wrong
input() in a worker. Auto-approve on timeout. Approve-all in the UI. Showing the model’s paragraph instead of the args. Letting the model “approve itself” with a tool named confirm. Mutating queued args after display. Dropping the key. These turn a human into theater.
How to test approval
Assert read tools never enqueue. Assert irreversible without human is needs_approval and handler flag false. Assert timeout does not set the flag. Assert approve sets it once. Assert a second approve is idempotent if you added a key. Snapshot the UI payload: name, risk, redacted args, policy version.
The queued object is the object that runs
When a write is irreversible, the runtime stores name, args, risk, policy version, tenant, actor, and idempotency key, then returns needs_approval. The human UI shows a packed, redacted view of that object. It does not show the model’s paragraph as evidence. It does not show a 2 MB log. Amounts, paths, and destinations must match what will execute. After approve, dispatch runs the stored args once. It does not re-parse a new assistant message. If the model emits another refund while one is pending, that is a second ticket or a key collision, not a quiet edit of the first.
Timeouts are denials. Queue length is not a signature. Auto-approve-all is how you un-invent the feature. Double-click approve must be idempotent: same receipt. Deny and timeout never call the handler. Invalid args and allowlist misses never reach the queue — humans are for legal-but-dangerous, not for garbage.
The loop pauses. It does not spin and emit sibling writes. A “still waiting” path, if you have one, cannot advertise refund. Store who clicked. If nobody clicks, deny and hand off. Approval is product UI plus runtime, not a Slack DM and hope. The pending row should expire into deny on a clock you named in the policy, and that clock should be visible to the human (“expires in 15 minutes”). If the job dies while pending, reload must still show the same stored args — not a reconstructed guess from chat. On-call should be able to list every open approval by tenant without opening a model trace. That list is part of the tool runtime, same as the dispatcher log.
How agents use this
The approval UI is part of the product. The loop should pause on needs_approval, not spin. When the human returns, the next observation is approved or denied. Do not call the model while waiting unless you have a separate “still waiting” path that cannot emit another write.
Store who clicked. If nobody clicks, deny and hand off. Do not auto-approve because the queue is long.
Watch out:Silence is not consent. Timeouts fail closed.
Check your understanding