bots-garden/mini-mepublic Fork
⑂ feature/resume-acp
Commits
⬇ Clone ▾
git clone https://git.rickub.com/bots-garden/mini-me.git
git clone ssh://git@rickub.com/bots-garden/mini-me.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

Download ZIP
🛟 Updated. bed1738Unverified · on feature/resume-acp · k33g · 10d ago
asking-the-user.md · 54 lines · 6.2 KBmarkdown
Blame HistoryOpen raw

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 /abort and 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/create form 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 -p and mm --ask answer 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 by elicit.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 properties declared, a question's fields had to come as default, options, question, type; a model that starts with question — every model does — can then never write its options. 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_user collects 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
# 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 `/abort` and 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/create` form 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 -p` and `mm --ask` answer 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 by `elicit.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 `properties` declared, a question's fields had to come as `default`, `options`, `question`, `type`; a model that starts with `question` — every model does — can then never write its `options`. 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_user` collects 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](yolo-and-permissions.md).
- Called agents: [asking another agent](asking-another-agent.md).
- Reference: [the tool](../reference/tools.md#ask_user), [ACP](../reference/acp.md); how to use it: [let the agent ask you](../how-to/let-the-agent-ask-you.md).