deepagentsdk

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.

Architecture: DeepAgent runs on the server and streams responses to the client using AI SDK's UI Message Stream Protocol. The client uses the 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
AI Elements installs components directly into your project (typically @/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:

app/api/chat/route.ts
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:

app/page.tsx
'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 PartPurpose
text-start/delta/endStreaming text responses
tool-input-availableTool called with arguments
tool-output-availableTool returned successfully
tool-output-errorTool execution failed
start-step/finish-stepAgent progress tracking
errorError messages
finishCompletion with usage stats

Custom Data Events (require custom UI handling):

Event NameDataUse 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:

hooks/useChatFullEvents.ts
'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:

components/FileEventList.tsx
'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

app/page.tsx
'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:

app/api/chat/route.ts
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:

app/api/chat/route.ts
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:

app/api/chat/route.ts
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:

lib/customHandler.ts
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:

app/api/chat/route.ts
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 TypeDescription
text-startBeginning of a text response
text-deltaIncremental text chunk
text-endEnd of text response
tool-input-availableTool was called with arguments
tool-output-availableTool returned a result
tool-output-errorTool execution failed
start-stepAgent started a new reasoning step
finish-stepAgent completed a reasoning step
finishMessage completion
errorError occurred during generation

Custom Data Events

Event NameDescription
file-write-start/writtenFile creation operations
file-editedFile modifications
file-readFile access
ls/glob/grepFile system searches
execute-start/finishCommand execution
web-search-start/finishWeb searches
http-request-start/finishHTTP requests
fetch-url-start/finishURL fetching
subagent-start/finish/stepSubagent lifecycle
checkpoint-saved/loadedState persistence
todos-changedTask 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:

app/page.tsx
'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:

app/api/chat/route.ts
// Next.js App Router
export const maxDuration = 60; // 60 seconds

2. 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(),
    });
  },
});

On this page