UI Streaming with useAgent
The useAgent hook is a powerful client-side hook for React that manages real-time, multi-threaded conversations with an AgentKit network. It encapsulates the entire lifecycle of agent interactions, including sending messages, receiving streaming events, handling out-of-order event sequences, and managing connection state.
While useChat is the recommended high-level hook for building chat interfaces, useAgent provides the low-level building blocks for more customized implementations where you need direct control over the event stream and state management.
How it Works
Section titled “How it Works”useAgent builds upon Inngest’s Realtime capabilities to create a persistent, unified stream of events for a user. Here’s a step-by-step breakdown of the data flow:
- Client Initialization: The
useAgenthook is initialized in your React component. It uses theuseInngestSubscriptionhook from@inngest/realtime/hooksto establish a WebSocket connection. - Authentication: To connect, it calls a backend API route (e.g.,
/api/realtime/token) to get a short-lived subscription token. This token authorizes the client to listen to events on a specific user channel. - Sending a Message: The user types a message and the UI calls the
sendMessagefunction returned by the hook. - API Request:
sendMessagemakes aPOSTrequest to a backend API route (e.g.,/api/chat). This request contains the message content, the currentthreadId, and the conversation history. - Triggering Inngest: The chat API route receives the request and sends an
agent/chat.requestedevent to Inngest usinginngest.send(). - Agent Execution: An Inngest function (
run-agent-chat.tsin our example) is triggered by the event. It sets up the AgentKit network, state, and history adapter. - Running the Network: The Inngest function calls
network.run(message, { streaming: ... }). - Streaming Events: As the network and its agents execute, they generate streaming events (e.g.,
run.started,part.created,text.delta). Thestreaming.publishfunction inside the Inngest function forwards these events back to the user’s realtime channel. - Realtime Push: The events are pushed over the WebSocket connection to the client.
- State Update:
useAgentreceives the raw events, processes them in the correct sequence, handles out-of-order events, and updates its internal state. - UI Re-render: The component using the hook re-renders with the new messages, agent status, and other UI parts.
Sequence Diagram
Section titled “Sequence Diagram”sequenceDiagram participant Client (React UI) participant useAgent Hook participant Backend API participant Inngest participant AgentKit Network
Client->>useAgent Hook: Initialize with threadId, userId useAgent Hook->>Backend API: POST /api/realtime/token Backend API-->>useAgent Hook: Subscription Token useAgent Hook->>Inngest: Establishes WebSocket Connection
Client->>useAgent Hook: sendMessage("Hello!") useAgent Hook->>Backend API: POST /api/chat (message, history) Backend API->>Inngest: inngest.send("agent/chat.requested")
Inngest->>AgentKit Network: Triggers Inngest Function (run-agent-chat.ts) AgentKit Network->>AgentKit Network: network.run(message, { streaming })
loop Event Streaming AgentKit Network-->>Inngest: publish(event) Inngest-->>useAgent Hook: Pushes event via WebSocket useAgent Hook->>useAgent Hook: Processes event, updates state useAgent Hook-->>Client: Re-renders UI with new data endUsage Guide
Section titled “Usage Guide”1. Backend Setup
Section titled “1. Backend Setup”First, you need to set up the backend infrastructure to handle chat requests and realtime communication.
Inngest Function
Section titled “Inngest Function”This function is the core of your agent’s execution. It listens for chat requests and runs your AgentKit network, streaming events back to the client.
import { inngest } from "../client";import { createCustomerSupportNetwork } from "../networks/customer-support-network";import { userChannel } from "../../lib/realtime";import { createState } from "@inngest/agent-kit";import type { CustomerSupportState } from "../types/state";import { PostgresHistoryAdapter } from "../db";
const historyAdapter = new PostgresHistoryAdapter<CustomerSupportState>({});
export const runAgentChat = inngest.createFunction( { id: "run-agent-chat" }, { event: "agent/chat.requested" }, async ({ event, step, publish }) => { await step.run("initialize-db-tables", () => historyAdapter.initializeTables() );
const { threadId, message, userId, history, messageId } = event.data;
const network = createCustomerSupportNetwork( threadId, createState<CustomerSupportState>( { customerId: userId }, { messages: history, threadId } ), historyAdapter );
// Run the network and stream events back to the client const result = await network.run(message, { streaming: { publish: async (chunk) => { const enrichedChunk = { ...chunk, data: { ...chunk.data, threadId, userId }, }; await publish(userChannel(userId).agent_stream(enrichedChunk)); }, }, messageId, });
return { success: true, threadId, result }; });API Routes
Section titled “API Routes”You’ll need a few API routes in your Next.js application.
import { NextRequest, NextResponse } from "next/server";import { inngest } from "@/inngest/client";import { randomUUID } from "crypto";
export async function POST(req: NextRequest) { const { message, threadId: providedThreadId, userId, history, messageId, } = await req.json(); const threadId = providedThreadId || randomUUID();
await inngest.send({ name: "agent/chat.requested", data: { threadId, message, messageId, history, userId }, });
return NextResponse.json({ success: true, threadId });}import { NextRequest, NextResponse } from "next/server";import { getSubscriptionToken } from "@inngest/realtime";import { inngest } from "@/inngest/client";import { userChannel } from "@/lib/realtime";
export async function POST(req: NextRequest) { const { userId } = await req.json(); // TODO: Add authentication/authorization here const token = await getSubscriptionToken(inngest, { channel: userChannel(userId), topics: ["agent_stream"], }); return NextResponse.json(token);}2. Frontend Setup
Section titled “2. Frontend Setup”Now, let’s wire up the useAgent hook in a React component.
"use client";
import { useAgent, type ConversationMessage } from "@/hooks/use-agent";
const USER_ID = "test-user-123";
export function ChatComponent({ threadId }: { threadId: string }) { const { messages, status, sendMessage, isConnected, error, clearError } = useAgent({ threadId: threadId, userId: USER_ID, debug: true, onError: (err) => console.error("Agent error:", err), });
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => { e.preventDefault(); const formData = new FormData(e.currentTarget); const message = formData.get("message") as string; if (message.trim()) { sendMessage(message); e.currentTarget.reset(); } };
return ( <div> <div>Status: {status}</div> <div>Connected: {isConnected ? "Yes" : "No"}</div> {error && ( <div> Error: {error.message} <button onClick={clearError}>Clear</button> </div> )}
<div class="message-list"> {messages.map((msg) => ( <Message key={msg.id} message={msg} /> ))} </div>
<form onSubmit={handleSubmit}> <input name="message" placeholder="Ask anything..." disabled={status !== "idle"} /> <button type="submit" disabled={status !== "idle"}> Send </button> </form> </div> );}
// A simple component to render a message with all its partsfunction Message({ message }: { message: ConversationMessage }) { return ( <div class={`message ${message.role}`}> <strong>{message.role}</strong> {message.parts.map((part, index) => { switch (part.type) { case "text": return <p key={index}>{part.content}</p>; case "tool-call": return ( <div key={index} class="tool-call" style={{ border: "1px solid #ccc", padding: "10px", marginTop: "10px", borderRadius: "5px", }} > <div> <strong>Tool Call:</strong> {part.toolName} </div> <pre style={{ whiteSpace: "pre-wrap", wordBreak: "break-all" }}> Status: {part.state} <br /> Input: {JSON.stringify(part.input, null, 2)} {part.output && ( <> <br /> Output: {JSON.stringify(part.output, null, 2)} </> )} </pre> </div> ); // TODO: Add cases for other part types like 'data', 'reasoning', etc. default: return null; } })} </div> );}API Reference
Section titled “API Reference”The useAgent hook returns an object with the following properties and methods.
State Properties
Section titled “State Properties”messages ConversationMessage[] The array of messages for the current thread. Each message contains an
array of parts that are streamed in real-time.
status AgentStatus The current status of the agent for the active thread. It can be one of:
"idle", "thinking", "calling-tool", "responding", or "error".
error { message: string; ... } | undefined An object containing details about the last error that occurred in the active
thread. undefined if there is no error.
threads Record<string, ThreadState> An object containing the full state for all active threads, indexed by
threadId. This allows you to manage multiple conversations in the
background.
currentThreadId string The ID of the currently active/displayed thread.
isConnected boolean true if the client is currently connected to the Inngest Realtime server.
connectionError { message: string; ... } | undefined An object containing details about a connection-level error (e.g., failure to get a subscription token).
Action Methods
Section titled “Action Methods”sendMessage (message: string) => Promise<void> Sends a message to the current thread. It handles optimistic UI updates and formats the history for the backend.
sendMessageToThread (threadId: string, message: string) => Promise<void> Sends a message to a specific thread, which can be different from the currently active one.
setCurrentThread (threadId: string) => void Switches the active thread. This updates which thread’s state is exposed via
the top-level messages, status, and error properties.
createThread (threadId: string) => void Creates a new, empty thread in the local state.
clearMessages () => void Clears all messages from the current thread’s local state.
replaceMessages (messages: ConversationMessage[]) => void Replaces all messages in the current thread’s local state. Useful for loading historical messages.
clearError () => void Clears the error state for the current thread.
clearConnectionError () => void Clears any connection-level error.
UI Data Models
Section titled “UI Data Models”The useAgent hook exposes a set of rich UI data models to make building chat interfaces easier. These types define the structure of messages and their constituent parts.
ConversationMessage
Section titled “ConversationMessage”Represents a complete message in the conversation, containing one or more parts.
export interface ConversationMessage { /** Unique identifier for this message */ id: string; /** Whether this message is from the user or the assistant */ role: "user" | "assistant"; /** Array of message parts that make up the complete message */ parts: MessagePart[]; /** ID of the agent that created this message (for assistant messages) */ agentId?: string; /** When this message was created */ timestamp: Date; /** The status of the message, particularly for optimistic user messages */ status?: "sending" | "sent" | "failed";}MessagePart
Section titled “MessagePart”A union type representing all possible parts of a message.
export type MessagePart = | TextUIPart | ToolCallUIPart | DataUIPart | FileUIPart | SourceUIPart | ReasoningUIPart | StatusUIPart | ErrorUIPart | HitlUIPart;TextUIPart
Section titled “TextUIPart”Represents a text message part that can be streamed character by character.
export interface TextUIPart { type: "text"; /** Unique identifier for this text part */ id: string; /** The text content, updated incrementally during streaming */ content: string; /** Whether the text is still being streamed or is complete */ status: "streaming" | "complete";}ToolCallUIPart
Section titled “ToolCallUIPart”Represents a tool call that the agent is making, with streaming input and output.
export interface ToolCallUIPart { type: "tool-call"; /** Unique identifier for this tool call */ toolCallId: string; /** Name of the tool being called */ toolName: string; /** Current state of the tool call execution */ state: | "input-streaming" | "input-available" | "awaiting-approval" | "executing" | "output-available"; /** Tool input parameters, streamed incrementally */ input: any; /** Tool output result, if available */ output?: any; /** Error information if the tool call failed */ error?: any;}(For brevity, other part types like DataUIPart, ReasoningUIPart, etc., are omitted here but follow a similar structure. You can find their full definitions in the use-agent.ts file in the example.)
AgentStatus
Section titled “AgentStatus”Represents the current activity status of the agent.
export type AgentStatus = | "idle" | "thinking" | "calling-tool" | "responding" | "error";