AI SDK Elements Integration
Build chat UIs with Vercel AI SDK Elements and DeepAgent
AI SDK Elements is Vercel's open-source library of customizable React components for building AI interfaces. This guide shows how to integrate DeepAgent with AI Elements to build production-ready chat applications.
useChat hook and AI Elements components to render the conversation.Overview
The integration follows AI SDK's recommended architecture:
┌──────────────────────────────┐ ┌─────────────────────────────┐
│ Server │ │ Client │
│ /api/chat route │────>│ useChat() hook │
│ └─> DeepAgent │ SSE │ └─> AI Elements UI │
│ └─> UI Message Stream │ │ │
└──────────────────────────────┘ └─────────────────────────────┘Key benefits:
- Server-side execution - API keys stay secure on the server
- Real-time streaming - Responses stream as they're generated
- Tool visibility - Tool calls and results display in the UI
- Full event visibility - File operations, web requests, subagent lifecycle
- Production-ready - Built on Vercel's battle-tested streaming protocol
Installation
First, install the required packages:
# DeepAgent SDK
bun install deepagentsdk
# AI SDK React hooks and your model provider
bun install @ai-sdk/react @ai-sdk/anthropic
# AI Elements CLI (scaffolds components into your project)
bunx ai-elements@latest@/components/ai-elements/), giving you full control to customize them.Quick Start
1. Create the API Route
Create a Next.js API route that handles chat requests:
import { createDeepAgent } from 'deepagentsdk';
import { createElementsRouteHandler } from 'deepagentsdk/adapters/elements';
import { anthropic } from '@ai-sdk/anthropic';
// Create the DeepAgent with your desired configuration
const agent = createDeepAgent({
model: anthropic('claude-sonnet-4-20250514'),
systemPrompt: 'You are a helpful assistant.',
maxSteps: 10,
});
// Export the route handler
export const POST = createElementsRouteHandler({ agent });
// Optional: Increase timeout for longer agent runs
export const maxDuration = 60;2. Create the Chat UI
Build your chat interface using useChat and AI Elements:
'use client';
import { useChat } from '@ai-sdk/react';
import { Message, Conversation, PromptInput } from '@/components/ai-elements';
export default function ChatPage() {
const { messages, input, setInput, handleSubmit, isLoading, stop } = useChat();
return (
<div className="flex flex-col h-screen max-w-3xl mx-auto p-4">
<Conversation className="flex-1 overflow-y-auto">
{messages.map((message) => (
<Message key={message.id} message={message} />
))}
</Conversation>
<form onSubmit={handleSubmit} className="mt-4">
<PromptInput
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Ask anything..."
disabled={isLoading}
/>
{isLoading && (
<button type="button" onClick={stop} className="mt-2">
Stop generating
</button>
)}
</form>
</div>
);
}That's it! You now have a working chat interface powered by DeepAgent.
Full Event Handling
The Elements adapter streams all 26+ DeepAgent event types to the client, giving you complete visibility into agent behavior:
- File operations - Reads, writes, edits, searches
- Command execution - Shell commands and their output
- Web requests - Searches, HTTP fetches, URL extraction
- Subagent lifecycle - When subagents start, finish, and their steps
- Checkpoints - State save/load operations
- Tools - All tool calls and results
Standard vs. Custom Events
Standard Protocol Events (work with off-the-shelf AI Elements):
| Stream Part | Purpose |
|---|---|
text-start/delta/end | Streaming text responses |
tool-input-available | Tool called with arguments |
tool-output-available | Tool returned successfully |
tool-output-error | Tool execution failed |
start-step/finish-step | Agent progress tracking |
error | Error messages |
finish | Completion with usage stats |
Custom Data Events (require custom UI handling):
| Event Name | Data | Use Case |
|---|---|---|
file-write-start/written | { path, content } | Show file creation in real-time |
file-edited | { path, occurrences } | Track file modifications |
file-read | { path, lines } | Display file access |
execute-start/finish | { command, exitCode } | Terminal UI for commands |
web-search-finish | { query, resultCount } | Search result indicators |
subagent-start/finish | { name, task/result } | Multi-agent visualization |
Custom Client-Side Hook
To handle custom data events, you can extend useChat with custom event processing:
'use client';
import { useState, useCallback, useRef } from 'react';
export interface FileEvent {
type: 'file-write-start' | 'file-written' | 'file-edited' | 'file-read';
path: string;
content?: string;
timestamp: number;
}
export function useChatFullEvents() {
const [fileEvents, setFileEvents] = useState<FileEvent[]>([]);
const [messages, setMessages] = useState<any[]>([]);
const abortControllerRef = useRef<AbortController | null>(null);
const sendMessage = useCallback(async (text: string) => {
abortControllerRef.current = new AbortController();
// Convert to UI Message format
const uiMessages = messages.map((msg) => ({
id: msg.id,
role: msg.role,
parts: msg.parts || [{ type: 'text', text: msg.content }],
}));
// Add user message
const userMessage = {
id: `msg-${Date.now()}-user`,
role: 'user',
parts: [{ type: 'text', text }],
};
// Create assistant message for streaming
const assistantMessage = {
id: `msg-${Date.now()}-assistant`,
role: 'assistant',
parts: [],
};
setMessages((prev) => [...prev, userMessage, assistantMessage]);
try {
const response = await fetch('/api/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ messages: [...uiMessages, userMessage] }),
signal: abortControllerRef.current.signal,
});
const reader = response.body?.getReader();
const decoder = new TextDecoder();
let currentText = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value);
const lines = chunk.split('\n');
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = line.slice(6);
if (data === '[DONE]') break;
try {
const event = JSON.parse(data);
// Handle text streaming
if (event.type === 'text-delta') {
currentText += event.delta || '';
setMessages((prev) => prev.map((msg) =>
msg.id === assistantMessage.id
? {
...msg,
parts: [{ type: 'text', text: currentText }],
}
: msg
));
}
// Handle custom data events
else if (event.type === 'data') {
if (event.name.startsWith('file-')) {
setFileEvents((prev) => [
...prev,
{ type: event.name, ...event.data, timestamp: Date.now() },
]);
}
}
} catch (e) {
console.error('Failed to parse event:', e);
}
}
}
}
} catch (error) {
if (error.name !== 'AbortError') {
console.error('Chat error:', error);
}
}
}, [messages]);
const abort = useCallback(() => {
abortControllerRef.current?.abort();
}, []);
return { messages, fileEvents, sendMessage, abort };
}Rendering Custom Events
Create a component to display file operations in your UI:
'use client';
import type { FileEvent } from '@/hooks/useChatFullEvents';
export function FileEventList({ events }: { events: FileEvent[] }) {
return (
<div className="space-y-2">
{events.map((event, i) => (
<div key={i} className="flex items-center gap-2 text-sm">
<span className="font-mono bg-gray-100 px-2 py-1 rounded">
{event.type}
</span>
<span className="text-gray-600">{event.path}</span>
{event.content && (
<span className="text-gray-400">
({event.content.length} bytes)
</span>
)}
</div>
))}
</div>
);
}Full Example with Custom Events
'use client';
import { useChatFullEvents } from '@/hooks/useChatFullEvents';
import { FileEventList } from '@/components/FileEventList';
export default function ChatPage() {
const { messages, fileEvents, sendMessage, abort } = useChatFullEvents();
return (
<div className="flex gap-4 h-screen">
{/* Main chat area */}
<div className="flex-1">
{messages.map((msg) => (
<div key={msg.id}>
<span className="font-bold">{msg.role}:</span>
{msg.parts?.map((part: any, i: number) => {
if (part.type === 'text') {
return <span key={i}>{part.text}</span>;
}
return null;
})}
</div>
))}
</div>
{/* Event sidebar */}
<div className="w-80 border-l p-4">
<h3 className="font-bold mb-2">File Operations</h3>
<FileEventList events={fileEvents} />
</div>
</div>
);
}Configuration Options
Route Handler Options
The createElementsRouteHandler accepts several options:
import { createDeepAgent, StateBackend } from 'deepagentsdk';
import { createElementsRouteHandler } from 'deepagentsdk/adapters/elements';
import { anthropic } from '@ai-sdk/anthropic';
const agent = createDeepAgent({
model: anthropic('claude-sonnet-4-20250514'),
});
export const POST = createElementsRouteHandler({
// Required: The DeepAgent instance
agent,
// Optional: Hook for authentication, logging, rate limiting
onRequest: async (req) => {
const authHeader = req.headers.get('Authorization');
if (!isValidToken(authHeader)) {
throw new Error('Unauthorized');
}
},
// Optional: Initial state for the agent
initialState: {
todos: [],
files: {},
},
// Optional: Thread ID for conversation persistence
threadId: 'user-123-conversation-1',
// Optional: Override max steps per request
maxSteps: 20,
// Optional: Custom ID generator
generateId: () => crypto.randomUUID(),
});Authentication Example
Protect your chat endpoint with authentication:
import { auth } from '@/lib/auth'; // Your auth library
export const POST = createElementsRouteHandler({
agent,
onRequest: async (req) => {
const session = await auth();
if (!session?.user) {
throw new Error('Please sign in to use the chat');
}
},
});Advanced Usage
Adding Custom Tools
DeepAgent supports custom tools that will show in the AI Elements UI:
import { createDeepAgent } from 'deepagentsdk';
import { createElementsRouteHandler } from 'deepagentsdk/adapters/elements';
import { anthropic } from '@ai-sdk/anthropic';
import { tool } from 'ai';
import { z } from 'zod';
const weatherTool = tool({
description: 'Get current weather for a location',
parameters: z.object({
location: z.string().describe('City name'),
}),
execute: async ({ location }) => {
// Your weather API call here
return { temperature: 72, condition: 'Sunny', location };
},
});
const agent = createDeepAgent({
model: anthropic('claude-sonnet-4-20250514'),
tools: {
get_weather: weatherTool,
},
});
export const POST = createElementsRouteHandler({ agent });The AI Elements Message component will automatically render tool calls and results.
Custom Event Mapping
The adapter exports mapEventToProtocol for customization:
import {
createElementsRouteHandler,
mapEventToProtocol,
type DeepAgentEvent,
} from 'deepagentsdk/adapters/elements';
function customMapper(
event: DeepAgentEvent,
writer: { write: (chunk: any) => void },
genId: () => string,
currentTextId: string | null
): string | null {
// Handle specific events differently
if (event.type === 'file-written') {
// Emit as source-document instead of data
writer.write({
type: 'source-document',
sourceId: genId(),
mediaType: 'text/markdown',
title: event.path,
});
return currentTextId;
}
// Default to standard mapping
return mapEventToProtocol(event, writer, genId, currentTextId);
}
// Create handler with custom mapper
export const POST = createElementsRouteHandler({
agent,
// Note: You'd need to fork createElementsRouteHandler to use customMapper
});Conversation Persistence
Enable conversation persistence using DeepAgent's checkpointer:
import { createDeepAgent, MemoryCheckpointSaver } from 'deepagentsdk';
import { createElementsRouteHandler } from 'deepagentsdk/adapters/elements';
import { anthropic } from '@ai-sdk/anthropic';
const checkpointer = new MemoryCheckpointSaver();
const agent = createDeepAgent({
model: anthropic('claude-sonnet-4-20250514'),
checkpointer,
});
export const POST = createElementsRouteHandler({
agent,
threadId: 'conversation-123',
});Streaming Protocol
The adapter uses AI SDK's UI Message Stream Protocol, which streams events as Server-Sent Events (SSE):
Standard Events
| Event Type | Description |
|---|---|
text-start | Beginning of a text response |
text-delta | Incremental text chunk |
text-end | End of text response |
tool-input-available | Tool was called with arguments |
tool-output-available | Tool returned a result |
tool-output-error | Tool execution failed |
start-step | Agent started a new reasoning step |
finish-step | Agent completed a reasoning step |
finish | Message completion |
error | Error occurred during generation |
Custom Data Events
| Event Name | Description |
|---|---|
file-write-start/written | File creation operations |
file-edited | File modifications |
file-read | File access |
ls/glob/grep | File system searches |
execute-start/finish | Command execution |
web-search-start/finish | Web searches |
http-request-start/finish | HTTP requests |
fetch-url-start/finish | URL fetching |
subagent-start/finish/step | Subagent lifecycle |
checkpoint-saved/loaded | State persistence |
todos-changed | Task list updates |
This protocol ensures compatibility with all AI SDK UI hooks and components while providing full visibility into agent behavior.
Error Handling
Handle errors gracefully in your UI:
'use client';
import { useChat } from '@ai-sdk/react';
export default function ChatPage() {
const { messages, error, reload, isLoading } = useChat();
if (error) {
return (
<div className="p-4 bg-red-50 text-red-700 rounded">
<p>Something went wrong: {error.message}</p>
<button onClick={() => reload()} className="mt-2 underline">
Try again
</button>
</div>
);
}
// ... rest of your UI
}Best Practices
1. Set Appropriate Timeouts
DeepAgent with multiple tool calls can take time. Set appropriate timeouts:
// Next.js App Router
export const maxDuration = 60; // 60 seconds2. Use Streaming for Long Responses
The adapter automatically streams responses, providing real-time feedback to users even during long operations.
3. Implement Rate Limiting
Protect your API with rate limiting:
import { Ratelimit } from '@upstash/ratelimit';
import { Redis } from '@upstash/redis';
const ratelimit = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.slidingWindow(10, '1 m'),
});
export const POST = createElementsRouteHandler({
agent,
onRequest: async (req) => {
const ip = req.headers.get('x-forwarded-for') ?? '127.0.0.1';
const { success } = await ratelimit.limit(ip);
if (!success) throw new Error('Rate limit exceeded');
},
});4. Log Requests for Debugging
Add logging in the onRequest hook for debugging:
export const POST = createElementsRouteHandler({
agent,
onRequest: async (req) => {
const body = await req.clone().json();
console.log('Chat request:', {
messageCount: body.messages?.length,
timestamp: new Date().toISOString(),
});
},
});