@axlsdk/studio 0.15.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.
@@ -6,6 +6,7 @@ import { cors } from "hono/cors";
6
6
  import { serveStatic } from "@hono/node-server/serve-static";
7
7
 
8
8
  // src/server/redact.ts
9
+ import { redactEvent } from "@axlsdk/axl";
9
10
  var REDACTED = "[redacted]";
10
11
  var SAFE_ERROR_NAMES = /* @__PURE__ */ new Set([
11
12
  "QuorumNotMet",
@@ -30,7 +31,8 @@ function redactExecutionInfo(info, redact) {
30
31
  return {
31
32
  ...info,
32
33
  ...info.result !== void 0 ? { result: REDACTED } : {},
33
- ...info.error !== void 0 ? { error: REDACTED } : {}
34
+ ...info.error !== void 0 ? { error: REDACTED } : {},
35
+ events: info.events.map((e) => redactStreamEvent(e, true))
34
36
  };
35
37
  }
36
38
  function redactExecutionList(infos, redact) {
@@ -71,30 +73,7 @@ function redactSessionHistory(history, redact) {
71
73
  }
72
74
  function redactStreamEvent(event, redact) {
73
75
  if (!redact) return event;
74
- switch (event.type) {
75
- case "token":
76
- return { type: "token", data: REDACTED };
77
- case "tool_call":
78
- return { ...event, args: REDACTED };
79
- case "tool_result":
80
- return { ...event, result: REDACTED };
81
- case "tool_approval":
82
- return {
83
- ...event,
84
- args: REDACTED,
85
- ...event.reason !== void 0 ? { reason: REDACTED } : {}
86
- };
87
- case "done":
88
- return { type: "done", data: REDACTED };
89
- case "error":
90
- return { type: "error", message: REDACTED };
91
- // Structural events have no user content to scrub.
92
- case "agent_start":
93
- case "agent_end":
94
- case "handoff":
95
- case "step":
96
- return event;
97
- }
76
+ return redactEvent(event);
98
77
  }
99
78
  function redactEvalItem(item) {
100
79
  const scrubbed = {
@@ -180,13 +159,17 @@ async function errorHandler(c, next) {
180
159
 
181
160
  // src/server/ws/connection-manager.ts
182
161
  var BUFFER_TTL_MS = 3e4;
183
- var MAX_BUFFER_EVENTS = 500;
162
+ var DEFAULT_MAX_BUFFER_EVENTS = 1e3;
163
+ var DEFAULT_MAX_BUFFER_BYTES = 4 * 1024 * 1024;
164
+ var DEFAULT_MAX_ACTIVE_BUFFERS = 256;
165
+ var UNBUFFERED_EVENT_TYPES = /* @__PURE__ */ new Set(["token", "partial_object"]);
184
166
  var MAX_WS_FRAME_BYTES = 65536;
185
167
  function isBufferedChannel(channel) {
186
168
  return channel.startsWith("execution:") || channel.startsWith("eval:");
187
169
  }
188
170
  function truncateIfOversized(msg, channel, data) {
189
- if (msg.length <= MAX_WS_FRAME_BYTES) return msg;
171
+ const msgBytes = Buffer.byteLength(msg, "utf8");
172
+ if (msgBytes <= MAX_WS_FRAME_BYTES) return msg;
190
173
  const event = data ?? {};
191
174
  const truncated = {
192
175
  type: "event",
@@ -195,7 +178,7 @@ function truncateIfOversized(msg, channel, data) {
195
178
  ...event,
196
179
  data: {
197
180
  __truncated: true,
198
- originalBytes: msg.length,
181
+ originalBytes: msgBytes,
199
182
  maxBytes: MAX_WS_FRAME_BYTES,
200
183
  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."
201
184
  }
@@ -212,6 +195,25 @@ var ConnectionManager = class {
212
195
  buffers = /* @__PURE__ */ new Map();
213
196
  maxConnections = 100;
214
197
  filter;
198
+ /** Resolved replay-buffer caps. Per-instance so embedders can dial them
199
+ * without monkey-patching module-level constants. */
200
+ maxEventsPerBuffer;
201
+ maxBytesPerBuffer;
202
+ maxActiveBuffers;
203
+ constructor(bufferCaps) {
204
+ const validatePositiveInt = (key, value) => {
205
+ if (value === void 0) return;
206
+ if (!Number.isFinite(value) || !Number.isInteger(value) || value < 1) {
207
+ throw new RangeError(`bufferCaps.${key} must be a positive integer (>= 1); got ${value}`);
208
+ }
209
+ };
210
+ validatePositiveInt("maxEventsPerBuffer", bufferCaps?.maxEventsPerBuffer);
211
+ validatePositiveInt("maxBytesPerBuffer", bufferCaps?.maxBytesPerBuffer);
212
+ validatePositiveInt("maxActiveBuffers", bufferCaps?.maxActiveBuffers);
213
+ this.maxEventsPerBuffer = bufferCaps?.maxEventsPerBuffer ?? DEFAULT_MAX_BUFFER_EVENTS;
214
+ this.maxBytesPerBuffer = bufferCaps?.maxBytesPerBuffer ?? DEFAULT_MAX_BUFFER_BYTES;
215
+ this.maxActiveBuffers = bufferCaps?.maxActiveBuffers ?? DEFAULT_MAX_ACTIVE_BUFFERS;
216
+ }
215
217
  /**
216
218
  * Register a broadcast filter. Called once at middleware construction.
217
219
  * The filter runs on every outbound event and can drop or deliver based
@@ -294,13 +296,37 @@ var ConnectionManager = class {
294
296
  if (isBufferedChannel(channel)) {
295
297
  let buffer = this.buffers.get(channel);
296
298
  if (!buffer) {
297
- buffer = { events: [], complete: false };
299
+ if (this.buffers.size >= this.maxActiveBuffers) {
300
+ let victim;
301
+ for (const [ch, buf] of this.buffers) {
302
+ if (buf.complete) {
303
+ victim = ch;
304
+ break;
305
+ }
306
+ }
307
+ if (victim === void 0) {
308
+ victim = this.buffers.keys().next().value;
309
+ }
310
+ if (victim !== void 0) {
311
+ const old = this.buffers.get(victim);
312
+ if (old?.timer) clearTimeout(old.timer);
313
+ this.buffers.delete(victim);
314
+ }
315
+ }
316
+ buffer = { events: [], complete: false, bytes: 0 };
298
317
  this.buffers.set(channel, buffer);
299
318
  }
300
319
  const event = data;
301
320
  const isTerminal = event.type === "done" || event.type === "error";
302
- if (buffer.events.length < MAX_BUFFER_EVENTS || isTerminal) {
303
- buffer.events.push({ msg, data });
321
+ const isUnbuffered = event.type !== void 0 && UNBUFFERED_EVENT_TYPES.has(event.type);
322
+ if (!isUnbuffered) {
323
+ const msgBytes = Buffer.byteLength(msg, "utf8");
324
+ const atCountCap = buffer.events.length >= this.maxEventsPerBuffer;
325
+ const atByteCap = buffer.bytes + msgBytes > this.maxBytesPerBuffer;
326
+ if (isTerminal || !atCountCap && !atByteCap) {
327
+ buffer.events.push({ msg, data });
328
+ buffer.bytes += msgBytes;
329
+ }
304
330
  }
305
331
  if (isTerminal) {
306
332
  buffer.complete = true;
@@ -385,7 +411,7 @@ var VALID_CHANNEL_PREFIXES = ["execution:", "trace:", "eval:"];
385
411
  var VALID_EXACT_CHANNELS = ["costs", "decisions", "eval-trends", "workflow-stats", "trace-stats"];
386
412
  var MAX_CHANNEL_LENGTH = 256;
387
413
  function handleWsMessage(raw, socket, connMgr) {
388
- if (raw.length > MAX_WS_FRAME_BYTES) {
414
+ if (Buffer.byteLength(raw, "utf8") > MAX_WS_FRAME_BYTES) {
389
415
  return JSON.stringify({ type: "error", message: "Message too large" });
390
416
  }
391
417
  let msg;
@@ -541,7 +567,7 @@ var TraceAggregator = class {
541
567
  this.options.windows.map((w) => [w, this.options.emptyState()])
542
568
  );
543
569
  for (const exec of capped) {
544
- for (const event of exec.steps) {
570
+ for (const event of exec.events) {
545
571
  for (const window of this.options.windows) {
546
572
  if (withinWindow(event.timestamp, window, now)) {
547
573
  fresh.set(window, this.options.reducer(fresh.get(window), event));
@@ -563,15 +589,131 @@ var TraceAggregator = class {
563
589
  }
564
590
  };
565
591
 
592
+ // src/server/aggregates/execution-aggregator.ts
593
+ var ExecutionAggregator = class {
594
+ snaps;
595
+ interval;
596
+ listener;
597
+ options;
598
+ /** Generation counter to prevent stale async fold after rebuild. */
599
+ generation = 0;
600
+ constructor(options) {
601
+ this.options = options;
602
+ this.snaps = new AggregateSnapshots(
603
+ options.windows,
604
+ options.emptyState,
605
+ options.connMgr,
606
+ options.channel,
607
+ options.broadcastTransform
608
+ );
609
+ }
610
+ async start() {
611
+ await this.rebuild();
612
+ this.listener = (event) => {
613
+ if (event.type !== "workflow_end") return;
614
+ const gen = this.generation;
615
+ this.options.runtime.getExecution(event.executionId).then((exec) => {
616
+ if (this.generation !== gen) return;
617
+ if (exec) {
618
+ this.snaps.fold(exec.startedAt, (prev) => this.options.reducer(prev, exec));
619
+ }
620
+ }).catch((err) => console.error("[axl-studio] execution fold failed:", err));
621
+ };
622
+ this.options.runtime.on("trace", this.listener);
623
+ this.interval = setInterval(
624
+ () => this.rebuild().catch((err) => console.error("[axl-studio] rebuild failed:", err)),
625
+ REBUILD_INTERVAL_MS
626
+ );
627
+ }
628
+ async rebuild() {
629
+ this.generation++;
630
+ const executions = await this.options.runtime.getExecutions();
631
+ const cap = this.options.executionCap ?? 2e3;
632
+ const capped = executions.slice(0, cap);
633
+ const now = Date.now();
634
+ const fresh = new Map(
635
+ this.options.windows.map((w) => [w, this.options.emptyState()])
636
+ );
637
+ for (const exec of capped) {
638
+ for (const window of this.options.windows) {
639
+ if (withinWindow(exec.startedAt, window, now)) {
640
+ fresh.set(window, this.options.reducer(fresh.get(window), exec));
641
+ }
642
+ }
643
+ }
644
+ this.snaps.replace(fresh);
645
+ }
646
+ getSnapshot(window) {
647
+ return this.snaps.get(window);
648
+ }
649
+ getAllSnapshots() {
650
+ return this.snaps.getAll();
651
+ }
652
+ close() {
653
+ if (this.listener) this.options.runtime.off("trace", this.listener);
654
+ if (this.interval) clearInterval(this.interval);
655
+ }
656
+ };
657
+
658
+ // src/server/aggregates/eval-aggregator.ts
659
+ var EvalAggregator = class {
660
+ snaps;
661
+ interval;
662
+ listener;
663
+ options;
664
+ constructor(options) {
665
+ this.options = options;
666
+ this.snaps = new AggregateSnapshots(
667
+ options.windows,
668
+ options.emptyState,
669
+ options.connMgr,
670
+ options.channel,
671
+ options.broadcastTransform
672
+ );
673
+ }
674
+ async start() {
675
+ await this.rebuild();
676
+ this.listener = (entry) => {
677
+ this.snaps.fold(entry.timestamp, (prev) => this.options.reducer(prev, entry));
678
+ };
679
+ this.options.runtime.on("eval_result", this.listener);
680
+ this.interval = setInterval(
681
+ () => this.rebuild().catch((err) => console.error("[axl-studio] rebuild failed:", err)),
682
+ REBUILD_INTERVAL_MS
683
+ );
684
+ }
685
+ async rebuild() {
686
+ const history = await this.options.runtime.getEvalHistory();
687
+ const cap = this.options.entryCap ?? 500;
688
+ const capped = history.slice(0, cap);
689
+ const now = Date.now();
690
+ const fresh = new Map(
691
+ this.options.windows.map((w) => [w, this.options.emptyState()])
692
+ );
693
+ for (const entry of capped) {
694
+ for (const window of this.options.windows) {
695
+ if (withinWindow(entry.timestamp, window, now)) {
696
+ fresh.set(window, this.options.reducer(fresh.get(window), entry));
697
+ }
698
+ }
699
+ }
700
+ this.snaps.replace(fresh);
701
+ }
702
+ getSnapshot(window) {
703
+ return this.snaps.get(window);
704
+ }
705
+ getAllSnapshots() {
706
+ return this.snaps.getAll();
707
+ }
708
+ close() {
709
+ if (this.listener) this.options.runtime.off("eval_result", this.listener);
710
+ if (this.interval) clearInterval(this.interval);
711
+ }
712
+ };
713
+
566
714
  // src/server/aggregates/reducers.ts
715
+ import { eventCostContribution } from "@axlsdk/axl";
567
716
  var finite = (v) => Number.isFinite(v) ? v : 0;
568
- function isLogEvent(event, eventName) {
569
- if (event.type === eventName) return true;
570
- if (event.type === "log" && event.data != null && typeof event.data === "object") {
571
- return event.data.event === eventName;
572
- }
573
- return false;
574
- }
575
717
  function emptyRetry() {
576
718
  return {
577
719
  primary: 0,
@@ -597,7 +739,7 @@ function emptyCostData() {
597
739
  };
598
740
  }
599
741
  function reduceCost(acc, event) {
600
- const isWorkflowStart = isLogEvent(event, "workflow_start");
742
+ const isWorkflowStart = event.type === "workflow_start";
601
743
  if (isWorkflowStart && event.workflow) {
602
744
  const byWorkflow2 = { ...acc.byWorkflow };
603
745
  const prev = byWorkflow2[event.workflow] ?? { cost: 0, executions: 0 };
@@ -605,9 +747,10 @@ function reduceCost(acc, event) {
605
747
  return { ...acc, byWorkflow: byWorkflow2 };
606
748
  }
607
749
  if (event.cost == null && !event.tokens) return acc;
608
- const cost = finite(event.cost);
750
+ const cost = eventCostContribution(event);
751
+ if (event.type === "ask_end") return acc;
609
752
  const tokens = event.tokens ?? {};
610
- const totalTokens = event.type === "agent_call" ? {
753
+ const totalTokens = event.type === "agent_call_end" ? {
611
754
  input: acc.totalTokens.input + finite(tokens.input),
612
755
  output: acc.totalTokens.output + finite(tokens.output),
613
756
  reasoning: acc.totalTokens.reasoning + finite(tokens.reasoning)
@@ -638,7 +781,7 @@ function reduceCost(acc, event) {
638
781
  };
639
782
  }
640
783
  let retry = acc.retry;
641
- if (event.type === "agent_call") {
784
+ if (event.type === "agent_call_end") {
642
785
  const d = event.data ?? {};
643
786
  const reason = d.retryReason;
644
787
  retry = { ...acc.retry };
@@ -660,19 +803,17 @@ function reduceCost(acc, event) {
660
803
  }
661
804
  }
662
805
  let byEmbedder = acc.byEmbedder;
663
- if (event.type === "log") {
664
- const d = event.data ?? {};
665
- if (d.event === "memory_remember" || d.event === "memory_recall") {
666
- byEmbedder = { ...acc.byEmbedder };
667
- const modelKey = d.usage?.model ?? "unknown";
668
- const embedTokens = typeof d.usage?.tokens === "number" ? finite(d.usage.tokens) : 0;
669
- const prev = byEmbedder[modelKey] ?? { cost: 0, calls: 0, tokens: 0 };
670
- byEmbedder[modelKey] = {
671
- cost: prev.cost + cost,
672
- calls: prev.calls + 1,
673
- tokens: prev.tokens + embedTokens
674
- };
675
- }
806
+ if (event.type === "memory_remember" || event.type === "memory_recall") {
807
+ const usage = event.data.usage;
808
+ byEmbedder = { ...acc.byEmbedder };
809
+ const modelKey = usage?.model ?? "unknown";
810
+ const embedTokens = typeof usage?.tokens === "number" ? finite(usage.tokens) : 0;
811
+ const prev = byEmbedder[modelKey] ?? { cost: 0, calls: 0, tokens: 0 };
812
+ byEmbedder[modelKey] = {
813
+ cost: prev.cost + cost,
814
+ calls: prev.calls + 1,
815
+ tokens: prev.tokens + embedTokens
816
+ };
676
817
  }
677
818
  return {
678
819
  totalCost: acc.totalCost + cost,
@@ -867,7 +1008,7 @@ function reduceTraceStats(acc, event) {
867
1008
  const eventTypeCounts = { ...acc.eventTypeCounts };
868
1009
  eventTypeCounts[event.type] = (eventTypeCounts[event.type] ?? 0) + 1;
869
1010
  const byTool = { ...acc.byTool };
870
- if (event.type === "tool_call" || event.type === "tool_denied" || event.type === "tool_approval") {
1011
+ if (event.type === "tool_call_end" || event.type === "tool_denied" || event.type === "tool_approval") {
871
1012
  const toolName = event.tool;
872
1013
  const prev = byTool[toolName] ?? { calls: 0, denied: 0, approved: 0 };
873
1014
  const isDeniedEvent = event.type === "tool_denied";
@@ -876,13 +1017,13 @@ function reduceTraceStats(acc, event) {
876
1017
  const isApproved = isDeniedEvent && eventData?.approved === true || isApprovalEvent && eventData?.approved === true;
877
1018
  const isDenied = isDeniedEvent && !eventData?.approved || isApprovalEvent && eventData?.approved === false;
878
1019
  byTool[toolName] = {
879
- calls: prev.calls + (event.type === "tool_call" ? 1 : 0),
1020
+ calls: prev.calls + (event.type === "tool_call_end" ? 1 : 0),
880
1021
  denied: prev.denied + (isDenied ? 1 : 0),
881
1022
  approved: prev.approved + (isApproved ? 1 : 0)
882
1023
  };
883
1024
  }
884
1025
  const retryByAgent = { ...acc.retryByAgent };
885
- if (event.agent && event.type === "agent_call") {
1026
+ if (event.agent && event.type === "agent_call_end") {
886
1027
  const data = event.data;
887
1028
  if (data?.retryReason) {
888
1029
  const prev = retryByAgent[event.agent] ?? { schema: 0, validate: 0, guardrail: 0 };
@@ -900,128 +1041,6 @@ function reduceTraceStats(acc, event) {
900
1041
  };
901
1042
  }
902
1043
 
903
- // src/server/aggregates/execution-aggregator.ts
904
- var ExecutionAggregator = class {
905
- snaps;
906
- interval;
907
- listener;
908
- options;
909
- /** Generation counter to prevent stale async fold after rebuild. */
910
- generation = 0;
911
- constructor(options) {
912
- this.options = options;
913
- this.snaps = new AggregateSnapshots(
914
- options.windows,
915
- options.emptyState,
916
- options.connMgr,
917
- options.channel,
918
- options.broadcastTransform
919
- );
920
- }
921
- async start() {
922
- await this.rebuild();
923
- this.listener = (event) => {
924
- if (!isLogEvent(event, "workflow_end")) return;
925
- const gen = this.generation;
926
- this.options.runtime.getExecution(event.executionId).then((exec) => {
927
- if (this.generation !== gen) return;
928
- if (exec) {
929
- this.snaps.fold(exec.startedAt, (prev) => this.options.reducer(prev, exec));
930
- }
931
- }).catch((err) => console.error("[axl-studio] execution fold failed:", err));
932
- };
933
- this.options.runtime.on("trace", this.listener);
934
- this.interval = setInterval(
935
- () => this.rebuild().catch((err) => console.error("[axl-studio] rebuild failed:", err)),
936
- REBUILD_INTERVAL_MS
937
- );
938
- }
939
- async rebuild() {
940
- this.generation++;
941
- const executions = await this.options.runtime.getExecutions();
942
- const cap = this.options.executionCap ?? 2e3;
943
- const capped = executions.slice(0, cap);
944
- const now = Date.now();
945
- const fresh = new Map(
946
- this.options.windows.map((w) => [w, this.options.emptyState()])
947
- );
948
- for (const exec of capped) {
949
- for (const window of this.options.windows) {
950
- if (withinWindow(exec.startedAt, window, now)) {
951
- fresh.set(window, this.options.reducer(fresh.get(window), exec));
952
- }
953
- }
954
- }
955
- this.snaps.replace(fresh);
956
- }
957
- getSnapshot(window) {
958
- return this.snaps.get(window);
959
- }
960
- getAllSnapshots() {
961
- return this.snaps.getAll();
962
- }
963
- close() {
964
- if (this.listener) this.options.runtime.off("trace", this.listener);
965
- if (this.interval) clearInterval(this.interval);
966
- }
967
- };
968
-
969
- // src/server/aggregates/eval-aggregator.ts
970
- var EvalAggregator = class {
971
- snaps;
972
- interval;
973
- listener;
974
- options;
975
- constructor(options) {
976
- this.options = options;
977
- this.snaps = new AggregateSnapshots(
978
- options.windows,
979
- options.emptyState,
980
- options.connMgr,
981
- options.channel,
982
- options.broadcastTransform
983
- );
984
- }
985
- async start() {
986
- await this.rebuild();
987
- this.listener = (entry) => {
988
- this.snaps.fold(entry.timestamp, (prev) => this.options.reducer(prev, entry));
989
- };
990
- this.options.runtime.on("eval_result", this.listener);
991
- this.interval = setInterval(
992
- () => this.rebuild().catch((err) => console.error("[axl-studio] rebuild failed:", err)),
993
- REBUILD_INTERVAL_MS
994
- );
995
- }
996
- async rebuild() {
997
- const history = await this.options.runtime.getEvalHistory();
998
- const cap = this.options.entryCap ?? 500;
999
- const capped = history.slice(0, cap);
1000
- const now = Date.now();
1001
- const fresh = new Map(
1002
- this.options.windows.map((w) => [w, this.options.emptyState()])
1003
- );
1004
- for (const entry of capped) {
1005
- for (const window of this.options.windows) {
1006
- if (withinWindow(entry.timestamp, window, now)) {
1007
- fresh.set(window, this.options.reducer(fresh.get(window), entry));
1008
- }
1009
- }
1010
- }
1011
- this.snaps.replace(fresh);
1012
- }
1013
- getSnapshot(window) {
1014
- return this.snaps.get(window);
1015
- }
1016
- getAllSnapshots() {
1017
- return this.snaps.getAll();
1018
- }
1019
- close() {
1020
- if (this.listener) this.options.runtime.off("eval_result", this.listener);
1021
- if (this.interval) clearInterval(this.interval);
1022
- }
1023
- };
1024
-
1025
1044
  // src/server/routes/health.ts
1026
1045
  import { Hono } from "hono";
1027
1046
  function createHealthRoutes(readOnly) {
@@ -1130,9 +1149,31 @@ app.get("/executions/:id", async (c) => {
1130
1149
  404
1131
1150
  );
1132
1151
  }
1152
+ const sinceParam = c.req.query("since");
1153
+ let paged = execution;
1154
+ if (sinceParam !== void 0) {
1155
+ const since = Number(sinceParam);
1156
+ if (!Number.isFinite(since) || !Number.isInteger(since)) {
1157
+ return c.json(
1158
+ {
1159
+ ok: false,
1160
+ error: {
1161
+ code: "INVALID_PARAM",
1162
+ message: `\`since\` must be a finite integer (got "${sinceParam}")`,
1163
+ param: "since"
1164
+ }
1165
+ },
1166
+ 400
1167
+ );
1168
+ }
1169
+ paged = {
1170
+ ...execution,
1171
+ events: execution.events.filter((e) => e.step > since)
1172
+ };
1173
+ }
1133
1174
  return c.json({
1134
1175
  ok: true,
1135
- data: redactExecutionInfo(execution, runtime.isRedactEnabled())
1176
+ data: redactExecutionInfo(paged, runtime.isRedactEnabled())
1136
1177
  });
1137
1178
  });
1138
1179
  app.post("/executions/:id/abort", (c) => {
@@ -1709,10 +1750,14 @@ function createEvalRoutes(connMgr, evalLoader) {
1709
1750
  const runtime = c.get("runtime");
1710
1751
  const redactOn = runtime.isRedactEnabled();
1711
1752
  const body = await c.req.json();
1753
+ const MAX_POOLED_RUNS = 25;
1712
1754
  const validateIdParam = (v, name) => {
1713
1755
  if (typeof v === "string") return v === "" ? `${name} must be non-empty` : null;
1714
1756
  if (Array.isArray(v)) {
1715
1757
  if (v.length === 0) return `${name} must be a non-empty array`;
1758
+ if (v.length > MAX_POOLED_RUNS) {
1759
+ return `${name} may contain at most ${MAX_POOLED_RUNS} ids (pooled comparison)`;
1760
+ }
1716
1761
  for (const elem of v) {
1717
1762
  if (typeof elem !== "string" || elem === "") {
1718
1763
  return `${name} array must contain only non-empty strings`;
@@ -1881,32 +1926,50 @@ function createPlaygroundRoutes(connMgr) {
1881
1926
  );
1882
1927
  }
1883
1928
  const sessionId = body.sessionId ?? `playground-${Date.now()}`;
1884
- const executionId = `playground-${sessionId}-${Date.now()}`;
1885
1929
  const store = runtime.getStateStore();
1886
1930
  const history = await store.getSession(sessionId);
1887
1931
  history.push({ role: "user", content: body.message });
1888
1932
  const redactOn = runtime.isRedactEnabled();
1889
- const broadcast = (event) => {
1933
+ const ctx = runtime.createContext({ sessionHistory: history });
1934
+ const executionId = ctx.executionId;
1935
+ const traceListener = (event) => {
1936
+ if (event.executionId !== executionId) return;
1890
1937
  connMgr.broadcastWithWildcard(`execution:${executionId}`, redactStreamEvent(event, redactOn));
1891
1938
  };
1892
- const ctx = runtime.createContext({
1893
- sessionHistory: history,
1894
- onToken: (token) => {
1895
- broadcast({ type: "token", data: token });
1896
- }
1897
- });
1939
+ runtime.on("trace", traceListener);
1898
1940
  (async () => {
1941
+ let stepCounter = Number.MAX_SAFE_INTEGER - 1;
1942
+ const terminalFields = () => ({
1943
+ executionId,
1944
+ step: stepCounter++,
1945
+ timestamp: Date.now()
1946
+ });
1899
1947
  try {
1900
1948
  const result = await ctx.ask(agent, body.message);
1901
1949
  const resultText = typeof result === "string" ? result : JSON.stringify(result);
1902
1950
  history.push({ role: "assistant", content: resultText });
1903
1951
  await store.saveSession(sessionId, history);
1904
- broadcast({ type: "done", data: resultText });
1952
+ const doneEvent = {
1953
+ ...terminalFields(),
1954
+ type: "done",
1955
+ data: { result: resultText }
1956
+ };
1957
+ connMgr.broadcastWithWildcard(
1958
+ `execution:${executionId}`,
1959
+ redactStreamEvent(doneEvent, redactOn)
1960
+ );
1905
1961
  } catch (err) {
1906
- broadcast({
1962
+ const errorEvent = {
1963
+ ...terminalFields(),
1907
1964
  type: "error",
1908
- message: err instanceof Error ? err.message : String(err)
1909
- });
1965
+ data: { message: err instanceof Error ? err.message : String(err) }
1966
+ };
1967
+ connMgr.broadcastWithWildcard(
1968
+ `execution:${executionId}`,
1969
+ redactStreamEvent(errorEvent, redactOn)
1970
+ );
1971
+ } finally {
1972
+ runtime.off("trace", traceListener);
1910
1973
  }
1911
1974
  })();
1912
1975
  return c.json({
@@ -1954,7 +2017,7 @@ function createTraceStatsRoutes(aggregator) {
1954
2017
  function createServer(options) {
1955
2018
  const { runtime, staticRoot, basePath = "", readOnly = false } = options;
1956
2019
  const app6 = new Hono15();
1957
- const connMgr = new ConnectionManager();
2020
+ const connMgr = new ConnectionManager(options.bufferCaps);
1958
2021
  const windows = ["24h", "7d", "30d", "all"];
1959
2022
  const costAggregator = new TraceAggregator({
1960
2023
  runtime,
@@ -2048,12 +2111,20 @@ function createServer(options) {
2048
2111
  api.route("/", createPlaygroundRoutes(connMgr));
2049
2112
  app6.route("/api", api);
2050
2113
  const traceListener = (event) => {
2051
- const traceEvent = event;
2052
- if (traceEvent.executionId) {
2053
- connMgr.broadcastWithWildcard(`trace:${traceEvent.executionId}`, traceEvent);
2054
- }
2055
- if (traceEvent.type === "await_human") {
2056
- connMgr.broadcast("decisions", traceEvent);
2114
+ try {
2115
+ const traceEvent = event;
2116
+ const redacted = redactStreamEvent(traceEvent, runtime.isRedactEnabled());
2117
+ if (traceEvent.executionId) {
2118
+ connMgr.broadcastWithWildcard(`trace:${traceEvent.executionId}`, redacted);
2119
+ }
2120
+ if (traceEvent.type === "await_human") {
2121
+ connMgr.broadcast("decisions", redacted);
2122
+ }
2123
+ } catch (err) {
2124
+ console.error(
2125
+ "[axl-studio] trace listener threw; event dropped:",
2126
+ err instanceof Error ? err.message : String(err)
2127
+ );
2057
2128
  }
2058
2129
  };
2059
2130
  runtime.on("trace", traceListener);
@@ -2139,4 +2210,4 @@ export {
2139
2210
  EvalAggregator,
2140
2211
  createServer
2141
2212
  };
2142
- //# sourceMappingURL=chunk-IPDMFFTQ.js.map
2213
+ //# sourceMappingURL=chunk-RE6VPUXA.js.map