@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.
@@ -48,6 +48,7 @@ var import_cors = require("hono/cors");
48
48
  var import_serve_static = require("@hono/node-server/serve-static");
49
49
 
50
50
  // src/server/redact.ts
51
+ var import_axl = require("@axlsdk/axl");
51
52
  var REDACTED = "[redacted]";
52
53
  var SAFE_ERROR_NAMES = /* @__PURE__ */ new Set([
53
54
  "QuorumNotMet",
@@ -72,7 +73,8 @@ function redactExecutionInfo(info, redact) {
72
73
  return {
73
74
  ...info,
74
75
  ...info.result !== void 0 ? { result: REDACTED } : {},
75
- ...info.error !== void 0 ? { error: REDACTED } : {}
76
+ ...info.error !== void 0 ? { error: REDACTED } : {},
77
+ events: info.events.map((e) => redactStreamEvent(e, true))
76
78
  };
77
79
  }
78
80
  function redactExecutionList(infos, redact) {
@@ -113,30 +115,7 @@ function redactSessionHistory(history, redact) {
113
115
  }
114
116
  function redactStreamEvent(event, redact) {
115
117
  if (!redact) return event;
116
- switch (event.type) {
117
- case "token":
118
- return { type: "token", data: REDACTED };
119
- case "tool_call":
120
- return { ...event, args: REDACTED };
121
- case "tool_result":
122
- return { ...event, result: REDACTED };
123
- case "tool_approval":
124
- return {
125
- ...event,
126
- args: REDACTED,
127
- ...event.reason !== void 0 ? { reason: REDACTED } : {}
128
- };
129
- case "done":
130
- return { type: "done", data: REDACTED };
131
- case "error":
132
- return { type: "error", message: REDACTED };
133
- // Structural events have no user content to scrub.
134
- case "agent_start":
135
- case "agent_end":
136
- case "handoff":
137
- case "step":
138
- return event;
139
- }
118
+ return (0, import_axl.redactEvent)(event);
140
119
  }
141
120
  function redactEvalItem(item) {
142
121
  const scrubbed = {
@@ -222,13 +201,17 @@ async function errorHandler(c, next) {
222
201
 
223
202
  // src/server/ws/connection-manager.ts
224
203
  var BUFFER_TTL_MS = 3e4;
225
- var MAX_BUFFER_EVENTS = 500;
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"]);
226
208
  var MAX_WS_FRAME_BYTES = 65536;
227
209
  function isBufferedChannel(channel) {
228
210
  return channel.startsWith("execution:") || channel.startsWith("eval:");
229
211
  }
230
212
  function truncateIfOversized(msg, channel, data) {
231
- if (msg.length <= MAX_WS_FRAME_BYTES) return msg;
213
+ const msgBytes = Buffer.byteLength(msg, "utf8");
214
+ if (msgBytes <= MAX_WS_FRAME_BYTES) return msg;
232
215
  const event = data ?? {};
233
216
  const truncated = {
234
217
  type: "event",
@@ -237,7 +220,7 @@ function truncateIfOversized(msg, channel, data) {
237
220
  ...event,
238
221
  data: {
239
222
  __truncated: true,
240
- originalBytes: msg.length,
223
+ originalBytes: msgBytes,
241
224
  maxBytes: MAX_WS_FRAME_BYTES,
242
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."
243
226
  }
@@ -254,6 +237,25 @@ var ConnectionManager = class {
254
237
  buffers = /* @__PURE__ */ new Map();
255
238
  maxConnections = 100;
256
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
+ }
257
259
  /**
258
260
  * Register a broadcast filter. Called once at middleware construction.
259
261
  * The filter runs on every outbound event and can drop or deliver based
@@ -336,13 +338,37 @@ var ConnectionManager = class {
336
338
  if (isBufferedChannel(channel)) {
337
339
  let buffer = this.buffers.get(channel);
338
340
  if (!buffer) {
339
- 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 };
340
359
  this.buffers.set(channel, buffer);
341
360
  }
342
361
  const event = data;
343
362
  const isTerminal = event.type === "done" || event.type === "error";
344
- if (buffer.events.length < MAX_BUFFER_EVENTS || isTerminal) {
345
- buffer.events.push({ msg, data });
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
+ }
346
372
  }
347
373
  if (isTerminal) {
348
374
  buffer.complete = true;
@@ -427,7 +453,7 @@ var VALID_CHANNEL_PREFIXES = ["execution:", "trace:", "eval:"];
427
453
  var VALID_EXACT_CHANNELS = ["costs", "decisions", "eval-trends", "workflow-stats", "trace-stats"];
428
454
  var MAX_CHANNEL_LENGTH = 256;
429
455
  function handleWsMessage(raw, socket, connMgr) {
430
- if (raw.length > MAX_WS_FRAME_BYTES) {
456
+ if (Buffer.byteLength(raw, "utf8") > MAX_WS_FRAME_BYTES) {
431
457
  return JSON.stringify({ type: "error", message: "Message too large" });
432
458
  }
433
459
  let msg;
@@ -583,7 +609,7 @@ var TraceAggregator = class {
583
609
  this.options.windows.map((w) => [w, this.options.emptyState()])
584
610
  );
585
611
  for (const exec of capped) {
586
- for (const event of exec.steps) {
612
+ for (const event of exec.events) {
587
613
  for (const window of this.options.windows) {
588
614
  if (withinWindow(event.timestamp, window, now)) {
589
615
  fresh.set(window, this.options.reducer(fresh.get(window), event));
@@ -605,15 +631,131 @@ var TraceAggregator = class {
605
631
  }
606
632
  };
607
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();
749
+ }
750
+ close() {
751
+ if (this.listener) this.options.runtime.off("eval_result", this.listener);
752
+ if (this.interval) clearInterval(this.interval);
753
+ }
754
+ };
755
+
608
756
  // src/server/aggregates/reducers.ts
757
+ var import_axl2 = require("@axlsdk/axl");
609
758
  var finite = (v) => Number.isFinite(v) ? v : 0;
610
- function isLogEvent(event, eventName) {
611
- if (event.type === eventName) return true;
612
- if (event.type === "log" && event.data != null && typeof event.data === "object") {
613
- return event.data.event === eventName;
614
- }
615
- return false;
616
- }
617
759
  function emptyRetry() {
618
760
  return {
619
761
  primary: 0,
@@ -639,7 +781,7 @@ function emptyCostData() {
639
781
  };
640
782
  }
641
783
  function reduceCost(acc, event) {
642
- const isWorkflowStart = isLogEvent(event, "workflow_start");
784
+ const isWorkflowStart = event.type === "workflow_start";
643
785
  if (isWorkflowStart && event.workflow) {
644
786
  const byWorkflow2 = { ...acc.byWorkflow };
645
787
  const prev = byWorkflow2[event.workflow] ?? { cost: 0, executions: 0 };
@@ -647,9 +789,10 @@ function reduceCost(acc, event) {
647
789
  return { ...acc, byWorkflow: byWorkflow2 };
648
790
  }
649
791
  if (event.cost == null && !event.tokens) return acc;
650
- const cost = finite(event.cost);
792
+ const cost = (0, import_axl2.eventCostContribution)(event);
793
+ if (event.type === "ask_end") return acc;
651
794
  const tokens = event.tokens ?? {};
652
- const totalTokens = event.type === "agent_call" ? {
795
+ const totalTokens = event.type === "agent_call_end" ? {
653
796
  input: acc.totalTokens.input + finite(tokens.input),
654
797
  output: acc.totalTokens.output + finite(tokens.output),
655
798
  reasoning: acc.totalTokens.reasoning + finite(tokens.reasoning)
@@ -680,7 +823,7 @@ function reduceCost(acc, event) {
680
823
  };
681
824
  }
682
825
  let retry = acc.retry;
683
- if (event.type === "agent_call") {
826
+ if (event.type === "agent_call_end") {
684
827
  const d = event.data ?? {};
685
828
  const reason = d.retryReason;
686
829
  retry = { ...acc.retry };
@@ -702,19 +845,17 @@ function reduceCost(acc, event) {
702
845
  }
703
846
  }
704
847
  let byEmbedder = acc.byEmbedder;
705
- if (event.type === "log") {
706
- const d = event.data ?? {};
707
- if (d.event === "memory_remember" || d.event === "memory_recall") {
708
- byEmbedder = { ...acc.byEmbedder };
709
- const modelKey = d.usage?.model ?? "unknown";
710
- const embedTokens = typeof d.usage?.tokens === "number" ? finite(d.usage.tokens) : 0;
711
- const prev = byEmbedder[modelKey] ?? { cost: 0, calls: 0, tokens: 0 };
712
- byEmbedder[modelKey] = {
713
- cost: prev.cost + cost,
714
- calls: prev.calls + 1,
715
- tokens: prev.tokens + embedTokens
716
- };
717
- }
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
+ };
718
859
  }
719
860
  return {
720
861
  totalCost: acc.totalCost + cost,
@@ -909,7 +1050,7 @@ function reduceTraceStats(acc, event) {
909
1050
  const eventTypeCounts = { ...acc.eventTypeCounts };
910
1051
  eventTypeCounts[event.type] = (eventTypeCounts[event.type] ?? 0) + 1;
911
1052
  const byTool = { ...acc.byTool };
912
- if (event.type === "tool_call" || event.type === "tool_denied" || event.type === "tool_approval") {
1053
+ if (event.type === "tool_call_end" || event.type === "tool_denied" || event.type === "tool_approval") {
913
1054
  const toolName = event.tool;
914
1055
  const prev = byTool[toolName] ?? { calls: 0, denied: 0, approved: 0 };
915
1056
  const isDeniedEvent = event.type === "tool_denied";
@@ -918,13 +1059,13 @@ function reduceTraceStats(acc, event) {
918
1059
  const isApproved = isDeniedEvent && eventData?.approved === true || isApprovalEvent && eventData?.approved === true;
919
1060
  const isDenied = isDeniedEvent && !eventData?.approved || isApprovalEvent && eventData?.approved === false;
920
1061
  byTool[toolName] = {
921
- calls: prev.calls + (event.type === "tool_call" ? 1 : 0),
1062
+ calls: prev.calls + (event.type === "tool_call_end" ? 1 : 0),
922
1063
  denied: prev.denied + (isDenied ? 1 : 0),
923
1064
  approved: prev.approved + (isApproved ? 1 : 0)
924
1065
  };
925
1066
  }
926
1067
  const retryByAgent = { ...acc.retryByAgent };
927
- if (event.agent && event.type === "agent_call") {
1068
+ if (event.agent && event.type === "agent_call_end") {
928
1069
  const data = event.data;
929
1070
  if (data?.retryReason) {
930
1071
  const prev = retryByAgent[event.agent] ?? { schema: 0, validate: 0, guardrail: 0 };
@@ -942,128 +1083,6 @@ function reduceTraceStats(acc, event) {
942
1083
  };
943
1084
  }
944
1085
 
945
- // src/server/aggregates/execution-aggregator.ts
946
- var ExecutionAggregator = class {
947
- snaps;
948
- interval;
949
- listener;
950
- options;
951
- /** Generation counter to prevent stale async fold after rebuild. */
952
- generation = 0;
953
- constructor(options) {
954
- this.options = options;
955
- this.snaps = new AggregateSnapshots(
956
- options.windows,
957
- options.emptyState,
958
- options.connMgr,
959
- options.channel,
960
- options.broadcastTransform
961
- );
962
- }
963
- async start() {
964
- await this.rebuild();
965
- this.listener = (event) => {
966
- if (!isLogEvent(event, "workflow_end")) return;
967
- const gen = this.generation;
968
- this.options.runtime.getExecution(event.executionId).then((exec) => {
969
- if (this.generation !== gen) return;
970
- if (exec) {
971
- this.snaps.fold(exec.startedAt, (prev) => this.options.reducer(prev, exec));
972
- }
973
- }).catch((err) => console.error("[axl-studio] execution fold failed:", err));
974
- };
975
- this.options.runtime.on("trace", this.listener);
976
- this.interval = setInterval(
977
- () => this.rebuild().catch((err) => console.error("[axl-studio] rebuild failed:", err)),
978
- REBUILD_INTERVAL_MS
979
- );
980
- }
981
- async rebuild() {
982
- this.generation++;
983
- const executions = await this.options.runtime.getExecutions();
984
- const cap = this.options.executionCap ?? 2e3;
985
- const capped = executions.slice(0, cap);
986
- const now = Date.now();
987
- const fresh = new Map(
988
- this.options.windows.map((w) => [w, this.options.emptyState()])
989
- );
990
- for (const exec of capped) {
991
- for (const window of this.options.windows) {
992
- if (withinWindow(exec.startedAt, window, now)) {
993
- fresh.set(window, this.options.reducer(fresh.get(window), exec));
994
- }
995
- }
996
- }
997
- this.snaps.replace(fresh);
998
- }
999
- getSnapshot(window) {
1000
- return this.snaps.get(window);
1001
- }
1002
- getAllSnapshots() {
1003
- return this.snaps.getAll();
1004
- }
1005
- close() {
1006
- if (this.listener) this.options.runtime.off("trace", this.listener);
1007
- if (this.interval) clearInterval(this.interval);
1008
- }
1009
- };
1010
-
1011
- // src/server/aggregates/eval-aggregator.ts
1012
- var EvalAggregator = class {
1013
- snaps;
1014
- interval;
1015
- listener;
1016
- options;
1017
- constructor(options) {
1018
- this.options = options;
1019
- this.snaps = new AggregateSnapshots(
1020
- options.windows,
1021
- options.emptyState,
1022
- options.connMgr,
1023
- options.channel,
1024
- options.broadcastTransform
1025
- );
1026
- }
1027
- async start() {
1028
- await this.rebuild();
1029
- this.listener = (entry) => {
1030
- this.snaps.fold(entry.timestamp, (prev) => this.options.reducer(prev, entry));
1031
- };
1032
- this.options.runtime.on("eval_result", this.listener);
1033
- this.interval = setInterval(
1034
- () => this.rebuild().catch((err) => console.error("[axl-studio] rebuild failed:", err)),
1035
- REBUILD_INTERVAL_MS
1036
- );
1037
- }
1038
- async rebuild() {
1039
- const history = await this.options.runtime.getEvalHistory();
1040
- const cap = this.options.entryCap ?? 500;
1041
- const capped = history.slice(0, cap);
1042
- const now = Date.now();
1043
- const fresh = new Map(
1044
- this.options.windows.map((w) => [w, this.options.emptyState()])
1045
- );
1046
- for (const entry of capped) {
1047
- for (const window of this.options.windows) {
1048
- if (withinWindow(entry.timestamp, window, now)) {
1049
- fresh.set(window, this.options.reducer(fresh.get(window), entry));
1050
- }
1051
- }
1052
- }
1053
- this.snaps.replace(fresh);
1054
- }
1055
- getSnapshot(window) {
1056
- return this.snaps.get(window);
1057
- }
1058
- getAllSnapshots() {
1059
- return this.snaps.getAll();
1060
- }
1061
- close() {
1062
- if (this.listener) this.options.runtime.off("eval_result", this.listener);
1063
- if (this.interval) clearInterval(this.interval);
1064
- }
1065
- };
1066
-
1067
1086
  // src/server/routes/health.ts
1068
1087
  var import_hono = require("hono");
1069
1088
  function createHealthRoutes(readOnly) {
@@ -1086,7 +1105,7 @@ function createHealthRoutes(readOnly) {
1086
1105
 
1087
1106
  // src/server/routes/workflows.ts
1088
1107
  var import_hono2 = require("hono");
1089
- var import_axl = require("@axlsdk/axl");
1108
+ var import_axl3 = require("@axlsdk/axl");
1090
1109
  function createWorkflowRoutes(connMgr) {
1091
1110
  const app6 = new import_hono2.Hono();
1092
1111
  app6.get("/workflows", (c) => {
@@ -1112,8 +1131,8 @@ function createWorkflowRoutes(connMgr) {
1112
1131
  ok: true,
1113
1132
  data: {
1114
1133
  name: workflow.name,
1115
- inputSchema: workflow.inputSchema ? (0, import_axl.zodToJsonSchema)(workflow.inputSchema) : null,
1116
- 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
1117
1136
  }
1118
1137
  });
1119
1138
  });
@@ -1172,9 +1191,31 @@ app.get("/executions/:id", async (c) => {
1172
1191
  404
1173
1192
  );
1174
1193
  }
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
+ }
1175
1216
  return c.json({
1176
1217
  ok: true,
1177
- data: redactExecutionInfo(execution, runtime.isRedactEnabled())
1218
+ data: redactExecutionInfo(paged, runtime.isRedactEnabled())
1178
1219
  });
1179
1220
  });
1180
1221
  app.post("/executions/:id/abort", (c) => {
@@ -1254,7 +1295,7 @@ function createSessionRoutes(connMgr) {
1254
1295
 
1255
1296
  // src/server/routes/agents.ts
1256
1297
  var import_hono5 = require("hono");
1257
- var import_axl2 = require("@axlsdk/axl");
1298
+ var import_axl4 = require("@axlsdk/axl");
1258
1299
  var app2 = new import_hono5.Hono();
1259
1300
  app2.get("/agents", (c) => {
1260
1301
  const runtime = c.get("runtime");
@@ -1295,7 +1336,7 @@ app2.get("/agents/:name", (c) => {
1295
1336
  tools: cfg.tools?.map((t) => ({
1296
1337
  name: t.name,
1297
1338
  description: t.description,
1298
- inputSchema: (0, import_axl2.zodToJsonSchema)(t.inputSchema)
1339
+ inputSchema: (0, import_axl4.zodToJsonSchema)(t.inputSchema)
1299
1340
  })) ?? [],
1300
1341
  handoffs: typeof cfg.handoffs === "function" ? [
1301
1342
  {
@@ -1335,14 +1376,14 @@ var agents_default = app2;
1335
1376
 
1336
1377
  // src/server/routes/tools.ts
1337
1378
  var import_hono6 = require("hono");
1338
- var import_axl3 = require("@axlsdk/axl");
1379
+ var import_axl5 = require("@axlsdk/axl");
1339
1380
  var app3 = new import_hono6.Hono();
1340
1381
  app3.get("/tools", (c) => {
1341
1382
  const runtime = c.get("runtime");
1342
1383
  const tools = runtime.getTools().map((t) => ({
1343
1384
  name: t.name,
1344
1385
  description: t.description,
1345
- inputSchema: t.inputSchema ? (0, import_axl3.zodToJsonSchema)(t.inputSchema) : {},
1386
+ inputSchema: t.inputSchema ? (0, import_axl5.zodToJsonSchema)(t.inputSchema) : {},
1346
1387
  sensitive: t.sensitive ?? false,
1347
1388
  requireApproval: t.requireApproval ?? false
1348
1389
  }));
@@ -1363,7 +1404,7 @@ app3.get("/tools/:name", (c) => {
1363
1404
  data: {
1364
1405
  name: tool.name,
1365
1406
  description: tool.description,
1366
- inputSchema: tool.inputSchema ? (0, import_axl3.zodToJsonSchema)(tool.inputSchema) : {},
1407
+ inputSchema: tool.inputSchema ? (0, import_axl5.zodToJsonSchema)(tool.inputSchema) : {},
1367
1408
  sensitive: tool.sensitive,
1368
1409
  requireApproval: tool.requireApproval,
1369
1410
  retry: tool.retry,
@@ -1751,10 +1792,14 @@ function createEvalRoutes(connMgr, evalLoader) {
1751
1792
  const runtime = c.get("runtime");
1752
1793
  const redactOn = runtime.isRedactEnabled();
1753
1794
  const body = await c.req.json();
1795
+ const MAX_POOLED_RUNS = 25;
1754
1796
  const validateIdParam = (v, name) => {
1755
1797
  if (typeof v === "string") return v === "" ? `${name} must be non-empty` : null;
1756
1798
  if (Array.isArray(v)) {
1757
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
+ }
1758
1803
  for (const elem of v) {
1759
1804
  if (typeof elem !== "string" || elem === "") {
1760
1805
  return `${name} array must contain only non-empty strings`;
@@ -1923,32 +1968,50 @@ function createPlaygroundRoutes(connMgr) {
1923
1968
  );
1924
1969
  }
1925
1970
  const sessionId = body.sessionId ?? `playground-${Date.now()}`;
1926
- const executionId = `playground-${sessionId}-${Date.now()}`;
1927
1971
  const store = runtime.getStateStore();
1928
1972
  const history = await store.getSession(sessionId);
1929
1973
  history.push({ role: "user", content: body.message });
1930
1974
  const redactOn = runtime.isRedactEnabled();
1931
- const broadcast = (event) => {
1975
+ const ctx = runtime.createContext({ sessionHistory: history });
1976
+ const executionId = ctx.executionId;
1977
+ const traceListener = (event) => {
1978
+ if (event.executionId !== executionId) return;
1932
1979
  connMgr.broadcastWithWildcard(`execution:${executionId}`, redactStreamEvent(event, redactOn));
1933
1980
  };
1934
- const ctx = runtime.createContext({
1935
- sessionHistory: history,
1936
- onToken: (token) => {
1937
- broadcast({ type: "token", data: token });
1938
- }
1939
- });
1981
+ runtime.on("trace", traceListener);
1940
1982
  (async () => {
1983
+ let stepCounter = Number.MAX_SAFE_INTEGER - 1;
1984
+ const terminalFields = () => ({
1985
+ executionId,
1986
+ step: stepCounter++,
1987
+ timestamp: Date.now()
1988
+ });
1941
1989
  try {
1942
1990
  const result = await ctx.ask(agent, body.message);
1943
1991
  const resultText = typeof result === "string" ? result : JSON.stringify(result);
1944
1992
  history.push({ role: "assistant", content: resultText });
1945
1993
  await store.saveSession(sessionId, history);
1946
- broadcast({ type: "done", data: resultText });
1994
+ const doneEvent = {
1995
+ ...terminalFields(),
1996
+ type: "done",
1997
+ data: { result: resultText }
1998
+ };
1999
+ connMgr.broadcastWithWildcard(
2000
+ `execution:${executionId}`,
2001
+ redactStreamEvent(doneEvent, redactOn)
2002
+ );
1947
2003
  } catch (err) {
1948
- broadcast({
2004
+ const errorEvent = {
2005
+ ...terminalFields(),
1949
2006
  type: "error",
1950
- message: err instanceof Error ? err.message : String(err)
1951
- });
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);
1952
2015
  }
1953
2016
  })();
1954
2017
  return c.json({
@@ -1996,7 +2059,7 @@ function createTraceStatsRoutes(aggregator) {
1996
2059
  function createServer(options) {
1997
2060
  const { runtime, staticRoot, basePath = "", readOnly = false } = options;
1998
2061
  const app6 = new import_hono15.Hono();
1999
- const connMgr = new ConnectionManager();
2062
+ const connMgr = new ConnectionManager(options.bufferCaps);
2000
2063
  const windows = ["24h", "7d", "30d", "all"];
2001
2064
  const costAggregator = new TraceAggregator({
2002
2065
  runtime,
@@ -2090,12 +2153,20 @@ function createServer(options) {
2090
2153
  api.route("/", createPlaygroundRoutes(connMgr));
2091
2154
  app6.route("/api", api);
2092
2155
  const traceListener = (event) => {
2093
- const traceEvent = event;
2094
- if (traceEvent.executionId) {
2095
- connMgr.broadcastWithWildcard(`trace:${traceEvent.executionId}`, traceEvent);
2096
- }
2097
- if (traceEvent.type === "await_human") {
2098
- 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
+ );
2099
2170
  }
2100
2171
  };
2101
2172
  runtime.on("trace", traceListener);
@@ -2374,7 +2445,8 @@ function createStudioMiddleware(options) {
2374
2445
  readOnly,
2375
2446
  cors: false,
2376
2447
  // Host framework owns CORS policy
2377
- evalLoader
2448
+ evalLoader,
2449
+ bufferCaps: options.bufferCaps
2378
2450
  });
2379
2451
  if (filterTraceEvent) {
2380
2452
  connMgr.setFilter(filterTraceEvent);