fix(ai): tool_use.name undefined after approving a tool then sending follow-up (#532)#536
Conversation
…follow-up (#532) Synthesized TOOL_CALL_START in the post-approval continuation now sets the AG-UI spec field `toolCallName` (in addition to the deprecated `toolName` alias), so the client's StreamProcessor records a tool-call part with a defined `name` instead of `undefined`. Without the fix, the next outbound request was rejected by Anthropic with `tool_use.name: String should have at least 1 character`. Defensively, StreamProcessor now also falls back to `chunk.toolName` when `chunk.toolCallName` is missing. Additionally, after running an approved tool server-side, the agent loop replaces the `pendingExecution: true` placeholder tool message in its message history instead of appending a duplicate. This stops the Anthropic adapter's tool_result de-dup (which keeps the first match) from discarding the real result, so the model sees the actual tool output during the post-approval streaming response. Includes unit tests (chat.test.ts, stream-processor.test.ts) and an E2E case in tool-approval.spec.ts plus a matching aimock fixture. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (3)
📝 WalkthroughWalkthroughThis PR ensures TOOL_CALL_START events include a defined toolCallName (falling back to deprecated toolName) and replaces pendingExecution placeholder tool messages with actual tool results so downstream adapters receive the real tool result in the same stream. ChangesTool Name & Message Deduplication Fix
Sequence Diagram(s)sequenceDiagram
participant User
participant StreamProcessor
participant TextEngine
participant MessageHistory
participant Adapter
User->>StreamProcessor: sends approval response / follow-up
StreamProcessor->>TextEngine: request tool execution handling
TextEngine->>MessageHistory: search for pendingExecution placeholder
alt Placeholder found
TextEngine->>MessageHistory: replace with real tool result
else
TextEngine->>MessageHistory: append real tool result
end
TextEngine-->>Adapter: emit tool message (with defined tool name)
Adapter-->>User: assistant response
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes 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 docstrings
🧪 Generate unit tests (beta)
Tip 💬 Introducing Slack Agent: The best way for teams to turn conversations into code.Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.
Built for teams:
One agent for your entire SDLC. Right inside Slack. 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 |
🚀 Changeset Version Preview3 package(s) bumped directly, 30 bumped as dependents. 🟥 Major bumps
🟨 Minor bumps
🟩 Patch bumps
|
|
View your CI Pipeline Execution ↗ for commit 82ea764
☁️ Nx Cloud last updated this comment at |
@tanstack/ai
@tanstack/ai-anthropic
@tanstack/ai-client
@tanstack/ai-code-mode
@tanstack/ai-code-mode-skills
@tanstack/ai-devtools-core
@tanstack/ai-elevenlabs
@tanstack/ai-event-client
@tanstack/ai-fal
@tanstack/ai-gemini
@tanstack/ai-grok
@tanstack/ai-groq
@tanstack/ai-isolate-cloudflare
@tanstack/ai-isolate-node
@tanstack/ai-isolate-quickjs
@tanstack/ai-ollama
@tanstack/ai-openai
@tanstack/ai-openrouter
@tanstack/ai-preact
@tanstack/ai-react
@tanstack/ai-react-ui
@tanstack/ai-solid
@tanstack/ai-solid-ui
@tanstack/ai-svelte
@tanstack/ai-vue
@tanstack/ai-vue-ui
@tanstack/preact-ai-devtools
@tanstack/react-ai-devtools
@tanstack/solid-ai-devtools
commit: |
Openrouter's chatStream fails on the multi-turn approval-then-followup flow for reasons unrelated to the core fix in @tanstack/ai. The unit tests in packages/typescript/ai cover the fix provider-agnostically; the other five tool-approval providers (openai, anthropic, ollama, groq, grok) still exercise it in E2E. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
OpenRouter's adapter previously received the testId via a query param on
serverURL, but the SDK calls `new URL(path, baseURL)` per request, which
drops the search component — so testId never reached aimock and every
openrouter test collided on the `__default__` testId bucket. As soon as
two openrouter tests sent the same userMessage, the second one's
sequenceIndex didn't reset, no fixture matched, and chatStream threw.
The OpenRouter SDK exposes an `httpClient` option whose `HTTPClient`
supports `addHook("beforeRequest", …)`. Use that to set X-Test-Id on
each request. This isolates openrouter tests the same way every other
provider already is.
Also reverts the earlier `test.skip(provider === 'openrouter')` on the
issue #532 follow-up case — with this fix the test passes for openrouter
too.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes #532
🎯 Changes
Fixes the
400 ValidationException: messages.N.content.M.tool_use.name: String should have at least 1 charactererror from Anthropic when sending a follow-up message after a tool that needed approval has been approved and executed.Root cause.
buildToolResultChunks(inpackages/typescript/ai/src/activities/chat/index.ts) synthesizes aTOOL_CALL_STARTchunk for the post-approval continuation flow. It only set the deprecatedtoolNamefield, not the AG-UI spec fieldtoolCallName. The client'sStreamProcessor.handleToolCallStartEventreadschunk.toolCallName, gotundefined, and wrote a tool-call part withname: undefined. On the next user message, that propagated throughuiMessageToModelMessagesinto aModelMessage, the Anthropic adapter wrote it intotool_use.name, and the API rejected the request.Changes:
packages/typescript/ai/src/activities/chat/index.ts— synthesizedTOOL_CALL_STARTnow includestoolCallNamealongside the deprecatedtoolNamealias. After running an approved tool server-side, the agent loop now replaces thependingExecution: trueplaceholder tool message in its history instead of appending a duplicate — this stops the Anthropic adapter'stool_resultde-dup (first-wins) from discarding the real result, so the model sees the actual tool output during the post-approval streaming response.packages/typescript/ai/src/activities/chat/stream/processor.ts— defensive:handleToolCallStartEventnow falls back tochunk.toolNamewhenchunk.toolCallNameis missing, sincetoolNameis documented as a deprecated alias.packages/typescript/ai/tests/chat.test.ts— new unit test simulates the full approved-UIMessage flow and asserts bothtoolCallName/toolNameon the synthesized chunk and that the adapter sees the real tool result, not the placeholder. Three existing TOOL_CALL_START assertions also now checktoolCallName.packages/typescript/ai/tests/stream-processor.test.ts— new unit test covers the deprecated-onlytoolNamechunk path.testing/e2e/tests/tool-approval.spec.ts+testing/e2e/fixtures/tool-approval/approval.json— new E2E case approves the tool then sends a follow-up message; matching aimock fixture entry added.Note on emitting both
toolCallNameandtoolName. The fix continues to emit both fields rather than dropping the deprecated alias. Two reasons: (1) every other emission site already emits both —TOOL_CALL_ENDtwo lines below in the same function, and all threeTOOL_CALL_START/ENDsites in the Anthropic adapter — so the bug was just that this one site was inconsistent; (2)toolNameis@deprecatedbut documented as "Kept for backward compatibility", so dropping it would be a breaking change for any middleware, devtools subscriber, or downstream consumer still reading it. RetiringtoolNameentirely (drop from every adapter, drop from the type, major bump) is out of scope for this bug fix and would be its own follow-up PR.✅ Checklist
pnpm run test:pr.🚀 Release Impact
🤖 Generated with Claude Code
Summary by CodeRabbit
Bug Fixes
Tests