Skip to content

feat(tools): add AskUserQuestion clarifying-question form for plan mode#661

Merged
edenreich merged 6 commits into
mainfrom
feat/ask-user-question
Jun 27, 2026
Merged

feat(tools): add AskUserQuestion clarifying-question form for plan mode#661
edenreich merged 6 commits into
mainfrom
feat/ask-user-question

Conversation

@edenreich

Copy link
Copy Markdown
Contributor

Summary

Adds AskUserQuestion: a plan-mode-only, read-only tool that lets the agent ask the user 1–4 multiple-choice clarifying questions as a keyboard-driven TUI form, instead of guessing or asking in prose. Answers are returned as the tool result and the agent loop continues (folds them into the plan, then RequestPlanApproval).

Part of #652 (v1; the follow-ups below remain tracked there).

What's included

  • Tool (internal/agent/tools/ask_user_question.go): draft-07 schema — 1–4 questions, each with a ≤12-char header, question, 2–4 options of {label, description}, optional multiSelect; bounds enforced in Validate. Headless/no-TTY runs degrade gracefully (returns "no interactive user — proceed with assumptions" instead of hanging).
  • Brokering: Execute publishes a request event carrying a buffered response channel and blocks; the broker is injected via context only on the chat path (GetChatHandler), so the loop resumes once answered. The chat-events listener is re-armed across the form so the tool result + next LLM turn reach the UI.
  • Form UI (question_form_view.go): floating box over the chat — ↑/↓ navigate, space toggles (multi-select), enter advances/submits, an auto-appended "Other" free-text row, esc/ctrl+c cancel. Single-select is a radio: a default is pre-selected (the (Recommended)-marked option, else the first) and the selection follows the cursor, so enter always confirms a visible choice. The collected answers render expanded in the conversation.
  • Plan-mode gating: exposed to the LLM only in plan mode (ListToolsForMode), excluded from standard/auto. The !! tool autocomplete uses the same gating, so plan-only tools no longer leak into other modes.
  • !! direct execution (testing / power-user escape hatch): !!AskUserQuestion({...}) renders the form without an LLM turn (raw-JSON arg parser + direct-exec broker).

Tests

Unit tests (schema/validation bounds, answer formatting, headless degrade, mode gating, state lifecycle, default/recommended selection, form rendering, autocomplete mode gating) plus a full-ChatApplication integration test that drives the request through Update, survives the agent's progress ticks, and asserts enter delivers the answers, clears the form, and the answers reach the LLM-facing tool result. task build / test / lint all green.

Deferred (separate PRs)

  • Telegram / channels IPC (QuestionRequest/QuestionResponse + QuestionChannel).
  • Web-terminal renderer.
  • Option preview mockups; exposing the tool outside plan mode.

Docs

Per the feat→docs rule, a [DOCS] ticket for inference-gateway/docs is drafted (not yet filed): document the plan-mode AskUserQuestion tool — question/option schema, keyboard controls, plan-mode-only + headless behavior.

Add a plan-mode-only AskUserQuestion tool that presents the user with an
interactive form of 1-4 multiple-choice questions (each 2-4 options, optional
multi-select, plus an auto-appended "Other" free-text choice). The agent
collects the answers and folds them into the plan before RequestPlanApproval.

The tool is read-only (no approval gate) and brokers the form like tool
approval: Execute publishes a UserQuestionRequestedEvent carrying a buffered
response channel and blocks until the user submits or cancels, then returns the
answers so the agent loop continues. The broker is injected into the tool
context only on the chat path (GetChatHandler is set), so headless/no-TTY runs
see a nil broker and degrade gracefully instead of hanging.

The form renders as a floating box over the chat (QuestionFormView) with its
keys intercepted in the chat view while the form state is set, mirroring the
tool-approval box; the view stays ViewStateChat.

Part of #652. Follow-ups (Telegram IPC, web terminal renderer, option preview
mockups) remain separate.
…chat

The interactive form rendered and collected answers, but reading the
UserQuestionRequestedEvent consumed the one-shot event listener and the handler
never re-armed it. So after the user submitted, the tool's completion and the
next LLM turn's events were published but never read - the agent finished in the
background while the UI stayed frozen on the form.

HandleUserQuestionRequestedEvent now re-arms the listener: the direct-exec tool
channel when the request came from a !! invocation, otherwise the chat-session
channel for the agent (LLM) path. A single re-arm per event, so no double
listener.

Adds an integration test that builds a real ChatApplication, drives the event
through Update, and asserts the form renders, survives progress ticks, and that
Enter delivers the answers and clears the form. Also asserts the answers reach
the LLM-facing tool result (FormatResult), i.e. the conversation context.
Lets the interactive form be exercised without an LLM turn, which is handy for
testing and as a power-user escape hatch.

- The !! parser accepts a raw JSON object, e.g. !!AskUserQuestion({"questions":
  [...]}). The flat key="value" parser cannot express the nested questions array.
- The direct-exec path injects a UserQuestionBroker (publishing on its own event
  channel and blocking on the response) so the tool renders the form instead of
  taking the headless "no interactive user" branch.
Single-select questions now behave like a radio group: a default option is
pre-selected (the one whose label is marked "(Recommended)", else the first), and
the selection follows the cursor, so Enter always confirms a visible choice
rather than silently picking the highlight. Multi-select is unchanged - it stays
explicit and still requires at least one selection (or Other text). Navigating to
the Other row clears the option selection so the free text becomes the answer.

Also expands the AskUserQuestion result in the conversation by default - the Q&A
is the point of the tool, so it shouldn't hide behind a collapsed one-liner.

(Also drops a couple of dangling count assignments in the MCP status handler left
over from removing its debug logs.)
The !! tool autocomplete kept its own hardcoded plan-mode tool list, which had
drifted: AskUserQuestion (and any plan-only tool) showed in standard/auto mode
and would have been missing in plan mode. The LLM was never affected - it uses
ListToolsForMode - this was only the suggestion list.

loadTools now sources suggestions from ListToolsForMode(currentMode), the exact
gating the agent uses for the model, so the two can't drift again. The
autocomplete already reloads on mode change (lastAgentMode).
!! ran any enabled tool regardless of mode, so e.g. !!AskUserQuestion worked in
standard mode and !!Bash worked in (read-only) plan mode. HandleToolCommand now
refuses tools that aren't exposed in the current mode, using the same
ListToolsForMode gating the agent applies for the LLM and the autocomplete - so
discovery and execution agree.
@edenreich edenreich merged commit 998bf37 into main Jun 27, 2026
1 check passed
@edenreich edenreich deleted the feat/ask-user-question branch June 27, 2026 14:41
@edenreich edenreich linked an issue Jun 27, 2026 that may be closed by this pull request
10 tasks
inference-gateway-releaser Bot added a commit that referenced this pull request Jun 27, 2026
## [0.124.0](v0.123.1...v0.124.0) (2026-06-27)

### 🚀 Features

* add /env shortcut for creating .env.example with provider API keys ([#655](#655)) ([3227e15](3227e15)), closes [#519](#519)
* **tools:** add Agent tool for local subagents ([#658](#658)) ([1712f6e](1712f6e)), closes [#653](#653) [#653](#653)
* **tools:** add AskUserQuestion clarifying-question form for plan mode ([#661](#661)) ([998bf37](998bf37)), closes [#652](#652)
* add sleep to default bash allow-list ([#660](#660)) ([2d945c3](2d945c3)), closes [#659](#659)

### 🧹 Maintenance

* **deps:** bump infer-action v0.17.2 -> v0.19.0 ([#654](#654)) ([c66e6d2](c66e6d2))
* **flox:** update codex and infer versions ([4268714](4268714))
@inference-gateway-releaser

Copy link
Copy Markdown
Contributor

🎉 This PR is included in version 0.124.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEATURE] AskUserQuestion: interactive clarifying-question form for plan mode

1 participant