Skip to content

fix: preserve signed thinking order around provider tools - #931

Open
ekkoitac wants to merge 1 commit into
TanStack:mainfrom
ekkoitac:fix/issue-910-thinking-order
Open

fix: preserve signed thinking order around provider tools#931
ekkoitac wants to merge 1 commit into
TanStack:mainfrom
ekkoitac:fix/issue-910-thinking-order

Conversation

@ekkoitac

@ekkoitacekkoitac commented Jul 14, 2026

Copy link
Copy Markdown

🎯 Changes

Anthropic requires each signed thinking block to remain in its original position relative to tool-use blocks. UI message replay could move a later thinking block ahead of an earlier provider-executed tool, causing Anthropic to reject the request.

This PR:

  • starts a new assistant segment when thinking follows a provider-executed tool, while keeping ordinary local tool calls in the same turn
  • adds unit coverage for the signed-thinking sequence and the local-tool round trip
  • adds a deterministic Anthropic-shaped E2E validator for the exact request block order

Fixes#910

✅ Checklist

  • I have followed the steps in the Contributing guide.
  • I have tested this code locally with pnpm run test:pr.

Validation

  • pnpm --filter @tanstack/ai test:lib -- message-converters.test.ts — 1,138 tests passed across 63 files
  • pnpm --filter @tanstack/ai-e2e test:e2e -- tests/anthropic-server-tool.spec.ts — 2 tests passed
  • pnpm test:pr — 260 tasks passed across 63 projects; 687 declaration files scanned clean
  • pnpm --filter @tanstack/ai-e2e test:e2e — 349 passed, 1 gated live smoke skipped, no retries or flaky failures

🚀 Release Impact

  • This change affects published code, and I have generated a changeset.
  • This change is docs/CI/dev-only (no release).

Summary by CodeRabbit

  • Bug Fixes

    • Preserved signed thinking content in the correct order around provider-executed tool calls.
    • Prevented thinking blocks from being replayed before related tool activity.
    • Maintained tool-call metadata and separate tool results during message conversion.
    • Improved round-trip handling of local tool calls, results, and assistant messages.
  • Tests

    • Added coverage for thinking-order scenarios and Anthropic streaming flows.

@coderabbitai

coderabbitaiBot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@tombeckenham, you've reached your PR review limit, so we couldn't start this review.

Next review available in:16 minutes

Limit details: You’ve used all 8 included reviews currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

Wait for the limit to reset, then comment @coderabbitai review or push new commits to the PR.

An organization admin can change what happens after included review limits in Billing.

How do review limits work?

CodeRabbit enforces per-developer PR review limits within each organization.

For paid Pro and Pro+ reviews, CodeRabbit uses a developer's included PR review attempts over the past 7 days to set the current hourly allowance. At typical activity levels, the full plan allowance applies. Higher sustained activity can lower the allowance until earlier attempts leave the 7-day window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: a75c0193-ea67-48e7-80ec-a46d662f7280

📥 Commits

Reviewing files that changed from the base of the PR and between c092231 and 9a6b74e.

📒 Files selected for processing (6)
  • .changeset/calm-thinkers-wait.md
  • packages/ai/src/activities/chat/messages.ts
  • packages/ai/tests/message-converters.test.ts
  • testing/e2e/global-setup.ts
  • testing/e2e/src/routes/api.anthropic-bug-test.ts
  • testing/e2e/tests/anthropic-server-tool.spec.ts
📝 Walkthrough

Walkthrough

Assistant message conversion now preserves signed thinking order by starting a new assistant segment after provider-executed tool calls. Unit tests and an Anthropic end-to-end regression test validate provider and local tool-call ordering.

Changes

Signed thinking order preservation

Layer / File(s)Summary
Assistant segment boundary conversion
packages/ai/src/activities/chat/messages.ts, packages/ai/tests/message-converters.test.ts
Conversion flushes assistant segments before thinking blocks that follow provider-executed tools, with coverage for provider and local tool-call round trips.
Anthropic ordering validation and route flow
testing/e2e/global-setup.ts, testing/e2e/src/routes/api.anthropic-bug-test.ts
The mock Anthropic server validates signed thinking/tool-use order, while the route streams a dedicated thinking-order scenario.
Regression assertion and release note
testing/e2e/tests/anthropic-server-tool.spec.ts, .changeset/calm-thinkers-wait.md
The e2e test verifies successful completion and RUN_FINISHED; the changeset records the patch-level behavior change.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Possibly related PRs

  • TanStack/ai#855: Introduced provider-executed tool-call metadata and related handling used by this segmentation change.

Suggested reviewers:tombeckenham, alemtuzlak

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check nameStatusExplanation
Title check✅ PassedThe title clearly and concisely summarizes the main fix around signed thinking order with provider tools.
Description check✅ PassedThe description follows the template and includes the change summary, checklist, validation, and release impact.
Linked Issues check✅ PassedThe code and tests address #910 by splitting assistant segments to preserve signed thinking order around provider-executed tools.
Out of Scope Changes check✅ PassedThe changes are focused on the bug fix, related tests, e2e validation, and release note, with no obvious unrelated additions.
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/ai/src/activities/chat/messages.ts (1)

240-255: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Preserve pending thinking when flushing an otherwise empty segment.flushSegment() only emits when content or toolCalls exist, so the new provider-tool split can leave a trailing signed thinking block stranded in pendingThinking and then dropped on the final flush. Add pendingThinking.length > 0 to the guard and cover the terminal-thinking case in tests.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/ai/src/activities/chat/messages.ts` around lines 240 - 255, The
flushSegment function currently drops pending thinking when a segment has no
content or tool calls. Include pendingThinking.length > 0 in its emission guard
so thinking-only segments are appended and cleared correctly, and add a test
covering a terminal thinking block during the final flush.
🧹 Nitpick comments (2)
packages/ai/tests/message-converters.test.ts (1)

387-404: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Round-trip assertion doesn't verify the thinking part survives.

The round-trip check only asserts the tool-call/tool-result parts are present; it never confirms { type: 'thinking', content: 'Thinking between local tool calls' } makes it through modelMessagesToUIMessages, despite that being the feature under test.

♻️ Proposed addition
 expect(roundTripped[0]?.parts).toEqual(
expect.arrayContaining([
expect.objectContaining({ type: 'tool-call', id: 'tool-call-a' }),
expect.objectContaining({ type: 'tool-call', id: 'tool-call-b' }),
expect.objectContaining({
type: 'tool-result',
toolCallId: 'tool-call-a',
}),
expect.objectContaining({
type: 'tool-result',
toolCallId: 'tool-call-b',
}),
+ expect.objectContaining({+ type: 'thinking',+ content: 'Thinking between local tool calls',+ }),
]),
)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/ai/tests/message-converters.test.ts` around lines 387 - 404,
Strengthen the round-trip assertion in the modelMessagesToUIMessages test to
also verify the thinking part is preserved, asserting a part with type
"thinking" and content "Thinking between local tool calls" alongside the
existing tool-call and tool-result checks.
testing/e2e/src/routes/api.anthropic-bug-test.ts (1)

32-133: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicated stream-to-response boilerplate.

The chunks/try-catch/Response pattern at Lines 110-131 duplicates the existing pattern at Lines 143-174 almost verbatim. Given this route already hosts two bug-repro branches (#604, #910) and is likely to grow more, extracting a small shared helper (e.g. streamChatToJsonResponse(options)) would avoid drift between the branches.

♻️ Suggested helper
asyncfunctionstreamChatToJsonResponse(options: Parameters<typeofchat>[0],): Promise<Response>{constchunks: Array<unknown>=[]try{forawait(constchunkofchat(options)){chunks.push(chunk)}}catch(error){returnnewResponse(JSON.stringify({
chunks,error: errorinstanceofError ? error.message : String(error),}),{status: 200,headers: {'Content-Type': 'application/json'}},)}returnnewResponse(JSON.stringify({ chunks,error: null}),{status: 200,headers: {'Content-Type': 'application/json'},})}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@testing/e2e/src/routes/api.anthropic-bug-test.ts` around lines 32 - 133,
Extract the duplicated chunks collection, chat streaming, error handling, and
JSON Response construction from the thinking-order branch and the other
bug-repro branch into a shared streamChatToJsonResponse helper. Update both
branches to pass their existing chat options through this helper, preserving the
current response status, headers, chunk payload, and error serialization.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@packages/ai/src/activities/chat/messages.ts`:
- Around line 240-255: The flushSegment function currently drops pending
thinking when a segment has no content or tool calls. Include
pendingThinking.length > 0 in its emission guard so thinking-only segments are
appended and cleared correctly, and add a test covering a terminal thinking
block during the final flush.
---
Nitpick comments:
In `@packages/ai/tests/message-converters.test.ts`:
- Around line 387-404: Strengthen the round-trip assertion in the
modelMessagesToUIMessages test to also verify the thinking part is preserved,
asserting a part with type "thinking" and content "Thinking between local tool
calls" alongside the existing tool-call and tool-result checks.
In `@testing/e2e/src/routes/api.anthropic-bug-test.ts`:
- Around line 32-133: Extract the duplicated chunks collection, chat streaming,
error handling, and JSON Response construction from the thinking-order branch
and the other bug-repro branch into a shared streamChatToJsonResponse helper.
Update both branches to pass their existing chat options through this helper,
preserving the current response status, headers, chunk payload, and error
serialization.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d6e02e8c-73ff-4696-9e9c-dba6c8e6b869

📥 Commits

Reviewing files that changed from the base of the PR and between 5fcaf90 and c81fc67.

📒 Files selected for processing (6)
  • .changeset/calm-thinkers-wait.md
  • packages/ai/src/activities/chat/messages.ts
  • packages/ai/tests/message-converters.test.ts
  • testing/e2e/global-setup.ts
  • testing/e2e/src/routes/api.anthropic-bug-test.ts
  • testing/e2e/tests/anthropic-server-tool.spec.ts

@nx-cloud

nx-cloudBot commented Aug 10, 2026

Copy link
Copy Markdown

View your CI Pipeline Execution ↗ for commit 9a6b74e

CommandStatusDurationResult
nx run-many --targets=build --exclude=examples/...✅ Succeeded1m 46sView ↗

☁️ Nx Cloud last updated this comment at 2026-08-20 11:15:11 UTC

@pkg-pr-new

pkg-pr-newBot commented Aug 10, 2026

Copy link
Copy Markdown

Open in StackBlitz

@tanstack/ai

npm i https://pkg.pr.new/@tanstack/ai@931

@tanstack/ai-acp

npm i https://pkg.pr.new/@tanstack/ai-acp@931

@tanstack/ai-angular

npm i https://pkg.pr.new/@tanstack/ai-angular@931

@tanstack/ai-anthropic

npm i https://pkg.pr.new/@tanstack/ai-anthropic@931

@tanstack/ai-bedrock

npm i https://pkg.pr.new/@tanstack/ai-bedrock@931

@tanstack/ai-byteplus

npm i https://pkg.pr.new/@tanstack/ai-byteplus@931

@tanstack/ai-claude-code

npm i https://pkg.pr.new/@tanstack/ai-claude-code@931

@tanstack/ai-client

npm i https://pkg.pr.new/@tanstack/ai-client@931

@tanstack/ai-code-mode

npm i https://pkg.pr.new/@tanstack/ai-code-mode@931

@tanstack/ai-code-mode-snippets

npm i https://pkg.pr.new/@tanstack/ai-code-mode-snippets@931

@tanstack/ai-codex

npm i https://pkg.pr.new/@tanstack/ai-codex@931

@tanstack/ai-cohere

npm i https://pkg.pr.new/@tanstack/ai-cohere@931

@tanstack/ai-devtools-core

npm i https://pkg.pr.new/@tanstack/ai-devtools-core@931

@tanstack/ai-durable-stream

npm i https://pkg.pr.new/@tanstack/ai-durable-stream@931

@tanstack/ai-elevenlabs

npm i https://pkg.pr.new/@tanstack/ai-elevenlabs@931

@tanstack/ai-event-client

npm i https://pkg.pr.new/@tanstack/ai-event-client@931

@tanstack/ai-fal

npm i https://pkg.pr.new/@tanstack/ai-fal@931

@tanstack/ai-gemini

npm i https://pkg.pr.new/@tanstack/ai-gemini@931

@tanstack/ai-grok

npm i https://pkg.pr.new/@tanstack/ai-grok@931

@tanstack/ai-grok-build

npm i https://pkg.pr.new/@tanstack/ai-grok-build@931

@tanstack/ai-groq

npm i https://pkg.pr.new/@tanstack/ai-groq@931

@tanstack/ai-isolate-cloudflare

npm i https://pkg.pr.new/@tanstack/ai-isolate-cloudflare@931

@tanstack/ai-isolate-daytona

npm i https://pkg.pr.new/@tanstack/ai-isolate-daytona@931

@tanstack/ai-isolate-node

npm i https://pkg.pr.new/@tanstack/ai-isolate-node@931

@tanstack/ai-isolate-quickjs

npm i https://pkg.pr.new/@tanstack/ai-isolate-quickjs@931

@tanstack/ai-isolate-quickjs-bun

npm i https://pkg.pr.new/@tanstack/ai-isolate-quickjs-bun@931

@tanstack/ai-mcp

npm i https://pkg.pr.new/@tanstack/ai-mcp@931

@tanstack/ai-memory

npm i https://pkg.pr.new/@tanstack/ai-memory@931

@tanstack/ai-mistral

npm i https://pkg.pr.new/@tanstack/ai-mistral@931

@tanstack/ai-ollama

npm i https://pkg.pr.new/@tanstack/ai-ollama@931

@tanstack/ai-openai

npm i https://pkg.pr.new/@tanstack/ai-openai@931

@tanstack/ai-opencode

npm i https://pkg.pr.new/@tanstack/ai-opencode@931

@tanstack/ai-openrouter

npm i https://pkg.pr.new/@tanstack/ai-openrouter@931

@tanstack/ai-perplexity

npm i https://pkg.pr.new/@tanstack/ai-perplexity@931

@tanstack/ai-persistence

npm i https://pkg.pr.new/@tanstack/ai-persistence@931

@tanstack/ai-preact

npm i https://pkg.pr.new/@tanstack/ai-preact@931

@tanstack/ai-react

npm i https://pkg.pr.new/@tanstack/ai-react@931

@tanstack/ai-react-ui

npm i https://pkg.pr.new/@tanstack/ai-react-ui@931

@tanstack/ai-sandbox

npm i https://pkg.pr.new/@tanstack/ai-sandbox@931

@tanstack/ai-sandbox-cloudflare

npm i https://pkg.pr.new/@tanstack/ai-sandbox-cloudflare@931

@tanstack/ai-sandbox-daytona

npm i https://pkg.pr.new/@tanstack/ai-sandbox-daytona@931

@tanstack/ai-sandbox-docker

npm i https://pkg.pr.new/@tanstack/ai-sandbox-docker@931

@tanstack/ai-sandbox-local-process

npm i https://pkg.pr.new/@tanstack/ai-sandbox-local-process@931

@tanstack/ai-sandbox-sprites

npm i https://pkg.pr.new/@tanstack/ai-sandbox-sprites@931

@tanstack/ai-sandbox-vercel

npm i https://pkg.pr.new/@tanstack/ai-sandbox-vercel@931

@tanstack/ai-solid

npm i https://pkg.pr.new/@tanstack/ai-solid@931

@tanstack/ai-solid-ui

npm i https://pkg.pr.new/@tanstack/ai-solid-ui@931

@tanstack/ai-svelte

npm i https://pkg.pr.new/@tanstack/ai-svelte@931

@tanstack/ai-utils

npm i https://pkg.pr.new/@tanstack/ai-utils@931

@tanstack/ai-vercel-gateway

npm i https://pkg.pr.new/@tanstack/ai-vercel-gateway@931

@tanstack/ai-vue

npm i https://pkg.pr.new/@tanstack/ai-vue@931

@tanstack/ai-vue-ui

npm i https://pkg.pr.new/@tanstack/ai-vue-ui@931

@tanstack/openai-base

npm i https://pkg.pr.new/@tanstack/openai-base@931

@tanstack/preact-ai-devtools

npm i https://pkg.pr.new/@tanstack/preact-ai-devtools@931

@tanstack/react-ai-devtools

npm i https://pkg.pr.new/@tanstack/react-ai-devtools@931

@tanstack/solid-ai-devtools

npm i https://pkg.pr.new/@tanstack/solid-ai-devtools@931

commit: 5091b5b

@github-actionsgithub-actionsBot added waiting-on: author Waiting for the author to respond or update merge-conflicts Conflicts with the base branch — needs a rebase and removed merge-conflicts Conflicts with the base branch — needs a rebase labels Aug 13, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Thanks for the PR, @ekkoitac! 🙌 @AlemTuzlak will take a look.

Automated pre-review checks

  • ⚠️ CI failing — worth a look before review
  • ⚠️ Merge conflicts with main — please rebase
  • ✅ Changeset present
  • ✅ E2E test changes included

Automated triage — a human review follows.

@github-actionsgithub-actionsBot added merge-conflicts Conflicts with the base branch — needs a rebase and removed merge-conflicts Conflicts with the base branch — needs a rebase labels Aug 20, 2026
@tombeckenham
tombeckenhamforce-pushed the fix/issue-910-thinking-order branch from c81fc67 to 5091b5bCompareAugust 20, 2026 10:31
@tombeckenham

Copy link
Copy Markdown
Contributor

Maintainer sweep: rebased onto main (force-with-lease). CI approval handled if it was waiting.

@tombeckenham
tombeckenhamforce-pushed the fix/issue-910-thinking-order branch from 5091b5b to 9a6b74eCompareAugust 20, 2026 10:44
@tombeckenham

Copy link
Copy Markdown
Contributor

Maintainer sweep: rebased onto main (force-with-lease). CI approval handled if it was waiting.

@github-actionsgithub-actionsBot removed the merge-conflicts Conflicts with the base branch — needs a rebase label Aug 20, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

waiting-on: authorWaiting for the author to respond or update

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Anthropic signed thinking blocks are reordered when a UIMessage interleaves provider-executed tools and thinking

3 participants

@ekkoitac@tombeckenham@AlemTuzlak