Managed agents endpoints addition

This commit is contained in:
2026-05-12 22:10:33 -04:00
parent 09b1021d63
commit 822944958f
+97 -46
View File
@@ -3,19 +3,29 @@ import Anthropic from '@anthropic-ai/sdk'
/** /**
* Netlify Functions v2 handler. * Netlify Functions v2 handler.
* *
* Proxies a request from the React frontend to the Anthropic Messages API, * Proxies a request from the React frontend to a Claude Console-defined
* configured for a Managed Agent workflow. We pipe `content_block_delta` * Managed Agent via the /v1/sessions endpoints. The agent's model, system
* events directly to the client through a ReadableStream so we never hit * prompt, tools, MCP servers, and skills are configured in the Console and
* Netlify's 26-second synchronous execution limit. * 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 // Managed Agent configuration
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// The Managed Agent ID is supplied via the ANTHROPIC_AGENT_ID env var. The // The Managed Agent ID and Environment ID come from the Claude Console.
// agent's system prompt and model are defined in the Claude Console and used // Optionally, vault IDs may be passed for MCP credential access.
// by default. ANTHROPIC_MODEL and ANTHROPIC_SYSTEM_PROMPT may optionally const AGENT_ID = process.env.ANTHROPIC_AGENT_ID
// override them. 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) // Anthropic client (reused across warm Lambda invocations)
@@ -28,6 +38,7 @@ function getClient() {
} }
_client = new Anthropic({ _client = new Anthropic({
apiKey: process.env.ANTHROPIC_API_KEY, apiKey: process.env.ANTHROPIC_API_KEY,
defaultHeaders: { 'anthropic-beta': MANAGED_AGENTS_BETA },
}) })
return _client return _client
} }
@@ -60,6 +71,13 @@ export default async (req /*, context */) => {
return new Response('Invalid station website URL', { status: 400 }) 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 let client
try { try {
client = getClient() client = getClient()
@@ -78,54 +96,83 @@ export default async (req /*, context */) => {
Please follow your instructions to produce the funding outlook brief.` Please follow your instructions to produce the funding outlook brief.`
// ----- Build the streaming request ----- // ----- Create the session -----
// The Managed Agent's system prompt and model (configured in the Claude let session
// Console) are used by default. Optional env vars can override them. try {
const streamArgs = { const sessionParams = {
model: process.env.ANTHROPIC_MODEL || 'claude-opus-4-6', agent: AGENT_ID,
max_tokens: 20000, environment_id: ENVIRONMENT_ID,
messages: [ }
{ if (VAULT_IDS.length) sessionParams.vault_ids = VAULT_IDS
role: 'user', session = await client.beta.sessions.create(sessionParams)
content: [{ type: 'text', text: userMessage }], } catch (err) {
}, return new Response(
], `Failed to create session: ${err.message || err}`,
} { status: 500 },
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
} }
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 encoder = new TextEncoder()
const seenEventIds = new Set()
const stream = new ReadableStream({ const stream = new ReadableStream({
async start(controller) { async start(controller) {
try { try {
for await (const event of upstream) { for await (const event of upstream) {
if ( if (event.id) {
event.type === 'content_block_delta' && if (seenEventIds.has(event.id)) {
event.delta?.type === 'text_delta' && // Still need to evaluate terminal events even if seen.
event.delta.text } else {
) { seenEventIds.add(event.id)
controller.enqueue(encoder.encode(event.delta.text)) 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. // Terminal-state gate.
// (Managed Agents may emit `input_json_delta` or `tool_use` blocks.) if (event.type === 'session.status_terminated') break
if ( if (
event.type === 'content_block_delta' && event.type === 'session.status_idle' &&
event.delta?.type === 'input_json_delta' && event.stop_reason?.type !== 'requires_action'
event.delta.partial_json
) { ) {
controller.enqueue( break
encoder.encode(
`\n\n_[agent requested tool input: ${event.delta.partial_json}]_\n\n`,
),
)
} }
} }
controller.close() controller.close()
@@ -137,8 +184,12 @@ Please follow your instructions to produce the funding outlook brief.`
} }
}, },
cancel() { cancel() {
// If the client disconnects, abort the upstream Anthropic request. // If the client disconnects, abort the upstream stream.
upstream.controller?.abort?.() try {
upstream.controller?.abort?.()
} catch {
/* ignore */
}
}, },
}) })