mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-08-11 07:19:03 +00:00
_apply_assistant_delta matched AssistantDelta.id anywhere in the transcript, which is correct for a genuine per-message id but not for an empty one. runtime._as_str() coerces a missing/None chunk id to "", and that value is shared by every id-less chunk from every turn, not just the current one. Once one assistant row had id="", every later, unrelated AssistantDelta that also carried id="" (e.g. from a provider that never stamps per-chunk ids) matched that same stale row instead of starting a fresh one, silently folding a second turn's answer backward into the first turn's bubble. Route empty-id deltas to a dedicated path that tracks the current turn's row by position (streaming_anonymous_row_index, reset on RunStarted/ RunEnded/ClearRows) instead of by id, mirroring the existing empty-id guards in _apply_tool_started/_apply_tool_result but adapted for assistant text: unlike a tool call, an id-less assistant delta still needs to be displayed, so it starts a new row rather than being dropped. Multiple id-less chunks legitimately arrive within one turn (per-token streaming), so they keep coalescing into that row -- but only while it is still the transcript tail; once a tool card is appended after it (the same way a genuine id naturally changes across a tool round-trip), the next empty-id delta starts fresh instead of reaching backward past the tool card. Add regression coverage for the cross-turn merge, same-turn coalescing, the tool-call-interleaved edge case, and non-interference with the existing id-keyed path.