React
August 28, 2026 · View on GitHub
Idiomatic React bindings: a provider, hooks for chat/messages/conversations, declarative helpers for functions, navigation and context, plus ready-made UI components.
npm install @useago/sdk react react-dom
import {
AgoProvider,
useChat,
useAgoFunction,
ChatWidget,
} from "@useago/sdk/react";
React (
>=17) is an optional peer dependency. The@useago/sdk/reactentry only loads if React is installed.
1. Wrap your app in <AgoProvider>
The provider creates one AgoClient and shares it with every hook/component below.
import { AgoProvider } from "@useago/sdk/react";
function Root() {
return (
<AgoProvider baseUrl="https://YOUR-DOMAIN.useago.com" agent="support-bot">
<App />
</AgoProvider>
);
}
Declarative config
The provider can wire app-wide tools, pre-built helpers and page context for you:
import { AgoProvider } from "@useago/sdk/react";
import { lookupOrder, cancelOrder } from "./agoFunctions";
<AgoProvider
baseUrl="https://YOUR-DOMAIN.useago.com"
tools={[lookupOrder, cancelOrder]} // registered app-wide
helpers={{
copyToClipboard: true, // use the built-in handler
showToast: (args) => toast(args.message as string), // custom handler
}}
pageContext="auto" // auto-capture URL + title
>
<App />
</AgoProvider>
Bring your own client (e.g. tests)
<AgoProvider client={myClient}>
<App />
</AgoProvider>
2. The fastest UI: <ChatWidget>
A complete, styled chat panel. Drop it anywhere under the provider.
import { ChatWidget } from "@useago/sdk/react";
function Support() {
return (
<ChatWidget
title="Support"
welcomeMessage="Hi! How can I help?"
placeholder="Ask anything…"
allowFiles
height={600}
logoUrl="/logo.svg"
showAgentName
onMessageSent={(text) => console.log("sent", text)}
onMessageReceived={(m) => console.log("received", m.content)}
/>
);
}
| Prop | Type | Default |
|---|---|---|
client? | AgoClient | from provider |
conversationId? | string | — |
title? | string | "Chat" |
welcomeMessage? | string | greeting |
placeholder? | string | "Type a message..." |
allowFiles? | boolean | false |
allowStop? | boolean | true (see Stop button) |
height? | string | number | 500 |
logoUrl? | string | — |
showAgentName? | boolean | false |
forms? | Array<CreateFormCollectorOptions | LoadFormCollectorOptions> | — |
onFollowUpClick? | ((reply) => void) | false | sends the reply |
className? | string | "" |
onMessageSent? | (content) => void | — |
onMessageReceived? | ({ id, content }) => void | — |
Showing what the agent is doing
While the agent works, the widget shows a status row naming the client function it is running rather than a bare typing indicator. That gap is widest during a pause/resume loop: the agent calls a function, the page fetches, and the per-message dots have already gone away because the first tokens arrived.
<ChatWidget
functionLabels={{
lookupInvoices: "Looking up invoices",
setPageState: "Updating the page",
}}
/>
A slow sheen travels across the label while the agent works, and each new step
fades in as its own element, so a long step still looks alive and a change of
step reads as a continuation. Re-skin it with --ago-activity-color,
--ago-activity-sheen and --ago-activity-bg; the animation stops under
prefers-reduced-motion.
The label renders simple inline Markdown (**bold**, *italic*, `code`)
and shows no animated dots beside it: the text is the progress indicator. While
the row is up, the not-yet-written answer bubble drops its dots too, so you
never get both at once. Building your own list? <Message showStreamingDots={false}>
does the same thing.
The row keeps each step on screen until another one replaces it or the turn
ends, rather than clearing when the call returns: a function that takes 40 ms
would otherwise flash and read as nothing. The agent's reasoning steps appear
in the same row (showReasoning={false} to leave them out), so it does not go
blank between two calls.
Unmapped function names fall back to a prettified version of the name, so the
row works without configuring anything. Pass a function for full control
((name, args) => string), or functionLabels={false} to keep the plain
indicator. Building your own UI? useAgoActivity is the same
data source, with approvals and server-side tool calls included.
Stop button
While the agent answers, <ChatWidget> turns its send button into a Stop
button. Clicking it closes the stream and tells the backend to stop generating,
so the partial answer stays in the transcript as CANCELED instead of the agent
finishing in the background. Pass allowStop={false} to keep the send button
disabled while answering instead.
Building your own input? <ChatInput> takes an onStop prop: when it is set and
disabled is true (the agent is answering), the button becomes an enabled Stop
button.
import { ChatInput, useMessages } from "@useago/sdk/react";
function Composer() {
const { sendMessage, stop, isLoading } = useMessages();
return (
<ChatInput
onSend={(text, files) => void sendMessage(text, files)}
onStop={() => void stop()}
disabled={isLoading}
/>
);
}
Suggested replies
When the agent returns follow-up suggestions, the widget renders them as
buttons below the message. By default clicking one sends it as the next user
message. Pass onFollowUpClick to handle clicks yourself, or
onFollowUpClick={false} to render them non-interactive.
Feedback ("this answer doesn't work")
feedback puts a thumbs up / thumbs down under every finished answer. On a
thumbs-down, a panel asks what went wrong (four reason chips and a comment box).
<ChatWidget feedback />
The thumb is sent the moment it is clicked, so the signal survives a user who
ignores the panel; sending the panel then files the detailed report, which is
what shows up in the AGO feedback dashboard. Pass the <MessageFeedback> props
to translate the strings or watch what goes out:
<ChatWidget
feedback={{
labels: { notHelpful: "Pas utile", reasons: { inaccurate: "Inexact" } },
onSubmit: ({ rating, reasons }) => track(rating, reasons),
}}
/>
Building your own message list? Drop the row in yourself, or drive it by hand:
import { MessageFeedback, useFeedback } from "@useago/sdk/react";
<MessageFeedback messageId={message.id} />;
const { ratings, submitFeedback, submitConversationFeedback } = useFeedback();
await submitFeedback(message.id, "negative", { reasons: ["inaccurate"] });
await submitConversationFeedback(conversationId, "negative", {
comment: "It never answered my question.",
});
Conversational forms (form creator)
Pass forms to let the agent collect and submit a structured form during the
chat. Each entry is installed as a form collector (see createFormCollector /
useFormCollector): the agent gets update_<name> / submit_<name> functions
plus two context entries. A stable one carries the full form schema (including
the requiredWhen conditions for conditional fields) once per conversation in
the cacheable part of the prompt; a small per-message one carries the data
collected so far and which required fields are still missing.
<ChatWidget
title="Book a demo"
welcomeMessage="Hi! Tell me a bit about your team and I'll set up a demo."
forms={[
{
name: "demo_request",
description: "A request to book a product demo.",
schema: {
type: "object",
properties: {
name: { type: "string" },
email: { type: "string" },
company: { type: "string" },
teamSize: { type: "number" },
},
required: ["name", "email", "company"],
},
// Relay to a server-configured destination (URL + secret stay server-side):
submit: { via: "backend" },
},
]}
/>
Keep the
formsarray stable (declare it outside render or memoize it); the collectors are reinstalled when a form's name, schema, description, or submit target changes. For full control over the live form state (e.g. a side panel that updates as fields fill in), use theuseFormCollectorhook directly instead of theformsprop.
To keep the schema in the backend instead of inline, pass an entry with just a
name ({ name: "demo_request" }): the widget fetches the definition via
loadFormCollector. The same works with the hook: useFormCollector({ name })
fetches the definition and exposes loading until it resolves.
Building your own UI? The widget is composed from exported building blocks you
can reuse: <Message> (accepts onFollowUpClick), <ChatInput> and
<Markdown content={...} /> (GitHub-flavored markdown, zero external CSS).
3. Custom UI with useChat
All-in-one state for a custom chat interface; composes useMessages +
useConversation.
import { useChat } from "@useago/sdk/react";
function Chat() {
const {
messages,
sendMessage,
stop,
isLoading,
error,
conversations,
selectConversation,
startNewConversation,
} = useChat();
return (
<div>
{messages.map((m) => (
<p key={m.id}><b>{m.role}:</b> {m.content}</p>
))}
{isLoading ? (
<button onClick={() => void stop()}>Stop</button>
) : (
<button onClick={() => sendMessage("Hello!")}>Send</button>
)}
{error && <p role="alert">{error.message}</p>}
</div>
);
}
messages updates token-by-token as the reply streams in (optimistic user
message included). sendMessage(content, files?) returns the final message or
null on error.
stop() interrupts the answer being generated: the stream closes and the
backend is told to stop, the partial text stays in messages with status
CANCELED, and isLoading goes back to false. It is a no-op when nothing is
generating. See Stop the answer.
Show the source docs the agent retrieved
Each assistant message carries the knowledge sources it used in
m.sources (an AgoSource[], each { id, title, url? }). Render them as links
to display the URL of every retrieved doc:
function Chat() {
const { messages, sendMessage, isLoading } = useChat();
return (
<div>
{messages.map((m) => (
<div key={m.id}>
<p><b>{m.role}:</b> {m.content}</p>
{m.sources?.length ? (
<ul>
{m.sources.map((s) => (
<li key={s.id}>
{s.url ? (
<a href={s.url} target="_blank" rel="noreferrer">
{s.title || s.url}
</a>
) : (
s.title
)}
</li>
))}
</ul>
) : null}
</div>
))}
<button onClick={() => sendMessage("Hello!")} disabled={isLoading}>
Send
</button>
</div>
);
}
Finer-grained hooks
| Hook | Returns |
|---|---|
useMessages({ conversationId? }) | { messages, isLoading, error, sendMessage, stop, clearMessages, conversationId } |
useConversation({ autoLoad? }) | { conversations, currentConversation, isLoading, error, selectConversation, startNewConversation, refreshConversations } |
useChat(options) | both of the above combined |
useFeedback() | { ratings, submitFeedback, submitConversationFeedback, isSubmitting, error } |
All hooks read the client from context by default; pass { client } to override.
Need the raw client?
import { useAgoClient } from "@useago/sdk/react";
const client = useAgoClient(); // throws if outside <AgoProvider>
// or useOptionalAgoClient() → AgoClient | null
4. Let the agent call your code: useAgoFunction
Registers a client-side function on mount and cleans it up on unmount.
import { useAgoFunction } from "@useago/sdk/react";
function OrdersPanel() {
useAgoFunction({
name: "lookupOrder",
description: "Look up an order by ID",
parameters: {
type: "object",
properties: { id: { type: "string", description: "Order ID" } },
required: ["id"],
},
handler: async (args) => fetchOrder(args.id as string),
});
return <OrdersTable />;
}
Reuse a definition created with defineFunction:
import { defineFunction } from "@useago/sdk";
const lookupOrder = defineFunction({ name: "lookupOrder", /* … */ });
useAgoFunction(lookupOrder);
See Client-side functions for schema details and the catalogue of pre-built helpers.
5. Let the agent navigate: useAgoNavigation
Wire AGO into your router. Works great with react-router's useNavigate.
import { useAgoNavigation } from "@useago/sdk/react";
import { useNavigate } from "react-router-dom";
function AppShell() {
const navigate = useNavigate();
useAgoNavigation(navigate, [
{ name: "dashboard", path: "/dashboard", description: "Main dashboard" },
{ name: "settings", path: "/settings", description: "User settings" },
]);
return <Outlet />;
}
Define each route once
React Router's <Route> has no description prop (it ignores unknown props, and
TypeScript rejects them), so the path and the agent description live in two
different places. Keep one route table and read both the router and
useAgoNavigation off it: add or rename a page in a single spot and they stay in
sync.
import { Routes, Route, useNavigate } from "react-router-dom";
import { useAgoNavigation } from "@useago/sdk/react";
// One source of truth: path + the description the agent reads to pick the page.
const ROUTES = {
dashboard: { name: "dashboard", path: "/dashboard", description: "KPIs and recent activity" },
invoices: { name: "invoices", path: "/invoices", description: "List and download invoices" },
settings: { name: "settings", path: "/settings", description: "Account, billing and team" },
} as const;
function AppShell() {
const navigate = useNavigate();
// The agent gets every route's path + description from the same object.
useAgoNavigation(navigate, Object.values(ROUTES));
return (
<Routes>
<Route path={ROUTES.dashboard.path} element={<Dashboard />} />
<Route path={ROUTES.invoices.path} element={<Invoices />} />
<Route path={ROUTES.settings.path} element={<Settings />} />
</Routes>
);
}
A detail page with a param (/invoices/:id) can be registered as-is. The
placeholder becomes a top-level id argument of navigateToPage, so "open
invoice 42" navigates to /invoices/42. One route covers every record:
useAgoNavigation(navigate, [
...Object.values(ROUTES),
{ name: "invoiceDetail", path: "/invoices/:id", description: "One invoice's detail page" },
]);
When the agent should pick a record by meaning instead of by id ("the invoice
for Acme"), it needs to know which ids exist. For a small, stable set, keep the
single parameterized route and list the ids in its description, derived from
your data so they stay in sync (examples/glacier
does this for its origin pages). For larger or user-specific sets, register
concrete paths derived from your data instead:
const invoiceRoutes = invoices.map((inv) => ({
name: `invoice-${inv.id}`,
path: `/invoices/${inv.id}`,
description: `Invoice ${inv.number} for ${inv.customer}`,
}));
useAgoNavigation(navigate, [...Object.values(ROUTES), ...invoiceRoutes]);
5b. Let the agent change the page: useAgoPageState
The mirror of useAgoNavigation. Instead of moving the user to another page,
let the agent change the state of the page they're on (filters, sort, view
mode…) and read the current state back.
import { useAgoPageState } from "@useago/sdk/react";
function InvoiceList() {
const [status, setStatus] = useState("all");
const [sort, setSort] = useState("newest");
useAgoPageState([
{
name: "statusFilter",
description: "Filter the list by invoice status",
schema: { type: "string", enum: ["all", "paid", "overdue"] },
get: () => status,
set: setStatus,
},
{
name: "sort",
description: "Sort order of the list",
schema: { type: "string", enum: ["newest", "oldest"] },
get: () => sort,
set: setSort,
},
]);
return /* … */;
}
Each control becomes one optional property of a single synthesized
setPageState function, so the agent sets only what the user asked for. Every
control's current get() value is sent as context, so the agent knows the
state before it changes it. Pass { functionName } to rename the function.
The hook re-registers when the client, function name, or a control's
model-visible signature changes (name, description, schema, clearable).
set/get closures can change every render without churn. The glacier
example dogfoods this: the ice cream's cone, scoops and toppings are
page-state controls.
The SDK validates each field before calling set(): type and enum mismatches
are rejected, unknown control names are rejected, and a value equal to the
current get() is skipped. null, undefined, and "" are silently dropped
(unless the control is a clearable string). The result envelope reports each
field in applied, unchanged, or rejected. See
Page state shortcut
for the full envelope shape, clearable, and tagged setter outcomes.
Give the agent back what the page shows
On its own, setPageState tells the agent what happened to its arguments, not
what appeared on screen. Add a data source and the rows come back as the result
of its own call, in the same turn.
const { data: invoices, isFetching } = useQuery({
queryKey: ["invoices", status, sort],
queryFn: fetchInvoices,
});
useAgoPageState(controls, {
data: {
description: "The invoices matching the current filters.",
get: () => invoices ?? [],
isLoading: () => isFetching, // isFetching, not isLoading
},
});
setPageState then returns { success, applied, unchanged, rejected?, data }, and a read-only
readPageData function is registered alongside it for "what's on screen?"
questions.
Better still, return a promise from get() and the SDK awaits it instead of
polling a flag, which removes the timing guess entirely:
useAgoPageState(controls, {
data: {
description: "The invoices matching the current filters.",
get: () => queryClient.ensureQueryData({ queryKey: ["invoices", status], queryFn: fetchInvoices }),
},
});
If you stay on isLoading, pass isFetching: TanStack Query's isLoading is
only true on the very first load, so a background refetch would read as idle.
Note the poll only gives the flag ~100 ms to go up, so a debounced fetch needs
the promise form. Over the size ceiling
(maxResultBytes, default 50 000 bytes) the snapshot keeps the first whole rows
that fit and adds a truncation field ({ truncated, returnedItems, totalItems, hint }) beside them. The verdict fields (success, applied, unchanged,
rejected) are never dropped. The round trip needs clientFunctionsMode: "pause"
(the default). See
functions and context.
Navigate then change the page: useAgoAutoContinueAfterNavigation
A cross-page request ("open the parfums page and show only lactose-free, sorted by price") needs the agent to navigate, then set the new page's state. The destination only registers its controls once it mounts, so it can't happen in one turn. Mount this hook once (near your router) to bridge it:
function AppShell() {
useAgoNavigation(navigate, routes);
useAgoAutoContinueAfterNavigation();
return <Outlet />;
}
With clientFunctionsMode: "pause" (the default, needs backend pause/resume
support) the backend pauses the turn on the navigation call and the SDK resumes
it once the result is submitted; the hook only delays the resume until the
destination's useAgoPageState registered — same turn, no extra prompt, and
continuationPrompt/maxDepth are unused. In the legacy placeholder mode the
hook instead waits for the destination to register, then sends a hidden
continuation so the agent applies the state in a second turn; it only continues
if the destination has editable state, caps continuations per gesture, and
cancels if the user takes a new turn. Options: navigationFunctions,
continuationPrompt, maxDepth, readinessTimeoutMs, enabled. The glacier
example wires this to its parfums page.
6. Give the agent context: useAgoContext
Expose what the user is looking at, sent with every message. A unique key is
generated per component via useId().
import { useAgoContext } from "@useago/sdk/react";
// Static object, captured from props/state
function OrderPage({ order }) {
useAgoContext({
name: "Order detail",
description: "The user is viewing a specific order",
data: { orderId: order.id, status: order.status },
});
return <OrderView order={order} />;
}
// Dynamic function, evaluated on every send (fresh data from a store)
function App() {
useAgoContext(() => ({
name: "App shell",
data: { userId: store.getState().auth.userId },
}));
}
// Share/reference context with an explicit key
useAgoContext({ name: "Sidebar filter", data: { filter } }, "sidebar-filter");
7. Reactive external state: useAgoStore
If you hold shared UI/request state in a core createStore
(handy when client-side functions and your components mutate the same value),
useAgoStore reads it reactively: the component re-renders on every store.set.
It's a thin useSyncExternalStore wrapper, so it's SSR-safe and batches correctly.
import { createStore } from "@useago/sdk";
import { useAgoStore } from "@useago/sdk/react";
const cart = createStore({ items: [] as string[] });
function CartBadge() {
const { items } = useAgoStore(cart); // re-renders when the store changes
return <span>{items.length}</span>;
}
// Mutate through the store, from anywhere, including a registered AGO function:
cart.set({ items: [...cart.get().items, "SKU-1"] });
8. Subscribe to events
Use the client directly from useAgoClient() and the standard
on / off API inside an effect:
import { useEffect } from "react";
import { useAgoClient } from "@useago/sdk/react";
function Notifier() {
const client = useAgoClient();
useEffect(() => {
const handler = (m) => toast(`AGO: ${m.content}`);
client.on("message:complete", handler);
return () => client.off("message:complete", handler);
}, [client]);
return null;
}
9. Show what the agent is doing: useAgoActivity
A live, normalized feed of the agent's actions (navigations, page-state changes, forms, confirmations, status, progress) merged from tool calls and client function invocations, plus the controls to approve/reject/submit the items that wait on the user.
import { useAgoActivity } from "@useago/sdk/react";
function ActivityFeed() {
const { items, latest, approve, reject, submitForm } = useAgoActivity();
return (
<ul>
{items.map((it) => (
<li key={it.id}>
{it.label} ({it.status})
{it.status === "awaiting-approval" && (
<>
<button onClick={() => approve(it.id)}>Approve</button>
<button onClick={() => reject(it.id)}>Reject</button>
</>
)}
</li>
))}
</ul>
);
}
latest is the most recent item (handy for a collapsed "latest action" line) and
isAwaitingApproval is true while anything waits on the user. Pass labelFor to
map function/route names to friendly copy, and includeReasoning: true to show
the model's chain-of-thought (off by default). Approval controls need the
client's approval gate
(pause mode).
The feed survives a page refresh. When you restore a conversation with
client.getConversation(...), the hook rebuilds the already-resolved activity
from the stored tool calls (steps still waiting on the user are left out; resume
those with
resumePendingClientFunctions).
Full example
A runnable React example lives in examples/simple-react.
Exports cheat-sheet (@useago/sdk/react)
- Provider/context:
AgoProvider,useAgoClient,useOptionalAgoClient - Hooks:
useAgo,useChat,useMessages,useConversation,useAgoFunction,useAgoNavigation,useAgoPageState,useAgoAutoContinueAfterNavigation,useAgoActivity,useAgoContext,useAgoStore,useFormCollector,useFeedback - Components:
ChatWidget,Message,ChatInput,Markdown,MessageFeedback - Forms:
createFormCollector(+CreateFormCollectorOptions,SubmitConfig, …) - Testing:
createMockClient - Types:
AgoConfig,AgoMessage,Conversation,AgoAgent,AgoSource,ToolCallData, plus per-export prop/option types (AgoAttachmentlives on the root@useago/sdkentry, not the/reactsubpath)
See also: Client functions & context · Testing · Configuration