Who Fills In the Form — We Only Sign What the Model Drafted

Who Fills In the Form — We Only Sign What the Model Drafted

Ask when you don’t know. Everyone puts that in the prompt. And the model does ask. When a value is missing, it asks the user.

The problem is upstream. The model decides what to ask about. It asks about whatever appears to be missing. So if it cannot recognize that it does not know something, it does not ask, and if it fills an empty slot itself, there is nothing left to ask about. No matter how strongly the instruction is written, it gets stuck at the same point.

The signature stayed, the drafting moved

A form used to have five steps. The user decided when and under what conditions to act, selected the form, entered the values, checked where those values came from, and confirmed that every required field was filled. Entering the values was also a human task. A person looked at each blank, filled it, and then pressed confirm.

A person still confirms at the end. Approval has not gone away. What moved is the first draft. The model fills in the values inside the form. The person now looks at the form presented to them and signs it.

So the nature of the check changes. It is no longer “is this right?” but “shall we go ahead?” The first requires judgment; the second is simply a pass-through. Ask someone to approve what you have shown them, and you get click-through rather than review.

Worse, the form looks clean. If it looked ragged, you would notice immediately. But a looked-up value and an invented value look identical, so reading the form tells you nothing, and a missing condition never appears on the form at all. Nothing reveals that you are approving a field you never actually checked.

So what needs to be taken back is not the signature. It is the drafting.

The shift

Treat what happens before execution not as validation, but as a place that asks questions using an external list. Seen as a validator, the work becomes about refining the criteria for a verdict. Seen as a place that asks questions, the work becomes about what gets asked.

Solve it by making the model more accurate → An external list decides what to ask, and the model asks the questions.

That one line is the argument of this piece. The existing approach already asks questions. What matters is what decides what to ask.

You can ask for confidence about what is there. You cannot ask for awareness of what is missing. That is why even an experienced surgeon uses a checklist.

What goes into the form is not something to infer, but something to look up, and for that, the only place to look is the user. Better computation does not make missing information exist.

Errors surface. A wrong computation leaves no visible trace, but when you ask, the user says no. A correction path appears.

And this is where the role of the list is settled. If what to ask is not defined externally, that judgment goes back to the model.

Change the goal

Two terms are defined up front. A slot is one line item that has to be confirmed for this execution. An unknown is a slot that remains empty after every designated source has been checked.

Make the goal of the computation the list of unknowns, not the decision of whether to execute. Whether to execute follows from the length of that list; it is not the goal itself. Correcting a wrong answer also becomes a matter of filling one slot.

If the goal is execution, an ordering emerges between slots. If the goal is the list, slots stop waiting on one another except where one is the input to another — a balance cannot be checked before the account number is settled. Adding a step sounds like it would be slower, but it is the opposite: lookups at the same depth run in parallel, they are memory comparisons rather than extra inference calls, and round trips scale with that depth rather than with the number of blanks.

If the goal is the list, the verdict is separated from execution. If execution accepts only a recorded verdict as its basis, there is no path that runs without one.

The rest follows from here: there has to be a list of what is needed, and it has to be divided by where it lives.

So what list

There are three ways execution goes wrong today.

  • Wrong execution — the value was wrong. An invented account number, an invented ID.
  • Uninstructed execution — there were no conditions. It ran without checking authority or timing.
  • Off-target execution — the intent wasn’t caught. That was not what the user meant to do.

So the list needs three things as well: values, conditions, and intent.

Two of these already exist: the tool list and the input schema. Nothing new needs to be built. The additional information goes here.

Values alone aren’t enough

The input schema holds values, and nothing else. But conditions also have to be filled before something can run. Is the balance sufficient? Does the recipient exist? When does this happen? Under what circumstances? Is there authority? Have safety considerations been addressed? This is where every argument is present and every type checks out, yet the action still must not run.

Put values and conditions on the same list, and there is one way to handle both. Both are slots; each is either filled or not. No separate machinery for conditions is needed.

There is no need to ask anyone to write conditions from scratch. Providers already write them in the tool description. There is simply no way to confirm that the model recognized that free text and acted on it.

Intent becomes a slot in the same way: what the user calls the action, and what change the user wants. If a tool is chosen while these remain empty, the result is an off-target execution.

A name is a label; what a tool can do is separate. One provider may call “turn off the living room light” turn_off_light, another may call it set_device_power, while light_control might only adjust brightness. So matching has to be based on whether the tool can produce the state change the user wants, not on matching names.

So here is how the list is organized

The axis for dividing the slots is who can answer them.

Fixed checklist — attached to every execution.

  • Which tool to choose
  • Whether the execution conditions are met — the timing and the circumstance
  • What the user calls this action

Provider checklist — differs per tool.

  • Required fields, types and formats, pre-execution confirmation, prohibiting conditions, extra-approval conditions
  • What changes if it runs — the state change this tool can produce

User checklist — differs per user and environment.

  • Intent, current context, execution limits, pre-execution confirmation, preferences

The fixed side is intent and context. The provider and user sides are values and conditions.

Where to look

A lookup is only a lookup if each slot has a designated source.

The user’s answer → the instruction → preset values → observed values → prior state

This is a search order, not a trust ranking. It does not mean that earlier sources are more reliable. It means that when an answer is found, you stop there.

If it is still empty after checking every source, it is an unknown. It is not the model declaring that it does not know. It is what remains after the search ends. If the value is needed, ask the user.

Needed values get answered by the user. No inference.

Telling it to exclude won’t work, giving it places to look will

The instruction “search, but leave out what you inferred” does not work. What was left out is not visible from outside, and the instruction cannot be carried out in the first place. Since a value that was retrieved and a value that was invented look the same, asking the model to leave things out amounts to asking it to classify its own output after the fact, and that classification is inference again.

So it has to be a whitelist, not a blacklist. Not: look at everything and subtract the inferences. Instead: define what may be looked at, add one item at a time, and record the source. Then nothing needs to be classified.

The same goes for the slot list. Not “don’t omit anything you need to confirm,” but writing down everything to be confirmed. An instruction not to omit anything only works for someone who knows what they omitted, and that is precisely what the model cannot do.

No result and unknown are different

The model records what it finds at the source it checked, and if nothing is there, that is no result. They are two outcomes of the same action, not two different judgments. Unknown is the state that remains outside, after all those reports come in.

Unknown should not be part of the model’s vocabulary. Make unknown a legitimate entry and the model gains one more answer it can choose. It has to be able to write “no result,” because when there is nothing to write, it invents. Filling a blank when one is encountered is trained behavior, not a defect, so it does not disappear simply by being forbidden. What stops invention is not a prohibition, but a place to say there is nothing.

How far does the instruction reach

Tool use is rarely decided by a single instruction. The user starts with a vague request, a few exchanges narrow down what they are trying to do, and the tool is settled at the end. So the scope of the conversation is not a single exchange in which a blank is asked about and answered. It is the entire interaction leading up to the point where tool use is decided.

That also defines the scope of the instruction. If the instruction used as a value source is limited to the last prompt, an account number mentioned three turns ago has nowhere to be found. The instruction therefore covers the entire conversation, and the number of values that do not need to be asked for again grows accordingly.

Split it into principles and code

A principle can only be used where code can catch a violation. Anything whose violation would otherwise go unnoticed has to live in code.

  • Take slots from the list, values from lookup, and what is absent from the user. Do not assemble.
  • Recognize when it was requested and what it was called, then decide the tool.
  • If the timing is unclear, do not default to immediate execution. Ask.
  • If more than one candidate remains, do not show the tool names. Ask the user to clarify the action.
  • Do not invent values; search in the fixed order. Point to the location, not the value.
  • Values that will be used again should be recorded with the user’s consent.
  • What is recorded should be looked up, not asked again. Do not retrieve it from memory.
  • Ask about blanks in one batch, naturally.

A principle is not a prompt. A sentence written into a prompt gets diluted as the conversation grows, and whether it has been lost is not visible from outside. A principle should sit outside the prompt, like the list, and be applied every time a tool is involved.

Note. Tool selection is not caught by code. If the model narrows to one without asking back, that tool is on the list and has the required capability, so it passes every later stage. There is no way to move this into code, which is why it remains a principle and a residual risk. State changes also have to be written in the description rather than the schema, so this part relies more heavily on confirming user intent. Recording what the user calls the action is one way to help.

Code — break these and nothing shows from outside.

  • Build the list and count it.
  • Check the pointed-at location to confirm that the value is actually there.
  • Read the labels and enforce them. Bind approval to the {slot, value} pair so that the approval becomes invalid if the value changes.
  • Record the verdict, and let execution accept only that record. Record what was blocked as well. If you keep only what executed, what caused the block disappears from the log.

All the code knows is form. Which slots are needed and what each label means are both data.

What is left for the model

The model looks up values, code counts what is empty, and what remains gets asked in conversation. Only the remainder goes to conversation, so the number of questions does not multiply.

Where it comes from
Value Lookup, or the user
Slot The list
Verdict Counting
Timing · the user’s words The user makes it, the model confirms it. If it can’t be confirmed, conversation finds the common ground
Which tool Matched from the list. What is not on the list can’t be chosen

The making step disappears. Not a constraint against inventing, but no room left to invent in. The shape of the execution is not assembled. Once the list is full, it is already there.

The record produces audit and improvement

Execution starts from a record. That record contains every answer. Which field was empty, what was filled in incorrectly, which required field did not exist — a record from the moment of execution remains.

  • A field empty because the user did not say. Asking resolves it.
  • A field absent because the provider did not declare it. Asking does not resolve it. You cannot ask the user what this tool’s preconditions are.

Right now, every anomaly in an agent lands in front of the agent developer. The cause may be an incomplete tool definition. Attach a source to the empty field and that case can be sent to the provider, and it remains in a form that can be sent. Responsibility is not being moved. The place where it already sat is being made visible.

What needs to be fixed is not only inside the agent.

Everything to this point is required. The two sections below are about filling the same list from different sources, and skipping them leaves everything above running as it is.

Internal rules

Today, internal rules live partly in prompts, partly in branches inside tool wrappers, and partly only as convention. The problem is less the scattering itself than what it makes impossible, and the fact that correct operation cannot be confirmed.

  • Changing one rule requires a deployment.
  • You cannot ask which rules applied to a given execution.
  • You cannot explain why that case landed in the approval queue.

Once the list is structured this way, a rule you set for yourself can be added as one line in the same place. No separate code is needed to enforce the rule. It means the code for that rule stops existing altogether.

Policy bodies, companies, and regulators are the same in kind, so they can be added under the provider list without a separate axis. These are mostly conditions rather than values.

Provider conditions, proposed as a minimal implementation

Nothing new needs to be written on the provider side. What is already in the tool description simply becomes one line on the list. Once it is separated from execution, the answer “the condition was not met” can exist, and only then can the model verify the condition.

  • [required] — subject to source checking. If it is not in state, it goes on the unknown list.
  • [confirm] — a slot that cannot be filled without user approval.
  • [notice] — something the user should know in advance, during the conversation.

The description is already available to the middleware when the tool list is fetched. Since the vocabulary is fixed, extracting the labels is just string parsing.

Once this way of expressing conditions is established, it can then be added to the input schema as a format. Declare what each condition is checked against, and the three-label split can become more precise: whether it is user approval, or which state gets looked up and compared.

Open questions

How far does this apply. Put only irreversible actions through it and let reads pass. If everything is immediate, the timing slot is not needed; if there is only one tool, the tool-selection slot is not needed; and some domains have no reason to hold user conditions. Even when the agent and the tool have the same owner, a list still has to be created, and the per-tool list can sit where the input schema sits.

Won’t this be unnecessary once models get better. That would mean using model performance to fill in absent information, which does not hold. And this structure benefits from improvements as models improve. It is not about fixing the model. It is about not having to.

Why not put the checklist in the prompt and get JSON back? That hands the job of deciding what to ask back to the model. If the model is the one that judges whether a slot is empty, there is no way to verify its report when it says everything is full.

Does this stop the wrong tool from being chosen? Tool selection itself is outside this structure. It only narrows the candidates and forces a clarification; which of the remaining candidates gets picked is still the model’s call. An invented tool is blocked because it is not on the list, and if state changes are declared, tools that cannot perform the requested change get filtered out as well. What remains is choosing the wrong tool among several tools that could all do it. That risk remains.

What if the record is forged? Separating the verdict from execution prevents execution without a verdict. It does not prevent a fabricated verdict.

This builds on what recent frameworks have achieved

Isolating tools and context behind a protocol. Binding output to a schema so that the source comes out with the value. These are capabilities recently established by agent orchestration and frameworks such as LangGraph and MCP. So this is not a criticism of that direction, nor a call to reverse it. It builds on it.

What this proposal claims

This is not a structure designed for safety. The aim is to know what is empty; safety follows from that. A value with no source cannot become an argument, a remaining blank halts execution, and without a verdict there is no path to execution. And when something goes wrong, the cause and the responsibility remain in the record.

Today, when an agent executes incorrectly, the only question available is “why did the model do that?” and there is no answer. Put the list outside, and the question changes. Which slot was empty? Which source failed to produce an answer? At which stage did it stop?

The black box does not open completely. Why that tool was chosen is still not visible. What becomes visible is what went into the decision and what came out. Since what you usually need to know after an incident is not the model’s internal state but where a value came from, even that much answers part of the question.

Asking is something models already do well. Keeping the model from deciding what to ask — that is the one thing that needs to move outside. And the missing parts of the list still have to be filled in.

Two links, for anyone who wants to go further.

Reference implementation. A skeleton showing what this looks like as code — the lookup chain, the checklists, and what the record ends up holding:

It is written as a specification rather than a library, so it refuses to run without the hooks you supply. The repo also carries implementation notes covering the parts this post leaves out.

Earlier version. This started post here:

Reasoning is a strength in conversation, but it becomes a problem in execution. So instead of asking “Did you actually run it?” and only analyzing the outcome, the model needs to have its authority to decide input values taken away, leaving it with computation only. This isn’t a technique for improving accuracy — it’s a condition for execution to be valid at all.

Hmm… for now, I took a quick look around the MCP side of things​:thinking::


After reading the implementation notes too, I think I understand the boundary a little better now.

I would not describe this as a way to make the model more careful. The stronger claim seems to be: the model should not own the authority to decide that an execution input is settled in the first place. The model can still do computation, extraction, conversation, and candidate matching, but unknown is produced by the surrounding state after the permitted lookups are exhausted, rather than by the model reporting its own uncertainty.

With that reading, most of the boundaries I initially wondered about are already explicitly present in the reference skeleton: trusted vs. untrusted instruction segments, the warning against filling userAnswers from LLM parsing, provider descriptions remaining advisory, lookup-hook failures failing closed, deferred execution being re-preflighted, stale measurements, no blind retry after a tool error, tool-selection residual risk, and the future signed decisions + TTL/nonce direction.

So I think the most useful thing I found is narrower:

The core idea seems compatible with current MCP, but inputSchema.required may be a little too narrow to be the thing that defines the external slot list once full JSON Schema is in scope.

I tried two small sanity checks against the current skeleton, plus a minimal deterministic branch resolver. They suggest a separation that might preserve your original model quite well.

Short answers to the open questions

First, for the explicit questions:

How far should this apply?
The current repo’s “irreversible actions” scope looks like a reasonable default to me. I would not make every observational call pay the full cost. MCP’s tool annotations (readOnlyHint, destructiveHint, idempotentHint, openWorldHint) may be useful routing vocabulary, although the MCP tool specification also warns that annotations from untrusted servers should not be treated as authoritative security information.

Would better models make this unnecessary?
I think better models can reduce how often the external layer finds a problem, without replacing the layer itself. Better extraction, clarification, or tool selection changes the probability that the first pass succeeds. It does not change who defines the condition “these things must be settled before this execution is valid.” Those seem like separate axes.

There is active work on making models better at recognizing missing information — for example, ToolSandbox explicitly evaluates insufficient-information cases — but that is still different from making the set of required checks external to the model.

Why not put the checklist in the prompt and ask for JSON?
That seems useful as behavioral guidance, but if the same model decides both what the slots are and whether they are all filled, the authority boundary moves back into the model. So I think this is the clearest distinction in the proposal.

Does this solve wrong-tool selection?
Not entirely, as you already note. It can eliminate nonexistent tools and reject candidates that fail externally checkable requirements, but selecting the wrong member of a remaining valid candidate set is a different problem. Interestingly, the August 2026 MCP roadmap now calls out degrading tool selection as catalogs grow and is pursuing progressive tool discovery. That looks complementary: discovery can reduce the candidate surface; preflight can govern what happens once a candidate reaches the execution boundary.

What if the verdict record is forged?
I think the important part is not just “sign the verdict,” but bind the verdict to exactly what it authorizes. The current MCP TypeScript SDK has a useful analogue: requestState round-trips through the client, so the SDK treats it as untrusted and recommends integrity protection such as HMAC/AEAD, bound to the principal, originating method/parameters, and an expiry.

For an execution record, the equivalent might eventually bind something like:

principal
+ server/tool identity
+ exact final arguments (or canonical digest)
+ checklist/slot result
+ schema/policy version
+ expiry
+ nonce

Your repo already has signed decisions / TTL / nonce in the future-work boundary, so I mostly see this as an existing MCP example of the same binding problem.

The main thing I found: required[] is not always the slot list anymore

The current skeleton intentionally makes the enforceable provider checklist:

function getRequiredFields(mcpTool) {
  if (!mcpTool?.inputSchema) return null;
  return mcpTool.inputSchema.required ?? [];
}

and then resolves those fields through the lookup chain.

For simple flat schemas, that maps very cleanly onto the proposal.

But the current 2026-07-28 MCP specification moved tool schemas to full JSON Schema 2020-12. The official Python SDK documentation explicitly shows that this includes oneOf, anyOf, allOf, if / then / else, $defs, local $ref, etc.

It even has a very useful example:

{
  "type": "object",
  "properties": {
    "isbn": {
      "type": "string",
      "pattern": "^[0-9]{13}$"
    },
    "title": {
      "type": "string"
    },
    "author": {
      "type": "string"
    }
  },
  "oneOf": [
    {
      "required": ["isbn"]
    },
    {
      "required": ["title", "author"]
    }
  ],
  "additionalProperties": false
}

The call is valid with either:

isbn

or:

title + author

but the root schema has no required array.

I ran that shape through the current reference skeleton. With the normal getRequiredFields, the provider-field result is effectively:

fields = []
unknown_count = 0
execution_decision = execute

while a JSON Schema 2020-12 validator correctly rejects {}.

So in this case:

unknown_count == 0

does not mean:

a complete legal invocation exists

I do not think this is a counterexample to the external-list idea. It looks more like a useful implementation boundary for the reference skeleton.

If arbitrary current MCP schemas are in scope, I think the same idea could be generalized from:

inputSchema.required
        ↓
slot list

to something like:

schema / provider policy
        +
current grounded partial state
        ↓
externally materialized slots

I’ll use “materialized obligations” below just as shorthand for that current set of slots; I do not mean to replace your terminology.

The important property remains unchanged:

The model still does not decide what is missing.

A tiny branch experiment

I tried a deliberately small materializer that understands only this kind of:

oneOf(required ...)

schema.

No model was involved in branch selection or missingness.

For the isbn OR (title + author) example, it produced:

Already grounded Result
nothing branch unresolved
isbn ISBN branch complete
title author remains
author title remains
title + author title/author branch complete
isbn + title + author oneOf conflict

That seems useful because it preserves the property I think you care about.

For example:

title = "Dune"

does not make a model answer:

“I think the missing parameter is author.”

Instead:

schema
+ grounded partial state
        ↓
title/author branch
        ↓
author is mechanically unresolved

The question then comes from the external remainder.

Likewise, if no unique branch can yet be selected:

{}

the system does not have to invent a branch merely to produce a flat unknown list. It can preserve:

branch unresolved

as an external state too.

This is the one place where I would slightly qualify the statement that the slots no longer relate to one another: within a selected branch they can still be independent, but modern schemas can make the existence of one slot conditional on another choice or value.

That dependency can still live outside the model.

A minimal implementation path could therefore be:

plain root required[]
    -> current behavior

simple oneOf / discriminator
    -> deterministically select a branch from grounded state
    -> materialize that branch's slots

branch still ambiguous
    -> leave branch unresolved
    -> ask rather than guess

unsupported schema construct
    -> hold / fail closed

That seems much cheaper than attempting a general “turn arbitrary JSON Schema into a conversational form” engine.

This also suggests that getRequiredFields(tool) may eventually need partial state

There is a small contract issue behind the branch example.

Right now the extension point is essentially:

getRequiredFields(mcpTool)

But whether author is an obligation may depend on what has already been grounded.

For example:

partial state:
  title = "Dune"

is enough to make:

author

the remaining obligation in the example above.

So, if this schema coverage is desired, the eventual seam may need to look more like:

materializeSlots(mcpTool, partialState)

or:

materializeObligations(schema, partialState)

rather than being purely a property of the tool definition.

The crucial restriction would be that partialState itself contains only values that have already passed the allowed-source rules. Otherwise a model-generated candidate could influence which branch is considered mandatory and the authority would creep back upstream.

So the dependency would be:

allowed-source lookup
        ↓
grounded partial state
        ↓
deterministic schema/policy evaluation
        ↓
current external slot set

not:

model proposes an argument object
        ↓
schema tells us what the model forgot

The first seems much closer to your original principle.

Second sanity check: the slots that block execution are not necessarily all of the arguments

The other small case was simpler.

Suppose a tool is:

search(query, limit=10)

with:

query = required
limit = optional

and the user explicitly says:

search for "agent safety", limit 3

limit should not become an unknown that blocks execution if it is absent.

But if it was supplied, it is still part of the intended call.

The current skeleton resolves only requiredFields into executionState.fields, and executeIfReady() constructs the actual MCP arguments from those fields:

const args = Object.fromEntries(
  executionState.fields.map(f => [f.name, f.value])
);

The code already notes that this assumes a flat "field name = argument key" tool.

I tried the optional-field case too. With:

query = "agent safety"
limit = 3

the resulting tool payload was:

{
  "query": "agent safety"
}

The explicit limit=3 did not reach the final call, because it was not part of the blocking field set.

Again, this looks less like a problem with the external-list idea than a useful separation to make inside the skeleton:

slot / obligation set
!=
final argument set

Maybe:

slots
= things that must be settled for this execution to be allowed

arguments
= everything that will actually be sent to the tool

An optional argument can therefore be absent from the first set and present in the second.

That also seems consistent with your “the shape is already there” point. I do not mean that the model should be given a final assembly step.

I mean the final execution object could be produced mechanically from values that were already settled:

grounded/accepted argument values
        +
resolved external slots
        ↓
complete argument object

followed by:

whole-object JSON Schema validation

before execution.

I think that gives four different checks

The distinction became clearer to me when I tried an invalid ISBN too.

An ISBN value can be present — therefore its slot is not empty — while still failing:

"pattern": "^[0-9]{13}$"

So:

slot resolved

and:

schema valid

are not the same fact either.

That suggests four fairly clean responsibilities:

1. Argument collection
   Where are candidate values allowed to come from?

2. Slot materialization
   Given schema/policy + grounded partial state,
   what has to be settled for this invocation?

3. Provenance / condition gate
   Have those slots actually been settled from permitted sources?

4. Final payload validation
   Does the complete argument object satisfy the complete inputSchema?

Then:

unknown_count == 0

belongs to step 3, while:

JSON Schema valid

belongs to step 4.

The model does not need authority over either result.

For a simple tool, all four stages collapse back into almost exactly what the current skeleton already does.

For a more expressive MCP schema, they stop being identical.

How I would keep this small rather than chase all of JSON Schema

I would probably not make full JSON Schema reasoning a requirement for the first version.

JSON Schema 2020-12 is expressive enough that a general questionnaire generator quickly becomes its own project. The useful part here seems to be defining the supported execution contract rather than supporting every possible schema.

Something like this could be enough:

Supported directly
------------------
flat properties
root required[]
simple constraints

Supported with a branch materializer
------------------------------------
oneOf alternatives based on required keys
explicit discriminator-like cases
simple if/then/else

Not understood by this preflight implementation
------------------------------------------------
complex recursive/composed schemas
ambiguous constructs the materializer cannot reduce

=> hold rather than interpret them with the model

That still gives a very clear rule:

unsupported does not mean zero slots.

The current code already makes the analogous distinction between:

inputSchema missing

and:

required: []

because collapsing those would turn an unavailable schema into an unconditional pass.

This would extend the same fail-closed principle one level further:

schema understood and entails zero obligations
!=
schema obligation structure not understood
Where MCP's new `input_required` mechanism seems to fit

The 2026-07-28 tool specification now allows a tool call to return an InputRequiredResult.

So MCP has gained a standard mechanism for:

call cannot complete yet
        ↓
ask for additional client/user input
        ↓
retry the original call with inputResponses

I think that answers a different question from your list.

MRTR / input_required answers:

How is another input round represented and transported?

Your external slot mechanism answers:

What determines that another input round is required?

So they seem naturally composable.

For example:

schema / policy / grounded state
        ↓
external unresolved slot(s)
        ↓
input_required / elicitation
        ↓
user response
        ↓
re-run slot resolution

In other words, MCP now supplies more of the transport plumbing, while your proposal is about ownership of the unresolved-state decision.

The wrong-tool boundary also looks complementary to MCP's current roadmap

You already mark tool selection as residual risk, and the current skeleton is quite explicit:

Tool selection accuracy never reaches 100%.
This skeleton does not improve selection.

That seems like the right boundary to keep.

The new MCP roadmap separately identifies a scaling problem: when a server exposes many tools, the model pays for a large catalog up front and tool selection gets worse. MCP is therefore starting a progressive discovery effort.

Those two pieces seem to compose:

progressive discovery
        ↓
smaller candidate surface
        ↓
model/caller selects a candidate
        ↓
preflight checks whether this candidate has a valid execution state

Progressive discovery cannot prove that the remaining selection is correct.

Preflight cannot guarantee that the model chose the right member of the candidate set.

But each reduces a different part of the problem.

On the forged-record question: MCP's `requestState` is a useful nearby example

This one is already in your implementation notes, so I would not add much machinery to the skeleton just for it.

But the current MCP TypeScript SDK’s requestState guidance is a useful concrete precedent.

requestState is produced by the server, passes through the client, then returns to the server on a later round. The SDK therefore treats it as untrusted and recommends:

  • HMAC or AEAD integrity protection,
  • binding to the principal,
  • binding to the originating method/parameters,
  • an expiry,
  • rejection when verification fails.

That seems directly analogous to an execution verdict that crosses a trust boundary.

For this system, I would think of the signed object less as:

"execute = yes"

and more like:

this principal
may execute
this exact tool/server identity
with this exact argument digest
under this slot/policy/schema version
before this expiry
with this nonce

Then changing the amount, recipient, tool, or policy after approval invalidates the authorization automatically.

That feels close to your {slot, value} approval-binding rule, just carried through to the whole execution record.

One scope choice I would leave explicit: generated content

There is one question I would leave as a design choice rather than silently generalize.

Consider:

send_email(to, body)

Both may be required in the tool schema.

But there are at least two possible architectures.

A. Preflight governs only execution-grounding inputs

The generated body is produced in an earlier stage:

model drafts body
        ↓
draft is frozen / reviewed
        ↓
artifact ID or content hash
        ↓
execution preflight

Then the execution layer still never asks the model to manufacture an unset execution value. It receives an already-existing artifact as an input.

B. Preflight directly wraps arbitrary MCP argument schemas

Then required alone may not tell the system whether a value is expected to be:

looked up / grounded

or:

deliberately generated

A recent provenance paper, Safeguarding LLM Agents from Misalignment through Provenance Analysis, uses the terms derivable parameters and generative parameters for a related distinction, including an email-recipient vs. email-body example.

I would treat that only as useful neighboring vocabulary, not as something this design necessarily needs to adopt.

If the intended scope is “execution parameters must already exist outside model generation,” option A may actually preserve the architecture more cleanly.

A few things I initially wondered about, but the repo already covers

After reading the implementation notes, I would not treat these as missing pieces:

  • trusted user text is separated from forwarded/tool-result/untrusted text;
  • an LLM-parsed value must not simply be injected as a highest-priority user_answer;
  • provider descriptions are advisory rather than automatically enforceable;
  • server annotations are self-reported and are not trusted by default;
  • a lookup hook throwing is different from a source simply returning no result;
  • deferred executions are re-run at trigger time rather than executing an old preliminary verdict;
  • measured state can become stale;
  • a failed tool call is not blindly retried because the side effect may already have happened;
  • action-key design controls the blast radius of inherited prior state;
  • tool selection remains an explicit residual risk;
  • integrity, signing, TTL/nonce, locking/serialization, masking, and reversibility are left to the adopting system or future work.

I think those boundaries make the “specification rather than library” description important. They also make the smaller schema-contract issue above more interesting, because it can be tested without needing to reopen the whole safety architecture.

So the revised shape I ended up with is roughly:

candidate action
      ↓
collect only values from permitted sources
      ↓
schema/policy + grounded partial state
      ↓
materialize the current external slot set
      ↓
resolve slots / ask for the external remainder
      ↓
build the complete argument object mechanically
      ↓
validate the entire object against inputSchema
      ↓
verify trigger-time / external conditions
      ↓
bind a verdict to that exact execution
      ↓
execute
      ↓
record what actually happened

The experiment did not make me think the decision should move back into the model.

If anything, it suggested a slightly more general place to draw the same external boundary:

not:
    the model decides what is missing

and not necessarily:
    root inputSchema.required[] is the complete list

but:
    schema/policy + already-grounded state
    deterministically determine the slots that must be settled

Then unknown can remain exactly what you defined it as: a remainder produced outside the model.

For simple tools, that collapses to the current required[] skeleton.

For current MCP schemas with conditional structure, the list can become conditional too without giving the model the authority to invent the condition, choose a convenient branch, or declare itself complete.

Thank you — this is a more careful reading than I expected, and both cases you ran are real.

On the code first: it is illustrative rather than authoritative. The skeleton exists to make the argument concrete, not to be a reference implementation, and I expect the actual shape to come from the people applying it — frameworks, or teams with a real tool catalog in front of them. That said, both are now fixed, and your diagnosis carried over almost unchanged.

getRequiredFields is superseded by materializeSlots(tool, groundedNames). Candidates are probed first — declared properties together with every branch’s required — and only the names that came back known are handed to the materializer. The branch is then selected by counting: one complete branch resolves, two is a conflict, one partially grounded branch resolves with the rest left unknown, anything else stays unresolved and is asked about. Unsupported constructs hold rather than yield an empty list. Your table reproduces exactly, conflict case included.

Slots and arguments are now separate. fields is what blocks; extra_fields is looked up through the same chain and never counted. The payload is assembled inside preflight from settled values and validated as a whole object, and executeIfReady sends that record verbatim rather than rebuilding it from fields. An execute verdict with no recorded payload is held.

Your four-way split is the part I would keep. Argument collection, slot materialization, provenance gate, payload validation — for a flat tool they collapse into one thing, which is exactly why the skeleton could conflate them without it showing.

On the line about slots not relating to one another:

You are right. I have reworded that paragraph — the sentence you quoted (“the slots no longer relate to one another”) was carrying a latency argument rather than an independence claim, and it now says that round trips scale with dependency depth instead. The point was that lookups do not have to queue behind one another, so adding a step does not add round trips in proportion to the number of blanks. The claim survives the correction — round trips scale with dependency depth, not with the number of slots, and depth rarely exceeds two or three, while asking about blanks one at a time scales with the blanks themselves.

Your branch case is one kind of dependency, and I had another in mind while writing: a balance check cannot be performed while the account number is still unknown. Both are the same shape — settled state determines the next slot set, deterministically and outside the model — so your generalization covers more than the example you used it on. What the piece does not yet have is a place to declare that dependency, or a rule that a slot being empty and a slot being answerable by the user are separate facts. The second is what keeps “is the balance sufficient?” from ever reaching the user; the source designated for that slot is a system lookup, not a person, so it stays in the lookup queue rather than the question queue.

On the forged-record question, requestState is a better example than anything I had. Binding to principal, tool identity, the exact arguments, the checklist result, schema version, expiry and nonce is far more specific than “sign the verdict,” and I would rather point at an existing case of the problem than propose one.

I would value your reading of the document as much as the code. I suspect much of the objection this kind of proposal attracts comes from not placing the model correctly. You stated the boundary better than I did: the model still extracts, converses, and matches candidates — it simply does not certify.

If the writing does not make that visible on a first read, it gets heard as “distrust the model,” which is a different and worse claim.

Still Needed When Models Improve, and No Reason to Wait

We don’t ask why the model hallucinates. But by the time it reaches execution, it always arrives as a blank already filled in. “Don’t fill it in” doesn’t work.

So nothing gets filled in. An external list says what the blanks are, and code counts them. Only what’s left over goes back to the person.

The model still extracts, converses, and matches candidates. One thing is taken away: certifying that the form is complete.


Models are getting better at this. There is real work on it — ToolSandbox, for one, evaluates insufficient-information cases explicitly. So when someone asks whether this structure becomes unnecessary once models improve, there are two answers. It stays necessary, and there is no reason to wait for it.

1. Still needed when models improve

Accuracy is a statement about a distribution. The person signing off is signing one case. When something goes wrong, what you need is not “this happens 0.01% of the time” but “here is what happened in this case, and why.” Higher accuracy lowers the frequency. It leaves the not-knowing-why intact. Accuracy lowers probability; being able to reach a verdict is what makes accountability possible.

Say the model extracted the supplier, the quantity, and the unit price, all of them plausible. But the delivery terms were nowhere in the request. The model can guess. The system still has to decide, separately, whether an order may go out on a guessed value.

Information that isn’t there doesn’t come into existence through computation. A condition the provider never declared won’t be known by the next model either. And the more accurate the values get, the more the approval step turns into a formality. A value that was looked up and a value that was invented look the same on the form, and a missing condition doesn’t appear on the form at all.

2. An act, not an outcome

Most discussion treats hallucination as getting it wrong. That compares a value against a correct answer, which leaves exactly one response available: make it wrong less often. This piece treats the same thing as filling in a blank. An act, not an outcome.

Whether an outcome was correct is usually something you learn afterward. Where a value came from, and whether the requirements were met, can be settled before execution.

  • Why there are no scores — either it was looked up or it wasn’t. There is no 0.87 lookup.
  • Why the source is recorded — as outcomes, a retrieved value and an invented one are the same string.
  • Why conditions and intent go on the same list — even when it isn’t a value, “proceeded without checking” is the same act.
  • Why better accuracy doesn’t resolve it — it improves a different thing.
  • Why this can be done now — judging an act doesn’t require knowing the right answer.

For reversible work, judging outcomes is still the better deal. If it’s wrong, fix it. This structure is not for every execution.

3. No reason to wait

Right now a model’s output is a draft. Someone reads it, edits it, approves it, and only then is it finished. As long as that approval is required, the verification cost stays with people, and the reach of the model grows only as fast as the number of people. Payments, purchase orders, sending things out — those stay closed because there is no way to hand that verification off. The reach of the model is bound to the release scope of agents. What drew that boundary was not performance, so performance doesn’t move it.

Irreversibility is not weaker in robotics or autonomous driving. It’s stronger. Writing the list for one tool today is not work that gets thrown away there. It’s groundwork.

And once causes are separated, the remedies separate too. A blank because the user didn’t say it — that’s conversation design. A blank because the provider never declared it — that’s a fix to the tool definition. A blank after every permitted source has been checked — add a source, or decide not to open that execution at all. None of the three needs a training budget. Improvement doesn’t get faster; it changes kind.

What it blocks

It blocks execution without a verdict. Being able to reach a verdict means knowing in advance what has to be settled, and being able to count, before execution, where each item was confirmed.

It does not stop the wrong tool from being picked. It narrows the candidates and forces a clarification, no more. It does not stop a forged verdict either.


Waiting means expecting someone else to write the list for you. But what a tool requires is known only to whoever wired that tool up, and which executions can’t be undone is known only to whoever knows the work.

The goal isn’t to remove uncertainty from the LLM. It’s to build something that can execute while the uncertainty is still there.

For now, I think the boundary is understandable:


I went back through the document with exactly the question you raised in mind. My answer is yes, I can recover the intended boundary, but I reached it much faster from your post 7 framing than from the main document itself.

This sentence in particular does most of the work for me:

The model still extracts, converses, and matches candidates. One thing is taken away: certifying that the form is complete.

That feels narrower, and stronger, than “distrust the model.” The model still has useful semantic work: extraction, conversation, computation, candidate matching, and so on. What it does not get to do is turn its own candidate state into proof that this exact execution is complete enough to proceed.

So if I were making the smallest document pass, I would probably do four things:

  1. move that certification boundary much earlier, probably around The shift;
  2. make unresolved and user-answerable explicitly different things;
  3. separate who defines an obligation from what source can resolve it;
  4. synchronize that vocabulary across the essay, README, design notes, and skeleton.

I think those changes would preserve the argument almost completely while making the first-read interpretation much harder to collapse into “models are untrustworthy, therefore remove reasoning.”

A compact way I now read the system is:

externally defined obligation
        ↓
permitted resolver / source
        ↓
settled or unresolved
        ↓
ask | measure | hold | repair definition
        ↓
completeness verdict
        ↓
execution bound to that exact state

That is only my map of what you already have; I do not think you need another vocabulary layer just for its own sake.

Why I think the post 7 boundary belongs earlier

The main essay eventually gets to the right place, but before reaching What is left for the model, the reader has already gone through phrases such as:

  • “take back the drafting”;
  • “what goes into the form is not something to infer”;
  • “the only place to look is the user”;
  • “needed values get answered by the user”;
  • “the making step disappears.”

Each line makes sense in its local argument. Taken together on a first pass, though, they pulled me toward a broader interpretation than the current implementation actually has.

The current skeleton still has model-facing semantic seams such as extractFromInstruction, action-name extraction, and candidate matching. It also has measureFromEnvironment, provenance checks, deterministic slot materialization, whole-payload validation, and the final execution decision outside that semantic work.

So I now read the boundary as something closer to:

not:
    remove model reasoning from preparation

but:
    do not let model-produced preparation
    certify its own execution completeness

That distinction also protects “take back the drafting” from being read too literally in workflows with deliberately generated content.

For example, an email body can perfectly reasonably be model-generated:

model drafts body
        ↓
draft becomes an existing artifact / settled content
        ↓
execution preflight decides whether this exact send may occur

The execution gate does not have to pretend that language generation never exists. It only has to keep “the model generated this argument” from silently becoming “therefore this argument has the provenance or authority required to execute.”

There is also some nearby human-factors evidence for the opening intuition. In Levy et al., CHI 2021, domain experts working with fully pre-populated automated suggestions took less initiative and accepted some improper suggestions. I would use that only as support for the narrow premise that pre-population can change the character of human review — not as evidence that human approval is useless, and certainly not as proof of this whole architecture.

That makes your distinction between:

the human constructing/checking the state

and:

the human seeing a completed-looking state and approving it

feel worth keeping.

But I think the post 7 sentence should arrive before the reader has time to interpret that as a general argument against model capability.

The next distinction I would make explicit: unresolved is not the same thing as ask_user

This was the most useful thing in your post 6 response for me.

Once dependencies exist, “empty” and “the user can answer this” are clearly separate properties.

The account/balance example makes it almost sufficient by itself:

account number unresolved
    -> the user can supply or confirm it
    -> ask_user

account number settled
    -> balance check becomes evaluable

balance not measured yet
    -> still unresolved
    -> but system-answerable
    -> measure / lookup, not ask_user

balance measured and insufficient
    -> no longer unresolved
    -> condition is resolved and prohibiting
    -> hold, not ask_user

provider never declared a required condition
    -> not a missing user answer
    -> provider/tool-definition gap

preflight cannot interpret the condition/schema
    -> implementation boundary
    -> hold rather than reinterpret it with the model

So I think the question queue is only one projection of unresolved state.

That seems useful because otherwise “unknown → ask the user” remains such a natural simplification that it keeps reappearing in the prose.

It also exposes what looks to me like two different axes in the current documents.

The README says:

the rules split by who defines them

while Who Fills In the Form says:

the axis for dividing the slots is who can answer them

and design.md describes three parties who can answer.

I do not think these are competing architectures. I think two classifications have simply been folded into one sentence.

A. Who/what defines the obligation?

For example:

fixed / adopting-system rule
provider / tool contract
user / environment-specific rule

B. What can settle it?

For example:

user answer
trusted instruction
pre-set / already-settled state
environment measurement
prior executed state
structured provider information, where applicable

Those axes can cross.

A provider can define:

balance must be sufficient

while the system measurement resolves whether it is true.

A user can define:

never spend more than X

while current system state determines whether this execution satisfies it.

A fixed timing requirement can be defined by the adopting system but require clarification from the user.

And a provider-definition gap cannot be repaired by asking the user what the tool provider meant.

That seems to give the post 6 answerability observation a natural home without changing the three-checklist structure.

There is also a small implementation seam here if you eventually want the distinction to become first-class in the skeleton.

The current execution-state-preflight.js already has the policy in comments:

answerable by the user
    -> unknown / question

not answerable
    -> hold

including the insufficient-balance example.

But the generic value gate still naturally converges on:

unknown_fields / unverified_checklist
        ↓
ask_user

So if you ever wanted to carry the post 6 distinction all the way into the generic record shape, I think the smallest extension would be something conceptually like:

unresolved item
+ resolver / answerability class
        ↓
ask_user | measure | hold | provider_gap

I would not make that a requirement for the argument. The document clarification may be enough.

If you did want one tiny canary for it, I think three cases would cover most of the semantic boundary:

user-answerable missing value
    -> ask

system-only unresolved condition
    -> do not enter a user re-ask loop

system-only condition resolved false
    -> hold

No model benchmark seems necessary for that.

One provider-condition distinction may be worth making explicit for future readers

I tripped over one other documentation boundary.

The essay’s Provider conditions, proposed as a minimal implementation section suggests roughly:

tool description
    ↓
[required] / [confirm] / [notice]
    ↓
string parsing
    ↓
entries on the enforceable list

But the current README and skeleton deliberately take the more conservative position:

structured/enforceable source
    -> may participate in the gate

natural-language description
    -> advisory_notes
    -> not itself an execution-opening condition

I think both ideas can coexist; the only thing I would clarify is which one describes the skeleton today and which one is the proposed provider convention.

For example:

Today:
    provider description is advisory.

Possible provider convention:
    publish conditions in a fixed structured/labeled form
    that middleware can parse into obligations.

Later:
    move that information into structured schema/metadata
    where the contract is unambiguous.

That would also fit the current MCP direction fairly naturally. The current MCP tool specification gives tools full JSON Schema 2020-12 for inputSchema, while descriptive metadata/annotations still need their own trust assumptions.

So I would probably keep the skeleton conservative and phrase the [required] / [confirm] / [notice] section explicitly as a proposed authoring convention / migration path, unless you want natural-language labels to become normative right now.

If they do become normative, then the parser contract itself becomes part of the gate:

trusted declaring party
+ fixed syntax
+ deterministic parsing
+ parse failure that does not silently pass

That is still compatible with the idea. It just deserves to be visible because otherwise a future reader can read the essay and README as describing two different current behaviors.

The dependency correction works; I would narrow only the performance claim

I think your correction from “slots do not relate to one another” to dependency depth is the right one.

The schema branch case and your balance example are really the same shape:

settled state
    ↓
deterministically exposes the next obligations

For data dependency:

account
    ↓
balance lookup

For obligation dependency:

grounded partial state
    ↓
active schema/policy branch

That preserves the important property: the model is still not asked to decide what is missing.

I noticed design.md still has the old sentence:

Slots do not depend on one another

so that may simply be a stale-doc sync after the post 6 change.

The only part I would narrow is the latency wording.

I think this is strong:

preflight does not imply one user round trip per blank

independent checks at the same dependency frontier
can be batched or parallelized where their sources allow

user questions can be batched over the currently
user-answerable unresolved frontier

I am less sure I would make the universal claim that adding the stage is literally faster.

Some “lookups” may be:

  • remote I/O;
  • live environmental measurements;
  • policy queries;
  • dependent on earlier state;
  • deliberately serialized to avoid rate/cost spikes.

In fact, the skeleton itself has places where conservative sequencing is perfectly reasonable.

So I would frame the win primarily as:

round-trip shape
and
available parallelism

rather than guaranteed wall-clock latency.

If this ever becomes an empirical claim, the measurements seem simple enough to keep separate:

user turns
external lookup count
critical-path dependency depth
wall-clock latency

Then a deployment can show which cost actually moved instead of treating them as interchangeable.

I would also move the reversibility scope slightly earlier

Post 7 helped here too.

The main document eventually says to put irreversible actions through the structure and let reads pass, and the README similarly says:

Apply this only to irreversible actions.

Post 7 makes the tradeoff even clearer: for reversible work, judging the outcome and correcting it may simply be the cheaper control.

I think that qualification should appear before, or very near, the strongest necessity language.

Lines such as:

this is a condition for execution to be valid at all

are much harder to misread if the execution class has already been named.

For example, the strong version still survives:

For an execution class where externally certifiable
completeness/provenance is required before a consequential
or irreversible action, the gate is part of the validity condition.

That is different from saying every read, search, reversible edit, or low-cost action is invalid without the full mechanism.

A trivial decision tree may be enough:

Can a wrong outcome be cheaply and reliably reversed?

yes
    -> outcome checking/correction may be the cheaper default

no
    -> pre-execution provenance/completeness becomes much more valuable

I think this actually protects the stronger claim rather than weakening it, because readers cannot dismiss the whole argument using a cheap reversible counterexample that the design already excludes.

So if I had to reduce all of this to the two changes with the highest payoff, they would be:

First, move the post 7 model/certifier boundary close to the beginning:

the model still extracts, converses, computes, and matches candidates;
it does not certify that the execution state is complete

Second, immediately after unknown is defined, prevent this collapse:

unresolved
    ==
ask the user

and replace it with the routing distinction:

unresolved
    -> who/what is allowed to resolve this?
    -> ask / lookup / measure / hold / repair the definition

Once those two are visible, I think most of the rest reads as implementation detail of the same idea rather than as additional restrictions on the model.

And that makes the post 7 ending land much more cleanly for me: the target is not to eliminate uncertainty from the model. It is to make execution possible without giving that uncertainty the authority to certify itself away.

Thank you for reading this carefully — this is one of the most useful responses the document has received.

I’m in the middle of a revision pass right now, and your points are going into it. The two you singled out at the end are the ones I most needed to hear: the certification boundary belongs much earlier than where it currently sits, and unresolved collapsing into ask the user is exactly the reading I kept leaving open. Both are being addressed.

I’ll follow up once the revision is out.