fix(streaming): add seq field for strict chunk ordering
- Add seq: u64 to RoomMessageStreamChunkEvent - Frontend sorts by seq on insert for ordered replay - Initial event now includes seq: 0
This commit is contained in:
parent
5b3a6700be
commit
ddd24bfb6d
@ -43,6 +43,7 @@ pub async fn process_message_ai_streaming(
|
||||
let initial_event = RoomMessageStreamChunkEvent {
|
||||
message_id: streaming_msg_id,
|
||||
room_id,
|
||||
seq: 0,
|
||||
content: String::new(),
|
||||
done: false,
|
||||
error: None,
|
||||
@ -81,9 +82,11 @@ pub async fn process_message_ai_streaming(
|
||||
agent::chat::AiChunkType::ToolCall => "tool_call",
|
||||
agent::chat::AiChunkType::ToolResult => "tool_result",
|
||||
};
|
||||
let seq = chunk_count.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||
let event = RoomMessageStreamChunkEvent {
|
||||
message_id: streaming_msg_id,
|
||||
room_id,
|
||||
seq,
|
||||
content: chunk.content,
|
||||
done: chunk.done,
|
||||
error: None,
|
||||
@ -91,7 +94,6 @@ pub async fn process_message_ai_streaming(
|
||||
chunk_type: Some(chunk_type_str.to_string()),
|
||||
};
|
||||
room_manager.broadcast_stream_chunk(event).await;
|
||||
chunk_count.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||
}
|
||||
}) as Pin<Box<dyn std::future::Future<Output = ()> + Send>>
|
||||
};
|
||||
@ -263,6 +265,7 @@ pub async fn process_message_ai_streaming(
|
||||
let event = RoomMessageStreamChunkEvent {
|
||||
message_id: streaming_msg_id,
|
||||
room_id: room_id_inner,
|
||||
seq: 0,
|
||||
content: String::new(),
|
||||
done: true,
|
||||
error: Some(e.to_string()),
|
||||
|
||||
@ -48,7 +48,7 @@ function parseSavedChunks(raw: string | null | undefined): Array<{ type: string;
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Render ordered stream chunks: consecutive thinking tokens are merged into one collapsible block, answer tokens rendered inline. tool_call is hidden. */
|
||||
/** Render ordered stream chunks: consecutive thinking tokens are merged into one collapsible block, answer tokens rendered inline. tool_call and tool_result hidden from UI. */
|
||||
function OrderedStreamChunks({
|
||||
chunks,
|
||||
onMentionClick,
|
||||
@ -59,10 +59,10 @@ function OrderedStreamChunks({
|
||||
/** Show blinking cursor — only during active streaming */
|
||||
showCursor?: boolean;
|
||||
}) {
|
||||
// Group consecutive same-type chunks (tool_call hidden)
|
||||
// Group consecutive same-type chunks (tool_call/tool_result hidden)
|
||||
const groups: Array<{ type: 'thinking' | 'answer'; content: string }> = [];
|
||||
for (const chunk of chunks) {
|
||||
if (chunk.type === 'tool_call') continue;
|
||||
if (chunk.type === 'tool_call' || chunk.type === 'tool_result') continue;
|
||||
const cType = chunk.type === 'thinking' ? 'thinking' : 'answer';
|
||||
const last = groups[groups.length - 1];
|
||||
if (last && last.type === cType) {
|
||||
@ -123,7 +123,7 @@ function ThinkingBlock({ content }: { content: string }) {
|
||||
);
|
||||
}
|
||||
|
||||
// Sender colors — AI Studio clean palette
|
||||
/** Sender colors — AI Studio clean palette */
|
||||
const SENDER_COLORS: Record<string, string> = {
|
||||
system: '#9ca3af',
|
||||
ai: '#1c7ded',
|
||||
|
||||
@ -575,7 +575,7 @@ export function RoomProvider({
|
||||
]);
|
||||
}
|
||||
},
|
||||
onAiStreamChunk: (chunk: { done: boolean; message_id: string; room_id: string; content: string; display_name?: string; chunk_type?: string }) => {
|
||||
onAiStreamChunk: (chunk: { done: boolean; message_id: string; room_id: string; seq?: number; content: string; display_name?: string; chunk_type?: string }) => {
|
||||
console.debug('[RoomContext] onAiStreamChunk', chunk.chunk_type, chunk.done ? '(done)' : '', 'msg:', chunk.message_id);
|
||||
const isToolCall = chunk.chunk_type === 'tool_call' || chunk.chunk_type === 'tool_result';
|
||||
|
||||
@ -622,11 +622,17 @@ export function RoomProvider({
|
||||
setActiveAiStream({ message_id: chunk.message_id, display_name: chunk.display_name });
|
||||
}
|
||||
|
||||
// Append chunk to ordered list — preserves think/answer/tool interleaving.
|
||||
// Insert chunk by seq for strict ordering — even if WS delivers out of order.
|
||||
setStreamingChunks((prev) => {
|
||||
const next = new Map(prev);
|
||||
const existing = next.get(chunk.message_id) ?? [];
|
||||
const newChunks = [...existing, { type: chunk.chunk_type ?? 'answer', content: chunk.content }];
|
||||
type Chunk = { type: string; content: string; seq?: number };
|
||||
const existing: Chunk[] = next.get(chunk.message_id) ?? [];
|
||||
const seq = chunk.seq ?? existing.length;
|
||||
const newChunk: Chunk = { type: chunk.chunk_type ?? 'answer', content: chunk.content, seq };
|
||||
const insertIdx = existing.findIndex(c => c.seq != null && c.seq > seq);
|
||||
const newChunks: Chunk[] = insertIdx === -1
|
||||
? [...existing, newChunk]
|
||||
: [...existing.slice(0, insertIdx), newChunk, ...existing.slice(insertIdx)];
|
||||
next.set(chunk.message_id, newChunks);
|
||||
// Sync ref for done handler access
|
||||
streamingChunksRef.current = new Map(next);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user