Skip to main content
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

Tools

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.

Rendering tool calls in your 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>
  );
}

Client toolcall registration and execution modes

  • 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.