Asking the user — explanation
What is this about?
An agent often reaches a point where the next step is the user's decision: which framework, which environment, whether to go ahead. Without a way to ask, a model either guesses — and builds on a guess — or ends its turn with a question in prose, which loses the work in progress and gives the user nothing to click. The ask_user tool (ticket 84) lets the model ask in the middle of a turn and carry on with the answer.
The ticket names the familiar shapes of an interactive prompt: pick one option, pick several, type a text, confirm. They are exactly the field types of ACP's elicitation forms, which made them the tool's four kinds: choice, multi, text, confirm.
One question, four front ends
mm has four ways to be used, and the question must reach a person in each — or say clearly that nobody is there. The design keeps everything that does not depend on a screen in one package, internal/elicit: the questions, the answers, what the model reads back, how a typed line answers a question, and the conversion to and from an elicitation form. Each front end only asks:
- the line mode prints numbered options and reads the next typed line, through the same waiting room as the a/s/r/n permission prompt — which is why
/abortand Ctrl-C keep working while a question waits. Arrow-key menus were considered and left to the window: the line mode reads whole lines, and switching the terminal to raw mode for one question would fight its own input reader; - the window draws the options over the conversation, as its completion popup does, and takes every key while the form stands;
- an editor gets an ACP
elicitation/createform when it advertised the form mode; otherwise each choice or yes/no question becomes a permission dialog whose buttons are the options, because every ACP editor can show that dialog. Free text and several choices cannot be buttons: the model is told nobody can be asked; - nobody:
mm -pandmm --askanswer at once that the user cannot be asked, and the model is told to assume and say what it assumed. Waiting for an answer that cannot come would hang a script.
A front end that can ask implements one optional interface, ui.Elicitor, beside the ui.Sink it already is — the same pattern as the token counts (UsageSink) and the speaker label (Speaker).
Agents asked by other agents
An agent called through /ask or ask_agent is a child mm -acp, and its permission requests already come up to its asker. Its questions take the same road: the asker's ACP client advertises the form mode, receives the child's elicitation/create, and asks its own user through its own front end, naming the child (homer asks: …). A coordinator in an editor therefore shows its helper's questions as editor forms, and one in the terminal as numbered lines.
Whether a called agent may ask at all is the user's decision, per agent: its own askUserTool, or the asker's mmConfigs.<name>.askUser, passed to the child process as AGENT_ASK_USER the way yolo travels as AGENT_YOLO. A worker that should never interrupt anyone simply does not get the tool.
A loose schema, on purpose
The tool declares its input loosely: no enum, nothing required, and each question described in words rather than with properties. Two measured reasons:
- Genkit validates a call against the declared schema before running the tool, and a rejected call ends the turn. A small model that writes
"type": "single"would lose its turn; read byelicit.Normalize, the synonym simply works, and a real mistake comes back as a sentence the model corrects. - llama.cpp turns the schema into a grammar, and that grammar imposes an object's properties in alphabetical order. With
propertiesdeclared, a question's fields had to come asdefault,options,question,type; a model that starts withquestion— every model does — can then never write itsoptions. Measured with Qwen3.5 9B: the options were lost 3 times out of 3 with the properties declared, and written 3 times out of 3 without. The raw completion, outside the grammar, always had them.
The tool's description ends with a complete example call, which the schema can no longer show.
Waiting for a human
mm's watchdog keeps counting while a tool runs (ticket 57), so a question left unanswered for 20 seconds would have ended the turn as a hung server. While a question waits, the tool beats the watchdog every second; a stopped turn still cancels the question. For a called agent's question, the asker's watchdog is beaten the same way.
An SDK gap
ACP ties a form to its session and tool call with top-level sessionId and toolCallId, but acp-go-sdk v0.13.5 has no such fields on either side. mm parks the scope in the form's _meta and writes the top-level fields as the line goes out; an mm client relaying the question reads the tool call back from _meta. Both halves are marked to go once the SDK carries the fields.
Rejected alternatives
- One question per call. Simpler, but a form of related questions (framework, TypeScript, package manager) is the ticket's own example, and an editor shows it as one form.
- A question asked in prose, answered by the next message. It ends the turn, and nothing distinguishes the answer from a new request.
- Declining when nobody can answer. "Declined" tells the model the user refused; "cannot be asked" tells it to assume. The difference changes what a model does next.
- Declaring the tool beside a
structuredOutput. Any declared tool stops such an agent from starting, and one that answers once, in JSON, cannot hold a conversation anyway.
How it relates to the rest
- Permissions (
yolo) decide whether a tool may run;ask_usercollects information. The two never mix: asking changes nothing, so the tool is not gated, and the editor's permission dialog is only borrowed as a fallback when no form can be shown. See yolo and permissions. - Called agents: asking another agent.
- Reference: the tool, ACP; how to use it: let the agent ask you.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
|