Skip to content

fix(ai): stop text events from corrupting tool-call input - #1020

Merged
AlemTuzlak merged 2 commits into
TanStack:mainfrom
season179:fix/interleaved-text-tool-args-input
Aug 21, 2026
Merged

fix(ai): stop text events from corrupting tool-call input#1020
AlemTuzlak merged 2 commits into
TanStack:mainfrom
season179:fix/interleaved-text-tool-args-input

Conversation

@season179

@season179 season179 commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

A TEXT_MESSAGE_CONTENT event between two TOOL_CALL_ARGS deltas permanently corrupted tool-call input. The processor guessed that text meant args were done, filled input from a lenient partial-JSON parse of the truncated string, then skipped the later TOOL_CALL_END. arguments held the full JSON. input held a truncated fake object. state said input-complete.

This PR stops that guess. Text events no longer complete tool calls. completeToolCall sets input only when JSON.parse of the accumulated arguments succeeds. Incomplete JSON leaves input unset. The raw arguments string is the fallback.

Changes

The earlier inferredComplete state machine is gone. Completing on text was the bug. TOOL_CALL_END and RUN_FINISHED already cover completion.

Checklist

  • I have followed the steps in the Contributing guide.
  • I have tested this code locally with pnpm run test:pr.
  • Docs: I updated packages/ai/docs/chat-architecture.md for this change. No public docs/ page covers this processor contract.
  • Changeset: I added a changeset (pnpm changeset), or this PR does not change a published package.

Release Impact

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

Testing

Commands run:

  1. pnpm exec vitest run in packages/ai: 1589 passed.
  2. pnpm exec oxlint src --type-aware in packages/ai: 0 errors.
  3. pnpm exec tsc --noEmit in packages/ai: passed.
  4. E2E interleaved-args.spec.ts did not run locally. This worktree has no built workspace packages for the Playwright app. CI will run it.

Manual test:

  1. Stream TOOL_CALL_ARGS with truncated JSON, then one TEXT_MESSAGE_CONTENT, then the rest of the args, then TOOL_CALL_END.
  2. Confirm part.input equals the full object, not a truncated parse.
  3. Confirm a stream that ends with truncated args leaves input unset.

How this PR makes testing easy: three unit tests in stream-processor.test.ts plus testing/e2e/tests/tools-test/interleaved-args.spec.ts.

Linked issues

Fixes #1017

Risk / rollback

Low. Tool completion now waits for TOOL_CALL_END or RUN_FINISHED, which the adapter contract already requires. Revert the PR if a client depended on input-complete at the first text delta.

Public API change

Callers still read ToolCallPart.input. The value is now the strict parse of complete arguments, or unset. No new exports.

@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: b68ba6d8-3fd4-4865-b68c-e6f2a054dfac

📥 Commits

Reviewing files that changed from the base of the PR and between efe3b07 and 8375442.

📒 Files selected for processing (8)
  • .changeset/fix-interleaved-text-tool-call-input.md
  • packages/ai/docs/chat-architecture.md
  • packages/ai/src/activities/chat/stream/processor.ts
  • packages/ai/src/activities/chat/stream/types.ts
  • packages/ai/tests/stream-processor.test.ts
  • testing/e2e/src/lib/tools-test-tools.ts
  • testing/e2e/src/routes/api.tools-test.ts
  • testing/e2e/tests/tools-test/interleaved-args.spec.ts
🚧 Files skipped from review as they are similar to previous changes (8)
  • .changeset/fix-interleaved-text-tool-call-input.md
  • packages/ai/src/activities/chat/stream/types.ts
  • testing/e2e/src/lib/tools-test-tools.ts
  • testing/e2e/tests/tools-test/interleaved-args.spec.ts
  • packages/ai/docs/chat-architecture.md
  • packages/ai/src/activities/chat/stream/processor.ts
  • testing/e2e/src/routes/api.tools-test.ts
  • packages/ai/tests/stream-processor.test.ts

📝 Walkthrough

Walkthrough

This change repairs tool-call input when text events interleave with argument chunks. Inferred completions can reopen for later arguments, authoritative completion re-parses the full input, and partial JSON is not exposed as parsed input.

Changes

Tool-call completion repair

Layer / File(s) Summary
Processor state and parsing flow
packages/ai/src/activities/chat/stream/types.ts, packages/ai/src/activities/chat/stream/processor.ts
Tracks inferred completion, protects terminal states, reopens calls for later arguments, re-completes calls on terminal events, and sets input only after strict JSON parsing succeeds.
Regression validation
packages/ai/tests/stream-processor.test.ts
Tests interleaved arguments, incomplete JSON, finalization recovery, errored states, approval states, and authoritative completion.
Integration scenario and documented contract
testing/e2e/src/..., packages/ai/docs/chat-architecture.md, .changeset/fix-interleaved-text-tool-call-input.md
Adds the e2e scenario and adapter, validates the completed tool call, and documents the updated completion and parsing behavior.

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

Mergeability Score: ⚪ Minimal · up to 83754

The change prevents interleaved text events from corrupting tool-call input and avoids exposing fabricated partial JSON; no actionable merge-blocking risk remains beyond normal checks and review.

Sequence Diagram(s)

sequenceDiagram
  participant TextStream
  participant StreamProcessor
  participant InternalToolCallState
  participant ToolCallPart
  TextStream->>StreamProcessor: TEXT_MESSAGE_CONTENT
  StreamProcessor->>InternalToolCallState: mark inferredComplete
  TextStream->>StreamProcessor: TOOL_CALL_ARGS
  StreamProcessor->>InternalToolCallState: clear inferredComplete
  TextStream->>StreamProcessor: TOOL_CALL_END
  StreamProcessor->>InternalToolCallState: complete with accumulated arguments
  StreamProcessor->>ToolCallPart: set input after strict JSON.parse
Loading

Possibly related PRs

  • TanStack/ai#1019: Extends the related StreamProcessor fix with inferred completion, strict input parsing, and regression coverage.

Suggested reviewers: alemtuzlak

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The implementation addresses issue #1017 by repairing inferred completions, enforcing strict parsing, preserving raw arguments, and adding regression coverage.
Out of Scope Changes check ✅ Passed The code, documentation, changeset, unit tests, and end-to-end tests directly support the linked issue and stated objectives.
Description check ✅ Passed The description includes the required Changes, Checklist, and Release Impact sections, plus detailed testing, issue, risk, and API information.
Title check ✅ Passed The title clearly and concisely describes the primary fix for text events corrupting tool-call input.
✨ 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.

@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 (2)
packages/ai/src/activities/chat/stream/processor.ts (2)

2089-2153: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

inferredComplete is stamped before the errored/awaiting-user-action guards, enabling a downstream UI regression.

Line 2099 sets toolCall.inferredComplete = opts?.inferred === true unconditionally, before the isToolCallPartErrored (2119) / isToolCallPartAwaitingUserAction (2126) early returns that intentionally prevent the rendered part from being downgraded. Those guards protect the message part write, but the internal inferredComplete flag still gets set to true for a call whose rendered part is terminally 'error' or awaiting approval. handleToolCallArgsEvent's new revert logic then trusts that flag on a later stray TOOL_CALL_ARGS and overwrites the rendered part back to input-streaming (see comment on that segment), undoing the terminal state these guards were meant to protect.

Consider only marking inferredComplete = true when the call isn't already in a guarded terminal UI state (or gate the revert on the consumer side as suggested there).

🤖 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/stream/processor.ts` around lines 2089 -
2153, Update completeToolCall so inferredComplete is not set to true when the
tool call’s rendered part is already errored or awaiting user action. Move or
condition the assignment using isToolCallPartErrored and
isToolCallPartAwaitingUserAction before the existing guarded early returns,
while preserving normal inferred completion behavior for unguarded calls.

1316-1368: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Reverting inferredComplete unconditionally can clobber a terminal error/approval state.

This block trusts existingToolCall.inferredComplete and always reverts to input-streaming, then the trailing updateToolCallPart call (lines 1353-1358) writes that state onto the rendered part with no check against isToolCallPartErrored/isToolCallPartAwaitingUserAction. If the call reached a terminal 'error' or approval state while inferredComplete was still true internally (see the companion comment on completeToolCall), a stray TOOL_CALL_ARGS here would silently downgrade the rendered part back to input-streaming, hiding the error/approval state from the UI.

🛡️ Proposed guard
     if (existingToolCall.inferredComplete) {
-      existingToolCall.state = 'input-streaming'
-      existingToolCall.inferredComplete = false
+      existingToolCall.inferredComplete = false
+      if (
+        !this.isToolCallPartErrored(existingToolCall.id) &&
+        !this.isToolCallPartAwaitingUserAction(existingToolCall.id)
+      ) {
+        existingToolCall.state = 'input-streaming'
+      }
     }
🤖 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/stream/processor.ts` around lines 1316 -
1368, Guard the inferredComplete recovery in handleToolCallArgsEvent so it does
not overwrite terminal error or user-approval states. Before resetting
existingToolCall.state to input-streaming, check the existing tool-call state or
the rendered part with isToolCallPartErrored/isToolCallPartAwaitingUserAction;
preserve terminal states and prevent updateToolCallPart from downgrading them.
🤖 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/stream/processor.ts`:
- Around line 2089-2153: Update completeToolCall so inferredComplete is not set
to true when the tool call’s rendered part is already errored or awaiting user
action. Move or condition the assignment using isToolCallPartErrored and
isToolCallPartAwaitingUserAction before the existing guarded early returns,
while preserving normal inferred completion behavior for unguarded calls.
- Around line 1316-1368: Guard the inferredComplete recovery in
handleToolCallArgsEvent so it does not overwrite terminal error or user-approval
states. Before resetting existingToolCall.state to input-streaming, check the
existing tool-call state or the rendered part with
isToolCallPartErrored/isToolCallPartAwaitingUserAction; preserve terminal states
and prevent updateToolCallPart from downgrading them.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: d9ec1b1b-ecb9-4cc8-b854-3fc85b888a5f

📥 Commits

Reviewing files that changed from the base of the PR and between 05280a5 and 84bd8a7.

📒 Files selected for processing (8)
  • .changeset/fix-interleaved-text-tool-call-input.md
  • packages/ai/docs/chat-architecture.md
  • packages/ai/src/activities/chat/stream/processor.ts
  • packages/ai/src/activities/chat/stream/types.ts
  • packages/ai/tests/stream-processor.test.ts
  • testing/e2e/src/lib/tools-test-tools.ts
  • testing/e2e/src/routes/api.tools-test.ts
  • testing/e2e/tests/tools-test/interleaved-args.spec.ts

@season179

Copy link
Copy Markdown
Contributor Author

@coderabbitai Both findings addressed in 7fb0058:

  1. completeToolCall now clears inferredComplete when the errored/awaiting-user-action guards fire, so a call whose rendered part is in a terminal state never carries the flag.
  2. The revert path in handleToolCallArgsEvent clears the flag first and only resets the state to input-streaming when the rendered part is not errored/awaiting user action.

Added a regression test covering the out-of-order TOOL_CALL_RESULT(output-error) → stray TOOL_CALL_ARGS sequence. Note the pre-existing behavior where a stray TOOL_CALL_ARGS part-update can overwrite a terminal part state with the internal state is unchanged by this PR — it predates it and applies to orderings unrelated to #1017.

@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

git show --stat --oneline --decorate --no-renames 7fb0058
git show --format=fuller --no-ext-diff --unified=80 7fb0058 -- \
  packages/ai/src/activities/chat/stream/processor.ts \
  packages/ai/tests/stream-processor.test.ts

ast-grep outline packages/ai/src/activities/chat/stream/processor.ts --items all --match 'completeToolCall|handleToolCallArgsEvent'
ast-grep outline packages/ai/tests/stream-processor.test.ts --items all --match 'out.of.order|output-error|stray'

Length of output: 24352


@season179 Verified 7fb0058: both terminal-state safeguards are in place, and the new out-of-order error-result/stray-args regression test covers the reopen path. The pre-existing broader terminal-part overwrite behavior is distinct from this PR’s inferred-completion repair logic. Thanks for the clear follow-up.


✏️ Learnings added
Learnt from: season179
URL: https://github.com/TanStack/ai/pull/1020

Timestamp: 2026-07-30T04:37:47.566Z
Learning: In `packages/ai/src/activities/chat/stream/processor.ts`, a protocol-violating stray `TOOL_CALL_ARGS` can still update a rendered terminal tool-call part using the internal state. This behavior predates the `#1017` interleaved-text fix and is outside that PR’s inferred-completion repair scope.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


If you found this review helpful, would you consider giving us a shout-out on X?

Thank you for using CodeRabbit!

You are interacting with an AI system.

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

Actionable comments posted: 1

🤖 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.

Inline comments:
In `@packages/ai/src/activities/chat/stream/processor.ts`:
- Around line 1332-1342: Update the existingToolCall handling in processor.ts to
preserve the rendered terminal error or approval state during later argument
updates; do not reset it to input-streaming or allow the internal input-complete
state to overwrite it. In stream-processor.test.ts, strengthen the regression
assertion to require the exact error state, ensuring the test detects accidental
rewriting to input-complete.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 44877f42-64cc-4cda-8b9d-3be0bd237bc8

📥 Commits

Reviewing files that changed from the base of the PR and between 84bd8a7 and 7fb0058.

📒 Files selected for processing (2)
  • packages/ai/src/activities/chat/stream/processor.ts
  • packages/ai/tests/stream-processor.test.ts

Comment thread packages/ai/src/activities/chat/stream/processor.ts Outdated

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

🧹 Nitpick comments (1)
packages/ai/tests/stream-processor.test.ts (1)

879-900: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Assert that approval input is preserved too.

The regression only checks state; it would still pass if TOOL_CALL_END.input overwrote the approval part’s input while leaving approval-requested intact.

Suggested assertion
         expect(toolCallPart(processor)?.state).toBe('approval-requested')
+        expect(toolCallPart(processor)?.input).toEqual(FULL_INPUT)
🤖 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/stream-processor.test.ts` around lines 879 - 900, Extend
the test for TOOL_CALL_END handling around toolCallPart(processor) to also
assert that the approval-requested part retains its original input, FULL_INPUT,
after processing the tool end event. Keep the existing state assertion and
verify input preservation separately.
🤖 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.

Nitpick comments:
In `@packages/ai/tests/stream-processor.test.ts`:
- Around line 879-900: Extend the test for TOOL_CALL_END handling around
toolCallPart(processor) to also assert that the approval-requested part retains
its original input, FULL_INPUT, after processing the tool end event. Keep the
existing state assertion and verify input preservation separately.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: b6939925-a8bc-4e63-b941-bd8f981d5cfb

📥 Commits

Reviewing files that changed from the base of the PR and between 7fb0058 and ff06a18.

📒 Files selected for processing (2)
  • packages/ai/src/activities/chat/stream/processor.ts
  • packages/ai/tests/stream-processor.test.ts

@season179
season179 force-pushed the fix/interleaved-text-tool-args-input branch from 08c4861 to 472af70 Compare August 8, 2026 07:21
@coderabbitai

coderabbitai Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

@tombeckenham
tombeckenham force-pushed the fix/interleaved-text-tool-args-input branch from 472af70 to c77d222 Compare August 10, 2026 09:06
@nx-cloud

nx-cloud Bot commented Aug 10, 2026

Copy link
Copy Markdown

View your CI Pipeline Execution ↗ for commit 77ee8d3

Command Status Duration Result
nx affected --targets=test:sherif,test:knip,tes... ✅ Succeeded 9m 28s View ↗
nx run-many --targets=build --exclude=examples/... ✅ Succeeded 1m 54s View ↗

☁️ Nx Cloud last updated this comment at 2026-08-21 11:31:03 UTC

@nx-cloud

nx-cloud Bot commented Aug 10, 2026

Copy link
Copy Markdown

View your CI Pipeline Execution ↗ for commit c77d222

Command Status Duration Result
nx run-many --targets=build --exclude=examples/... ✅ Succeeded 1m 50s View ↗

☁️ Nx Cloud last updated this comment at 2026-08-10 09:09:36 UTC

@pkg-pr-new

pkg-pr-new Bot commented Aug 10, 2026

Copy link
Copy Markdown

Open in StackBlitz

@tanstack/ai

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

@tanstack/ai-acp

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

@tanstack/ai-angular

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

@tanstack/ai-anthropic

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

@tanstack/ai-bedrock

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

@tanstack/ai-byteplus

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

@tanstack/ai-claude-code

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

@tanstack/ai-client

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

@tanstack/ai-code-mode

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

@tanstack/ai-code-mode-snippets

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

@tanstack/ai-codex

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

@tanstack/ai-cohere

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

@tanstack/ai-devtools-core

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

@tanstack/ai-durable-stream

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

@tanstack/ai-elevenlabs

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

@tanstack/ai-event-client

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

@tanstack/ai-fal

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

@tanstack/ai-gemini

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

@tanstack/ai-grok

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

@tanstack/ai-grok-build

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

@tanstack/ai-groq

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

@tanstack/ai-isolate-cloudflare

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

@tanstack/ai-isolate-daytona

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

@tanstack/ai-isolate-node

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

@tanstack/ai-isolate-quickjs

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

@tanstack/ai-isolate-quickjs-bun

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

@tanstack/ai-llmgateway

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-llmgateway@1020

@tanstack/ai-mcp

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

@tanstack/ai-memory

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

@tanstack/ai-mistral

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

@tanstack/ai-octane

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-octane@1020

@tanstack/ai-ollama

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

@tanstack/ai-openai

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

@tanstack/ai-opencode

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

@tanstack/ai-openrouter

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

@tanstack/ai-perplexity

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

@tanstack/ai-persistence

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

@tanstack/ai-preact

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

@tanstack/ai-react

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

@tanstack/ai-react-ui

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

@tanstack/ai-sandbox

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

@tanstack/ai-sandbox-cloudflare

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

@tanstack/ai-sandbox-daytona

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

@tanstack/ai-sandbox-docker

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

@tanstack/ai-sandbox-local-process

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

@tanstack/ai-sandbox-sprites

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

@tanstack/ai-sandbox-vercel

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

@tanstack/ai-solid

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

@tanstack/ai-solid-ui

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

@tanstack/ai-svelte

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

@tanstack/ai-utils

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

@tanstack/ai-vercel-gateway

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

@tanstack/ai-vertex

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-vertex@1020

@tanstack/ai-vue

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

@tanstack/ai-vue-ui

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

@tanstack/openai-base

npm i https://pkg.pr.new/TanStack/ai/@tanstack/openai-base@1020

@tanstack/preact-ai-devtools

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

@tanstack/react-ai-devtools

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

@tanstack/solid-ai-devtools

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

commit: 77ee8d3

@github-actions

Copy link
Copy Markdown
Contributor

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

Automated pre-review checks

  • ✅ CI passing
  • ✅ No merge conflicts
  • ✅ Changeset present
  • ✅ E2E test changes included

Automated triage — a human review follows.

@github-actions github-actions Bot added the waiting-on: author Waiting for the author to respond or update label Aug 13, 2026
@coderabbitai

coderabbitai Bot commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

@github-actions github-actions Bot added waiting-on: maintainer The ball is in the maintainers’ court and removed waiting-on: author Waiting for the author to respond or update labels Aug 13, 2026
@season179
season179 force-pushed the fix/interleaved-text-tool-args-input branch from f7f3ce3 to 77e3606 Compare August 18, 2026 02:00
@github-actions github-actions Bot added merge-conflicts Conflicts with the base branch — needs a rebase waiting-on: author Waiting for the author to respond or update waiting-on: maintainer The ball is in the maintainers’ court and removed waiting-on: maintainer The ball is in the maintainers’ court waiting-on: author Waiting for the author to respond or update merge-conflicts Conflicts with the base branch — needs a rebase labels Aug 18, 2026
@season179
season179 force-pushed the fix/interleaved-text-tool-args-input branch from 77e3606 to c2a02d9 Compare August 20, 2026 01:54
@github-actions github-actions Bot added waiting-on: maintainer The ball is in the maintainers’ court and removed waiting-on: author Waiting for the author to respond or update merge-conflicts Conflicts with the base branch — needs a rebase labels Aug 20, 2026
@tombeckenham
tombeckenham force-pushed the fix/interleaved-text-tool-args-input branch 2 times, most recently from f321456 to 3f05389 Compare August 20, 2026 10:40
@github-actions github-actions Bot added waiting-on: author Waiting for the author to respond or update waiting-on: maintainer The ball is in the maintainers’ court and removed waiting-on: maintainer The ball is in the maintainers’ court waiting-on: author Waiting for the author to respond or update labels Aug 20, 2026

@AlemTuzlak AlemTuzlak 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.

The bug is real. I ran the issue #1017 sequence on current main. After one TEXT_MESSAGE_CONTENT between two TOOL_CALL_ARGS deltas, arguments holds the full JSON and input is { templateIds: ["mock-gsk-e"] }. That is silent data corruption. Consumers that read part.input cannot detect it from state.

This PR does not earn its keep in this shape.

Root cause

handleTextMessageContentEvent and handleTextMessageEndEvent call completeAllToolCallsForMessage(). That guess says: text arrived, so tool args are done. completeToolCall then writes input from the lenient partial-JSON parser, which closes unterminated strings. handleToolCallEndEvent no-ops when state is already input-complete, so the later authoritative END cannot repair it.

The adapter contract already names TOOL_CALL_END as the complete signal. RUN_FINISHED / finalizeStream already force-complete as the safety net. The text heuristic is extra, and it is what creates the bug.

Why this PR is too large

This change keeps that heuristic, then adds a machine to undo it:

  • inferredComplete on internal tool-call state
  • reopen on later TOOL_CALL_ARGS
  • re-complete on TOOL_CALL_END
  • settled-part guards so reopen does not smash error / approval states
  • a residual edge the PR itself leaves open (TOOL_CALL_RESULT before TOOL_CALL_END while inferred-complete)

The new tests also lock the heuristic in. They assert state === 'input-complete' right after the interleaved text. That makes the guess part of the public contract.

Smaller fix that covers the bug

  1. Stop calling completeAllToolCallsForMessage from text events (TEXT_MESSAGE_CONTENT and TEXT_MESSAGE_END).
  2. In completeToolCall, set input only when JSON.parse of the accumulated arguments succeeds. If parse fails, leave input unset. The raw arguments string stays the documented fallback.

Then TOOL_CALL_END still runs, because the call is not already input-complete. An aborted stream with truncated args does not publish a fake object.

That is about 10–15 lines. No flag. No reopen. No extra settled-part path.

Other open PRs for #1017

  • #1019: 8-line reopen of any input-complete call when more args arrive. Fixes the reported sequence. Still publishes truncated input if the stream dies. Can reopen after a real TOOL_CALL_END.
  • #1183: strict parse only. Stops the fake object at force-complete time, but TOOL_CALL_END still no-ops, so final input stays unset even after the full JSON arrived. That does not match the issue's primary ask.

Tests

I added two unit tests for the issue sequence on current main. Both fail today with the exact truncated input. They assert the user-visible contract (input equals the full object, or stays unset). They do not require text to complete tool calls.

If we land the smaller fix, keep a regression like that, plus the e2e interleaved-args scenario from this PR. Drop tests that require inferred completion.

@AlemTuzlak AlemTuzlak changed the title fix(ai): repair tool-call input corrupted by interleaved text deltas fix(ai): stop interleaved text from corrupting tool-call input Aug 21, 2026
@AlemTuzlak

Copy link
Copy Markdown
Contributor

Addressed the requested-changes review by shrinking the fix.

The inferredComplete reopen machine is gone. Text events no longer call completeAllToolCallsForMessage. completeToolCall sets input only after JSON.parse succeeds.

Proof: the three #1017 unit tests fail on the old processor with input.templateIds = ["mock-gsk-e"]. They pass after this change. Full stream-processor.test.ts: 180 passed.

A TEXT_MESSAGE_CONTENT delta between TOOL_CALL_ARGS deltas
force-completed the call from a lenient partial-JSON parse.
TOOL_CALL_END then no-oped, so input stayed truncated while
arguments held the full JSON.

Text events no longer complete tool calls. completeToolCall
sets input only when JSON.parse of the arguments succeeds.

Fixes TanStack#1017
@AlemTuzlak
AlemTuzlak force-pushed the fix/interleaved-text-tool-args-input branch from 5b1dfe9 to 8775570 Compare August 21, 2026 10:49
@AlemTuzlak AlemTuzlak changed the title fix(ai): stop interleaved text from corrupting tool-call input fix(ai): stop text events from corrupting tool-call input Aug 21, 2026
@AlemTuzlak
AlemTuzlak dismissed their stale review August 21, 2026 10:50

Replaced the inferredComplete machine with the smaller fix: text events no longer complete tool calls, and input is set only after JSON.parse succeeds.

@AlemTuzlak

Copy link
Copy Markdown
Contributor

Reshaped this PR to the smaller fix from review.

The bug is real. The inferredComplete flag, reopen path, and settled-part guards are gone. Completing tool calls on text was the cause. TOOL_CALL_END and RUN_FINISHED already complete them.

What landed:

  1. Text events no longer call completeAllToolCallsForMessage.
  2. completeToolCall sets input only when JSON.parse of the arguments succeeds.
  3. Three unit tests for issue Interleaved TEXT_MESSAGE_CONTENT permanently corrupts tool-call input with a partial-JSON parse #1017, plus the interleaved-args e2e scenario.

@tanstack/ai unit tests: 1589 passed.

Add coverage for END.input after interleaved text, two parallel
calls, and a full TEXT_START/CONTENT/END block between arg deltas.
Retarget toolCallToMessage when TEXT_MESSAGE_START remaps the
assistant message id, so the later ARGS deltas still accumulate.
@AlemTuzlak

Copy link
Copy Markdown
Contributor

Closed the two overlapping PRs as duplicates of this one:

#1020 is the one that fixes #1017: text no longer completes tool calls, and input is set only after JSON.parse succeeds.

@AlemTuzlak
AlemTuzlak enabled auto-merge (squash) August 21, 2026 11:33
@AlemTuzlak
AlemTuzlak merged commit c0ba484 into TanStack:main Aug 21, 2026
9 checks passed
@github-actions github-actions Bot mentioned this pull request Aug 21, 2026
@season179
season179 deleted the fix/interleaved-text-tool-args-input branch August 22, 2026 06:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

waiting-on: maintainer The ball is in the maintainers’ court

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Interleaved TEXT_MESSAGE_CONTENT permanently corrupts tool-call input with a partial-JSON parse

2 participants