Managed agents endpoints addition
This commit is contained in:
@@ -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 */
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user