feat(ai): add 'error' terminal to ToolCallState (#718)#727
Conversation
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.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthroughAdds 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. ChangesTool Error Terminal State Machine
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Warning Review ran into problems🔥 ProblemsStopped 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 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 |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
packages/ai-client/src/chat-client.ts (1)
1221-1231: 💤 Low valueVerify error-signal precedence: state vs. message truthiness.
The code ensures a non-empty error message when
result.state === 'output-error'(fixing#718), but whenstate !== 'output-error', it still passesresult.errorTextto the processor. Since the processor infers error-ness from message truthiness (error ? 'error' : undefinedat 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 setstate: '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 accidentalerrorTextpresence.🤖 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
📒 Files selected for processing (13)
.changeset/tool-call-error-terminal.mdpackages/ai-client/src/chat-client.tspackages/ai-client/src/types.tspackages/ai-client/tests/chat-client-context.test.tspackages/ai-event-client/src/index.tspackages/ai/docs/chat-architecture.mdpackages/ai/src/activities/chat/messages.tspackages/ai/src/activities/chat/stream/message-updaters.tspackages/ai/src/activities/chat/stream/processor.tspackages/ai/src/types.tspackages/ai/tests/message-updaters.test.tspackages/ai/tests/stream-processor.test.tstesting/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.
Summary
Closes #718.
ToolCallStatehad 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 frompart.state(the natural reading of a state machine) therefore couldn't distinguish "still executing" from "failed" without reverse-engineering the error-shapedoutputor the sibling part, producing an infinite spinner on every tool failure.This adds an
'error'member toToolCallStateand transitions the tool-call part to it on an output error, making the state machine self-describing and symmetric withToolResultState:What changed
'error'added toToolCallStatein the three hand-synced copies:@tanstack/ai,@tanstack/ai-client,@tanstack/ai-event-client.StreamProcessormaps output errors tostate: 'error'for the tool-call part inaddToolResult,handleToolCallEndEvent, andhandleToolCallResultEvent(the tool-result part already used'error').RUN_FINISHED/finalizeStream): still finalizes the call's internal bookkeeping (so it remains ingetResult().toolCalls/getState()), but no longer downgrades a rendered'error'part back to'input-complete'— including the AG-UI ordering where anoutput-errorTOOL_CALL_RESULTarrives beforeTOOL_CALL_END.throw new Error()) now still reaches'error'— error-ness derives from theoutput-errorstate, not message truthiness.isToolCallIncludedgains an explicit'error'arm so failed calls stay in conversation history for the LLM.chat-architecture.mdnotes the new terminal.Test plan
@tanstack/ai(stream-processor,message-updaters) and@tanstack/ai-client(chat-client-context), incl. new regression tests for the result-before-ENDordering and the empty-message error.tool-error.spec.tsasserts the failed tool-call part reachesstate === 'error'end-to-end; the fulltools-test/suite (40 specs) confirms normal completion, parallel tools, approvals, and continuations are unaffected.pnpm test:prgreen 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.
ToolCallStateis currently duplicated across three packages (kept in sync by hand); de-duplicating it is out of scope here.Summary by CodeRabbit
New Features
Bug Fixes
Documentation