Continue generation ("say more") affordance #12
Labels
No labels
Compat/Breaking
Kind/Bug
Kind/Documentation
Kind/Enhancement
Kind/Feature
Kind/Security
Kind/Testing
Priority
Critical
Priority
High
Priority
Low
Priority
Medium
Reviewed
Confirmed
Reviewed
Duplicate
Reviewed
Invalid
Reviewed
Won't Fix
Status
Abandoned
Status
Blocked
Status
Need More Info
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
lyssieth/sermones#12
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Problem
The user can stop a live stream and branch (
regenerate_assistant), but cannot ask the model to continue a finished reply that ended withfinish_reason: "length"(token cap) or otherwise feels cut short.regenerate_assistantbranches a sibling assistant message under the same user message (crates/sermones-core/src/llm/conversation.rs:640), so it does not satisfy "say more" — the user's mental model is the same prompt, the same reply, with more appended.What changed underneath this issue
Exchange, noTurnBody, nopath_idx. The tree is now flat and strictly alternating:UserMessageandAssistantMessagenodes withparent/children(conversation.rs:124-149,:194-233). A user message's children are assistant replies; an assistant message's children are follow-up user messages. So "append a second assistant message to the same turn" is not expressible — a continuation must extend the existingAssistantMessage.contentwith moreBlock::Output. That is simpler than the old plan, not harder.read_streamhardcodesChatOptions::default(). It no longer does:read_stream(client, opts, context, cancel, tx)takes options (crates/sermones/src/streaming.rs:307,321,341), andbegin_turn_streamresolves them per request from the selected model viachat_options_from_model(crates/sermones/src/bridge.rs:589,1065→crates/sermones-runtime/src/providers/llama.rs:263-268). The only survivingChatOptions::default()calls are in tests (streaming.rs:1036,1139).AssistantMessage.model: Option<ModelKey>is the immutable record of what produced this message, distinct from the conversation's mutableselected_model(conversation.rs:220-233,:455). A continuation must re-use the message's ownmodel, not the conversation's current selection, or the second leg comes from a different model than the first.max_tokens/temperaturetherefore need no new storage — they are derivable from the frozenModelKey. The one knob still missing for a faithful continuation isseed: absent from bothChatOptions(llama.rs:241-249) and the wireChatRequest(llama.rs:274-281).The actual crux now
Two things fight the naive implementation:
1.
context_messagesdrops the message being continued. It skips any assistant whosestatus == Streaming— "it is the response, not history" (crates/sermones-runtime/src/session/mod.rs:562-567). But a continuation's whole point is that the partial reply is in the prompt. Marking the targetStreamingand then building context silently loses the text the model is meant to extend. Options, cheapest first:begin_turn_streamcallsconversations.context(...)atbridge.rs:568— it would need the flag, or the ordering, threaded through).AssistantStatus::Continuingand teachcontext_messagesto emit it. Costs a variant on a persisted enum, so it costs a config/conversation migration — checkdocs/components/conversation-migration.mdbefore choosing this.2. Chunks carry the whole fragment, not a delta.
stream_chunksends the fully re-rendered bubble and the frontend replaces wholesale (assistant.html = payload.html,frontend/src/lib/conversations.svelte.ts:365-371;compose_fragmentatstreaming.rs:188-196). If the continuation's reader starts from an empty assembler, the bubble visibly resets to just the new text mid-stream and only recovers on the terminal. So the reader must be seeded with the target message's existing blocks, andcompose_fragmentmust render prior + new output as one fragment.Design
1. IPC surface
ContinueAssistantParams { conversation_id: String, assistant_id: String }incrates/sermones-core/src/ipc.rs, mirroringRegenerateAssistantParams(ipc.rs:154-161); registered inIPC_TYPES. Codegen emits the TS type, schema, and hash.continue_assistantslot incrates/sermones/src/bridge.rsnext toregenerate_assistant(bridge.rs:448). It resolves the existingassistant_idrather than branching a sibling, then runsbegin_turn_streamagainst it.TurnStreamResultneeds no change —{ sub_id, assistant_message_id }already says everything, and the id is the button's own message.2. Engine
Conversation::extend_assistant_content(assistant_id, Vec<Block>)next toset_assistant_content(conversation.rs:774) — appends instead of replacing.ConversationStore::continue_stream(id, assistant_id, message, model)next tofinish_stream(session/mod.rs:407-431): same status/finish_reason/usage handling, butextend_assistant_contentinstead ofset_assistant_content, and it must not overwritemodelwith a fresh snapshot.TurnOutcome::Continued(Box<Message>)alongsideFinished/PartialFinished/Errored, routed insettle_turn(bridge.rs:648-668).PartialFinishedneeds a continuation twin too — cancelling a continuation mid-flight must keep both legs, andabandon_with_contentcurrently replaces (session/mod.rs:485-498).Subscription/StreamTarget(bridge.rs:616-628) is where thecontinue: boolgoes, set at allocation; the forwarder picks the terminal from it.seedonChatOptionsandChatRequest(llama.rs), dropped from the wire body whenNone(llama.cpp/gemma servers do not take it, and must tolerate its absence).3. Frontend
bridge.ts:continueAssistant(bridge, { conversation_id, assistant_id }, handler)mirroringregenerateAssistant(bridge.ts:251-257).conversations.svelte.ts:continue(assistantId)mirroringregenerate(:183).#onChunkand#onEndedneed no change — the sub mapping already keyssub_id → (conversation, assistant message)and the message id is unchanged.MessageView.svelte: a new button in the assistant toolbar (:350-380, e.g.MessageSquarePlusfrom lucide-svelte, which is already the icon source at:29-35), gated on!isStreaming(:158) and only when the message is the leaf of the current path — you cannot continue a message you have navigated away from. WireChatView.svelte→App.sveltealongsideonRegenerate(App.svelte:207).conversations.test.ts: terminal-routing + chunk-routing coverage for the continue path, including "the bubble does not lose its existing html on the first continuation chunk".4. Not gating on
finish_reasonTempting: show the button only when the provider cut the reply — the label already exists (
MessageView.svelte:164-171, non-stopreasons only,:423-424). Rejected for v1: a user can reasonably want Continue on astopreply ("now go on"), and the affordance speaks for itself. Available on anyDonemessage; gate later if usage says otherwise.Files expected to change
crates/sermones-core/src/ipc.rs—ContinueAssistantParams, registrationcrates/sermones-core/src/llm/conversation.rs—extend_assistant_contentcrates/sermones-runtime/src/providers/llama.rs—seedonChatOptions+ChatRequest(optional on the wire)crates/sermones-runtime/src/session/mod.rs—continue_stream, and thecontext_messagesfix abovecrates/sermones/src/bridge.rs—continue_assistantslot,TurnOutcome::Continued, subscription mode, seeded readercrates/sermones/src/streaming.rs— assembler seeded from existing blocks socompose_fragmentemits prior + newfrontend/src/lib/bridge.ts,conversations.svelte.ts,MessageView.svelte,App.svelte,ChatView.sveltefrontend/src/lib/conversations.test.tsOpen questions
Where doesResolved. Options resolve from the frozenRequestOptionslive?AssistantMessage.modelthroughchat_options_from_model; no per-turn options struct is needed. Onlyseedis genuinely missing, and it belongs onModelConfig/ChatOptionslike the other knobs.How does the projection render multiple assistant exchanges in one turn?Resolved by the tree refactor. A continuation extends oneAssistantMessage, soAssistantMessageViewkeeps its singletext/html(ipc.rs:242-257) and the projection is untouched.Block::Outputruns concatenate into one paragraph flow. That is right for a mid-sentencelengthcut and wrong for "now do it again, longer". Cheapest answer: no marker in the data, no separator in the render — revisit if it reads badly.AssistantStatus::Continuingor context-before-mark? See the crux above; the migration cost is the deciding factor.Out of scope
RequestMessagestill does not carrytool_call_id(llama.rs:199-205). Separate issue.continueaffordance on user-initiated branching.Related
seedwould become user-visibledocs/architecture/streaming.md— cancellation / partial-text semantics the continuation twin must respect