Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/undeprecate-client-tools.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@cloudflare/ai-chat": patch
---

Undeprecate client tool APIs (`createToolsFromClientSchemas`, `clientTools`, `AITool`, `extractClientToolSchemas`, and the `tools` option on `useAgentChat`) for SDK/platform use cases where tools are defined dynamically at runtime. Fix spurious `detectToolsRequiringConfirmation` deprecation warning when using the `tools` option.
49 changes: 49 additions & 0 deletions docs/chat-agents.md
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,7 @@ function Chat() {
| `resume` | `boolean` | `true` | Enable automatic stream resumption on reconnect |
| `body` | `object \| () => object` | — | Custom data sent with every request |
| `prepareSendMessagesRequest` | `(options) => { body?, headers? }` | — | Advanced per-request customization |
| `tools` | `Record<string, AITool>` | — | Dynamic client-defined tools for SDK/platform use cases. Schemas are sent to the server automatically |
| `getInitialMessages` | `(options) => Promise<UIMessage[]>` or `null` | — | Custom initial message loader. Set to `null` to skip the HTTP fetch entirely (useful when providing `messages` directly) |

### Return Values
Expand Down Expand Up @@ -409,6 +410,53 @@ const { messages, sendMessage } = useAgentChat({

When the LLM invokes `getLocation`, the stream pauses. The `onToolCall` callback fires, your code provides the output, and the conversation continues.

### Dynamic Client Tools (SDK/Platform Pattern)

For SDKs and platforms where tools are defined dynamically by the embedding application at runtime, use the `tools` option on `useAgentChat` and `createToolsFromClientSchemas()` on the server:

**Server:**

```typescript
import { createToolsFromClientSchemas } from "@cloudflare/ai-chat";

async onChatMessage(_onFinish, options) {
const result = streamText({
model: workersai("@cf/zai-org/glm-4.7-flash"),
messages: await convertToModelMessages(this.messages),
tools: createToolsFromClientSchemas(options?.clientTools)
});
return result.toUIMessageStreamResponse();
}
```

**Client:**

```tsx
import { useAgentChat, type AITool } from "@cloudflare/ai-chat/react";

const tools: Record<string, AITool> = {
getPageTitle: {
description: "Get the current page title",
parameters: { type: "object", properties: {} },
execute: async () => ({ title: document.title })
}
};

const { messages, sendMessage } = useAgentChat({
agent,
tools,
onToolCall: async ({ toolCall, addToolOutput }) => {
const tool = tools[toolCall.toolName];
if (tool?.execute) {
const output = await tool.execute(toolCall.input);
addToolOutput({ toolCallId: toolCall.toolCallId, output });
}
}
});
```

For most apps, server-side tools with `tool()` and `onToolCall` are simpler and provide full Zod type safety. Use dynamic client tools when the server does not know the tool surface at deploy time.

### Tool Approval (Human-in-the-Loop)

Use `needsApproval` for tools that require user confirmation before executing:
Expand Down Expand Up @@ -1001,6 +1049,7 @@ The chat protocol uses typed JSON messages over WebSocket:
## Examples

- [AI Chat Example](../examples/ai-chat/) — Modern example with server tools, client tools, and approval
- [Dynamic Tools](../examples/dynamic-tools/) — SDK/platform pattern with dynamic client-defined tools
- [Resumable Stream Chat](../examples/resumable-stream-chat/) — Automatic stream resumption demo
- [Human in the Loop Guide](../guides/human-in-the-loop/) — Tool approval with `needsApproval` and `onToolCall`
- [Playground](../examples/playground/) — Kitchen-sink demo of all SDK features
Expand Down
46 changes: 31 additions & 15 deletions docs/migration-to-ai-sdk-v6.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,9 @@ import { convertToCoreMessages, type CoreMessage } from "ai";
import { convertToModelMessages, type ModelMessage } from "ai";
```

### 3. Tool pattern: define everything on the server
### 3. Tool pattern: server-side tools (recommended)

v6 introduces `needsApproval` and the `onToolCall` callback, replacing the old client-side tool definitions:
v6 introduces `needsApproval` and the `onToolCall` callback. For most apps, define tools on the server with `tool()` from `"ai"` for full Zod type safety:

**Before (v5):**

Expand All @@ -54,12 +54,6 @@ useAgentChat({
experimental_automaticToolResolution: true,
toolsRequiringConfirmation: ["askConfirmation"]
});

// Server converted client schemas
const tools = {
...serverTools,
...createToolsFromClientSchemas(clientTools)
};
```

**After (v6):**
Expand Down Expand Up @@ -100,6 +94,31 @@ useAgentChat({
});
```

**Dynamic client tools (SDK/platform pattern):**

If you are building an SDK or platform where tools are defined dynamically by the embedding application at runtime, the `tools` option on `useAgentChat` and `createToolsFromClientSchemas()` on the server are still fully supported:

```typescript
// Server: accept whatever tools the client sends
const tools = {
...createToolsFromClientSchemas(options.clientTools),
...serverTools
};

// Client: register tools dynamically
useAgentChat({
agent,
tools: dynamicTools,
onToolCall: async ({ toolCall, addToolOutput }) => {
const tool = dynamicTools[toolCall.toolName];
if (tool?.execute) {
const output = await tool.execute(toolCall.input);
addToolOutput({ toolCallId: toolCall.toolCallId, output });
}
}
});
```

### 4. `generateObject` mode option removed

Remove `mode: "json"` or similar from `generateObject` calls.
Expand All @@ -112,14 +131,12 @@ In v6, these check both static and dynamic tool parts. For the old behavior, use

| Deprecated | Replacement |
| -------------------------------------- | --------------------------------------------------------- |
| `AITool` type | `tool()` from "ai" on the server |
| `extractClientToolSchemas()` | Define tools on server |
| `createToolsFromClientSchemas()` | Define tools on server with `tool()` |
| `toolsRequiringConfirmation` | [`needsApproval`](./human-in-the-loop.md) on server tools |
| `experimental_automaticToolResolution` | [`onToolCall`](./client-tools-continuation.md) callback |
| `tools` option in `useAgentChat` | [`onToolCall`](./client-tools-continuation.md) |
| `addToolResult()` | `addToolOutput()` or `addToolApprovalResponse()` |

**Not deprecated:** `AITool`, `createToolsFromClientSchemas()`, `extractClientToolSchemas()`, and the `tools` option on `useAgentChat` are supported for SDK/platform use cases where tools are defined dynamically at runtime.

## Migration checklist

**Packages:**
Expand All @@ -134,11 +151,10 @@ In v6, these check both static and dynamic tool parts. For the old behavior, use
- Replace `CoreMessage` with `ModelMessage`
- Replace `convertToCoreMessages()` with `convertToModelMessages()`
- Remove `mode` from `generateObject` calls
- Move client tool definitions to server using `tool()`
- Replace `tools` option with `onToolCall` in `useAgentChat`
- Move static tool definitions to server using `tool()` (recommended for most apps)
- Use `onToolCall` in `useAgentChat` for client-side tool execution
- Replace `toolsRequiringConfirmation` with `needsApproval`
- Replace `addToolResult()` with `addToolOutput()` or `addToolApprovalResponse()`
- Remove `createToolsFromClientSchemas()` usage

## Further reading

Expand Down
70 changes: 70 additions & 0 deletions examples/dynamic-tools/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# Dynamic Tools

Demonstrates dynamic client-defined tools — the **SDK/platform pattern** where tools are registered at runtime by the embedding application, not known by the server at deploy time.

## The pattern

This example shows how to build a chat agent where:

1. The **server** is generic infrastructure — it accepts whatever tools the client sends
2. The **client** defines tools dynamically (schemas + execute functions)
3. Tool schemas are automatically sent to the server via the WebSocket protocol
4. The LLM calls the tools, and results are routed back to the client for execution

This is the same architecture you would use when building an **SDK or platform** where third-party developers define tools in their embedding application.

## Key code

### Server (`src/server.ts`)

The server uses `createToolsFromClientSchemas()` to convert client-provided schemas into AI SDK tools:

```typescript
import { createToolsFromClientSchemas } from "@cloudflare/ai-chat";

async onChatMessage(_onFinish, options) {
const result = streamText({
model: workersai("@cf/zai-org/glm-4.7-flash"),
tools: createToolsFromClientSchemas(options?.clientTools),
// ...
});
return result.toUIMessageStreamResponse();
}
```

### Client (`src/client.tsx`)

The client passes tools via the `tools` option on `useAgentChat`:

```typescript
import { useAgentChat, type AITool } from "@cloudflare/ai-chat/react";

const tools: Record<string, AITool> = {
getPageTitle: {
description: "Get the current page title",
parameters: { type: "object", properties: {} },
execute: async () => ({ title: document.title })
}
};

const { messages, sendMessage } = useAgentChat({
agent,
tools
});
```

## Run it

```bash
npm install && npm start
```

## When to use this vs server-side tools

- **Server-side tools** (`tool()` from `"ai"`): Best for most apps. Full Zod type safety, simpler code, tools defined in one place. Use `onToolCall` for client-side execution.
- **Dynamic client tools** (this pattern): Best for SDKs, platforms, and multi-tenant systems where the tool surface is determined by the embedding application at runtime.

## Related examples

- [`ai-chat`](../ai-chat/) — Server-side tools with `tool()`, approval, and `onToolCall`
- [`playground`](../playground/) — Kitchen-sink showcase of all SDK features
15 changes: 15 additions & 0 deletions examples/dynamic-tools/env.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/* eslint-disable */
// Generated by Wrangler by running `wrangler types env.d.ts --include-runtime false` (hash: placeholder)
declare namespace Cloudflare {
interface GlobalProps {
mainModule: typeof import("./src/server");
durableNamespaces: "DynamicToolsAgent";
}
interface Env {
AI: Ai;
DynamicToolsAgent: DurableObjectNamespace<
import("./src/server").DynamicToolsAgent
>;
}
}
interface Env extends Cloudflare.Env {}
20 changes: 20 additions & 0 deletions examples/dynamic-tools/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<!doctype html>
<html lang="en" data-theme="workers">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" href="/favicon.ico" />
<title>Dynamic Tools</title>
<script>
(() => {
const mode = localStorage.getItem("theme") || "light";
document.documentElement.setAttribute("data-mode", mode);
document.documentElement.style.colorScheme = mode;
})();
</script>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/index.tsx"></script>
</body>
</html>
23 changes: 23 additions & 0 deletions examples/dynamic-tools/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"name": "@cloudflare/agents-dynamic-tools-example",
"description": "Dynamic client-defined tools — the SDK/platform pattern where tools are registered at runtime by the embedding application",
"private": true,
"type": "module",
"version": "0.0.0",
"scripts": {
"start": "vite dev",
"deploy": "vite build && wrangler deploy",
"types": "wrangler types env.d.ts --include-runtime false"
},
"dependencies": {
"@cloudflare/agents-ui": "*",
"@cloudflare/ai-chat": "*",
"@cloudflare/kumo": "^1.7.0",
"@phosphor-icons/react": "^2.1.10",
"agents": "*"
},
"devDependencies": {
"@tailwindcss/vite": "^4",
"tailwindcss": "^4.2.0"
}
}
Binary file added examples/dynamic-tools/public/favicon.ico
Binary file not shown.
Loading
Loading