@axlsdk/studio 0.15.0 → 0.16.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -44,6 +44,7 @@ var import_cors = require("hono/cors");
44
44
  var import_serve_static = require("@hono/node-server/serve-static");
45
45
 
46
46
  // src/server/redact.ts
47
+ var import_axl = require("@axlsdk/axl");
47
48
  var REDACTED = "[redacted]";
48
49
  var SAFE_ERROR_NAMES = /* @__PURE__ */ new Set([
49
50
  "QuorumNotMet",
@@ -68,7 +69,8 @@ function redactExecutionInfo(info, redact) {
68
69
  return {
69
70
  ...info,
70
71
  ...info.result !== void 0 ? { result: REDACTED } : {},
71
- ...info.error !== void 0 ? { error: REDACTED } : {}
72
+ ...info.error !== void 0 ? { error: REDACTED } : {},
73
+ events: info.events.map((e) => redactStreamEvent(e, true))
72
74
  };
73
75
  }
74
76
  function redactExecutionList(infos, redact) {
@@ -109,30 +111,7 @@ function redactSessionHistory(history, redact) {
109
111
  }
110
112
  function redactStreamEvent(event, redact) {
111
113
  if (!redact) return event;
112
- switch (event.type) {
113
- case "token":
114
- return { type: "token", data: REDACTED };
115
- case "tool_call":
116
- return { ...event, args: REDACTED };
117
- case "tool_result":
118
- return { ...event, result: REDACTED };
119
- case "tool_approval":
120
- return {
121
- ...event,
122
- args: REDACTED,
123
- ...event.reason !== void 0 ? { reason: REDACTED } : {}
124
- };
125
- case "done":
126
- return { type: "done", data: REDACTED };
127
- case "error":
128
- return { type: "error", message: REDACTED };
129
- // Structural events have no user content to scrub.
130
- case "agent_start":
131
- case "agent_end":
132
- case "handoff":
133
- case "step":
134
- return event;
135
- }
114
+ return (0, import_axl.redactEvent)(event);
136
115
  }
137
116
  function redactEvalItem(item) {
138
117
  const scrubbed = {
@@ -218,13 +197,17 @@ async function errorHandler(c, next) {
218
197
 
219
198
  // src/server/ws/connection-manager.ts
220
199
  var BUFFER_TTL_MS = 3e4;
221
- var MAX_BUFFER_EVENTS = 500;
200
+ var DEFAULT_MAX_BUFFER_EVENTS = 1e3;
201
+ var DEFAULT_MAX_BUFFER_BYTES = 4 * 1024 * 1024;
202
+ var DEFAULT_MAX_ACTIVE_BUFFERS = 256;
203
+ var UNBUFFERED_EVENT_TYPES = /* @__PURE__ */ new Set(["token", "partial_object"]);
222
204
  var MAX_WS_FRAME_BYTES = 65536;
223
205
  function isBufferedChannel(channel) {
224
206
  return channel.startsWith("execution:") || channel.startsWith("eval:");
225
207
  }
226
208
  function truncateIfOversized(msg, channel, data) {
227
- if (msg.length <= MAX_WS_FRAME_BYTES) return msg;
209
+ const msgBytes = Buffer.byteLength(msg, "utf8");
210
+ if (msgBytes <= MAX_WS_FRAME_BYTES) return msg;
228
211
  const event = data ?? {};
229
212
  const truncated = {
230
213
  type: "event",
@@ -233,7 +216,7 @@ function truncateIfOversized(msg, channel, data) {
233
216
  ...event,
234
217
  data: {
235
218
  __truncated: true,
236
- originalBytes: msg.length,
219
+ originalBytes: msgBytes,
237
220
  maxBytes: MAX_WS_FRAME_BYTES,
238
221
  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."
239
222
  }
@@ -250,6 +233,25 @@ var ConnectionManager = class {
250
233
  buffers = /* @__PURE__ */ new Map();
251
234
  maxConnections = 100;
252
235
  filter;
236
+ /** Resolved replay-buffer caps. Per-instance so embedders can dial them
237
+ * without monkey-patching module-level constants. */
238
+ maxEventsPerBuffer;
239
+ maxBytesPerBuffer;
240
+ maxActiveBuffers;
241
+ constructor(bufferCaps) {
242
+ const validatePositiveInt = (key, value) => {
243
+ if (value === void 0) return;
244
+ if (!Number.isFinite(value) || !Number.isInteger(value) || value < 1) {
245
+ throw new RangeError(`bufferCaps.${key} must be a positive integer (>= 1); got ${value}`);
246
+ }
247
+ };
248
+ validatePositiveInt("maxEventsPerBuffer", bufferCaps?.maxEventsPerBuffer);
249
+ validatePositiveInt("maxBytesPerBuffer", bufferCaps?.maxBytesPerBuffer);
250
+ validatePositiveInt("maxActiveBuffers", bufferCaps?.maxActiveBuffers);
251
+ this.maxEventsPerBuffer = bufferCaps?.maxEventsPerBuffer ?? DEFAULT_MAX_BUFFER_EVENTS;
252
+ this.maxBytesPerBuffer = bufferCaps?.maxBytesPerBuffer ?? DEFAULT_MAX_BUFFER_BYTES;
253
+ this.maxActiveBuffers = bufferCaps?.maxActiveBuffers ?? DEFAULT_MAX_ACTIVE_BUFFERS;
254
+ }
253
255
  /**
254
256
  * Register a broadcast filter. Called once at middleware construction.
255
257
  * The filter runs on every outbound event and can drop or deliver based
@@ -332,13 +334,37 @@ var ConnectionManager = class {
332
334
  if (isBufferedChannel(channel)) {
333
335
  let buffer = this.buffers.get(channel);
334
336
  if (!buffer) {
335
- buffer = { events: [], complete: false };
337
+ if (this.buffers.size >= this.maxActiveBuffers) {
338
+ let victim;
339
+ for (const [ch, buf] of this.buffers) {
340
+ if (buf.complete) {
341
+ victim = ch;
342
+ break;
343
+ }
344
+ }
345
+ if (victim === void 0) {
346
+ victim = this.buffers.keys().next().value;
347
+ }
348
+ if (victim !== void 0) {
349
+ const old = this.buffers.get(victim);
350
+ if (old?.timer) clearTimeout(old.timer);
351
+ this.buffers.delete(victim);
352
+ }
353
+ }
354
+ buffer = { events: [], complete: false, bytes: 0 };
336
355
  this.buffers.set(channel, buffer);
337
356
  }
338
357
  const event = data;
339
358
  const isTerminal = event.type === "done" || event.type === "error";
340
- if (buffer.events.length < MAX_BUFFER_EVENTS || isTerminal) {
341
- buffer.events.push({ msg, data });
359
+ const isUnbuffered = event.type !== void 0 && UNBUFFERED_EVENT_TYPES.has(event.type);
360
+ if (!isUnbuffered) {
361
+ const msgBytes = Buffer.byteLength(msg, "utf8");
362
+ const atCountCap = buffer.events.length >= this.maxEventsPerBuffer;
363
+ const atByteCap = buffer.bytes + msgBytes > this.maxBytesPerBuffer;
364
+ if (isTerminal || !atCountCap && !atByteCap) {
365
+ buffer.events.push({ msg, data });
366
+ buffer.bytes += msgBytes;
367
+ }
342
368
  }
343
369
  if (isTerminal) {
344
370
  buffer.complete = true;
@@ -423,7 +449,7 @@ var VALID_CHANNEL_PREFIXES = ["execution:", "trace:", "eval:"];
423
449
  var VALID_EXACT_CHANNELS = ["costs", "decisions", "eval-trends", "workflow-stats", "trace-stats"];
424
450
  var MAX_CHANNEL_LENGTH = 256;
425
451
  function handleWsMessage(raw, socket, connMgr) {
426
- if (raw.length > MAX_WS_FRAME_BYTES) {
452
+ if (Buffer.byteLength(raw, "utf8") > MAX_WS_FRAME_BYTES) {
427
453
  return JSON.stringify({ type: "error", message: "Message too large" });
428
454
  }
429
455
  let msg;
@@ -579,7 +605,7 @@ var TraceAggregator = class {
579
605
  this.options.windows.map((w) => [w, this.options.emptyState()])
580
606
  );
581
607
  for (const exec of capped) {
582
- for (const event of exec.steps) {
608
+ for (const event of exec.events) {
583
609
  for (const window of this.options.windows) {
584
610
  if (withinWindow(event.timestamp, window, now)) {
585
611
  fresh.set(window, this.options.reducer(fresh.get(window), event));
@@ -601,15 +627,131 @@ var TraceAggregator = class {
601
627
  }
602
628
  };
603
629
 
630
+ // src/server/aggregates/execution-aggregator.ts
631
+ var ExecutionAggregator = class {
632
+ snaps;
633
+ interval;
634
+ listener;
635
+ options;
636
+ /** Generation counter to prevent stale async fold after rebuild. */
637
+ generation = 0;
638
+ constructor(options) {
639
+ this.options = options;
640
+ this.snaps = new AggregateSnapshots(
641
+ options.windows,
642
+ options.emptyState,
643
+ options.connMgr,
644
+ options.channel,
645
+ options.broadcastTransform
646
+ );
647
+ }
648
+ async start() {
649
+ await this.rebuild();
650
+ this.listener = (event) => {
651
+ if (event.type !== "workflow_end") return;
652
+ const gen = this.generation;
653
+ this.options.runtime.getExecution(event.executionId).then((exec) => {
654
+ if (this.generation !== gen) return;
655
+ if (exec) {
656
+ this.snaps.fold(exec.startedAt, (prev) => this.options.reducer(prev, exec));
657
+ }
658
+ }).catch((err) => console.error("[axl-studio] execution fold failed:", err));
659
+ };
660
+ this.options.runtime.on("trace", this.listener);
661
+ this.interval = setInterval(
662
+ () => this.rebuild().catch((err) => console.error("[axl-studio] rebuild failed:", err)),
663
+ REBUILD_INTERVAL_MS
664
+ );
665
+ }
666
+ async rebuild() {
667
+ this.generation++;
668
+ const executions = await this.options.runtime.getExecutions();
669
+ const cap = this.options.executionCap ?? 2e3;
670
+ const capped = executions.slice(0, cap);
671
+ const now = Date.now();
672
+ const fresh = new Map(
673
+ this.options.windows.map((w) => [w, this.options.emptyState()])
674
+ );
675
+ for (const exec of capped) {
676
+ for (const window of this.options.windows) {
677
+ if (withinWindow(exec.startedAt, window, now)) {
678
+ fresh.set(window, this.options.reducer(fresh.get(window), exec));
679
+ }
680
+ }
681
+ }
682
+ this.snaps.replace(fresh);
683
+ }
684
+ getSnapshot(window) {
685
+ return this.snaps.get(window);
686
+ }
687
+ getAllSnapshots() {
688
+ return this.snaps.getAll();
689
+ }
690
+ close() {
691
+ if (this.listener) this.options.runtime.off("trace", this.listener);
692
+ if (this.interval) clearInterval(this.interval);
693
+ }
694
+ };
695
+
696
+ // src/server/aggregates/eval-aggregator.ts
697
+ var EvalAggregator = class {
698
+ snaps;
699
+ interval;
700
+ listener;
701
+ options;
702
+ constructor(options) {
703
+ this.options = options;
704
+ this.snaps = new AggregateSnapshots(
705
+ options.windows,
706
+ options.emptyState,
707
+ options.connMgr,
708
+ options.channel,
709
+ options.broadcastTransform
710
+ );
711
+ }
712
+ async start() {
713
+ await this.rebuild();
714
+ this.listener = (entry) => {
715
+ this.snaps.fold(entry.timestamp, (prev) => this.options.reducer(prev, entry));
716
+ };
717
+ this.options.runtime.on("eval_result", this.listener);
718
+ this.interval = setInterval(
719
+ () => this.rebuild().catch((err) => console.error("[axl-studio] rebuild failed:", err)),
720
+ REBUILD_INTERVAL_MS
721
+ );
722
+ }
723
+ async rebuild() {
724
+ const history = await this.options.runtime.getEvalHistory();
725
+ const cap = this.options.entryCap ?? 500;
726
+ const capped = history.slice(0, cap);
727
+ const now = Date.now();
728
+ const fresh = new Map(
729
+ this.options.windows.map((w) => [w, this.options.emptyState()])
730
+ );
731
+ for (const entry of capped) {
732
+ for (const window of this.options.windows) {
733
+ if (withinWindow(entry.timestamp, window, now)) {
734
+ fresh.set(window, this.options.reducer(fresh.get(window), entry));
735
+ }
736
+ }
737
+ }
738
+ this.snaps.replace(fresh);
739
+ }
740
+ getSnapshot(window) {
741
+ return this.snaps.get(window);
742
+ }
743
+ getAllSnapshots() {
744
+ return this.snaps.getAll();
745
+ }
746
+ close() {
747
+ if (this.listener) this.options.runtime.off("eval_result", this.listener);
748
+ if (this.interval) clearInterval(this.interval);
749
+ }
750
+ };
751
+
604
752
  // src/server/aggregates/reducers.ts
753
+ var import_axl2 = require("@axlsdk/axl");
605
754
  var finite = (v) => Number.isFinite(v) ? v : 0;
606
- function isLogEvent(event, eventName) {
607
- if (event.type === eventName) return true;
608
- if (event.type === "log" && event.data != null && typeof event.data === "object") {
609
- return event.data.event === eventName;
610
- }
611
- return false;
612
- }
613
755
  function emptyRetry() {
614
756
  return {
615
757
  primary: 0,
@@ -635,7 +777,7 @@ function emptyCostData() {
635
777
  };
636
778
  }
637
779
  function reduceCost(acc, event) {
638
- const isWorkflowStart = isLogEvent(event, "workflow_start");
780
+ const isWorkflowStart = event.type === "workflow_start";
639
781
  if (isWorkflowStart && event.workflow) {
640
782
  const byWorkflow2 = { ...acc.byWorkflow };
641
783
  const prev = byWorkflow2[event.workflow] ?? { cost: 0, executions: 0 };
@@ -643,9 +785,10 @@ function reduceCost(acc, event) {
643
785
  return { ...acc, byWorkflow: byWorkflow2 };
644
786
  }
645
787
  if (event.cost == null && !event.tokens) return acc;
646
- const cost = finite(event.cost);
788
+ const cost = (0, import_axl2.eventCostContribution)(event);
789
+ if (event.type === "ask_end") return acc;
647
790
  const tokens = event.tokens ?? {};
648
- const totalTokens = event.type === "agent_call" ? {
791
+ const totalTokens = event.type === "agent_call_end" ? {
649
792
  input: acc.totalTokens.input + finite(tokens.input),
650
793
  output: acc.totalTokens.output + finite(tokens.output),
651
794
  reasoning: acc.totalTokens.reasoning + finite(tokens.reasoning)
@@ -676,7 +819,7 @@ function reduceCost(acc, event) {
676
819
  };
677
820
  }
678
821
  let retry = acc.retry;
679
- if (event.type === "agent_call") {
822
+ if (event.type === "agent_call_end") {
680
823
  const d = event.data ?? {};
681
824
  const reason = d.retryReason;
682
825
  retry = { ...acc.retry };
@@ -698,19 +841,17 @@ function reduceCost(acc, event) {
698
841
  }
699
842
  }
700
843
  let byEmbedder = acc.byEmbedder;
701
- if (event.type === "log") {
702
- const d = event.data ?? {};
703
- if (d.event === "memory_remember" || d.event === "memory_recall") {
704
- byEmbedder = { ...acc.byEmbedder };
705
- const modelKey = d.usage?.model ?? "unknown";
706
- const embedTokens = typeof d.usage?.tokens === "number" ? finite(d.usage.tokens) : 0;
707
- const prev = byEmbedder[modelKey] ?? { cost: 0, calls: 0, tokens: 0 };
708
- byEmbedder[modelKey] = {
709
- cost: prev.cost + cost,
710
- calls: prev.calls + 1,
711
- tokens: prev.tokens + embedTokens
712
- };
713
- }
844
+ if (event.type === "memory_remember" || event.type === "memory_recall") {
845
+ const usage = event.data.usage;
846
+ byEmbedder = { ...acc.byEmbedder };
847
+ const modelKey = usage?.model ?? "unknown";
848
+ const embedTokens = typeof usage?.tokens === "number" ? finite(usage.tokens) : 0;
849
+ const prev = byEmbedder[modelKey] ?? { cost: 0, calls: 0, tokens: 0 };
850
+ byEmbedder[modelKey] = {
851
+ cost: prev.cost + cost,
852
+ calls: prev.calls + 1,
853
+ tokens: prev.tokens + embedTokens
854
+ };
714
855
  }
715
856
  return {
716
857
  totalCost: acc.totalCost + cost,
@@ -905,7 +1046,7 @@ function reduceTraceStats(acc, event) {
905
1046
  const eventTypeCounts = { ...acc.eventTypeCounts };
906
1047
  eventTypeCounts[event.type] = (eventTypeCounts[event.type] ?? 0) + 1;
907
1048
  const byTool = { ...acc.byTool };
908
- if (event.type === "tool_call" || event.type === "tool_denied" || event.type === "tool_approval") {
1049
+ if (event.type === "tool_call_end" || event.type === "tool_denied" || event.type === "tool_approval") {
909
1050
  const toolName = event.tool;
910
1051
  const prev = byTool[toolName] ?? { calls: 0, denied: 0, approved: 0 };
911
1052
  const isDeniedEvent = event.type === "tool_denied";
@@ -914,13 +1055,13 @@ function reduceTraceStats(acc, event) {
914
1055
  const isApproved = isDeniedEvent && eventData?.approved === true || isApprovalEvent && eventData?.approved === true;
915
1056
  const isDenied = isDeniedEvent && !eventData?.approved || isApprovalEvent && eventData?.approved === false;
916
1057
  byTool[toolName] = {
917
- calls: prev.calls + (event.type === "tool_call" ? 1 : 0),
1058
+ calls: prev.calls + (event.type === "tool_call_end" ? 1 : 0),
918
1059
  denied: prev.denied + (isDenied ? 1 : 0),
919
1060
  approved: prev.approved + (isApproved ? 1 : 0)
920
1061
  };
921
1062
  }
922
1063
  const retryByAgent = { ...acc.retryByAgent };
923
- if (event.agent && event.type === "agent_call") {
1064
+ if (event.agent && event.type === "agent_call_end") {
924
1065
  const data = event.data;
925
1066
  if (data?.retryReason) {
926
1067
  const prev = retryByAgent[event.agent] ?? { schema: 0, validate: 0, guardrail: 0 };
@@ -938,128 +1079,6 @@ function reduceTraceStats(acc, event) {
938
1079
  };
939
1080
  }
940
1081
 
941
- // src/server/aggregates/execution-aggregator.ts
942
- var ExecutionAggregator = class {
943
- snaps;
944
- interval;
945
- listener;
946
- options;
947
- /** Generation counter to prevent stale async fold after rebuild. */
948
- generation = 0;
949
- constructor(options) {
950
- this.options = options;
951
- this.snaps = new AggregateSnapshots(
952
- options.windows,
953
- options.emptyState,
954
- options.connMgr,
955
- options.channel,
956
- options.broadcastTransform
957
- );
958
- }
959
- async start() {
960
- await this.rebuild();
961
- this.listener = (event) => {
962
- if (!isLogEvent(event, "workflow_end")) return;
963
- const gen = this.generation;
964
- this.options.runtime.getExecution(event.executionId).then((exec) => {
965
- if (this.generation !== gen) return;
966
- if (exec) {
967
- this.snaps.fold(exec.startedAt, (prev) => this.options.reducer(prev, exec));
968
- }
969
- }).catch((err) => console.error("[axl-studio] execution fold failed:", err));
970
- };
971
- this.options.runtime.on("trace", this.listener);
972
- this.interval = setInterval(
973
- () => this.rebuild().catch((err) => console.error("[axl-studio] rebuild failed:", err)),
974
- REBUILD_INTERVAL_MS
975
- );
976
- }
977
- async rebuild() {
978
- this.generation++;
979
- const executions = await this.options.runtime.getExecutions();
980
- const cap = this.options.executionCap ?? 2e3;
981
- const capped = executions.slice(0, cap);
982
- const now = Date.now();
983
- const fresh = new Map(
984
- this.options.windows.map((w) => [w, this.options.emptyState()])
985
- );
986
- for (const exec of capped) {
987
- for (const window of this.options.windows) {
988
- if (withinWindow(exec.startedAt, window, now)) {
989
- fresh.set(window, this.options.reducer(fresh.get(window), exec));
990
- }
991
- }
992
- }
993
- this.snaps.replace(fresh);
994
- }
995
- getSnapshot(window) {
996
- return this.snaps.get(window);
997
- }
998
- getAllSnapshots() {
999
- return this.snaps.getAll();
1000
- }
1001
- close() {
1002
- if (this.listener) this.options.runtime.off("trace", this.listener);
1003
- if (this.interval) clearInterval(this.interval);
1004
- }
1005
- };
1006
-
1007
- // src/server/aggregates/eval-aggregator.ts
1008
- var EvalAggregator = class {
1009
- snaps;
1010
- interval;
1011
- listener;
1012
- options;
1013
- constructor(options) {
1014
- this.options = options;
1015
- this.snaps = new AggregateSnapshots(
1016
- options.windows,
1017
- options.emptyState,
1018
- options.connMgr,
1019
- options.channel,
1020
- options.broadcastTransform
1021
- );
1022
- }
1023
- async start() {
1024
- await this.rebuild();
1025
- this.listener = (entry) => {
1026
- this.snaps.fold(entry.timestamp, (prev) => this.options.reducer(prev, entry));
1027
- };
1028
- this.options.runtime.on("eval_result", this.listener);
1029
- this.interval = setInterval(
1030
- () => this.rebuild().catch((err) => console.error("[axl-studio] rebuild failed:", err)),
1031
- REBUILD_INTERVAL_MS
1032
- );
1033
- }
1034
- async rebuild() {
1035
- const history = await this.options.runtime.getEvalHistory();
1036
- const cap = this.options.entryCap ?? 500;
1037
- const capped = history.slice(0, cap);
1038
- const now = Date.now();
1039
- const fresh = new Map(
1040
- this.options.windows.map((w) => [w, this.options.emptyState()])
1041
- );
1042
- for (const entry of capped) {
1043
- for (const window of this.options.windows) {
1044
- if (withinWindow(entry.timestamp, window, now)) {
1045
- fresh.set(window, this.options.reducer(fresh.get(window), entry));
1046
- }
1047
- }
1048
- }
1049
- this.snaps.replace(fresh);
1050
- }
1051
- getSnapshot(window) {
1052
- return this.snaps.get(window);
1053
- }
1054
- getAllSnapshots() {
1055
- return this.snaps.getAll();
1056
- }
1057
- close() {
1058
- if (this.listener) this.options.runtime.off("eval_result", this.listener);
1059
- if (this.interval) clearInterval(this.interval);
1060
- }
1061
- };
1062
-
1063
1082
  // src/server/routes/health.ts
1064
1083
  var import_hono = require("hono");
1065
1084
  function createHealthRoutes(readOnly) {
@@ -1082,7 +1101,7 @@ function createHealthRoutes(readOnly) {
1082
1101
 
1083
1102
  // src/server/routes/workflows.ts
1084
1103
  var import_hono2 = require("hono");
1085
- var import_axl = require("@axlsdk/axl");
1104
+ var import_axl3 = require("@axlsdk/axl");
1086
1105
  function createWorkflowRoutes(connMgr) {
1087
1106
  const app6 = new import_hono2.Hono();
1088
1107
  app6.get("/workflows", (c) => {
@@ -1108,8 +1127,8 @@ function createWorkflowRoutes(connMgr) {
1108
1127
  ok: true,
1109
1128
  data: {
1110
1129
  name: workflow.name,
1111
- inputSchema: workflow.inputSchema ? (0, import_axl.zodToJsonSchema)(workflow.inputSchema) : null,
1112
- outputSchema: workflow.outputSchema ? (0, import_axl.zodToJsonSchema)(workflow.outputSchema) : null
1130
+ inputSchema: workflow.inputSchema ? (0, import_axl3.zodToJsonSchema)(workflow.inputSchema) : null,
1131
+ outputSchema: workflow.outputSchema ? (0, import_axl3.zodToJsonSchema)(workflow.outputSchema) : null
1113
1132
  }
1114
1133
  });
1115
1134
  });
@@ -1168,9 +1187,31 @@ app.get("/executions/:id", async (c) => {
1168
1187
  404
1169
1188
  );
1170
1189
  }
1190
+ const sinceParam = c.req.query("since");
1191
+ let paged = execution;
1192
+ if (sinceParam !== void 0) {
1193
+ const since = Number(sinceParam);
1194
+ if (!Number.isFinite(since) || !Number.isInteger(since)) {
1195
+ return c.json(
1196
+ {
1197
+ ok: false,
1198
+ error: {
1199
+ code: "INVALID_PARAM",
1200
+ message: `\`since\` must be a finite integer (got "${sinceParam}")`,
1201
+ param: "since"
1202
+ }
1203
+ },
1204
+ 400
1205
+ );
1206
+ }
1207
+ paged = {
1208
+ ...execution,
1209
+ events: execution.events.filter((e) => e.step > since)
1210
+ };
1211
+ }
1171
1212
  return c.json({
1172
1213
  ok: true,
1173
- data: redactExecutionInfo(execution, runtime.isRedactEnabled())
1214
+ data: redactExecutionInfo(paged, runtime.isRedactEnabled())
1174
1215
  });
1175
1216
  });
1176
1217
  app.post("/executions/:id/abort", (c) => {
@@ -1250,7 +1291,7 @@ function createSessionRoutes(connMgr) {
1250
1291
 
1251
1292
  // src/server/routes/agents.ts
1252
1293
  var import_hono5 = require("hono");
1253
- var import_axl2 = require("@axlsdk/axl");
1294
+ var import_axl4 = require("@axlsdk/axl");
1254
1295
  var app2 = new import_hono5.Hono();
1255
1296
  app2.get("/agents", (c) => {
1256
1297
  const runtime = c.get("runtime");
@@ -1291,7 +1332,7 @@ app2.get("/agents/:name", (c) => {
1291
1332
  tools: cfg.tools?.map((t) => ({
1292
1333
  name: t.name,
1293
1334
  description: t.description,
1294
- inputSchema: (0, import_axl2.zodToJsonSchema)(t.inputSchema)
1335
+ inputSchema: (0, import_axl4.zodToJsonSchema)(t.inputSchema)
1295
1336
  })) ?? [],
1296
1337
  handoffs: typeof cfg.handoffs === "function" ? [
1297
1338
  {
@@ -1331,14 +1372,14 @@ var agents_default = app2;
1331
1372
 
1332
1373
  // src/server/routes/tools.ts
1333
1374
  var import_hono6 = require("hono");
1334
- var import_axl3 = require("@axlsdk/axl");
1375
+ var import_axl5 = require("@axlsdk/axl");
1335
1376
  var app3 = new import_hono6.Hono();
1336
1377
  app3.get("/tools", (c) => {
1337
1378
  const runtime = c.get("runtime");
1338
1379
  const tools = runtime.getTools().map((t) => ({
1339
1380
  name: t.name,
1340
1381
  description: t.description,
1341
- inputSchema: t.inputSchema ? (0, import_axl3.zodToJsonSchema)(t.inputSchema) : {},
1382
+ inputSchema: t.inputSchema ? (0, import_axl5.zodToJsonSchema)(t.inputSchema) : {},
1342
1383
  sensitive: t.sensitive ?? false,
1343
1384
  requireApproval: t.requireApproval ?? false
1344
1385
  }));
@@ -1359,7 +1400,7 @@ app3.get("/tools/:name", (c) => {
1359
1400
  data: {
1360
1401
  name: tool.name,
1361
1402
  description: tool.description,
1362
- inputSchema: tool.inputSchema ? (0, import_axl3.zodToJsonSchema)(tool.inputSchema) : {},
1403
+ inputSchema: tool.inputSchema ? (0, import_axl5.zodToJsonSchema)(tool.inputSchema) : {},
1363
1404
  sensitive: tool.sensitive,
1364
1405
  requireApproval: tool.requireApproval,
1365
1406
  retry: tool.retry,
@@ -1747,10 +1788,14 @@ function createEvalRoutes(connMgr, evalLoader) {
1747
1788
  const runtime = c.get("runtime");
1748
1789
  const redactOn = runtime.isRedactEnabled();
1749
1790
  const body = await c.req.json();
1791
+ const MAX_POOLED_RUNS = 25;
1750
1792
  const validateIdParam = (v, name) => {
1751
1793
  if (typeof v === "string") return v === "" ? `${name} must be non-empty` : null;
1752
1794
  if (Array.isArray(v)) {
1753
1795
  if (v.length === 0) return `${name} must be a non-empty array`;
1796
+ if (v.length > MAX_POOLED_RUNS) {
1797
+ return `${name} may contain at most ${MAX_POOLED_RUNS} ids (pooled comparison)`;
1798
+ }
1754
1799
  for (const elem of v) {
1755
1800
  if (typeof elem !== "string" || elem === "") {
1756
1801
  return `${name} array must contain only non-empty strings`;
@@ -1919,32 +1964,50 @@ function createPlaygroundRoutes(connMgr) {
1919
1964
  );
1920
1965
  }
1921
1966
  const sessionId = body.sessionId ?? `playground-${Date.now()}`;
1922
- const executionId = `playground-${sessionId}-${Date.now()}`;
1923
1967
  const store = runtime.getStateStore();
1924
1968
  const history = await store.getSession(sessionId);
1925
1969
  history.push({ role: "user", content: body.message });
1926
1970
  const redactOn = runtime.isRedactEnabled();
1927
- const broadcast = (event) => {
1971
+ const ctx = runtime.createContext({ sessionHistory: history });
1972
+ const executionId = ctx.executionId;
1973
+ const traceListener = (event) => {
1974
+ if (event.executionId !== executionId) return;
1928
1975
  connMgr.broadcastWithWildcard(`execution:${executionId}`, redactStreamEvent(event, redactOn));
1929
1976
  };
1930
- const ctx = runtime.createContext({
1931
- sessionHistory: history,
1932
- onToken: (token) => {
1933
- broadcast({ type: "token", data: token });
1934
- }
1935
- });
1977
+ runtime.on("trace", traceListener);
1936
1978
  (async () => {
1979
+ let stepCounter = Number.MAX_SAFE_INTEGER - 1;
1980
+ const terminalFields = () => ({
1981
+ executionId,
1982
+ step: stepCounter++,
1983
+ timestamp: Date.now()
1984
+ });
1937
1985
  try {
1938
1986
  const result = await ctx.ask(agent, body.message);
1939
1987
  const resultText = typeof result === "string" ? result : JSON.stringify(result);
1940
1988
  history.push({ role: "assistant", content: resultText });
1941
1989
  await store.saveSession(sessionId, history);
1942
- broadcast({ type: "done", data: resultText });
1990
+ const doneEvent = {
1991
+ ...terminalFields(),
1992
+ type: "done",
1993
+ data: { result: resultText }
1994
+ };
1995
+ connMgr.broadcastWithWildcard(
1996
+ `execution:${executionId}`,
1997
+ redactStreamEvent(doneEvent, redactOn)
1998
+ );
1943
1999
  } catch (err) {
1944
- broadcast({
2000
+ const errorEvent = {
2001
+ ...terminalFields(),
1945
2002
  type: "error",
1946
- message: err instanceof Error ? err.message : String(err)
1947
- });
2003
+ data: { message: err instanceof Error ? err.message : String(err) }
2004
+ };
2005
+ connMgr.broadcastWithWildcard(
2006
+ `execution:${executionId}`,
2007
+ redactStreamEvent(errorEvent, redactOn)
2008
+ );
2009
+ } finally {
2010
+ runtime.off("trace", traceListener);
1948
2011
  }
1949
2012
  })();
1950
2013
  return c.json({
@@ -1992,7 +2055,7 @@ function createTraceStatsRoutes(aggregator) {
1992
2055
  function createServer(options) {
1993
2056
  const { runtime, staticRoot, basePath = "", readOnly = false } = options;
1994
2057
  const app6 = new import_hono15.Hono();
1995
- const connMgr = new ConnectionManager();
2058
+ const connMgr = new ConnectionManager(options.bufferCaps);
1996
2059
  const windows = ["24h", "7d", "30d", "all"];
1997
2060
  const costAggregator = new TraceAggregator({
1998
2061
  runtime,
@@ -2086,12 +2149,20 @@ function createServer(options) {
2086
2149
  api.route("/", createPlaygroundRoutes(connMgr));
2087
2150
  app6.route("/api", api);
2088
2151
  const traceListener = (event) => {
2089
- const traceEvent = event;
2090
- if (traceEvent.executionId) {
2091
- connMgr.broadcastWithWildcard(`trace:${traceEvent.executionId}`, traceEvent);
2092
- }
2093
- if (traceEvent.type === "await_human") {
2094
- connMgr.broadcast("decisions", traceEvent);
2152
+ try {
2153
+ const traceEvent = event;
2154
+ const redacted = redactStreamEvent(traceEvent, runtime.isRedactEnabled());
2155
+ if (traceEvent.executionId) {
2156
+ connMgr.broadcastWithWildcard(`trace:${traceEvent.executionId}`, redacted);
2157
+ }
2158
+ if (traceEvent.type === "await_human") {
2159
+ connMgr.broadcast("decisions", redacted);
2160
+ }
2161
+ } catch (err) {
2162
+ console.error(
2163
+ "[axl-studio] trace listener threw; event dropped:",
2164
+ err instanceof Error ? err.message : String(err)
2165
+ );
2095
2166
  }
2096
2167
  };
2097
2168
  runtime.on("trace", traceListener);