Wire a Cloudflare Agents backend
useAISDKRuntime over useAgentChat sends no model context by itself. The browser sends the system text in the message body and the tools through useAgentChat’s tools option, and the agent hands both to the model.
In the browser
Section titled “In the browser”- Give
useAgentChatabodyfunction. It returns the runtime’s system text at send time, through a ref set afteruseAISDKRuntimereturns. - Give
useAgentChatatoolsobject that mirrors the runtime’s model context tools. Keep one object and refill it in place whenever the model context changes, throughruntime.thread.unstable_on("modelContextUpdate", …). - Cast at the
useAISDKRuntimecall.useAgentChat’s return type differs fromuseChat’s onaddToolOutput. - Pass the runtime to
AssistedProvider.
"use client";
import { type ReactNode, useEffect, useRef, useState } from "react";import { useAgent } from "agents/react";import { type AITool, useAgentChat } from "@cloudflare/ai-chat/react";import { useAISDKRuntime } from "@assistant-ui/ai-sdk";import type { AssistantRuntime } from "@assistant-ui/react";import { AssistedProvider } from "@super-assist/react";import { toToolsJSONSchema } from "assistant-stream";import { AssistedPanel } from "@/components/assisted-panel";
export const Assistant = ({ children }: { children: ReactNode }) => { const agent = useAgent({ agent: "Chat", name: "default", host: process.env.NEXT_PUBLIC_AGENT_HOST!, }); const runtimeRef = useRef<AssistantRuntime>(null); const [tools] = useState<Record<string, AITool>>({}); const chat = useAgentChat({ agent, body: () => ({ system: runtimeRef.current?.thread.getModelContext().system }), tools, }); const runtime = useAISDKRuntime(chat as Parameters<typeof useAISDKRuntime>[0]); runtimeRef.current = runtime; useEffect(() => { const sync = () => { for (const name of Object.keys(tools)) delete tools[name]; Object.assign(tools, toolsOf(runtime)); }; sync(); return runtime.thread.unstable_on("modelContextUpdate", sync); }, [runtime, tools]);
return ( <AssistedProvider runtime={runtime}> <main className="min-h-dvh p-6">{children}</main> <AssistedPanel mode="floating" /> </AssistedProvider> );};
const toolsOf = (runtime: AssistantRuntime) => Object.fromEntries( Object.entries(toToolsJSONSchema(runtime.thread.getModelContext().tools)).map( ([name, tool]) => [name, { ...tool, execute: ranByAssistantUi }], ), );
const ranByAssistantUi = () => undefined;useAgentChat reads tools each time it sends a message or a tool result, and sends the schemas as clientTools. Mind three details:
- Every tool needs an
execute.useAgentChatsends only the schemas of tools that have one. assistant-ui runs the tools, so the function does nothing. Leaveexperimental_automaticToolResolutionoff, souseAgentChatnever calls it. - Refill the object, do not replace it. A tool result goes out as soon as the tool returns, before React renders
Assistantagain. A new object passed through state would arrive one render late, and the result ofgo_to_pagewould miss part of the new page’s tools. - Install
assistant-streamas a direct dependency fortoToolsJSONSchema. Leave outsendAutomaticallyWhen:useAgentChatcontinues the turn after tool results on its own.
In the Worker
Section titled “In the Worker”In onChatMessage, pass system from the body to the model and build the tools from options.clientTools with createToolsFromClientSchemas. From examples/agent/src/chat.ts:
type Body = { system?: string };
export class Chat extends AIChatAgent<Env> { async onChatMessage(_onFinish: unknown, options?: OnChatMessageOptions) { const { system } = (options?.body ?? {}) as Body; const openai = createOpenAI({ apiKey: this.env.OPENAI_API_KEY }); const result = streamText({ model: openai.responses("gpt-6-luna"), system, messages: await convertToModelMessages(this.messages), tools: createToolsFromClientSchemas(options?.clientTools), }); return result.toUIMessageStreamResponse(); }}The tools carry no server execute. The browser runs them and sends each result back over the socket, together with the tools as they are at that moment.
Know the limit
Section titled “Know the limit”A continuation after a tool result reuses the system text of the user’s last message. When a tool opens another page mid-turn, the model gets the new page’s tools on its next step, while the page section of the system text still names the old page. go_to_page and every press answer with the new page’s note, and the universal instructions tell the model that such a note is newer than the page section.
For several threads per user, see Bring your own thread list. For why the backend works this way, see What the backend must do.