Ask several questions on one card

One `ask_user` call can now carry several questions, and they come back in a
single submit. Asking one at a time cost a round trip and an interruption
each, and by the third you had forgotten the first.

Each question becomes an item with its own key; several items share a call
index, because they belong to one call and one tool turn has to answer them
all. Each answer is quoted beside the question it belongs to -- with four on
a card, a bare list would leave the model matching them up by position and
sometimes getting it wrong.

Options are radios rather than submit buttons, so picking one does not send
the form while two other questions are still blank. What you type beats what
you picked: someone who writes in the box after clicking an option meant the
writing.

`_questions_in` also reads the shapes a small model actually sends -- a bare
`question` string, a list of plain strings, one object where a list belonged.
Getting that wrong costs a whole round trip and shows a card saying nothing.

Two test fixes, both mine. `test_posting_a_message_stores_both_turns` raced
the background generation it started: against a connection that refuses
instantly the reply sometimes won, writing the error and marking the row
complete before the assertions could read it. And the generation registry is
module-global, so a test that started a reply left an entry -- and a Task
belonging to a closed event loop -- for the rest of the session.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jaroslav Beneš
2026-08-01 22:01:46 +02:00
co-authored by Claude Opus 5
parent fe25f596da
commit 4b892054a4
9 changed files with 628 additions and 135 deletions
+34 -17
View File
@@ -750,31 +750,48 @@ REGISTRY: dict[str, ToolDef] = {
name="ask_user",
family=FAMILY_ASK,
description=(
"Ask the person you are talking to a question, and wait for their "
"answer before going on. Use it when you genuinely need a decision "
"only they can make — which of several approaches to take, a "
"detail you cannot infer, permission for something consequential. "
"Offer options when there is a small set of sensible answers; they "
"can always type something else instead. Do not use it for "
"anything you can work out yourself, and never ask for a password, "
"a key or any other secret."
"Ask the person you are talking to one or more questions, and wait "
"for their answers before going on. Use it when you genuinely need "
"a decision only they can make — which of several approaches to "
"take, a detail you cannot infer, permission for something "
"consequential. Offer options when there is a small set of "
"sensible answers; they can always write their own instead. "
"\n\n"
"Ask everything you need in ONE call: they answer the whole card at "
"once and it costs them a single interruption, where asking twice "
"in a row costs two. Do not use it for anything you can work out "
"yourself, and never ask for a password, a key or any other secret."
),
parameters=_object(
{
"question": {
**_STRING,
"description": "One clear question, in plain language.",
},
"options": {
"questions": {
"type": "array",
"items": _STRING,
"description": (
"Up to six answers to offer as buttons. Optional; they "
"can always write their own."
"The questions to put, answered together. Ask up to "
"about four at a time; more than that is a form, not a "
"conversation."
),
"items": {
"type": "object",
"properties": {
"question": {
**_STRING,
"description": "One question, in plain language.",
},
"options": {
"type": "array",
"items": _STRING,
"description": (
"Up to six answers to offer for this "
"question. Optional."
),
},
},
"required": ["question"],
},
},
},
["question"],
["questions"],
),
# Never resolved by this runner. The reader answers it, in every
# mode, and the loop turns their answer into the outcome -- see