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.
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
const handler = createAgentHandler(config);
handler.POST // (req: Request) => Promise<Response> — the route handler
handler._pendingActions // PendingActionStore — pass to createActionResultHandlerAdding Authentication
The handler expects { message, sessionId, userId? } in the request body. You can wrap the handler to inject authentication:
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.
import { createActionResultHandler } from '@lens-os/sdk/server';
import { handler } from '../route';
export const { POST } = createActionResultHandler(handler._pendingActions);Note
_pendingActions store as the agent handler. This is why you import handler from the chat route.Request Body Format
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.
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
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
'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:
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 → newSessionComplete Next.js Example
A full working example with authentication, tools, and session management.
File 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 componentTool Definitions
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