@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/README.md +6 -5
- package/dist/{chunk-IPDMFFTQ.js → chunk-RE6VPUXA.js} +274 -203
- package/dist/chunk-RE6VPUXA.js.map +1 -0
- package/dist/cli.cjs +281 -210
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +1 -1
- package/dist/client/assets/index-ClajLxib.js +288 -0
- package/dist/client/assets/index-DnHL_gtF.css +1 -0
- package/dist/client/index.html +2 -2
- package/dist/{connection-manager-BMPahDuY.d.cts → connection-manager-DAuqk9lM.d.cts} +24 -1
- package/dist/{connection-manager-BMPahDuY.d.ts → connection-manager-DAuqk9lM.d.ts} +24 -1
- package/dist/middleware.cjs +283 -211
- package/dist/middleware.cjs.map +1 -1
- package/dist/middleware.d.cts +32 -9
- package/dist/middleware.d.ts +32 -9
- package/dist/middleware.js +3 -2
- package/dist/middleware.js.map +1 -1
- package/dist/server/index.cjs +281 -210
- package/dist/server/index.cjs.map +1 -1
- package/dist/server/index.d.cts +12 -6
- package/dist/server/index.d.ts +12 -6
- package/dist/server/index.js +1 -1
- package/package.json +7 -7
- package/dist/chunk-IPDMFFTQ.js.map +0 -1
- package/dist/client/assets/index-CLKKOaE2.css +0 -1
- package/dist/client/assets/index-rvds50cZ.js +0 -278
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
303
|
-
|
|
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
|
|
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.
|
|
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 =
|
|
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 =
|
|
750
|
+
const cost = eventCostContribution(event);
|
|
751
|
+
if (event.type === "ask_end") return acc;
|
|
609
752
|
const tokens = event.tokens ?? {};
|
|
610
|
-
const totalTokens = event.type === "
|
|
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 === "
|
|
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 === "
|
|
664
|
-
const
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
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 === "
|
|
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 === "
|
|
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 === "
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
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-
|
|
2213
|
+
//# sourceMappingURL=chunk-RE6VPUXA.js.map
|