@axlsdk/studio 0.9.1 → 0.10.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 CHANGED
@@ -163,36 +163,254 @@ Single endpoint at `ws://localhost:4400/ws` with channel multiplexing:
163
163
 
164
164
  Channels: `execution:{id}`, `trace:{id}`, `trace:*`, `costs`, `decisions`.
165
165
 
166
+ ## Embeddable Middleware
167
+
168
+ For applications using dependency injection (NestJS, etc.) or existing HTTP servers, Studio can be mounted as middleware instead of running as a standalone CLI.
169
+
170
+ ```typescript
171
+ import express from 'express';
172
+ import { AxlRuntime } from '@axlsdk/axl';
173
+ import { createStudioMiddleware } from '@axlsdk/studio/middleware';
174
+
175
+ const runtime = new AxlRuntime({ providers: ['openai'] });
176
+ // ... register workflows, agents, tools ...
177
+
178
+ const studio = createStudioMiddleware({
179
+ runtime,
180
+ basePath: '/studio',
181
+ // Your auth logic here — check a token, cookie, or header.
182
+ // This runs on WebSocket upgrades, which bypass Express middleware.
183
+ verifyUpgrade: (req) => {
184
+ const url = new URL(req.url!, `http://${req.headers.host}`);
185
+ return url.searchParams.get('token') === process.env.MY_SECRET;
186
+ },
187
+ });
188
+
189
+ const app = express();
190
+ app.use('/studio', studio.handler);
191
+
192
+ const server = app.listen(3000);
193
+ studio.upgradeWebSocket(server);
194
+ ```
195
+
196
+ ### Options
197
+
198
+ | Option | Type | Default | Description |
199
+ |--------|------|---------|-------------|
200
+ | `runtime` | `AxlRuntime` | required | The runtime instance to observe and control |
201
+ | `basePath` | `string` | `''` | URL path prefix (e.g., `'/studio'`) |
202
+ | `serveClient` | `boolean` | `true` | Serve the pre-built SPA |
203
+ | `verifyUpgrade` | `(req) => boolean \| Promise<boolean>` | — | Auth callback for WebSocket upgrades |
204
+ | `readOnly` | `boolean` | `false` | Disable all mutating endpoints |
205
+ | `evals` | `string \| string[] \| { files, conditions? }` | — | Lazy-load eval files for the Eval Runner panel |
206
+
207
+ ### Return value
208
+
209
+ | Property | Description |
210
+ |----------|-------------|
211
+ | `handler` | Node.js `(req, res)` handler for Express/Fastify/Koa/raw HTTP |
212
+ | `handleWebSocket(ws)` | Handle an individual WebSocket (framework-agnostic) |
213
+ | `upgradeWebSocket(server)` | Attach WS upgrade handling to an `http.Server` |
214
+ | `app` | Underlying Hono app (for Hono-in-Hono mounting) |
215
+ | `connectionManager` | WS connection/channel manager |
216
+ | `close()` | Shut down middleware (removes listeners, closes connections) |
217
+
218
+ **Note:** `upgradeWebSocket(server)` is required for real-time features (trace streaming, cost updates, execution events, decision resolution). Without it, the Studio SPA loads but panels relying on live data will show no updates. If your framework manages WebSocket connections itself (NestJS gateway, Fastify plugin), use `handleWebSocket()` instead.
219
+
220
+ ### Framework examples
221
+
222
+ #### NestJS
223
+
224
+ ```typescript
225
+ import { Module, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
226
+ import { HttpAdapterHost } from '@nestjs/core';
227
+ import { createStudioMiddleware, type StudioMiddleware } from '@axlsdk/studio/middleware';
228
+
229
+ @Module({ /* ... */ })
230
+ export class AppModule implements OnModuleInit, OnModuleDestroy {
231
+ private studio!: StudioMiddleware;
232
+
233
+ constructor(
234
+ private readonly httpAdapterHost: HttpAdapterHost,
235
+ private readonly runtime: AxlRuntime, // injected via custom provider
236
+ ) {}
237
+
238
+ onModuleInit() {
239
+ this.studio = createStudioMiddleware({
240
+ runtime: this.runtime,
241
+ basePath: '/studio',
242
+ verifyUpgrade: (req) => req.headers['authorization'] === `Bearer ${process.env.MY_SECRET}`,
243
+ });
244
+
245
+ // Mount on the underlying Express instance — this is the recommended
246
+ // NestJS pattern for sub-application mounting (see NestJS HTTP adapter docs).
247
+ const expressApp = this.httpAdapterHost.httpAdapter.getInstance();
248
+ expressApp.use('/studio', this.studio.handler);
249
+ this.studio.upgradeWebSocket(this.httpAdapterHost.httpAdapter.getHttpServer());
250
+ }
251
+
252
+ onModuleDestroy() {
253
+ this.studio.close();
254
+ }
255
+ }
256
+ ```
257
+
258
+ #### Fastify
259
+
260
+ ```typescript
261
+ import Fastify from 'fastify';
262
+ import middie from '@fastify/middie';
263
+ import { createStudioMiddleware } from '@axlsdk/studio/middleware';
264
+
265
+ const studio = createStudioMiddleware({ runtime, basePath: '/studio' });
266
+ const fastify = Fastify();
267
+
268
+ await fastify.register(middie);
269
+ fastify.use('/studio', studio.handler);
270
+
271
+ await fastify.listen({ port: 3000 });
272
+ studio.upgradeWebSocket(fastify.server);
273
+ ```
274
+
275
+ #### Raw Node.js
276
+
277
+ ```typescript
278
+ import { createServer } from 'node:http';
279
+ import { createStudioMiddleware } from '@axlsdk/studio/middleware';
280
+
281
+ const studio = createStudioMiddleware({ runtime });
282
+ const server = createServer(studio.handler);
283
+ studio.upgradeWebSocket(server);
284
+ server.listen(3000);
285
+ ```
286
+
287
+ #### Hono-in-Hono
288
+
289
+ ```typescript
290
+ import { Hono } from 'hono';
291
+ import { createStudioMiddleware, handleWsMessage } from '@axlsdk/studio/middleware';
292
+
293
+ const studio = createStudioMiddleware({ runtime, basePath: '/studio' });
294
+ const app = new Hono();
295
+ app.route('/studio', studio.app);
296
+ // Wire WebSocket via Hono's native WS support — see spec for full example
297
+ ```
298
+
299
+ ### Important: `basePath` must match your mount path
300
+
301
+ `basePath` tells the SPA where it's mounted in the browser URL. It must match the path in your framework's mount call:
302
+
303
+ ```typescript
304
+ // These must match:
305
+ createStudioMiddleware({ basePath: '/studio' }) // tells the SPA
306
+ app.use('/studio', studio.handler) // tells Express
307
+ ```
308
+
309
+ If they don't match, the SPA will load but API calls will fail (the SPA sends requests to the wrong path).
310
+
311
+ ### Lazy eval loading
312
+
313
+ In monorepos, eval files often import from domain modules (prompt builders, validators, fixture datasets) that would create circular dependencies if statically imported from the module that owns the runtime. The `evals` option solves this by dynamically importing eval files on first access to the Eval Runner panel — never during normal API operation.
314
+
315
+ ```typescript
316
+ const studio = createStudioMiddleware({
317
+ runtime,
318
+ basePath: '/studio',
319
+ evals: 'evals/**/*.eval.ts',
320
+ });
321
+ ```
322
+
323
+ Eval files are standalone entry points (like `axl.config.ts`). They can import from any module without creating circular deps in the static module graph, and `@axlsdk/eval` can remain a `devDependency` since bundlers can't see dynamic `import()` calls.
324
+
325
+ **Multiple patterns or explicit paths:**
326
+
327
+ ```typescript
328
+ evals: ['evals/*.eval.ts', 'tests/evals/*.eval.ts']
329
+ ```
330
+
331
+ **Monorepo import conditions** (process-wide via `module.register()`):
332
+
333
+ ```typescript
334
+ evals: {
335
+ files: 'libs/api/evals/*.eval.ts',
336
+ conditions: ['development'],
337
+ }
338
+ ```
339
+
340
+ Each file should `export default` a config with `{ workflow, dataset, scorers }` (the result of `defineEval()`). By default, the runtime executes the named workflow for each dataset item. For self-contained evals that don't depend on a registered workflow, export an `executeWorkflow` function — it will be called instead of `runtime.execute()`. See the [`@axlsdk/eval` README](../axl-eval/README.md#defineevalconfig) for details.
341
+
342
+ Eval names are the file's path relative to the project root (`cwd`), minus the `.eval.*` suffix:
343
+
344
+ ```
345
+ evals/suggestions.eval.ts → "evals/suggestions"
346
+ evals/api/accuracy.eval.ts → "evals/api/accuracy"
347
+ libs/search/accuracy.eval.ts → "libs/search/accuracy"
348
+ ```
349
+
350
+ This makes names completely stable — a file's name never changes regardless of what other files or patterns exist. You can look at a file path and know its eval name.
351
+
352
+ Lazy-loaded evals coexist with evals registered directly via `runtime.registerEval()`.
353
+
354
+ **Important notes:**
355
+
356
+ - **Caching**: Eval files are loaded once on first access and cached for the lifetime of the middleware. Changes to eval files require a server restart to take effect (both the loader and Node.js module cache are one-shot).
357
+ - **Running nested evals**: Names containing `/` must be URL-encoded in the run endpoint: `POST /api/evals/api%2Faccuracy/run`.
358
+ - **Name stability**: Names are project-relative paths, so they never change when other files or patterns are added/removed.
359
+ - **Supported glob patterns**: `dir/*.eval.ts` (single directory), `dir/**/*.eval.ts` (recursive), `**/*.eval.ts` (recursive from cwd). Multi-segment `**` (e.g., `a/**/b/**/*.ts`) is not supported.
360
+
361
+ ### Security
362
+
363
+ - **Always** provide `verifyUpgrade` — WebSocket upgrades bypass Express/Fastify/Koa middleware, so your auth middleware does NOT protect WebSocket connections
364
+ - Consider `readOnly: true` for production monitoring — view traces, costs, and schemas without execution capability
365
+ - CORS is not applied in embedded mode — the host framework owns CORS policy
366
+ - `basePath` is validated against unsafe characters and path traversal
367
+
368
+ ### Migrating from the standalone CLI
369
+
370
+ If you currently use `npx @axlsdk/studio` with a config file:
371
+
372
+ 1. Move runtime creation from `axl.config.ts` into your app's initialization code
373
+ 2. Register workflows, agents, and tools on the runtime where they have access to your services
374
+ 3. Call `createStudioMiddleware({ runtime, basePath: '/studio' })` and mount the handler
375
+ 4. Call `upgradeWebSocket(server)` for WebSocket support
376
+ 5. Remove the `axl-studio` CLI from your dev scripts
377
+
378
+ The `axl.config.ts` file is no longer needed. The standalone CLI continues to work for projects that don't need embedded middleware.
379
+
166
380
  ## Architecture
167
381
 
168
382
  ```
169
383
  src/
170
384
  cli.ts CLI entry — loads config, starts server
385
+ middleware.ts Embeddable middleware: createStudioMiddleware()
171
386
  resolve-runtime.ts Config module interop (ESM default, CJS wrapping, named exports)
172
387
  server/
173
- index.ts createServer() — Hono app composition
388
+ index.ts createServer() — Hono app composition (basePath, readOnly, cors)
174
389
  types.ts API types, WebSocket message types
175
390
  cost-aggregator.ts Accumulates cost from trace events
176
391
  middleware/
177
392
  error-handler.ts Axl errors → JSON error envelope
178
393
  routes/ One file per resource (health, workflows, agents, tools, etc.)
179
394
  ws/
180
- handler.ts WebSocket message routing
181
- connection-manager.ts Channel subscriptions + broadcast
395
+ handler.ts WebSocket message routing (Hono adapter)
396
+ connection-manager.ts Channel subscriptions + broadcast (BroadcastTarget)
397
+ protocol.ts Shared WS protocol: handleWsMessage(), channel validation
182
398
  client/
183
399
  App.tsx React SPA — sidebar + 8 panel routes
184
400
  lib/
185
- api.ts Typed fetch wrappers for all endpoints
186
- ws.ts WebSocket client with channel subscriptions
401
+ api.ts Typed fetch wrappers (reads window.__AXL_STUDIO_BASE__)
402
+ ws.ts WebSocket client with channel subscriptions (reads base path)
187
403
  panels/ One directory per panel
188
404
  ```
189
405
 
190
- **Server:** Hono HTTP server wrapping the user's `AxlRuntime`. REST endpoints for CRUD, WebSocket for live streaming.
406
+ **Server:** Hono HTTP server wrapping the user's `AxlRuntime`. REST endpoints for CRUD, WebSocket for live streaming. Supports standalone CLI and embeddable middleware modes.
191
407
 
192
- **Client:** React 19 SPA with Tailwind CSS v4, TanStack Query, and react-router-dom. Pre-built at publish time and served as static assets.
408
+ **Client:** React 19 SPA with Tailwind CSS v4, TanStack Query, and react-router-dom. Pre-built at publish time and served as static assets. Reads `window.__AXL_STUDIO_BASE__` for runtime base path configuration.
193
409
 
194
410
  **CLI:** Auto-detects and loads the user's config via `tsx` (with ESM-forcing resolve hook for `.ts` files), validates the runtime, starts the server, and optionally opens the browser.
195
411
 
412
+ **Middleware:** `createStudioMiddleware()` wraps the Hono app as a Node.js `(req, res)` handler via `@hono/node-server`. Adds `verifyUpgrade` for WS auth, `readOnly` mode, and `basePath` injection into the SPA.
413
+
196
414
  ## Development
197
415
 
198
416
  ```bash