feat(server): render model reasoning as timeline activities - #7632
feat(server): render model reasoning as timeline activities#7632jbbottoms wants to merge 2 commits into
Conversation
Reasoning arrives from adapters as content.delta events with streamKind reasoning_text / reasoning_summary_text (and, for some adapters, as item lifecycle events with itemType "reasoning"), but ingestion only consumed assistant_text deltas and tool-lifecycle item types, so no client ever rendered a thought. Buffer reasoning deltas per (thread, item-or-turn), emit a throttled tool.updated activity (title "Thinking") while streaming, and flush a tool.completed activity (title "Thought") on reasoning item completion, turn boundaries, and session exit. Reusing the existing tool activity shape means web, desktop, and mobile all render and collapse the rows with zero client changes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
ApprovabilityVerdict: Not approved Macroscope's review found this PR not approvable — This PR introduces a new feature (reasoning activity rendering) with substantial new logic including buffer management and event handling. There is also an unresolved Medium severity finding about steered reasoning states remaining in progress. Both factors warrant human review. You can add or adjust custom eligibility rules. Learn more. |
…rs only after dispatch succeeds Review findings from the PR bots, both real: - A late turn.completed/turn.aborted (or request boundary) from a superseded turn flushed and wiped EVERY reasoning buffer on the thread, including the active turn's. Boundaries now flush only buffers attributed to the boundary turn (unattributed buffers still flush on any boundary; session.exited still flushes and drops everything). - Buffer state advanced before dispatch: emittedLength moved before the streaming append and the buffer was deleted before the completion flush, so a failed dispatch silently lost the thought. Both now mutate only after the dispatch succeeds, so the logged-and-skipped failure path gets a natural retry at the next delta or boundary. New regression test: a stale turn's boundary leaves the active turn's buffer intact and its own flush id never appears. 53/53 pass. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Both findings were real — fixed in 26afe44:
Added a regression test for the superseded-turn case (stale boundary leaves the active buffer intact, active boundary flushes it). 53/53 pass, typecheck clean. |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 26afe44. Configure here.
| continue; | ||
| } | ||
| yield* flushReasoningBuffer(event, threadId, buffer); | ||
| byKey.delete(key); |
There was a problem hiding this comment.
Steered reasoning stays in progress
Medium Severity
Turn-boundary flush now skips every buffer whose turnId does not match the completing turn. OpenCode-style steers never emit turn.completed for the superseded turn, so its already-emitted Thinking row stays inProgress until session.exited instead of completing when the replacement turn settles.
Reviewed by Cursor Bugbot for commit 26afe44. Configure here.



Models that stream reasoning (Claude thinking blocks, Codex
item/reasoning/*, OpenCode reasoning parts, local reasoning models behind OpenCode) currently show nothing in any T3 client — the thinking never renders on web, desktop, or mobile.Why
Adapters already emit reasoning. Ingestion drops it in two places:
content.deltawithstreamKind: "reasoning_text"/"reasoning_summary_text"has no consumer — the assistant-delta path inProviderRuntimeIngestionmatchesassistant_textexactly.item.started/updated/completedwithitemType: "reasoning"fail theisToolLifecycleItemTypegate in all three activity projections, so they project to[].(Separately, ClaudeAdapter emits thinking deltas without an
itemIdbecausecontent_block_startcreates no block state forthinkingblocks — this change works with or without that, by falling back to a per-turn buffer key.)What
Server-only, one file plus tests. In
ProviderRuntimeIngestion:(thread, itemId ?? turn).tool.updatedactivity (summary/title "Thinking",status: "inProgress", detail = tail of the accumulated text). The first delta emits immediately so the row appears; afterwards re-emit only every 600 buffered chars, mirroring the O(N²) persistence guard the toolitem.updatedpath documents.tool.completedactivity (summary/title "Thought", full text capped at 6000 chars) onitem.completedfor a reasoning item (richer of streamed text vs item detail wins, and the buffer is consumed so turn boundaries cannot double-flush), onturn.completed/turn.aborted/request.opened/user-input.requested, and onsession.exited(which also drops the thread's buffers).Reusing the existing tool activity shape (
tone: "tool",toolCallIdfor collapse) means web, desktop, and mobile all render, collapse, and expand the rows with zero client changes — streaming updates collapse into one row viaderiveToolLifecycleCollapseKey, exactly like streaming tool output.Proof
ProviderRuntimeIngestion.test.ts: streaming + turn-boundary flush; reasoningitem.completedflush picking the richer text and not double-flushing at the turn boundary; no reasoning activities for plain assistant text. Full file: 52/52 pass.tsgo --noEmit: clean.Notes for review
reasoning_summary_text) share the item's buffer with full reasoning text; adapters that emit both for one item interleave. Codex GPT models emit only summaries, so in practice each item carries one kind."thinking": ""with token estimates only), and Codex reasoning items can arrive with emptycontent/summarywhen summaries are not enabled. In both cases this change correctly renders nothing.payload.detailpersists to the projection table and the event store.🤖 Generated with Claude Code
Note
Medium Risk
Adds in-memory buffering and new persisted activity payloads on a hot ingestion path; turn-scoping and caps limit wrong-flush and storage blow-up, but timeline volume and ordering still depend on provider event timing.
Overview
Provider runtime ingestion now turns streamed model reasoning into thread timeline activities so web, desktop, and mobile can show “Thinking” / “Thought” without client changes.
Reasoning from
content.delta(reasoning_text/reasoning_summary_text) anditem.completedwithitemType: "reasoning"is buffered per thread and item (or per turn whenitemIdis missing). While streaming, ingestion emits throttledtool.updatedrows (summary Thinking,inProgress, tail-truncated detail); on flush it emitstool.completed(Thought, full text capped at 6k with an optional truncation marker). Flushes happen on reasoning item completion (preferring richer item detail over streamed text), on turn boundaries (turn.completed/turn.abortedand approval/user-input pauses) scoped to that turn so stale completions cannot wipe active reasoning, and onsession.exited(buffers cleared).Reusing the existing tool activity shape and
toolCallIdkeeps collapse behavior aligned with streaming tools.ProviderRuntimeIngestion.test.tsadds coverage for streaming + turn flush, item.completed richness and no double-flush, superseded-turn boundaries, and no noise from plain assistant text.Reviewed by Cursor Bugbot for commit 26afe44. Bugbot is set up for automated code reviews on this repo. Configure here.
Note
Render model reasoning as 'Thinking' and 'Thought' timeline activities
handleReasoningEventto ProviderRuntimeIngestion.ts to bufferreasoning_textdeltas and emit periodictool.updated('Thinking') activities.tool.completed('Thought') activity onitem.completedor turn/session boundaries, using the richer of buffered or item text.REASONING_BUFFER_TEXT_CAP(60000) andREASONING_FINAL_DETAIL_LIMIT(6000), adding truncation markers when text is capped.reasoningBuffersByThreadstate that retains up to 60000 characters per active reasoning stream until flushed.Macroscope summarized 26afe44.