cloudflare / cloudflare/agents

useAgentChat: onToolCall fires for server-side tools, and answering one sends the server a false failure verdict

Open
#2,195 0 comments 0 reactions 1 assignee Claimed by @threepointone View on GitHub
bug
Dominant language
TypeScript
Stars
5.6k
Forks
711
Avg merge
1d 20h
Merged PRs (30d)
53

Description

**Describe the bug**

`useAgentChat`'s `onToolCall` dispatch fires for **every** tool part in `input-available` on the leaf assistant message, with no filter for whether the client is actually responsible for that tool. Server-side tools sit in `input-available` for their entire execution, so the client callback is invoked for tools it cannot and should not resolve.

`packages/agents/src/chat/react.tsx:1793`:

```ts
const pendingToolCalls = lastMsg.parts.filter(
(part) =>
isToolUIPart(part) &&
part.state === "input-available" &&
!processedToolCalls.current.has(part.toolCallId)
);
```

This is more than a spurious callback, because answering is destructive. `addToolOutput` routes through `sendToolOutputToServer`, which at `react.tsx:1727` does:

```ts
const shouldAutoContinue =
state === "output-error" ? false : autoContinueAfterToolResult;
```

and then sends `CF_AGENT_TOOL_RESULT` upstream. So an app that answers an unknown tool with an error — the natural defensive pattern, since an unanswered client tool hangs the turn — emits a **failure verdict on work the server is executing correctly**, and simultaneously suppresses auto-continuation for that turn.

The observable result is a tool part carrying both a successful server result and a spurious `output-error`, plus a client `status` of `error` while the server keeps streaming normally. The UI reports a failed turn over a perfectly healthy stream.

**The asymmetry**

The framework already models "which tools can the client resolve" — but only on the server. `Think.hasPendingInteraction()` narrows pending interactions through `clientResolvableToolNames()` (`packages/agents/src/chat/tool-state.ts`). The client-side hook holds the same information in the same file — `toolsRef.current`, used for `extractClientToolSchemas` at `react.tsx:1001` — but does not apply it to the dispatch.

**To Reproduce**

1. Mount `useAgentChat({ agent, tools: { someClientTool }, onToolCall })`, where `onToolCall` answers with `addToolOutput({ state: "output-error", ... })` for a tool it does not recognise.
2. Have the server-side agent define and call a tool of its own (any server-executed tool, not present in `tools`).
3. While the server executes it, the part is `input-available` and `onToolCall` fires for it on the client.
4. The app answers, and the server receives `CF_AGENT_TOOL_RESULT` with `state: "output-error"` and `autoContinue: false` for a call it is completing successfully.

**Expected behavior**

Either:

- `onToolCall` only fires for tools the client is responsible for, or
- the hook exposes the client-resolvable tool set, so an app can filter reliably.

The second matters because there is currently no supported signal for "this tool belongs to the server". `tools` is marked deprecated in the source (`react.tsx:998`, "deprecated client tools"), and `onToolCall` is the recommended path, so an app on the recommended path has nothing to discriminate against.

**Version**

`agents@0.20.1`. The dispatch is unchanged in the 0.21.0 and 0.22.0 release notes.

**Additional context**

Current workaround is an app-level guard at the top of `onToolCall` that returns early for any tool the app did not declare. That works, but every app has to reconstruct by hand a set the server already maintains internally — and the failure mode for not doing it is silent and misleading rather than loud.

If the intended contract is in fact "the app must filter", it would help to state that in the `onToolCall` docs, since the natural reading of "resolve the tool or report an error" is what produces the bug.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.