Ask-User Modal: Design Spec
Status: design spec, not implemented as written.
This document proposes a
request_user_inputfunction-calling tool. That tool does not exist: the shipped implementation insrc/openclaw_pipe_pkg/askuser.pytook the other path: it intercepts assistant text beginning withOpenClaw needs input:orCodex needs input:and synthesizes the modal from the prompt text. Notable divergences from this spec:
- Trigger: assistant text deltas, not a typed tool-call event.
secret: inferred by keyword-scanning the prompt, not a tool parameter.- N-way choices: options are scraped from numbered lines (
1. foo) and rendered as Open WebUI's native question card, one row per option, as are yes/no and free-text prompts. Only secret and multi-select prompts get a dialog.- Timeouts: the card's own timeout is 4 minutes; a dialog waits up to an hour across reconnects. Neither is the single 5-minute timeout described below, and there is no
{"error": "User did not respond"}payload.Kept for the design rationale and as the reference for a future typed-event version. Read
askuser.pyfor current behavior.
Problem
Agents need to ask the user a question during a run. OpenClaw core supports this via Codex request_user_input / Claude AskUserQuestion, but these are not available in Default / model-agnostic mode. The current pipe MVP (P22) tries to detect "needs input:" text patterns, which is brittle and has no reliable typed event to trigger on.
Solution
The pipe exposes a function-calling tool called request_user_input that any model can call. The tool:
- Opens a native OWUI input modal / choices UI
- Waits for the user to answer
- Returns the answer as the tool result
- The model receives the answer and continues its reasoning
Why this works cross-provider
- Function calling is supported by every mainstream provider (OpenAI, Anthropic, DeepSeek, Google, OpenRouter)
- The pipe defines the tool, not the Gateway, so no OpenClaw core change is needed
- The tool only needs
text+ optionallychoices; no provider-specific types
Tool schema
name: request_user_input
description: Ask the user a question. Returns their answer as a string.
parameters:
type: object
properties:
question:
type: string
description: The question to ask the user. Clear, specific, one question.
choices:
type: array
items:
type: string
description: Optional list of predefined choices. The user can pick one or
type a custom answer.
secret:
type: boolean
description: If true, the input should not be echoed visually. Use for
passwords or sensitive values.
required: [question]Pipe behavior
When the agent calls the tool
- The pipe receives the tool call event (
stream="tool",phase="start") - If the user is in the same OWUI chat (most common case):
- Choices present (2 to 5): render as OWUI
confirmationevent with button-like options - Choices > 5 or free-text only: render as an OWUI
inputevent secret=True: render asinputwithtype: "password"
- Choices present (2 to 5): render as OWUI
- The pipe blocks (does not yield further events) until the user answers
- When the answer arrives (as the next
chat.sendin the same session), yield atoolresult event with the answer asoutput
Timeout / safety
- If no answer arrives within 5 minutes, yield
{"error": "User did not respond"} - If the user switches to a different OWUI chat, the pending input is silently cancelled (the answer would go to the wrong session)
Fallback
If __event_emitter__ is unavailable or the modal call fails:
- Yield the question as plain assistant text in the streaming output
- Wait for the user's next message in the same session
- Continue as if the tool had returned that message as the answer
This means even without modal support, the agent can still ask questions.
Implementation plan
Phase 1: Tool registration
In Pipe.__init__ or a new method, register request_user_input as an available function tool. The Gateway must be informed of the tool schema so it can offer it to the agent.
Key detail: The tool schema must be sent to the Gateway via the functions / tools field at session start (or session patch). The pipe already sends messages via the WS; we need to include tool definitions at __meta__ or in the first message.
Phase 2: Tool call handling
In pipe(), when the Gateway yields a tool-call start event for request_user_input:
- Parse the tool arguments (
question, optionalchoices, optionalsecret) - Call
__event_emitter__with the appropriate modal event - Save pending state (session key, run id, tool call id)
- Block the generator (store a future, yield nothing more)
- When the next user message arrives for this session, resolve the future with the user's answer text
Phase 3: Answer routing
The pipe already has a mechanism to handle user messages during an active run (steering). We extend it: if there is a pending request_user_input for this session+run, treat the new message as the answer to that tool call, not as a new steer message.
Phase 4: Persistence
Pending user inputs survive only in memory (pipe instance state). If the pipe restarts, pending inputs are lost. This is acceptable for v1; the agent will see a timeout error and can re-ask.
Open questions
How does the pipe inform the Gateway of available tools?
- Is it via
__functions__/__tools__in the first WS message? - Or does the Gateway discover tools from the agent config?
- Needs investigation of the Gateway WS protocol for tool registration.
- Is it via
Does the pipe need to manage tool call state across yield points?
- Since
pipe()is an async generator, we can hold aFuturebetween yields, but the OWUI event loop also runs, so we need to ensure the answer arrives on the same run context.
- Since
What happens if the user types a free-form answer when choices are given?
- Proposal: always accept free text; choices are UI hints, not constraints.
- The model sees the actual user text and decides how to interpret it.