Skip to content

useAgent API Reference

The useAgent hook provides advanced, low-level control over AgentKit’s real-time streaming system. It manages WebSocket connections, processes streaming events, and maintains conversation state across multiple threads simultaneously.

import { useAgent } from "@inngest/use-agent";
function CustomChatComponent() {
const {
messages,
status,
sendMessage,
isConnected,
threads,
setCurrentThread
} = useAgent({
threadId: 'conversation-123',
userId: 'user-456',
debug: true
});
return (
<div>
<div>Status: {status}</div>
<div>Connected: {isConnected ? 'Yes' : 'No'}</div>
{/* Manual thread switching */}
{Object.keys(threads).map(threadId => (
<button
key={threadId}
onClick={() => setCurrentThread(threadId)}
>
Switch to {threadId}
</button>
))}
{messages.map(msg => (
<div key={msg.id}>{/* Message rendering */}</div>
))}
</div>
);
}
threadId string required

Unique identifier for the conversation thread. This is the primary thread that the hook will manage.

useAgent({ threadId: "conversation-123" });
userId string

User identifier for attribution and data ownership. If not provided, automatically generates an anonymous ID.

// Authenticated user
useAgent({ threadId: "thread-123", userId: "user-456" });
// Anonymous user (auto-generated ID)
useAgent({ threadId: "thread-123" });
channelKey string

Channel key for subscription targeting. Enables collaborative features and flexible connection management.

// Private chat (default)
useAgent({ threadId: "thread-123", userId: "user-456" });
// Collaborative chat
useAgent({
threadId: "thread-123",
userId: "user-456",
channelKey: "project-789", // Multiple users can share this channel
});
debug boolean default: true in development

Enable comprehensive debug logging for event processing, connection management, and state updates.

useAgent({
threadId: "thread-123",
debug: process.env.NODE_ENV === "development",
});
state () => Record<string, unknown>

Function to capture client-side state with each message for debugging and regeneration workflows.

useAgent({
threadId: "thread-123",
state: () => ({
currentPage: window.location.pathname,
formData: getActiveFormData(),
uiMode: getCurrentMode(),
timestamp: Date.now(),
}),
});
transport AgentTransport

Custom transport instance for API calls. If not provided, uses default transport or inherits from AgentProvider.

import { createDefaultAgentTransport } from "@inngest/use-agent";
const customTransport = createDefaultAgentTransport({
api: { sendMessage: "/api/v2/chat" },
headers: { Authorization: `Bearer ${token}` },
});
useAgent({
threadId: "thread-123",
transport: customTransport,
});
onError (error: Error) => void

Callback for handling errors during agent execution.

useAgent({
threadId: "thread-123",
onError: (error) => {
console.error("Agent error:", error);
showErrorNotification(error.message);
analytics.track("agent_error", { error: error.message });
},
});
__disableSubscription boolean default: false

Internal: Disable WebSocket subscription for this instance. Used internally by AgentProvider for connection sharing.

Current Thread State (Backward Compatible)

Section titled “Current Thread State (Backward Compatible)”
messages ConversationMessage[]

Messages in the currently active thread, updated in real-time as streaming events arrive.

messages.forEach((msg) => {
console.log(`${msg.role}: ${msg.parts.length} parts`);
msg.parts.forEach((part) => {
switch (part.type) {
case "text":
console.log(`Text: ${part.content} (${part.status})`);
break;
case "tool-call":
console.log(`Tool: ${part.toolName} (${part.state})`);
break;
case "reasoning":
console.log(`Reasoning: ${part.content}`);
break;
}
});
});
status AgentStatus

Current agent execution status for the active thread: "idle", "thinking", "calling-tool", "responding", or "error".

// UI feedback based on status
switch (status) {
case 'thinking':
return <ThinkingIndicator />;
case 'calling-tool':
return <ToolExecutionIndicator />;
case 'responding':
return <StreamingIndicator />;
case 'error':
return <ErrorIndicator />;
default:
return <IdleState />;
}
currentAgent string | undefined

Name of the agent currently processing requests for the active thread.

<div class="agent-indicator">
{currentAgent ? `${currentAgent} is responding...` : 'Assistant'}
</div>
error { message: string; timestamp: Date; recoverable: boolean } | undefined

Error information for the active thread, if any.

{error && (
<ErrorMessage
message={error.message}
canRetry={error.recoverable}
onRetry={() => {
clearError();
// Retry logic
}}
/>
)}
threads Record<string, ThreadState>

Complete state for all active threads, indexed by threadId. Enables background streaming and thread management.

// Access any thread's state
const threadState = threads["thread-789"];
if (threadState) {
console.log({
messages: threadState.messages.length,
status: threadState.status,
hasNewMessages: threadState.hasNewMessages,
lastActivity: threadState.lastActivity,
});
}
// List all active threads
Object.keys(threads).forEach((threadId) => {
const thread = threads[threadId];
console.log(
`Thread ${threadId}: ${thread.messages.length} messages, status: ${thread.status}`
);
});
currentThreadId string

ID of the currently active/displayed thread.

console.log("Currently viewing thread:", currentThreadId);
console.log("Total active threads:", Object.keys(threads).length);
isConnected boolean

WebSocket connection status to the real-time event stream.

// Show connection indicator
<div class={`connection-status ${isConnected ? 'connected' : 'disconnected'}`}>
{isConnected ? '🟢 Connected' : '🔴 Disconnected'}
</div>
connectionError { message: string; timestamp: Date; recoverable: boolean } | undefined

Connection-level error information (distinct from thread-specific errors).

{connectionError && (
<ConnectionErrorBanner
error={connectionError}
onRetry={clearConnectionError}
/>
)}
sendMessage (message: string, options?: { messageId?: string }) => Promise<void>

Send a message to the current thread with optimistic updates and error handling.

// Basic message sending
await sendMessage("Hello!");
// With custom message ID
await sendMessage("Hello!", { messageId: "custom-msg-123" });
// Automatic optimistic update → backend request → success/failure handling
sendMessageToThread (threadId: string, message: string, options?: { messageId?: string; state?: Record<string, unknown> | (() => Record<string, unknown>) }) => Promise<void>

Send a message to a specific thread (can be different from current thread). Advanced use cases like conversation branching.

// Send to background thread
await sendMessageToThread("thread-789", "Background message");
// Send with custom client state (conversation branching)
await sendMessageToThread("thread-123", "Edited message", {
state: () => ({
mode: "conversation_branching",
editFromMessageId: "msg-456",
branchHistory: previousMessages,
}),
});
cancel () => Promise<void>

Cancel the current agent run if the transport supports cancellation.

const handleCancel = async () => {
try {
await cancel();
console.log("Agent run cancelled successfully");
} catch (error) {
console.error("Failed to cancel:", error);
}
};
regenerate () => void

Regenerate the last response in the current thread by resending the most recent user message.

<button onClick={regenerate} disabled={status !== 'idle'}>
🔄 Regenerate Response
</button>
clearError () => void

Clear error state for the active thread.

clearConnectionError () => void

Clear connection-level error state.

setCurrentThread (threadId: string) => void

Switch the active thread. Updates which thread’s state is exposed via top-level properties (messages, status, etc.).

const handleThreadSwitch = (threadId: string) => {
setCurrentThread(threadId);
// Now `messages` and `status` reflect the new thread
};
getThread (threadId: string) => ThreadState | undefined

Get a specific thread’s complete state without switching to it.

const threadState = getThread("thread-789");
if (threadState) {
console.log({
messageCount: threadState.messages.length,
agentStatus: threadState.status,
hasUnread: threadState.hasNewMessages,
lastActivity: threadState.lastActivity,
});
}
createThread (threadId: string) => void

Create a new empty thread in local state (does not persist to backend).

const newThreadId = `thread-${Date.now()}`;
createThread(newThreadId);
setCurrentThread(newThreadId);
removeThread (threadId: string) => void

Remove a thread completely from local state.

removeThread("old-thread-123");
// Thread and all its messages removed from memory
clearMessages () => void

Clear all messages from the current thread’s local state.

const handleClearChat = () => {
clearMessages();
// Current thread now has empty messages array
};
clearThreadMessages (threadId: string) => void

Clear messages from a specific thread.

clearThreadMessages("thread-789");
// Specified thread now has empty messages array
replaceMessages (messages: ConversationMessage[]) => void

Replace all messages in the current thread (used for loading history).

// Load historical messages
const historyMessages = await fetchHistoryFromAPI(currentThreadId);
replaceMessages(historyMessages);
replaceThreadMessages (threadId: string, messages: ConversationMessage[]) => void

Replace messages in a specific thread.

// Load history for background thread
const backgroundHistory = await fetchHistoryFromAPI("thread-789");
replaceThreadMessages("thread-789", backgroundHistory);
markThreadViewed (threadId: string) => void

Mark a thread as viewed (clear hasNewMessages flag).

const handleThreadClick = (threadId: string) => {
setCurrentThread(threadId);
markThreadViewed(threadId); // Clear unread indicator
};

Each thread in the threads object has the following structure:

interface ThreadState {
messages: ConversationMessage[]; // Thread's conversation
status: AgentStatus; // Agent execution status
currentAgent?: string; // Active agent name
hasNewMessages: boolean; // Unread indicator
lastActivity: Date; // Last update timestamp
error?: {
// Thread-specific error
message: string;
timestamp: Date;
recoverable: boolean;
};
}

useAgent processes events for all threads simultaneously:

function MultiThreadChat() {
const { threads, currentThreadId, setCurrentThread } = useAgent({
threadId: 'primary-thread',
userId: 'user-123'
});
// Monitor background thread activity
const backgroundThreads = Object.entries(threads).filter(
([threadId, _]) => threadId !== currentThreadId
);
const unreadCount = backgroundThreads.reduce(
(count, [_, threadState]) =>
count + (threadState.hasNewMessages ? 1 : 0),
0
);
return (
<div>
<div>Active Threads: {Object.keys(threads).length}</div>
<div>Unread: {unreadCount}</div>
{backgroundThreads.map(([threadId, threadState]) => (
<div
key={threadId}
onClick={() => setCurrentThread(threadId)}
class={threadState.hasNewMessages ? 'unread' : ''}
>
{threadId}: {threadState.messages.length} messages
{threadState.hasNewMessages && ' 🔴'}
</div>
))}
</div>
);
}
function AdvancedThreadManager() {
const {
threads,
sendMessageToThread,
replaceThreadMessages,
clearThreadMessages,
removeThread,
} = useAgent({ threadId: "main-thread", userId: "user-123" });
// Send to specific thread without switching
const sendToBackground = async (threadId: string, message: string) => {
await sendMessageToThread(threadId, message);
// Message sent, events processed in background
};
// Batch thread operations
const cleanupOldThreads = () => {
Object.entries(threads).forEach(([threadId, threadState]) => {
const daysSinceActivity =
(Date.now() - threadState.lastActivity.getTime()) /
(1000 * 60 * 60 * 24);
if (daysSinceActivity > 30) {
removeThread(threadId); // Clean up old threads
}
});
};
// Archive thread messages
const archiveThread = async (threadId: string) => {
const threadState = threads[threadId];
if (threadState) {
// Save to archive API
await saveToArchive(threadId, threadState.messages);
// Clear from memory
clearThreadMessages(threadId);
}
};
}

Unlike useChat, useAgent gives you access to the underlying streaming system:

function EventDebugger() {
const agent = useAgent({
threadId: "debug-thread",
debug: true, // Enable event logging
onError: (error) => {
console.error("Streaming error:", error);
},
});
// Debug logging shows:
// 🔄 [PROCESS-EVENT] seq:4 type:text.delta threadId:debug-thread
// 🔍 [TEXT-DELTA] Applied delta "Hello"
// 🔍 [SEQUENCE-DEBUG] Thread debug-thread after processing: 5 events
}

useAgent handles out-of-order events automatically:

// Events may arrive out of sequence due to network conditions
// Incoming: seq 5, 3, 4, 6
// useAgent automatically:
// 1. Buffers events 5, 6 (waiting for 3, 4)
// 2. Processes 3, then 4 from buffer
// 3. Processes 5, 6 in correct order
// Result: Perfect chronological message updates
function CustomEventHandler() {
const agent = useAgent({
threadId: "custom-thread",
userId: "user-123",
});
// Access low-level state for custom processing
useEffect(() => {
const currentThread = agent.getThread(agent.currentThreadId);
if (currentThread) {
// Custom logic based on thread state
if (currentThread.status === "error") {
handleAgentError(currentThread.error);
}
if (
currentThread.hasNewMessages &&
currentThread.id !== agent.currentThreadId
) {
showUnreadNotification(currentThread.id);
}
}
}, [agent.threads, agent.currentThreadId]);
}
function MultiAgentInterface() {
const customerSupport = useAgent({
threadId: 'support-thread',
channelKey: 'customer-support',
userId: 'user-123'
});
const technicalSupport = useAgent({
threadId: 'technical-thread',
channelKey: 'technical-support', // Different channel
userId: 'user-123'
});
// Handle escalation between agents
const escalateToTechnical = async (message: string) => {
// Send context from customer support to technical support
const context = customerSupport.messages.map(m =>
m.parts.filter(p => p.type === 'text').map(p => p.content).join('')
).join('\n');
await technicalSupport.sendMessage(
`Escalated from customer support:\n${context}\n\nUser question: ${message}`
);
};
return (
<div class="multi-agent-interface">
<div class="customer-support">
<ChatArea agent={customerSupport} onEscalate={escalateToTechnical} />
</div>
<div class="technical-support">
<ChatArea agent={technicalSupport} />
</div>
</div>
);
}

Advanced client state capture for debugging and message editing:

function StatefulChat() {
const [formData, setFormData] = useState({});
const [activeTab, setActiveTab] = useState("chat");
const agent = useAgent({
threadId: "stateful-thread",
// Capture comprehensive client state
state: () => ({
formData: formData,
activeTab: activeTab,
viewport: {
width: window.innerWidth,
height: window.innerHeight,
},
userAgent: navigator.userAgent,
timestamp: Date.now(),
url: window.location.href,
}),
});
// Every message sent includes this context for debugging/regeneration
}

When used within AgentProvider, useAgent inherits configuration:

<AgentProvider userId="user-123" debug={true}>
<ChatComponent />
</AgentProvider>
function ChatComponent() {
// Inherits userId and debug from provider
const agent = useAgent({
threadId: 'thread-456'
// userId and debug inherited automatically
});
}

The provider enables intelligent connection sharing:

<AgentProvider channelKey="shared-project">
<ComponentA /> {/* Uses shared connection */}
<ComponentB /> {/* Uses shared connection */}
<ComponentC channelKey="isolated" /> {/* Separate connection */}
</AgentProvider>
function ComponentA() {
// Uses provider's shared connection for "shared-project"
const agent = useAgent({ threadId: 'thread-a' });
}
function ComponentC() {
// Creates separate connection for "isolated" channel
const agent = useAgent({
threadId: 'thread-c',
channelKey: 'isolated'
});
}
// ✅ Efficient: Use provider for shared connections
<AgentProvider userId="user-123">
<ChatSidebar /> {/* Shares connection */}
<ChatMessages /> {/* Shares connection */}
<ChatInput /> {/* Shares connection */}
</AgentProvider>
// ❌ Inefficient: Multiple separate connections
function App() {
const agent1 = useAgent({ threadId: 'thread-1', userId: 'user-123' }); // Connection 1
const agent2 = useAgent({ threadId: 'thread-2', userId: 'user-123' }); // Connection 2
const agent3 = useAgent({ threadId: 'thread-3', userId: 'user-123' }); // Connection 3
// 3 separate WebSocket connections!
}
// ✅ Efficient: Reasonable state capture
state: () => ({
currentForm: getCurrentFormData(),
activeTab: getActiveTab(),
});
// ❌ Memory leak: Capturing massive objects
state: () => ({
entireAppState: store.getState(), // Potentially huge!
allUserHistory: getUserHistory(), // Potentially huge!
globalCache: getGlobalCache(), // Potentially huge!
});
function ChatWithCleanup() {
const agent = useAgent({ threadId: "main-thread", userId: "user-123" });
// Clean up inactive threads periodically
useEffect(() => {
const cleanup = setInterval(
() => {
const now = Date.now();
Object.entries(agent.threads).forEach(([threadId, threadState]) => {
const inactiveTime = now - threadState.lastActivity.getTime();
const thirtyMinutes = 30 * 60 * 1000;
if (
inactiveTime > thirtyMinutes &&
threadId !== agent.currentThreadId
) {
agent.removeThread(threadId);
}
});
},
5 * 60 * 1000
); // Check every 5 minutes
return () => clearInterval(cleanup);
}, [agent]);
}

useAgent provides detailed error information:

const { error, connectionError, onError } = useAgent({
threadId: "thread-123",
onError: (error) => {
// Handle errors from agent execution
console.error("Agent error:", {
message: error.message,
stack: error.stack,
timestamp: new Date().toISOString(),
});
},
});
// Thread-specific error
if (error) {
console.log("Thread error:", {
message: error.message, // "Failed to send message"
recoverable: error.recoverable, // true/false
timestamp: error.timestamp, // When error occurred
});
}
// Connection-level error
if (connectionError) {
console.log("Connection error:", {
message: connectionError.message, // "WebSocket connection failed"
recoverable: connectionError.recoverable, // true/false
timestamp: connectionError.timestamp, // When error occurred
});
}
function ChatWithRecovery() {
const {
error,
connectionError,
clearError,
clearConnectionError,
regenerate,
} = useAgent({
threadId: "recovery-thread",
userId: "user-123",
});
// Auto-recovery for recoverable errors
useEffect(() => {
if (error?.recoverable) {
const timer = setTimeout(() => {
clearError();
regenerate(); // Retry last message
}, 3000);
return () => clearTimeout(timer);
}
}, [error]);
// Connection recovery
useEffect(() => {
if (connectionError?.recoverable) {
const timer = setTimeout(() => {
clearConnectionError();
// Connection will automatically retry
}, 5000);
return () => clearTimeout(timer);
}
}, [connectionError]);
}

Enable debug mode to see detailed event processing:

useAgent({
threadId: "debug-thread",
debug: true,
});
// Console output includes:
// 🔄 [PROCESS-EVENT] seq:5 type:text.delta threadId:debug-thread
// 🔍 [TEXT-DELTA] Applied delta seq:5 "Hello" | before:"" after:"Hello"
// 🔍 [THREAD-SWITCH] debug-thread → new-thread (0 → 0 messages)
// 🔍 [MESSAGE-SENT] Starting new conversation in thread new-thread
// Monitor event sequence integrity
const agent = useAgent({
threadId: "sequence-debug",
debug: true,
});
// Debug logs show sequence management:
// 🔍 [SEQUENCE-DEBUG] Thread filtering events: totalEvents=10, lastProcessed=5
// 🔍 [SEQUENCE-DEBUG] After filtering: unprocessedCount=5, filteredOut=5
// [Thread thread-123] Processing 5/10 new events: text.delta:6,part.created:7...
// Track memory usage across threads
function MemoryMonitor() {
const agent = useAgent({ threadId: "monitor", userId: "user-123" });
useEffect(() => {
const logMemoryStats = () => {
console.log("Thread Memory Stats:", {
totalThreads: Object.keys(agent.threads).length,
totalMessages: Object.values(agent.threads).reduce(
(sum, thread) => sum + thread.messages.length,
0
),
currentThread: agent.currentThreadId,
threadsWithUnread: Object.values(agent.threads).filter(
(t) => t.hasNewMessages
).length,
});
};
const interval = setInterval(logMemoryStats, 30000); // Every 30s
return () => clearInterval(interval);
}, [agent]);
}
function CustomChat({ initialThreadId }) {
const agent = useAgent({
threadId: initialThreadId || `thread-${Date.now()}`,
userId: 'user-123',
debug: true,
state: () => ({
chatMode: 'custom',
timestamp: Date.now()
})
});
// Custom thread switching with animation
const switchThread = useCallback(async (threadId: string) => {
setIsTransitioning(true);
agent.setCurrentThread(threadId);
agent.markThreadViewed(threadId);
// Custom history loading
try {
const history = await fetchCustomHistory(threadId);
agent.replaceThreadMessages(threadId, history);
} catch (error) {
console.warn('Failed to load history:', error);
}
setIsTransitioning(false);
}, [agent]);
return (
<div>
<CustomThreadSidebar
threads={agent.threads}
currentThreadId={agent.currentThreadId}
onThreadSelect={switchThread}
/>
<CustomMessageArea
messages={agent.messages}
status={agent.status}
onSendMessage={agent.sendMessage}
/>
</div>
);
}
function EmbeddedChatWidget({ containerId, config }) {
const agent = useAgent({
threadId: `embedded-${containerId}`,
userId: config.userId,
channelKey: config.channelKey,
transport: createCustomTransport(config.apiEndpoints),
debug: false // Production mode
});
// Minimal UI suitable for embedding
return (
<div class="embedded-chat-widget">
<div class="widget-header">
<span>AI Assistant</span>
<ConnectionIndicator connected={agent.isConnected} />
</div>
<div class="widget-messages">
{agent.messages.map(msg => (
<EmbeddedMessage key={msg.id} message={msg} />
))}
</div>
<div class="widget-input">
<EmbeddedInput onSend={agent.sendMessage} disabled={agent.status !== 'idle'} />
</div>
</div>
);
}
function ResearchInterface() {
const agent = useAgent({
threadId: "research-thread",
userId: "researcher-123",
debug: true,
state: () => ({
experimentId: getCurrentExperiment(),
participantId: getParticipantId(),
condition: getExperimentalCondition(),
sessionStartTime: getSessionStart(),
interactionCount: getInteractionCount(),
}),
});
// Log all events for research
useEffect(() => {
// Custom event logging for research
const logInteraction = (type: string, data: any) => {
analytics.track("research_interaction", {
type,
threadId: agent.currentThreadId,
messageCount: agent.messages.length,
agentStatus: agent.status,
timestamp: Date.now(),
...data,
});
};
// Log state changes
logInteraction("thread_switch", { newThreadId: agent.currentThreadId });
}, [agent.currentThreadId]);
}

If you need to migrate from useChat to useAgent for more control:

// Before: useChat (automatic coordination)
const { messages, sendMessage, threads, switchToThread } = useChat({
initialThreadId: threadId,
});
// After: useAgent (manual coordination)
const agent = useAgent({
threadId: threadId,
userId: "user-123",
});
// Manual thread management (what useChat did automatically)
const threads = useThreads({ userId: "user-123" });
useEffect(() => {
// Sync thread state manually
if (threads.currentThreadId !== agent.currentThreadId) {
agent.setCurrentThread(threads.currentThreadId);
}
}, [threads.currentThreadId, agent.currentThreadId]);
const switchToThread = useCallback(
async (threadId: string) => {
// Manual history loading (what useChat did automatically)
threads.setCurrentThreadId(threadId);
agent.setCurrentThread(threadId);
try {
const history = await loadThreadHistory(threadId);
agent.replaceThreadMessages(threadId, history);
} catch (error) {
console.warn("Failed to load thread history:", error);
}
},
[agent, threads]
);

The useAgent hook provides the foundation for all AgentKit React integrations. While useChat is recommended for most applications, useAgent gives you the granular control needed for advanced implementations, custom UI patterns, and specialized use cases.