onAfterToolCall failure records a second, contradictory result for a successful server tool
Maintainer thường phản hồi trong vòng 1 ngày
@AlemTuzlak đang làm issue này rồi.
Từ ngày 28/9/2026.
Đánh giá
Issue này chưa được đánh giá.
Mô tả
TanStack AI version: @tanstack/ai 0.63.0 at main commit 62bec34bb.
Framework/Library version: Node.js 24.11.1; framework-independent server tool path.
Describe the bug and the steps to reproduce it
When a server tool returns successfully and onAfterToolCall throws, one tool-call ID gets both a success result and an error result. TanStack AI also invokes the hook twice: first with ok: true, then with ok: false. If the hook's second invocation returns, the run continues as though the tool itself had failed.
This can mislead the model and downstream consumers. It also repeats a hook with side effects, such as an audit write or trace export.
The exact path on main is:
executeToolCalls()selects a server tool and delegates toexecuteServerTool().tool.execute()returns{ sent: true }.executeServerTool()pushes that success result intoresults.onAfterToolCall({ ok: true, result: { sent: true } })throwspost-hook failed.- The surrounding
catchtreats the hook error as a tool execution error. It pushes{ error: 'post-hook failed' }for the sametoolCallIdand callsonAfterToolCall({ ok: false, error }). - The second hook call returns, so
executeToolCalls()returns both results and does not propagate the hook error to the run.
This differs from a real tool failure. If tool.execute() throws before a result exists, one error result and one onAfterToolCall({ ok: false }) call are expected.
Minimal reproduction
The following is the complete script used for the red/green check. Save it as repro.mts and run pnpm exec tsx repro.mts <absolute-checkout-root> after pnpm install. Run it once against clean main at 62bec34bb and once against the proposed fix.
import { pathToFileURL } from 'node:url'
import path from 'node:path'
const root = process.argv[2]
if (!root) throw new Error('Pass the checkout root')
const url = pathToFileURL(path.join(root, 'packages/ai/src/activities/chat/tools/tool-calls.ts')).href
const { executeToolCalls } = await import(url)
let executed = 0
let afterCalls = 0
const calls = [{ id: 'call-1', type: 'function', function: { name: 'sendEmail', arguments: '{}' } }]
const tools = [{ name: 'sendEmail', execute: () => { executed++; return { sent: true } } }]
const generator = executeToolCalls(calls, tools, new Map(), new Map(), undefined, {
onAfterToolCall: async () => {
afterCalls++
if (afterCalls === 1) throw new Error('post-hook failed')
},
})
let result
let error
try {
for (;;) {
const step = await generator.next()
if (step.done) { result = step.value; break }
}
} catch (caught) {
error = caught instanceof Error ? caught.message : String(caught)
}
const observed = { executed, afterCalls, error: error ?? null, results: result?.results ?? null }
console.log(JSON.stringify(observed))
if (executed !== 1 || afterCalls !== 1 || error !== 'post-hook failed' || result !== undefined) {
throw new Error('after-hook failure was reported as a tool result or hook ran twice')
}
Actual on clean main:
{"executed":1,"afterCalls":2,"error":null,"results":[{"toolCallId":"call-1","toolName":"sendEmail","result":{"sent":true},"input":{},"output":{"sent":true},"duration":0},{"toolCallId":"call-1","toolName":"sendEmail","result":{"error":"post-hook failed"},"input":{},"state":"output-error","duration":0}]}
The script exits with code 1 on main. The duration: 0 values are observations from this run; durations vary with the clock.
Expected: executed and afterCalls both equal 1. The hook error propagates, so error is post-hook failed. The chat run calls onError; its SSE stream ends with RUN_ERROR and no TOOL_CALL_RESULT for this call.
With the proposed fix, the same script returned {"executed":1,"afterCalls":1,"error":"post-hook failed","results":null} and exited with code 0. The chat-level regression test also checks that onError runs once.
Your Minimal, Reproducible Example - (Sandbox Highly Recommended)
Regression test on the fork branch and the standalone repro.mts above.
Screenshots or Videos (Optional)
Not applicable. The JSON output above shows the duplicate result.
Do you intend to try to help solve this bug with your own PR?
Yes, I am also opening a PR that solves the problem alongside this issue.
Related reports and PRs
I searched open and closed issues and PRs for onAfterToolCall, duplicate tool results, and after-tool errors. I found no report of this failure. #1547 concerns serial execution of independent tool calls. #1513 changes subagent return values near this function, but it does not address hook failures.
Terms & Code of Conduct
- I agree to follow this project's Code of Conduct.
- I understand that a bug without a reliable reproduction may be closed.
- Ngôn ngữ chính
- TypeScript
- Star
- 3.1k
- Fork
- 340
- Merge trung bình
- 2 ngày 7 giờ
- Pull request đã merge (30 ngày)
- 175
Chuẩn bị môi trường
- Không có Dockerfile hay tệp Docker Compose
- Có mẫu pull request
- Đọc hướng dẫn đóng góp
Bắt đầu từ đâu
- Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
- Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
- Fork repository và làm thay đổi trên một nhánh.
- Mở pull request có tham chiếu số hiệu của issue.
Issue khác của TanStack/ai
-
Độ khó 4/5 3-5 ngày Mức phù hợp với người mới 68/100
Maintainer thường phản hồi trong vòng 1 ngày
-
signin with openaiĐang mở
Độ khó 5/5 Hơn một tuần Mức phù hợp với người mới 45/100
Maintainer thường phản hồi trong vòng 1 ngày
-
update elevenlabsCó thể đã có người làm @tombeckenham đã nhận 1 ngày trước. Đang mởhas-pr waiting-on: maintainer
Độ khó 4/5 3-5 ngày Mức phù hợp với người mới 25/100
TanStack/ai#1564 · 1 người được giao ·
Maintainer thường phản hồi trong vòng 1 ngày
-
SSE and NDJSON response streams drain unread sources without backpressureCó thể đã có người làm @tombeckenham đã nhận 1 ngày trước. Đang mởhas-pr waiting-on: maintainer
TanStack/ai#1556 · 1 người được giao ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Solid useChat drops earlier turns after a reactive request option changesCó thể đã có người làm @AlemTuzlak đã nhận 1 ngày trước. Đang mởhas-pr waiting-on: maintainer
TanStack/ai#1552 · 1 người được giao ·
Maintainer thường phản hồi trong vòng 1 ngày
Issue tương tự
-
Mend: dependency security vulnerability untriaged
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 68/100
opensearch-project/security-dashboards-plugin#2545 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Add: Dream TR SDĐang mởcheck:passed streams:add
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 70/100
Maintainer thường phản hồi trong vòng 1 ngày
-
doctor integrity sample scans soft-deleted pages on Postgres (batch path has no deleted_at filter)Đang mở
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 88/100
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 1/5 Dưới một giờ Mức phù hợp với người mới 72/100
SocialGouv/egapro#4672 · 1 bình luận ·
Maintainer thường phản hồi trong vòng 2 ngày
-
area:agents area:tui bug
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 74/100
anthropics/claude-code#98358 ·
Maintainer thường phản hồi trong vòng 1 ngày