@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 +225 -7
- package/dist/{chunk-EG74VI3M.js → chunk-GKIPF45K.js} +206 -101
- package/dist/chunk-GKIPF45K.js.map +1 -0
- package/dist/cli.cjs +220 -116
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +1 -1
- package/dist/client/assets/{index-DDlRZgfC.js → index-jeUeToM_.js} +13 -13
- package/dist/client/index.html +2 -2
- package/dist/connection-manager-DbOgO_gK.d.cts +75 -0
- package/dist/connection-manager-DbOgO_gK.d.ts +75 -0
- package/dist/middleware.cjs +1243 -0
- package/dist/middleware.cjs.map +1 -0
- package/dist/middleware.d.cts +131 -0
- package/dist/middleware.d.ts +131 -0
- package/dist/middleware.js +346 -0
- package/dist/middleware.js.map +1 -0
- package/dist/server/index.cjs +204 -100
- package/dist/server/index.cjs.map +1 -1
- package/dist/server/index.d.cts +12 -62
- package/dist/server/index.d.ts +12 -62
- package/dist/server/index.js +1 -1
- package/package.json +16 -4
- package/dist/chunk-EG74VI3M.js.map +0 -1
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
|
|
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
|