# Alchemist: Embedded Agent — Backend Client Call a bound Chipp agent from your application server for search, recommendations, or autonomous workflows. --- The **backend client** lets your application server invoke a bound Chipp agent programmatically. Use it to power search features, generate recommendations, automate workflows, or any server-side task that benefits from an AI agent. Unlike the frontend UI (inline panel or widget), the backend client is never exposed to the browser. Your server controls the request, manages timeouts and retries, and processes the response. ## Installation The embedded-Chipp-agent feature pack includes the backend client library. Install it in your project: ```bash npm install @chipp/embed-client # or deno add npm:@chipp/embed-client ``` ## Quick start ### 1. Initialize the client In your backend (routes, jobs, or services): ```typescript import { ChippEmbedServerClient } from '@chipp/embed-client/server'; const client = new ChippEmbedServerClient({ // Your Alchemist project endpoint (e.g., https://api.example.com) baseUrl: process.env.ALCHEMIST_API_URL, // Your project's client key (never expose to the browser) projectClientKey: process.env.ALCHEMIST_PROJECT_CLIENT_KEY, }); ``` ### 2. Make a request **Non-streaming** (wait for complete response): ```typescript const response = await client.sendMessageAndWait({ // Required: user's identity userId: 'user@example.com', // Authenticated email // OR // anonymousSessionId: 'anon_...', // Anonymous visitor (from frontend) // Required: the message message: 'What are the trending products this week?', // Optional: conversation context threadId: 'existing-thread-id', // Resume a conversation // Optional: metadata metadata: { source: 'search-widget', user_plan: 'pro' }, // Optional: timeout timeout: 30_000, // 30 seconds }); console.log(response.message); // Agent's reply console.log(response.threadId); // For resuming later ``` **Streaming** (receive response tokens in real-time): ```typescript const stream = await client.sendMessage({ userId: 'user@example.com', message: 'Summarize my last 10 orders', }); for await (const event of stream) { if (event.type === 'message_chunk') { process.stdout.write(event.text); } else if (event.type === 'tool_call') { console.log('Agent called tool:', event.tool, event.input); } else if (event.type === 'citation') { console.log('Citation:', event.source, event.text); } } ``` ## Client initialization options ```typescript interface ChippEmbedServerClientConfig { // Required baseUrl: string; // Alchemist project API URL projectClientKey: string; // Project client key (alch_proj_...) // Optional timeout?: number; // Default request timeout in ms (default: 30000) retryAttempts?: number; // Max retry attempts for transient errors (default: 3) retryDelay?: number; // Base retry delay in ms (default: 1000) retryJitter?: boolean; // Add jitter to retry delays (default: true) } ``` ## Request options ```typescript interface SendMessageOptions { // Identity: exactly one of these userId?: string; // Authenticated user email anonymousSessionId?: string; // Anonymous session ID externalTenantId?: string; // Multi-tenant context // Required message: string; // The user's message // Optional threadId?: string; // Existing conversation to continue metadata?: Record; // Context (source, plan, etc.) clientMessageId?: string; // For idempotency timeout?: number; // Override default timeout signal?: AbortSignal; // Cancellation signal } ``` ## Non-streaming response ```typescript interface SendMessageAndWaitResponse { threadId: string; // Conversation ID message: string; // Agent's reply citations?: Citation[]; // Sources cited toolCalls?: ToolCall[]; // Tools executed files?: FileAttachment[]; // Files returned by agent metadata?: Record; // Agent-supplied context } ``` ## Streaming response When you stream, you receive a series of events: ```typescript type StreamEvent = | { type: 'message_chunk'; text: string } | { type: 'message_start'; threadId: string } | { type: 'message_complete'; message: string } | { type: 'tool_call'; tool: string; input: unknown; callId: string } | { type: 'tool_result'; callId: string; result: string } | { type: 'citation'; source: string; text: string; url?: string } | { type: 'file'; filename: string; mimeType: string; data: string } | { type: 'error'; error: string }; ``` Example stream processor: ```typescript const stream = await client.sendMessage({ userId: 'user@example.com', message: 'Search for customer complaints', }); let fullMessage = ''; const tools: Array<{ tool: string; input: unknown }> = []; const citations: Array<{ source: string; text: string }> = []; for await (const event of stream) { switch (event.type) { case 'message_chunk': fullMessage += event.text; break; case 'tool_call': tools.push({ tool: event.tool, input: event.input }); break; case 'citation': citations.push({ source: event.source, text: event.text }); break; case 'error': console.error('Stream error:', event.error); break; } } return { message: fullMessage, tools, citations }; ``` ## Identity management ### Authenticated users For authenticated end-users, pass their email: ```typescript const response = await client.sendMessageAndWait({ userId: 'alice@example.com', // ← Email is the unique ID message: 'Show my account activity', }); ``` **Important:** The `userId` should be stable and unique per user (typically an email or internal ID). Conversations are attributed to this ID; changing it creates a new conversation. ### Anonymous/visitor sessions For unauthenticated visitors, use their session ID (from the frontend): ```typescript // From your frontend (stored in browser) const sessionId = getStoredAnonymousSessionId(); const response = await client.sendMessageAndWait({ anonymousSessionId: sessionId, message: 'Help me find products', }); ``` The backend client never creates anonymous sessions; the frontend must mint them via the session endpoint. ### Multi-tenant context For multi-tenant applications, optionally pass tenant context: ```typescript const response = await client.sendMessageAndWait({ userId: 'user@example.com', externalTenantId: 'tenant_123', // Optional, for context message: 'Generate a report for this tenant', metadata: { tenant_id: 'tenant_123', tenant_plan: 'enterprise' }, }); ``` ## Retries & error handling The client automatically retries transient errors (network timeouts, 5xx responses, etc.). Customize retry behavior: ```typescript const client = new ChippEmbedServerClient({ baseUrl: process.env.ALCHEMIST_API_URL, projectClientKey: process.env.ALCHEMIST_PROJECT_CLIENT_KEY, retryAttempts: 5, // Max retries retryDelay: 500, // Initial delay in ms retryJitter: true, // Add randomness to avoid thundering herd }); ``` ### Handling errors All errors are normalized to `EmbedServerClientError`: ```typescript import { EmbedServerClientError } from '@chipp/embed-client/server'; try { const response = await client.sendMessageAndWait({ userId: 'user@example.com', message: 'Help me', }); } catch (error) { if (error instanceof EmbedServerClientError) { console.error('Code:', error.code); // AUTH_FAILED, TIMEOUT, etc. console.error('Message:', error.message); // Human-readable console.error('Retryable:', error.retryable); // Should we retry? console.error('Status:', error.statusCode); // HTTP status, if applicable } else { // Unknown error throw error; } } ``` ### Error codes | Code | Meaning | Retryable | |------|---------|-----------| | `AUTH_FAILED` | Invalid client key or session | No | | `SESSION_EXPIRED` | Session TTL exceeded | No | | `UNAUTHORIZED` | User not authorized for this binding | No | | `TIMEOUT` | Request exceeded timeout | Yes | | `NETWORK_ERROR` | Connection failed | Yes | | `SERVICE_UNAVAILABLE` | Agent or backend is down | Yes | | `RATE_LIMITED` | Too many requests | Yes | | `VALIDATION_ERROR` | Invalid input (empty message, etc.) | No | ## Cancellation & timeouts ### Custom timeout per request ```typescript const response = await client.sendMessageAndWait({ userId: 'user@example.com', message: 'Quick question', timeout: 5_000, // 5 seconds for this request }); ``` ### Cancellation via AbortSignal ```typescript const controller = new AbortController(); // Set a 30-second timeout const timeoutId = setTimeout(() => controller.abort(), 30_000); try { const response = await client.sendMessageAndWait({ userId: 'user@example.com', message: 'Generate a report', signal: controller.signal, }); } finally { clearTimeout(timeoutId); } ``` ### Cancelling a stream ```typescript const controller = new AbortController(); const stream = await client.sendMessage({ userId: 'user@example.com', message: 'Stream results', signal: controller.signal, }); // Cancel after receiving first 10 events let eventCount = 0; for await (const event of stream) { console.log(event); if (++eventCount >= 10) { controller.abort(); // Stops the iteration } } ``` ## Idempotency For operations that must not be retried (e.g., order placement), provide a client-generated message ID: ```typescript const response = await client.sendMessageAndWait({ userId: 'user@example.com', message: 'Place order ABC123', clientMessageId: 'order-abc123-1692360000', // Unique per request }); ``` If the request fails and is retried, the same `clientMessageId` ensures the agent does not process the order twice. ## Common use cases ### Search feature ```typescript app.post('/search', async (req) => { const { query, userId } = req.body; const response = await client.sendMessageAndWait({ userId, message: `Search for: ${query}`, metadata: { source: 'search-page' }, timeout: 10_000, // Fast timeout for search }); return response.json({ results: response.message }); }); ``` ### Recommendation engine ```typescript async function generateRecommendations(userId: string, context: any) { const prompt = `Based on this context, generate 5 recommendations: ${JSON.stringify(context)}`; const response = await client.sendMessageAndWait({ userId, message: prompt, metadata: { source: 'recommendation-engine' }, }); // Parse agent's response (can be structured or free-form) return response.message; } ``` ### Autonomous workflow / job ```typescript async function processRefund(orderId: string) { const response = await client.sendMessageAndWait({ // Server-side operation, no specific user userId: 'system@internal', message: `Process refund for order ${orderId}. Check status, contact customer if needed.`, metadata: { source: 'async-job', operation: 'refund' }, timeout: 60_000, // Longer timeout for complex job }); console.log('Refund processing result:', response.message); return response; } ``` ### Conversation resumption ```typescript async function continueCustomerChat(userId: string, threadId: string) { const response = await client.sendMessageAndWait({ userId, threadId, // Resume previous conversation message: 'What else can I help with?', }); return response; } ``` ## Billing & attribution Requests via the backend client are billed through the normal Chipp metering path, attributed to the organization that owns the bound agent's application. Your organization receives the invoice; no separate billing occurs. For cost control, consider: - Setting reasonable timeouts to avoid long-running requests - Monitoring token usage via the Chipp dashboard - Implementing rate limiting on backend endpoints that call the agent ## Troubleshooting ### "AUTH_FAILED: Invalid client key" - Verify `projectClientKey` is set correctly: `alch_proj_...` - Check that the key's scope is `"full"` (if restricted, it cannot access embed endpoints) - Rotate the key: **Project Settings → API Keys → Rotate Client Key** ### "SESSION_EXPIRED: Token TTL exceeded" - Verify the bound agent is online (check Chipp workspace) - For long-running operations, use a shorter streaming window or break into multiple requests - Anonymous sessions expire after ~1 hour; authenticated sessions last ~8 hours ### "SERVICE_UNAVAILABLE" - Check if the bound Chipp agent is online - Verify the agent's backend is responding (check Chipp infrastructure status) - The client will automatically retry; monitor error frequency ### "VALIDATION_ERROR: Message required" - Ensure the `message` field is non-empty - Check that one of `userId` or `anonymousSessionId` is provided ### "Timeout on streaming requests" - Increase the timeout for the request - If streaming, process events incrementally (don't wait for the entire stream to buffer) - Consider breaking large requests into multiple smaller requests ## Next steps - **[Inline panel](/docs/guides/alchemist-embedded-chipp-agent-inline-panel/)** — Embed UI for end users - **[Floating widget](/docs/guides/alchemist-embedded-chipp-agent-widget/)** — Persistent chat widget - **[Installation guide](/docs/guides/alchemist-embedded-chipp-agent-install/)** — Bind an agent to your project