SDK Integration

Complete server and client setup for @lens-os/sdk.

This guide covers the full server-side and client-side setup for integrating @lens-os/sdk into your application. For a quick 3-file setup, see the Quick Start guide.

Server Setup

createAgentHandler(config)

Creates a route handler that runs SupervisorAgent server-side and streams SSE events to the client.

AgentHandlerConfig
interface AgentHandlerConfig {
  /** Lens OS API Key (required, server-side only) */
  apiKey: string;

  /** OpenAI API Key (required, server-side only) */
  openaiKey: string;

  /** Base URL for Lens OS API (default: https://osapi.ask-lens.ai) */
  baseUrl?: string;

  /** Pre-existing LensClient instance for sharing config cache */
  client?: LensClient;

  /** LLM model (default: gpt-4o) */
  model?: string;

  /** Max agent loop turns (default: 10) */
  maxTurns?: number;

  /** Language (default: zh-TW) */
  language?: 'zh-TW' | 'en-US';

  /** Custom tool executors (see Tool Executors page) */
  toolExecutors?: Record<string, ToolExecutorFunction | ToolExecutorConfig>;

  /** Action request timeout in ms (default: 30000) */
  actionTimeout?: number;

  /** Callback for LLM traces (logging, analytics) */
  onTrace?: (trace: LLMTrace) => void;
}

Return Value

handler
const handler = createAgentHandler(config);

handler.POST             // (req: Request) => Promise<Response>  — the route handler
handler._pendingActions  // PendingActionStore — pass to createActionResultHandler

Adding Authentication

The handler expects { message, sessionId, userId? } in the request body. You can wrap the handler to inject authentication:

src/app/api/agent/chat/route.ts
import { createAgentHandler } from '@lens-os/sdk/server';
import { auth } from '@/auth'; // your auth library

export const handler = createAgentHandler({
  apiKey: process.env.LENS_API_KEY!,
  openaiKey: process.env.OPENAI_API_KEY!,
  model: process.env.OPENAI_MODEL || 'gpt-4o',
  maxTurns: 10,
  toolExecutors: buildToolExecutors(),
});

export async function POST(req: Request) {
  // 1. Authenticate
  const session = await auth();
  if (!session?.user) {
    return new Response(JSON.stringify({ error: 'Unauthorized' }), {
      status: 401,
      headers: { 'Content-Type': 'application/json' },
    });
  }

  // 2. Inject userId into the request body
  const userId = session.user.id;
  const body = await req.json();
  const newReq = new Request(req.url, {
    method: 'POST',
    headers: req.headers,
    body: JSON.stringify({ ...body, userId }),
  });

  // 3. Delegate to SDK handler
  return handler.POST(newReq);
}

createActionResultHandler(pendingActions)

Creates a route handler for receiving DOM action results from the client.

src/app/api/agent/chat/action-result/route.ts
import { createActionResultHandler } from '@lens-os/sdk/server';
import { handler } from '../route';

export const { POST } = createActionResultHandler(handler._pendingActions);

Note

The action-result endpoint must share the same _pendingActions store as the agent handler. This is why you import handler from the chat route.

Request Body Format

AgentRequestBody
interface AgentRequestBody {
  /** User's message text */
  message: string;
  /** Session ID (generated by client) */
  sessionId: string;
  /** User ID (optional, can be injected server-side) */
  userId?: string;
  /** Current page state with screenshot (optional) */
  pageState?: PageState;
  /** Current page URL (optional) */
  currentUrl?: string;
}

Client Setup

useLensAgent(options)

React hook that communicates with the server-side createAgentHandler via SSE. No API keys needed on the client.

UseLensAgentOptions
interface UseLensAgentOptions {
  /** Server endpoint URL (e.g., '/api/agent/chat') */
  endpoint: string;

  /** Action result endpoint (default: endpoint + '/action-result') */
  actionResultEndpoint?: string;

  /** User ID (optional) */
  userId?: string;

  /** Event callback — called for every SSE event */
  onEvent?: (event: SSEEvent) => void;

  /** Custom headers for requests (e.g., auth tokens) */
  headers?: Record<string, string> | (() => Record<string, string>);

  /**
   * Page state provider — called before each message to capture
   * the current page state (screenshot, markdown, elements).
   */
  getPageState?: () => Promise<PageState>;

  /**
   * Custom action handler for client-side DOM actions.
   * If not provided, uses the built-in WebUseTool.
   */
  onActionRequest?: (action: string, params: Record<string, any>) => Promise<ToolResult>;
}

Return Value

UseLensAgentReturn
interface UseLensAgentReturn {
  /** Conversation message history */
  messages: Message[];

  /** Whether the agent is currently processing */
  isLoading: boolean;

  /** Current session ID */
  sessionId: string;

  /** Last error, if any */
  error: Error | null;

  /** Send a message to the agent */
  sendMessage: (message: string, context?: {
    pageState?: PageState;
    currentUrl?: string;
  }) => Promise<void>;

  /** Abort current agent execution */
  abort: () => void;

  /** Clear conversation messages */
  clearMessages: () => void;

  /** Start a new session (clears messages + generates new sessionId) */
  newSession: () => void;
}

Full Usage Example

src/components/AgentPanel.tsx
'use client';
import { useState, useRef, useEffect } from 'react';
import { useLensAgent } from '@lens-os/sdk/react';
import type { SSEEvent } from '@lens-os/sdk';

interface ToolCard {
  id: string;
  name: string;
  parameters: Record<string, any>;
  result?: any;
  status: 'pending' | 'completed' | 'error';
}

export default function AgentPanel({ userId }: { userId: string }) {
  const [input, setInput] = useState('');
  const [toolCards, setToolCards] = useState<ToolCard[]>([]);
  const messagesEndRef = useRef<HTMLDivElement>(null);

  const {
    messages,
    isLoading,
    error,
    sendMessage,
    abort,
    newSession,
  } = useLensAgent({
    endpoint: '/api/agent/chat',
    userId,
    onEvent: (event: SSEEvent) => {
      if (event.type === 'tool_call') {
        setToolCards(prev => [...prev, {
          id: `tool-${Date.now()}`,
          name: event.name,
          parameters: event.parameters,
          status: 'pending',
        }]);
      } else if (event.type === 'tool_result') {
        setToolCards(prev => prev.map(card =>
          card.name === event.name && card.status === 'pending'
            ? { ...card, result: event.result, status: event.result.success ? 'completed' : 'error' }
            : card
        ));
      }
    },
  });

  useEffect(() => {
    messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
  }, [messages]);

  const handleSend = async () => {
    if (!input.trim() || isLoading) return;
    const msg = input;
    setInput('');
    setToolCards([]);
    await sendMessage(msg);
  };

  return (
    <div className="agent-panel">
      <div className="messages">
        {messages.map((msg, i) => (
          <div key={i} className={`message ${msg.role}`}>
            {typeof msg.content === 'string' ? msg.content : ''}
          </div>
        ))}
        {toolCards.map(card => (
          <div key={card.id} className={`tool-card ${card.status}`}>
            <span>{card.name}</span>
            {card.status === 'pending' && <span>Loading...</span>}
            {card.status === 'completed' && <span>Done</span>}
            {card.status === 'error' && <span>Error</span>}
          </div>
        ))}
        <div ref={messagesEndRef} />
      </div>
      {error && <div className="error">{error.message}</div>}
      <div className="input-area">
        <input
          value={input}
          onChange={(e) => setInput(e.target.value)}
          onKeyDown={(e) => e.key === 'Enter' && handleSend()}
          placeholder="Type a message..."
          disabled={isLoading}
        />
        {isLoading ? (
          <button onClick={abort}>Stop</button>
        ) : (
          <button onClick={handleSend}>Send</button>
        )}
        <button onClick={newSession}>New Chat</button>
      </div>
    </div>
  );
}

useChat — Simplified Alias

useChat is a thin wrapper around useLensAgent with renamed methods:

useChat
import { useChat } from '@lens-os/sdk/react';

const { messages, isLoading, error, send, stop, clear, reset } = useChat({
  endpoint: '/api/agent/chat',
});

// Mapping:
// send → sendMessage
// stop → abort
// clear → clearMessages
// reset → newSession

Complete Next.js Example

A full working example with authentication, tools, and session management.

File Structure

Project Structure
src/
├── app/
│   └── api/
│       └── agent/
│           ├── chat/
│           │   ├── route.ts              # Agent handler
│           │   └── action-result/
│           │       └── route.ts          # Action result handler
│           └── sessions/
│               ├── route.ts              # List sessions
│               └── [id]/
│                   └── route.ts          # Get session messages
├── lib/
│   └── tools.ts                          # Tool executor definitions
└── components/
    └── AgentPanel.tsx                    # Chat UI component

Tool Definitions

src/lib/tools.ts
import type { ToolExecutorConfig, SessionContext } from '@lens-os/sdk';

const BASE_URL = process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:3000';

export function buildToolExecutors(): Record<string, ToolExecutorConfig> {
  return {
    product_search: {
      description: 'Search the product database to find relevant items',
      whenToUse: 'When the user wants to find products, compare items, or browse catalog',
      schema: {
        query: { type: 'string', required: true, description: 'Search keywords' },
        topK: { type: 'number', required: false, default: 10, description: 'Number of results' },
      },
      output: 'Array of products with title, price, imageUrl, description',
      execute: async (params) => {
        try {
          const res = await fetch(`${BASE_URL}/api/products/search`, {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(params),
          });
          return await res.json();
        } catch (err) {
          return { success: false, error: err instanceof Error ? err.message : 'Unknown error' };
        }
      },
    },

    user_orders: {
      description: 'Query or update user orders',
      whenToUse: 'When the user asks about their orders, purchase history, or wants to modify an order',
      schema: {
        action: { type: 'string', required: true, description: '"get" or "update"' },
        orderId: { type: 'string', required: false, description: 'Specific order ID' },
      },
      output: 'Order details with status, items, and amounts',
      execute: async (params, context?: SessionContext) => {
        try {
          const res = await fetch(`${BASE_URL}/api/orders`, {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ ...params, userId: context?.userId }),
          });
          return await res.json();
        } catch (err) {
          return { success: false, error: err instanceof Error ? err.message : 'Unknown error' };
        }
      },
    },
  };
}

Tip

For a complete understanding of tool executors, see the dedicated Tool Executors page.