"Deny" sounds like it should mean "send nothing back."
It cannot.
Every tool_use needs exactly one tool_result
This is rule 2 from project #2 collecting its debt.
Every tool_use block in an assistant turn needs exactly one matching
tool_result, or the API rejects the entire request β not the tool, the whole
message.
So a denial has to be a result that says no.
The wording matters more than it looks
Here is where it gets expensive.
Say only "denied" and the model very reasonably tries the same call again. Which gets denied again. Which it tries again.
So the text does three specific jobs:
| The text says | Because otherwise |
|---|---|
| a human denied this | the model treats it as a transient failure |
| the tool was not run | the model assumes partial completion |
| do not retry β explain what you'd do instead | it retries, forever |
That third clause is what turns a refusal into a useful answer rather than a dead end. Here is a real denial, verbatim:
ANSWER: I attempted to smash the jar as you asked, but the operator reviewing this action denied the request, so nothing happened β the jar and its history are still intact. If you want, here's what I can do instead: - Eat or add cookies to change the count (reversible, non-destructive) - Look at the current jar contents - Check the history of what's happened to the jar so far
Nobody wrote that recovery logic. It falls out of a well-worded sentence in a
tool_result.
The two demos
It recovers from failure, and remembers
Eat 500 cookies from the jar.
There aren't 500. Real output:
-> WANTS cookiejar__cookie_jar {"action":"eat","count":500}
βΈ PAUSED. The request ends. The agent is now a row in Postgres.
why it stopped: Eating removes cookies from the jar. Cookies cannot
be un-eaten, so a human should see the number first.
[request 2] human clicks APPROVE
<- RAN Can't eat 500 -- there are only 78 cookie(s) in the jar. Nice try.
ANSWER: Looks like the jar only has 78 cookies in it right now, so I can't
eat 500 β that would leave it in cookie-debt! Would you like me to eat all
78 instead, or a smaller amount?Note that this pauses even though it was going to fail anyway. The gate does not know the jar only has 78 in it, and it should not β a gate that tried to predict outcomes would be a second, worse implementation of the tool.
And the whole run, including the failed attempt, is still in the database, replayable next week.
It stops and asks
Empty the jar completely.
The agent wants to call:
cookiejar__cookie_jar { "action": "eat", "count": 78 }
[ Approve ] [ Deny ]Where does 78 come from? The model called look first β not gated, so it
just ran β read "The jar has 78 cookie(s)", and used it.