UNPKG

agentfootprint

Version:

The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.

179 lines • 9.38 kB
"use strict"; /** * session — the session strategies: which AgentCore Runtime session a request * lands in. * * ── The facts they rest on ─────────────────────────────────────────────────── * From the AgentCore Runtime MCP protocol contract and the `InvokeAgentRuntime` * API reference: * * - The runtime routes by `Mcp-Session-Id` and always returns one. A client * "must capture the Mcp-Session-Id returned in the response and include it * in all subsequent requests" for microVM affinity; without it "each request * may be routed to a new microVM". * - `InvokeAgentRuntime` also carries `runtimeSessionId` * (`X-Amzn-Bedrock-AgentCore-Runtime-Session-Id`, 33–256 characters), and * the AWS SDK models it as an idempotency token — it fills a FRESH random id * on every call that leaves it unset. * * ── 'per-call' (the default) ───────────────────────────────────────────────── * Every request in a session of its own: this strategy sends a FRESH runtime * session id with every request and no `Mcp-Session-Id`, and the * `Mcp-Session-Id` a response carries is not handed to the MCP client, so * nothing can carry one call's session into the next. Isolation by * construction: a client shared by every user of a process (tools built once at * startup — the common shape) never puts two users in one microVM. The price is * a cold start per call, and it needs a STATELESS server (`stateless_http=True`, * the runtime's recommended default) — a stateful one sees every request as a * stranger. * * ── 'reuse' ────────────────────────────────────────────────────────────────── * One session per CONNECTION (per `mcpClient` built on the transport), and the * TRANSPORT owns the key. A request with no `Mcp-Session-Id` (a client's * `initialize`) opens a connection with a fresh runtime session id; the * `Mcp-Session-Id` the server returns for it is stored WITH that connection, * and the client is handed an opaque handle of the transport's own in its * place. Every later request carrying the handle is sent in that connection's * runtime session with the server's own id. A handle the transport does not * know — one it never issued, or one it forgot — is never sent: the request * fails with `LocalSessionHandleLostError`, thrown from the transport's * `fetch`. It is not a 404: no server said anything, and the runtime session * behind a forgotten handle may still run — it can no longer be stopped from * here, because its id went with the handle. * * Why a handle and not the server's id: the server's ids are unique only as far * as the server makes them. One that numbers sessions per process hands every * fresh microVM the same first id, and a map keyed by it would send the second * client into the first one's microVM. A handle is ours, random, and per * connection, so two clients on one transport are two sessions whatever the * server returns — while ONE client is still one session for whoever calls * through it. Build a client per conversation; never share a 'reuse' client * between users. * * At most {@link MAX_CONNECTIONS} connections are remembered, least recently * used forgotten first. A client's `DELETE` — sent by `terminate()`, never by * `close()`, which ends only the local side — forgets its own handle first, and * then becomes `StopRuntimeSession` for that connection's runtime session * (`transport.ts`), answered like a server would answer the `DELETE`. * * Pattern: Strategy. Role: outer ring, vendor fact. Holds no credential. */ Object.defineProperty(exports, "__esModule", { value: true }); exports.sessionStrategy = exports.MAX_CONNECTIONS = exports.MCP_SESSION_HEADER = exports.AGENTCORE_RUNTIME_SESSION_HEADER = void 0; const toolSessions_js_1 = require("../../../tools/toolSessions.js"); /** The runtime session header, as `InvokeAgentRuntime` binds `runtimeSessionId` to it. */ exports.AGENTCORE_RUNTIME_SESSION_HEADER = 'X-Amzn-Bedrock-AgentCore-Runtime-Session-Id'; /** The MCP Streamable HTTP session header. */ exports.MCP_SESSION_HEADER = 'Mcp-Session-Id'; /** How many connections a 'reuse' transport remembers before it forgets the least recently used. */ exports.MAX_CONNECTIONS = 256; /** * A fresh runtime session id — `crypto.randomUUID()`, 36 characters, inside the * 33–256 the API takes. Never injectable: freshness IS the isolation, and a * source that repeated itself would put two requests (or two clients) in one * session. */ const newId = () => globalThis.crypto.randomUUID(); /** * Build the strategy for one transport. * * @throws when `kind` is not one of the two strategies. */ function sessionStrategy(kind) { if (kind === 'reuse') return reuse(); if (kind === 'per-call') return perCall(); throw new TypeError(`agentCoreRuntimeMcpTransport: \`session\` must be 'reuse' or 'per-call' — got ` + `${JSON.stringify(kind)}. 'per-call' gives every request its own session (safe to share); ` + `'reuse' keeps one runtime session per client (build one client per conversation).`); } exports.sessionStrategy = sessionStrategy; function reuse() { const connections = new Map(); const remember = (handle, connection) => { connections.delete(handle); connections.set(handle, connection); // most recently used last while (connections.size > exports.MAX_CONNECTIONS) { const oldest = connections.keys().next().value; if (oldest === undefined) break; connections.delete(oldest); } }; return { request(headers, method) { const handle = headers.get(exports.MCP_SESSION_HEADER); const known = handle === null ? undefined : connections.get(handle); // A handle this transport did not issue, or has forgotten: fail closed, // and say what happened — nothing was sent, and nothing answered. if (handle !== null && known === undefined) throw new toolSessions_js_1.LocalSessionHandleLostError(); const connection = known ?? { runtimeId: newId() }; if (handle !== null) { // Ending the session: the handle is forgotten FIRST, so nothing more is // ever sent under it, then the runtime session is stopped. if (method === 'DELETE') { connections.delete(handle); return { stop: connection.runtimeId }; } remember(handle, connection); } // Set, not merged: these are the only session ids this request sends. headers.set(exports.AGENTCORE_RUNTIME_SESSION_HEADER, connection.runtimeId); if (connection.serverSessionId !== undefined) { headers.set(exports.MCP_SESSION_HEADER, connection.serverSessionId); } else { headers.delete(exports.MCP_SESSION_HEADER); } return { settle: (response) => { const returned = response.headers.get(exports.MCP_SESSION_HEADER); if (returned === null) return response; if (handle === null) { // A new connection: bound under a handle of our own, shown in the server's id's place. const issued = newId(); remember(issued, { ...connection, serverSessionId: returned }); return withSessionHeader(response, issued); } connection.serverSessionId = returned; return withSessionHeader(response, handle); }, }; }, }; } function perCall() { return { request(headers) { headers.delete(exports.MCP_SESSION_HEADER); // A fresh runtime session of our own for every request — as fresh as the // SDK's own default, and set the same way on both auth paths. headers.set(exports.AGENTCORE_RUNTIME_SESSION_HEADER, newId()); return { settle: (response) => withSessionHeader(response, undefined) }; }, }; } /** * The response with its `Mcp-Session-Id` replaced by `value`, or removed when * `value` is undefined. A fetched Response's headers are immutable, so the copy * is rebuilt around the same body rather than edited. */ function withSessionHeader(response, value) { const current = response.headers.get(exports.MCP_SESSION_HEADER); if (current === (value ?? null)) return response; const headers = new Headers(response.headers); if (value === undefined) headers.delete(exports.MCP_SESSION_HEADER); else headers.set(exports.MCP_SESSION_HEADER, value); return new Response(response.body, { status: response.status, statusText: response.statusText, headers, }); } //# sourceMappingURL=session.js.map