@axlsdk/studio 0.21.1 → 0.22.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/{chunk-YGLWYDQ3.js → chunk-Z374DI7D.js} +226 -11
- package/dist/chunk-Z374DI7D.js.map +1 -0
- package/dist/cli.cjs +226 -11
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +2 -2
- package/dist/cli.js.map +1 -1
- package/dist/client/assets/index-7nGvwNtb.js +318 -0
- package/dist/client/assets/index-Bc4iQdXy.css +1 -0
- package/dist/client/index.html +2 -2
- package/dist/middleware.cjs +225 -10
- package/dist/middleware.cjs.map +1 -1
- package/dist/middleware.js +1 -1
- package/dist/middleware.js.map +1 -1
- package/dist/server/index.cjs +225 -10
- package/dist/server/index.cjs.map +1 -1
- package/dist/server/index.js +1 -1
- package/package.json +5 -5
- package/dist/chunk-YGLWYDQ3.js.map +0 -1
- package/dist/client/assets/index-C8R_CZcW.css +0 -1
- package/dist/client/assets/index-Dq8QQtXe.js +0 -313
package/dist/middleware.js
CHANGED
package/dist/middleware.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/middleware.ts","../src/eval-loader.ts"],"sourcesContent":["import { resolve, dirname } from 'node:path';\nimport { existsSync } from 'node:fs';\nimport { fileURLToPath } from 'node:url';\nimport { getRequestListener } from '@hono/node-server';\nimport { WebSocketServer } from 'ws';\nimport { createServer } from './server/index.js';\nimport { handleWsMessage } from './server/ws/protocol.js';\nimport { createEvalLoader } from './eval-loader.js';\nimport type { EvalLoaderConfig } from './eval-loader.js';\nimport type { BufferCaps } from './server/ws/connection-manager.js';\nimport type { AxlRuntime } from '@axlsdk/axl';\nimport type { IncomingMessage, ServerResponse, Server } from 'node:http';\n\nexport type { EvalLoaderConfig } from './eval-loader.js';\nexport type { BufferCaps } from './server/ws/connection-manager.js';\n\nexport type StudioMiddlewareOptions = {\n /** The AxlRuntime instance to observe and control. */\n runtime: AxlRuntime;\n\n /**\n * URL path prefix where Studio is mounted.\n * Must match the mount path in your framework (Express `app.use()`,\n * Fastify `register()`, Hono `app.route()`, etc.). The framework is\n * expected to strip the prefix from `req.url` before calling the handler.\n *\n * Do not set basePath when using a raw `http.Server` as the root handler —\n * leave it empty and mount at root instead.\n *\n * Must start with '/' when non-empty. Trailing slashes are stripped.\n * Only URL-safe characters allowed: [a-zA-Z0-9/_-]\n *\n * @example '/studio'\n * @example '/admin/studio'\n * @example '' — mounted at root (default)\n */\n basePath?: string;\n\n /**\n * Serve the pre-built Studio SPA.\n * Set to false if serving the client from a CDN or separate build.\n * @default true\n */\n serveClient?: boolean;\n\n /**\n * Verify a WebSocket upgrade request before completing the handshake.\n *\n * Return either a boolean (`true` allow, `false` reject) or an object\n * `{ allowed: boolean, metadata?: unknown }` to ALSO attach per-connection\n * metadata that `filterTraceEvent` can later read — typically the auth\n * subject (userId, tenantId, role). Throw to reject with an error.\n *\n * IMPORTANT: WebSocket upgrades bypass Express/Fastify/Koa middleware.\n * If your HTTP routes are behind auth middleware, WS connections are NOT\n * automatically protected. Use this callback to enforce authentication\n * on WebSocket connections.\n *\n * @example Multi-tenant: extract tenant from JWT, attach as metadata\n * verifyUpgrade: async (req) => {\n * const token = req.headers.authorization?.slice(7);\n * const claims = await verifyJwt(token);\n * if (!claims) return false;\n * return { allowed: true, metadata: { userId: claims.sub, tenantId: claims.tid } };\n * }\n */\n verifyUpgrade?: (\n req: IncomingMessage,\n ) =>\n | boolean\n | { allowed: boolean; metadata?: unknown }\n | Promise<boolean | { allowed: boolean; metadata?: unknown }>;\n\n /**\n * Permit `upgradeWebSocket()` without `verifyUpgrade` in production.\n * This exposes Studio's administrative event channels without SDK-level\n * authentication and should be used only when an outer upgrade gate is\n * independently guaranteed.\n * @default false\n */\n dangerouslyAllowUnauthenticatedWebSockets?: boolean;\n\n /**\n * Optional per-event filter used by multi-tenant deployments to scope the\n * trace firehose. Called on every outbound WebSocket broadcast; return\n * `true` to deliver to this connection, `false` to drop.\n *\n * `event` is the parsed event payload — for the `trace:*` channel this is\n * an `AxlEvent` from `@axlsdk/axl` (narrow via the discriminated union);\n * for `costs` it's a `CostData`; for `execution:*` / `eval:*` it's the\n * legacy stream-event shape (until PR 3 collapses the wire to `AxlEvent`).\n * Typed `unknown` because the filter runs for every channel.\n * `metadata` is the per-connection metadata attached by `verifyUpgrade`,\n * or `undefined` if `verifyUpgrade` returned a bare boolean.\n *\n * Filter errors are treated as `drop` (fail-closed) so a buggy predicate\n * cannot accidentally leak events cross-tenant.\n *\n * @example Scope trace events by tenant id stored on agent metadata\n * import type { AxlEvent } from '@axlsdk/axl';\n *\n * filterTraceEvent: (event, meta) => {\n * const m = meta as { tenantId?: string } | undefined;\n * if (!m?.tenantId) return false;\n * const e = event as AxlEvent;\n * // Only narrow event.type === 'agent_call_end' events; pass structural\n * // events (workflow_start/end, cost updates) through unconditionally.\n * if (e.type !== 'agent_call_end') return true;\n * return e.workflow?.startsWith(`${m.tenantId}:`) ?? false;\n * }\n */\n filterTraceEvent?: (event: unknown, metadata: unknown) => boolean;\n\n /**\n * Disable all mutating endpoints (execute, test, send, delete, resolve).\n * When true, Studio is observation-only.\n * @default false\n */\n readOnly?: boolean;\n\n /**\n * Lazy-load eval files for the Eval Runner panel.\n *\n * Eval files are dynamically imported on first access to eval endpoints,\n * not at middleware construction time. This means:\n * - Zero cost during normal API operation\n * - Eval files can import from any module without creating circular deps\n * in the static module graph (they're loaded as standalone entry points)\n * - `@axlsdk/eval` can remain a devDependency — eval files never enter\n * the production bundle since bundlers can't see dynamic imports\n *\n * Accepts glob patterns, explicit file paths, or an object with\n * `conditions` for monorepo source export resolution.\n *\n * Eval files are loaded once and cached for the middleware's lifetime.\n * Changes to eval files require a server restart.\n *\n * @example\n * // Single glob pattern\n * evals: 'evals/*.eval.ts'\n *\n * @example\n * // Multiple patterns\n * evals: ['libs/api/evals/*.eval.ts', 'libs/ai/evals/*.eval.ts']\n *\n * @example\n * // With import conditions for monorepo source exports\n * evals: {\n * files: 'libs/api/evals/*.eval.ts',\n * conditions: ['development'],\n * }\n */\n evals?: EvalLoaderConfig;\n\n /**\n * Override the default WebSocket replay-buffer resource caps for\n * production deployments under sustained execution churn.\n *\n * Defaults: `maxEventsPerBuffer: 1000`, `maxBytesPerBuffer: 4 MiB`,\n * `maxActiveBuffers: 256`. Worst-case memory is roughly\n * `maxActiveBuffers × maxBytesPerBuffer` (≈1 GiB at defaults). Tighten\n * either dimension to lower the ceiling at the cost of late-subscriber\n * replay coverage; raise them if you need more event headroom on long\n * verbose-mode streams.\n *\n * Terminal `done` / `error` events are always buffered regardless of\n * caps, so a late subscriber to a completed stream still sees the\n * outcome — only intermediate non-terminal events are dropped.\n *\n * All fields are optional; passing `{}` is a no-op.\n *\n * @example\n * // Halve memory ceiling on a high-churn deployment\n * bufferCaps: { maxActiveBuffers: 128, maxBytesPerBuffer: 2 * 1024 * 1024 }\n */\n bufferCaps?: BufferCaps;\n};\n\n/**\n * Minimal contract a WebSocket connection must satisfy.\n * Matches the `ws` library API (de facto standard in Node.js).\n */\nexport interface StudioWebSocket {\n send(data: string): void;\n close(): void;\n on(event: 'message', fn: (data: string | Buffer) => void): void;\n on(event: 'close', fn: () => void): void;\n on(event: 'error', fn: (err: Error) => void): void;\n}\n\n// Re-export for Hono-in-Hono consumers\nexport { handleWsMessage } from './server/ws/protocol.js';\n\nexport type StudioMiddleware = ReturnType<typeof createStudioMiddleware>;\n\nexport function createStudioMiddleware(options: StudioMiddlewareOptions) {\n const {\n runtime,\n serveClient = true,\n verifyUpgrade,\n dangerouslyAllowUnauthenticatedWebSockets = false,\n readOnly = false,\n filterTraceEvent,\n } = options;\n\n // Normalize basePath: strip trailing slashes, validate format\n const basePath = normalizeBasePath(options.basePath);\n\n // Resolve pre-built SPA assets from this package's dist/\n const staticRoot = serveClient ? resolveClientDist() : undefined;\n\n if (serveClient && !staticRoot) {\n const dir =\n import.meta.dirname ??\n (typeof __dirname !== 'undefined' ? __dirname : dirname(fileURLToPath(import.meta.url)));\n console.warn(\n '[axl-studio] serveClient is true but no pre-built client found at ' +\n `${resolve(dir, 'client')}. Studio UI will not be available. ` +\n 'Set serveClient: false to suppress this warning.',\n );\n }\n\n // Create lazy eval loader if eval files are configured\n const evalLoader = options.evals ? createEvalLoader(options.evals, runtime) : undefined;\n\n const { app, connMgr, traceListener, closeActiveRuns, closeAggregators } = createServer({\n runtime,\n staticRoot,\n basePath,\n readOnly,\n cors: false, // Host framework owns CORS policy\n evalLoader,\n bufferCaps: options.bufferCaps,\n });\n\n // Install the broadcast filter once at construction time. The connMgr\n // applies it on every outbound event; `metadata` is attached to each\n // connection after a successful `verifyUpgrade`.\n if (filterTraceEvent) {\n connMgr.setFilter(filterTraceEvent);\n }\n\n // Warn early; upgradeWebSocket() also fails closed unless explicitly acknowledged.\n if (\n process.env.NODE_ENV === 'production' &&\n !verifyUpgrade &&\n !dangerouslyAllowUnauthenticatedWebSockets\n ) {\n console.warn(\n '[axl-studio] WARNING: Studio middleware mounted in production without verifyUpgrade. ' +\n 'upgradeWebSocket() will refuse to attach. Provide verifyUpgrade or explicitly set ' +\n 'dangerouslyAllowUnauthenticatedWebSockets: true only when an outer upgrade gate is guaranteed.',\n );\n }\n\n // Convert Hono app → Node.js (req, res) handler via @hono/node-server.\n // overrideGlobalObjects: false prevents replacing global.Request/Response,\n // which could break the host application.\n const listener = getRequestListener(app.fetch, {\n overrideGlobalObjects: false,\n });\n\n let closed = false;\n\n function handler(req: IncomingMessage, res: ServerResponse) {\n if (closed) {\n res.writeHead(503, { 'Content-Type': 'application/json' });\n res.end(\n JSON.stringify({\n ok: false,\n error: { code: 'CLOSED', message: 'Studio middleware has been shut down' },\n }),\n );\n return;\n }\n\n // Express/NestJS/Koa body parsers consume the raw IncomingMessage stream\n // and store the parsed result on req.body. When that happens,\n // @hono/node-server's getRequestListener reads an empty stream and Hono\n // sees no body. To fix this, we re-serialize req.body as req.rawBody —\n // a Buffer that getRequestListener checks before falling back to the\n // stream (see newRequestFromIncoming in @hono/node-server/dist/listener.js).\n //\n // Verified against @hono/node-server@1.19.9. If this breaks after an\n // upgrade, check whether the rawBody instanceof Buffer check still exists\n // in newRequestFromIncoming.\n const reqAny = req as unknown as Record<string, unknown>;\n if (reqAny.body != null && !reqAny.rawBody) {\n try {\n reqAny.rawBody = Buffer.from(JSON.stringify(reqAny.body));\n } catch {\n // Non-serializable body — Hono will see an empty body\n }\n }\n\n listener(req, res).catch((err) => {\n console.error('[axl-studio] Unhandled error in request handler:', err);\n if (!res.headersSent) {\n res.writeHead(500, { 'Content-Type': 'application/json' });\n res.end(\n JSON.stringify({\n ok: false,\n error: { code: 'INTERNAL_ERROR', message: 'Internal server error' },\n }),\n );\n }\n });\n }\n\n // Handle an individual WebSocket using the Studio protocol.\n // Adapts any StudioWebSocket to ConnectionManager's internal BroadcastTarget.\n // `metadata` is whatever `verifyUpgrade` attached — passed through so the\n // `filterTraceEvent` callback can read it on every outbound event.\n function handleWebSocket(ws: StudioWebSocket, metadata?: unknown) {\n if (closed) {\n ws.close();\n return;\n }\n const socket = {\n send: (data: string) => ws.send(data),\n close: () => ws.close(),\n };\n connMgr.add(socket);\n if (metadata !== undefined) {\n connMgr.setMetadata(socket, metadata);\n }\n\n ws.on('message', (raw) => {\n const reply = handleWsMessage(String(raw), socket, connMgr);\n if (reply) ws.send(reply);\n });\n\n ws.on('close', () => connMgr.remove(socket));\n ws.on('error', () => connMgr.remove(socket));\n }\n\n // Internal WebSocketServer — created lazily by upgradeWebSocket()\n let wss: InstanceType<typeof WebSocketServer> | undefined;\n // References for cleanup: the upgrade handler and server it's attached to\n let upgradeHandler: ((...args: any[]) => void) | undefined;\n let serverRef: Server | undefined;\n\n // Convenience: attach WS handling to an http.Server.\n function upgradeWebSocket(server: Server, path?: string) {\n if (\n process.env.NODE_ENV === 'production' &&\n !verifyUpgrade &&\n !dangerouslyAllowUnauthenticatedWebSockets\n ) {\n throw new Error(\n '[axl-studio] Refusing unauthenticated WebSocket upgrades in production. ' +\n 'Provide verifyUpgrade or explicitly set dangerouslyAllowUnauthenticatedWebSockets: true.',\n );\n }\n\n if (wss) {\n throw new Error(\n '[axl-studio] upgradeWebSocket() has already been called. ' +\n 'Call close() first if you need to re-attach.',\n );\n }\n\n const wsPath = path ?? (basePath ? `${basePath}/ws` : '/ws');\n\n wss = new WebSocketServer({ noServer: true });\n serverRef = server;\n\n upgradeHandler = async (req: IncomingMessage, socket: any, head: Buffer) => {\n // Match path, ignoring query string\n const pathname = new URL(req.url!, `http://${req.headers.host}`).pathname;\n if (pathname !== wsPath) return; // Let other upgrade handlers run\n\n // Early-reject if close() already started. This is a cheap pre-check —\n // we repeat it AFTER the async verifyUpgrade resolves to close the race\n // where close() runs while verifyUpgrade is still in flight.\n if (closed) {\n socket.destroy();\n return;\n }\n\n // Apply auth verification and capture optional per-connection metadata\n // (e.g. { userId, tenantId }) that filterTraceEvent can later read.\n let connectionMetadata: unknown;\n if (verifyUpgrade) {\n try {\n const result = await verifyUpgrade(req);\n // Normalize both return shapes: plain boolean or { allowed, metadata }\n const allowed = typeof result === 'boolean' ? result : result.allowed;\n if (!allowed) {\n socket.write('HTTP/1.1 401 Unauthorized\\r\\n\\r\\n');\n socket.destroy();\n return;\n }\n if (typeof result === 'object' && result !== null) {\n connectionMetadata = result.metadata;\n }\n } catch {\n socket.write('HTTP/1.1 403 Forbidden\\r\\n\\r\\n');\n socket.destroy();\n return;\n }\n }\n\n // Guard against close() having run during async verifyUpgrade. We check\n // BOTH `closed` (set at the top of `close()`) and `!wss` (set at the\n // end). `closed` is the authoritative signal — checking `!wss` alone\n // wouldn't catch the window where `close()` has marked the middleware\n // as shut down but hasn't yet torn down the WebSocketServer. Failing\n // open here would leak a connection past the middleware's lifetime\n // and leave it holding references the host expected to be released.\n if (closed || !wss) {\n socket.destroy();\n return;\n }\n\n wss.handleUpgrade(req, socket, head, (ws) => {\n handleWebSocket(ws, connectionMetadata);\n });\n };\n\n server.on('upgrade', upgradeHandler);\n }\n\n // Cleanup function for lifecycle management.\n function close() {\n closed = true;\n\n // Abort active streaming eval runs before closing connections\n closeActiveRuns();\n\n // Close all WebSocket connections\n connMgr.closeAll();\n\n // Remove the upgrade listener from the server before closing WSS\n if (upgradeHandler && serverRef) {\n serverRef.removeListener('upgrade', upgradeHandler);\n upgradeHandler = undefined;\n serverRef = undefined;\n }\n\n // Shut down the internal WebSocketServer if one was created\n if (wss) {\n wss.close();\n wss = undefined;\n }\n\n // Remove only our trace event listener from the runtime\n if (traceListener) {\n runtime.removeListener('trace', traceListener);\n }\n\n // Close all aggregators (clear intervals and unsubscribe listeners)\n closeAggregators();\n }\n\n return {\n handler,\n handleWebSocket,\n upgradeWebSocket,\n app,\n connectionManager: connMgr,\n close,\n };\n}\n\n/**\n * Normalize and validate basePath.\n * - Empty string and undefined → ''\n * - Strip trailing slashes\n * - Validate leading slash when non-empty\n * - Reject unsafe characters\n */\nfunction normalizeBasePath(raw?: string): string {\n if (!raw) return '';\n\n // Strip trailing slashes\n const normalized = raw.replace(/\\/+$/, '');\n if (!normalized) return '';\n\n // Must start with /\n if (!normalized.startsWith('/')) {\n throw new Error(`basePath must start with '/' (got '${raw}'). Example: '/studio'`);\n }\n\n // Reject path traversal, consecutive slashes, and unsafe characters\n if (normalized.includes('..')) {\n throw new Error(`basePath must not contain '..' segments (got '${raw}')`);\n }\n if (normalized.includes('//')) {\n throw new Error(`basePath must not contain consecutive slashes (got '${raw}')`);\n }\n if (!/^\\/[a-zA-Z0-9/_-]*$/.test(normalized)) {\n throw new Error(\n `basePath contains invalid characters (got '${raw}'). ` +\n 'Only alphanumeric characters, /, _, and - are allowed.',\n );\n }\n\n return normalized;\n}\n\nfunction resolveClientDist(): string | undefined {\n // Resolve the directory of this file (dist/ in published package).\n const dir =\n import.meta.dirname ??\n (typeof __dirname !== 'undefined' ? __dirname : dirname(fileURLToPath(import.meta.url)));\n const candidate = resolve(dir, 'client');\n return existsSync(resolve(candidate, 'index.html')) ? candidate : undefined;\n}\n","import { resolve, relative, basename } from 'node:path';\nimport { pathToFileURL } from 'node:url';\nimport type { AxlRuntime, EvalExecuteWorkflow } from '@axlsdk/axl';\nimport { importModule, expandGlob, registerConditions, pickDefault, pickExport } from '@axlsdk/axl';\n\n// In the CJS bundle, tsup stubs import.meta as {} so import.meta.url is\n// undefined. Fall back to __filename (which CJS defines) converted to a\n// file:// URL so tsImport() gets a valid parentURL.\nconst parentURL: string =\n import.meta.url ?? pathToFileURL(typeof __filename !== 'undefined' ? __filename : __dirname).href;\n\n/**\n * Configuration for lazy eval file discovery.\n *\n * - `string` — a glob pattern or explicit file path\n * - `string[]` — multiple patterns/paths\n * - `object` — patterns with optional import conditions\n */\nexport type EvalLoaderConfig =\n | string\n | string[]\n | {\n files: string | string[];\n\n /**\n * Custom Node.js import conditions (e.g., `['development']`).\n *\n * In monorepos, package.json `exports` often use the `development` condition\n * to point at source (`.ts`) instead of built dist. Without this, eval files\n * that import workspace packages resolve to dist files, which may not exist.\n *\n * **WARNING**: Conditions are registered process-wide via `module.register()`.\n * They affect all subsequent imports in the process, not just eval files.\n */\n conditions?: string[];\n };\n\n/**\n * Create a lazy eval loader that resolves file patterns and dynamically imports\n * eval files on first call, registering them with the runtime.\n *\n * The loader is idempotent — subsequent calls return the same promise.\n * Concurrent callers all await the same loading work.\n *\n * Eval files should export a default config with `{ workflow, dataset, scorers }`\n * (the result of `defineEval()` from `@axlsdk/eval`). An optional named export\n * `executeWorkflow` overrides the default `runtime.execute()` behavior.\n *\n * Eval names are the file's path relative to `cwd` (project root), minus the\n * `.eval.*` suffix. This makes names completely stable — a file's name never\n * changes regardless of what other files or patterns exist.\n *\n * @param config Glob patterns, file paths, or object with conditions\n * @param runtime The AxlRuntime to register discovered evals on\n * @param cwd Base directory for resolving patterns and deriving names (default: `process.cwd()`)\n */\nexport function createEvalLoader(\n config: EvalLoaderConfig,\n runtime: AxlRuntime,\n cwd?: string,\n): () => Promise<void> {\n let loadPromise: Promise<void> | undefined;\n const { patterns, conditions } = normalizeConfig(config);\n const baseCwd = cwd ?? process.cwd();\n\n return () => {\n if (!loadPromise) {\n loadPromise = loadEvalFiles(patterns, conditions, baseCwd, runtime).catch((err) => {\n loadPromise = undefined; // Allow retry on next request\n throw err;\n });\n }\n return loadPromise;\n };\n}\n\n// ── Core loading logic ─────────────────────────────────────────────\n\nasync function loadEvalFiles(\n patterns: string[],\n conditions: string[],\n cwd: string,\n runtime: AxlRuntime,\n): Promise<void> {\n if (conditions.length > 0) {\n await registerConditions(conditions);\n }\n\n const files = resolvePatterns(patterns, cwd);\n\n if (files.length === 0) {\n console.warn(`[axl-studio] No eval files found matching: ${patterns.join(', ')}`);\n return;\n }\n\n for (const file of files) {\n try {\n const mod = await importModule(file, parentURL);\n const evalConfig = pickDefault<{\n workflow?: string;\n dataset?: unknown;\n scorers?: unknown;\n }>(mod);\n\n if (!evalConfig || typeof evalConfig !== 'object') {\n console.warn(\n `[axl-studio] Skipping ${file}: default export is ` +\n `${evalConfig === null ? 'null' : typeof evalConfig}, expected an eval config object.`,\n );\n continue;\n }\n if (Array.isArray(evalConfig.scorers) && evalConfig.scorers.length === 0) {\n console.warn(\n `[axl-studio] Skipping ${file}: scorers array is empty — at least one scorer is required.`,\n );\n continue;\n }\n if (!evalConfig.workflow || !evalConfig.dataset || !evalConfig.scorers) {\n const missing = [\n !evalConfig.workflow && 'workflow',\n !evalConfig.dataset && 'dataset',\n !evalConfig.scorers && 'scorers',\n ]\n .filter(Boolean)\n .join(', ');\n const keys = Object.keys(evalConfig).slice(0, 10);\n const got = keys.length > 0 ? ` Got: { ${keys.join(', ')} }.` : '';\n console.warn(\n `[axl-studio] Skipping ${file}: not a valid eval config (missing ${missing}).${got}`,\n );\n continue;\n }\n\n const name = deriveEvalName(file, cwd);\n\n if (runtime.getRegisteredEval(name)) {\n console.warn(\n `[axl-studio] Eval name \"${name}\" from ${file} collides with an ` +\n `already-registered eval — overwriting`,\n );\n }\n\n // Walk the ESM/CJS interop chain symmetrically with the default export\n // so `executeWorkflow` stays visible when tsx loads the eval module as CJS\n // (e.g. a `.ts` file in a package without `\"type\": \"module\"`). Validate\n // the export is callable — a non-function `executeWorkflow` is silently\n // ignored so registration falls back to `runtime.execute()`, with a\n // warning so the user knows their export was rejected.\n const exported = pickExport<unknown>(mod, 'executeWorkflow');\n let customExecute: EvalExecuteWorkflow | undefined;\n if (typeof exported === 'function') {\n customExecute = exported as EvalExecuteWorkflow;\n } else if (exported !== undefined) {\n console.warn(\n `[axl-studio] ${file} exports executeWorkflow but it is ${typeof exported}, ` +\n `not a function — ignoring and falling back to runtime.execute()`,\n );\n }\n runtime.registerEval(name, evalConfig, customExecute);\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n console.warn(`[axl-studio] Failed to load eval ${file}: ${msg}`);\n }\n }\n}\n\n// ── Internal helpers ───────────────────────────────────────────────\n\nfunction normalizeConfig(config: EvalLoaderConfig): {\n patterns: string[];\n conditions: string[];\n} {\n if (typeof config === 'string') {\n return { patterns: [config], conditions: [] };\n }\n if (Array.isArray(config)) {\n return { patterns: config, conditions: [] };\n }\n const files = typeof config.files === 'string' ? [config.files] : config.files;\n return { patterns: files, conditions: config.conditions ?? [] };\n}\n\n/**\n * Derive eval name from file path relative to cwd.\n *\n * Examples (cwd = `/project`):\n * - `/project/evals/suggestions.eval.ts` → `\"evals/suggestions\"`\n * - `/project/evals/api/accuracy.eval.ts` → `\"evals/api/accuracy\"`\n */\nfunction deriveEvalName(filePath: string, cwd: string): string {\n const rel = relative(cwd, filePath);\n // Normalize to forward slashes for cross-platform consistency\n const normalized = rel.replace(/\\\\/g, '/');\n // Guard: file outside cwd (symlink, absolute path) — fall back to basename\n if (normalized.startsWith('../')) {\n const base = basename(filePath);\n const stripped = base.replace(/\\.eval\\.[mc]?[jt]sx?$/, '');\n return stripped !== base ? stripped : base.replace(/\\.[mc]?[jt]sx?$/, '') || base;\n }\n // Strip .eval.ts, .eval.mjs, .eval.js, etc.\n const withoutEval = normalized.replace(/\\.eval\\.[mc]?[jt]sx?$/, '');\n if (withoutEval !== normalized) return withoutEval;\n // Fallback: strip extension\n const withoutExt = normalized.replace(/\\.[mc]?[jt]sx?$/, '');\n return withoutExt || normalized;\n}\n\n/**\n * Resolve patterns to absolute file paths.\n *\n * Supports:\n * - Explicit file paths (no wildcards)\n * - Single-directory globs: `dir/*.eval.ts`\n * - Recursive globs: `dir/**\\/*.eval.ts` or `**\\/*.eval.ts`\n *\n * Multi-segment `**` (e.g., `a/**\\/b/**\\/*.ts`) is not supported.\n */\nfunction resolvePatterns(patterns: string[], cwd: string): string[] {\n const files: string[] = [];\n const seen = new Set<string>();\n for (const pattern of patterns) {\n const resolved = pattern.includes('*') ? expandGlob(pattern, cwd) : [resolve(cwd, pattern)];\n for (const file of resolved) {\n if (!seen.has(file)) {\n seen.add(file);\n files.push(file);\n }\n }\n }\n return files;\n}\n\n// `expandGlob` and `registerConditions` are imported from @axlsdk/axl above.\n// The studio-specific glob helpers and conditions registration that used to\n// live here are now shared with @axlsdk/eval to prevent drift.\n"],"mappings":";;;;;;AAAA,SAAS,WAAAA,UAAS,eAAe;AACjC,SAAS,kBAAkB;AAC3B,SAAS,qBAAqB;AAC9B,SAAS,0BAA0B;AACnC,SAAS,uBAAuB;;;ACJhC,SAAS,SAAS,UAAU,gBAAgB;AAC5C,SAAS,qBAAqB;AAE9B,SAAS,cAAc,YAAY,oBAAoB,aAAa,kBAAkB;AAKtF,IAAM,YACJ,YAAY,OAAO,cAAc,OAAO,eAAe,cAAc,aAAa,SAAS,EAAE;AA+CxF,SAAS,iBACd,QACA,SACA,KACqB;AACrB,MAAI;AACJ,QAAM,EAAE,UAAU,WAAW,IAAI,gBAAgB,MAAM;AACvD,QAAM,UAAU,OAAO,QAAQ,IAAI;AAEnC,SAAO,MAAM;AACX,QAAI,CAAC,aAAa;AAChB,oBAAc,cAAc,UAAU,YAAY,SAAS,OAAO,EAAE,MAAM,CAAC,QAAQ;AACjF,sBAAc;AACd,cAAM;AAAA,MACR,CAAC;AAAA,IACH;AACA,WAAO;AAAA,EACT;AACF;AAIA,eAAe,cACb,UACA,YACA,KACA,SACe;AACf,MAAI,WAAW,SAAS,GAAG;AACzB,UAAM,mBAAmB,UAAU;AAAA,EACrC;AAEA,QAAM,QAAQ,gBAAgB,UAAU,GAAG;AAE3C,MAAI,MAAM,WAAW,GAAG;AACtB,YAAQ,KAAK,8CAA8C,SAAS,KAAK,IAAI,CAAC,EAAE;AAChF;AAAA,EACF;AAEA,aAAW,QAAQ,OAAO;AACxB,QAAI;AACF,YAAM,MAAM,MAAM,aAAa,MAAM,SAAS;AAC9C,YAAM,aAAa,YAIhB,GAAG;AAEN,UAAI,CAAC,cAAc,OAAO,eAAe,UAAU;AACjD,gBAAQ;AAAA,UACN,yBAAyB,IAAI,uBACxB,eAAe,OAAO,SAAS,OAAO,UAAU;AAAA,QACvD;AACA;AAAA,MACF;AACA,UAAI,MAAM,QAAQ,WAAW,OAAO,KAAK,WAAW,QAAQ,WAAW,GAAG;AACxE,gBAAQ;AAAA,UACN,yBAAyB,IAAI;AAAA,QAC/B;AACA;AAAA,MACF;AACA,UAAI,CAAC,WAAW,YAAY,CAAC,WAAW,WAAW,CAAC,WAAW,SAAS;AACtE,cAAM,UAAU;AAAA,UACd,CAAC,WAAW,YAAY;AAAA,UACxB,CAAC,WAAW,WAAW;AAAA,UACvB,CAAC,WAAW,WAAW;AAAA,QACzB,EACG,OAAO,OAAO,EACd,KAAK,IAAI;AACZ,cAAM,OAAO,OAAO,KAAK,UAAU,EAAE,MAAM,GAAG,EAAE;AAChD,cAAM,MAAM,KAAK,SAAS,IAAI,WAAW,KAAK,KAAK,IAAI,CAAC,QAAQ;AAChE,gBAAQ;AAAA,UACN,yBAAyB,IAAI,sCAAsC,OAAO,KAAK,GAAG;AAAA,QACpF;AACA;AAAA,MACF;AAEA,YAAM,OAAO,eAAe,MAAM,GAAG;AAErC,UAAI,QAAQ,kBAAkB,IAAI,GAAG;AACnC,gBAAQ;AAAA,UACN,2BAA2B,IAAI,UAAU,IAAI;AAAA,QAE/C;AAAA,MACF;AAQA,YAAM,WAAW,WAAoB,KAAK,iBAAiB;AAC3D,UAAI;AACJ,UAAI,OAAO,aAAa,YAAY;AAClC,wBAAgB;AAAA,MAClB,WAAW,aAAa,QAAW;AACjC,gBAAQ;AAAA,UACN,gBAAgB,IAAI,sCAAsC,OAAO,QAAQ;AAAA,QAE3E;AAAA,MACF;AACA,cAAQ,aAAa,MAAM,YAAY,aAAa;AAAA,IACtD,SAAS,KAAK;AACZ,YAAM,MAAM,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAC3D,cAAQ,KAAK,oCAAoC,IAAI,KAAK,GAAG,EAAE;AAAA,IACjE;AAAA,EACF;AACF;AAIA,SAAS,gBAAgB,QAGvB;AACA,MAAI,OAAO,WAAW,UAAU;AAC9B,WAAO,EAAE,UAAU,CAAC,MAAM,GAAG,YAAY,CAAC,EAAE;AAAA,EAC9C;AACA,MAAI,MAAM,QAAQ,MAAM,GAAG;AACzB,WAAO,EAAE,UAAU,QAAQ,YAAY,CAAC,EAAE;AAAA,EAC5C;AACA,QAAM,QAAQ,OAAO,OAAO,UAAU,WAAW,CAAC,OAAO,KAAK,IAAI,OAAO;AACzE,SAAO,EAAE,UAAU,OAAO,YAAY,OAAO,cAAc,CAAC,EAAE;AAChE;AASA,SAAS,eAAe,UAAkB,KAAqB;AAC7D,QAAM,MAAM,SAAS,KAAK,QAAQ;AAElC,QAAM,aAAa,IAAI,QAAQ,OAAO,GAAG;AAEzC,MAAI,WAAW,WAAW,KAAK,GAAG;AAChC,UAAM,OAAO,SAAS,QAAQ;AAC9B,UAAM,WAAW,KAAK,QAAQ,yBAAyB,EAAE;AACzD,WAAO,aAAa,OAAO,WAAW,KAAK,QAAQ,mBAAmB,EAAE,KAAK;AAAA,EAC/E;AAEA,QAAM,cAAc,WAAW,QAAQ,yBAAyB,EAAE;AAClE,MAAI,gBAAgB,WAAY,QAAO;AAEvC,QAAM,aAAa,WAAW,QAAQ,mBAAmB,EAAE;AAC3D,SAAO,cAAc;AACvB;AAYA,SAAS,gBAAgB,UAAoB,KAAuB;AAClE,QAAM,QAAkB,CAAC;AACzB,QAAM,OAAO,oBAAI,IAAY;AAC7B,aAAW,WAAW,UAAU;AAC9B,UAAM,WAAW,QAAQ,SAAS,GAAG,IAAI,WAAW,SAAS,GAAG,IAAI,CAAC,QAAQ,KAAK,OAAO,CAAC;AAC1F,eAAW,QAAQ,UAAU;AAC3B,UAAI,CAAC,KAAK,IAAI,IAAI,GAAG;AACnB,aAAK,IAAI,IAAI;AACb,cAAM,KAAK,IAAI;AAAA,MACjB;AAAA,IACF;AAAA,EACF;AACA,SAAO;AACT;;;ADnCO,SAAS,uBAAuB,SAAkC;AACvE,QAAM;AAAA,IACJ;AAAA,IACA,cAAc;AAAA,IACd;AAAA,IACA,4CAA4C;AAAA,IAC5C,WAAW;AAAA,IACX;AAAA,EACF,IAAI;AAGJ,QAAM,WAAW,kBAAkB,QAAQ,QAAQ;AAGnD,QAAM,aAAa,cAAc,kBAAkB,IAAI;AAEvD,MAAI,eAAe,CAAC,YAAY;AAC9B,UAAM,MACJ,YAAY,YACX,OAAO,cAAc,cAAc,YAAY,QAAQ,cAAc,YAAY,GAAG,CAAC;AACxF,YAAQ;AAAA,MACN,qEACKC,SAAQ,KAAK,QAAQ,CAAC;AAAA,IAE7B;AAAA,EACF;AAGA,QAAM,aAAa,QAAQ,QAAQ,iBAAiB,QAAQ,OAAO,OAAO,IAAI;AAE9E,QAAM,EAAE,KAAK,SAAS,eAAe,iBAAiB,iBAAiB,IAAI,aAAa;AAAA,IACtF;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,MAAM;AAAA;AAAA,IACN;AAAA,IACA,YAAY,QAAQ;AAAA,EACtB,CAAC;AAKD,MAAI,kBAAkB;AACpB,YAAQ,UAAU,gBAAgB;AAAA,EACpC;AAGA,MACE,QAAQ,IAAI,aAAa,gBACzB,CAAC,iBACD,CAAC,2CACD;AACA,YAAQ;AAAA,MACN;AAAA,IAGF;AAAA,EACF;AAKA,QAAM,WAAW,mBAAmB,IAAI,OAAO;AAAA,IAC7C,uBAAuB;AAAA,EACzB,CAAC;AAED,MAAI,SAAS;AAEb,WAAS,QAAQ,KAAsB,KAAqB;AAC1D,QAAI,QAAQ;AACV,UAAI,UAAU,KAAK,EAAE,gBAAgB,mBAAmB,CAAC;AACzD,UAAI;AAAA,QACF,KAAK,UAAU;AAAA,UACb,IAAI;AAAA,UACJ,OAAO,EAAE,MAAM,UAAU,SAAS,uCAAuC;AAAA,QAC3E,CAAC;AAAA,MACH;AACA;AAAA,IACF;AAYA,UAAM,SAAS;AACf,QAAI,OAAO,QAAQ,QAAQ,CAAC,OAAO,SAAS;AAC1C,UAAI;AACF,eAAO,UAAU,OAAO,KAAK,KAAK,UAAU,OAAO,IAAI,CAAC;AAAA,MAC1D,QAAQ;AAAA,MAER;AAAA,IACF;AAEA,aAAS,KAAK,GAAG,EAAE,MAAM,CAAC,QAAQ;AAChC,cAAQ,MAAM,oDAAoD,GAAG;AACrE,UAAI,CAAC,IAAI,aAAa;AACpB,YAAI,UAAU,KAAK,EAAE,gBAAgB,mBAAmB,CAAC;AACzD,YAAI;AAAA,UACF,KAAK,UAAU;AAAA,YACb,IAAI;AAAA,YACJ,OAAO,EAAE,MAAM,kBAAkB,SAAS,wBAAwB;AAAA,UACpE,CAAC;AAAA,QACH;AAAA,MACF;AAAA,IACF,CAAC;AAAA,EACH;AAMA,WAAS,gBAAgB,IAAqB,UAAoB;AAChE,QAAI,QAAQ;AACV,SAAG,MAAM;AACT;AAAA,IACF;AACA,UAAM,SAAS;AAAA,MACb,MAAM,CAAC,SAAiB,GAAG,KAAK,IAAI;AAAA,MACpC,OAAO,MAAM,GAAG,MAAM;AAAA,IACxB;AACA,YAAQ,IAAI,MAAM;AAClB,QAAI,aAAa,QAAW;AAC1B,cAAQ,YAAY,QAAQ,QAAQ;AAAA,IACtC;AAEA,OAAG,GAAG,WAAW,CAAC,QAAQ;AACxB,YAAM,QAAQ,gBAAgB,OAAO,GAAG,GAAG,QAAQ,OAAO;AAC1D,UAAI,MAAO,IAAG,KAAK,KAAK;AAAA,IAC1B,CAAC;AAED,OAAG,GAAG,SAAS,MAAM,QAAQ,OAAO,MAAM,CAAC;AAC3C,OAAG,GAAG,SAAS,MAAM,QAAQ,OAAO,MAAM,CAAC;AAAA,EAC7C;AAGA,MAAI;AAEJ,MAAI;AACJ,MAAI;AAGJ,WAAS,iBAAiB,QAAgB,MAAe;AACvD,QACE,QAAQ,IAAI,aAAa,gBACzB,CAAC,iBACD,CAAC,2CACD;AACA,YAAM,IAAI;AAAA,QACR;AAAA,MAEF;AAAA,IACF;AAEA,QAAI,KAAK;AACP,YAAM,IAAI;AAAA,QACR;AAAA,MAEF;AAAA,IACF;AAEA,UAAM,SAAS,SAAS,WAAW,GAAG,QAAQ,QAAQ;AAEtD,UAAM,IAAI,gBAAgB,EAAE,UAAU,KAAK,CAAC;AAC5C,gBAAY;AAEZ,qBAAiB,OAAO,KAAsB,QAAa,SAAiB;AAE1E,YAAM,WAAW,IAAI,IAAI,IAAI,KAAM,UAAU,IAAI,QAAQ,IAAI,EAAE,EAAE;AACjE,UAAI,aAAa,OAAQ;AAKzB,UAAI,QAAQ;AACV,eAAO,QAAQ;AACf;AAAA,MACF;AAIA,UAAI;AACJ,UAAI,eAAe;AACjB,YAAI;AACF,gBAAM,SAAS,MAAM,cAAc,GAAG;AAEtC,gBAAM,UAAU,OAAO,WAAW,YAAY,SAAS,OAAO;AAC9D,cAAI,CAAC,SAAS;AACZ,mBAAO,MAAM,mCAAmC;AAChD,mBAAO,QAAQ;AACf;AAAA,UACF;AACA,cAAI,OAAO,WAAW,YAAY,WAAW,MAAM;AACjD,iCAAqB,OAAO;AAAA,UAC9B;AAAA,QACF,QAAQ;AACN,iBAAO,MAAM,gCAAgC;AAC7C,iBAAO,QAAQ;AACf;AAAA,QACF;AAAA,MACF;AASA,UAAI,UAAU,CAAC,KAAK;AAClB,eAAO,QAAQ;AACf;AAAA,MACF;AAEA,UAAI,cAAc,KAAK,QAAQ,MAAM,CAAC,OAAO;AAC3C,wBAAgB,IAAI,kBAAkB;AAAA,MACxC,CAAC;AAAA,IACH;AAEA,WAAO,GAAG,WAAW,cAAc;AAAA,EACrC;AAGA,WAAS,QAAQ;AACf,aAAS;AAGT,oBAAgB;AAGhB,YAAQ,SAAS;AAGjB,QAAI,kBAAkB,WAAW;AAC/B,gBAAU,eAAe,WAAW,cAAc;AAClD,uBAAiB;AACjB,kBAAY;AAAA,IACd;AAGA,QAAI,KAAK;AACP,UAAI,MAAM;AACV,YAAM;AAAA,IACR;AAGA,QAAI,eAAe;AACjB,cAAQ,eAAe,SAAS,aAAa;AAAA,IAC/C;AAGA,qBAAiB;AAAA,EACnB;AAEA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,mBAAmB;AAAA,IACnB;AAAA,EACF;AACF;AASA,SAAS,kBAAkB,KAAsB;AAC/C,MAAI,CAAC,IAAK,QAAO;AAGjB,QAAM,aAAa,IAAI,QAAQ,QAAQ,EAAE;AACzC,MAAI,CAAC,WAAY,QAAO;AAGxB,MAAI,CAAC,WAAW,WAAW,GAAG,GAAG;AAC/B,UAAM,IAAI,MAAM,sCAAsC,GAAG,wBAAwB;AAAA,EACnF;AAGA,MAAI,WAAW,SAAS,IAAI,GAAG;AAC7B,UAAM,IAAI,MAAM,iDAAiD,GAAG,IAAI;AAAA,EAC1E;AACA,MAAI,WAAW,SAAS,IAAI,GAAG;AAC7B,UAAM,IAAI,MAAM,uDAAuD,GAAG,IAAI;AAAA,EAChF;AACA,MAAI,CAAC,sBAAsB,KAAK,UAAU,GAAG;AAC3C,UAAM,IAAI;AAAA,MACR,8CAA8C,GAAG;AAAA,IAEnD;AAAA,EACF;AAEA,SAAO;AACT;AAEA,SAAS,oBAAwC;AAE/C,QAAM,MACJ,YAAY,YACX,OAAO,cAAc,cAAc,YAAY,QAAQ,cAAc,YAAY,GAAG,CAAC;AACxF,QAAM,YAAYA,SAAQ,KAAK,QAAQ;AACvC,SAAO,WAAWA,SAAQ,WAAW,YAAY,CAAC,IAAI,YAAY;AACpE;","names":["resolve","resolve"]}
|
|
1
|
+
{"version":3,"sources":["../src/middleware.ts","../src/eval-loader.ts"],"sourcesContent":["import { resolve, dirname } from 'node:path';\nimport { existsSync } from 'node:fs';\nimport { fileURLToPath } from 'node:url';\nimport { getRequestListener } from '@hono/node-server';\nimport { WebSocketServer } from 'ws';\nimport { createServer } from './server/index.js';\nimport { handleWsMessage } from './server/ws/protocol.js';\nimport { createEvalLoader } from './eval-loader.js';\nimport type { EvalLoaderConfig } from './eval-loader.js';\nimport type { BufferCaps } from './server/ws/connection-manager.js';\nimport type { AxlRuntime } from '@axlsdk/axl';\nimport type { IncomingMessage, ServerResponse, Server } from 'node:http';\nimport type { Duplex } from 'node:stream';\n\nexport type { EvalLoaderConfig } from './eval-loader.js';\nexport type { BufferCaps } from './server/ws/connection-manager.js';\n\nexport type StudioMiddlewareOptions = {\n /** The AxlRuntime instance to observe and control. */\n runtime: AxlRuntime;\n\n /**\n * URL path prefix where Studio is mounted.\n * Must match the mount path in your framework (Express `app.use()`,\n * Fastify `register()`, Hono `app.route()`, etc.). The framework is\n * expected to strip the prefix from `req.url` before calling the handler.\n *\n * Do not set basePath when using a raw `http.Server` as the root handler —\n * leave it empty and mount at root instead.\n *\n * Must start with '/' when non-empty. Trailing slashes are stripped.\n * Only URL-safe characters allowed: [a-zA-Z0-9/_-]\n *\n * @example '/studio'\n * @example '/admin/studio'\n * @example '' — mounted at root (default)\n */\n basePath?: string;\n\n /**\n * Serve the pre-built Studio SPA.\n * Set to false if serving the client from a CDN or separate build.\n * @default true\n */\n serveClient?: boolean;\n\n /**\n * Verify a WebSocket upgrade request before completing the handshake.\n *\n * Return either a boolean (`true` allow, `false` reject) or an object\n * `{ allowed: boolean, metadata?: unknown }` to ALSO attach per-connection\n * metadata that `filterTraceEvent` can later read — typically the auth\n * subject (userId, tenantId, role). Throw to reject with an error.\n *\n * IMPORTANT: WebSocket upgrades bypass Express/Fastify/Koa middleware.\n * If your HTTP routes are behind auth middleware, WS connections are NOT\n * automatically protected. Use this callback to enforce authentication\n * on WebSocket connections.\n *\n * @example Multi-tenant: extract tenant from JWT, attach as metadata\n * verifyUpgrade: async (req) => {\n * const token = req.headers.authorization?.slice(7);\n * const claims = await verifyJwt(token);\n * if (!claims) return false;\n * return { allowed: true, metadata: { userId: claims.sub, tenantId: claims.tid } };\n * }\n */\n verifyUpgrade?: (\n req: IncomingMessage,\n ) =>\n | boolean\n | { allowed: boolean; metadata?: unknown }\n | Promise<boolean | { allowed: boolean; metadata?: unknown }>;\n\n /**\n * Permit `upgradeWebSocket()` without `verifyUpgrade` in production.\n * This exposes Studio's administrative event channels without SDK-level\n * authentication and should be used only when an outer upgrade gate is\n * independently guaranteed.\n * @default false\n */\n dangerouslyAllowUnauthenticatedWebSockets?: boolean;\n\n /**\n * Optional per-event filter used by multi-tenant deployments to scope the\n * trace firehose. Called on every outbound WebSocket broadcast; return\n * `true` to deliver to this connection, `false` to drop.\n *\n * `event` is the parsed event payload — for the `trace:*` channel this is\n * an `AxlEvent` from `@axlsdk/axl` (narrow via the discriminated union);\n * for `costs` it's a `CostData`; for `execution:*` / `eval:*` it's the\n * legacy stream-event shape (until PR 3 collapses the wire to `AxlEvent`).\n * Typed `unknown` because the filter runs for every channel.\n * `metadata` is the per-connection metadata attached by `verifyUpgrade`,\n * or `undefined` if `verifyUpgrade` returned a bare boolean.\n *\n * Filter errors are treated as `drop` (fail-closed) so a buggy predicate\n * cannot accidentally leak events cross-tenant.\n *\n * @example Scope trace events by tenant id stored on agent metadata\n * import type { AxlEvent } from '@axlsdk/axl';\n *\n * filterTraceEvent: (event, meta) => {\n * const m = meta as { tenantId?: string } | undefined;\n * if (!m?.tenantId) return false;\n * const e = event as AxlEvent;\n * // Only narrow event.type === 'agent_call_end' events; pass structural\n * // events (workflow_start/end, cost updates) through unconditionally.\n * if (e.type !== 'agent_call_end') return true;\n * return e.workflow?.startsWith(`${m.tenantId}:`) ?? false;\n * }\n */\n filterTraceEvent?: (event: unknown, metadata: unknown) => boolean;\n\n /**\n * Disable all mutating endpoints (execute, test, send, delete, resolve).\n * When true, Studio is observation-only.\n * @default false\n */\n readOnly?: boolean;\n\n /**\n * Lazy-load eval files for the Eval Runner panel.\n *\n * Eval files are dynamically imported on first access to eval endpoints,\n * not at middleware construction time. This means:\n * - Zero cost during normal API operation\n * - Eval files can import from any module without creating circular deps\n * in the static module graph (they're loaded as standalone entry points)\n * - `@axlsdk/eval` can remain a devDependency — eval files never enter\n * the production bundle since bundlers can't see dynamic imports\n *\n * Accepts glob patterns, explicit file paths, or an object with\n * `conditions` for monorepo source export resolution.\n *\n * Eval files are loaded once and cached for the middleware's lifetime.\n * Changes to eval files require a server restart.\n *\n * @example\n * // Single glob pattern\n * evals: 'evals/*.eval.ts'\n *\n * @example\n * // Multiple patterns\n * evals: ['libs/api/evals/*.eval.ts', 'libs/ai/evals/*.eval.ts']\n *\n * @example\n * // With import conditions for monorepo source exports\n * evals: {\n * files: 'libs/api/evals/*.eval.ts',\n * conditions: ['development'],\n * }\n */\n evals?: EvalLoaderConfig;\n\n /**\n * Override the default WebSocket replay-buffer resource caps for\n * production deployments under sustained execution churn.\n *\n * Defaults: `maxEventsPerBuffer: 1000`, `maxBytesPerBuffer: 4 MiB`,\n * `maxActiveBuffers: 256`. Worst-case memory is roughly\n * `maxActiveBuffers × maxBytesPerBuffer` (≈1 GiB at defaults). Tighten\n * either dimension to lower the ceiling at the cost of late-subscriber\n * replay coverage; raise them if you need more event headroom on long\n * verbose-mode streams.\n *\n * Terminal `done` / `error` events are always buffered regardless of\n * caps, so a late subscriber to a completed stream still sees the\n * outcome — only intermediate non-terminal events are dropped.\n *\n * All fields are optional; passing `{}` is a no-op.\n *\n * @example\n * // Halve memory ceiling on a high-churn deployment\n * bufferCaps: { maxActiveBuffers: 128, maxBytesPerBuffer: 2 * 1024 * 1024 }\n */\n bufferCaps?: BufferCaps;\n};\n\n/**\n * Minimal contract a WebSocket connection must satisfy.\n * Matches the `ws` library API (de facto standard in Node.js).\n */\nexport interface StudioWebSocket {\n send(data: string): void;\n close(): void;\n on(event: 'message', fn: (data: string | Buffer) => void): void;\n on(event: 'close', fn: () => void): void;\n on(event: 'error', fn: (err: Error) => void): void;\n}\n\n// Re-export for Hono-in-Hono consumers\nexport { handleWsMessage } from './server/ws/protocol.js';\n\nexport type StudioMiddleware = ReturnType<typeof createStudioMiddleware>;\n\nexport function createStudioMiddleware(options: StudioMiddlewareOptions) {\n const {\n runtime,\n serveClient = true,\n verifyUpgrade,\n dangerouslyAllowUnauthenticatedWebSockets = false,\n readOnly = false,\n filterTraceEvent,\n } = options;\n\n // Normalize basePath: strip trailing slashes, validate format\n const basePath = normalizeBasePath(options.basePath);\n\n // Resolve pre-built SPA assets from this package's dist/\n const staticRoot = serveClient ? resolveClientDist() : undefined;\n\n if (serveClient && !staticRoot) {\n const dir =\n import.meta.dirname ??\n (typeof __dirname !== 'undefined' ? __dirname : dirname(fileURLToPath(import.meta.url)));\n console.warn(\n '[axl-studio] serveClient is true but no pre-built client found at ' +\n `${resolve(dir, 'client')}. Studio UI will not be available. ` +\n 'Set serveClient: false to suppress this warning.',\n );\n }\n\n // Create lazy eval loader if eval files are configured\n const evalLoader = options.evals ? createEvalLoader(options.evals, runtime) : undefined;\n\n const { app, connMgr, traceListener, closeActiveRuns, closeAggregators } = createServer({\n runtime,\n staticRoot,\n basePath,\n readOnly,\n cors: false, // Host framework owns CORS policy\n evalLoader,\n bufferCaps: options.bufferCaps,\n });\n\n // Install the broadcast filter once at construction time. The connMgr\n // applies it on every outbound event; `metadata` is attached to each\n // connection after a successful `verifyUpgrade`.\n if (filterTraceEvent) {\n connMgr.setFilter(filterTraceEvent);\n }\n\n // Warn early; upgradeWebSocket() also fails closed unless explicitly acknowledged.\n if (\n process.env.NODE_ENV === 'production' &&\n !verifyUpgrade &&\n !dangerouslyAllowUnauthenticatedWebSockets\n ) {\n console.warn(\n '[axl-studio] WARNING: Studio middleware mounted in production without verifyUpgrade. ' +\n 'upgradeWebSocket() will refuse to attach. Provide verifyUpgrade or explicitly set ' +\n 'dangerouslyAllowUnauthenticatedWebSockets: true only when an outer upgrade gate is guaranteed.',\n );\n }\n\n // Convert Hono app → Node.js (req, res) handler via @hono/node-server.\n // overrideGlobalObjects: false prevents replacing global.Request/Response,\n // which could break the host application.\n const listener = getRequestListener(app.fetch, {\n overrideGlobalObjects: false,\n });\n\n let closed = false;\n\n function handler(req: IncomingMessage, res: ServerResponse) {\n if (closed) {\n res.writeHead(503, { 'Content-Type': 'application/json' });\n res.end(\n JSON.stringify({\n ok: false,\n error: { code: 'CLOSED', message: 'Studio middleware has been shut down' },\n }),\n );\n return;\n }\n\n // Express/NestJS/Koa body parsers consume the raw IncomingMessage stream\n // and store the parsed result on req.body. When that happens,\n // @hono/node-server's getRequestListener reads an empty stream and Hono\n // sees no body. To fix this, we re-serialize req.body as req.rawBody —\n // a Buffer that getRequestListener checks before falling back to the\n // stream (see newRequestFromIncoming in @hono/node-server/dist/listener.js).\n //\n // Verified against @hono/node-server@1.19.9. If this breaks after an\n // upgrade, check whether the rawBody instanceof Buffer check still exists\n // in newRequestFromIncoming.\n const reqAny = req as unknown as Record<string, unknown>;\n if (reqAny.body != null && !reqAny.rawBody) {\n try {\n reqAny.rawBody = Buffer.from(JSON.stringify(reqAny.body));\n } catch {\n // Non-serializable body — Hono will see an empty body\n }\n }\n\n listener(req, res).catch((err) => {\n console.error('[axl-studio] Unhandled error in request handler:', err);\n if (!res.headersSent) {\n res.writeHead(500, { 'Content-Type': 'application/json' });\n res.end(\n JSON.stringify({\n ok: false,\n error: { code: 'INTERNAL_ERROR', message: 'Internal server error' },\n }),\n );\n }\n });\n }\n\n // Handle an individual WebSocket using the Studio protocol.\n // Adapts any StudioWebSocket to ConnectionManager's internal BroadcastTarget.\n // `metadata` is whatever `verifyUpgrade` attached — passed through so the\n // `filterTraceEvent` callback can read it on every outbound event.\n function handleWebSocket(ws: StudioWebSocket, metadata?: unknown) {\n if (closed) {\n ws.close();\n return;\n }\n const socket = {\n send: (data: string) => ws.send(data),\n close: () => ws.close(),\n };\n connMgr.add(socket);\n if (metadata !== undefined) {\n connMgr.setMetadata(socket, metadata);\n }\n\n ws.on('message', (raw) => {\n const reply = handleWsMessage(String(raw), socket, connMgr);\n if (reply) ws.send(reply);\n });\n\n ws.on('close', () => connMgr.remove(socket));\n ws.on('error', () => connMgr.remove(socket));\n }\n\n // Internal WebSocketServer — created lazily by upgradeWebSocket()\n let wss: InstanceType<typeof WebSocketServer> | undefined;\n // References for cleanup: the upgrade handler and server it's attached to\n let upgradeHandler:\n | ((request: IncomingMessage, socket: Duplex, head: Buffer) => void)\n | undefined;\n let serverRef: Server | undefined;\n\n // Convenience: attach WS handling to an http.Server.\n function upgradeWebSocket(server: Server, path?: string) {\n if (\n process.env.NODE_ENV === 'production' &&\n !verifyUpgrade &&\n !dangerouslyAllowUnauthenticatedWebSockets\n ) {\n throw new Error(\n '[axl-studio] Refusing unauthenticated WebSocket upgrades in production. ' +\n 'Provide verifyUpgrade or explicitly set dangerouslyAllowUnauthenticatedWebSockets: true.',\n );\n }\n\n if (wss) {\n throw new Error(\n '[axl-studio] upgradeWebSocket() has already been called. ' +\n 'Call close() first if you need to re-attach.',\n );\n }\n\n const wsPath = path ?? (basePath ? `${basePath}/ws` : '/ws');\n\n wss = new WebSocketServer({ noServer: true });\n serverRef = server;\n\n upgradeHandler = async (req: IncomingMessage, socket: Duplex, head: Buffer) => {\n // Match path, ignoring query string\n const pathname = new URL(req.url!, `http://${req.headers.host}`).pathname;\n if (pathname !== wsPath) return; // Let other upgrade handlers run\n\n // Early-reject if close() already started. This is a cheap pre-check —\n // we repeat it AFTER the async verifyUpgrade resolves to close the race\n // where close() runs while verifyUpgrade is still in flight.\n if (closed) {\n socket.destroy();\n return;\n }\n\n // Apply auth verification and capture optional per-connection metadata\n // (e.g. { userId, tenantId }) that filterTraceEvent can later read.\n let connectionMetadata: unknown;\n if (verifyUpgrade) {\n try {\n const result = await verifyUpgrade(req);\n // Normalize both return shapes: plain boolean or { allowed, metadata }\n const allowed = typeof result === 'boolean' ? result : result.allowed;\n if (!allowed) {\n socket.write('HTTP/1.1 401 Unauthorized\\r\\n\\r\\n');\n socket.destroy();\n return;\n }\n if (typeof result === 'object' && result !== null) {\n connectionMetadata = result.metadata;\n }\n } catch {\n socket.write('HTTP/1.1 403 Forbidden\\r\\n\\r\\n');\n socket.destroy();\n return;\n }\n }\n\n // Guard against close() having run during async verifyUpgrade. We check\n // BOTH `closed` (set at the top of `close()`) and `!wss` (set at the\n // end). `closed` is the authoritative signal — checking `!wss` alone\n // wouldn't catch the window where `close()` has marked the middleware\n // as shut down but hasn't yet torn down the WebSocketServer. Failing\n // open here would leak a connection past the middleware's lifetime\n // and leave it holding references the host expected to be released.\n if (closed || !wss) {\n socket.destroy();\n return;\n }\n\n wss.handleUpgrade(req, socket, head, (ws) => {\n handleWebSocket(ws, connectionMetadata);\n });\n };\n\n server.on('upgrade', upgradeHandler);\n }\n\n // Cleanup function for lifecycle management.\n function close() {\n closed = true;\n\n // Abort active streaming eval runs before closing connections\n closeActiveRuns();\n\n // Close all WebSocket connections\n connMgr.closeAll();\n\n // Remove the upgrade listener from the server before closing WSS\n if (upgradeHandler && serverRef) {\n serverRef.removeListener('upgrade', upgradeHandler);\n upgradeHandler = undefined;\n serverRef = undefined;\n }\n\n // Shut down the internal WebSocketServer if one was created\n if (wss) {\n wss.close();\n wss = undefined;\n }\n\n // Remove only our trace event listener from the runtime\n if (traceListener) {\n runtime.removeListener('trace', traceListener);\n }\n\n // Close all aggregators (clear intervals and unsubscribe listeners)\n closeAggregators();\n }\n\n return {\n handler,\n handleWebSocket,\n upgradeWebSocket,\n app,\n connectionManager: connMgr,\n close,\n };\n}\n\n/**\n * Normalize and validate basePath.\n * - Empty string and undefined → ''\n * - Strip trailing slashes\n * - Validate leading slash when non-empty\n * - Reject unsafe characters\n */\nfunction normalizeBasePath(raw?: string): string {\n if (!raw) return '';\n\n // Strip trailing slashes\n const normalized = raw.replace(/\\/+$/, '');\n if (!normalized) return '';\n\n // Must start with /\n if (!normalized.startsWith('/')) {\n throw new Error(`basePath must start with '/' (got '${raw}'). Example: '/studio'`);\n }\n\n // Reject path traversal, consecutive slashes, and unsafe characters\n if (normalized.includes('..')) {\n throw new Error(`basePath must not contain '..' segments (got '${raw}')`);\n }\n if (normalized.includes('//')) {\n throw new Error(`basePath must not contain consecutive slashes (got '${raw}')`);\n }\n if (!/^\\/[a-zA-Z0-9/_-]*$/.test(normalized)) {\n throw new Error(\n `basePath contains invalid characters (got '${raw}'). ` +\n 'Only alphanumeric characters, /, _, and - are allowed.',\n );\n }\n\n return normalized;\n}\n\nfunction resolveClientDist(): string | undefined {\n // Resolve the directory of this file (dist/ in published package).\n const dir =\n import.meta.dirname ??\n (typeof __dirname !== 'undefined' ? __dirname : dirname(fileURLToPath(import.meta.url)));\n const candidate = resolve(dir, 'client');\n return existsSync(resolve(candidate, 'index.html')) ? candidate : undefined;\n}\n","import { resolve, relative, basename } from 'node:path';\nimport { pathToFileURL } from 'node:url';\nimport type { AxlRuntime, EvalExecuteWorkflow } from '@axlsdk/axl';\nimport { importModule, expandGlob, registerConditions, pickDefault, pickExport } from '@axlsdk/axl';\n\n// In the CJS bundle, tsup stubs import.meta as {} so import.meta.url is\n// undefined. Fall back to __filename (which CJS defines) converted to a\n// file:// URL so tsImport() gets a valid parentURL.\nconst parentURL: string =\n import.meta.url ?? pathToFileURL(typeof __filename !== 'undefined' ? __filename : __dirname).href;\n\n/**\n * Configuration for lazy eval file discovery.\n *\n * - `string` — a glob pattern or explicit file path\n * - `string[]` — multiple patterns/paths\n * - `object` — patterns with optional import conditions\n */\nexport type EvalLoaderConfig =\n | string\n | string[]\n | {\n files: string | string[];\n\n /**\n * Custom Node.js import conditions (e.g., `['development']`).\n *\n * In monorepos, package.json `exports` often use the `development` condition\n * to point at source (`.ts`) instead of built dist. Without this, eval files\n * that import workspace packages resolve to dist files, which may not exist.\n *\n * **WARNING**: Conditions are registered process-wide via `module.register()`.\n * They affect all subsequent imports in the process, not just eval files.\n */\n conditions?: string[];\n };\n\n/**\n * Create a lazy eval loader that resolves file patterns and dynamically imports\n * eval files on first call, registering them with the runtime.\n *\n * The loader is idempotent — subsequent calls return the same promise.\n * Concurrent callers all await the same loading work.\n *\n * Eval files should export a default config with `{ workflow, dataset, scorers }`\n * (the result of `defineEval()` from `@axlsdk/eval`). An optional named export\n * `executeWorkflow` overrides the default `runtime.execute()` behavior.\n *\n * Eval names are the file's path relative to `cwd` (project root), minus the\n * `.eval.*` suffix. This makes names completely stable — a file's name never\n * changes regardless of what other files or patterns exist.\n *\n * @param config Glob patterns, file paths, or object with conditions\n * @param runtime The AxlRuntime to register discovered evals on\n * @param cwd Base directory for resolving patterns and deriving names (default: `process.cwd()`)\n */\nexport function createEvalLoader(\n config: EvalLoaderConfig,\n runtime: AxlRuntime,\n cwd?: string,\n): () => Promise<void> {\n let loadPromise: Promise<void> | undefined;\n const { patterns, conditions } = normalizeConfig(config);\n const baseCwd = cwd ?? process.cwd();\n\n return () => {\n if (!loadPromise) {\n loadPromise = loadEvalFiles(patterns, conditions, baseCwd, runtime).catch((err) => {\n loadPromise = undefined; // Allow retry on next request\n throw err;\n });\n }\n return loadPromise;\n };\n}\n\n// ── Core loading logic ─────────────────────────────────────────────\n\nasync function loadEvalFiles(\n patterns: string[],\n conditions: string[],\n cwd: string,\n runtime: AxlRuntime,\n): Promise<void> {\n if (conditions.length > 0) {\n await registerConditions(conditions);\n }\n\n const files = resolvePatterns(patterns, cwd);\n\n if (files.length === 0) {\n console.warn(`[axl-studio] No eval files found matching: ${patterns.join(', ')}`);\n return;\n }\n\n for (const file of files) {\n try {\n const mod = await importModule(file, parentURL);\n const evalConfig = pickDefault<{\n workflow?: string;\n dataset?: unknown;\n scorers?: unknown;\n }>(mod);\n\n if (!evalConfig || typeof evalConfig !== 'object') {\n console.warn(\n `[axl-studio] Skipping ${file}: default export is ` +\n `${evalConfig === null ? 'null' : typeof evalConfig}, expected an eval config object.`,\n );\n continue;\n }\n if (Array.isArray(evalConfig.scorers) && evalConfig.scorers.length === 0) {\n console.warn(\n `[axl-studio] Skipping ${file}: scorers array is empty — at least one scorer is required.`,\n );\n continue;\n }\n if (!evalConfig.workflow || !evalConfig.dataset || !evalConfig.scorers) {\n const missing = [\n !evalConfig.workflow && 'workflow',\n !evalConfig.dataset && 'dataset',\n !evalConfig.scorers && 'scorers',\n ]\n .filter(Boolean)\n .join(', ');\n const keys = Object.keys(evalConfig).slice(0, 10);\n const got = keys.length > 0 ? ` Got: { ${keys.join(', ')} }.` : '';\n console.warn(\n `[axl-studio] Skipping ${file}: not a valid eval config (missing ${missing}).${got}`,\n );\n continue;\n }\n\n const name = deriveEvalName(file, cwd);\n\n if (runtime.getRegisteredEval(name)) {\n console.warn(\n `[axl-studio] Eval name \"${name}\" from ${file} collides with an ` +\n `already-registered eval — overwriting`,\n );\n }\n\n // Walk the ESM/CJS interop chain symmetrically with the default export\n // so `executeWorkflow` stays visible when tsx loads the eval module as CJS\n // (e.g. a `.ts` file in a package without `\"type\": \"module\"`). Validate\n // the export is callable — a non-function `executeWorkflow` is silently\n // ignored so registration falls back to `runtime.execute()`, with a\n // warning so the user knows their export was rejected.\n const exported = pickExport<unknown>(mod, 'executeWorkflow');\n let customExecute: EvalExecuteWorkflow | undefined;\n if (typeof exported === 'function') {\n customExecute = exported as EvalExecuteWorkflow;\n } else if (exported !== undefined) {\n console.warn(\n `[axl-studio] ${file} exports executeWorkflow but it is ${typeof exported}, ` +\n `not a function — ignoring and falling back to runtime.execute()`,\n );\n }\n runtime.registerEval(name, evalConfig, customExecute);\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n console.warn(`[axl-studio] Failed to load eval ${file}: ${msg}`);\n }\n }\n}\n\n// ── Internal helpers ───────────────────────────────────────────────\n\nfunction normalizeConfig(config: EvalLoaderConfig): {\n patterns: string[];\n conditions: string[];\n} {\n if (typeof config === 'string') {\n return { patterns: [config], conditions: [] };\n }\n if (Array.isArray(config)) {\n return { patterns: config, conditions: [] };\n }\n const files = typeof config.files === 'string' ? [config.files] : config.files;\n return { patterns: files, conditions: config.conditions ?? [] };\n}\n\n/**\n * Derive eval name from file path relative to cwd.\n *\n * Examples (cwd = `/project`):\n * - `/project/evals/suggestions.eval.ts` → `\"evals/suggestions\"`\n * - `/project/evals/api/accuracy.eval.ts` → `\"evals/api/accuracy\"`\n */\nfunction deriveEvalName(filePath: string, cwd: string): string {\n const rel = relative(cwd, filePath);\n // Normalize to forward slashes for cross-platform consistency\n const normalized = rel.replace(/\\\\/g, '/');\n // Guard: file outside cwd (symlink, absolute path) — fall back to basename\n if (normalized.startsWith('../')) {\n const base = basename(filePath);\n const stripped = base.replace(/\\.eval\\.[mc]?[jt]sx?$/, '');\n return stripped !== base ? stripped : base.replace(/\\.[mc]?[jt]sx?$/, '') || base;\n }\n // Strip .eval.ts, .eval.mjs, .eval.js, etc.\n const withoutEval = normalized.replace(/\\.eval\\.[mc]?[jt]sx?$/, '');\n if (withoutEval !== normalized) return withoutEval;\n // Fallback: strip extension\n const withoutExt = normalized.replace(/\\.[mc]?[jt]sx?$/, '');\n return withoutExt || normalized;\n}\n\n/**\n * Resolve patterns to absolute file paths.\n *\n * Supports:\n * - Explicit file paths (no wildcards)\n * - Single-directory globs: `dir/*.eval.ts`\n * - Recursive globs: `dir/**\\/*.eval.ts` or `**\\/*.eval.ts`\n *\n * Multi-segment `**` (e.g., `a/**\\/b/**\\/*.ts`) is not supported.\n */\nfunction resolvePatterns(patterns: string[], cwd: string): string[] {\n const files: string[] = [];\n const seen = new Set<string>();\n for (const pattern of patterns) {\n const resolved = pattern.includes('*') ? expandGlob(pattern, cwd) : [resolve(cwd, pattern)];\n for (const file of resolved) {\n if (!seen.has(file)) {\n seen.add(file);\n files.push(file);\n }\n }\n }\n return files;\n}\n\n// `expandGlob` and `registerConditions` are imported from @axlsdk/axl above.\n// The studio-specific glob helpers and conditions registration that used to\n// live here are now shared with @axlsdk/eval to prevent drift.\n"],"mappings":"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","names":["resolve","resolve"]}
|
package/dist/server/index.cjs
CHANGED
|
@@ -121,6 +121,19 @@ function redactStreamEvent(event, redact) {
|
|
|
121
121
|
if (!redact) return event;
|
|
122
122
|
return (0, import_axl.redactHistoricalEvent)(event);
|
|
123
123
|
}
|
|
124
|
+
function sanitizeRichInputFailure(event) {
|
|
125
|
+
const message = "Playground media input failed";
|
|
126
|
+
if (event.type === "ask_end" && !event.outcome.ok) {
|
|
127
|
+
return { ...event, outcome: { ok: false, error: message } };
|
|
128
|
+
}
|
|
129
|
+
if (event.type === "agent_call_end" && event.data.error !== void 0) {
|
|
130
|
+
return { ...event, data: { ...event.data, error: message } };
|
|
131
|
+
}
|
|
132
|
+
if (event.type === "error") {
|
|
133
|
+
return { ...event, data: { ...event.data, message } };
|
|
134
|
+
}
|
|
135
|
+
return event;
|
|
136
|
+
}
|
|
124
137
|
function redactEvalItem(item) {
|
|
125
138
|
const scrubbed = {
|
|
126
139
|
...item,
|
|
@@ -839,11 +852,11 @@ function reduceCost(acc, event) {
|
|
|
839
852
|
byWorkflow2[event.workflow] = { ...prev, executions: prev.executions + 1 };
|
|
840
853
|
return { ...acc, byWorkflow: byWorkflow2 };
|
|
841
854
|
}
|
|
842
|
-
|
|
855
|
+
const unpricedLeaf = (0, import_axl2.isUnpricedLeaf)(event);
|
|
856
|
+
if (event.cost == null && !event.tokens && !unpricedLeaf) return acc;
|
|
843
857
|
const cost = (0, import_axl2.eventCostContribution)(event);
|
|
844
858
|
if (event.type === "ask_end") return acc;
|
|
845
859
|
const tokens = event.tokens ?? {};
|
|
846
|
-
const unpricedLeaf = (0, import_axl2.isUnpricedLeaf)(event);
|
|
847
860
|
const totalTokens = event.type === "agent_call_end" ? {
|
|
848
861
|
input: acc.totalTokens.input + finite(tokens.input),
|
|
849
862
|
output: acc.totalTokens.output + finite(tokens.output),
|
|
@@ -1356,6 +1369,24 @@ var executions_default = createExecutionRoutes();
|
|
|
1356
1369
|
|
|
1357
1370
|
// src/server/routes/sessions.ts
|
|
1358
1371
|
var import_hono4 = require("hono");
|
|
1372
|
+
function describeStudioInput(content) {
|
|
1373
|
+
if (typeof content === "string") return void 0;
|
|
1374
|
+
return {
|
|
1375
|
+
parts: content.map((part) => {
|
|
1376
|
+
if (part.type === "text") return { type: "text", characters: part.text.length };
|
|
1377
|
+
const { source } = part;
|
|
1378
|
+
const bytes = source.type === "bytes" ? source.data.byteLength : source.type === "base64" ? Math.floor(source.data.length * 3 / 4) - (source.data.endsWith("==") ? 2 : source.data.endsWith("=") ? 1 : 0) : void 0;
|
|
1379
|
+
return {
|
|
1380
|
+
type: "image",
|
|
1381
|
+
source: source.type,
|
|
1382
|
+
...source.mediaType ? { mediaType: source.mediaType } : {},
|
|
1383
|
+
...bytes !== void 0 ? { bytes } : {}
|
|
1384
|
+
// URLs, provider-file references, and labels are intentionally not
|
|
1385
|
+
// included: this is a Studio-safe descriptor, not a replay payload.
|
|
1386
|
+
};
|
|
1387
|
+
})
|
|
1388
|
+
};
|
|
1389
|
+
}
|
|
1359
1390
|
function createSessionRoutes(connMgr) {
|
|
1360
1391
|
const app5 = new import_hono4.Hono();
|
|
1361
1392
|
app5.get("/sessions", async (c) => {
|
|
@@ -1378,11 +1409,17 @@ function createSessionRoutes(connMgr) {
|
|
|
1378
1409
|
const id = c.req.param("id");
|
|
1379
1410
|
const history = await store.getSession(id);
|
|
1380
1411
|
const handoffHistory = await store.getSessionMeta(id, "handoffHistory");
|
|
1412
|
+
const redact = runtime.isRedactEnabled();
|
|
1413
|
+
const studioHistory = history.map((message) => {
|
|
1414
|
+
const input = describeStudioInput(message.content);
|
|
1415
|
+
if (input) return { ...message, content: input };
|
|
1416
|
+
return redact ? redactSessionHistory([message], true)[0] : message;
|
|
1417
|
+
});
|
|
1381
1418
|
return c.json({
|
|
1382
1419
|
ok: true,
|
|
1383
1420
|
data: {
|
|
1384
1421
|
id,
|
|
1385
|
-
history:
|
|
1422
|
+
history: studioHistory,
|
|
1386
1423
|
// HandoffRecord has no content fields (source/target/mode/
|
|
1387
1424
|
// timestamp/duration) — nothing to scrub.
|
|
1388
1425
|
handoffHistory: handoffHistory ?? []
|
|
@@ -15952,6 +15989,54 @@ function date4(params) {
|
|
|
15952
15989
|
// ../../node_modules/.pnpm/zod@4.3.6/node_modules/zod/v4/classic/external.js
|
|
15953
15990
|
config(en_default());
|
|
15954
15991
|
|
|
15992
|
+
// src/playground-image.ts
|
|
15993
|
+
var PLAYGROUND_IMAGE_MAX_BYTES = 5 * 1024 * 1024;
|
|
15994
|
+
var PLAYGROUND_IMAGE_MAX_BASE64_CHARACTERS = Math.ceil(PLAYGROUND_IMAGE_MAX_BYTES / 3) * 4;
|
|
15995
|
+
var PLAYGROUND_IMAGE_REQUEST_MAX_BYTES = PLAYGROUND_IMAGE_MAX_BASE64_CHARACTERS + 16 * 1024;
|
|
15996
|
+
var PLAYGROUND_IMAGE_MEDIA_TYPES = [
|
|
15997
|
+
"image/png",
|
|
15998
|
+
"image/jpeg",
|
|
15999
|
+
"image/webp",
|
|
16000
|
+
"image/gif"
|
|
16001
|
+
];
|
|
16002
|
+
function isPlaygroundImageMediaType(value) {
|
|
16003
|
+
return typeof value === "string" && PLAYGROUND_IMAGE_MEDIA_TYPES.includes(value);
|
|
16004
|
+
}
|
|
16005
|
+
function base64ByteLength(value) {
|
|
16006
|
+
if (value.length === 0 || value.length % 4 !== 0 || !/^[A-Za-z0-9+/]*={0,2}$/.test(value)) {
|
|
16007
|
+
return void 0;
|
|
16008
|
+
}
|
|
16009
|
+
const padding = value.endsWith("==") ? 2 : value.endsWith("=") ? 1 : 0;
|
|
16010
|
+
return value.length / 4 * 3 - padding;
|
|
16011
|
+
}
|
|
16012
|
+
function decodedPrefix(value, length) {
|
|
16013
|
+
try {
|
|
16014
|
+
const prefix = value.slice(0, Math.ceil(length / 3) * 4);
|
|
16015
|
+
const decoded = atob(prefix);
|
|
16016
|
+
return Uint8Array.from(decoded, (character) => character.charCodeAt(0));
|
|
16017
|
+
} catch {
|
|
16018
|
+
return void 0;
|
|
16019
|
+
}
|
|
16020
|
+
}
|
|
16021
|
+
function hasMatchingImageSignature(mediaType, data) {
|
|
16022
|
+
const bytes = decodedPrefix(data, 12);
|
|
16023
|
+
if (!bytes) return false;
|
|
16024
|
+
const startsWith = (...prefix) => prefix.every((byte, index) => bytes[index] === byte);
|
|
16025
|
+
switch (mediaType) {
|
|
16026
|
+
case "image/png":
|
|
16027
|
+
return startsWith(137, 80, 78, 71, 13, 10, 26, 10);
|
|
16028
|
+
case "image/jpeg":
|
|
16029
|
+
return startsWith(255, 216, 255);
|
|
16030
|
+
case "image/gif":
|
|
16031
|
+
return startsWith(71, 73, 70, 56) && (bytes[4] === 55 || bytes[4] === 57) && bytes[5] === 97;
|
|
16032
|
+
case "image/webp":
|
|
16033
|
+
return startsWith(82, 73, 70, 70) && startsWithAt(bytes, 8, 87, 69, 66, 80);
|
|
16034
|
+
}
|
|
16035
|
+
}
|
|
16036
|
+
function startsWithAt(bytes, offset, ...prefix) {
|
|
16037
|
+
return prefix.every((byte, index) => bytes[offset + index] === byte);
|
|
16038
|
+
}
|
|
16039
|
+
|
|
15955
16040
|
// src/server/routes/playground.ts
|
|
15956
16041
|
var DEMO_SCHEMA_BY_AGENT = {
|
|
15957
16042
|
"streaming-structured-agent": external_exports.object({
|
|
@@ -15961,11 +16046,74 @@ var DEMO_SCHEMA_BY_AGENT = {
|
|
|
15961
16046
|
confidence: external_exports.number().min(0).max(1)
|
|
15962
16047
|
})
|
|
15963
16048
|
};
|
|
16049
|
+
var PlaygroundBodyTooLargeError = class extends Error {
|
|
16050
|
+
};
|
|
16051
|
+
async function readPlaygroundBody(request) {
|
|
16052
|
+
const contentLength = request.headers.get("content-length");
|
|
16053
|
+
if (contentLength !== null) {
|
|
16054
|
+
const bytes = Number(contentLength);
|
|
16055
|
+
if (!Number.isSafeInteger(bytes) || bytes < 0 || bytes > PLAYGROUND_IMAGE_REQUEST_MAX_BYTES) {
|
|
16056
|
+
throw new PlaygroundBodyTooLargeError();
|
|
16057
|
+
}
|
|
16058
|
+
}
|
|
16059
|
+
if (!request.body) throw new SyntaxError("missing body");
|
|
16060
|
+
const reader = request.body.getReader();
|
|
16061
|
+
const chunks = [];
|
|
16062
|
+
let total = 0;
|
|
16063
|
+
try {
|
|
16064
|
+
while (true) {
|
|
16065
|
+
const next = await reader.read();
|
|
16066
|
+
if (next.done) break;
|
|
16067
|
+
total += next.value.byteLength;
|
|
16068
|
+
if (total > PLAYGROUND_IMAGE_REQUEST_MAX_BYTES) {
|
|
16069
|
+
await reader.cancel();
|
|
16070
|
+
throw new PlaygroundBodyTooLargeError();
|
|
16071
|
+
}
|
|
16072
|
+
chunks.push(next.value);
|
|
16073
|
+
}
|
|
16074
|
+
} finally {
|
|
16075
|
+
reader.releaseLock();
|
|
16076
|
+
}
|
|
16077
|
+
const body = new Uint8Array(total);
|
|
16078
|
+
let offset = 0;
|
|
16079
|
+
for (const chunk of chunks) {
|
|
16080
|
+
body.set(chunk, offset);
|
|
16081
|
+
offset += chunk.byteLength;
|
|
16082
|
+
}
|
|
16083
|
+
return JSON.parse(new TextDecoder().decode(body));
|
|
16084
|
+
}
|
|
15964
16085
|
function createPlaygroundRoutes(connMgr) {
|
|
15965
16086
|
const app5 = new import_hono11.Hono();
|
|
15966
16087
|
app5.post("/playground/chat", async (c) => {
|
|
15967
16088
|
const runtime = c.get("runtime");
|
|
15968
|
-
|
|
16089
|
+
let parsedBody;
|
|
16090
|
+
try {
|
|
16091
|
+
parsedBody = await readPlaygroundBody(c.req.raw);
|
|
16092
|
+
} catch (error48) {
|
|
16093
|
+
if (error48 instanceof PlaygroundBodyTooLargeError) {
|
|
16094
|
+
return c.json(
|
|
16095
|
+
{
|
|
16096
|
+
ok: false,
|
|
16097
|
+
error: { code: "REQUEST_TOO_LARGE", message: "playground image request is too large" }
|
|
16098
|
+
},
|
|
16099
|
+
413
|
|
16100
|
+
);
|
|
16101
|
+
}
|
|
16102
|
+
return c.json(
|
|
16103
|
+
{ ok: false, error: { code: "INVALID_INPUT", message: "request body must be valid JSON" } },
|
|
16104
|
+
400
|
|
16105
|
+
);
|
|
16106
|
+
}
|
|
16107
|
+
if (!parsedBody || typeof parsedBody !== "object" || Array.isArray(parsedBody)) {
|
|
16108
|
+
return c.json(
|
|
16109
|
+
{
|
|
16110
|
+
ok: false,
|
|
16111
|
+
error: { code: "INVALID_INPUT", message: "request body must be a JSON object" }
|
|
16112
|
+
},
|
|
16113
|
+
400
|
|
16114
|
+
);
|
|
16115
|
+
}
|
|
16116
|
+
const body = parsedBody;
|
|
15969
16117
|
if (!body.message || typeof body.message !== "string" || !body.message.trim()) {
|
|
15970
16118
|
return c.json(
|
|
15971
16119
|
{
|
|
@@ -15978,6 +16126,58 @@ function createPlaygroundRoutes(connMgr) {
|
|
|
15978
16126
|
400
|
|
15979
16127
|
);
|
|
15980
16128
|
}
|
|
16129
|
+
const message = body.message;
|
|
16130
|
+
let image;
|
|
16131
|
+
if (body.image !== void 0) {
|
|
16132
|
+
const candidate = body.image;
|
|
16133
|
+
if (!candidate || typeof candidate !== "object" || !isPlaygroundImageMediaType(candidate.mediaType) || typeof candidate.data !== "string") {
|
|
16134
|
+
return c.json(
|
|
16135
|
+
{
|
|
16136
|
+
ok: false,
|
|
16137
|
+
error: { code: "INVALID_IMAGE", message: "image must be a supported base64 image" }
|
|
16138
|
+
},
|
|
16139
|
+
400
|
|
16140
|
+
);
|
|
16141
|
+
}
|
|
16142
|
+
if (candidate.data.length > PLAYGROUND_IMAGE_MAX_BASE64_CHARACTERS) {
|
|
16143
|
+
return c.json(
|
|
16144
|
+
{ ok: false, error: { code: "IMAGE_TOO_LARGE", message: "image must be at most 5 MiB" } },
|
|
16145
|
+
400
|
|
16146
|
+
);
|
|
16147
|
+
}
|
|
16148
|
+
const bytes = base64ByteLength(candidate.data);
|
|
16149
|
+
if (bytes === void 0) {
|
|
16150
|
+
return c.json(
|
|
16151
|
+
{
|
|
16152
|
+
ok: false,
|
|
16153
|
+
error: { code: "INVALID_IMAGE", message: "image data must be valid base64" }
|
|
16154
|
+
},
|
|
16155
|
+
400
|
|
16156
|
+
);
|
|
16157
|
+
}
|
|
16158
|
+
if (bytes > PLAYGROUND_IMAGE_MAX_BYTES) {
|
|
16159
|
+
return c.json(
|
|
16160
|
+
{
|
|
16161
|
+
ok: false,
|
|
16162
|
+
error: {
|
|
16163
|
+
code: "IMAGE_TOO_LARGE",
|
|
16164
|
+
message: `image must be at most ${PLAYGROUND_IMAGE_MAX_BYTES / 1024 / 1024} MiB`
|
|
16165
|
+
}
|
|
16166
|
+
},
|
|
16167
|
+
400
|
|
16168
|
+
);
|
|
16169
|
+
}
|
|
16170
|
+
if (!hasMatchingImageSignature(candidate.mediaType, candidate.data)) {
|
|
16171
|
+
return c.json(
|
|
16172
|
+
{
|
|
16173
|
+
ok: false,
|
|
16174
|
+
error: { code: "INVALID_IMAGE", message: "image data does not match its media type" }
|
|
16175
|
+
},
|
|
16176
|
+
400
|
|
16177
|
+
);
|
|
16178
|
+
}
|
|
16179
|
+
image = { mediaType: candidate.mediaType, data: candidate.data };
|
|
16180
|
+
}
|
|
15981
16181
|
const agents = runtime.getAgents();
|
|
15982
16182
|
const agent = body.agent ? agents.find((a) => a._name === body.agent) : agents[0];
|
|
15983
16183
|
if (!agent) {
|
|
@@ -15992,14 +16192,18 @@ function createPlaygroundRoutes(connMgr) {
|
|
|
15992
16192
|
const sessionId = body.sessionId ?? `playground-${Date.now()}`;
|
|
15993
16193
|
const store = runtime.getStateStore();
|
|
15994
16194
|
const history = await store.getSession(sessionId);
|
|
15995
|
-
history
|
|
16195
|
+
const persistedHistory = [...history, { role: "user", content: message }];
|
|
15996
16196
|
const redactOn = runtime.isRedactEnabled();
|
|
15997
16197
|
const ctx = runtime.createContext({ sessionHistory: history });
|
|
15998
16198
|
const executionId = ctx.executionId;
|
|
15999
16199
|
void ctx.events;
|
|
16000
16200
|
const traceListener = (event) => {
|
|
16001
16201
|
if (event.executionId !== executionId) return;
|
|
16002
|
-
|
|
16202
|
+
const safeEvent = image ? sanitizeRichInputFailure(event) : event;
|
|
16203
|
+
connMgr.broadcastWithWildcard(
|
|
16204
|
+
`execution:${executionId}`,
|
|
16205
|
+
redactStreamEvent(safeEvent, redactOn)
|
|
16206
|
+
);
|
|
16003
16207
|
};
|
|
16004
16208
|
runtime.on("trace", traceListener);
|
|
16005
16209
|
(async () => {
|
|
@@ -16012,10 +16216,17 @@ function createPlaygroundRoutes(connMgr) {
|
|
|
16012
16216
|
});
|
|
16013
16217
|
try {
|
|
16014
16218
|
const schema = DEMO_SCHEMA_BY_AGENT[agent._name];
|
|
16015
|
-
const
|
|
16219
|
+
const input = image ? [
|
|
16220
|
+
{ type: "text", text: message },
|
|
16221
|
+
{
|
|
16222
|
+
type: "image",
|
|
16223
|
+
source: { type: "base64", data: image.data, mediaType: image.mediaType }
|
|
16224
|
+
}
|
|
16225
|
+
] : message;
|
|
16226
|
+
const result = await ctx.ask(agent, input, schema ? { schema } : void 0);
|
|
16016
16227
|
const resultText = typeof result === "string" ? result : JSON.stringify(result);
|
|
16017
|
-
|
|
16018
|
-
await store.saveSession(sessionId,
|
|
16228
|
+
persistedHistory.push({ role: "assistant", content: resultText });
|
|
16229
|
+
await store.saveSession(sessionId, persistedHistory);
|
|
16019
16230
|
const doneEvent = {
|
|
16020
16231
|
...terminalFields(),
|
|
16021
16232
|
type: "done",
|
|
@@ -16029,7 +16240,11 @@ function createPlaygroundRoutes(connMgr) {
|
|
|
16029
16240
|
const errorEvent = {
|
|
16030
16241
|
...terminalFields(),
|
|
16031
16242
|
type: "error",
|
|
16032
|
-
|
|
16243
|
+
// Media payloads can make providers echo user-controlled content in
|
|
16244
|
+
// errors. The manual terminal event is deliberately fixed and safe.
|
|
16245
|
+
data: {
|
|
16246
|
+
message: image ? "Playground media input failed" : err instanceof Error ? err.message : String(err)
|
|
16247
|
+
}
|
|
16033
16248
|
};
|
|
16034
16249
|
connMgr.broadcastWithWildcard(
|
|
16035
16250
|
`execution:${executionId}`,
|