Skip to content

feat(codex): surface non-blocking asks from request_user_input_async - #536

Merged
Tryanks merged 2 commits into
mainfrom
feat/codex-async-user-input
Sep 24, 2026
Merged

Tryanks merged 2 commits into
mainfrom
feat/codex-async-user-input

Conversation

@Tryanks

@Tryanks Tryanks commented Sep 24, 2026 •

Copy link
Copy Markdown
Owner

What changes

Codex's app-server (checked against codex-cli 0.156.1) exposes request_user_input_async to models whose catalog advertises it. This is the non-blocking ask in the Codex desktop app. It is not a server request. The tool completes an agentMessage item with questions: [{title, options?}] and delivery: "async", and the turn keeps running. The desktop app and the TUI answer with ordinary user input wrapped in <send_user_message_question_reply>[{answer, question, questionItemId}]</send_user_message_question_reply>. The questionItemId is JSON.stringify(["request_user_input_async", itemId, index]).

Tcode now surfaces it natively:

  • Contract. AgentEvent::UserInputRequested gains delivery: UserInputDelivery (Blocking or Async). It defaults to Blocking and is omitted when blocking, so persisted logs and the other providers are unchanged.
  • Codex client. A completed agentMessage with questions becomes an Async request. The item itself is recorded as a request_user_input_async tool call, not assistant text, because its text only repeats the questions the panel shows. Tool rows summarize question tools by their first question. Open questions of a turn accumulate under the latest message id, so a second ask does not hide an unanswered first one. RespondUserInput sends a turn/steer carrying Codex's envelope. It emits SteerRequested with the TUI's plain > question / answer rendering, and the existing userMessage echo matching emits SteerAccepted. turn/completed discards unanswered questions, as the TUI does.
  • Timeline. The pending request is now a PendingUserInput struct with the delivery, replacing the tuple.
  • Composer. While an async question is open, Enter, steering and digit keys behave as usual. The panel shows a header in place of the missing one, a hint that the agent keeps working, and a close button that hides it on this client until a newer request arrives. New strings are added in English and Chinese.

Tests

  • async_question_answers_steer_the_running_turn_with_codex_reply_envelope uses the recorded item shape. It asserts the tool-call item, the question ids, the literal turn/steer envelope, steer acceptance on the echo, and rejection once the turn has ended.
  • user_input_delivery_is_async_only_when_recorded asserts that a legacy persisted event decodes as blocking and checks the literal async encoding.

Checks run

  • cargo fmt --all --check, cargo clippy --workspace --all-targets --locked -- -D warnings, cargo nextest run --workspace --locked: all pass locally on macOS (941 tests).
  • Live probe against codex-cli 0.156.1 on gpt-6-astra: the model asked, kept running, received the steered answer and replied with it.
  • Desktop app on a throwaway profile, light theme at wide width and dark theme at narrow width: the panel appears while sleep runs, answering closes it and steers, and the close button hides it without stopping the turn.
  • Not run locally: the iOS, Android, Web and machete CI jobs.

Codex's app-server exposes a request_user_input_async tool to models whose
catalog advertises it (the Codex desktop app's non-blocking ask). It does
not send a server request. It completes an agentMessage item carrying
`questions` and `delivery: "async"`, and the turn keeps running. The answer
goes back as ordinary user input wrapped in Codex's
<send_user_message_question_reply> envelope, keyed by the desktop app's
question id JSON.stringify(["request_user_input_async", itemId, index]).

- UserInputRequested gains `delivery` (Blocking by default, so existing
  logs and other providers are unchanged).
- The Codex client turns such messages into an Async request. Open
  questions of the turn accumulate under the latest message id. Answers
  steer the running turn with the envelope, and the timeline shows the same
  plain "> question / answer" rendering the Codex TUI uses. The turn's end
  discards unanswered questions.
- The timeline's pending request becomes a PendingUserInput struct that
  carries the delivery.
- The composer keeps sending and steering while an async question is open.
  The panel explains that the agent keeps working and can be hidden on
  this client.
The request_user_input_async item's text only lists the questions the input
panel already shows, so rendering it as an assistant message duplicated
them in the timeline. The item now maps to a request_user_input_async tool
call carrying the questions. Tool rows summarize a question tool by its
first question, which covers Claude AskUserQuestion input as well.
@Tryanks
Tryanks merged commit 85b311e into main Sep 24, 2026
7 checks passed
@Tryanks
Tryanks deleted the feat/codex-async-user-input branch September 24, 2026 05:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant