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:
@@ -245,9 +245,51 @@ work: a follower arriving late has no earlier fragments to append to. It also
|
||||
means Markdown is re-rendered whole, which is required anyway -- a list or code
|
||||
fence is only correct once its context exists.
|
||||
|
||||
**Stopping sets a flag the producer checks.** `generation.request_stop()`;
|
||||
whatever arrived is kept and the message is marked `stopped`, which is distinct
|
||||
from `error`. In-process, so single-worker only.
|
||||
**Two frames must be able to blank themselves, and the rest must not.**
|
||||
`reasoning`, `tools` and `render` are only sent when they have something in
|
||||
them, so a frame can never wipe what is on screen. `metrics`, `status` and
|
||||
`ask` are sent on every version bump *including empty*, because each has to be
|
||||
able to clear: an approval card that survived being answered would be a button
|
||||
you could press twice.
|
||||
|
||||
**Stopping sets a flag the producer checks -- except while it is paused.**
|
||||
`generation.request_stop()`; whatever arrived is kept and the message is marked
|
||||
`stopped`, distinct from `error`. In-process, so single-worker only. `cancel` is
|
||||
read in exactly one place, between streamed chunks, and a reply waiting on an
|
||||
approval produces no chunks -- so `request_stop` also resolves
|
||||
`generation.pending`, and that is the wakeup. Without it the Stop button does
|
||||
nothing at all while a card is on screen, silently.
|
||||
|
||||
**Asking a person is one primitive with three uses.**
|
||||
`services/interaction.py`: a command waiting to be allowed, a question the model
|
||||
asked, and "this reply is waiting for you" are all *pause, render a block in the
|
||||
bubble, wait for a POST, resume*. It pauses a **round, not a call** -- a round's
|
||||
calls run together under a semaphore, and parking four coroutines on four
|
||||
separate answers inside that gather would queue them behind each other
|
||||
invisibly, and hand the reader four cards for commands whose order matters. So
|
||||
one card covers everything in the round, and `_authorise` returns pre-decided
|
||||
outcomes keyed by call index, which is what keeps
|
||||
`zip(calls, outcomes, strict=True)` aligned.
|
||||
|
||||
**One `ask_user` call may carry several questions, and they come back at once.**
|
||||
Each becomes an `Item` with its own `key`; several items can share an `index`
|
||||
because they belong to one call, and one tool turn answers them all with each
|
||||
answer quoted beside its question. Asking one at a time would cost a round trip
|
||||
and an interruption each, and answering the third would mean having forgotten
|
||||
the first. `_questions_in` reads the singular form and bare strings too: a small
|
||||
model sends something close to the schema rather than the schema, and getting it
|
||||
wrong costs a whole round trip to show a card that says nothing.
|
||||
|
||||
**A paused reply is deliberately not `done`.** That is what lets a page reload
|
||||
reattach to it. Its *timeout* is what stops it lingering, not `_prune`, which
|
||||
only drops finished ones -- so `approval_timeout` is clamped to at least a
|
||||
minute on read, and `_prune` resolves anything whose deadline is long past as a
|
||||
backstop.
|
||||
|
||||
**Nothing is persisted while paused.** A restart abandons the pending question
|
||||
along with the reply, and a reload starts the turn afresh -- the model asks
|
||||
again. That is consistent with "a restart abandons replies in flight", but it
|
||||
means an approval is not a durable record of consent.
|
||||
|
||||
**Unread is polled, not pushed.** A browser on another chat has no connection
|
||||
to the one that finished. `/api/chats/unread` returns out-of-band dot spans and
|
||||
|
||||
Reference in New Issue
Block a user