Documentation Index
Fetch the complete documentation index at: https://docs.ag-kit.dev/llms.txt
Use this file to discover all available pages before exploring further.
The @ag-kit/ui-react package provides a lightweight React hook and TypeScript types to build agent chat interfaces that support streaming messages, tool calls with optional UI, and human-in-the-loop interrupts.
Installation
npm install @ag-kit/ui-react zod
- Peer requirement:
zod (v3.25+ or v4)
- Uses
fetch + Server-Sent Events (SSE). Your server endpoint must stream events compatible with AG‑Kit.
Quick start
import React, { useState } from "react";
import { useChat } from "@ag-kit/ui-react";
export default function App() {
const { uiMessages, sendMessage, streaming } = useChat({
url: "/send-message", // your agent server endpoint (SSE)
});
const [input, setInput] = useState("");
return (
<div>
<div>
{uiMessages.map((m, i) => (
<div key={i}>
{m.role === "user" && m.parts.map((p, idx) => p.type === "text" ? <p key={idx}>{p.text}</p> : null)}
{m.role === "assistant" && m.parts.map((p, idx) => {
if (p.type === "text") return <p key={idx}>{p.text}</p>;
if (p.type === "tool-call") return (
<div key={idx}>
{p.render ? p.render() : <pre>{p.arguments}</pre>}
</div>
);
if (p.type === "interrupt" && p.render) return <div key={idx}>{p.render()}</div>;
return null;
})}
</div>
))}
</div>
<form onSubmit={(e) => { e.preventDefault(); sendMessage(input); setInput(""); }}>
<input value={input} onChange={(e) => setInput(e.target.value)} placeholder="Type..." />
<button type="submit" disabled={streaming}>Send</button>
</form>
</div>
);
}
API
useChat
React hook for chat state, streaming, tool calls, and interrupts.
function useChat(options?: {
url?: string; // default: "/send-message"
clientTools?: ReadonlyArray<ClientTool<z.ZodTypeAny>>;
serverTools?: ReadonlyArray<ServerTool<z.ZodTypeAny>>;
threadId?: string; // default: generated UUID
interrupt?: InterruptWithResume;
}): {
messages: Array<ClientMessage>; // low-level message stream from server
setMessages: (
next: Array<ClientMessage> | ((prev: Array<ClientMessage>) => Array<ClientMessage>)
) => void;
uiMessages: Array<UIMessage>; // UI-friendly message parts
uiToolCalls: Array<{
id: string;
name: string;
arguments: string;
result?: string;
state: ToolStreamingState;
errorText?: string;
}>;
sendMessage: (arg?:
| string
| { toolCallId: string; content: string }
| { interruptId: string; payload: unknown }
) => Promise<void>;
loading: boolean;
streaming: boolean;
}
- sendMessage forms:
sendMessage("text"): send a user message
sendMessage({ toolCallId, content }): submit a tool result message
sendMessage({ interruptId, payload }): resume from an interrupt with payload
Create strongly-typed tools with zod. Client tools can be declared with one of three capabilities: handler, render, or renderAndWaitForResponse. Server tools can be declared with render to render a UI for the tool call.
import { z } from "zod";
import { clientTool, serverTool } from "@ag-kit/ui-react";
// 1) Handler tool: runs automatically when input is available
const add = clientTool({
name: "add",
description: "Add two numbers",
parameters: z.object({ a: z.number(), b: z.number() }),
handler: ({ a, b }) => a + b,
});
// 2) Render-only tool: renders UI for visibility (no automatic handler)
const show = clientTool({
name: "show",
description: "Show payload",
parameters: z.object({ data: z.string() }),
render: ({ input }) => <div>Payload: {input.data}</div>,
});
// 3) Render and wait: render UI and submit a result back to the agent
const confirm = clientTool({
name: "confirm",
description: "Ask for confirmation",
parameters: z.object({ prompt: z.string() }),
renderAndWaitForResponse: ({ input, submitToolResult }) => (
<button onClick={() => submitToolResult?.("confirmed")}>Confirm: {input.prompt}</button>
),
});
// 4) Server tool: render UI for the tool call
const myTool = serverTool({
name: 'show_info',
parameters: z.object({ text: z.string() }),
render: ({ input }) => <div>{input.text}</div>
});
Pass tools to useChat({ clientTools: [...], serverTools: [...] }). When the server emits a tool call that matches a provided tool name:
- If the client tool has a
handler, it runs automatically when the input finishes streaming and its result is sent as a tool message.
- If the client tool has
render, it will appear in uiMessages with an optional render() function for custom UI.
- If the client tool has
renderAndWaitForResponse, a submitToolResult callback is provided to return a string result, which is sent back to the agent.
- If the server tool has
render, it will appear in uiMessages with an optional render() function for custom UI.
// Inside your message renderer
if (part.type === "tool-call") {
return (
<div>
{/* Prefer tool-provided UI when available */}
{part.render ? part.render() : (
<details>
<summary>Tool: {part.name}</summary>
<pre>args: {part.arguments}</pre>
{part.result && <pre>result: {part.result}</pre>}
</details>
)}
</div>
);
}
- Auto-run (handler): Provide a tool with
handler. It will auto-execute when input-available and continue the round.
- Self-handled run (renderAndWaitForResponse): Provide
renderAndWaitForResponse and call submitToolResult("...") to send the result.
- Manual submission (UI-only tool): For tools without a handler, submit a result manually using
sendMessage({ toolCallId, content }).
// Manual submission example for a tool without handler
import { useChat } from "@ag-kit/ui-react";
const { uiMessages, sendMessage } = useChat({ /* tools: [...] */ });
// ... in render
if (part.type === "tool-call") {
return (
<div>
{part.render ? part.render() : <pre>{part.arguments}</pre>}
<button
onClick={() => {
// content must be a string; JSON.stringify if sending structured data
sendMessage({ toolCallId: part.id, content: JSON.stringify({ ok: true }) });
}}
>
Submit result
</button>
</div>
);
}
You can also inspect uiToolCalls to show progress/state:
const { uiToolCalls } = useChat();
return (
<ul>
{uiToolCalls.map(tc => (
<li key={tc.id}>{tc.name}: {tc.state}</li>
))}
</ul>
);
Interrupts
Provide a custom renderer for human-in-the-loop approval/inputs via interrupt option.
import { useChat } from "@ag-kit/ui-react";
const chat = useChat({
interrupt: {
renderWithResume: ({ interrupt, resume }) => (
<div>
<p>{interrupt.reason}</p>
<button onClick={() => resume({ approved: true })}>Approve</button>
</div>
),
},
});
When an interrupt arrives, uiMessages includes a part with type: "interrupt". If provided, your renderWithResume UI will be used, and calling resume(payload) continues the conversation.
Types
These TypeScript types are exported for building UIs:
// Tools
type ClientToolWithInputSchema<TSchema extends z.ZodTypeAny> = Omit<
AgKitTool,
"parameters"
> & {
parameters: TSchema;
};
type ClientToolWithHandler<TSchema extends z.ZodTypeAny> =
ClientToolWithInputSchema<TSchema> & {
handler: (input: z.infer<TSchema>) => unknown;
};
type ClientToolWithRender<TSchema extends z.ZodTypeAny> =
ClientToolWithInputSchema<TSchema> & {
render: (props: {
input: z.infer<TSchema>;
part: UIMessagePartToolCall;
}) => React.ReactNode;
};
type ClientToolWithRenderAndWaitForResponse<
TSchema extends z.ZodTypeAny,
> = ClientToolWithInputSchema<TSchema> & {
renderAndWaitForResponse: (props: {
part: UIMessagePartToolCall;
submitToolResult?: (output: string) => void;
}) => React.ReactNode;
};
type ClientToolBase<TSchema extends z.ZodTypeAny> =
ClientToolWithInputSchema<TSchema> & {
handler?: never;
render?: never;
renderAndWaitForResponse?: never;
};
type ClientTool<TSchema extends z.ZodTypeAny> =
| ClientToolBase<TSchema>
| (ClientToolWithHandler<TSchema> & {
render?: never;
renderAndWaitForResponse?: never;
})
| (ClientToolWithRender<TSchema> & {
handler?: never;
renderAndWaitForResponse?: never;
})
| (ClientToolWithRenderAndWaitForResponse<TSchema> & {
handler?: never;
render?: never;
});
type ServerTool<TSchema extends z.ZodTypeAny> = {
parameters?: TSchema;
name: string;
render: (props: {
input: z.infer<TSchema>;
part: UIMessagePartToolCall;
}) => React.ReactNode;
};
type ToolStreamingState = "input-streaming" | "input-available" | "output-available" | "output-error";
// Messages
type UIMessagePartText = { type: "text"; text: string };
type UIMessagePartToolCall = {
type: "tool-call";
id: string;
name: string;
arguments: string;
result?: string;
state?: ToolStreamingState;
errorText?: string;
};
type UIMessagePartInterrupt = { type: "interrupt"; interrupt: Interrupt; render?: () => React.ReactNode };
type UIMessageUser = { role: "user"; parts: Array<UIMessagePartText> };
type UIMessageAssistant = { role: "assistant"; parts: Array<UIMessagePartText | UIMessagePartToolCall | UIMessagePartInterrupt> };
type UIMessage = UIMessageUser | UIMessageAssistant;
// Interrupt
type Interrupt = { id: string; reason: string; payload: unknown };
type InterruptWithResume = { renderWithResume: (props: { interrupt: Interrupt; resume: (result: unknown) => void }) => React.ReactNode };
Server expectations
url must accept POST requests with JSON and respond with SSE events that conform to AG‑Kit’s sendMessageEvent schema (text deltas, tool-call start/args/end, tool-result, interrupt, and [DONE]).
- Default
url is "/send-message"; set it to your agent server endpoint as needed.