You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
UEP: requires_input — a task can end by asking the user, and the answer resumes it #476
An agent working on a task often needs one answer from the user: a choice, a missing value, a confirmation.
UHP has no way to express this. A task ends completed, failed, incomplete or cancelled (lifecycle.md §state diagram), and the question is buried in the final text.
A client can't tell "done" from "waiting for you", and can't route the answer back in a structured way.
This needs no new route. The server resumes the session (hydrate + native resume, as it already does) and delivers the answer as the tool result, or as the next user message for harnesses without a question tool.
Harness mapping:
harnesses with a native question tool map it directly;
others get a server-provided MCP tool (ask_user) through the existing MCP/plugin wiring;
a server that offers neither for a harness says so in /v1/uhp capabilities.
Errors:
answer to an unknown request → 404;
second answer → 409;
answer after expires_at → 410;
any other input while requires_input is pending → treated as an answer-less continuation, reported in metadata.
Compatibility
Additive. Clients that don't know requires_input see a terminal non-completed status and the question text in output.
Servers that don't implement it never emit it.
Because the question ends the turn, there is no long-lived process, and the existing checkpoint/hydrate path covers waits of any length.
Related proposal: #479 (filed after this one) covers the same problem — a task that needs an answer from the user — with a different design on one axis: pause vs. end.
#476 ends the task with terminal requires_input, and answering is a normal continuation. #479 pauses it with non-terminal awaiting_input, answers via POST /v1/responses/{id}/input, and pauses the task's timeout/step budgets during the hold.
The pause model exists for live-loop harnesses: in Operario (a vision/VNC control loop), the hold happens mid-task — ambiguity disambiguation, credential gates — and the harness stays live. Splitting the hold into a task boundary would break the loop's continuity and its budget accounting. The end model fits process-per-turn runtimes better, and #476's MCP ask_user fallback is a good answer for harnesses without a native question tool.
The two proposals agree on the important parts: typed questions, redaction for secrets, timeout/expired semantics, 409/410 handling. They differ on whether the wait is a task boundary or a mid-task state — which may come down to what the runtime supports. Happy to converge if maintainers prefer one shape, or for the spec to support both (pause where the runtime stays live, end where it doesn't).
Thank you, @phanisarman, and @jpelaez-23blocks for linking the two. Our assessment: one proposal, in the end-the-turn shape this issue proposed first. Accepted in principle (GOVERNANCE), with the changes below. We asked #479 for this same shape on 10-18 without linking it here. The two now agree on nearly everything, so the result takes its shape from #476 and its typed request from #479.
Capability input_requests in GET /v1/uhp, saying which bases can ask. (both)
Changes to this proposal
No new status. The turn ends incomplete with incomplete_details: {"reason": "input_required"}, not requires_input.
incomplete is already terminal and keeps partial output, and a client already reads it as "usually worth continuing" (Lifecycle §3). A client that switches on today's five statuses keeps working, and Responses SDKs parse it.
This widens incomplete from "a budget stopped it" to "stopped short, with a reason". So the PR amends Lifecycle §3 and defines incomplete_details, which the response example in Tasks shows but the field table does not.
The request is an output item, and the answer is an input item. This is the pattern function_call and function_call_output already follow. It streams through the existing output item events, so no new event type is needed:
The turn's last message also carries the question as text, because a client that renders only message items is a valid client (Tasks §3.1).
The answer arrives as the next user message, not as the tool result. Once the turn has ended, no tool call can still be open. So ask_user returns at once ("asked; your turn ends here"), the runner ends the turn, and the follow-up resumes the session with the answer as its message. A base that can natively resume a pending tool call may deliver the answer there instead; that is an implementation choice, not the contract.
Errors, one code per remedy:
409 input_not_pending: the response has no pending request with that id. It is unknown, it was already answered, or the session has had another turn since. Not 404: the response exists, and in UHP a 404 says the addressed object does not.
410 input_request_expired: the answer came after expires_at. The field stays optional. Nothing runs during the wait, so this is a check at answer time.
422 invalid_input: the answer does not fit kind or options. The request stays pending.
The PR follows GOVERNANCE: spec, schema, reference implementation, conformance test and changelog, together.
Reference implementation: one base end to end is enough, for example Claude Code with the server's ask_user tool.
Conformance:
A question ends the turn incomplete with reason input_required and an input_request item.
An answer resumes the session, and the turn completes.
A stale or already-answered id returns 409 input_not_pending.
An option id outside options returns 422 invalid_input, and the request stays pending.
A secret answer appears nowhere in the stream, the stored record or GET /v1/responses/{id}.
@phanisarman, @jpelaez-23blocks: if this works for you both, please say here who is writing the PR, so only one gets written, and credit the other as co-author. Questions are welcome here first.
Scope for #476, in the end-the-turn shape the assessment settled.
What breaks today. The question already arrives and we throw it away. runner/server.py:14531 answers codex's item/tool/requestUserInput with {"answers": {}}, and runner/tests/test_codex_requests.py:52 locks that in.
On the other base, runner/cheetahclaws_driver.py:68 removes AskUserQuestion
from the tool list, with the reason written above it: "a question goes into the
answer instead, where the person reads it". So the question lands in prose and
the turn reports completed. A client cannot tell "done" from "waiting for
you", and no client can route an answer back.
Shape. Four steps, no new route and no new event type.
ask_user is served by the runner through the MCP wiring plugin tools already
use. It returns a structured result — {"status": "input_required"}, no prose
the model has to interpret — and the runner ends the turn at that point,
emitting the input_request item with incomplete and incomplete_details.reason = "input_required". The turn ends on the runner's
decision, not on the model's cooperation. The last message carries the
question as text as well, for clients that render only message items.
The pending request is stored with the response that asked, not in the process.
The answer arrives as input_response on a normal previous_response_id
follow-up. Three refusals before anything runs: 409 input_not_pending,
410 input_request_expired, 422 invalid_input. 422 leaves the request
pending; any other input voids it and the response's metadata names it.
On accept the runner opens the next turn on the same thread
(thread/resume with the same id — runner/server.py:14568), and the answer
is that turn's user message, carrying the question back with it. Nothing holds
a process open in between.
One thing I would verify first. Terminating at the tool result — rather than
letting the agent run on — is the right call: a wait can last days, and anything
the agent does in that window is a side effect that cannot be discarded
afterwards. What that costs is resuming a session whose process was terminated
rather than one that exited on its own. Mature CLIs write their history as they
go, so I expect resume to work here; I would confirm it on the reference base
before relying on it. The resume message carries the question together with the
answer, so the turn does not depend on the ask surviving in the transcript either
way.
Where the request lives.gateway/control_store.py is the right home, for the
same reason you fixed four things in #613: the window. The request goes on resp::{org}::{rid} and is written in the same conditional replace that marks the
response terminal (control_store.py:393). Written after, there is a moment where
a client reads incomplete with input_required and finds no request behind it —
and on Cloud a redeploy between the two writes loses the question for good.
Consuming the answer is a conditional replace on the same ETag, so two replicas
holding the same answer produce one turn and one 409, not two turns. expires_at
needs no sweeper: nothing runs during the wait, so it is a comparison at answer
time.
The question text belongs to the response the client reads. The store keeps only
what an answer is checked against: request id, kind, option ids, expiry.
Commits — one PR, five commits, in the order you can review them:
spec and schema: Tasks §3.1 for the item, §2 for the answer, incomplete_details in the §3 table it is missing from, Lifecycle §3 and the
capability row, errors, schema.
gateway: the item, the capability, the store, the three refusals.
runner: ask_user and the turn end.
conformance.
changelog and suite bump.
Tests, one per clause — F-11 to F-15, each skipping when the server does not
claim input_requests, the way F-10 skips at checks.py:1362:
a question ends the turn incomplete with reason input_required and an input_request item
an answer resumes the thread and the turn completes
an unknown or already-answered id returns 409 input_not_pending
an option id outside options returns 422 invalid_input, request still
pending
a secret answer appears in no stream event, no stored record, and not in GET /v1/responses/{id}
What I'd leave out. Codex's native requestUserInput: it already reaches _codex_request_answer and maps onto the same item cleanly, but it is a second
channel, and one channel that works beats two that half-work. The #479 hold — awaiting_input, paused budgets, a live sandbox. Per-base integrations. A UI to
answer in.
The number. If you want it built, say so and I'll send you a number for it —
I'd rather that be between us than in a thread other contributors read. If the
split isn't right, tell me what to cut and I'll re-scope around it.
Activity
jpelaez-23blocks commentedon Oct 17, 2026
Related proposal: #479 (filed after this one) covers the same problem — a task that needs an answer from the user — with a different design on one axis: pause vs. end.
#476 ends the task with terminal
requires_input, and answering is a normal continuation. #479 pauses it with non-terminalawaiting_input, answers viaPOST /v1/responses/{id}/input, and pauses the task's timeout/step budgets during the hold.The pause model exists for live-loop harnesses: in Operario (a vision/VNC control loop), the hold happens mid-task — ambiguity disambiguation, credential gates — and the harness stays live. Splitting the hold into a task boundary would break the loop's continuity and its budget accounting. The end model fits process-per-turn runtimes better, and #476's MCP
ask_userfallback is a good answer for harnesses without a native question tool.The two proposals agree on the important parts: typed questions, redaction for secrets, timeout/expired semantics, 409/410 handling. They differ on whether the wait is a task boundary or a mid-task state — which may come down to what the runtime supports. Happy to converge if maintainers prefer one shape, or for the spec to support both (pause where the runtime stays live, end where it doesn't).
richard-epsilla commentedon Oct 18, 2026
Thank you, @phanisarman, and @jpelaez-23blocks for linking the two. Our assessment: one proposal, in the end-the-turn shape this issue proposed first. Accepted in principle (GOVERNANCE), with the changes below. We asked #479 for this same shape on 10-18 without linking it here. The two now agree on nearly everything, so the result takes its shape from #476 and its typed request from #479.
Where the two meet
previous_response_id. Nothing holds a sandbox or a process open, and the existing checkpoint and native resume cover a wait of any length. (UEP:requires_input— a task can end by asking the user, and the answer resumes it #476)ask_user, through the existing MCP wiring. (UEP:requires_input— a task can end by asking the user, and the answer resumes it #476)kindischoice,confirm,free_textorsecret, andoptions[]carry{id, label}. (UEP: First-class input requests —input_requiredevent,awaiting_inputstatus, andPOST /v1/responses/{id}/input#479)input_requestsinGET /v1/uhp, saying which bases can ask. (both)Changes to this proposal
incompletewithincomplete_details: {"reason": "input_required"}, notrequires_input.incompleteis already terminal and keeps partial output, and a client already reads it as "usually worth continuing" (Lifecycle §3). A client that switches on today's five statuses keeps working, and Responses SDKs parse it.incompletefrom "a budget stopped it" to "stopped short, with a reason". So the PR amends Lifecycle §3 and definesincomplete_details, which the response example in Tasks shows but the field table does not.function_callandfunction_call_outputalready follow. It streams through the existing output item events, so no new event type is needed:{"type": "input_request", "id": "inreq_…", "kind": "choice", "prompt": "Which invoice?", "options": [{"id": "left", "label": "Top toolbar"}, {"id": "right", "label": "Sidebar"}], "expires_at": 1760000000}{"previous_response_id": "resp_…", "input": [{"type": "input_response", "input_request_id": "inreq_…", "answer": "left"}]}messagealso carries the question as text, because a client that renders onlymessageitems is a valid client (Tasks §3.1).ask_userreturns at once ("asked; your turn ends here"), the runner ends the turn, and the follow-up resumes the session with the answer as its message. A base that can natively resume a pending tool call may deliver the answer there instead; that is an implementation choice, not the contract.409 input_not_pending: the response has no pending request with that id. It is unknown, it was already answered, or the session has had another turn since. Not 404: the response exists, and in UHP a 404 says the addressed object does not.410 input_request_expired: the answer came afterexpires_at. The field stays optional. Nothing runs during the wait, so this is a check at answer time.422 invalid_input: the answer does not fitkindoroptions. The request stays pending.requires_input— a task can end by asking the user, and the answer resumes it #476)secretanswer never enters the conversation. It goes to the server's secret store, scoped to the session, and the harness gets a reference to it, as referenced secrets work today. (UEP: First-class input requests —input_requiredevent,awaiting_inputstatus, andPOST /v1/responses/{id}/input#479)How the two issues split
requires_input— a task can end by asking the user, and the answer resumes it #476 carries this proposal and its PR.input_requiredevent,awaiting_inputstatus, andPOST /v1/responses/{id}/input#479 keeps the mid-turn hold (awaiting_input, budgets paused, a live sandbox) as a later extension. It waits on one answer there: can Operario's loop resume from checkpointed state after its turn ends, and if not, what has to stay live?The PR follows GOVERNANCE: spec, schema, reference implementation, conformance test and changelog, together.
ask_usertool.incompletewith reasoninput_requiredand aninput_requestitem.input_not_pending.optionsreturns 422invalid_input, and the request stays pending.secretanswer appears nowhere in the stream, the stored record orGET /v1/responses/{id}.@phanisarman, @jpelaez-23blocks: if this works for you both, please say here who is writing the PR, so only one gets written, and credit the other as co-author. Questions are welcome here first.
Jahanzeb-git commentedon Oct 31, 2026
Scope for #476, in the end-the-turn shape the assessment settled.
What breaks today. The question already arrives and we throw it away.
runner/server.py:14531answers codex'sitem/tool/requestUserInputwith{"answers": {}}, andrunner/tests/test_codex_requests.py:52locks that in.On the other base,
runner/cheetahclaws_driver.py:68removesAskUserQuestionfrom the tool list, with the reason written above it: "a question goes into the
answer instead, where the person reads it". So the question lands in prose and
the turn reports
completed. A client cannot tell "done" from "waiting foryou", and no client can route an answer back.
Shape. Four steps, no new route and no new event type.
ask_useris served by the runner through the MCP wiring plugin tools alreadyuse. It returns a structured result —
{"status": "input_required"}, no prosethe model has to interpret — and the runner ends the turn at that point,
emitting the
input_requestitem withincompleteandincomplete_details.reason = "input_required". The turn ends on the runner'sdecision, not on the model's cooperation. The last
messagecarries thequestion as text as well, for clients that render only message items.
input_responseon a normalprevious_response_idfollow-up. Three refusals before anything runs: 409
input_not_pending,410
input_request_expired, 422invalid_input. 422 leaves the requestpending; any other input voids it and the response's metadata names it.
(
thread/resumewith the same id —runner/server.py:14568), and the answeris that turn's user message, carrying the question back with it. Nothing holds
a process open in between.
One thing I would verify first. Terminating at the tool result — rather than
letting the agent run on — is the right call: a wait can last days, and anything
the agent does in that window is a side effect that cannot be discarded
afterwards. What that costs is resuming a session whose process was terminated
rather than one that exited on its own. Mature CLIs write their history as they
go, so I expect resume to work here; I would confirm it on the reference base
before relying on it. The resume message carries the question together with the
answer, so the turn does not depend on the ask surviving in the transcript either
way.
Where the request lives.
gateway/control_store.pyis the right home, for thesame reason you fixed four things in #613: the window. The request goes on
resp::{org}::{rid}and is written in the same conditional replace that marks theresponse terminal (
control_store.py:393). Written after, there is a moment wherea client reads
incompletewithinput_requiredand finds no request behind it —and on Cloud a redeploy between the two writes loses the question for good.
Consuming the answer is a conditional replace on the same ETag, so two replicas
holding the same answer produce one turn and one 409, not two turns.
expires_atneeds no sweeper: nothing runs during the wait, so it is a comparison at answer
time.
The question text belongs to the response the client reads. The store keeps only
what an answer is checked against: request id,
kind, option ids, expiry.Commits — one PR, five commits, in the order you can review them:
incomplete_detailsin the §3 table it is missing from, Lifecycle §3 and thecapability row, errors, schema.
ask_userand the turn end.Tests, one per clause — F-11 to F-15, each skipping when the server does not
claim
input_requests, the way F-10 skips atchecks.py:1362:incompletewith reasoninput_requiredand aninput_requestiteminput_not_pendingoptionsreturns 422invalid_input, request stillpending
secretanswer appears in no stream event, no stored record, and not inGET /v1/responses/{id}What I'd leave out. Codex's native
requestUserInput: it already reaches_codex_request_answerand maps onto the same item cleanly, but it is a secondchannel, and one channel that works beats two that half-work. The #479 hold —
awaiting_input, paused budgets, a live sandbox. Per-base integrations. A UI toanswer in.
The number. If you want it built, say so and I'll send you a number for it —
I'd rather that be between us than in a thread other contributors read. If the
split isn't right, tell me what to cut and I'll re-scope around it.