Skip to content

Commit 380b677

Browse files
committed
fix: break thinking flow when user is steering the message
AdminForth/1755/i-can't-write-to-this-input-wh
1 parent 0294f43 commit 380b677

2 files changed

Lines changed: 73 additions & 2 deletions

File tree

custom/composables/agentStore/useAgentSteerQueue.ts

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,21 @@ export function createAgentSteerQueue({
3535
queue.value = queue.value.filter((item: QueuedMessage) => item.id !== id);
3636
}
3737

38-
function injectSteerMessage(text: string) {
38+
// Break the assistant's live stream at the steer point: freeze whatever streamed so far
39+
// (e.g. the current reasoning block), drop the steer message in, and let the rest stream
40+
// into a fresh assistant message after it — so the conversation reads
41+
// "reasoning block → steer → new reasoning block" instead of the steer landing next to
42+
// the user's original prompt.
43+
//
44+
// This reaches into the AI SDK's in-flight streaming state (`chat.activeResponse`):
45+
// - reasoning/text parts are looked up by id reference in `active*Parts`, so pointing
46+
// those entries at fresh (empty) parts redirects subsequent deltas past the steer;
47+
// - the stream writer pushes a *new* message whenever `state.message.id` no longer
48+
// matches the last displayed message, so swapping in a new `message` splits the bubble
49+
// without duplicating the frozen block.
50+
// If that internal shape isn't present (not streaming, or a future SDK change), we fall
51+
// back to inserting the steer just above the streaming assistant.
52+
async function injectSteerMessage(text: string) {
3953
const chat = currentChat.value;
4054
if (!chat) {
4155
return;
@@ -46,6 +60,11 @@ export function createAgentSteerQueue({
4660
parts: [{ type: 'text', text, state: 'done' }],
4761
};
4862
const messages = chat.messages;
63+
64+
if (breakStreamWithSteer(chat, messages, steerMessage)) {
65+
return;
66+
}
67+
4968
const lastMessage = messages[messages.length - 1];
5069
if (lastMessage?.role === 'assistant') {
5170
messages.splice(messages.length - 1, 0, steerMessage);
@@ -54,6 +73,58 @@ export function createAgentSteerQueue({
5473
}
5574
}
5675

76+
function breakStreamWithSteer(chat: Chat<any>, messages: any[], steerMessage: IMessage): boolean {
77+
try {
78+
const streamingState = (chat as any).activeResponse?.state;
79+
const streamingMessage = streamingState?.message;
80+
const lastIndex = messages.length - 1;
81+
const lastMessage = messages[lastIndex];
82+
const isStreamingIntoLastAssistant =
83+
streamingMessage?.role === 'assistant'
84+
&& Array.isArray(streamingMessage.parts)
85+
&& streamingMessage.parts.length > 0
86+
&& lastMessage?.role === 'assistant';
87+
88+
if (!isStreamingIntoLastAssistant) {
89+
return false;
90+
}
91+
92+
// 1) Freeze block 1 as a fresh, decoupled assistant message. Copying the parts (and
93+
// reassigning the array slot) detaches it from the SDK's live parts and forces a
94+
// re-render so the block stops showing as "streaming".
95+
const frozenParts = streamingMessage.parts.map((part: any) => ({
96+
...part,
97+
state: part?.state === 'streaming' ? 'done' : part?.state,
98+
}));
99+
messages[lastIndex] = { ...streamingMessage, parts: frozenParts };
100+
console.log('Frozen parts:', frozenParts);
101+
// 2) Redirect the SDK's in-flight reasoning/text parts into a fresh continuation
102+
// message so subsequent deltas (resolved by id reference) land after the steer.
103+
const continuation: any = { id: crypto.randomUUID(), role: 'assistant', parts: [] };
104+
const redirectActiveParts = (activeParts: Record<string, any> | undefined) => {
105+
if (!activeParts) {
106+
return;
107+
}
108+
for (const id of Object.keys(activeParts)) {
109+
const continued = { ...activeParts[id], text: '', state: 'streaming' };
110+
activeParts[id] = continued;
111+
continuation.parts.push(continued);
112+
}
113+
};
114+
redirectActiveParts(streamingState.activeReasoningParts);
115+
redirectActiveParts(streamingState.activeTextParts);
116+
117+
// 3) Hand the SDK the continuation as its streaming message; its next write() pushes
118+
// it as a new assistant bubble. Slot the steer between the frozen block and it.
119+
streamingState.message = continuation;
120+
messages.push(steerMessage);
121+
return true;
122+
} catch (error) {
123+
console.error('Steer stream-break failed, inserting steer inline instead', error);
124+
return false;
125+
}
126+
}
127+
57128
// Persist the steer by appending it onto the running turn's prompt (display only — the
58129
// LLM already has it via the running turn + checkpointer, so this never re-enters the
59130
// model context). The backend adds the marker; the reload path splits it back out.

custom/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
"@shikijs/langs": "^4.0.2",
1919
"@shikijs/themes": "^4.0.2",
2020
"@vueuse/core": "^14.2.1",
21-
"ai": "^6.0.168",
21+
"ai": "6.0.168",
2222
"custom-vue-scrollbar": "^0.0.8",
2323
"dompurify": "^3.3.3",
2424
"katex": "^0.16.45",

0 commit comments

Comments
 (0)