Skip to content

Questions

A questions tool gives the model a structured way to pause execution and request human decisions.

Without a structured question tool, an agent has two bad options:

  • guess and continue (risking wrong implementation)
  • emit plain text questions in assistant output (which can break deterministic tool loops)

A dedicated tool solves this by making user clarification part of the tool-call protocol:

  1. model emits a typed question payload
  2. runtime blocks the turn
  3. UI renders controlled choices/freeform fields
  4. answer is returned as structured data
  5. model resumes with that answer in context

This becomes critical in planning, architectural tradeoff selection, and ambiguous requirement interpretation.

See also Skills for how question-style prompts are reused during skill dependency and environment setup flows. For host-side confirmation UX (approve/deny tool execution) rather than model-initiated questions, see Approval Flow.

The hard part is not asking a question.

The hard part is preserving determinism and safety across:

  • different clients (TUI, app server, ACP, headless)
  • different turn modes (interactive vs non-interactive)
  • different answer shapes (single choice, multi choice, freeform, secret)
  • cancellation and interruption semantics

If this is under-specified, the agent can deadlock waiting for input that no client can provide.


Aider at commit b9050e1d5faf8096eae7a46a9ecc05a86231384b has no model-callable question tool.

Aider’s model loop does not expose a JSON-schema tool for “ask user”.

When a model needs clarification, it asks in normal assistant text.

There is no dedicated runtime object like:

  • QuestionRequest
  • pending question queue
  • typed answer payload returned to tool call

Aider has a chat mode switch for Q&A behavior:

  • aider/commands.py:
    • cmd_ask() delegates to _generic_chat_command(args, "ask")
    • cmd_chat_mode() exposes ask as “Ask questions about your code without making any changes”
  • aider/coders/ask_coder.py:
    • AskCoder sets edit_format = "ask"
  • aider/coders/ask_prompts.py:
    • system prompt is analysis-oriented and non-edit focused

This is a useful mode, not a runtime question tool.

Runtime Confirmation Prompts (confirm_ask and prompt_ask)

Section titled “Runtime Confirmation Prompts (confirm_ask and prompt_ask)”

Aider does ask the human for runtime confirmations, but these are host-side procedural prompts, not model tools:

  • aider/io.py:confirm_ask()
    • yes/no(+all/skip/don’t ask again) interactions
    • supports grouped confirmations
    • supports explicit_yes_required
  • aider/io.py:prompt_ask()
    • freeform input prompt

These are called from command and coder logic directly (lint fixes, shell operations, file creation, etc.).

The model does not emit a typed question call to trigger them.

Aider supports human-in-the-loop clarification, but through:

  • conversational text
  • host-side confirmation functions
  • mode switching (/ask)

not through a first-class question tool protocol.


Codex at commit 4ab44e2c5cc54ed47e47a6729dfd8aa5a3dc2476 exposes a dedicated request_user_input function tool, but with strict mode and schema behavior.

Tool definition lives in:

  • references/codex/codex-rs/core/src/tools/spec.rs (create_request_user_input_tool())

Registration is gated by collaboration-mode feature wiring:

  • build_tool_registry(...) in spec.rs
  • only registered when config.collaboration_modes_tools is enabled

Mode gating is explicit:

  • references/codex/codex-rs/protocol/src/config_types.rs
    • ModeKind::allows_request_user_input() is true only for Plan
  • references/codex/codex-rs/core/src/tools/handlers/request_user_input.rs
    • returns model-visible error if called outside allowed mode

So the tool exists primarily for plan-time clarification loops.

Schema Contract in create_request_user_input_tool()

Section titled “Schema Contract in create_request_user_input_tool()”

The model-facing schema requires:

  • questions: []
  • each question includes:
    • id
    • header
    • question
    • options (2-3 mutually exclusive choices encouraged)

Important nuance:

  • protocol structs (protocol/src/request_user_input.rs) support richer fields like isSecret and optional options
  • but the model-facing tool schema intentionally requires options

Implementation:

  • references/codex/codex-rs/core/src/tools/handlers/request_user_input.rs

Flow:

  1. parse function args into RequestUserInputArgs
  2. reject if current collaboration mode disallows tool
  3. reject if any question has missing/empty options
  4. force question.is_other = true for every question
  5. call session.request_user_input(...)
  6. serialize RequestUserInputResponse JSON back to tool output text

The is_other = true mutation means UI always gets an extra freeform “Other” path for tool-generated questions.

Session/Event Flow (Session::request_user_input)

Section titled “Session/Event Flow (Session::request_user_input)”

Core event plumbing:

  • references/codex/codex-rs/core/src/codex.rs
    • Session::request_user_input(...)
    • Session::notify_user_input_response(...)
  • references/codex/codex-rs/core/src/state/turn.rs
    • pending user input map stored per active turn

Runtime behavior:

  1. create oneshot channel
  2. store pending responder in turn state
  3. emit EventMsg::RequestUserInput
  4. wait asynchronously for Op::UserInputAnswer
  5. resume tool handler with structured answers

Protocol vocabulary is documented in:

  • references/codex/codex-rs/docs/protocol_v1.md
    • EventMsg::RequestUserInput
    • Op::UserInputAnswer

TUI implementation:

  • references/codex/codex-rs/tui/src/bottom_pane/request_user_input/mod.rs
  • history rendering:
    • references/codex/codex-rs/tui/src/history_cell.rs

Behavior highlights:

  • question-by-question navigation UI
  • options list + notes field
  • optional “None of the above” injected when is_other is true
  • unanswered confirmation before submit
  • FIFO queue for multiple incoming requests

Answer encoding detail:

  • selected options are returned as labels
  • notes are appended as synthetic entries with prefix:
user_note: <freeform text>

history_cell.rs splits answers by detecting this prefix.

Codex app-server bridges question requests over v2 protocol:

  • references/codex/codex-rs/app-server/src/bespoke_event_handling.rs
    • maps core RequestUserInputEvent -> app-server ToolRequestUserInputParams
    • waits for client response
    • submits Op::UserInputAnswer back to core

Sub-agent delegate forwarding also supports question events:

  • references/codex/codex-rs/core/src/codex_delegate.rs
    • intercepts child EventMsg::RequestUserInput
    • relays to parent session
    • returns answer to child as Op::UserInputAnswer

Internal Codex Uses Beyond Model Tool Calls

Section titled “Internal Codex Uses Beyond Model Tool Calls”

Codex also uses the same request-user-input event pathway internally for non-tool workflows:

  • skill environment variable collection:
    • core/src/skills/env_var_dependencies.rs
  • skill MCP dependency install confirmation:
    • core/src/mcp/skill_dependencies.rs
  • MCP app/tool approval prompts:
    • core/src/mcp_tool_call.rs

Those code paths can use richer fields (is_secret, optional options), even when model-facing tool calls are constrained.


OpenCode at commit 7ed449974864361bad2c1f1405769fd2c2fcdf42 exposes a first-class question tool plus API/UI infrastructure around a pending-request broker.

Tool definition:

  • references/opencode/packages/opencode/src/tool/question.ts

Schema:

  • questions: Question.Info[] (without custom override in tool input)

Execution:

  1. QuestionTool.execute(...) calls Question.ask(...)
  2. blocks until user replies/rejects
  3. returns formatted summary in tool output and raw answers in metadata

In-Memory Pending Request Broker (Question.ask)

Section titled “In-Memory Pending Request Broker (Question.ask)”

Core broker:

  • references/opencode/packages/opencode/src/question/index.ts

Data model:

  • Question.Info: question, header, options, optional multiple, optional custom
  • Question.Request: question packet with sessionID and optional tool call linkage
  • Question.Answer: string[]

Flow:

  1. generate question id (Identifier.ascending("question"))
  2. store pending resolver in in-memory map
  3. publish question.asked bus event
  4. wait on Promise
  5. resolve on Question.reply(...)
  6. reject on Question.reject(...) with RejectedError

This is a clean async rendezvous model.

HTTP routes:

  • references/opencode/packages/opencode/src/server/routes/question.ts
    • GET /question list pending
    • POST /question/:requestID/reply
    • POST /question/:requestID/reject

TUI sync/event ingestion:

  • references/opencode/packages/opencode/src/cli/cmd/tui/context/sync.tsx
    • tracks question.asked / question.replied / question.rejected

Question modal UI:

  • references/opencode/packages/opencode/src/cli/cmd/tui/routes/session/question.tsx

Notable UI behavior:

  • single-question auto-submit for simple single-select
  • multi-question tabbed prompt with final confirm tab
  • multi-select support (multiple)
  • custom typed answer support (custom)
  • keyboard-first navigation and escape-to-reject

Main session view prioritizes overlays:

  • session/index.tsx
    • permission prompt first
    • then question prompt
    • main composer disabled while either is active

Agent Permissions and Non-Interactive Mode

Section titled “Agent Permissions and Non-Interactive Mode”

Question execution is permission-aware.

Defaults live in:

  • references/opencode/packages/opencode/src/agent/agent.ts

Important defaults:

  • global default permission includes question: deny
  • built-in build and plan agents explicitly allow question

CLI non-interactive run mode hard-denies question prompts:

  • references/opencode/packages/opencode/src/cli/cmd/run.ts
    • session rules include question: deny

This prevents deadlocks in headless execution.

Tool registration:

  • references/opencode/packages/opencode/src/tool/registry.ts

Question tool inclusion is conditional:

  • enabled for app/cli/desktop clients
  • or explicit OPENCODE_ENABLE_QUESTION_TOOL

ACP docs explicitly call out opt-in:

  • references/opencode/packages/opencode/src/acp/README.md
    • QuestionTool excluded by default in ACP
    • enable only when client can handle interactive prompts

Codex and OpenCode support different question primitives.

Codex model tool is effectively single-select (+other notes). OpenCode supports multi-select and custom freeform answers per question.

If OpenOxide abstracts this poorly, provider behavior will diverge by client/runtime.

Codex forces is_other = true in handler. OpenCode treats custom answers as configurable.

If OpenOxide toggles this dynamically without a stable contract, prompts and UI will disagree about what answers are possible.

Headless clients cannot satisfy blocking question requests.

OpenCode solves this by hard-denying question permission in run mode. Codex constrains availability to Plan mode and emits explicit errors outside it.

OpenOxide should fail fast in non-interactive contexts, not wait forever.

Cancellation/Interruption Semantics Are Easy to Break

Section titled “Cancellation/Interruption Semantics Are Easy to Break”

Codex TUI has TODOs around interrupted question result persistence. OpenCode rejects pending question promises explicitly.

If interruptions are not modeled clearly, pending question state leaks and subsequent turns can become inconsistent.

Codex protocol supports isSecret; OpenCode question model does not expose an equivalent dedicated secret field in this path.

Packing secrets into generic freeform text risks accidental logging or history rendering.

OpenOxide should treat secret answer handling as first-class, not a UI hint.


Create openoxide-question with these core types:

  • QuestionRequest { request_id, turn_id, source, questions, mode }
  • Question { id, header, prompt, kind, options, allow_custom, secret }
  • QuestionKind:
    • SingleSelect
    • MultiSelect
    • Freeform
  • QuestionResponse { answers: HashMap<QuestionId, AnswerPayload> }
  • AnswerPayload { selected: Vec<String>, note: Option<String> }

Do not encode notes as sentinel strings ("user_note: ..."). Keep note as typed field.

Define two request constructors:

  1. QuestionRequest::from_model_tool(...)
    • enforces model-safe schema
    • applies defaults (max question count, options shape)
  2. QuestionRequest::from_internal_system(...)
    • allows secret/freeform questions for runtime subsystems

This mirrors Codex’s separation between model-tool constraints and internal event reuse.

Standard flow:

  1. tool handler or subsystem builds request
  2. runtime registers pending request in turn state
  3. event bus emits QuestionRequested
  4. client responds with QuestionAnswered or QuestionRejected
  5. runtime resolves or cancels pending promise

Use timeout/cancellation hooks to auto-resolve with explicit Rejected state when session interrupts.

Transport contract should include:

  • API endpoint(s) to list pending questions
  • reply/reject endpoints
  • event stream notifications for ask/reply/reject

UI contract should include:

  • overlay precedence (permissions first, then questions)
  • input locking while question unresolved
  • keyboard-first and mouse support
  • explicit unanswered confirmation for multi-question batches
  • disable question tool in non-interactive sessions by default
  • return deterministic model-facing error when unavailable
  • cap question count per call (recommend <=3)
  • require stable question ids for mapping
  • redact secret answers from normal history renderers and logs
  • serde/schemars for schema and wire types
  • tokio::sync::oneshot + tokio::select! for pending response orchestration
  • indexmap for stable answer ordering where UI requires deterministic display
  • tracing with structured redaction for secret input paths

Minimum tests:

  • tool unavailable mode returns model-facing error
  • question request + answer round trip
  • reject path and interruption path
  • queued question ordering
  • secret answer redaction in history/log serialization
  • non-interactive policy auto-denial
  • multi-question unanswered confirmation behavior