diff --git a/src/content/docs/agents/api-reference/agents-api.mdx b/src/content/docs/agents/api-reference/agents-api.mdx index 4b2ac2b04ec19b8..40cdd084448bf71 100644 --- a/src/content/docs/agents/api-reference/agents-api.mdx +++ b/src/content/docs/agents/api-reference/agents-api.mdx @@ -18,7 +18,7 @@ This page provides an overview of the Agent SDK API, including the `Agent` class The Agents SDK exposes two main APIs: - The server-side `Agent` class. An Agent encapsulates all of the logic for an Agent, including how clients can connect to it, how it stores state, the methods it exposes, how to call AI models, and any error handling. -- The client-side `AgentClient` class, which allows you to connect to an Agent instance from a client-side application. The client APIs also include React hooks, including `useAgent` and `useAgentChat`, and allow you to automatically synchronize state between each unique Agent (running server-side) and your client applications. +- The client-side `AgentClient` class, which allows you to connect to an Agent instance from a client-side application. The client APIs also include React hooks, including `useAgent` and `useChat`, and allow you to automatically synchronize state between each unique Agent (running server-side) and your client applications. :::note @@ -635,7 +635,7 @@ For complete MCP client API documentation, including OAuth configuration and adv The Agents SDK provides a set of client APIs for interacting with Agents from client-side JavaScript code, including: -- React hooks, including `useAgent` and `useAgentChat`, for connecting to Agents from client applications. +- React hooks, including `useAgent` and `useChat`, for connecting to Agents from client applications. - Client-side [state syncing](/agents/api-reference/store-and-sync-state/) that allows you to subscribe to state updates between the Agent and any connected client(s) when calling `this.setState` within your Agent's code. - The ability to call remote methods (Remote Procedure Calls; RPC) on the Agent from client-side JavaScript code using the `@callable` method decorator. @@ -804,7 +804,7 @@ function useAgent( The Agents SDK exposes an `AIChatAgent` class that extends the `Agent` class and exposes an `onChatMessage` method that simplifies building interactive chat agents. -You can combine this with the `useAgentChat` React hook from the `agents/ai-react` package to manage chat state and messages between a user and your Agent(s). +You can combine this with the `useChat` React hook from the `agents/react` package to manage chat state and messages between a user and your Agent(s). #### AIChatAgent @@ -886,7 +886,7 @@ The `AIChatAgent` class provides **automatic resumable streaming** out of the bo ##### How it works -When you use `AIChatAgent` with `useAgentChat`: +When you use `AIChatAgent` with `useChat`: 1. **During streaming**: All chunks are automatically persisted to SQLite 2. **On disconnect**: The stream continues server-side, buffering chunks @@ -923,9 +923,9 @@ If you don't want automatic resume (for example, for short responses), disable i ```tsx -const { messages } = useAgentChat({ - agent, - resume: false, // Disable automatic stream resumption +const { messages } = useChat({ + agent: "my-agent", + // Stream resumption is enabled by default }); ``` @@ -933,134 +933,186 @@ const { messages } = useAgentChat({ ### Chat Agent React API -#### useAgentChat +#### useChat -React hook for building AI chat interfaces using an Agent. +Unified React hook for building AI chat interfaces with human-in-the-loop tool approval. This hook replaces the previous two-hook pattern (`useAgent` + `useAgentChat`) with a single, declarative API. ```ts -import { useAgentChat } from "agents/ai-react"; -import { useAgent } from "agents/react"; -import type { Message } from "ai"; +import { useChat } from "agents/react"; +import type { UIMessage } from "ai"; +import type { PartySocket } from "partysocket"; + +// Tool definition for client-side tools +type Tool = { + // Tool description for the LLM + description?: string; + // Client-side execution function (optional) + execute?: (input: TInput) => TOutput | Promise; + // Whether to require human approval before execution + confirm?: boolean; +}; + +// Pending tool call awaiting user approval +type PendingToolCall = { + toolCallId: string; + toolName: string; + input: unknown; + messageId: string; +}; -// Options for the useAgentChat hook -type UseAgentChatOptions = Omit< - Parameters[0] & { - // Agent connection from useAgent - agent: ReturnType; - }, - "fetch" ->; +// Options for the useChat hook +type UseChatOptions = { + // Name of the agent to connect to + agent: string; + // Name of the specific Agent instance (optional) + name?: string; + // Client-side tool definitions with confirmation behavior + tools?: Record; + // Error handler + onError?: (error: Error) => void; + // State update callback + onStateUpdate?: (state: unknown, source: "server" | "client") => void; +}; // React hook for building AI chat interfaces using an Agent -function useAgentChat(options: UseAgentChatOptions): { +function useChat(options: UseChatOptions): { // Current chat messages - messages: Message[]; + messages: UIMessage[]; + // Send a message to the agent + sendMessage: (message: UIMessage) => void; // Set messages and synchronize with the Agent - setMessages: (messages: Message[]) => void; + setMessages: (messages: UIMessage[]) => void; // Clear chat history on both client and Agent clearHistory: () => void; - // Append a new message to the conversation - append: ( - message: Message, - chatRequestOptions?: any, - ) => Promise; - // Reload the last user message - reload: (chatRequestOptions?: any) => Promise; - // Stop the AI response generation - stop: () => void; - // Current input text - input: string; - // Set the input text - setInput: React.Dispatch>; - // Handle input changes - handleInputChange: ( - e: React.ChangeEvent, - ) => void; - // Submit the current input - handleSubmit: ( - event?: { preventDefault?: () => void }, - chatRequestOptions?: any, - ) => void; - // Additional metadata - metadata?: Object; + // Tool calls awaiting approval + pendingToolCalls: PendingToolCall[]; + // Approve a tool execution + approve: (toolCallId: string) => Promise; + // Deny a tool execution with optional reason + deny: (toolCallId: string, reason?: string) => Promise; // Whether a response is currently being generated isLoading: boolean; - // Current status of the chat - status: "submitted" | "streaming" | "ready" | "error"; - // Tool data from the AI response - data?: any[]; - // Set tool data - setData: ( - data: any[] | undefined | ((data: any[] | undefined) => any[] | undefined), - ) => void; - // Unique ID for the chat - id: string; - // Add a tool result for a specific tool call - addToolResult: ({ - toolCallId, - result, - }: { - toolCallId: string; - result: any; - }) => void; + // Unique session ID + sessionId: string; + // WebSocket connection to the agent + connection: PartySocket; // Current error if any error: Error | undefined; }; ``` +:::note +The `useChat` hook simplifies the human-in-the-loop pattern by: +- Combining agent connection and chat management in a single hook +- Automatically detecting tools that require confirmation based on the `confirm` property +- Providing a simple `approve(id)` and `deny(id)` API instead of manual `addToolResult()` calls +- Exposing `pendingToolCalls` to track which tools are awaiting approval +- Handling both client-side and server-side tool execution automatically +::: + ```tsx -// Example of using useAgentChat in a React component -import { useAgentChat } from "agents/ai-react"; -import { useAgent } from "agents/react"; -import { useState } from "react"; +// Example of using useChat in a React component +import { useChat, type PendingToolCall } from "agents/react"; +import { getToolName, isToolUIPart } from "ai"; function ChatInterface() { - // Connect to the chat agent - const agentConnection = useAgent({ - agent: "customer-support", - name: "session-12345", - }); - - // Use the useAgentChat hook with the agent connection const { messages, - input, - handleInputChange, - handleSubmit, + sendMessage, + pendingToolCalls, + approve, + deny, + clearHistory, isLoading, error, - clearHistory, - } = useAgentChat({ - agent: agentConnection, - initialMessages: [ - { role: "system", content: "You're chatting with our AI assistant." }, - { role: "assistant", content: "Hello! How can I help you today?" }, - ], + sessionId, + } = useChat({ + agent: "customer-support", + name: "session-12345", + tools: { + getWeather: { + description: "Get weather for a city", + confirm: true, // Requires approval + }, + getCurrentTime: { + description: "Get current time", + execute: async () => new Date().toISOString(), + // No confirm = auto-executes (default when execute is present) + }, + }, + onError: (error) => console.error("Chat error:", error), }); return (
+
Session: {sessionId}
+
- {messages.map((message, i) => ( -
- {message.role === "user" ? "👤" : "🤖"} {message.content} + {messages.map((message) => ( +
+ {message.parts?.map((part, i) => { + if (part.type === "text") { + return
{part.text}
; + } + + if (isToolUIPart(part)) { + const toolCallId = part.toolCallId; + const toolName = getToolName(part); + + // Check if this tool is pending approval + const pending = pendingToolCalls.find( + (p: PendingToolCall) => p.toolCallId === toolCallId, + ); + + if (pending) { + return ( +
+

Approve {toolName}?

+ + +
+ ); + } + + // Show tool results + if (part.state === "output-available") { + return ( +
+ {toolName}: {JSON.stringify(part.output)} +
+ ); + } + } + return null; + })}
))} - {isLoading &&
AI is typing...
} + {isLoading &&
AI is responding...
} {error &&
Error: {error.message}
}
-
+ { + e.preventDefault(); + const input = e.currentTarget.message.value; + if (input.trim()) { + sendMessage({ + role: "user", + parts: [{ type: "text", text: input }], + }); + e.currentTarget.reset(); + } + }} + > 0} /> - + Session: {sessionId || "Connecting..."} +
+
{messages?.map((m: Message) => (
@@ -377,11 +506,11 @@ export default function Chat() { const toolCallId = part.toolCallId; const toolName = getToolName(part); - // Show tool results for automatic tools + // Show tool results if (part.state === "output-available") { return (
- {toolName}{" "} + {toolName}{" "} returned:{" "} {JSON.stringify(part.output, null, 2)} @@ -390,14 +519,18 @@ export default function Chat() { ); } - // Render confirmation UI for tools requiring approval + // Show pending tool calls if (part.state === "input-available") { - const tool = clientTools[toolName]; + // Check if this tool is in pendingToolCalls + const pending = pendingToolCalls.find( + (p: PendingToolCall) => p.toolCallId === toolCallId + ); - if (!toolsRequiringConfirmation.includes(toolName)) { + // Not in pending = auto-executing + if (!pending) { return (
- {toolName}{" "} + {toolName}{" "} executing...
); @@ -405,8 +538,8 @@ export default function Chat() { return (
- Run {toolName} with - args:{" "} + Run {toolName}{" "} + with args:{" "} {JSON.stringify(part.input)} @@ -414,34 +547,16 @@ export default function Chat() {
@@ -458,18 +573,26 @@ export default function Chat() { setInput(e.target.value)} /> -
+ ); } ``` +:::note +The `useChat` hook simplifies the human-in-the-loop pattern by: +- Automatically detecting tools that require confirmation based on the `confirm` property +- Providing a simple `approve(id)` and `deny(id)` API instead of manual `addToolResult()` calls +- Exposing `pendingToolCalls` to track which tools are awaiting approval +- Handling both client-side and server-side tool execution automatically +::: + ## 7. Test locally Start your development server: @@ -485,8 +608,8 @@ Your agent is now running at `http://localhost:8787`. 1. Open `http://localhost:8787` in your browser. 2. Ask the agent about the weather: "What's the weather in San Francisco?" 3. The agent will attempt to call the `getWeatherInformation` tool. -4. You will see an approval prompt with **Approve** and **Reject** buttons. -5. Click **Approve** to allow the tool execution, or **Reject** to deny it. +4. You will see an approval prompt with **Yes** and **No** buttons. +5. Click **Yes** to allow the tool execution, or **No** to deny it. 6. The agent will respond with the result or acknowledge the rejection. ### Test automatic tools @@ -521,18 +644,34 @@ https://human-in-the-loop.your-account.workers.dev The human-in-the-loop pattern works by intercepting tool calls before execution: 1. **Tool invocation**: The AI decides to call a tool based on user input. -2. **Approval check**: The system checks if the tool requires human confirmation. -3. **Confirmation prompt**: If approval is required, the UI displays the tool name and arguments with Approve/Reject buttons. -4. **User decision**: The user reviews the action and makes a decision. -5. **Execution or rejection**: Based on the user's choice, the tool either executes or returns a rejection message. +2. **Approval check**: The `useChat` hook checks if the tool has `confirm: true`. +3. **Confirmation prompt**: If approval is required, the UI displays the tool name and arguments with Yes/No buttons. +4. **User decision**: The user reviews the action by clicking `approve(toolCallId)` or `deny(toolCallId)`. +5. **Execution or rejection**: Based on the user's choice, the tool either executes (client-side with `execute`, or server-side without) or returns a rejection message using the `TOOL_CONFIRMATION` protocol constants. -### Message streaming with confirmations +### Simplified API with useChat -The agent uses the Vercel AI SDK's streaming capabilities: +The new `useChat` hook replaces the previous two-hook pattern (`useAgent` + `useAgentChat`) with a single, declarative API: -- `createUIMessageStream` creates a stream for processing tool confirmations. -- `streamText` handles normal AI responses with tool calls. -- The `hasToolConfirmation` function detects when a message contains a tool confirmation response. +```tsx +// Old pattern (deprecated) +const agent = useAgent({ agent: "my-agent" }); +const { messages, sendMessage, addToolResult } = useAgentChat({ + agent, + tools: clientTools, + toolsRequiringConfirmation: ["toolA", "toolB"] +}); +// Manual approval handling +addToolResult({ tool: "toolA", output: "Yes, confirmed.", toolCallId }); + +// New pattern (current) +const { messages, sendMessage, approve, deny, pendingToolCalls } = useChat({ + agent: "my-agent", + tools: clientTools // confirm property defines approval behavior +}); +// Simple approval handling +approve(toolCallId); +``` ### State persistence @@ -546,38 +685,38 @@ Your agent uses [Durable Objects](/durable-objects/) to maintain conversation st ### Add more tools requiring confirmation -Add new tools to the `toolsRequiringConfirmation` array in `src/utils.ts`: +Add the `confirm: true` property to tools in `src/tools.ts`: ```ts -export const toolsRequiringConfirmation = [ - "getLocalTime", - "getWeatherInformation", - "sendEmail", // Add your new tools here - "makePurchase" -]; +export const clientTools: Record = { + sendEmail: { + description: "Send an email to a recipient", + confirm: true // Requires approval + }, + makePurchase: { + description: "Make a purchase", + execute: executePurchase, + confirm: true // Requires approval even with execute function + } +}; ``` ### Implement custom tool handlers -For server-side tools requiring confirmation, add execute functions in your agent: +For server-side tools requiring confirmation, add execute functions in `src/utils.ts`: ```ts -if (hasToolConfirmation(lastMessage)) { - const stream = createUIMessageStream({ - execute: async ({ writer }) => { - await processToolCalls( - { writer, messages: this.messages, tools }, - { - getWeatherInformation, - sendEmail: async ({ to, subject, body }) => { - // Your email sending logic - return `Email sent to ${to}`; - } - } - ); +export async function processToolCalls( + { messages, tools }, + { + getWeatherInformation, + sendEmail: async ({ to, subject, body }) => { + // Your email sending logic + return `Email sent to ${to}`; } - }); - return createUIMessageStreamResponse({ stream }); + } +) { + // Processing logic } ``` @@ -586,7 +725,7 @@ if (hasToolConfirmation(lastMessage)) { Enhance the confirmation interface with more context: ```tsx -if (part.state === "input-available") { +if (pending) { return (

Action Required

@@ -595,11 +734,11 @@ if (part.state === "input-available") {

{JSON.stringify(part.input, null, 2)}
- -
@@ -607,6 +746,18 @@ if (part.state === "input-available") { } ``` +### Custom denial messages + +Provide custom denial reasons: + +```tsx + +``` + ### Use different LLM providers Replace OpenAI with [Workers AI](/workers-ai/): @@ -622,7 +773,7 @@ export class HumanInTheLoop extends AIChatAgent { messages: convertToModelMessages(this.messages), model: workersai("@cf/meta/llama-3-8b-instruct"), onFinish, - tools + tools: serverTools }); return result.toUIMessageStreamResponse(); @@ -634,9 +785,10 @@ export class HumanInTheLoop extends AIChatAgent { - **Define clear approval workflows** — Only require confirmation for actions with meaningful consequences (payments, emails, data changes). - **Provide detailed context** — Show users exactly what the tool will do, including all arguments. +- **Use declarative tool configuration** — Set `confirm: true` on tools requiring approval instead of maintaining separate lists. - **Implement timeouts** — Consider auto-rejecting tools after a reasonable timeout period. - **Handle connection drops** — Ensure the UI can recover if the WebSocket connection is interrupted. -- **Log all decisions** — Track approval/rejection decisions for audit trails. +- **Log all decisions** — Track approval and rejection decisions for audit trails. - **Graceful degradation** — Provide fallback behavior if tools are rejected. ## Next steps