useChat API Reference
The useChat hook is the recommended entry point for building AI chat applications with AgentKit. It provides a unified API that combines real-time streaming capabilities with thread management, making it perfect for building complete chat applications.
Import
Section titled “Import”import { useChat } from "@inngest/use-agent";Basic Usage
Section titled “Basic Usage”function ChatComponent() { const { messages, sendMessage, status, threads, createNewThread } = useChat({ initialThreadId: 'thread-123', userId: 'user-456' });
return ( <div> <ThreadSidebar threads={threads} onNewThread={createNewThread} /> <ChatArea messages={messages} onSendMessage={sendMessage} status={status} /> </div> );}Configuration
Section titled “Configuration”Interface: UseChatConfig
Section titled “Interface: UseChatConfig”userId string required User identifier for attribution and data ownership. Required unless provided by AgentProvider.
useChat({ userId: "user-123" });channelKey string Channel key for subscription targeting. Enables collaborative features when multiple users share the same key.
// Private chat (default)useChat({ userId: "user-123" });
// Collaborative chatuseChat({ channelKey: "project-456", userId: "user-123" });initialThreadId string Thread ID to load on initialization. Perfect for URL-driven chat pages.
// URL: /chat/[threadId]useChat({ initialThreadId: params.threadId });debug boolean default: false Enable comprehensive debug logging for development.
useChat({ debug: process.env.NODE_ENV === "development" });enableThreadValidation boolean default: true Validate that initialThreadId exists in the database. Set to false for custom persistence layers.
// Disable for ephemeral/custom storageuseChat({ initialThreadId: threadId, enableThreadValidation: false,});onThreadNotFound (threadId: string) => void Custom handler for missing threads. Default behavior redirects to homepage.
useChat({ initialThreadId: threadId, onThreadNotFound: (missingThreadId) => { showError(`Thread ${missingThreadId} not found`); router.push("/chat"); },});Advanced Configuration
Section titled “Advanced Configuration”state () => Record<string, unknown> Function to capture client-side state with each message. Essential for message editing and debugging.
useChat({ state: () => ({ currentPage: window.location.pathname, formData: getActiveFormData(), userPreferences: getUserSettings(), timestamp: Date.now(), }),});onStateRehydrate (messageState: Record<string, unknown>, messageId: string) => void Callback to restore UI state when editing messages from previous contexts.
useChat({ onStateRehydrate: (messageState, messageId) => { // Restore form state if (messageState.formData) { restoreFormData(messageState.formData); }
// Navigate to original page if (messageState.currentPage) { router.push(messageState.currentPage); } },});Custom Functions
Section titled “Custom Functions”Override default API behavior for custom backends:
fetchThreads (userId: string, pagination: { limit: number; offset: number }) => Promise<{threads: Thread[]; hasMore: boolean; total: number}> Custom function for fetching threads list.
fetchHistory (threadId: string) => Promise<any[]> Custom function for loading thread message history.
createThread (userId: string) => Promise<{threadId: string; title: string}> Custom function for creating new threads.
deleteThread (threadId: string) => Promise<void> Custom function for deleting threads.
renameThread (threadId: string, title: string) => Promise<void> Custom function for renaming threads.
Return Value: UseChatReturn
Section titled “Return Value: UseChatReturn”Real-time Agent State
Section titled “Real-time Agent State”messages ConversationMessage[] Current thread’s messages with real-time streaming updates. Each message contains parts that stream incrementally.
messages.forEach((msg) => { console.log(`${msg.role}: ${msg.parts.length} parts`); msg.parts.forEach((part) => { if (part.type === "text") { console.log(`Text: ${part.content}`); } });});status AgentStatus Current agent execution status: "idle", "thinking", "calling-tool", "responding", or "error".
// Show appropriate UI based on status{status === 'thinking' && <ThinkingIndicator />}{status === 'responding' && <StreamingIndicator />}{status === 'error' && <ErrorMessage />}isConnected boolean WebSocket connection status to AgentKit networks.
// Show connection status<div>Status: {isConnected ? '🟢 Connected' : '🔴 Disconnected'}</div>currentAgent string | undefined Name of the currently active agent (if available).
<div>Agent: {currentAgent || 'Assistant'}</div>error { message: string; timestamp: Date; recoverable: boolean } | undefined Current error state with recovery information.
{error && ( <ErrorBanner message={error.message} canRetry={error.recoverable} onDismiss={clearError} />)}Thread Management State
Section titled “Thread Management State”threads Thread[] Array of all conversation threads with metadata and unread indicators.
threads.forEach((thread) => { console.log({ id: thread.id, title: thread.title, messageCount: thread.messageCount, hasNewMessages: thread.hasNewMessages, lastActivity: thread.lastMessageAt, });});threadsLoading boolean Loading state for the threads list.
threadsHasMore boolean Whether more threads are available for pagination.
threadsError string | null Error state for thread operations.
currentThreadId string | null ID of the currently active thread.
Loading States
Section titled “Loading States”isLoadingInitialThread boolean true when loading history for a URL-provided initialThreadId.
// Show loading state while fetching initial thread{isLoadingInitialThread ? ( <ThreadLoadingSpinner />) : ( <ChatMessages messages={messages} />)}Actions
Section titled “Actions”Message Sending
Section titled “Message Sending”sendMessage (message: string, options?: { messageId?: string }) => Promise<void> Send a message to the current thread with automatic coordination.
// Basic usageawait sendMessage("Hello!");
// With custom message IDawait sendMessage("Hello!", { messageId: "custom-id" });
// Handles thread creation, optimistic updates, and streaming automaticallysendMessageToThread (threadId: string, message: string, options?: { messageId?: string; state?: Record<string, unknown> | (() => Record<string, unknown>) }) => Promise<void> Send a message to a specific thread (advanced use cases like conversation branching).
// Send to specific threadawait sendMessageToThread("thread-789", "Hello from another thread!");
// With custom client stateawait sendMessageToThread("thread-789", "Edit message", { state: () => ({ mode: "conversation_branching", editFromMessageId: "msg-456", branchHistory: formatBranchHistory(messages.slice(0, editIndex)), }),});Agent Control
Section titled “Agent Control”cancel () => Promise<void> Cancel the current agent run.
<button onClick={cancel} disabled={status === 'idle'}> Cancel Agent</button>approveToolCall (toolCallId: string, reason?: string) => Promise<void> Approve a tool call in Human-in-the-Loop workflows.
// In your tool call rendering{part.type === 'tool-call' && part.state === 'awaiting-approval' && ( <div> <button onClick={() => approveToolCall(part.toolCallId, "Approved by user")}> ✅ Approve </button> </div>)}denyToolCall (toolCallId: string, reason?: string) => Promise<void> Deny a tool call in Human-in-the-Loop workflows.
<button onClick={() => denyToolCall(part.toolCallId, "Security concern")}> ❌ Deny</button>Thread Navigation
Section titled “Thread Navigation”switchToThread (threadId: string) => Promise<void> Switch to a thread with automatic history loading and state reconciliation.
// High-level navigation with history loadingconst handleThreadClick = async (threadId: string) => { await switchToThread(threadId); // History automatically loaded and merged with any optimistic messages};setCurrentThreadId (threadId: string) => void Immediate thread switch without history loading (escape hatch for ephemeral scenarios).
// Low-level escape hatch for immediate switchingsetCurrentThreadId(threadId); // No history loading, immediate switchAdvanced Thread Operations
Section titled “Advanced Thread Operations”loadThreadHistory (threadId: string) => Promise<ConversationMessage[]> Load thread history without switching to it.
const messages = await loadThreadHistory("thread-789");console.log(`Thread has ${messages.length} historical messages`);clearThreadMessages (threadId: string) => void Clear all messages from a specific thread.
clearThreadMessages(currentThreadId); // Clear current threadreplaceThreadMessages (threadId: string, messages: ConversationMessage[]) => void Replace all messages in a specific thread (used for conversation branching).
// Restore conversation to a previous stateconst messagesBeforeEdit = conversation.slice(0, editIndex);replaceThreadMessages(threadId, messagesBeforeEdit);Thread CRUD Operations
Section titled “Thread CRUD Operations”createNewThread () => string Create a new thread and return its ID. Perfect for “New Chat” buttons.
const handleNewChat = () => { const newThreadId = createNewThread(); router.push(`/chat/${newThreadId}`);};deleteThread (threadId: string) => Promise<void> Delete a thread and all its messages permanently.
const handleDeleteThread = async (threadId: string) => { if (confirm("Delete this conversation?")) { await deleteThread(threadId); // Thread removed from sidebar automatically }};loadMoreThreads () => Promise<void> Load the next page of threads for pagination.
// Infinite scroll implementationconst handleScroll = (e) => { const { scrollTop, scrollHeight, clientHeight } = e.target; const isNearBottom = scrollHeight - scrollTop <= clientHeight + 100;
if (isNearBottom && threadsHasMore && !threadsLoading) { loadMoreThreads(); }};refreshThreads () => Promise<void> Refresh the threads list from the server.
<button onClick={refreshThreads}> 🔄 Refresh Threads</button>State Management
Section titled “State Management”rehydrateMessageState (messageId: string) => void Restore client state for editing messages from previous contexts.
const handleEditMessage = (messageId: string) => { // Restore UI state from when this message was originally sent rehydrateMessageState(messageId); // Now start editing with proper context restored startEditing(messageId);};clearError () => void Clear the current error state.
{error && ( <ErrorBanner message={error.message} onDismiss={clearError} />)}Usage Examples
Section titled “Usage Examples”URL-driven Chat Pages
Section titled “URL-driven Chat Pages”Perfect for /chat/[threadId] routes:
import { useChat } from "@inngest/use-agent";
export default function ChatPage({ params }) { const { messages, sendMessage, status, threads, switchToThread } = useChat({ initialThreadId: params.threadId, // Auto-loads this thread state: () => ({ currentPage: `/chat/${params.threadId}`, timestamp: Date.now() }) });
return ( <div class="flex h-screen"> <ThreadSidebar threads={threads} onThreadSelect={switchToThread} /> <ChatArea messages={messages} onSendMessage={sendMessage} status={status} /> </div> );}Homepage with New Conversations
Section titled “Homepage with New Conversations”Handle new conversations from homepage:
export default function HomePage() { const { messages, sendMessage, createNewThread, currentThreadId } = useChat(); // No initialThreadId = fresh conversation
const handleSendMessage = async (text: string) => { if (messages.length === 0) { // First message - create thread and navigate const newThreadId = createNewThread(); await sendMessage(text); router.push(`/chat/${newThreadId}`); } else { await sendMessage(text); } };
return messages.length === 0 ? ( <EmptyState onSendMessage={handleSendMessage} /> ) : ( <ChatInterface messages={messages} onSendMessage={handleSendMessage} /> );}Client State Capture & Rehydration
Section titled “Client State Capture & Rehydration”Capture UI context for advanced debugging and message editing:
function SqlPlaygroundChat() { const [currentSql, setCurrentSql] = useState("SELECT * FROM users;"); const [activeTab, setActiveTab] = useState("query");
const { messages, sendMessage, rehydrateMessageState } = useChat({ // 📸 CAPTURE: Record UI context when messages are sent state: () => ({ sqlQuery: currentSql, activeTab: activeTab, editorConfig: getEditorSettings(), timestamp: Date.now(), }),
// 🔄 REHYDRATE: Restore UI context when editing old messages onStateRehydrate: (messageState, messageId) => { // Restore SQL query if (messageState.sqlQuery) { setCurrentSql(messageState.sqlQuery); }
// Restore active tab if (messageState.activeTab) { setActiveTab(messageState.activeTab); }
console.log(`Restored context for message ${messageId}:`, messageState); }, });
const handleEditMessage = (messageId: string) => { // Restore UI to match when this message was originally sent rehydrateMessageState(messageId); // Now start editing with proper context startMessageEdit(messageId); };}Custom Backend Integration
Section titled “Custom Backend Integration”Use custom API functions for non-standard backends:
const customFetchThreads = async (userId, { limit, offset }) => { const response = await fetch( `/api/v2/conversations?user=${userId}&limit=${limit}&offset=${offset}` ); const data = await response.json();
return { threads: data.conversations.map((conv) => ({ id: conv.id, title: conv.name, messageCount: conv.messageCount, lastMessageAt: new Date(conv.updatedAt), createdAt: new Date(conv.createdAt), updatedAt: new Date(conv.updatedAt), })), hasMore: data.hasNextPage, total: data.totalCount, };};
const chat = useChat({ fetchThreads: customFetchThreads, createThread: customCreateThread, deleteThread: customDeleteThread,});Multi-Chat Application
Section titled “Multi-Chat Application”Handle multiple concurrent conversations:
function MultiChatApp() { const [tabs, setTabs] = useState([ { id: 'tab-1', threadId: 'thread-1', title: 'Chat 1' }, { id: 'tab-2', threadId: 'thread-2', title: 'Chat 2' } ]);
const [activeTabId, setActiveTabId] = useState('tab-1'); const activeTab = tabs.find(t => t.id === activeTabId);
const chat = useChat({ initialThreadId: activeTab?.threadId, enableThreadValidation: false, // Tabs might not exist in DB yet state: () => ({ activeTabId, tabConfiguration: tabs, multiChatMode: true }) });
// Switch threads when tabs change useEffect(() => { if (activeTab?.threadId && activeTab.threadId !== chat.currentThreadId) { chat.setCurrentThreadId(activeTab.threadId); } }, [activeTab?.threadId, chat.currentThreadId]);
return ( <div> <TabBar tabs={tabs} activeTabId={activeTabId} onTabChange={setActiveTabId} /> <ChatArea {...chat} /> </div> );}Error Handling
Section titled “Error Handling”Error Types
Section titled “Error Types”const { error, threadsError, clearError } = useChat();
// Thread-specific error (agent execution, message sending)if (error) { console.log("Agent error:", { message: error.message, recoverable: error.recoverable, timestamp: error.timestamp, });}
// Threads operation error (loading, creating, deleting threads)if (threadsError) { console.log("Threads error:", threadsError);}Error Recovery
Section titled “Error Recovery”function ChatWithErrorHandling() { const { error, clearError, sendMessage } = useChat();
const handleRetry = () => { clearError(); // Optionally resend the last message sendMessage(lastMessage); };
return ( <div> {error && ( <ErrorBanner message={error.message} canRetry={error.recoverable} onRetry={error.recoverable ? handleRetry : undefined} onDismiss={clearError} /> )} <ChatInterface /> </div> );}Provider Integration
Section titled “Provider Integration”Inheriting Configuration
Section titled “Inheriting Configuration”When used within AgentProvider, useChat inherits configuration automatically:
<AgentProvider userId="user-123" debug={true}> <ChatComponent /></AgentProvider>
function ChatComponent() { // Automatically inherits userId="user-123" and debug=true const chat = useChat({ initialThreadId: 'thread-456' // No need to specify userId or debug - inherited from provider });}Overriding Provider Configuration
Section titled “Overriding Provider Configuration”Hook-level options take precedence over provider values:
<AgentProvider userId="user-123"> <ChatComponent /></AgentProvider>
function ChatComponent() { // Override userId while inheriting other provider config const chat = useChat({ userId: "different-user", // Override initialThreadId: 'thread-456' // Other options inherited from provider });}Performance Considerations
Section titled “Performance Considerations”Optimistic Updates
Section titled “Optimistic Updates”useChat automatically handles optimistic UI updates:
const handleSendMessage = async (message: string) => { // Message appears in UI immediately (optimistic) await sendMessage(message); // If successful, message marked as 'sent' // If failed, message marked as 'failed' with retry option};History Reconciliation
Section titled “History Reconciliation”Smart merging prevents duplicate messages when loading thread history:
// When switching threads, useChat automatically:// 1. Loads historical messages from database// 2. Identifies optimistic messages not yet in database// 3. Merges them intelligently to prevent duplicates// 4. Updates UI with combined timeline
// This prevents flashing UIs and lost messages during navigationBackground Thread Updates
Section titled “Background Thread Updates”Receive updates for inactive threads without UI disruption:
// Thread A is active, Thread B gets new message// useChat automatically:// 1. Processes Thread B events in background// 2. Updates Thread B's hasNewMessages flag// 3. Shows unread indicator in sidebar// 4. Doesn't disrupt Thread A's current conversation
const unreadThreads = threads.filter((t) => t.hasNewMessages);console.log(`${unreadThreads.length} threads have new messages`);Integration with Other Hooks
Section titled “Integration with Other Hooks”With Message Actions
Section titled “With Message Actions”import { useChat, useMessageActions } from "@inngest/use-agent";
function ChatWithActions() { const { messages, sendMessage } = useChat();
const { copyMessage, likeMessage, shareMessage } = useMessageActions({ showToast: (message, type) => toast[type](message) });
return ( <div> {messages.map(msg => ( <div key={msg.id}> <MessageContent message={msg} /> <MessageActions onCopy={() => copyMessage(msg)} onLike={() => likeMessage(msg.id)} onShare={() => shareMessage(msg)} /> </div> ))} </div> );}With Conversation Branching
Section titled “With Conversation Branching”import { useChat, useConversationBranching } from "@inngest/use-agent";
function ChatWithBranching() { const { messages, sendMessage: originalSendMessage, sendMessageToThread, replaceThreadMessages, currentThreadId, } = useChat();
const branching = useConversationBranching({ userId: "user-123", storageType: "session", });
// Wrap sendMessage to support branching const sendMessage = useCallback( async ( message: string, options?: { editFromMessageId?: string; } ) => { await branching.sendMessage( originalSendMessage, sendMessageToThread, replaceThreadMessages, currentThreadId!, message, messages, options ); }, [ /* dependencies */ ] );
const handleEditMessage = (messageId: string) => { const newContent = prompt("Enter new message:"); if (newContent) { sendMessage(newContent, { editFromMessageId: messageId }); } };}Common Patterns
Section titled “Common Patterns”Responsive Chat Interface
Section titled “Responsive Chat Interface”function ResponsiveChat({ threadId }) { const { messages, sendMessage, status, threads, switchToThread, createNewThread, deleteThread } = useChat({ initialThreadId: threadId, state: () => ({ viewport: getViewportSize(), deviceType: getDeviceType(), timestamp: Date.now() }) });
return ( <div class="flex h-screen"> <ThreadSidebar threads={threads} onThreadSelect={switchToThread} onNewThread={createNewThread} onDeleteThread={deleteThread} /> <ChatArea messages={messages} onSendMessage={sendMessage} status={status} /> </div> );}Demo/Prototype Mode
Section titled “Demo/Prototype Mode”import { useChat, useEphemeralThreads } from "@inngest/use-agent";
function DemoChat() { const ephemeralThreads = useEphemeralThreads({ userId: "demo-user", storageType: "session" // Clears when tab closes });
const chat = useChat({ userId: "demo-user", enableThreadValidation: false, // No backend validation ...ephemeralThreads // Provide ephemeral implementations });
return <ChatInterface {...chat} />;}Next Steps
Section titled “Next Steps”The useChat hook is designed to handle the 90% use case of building AI chat applications while providing escape hatches for advanced customization. It combines the power of real-time streaming with the convenience of automatic thread management, making it the perfect foundation for sophisticated AI chat interfaces.