Skip to content
MCP Five

The GateProject #324 / 51

The deny branch

A denied tool still needs a tool_result. And the wording matters more than it looks.

"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 host synthesises a tool_result carrying the refusal, marked is_error, and hands it back to the loop as though a tool had returned it. The model reads it like any other result and adapts.

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 saysBecause otherwise
a human denied thisthe model treats it as a transient failure
the tool was not runthe model assumes partial completion
do not retry β€” explain what you'd do insteadit 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:

terminal
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:

terminal
  -> 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.

terminal
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.