Repository navigation
Adopt the SDK's native AskUserQuestion and rebuild the ask card (#4) - #96
Merged
Merged
Conversation
buildEngineOptions turns on the SDK's own question tool with
toolConfig.askUserQuestion.previewFormat 'html' and
settings.askUserQuestionTimeout 'never' (a Settings field, not an
Option). Read off the installed SDK 0.3.278 / CLI 2.1.278: the tool's
permission check always asks, so every call reaches canUseTool, and the
host answers by allowing with updatedInput.answers (question text ->
answer, multi-select comma-separated), which the CLI echoes back as
tool_use_result.answers.
The engine hands the call to the run's askUser host
($lib/engine/ask-user-question): no tool frames, refused at once with a
reason when there is no host or a subagent asked, settled on Stop, and
never left pending. The chat's host records the question on
chat_runs.pending_questions (opening the /review item and needs-input
push as before), emits the ask_user frame under the SDK's tool_use id,
and waits; the wait now ends early on abort. Saved blocks keep the
answers as details { kind: 'ask_user_question' }.
A question is exempt from approval settings and modes but not from an
agent's tool scope; ask_user is kept as an alias so an agent listing it
can still ask, and every subagent disallows AskUserQuestion.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Nothing live needs it once the SDK's AskUserQuestion is on: its schema, description and "cannot run directly" handler leave the registry, the engine's MCP server no longer special-cases it, and the old loop's ask_user branch (and the trimToolResult helper only it used) goes with the isOrchestrator flag that gated it — that loop's callers are unattended and were never offered it; a model that asks anyway meets the offered-list gate. /api/mcp and the settings approval list no longer have a host-owned registry tool to leave out. The read-only agents' allow-list carries AskUserQuestion in place of ask_user, and the orchestrator prompt tells the model "Other" is automatic and when to give an HTML preview. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The card now shows each question with its header chip, option cards (label, description, a Recommended badge in place of the model's "(Recommended)" suffix), a preview pane for the option under the pointer or keyboard, multi-select, and an automatic free-text Other. The same AskUserQuestionCard serves the inline card, the modal a reloaded page opens and the /review inbox; the click rules and the answer string (labels in option order, then Other, comma-separated) live in the pure $lib/engine/ask-user-question so the specs reach them. Previews are model HTML, so they only ever render in an <iframe sandbox=""> whose srcdoc forbids every load and turns acting tags into text. Answers are keyed by question text (header for an old ask_user question), the /review command accepts keys that long, and a saved AskUserQuestion shows each question with its answer after a reload, or "Not answered." when the call was refused. Old ask_user transcripts still render as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The chat spec describes the new card, how and where a question is answered, what happens when nobody answers, who may ask, why previews are safe, and what was verified in the SDK rather than assumed. Runs, tools, settings, hooks, notifications, observability, UI and structure docs drop ask_user; the README says what the card does; the parity doc gains a row and the re-audit marks #4 shipped. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Focusing the Other box chose it, which on a single-select question dropped the option already picked. Other sits between the options and Submit in tab order, so a keyboard user who picked an option and tabbed on to Submit cleared their own answer on the way and could not answer with an option at all. Other is now a button of its own (aria-pressed, like the option cards): clicking it chooses it and puts the caret in the box, and typing in the box chooses it as before. Focus alone changes nothing. chooseOther replaces focusOther (a toggle on a multi-select question), and a blank edit of an unchosen Other no longer takes away the picked option. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The bundled CLI marks AskUserQuestion concurrency-safe, so one assistant message can open two or three cards at once. Every inline card sent its answers under the page's single pendingAskUser token, which is always the newest card's: answering the first card recorded its answers against the last question, the engine then refused that question as unanswered, and the first one waited out the five minutes. Each inline card now submits with its own block's token. pendingAskUser is only what the composer and the modal answer; settlePendingAskUser keeps it when another card settles and, when its own card is answered or gets its result, hands the role to the newest card still waiting. A tool_result settles the card it landed on (askUserTokenFor), not whichever question was pending, and a retry resends to the same token. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The under-1.5 s refusal check started its clock before driveQuestions imported stream.server, so run alone or as a retry in a fresh worker the cold import counted against the budget. Import it first. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The chat spec says that several questions can wait at once, that each card answers only its own, and where a composer reply goes; and that Other is chosen by clicking or typing, so an option survives tabbing on to Submit. The re-audit's #4 entry records both review follow-ups. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings in #24 (per-turn SDKUserMessage with a minted uuid, sdkTurn / sdkTailUuid on messages.metadata, edit/regenerate cutting the session with resumeSessionAt, enableFileCheckpointing, the rewind control session and /compact) together with the #22 composer (`@` mentions, `/` palette), the #9 gateway (SUBSCRIPTION_MODEL_IDS allow-list, OpenRouter gateway, backend labels) and the #14 right rail that feat/24-rewind already stacks. Conflicts: - src/lib/engine/agent-definitions.ts: keep both imports -- #4's ASK_USER_QUESTION_TOOL (SUBAGENT_DISALLOWED_TOOLS) and #9's isSubscriptionModel/normalizeModelId (a subagent's Claude model). - README.md: one paragraph per feature -- edit/regenerate/restore (#24), the AskUserQuestion card (#4), models and the gateway (#9), composer shortcuts (#22), read aloud, the right rail (#14); the creation-flow paragraph keeps #4's "question card" wording instead of ask_user. - docs/chat/spec.md: blocking actions keep #4's "answers to the agent's questions"; mobile behaviour takes #14's right-rail drawer. - docs/structure/claude-desktop-parity.md: both rows kept -- "Agent asks you a question" (#4 landed) and "Slash commands and @ file mentions" (#22). Checked for semantic conflicts in the files that merged cleanly: - stream/+server.ts: #4's askUser host (createAskUserHost) now rides inside #24's runWithResumeFallback; its tokens are keyed by the SDK's tool_use id, so a fallback attempt cannot collide with the first. #4's removal of the enqueuePendingQuestion/awaitQuestionAnswers import and fulfilAskUser stays removed. - stream.server.ts: #24's transcript-tail tracking runs before #4's host-owned AskUserQuestion branch, so a question turn still records its sdkTailUuid; the streamed single user message keeps stdin open for canUseTool, which is how the question is answered. - options.server.ts: #4's toolConfig/settings sit beside #9's gateway env and #24's resumeSessionAt/enableFileCheckpointing. - parity-reaudit: #14 struck through as shipped in the "cheap independents" list alongside #4. - composer-commands.ts / chat-page-commands.ts (#22): comments name the agent's AskUserQuestion rather than the retired ask_user. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
AgentStudio's custom ask_user tool is replaced by the Agent SDK's own AskUserQuestion. I read the installed SDK (0.3.278, CLI 2.1.278) first. buildEngineOptions now sets toolConfig.askUserQuestion.previewFormat 'html' and settings.askUserQuestionTimeout 'never'. The timeout is a Settings field, not an Option. The CLI's permission check for this tool always asks, so every call reaches canUseTool. The engine hands the call to a new pure module ($lib/engine/ask-user-question). That module allows the call with updatedInput.answers, keyed by question text, or refuses it straight away when there is no host, a subagent asked, the call is malformed, nobody answered, or the run stopped. It never hangs. The chat host (ask-user-host.server.ts) saves the question to chat_runs.pending_questions, which still opens the /review inbox item and the needs-input push. It then emits the ask_user frame under the SDK's tool_use id and waits; the wait now ends early when the run is stopped. Saved blocks keep the answers as details {kind:'ask_user_question'}, so the questions and answers still show after a reload. AskUserQuestionCard was rebuilt with a header chip, option cards with a Recommended badge, and an HTML preview shown in an iframe sandbox="" whose document blocks every load. It also has multi-select and an automatic Other box. The chat card, the modal a reloaded page opens and the /review inbox all use it. The old ask_user tool is removed: its schema, handler, special cases in the engine and MCP server, the old loop's branch plus its isOrchestrator flag, and the dead trimToolResult helper. Old ask_user transcripts still display.
After review: The branch replaces AgentStudio's in-house ask_user tool with the SDK's native AskUserQuestion. It is enabled with toolConfig.askUserQuestion.previewFormat 'html' and settings.askUserQuestionTimeout 'never'. Each call is answered through canUseTool via the chat's ask-user host, which records the question in chat_runs.pending_questions (so /review and the needs-input push still work), sends the ask_user frame and waits up to 5 minutes or until Stop. A saved block carries the answers so a reload shows them. The card was rebuilt with a header chip, option cards with a Recommended badge, a sandboxed srcdoc preview, multi-select and an automatic Other. The legacy tool was removed. This round fixed the three should_fix review items. (1) Each inline card now submits under its own block token. pendingAskUser is now only what the composer and modal answer, and a new pure helper, settlePendingAskUser, hands it to the newest card still waiting when its own card is settled. A tool_result settles the card it landed on (askUserTokenFor), and a retry resends to the same token. (2) Other is now its own aria-pressed button: clicking it or typing in the box chooses it, and focus alone never does, so a keyboard user can pick an option and Tab on to Submit. chooseOther replaces focusOther, and a blank edit of an Other box that is not chosen no longer clears the picked option. (3) The under-1.5 s refusal spec now imports stream.server before starting its clock.
Closes #4
What was addressed
Audit findings (from the 2026-09-22 subsystem audit) were re-checked against the code before any change; "not a bug" means the claim did not hold up.
const started = Date.now(). It passes when run alone with --grep on both projects.Verification
bun run check: 0 errorsbun run build: passesDocs updated
Follow-ups / residual risk
Deferred on purpose, from the reviewer's minor notes:
(a) The server does not check answer keys. recordQuestionAnswers and /chat/[id]/ask-user still accept any keys, so a wrong-token post would still use up that question. The client no longer sends one. A server check would need a separate refusal reason in both /ask-user and answerQuestionFromReview, because the review path closes the item as 'expired' on any non-resolved result. I left it out rather than widen the change.
(b) The abort branch of awaitQuestionAnswers (removes the pending row, closes the review item as 'expired') is covered only through the fake store. No DB-level spec covers it.
(c) The engine's fallback that copies its recorded answers onto
detailsdoes not check is_error. A call the CLI fails after canUseTool allowed it would show those answers on the saved card.(d) The preview frame forces color-scheme: light, so the preview pane is white in dark mode.
Carried over from the earlier round: the
settingsOptions key may collide with other branches that edit options.server.ts; the host waits 5 minutes (QUESTION_TIMEOUT_MS); there is no per-answer length cap on /chat/[id]/ask-user.In this round the only hot file edited was src/routes/chat/[id]/+page.svelte, with small localized changes: 2 new imports, the retry-intent token, resolveAskUser/settleAskUser, one line in the tool_result handler and the inline card's onSubmit. The new logic is in streaming-blocks.ts. Nothing was pushed or merged.
🤖 Generated with Claude Code