Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
129e6b6
add client-defined tools and prepareSendMessagesRequest options
whoiskatrin Dec 10, 2025
68071e6
fix client tools execution
whoiskatrin Dec 10, 2025
e6e76cf
minor nits
whoiskatrin Dec 10, 2025
bc2e874
some claude review feedback
whoiskatrin Dec 10, 2025
fca9db3
refactor
whoiskatrin Dec 10, 2025
f99d9b7
Refactor useAgentChat
whoiskatrin Dec 10, 2025
63f9996
more nits
whoiskatrin Dec 10, 2025
3620f91
take a different approach
whoiskatrin Dec 10, 2025
800bbd9
cleanup after the refactor
whoiskatrin Dec 10, 2025
a526cc0
fix param convertion
whoiskatrin Dec 10, 2025
44e9333
improve tool execution handling in useAgentChat
whoiskatrin Dec 10, 2025
6cdec1f
fix request body structure in useAgentChat to include id, messages, a…
whoiskatrin Dec 11, 2025
2e7f062
review comments on types
whoiskatrin Dec 11, 2025
1f62a81
filter broadcast id's out
whoiskatrin Dec 11, 2025
461306e
fix types
whoiskatrin Dec 11, 2025
44e9f40
fix msg duplication
whoiskatrin Dec 11, 2025
7bbbb1b
fix the duplication issue in client tools
whoiskatrin Dec 11, 2025
0faeb1a
fix types
whoiskatrin Dec 11, 2025
365394d
fix types
whoiskatrin Dec 11, 2025
ab2b997
remove example from the git
whoiskatrin Dec 11, 2025
afb31c1
Send tool results to server as source of truth
whoiskatrin Dec 11, 2025
6cae864
remove automatic message sending after tool results
whoiskatrin Dec 12, 2025
8585098
cleanup the tests
whoiskatrin Dec 12, 2025
dc0736b
fix OpenAI item ID stripping to prevent duplicate errors in persisted…
whoiskatrin Dec 12, 2025
939c0fc
minor nits
whoiskatrin Dec 12, 2025
b8026ae
add server-side auto-continuation after tool results for improved UX;…
whoiskatrin Dec 12, 2025
27f8a8e
update useAgentChat to include autoContinueAfterToolResult in message…
whoiskatrin Dec 12, 2025
88ed2e2
fix for non human in the loop tools
whoiskatrin Dec 12, 2025
7d1aede
update the tests
whoiskatrin Dec 12, 2025
2f2edf6
Update slow-jobs-boil.md with new options
whoiskatrin Dec 12, 2025
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
121 changes: 121 additions & 0 deletions packages/agents/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -515,6 +515,127 @@ This creates:
- Intuitive input handling
- Easy conversation reset

#### Client-Defined Tools

For scenarios where each client needs to register its own tools dynamically (e.g., embeddable chat widgets), use the `tools` option with `execute` functions.

Tools with an `execute` function are automatically:

1. Sent to the server as schemas with each request
2. Executed on the client when the AI model calls them

##### Client-Side Tool Definition

```tsx
import { useAgent } from "agents/react";
import { useAgentChat, type AITool } from "agents/ai-react";

// Define tools outside component to avoid recreation on every render
const tools: Record<string, AITool> = {
showAlert: {
description: "Shows an alert dialog to the user",
parameters: {
type: "object",
properties: { message: { type: "string" } },
required: ["message"]
},
execute: async (input) => {
const { message } = input as { message: string };
alert(message);
return { success: true };
}
},
changeBackgroundColor: {
description: "Changes the page background color",
parameters: {
type: "object",
properties: { color: { type: "string" } }
},
execute: async (input) => {
const { color } = input as { color: string };
document.body.style.backgroundColor = color;
return { success: true, color };
}
}
};

function EmbeddableChat() {
const agent = useAgent({ agent: "chat-widget" });

const { messages, input, handleInputChange, handleSubmit } = useAgentChat({
agent,
tools // Schema + execute in one place
});

return (
<div className="chat-widget">
{messages.map((message) => (
<div key={message.id}>{/* Render message */}</div>
))}
<form onSubmit={handleSubmit}>
<input value={input} onChange={handleInputChange} />
</form>
</div>
);
}
```

##### Server-Side Tool Handling

On the server, use `createToolsFromClientSchemas` to convert client tool schemas to AI SDK format:

```typescript
import {
AIChatAgent,
createToolsFromClientSchemas
} from "agents/ai-chat-agent";
import { openai } from "@ai-sdk/openai";
import { streamText, convertToModelMessages } from "ai";

export class ChatWidget extends AIChatAgent {
async onChatMessage(onFinish, options) {
const result = streamText({
model: openai("gpt-4o"),
messages: convertToModelMessages(this.messages),
tools: {
// Server-side tools (execute on server)
getWeather: tool({
description: "Get weather for a city",
parameters: z.object({ city: z.string() }),
execute: async ({ city }) => fetchWeather(city)
}),
// Client-side tools (sent back to client for execution)
...createToolsFromClientSchemas(options?.clientTools)
},
onFinish
});
return result.toUIMessageStreamResponse();
}
}
```

##### Advanced: Custom Request Data

For additional control (custom headers, dynamic context), use `prepareSendMessagesRequest`:

```tsx
const { messages, handleSubmit } = useAgentChat({
agent,
tools, // Tool schemas auto-extracted and sent
prepareSendMessagesRequest: ({ id, messages }) => ({
body: {
// Add dynamic context alongside auto-extracted tool schemas
currentUrl: window.location.href,
userTimezone: Intl.DateTimeFormat().resolvedOptions().timeZone
},
headers: {
"X-Widget-Version": "1.0.0",
"X-Request-ID": crypto.randomUUID()
}
})
});
```

### 🔗 MCP (Model Context Protocol) Integration

Agents can seamlessly integrate with the Model Context Protocol, allowing them to act as both MCP servers (providing tools to AI assistants) and MCP clients (using tools from other services).
Expand Down
82 changes: 78 additions & 4 deletions packages/agents/src/ai-chat-agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type {
ToolUIPart,
UIMessageChunk
} from "ai";
import { tool, jsonSchema } from "ai";
import {
Agent,
type AgentContext,
Expand All @@ -25,6 +26,72 @@ import {
import { autoTransformMessages } from "./ai-chat-v5-migration";
import { nanoid } from "nanoid";

/**
* Schema for a client-defined tool sent from the browser.
* These tools are executed on the client, not the server.
*/
export type ClientToolSchema = {
/** Unique name for the tool */
name: string;
/** Human-readable description of what the tool does */
description?: string;
/** JSON Schema defining the tool's input parameters */
parameters?: Record<string, unknown>;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: naming here. Can we not build this type from ai sdk tools omitting execute

};

/**
* Options passed to the onChatMessage handler.
*/
export type OnChatMessageOptions = {
/** AbortSignal for cancelling the request */
abortSignal?: AbortSignal;
/**
* Tool schemas sent from the client for dynamic tool registration.
* These represent tools that will be executed on the client side.
* Use `createToolsFromClientSchemas()` to convert these to AI SDK tool format.
*/
clientTools?: ClientToolSchema[];
};

/**
* Converts client tool schemas to AI SDK tool format.
*
* These tools have no `execute` function - when the AI model calls them,
* the tool call is sent back to the client for execution.
*
* @param clientTools - Array of tool schemas from the client
* @returns Record of AI SDK tools that can be spread into your tools object
*/
export function createToolsFromClientSchemas(
clientTools?: ClientToolSchema[]
): ToolSet {
if (!clientTools || clientTools.length === 0) {
return {};
}

// Check for duplicate tool names
const seenNames = new Set<string>();
for (const t of clientTools) {
if (seenNames.has(t.name)) {
console.warn(
`[createToolsFromClientSchemas] Duplicate tool name "${t.name}" found. Later definitions will override earlier ones.`
);
}
seenNames.add(t.name);
}

return Object.fromEntries(
clientTools.map((t) => [
t.name,
tool({
description: t.description ?? "",
inputSchema: jsonSchema(t.parameters ?? { type: "object" })
// No execute function = tool call is sent back to client
})
])
);
}

/** Number of chunks to buffer before flushing to SQLite */
const CHUNK_BUFFER_SIZE = 10;
/** Maximum buffer size to prevent memory issues on rapid reconnections */
Expand Down Expand Up @@ -183,7 +250,11 @@ export class AIChatAgent<Env = unknown, State = unknown> extends Agent<
data.init.method === "POST"
) {
const { body } = data.init;
const { messages } = JSON.parse(body as string);
const parsed = JSON.parse(body as string);
const { messages, clientTools } = parsed as {
messages: ChatMessage[];
clientTools?: ClientToolSchema[];
};

// Automatically transform any incoming messages
const transformedMessages = autoTransformMessages(messages);
Expand Down Expand Up @@ -238,7 +309,10 @@ export class AIChatAgent<Env = unknown, State = unknown> extends Agent<
this.ctx
);
},
abortSignal ? { abortSignal } : undefined
{
abortSignal,
clientTools
}
);

if (response) {
Expand Down Expand Up @@ -596,14 +670,14 @@ export class AIChatAgent<Env = unknown, State = unknown> extends Agent<
/**
* Handle incoming chat messages and generate a response
* @param onFinish Callback to be called when the response is finished
* @param options.signal A signal to pass to any child requests which can be used to cancel them
* @param options Options including abort signal and client-defined tools
* @returns Response to send to the client or undefined
*/
async onChatMessage(
// biome-ignore lint/correctness/noUnusedFunctionParameters: overridden later
onFinish: StreamTextOnFinishCallback<ToolSet>,
// biome-ignore lint/correctness/noUnusedFunctionParameters: overridden later
options?: { abortSignal: AbortSignal | undefined }
options?: OnChatMessageOptions
): Promise<Response | undefined> {
throw new Error(
"recieved a chat message, override onChatMessage and return a Response to send to the client"
Expand Down
Loading
Loading