Skip to content

fix(tui): keep streaming transcript entries in order around tool calls#72

Merged
ZouR-Ma merged 1 commit into
stepfun-ai:mainfrom
li-xiu-qi:fix/streaming-transcript-order
Jul 17, 2026
Merged

fix(tui): keep streaming transcript entries in order around tool calls#72
ZouR-Ma merged 1 commit into
stepfun-ai:mainfrom
li-xiu-qi:fix/streaming-transcript-order

Conversation

@li-xiu-qi

Copy link
Copy Markdown
Contributor

Problem

During an agent run (before it finishes), the TUI transcript could display assistant text and tool-call entries in the wrong order. Specifically, if a model streamed text, emitted a tool call, and then streamed more text within the same response, the text after the tool call was appended to the assistant entry that appeared before the tool entry instead of creating a new entry after it.

Closes #71

Root cause

LocalOpenTuiTranscriptBridge.onModelToolCall (and onToolStart) called discardEmptyAssistantEntry() but did not reset currentAssistantEntryId. discardEmptyAssistantEntry only removes the entry when it is blank, so once text had streamed in, the ID stayed alive and pointed at the pre-tool entry.

Additionally, when currentAssistantEntryId was null at commitAssistantMessage time, it would create a duplicate assistant entry rather than updating the streaming entry with the final message content.

Solution

  • Reset currentAssistantEntryId to null after appending/updating a tool entry in onModelToolCall and onToolStart.
  • Add findLastStreamingAssistantEntryId() to locate the last transient assistant entry for the current turn.
  • In commitAssistantMessage, when currentAssistantEntryId is null, update that last streaming entry with the final content instead of creating a duplicate.

Verification

  • Added src/runtime/local-opentui-bridge.test.ts with 5 unit tests covering:
    • Text followed by tool call keeps tool entry after assistant entry
    • Text after tool call creates a new assistant entry after the tool entry
    • commitAssistantMessage correctly updates the streaming entry after tool-call reset
    • Multiple consecutive tool calls without duplicate assistant entries
    • Separate model requests reset streaming state correctly
  • pnpm test passes (1479 passed)
  • pnpm check passes tests, lint (only pre-existing warnings), dep-guard, dead-code, and formatting.

Related

@github-actions github-actions Bot added the area/cli src/cli, src/commands, src/runtime, src/bootstrap, src/*.ts label Jun 19, 2026
@li-xiu-qi
li-xiu-qi force-pushed the fix/streaming-transcript-order branch from a225d63 to 933c414 Compare June 19, 2026 09:17
@li-xiu-qi
li-xiu-qi force-pushed the fix/streaming-transcript-order branch from 933c414 to 7142c9c Compare July 15, 2026 05:27
@li-xiu-qi

Copy link
Copy Markdown
Contributor Author

Rebased onto the latest main (which now includes #32). No conflicts. pnpm test src/runtime/local-opentui-bridge.test.ts passes. Ready for review.

@ZouR-Ma

ZouR-Ma commented Jul 17, 2026

Copy link
Copy Markdown
Collaborator

The root-cause analysis is right — currentAssistantEntryId never being reset in onModelToolCall/onToolStart is exactly what produced the ordering bug in #71, and the test cases pin down the streaming scenarios (text-then-tool, text-after-tool, consecutive tool calls, cross-request reset) well; giving this previously untested bridge test coverage is a net win by itself.

Status update: #69 has landed on main (c6ed3c7), so this PR now conflicts. Two items before it lands:

  1. (Must) Rebase to resolve the conflict. main now has src/runtime/local-opentui-bridge.test.ts (added by feat(tui): collapse tool-call and reasoning output, toggle with Ctrl+O #69 with the reconcile/reasoning-split cases) — fold your streaming describe block into that same file. The changes to local-opentui-bridge.ts itself don't overlap (yours are in the streaming callbacks, feat(tui): collapse tool-call and reasoning output, toggle with Ctrl+O #69's in the mapping/reconcile area), so git merges those automatically.
  2. (Must) Turn-settle reconciliation leaves an orphaned fragment. With the entry split, the streamed text from before the tool call becomes its own entry (e.g. "Before tool. "), but doesSessionAssistantCoverLocalAssistant (local-opentui-bridge.ts:809) only treats a local entry as covered on an exact content match, or when the remainder starts with \n[. At settle time the session history holds the full message "Before tool. After tool." — the prefix fragment satisfies neither rule, so that transient entry stays in the transcript permanently, duplicated right next to the authoritative entry. Repro: text → tool → text, let the turn settle, and an extra "Before tool." line remains. Suggested fix: relax the coverage check so that session content starting with the local content counts as covered, and drop the \n[ special case at :827 while you're there — feat(tui): collapse tool-call and reasoning output, toggle with Ctrl+O #69 removed the inline [reasoning] suffix, so that branch is dead code now. Add one settle test: after text→tool→text, call reconcileWithSessionMessages(messages, turnId) and assert no leftover fragment. Acceptance: the new test passes and the existing ones don't regress.

One known behavior that does not block the merge: commitAssistantMessage writes the full final text into the post-tool entry, so the prefix shows twice while the turn is still running; once the coverage fix above is in, settle cleans it up. Committing only the not-yet-displayed suffix would keep it clean mid-turn too — fine as a follow-up.

Ping us once both items are in — this lands right after.

Reset currentAssistantEntryId in onModelToolCall/onToolStart so text
that arrives after a tool call within the same streaming response is
rendered in a new assistant entry positioned after the tool entry.

When currentAssistantEntryId is null at commitAssistantMessage time,
find the last transient assistant entry for the current turn and apply
the final message content there, avoiding a duplicate assistant entry.

Fixes stepfun-ai#71
@li-xiu-qi
li-xiu-qi force-pushed the fix/streaming-transcript-order branch from 7142c9c to fa1a001 Compare July 17, 2026 07:29
@ZouR-Ma
ZouR-Ma merged commit 87c3445 into stepfun-ai:main Jul 17, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/cli src/cli, src/commands, src/runtime, src/bootstrap, src/*.ts

Projects

None yet

Development

Successfully merging this pull request may close these issues.

tui: streaming transcript entries appear out of order when tool calls interleave with text

2 participants