Rejected goes back to the model
A UI tool call is checked on the server first. Valid: it goes to the screen and the turn ends. Invalid: the person sees nothing, and the model gets the reason, with the allowed values, to try again.
When the model calls a client-side tool like “filter”, “sort” or “navigate”, there are two bad defaults. Send every call to the browser, and an invented filter key or a made-up route fails silently on the person’s screen. Show the person every failure, and they read “Cannot navigate to /order-list” for a mistake the model could have fixed itself in one more round.
So the server checks the call against what the page declared before anything leaves. Then it routes the result by outcome.
const check = validateViewToolCall(call, declared); // the page's filter/sort schemas, the route catalog
if (!check.ok) return { rejected: true, reason: check.reason }; // to the model, not the person
if (check.noop) { say(`_${check.reason}_`); return { noop: true }; } // "You're already on this page."
emit("client_tool_use", call.type === "navigate" ? { ...call, to: check.to } : call);
return { applied: true };
// in the loop
const rejection = rejectionOf(result);
if (rejection) return { block: toolResult(id, { error: rejection }) }; // the model sees why, next round
return { client: true };
// …after the round
if (outcomes.some((o) => o.client)) return; // an applied UI action ends the turn
Errors that carry the fix
A rejection is written so the model can correct itself without guessing:
Unknown filter keys: colour. Allowed: status, customer, placedAfter.
"rush" is not in allowed enum: standard, express, overnight.
Cannot navigate to "/order-list": it is not a declared target. Available: "/orders" (Orders), "/customers" (Customers), …
"/orders" matches more than one navigate target. Use the exact path.
Rules
- Every tool call gets a result. That includes an unknown tool, invalid input, or a call the server rejected. Model APIs refuse the next round if a tool call has no matching result.
- Applied ends the turn. Once an action reaches the screen, the turn is over. The browser doesn’t report back mid-turn, so the model’s next chance is the person’s next message.
- Only the canonical form leaves. A navigate can name a route by path or by name. The server resolves it and sends the real path, and the client validates it again.
- Choices are enforced. If a route publishes the values a param can take, a value outside them is a rejection with the list.
What it gives up
The server can only reject what it can check statically. A navigation that fails in the browser, or a page that changed after the request, doesn’t reach the model in the same turn.
Why it works
The person only ever sees actions that will work. The model’s mistakes cost it one round, not the person’s trust. And because the allowed values ride on the error, a second try almost always lands.
How this fits the larger system: The UI is the engine.