Skip to content

feat(ai): add 'error' terminal to ToolCallState (#718)#727

Open
season179 wants to merge 2 commits into
TanStack:mainfrom
season179:fix/718-tool-call-error-terminal
Open

feat(ai): add 'error' terminal to ToolCallState (#718)#727
season179 wants to merge 2 commits into
TanStack:mainfrom
season179:fix/718-tool-call-error-terminal

Conversation

@season179

@season179 season179 commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Summary

Closes #718.

ToolCallState had no error terminal. When a tool execution produced an output error, the tool-call UIMessage part parked at "input-complete" forever — while its sibling tool-result part correctly reached "error". UIs that render lifecycle from part.state (the natural reading of a state machine) therefore couldn't distinguish "still executing" from "failed" without reverse-engineering the error-shaped output or the sibling part, producing an infinite spinner on every tool failure.

This adds an 'error' member to ToolCallState and transitions the tool-call part to it on an output error, making the state machine self-describing and symmetric with ToolResultState:

if (part.type === 'tool-call' && part.state === 'error') {
  // render failure — no more inferring from output shape
}

What changed

  • 'error' added to ToolCallState in the three hand-synced copies: @tanstack/ai, @tanstack/ai-client, @tanstack/ai-event-client.
  • StreamProcessor maps output errors to state: 'error' for the tool-call part in addToolResult, handleToolCallEndEvent, and handleToolCallResultEvent (the tool-result part already used 'error').
  • Completion safety net (RUN_FINISHED / finalizeStream): still finalizes the call's internal bookkeeping (so it remains in getResult().toolCalls / getState()), but no longer downgrades a rendered 'error' part back to 'input-complete' — including the AG-UI ordering where an output-error TOOL_CALL_RESULT arrives before TOOL_CALL_END.
  • Empty-message errors: a failed client tool that throws with an empty message (e.g. throw new Error()) now still reaches 'error' — error-ness derives from the output-error state, not message truthiness.
  • isToolCallIncluded gains an explicit 'error' arm so failed calls stay in conversation history for the LLM.
  • Docs: chat-architecture.md notes the new terminal.

Test plan

  • Unit: @tanstack/ai (stream-processor, message-updaters) and @tanstack/ai-client (chat-client-context), incl. new regression tests for the result-before-END ordering and the empty-message error.
  • E2E: tool-error.spec.ts asserts the failed tool-call part reaches state === 'error' end-to-end; the full tools-test/ suite (40 specs) confirms normal completion, parallel tools, approvals, and continuations are unaffected.
  • pnpm test:pr green across all affected projects (sherif, knip, docs, eslint, lib, types, build).

Notes

Scoped intentionally to the error terminal. The secondary "aborted mid-drain" terminal mentioned in #718 is left for a follow-up. ToolCallState is currently duplicated across three packages (kept in sync by hand); de-duplicating it is out of scope here.

Summary by CodeRabbit

  • New Features

    • Tool calls now expose a terminal "error" state so failed executions are shown distinctly from in-progress or completed runs.
  • Bug Fixes

    • Prevented failed tool calls from being left in an ambiguous "input-complete" state and from being overwritten during finalization.
  • Documentation

    • Clarified tool-call lifecycle and error handling in the chat/streaming docs.

When a tool execution produced an output error, the tool-call UIMessage
part parked at "input-complete" forever, so UIs rendering lifecycle from
part.state could not distinguish "still executing" from "failed" without
reverse-engineering the error-shaped output or the sibling tool-result
part.

Add an 'error' member to ToolCallState (in @tanstack/ai, @tanstack/ai-client,
and @tanstack/ai-event-client) and transition the tool-call part to it on an
output error, making the tool-call state machine self-describing and
symmetric with ToolResultState.

- StreamProcessor maps output errors to state 'error' in addToolResult,
  handleToolCallEndEvent, and handleToolCallResultEvent.
- The completion safety net (RUN_FINISHED / finalizeStream) finalizes the
  call's internal state but no longer downgrades a rendered 'error' part
  back to 'input-complete', including when an output-error result arrives
  before TOOL_CALL_END.
- A failed client tool with an empty error message now still reaches
  'error' (error-ness comes from the output-error state, not message
  truthiness).
- isToolCallIncluded keeps errored tool calls in conversation history.

Adds unit coverage in @tanstack/ai and @tanstack/ai-client plus an E2E
assertion in tool-error.spec.ts.
@coderabbitai

coderabbitai Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 162b34eb-0e65-4297-aae3-4614ec5b5502

📥 Commits

Reviewing files that changed from the base of the PR and between d8e0324 and ef26671.

📒 Files selected for processing (1)
  • packages/ai-client/src/chat-client.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/ai-client/src/chat-client.ts

📝 Walkthrough

Walkthrough

Adds a terminal 'error' to ToolCallState and updates stream processing, client wiring, message inclusion, docs, and tests so tool-call parts render terminal 'error' on output errors and finalization does not overwrite that rendered error.

Changes

Tool Error Terminal State Machine

Layer / File(s) Summary
Type system update: ToolCallState error terminal
packages/ai-client/src/types.ts, packages/ai-event-client/src/index.ts, packages/ai/src/types.ts
Add 'error' string literal to ToolCallState union across type packages.
StreamProcessor error state handling
packages/ai/src/activities/chat/stream/message-updaters.ts, packages/ai/src/activities/chat/stream/processor.ts
Map output-error to rendered state: 'error' in updateToolCallWithOutput, handle TOOL_CALL_END/TOOL_CALL_RESULT accordingly, add isToolCallPartErrored helper, and prevent completeToolCall from downgrading rendered error parts.
ChatClient error result integration
packages/ai-client/src/chat-client.ts
Derive error signaling from result.state === 'output-error', passing a definitive error message (or a default) to processor.addToolResult; non-error states pass undefined.
Message lifecycle and documentation
packages/ai/src/activities/chat/messages.ts, packages/ai/docs/chat-architecture.md
Include 'error' in isToolCallIncluded and clarify TOOL_CALL_END behavior when result indicates output-error, plus safety-net non-downgrade description.
Test coverage for error terminal behavior
packages/ai-client/tests/chat-client-context.test.ts, packages/ai/tests/message-updaters.test.ts, packages/ai/tests/stream-processor.test.ts, testing/e2e/tests/tool-error.spec.ts
Update assertions to expect 'error' state, add regression tests for empty error messages (#718) and TOOL_CALL_RESULT arriving before TOOL_CALL_END, and validate e2e failing-tool path.
Changeset and version documentation
.changeset/tool-call-error-terminal.md
Record minor version bumps and document the functional state-machine change and completion safety-net behavior.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • TanStack/ai#596: Modifies tool-call lifecycle and state handling; related to ToolCallState and updateToolCallWithOutput changes.

Suggested reviewers

  • tombeckenham
  • AlemTuzlak

Poem

🐰 I hopped through the stream and found a bug,
The spinner that lingered, a slow, hollow shrug.
Now 'error' declares what once hid in the gray—
No more guessing the call has been led astray.
Hooray for clear endings, and carrots today!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% 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
Title check ✅ Passed The title 'feat(ai): add 'error' terminal to ToolCallState (#718)' is concise, specific, and clearly summarizes the primary change—adding an error state to the ToolCallState type.
Description check ✅ Passed The description provides a clear summary of changes, motivation, test coverage, and notes on scope; all template sections are adequately addressed with substantive detail.
Linked Issues check ✅ Passed The PR fully addresses issue #718 by adding an 'error' terminal to ToolCallState, mapping output errors to state: 'error' in StreamProcessor, updating finalization logic, and ensuring failed calls remain in conversation history.
Out of Scope Changes check ✅ Passed All changes are tightly scoped to the error terminal objective; the PR explicitly defers the separate 'aborted mid-drain' terminal and type de-duplication to future work, and all code modifications directly support the stated goal.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

Review ran into problems

🔥 Problems

Stopped waiting for pipeline failures after 30000ms. One of your pipelines takes longer than our 30000ms fetch window to run, so review may not consider pipeline-failure results for inline comments if any failures occurred after the fetch window. Increase the timeout if you want to wait longer or run a @coderabbit review after the pipeline has finished.


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 and usage tips.

@season179 season179 marked this pull request as ready for review June 8, 2026 13:32

@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-client/src/chat-client.ts (1)

1221-1231: 💤 Low value

Verify error-signal precedence: state vs. message truthiness.

The code ensures a non-empty error message when result.state === 'output-error' (fixing #718), but when state !== 'output-error', it still passes result.errorText to the processor. Since the processor infers error-ness from message truthiness (error ? 'error' : undefined at processor.ts:320), a caller passing { state: 'output-available', errorText: 'some warning' } would incorrectly trigger error state.

Actual usage appears safe (success paths never set errorText, error paths always set state: 'output-error'), but the API surface could be hardened:

 this.processor.addToolResult(
   result.toolCallId,
   result.output,
   result.state === 'output-error'
     ? result.errorText || 'Tool execution failed'
-    : result.errorText,
+    : undefined,
 )

This would ensure error-ness derives purely from state, not from accidental errorText presence.

🤖 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-client/src/chat-client.ts` around lines 1221 - 1231, The
processor currently infers error-ness from the errorText truthiness when calling
this.processor.addToolResult(result.toolCallId, result.output, ...); change the
call so the third argument is passed only when result.state === 'output-error'
(use result.errorText or a default message) and pass undefined otherwise,
ensuring error signalling derives solely from result.state and not from
accidental result.errorText; update the call site in chat-client.ts (the
this.processor.addToolResult invocation) accordingly.
🤖 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-client/src/chat-client.ts`:
- Around line 1221-1231: The processor currently infers error-ness from the
errorText truthiness when calling
this.processor.addToolResult(result.toolCallId, result.output, ...); change the
call so the third argument is passed only when result.state === 'output-error'
(use result.errorText or a default message) and pass undefined otherwise,
ensuring error signalling derives solely from result.state and not from
accidental result.errorText; update the call site in chat-client.ts (the
this.processor.addToolResult invocation) accordingly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 24023878-b4a3-417e-ac38-b90e658fb799

📥 Commits

Reviewing files that changed from the base of the PR and between 22c9b42 and d8e0324.

📒 Files selected for processing (13)
  • .changeset/tool-call-error-terminal.md
  • packages/ai-client/src/chat-client.ts
  • packages/ai-client/src/types.ts
  • packages/ai-client/tests/chat-client-context.test.ts
  • packages/ai-event-client/src/index.ts
  • packages/ai/docs/chat-architecture.md
  • packages/ai/src/activities/chat/messages.ts
  • packages/ai/src/activities/chat/stream/message-updaters.ts
  • packages/ai/src/activities/chat/stream/processor.ts
  • packages/ai/src/types.ts
  • packages/ai/tests/message-updaters.test.ts
  • packages/ai/tests/stream-processor.test.ts
  • testing/e2e/tests/tool-error.spec.ts

…result.state

Pass an error message to processor.addToolResult only for output-error
results; pass undefined otherwise. Previously the non-error branch
forwarded result.errorText, so a stray errorText on a successful result
could be misread as an error by addToolResult's message-truthiness check.
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.

ToolCallState has no error terminal — an errored server-tool execution parks at state "input-complete"

1 participant