diff --git a/netlify/functions/agent-proxy.js b/netlify/functions/agent-proxy.js index bd07ba7..1c087ba 100644 --- a/netlify/functions/agent-proxy.js +++ b/netlify/functions/agent-proxy.js @@ -3,19 +3,29 @@ import Anthropic from '@anthropic-ai/sdk' /** * Netlify Functions v2 handler. * - * Proxies a request from the React frontend to the Anthropic Messages API, - * configured for a Managed Agent workflow. We pipe `content_block_delta` - * events directly to the client through a ReadableStream so we never hit - * Netlify's 26-second synchronous execution limit. + * Proxies a request from the React frontend to a Claude Console-defined + * Managed Agent via the /v1/sessions endpoints. The agent's model, system + * prompt, tools, MCP servers, and skills are configured in the Console and + * referenced here by ID. We open the SSE event stream first, then send a + * `user.message` event, and pipe each `agent.message` text block to the + * client through a ReadableStream so we never hit Netlify's 26-second + * synchronous execution limit. */ // --------------------------------------------------------------------------- // Managed Agent configuration // --------------------------------------------------------------------------- -// The Managed Agent ID is supplied via the ANTHROPIC_AGENT_ID env var. The -// agent's system prompt and model are defined in the Claude Console and used -// by default. ANTHROPIC_MODEL and ANTHROPIC_SYSTEM_PROMPT may optionally -// override them. +// The Managed Agent ID and Environment ID come from the Claude Console. +// Optionally, vault IDs may be passed for MCP credential access. +const AGENT_ID = process.env.ANTHROPIC_AGENT_ID +const ENVIRONMENT_ID = process.env.ANTHROPIC_ENVIRONMENT_ID +const VAULT_IDS = (process.env.ANTHROPIC_VAULT_IDS || '') + .split(',') + .map((s) => s.trim()) + .filter(Boolean) + +// All Managed Agents endpoints require this beta header. +const MANAGED_AGENTS_BETA = 'managed-agents-2026-04-01' // --------------------------------------------------------------------------- // Anthropic client (reused across warm Lambda invocations) @@ -28,6 +38,7 @@ function getClient() { } _client = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY, + defaultHeaders: { 'anthropic-beta': MANAGED_AGENTS_BETA }, }) return _client } @@ -60,6 +71,13 @@ export default async (req /*, context */) => { return new Response('Invalid station website URL', { status: 400 }) } + if (!AGENT_ID || !ENVIRONMENT_ID) { + return new Response( + 'Server is missing ANTHROPIC_AGENT_ID or ANTHROPIC_ENVIRONMENT_ID', + { status: 500 }, + ) + } + let client try { client = getClient() @@ -78,54 +96,83 @@ export default async (req /*, context */) => { Please follow your instructions to produce the funding outlook brief.` - // ----- Build the streaming request ----- - // The Managed Agent's system prompt and model (configured in the Claude - // Console) are used by default. Optional env vars can override them. - const streamArgs = { - model: process.env.ANTHROPIC_MODEL || 'claude-opus-4-6', - max_tokens: 20000, - messages: [ - { - role: 'user', - content: [{ type: 'text', text: userMessage }], - }, - ], - } - if (process.env.ANTHROPIC_AGENT_ID) { - streamArgs.agent_id = process.env.ANTHROPIC_AGENT_ID - } - if (process.env.ANTHROPIC_SYSTEM_PROMPT) { - streamArgs.system = process.env.ANTHROPIC_SYSTEM_PROMPT + // ----- Create the session ----- + let session + try { + const sessionParams = { + agent: AGENT_ID, + environment_id: ENVIRONMENT_ID, + } + if (VAULT_IDS.length) sessionParams.vault_ids = VAULT_IDS + session = await client.beta.sessions.create(sessionParams) + } catch (err) { + return new Response( + `Failed to create session: ${err.message || err}`, + { status: 500 }, + ) } - const upstream = client.messages.stream(streamArgs) + // ----- Open the event stream BEFORE sending the user message ----- + // (Stream-first ensures we don't miss any events the agent emits.) + let upstream + try { + upstream = await client.beta.sessions.events.stream(session.id) + } catch (err) { + return new Response( + `Failed to open session event stream: ${err.message || err}`, + { status: 500 }, + ) + } - // ----- Pipe content_block_delta events straight to the client ----- + // ----- Send the kickoff user.message event ----- + try { + await client.beta.sessions.events.send(session.id, { + events: [ + { + type: 'user.message', + content: [{ type: 'text', text: userMessage }], + }, + ], + }) + } catch (err) { + return new Response( + `Failed to send user message: ${err.message || err}`, + { status: 500 }, + ) + } + + // ----- Pipe agent.message text blocks to the client ----- const encoder = new TextEncoder() + const seenEventIds = new Set() const stream = new ReadableStream({ async start(controller) { try { for await (const event of upstream) { - if ( - event.type === 'content_block_delta' && - event.delta?.type === 'text_delta' && - event.delta.text - ) { - controller.enqueue(encoder.encode(event.delta.text)) + if (event.id) { + if (seenEventIds.has(event.id)) { + // Still need to evaluate terminal events even if seen. + } else { + seenEventIds.add(event.id) + if (event.type === 'agent.message' && Array.isArray(event.content)) { + for (const block of event.content) { + if (block?.type === 'text' && block.text) { + controller.enqueue(encoder.encode(block.text)) + } + } + } else if (event.type === 'session.error') { + const msg = event.error?.message || 'unknown session error' + controller.enqueue(encoder.encode(`\n\n[session error: ${msg}]`)) + } + } } - // Surface an explicit message if the agent decides to call a tool. - // (Managed Agents may emit `input_json_delta` or `tool_use` blocks.) + // Terminal-state gate. + if (event.type === 'session.status_terminated') break if ( - event.type === 'content_block_delta' && - event.delta?.type === 'input_json_delta' && - event.delta.partial_json + event.type === 'session.status_idle' && + event.stop_reason?.type !== 'requires_action' ) { - controller.enqueue( - encoder.encode( - `\n\n_[agent requested tool input: ${event.delta.partial_json}]_\n\n`, - ), - ) + break } } controller.close() @@ -137,8 +184,12 @@ Please follow your instructions to produce the funding outlook brief.` } }, cancel() { - // If the client disconnects, abort the upstream Anthropic request. - upstream.controller?.abort?.() + // If the client disconnects, abort the upstream stream. + try { + upstream.controller?.abort?.() + } catch { + /* ignore */ + } }, })