@axlsdk/studio 0.14.0 → 0.16.0

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.
@@ -43,16 +43,140 @@ var import_ws = require("ws");
43
43
  // src/server/index.ts
44
44
  var import_node_fs = require("fs");
45
45
  var import_node_path = require("path");
46
- var import_hono12 = require("hono");
46
+ var import_hono15 = require("hono");
47
47
  var import_cors = require("hono/cors");
48
48
  var import_serve_static = require("@hono/node-server/serve-static");
49
49
 
50
+ // src/server/redact.ts
51
+ var import_axl = require("@axlsdk/axl");
52
+ var REDACTED = "[redacted]";
53
+ var SAFE_ERROR_NAMES = /* @__PURE__ */ new Set([
54
+ "QuorumNotMet",
55
+ "NoConsensus",
56
+ "TimeoutError",
57
+ "MaxTurnsError",
58
+ "BudgetExceededError",
59
+ "ToolDenied"
60
+ ]);
61
+ function redactErrorMessage(err, redact) {
62
+ const raw = err instanceof Error ? err.message : String(err);
63
+ if (!redact) return raw;
64
+ const name = err instanceof Error ? err.name : "";
65
+ return SAFE_ERROR_NAMES.has(name) ? raw : REDACTED;
66
+ }
67
+ function redactValue(value, redact) {
68
+ if (!redact) return value;
69
+ return REDACTED;
70
+ }
71
+ function redactExecutionInfo(info, redact) {
72
+ if (!redact) return info;
73
+ return {
74
+ ...info,
75
+ ...info.result !== void 0 ? { result: REDACTED } : {},
76
+ ...info.error !== void 0 ? { error: REDACTED } : {},
77
+ events: info.events.map((e) => redactStreamEvent(e, true))
78
+ };
79
+ }
80
+ function redactExecutionList(infos, redact) {
81
+ if (!redact) return infos;
82
+ return infos.map((info) => redactExecutionInfo(info, redact));
83
+ }
84
+ function redactMemoryValue(value, redact) {
85
+ if (!redact) return value;
86
+ return REDACTED;
87
+ }
88
+ function redactMemoryList(entries, redact) {
89
+ if (!redact) return entries;
90
+ return entries.map((entry) => ({ key: entry.key, value: REDACTED }));
91
+ }
92
+ function redactChatMessage(msg) {
93
+ const scrubbed = {
94
+ role: msg.role,
95
+ content: REDACTED,
96
+ ...msg.name !== void 0 ? { name: msg.name } : {},
97
+ ...msg.tool_call_id !== void 0 ? { tool_call_id: msg.tool_call_id } : {},
98
+ ...msg.tool_calls !== void 0 ? {
99
+ tool_calls: msg.tool_calls.map((tc) => ({
100
+ id: tc.id,
101
+ type: tc.type,
102
+ function: {
103
+ name: tc.function.name,
104
+ arguments: REDACTED
105
+ }
106
+ }))
107
+ } : {}
108
+ // providerMetadata deliberately omitted — opaque content.
109
+ };
110
+ return scrubbed;
111
+ }
112
+ function redactSessionHistory(history, redact) {
113
+ if (!redact) return history;
114
+ return history.map(redactChatMessage);
115
+ }
116
+ function redactStreamEvent(event, redact) {
117
+ if (!redact) return event;
118
+ return (0, import_axl.redactEvent)(event);
119
+ }
120
+ function redactEvalItem(item) {
121
+ const scrubbed = {
122
+ ...item,
123
+ input: REDACTED,
124
+ output: REDACTED,
125
+ ...item.annotations !== void 0 ? { annotations: REDACTED } : {},
126
+ ...item.error !== void 0 ? { error: REDACTED } : {},
127
+ ...item.scorerErrors !== void 0 ? { scorerErrors: item.scorerErrors.map(() => REDACTED) } : {}
128
+ };
129
+ if (item.scoreDetails) {
130
+ const detailsOut = {};
131
+ for (const [name, detail] of Object.entries(item.scoreDetails)) {
132
+ detailsOut[name] = {
133
+ score: detail.score,
134
+ ...detail.duration !== void 0 ? { duration: detail.duration } : {},
135
+ ...detail.cost !== void 0 ? { cost: detail.cost } : {}
136
+ // metadata deliberately omitted — may contain LLM scorer reasoning
137
+ };
138
+ }
139
+ scrubbed.scoreDetails = detailsOut;
140
+ }
141
+ return scrubbed;
142
+ }
143
+ function redactEvalResult(result, redact) {
144
+ if (!redact) return result;
145
+ return {
146
+ ...result,
147
+ items: result.items.map(redactEvalItem)
148
+ };
149
+ }
150
+ function redactEvalHistoryEntry(entry, redact) {
151
+ if (!redact) return entry;
152
+ return {
153
+ ...entry,
154
+ data: redactEvalResult(entry.data, redact)
155
+ };
156
+ }
157
+ function redactEvalHistoryList(entries, redact) {
158
+ if (!redact) return entries;
159
+ return entries.map((e) => redactEvalHistoryEntry(e, redact));
160
+ }
161
+ function redactPendingDecision(decision, redact) {
162
+ if (!redact) return decision;
163
+ return {
164
+ ...decision,
165
+ prompt: REDACTED,
166
+ ...decision.metadata !== void 0 ? { metadata: { redacted: true } } : {}
167
+ };
168
+ }
169
+ function redactPendingDecisionList(decisions, redact) {
170
+ if (!redact) return decisions;
171
+ return decisions.map((d) => redactPendingDecision(d, redact));
172
+ }
173
+
50
174
  // src/server/middleware/error-handler.ts
51
175
  async function errorHandler(c, next) {
52
176
  try {
53
177
  await next();
54
178
  } catch (err) {
55
- const message = err instanceof Error ? err.message : String(err);
179
+ const rawMessage = err instanceof Error ? err.message : String(err);
56
180
  const code = err.code ?? "INTERNAL_ERROR";
57
181
  let status = 500;
58
182
  if ("status" in err) {
@@ -60,46 +184,104 @@ async function errorHandler(c, next) {
60
184
  if (typeof errStatus === "number" && errStatus >= 400 && errStatus < 600) {
61
185
  status = errStatus;
62
186
  }
63
- } else if (code === "NOT_FOUND" || message.includes("not found") || message.includes("not registered")) {
187
+ } else if (code === "NOT_FOUND" || rawMessage.includes("not found") || rawMessage.includes("not registered")) {
64
188
  status = 404;
65
- } else if (code === "VALIDATION_ERROR" || message.includes("Expected") || message.includes("invalid")) {
189
+ } else if (code === "VALIDATION_ERROR" || rawMessage.includes("Expected") || rawMessage.includes("invalid")) {
66
190
  status = 400;
67
191
  }
192
+ const runtime = c.get("runtime");
193
+ const redactOn = runtime?.isRedactEnabled?.() ?? false;
68
194
  const body = {
69
195
  ok: false,
70
- error: { code, message }
196
+ error: { code, message: redactErrorMessage(err, redactOn) }
71
197
  };
72
198
  return c.json(body, status);
73
199
  }
74
200
  }
75
201
 
76
202
  // src/server/ws/connection-manager.ts
203
+ var BUFFER_TTL_MS = 3e4;
204
+ var DEFAULT_MAX_BUFFER_EVENTS = 1e3;
205
+ var DEFAULT_MAX_BUFFER_BYTES = 4 * 1024 * 1024;
206
+ var DEFAULT_MAX_ACTIVE_BUFFERS = 256;
207
+ var UNBUFFERED_EVENT_TYPES = /* @__PURE__ */ new Set(["token", "partial_object"]);
208
+ var MAX_WS_FRAME_BYTES = 65536;
77
209
  function isBufferedChannel(channel) {
78
- return channel.startsWith("execution:");
210
+ return channel.startsWith("execution:") || channel.startsWith("eval:");
211
+ }
212
+ function truncateIfOversized(msg, channel, data) {
213
+ const msgBytes = Buffer.byteLength(msg, "utf8");
214
+ if (msgBytes <= MAX_WS_FRAME_BYTES) return msg;
215
+ const event = data ?? {};
216
+ const truncated = {
217
+ type: "event",
218
+ channel,
219
+ data: {
220
+ ...event,
221
+ data: {
222
+ __truncated: true,
223
+ originalBytes: msgBytes,
224
+ maxBytes: MAX_WS_FRAME_BYTES,
225
+ hint: "Event exceeded WS frame budget (likely a verbose agent_call with a large messages[] snapshot). Fetch via REST if you need the full payload."
226
+ }
227
+ }
228
+ };
229
+ return JSON.stringify(truncated);
79
230
  }
80
- var BUFFER_TTL_MS = 3e4;
81
- var MAX_BUFFER_EVENTS = 500;
82
231
  var ConnectionManager = class {
83
232
  /** channel -> set of WS connections */
84
233
  channels = /* @__PURE__ */ new Map();
85
- /** ws -> set of subscribed channels (for cleanup) */
234
+ /** ws -> subscribed channels + optional integrator-supplied metadata */
86
235
  connections = /* @__PURE__ */ new Map();
87
236
  /** channel -> replay buffer for execution streams */
88
237
  buffers = /* @__PURE__ */ new Map();
89
238
  maxConnections = 100;
239
+ filter;
240
+ /** Resolved replay-buffer caps. Per-instance so embedders can dial them
241
+ * without monkey-patching module-level constants. */
242
+ maxEventsPerBuffer;
243
+ maxBytesPerBuffer;
244
+ maxActiveBuffers;
245
+ constructor(bufferCaps) {
246
+ const validatePositiveInt = (key, value) => {
247
+ if (value === void 0) return;
248
+ if (!Number.isFinite(value) || !Number.isInteger(value) || value < 1) {
249
+ throw new RangeError(`bufferCaps.${key} must be a positive integer (>= 1); got ${value}`);
250
+ }
251
+ };
252
+ validatePositiveInt("maxEventsPerBuffer", bufferCaps?.maxEventsPerBuffer);
253
+ validatePositiveInt("maxBytesPerBuffer", bufferCaps?.maxBytesPerBuffer);
254
+ validatePositiveInt("maxActiveBuffers", bufferCaps?.maxActiveBuffers);
255
+ this.maxEventsPerBuffer = bufferCaps?.maxEventsPerBuffer ?? DEFAULT_MAX_BUFFER_EVENTS;
256
+ this.maxBytesPerBuffer = bufferCaps?.maxBytesPerBuffer ?? DEFAULT_MAX_BUFFER_BYTES;
257
+ this.maxActiveBuffers = bufferCaps?.maxActiveBuffers ?? DEFAULT_MAX_ACTIVE_BUFFERS;
258
+ }
259
+ /**
260
+ * Register a broadcast filter. Called once at middleware construction.
261
+ * The filter runs on every outbound event and can drop or deliver based
262
+ * on the destination connection's metadata.
263
+ */
264
+ setFilter(filter) {
265
+ this.filter = filter;
266
+ }
267
+ /** Attach integrator-supplied metadata to an already-added connection. */
268
+ setMetadata(ws, metadata) {
269
+ const entry = this.connections.get(ws);
270
+ if (entry) entry.metadata = metadata;
271
+ }
90
272
  /** Register a new WS connection. */
91
273
  add(ws) {
92
274
  if (this.connections.size >= this.maxConnections) {
93
275
  ws.close?.();
94
276
  return;
95
277
  }
96
- this.connections.set(ws, /* @__PURE__ */ new Set());
278
+ this.connections.set(ws, { channels: /* @__PURE__ */ new Set() });
97
279
  }
98
280
  /** Remove a WS connection and all its subscriptions. */
99
281
  remove(ws) {
100
- const channels = this.connections.get(ws);
101
- if (channels) {
102
- for (const ch of channels) {
282
+ const entry = this.connections.get(ws);
283
+ if (entry) {
284
+ for (const ch of entry.channels) {
103
285
  this.channels.get(ch)?.delete(ws);
104
286
  if (this.channels.get(ch)?.size === 0) {
105
287
  this.channels.delete(ch);
@@ -117,12 +299,20 @@ var ConnectionManager = class {
117
299
  this.channels.set(channel, subs);
118
300
  }
119
301
  subs.add(ws);
120
- this.connections.get(ws).add(channel);
302
+ this.connections.get(ws).channels.add(channel);
121
303
  const buffer = this.buffers.get(channel);
122
304
  if (buffer) {
123
- for (const msg of buffer.events) {
305
+ const metadata = this.connections.get(ws)?.metadata;
306
+ for (const event of buffer.events) {
307
+ if (this.filter) {
308
+ try {
309
+ if (!this.filter(event.data, metadata)) continue;
310
+ } catch {
311
+ continue;
312
+ }
313
+ }
124
314
  try {
125
- ws.send(msg);
315
+ ws.send(event.msg);
126
316
  } catch {
127
317
  this.remove(ws);
128
318
  return;
@@ -136,21 +326,49 @@ var ConnectionManager = class {
136
326
  if (this.channels.get(channel)?.size === 0) {
137
327
  this.channels.delete(channel);
138
328
  }
139
- this.connections.get(ws)?.delete(channel);
329
+ this.connections.get(ws)?.channels.delete(channel);
140
330
  }
141
331
  /** Broadcast data to all subscribers of a channel. Buffers events for execution channels. */
142
332
  broadcast(channel, data) {
143
- const msg = JSON.stringify({ type: "event", channel, data });
333
+ const msg = truncateIfOversized(
334
+ JSON.stringify({ type: "event", channel, data }),
335
+ channel,
336
+ data
337
+ );
144
338
  if (isBufferedChannel(channel)) {
145
339
  let buffer = this.buffers.get(channel);
146
340
  if (!buffer) {
147
- buffer = { events: [], complete: false };
341
+ if (this.buffers.size >= this.maxActiveBuffers) {
342
+ let victim;
343
+ for (const [ch, buf] of this.buffers) {
344
+ if (buf.complete) {
345
+ victim = ch;
346
+ break;
347
+ }
348
+ }
349
+ if (victim === void 0) {
350
+ victim = this.buffers.keys().next().value;
351
+ }
352
+ if (victim !== void 0) {
353
+ const old = this.buffers.get(victim);
354
+ if (old?.timer) clearTimeout(old.timer);
355
+ this.buffers.delete(victim);
356
+ }
357
+ }
358
+ buffer = { events: [], complete: false, bytes: 0 };
148
359
  this.buffers.set(channel, buffer);
149
360
  }
150
361
  const event = data;
151
362
  const isTerminal = event.type === "done" || event.type === "error";
152
- if (buffer.events.length < MAX_BUFFER_EVENTS || isTerminal) {
153
- buffer.events.push(msg);
363
+ const isUnbuffered = event.type !== void 0 && UNBUFFERED_EVENT_TYPES.has(event.type);
364
+ if (!isUnbuffered) {
365
+ const msgBytes = Buffer.byteLength(msg, "utf8");
366
+ const atCountCap = buffer.events.length >= this.maxEventsPerBuffer;
367
+ const atByteCap = buffer.bytes + msgBytes > this.maxBytesPerBuffer;
368
+ if (isTerminal || !atCountCap && !atByteCap) {
369
+ buffer.events.push({ msg, data });
370
+ buffer.bytes += msgBytes;
371
+ }
154
372
  }
155
373
  if (isTerminal) {
156
374
  buffer.complete = true;
@@ -163,6 +381,14 @@ var ConnectionManager = class {
163
381
  const subs = this.channels.get(channel);
164
382
  if (!subs || subs.size === 0) return;
165
383
  for (const ws of [...subs]) {
384
+ if (this.filter) {
385
+ const metadata = this.connections.get(ws)?.metadata;
386
+ try {
387
+ if (!this.filter(data, metadata)) continue;
388
+ } catch {
389
+ continue;
390
+ }
391
+ }
166
392
  try {
167
393
  ws.send(msg);
168
394
  } catch {
@@ -178,8 +404,20 @@ var ConnectionManager = class {
178
404
  const wildcardChannel = channel.substring(0, colonIdx) + ":*";
179
405
  const subs = this.channels.get(wildcardChannel);
180
406
  if (!subs || subs.size === 0) return;
181
- const msg = JSON.stringify({ type: "event", channel, data });
407
+ const msg = truncateIfOversized(
408
+ JSON.stringify({ type: "event", channel, data }),
409
+ channel,
410
+ data
411
+ );
182
412
  for (const ws of [...subs]) {
413
+ if (this.filter) {
414
+ const metadata = this.connections.get(ws)?.metadata;
415
+ try {
416
+ if (!this.filter(data, metadata)) continue;
417
+ } catch {
418
+ continue;
419
+ }
420
+ }
183
421
  try {
184
422
  ws.send(msg);
185
423
  } catch {
@@ -211,11 +449,11 @@ var ConnectionManager = class {
211
449
  };
212
450
 
213
451
  // src/server/ws/protocol.ts
214
- var VALID_CHANNEL_PREFIXES = ["execution:", "trace:"];
215
- var VALID_EXACT_CHANNELS = ["costs", "decisions"];
452
+ var VALID_CHANNEL_PREFIXES = ["execution:", "trace:", "eval:"];
453
+ var VALID_EXACT_CHANNELS = ["costs", "decisions", "eval-trends", "workflow-stats", "trace-stats"];
216
454
  var MAX_CHANNEL_LENGTH = 256;
217
455
  function handleWsMessage(raw, socket, connMgr) {
218
- if (raw.length > 65536) {
456
+ if (Buffer.byteLength(raw, "utf8") > MAX_WS_FRAME_BYTES) {
219
457
  return JSON.stringify({ type: "error", message: "Message too large" });
220
458
  }
221
459
  let msg;
@@ -275,68 +513,575 @@ function createWsHandlers(connMgr) {
275
513
  };
276
514
  }
277
515
 
278
- // src/server/cost-aggregator.ts
279
- var CostAggregator = class {
280
- constructor(connMgr) {
516
+ // src/server/aggregates/aggregate-snapshots.ts
517
+ var WINDOW_MS = {
518
+ "24h": 24 * 60 * 60 * 1e3,
519
+ "7d": 7 * 24 * 60 * 60 * 1e3,
520
+ "30d": 30 * 24 * 60 * 60 * 1e3,
521
+ all: Number.POSITIVE_INFINITY
522
+ };
523
+ function withinWindow(ts, window, now) {
524
+ return ts >= now - WINDOW_MS[window];
525
+ }
526
+ var REBUILD_INTERVAL_MS = 5 * 6e4;
527
+ var ALL_WINDOWS = new Set(Object.keys(WINDOW_MS));
528
+ function parseWindowParam(raw, fallback = "7d") {
529
+ return raw && ALL_WINDOWS.has(raw) ? raw : fallback;
530
+ }
531
+ var AggregateSnapshots = class {
532
+ constructor(windows, emptyState, connMgr, channel, broadcastTransform) {
533
+ this.windows = windows;
534
+ this.emptyState = emptyState;
281
535
  this.connMgr = connMgr;
536
+ this.channel = channel;
537
+ this.broadcastTransform = broadcastTransform;
538
+ this.snapshots = new Map(windows.map((w) => [w, emptyState()]));
539
+ }
540
+ snapshots;
541
+ /** Replace all snapshots atomically — used after a full rebuild. */
542
+ replace(fresh) {
543
+ this.snapshots = fresh;
544
+ this.broadcast();
545
+ }
546
+ /** Apply a reducer update to every window where `ts` falls inside the window. */
547
+ fold(ts, update) {
548
+ const now = Date.now();
549
+ let changed = false;
550
+ for (const window of this.windows) {
551
+ if (withinWindow(ts, window, now)) {
552
+ const prev = this.snapshots.get(window);
553
+ this.snapshots.set(window, update(prev));
554
+ changed = true;
555
+ }
556
+ }
557
+ if (changed) this.broadcast();
558
+ }
559
+ get(window) {
560
+ return this.snapshots.get(window) ?? this.emptyState();
561
+ }
562
+ getAll() {
563
+ return Object.fromEntries(this.snapshots);
564
+ }
565
+ broadcast() {
566
+ const snapshots = this.broadcastTransform ? Object.fromEntries(
567
+ this.windows.map((w) => [w, this.broadcastTransform(this.snapshots.get(w))])
568
+ ) : this.getAll();
569
+ this.connMgr.broadcast(this.channel, {
570
+ snapshots,
571
+ updatedAt: Date.now()
572
+ });
573
+ }
574
+ };
575
+
576
+ // src/server/aggregates/trace-aggregator.ts
577
+ var TraceAggregator = class {
578
+ snaps;
579
+ interval;
580
+ listener;
581
+ options;
582
+ constructor(options) {
583
+ this.options = options;
584
+ this.snaps = new AggregateSnapshots(
585
+ options.windows,
586
+ options.emptyState,
587
+ options.connMgr,
588
+ options.channel,
589
+ options.broadcastTransform
590
+ );
591
+ }
592
+ async start() {
593
+ await this.rebuild();
594
+ this.listener = (event) => {
595
+ this.snaps.fold(event.timestamp, (prev) => this.options.reducer(prev, event));
596
+ };
597
+ this.options.runtime.on("trace", this.listener);
598
+ this.interval = setInterval(
599
+ () => this.rebuild().catch((err) => console.error("[axl-studio] rebuild failed:", err)),
600
+ REBUILD_INTERVAL_MS
601
+ );
602
+ }
603
+ async rebuild() {
604
+ const executions = await this.options.runtime.getExecutions();
605
+ const cap = this.options.executionCap ?? 2e3;
606
+ const capped = executions.slice(0, cap);
607
+ const now = Date.now();
608
+ const fresh = new Map(
609
+ this.options.windows.map((w) => [w, this.options.emptyState()])
610
+ );
611
+ for (const exec of capped) {
612
+ for (const event of exec.events) {
613
+ for (const window of this.options.windows) {
614
+ if (withinWindow(event.timestamp, window, now)) {
615
+ fresh.set(window, this.options.reducer(fresh.get(window), event));
616
+ }
617
+ }
618
+ }
619
+ }
620
+ this.snaps.replace(fresh);
621
+ }
622
+ getSnapshot(window) {
623
+ return this.snaps.get(window);
624
+ }
625
+ getAllSnapshots() {
626
+ return this.snaps.getAll();
627
+ }
628
+ close() {
629
+ if (this.listener) this.options.runtime.off("trace", this.listener);
630
+ if (this.interval) clearInterval(this.interval);
631
+ }
632
+ };
633
+
634
+ // src/server/aggregates/execution-aggregator.ts
635
+ var ExecutionAggregator = class {
636
+ snaps;
637
+ interval;
638
+ listener;
639
+ options;
640
+ /** Generation counter to prevent stale async fold after rebuild. */
641
+ generation = 0;
642
+ constructor(options) {
643
+ this.options = options;
644
+ this.snaps = new AggregateSnapshots(
645
+ options.windows,
646
+ options.emptyState,
647
+ options.connMgr,
648
+ options.channel,
649
+ options.broadcastTransform
650
+ );
651
+ }
652
+ async start() {
653
+ await this.rebuild();
654
+ this.listener = (event) => {
655
+ if (event.type !== "workflow_end") return;
656
+ const gen = this.generation;
657
+ this.options.runtime.getExecution(event.executionId).then((exec) => {
658
+ if (this.generation !== gen) return;
659
+ if (exec) {
660
+ this.snaps.fold(exec.startedAt, (prev) => this.options.reducer(prev, exec));
661
+ }
662
+ }).catch((err) => console.error("[axl-studio] execution fold failed:", err));
663
+ };
664
+ this.options.runtime.on("trace", this.listener);
665
+ this.interval = setInterval(
666
+ () => this.rebuild().catch((err) => console.error("[axl-studio] rebuild failed:", err)),
667
+ REBUILD_INTERVAL_MS
668
+ );
669
+ }
670
+ async rebuild() {
671
+ this.generation++;
672
+ const executions = await this.options.runtime.getExecutions();
673
+ const cap = this.options.executionCap ?? 2e3;
674
+ const capped = executions.slice(0, cap);
675
+ const now = Date.now();
676
+ const fresh = new Map(
677
+ this.options.windows.map((w) => [w, this.options.emptyState()])
678
+ );
679
+ for (const exec of capped) {
680
+ for (const window of this.options.windows) {
681
+ if (withinWindow(exec.startedAt, window, now)) {
682
+ fresh.set(window, this.options.reducer(fresh.get(window), exec));
683
+ }
684
+ }
685
+ }
686
+ this.snaps.replace(fresh);
687
+ }
688
+ getSnapshot(window) {
689
+ return this.snaps.get(window);
690
+ }
691
+ getAllSnapshots() {
692
+ return this.snaps.getAll();
693
+ }
694
+ close() {
695
+ if (this.listener) this.options.runtime.off("trace", this.listener);
696
+ if (this.interval) clearInterval(this.interval);
697
+ }
698
+ };
699
+
700
+ // src/server/aggregates/eval-aggregator.ts
701
+ var EvalAggregator = class {
702
+ snaps;
703
+ interval;
704
+ listener;
705
+ options;
706
+ constructor(options) {
707
+ this.options = options;
708
+ this.snaps = new AggregateSnapshots(
709
+ options.windows,
710
+ options.emptyState,
711
+ options.connMgr,
712
+ options.channel,
713
+ options.broadcastTransform
714
+ );
715
+ }
716
+ async start() {
717
+ await this.rebuild();
718
+ this.listener = (entry) => {
719
+ this.snaps.fold(entry.timestamp, (prev) => this.options.reducer(prev, entry));
720
+ };
721
+ this.options.runtime.on("eval_result", this.listener);
722
+ this.interval = setInterval(
723
+ () => this.rebuild().catch((err) => console.error("[axl-studio] rebuild failed:", err)),
724
+ REBUILD_INTERVAL_MS
725
+ );
726
+ }
727
+ async rebuild() {
728
+ const history = await this.options.runtime.getEvalHistory();
729
+ const cap = this.options.entryCap ?? 500;
730
+ const capped = history.slice(0, cap);
731
+ const now = Date.now();
732
+ const fresh = new Map(
733
+ this.options.windows.map((w) => [w, this.options.emptyState()])
734
+ );
735
+ for (const entry of capped) {
736
+ for (const window of this.options.windows) {
737
+ if (withinWindow(entry.timestamp, window, now)) {
738
+ fresh.set(window, this.options.reducer(fresh.get(window), entry));
739
+ }
740
+ }
741
+ }
742
+ this.snaps.replace(fresh);
743
+ }
744
+ getSnapshot(window) {
745
+ return this.snaps.get(window);
746
+ }
747
+ getAllSnapshots() {
748
+ return this.snaps.getAll();
282
749
  }
283
- data = {
750
+ close() {
751
+ if (this.listener) this.options.runtime.off("eval_result", this.listener);
752
+ if (this.interval) clearInterval(this.interval);
753
+ }
754
+ };
755
+
756
+ // src/server/aggregates/reducers.ts
757
+ var import_axl2 = require("@axlsdk/axl");
758
+ var finite = (v) => Number.isFinite(v) ? v : 0;
759
+ function emptyRetry() {
760
+ return {
761
+ primary: 0,
762
+ primaryCalls: 0,
763
+ schema: 0,
764
+ schemaCalls: 0,
765
+ validate: 0,
766
+ validateCalls: 0,
767
+ guardrail: 0,
768
+ guardrailCalls: 0,
769
+ retryCalls: 0
770
+ };
771
+ }
772
+ function emptyCostData() {
773
+ return {
284
774
  totalCost: 0,
285
775
  totalTokens: { input: 0, output: 0, reasoning: 0 },
286
776
  byAgent: {},
287
777
  byModel: {},
288
- byWorkflow: {}
778
+ byWorkflow: {},
779
+ retry: emptyRetry(),
780
+ byEmbedder: {}
289
781
  };
290
- /** Process a trace event and update cost data. */
291
- onTrace(event) {
292
- if (event.cost == null && !event.tokens) return;
293
- const cost = Number.isFinite(event.cost) ? event.cost : 0;
294
- const tokens = event.tokens ?? {};
295
- this.data.totalCost += cost;
296
- this.data.totalTokens.input += tokens.input ?? 0;
297
- this.data.totalTokens.output += tokens.output ?? 0;
298
- this.data.totalTokens.reasoning += tokens.reasoning ?? 0;
299
- if (event.agent) {
300
- const entry = this.data.byAgent[event.agent] ?? { cost: 0, calls: 0 };
301
- entry.cost += cost;
302
- entry.calls += 1;
303
- this.data.byAgent[event.agent] = entry;
304
- }
305
- if (event.model) {
306
- const entry = this.data.byModel[event.model] ?? {
307
- cost: 0,
308
- calls: 0,
309
- tokens: { input: 0, output: 0 }
310
- };
311
- entry.cost += cost;
312
- entry.calls += 1;
313
- entry.tokens.input += tokens.input ?? 0;
314
- entry.tokens.output += tokens.output ?? 0;
315
- this.data.byModel[event.model] = entry;
316
- }
317
- if (event.workflow) {
318
- const entry = this.data.byWorkflow[event.workflow] ?? { cost: 0, executions: 0 };
319
- entry.cost += cost;
320
- if (event.type === "workflow_start") entry.executions += 1;
321
- this.data.byWorkflow[event.workflow] = entry;
322
- }
323
- this.connMgr.broadcast("costs", this.data);
324
- }
325
- /** Get current aggregated cost data. */
326
- getData() {
327
- return this.data;
328
- }
329
- /** Reset all accumulated data. */
330
- reset() {
331
- this.data = {
332
- totalCost: 0,
333
- totalTokens: { input: 0, output: 0, reasoning: 0 },
334
- byAgent: {},
335
- byModel: {},
336
- byWorkflow: {}
782
+ }
783
+ function reduceCost(acc, event) {
784
+ const isWorkflowStart = event.type === "workflow_start";
785
+ if (isWorkflowStart && event.workflow) {
786
+ const byWorkflow2 = { ...acc.byWorkflow };
787
+ const prev = byWorkflow2[event.workflow] ?? { cost: 0, executions: 0 };
788
+ byWorkflow2[event.workflow] = { ...prev, executions: prev.executions + 1 };
789
+ return { ...acc, byWorkflow: byWorkflow2 };
790
+ }
791
+ if (event.cost == null && !event.tokens) return acc;
792
+ const cost = (0, import_axl2.eventCostContribution)(event);
793
+ if (event.type === "ask_end") return acc;
794
+ const tokens = event.tokens ?? {};
795
+ const totalTokens = event.type === "agent_call_end" ? {
796
+ input: acc.totalTokens.input + finite(tokens.input),
797
+ output: acc.totalTokens.output + finite(tokens.output),
798
+ reasoning: acc.totalTokens.reasoning + finite(tokens.reasoning)
799
+ } : acc.totalTokens;
800
+ const byAgent = { ...acc.byAgent };
801
+ if (event.agent) {
802
+ const prev = byAgent[event.agent] ?? { cost: 0, calls: 0 };
803
+ byAgent[event.agent] = { cost: prev.cost + cost, calls: prev.calls + 1 };
804
+ }
805
+ const byModel = { ...acc.byModel };
806
+ if (event.model) {
807
+ const prev = byModel[event.model] ?? { cost: 0, calls: 0, tokens: { input: 0, output: 0 } };
808
+ byModel[event.model] = {
809
+ cost: prev.cost + cost,
810
+ calls: prev.calls + 1,
811
+ tokens: {
812
+ input: prev.tokens.input + finite(tokens.input),
813
+ output: prev.tokens.output + finite(tokens.output)
814
+ }
337
815
  };
338
816
  }
339
- };
817
+ const byWorkflow = { ...acc.byWorkflow };
818
+ if (event.workflow) {
819
+ const prev = byWorkflow[event.workflow] ?? { cost: 0, executions: 0 };
820
+ byWorkflow[event.workflow] = {
821
+ cost: prev.cost + cost,
822
+ executions: prev.executions + (isWorkflowStart ? 1 : 0)
823
+ };
824
+ }
825
+ let retry = acc.retry;
826
+ if (event.type === "agent_call_end") {
827
+ const d = event.data ?? {};
828
+ const reason = d.retryReason;
829
+ retry = { ...acc.retry };
830
+ if (reason === "schema") {
831
+ retry.schema += cost;
832
+ retry.schemaCalls += 1;
833
+ retry.retryCalls += 1;
834
+ } else if (reason === "validate") {
835
+ retry.validate += cost;
836
+ retry.validateCalls += 1;
837
+ retry.retryCalls += 1;
838
+ } else if (reason === "guardrail") {
839
+ retry.guardrail += cost;
840
+ retry.guardrailCalls += 1;
841
+ retry.retryCalls += 1;
842
+ } else {
843
+ retry.primary += cost;
844
+ retry.primaryCalls += 1;
845
+ }
846
+ }
847
+ let byEmbedder = acc.byEmbedder;
848
+ if (event.type === "memory_remember" || event.type === "memory_recall") {
849
+ const usage = event.data.usage;
850
+ byEmbedder = { ...acc.byEmbedder };
851
+ const modelKey = usage?.model ?? "unknown";
852
+ const embedTokens = typeof usage?.tokens === "number" ? finite(usage.tokens) : 0;
853
+ const prev = byEmbedder[modelKey] ?? { cost: 0, calls: 0, tokens: 0 };
854
+ byEmbedder[modelKey] = {
855
+ cost: prev.cost + cost,
856
+ calls: prev.calls + 1,
857
+ tokens: prev.tokens + embedTokens
858
+ };
859
+ }
860
+ return {
861
+ totalCost: acc.totalCost + cost,
862
+ totalTokens,
863
+ byAgent,
864
+ byModel,
865
+ byWorkflow,
866
+ retry,
867
+ byEmbedder
868
+ };
869
+ }
870
+ function emptyEvalTrendData() {
871
+ return { byEval: {}, totalRuns: 0, totalCost: 0 };
872
+ }
873
+ function extractScores(data) {
874
+ if (!data || typeof data !== "object") return {};
875
+ const result = data;
876
+ const summary = result.summary;
877
+ const scorers = summary?.scorers;
878
+ if (!scorers) return {};
879
+ const out = {};
880
+ for (const [name, entry] of Object.entries(scorers)) {
881
+ if (typeof entry === "number" && Number.isFinite(entry)) {
882
+ out[name] = entry;
883
+ } else if (entry && typeof entry === "object" && Number.isFinite(entry.mean)) {
884
+ out[name] = entry.mean;
885
+ }
886
+ }
887
+ return out;
888
+ }
889
+ function extractCost(data) {
890
+ if (!data || typeof data !== "object") return 0;
891
+ const result = data;
892
+ if (Number.isFinite(result.totalCost)) return result.totalCost;
893
+ const summary = result.summary;
894
+ return Number.isFinite(summary?.totalCost) ? summary.totalCost : 0;
895
+ }
896
+ function extractModel(data) {
897
+ if (!data || typeof data !== "object") return void 0;
898
+ const result = data;
899
+ const metadata = result.metadata;
900
+ const counts = metadata?.modelCounts;
901
+ if (counts && typeof counts === "object" && !Array.isArray(counts)) {
902
+ const entries = Object.entries(counts).filter(
903
+ ([, v]) => typeof v === "number"
904
+ );
905
+ if (entries.length > 0) {
906
+ entries.sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]));
907
+ return entries[0][0];
908
+ }
909
+ }
910
+ const models = metadata?.models;
911
+ if (Array.isArray(models) && typeof models[0] === "string") return models[0];
912
+ return void 0;
913
+ }
914
+ function extractDuration(data) {
915
+ if (!data || typeof data !== "object") return void 0;
916
+ const result = data;
917
+ return Number.isFinite(result.duration) ? result.duration : void 0;
918
+ }
919
+ function computeScoreStats(runs) {
920
+ const scorerNames = /* @__PURE__ */ new Set();
921
+ for (const run of runs) {
922
+ for (const name of Object.keys(run.scores)) scorerNames.add(name);
923
+ }
924
+ const mean = {};
925
+ const std = {};
926
+ for (const name of scorerNames) {
927
+ const values = runs.map((r) => r.scores[name]).filter((v) => v != null);
928
+ if (values.length === 0) continue;
929
+ const m = values.reduce((a, b) => a + b, 0) / values.length;
930
+ mean[name] = m;
931
+ const variance = values.reduce((sum, v) => sum + (v - m) ** 2, 0) / values.length;
932
+ std[name] = Math.sqrt(variance);
933
+ }
934
+ return { mean, std };
935
+ }
936
+ function reduceEvalTrends(acc, entry) {
937
+ const scores = extractScores(entry.data);
938
+ const cost = extractCost(entry.data);
939
+ const model = extractModel(entry.data);
940
+ const duration = extractDuration(entry.data);
941
+ const run = {
942
+ timestamp: entry.timestamp,
943
+ id: entry.id,
944
+ scores,
945
+ cost,
946
+ ...model !== void 0 ? { model } : {},
947
+ ...duration !== void 0 ? { duration } : {}
948
+ };
949
+ const byEval = { ...acc.byEval };
950
+ const prev = byEval[entry.eval];
951
+ const MAX_EVAL_RUNS = 50;
952
+ const allRuns = prev ? [...prev.runs, run] : [run];
953
+ const runs = allRuns.length > MAX_EVAL_RUNS ? allRuns.slice(-MAX_EVAL_RUNS) : allRuns;
954
+ const { mean, std } = computeScoreStats(runs);
955
+ const latestScores = prev && prev.runs.length > 0 && prev.runs[prev.runs.length - 1].timestamp > run.timestamp ? prev.latestScores : scores;
956
+ byEval[entry.eval] = {
957
+ runs,
958
+ latestScores,
959
+ scoreMean: mean,
960
+ scoreStd: std,
961
+ costTotal: (prev?.costTotal ?? 0) + cost,
962
+ runCount: (prev?.runCount ?? 0) + 1
963
+ };
964
+ return {
965
+ byEval,
966
+ totalRuns: acc.totalRuns + 1,
967
+ totalCost: acc.totalCost + cost
968
+ };
969
+ }
970
+ var MAX_DURATIONS = 200;
971
+ function emptyWorkflowStatsData() {
972
+ return { byWorkflow: {}, totalExecutions: 0, failureRate: 0 };
973
+ }
974
+ function percentile(sorted, p) {
975
+ if (sorted.length === 0) return 0;
976
+ const idx = p / 100 * (sorted.length - 1);
977
+ const lower = Math.floor(idx);
978
+ const upper = Math.ceil(idx);
979
+ if (lower === upper) return sorted[lower];
980
+ return sorted[lower] + (sorted[upper] - sorted[lower]) * (idx - lower);
981
+ }
982
+ function reduceWorkflowStats(acc, execution) {
983
+ const byWorkflow = { ...acc.byWorkflow };
984
+ const prev = byWorkflow[execution.workflow] ?? {
985
+ total: 0,
986
+ completed: 0,
987
+ failed: 0,
988
+ durations: [],
989
+ durationSum: 0,
990
+ avgDuration: 0
991
+ };
992
+ const dur = finite(execution.duration);
993
+ const durations = [...prev.durations];
994
+ const insertIdx = durations.findIndex((d) => d > dur);
995
+ if (insertIdx === -1) durations.push(dur);
996
+ else durations.splice(insertIdx, 0, dur);
997
+ if (durations.length > MAX_DURATIONS) durations.shift();
998
+ const total = prev.total + 1;
999
+ const completed = prev.completed + (execution.status === "completed" ? 1 : 0);
1000
+ const failed = prev.failed + (execution.status === "failed" ? 1 : 0);
1001
+ const durationSum = prev.durationSum + dur;
1002
+ const avgDuration = durationSum / total;
1003
+ byWorkflow[execution.workflow] = {
1004
+ total,
1005
+ completed,
1006
+ failed,
1007
+ durations,
1008
+ durationSum,
1009
+ avgDuration
1010
+ };
1011
+ const totalExecutions = acc.totalExecutions + 1;
1012
+ const totalFailed = Object.values(byWorkflow).reduce((sum, w) => sum + w.failed, 0);
1013
+ const failureRate = totalExecutions > 0 ? totalFailed / totalExecutions : 0;
1014
+ return { byWorkflow, totalExecutions, failureRate };
1015
+ }
1016
+ function getWorkflowPercentiles(entry) {
1017
+ return {
1018
+ durationP50: percentile(entry.durations, 50),
1019
+ durationP95: percentile(entry.durations, 95)
1020
+ };
1021
+ }
1022
+ function enrichWorkflowStats(data) {
1023
+ const byWorkflow = {};
1024
+ for (const [name, entry] of Object.entries(data.byWorkflow)) {
1025
+ const { durationP50, durationP95 } = getWorkflowPercentiles(entry);
1026
+ byWorkflow[name] = {
1027
+ total: entry.total,
1028
+ completed: entry.completed,
1029
+ failed: entry.failed,
1030
+ durationP50,
1031
+ durationP95,
1032
+ avgDuration: entry.avgDuration
1033
+ };
1034
+ }
1035
+ return {
1036
+ byWorkflow,
1037
+ totalExecutions: data.totalExecutions,
1038
+ failureRate: data.failureRate
1039
+ };
1040
+ }
1041
+ function emptyTraceStatsData() {
1042
+ return {
1043
+ eventTypeCounts: {},
1044
+ byTool: {},
1045
+ retryByAgent: {},
1046
+ totalEvents: 0
1047
+ };
1048
+ }
1049
+ function reduceTraceStats(acc, event) {
1050
+ const eventTypeCounts = { ...acc.eventTypeCounts };
1051
+ eventTypeCounts[event.type] = (eventTypeCounts[event.type] ?? 0) + 1;
1052
+ const byTool = { ...acc.byTool };
1053
+ if (event.type === "tool_call_end" || event.type === "tool_denied" || event.type === "tool_approval") {
1054
+ const toolName = event.tool;
1055
+ const prev = byTool[toolName] ?? { calls: 0, denied: 0, approved: 0 };
1056
+ const isDeniedEvent = event.type === "tool_denied";
1057
+ const isApprovalEvent = event.type === "tool_approval";
1058
+ const eventData = isDeniedEvent || isApprovalEvent ? event.data : void 0;
1059
+ const isApproved = isDeniedEvent && eventData?.approved === true || isApprovalEvent && eventData?.approved === true;
1060
+ const isDenied = isDeniedEvent && !eventData?.approved || isApprovalEvent && eventData?.approved === false;
1061
+ byTool[toolName] = {
1062
+ calls: prev.calls + (event.type === "tool_call_end" ? 1 : 0),
1063
+ denied: prev.denied + (isDenied ? 1 : 0),
1064
+ approved: prev.approved + (isApproved ? 1 : 0)
1065
+ };
1066
+ }
1067
+ const retryByAgent = { ...acc.retryByAgent };
1068
+ if (event.agent && event.type === "agent_call_end") {
1069
+ const data = event.data;
1070
+ if (data?.retryReason) {
1071
+ const prev = retryByAgent[event.agent] ?? { schema: 0, validate: 0, guardrail: 0 };
1072
+ const reason = data.retryReason;
1073
+ if (reason in prev) {
1074
+ retryByAgent[event.agent] = { ...prev, [reason]: prev[reason] + 1 };
1075
+ }
1076
+ }
1077
+ }
1078
+ return {
1079
+ eventTypeCounts,
1080
+ byTool,
1081
+ retryByAgent,
1082
+ totalEvents: acc.totalEvents + 1
1083
+ };
1084
+ }
340
1085
 
341
1086
  // src/server/routes/health.ts
342
1087
  var import_hono = require("hono");
@@ -360,7 +1105,7 @@ function createHealthRoutes(readOnly) {
360
1105
 
361
1106
  // src/server/routes/workflows.ts
362
1107
  var import_hono2 = require("hono");
363
- var import_axl = require("@axlsdk/axl");
1108
+ var import_axl3 = require("@axlsdk/axl");
364
1109
  function createWorkflowRoutes(connMgr) {
365
1110
  const app6 = new import_hono2.Hono();
366
1111
  app6.get("/workflows", (c) => {
@@ -386,8 +1131,8 @@ function createWorkflowRoutes(connMgr) {
386
1131
  ok: true,
387
1132
  data: {
388
1133
  name: workflow.name,
389
- inputSchema: workflow.inputSchema ? (0, import_axl.zodToJsonSchema)(workflow.inputSchema) : null,
390
- outputSchema: workflow.outputSchema ? (0, import_axl.zodToJsonSchema)(workflow.outputSchema) : null
1134
+ inputSchema: workflow.inputSchema ? (0, import_axl3.zodToJsonSchema)(workflow.inputSchema) : null,
1135
+ outputSchema: workflow.outputSchema ? (0, import_axl3.zodToJsonSchema)(workflow.outputSchema) : null
391
1136
  }
392
1137
  });
393
1138
  });
@@ -405,15 +1150,22 @@ function createWorkflowRoutes(connMgr) {
405
1150
  if (body.stream) {
406
1151
  const stream = runtime.stream(name, body.input ?? {}, { metadata: body.metadata });
407
1152
  const executionId = `stream-${Date.now()}`;
1153
+ const redactOn = runtime.isRedactEnabled();
408
1154
  (async () => {
409
1155
  for await (const event of stream) {
410
- connMgr.broadcastWithWildcard(`execution:${executionId}`, event);
1156
+ connMgr.broadcastWithWildcard(
1157
+ `execution:${executionId}`,
1158
+ redactStreamEvent(event, redactOn)
1159
+ );
411
1160
  }
412
1161
  })();
413
1162
  return c.json({ ok: true, data: { executionId, streaming: true } });
414
1163
  }
415
1164
  const result = await runtime.execute(name, body.input ?? {}, { metadata: body.metadata });
416
- return c.json({ ok: true, data: { result } });
1165
+ return c.json({
1166
+ ok: true,
1167
+ data: { result: redactValue(result, runtime.isRedactEnabled()) }
1168
+ });
417
1169
  });
418
1170
  return app6;
419
1171
  }
@@ -424,7 +1176,10 @@ var app = new import_hono3.Hono();
424
1176
  app.get("/executions", async (c) => {
425
1177
  const runtime = c.get("runtime");
426
1178
  const executions = await runtime.getExecutions();
427
- return c.json({ ok: true, data: executions });
1179
+ return c.json({
1180
+ ok: true,
1181
+ data: redactExecutionList(executions, runtime.isRedactEnabled())
1182
+ });
428
1183
  });
429
1184
  app.get("/executions/:id", async (c) => {
430
1185
  const runtime = c.get("runtime");
@@ -436,7 +1191,32 @@ app.get("/executions/:id", async (c) => {
436
1191
  404
437
1192
  );
438
1193
  }
439
- return c.json({ ok: true, data: execution });
1194
+ const sinceParam = c.req.query("since");
1195
+ let paged = execution;
1196
+ if (sinceParam !== void 0) {
1197
+ const since = Number(sinceParam);
1198
+ if (!Number.isFinite(since) || !Number.isInteger(since)) {
1199
+ return c.json(
1200
+ {
1201
+ ok: false,
1202
+ error: {
1203
+ code: "INVALID_PARAM",
1204
+ message: `\`since\` must be a finite integer (got "${sinceParam}")`,
1205
+ param: "since"
1206
+ }
1207
+ },
1208
+ 400
1209
+ );
1210
+ }
1211
+ paged = {
1212
+ ...execution,
1213
+ events: execution.events.filter((e) => e.step > since)
1214
+ };
1215
+ }
1216
+ return c.json({
1217
+ ok: true,
1218
+ data: redactExecutionInfo(paged, runtime.isRedactEnabled())
1219
+ });
440
1220
  });
441
1221
  app.post("/executions/:id/abort", (c) => {
442
1222
  const runtime = c.get("runtime");
@@ -470,7 +1250,16 @@ function createSessionRoutes(connMgr) {
470
1250
  const id = c.req.param("id");
471
1251
  const history = await store.getSession(id);
472
1252
  const handoffHistory = await store.getSessionMeta(id, "handoffHistory");
473
- return c.json({ ok: true, data: { id, history, handoffHistory: handoffHistory ?? [] } });
1253
+ return c.json({
1254
+ ok: true,
1255
+ data: {
1256
+ id,
1257
+ history: redactSessionHistory(history, runtime.isRedactEnabled()),
1258
+ // HandoffRecord has no content fields (source/target/mode/
1259
+ // timestamp/duration) — nothing to scrub.
1260
+ handoffHistory: handoffHistory ?? []
1261
+ }
1262
+ });
474
1263
  });
475
1264
  app6.post("/sessions/:id/send", async (c) => {
476
1265
  const runtime = c.get("runtime");
@@ -506,7 +1295,7 @@ function createSessionRoutes(connMgr) {
506
1295
 
507
1296
  // src/server/routes/agents.ts
508
1297
  var import_hono5 = require("hono");
509
- var import_axl2 = require("@axlsdk/axl");
1298
+ var import_axl4 = require("@axlsdk/axl");
510
1299
  var app2 = new import_hono5.Hono();
511
1300
  app2.get("/agents", (c) => {
512
1301
  const runtime = c.get("runtime");
@@ -547,7 +1336,7 @@ app2.get("/agents/:name", (c) => {
547
1336
  tools: cfg.tools?.map((t) => ({
548
1337
  name: t.name,
549
1338
  description: t.description,
550
- inputSchema: (0, import_axl2.zodToJsonSchema)(t.inputSchema)
1339
+ inputSchema: (0, import_axl4.zodToJsonSchema)(t.inputSchema)
551
1340
  })) ?? [],
552
1341
  handoffs: typeof cfg.handoffs === "function" ? [
553
1342
  {
@@ -587,14 +1376,14 @@ var agents_default = app2;
587
1376
 
588
1377
  // src/server/routes/tools.ts
589
1378
  var import_hono6 = require("hono");
590
- var import_axl3 = require("@axlsdk/axl");
1379
+ var import_axl5 = require("@axlsdk/axl");
591
1380
  var app3 = new import_hono6.Hono();
592
1381
  app3.get("/tools", (c) => {
593
1382
  const runtime = c.get("runtime");
594
1383
  const tools = runtime.getTools().map((t) => ({
595
1384
  name: t.name,
596
1385
  description: t.description,
597
- inputSchema: t.inputSchema ? (0, import_axl3.zodToJsonSchema)(t.inputSchema) : {},
1386
+ inputSchema: t.inputSchema ? (0, import_axl5.zodToJsonSchema)(t.inputSchema) : {},
598
1387
  sensitive: t.sensitive ?? false,
599
1388
  requireApproval: t.requireApproval ?? false
600
1389
  }));
@@ -615,7 +1404,7 @@ app3.get("/tools/:name", (c) => {
615
1404
  data: {
616
1405
  name: tool.name,
617
1406
  description: tool.description,
618
- inputSchema: tool.inputSchema ? (0, import_axl3.zodToJsonSchema)(tool.inputSchema) : {},
1407
+ inputSchema: tool.inputSchema ? (0, import_axl5.zodToJsonSchema)(tool.inputSchema) : {},
619
1408
  sensitive: tool.sensitive,
620
1409
  requireApproval: tool.requireApproval,
621
1410
  retry: tool.retry,
@@ -640,7 +1429,10 @@ app3.post("/tools/:name/test", async (c) => {
640
1429
  const body = await c.req.json();
641
1430
  const ctx = runtime.createContext();
642
1431
  const result = await tool.run(ctx, body.input);
643
- return c.json({ ok: true, data: { result } });
1432
+ return c.json({
1433
+ ok: true,
1434
+ data: { result: redactValue(result, runtime.isRedactEnabled()) }
1435
+ });
644
1436
  });
645
1437
  var tools_default = app3;
646
1438
 
@@ -655,7 +1447,7 @@ app4.get("/memory/:scope", async (c) => {
655
1447
  return c.json({ ok: true, data: [] });
656
1448
  }
657
1449
  const entries = await store.getAllMemory(scope);
658
- return c.json({ ok: true, data: entries });
1450
+ return c.json({ ok: true, data: redactMemoryList(entries, runtime.isRedactEnabled()) });
659
1451
  });
660
1452
  app4.get("/memory/:scope/:key", async (c) => {
661
1453
  const runtime = c.get("runtime");
@@ -675,7 +1467,10 @@ app4.get("/memory/:scope/:key", async (c) => {
675
1467
  404
676
1468
  );
677
1469
  }
678
- return c.json({ ok: true, data: { key, value } });
1470
+ return c.json({
1471
+ ok: true,
1472
+ data: { key, value: redactMemoryValue(value, runtime.isRedactEnabled()) }
1473
+ });
679
1474
  });
680
1475
  app4.put("/memory/:scope/:key", async (c) => {
681
1476
  const runtime = c.get("runtime");
@@ -720,7 +1515,10 @@ var app5 = new import_hono8.Hono();
720
1515
  app5.get("/decisions", async (c) => {
721
1516
  const runtime = c.get("runtime");
722
1517
  const decisions = await runtime.getPendingDecisions();
723
- return c.json({ ok: true, data: decisions });
1518
+ return c.json({
1519
+ ok: true,
1520
+ data: redactPendingDecisionList(decisions, runtime.isRedactEnabled())
1521
+ });
724
1522
  });
725
1523
  app5.post("/decisions/:executionId/resolve", async (c) => {
726
1524
  const runtime = c.get("runtime");
@@ -736,11 +1534,23 @@ var import_hono9 = require("hono");
736
1534
  function createCostRoutes(costAggregator) {
737
1535
  const app6 = new import_hono9.Hono();
738
1536
  app6.get("/costs", (c) => {
739
- return c.json({ ok: true, data: costAggregator.getData() });
1537
+ if (c.req.query("windows") === "all") {
1538
+ return c.json({ ok: true, data: costAggregator.getAllSnapshots() });
1539
+ }
1540
+ const window = parseWindowParam(c.req.query("window"));
1541
+ return c.json({ ok: true, data: costAggregator.getSnapshot(window) });
740
1542
  });
741
1543
  app6.post("/costs/reset", (c) => {
742
- costAggregator.reset();
743
- return c.json({ ok: true, data: { reset: true } });
1544
+ return c.json(
1545
+ {
1546
+ ok: false,
1547
+ error: {
1548
+ code: "GONE",
1549
+ message: "POST /api/costs/reset was removed in @axlsdk/studio 0.15. Cost aggregates are now time-windowed and rebuilt from StateStore history. Use GET /api/costs?window=24h|7d|30d|all to narrow the view instead of resetting."
1550
+ }
1551
+ },
1552
+ 410
1553
+ );
744
1554
  });
745
1555
  return app6;
746
1556
  }
@@ -748,8 +1558,9 @@ function createCostRoutes(costAggregator) {
748
1558
  // src/server/routes/evals.ts
749
1559
  var import_node_crypto = require("crypto");
750
1560
  var import_hono10 = require("hono");
751
- function createEvalRoutes(evalLoader) {
1561
+ function createEvalRoutes(connMgr, evalLoader) {
752
1562
  const app6 = new import_hono10.Hono();
1563
+ const activeRuns = /* @__PURE__ */ new Map();
753
1564
  app6.get("/evals", async (c) => {
754
1565
  if (evalLoader) await evalLoader();
755
1566
  const runtime = c.get("runtime");
@@ -759,7 +1570,10 @@ function createEvalRoutes(evalLoader) {
759
1570
  app6.get("/evals/history", async (c) => {
760
1571
  const runtime = c.get("runtime");
761
1572
  const history = await runtime.getEvalHistory();
762
- return c.json({ ok: true, data: history });
1573
+ return c.json({
1574
+ ok: true,
1575
+ data: redactEvalHistoryList(history, runtime.isRedactEnabled())
1576
+ });
763
1577
  });
764
1578
  app6.delete("/evals/history/:id", async (c) => {
765
1579
  const runtime = c.get("runtime");
@@ -780,6 +1594,7 @@ function createEvalRoutes(evalLoader) {
780
1594
  if (evalLoader) await evalLoader();
781
1595
  const runtime = c.get("runtime");
782
1596
  const name = c.req.param("name");
1597
+ const redactOn = runtime.isRedactEnabled();
783
1598
  const entry = runtime.getRegisteredEval(name);
784
1599
  if (!entry) {
785
1600
  return c.json(
@@ -788,13 +1603,89 @@ function createEvalRoutes(evalLoader) {
788
1603
  );
789
1604
  }
790
1605
  let runs = 1;
1606
+ let stream = false;
1607
+ let captureTraces = false;
791
1608
  try {
792
1609
  const body = await c.req.json().catch(() => ({}));
793
1610
  if (typeof body.runs === "number" && Number.isFinite(body.runs) && body.runs > 1) {
794
1611
  runs = Math.min(Math.floor(body.runs), 25);
795
1612
  }
1613
+ if (body.stream === true) {
1614
+ stream = true;
1615
+ }
1616
+ if (body.captureTraces === true) {
1617
+ captureTraces = true;
1618
+ }
796
1619
  } catch {
797
1620
  }
1621
+ if (stream) {
1622
+ const evalRunId = `eval-${(0, import_node_crypto.randomUUID)()}`;
1623
+ const ac = new AbortController();
1624
+ activeRuns.set(evalRunId, ac);
1625
+ (async () => {
1626
+ try {
1627
+ if (runs > 1) {
1628
+ const runGroupId = (0, import_node_crypto.randomUUID)();
1629
+ const results = [];
1630
+ for (let r = 0; r < runs; r++) {
1631
+ if (ac.signal.aborted) break;
1632
+ const result = await runtime.runRegisteredEval(name, {
1633
+ metadata: { runGroupId, runIndex: r },
1634
+ signal: ac.signal,
1635
+ captureTraces,
1636
+ onProgress: (event) => {
1637
+ if (event.type === "run_done") return;
1638
+ connMgr.broadcastWithWildcard(`eval:${evalRunId}`, {
1639
+ ...event,
1640
+ run: r + 1,
1641
+ totalRuns: runs
1642
+ });
1643
+ }
1644
+ });
1645
+ results.push(result);
1646
+ connMgr.broadcastWithWildcard(`eval:${evalRunId}`, {
1647
+ type: "run_done",
1648
+ run: r + 1,
1649
+ totalRuns: runs
1650
+ });
1651
+ }
1652
+ if (results.length > 0) {
1653
+ connMgr.broadcastWithWildcard(`eval:${evalRunId}`, {
1654
+ type: "done",
1655
+ evalResultId: results[0].id,
1656
+ runGroupId
1657
+ });
1658
+ } else {
1659
+ connMgr.broadcastWithWildcard(`eval:${evalRunId}`, {
1660
+ type: "error",
1661
+ message: "All runs were cancelled"
1662
+ });
1663
+ }
1664
+ } else {
1665
+ const result = await runtime.runRegisteredEval(name, {
1666
+ signal: ac.signal,
1667
+ captureTraces,
1668
+ onProgress: (event) => {
1669
+ if (event.type === "run_done") return;
1670
+ connMgr.broadcastWithWildcard(`eval:${evalRunId}`, event);
1671
+ }
1672
+ });
1673
+ connMgr.broadcastWithWildcard(`eval:${evalRunId}`, {
1674
+ type: "done",
1675
+ evalResultId: result.id
1676
+ });
1677
+ }
1678
+ } catch (err) {
1679
+ connMgr.broadcastWithWildcard(`eval:${evalRunId}`, {
1680
+ type: "error",
1681
+ message: redactErrorMessage(err, redactOn)
1682
+ });
1683
+ } finally {
1684
+ activeRuns.delete(evalRunId);
1685
+ }
1686
+ })();
1687
+ return c.json({ ok: true, data: { evalRunId } });
1688
+ }
798
1689
  try {
799
1690
  if (runs > 1) {
800
1691
  const { aggregateRuns } = await import("@axlsdk/eval");
@@ -802,27 +1693,53 @@ function createEvalRoutes(evalLoader) {
802
1693
  const results = [];
803
1694
  for (let r = 0; r < runs; r++) {
804
1695
  const result2 = await runtime.runRegisteredEval(name, {
805
- metadata: { runGroupId, runIndex: r }
1696
+ metadata: { runGroupId, runIndex: r },
1697
+ captureTraces
806
1698
  });
807
1699
  results.push(result2);
808
1700
  }
809
1701
  const typedResults = results;
810
1702
  const aggregate = aggregateRuns(typedResults);
811
1703
  const first = typedResults[0];
812
- const result = { ...first, _multiRun: { aggregate, allRuns: typedResults } };
813
- return c.json({ ok: true, data: result });
1704
+ const result = {
1705
+ ...first,
1706
+ _multiRun: { aggregate, allRuns: typedResults }
1707
+ };
1708
+ return c.json({
1709
+ ok: true,
1710
+ data: redactEvalResult(result, redactOn)
1711
+ });
814
1712
  } else {
815
- const result = await runtime.runRegisteredEval(name);
816
- return c.json({ ok: true, data: result });
1713
+ const result = await runtime.runRegisteredEval(name, { captureTraces });
1714
+ return c.json({
1715
+ ok: true,
1716
+ data: redactEvalResult(result, redactOn)
1717
+ });
817
1718
  }
818
1719
  } catch (err) {
819
- const message = err instanceof Error ? err.message : String(err);
820
- return c.json({ ok: false, error: { code: "EVAL_ERROR", message } }, 400);
1720
+ return c.json(
1721
+ { ok: false, error: { code: "EVAL_ERROR", message: redactErrorMessage(err, redactOn) } },
1722
+ 400
1723
+ );
1724
+ }
1725
+ });
1726
+ app6.post("/evals/runs/:evalRunId/cancel", (c) => {
1727
+ const evalRunId = c.req.param("evalRunId");
1728
+ const ac = activeRuns.get(evalRunId);
1729
+ if (!ac) {
1730
+ return c.json(
1731
+ { ok: false, error: { code: "NOT_FOUND", message: "No active eval run found" } },
1732
+ 404
1733
+ );
821
1734
  }
1735
+ ac.abort();
1736
+ activeRuns.delete(evalRunId);
1737
+ return c.json({ ok: true, data: { cancelled: true } });
822
1738
  });
823
1739
  app6.post("/evals/:name/rescore", async (c) => {
824
1740
  if (evalLoader) await evalLoader();
825
1741
  const runtime = c.get("runtime");
1742
+ const redactOn = runtime.isRedactEnabled();
826
1743
  const name = c.req.param("name");
827
1744
  const body = await c.req.json();
828
1745
  if (!body.resultId || typeof body.resultId !== "string") {
@@ -860,19 +1777,29 @@ function createEvalRoutes(evalLoader) {
860
1777
  timestamp: Date.now(),
861
1778
  data: result
862
1779
  });
863
- return c.json({ ok: true, data: result });
1780
+ return c.json({
1781
+ ok: true,
1782
+ data: redactEvalResult(result, redactOn)
1783
+ });
864
1784
  } catch (err) {
865
- const message = err instanceof Error ? err.message : String(err);
866
- return c.json({ ok: false, error: { code: "EVAL_ERROR", message } }, 400);
1785
+ return c.json(
1786
+ { ok: false, error: { code: "EVAL_ERROR", message: redactErrorMessage(err, redactOn) } },
1787
+ 400
1788
+ );
867
1789
  }
868
1790
  });
869
1791
  app6.post("/evals/compare", async (c) => {
870
1792
  const runtime = c.get("runtime");
1793
+ const redactOn = runtime.isRedactEnabled();
871
1794
  const body = await c.req.json();
1795
+ const MAX_POOLED_RUNS = 25;
872
1796
  const validateIdParam = (v, name) => {
873
1797
  if (typeof v === "string") return v === "" ? `${name} must be non-empty` : null;
874
1798
  if (Array.isArray(v)) {
875
1799
  if (v.length === 0) return `${name} must be a non-empty array`;
1800
+ if (v.length > MAX_POOLED_RUNS) {
1801
+ return `${name} may contain at most ${MAX_POOLED_RUNS} ids (pooled comparison)`;
1802
+ }
876
1803
  for (const elem of v) {
877
1804
  if (typeof elem !== "string" || elem === "") {
878
1805
  return `${name} array must contain only non-empty strings`;
@@ -935,8 +1862,13 @@ function createEvalRoutes(evalLoader) {
935
1862
  const result = await runtime.evalCompare(baseline, candidate, body.options);
936
1863
  return c.json({ ok: true, data: result });
937
1864
  } catch (err) {
938
- const message = err instanceof Error ? err.message : String(err);
939
- return c.json({ ok: false, error: { code: "COMPARE_FAILED", message } }, 400);
1865
+ return c.json(
1866
+ {
1867
+ ok: false,
1868
+ error: { code: "COMPARE_FAILED", message: redactErrorMessage(err, redactOn) }
1869
+ },
1870
+ 400
1871
+ );
940
1872
  }
941
1873
  });
942
1874
  app6.post("/evals/import", async (c) => {
@@ -998,7 +1930,11 @@ function createEvalRoutes(evalLoader) {
998
1930
  });
999
1931
  return c.json({ ok: true, data: { id, eval: evalName, timestamp } });
1000
1932
  });
1001
- return app6;
1933
+ function closeActiveRuns() {
1934
+ for (const ac of activeRuns.values()) ac.abort();
1935
+ activeRuns.clear();
1936
+ }
1937
+ return { app: app6, closeActiveRuns };
1002
1938
  }
1003
1939
 
1004
1940
  // src/server/routes/playground.ts
@@ -1032,34 +1968,50 @@ function createPlaygroundRoutes(connMgr) {
1032
1968
  );
1033
1969
  }
1034
1970
  const sessionId = body.sessionId ?? `playground-${Date.now()}`;
1035
- const executionId = `playground-${sessionId}-${Date.now()}`;
1036
1971
  const store = runtime.getStateStore();
1037
1972
  const history = await store.getSession(sessionId);
1038
1973
  history.push({ role: "user", content: body.message });
1039
- const ctx = runtime.createContext({
1040
- sessionHistory: history,
1041
- onToken: (token) => {
1042
- connMgr.broadcastWithWildcard(`execution:${executionId}`, {
1043
- type: "token",
1044
- data: token
1045
- });
1046
- }
1047
- });
1974
+ const redactOn = runtime.isRedactEnabled();
1975
+ const ctx = runtime.createContext({ sessionHistory: history });
1976
+ const executionId = ctx.executionId;
1977
+ const traceListener = (event) => {
1978
+ if (event.executionId !== executionId) return;
1979
+ connMgr.broadcastWithWildcard(`execution:${executionId}`, redactStreamEvent(event, redactOn));
1980
+ };
1981
+ runtime.on("trace", traceListener);
1048
1982
  (async () => {
1983
+ let stepCounter = Number.MAX_SAFE_INTEGER - 1;
1984
+ const terminalFields = () => ({
1985
+ executionId,
1986
+ step: stepCounter++,
1987
+ timestamp: Date.now()
1988
+ });
1049
1989
  try {
1050
1990
  const result = await ctx.ask(agent, body.message);
1051
1991
  const resultText = typeof result === "string" ? result : JSON.stringify(result);
1052
1992
  history.push({ role: "assistant", content: resultText });
1053
1993
  await store.saveSession(sessionId, history);
1054
- connMgr.broadcastWithWildcard(`execution:${executionId}`, {
1994
+ const doneEvent = {
1995
+ ...terminalFields(),
1055
1996
  type: "done",
1056
- data: resultText
1057
- });
1997
+ data: { result: resultText }
1998
+ };
1999
+ connMgr.broadcastWithWildcard(
2000
+ `execution:${executionId}`,
2001
+ redactStreamEvent(doneEvent, redactOn)
2002
+ );
1058
2003
  } catch (err) {
1059
- connMgr.broadcastWithWildcard(`execution:${executionId}`, {
2004
+ const errorEvent = {
2005
+ ...terminalFields(),
1060
2006
  type: "error",
1061
- message: err instanceof Error ? err.message : String(err)
1062
- });
2007
+ data: { message: err instanceof Error ? err.message : String(err) }
2008
+ };
2009
+ connMgr.broadcastWithWildcard(
2010
+ `execution:${executionId}`,
2011
+ redactStreamEvent(errorEvent, redactOn)
2012
+ );
2013
+ } finally {
2014
+ runtime.off("trace", traceListener);
1063
2015
  }
1064
2016
  })();
1065
2017
  return c.json({
@@ -1070,12 +2022,78 @@ function createPlaygroundRoutes(connMgr) {
1070
2022
  return app6;
1071
2023
  }
1072
2024
 
2025
+ // src/server/routes/eval-trends.ts
2026
+ var import_hono12 = require("hono");
2027
+ function createEvalTrendsRoutes(aggregator) {
2028
+ const app6 = new import_hono12.Hono();
2029
+ app6.get("/eval-trends", (c) => {
2030
+ const window = parseWindowParam(c.req.query("window"));
2031
+ return c.json({ ok: true, data: aggregator.getSnapshot(window) });
2032
+ });
2033
+ return app6;
2034
+ }
2035
+
2036
+ // src/server/routes/workflow-stats.ts
2037
+ var import_hono13 = require("hono");
2038
+ function createWorkflowStatsRoutes(aggregator) {
2039
+ const app6 = new import_hono13.Hono();
2040
+ app6.get("/workflow-stats", (c) => {
2041
+ const window = parseWindowParam(c.req.query("window"));
2042
+ return c.json({ ok: true, data: enrichWorkflowStats(aggregator.getSnapshot(window)) });
2043
+ });
2044
+ return app6;
2045
+ }
2046
+
2047
+ // src/server/routes/trace-stats.ts
2048
+ var import_hono14 = require("hono");
2049
+ function createTraceStatsRoutes(aggregator) {
2050
+ const app6 = new import_hono14.Hono();
2051
+ app6.get("/trace-stats", (c) => {
2052
+ const window = parseWindowParam(c.req.query("window"));
2053
+ return c.json({ ok: true, data: aggregator.getSnapshot(window) });
2054
+ });
2055
+ return app6;
2056
+ }
2057
+
1073
2058
  // src/server/index.ts
1074
2059
  function createServer(options) {
1075
2060
  const { runtime, staticRoot, basePath = "", readOnly = false } = options;
1076
- const app6 = new import_hono12.Hono();
1077
- const connMgr = new ConnectionManager();
1078
- const costAggregator = new CostAggregator(connMgr);
2061
+ const app6 = new import_hono15.Hono();
2062
+ const connMgr = new ConnectionManager(options.bufferCaps);
2063
+ const windows = ["24h", "7d", "30d", "all"];
2064
+ const costAggregator = new TraceAggregator({
2065
+ runtime,
2066
+ connMgr,
2067
+ channel: "costs",
2068
+ reducer: reduceCost,
2069
+ emptyState: emptyCostData,
2070
+ windows
2071
+ });
2072
+ const workflowStatsAggregator = new ExecutionAggregator({
2073
+ runtime,
2074
+ connMgr,
2075
+ channel: "workflow-stats",
2076
+ reducer: reduceWorkflowStats,
2077
+ emptyState: emptyWorkflowStatsData,
2078
+ windows,
2079
+ broadcastTransform: enrichWorkflowStats
2080
+ });
2081
+ const traceStatsAggregator = new TraceAggregator({
2082
+ runtime,
2083
+ connMgr,
2084
+ channel: "trace-stats",
2085
+ reducer: reduceTraceStats,
2086
+ emptyState: emptyTraceStatsData,
2087
+ windows
2088
+ });
2089
+ const evalTrendsAggregator = new EvalAggregator({
2090
+ runtime,
2091
+ connMgr,
2092
+ channel: "eval-trends",
2093
+ reducer: reduceEvalTrends,
2094
+ emptyState: emptyEvalTrendData,
2095
+ windows
2096
+ });
1079
2097
  if (options.cors !== false) {
1080
2098
  app6.use("*", (0, import_cors.cors)());
1081
2099
  }
@@ -1093,11 +2111,11 @@ function createServer(options) {
1093
2111
  /^PUT \/api\/memory(\/|$)/,
1094
2112
  /^DELETE \/api\/memory(\/|$)/,
1095
2113
  /^POST \/api\/decisions(\/|$)/,
1096
- /^POST \/api\/costs(\/|$)/,
1097
2114
  /^POST \/api\/tools(\/|$)/,
1098
2115
  /^POST \/api\/evals\/import$/,
1099
2116
  /^POST \/api\/evals\/[^/]+\/run$/,
1100
2117
  /^POST \/api\/evals\/[^/]+\/rescore$/,
2118
+ /^POST \/api\/evals\/runs\/[^/]+\/cancel$/,
1101
2119
  /^DELETE \/api\/evals\/history\/[^/]+$/,
1102
2120
  /^POST \/api\/playground(\/|$)/
1103
2121
  ];
@@ -1117,7 +2135,7 @@ function createServer(options) {
1117
2135
  await next();
1118
2136
  });
1119
2137
  }
1120
- const api = new import_hono12.Hono();
2138
+ const api = new import_hono15.Hono();
1121
2139
  api.route("/", createHealthRoutes(readOnly));
1122
2140
  api.route("/", createWorkflowRoutes(connMgr));
1123
2141
  api.route("/", executions_default);
@@ -1127,20 +2145,37 @@ function createServer(options) {
1127
2145
  api.route("/", memory_default);
1128
2146
  api.route("/", decisions_default);
1129
2147
  api.route("/", createCostRoutes(costAggregator));
1130
- api.route("/", createEvalRoutes(options.evalLoader));
2148
+ api.route("/", createEvalTrendsRoutes(evalTrendsAggregator));
2149
+ api.route("/", createWorkflowStatsRoutes(workflowStatsAggregator));
2150
+ api.route("/", createTraceStatsRoutes(traceStatsAggregator));
2151
+ const { app: evalApp, closeActiveRuns } = createEvalRoutes(connMgr, options.evalLoader);
2152
+ api.route("/", evalApp);
1131
2153
  api.route("/", createPlaygroundRoutes(connMgr));
1132
2154
  app6.route("/api", api);
1133
2155
  const traceListener = (event) => {
1134
- const traceEvent = event;
1135
- if (traceEvent.executionId) {
1136
- connMgr.broadcastWithWildcard(`trace:${traceEvent.executionId}`, traceEvent);
1137
- }
1138
- costAggregator.onTrace(traceEvent);
1139
- if (traceEvent.type === "await_human") {
1140
- connMgr.broadcast("decisions", traceEvent);
2156
+ try {
2157
+ const traceEvent = event;
2158
+ const redacted = redactStreamEvent(traceEvent, runtime.isRedactEnabled());
2159
+ if (traceEvent.executionId) {
2160
+ connMgr.broadcastWithWildcard(`trace:${traceEvent.executionId}`, redacted);
2161
+ }
2162
+ if (traceEvent.type === "await_human") {
2163
+ connMgr.broadcast("decisions", redacted);
2164
+ }
2165
+ } catch (err) {
2166
+ console.error(
2167
+ "[axl-studio] trace listener threw; event dropped:",
2168
+ err instanceof Error ? err.message : String(err)
2169
+ );
1141
2170
  }
1142
2171
  };
1143
2172
  runtime.on("trace", traceListener);
2173
+ const aggregatorStartPromise = Promise.all([
2174
+ costAggregator.start(),
2175
+ workflowStatsAggregator.start(),
2176
+ traceStatsAggregator.start(),
2177
+ evalTrendsAggregator.start()
2178
+ ]).catch((err) => console.error("[axl-studio] aggregator start failed:", err));
1144
2179
  if (staticRoot) {
1145
2180
  const indexPath = (0, import_node_path.resolve)(staticRoot, "index.html");
1146
2181
  let spaHtml;
@@ -1190,9 +2225,22 @@ function createServer(options) {
1190
2225
  app: app6,
1191
2226
  connMgr,
1192
2227
  costAggregator,
2228
+ workflowStatsAggregator,
2229
+ traceStatsAggregator,
2230
+ evalTrendsAggregator,
2231
+ aggregatorStartPromise,
1193
2232
  /** Create WS handlers. Call before registering static/SPA routes are reached. */
1194
2233
  createWsHandlers: () => createWsHandlers(connMgr),
1195
- traceListener
2234
+ traceListener,
2235
+ /** Abort all active streaming eval runs. */
2236
+ closeActiveRuns,
2237
+ /** Close all aggregators (clear intervals and unsubscribe listeners). */
2238
+ closeAggregators: () => {
2239
+ costAggregator.close();
2240
+ workflowStatsAggregator.close();
2241
+ traceStatsAggregator.close();
2242
+ evalTrendsAggregator.close();
2243
+ }
1196
2244
  };
1197
2245
  }
1198
2246
 
@@ -1374,7 +2422,13 @@ async function registerConditions(conditions) {
1374
2422
  // src/middleware.ts
1375
2423
  var import_meta2 = {};
1376
2424
  function createStudioMiddleware(options) {
1377
- const { runtime, serveClient = true, verifyUpgrade, readOnly = false } = options;
2425
+ const {
2426
+ runtime,
2427
+ serveClient = true,
2428
+ verifyUpgrade,
2429
+ readOnly = false,
2430
+ filterTraceEvent
2431
+ } = options;
1378
2432
  const basePath = normalizeBasePath(options.basePath);
1379
2433
  const staticRoot = serveClient ? resolveClientDist() : void 0;
1380
2434
  if (serveClient && !staticRoot) {
@@ -1384,15 +2438,19 @@ function createStudioMiddleware(options) {
1384
2438
  );
1385
2439
  }
1386
2440
  const evalLoader = options.evals ? createEvalLoader(options.evals, runtime) : void 0;
1387
- const { app: app6, connMgr, traceListener } = createServer({
2441
+ const { app: app6, connMgr, traceListener, closeActiveRuns, closeAggregators } = createServer({
1388
2442
  runtime,
1389
2443
  staticRoot,
1390
2444
  basePath,
1391
2445
  readOnly,
1392
2446
  cors: false,
1393
2447
  // Host framework owns CORS policy
1394
- evalLoader
2448
+ evalLoader,
2449
+ bufferCaps: options.bufferCaps
1395
2450
  });
2451
+ if (filterTraceEvent) {
2452
+ connMgr.setFilter(filterTraceEvent);
2453
+ }
1396
2454
  if (process.env.NODE_ENV === "production" && !verifyUpgrade) {
1397
2455
  console.warn(
1398
2456
  "[axl-studio] WARNING: Studio middleware mounted in production without verifyUpgrade. WebSocket connections are not authenticated. All registered workflows, tools, and agents are accessible. See https://axlsdk.com/docs/studio/security"
@@ -1433,7 +2491,7 @@ function createStudioMiddleware(options) {
1433
2491
  }
1434
2492
  });
1435
2493
  }
1436
- function handleWebSocket(ws) {
2494
+ function handleWebSocket(ws, metadata) {
1437
2495
  if (closed) {
1438
2496
  ws.close();
1439
2497
  return;
@@ -1443,6 +2501,9 @@ function createStudioMiddleware(options) {
1443
2501
  close: () => ws.close()
1444
2502
  };
1445
2503
  connMgr.add(socket);
2504
+ if (metadata !== void 0) {
2505
+ connMgr.setMetadata(socket, metadata);
2506
+ }
1446
2507
  ws.on("message", (raw) => {
1447
2508
  const reply = handleWsMessage(String(raw), socket, connMgr);
1448
2509
  if (reply) ws.send(reply);
@@ -1465,32 +2526,42 @@ function createStudioMiddleware(options) {
1465
2526
  upgradeHandler = async (req, socket, head) => {
1466
2527
  const pathname = new URL(req.url, `http://${req.headers.host}`).pathname;
1467
2528
  if (pathname !== wsPath) return;
2529
+ if (closed) {
2530
+ socket.destroy();
2531
+ return;
2532
+ }
2533
+ let connectionMetadata;
1468
2534
  if (verifyUpgrade) {
1469
2535
  try {
1470
- const allowed = await verifyUpgrade(req);
2536
+ const result = await verifyUpgrade(req);
2537
+ const allowed = typeof result === "boolean" ? result : result.allowed;
1471
2538
  if (!allowed) {
1472
2539
  socket.write("HTTP/1.1 401 Unauthorized\r\n\r\n");
1473
2540
  socket.destroy();
1474
2541
  return;
1475
2542
  }
2543
+ if (typeof result === "object" && result !== null) {
2544
+ connectionMetadata = result.metadata;
2545
+ }
1476
2546
  } catch {
1477
2547
  socket.write("HTTP/1.1 403 Forbidden\r\n\r\n");
1478
2548
  socket.destroy();
1479
2549
  return;
1480
2550
  }
1481
2551
  }
1482
- if (!wss) {
2552
+ if (closed || !wss) {
1483
2553
  socket.destroy();
1484
2554
  return;
1485
2555
  }
1486
2556
  wss.handleUpgrade(req, socket, head, (ws) => {
1487
- handleWebSocket(ws);
2557
+ handleWebSocket(ws, connectionMetadata);
1488
2558
  });
1489
2559
  };
1490
2560
  server.on("upgrade", upgradeHandler);
1491
2561
  }
1492
2562
  function close() {
1493
2563
  closed = true;
2564
+ closeActiveRuns();
1494
2565
  connMgr.closeAll();
1495
2566
  if (upgradeHandler && serverRef) {
1496
2567
  serverRef.removeListener("upgrade", upgradeHandler);
@@ -1504,6 +2575,7 @@ function createStudioMiddleware(options) {
1504
2575
  if (traceListener) {
1505
2576
  runtime.removeListener("trace", traceListener);
1506
2577
  }
2578
+ closeAggregators();
1507
2579
  }
1508
2580
  return {
1509
2581
  handler,