> ## Documentation Index
> Fetch the complete documentation index at: https://mintlify.com/cloudflare/agents/llms.txt
> Use this file to discover all available pages before exploring further.

# Client-Side Tools and Auto-Continuation

> Execute tools in the browser and automatically continue LLM conversations in the same turn

## Overview

Tools in `AIChatAgent` can be divided into two categories:

* **Server tools**: Have an `execute` function on the server. The AI SDK runs them automatically and the LLM continues responding in the same turn.
* **Client tools**: No `execute` function on the server. The tool call is sent to the client via `onToolCall`, and the client provides the result. By default, this requires a new request to continue.

With `autoContinueAfterToolResult`, client tools can behave like server tools — the LLM calls a tool, the client executes it, and the server automatically continues the conversation in the same turn.

## Server Setup

Define a tool without an `execute` function. The AI SDK will pause and send `tool-input-available` to the client:

```typescript theme={null}
import { AIChatAgent } from "@cloudflare/ai-chat";
import { createWorkersAI } from "workers-ai-provider";
import { streamText, tool, convertToModelMessages, stepCountIs } from "ai";
import { z } from "zod";

export class MyAgent extends AIChatAgent {
  async onChatMessage() {
    const workersai = createWorkersAI({ binding: this.env.AI });

    const result = streamText({
      model: workersai("@cf/zai-org/glm-4.7-flash"),
      messages: await convertToModelMessages(this.messages),
      tools: {
        // Client-side tool: no execute function
        getUserLocation: tool({
          description: "Get the user's location from their browser",
          inputSchema: z.object({})
        }),

        // Server-side tool: has execute, runs automatically
        getWeather: tool({
          description: "Get weather for a city",
          inputSchema: z.object({ city: z.string() }),
          execute: async ({ city }) => fetchWeather(city)
        })
      },
      stopWhen: stepCountIs(5) // Allow multi-step so the LLM can respond after tool results
    });

    return result.toUIMessageStreamResponse();
  }
}
```

## Client Setup

Use `onToolCall` to handle client-side tool execution. Auto-continuation is enabled by default (`autoContinueAfterToolResult: true`), so the server automatically calls `onChatMessage()` again after receiving the tool result, letting the LLM continue in the same assistant message.

```tsx theme={null}
import { useAgent } from "agents/react";
import { useAgentChat } from "@cloudflare/ai-chat/react";

function Chat() {
  const agent = useAgent({ agent: "MyAgent" });

  const { messages, sendMessage } = useAgentChat({
    agent,
    // Auto-continuation is enabled by default — no need to set this explicitly
    // autoContinueAfterToolResult: true,
    onToolCall: async ({ toolCall, addToolOutput }) => {
      if (toolCall.toolName === "getUserLocation") {
        const pos = await new Promise((resolve, reject) => {
          navigator.geolocation.getCurrentPosition(resolve, reject);
        });
        addToolOutput({
          toolCallId: toolCall.toolCallId,
          output: {
            lat: pos.coords.latitude,
            lng: pos.coords.longitude
          }
        });
      }
    }
  });

  // Render messages...
}
```

## How It Works

<Steps>
  <Step title="User sends message">
    User: "What's the weather near me?"

    Client sends message → Server calls LLM
  </Step>

  <Step title="LLM requests client tool">
    LLM decides to call getUserLocation (no server execute)

    Stream sends tool-input-available to client
  </Step>

  <Step title="Client executes tool">
    `onToolCall` fires → client gets geolocation → sends `CF_AGENT_TOOL_RESULT`
  </Step>

  <Step title="Server receives result">
    Server receives result with `autoContinue: true`

    Server waits for the original stream to complete
  </Step>

  <Step title="Automatic continuation">
    Server calls `onChatMessage()` again (continuation)

    LLM sees the location result, calls getWeather (server execute)
  </Step>

  <Step title="Final response">
    LLM responds: "It's sunny and 72°F near you!"

    Continuation parts are merged into the same assistant message
  </Step>
</Steps>

<Note>
  The user sees a single seamless response, even though it involved a client-side tool call mid-stream.
</Note>

## Without Auto-Continuation

When `autoContinueAfterToolResult` is set to `false`, the client must explicitly send a follow-up message after providing the tool result:

```tsx theme={null}
const { messages, sendMessage, addToolOutput } = useAgentChat({
  agent,
  onToolCall: async ({ toolCall, addToolOutput: provide }) => {
    if (toolCall.toolName === "getUserLocation") {
      const pos = await getPosition();
      provide({
        toolCallId: toolCall.toolCallId,
        output: { lat: pos.coords.latitude, lng: pos.coords.longitude }
      });
    }
  },
  autoContinueAfterToolResult: false // Disable auto-continuation
});

// After tool result is provided, send a follow-up to continue
// This creates a new assistant message rather than continuing the existing one
```

<Note>
  Use this when you want explicit control over when the conversation continues, or when tool results need user review before proceeding.
</Note>

## Combining with needsApproval

You can use client-side tools and approval together. For example, a tool that needs both user approval and browser execution:

<CodeGroup>
  ```typescript Server theme={null}
  // Server: tool with needsApproval but no execute
  const shareLocation = tool({
    description: "Share the user's location with a third party",
    inputSchema: z.object({ service: z.string() }),
    needsApproval: true
    // No execute - client handles after approval
  });
  ```

  ```tsx Client theme={null}
  // Client: handle approval, then execute
  const { addToolApprovalResponse } = useAgentChat({
    agent,
    autoContinueAfterToolResult: true,
    onToolCall: async ({ toolCall, addToolOutput }) => {
      if (toolCall.toolName === "shareLocation") {
        const pos = await getPosition();
        addToolOutput({
          toolCallId: toolCall.toolCallId,
          output: { lat: pos.coords.latitude, lng: pos.coords.longitude }
        });
      }
    }
  });
  ```
</CodeGroup>

The flow becomes: LLM calls tool → user approves → client executes → server auto-continues.

### Custom Denial Messages

If the user denies the tool, you can provide a custom error message using `addToolOutput` with `state: "output-error"`:

```tsx theme={null}
// Deny with a reason instead of generic rejection
addToolOutput({
  toolCallId: toolCall.toolCallId,
  state: "output-error",
  errorText: "User declined to share location"
});
```

## Related Documentation

* [Chat Agents](/ai/chat-agents) — Full `AIChatAgent` and `useAgentChat` reference
* [Human in the Loop](/advanced/human-in-the-loop) — Approval patterns including `needsApproval`
