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

@ekkoitac ekkoitac 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

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 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 name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main fix around signed thinking order with provider tools.
Description check ✅ Passed The description follows the template and includes the change summary, checklist, validation, and release impact.
Linked Issues check ✅ Passed The code and tests address #910 by splitting assistant segments to preserve signed thinking order around provider-executed tools.
Out of Scope Changes check ✅ Passed The changes are focused on the bug fix, related tests, e2e validation, and release note, with no obvious unrelated additions.
Docstring Coverage ✅ Passed No 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

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

testing/e2e/global-setup.ts

Parsing error: "parserOptions.project" has been provided for @typescript-eslint/parser.
The file was not found in any of the provided project(s): testing/e2e/global-setup.ts

testing/e2e/tests/anthropic-server-tool.spec.ts

Parsing error: "parserOptions.project" has been provided for @typescript-eslint/parser.
The file was not found in any of the provided project(s): testing/e2e/tests/anthropic-server-tool.spec.ts


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.

@coderabbitai coderabbitai Bot 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
async function streamChatToJsonResponse(
  options: Parameters<typeof chat>[0],
): Promise<Response> {
  const chunks: Array<unknown> = []
  try {
    for await (const chunk of chat(options)) {
      chunks.push(chunk)
    }
  } catch (error) {
    return new Response(
      JSON.stringify({
        chunks,
        error: error instanceof Error ? error.message : String(error),
      }),
      { status: 200, headers: { 'Content-Type': 'application/json' } },
    )
  }
  return new Response(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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

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

1 participant