@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.
package/dist/cli.cjs CHANGED
@@ -37,6 +37,7 @@ var import_cors = require("hono/cors");
37
37
  var import_serve_static = require("@hono/node-server/serve-static");
38
38
 
39
39
  // src/server/redact.ts
40
+ var import_axl = require("@axlsdk/axl");
40
41
  var REDACTED = "[redacted]";
41
42
  var SAFE_ERROR_NAMES = /* @__PURE__ */ new Set([
42
43
  "QuorumNotMet",
@@ -61,7 +62,8 @@ function redactExecutionInfo(info, redact) {
61
62
  return {
62
63
  ...info,
63
64
  ...info.result !== void 0 ? { result: REDACTED } : {},
64
- ...info.error !== void 0 ? { error: REDACTED } : {}
65
+ ...info.error !== void 0 ? { error: REDACTED } : {},
66
+ events: info.events.map((e) => redactStreamEvent(e, true))
65
67
  };
66
68
  }
67
69
  function redactExecutionList(infos, redact) {
@@ -102,30 +104,7 @@ function redactSessionHistory(history, redact) {
102
104
  }
103
105
  function redactStreamEvent(event, redact) {
104
106
  if (!redact) return event;
105
- switch (event.type) {
106
- case "token":
107
- return { type: "token", data: REDACTED };
108
- case "tool_call":
109
- return { ...event, args: REDACTED };
110
- case "tool_result":
111
- return { ...event, result: REDACTED };
112
- case "tool_approval":
113
- return {
114
- ...event,
115
- args: REDACTED,
116
- ...event.reason !== void 0 ? { reason: REDACTED } : {}
117
- };
118
- case "done":
119
- return { type: "done", data: REDACTED };
120
- case "error":
121
- return { type: "error", message: REDACTED };
122
- // Structural events have no user content to scrub.
123
- case "agent_start":
124
- case "agent_end":
125
- case "handoff":
126
- case "step":
127
- return event;
128
- }
107
+ return (0, import_axl.redactEvent)(event);
129
108
  }
130
109
  function redactEvalItem(item) {
131
110
  const scrubbed = {
@@ -211,13 +190,17 @@ async function errorHandler(c, next) {
211
190
 
212
191
  // src/server/ws/connection-manager.ts
213
192
  var BUFFER_TTL_MS = 3e4;
214
- var MAX_BUFFER_EVENTS = 500;
193
+ var DEFAULT_MAX_BUFFER_EVENTS = 1e3;
194
+ var DEFAULT_MAX_BUFFER_BYTES = 4 * 1024 * 1024;
195
+ var DEFAULT_MAX_ACTIVE_BUFFERS = 256;
196
+ var UNBUFFERED_EVENT_TYPES = /* @__PURE__ */ new Set(["token", "partial_object"]);
215
197
  var MAX_WS_FRAME_BYTES = 65536;
216
198
  function isBufferedChannel(channel) {
217
199
  return channel.startsWith("execution:") || channel.startsWith("eval:");
218
200
  }
219
201
  function truncateIfOversized(msg, channel, data) {
220
- if (msg.length <= MAX_WS_FRAME_BYTES) return msg;
202
+ const msgBytes = Buffer.byteLength(msg, "utf8");
203
+ if (msgBytes <= MAX_WS_FRAME_BYTES) return msg;
221
204
  const event = data ?? {};
222
205
  const truncated = {
223
206
  type: "event",
@@ -226,7 +209,7 @@ function truncateIfOversized(msg, channel, data) {
226
209
  ...event,
227
210
  data: {
228
211
  __truncated: true,
229
- originalBytes: msg.length,
212
+ originalBytes: msgBytes,
230
213
  maxBytes: MAX_WS_FRAME_BYTES,
231
214
  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."
232
215
  }
@@ -243,6 +226,25 @@ var ConnectionManager = class {
243
226
  buffers = /* @__PURE__ */ new Map();
244
227
  maxConnections = 100;
245
228
  filter;
229
+ /** Resolved replay-buffer caps. Per-instance so embedders can dial them
230
+ * without monkey-patching module-level constants. */
231
+ maxEventsPerBuffer;
232
+ maxBytesPerBuffer;
233
+ maxActiveBuffers;
234
+ constructor(bufferCaps) {
235
+ const validatePositiveInt = (key, value) => {
236
+ if (value === void 0) return;
237
+ if (!Number.isFinite(value) || !Number.isInteger(value) || value < 1) {
238
+ throw new RangeError(`bufferCaps.${key} must be a positive integer (>= 1); got ${value}`);
239
+ }
240
+ };
241
+ validatePositiveInt("maxEventsPerBuffer", bufferCaps?.maxEventsPerBuffer);
242
+ validatePositiveInt("maxBytesPerBuffer", bufferCaps?.maxBytesPerBuffer);
243
+ validatePositiveInt("maxActiveBuffers", bufferCaps?.maxActiveBuffers);
244
+ this.maxEventsPerBuffer = bufferCaps?.maxEventsPerBuffer ?? DEFAULT_MAX_BUFFER_EVENTS;
245
+ this.maxBytesPerBuffer = bufferCaps?.maxBytesPerBuffer ?? DEFAULT_MAX_BUFFER_BYTES;
246
+ this.maxActiveBuffers = bufferCaps?.maxActiveBuffers ?? DEFAULT_MAX_ACTIVE_BUFFERS;
247
+ }
246
248
  /**
247
249
  * Register a broadcast filter. Called once at middleware construction.
248
250
  * The filter runs on every outbound event and can drop or deliver based
@@ -325,13 +327,37 @@ var ConnectionManager = class {
325
327
  if (isBufferedChannel(channel)) {
326
328
  let buffer = this.buffers.get(channel);
327
329
  if (!buffer) {
328
- buffer = { events: [], complete: false };
330
+ if (this.buffers.size >= this.maxActiveBuffers) {
331
+ let victim;
332
+ for (const [ch, buf] of this.buffers) {
333
+ if (buf.complete) {
334
+ victim = ch;
335
+ break;
336
+ }
337
+ }
338
+ if (victim === void 0) {
339
+ victim = this.buffers.keys().next().value;
340
+ }
341
+ if (victim !== void 0) {
342
+ const old = this.buffers.get(victim);
343
+ if (old?.timer) clearTimeout(old.timer);
344
+ this.buffers.delete(victim);
345
+ }
346
+ }
347
+ buffer = { events: [], complete: false, bytes: 0 };
329
348
  this.buffers.set(channel, buffer);
330
349
  }
331
350
  const event = data;
332
351
  const isTerminal = event.type === "done" || event.type === "error";
333
- if (buffer.events.length < MAX_BUFFER_EVENTS || isTerminal) {
334
- buffer.events.push({ msg, data });
352
+ const isUnbuffered = event.type !== void 0 && UNBUFFERED_EVENT_TYPES.has(event.type);
353
+ if (!isUnbuffered) {
354
+ const msgBytes = Buffer.byteLength(msg, "utf8");
355
+ const atCountCap = buffer.events.length >= this.maxEventsPerBuffer;
356
+ const atByteCap = buffer.bytes + msgBytes > this.maxBytesPerBuffer;
357
+ if (isTerminal || !atCountCap && !atByteCap) {
358
+ buffer.events.push({ msg, data });
359
+ buffer.bytes += msgBytes;
360
+ }
335
361
  }
336
362
  if (isTerminal) {
337
363
  buffer.complete = true;
@@ -416,7 +442,7 @@ var VALID_CHANNEL_PREFIXES = ["execution:", "trace:", "eval:"];
416
442
  var VALID_EXACT_CHANNELS = ["costs", "decisions", "eval-trends", "workflow-stats", "trace-stats"];
417
443
  var MAX_CHANNEL_LENGTH = 256;
418
444
  function handleWsMessage(raw, socket, connMgr) {
419
- if (raw.length > MAX_WS_FRAME_BYTES) {
445
+ if (Buffer.byteLength(raw, "utf8") > MAX_WS_FRAME_BYTES) {
420
446
  return JSON.stringify({ type: "error", message: "Message too large" });
421
447
  }
422
448
  let msg;
@@ -572,7 +598,7 @@ var TraceAggregator = class {
572
598
  this.options.windows.map((w) => [w, this.options.emptyState()])
573
599
  );
574
600
  for (const exec of capped) {
575
- for (const event of exec.steps) {
601
+ for (const event of exec.events) {
576
602
  for (const window of this.options.windows) {
577
603
  if (withinWindow(event.timestamp, window, now)) {
578
604
  fresh.set(window, this.options.reducer(fresh.get(window), event));
@@ -594,15 +620,131 @@ var TraceAggregator = class {
594
620
  }
595
621
  };
596
622
 
623
+ // src/server/aggregates/execution-aggregator.ts
624
+ var ExecutionAggregator = class {
625
+ snaps;
626
+ interval;
627
+ listener;
628
+ options;
629
+ /** Generation counter to prevent stale async fold after rebuild. */
630
+ generation = 0;
631
+ constructor(options) {
632
+ this.options = options;
633
+ this.snaps = new AggregateSnapshots(
634
+ options.windows,
635
+ options.emptyState,
636
+ options.connMgr,
637
+ options.channel,
638
+ options.broadcastTransform
639
+ );
640
+ }
641
+ async start() {
642
+ await this.rebuild();
643
+ this.listener = (event) => {
644
+ if (event.type !== "workflow_end") return;
645
+ const gen = this.generation;
646
+ this.options.runtime.getExecution(event.executionId).then((exec) => {
647
+ if (this.generation !== gen) return;
648
+ if (exec) {
649
+ this.snaps.fold(exec.startedAt, (prev) => this.options.reducer(prev, exec));
650
+ }
651
+ }).catch((err) => console.error("[axl-studio] execution fold failed:", err));
652
+ };
653
+ this.options.runtime.on("trace", this.listener);
654
+ this.interval = setInterval(
655
+ () => this.rebuild().catch((err) => console.error("[axl-studio] rebuild failed:", err)),
656
+ REBUILD_INTERVAL_MS
657
+ );
658
+ }
659
+ async rebuild() {
660
+ this.generation++;
661
+ const executions = await this.options.runtime.getExecutions();
662
+ const cap = this.options.executionCap ?? 2e3;
663
+ const capped = executions.slice(0, cap);
664
+ const now = Date.now();
665
+ const fresh = new Map(
666
+ this.options.windows.map((w) => [w, this.options.emptyState()])
667
+ );
668
+ for (const exec of capped) {
669
+ for (const window of this.options.windows) {
670
+ if (withinWindow(exec.startedAt, window, now)) {
671
+ fresh.set(window, this.options.reducer(fresh.get(window), exec));
672
+ }
673
+ }
674
+ }
675
+ this.snaps.replace(fresh);
676
+ }
677
+ getSnapshot(window) {
678
+ return this.snaps.get(window);
679
+ }
680
+ getAllSnapshots() {
681
+ return this.snaps.getAll();
682
+ }
683
+ close() {
684
+ if (this.listener) this.options.runtime.off("trace", this.listener);
685
+ if (this.interval) clearInterval(this.interval);
686
+ }
687
+ };
688
+
689
+ // src/server/aggregates/eval-aggregator.ts
690
+ var EvalAggregator = class {
691
+ snaps;
692
+ interval;
693
+ listener;
694
+ options;
695
+ constructor(options) {
696
+ this.options = options;
697
+ this.snaps = new AggregateSnapshots(
698
+ options.windows,
699
+ options.emptyState,
700
+ options.connMgr,
701
+ options.channel,
702
+ options.broadcastTransform
703
+ );
704
+ }
705
+ async start() {
706
+ await this.rebuild();
707
+ this.listener = (entry) => {
708
+ this.snaps.fold(entry.timestamp, (prev) => this.options.reducer(prev, entry));
709
+ };
710
+ this.options.runtime.on("eval_result", this.listener);
711
+ this.interval = setInterval(
712
+ () => this.rebuild().catch((err) => console.error("[axl-studio] rebuild failed:", err)),
713
+ REBUILD_INTERVAL_MS
714
+ );
715
+ }
716
+ async rebuild() {
717
+ const history = await this.options.runtime.getEvalHistory();
718
+ const cap = this.options.entryCap ?? 500;
719
+ const capped = history.slice(0, cap);
720
+ const now = Date.now();
721
+ const fresh = new Map(
722
+ this.options.windows.map((w) => [w, this.options.emptyState()])
723
+ );
724
+ for (const entry of capped) {
725
+ for (const window of this.options.windows) {
726
+ if (withinWindow(entry.timestamp, window, now)) {
727
+ fresh.set(window, this.options.reducer(fresh.get(window), entry));
728
+ }
729
+ }
730
+ }
731
+ this.snaps.replace(fresh);
732
+ }
733
+ getSnapshot(window) {
734
+ return this.snaps.get(window);
735
+ }
736
+ getAllSnapshots() {
737
+ return this.snaps.getAll();
738
+ }
739
+ close() {
740
+ if (this.listener) this.options.runtime.off("eval_result", this.listener);
741
+ if (this.interval) clearInterval(this.interval);
742
+ }
743
+ };
744
+
597
745
  // src/server/aggregates/reducers.ts
746
+ var import_axl2 = require("@axlsdk/axl");
598
747
  var finite = (v) => Number.isFinite(v) ? v : 0;
599
- function isLogEvent(event, eventName) {
600
- if (event.type === eventName) return true;
601
- if (event.type === "log" && event.data != null && typeof event.data === "object") {
602
- return event.data.event === eventName;
603
- }
604
- return false;
605
- }
606
748
  function emptyRetry() {
607
749
  return {
608
750
  primary: 0,
@@ -628,7 +770,7 @@ function emptyCostData() {
628
770
  };
629
771
  }
630
772
  function reduceCost(acc, event) {
631
- const isWorkflowStart = isLogEvent(event, "workflow_start");
773
+ const isWorkflowStart = event.type === "workflow_start";
632
774
  if (isWorkflowStart && event.workflow) {
633
775
  const byWorkflow2 = { ...acc.byWorkflow };
634
776
  const prev = byWorkflow2[event.workflow] ?? { cost: 0, executions: 0 };
@@ -636,9 +778,10 @@ function reduceCost(acc, event) {
636
778
  return { ...acc, byWorkflow: byWorkflow2 };
637
779
  }
638
780
  if (event.cost == null && !event.tokens) return acc;
639
- const cost = finite(event.cost);
781
+ const cost = (0, import_axl2.eventCostContribution)(event);
782
+ if (event.type === "ask_end") return acc;
640
783
  const tokens = event.tokens ?? {};
641
- const totalTokens = event.type === "agent_call" ? {
784
+ const totalTokens = event.type === "agent_call_end" ? {
642
785
  input: acc.totalTokens.input + finite(tokens.input),
643
786
  output: acc.totalTokens.output + finite(tokens.output),
644
787
  reasoning: acc.totalTokens.reasoning + finite(tokens.reasoning)
@@ -669,7 +812,7 @@ function reduceCost(acc, event) {
669
812
  };
670
813
  }
671
814
  let retry = acc.retry;
672
- if (event.type === "agent_call") {
815
+ if (event.type === "agent_call_end") {
673
816
  const d = event.data ?? {};
674
817
  const reason = d.retryReason;
675
818
  retry = { ...acc.retry };
@@ -691,19 +834,17 @@ function reduceCost(acc, event) {
691
834
  }
692
835
  }
693
836
  let byEmbedder = acc.byEmbedder;
694
- if (event.type === "log") {
695
- const d = event.data ?? {};
696
- if (d.event === "memory_remember" || d.event === "memory_recall") {
697
- byEmbedder = { ...acc.byEmbedder };
698
- const modelKey = d.usage?.model ?? "unknown";
699
- const embedTokens = typeof d.usage?.tokens === "number" ? finite(d.usage.tokens) : 0;
700
- const prev = byEmbedder[modelKey] ?? { cost: 0, calls: 0, tokens: 0 };
701
- byEmbedder[modelKey] = {
702
- cost: prev.cost + cost,
703
- calls: prev.calls + 1,
704
- tokens: prev.tokens + embedTokens
705
- };
706
- }
837
+ if (event.type === "memory_remember" || event.type === "memory_recall") {
838
+ const usage = event.data.usage;
839
+ byEmbedder = { ...acc.byEmbedder };
840
+ const modelKey = usage?.model ?? "unknown";
841
+ const embedTokens = typeof usage?.tokens === "number" ? finite(usage.tokens) : 0;
842
+ const prev = byEmbedder[modelKey] ?? { cost: 0, calls: 0, tokens: 0 };
843
+ byEmbedder[modelKey] = {
844
+ cost: prev.cost + cost,
845
+ calls: prev.calls + 1,
846
+ tokens: prev.tokens + embedTokens
847
+ };
707
848
  }
708
849
  return {
709
850
  totalCost: acc.totalCost + cost,
@@ -898,7 +1039,7 @@ function reduceTraceStats(acc, event) {
898
1039
  const eventTypeCounts = { ...acc.eventTypeCounts };
899
1040
  eventTypeCounts[event.type] = (eventTypeCounts[event.type] ?? 0) + 1;
900
1041
  const byTool = { ...acc.byTool };
901
- if (event.type === "tool_call" || event.type === "tool_denied" || event.type === "tool_approval") {
1042
+ if (event.type === "tool_call_end" || event.type === "tool_denied" || event.type === "tool_approval") {
902
1043
  const toolName = event.tool;
903
1044
  const prev = byTool[toolName] ?? { calls: 0, denied: 0, approved: 0 };
904
1045
  const isDeniedEvent = event.type === "tool_denied";
@@ -907,13 +1048,13 @@ function reduceTraceStats(acc, event) {
907
1048
  const isApproved = isDeniedEvent && eventData?.approved === true || isApprovalEvent && eventData?.approved === true;
908
1049
  const isDenied = isDeniedEvent && !eventData?.approved || isApprovalEvent && eventData?.approved === false;
909
1050
  byTool[toolName] = {
910
- calls: prev.calls + (event.type === "tool_call" ? 1 : 0),
1051
+ calls: prev.calls + (event.type === "tool_call_end" ? 1 : 0),
911
1052
  denied: prev.denied + (isDenied ? 1 : 0),
912
1053
  approved: prev.approved + (isApproved ? 1 : 0)
913
1054
  };
914
1055
  }
915
1056
  const retryByAgent = { ...acc.retryByAgent };
916
- if (event.agent && event.type === "agent_call") {
1057
+ if (event.agent && event.type === "agent_call_end") {
917
1058
  const data = event.data;
918
1059
  if (data?.retryReason) {
919
1060
  const prev = retryByAgent[event.agent] ?? { schema: 0, validate: 0, guardrail: 0 };
@@ -931,128 +1072,6 @@ function reduceTraceStats(acc, event) {
931
1072
  };
932
1073
  }
933
1074
 
934
- // src/server/aggregates/execution-aggregator.ts
935
- var ExecutionAggregator = class {
936
- snaps;
937
- interval;
938
- listener;
939
- options;
940
- /** Generation counter to prevent stale async fold after rebuild. */
941
- generation = 0;
942
- constructor(options) {
943
- this.options = options;
944
- this.snaps = new AggregateSnapshots(
945
- options.windows,
946
- options.emptyState,
947
- options.connMgr,
948
- options.channel,
949
- options.broadcastTransform
950
- );
951
- }
952
- async start() {
953
- await this.rebuild();
954
- this.listener = (event) => {
955
- if (!isLogEvent(event, "workflow_end")) return;
956
- const gen = this.generation;
957
- this.options.runtime.getExecution(event.executionId).then((exec) => {
958
- if (this.generation !== gen) return;
959
- if (exec) {
960
- this.snaps.fold(exec.startedAt, (prev) => this.options.reducer(prev, exec));
961
- }
962
- }).catch((err) => console.error("[axl-studio] execution fold failed:", err));
963
- };
964
- this.options.runtime.on("trace", this.listener);
965
- this.interval = setInterval(
966
- () => this.rebuild().catch((err) => console.error("[axl-studio] rebuild failed:", err)),
967
- REBUILD_INTERVAL_MS
968
- );
969
- }
970
- async rebuild() {
971
- this.generation++;
972
- const executions = await this.options.runtime.getExecutions();
973
- const cap = this.options.executionCap ?? 2e3;
974
- const capped = executions.slice(0, cap);
975
- const now = Date.now();
976
- const fresh = new Map(
977
- this.options.windows.map((w) => [w, this.options.emptyState()])
978
- );
979
- for (const exec of capped) {
980
- for (const window of this.options.windows) {
981
- if (withinWindow(exec.startedAt, window, now)) {
982
- fresh.set(window, this.options.reducer(fresh.get(window), exec));
983
- }
984
- }
985
- }
986
- this.snaps.replace(fresh);
987
- }
988
- getSnapshot(window) {
989
- return this.snaps.get(window);
990
- }
991
- getAllSnapshots() {
992
- return this.snaps.getAll();
993
- }
994
- close() {
995
- if (this.listener) this.options.runtime.off("trace", this.listener);
996
- if (this.interval) clearInterval(this.interval);
997
- }
998
- };
999
-
1000
- // src/server/aggregates/eval-aggregator.ts
1001
- var EvalAggregator = class {
1002
- snaps;
1003
- interval;
1004
- listener;
1005
- options;
1006
- constructor(options) {
1007
- this.options = options;
1008
- this.snaps = new AggregateSnapshots(
1009
- options.windows,
1010
- options.emptyState,
1011
- options.connMgr,
1012
- options.channel,
1013
- options.broadcastTransform
1014
- );
1015
- }
1016
- async start() {
1017
- await this.rebuild();
1018
- this.listener = (entry) => {
1019
- this.snaps.fold(entry.timestamp, (prev) => this.options.reducer(prev, entry));
1020
- };
1021
- this.options.runtime.on("eval_result", this.listener);
1022
- this.interval = setInterval(
1023
- () => this.rebuild().catch((err) => console.error("[axl-studio] rebuild failed:", err)),
1024
- REBUILD_INTERVAL_MS
1025
- );
1026
- }
1027
- async rebuild() {
1028
- const history = await this.options.runtime.getEvalHistory();
1029
- const cap = this.options.entryCap ?? 500;
1030
- const capped = history.slice(0, cap);
1031
- const now = Date.now();
1032
- const fresh = new Map(
1033
- this.options.windows.map((w) => [w, this.options.emptyState()])
1034
- );
1035
- for (const entry of capped) {
1036
- for (const window of this.options.windows) {
1037
- if (withinWindow(entry.timestamp, window, now)) {
1038
- fresh.set(window, this.options.reducer(fresh.get(window), entry));
1039
- }
1040
- }
1041
- }
1042
- this.snaps.replace(fresh);
1043
- }
1044
- getSnapshot(window) {
1045
- return this.snaps.get(window);
1046
- }
1047
- getAllSnapshots() {
1048
- return this.snaps.getAll();
1049
- }
1050
- close() {
1051
- if (this.listener) this.options.runtime.off("eval_result", this.listener);
1052
- if (this.interval) clearInterval(this.interval);
1053
- }
1054
- };
1055
-
1056
1075
  // src/server/routes/health.ts
1057
1076
  var import_hono = require("hono");
1058
1077
  function createHealthRoutes(readOnly) {
@@ -1075,7 +1094,7 @@ function createHealthRoutes(readOnly) {
1075
1094
 
1076
1095
  // src/server/routes/workflows.ts
1077
1096
  var import_hono2 = require("hono");
1078
- var import_axl = require("@axlsdk/axl");
1097
+ var import_axl3 = require("@axlsdk/axl");
1079
1098
  function createWorkflowRoutes(connMgr) {
1080
1099
  const app6 = new import_hono2.Hono();
1081
1100
  app6.get("/workflows", (c) => {
@@ -1101,8 +1120,8 @@ function createWorkflowRoutes(connMgr) {
1101
1120
  ok: true,
1102
1121
  data: {
1103
1122
  name: workflow.name,
1104
- inputSchema: workflow.inputSchema ? (0, import_axl.zodToJsonSchema)(workflow.inputSchema) : null,
1105
- outputSchema: workflow.outputSchema ? (0, import_axl.zodToJsonSchema)(workflow.outputSchema) : null
1123
+ inputSchema: workflow.inputSchema ? (0, import_axl3.zodToJsonSchema)(workflow.inputSchema) : null,
1124
+ outputSchema: workflow.outputSchema ? (0, import_axl3.zodToJsonSchema)(workflow.outputSchema) : null
1106
1125
  }
1107
1126
  });
1108
1127
  });
@@ -1161,9 +1180,31 @@ app.get("/executions/:id", async (c) => {
1161
1180
  404
1162
1181
  );
1163
1182
  }
1183
+ const sinceParam = c.req.query("since");
1184
+ let paged = execution;
1185
+ if (sinceParam !== void 0) {
1186
+ const since = Number(sinceParam);
1187
+ if (!Number.isFinite(since) || !Number.isInteger(since)) {
1188
+ return c.json(
1189
+ {
1190
+ ok: false,
1191
+ error: {
1192
+ code: "INVALID_PARAM",
1193
+ message: `\`since\` must be a finite integer (got "${sinceParam}")`,
1194
+ param: "since"
1195
+ }
1196
+ },
1197
+ 400
1198
+ );
1199
+ }
1200
+ paged = {
1201
+ ...execution,
1202
+ events: execution.events.filter((e) => e.step > since)
1203
+ };
1204
+ }
1164
1205
  return c.json({
1165
1206
  ok: true,
1166
- data: redactExecutionInfo(execution, runtime.isRedactEnabled())
1207
+ data: redactExecutionInfo(paged, runtime.isRedactEnabled())
1167
1208
  });
1168
1209
  });
1169
1210
  app.post("/executions/:id/abort", (c) => {
@@ -1243,7 +1284,7 @@ function createSessionRoutes(connMgr) {
1243
1284
 
1244
1285
  // src/server/routes/agents.ts
1245
1286
  var import_hono5 = require("hono");
1246
- var import_axl2 = require("@axlsdk/axl");
1287
+ var import_axl4 = require("@axlsdk/axl");
1247
1288
  var app2 = new import_hono5.Hono();
1248
1289
  app2.get("/agents", (c) => {
1249
1290
  const runtime = c.get("runtime");
@@ -1284,7 +1325,7 @@ app2.get("/agents/:name", (c) => {
1284
1325
  tools: cfg.tools?.map((t) => ({
1285
1326
  name: t.name,
1286
1327
  description: t.description,
1287
- inputSchema: (0, import_axl2.zodToJsonSchema)(t.inputSchema)
1328
+ inputSchema: (0, import_axl4.zodToJsonSchema)(t.inputSchema)
1288
1329
  })) ?? [],
1289
1330
  handoffs: typeof cfg.handoffs === "function" ? [
1290
1331
  {
@@ -1324,14 +1365,14 @@ var agents_default = app2;
1324
1365
 
1325
1366
  // src/server/routes/tools.ts
1326
1367
  var import_hono6 = require("hono");
1327
- var import_axl3 = require("@axlsdk/axl");
1368
+ var import_axl5 = require("@axlsdk/axl");
1328
1369
  var app3 = new import_hono6.Hono();
1329
1370
  app3.get("/tools", (c) => {
1330
1371
  const runtime = c.get("runtime");
1331
1372
  const tools = runtime.getTools().map((t) => ({
1332
1373
  name: t.name,
1333
1374
  description: t.description,
1334
- inputSchema: t.inputSchema ? (0, import_axl3.zodToJsonSchema)(t.inputSchema) : {},
1375
+ inputSchema: t.inputSchema ? (0, import_axl5.zodToJsonSchema)(t.inputSchema) : {},
1335
1376
  sensitive: t.sensitive ?? false,
1336
1377
  requireApproval: t.requireApproval ?? false
1337
1378
  }));
@@ -1352,7 +1393,7 @@ app3.get("/tools/:name", (c) => {
1352
1393
  data: {
1353
1394
  name: tool.name,
1354
1395
  description: tool.description,
1355
- inputSchema: tool.inputSchema ? (0, import_axl3.zodToJsonSchema)(tool.inputSchema) : {},
1396
+ inputSchema: tool.inputSchema ? (0, import_axl5.zodToJsonSchema)(tool.inputSchema) : {},
1356
1397
  sensitive: tool.sensitive,
1357
1398
  requireApproval: tool.requireApproval,
1358
1399
  retry: tool.retry,
@@ -1740,10 +1781,14 @@ function createEvalRoutes(connMgr, evalLoader) {
1740
1781
  const runtime = c.get("runtime");
1741
1782
  const redactOn = runtime.isRedactEnabled();
1742
1783
  const body = await c.req.json();
1784
+ const MAX_POOLED_RUNS = 25;
1743
1785
  const validateIdParam = (v, name) => {
1744
1786
  if (typeof v === "string") return v === "" ? `${name} must be non-empty` : null;
1745
1787
  if (Array.isArray(v)) {
1746
1788
  if (v.length === 0) return `${name} must be a non-empty array`;
1789
+ if (v.length > MAX_POOLED_RUNS) {
1790
+ return `${name} may contain at most ${MAX_POOLED_RUNS} ids (pooled comparison)`;
1791
+ }
1747
1792
  for (const elem of v) {
1748
1793
  if (typeof elem !== "string" || elem === "") {
1749
1794
  return `${name} array must contain only non-empty strings`;
@@ -1912,32 +1957,50 @@ function createPlaygroundRoutes(connMgr) {
1912
1957
  );
1913
1958
  }
1914
1959
  const sessionId = body.sessionId ?? `playground-${Date.now()}`;
1915
- const executionId = `playground-${sessionId}-${Date.now()}`;
1916
1960
  const store = runtime.getStateStore();
1917
1961
  const history = await store.getSession(sessionId);
1918
1962
  history.push({ role: "user", content: body.message });
1919
1963
  const redactOn = runtime.isRedactEnabled();
1920
- const broadcast = (event) => {
1964
+ const ctx = runtime.createContext({ sessionHistory: history });
1965
+ const executionId = ctx.executionId;
1966
+ const traceListener = (event) => {
1967
+ if (event.executionId !== executionId) return;
1921
1968
  connMgr.broadcastWithWildcard(`execution:${executionId}`, redactStreamEvent(event, redactOn));
1922
1969
  };
1923
- const ctx = runtime.createContext({
1924
- sessionHistory: history,
1925
- onToken: (token) => {
1926
- broadcast({ type: "token", data: token });
1927
- }
1928
- });
1970
+ runtime.on("trace", traceListener);
1929
1971
  (async () => {
1972
+ let stepCounter = Number.MAX_SAFE_INTEGER - 1;
1973
+ const terminalFields = () => ({
1974
+ executionId,
1975
+ step: stepCounter++,
1976
+ timestamp: Date.now()
1977
+ });
1930
1978
  try {
1931
1979
  const result = await ctx.ask(agent, body.message);
1932
1980
  const resultText = typeof result === "string" ? result : JSON.stringify(result);
1933
1981
  history.push({ role: "assistant", content: resultText });
1934
1982
  await store.saveSession(sessionId, history);
1935
- broadcast({ type: "done", data: resultText });
1983
+ const doneEvent = {
1984
+ ...terminalFields(),
1985
+ type: "done",
1986
+ data: { result: resultText }
1987
+ };
1988
+ connMgr.broadcastWithWildcard(
1989
+ `execution:${executionId}`,
1990
+ redactStreamEvent(doneEvent, redactOn)
1991
+ );
1936
1992
  } catch (err) {
1937
- broadcast({
1993
+ const errorEvent = {
1994
+ ...terminalFields(),
1938
1995
  type: "error",
1939
- message: err instanceof Error ? err.message : String(err)
1940
- });
1996
+ data: { message: err instanceof Error ? err.message : String(err) }
1997
+ };
1998
+ connMgr.broadcastWithWildcard(
1999
+ `execution:${executionId}`,
2000
+ redactStreamEvent(errorEvent, redactOn)
2001
+ );
2002
+ } finally {
2003
+ runtime.off("trace", traceListener);
1941
2004
  }
1942
2005
  })();
1943
2006
  return c.json({
@@ -1985,7 +2048,7 @@ function createTraceStatsRoutes(aggregator) {
1985
2048
  function createServer(options) {
1986
2049
  const { runtime, staticRoot, basePath = "", readOnly = false } = options;
1987
2050
  const app6 = new import_hono15.Hono();
1988
- const connMgr = new ConnectionManager();
2051
+ const connMgr = new ConnectionManager(options.bufferCaps);
1989
2052
  const windows = ["24h", "7d", "30d", "all"];
1990
2053
  const costAggregator = new TraceAggregator({
1991
2054
  runtime,
@@ -2079,12 +2142,20 @@ function createServer(options) {
2079
2142
  api.route("/", createPlaygroundRoutes(connMgr));
2080
2143
  app6.route("/api", api);
2081
2144
  const traceListener = (event) => {
2082
- const traceEvent = event;
2083
- if (traceEvent.executionId) {
2084
- connMgr.broadcastWithWildcard(`trace:${traceEvent.executionId}`, traceEvent);
2085
- }
2086
- if (traceEvent.type === "await_human") {
2087
- connMgr.broadcast("decisions", traceEvent);
2145
+ try {
2146
+ const traceEvent = event;
2147
+ const redacted = redactStreamEvent(traceEvent, runtime.isRedactEnabled());
2148
+ if (traceEvent.executionId) {
2149
+ connMgr.broadcastWithWildcard(`trace:${traceEvent.executionId}`, redacted);
2150
+ }
2151
+ if (traceEvent.type === "await_human") {
2152
+ connMgr.broadcast("decisions", redacted);
2153
+ }
2154
+ } catch (err) {
2155
+ console.error(
2156
+ "[axl-studio] trace listener threw; event dropped:",
2157
+ err instanceof Error ? err.message : String(err)
2158
+ );
2088
2159
  }
2089
2160
  };
2090
2161
  runtime.on("trace", traceListener);