@axlsdk/studio 0.14.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 +69 -10
- package/dist/chunk-RE6VPUXA.js +2213 -0
- package/dist/chunk-RE6VPUXA.js.map +1 -0
- package/dist/cli.cjs +1191 -143
- 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-DAuqk9lM.d.cts +166 -0
- package/dist/connection-manager-DAuqk9lM.d.ts +166 -0
- package/dist/middleware.cjs +1222 -150
- package/dist/middleware.cjs.map +1 -1
- package/dist/middleware.d.cts +76 -6
- package/dist/middleware.d.ts +76 -6
- package/dist/middleware.js +32 -8
- package/dist/middleware.js.map +1 -1
- package/dist/server/index.cjs +1194 -142
- package/dist/server/index.cjs.map +1 -1
- package/dist/server/index.d.cts +171 -28
- package/dist/server/index.d.ts +171 -28
- package/dist/server/index.js +7 -3
- package/package.json +13 -9
- package/dist/chunk-HUKUQDYL.js +0 -1163
- package/dist/chunk-HUKUQDYL.js.map +0 -1
- package/dist/client/assets/index-7aDhMztu.css +0 -1
- package/dist/client/assets/index-Bzr3vDPz.js +0 -255
- package/dist/connection-manager-B7AWpsCD.d.cts +0 -81
- package/dist/connection-manager-B7AWpsCD.d.ts +0 -81
package/dist/middleware.cjs
CHANGED
|
@@ -43,16 +43,140 @@ var import_ws = require("ws");
|
|
|
43
43
|
// src/server/index.ts
|
|
44
44
|
var import_node_fs = require("fs");
|
|
45
45
|
var import_node_path = require("path");
|
|
46
|
-
var
|
|
46
|
+
var import_hono15 = require("hono");
|
|
47
47
|
var import_cors = require("hono/cors");
|
|
48
48
|
var import_serve_static = require("@hono/node-server/serve-static");
|
|
49
49
|
|
|
50
|
+
// src/server/redact.ts
|
|
51
|
+
var import_axl = require("@axlsdk/axl");
|
|
52
|
+
var REDACTED = "[redacted]";
|
|
53
|
+
var SAFE_ERROR_NAMES = /* @__PURE__ */ new Set([
|
|
54
|
+
"QuorumNotMet",
|
|
55
|
+
"NoConsensus",
|
|
56
|
+
"TimeoutError",
|
|
57
|
+
"MaxTurnsError",
|
|
58
|
+
"BudgetExceededError",
|
|
59
|
+
"ToolDenied"
|
|
60
|
+
]);
|
|
61
|
+
function redactErrorMessage(err, redact) {
|
|
62
|
+
const raw = err instanceof Error ? err.message : String(err);
|
|
63
|
+
if (!redact) return raw;
|
|
64
|
+
const name = err instanceof Error ? err.name : "";
|
|
65
|
+
return SAFE_ERROR_NAMES.has(name) ? raw : REDACTED;
|
|
66
|
+
}
|
|
67
|
+
function redactValue(value, redact) {
|
|
68
|
+
if (!redact) return value;
|
|
69
|
+
return REDACTED;
|
|
70
|
+
}
|
|
71
|
+
function redactExecutionInfo(info, redact) {
|
|
72
|
+
if (!redact) return info;
|
|
73
|
+
return {
|
|
74
|
+
...info,
|
|
75
|
+
...info.result !== void 0 ? { result: REDACTED } : {},
|
|
76
|
+
...info.error !== void 0 ? { error: REDACTED } : {},
|
|
77
|
+
events: info.events.map((e) => redactStreamEvent(e, true))
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
function redactExecutionList(infos, redact) {
|
|
81
|
+
if (!redact) return infos;
|
|
82
|
+
return infos.map((info) => redactExecutionInfo(info, redact));
|
|
83
|
+
}
|
|
84
|
+
function redactMemoryValue(value, redact) {
|
|
85
|
+
if (!redact) return value;
|
|
86
|
+
return REDACTED;
|
|
87
|
+
}
|
|
88
|
+
function redactMemoryList(entries, redact) {
|
|
89
|
+
if (!redact) return entries;
|
|
90
|
+
return entries.map((entry) => ({ key: entry.key, value: REDACTED }));
|
|
91
|
+
}
|
|
92
|
+
function redactChatMessage(msg) {
|
|
93
|
+
const scrubbed = {
|
|
94
|
+
role: msg.role,
|
|
95
|
+
content: REDACTED,
|
|
96
|
+
...msg.name !== void 0 ? { name: msg.name } : {},
|
|
97
|
+
...msg.tool_call_id !== void 0 ? { tool_call_id: msg.tool_call_id } : {},
|
|
98
|
+
...msg.tool_calls !== void 0 ? {
|
|
99
|
+
tool_calls: msg.tool_calls.map((tc) => ({
|
|
100
|
+
id: tc.id,
|
|
101
|
+
type: tc.type,
|
|
102
|
+
function: {
|
|
103
|
+
name: tc.function.name,
|
|
104
|
+
arguments: REDACTED
|
|
105
|
+
}
|
|
106
|
+
}))
|
|
107
|
+
} : {}
|
|
108
|
+
// providerMetadata deliberately omitted — opaque content.
|
|
109
|
+
};
|
|
110
|
+
return scrubbed;
|
|
111
|
+
}
|
|
112
|
+
function redactSessionHistory(history, redact) {
|
|
113
|
+
if (!redact) return history;
|
|
114
|
+
return history.map(redactChatMessage);
|
|
115
|
+
}
|
|
116
|
+
function redactStreamEvent(event, redact) {
|
|
117
|
+
if (!redact) return event;
|
|
118
|
+
return (0, import_axl.redactEvent)(event);
|
|
119
|
+
}
|
|
120
|
+
function redactEvalItem(item) {
|
|
121
|
+
const scrubbed = {
|
|
122
|
+
...item,
|
|
123
|
+
input: REDACTED,
|
|
124
|
+
output: REDACTED,
|
|
125
|
+
...item.annotations !== void 0 ? { annotations: REDACTED } : {},
|
|
126
|
+
...item.error !== void 0 ? { error: REDACTED } : {},
|
|
127
|
+
...item.scorerErrors !== void 0 ? { scorerErrors: item.scorerErrors.map(() => REDACTED) } : {}
|
|
128
|
+
};
|
|
129
|
+
if (item.scoreDetails) {
|
|
130
|
+
const detailsOut = {};
|
|
131
|
+
for (const [name, detail] of Object.entries(item.scoreDetails)) {
|
|
132
|
+
detailsOut[name] = {
|
|
133
|
+
score: detail.score,
|
|
134
|
+
...detail.duration !== void 0 ? { duration: detail.duration } : {},
|
|
135
|
+
...detail.cost !== void 0 ? { cost: detail.cost } : {}
|
|
136
|
+
// metadata deliberately omitted — may contain LLM scorer reasoning
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
scrubbed.scoreDetails = detailsOut;
|
|
140
|
+
}
|
|
141
|
+
return scrubbed;
|
|
142
|
+
}
|
|
143
|
+
function redactEvalResult(result, redact) {
|
|
144
|
+
if (!redact) return result;
|
|
145
|
+
return {
|
|
146
|
+
...result,
|
|
147
|
+
items: result.items.map(redactEvalItem)
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
function redactEvalHistoryEntry(entry, redact) {
|
|
151
|
+
if (!redact) return entry;
|
|
152
|
+
return {
|
|
153
|
+
...entry,
|
|
154
|
+
data: redactEvalResult(entry.data, redact)
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
function redactEvalHistoryList(entries, redact) {
|
|
158
|
+
if (!redact) return entries;
|
|
159
|
+
return entries.map((e) => redactEvalHistoryEntry(e, redact));
|
|
160
|
+
}
|
|
161
|
+
function redactPendingDecision(decision, redact) {
|
|
162
|
+
if (!redact) return decision;
|
|
163
|
+
return {
|
|
164
|
+
...decision,
|
|
165
|
+
prompt: REDACTED,
|
|
166
|
+
...decision.metadata !== void 0 ? { metadata: { redacted: true } } : {}
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
function redactPendingDecisionList(decisions, redact) {
|
|
170
|
+
if (!redact) return decisions;
|
|
171
|
+
return decisions.map((d) => redactPendingDecision(d, redact));
|
|
172
|
+
}
|
|
173
|
+
|
|
50
174
|
// src/server/middleware/error-handler.ts
|
|
51
175
|
async function errorHandler(c, next) {
|
|
52
176
|
try {
|
|
53
177
|
await next();
|
|
54
178
|
} catch (err) {
|
|
55
|
-
const
|
|
179
|
+
const rawMessage = err instanceof Error ? err.message : String(err);
|
|
56
180
|
const code = err.code ?? "INTERNAL_ERROR";
|
|
57
181
|
let status = 500;
|
|
58
182
|
if ("status" in err) {
|
|
@@ -60,46 +184,104 @@ async function errorHandler(c, next) {
|
|
|
60
184
|
if (typeof errStatus === "number" && errStatus >= 400 && errStatus < 600) {
|
|
61
185
|
status = errStatus;
|
|
62
186
|
}
|
|
63
|
-
} else if (code === "NOT_FOUND" ||
|
|
187
|
+
} else if (code === "NOT_FOUND" || rawMessage.includes("not found") || rawMessage.includes("not registered")) {
|
|
64
188
|
status = 404;
|
|
65
|
-
} else if (code === "VALIDATION_ERROR" ||
|
|
189
|
+
} else if (code === "VALIDATION_ERROR" || rawMessage.includes("Expected") || rawMessage.includes("invalid")) {
|
|
66
190
|
status = 400;
|
|
67
191
|
}
|
|
192
|
+
const runtime = c.get("runtime");
|
|
193
|
+
const redactOn = runtime?.isRedactEnabled?.() ?? false;
|
|
68
194
|
const body = {
|
|
69
195
|
ok: false,
|
|
70
|
-
error: { code, message }
|
|
196
|
+
error: { code, message: redactErrorMessage(err, redactOn) }
|
|
71
197
|
};
|
|
72
198
|
return c.json(body, status);
|
|
73
199
|
}
|
|
74
200
|
}
|
|
75
201
|
|
|
76
202
|
// src/server/ws/connection-manager.ts
|
|
203
|
+
var BUFFER_TTL_MS = 3e4;
|
|
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"]);
|
|
208
|
+
var MAX_WS_FRAME_BYTES = 65536;
|
|
77
209
|
function isBufferedChannel(channel) {
|
|
78
|
-
return channel.startsWith("execution:");
|
|
210
|
+
return channel.startsWith("execution:") || channel.startsWith("eval:");
|
|
211
|
+
}
|
|
212
|
+
function truncateIfOversized(msg, channel, data) {
|
|
213
|
+
const msgBytes = Buffer.byteLength(msg, "utf8");
|
|
214
|
+
if (msgBytes <= MAX_WS_FRAME_BYTES) return msg;
|
|
215
|
+
const event = data ?? {};
|
|
216
|
+
const truncated = {
|
|
217
|
+
type: "event",
|
|
218
|
+
channel,
|
|
219
|
+
data: {
|
|
220
|
+
...event,
|
|
221
|
+
data: {
|
|
222
|
+
__truncated: true,
|
|
223
|
+
originalBytes: msgBytes,
|
|
224
|
+
maxBytes: MAX_WS_FRAME_BYTES,
|
|
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."
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
return JSON.stringify(truncated);
|
|
79
230
|
}
|
|
80
|
-
var BUFFER_TTL_MS = 3e4;
|
|
81
|
-
var MAX_BUFFER_EVENTS = 500;
|
|
82
231
|
var ConnectionManager = class {
|
|
83
232
|
/** channel -> set of WS connections */
|
|
84
233
|
channels = /* @__PURE__ */ new Map();
|
|
85
|
-
/** ws ->
|
|
234
|
+
/** ws -> subscribed channels + optional integrator-supplied metadata */
|
|
86
235
|
connections = /* @__PURE__ */ new Map();
|
|
87
236
|
/** channel -> replay buffer for execution streams */
|
|
88
237
|
buffers = /* @__PURE__ */ new Map();
|
|
89
238
|
maxConnections = 100;
|
|
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
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Register a broadcast filter. Called once at middleware construction.
|
|
261
|
+
* The filter runs on every outbound event and can drop or deliver based
|
|
262
|
+
* on the destination connection's metadata.
|
|
263
|
+
*/
|
|
264
|
+
setFilter(filter) {
|
|
265
|
+
this.filter = filter;
|
|
266
|
+
}
|
|
267
|
+
/** Attach integrator-supplied metadata to an already-added connection. */
|
|
268
|
+
setMetadata(ws, metadata) {
|
|
269
|
+
const entry = this.connections.get(ws);
|
|
270
|
+
if (entry) entry.metadata = metadata;
|
|
271
|
+
}
|
|
90
272
|
/** Register a new WS connection. */
|
|
91
273
|
add(ws) {
|
|
92
274
|
if (this.connections.size >= this.maxConnections) {
|
|
93
275
|
ws.close?.();
|
|
94
276
|
return;
|
|
95
277
|
}
|
|
96
|
-
this.connections.set(ws, /* @__PURE__ */ new Set());
|
|
278
|
+
this.connections.set(ws, { channels: /* @__PURE__ */ new Set() });
|
|
97
279
|
}
|
|
98
280
|
/** Remove a WS connection and all its subscriptions. */
|
|
99
281
|
remove(ws) {
|
|
100
|
-
const
|
|
101
|
-
if (
|
|
102
|
-
for (const ch of channels) {
|
|
282
|
+
const entry = this.connections.get(ws);
|
|
283
|
+
if (entry) {
|
|
284
|
+
for (const ch of entry.channels) {
|
|
103
285
|
this.channels.get(ch)?.delete(ws);
|
|
104
286
|
if (this.channels.get(ch)?.size === 0) {
|
|
105
287
|
this.channels.delete(ch);
|
|
@@ -117,12 +299,20 @@ var ConnectionManager = class {
|
|
|
117
299
|
this.channels.set(channel, subs);
|
|
118
300
|
}
|
|
119
301
|
subs.add(ws);
|
|
120
|
-
this.connections.get(ws).add(channel);
|
|
302
|
+
this.connections.get(ws).channels.add(channel);
|
|
121
303
|
const buffer = this.buffers.get(channel);
|
|
122
304
|
if (buffer) {
|
|
123
|
-
|
|
305
|
+
const metadata = this.connections.get(ws)?.metadata;
|
|
306
|
+
for (const event of buffer.events) {
|
|
307
|
+
if (this.filter) {
|
|
308
|
+
try {
|
|
309
|
+
if (!this.filter(event.data, metadata)) continue;
|
|
310
|
+
} catch {
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
124
314
|
try {
|
|
125
|
-
ws.send(msg);
|
|
315
|
+
ws.send(event.msg);
|
|
126
316
|
} catch {
|
|
127
317
|
this.remove(ws);
|
|
128
318
|
return;
|
|
@@ -136,21 +326,49 @@ var ConnectionManager = class {
|
|
|
136
326
|
if (this.channels.get(channel)?.size === 0) {
|
|
137
327
|
this.channels.delete(channel);
|
|
138
328
|
}
|
|
139
|
-
this.connections.get(ws)?.delete(channel);
|
|
329
|
+
this.connections.get(ws)?.channels.delete(channel);
|
|
140
330
|
}
|
|
141
331
|
/** Broadcast data to all subscribers of a channel. Buffers events for execution channels. */
|
|
142
332
|
broadcast(channel, data) {
|
|
143
|
-
const msg =
|
|
333
|
+
const msg = truncateIfOversized(
|
|
334
|
+
JSON.stringify({ type: "event", channel, data }),
|
|
335
|
+
channel,
|
|
336
|
+
data
|
|
337
|
+
);
|
|
144
338
|
if (isBufferedChannel(channel)) {
|
|
145
339
|
let buffer = this.buffers.get(channel);
|
|
146
340
|
if (!buffer) {
|
|
147
|
-
|
|
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 };
|
|
148
359
|
this.buffers.set(channel, buffer);
|
|
149
360
|
}
|
|
150
361
|
const event = data;
|
|
151
362
|
const isTerminal = event.type === "done" || event.type === "error";
|
|
152
|
-
|
|
153
|
-
|
|
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
|
+
}
|
|
154
372
|
}
|
|
155
373
|
if (isTerminal) {
|
|
156
374
|
buffer.complete = true;
|
|
@@ -163,6 +381,14 @@ var ConnectionManager = class {
|
|
|
163
381
|
const subs = this.channels.get(channel);
|
|
164
382
|
if (!subs || subs.size === 0) return;
|
|
165
383
|
for (const ws of [...subs]) {
|
|
384
|
+
if (this.filter) {
|
|
385
|
+
const metadata = this.connections.get(ws)?.metadata;
|
|
386
|
+
try {
|
|
387
|
+
if (!this.filter(data, metadata)) continue;
|
|
388
|
+
} catch {
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
166
392
|
try {
|
|
167
393
|
ws.send(msg);
|
|
168
394
|
} catch {
|
|
@@ -178,8 +404,20 @@ var ConnectionManager = class {
|
|
|
178
404
|
const wildcardChannel = channel.substring(0, colonIdx) + ":*";
|
|
179
405
|
const subs = this.channels.get(wildcardChannel);
|
|
180
406
|
if (!subs || subs.size === 0) return;
|
|
181
|
-
const msg =
|
|
407
|
+
const msg = truncateIfOversized(
|
|
408
|
+
JSON.stringify({ type: "event", channel, data }),
|
|
409
|
+
channel,
|
|
410
|
+
data
|
|
411
|
+
);
|
|
182
412
|
for (const ws of [...subs]) {
|
|
413
|
+
if (this.filter) {
|
|
414
|
+
const metadata = this.connections.get(ws)?.metadata;
|
|
415
|
+
try {
|
|
416
|
+
if (!this.filter(data, metadata)) continue;
|
|
417
|
+
} catch {
|
|
418
|
+
continue;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
183
421
|
try {
|
|
184
422
|
ws.send(msg);
|
|
185
423
|
} catch {
|
|
@@ -211,11 +449,11 @@ var ConnectionManager = class {
|
|
|
211
449
|
};
|
|
212
450
|
|
|
213
451
|
// src/server/ws/protocol.ts
|
|
214
|
-
var VALID_CHANNEL_PREFIXES = ["execution:", "trace:"];
|
|
215
|
-
var VALID_EXACT_CHANNELS = ["costs", "decisions"];
|
|
452
|
+
var VALID_CHANNEL_PREFIXES = ["execution:", "trace:", "eval:"];
|
|
453
|
+
var VALID_EXACT_CHANNELS = ["costs", "decisions", "eval-trends", "workflow-stats", "trace-stats"];
|
|
216
454
|
var MAX_CHANNEL_LENGTH = 256;
|
|
217
455
|
function handleWsMessage(raw, socket, connMgr) {
|
|
218
|
-
if (raw
|
|
456
|
+
if (Buffer.byteLength(raw, "utf8") > MAX_WS_FRAME_BYTES) {
|
|
219
457
|
return JSON.stringify({ type: "error", message: "Message too large" });
|
|
220
458
|
}
|
|
221
459
|
let msg;
|
|
@@ -275,68 +513,575 @@ function createWsHandlers(connMgr) {
|
|
|
275
513
|
};
|
|
276
514
|
}
|
|
277
515
|
|
|
278
|
-
// src/server/
|
|
279
|
-
var
|
|
280
|
-
|
|
516
|
+
// src/server/aggregates/aggregate-snapshots.ts
|
|
517
|
+
var WINDOW_MS = {
|
|
518
|
+
"24h": 24 * 60 * 60 * 1e3,
|
|
519
|
+
"7d": 7 * 24 * 60 * 60 * 1e3,
|
|
520
|
+
"30d": 30 * 24 * 60 * 60 * 1e3,
|
|
521
|
+
all: Number.POSITIVE_INFINITY
|
|
522
|
+
};
|
|
523
|
+
function withinWindow(ts, window, now) {
|
|
524
|
+
return ts >= now - WINDOW_MS[window];
|
|
525
|
+
}
|
|
526
|
+
var REBUILD_INTERVAL_MS = 5 * 6e4;
|
|
527
|
+
var ALL_WINDOWS = new Set(Object.keys(WINDOW_MS));
|
|
528
|
+
function parseWindowParam(raw, fallback = "7d") {
|
|
529
|
+
return raw && ALL_WINDOWS.has(raw) ? raw : fallback;
|
|
530
|
+
}
|
|
531
|
+
var AggregateSnapshots = class {
|
|
532
|
+
constructor(windows, emptyState, connMgr, channel, broadcastTransform) {
|
|
533
|
+
this.windows = windows;
|
|
534
|
+
this.emptyState = emptyState;
|
|
281
535
|
this.connMgr = connMgr;
|
|
536
|
+
this.channel = channel;
|
|
537
|
+
this.broadcastTransform = broadcastTransform;
|
|
538
|
+
this.snapshots = new Map(windows.map((w) => [w, emptyState()]));
|
|
539
|
+
}
|
|
540
|
+
snapshots;
|
|
541
|
+
/** Replace all snapshots atomically — used after a full rebuild. */
|
|
542
|
+
replace(fresh) {
|
|
543
|
+
this.snapshots = fresh;
|
|
544
|
+
this.broadcast();
|
|
545
|
+
}
|
|
546
|
+
/** Apply a reducer update to every window where `ts` falls inside the window. */
|
|
547
|
+
fold(ts, update) {
|
|
548
|
+
const now = Date.now();
|
|
549
|
+
let changed = false;
|
|
550
|
+
for (const window of this.windows) {
|
|
551
|
+
if (withinWindow(ts, window, now)) {
|
|
552
|
+
const prev = this.snapshots.get(window);
|
|
553
|
+
this.snapshots.set(window, update(prev));
|
|
554
|
+
changed = true;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
if (changed) this.broadcast();
|
|
558
|
+
}
|
|
559
|
+
get(window) {
|
|
560
|
+
return this.snapshots.get(window) ?? this.emptyState();
|
|
561
|
+
}
|
|
562
|
+
getAll() {
|
|
563
|
+
return Object.fromEntries(this.snapshots);
|
|
564
|
+
}
|
|
565
|
+
broadcast() {
|
|
566
|
+
const snapshots = this.broadcastTransform ? Object.fromEntries(
|
|
567
|
+
this.windows.map((w) => [w, this.broadcastTransform(this.snapshots.get(w))])
|
|
568
|
+
) : this.getAll();
|
|
569
|
+
this.connMgr.broadcast(this.channel, {
|
|
570
|
+
snapshots,
|
|
571
|
+
updatedAt: Date.now()
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
};
|
|
575
|
+
|
|
576
|
+
// src/server/aggregates/trace-aggregator.ts
|
|
577
|
+
var TraceAggregator = class {
|
|
578
|
+
snaps;
|
|
579
|
+
interval;
|
|
580
|
+
listener;
|
|
581
|
+
options;
|
|
582
|
+
constructor(options) {
|
|
583
|
+
this.options = options;
|
|
584
|
+
this.snaps = new AggregateSnapshots(
|
|
585
|
+
options.windows,
|
|
586
|
+
options.emptyState,
|
|
587
|
+
options.connMgr,
|
|
588
|
+
options.channel,
|
|
589
|
+
options.broadcastTransform
|
|
590
|
+
);
|
|
591
|
+
}
|
|
592
|
+
async start() {
|
|
593
|
+
await this.rebuild();
|
|
594
|
+
this.listener = (event) => {
|
|
595
|
+
this.snaps.fold(event.timestamp, (prev) => this.options.reducer(prev, event));
|
|
596
|
+
};
|
|
597
|
+
this.options.runtime.on("trace", this.listener);
|
|
598
|
+
this.interval = setInterval(
|
|
599
|
+
() => this.rebuild().catch((err) => console.error("[axl-studio] rebuild failed:", err)),
|
|
600
|
+
REBUILD_INTERVAL_MS
|
|
601
|
+
);
|
|
602
|
+
}
|
|
603
|
+
async rebuild() {
|
|
604
|
+
const executions = await this.options.runtime.getExecutions();
|
|
605
|
+
const cap = this.options.executionCap ?? 2e3;
|
|
606
|
+
const capped = executions.slice(0, cap);
|
|
607
|
+
const now = Date.now();
|
|
608
|
+
const fresh = new Map(
|
|
609
|
+
this.options.windows.map((w) => [w, this.options.emptyState()])
|
|
610
|
+
);
|
|
611
|
+
for (const exec of capped) {
|
|
612
|
+
for (const event of exec.events) {
|
|
613
|
+
for (const window of this.options.windows) {
|
|
614
|
+
if (withinWindow(event.timestamp, window, now)) {
|
|
615
|
+
fresh.set(window, this.options.reducer(fresh.get(window), event));
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
this.snaps.replace(fresh);
|
|
621
|
+
}
|
|
622
|
+
getSnapshot(window) {
|
|
623
|
+
return this.snaps.get(window);
|
|
624
|
+
}
|
|
625
|
+
getAllSnapshots() {
|
|
626
|
+
return this.snaps.getAll();
|
|
627
|
+
}
|
|
628
|
+
close() {
|
|
629
|
+
if (this.listener) this.options.runtime.off("trace", this.listener);
|
|
630
|
+
if (this.interval) clearInterval(this.interval);
|
|
631
|
+
}
|
|
632
|
+
};
|
|
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();
|
|
282
749
|
}
|
|
283
|
-
|
|
750
|
+
close() {
|
|
751
|
+
if (this.listener) this.options.runtime.off("eval_result", this.listener);
|
|
752
|
+
if (this.interval) clearInterval(this.interval);
|
|
753
|
+
}
|
|
754
|
+
};
|
|
755
|
+
|
|
756
|
+
// src/server/aggregates/reducers.ts
|
|
757
|
+
var import_axl2 = require("@axlsdk/axl");
|
|
758
|
+
var finite = (v) => Number.isFinite(v) ? v : 0;
|
|
759
|
+
function emptyRetry() {
|
|
760
|
+
return {
|
|
761
|
+
primary: 0,
|
|
762
|
+
primaryCalls: 0,
|
|
763
|
+
schema: 0,
|
|
764
|
+
schemaCalls: 0,
|
|
765
|
+
validate: 0,
|
|
766
|
+
validateCalls: 0,
|
|
767
|
+
guardrail: 0,
|
|
768
|
+
guardrailCalls: 0,
|
|
769
|
+
retryCalls: 0
|
|
770
|
+
};
|
|
771
|
+
}
|
|
772
|
+
function emptyCostData() {
|
|
773
|
+
return {
|
|
284
774
|
totalCost: 0,
|
|
285
775
|
totalTokens: { input: 0, output: 0, reasoning: 0 },
|
|
286
776
|
byAgent: {},
|
|
287
777
|
byModel: {},
|
|
288
|
-
byWorkflow: {}
|
|
778
|
+
byWorkflow: {},
|
|
779
|
+
retry: emptyRetry(),
|
|
780
|
+
byEmbedder: {}
|
|
289
781
|
};
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
const
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
this.connMgr.broadcast("costs", this.data);
|
|
324
|
-
}
|
|
325
|
-
/** Get current aggregated cost data. */
|
|
326
|
-
getData() {
|
|
327
|
-
return this.data;
|
|
328
|
-
}
|
|
329
|
-
/** Reset all accumulated data. */
|
|
330
|
-
reset() {
|
|
331
|
-
this.data = {
|
|
332
|
-
totalCost: 0,
|
|
333
|
-
totalTokens: { input: 0, output: 0, reasoning: 0 },
|
|
334
|
-
byAgent: {},
|
|
335
|
-
byModel: {},
|
|
336
|
-
byWorkflow: {}
|
|
782
|
+
}
|
|
783
|
+
function reduceCost(acc, event) {
|
|
784
|
+
const isWorkflowStart = event.type === "workflow_start";
|
|
785
|
+
if (isWorkflowStart && event.workflow) {
|
|
786
|
+
const byWorkflow2 = { ...acc.byWorkflow };
|
|
787
|
+
const prev = byWorkflow2[event.workflow] ?? { cost: 0, executions: 0 };
|
|
788
|
+
byWorkflow2[event.workflow] = { ...prev, executions: prev.executions + 1 };
|
|
789
|
+
return { ...acc, byWorkflow: byWorkflow2 };
|
|
790
|
+
}
|
|
791
|
+
if (event.cost == null && !event.tokens) return acc;
|
|
792
|
+
const cost = (0, import_axl2.eventCostContribution)(event);
|
|
793
|
+
if (event.type === "ask_end") return acc;
|
|
794
|
+
const tokens = event.tokens ?? {};
|
|
795
|
+
const totalTokens = event.type === "agent_call_end" ? {
|
|
796
|
+
input: acc.totalTokens.input + finite(tokens.input),
|
|
797
|
+
output: acc.totalTokens.output + finite(tokens.output),
|
|
798
|
+
reasoning: acc.totalTokens.reasoning + finite(tokens.reasoning)
|
|
799
|
+
} : acc.totalTokens;
|
|
800
|
+
const byAgent = { ...acc.byAgent };
|
|
801
|
+
if (event.agent) {
|
|
802
|
+
const prev = byAgent[event.agent] ?? { cost: 0, calls: 0 };
|
|
803
|
+
byAgent[event.agent] = { cost: prev.cost + cost, calls: prev.calls + 1 };
|
|
804
|
+
}
|
|
805
|
+
const byModel = { ...acc.byModel };
|
|
806
|
+
if (event.model) {
|
|
807
|
+
const prev = byModel[event.model] ?? { cost: 0, calls: 0, tokens: { input: 0, output: 0 } };
|
|
808
|
+
byModel[event.model] = {
|
|
809
|
+
cost: prev.cost + cost,
|
|
810
|
+
calls: prev.calls + 1,
|
|
811
|
+
tokens: {
|
|
812
|
+
input: prev.tokens.input + finite(tokens.input),
|
|
813
|
+
output: prev.tokens.output + finite(tokens.output)
|
|
814
|
+
}
|
|
337
815
|
};
|
|
338
816
|
}
|
|
339
|
-
};
|
|
817
|
+
const byWorkflow = { ...acc.byWorkflow };
|
|
818
|
+
if (event.workflow) {
|
|
819
|
+
const prev = byWorkflow[event.workflow] ?? { cost: 0, executions: 0 };
|
|
820
|
+
byWorkflow[event.workflow] = {
|
|
821
|
+
cost: prev.cost + cost,
|
|
822
|
+
executions: prev.executions + (isWorkflowStart ? 1 : 0)
|
|
823
|
+
};
|
|
824
|
+
}
|
|
825
|
+
let retry = acc.retry;
|
|
826
|
+
if (event.type === "agent_call_end") {
|
|
827
|
+
const d = event.data ?? {};
|
|
828
|
+
const reason = d.retryReason;
|
|
829
|
+
retry = { ...acc.retry };
|
|
830
|
+
if (reason === "schema") {
|
|
831
|
+
retry.schema += cost;
|
|
832
|
+
retry.schemaCalls += 1;
|
|
833
|
+
retry.retryCalls += 1;
|
|
834
|
+
} else if (reason === "validate") {
|
|
835
|
+
retry.validate += cost;
|
|
836
|
+
retry.validateCalls += 1;
|
|
837
|
+
retry.retryCalls += 1;
|
|
838
|
+
} else if (reason === "guardrail") {
|
|
839
|
+
retry.guardrail += cost;
|
|
840
|
+
retry.guardrailCalls += 1;
|
|
841
|
+
retry.retryCalls += 1;
|
|
842
|
+
} else {
|
|
843
|
+
retry.primary += cost;
|
|
844
|
+
retry.primaryCalls += 1;
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
let byEmbedder = acc.byEmbedder;
|
|
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
|
+
};
|
|
859
|
+
}
|
|
860
|
+
return {
|
|
861
|
+
totalCost: acc.totalCost + cost,
|
|
862
|
+
totalTokens,
|
|
863
|
+
byAgent,
|
|
864
|
+
byModel,
|
|
865
|
+
byWorkflow,
|
|
866
|
+
retry,
|
|
867
|
+
byEmbedder
|
|
868
|
+
};
|
|
869
|
+
}
|
|
870
|
+
function emptyEvalTrendData() {
|
|
871
|
+
return { byEval: {}, totalRuns: 0, totalCost: 0 };
|
|
872
|
+
}
|
|
873
|
+
function extractScores(data) {
|
|
874
|
+
if (!data || typeof data !== "object") return {};
|
|
875
|
+
const result = data;
|
|
876
|
+
const summary = result.summary;
|
|
877
|
+
const scorers = summary?.scorers;
|
|
878
|
+
if (!scorers) return {};
|
|
879
|
+
const out = {};
|
|
880
|
+
for (const [name, entry] of Object.entries(scorers)) {
|
|
881
|
+
if (typeof entry === "number" && Number.isFinite(entry)) {
|
|
882
|
+
out[name] = entry;
|
|
883
|
+
} else if (entry && typeof entry === "object" && Number.isFinite(entry.mean)) {
|
|
884
|
+
out[name] = entry.mean;
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
return out;
|
|
888
|
+
}
|
|
889
|
+
function extractCost(data) {
|
|
890
|
+
if (!data || typeof data !== "object") return 0;
|
|
891
|
+
const result = data;
|
|
892
|
+
if (Number.isFinite(result.totalCost)) return result.totalCost;
|
|
893
|
+
const summary = result.summary;
|
|
894
|
+
return Number.isFinite(summary?.totalCost) ? summary.totalCost : 0;
|
|
895
|
+
}
|
|
896
|
+
function extractModel(data) {
|
|
897
|
+
if (!data || typeof data !== "object") return void 0;
|
|
898
|
+
const result = data;
|
|
899
|
+
const metadata = result.metadata;
|
|
900
|
+
const counts = metadata?.modelCounts;
|
|
901
|
+
if (counts && typeof counts === "object" && !Array.isArray(counts)) {
|
|
902
|
+
const entries = Object.entries(counts).filter(
|
|
903
|
+
([, v]) => typeof v === "number"
|
|
904
|
+
);
|
|
905
|
+
if (entries.length > 0) {
|
|
906
|
+
entries.sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]));
|
|
907
|
+
return entries[0][0];
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
const models = metadata?.models;
|
|
911
|
+
if (Array.isArray(models) && typeof models[0] === "string") return models[0];
|
|
912
|
+
return void 0;
|
|
913
|
+
}
|
|
914
|
+
function extractDuration(data) {
|
|
915
|
+
if (!data || typeof data !== "object") return void 0;
|
|
916
|
+
const result = data;
|
|
917
|
+
return Number.isFinite(result.duration) ? result.duration : void 0;
|
|
918
|
+
}
|
|
919
|
+
function computeScoreStats(runs) {
|
|
920
|
+
const scorerNames = /* @__PURE__ */ new Set();
|
|
921
|
+
for (const run of runs) {
|
|
922
|
+
for (const name of Object.keys(run.scores)) scorerNames.add(name);
|
|
923
|
+
}
|
|
924
|
+
const mean = {};
|
|
925
|
+
const std = {};
|
|
926
|
+
for (const name of scorerNames) {
|
|
927
|
+
const values = runs.map((r) => r.scores[name]).filter((v) => v != null);
|
|
928
|
+
if (values.length === 0) continue;
|
|
929
|
+
const m = values.reduce((a, b) => a + b, 0) / values.length;
|
|
930
|
+
mean[name] = m;
|
|
931
|
+
const variance = values.reduce((sum, v) => sum + (v - m) ** 2, 0) / values.length;
|
|
932
|
+
std[name] = Math.sqrt(variance);
|
|
933
|
+
}
|
|
934
|
+
return { mean, std };
|
|
935
|
+
}
|
|
936
|
+
function reduceEvalTrends(acc, entry) {
|
|
937
|
+
const scores = extractScores(entry.data);
|
|
938
|
+
const cost = extractCost(entry.data);
|
|
939
|
+
const model = extractModel(entry.data);
|
|
940
|
+
const duration = extractDuration(entry.data);
|
|
941
|
+
const run = {
|
|
942
|
+
timestamp: entry.timestamp,
|
|
943
|
+
id: entry.id,
|
|
944
|
+
scores,
|
|
945
|
+
cost,
|
|
946
|
+
...model !== void 0 ? { model } : {},
|
|
947
|
+
...duration !== void 0 ? { duration } : {}
|
|
948
|
+
};
|
|
949
|
+
const byEval = { ...acc.byEval };
|
|
950
|
+
const prev = byEval[entry.eval];
|
|
951
|
+
const MAX_EVAL_RUNS = 50;
|
|
952
|
+
const allRuns = prev ? [...prev.runs, run] : [run];
|
|
953
|
+
const runs = allRuns.length > MAX_EVAL_RUNS ? allRuns.slice(-MAX_EVAL_RUNS) : allRuns;
|
|
954
|
+
const { mean, std } = computeScoreStats(runs);
|
|
955
|
+
const latestScores = prev && prev.runs.length > 0 && prev.runs[prev.runs.length - 1].timestamp > run.timestamp ? prev.latestScores : scores;
|
|
956
|
+
byEval[entry.eval] = {
|
|
957
|
+
runs,
|
|
958
|
+
latestScores,
|
|
959
|
+
scoreMean: mean,
|
|
960
|
+
scoreStd: std,
|
|
961
|
+
costTotal: (prev?.costTotal ?? 0) + cost,
|
|
962
|
+
runCount: (prev?.runCount ?? 0) + 1
|
|
963
|
+
};
|
|
964
|
+
return {
|
|
965
|
+
byEval,
|
|
966
|
+
totalRuns: acc.totalRuns + 1,
|
|
967
|
+
totalCost: acc.totalCost + cost
|
|
968
|
+
};
|
|
969
|
+
}
|
|
970
|
+
var MAX_DURATIONS = 200;
|
|
971
|
+
function emptyWorkflowStatsData() {
|
|
972
|
+
return { byWorkflow: {}, totalExecutions: 0, failureRate: 0 };
|
|
973
|
+
}
|
|
974
|
+
function percentile(sorted, p) {
|
|
975
|
+
if (sorted.length === 0) return 0;
|
|
976
|
+
const idx = p / 100 * (sorted.length - 1);
|
|
977
|
+
const lower = Math.floor(idx);
|
|
978
|
+
const upper = Math.ceil(idx);
|
|
979
|
+
if (lower === upper) return sorted[lower];
|
|
980
|
+
return sorted[lower] + (sorted[upper] - sorted[lower]) * (idx - lower);
|
|
981
|
+
}
|
|
982
|
+
function reduceWorkflowStats(acc, execution) {
|
|
983
|
+
const byWorkflow = { ...acc.byWorkflow };
|
|
984
|
+
const prev = byWorkflow[execution.workflow] ?? {
|
|
985
|
+
total: 0,
|
|
986
|
+
completed: 0,
|
|
987
|
+
failed: 0,
|
|
988
|
+
durations: [],
|
|
989
|
+
durationSum: 0,
|
|
990
|
+
avgDuration: 0
|
|
991
|
+
};
|
|
992
|
+
const dur = finite(execution.duration);
|
|
993
|
+
const durations = [...prev.durations];
|
|
994
|
+
const insertIdx = durations.findIndex((d) => d > dur);
|
|
995
|
+
if (insertIdx === -1) durations.push(dur);
|
|
996
|
+
else durations.splice(insertIdx, 0, dur);
|
|
997
|
+
if (durations.length > MAX_DURATIONS) durations.shift();
|
|
998
|
+
const total = prev.total + 1;
|
|
999
|
+
const completed = prev.completed + (execution.status === "completed" ? 1 : 0);
|
|
1000
|
+
const failed = prev.failed + (execution.status === "failed" ? 1 : 0);
|
|
1001
|
+
const durationSum = prev.durationSum + dur;
|
|
1002
|
+
const avgDuration = durationSum / total;
|
|
1003
|
+
byWorkflow[execution.workflow] = {
|
|
1004
|
+
total,
|
|
1005
|
+
completed,
|
|
1006
|
+
failed,
|
|
1007
|
+
durations,
|
|
1008
|
+
durationSum,
|
|
1009
|
+
avgDuration
|
|
1010
|
+
};
|
|
1011
|
+
const totalExecutions = acc.totalExecutions + 1;
|
|
1012
|
+
const totalFailed = Object.values(byWorkflow).reduce((sum, w) => sum + w.failed, 0);
|
|
1013
|
+
const failureRate = totalExecutions > 0 ? totalFailed / totalExecutions : 0;
|
|
1014
|
+
return { byWorkflow, totalExecutions, failureRate };
|
|
1015
|
+
}
|
|
1016
|
+
function getWorkflowPercentiles(entry) {
|
|
1017
|
+
return {
|
|
1018
|
+
durationP50: percentile(entry.durations, 50),
|
|
1019
|
+
durationP95: percentile(entry.durations, 95)
|
|
1020
|
+
};
|
|
1021
|
+
}
|
|
1022
|
+
function enrichWorkflowStats(data) {
|
|
1023
|
+
const byWorkflow = {};
|
|
1024
|
+
for (const [name, entry] of Object.entries(data.byWorkflow)) {
|
|
1025
|
+
const { durationP50, durationP95 } = getWorkflowPercentiles(entry);
|
|
1026
|
+
byWorkflow[name] = {
|
|
1027
|
+
total: entry.total,
|
|
1028
|
+
completed: entry.completed,
|
|
1029
|
+
failed: entry.failed,
|
|
1030
|
+
durationP50,
|
|
1031
|
+
durationP95,
|
|
1032
|
+
avgDuration: entry.avgDuration
|
|
1033
|
+
};
|
|
1034
|
+
}
|
|
1035
|
+
return {
|
|
1036
|
+
byWorkflow,
|
|
1037
|
+
totalExecutions: data.totalExecutions,
|
|
1038
|
+
failureRate: data.failureRate
|
|
1039
|
+
};
|
|
1040
|
+
}
|
|
1041
|
+
function emptyTraceStatsData() {
|
|
1042
|
+
return {
|
|
1043
|
+
eventTypeCounts: {},
|
|
1044
|
+
byTool: {},
|
|
1045
|
+
retryByAgent: {},
|
|
1046
|
+
totalEvents: 0
|
|
1047
|
+
};
|
|
1048
|
+
}
|
|
1049
|
+
function reduceTraceStats(acc, event) {
|
|
1050
|
+
const eventTypeCounts = { ...acc.eventTypeCounts };
|
|
1051
|
+
eventTypeCounts[event.type] = (eventTypeCounts[event.type] ?? 0) + 1;
|
|
1052
|
+
const byTool = { ...acc.byTool };
|
|
1053
|
+
if (event.type === "tool_call_end" || event.type === "tool_denied" || event.type === "tool_approval") {
|
|
1054
|
+
const toolName = event.tool;
|
|
1055
|
+
const prev = byTool[toolName] ?? { calls: 0, denied: 0, approved: 0 };
|
|
1056
|
+
const isDeniedEvent = event.type === "tool_denied";
|
|
1057
|
+
const isApprovalEvent = event.type === "tool_approval";
|
|
1058
|
+
const eventData = isDeniedEvent || isApprovalEvent ? event.data : void 0;
|
|
1059
|
+
const isApproved = isDeniedEvent && eventData?.approved === true || isApprovalEvent && eventData?.approved === true;
|
|
1060
|
+
const isDenied = isDeniedEvent && !eventData?.approved || isApprovalEvent && eventData?.approved === false;
|
|
1061
|
+
byTool[toolName] = {
|
|
1062
|
+
calls: prev.calls + (event.type === "tool_call_end" ? 1 : 0),
|
|
1063
|
+
denied: prev.denied + (isDenied ? 1 : 0),
|
|
1064
|
+
approved: prev.approved + (isApproved ? 1 : 0)
|
|
1065
|
+
};
|
|
1066
|
+
}
|
|
1067
|
+
const retryByAgent = { ...acc.retryByAgent };
|
|
1068
|
+
if (event.agent && event.type === "agent_call_end") {
|
|
1069
|
+
const data = event.data;
|
|
1070
|
+
if (data?.retryReason) {
|
|
1071
|
+
const prev = retryByAgent[event.agent] ?? { schema: 0, validate: 0, guardrail: 0 };
|
|
1072
|
+
const reason = data.retryReason;
|
|
1073
|
+
if (reason in prev) {
|
|
1074
|
+
retryByAgent[event.agent] = { ...prev, [reason]: prev[reason] + 1 };
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
return {
|
|
1079
|
+
eventTypeCounts,
|
|
1080
|
+
byTool,
|
|
1081
|
+
retryByAgent,
|
|
1082
|
+
totalEvents: acc.totalEvents + 1
|
|
1083
|
+
};
|
|
1084
|
+
}
|
|
340
1085
|
|
|
341
1086
|
// src/server/routes/health.ts
|
|
342
1087
|
var import_hono = require("hono");
|
|
@@ -360,7 +1105,7 @@ function createHealthRoutes(readOnly) {
|
|
|
360
1105
|
|
|
361
1106
|
// src/server/routes/workflows.ts
|
|
362
1107
|
var import_hono2 = require("hono");
|
|
363
|
-
var
|
|
1108
|
+
var import_axl3 = require("@axlsdk/axl");
|
|
364
1109
|
function createWorkflowRoutes(connMgr) {
|
|
365
1110
|
const app6 = new import_hono2.Hono();
|
|
366
1111
|
app6.get("/workflows", (c) => {
|
|
@@ -386,8 +1131,8 @@ function createWorkflowRoutes(connMgr) {
|
|
|
386
1131
|
ok: true,
|
|
387
1132
|
data: {
|
|
388
1133
|
name: workflow.name,
|
|
389
|
-
inputSchema: workflow.inputSchema ? (0,
|
|
390
|
-
outputSchema: workflow.outputSchema ? (0,
|
|
1134
|
+
inputSchema: workflow.inputSchema ? (0, import_axl3.zodToJsonSchema)(workflow.inputSchema) : null,
|
|
1135
|
+
outputSchema: workflow.outputSchema ? (0, import_axl3.zodToJsonSchema)(workflow.outputSchema) : null
|
|
391
1136
|
}
|
|
392
1137
|
});
|
|
393
1138
|
});
|
|
@@ -405,15 +1150,22 @@ function createWorkflowRoutes(connMgr) {
|
|
|
405
1150
|
if (body.stream) {
|
|
406
1151
|
const stream = runtime.stream(name, body.input ?? {}, { metadata: body.metadata });
|
|
407
1152
|
const executionId = `stream-${Date.now()}`;
|
|
1153
|
+
const redactOn = runtime.isRedactEnabled();
|
|
408
1154
|
(async () => {
|
|
409
1155
|
for await (const event of stream) {
|
|
410
|
-
connMgr.broadcastWithWildcard(
|
|
1156
|
+
connMgr.broadcastWithWildcard(
|
|
1157
|
+
`execution:${executionId}`,
|
|
1158
|
+
redactStreamEvent(event, redactOn)
|
|
1159
|
+
);
|
|
411
1160
|
}
|
|
412
1161
|
})();
|
|
413
1162
|
return c.json({ ok: true, data: { executionId, streaming: true } });
|
|
414
1163
|
}
|
|
415
1164
|
const result = await runtime.execute(name, body.input ?? {}, { metadata: body.metadata });
|
|
416
|
-
return c.json({
|
|
1165
|
+
return c.json({
|
|
1166
|
+
ok: true,
|
|
1167
|
+
data: { result: redactValue(result, runtime.isRedactEnabled()) }
|
|
1168
|
+
});
|
|
417
1169
|
});
|
|
418
1170
|
return app6;
|
|
419
1171
|
}
|
|
@@ -424,7 +1176,10 @@ var app = new import_hono3.Hono();
|
|
|
424
1176
|
app.get("/executions", async (c) => {
|
|
425
1177
|
const runtime = c.get("runtime");
|
|
426
1178
|
const executions = await runtime.getExecutions();
|
|
427
|
-
return c.json({
|
|
1179
|
+
return c.json({
|
|
1180
|
+
ok: true,
|
|
1181
|
+
data: redactExecutionList(executions, runtime.isRedactEnabled())
|
|
1182
|
+
});
|
|
428
1183
|
});
|
|
429
1184
|
app.get("/executions/:id", async (c) => {
|
|
430
1185
|
const runtime = c.get("runtime");
|
|
@@ -436,7 +1191,32 @@ app.get("/executions/:id", async (c) => {
|
|
|
436
1191
|
404
|
|
437
1192
|
);
|
|
438
1193
|
}
|
|
439
|
-
|
|
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
|
+
}
|
|
1216
|
+
return c.json({
|
|
1217
|
+
ok: true,
|
|
1218
|
+
data: redactExecutionInfo(paged, runtime.isRedactEnabled())
|
|
1219
|
+
});
|
|
440
1220
|
});
|
|
441
1221
|
app.post("/executions/:id/abort", (c) => {
|
|
442
1222
|
const runtime = c.get("runtime");
|
|
@@ -470,7 +1250,16 @@ function createSessionRoutes(connMgr) {
|
|
|
470
1250
|
const id = c.req.param("id");
|
|
471
1251
|
const history = await store.getSession(id);
|
|
472
1252
|
const handoffHistory = await store.getSessionMeta(id, "handoffHistory");
|
|
473
|
-
return c.json({
|
|
1253
|
+
return c.json({
|
|
1254
|
+
ok: true,
|
|
1255
|
+
data: {
|
|
1256
|
+
id,
|
|
1257
|
+
history: redactSessionHistory(history, runtime.isRedactEnabled()),
|
|
1258
|
+
// HandoffRecord has no content fields (source/target/mode/
|
|
1259
|
+
// timestamp/duration) — nothing to scrub.
|
|
1260
|
+
handoffHistory: handoffHistory ?? []
|
|
1261
|
+
}
|
|
1262
|
+
});
|
|
474
1263
|
});
|
|
475
1264
|
app6.post("/sessions/:id/send", async (c) => {
|
|
476
1265
|
const runtime = c.get("runtime");
|
|
@@ -506,7 +1295,7 @@ function createSessionRoutes(connMgr) {
|
|
|
506
1295
|
|
|
507
1296
|
// src/server/routes/agents.ts
|
|
508
1297
|
var import_hono5 = require("hono");
|
|
509
|
-
var
|
|
1298
|
+
var import_axl4 = require("@axlsdk/axl");
|
|
510
1299
|
var app2 = new import_hono5.Hono();
|
|
511
1300
|
app2.get("/agents", (c) => {
|
|
512
1301
|
const runtime = c.get("runtime");
|
|
@@ -547,7 +1336,7 @@ app2.get("/agents/:name", (c) => {
|
|
|
547
1336
|
tools: cfg.tools?.map((t) => ({
|
|
548
1337
|
name: t.name,
|
|
549
1338
|
description: t.description,
|
|
550
|
-
inputSchema: (0,
|
|
1339
|
+
inputSchema: (0, import_axl4.zodToJsonSchema)(t.inputSchema)
|
|
551
1340
|
})) ?? [],
|
|
552
1341
|
handoffs: typeof cfg.handoffs === "function" ? [
|
|
553
1342
|
{
|
|
@@ -587,14 +1376,14 @@ var agents_default = app2;
|
|
|
587
1376
|
|
|
588
1377
|
// src/server/routes/tools.ts
|
|
589
1378
|
var import_hono6 = require("hono");
|
|
590
|
-
var
|
|
1379
|
+
var import_axl5 = require("@axlsdk/axl");
|
|
591
1380
|
var app3 = new import_hono6.Hono();
|
|
592
1381
|
app3.get("/tools", (c) => {
|
|
593
1382
|
const runtime = c.get("runtime");
|
|
594
1383
|
const tools = runtime.getTools().map((t) => ({
|
|
595
1384
|
name: t.name,
|
|
596
1385
|
description: t.description,
|
|
597
|
-
inputSchema: t.inputSchema ? (0,
|
|
1386
|
+
inputSchema: t.inputSchema ? (0, import_axl5.zodToJsonSchema)(t.inputSchema) : {},
|
|
598
1387
|
sensitive: t.sensitive ?? false,
|
|
599
1388
|
requireApproval: t.requireApproval ?? false
|
|
600
1389
|
}));
|
|
@@ -615,7 +1404,7 @@ app3.get("/tools/:name", (c) => {
|
|
|
615
1404
|
data: {
|
|
616
1405
|
name: tool.name,
|
|
617
1406
|
description: tool.description,
|
|
618
|
-
inputSchema: tool.inputSchema ? (0,
|
|
1407
|
+
inputSchema: tool.inputSchema ? (0, import_axl5.zodToJsonSchema)(tool.inputSchema) : {},
|
|
619
1408
|
sensitive: tool.sensitive,
|
|
620
1409
|
requireApproval: tool.requireApproval,
|
|
621
1410
|
retry: tool.retry,
|
|
@@ -640,7 +1429,10 @@ app3.post("/tools/:name/test", async (c) => {
|
|
|
640
1429
|
const body = await c.req.json();
|
|
641
1430
|
const ctx = runtime.createContext();
|
|
642
1431
|
const result = await tool.run(ctx, body.input);
|
|
643
|
-
return c.json({
|
|
1432
|
+
return c.json({
|
|
1433
|
+
ok: true,
|
|
1434
|
+
data: { result: redactValue(result, runtime.isRedactEnabled()) }
|
|
1435
|
+
});
|
|
644
1436
|
});
|
|
645
1437
|
var tools_default = app3;
|
|
646
1438
|
|
|
@@ -655,7 +1447,7 @@ app4.get("/memory/:scope", async (c) => {
|
|
|
655
1447
|
return c.json({ ok: true, data: [] });
|
|
656
1448
|
}
|
|
657
1449
|
const entries = await store.getAllMemory(scope);
|
|
658
|
-
return c.json({ ok: true, data: entries });
|
|
1450
|
+
return c.json({ ok: true, data: redactMemoryList(entries, runtime.isRedactEnabled()) });
|
|
659
1451
|
});
|
|
660
1452
|
app4.get("/memory/:scope/:key", async (c) => {
|
|
661
1453
|
const runtime = c.get("runtime");
|
|
@@ -675,7 +1467,10 @@ app4.get("/memory/:scope/:key", async (c) => {
|
|
|
675
1467
|
404
|
|
676
1468
|
);
|
|
677
1469
|
}
|
|
678
|
-
return c.json({
|
|
1470
|
+
return c.json({
|
|
1471
|
+
ok: true,
|
|
1472
|
+
data: { key, value: redactMemoryValue(value, runtime.isRedactEnabled()) }
|
|
1473
|
+
});
|
|
679
1474
|
});
|
|
680
1475
|
app4.put("/memory/:scope/:key", async (c) => {
|
|
681
1476
|
const runtime = c.get("runtime");
|
|
@@ -720,7 +1515,10 @@ var app5 = new import_hono8.Hono();
|
|
|
720
1515
|
app5.get("/decisions", async (c) => {
|
|
721
1516
|
const runtime = c.get("runtime");
|
|
722
1517
|
const decisions = await runtime.getPendingDecisions();
|
|
723
|
-
return c.json({
|
|
1518
|
+
return c.json({
|
|
1519
|
+
ok: true,
|
|
1520
|
+
data: redactPendingDecisionList(decisions, runtime.isRedactEnabled())
|
|
1521
|
+
});
|
|
724
1522
|
});
|
|
725
1523
|
app5.post("/decisions/:executionId/resolve", async (c) => {
|
|
726
1524
|
const runtime = c.get("runtime");
|
|
@@ -736,11 +1534,23 @@ var import_hono9 = require("hono");
|
|
|
736
1534
|
function createCostRoutes(costAggregator) {
|
|
737
1535
|
const app6 = new import_hono9.Hono();
|
|
738
1536
|
app6.get("/costs", (c) => {
|
|
739
|
-
|
|
1537
|
+
if (c.req.query("windows") === "all") {
|
|
1538
|
+
return c.json({ ok: true, data: costAggregator.getAllSnapshots() });
|
|
1539
|
+
}
|
|
1540
|
+
const window = parseWindowParam(c.req.query("window"));
|
|
1541
|
+
return c.json({ ok: true, data: costAggregator.getSnapshot(window) });
|
|
740
1542
|
});
|
|
741
1543
|
app6.post("/costs/reset", (c) => {
|
|
742
|
-
|
|
743
|
-
|
|
1544
|
+
return c.json(
|
|
1545
|
+
{
|
|
1546
|
+
ok: false,
|
|
1547
|
+
error: {
|
|
1548
|
+
code: "GONE",
|
|
1549
|
+
message: "POST /api/costs/reset was removed in @axlsdk/studio 0.15. Cost aggregates are now time-windowed and rebuilt from StateStore history. Use GET /api/costs?window=24h|7d|30d|all to narrow the view instead of resetting."
|
|
1550
|
+
}
|
|
1551
|
+
},
|
|
1552
|
+
410
|
|
1553
|
+
);
|
|
744
1554
|
});
|
|
745
1555
|
return app6;
|
|
746
1556
|
}
|
|
@@ -748,8 +1558,9 @@ function createCostRoutes(costAggregator) {
|
|
|
748
1558
|
// src/server/routes/evals.ts
|
|
749
1559
|
var import_node_crypto = require("crypto");
|
|
750
1560
|
var import_hono10 = require("hono");
|
|
751
|
-
function createEvalRoutes(evalLoader) {
|
|
1561
|
+
function createEvalRoutes(connMgr, evalLoader) {
|
|
752
1562
|
const app6 = new import_hono10.Hono();
|
|
1563
|
+
const activeRuns = /* @__PURE__ */ new Map();
|
|
753
1564
|
app6.get("/evals", async (c) => {
|
|
754
1565
|
if (evalLoader) await evalLoader();
|
|
755
1566
|
const runtime = c.get("runtime");
|
|
@@ -759,7 +1570,10 @@ function createEvalRoutes(evalLoader) {
|
|
|
759
1570
|
app6.get("/evals/history", async (c) => {
|
|
760
1571
|
const runtime = c.get("runtime");
|
|
761
1572
|
const history = await runtime.getEvalHistory();
|
|
762
|
-
return c.json({
|
|
1573
|
+
return c.json({
|
|
1574
|
+
ok: true,
|
|
1575
|
+
data: redactEvalHistoryList(history, runtime.isRedactEnabled())
|
|
1576
|
+
});
|
|
763
1577
|
});
|
|
764
1578
|
app6.delete("/evals/history/:id", async (c) => {
|
|
765
1579
|
const runtime = c.get("runtime");
|
|
@@ -780,6 +1594,7 @@ function createEvalRoutes(evalLoader) {
|
|
|
780
1594
|
if (evalLoader) await evalLoader();
|
|
781
1595
|
const runtime = c.get("runtime");
|
|
782
1596
|
const name = c.req.param("name");
|
|
1597
|
+
const redactOn = runtime.isRedactEnabled();
|
|
783
1598
|
const entry = runtime.getRegisteredEval(name);
|
|
784
1599
|
if (!entry) {
|
|
785
1600
|
return c.json(
|
|
@@ -788,13 +1603,89 @@ function createEvalRoutes(evalLoader) {
|
|
|
788
1603
|
);
|
|
789
1604
|
}
|
|
790
1605
|
let runs = 1;
|
|
1606
|
+
let stream = false;
|
|
1607
|
+
let captureTraces = false;
|
|
791
1608
|
try {
|
|
792
1609
|
const body = await c.req.json().catch(() => ({}));
|
|
793
1610
|
if (typeof body.runs === "number" && Number.isFinite(body.runs) && body.runs > 1) {
|
|
794
1611
|
runs = Math.min(Math.floor(body.runs), 25);
|
|
795
1612
|
}
|
|
1613
|
+
if (body.stream === true) {
|
|
1614
|
+
stream = true;
|
|
1615
|
+
}
|
|
1616
|
+
if (body.captureTraces === true) {
|
|
1617
|
+
captureTraces = true;
|
|
1618
|
+
}
|
|
796
1619
|
} catch {
|
|
797
1620
|
}
|
|
1621
|
+
if (stream) {
|
|
1622
|
+
const evalRunId = `eval-${(0, import_node_crypto.randomUUID)()}`;
|
|
1623
|
+
const ac = new AbortController();
|
|
1624
|
+
activeRuns.set(evalRunId, ac);
|
|
1625
|
+
(async () => {
|
|
1626
|
+
try {
|
|
1627
|
+
if (runs > 1) {
|
|
1628
|
+
const runGroupId = (0, import_node_crypto.randomUUID)();
|
|
1629
|
+
const results = [];
|
|
1630
|
+
for (let r = 0; r < runs; r++) {
|
|
1631
|
+
if (ac.signal.aborted) break;
|
|
1632
|
+
const result = await runtime.runRegisteredEval(name, {
|
|
1633
|
+
metadata: { runGroupId, runIndex: r },
|
|
1634
|
+
signal: ac.signal,
|
|
1635
|
+
captureTraces,
|
|
1636
|
+
onProgress: (event) => {
|
|
1637
|
+
if (event.type === "run_done") return;
|
|
1638
|
+
connMgr.broadcastWithWildcard(`eval:${evalRunId}`, {
|
|
1639
|
+
...event,
|
|
1640
|
+
run: r + 1,
|
|
1641
|
+
totalRuns: runs
|
|
1642
|
+
});
|
|
1643
|
+
}
|
|
1644
|
+
});
|
|
1645
|
+
results.push(result);
|
|
1646
|
+
connMgr.broadcastWithWildcard(`eval:${evalRunId}`, {
|
|
1647
|
+
type: "run_done",
|
|
1648
|
+
run: r + 1,
|
|
1649
|
+
totalRuns: runs
|
|
1650
|
+
});
|
|
1651
|
+
}
|
|
1652
|
+
if (results.length > 0) {
|
|
1653
|
+
connMgr.broadcastWithWildcard(`eval:${evalRunId}`, {
|
|
1654
|
+
type: "done",
|
|
1655
|
+
evalResultId: results[0].id,
|
|
1656
|
+
runGroupId
|
|
1657
|
+
});
|
|
1658
|
+
} else {
|
|
1659
|
+
connMgr.broadcastWithWildcard(`eval:${evalRunId}`, {
|
|
1660
|
+
type: "error",
|
|
1661
|
+
message: "All runs were cancelled"
|
|
1662
|
+
});
|
|
1663
|
+
}
|
|
1664
|
+
} else {
|
|
1665
|
+
const result = await runtime.runRegisteredEval(name, {
|
|
1666
|
+
signal: ac.signal,
|
|
1667
|
+
captureTraces,
|
|
1668
|
+
onProgress: (event) => {
|
|
1669
|
+
if (event.type === "run_done") return;
|
|
1670
|
+
connMgr.broadcastWithWildcard(`eval:${evalRunId}`, event);
|
|
1671
|
+
}
|
|
1672
|
+
});
|
|
1673
|
+
connMgr.broadcastWithWildcard(`eval:${evalRunId}`, {
|
|
1674
|
+
type: "done",
|
|
1675
|
+
evalResultId: result.id
|
|
1676
|
+
});
|
|
1677
|
+
}
|
|
1678
|
+
} catch (err) {
|
|
1679
|
+
connMgr.broadcastWithWildcard(`eval:${evalRunId}`, {
|
|
1680
|
+
type: "error",
|
|
1681
|
+
message: redactErrorMessage(err, redactOn)
|
|
1682
|
+
});
|
|
1683
|
+
} finally {
|
|
1684
|
+
activeRuns.delete(evalRunId);
|
|
1685
|
+
}
|
|
1686
|
+
})();
|
|
1687
|
+
return c.json({ ok: true, data: { evalRunId } });
|
|
1688
|
+
}
|
|
798
1689
|
try {
|
|
799
1690
|
if (runs > 1) {
|
|
800
1691
|
const { aggregateRuns } = await import("@axlsdk/eval");
|
|
@@ -802,27 +1693,53 @@ function createEvalRoutes(evalLoader) {
|
|
|
802
1693
|
const results = [];
|
|
803
1694
|
for (let r = 0; r < runs; r++) {
|
|
804
1695
|
const result2 = await runtime.runRegisteredEval(name, {
|
|
805
|
-
metadata: { runGroupId, runIndex: r }
|
|
1696
|
+
metadata: { runGroupId, runIndex: r },
|
|
1697
|
+
captureTraces
|
|
806
1698
|
});
|
|
807
1699
|
results.push(result2);
|
|
808
1700
|
}
|
|
809
1701
|
const typedResults = results;
|
|
810
1702
|
const aggregate = aggregateRuns(typedResults);
|
|
811
1703
|
const first = typedResults[0];
|
|
812
|
-
const result = {
|
|
813
|
-
|
|
1704
|
+
const result = {
|
|
1705
|
+
...first,
|
|
1706
|
+
_multiRun: { aggregate, allRuns: typedResults }
|
|
1707
|
+
};
|
|
1708
|
+
return c.json({
|
|
1709
|
+
ok: true,
|
|
1710
|
+
data: redactEvalResult(result, redactOn)
|
|
1711
|
+
});
|
|
814
1712
|
} else {
|
|
815
|
-
const result = await runtime.runRegisteredEval(name);
|
|
816
|
-
return c.json({
|
|
1713
|
+
const result = await runtime.runRegisteredEval(name, { captureTraces });
|
|
1714
|
+
return c.json({
|
|
1715
|
+
ok: true,
|
|
1716
|
+
data: redactEvalResult(result, redactOn)
|
|
1717
|
+
});
|
|
817
1718
|
}
|
|
818
1719
|
} catch (err) {
|
|
819
|
-
|
|
820
|
-
|
|
1720
|
+
return c.json(
|
|
1721
|
+
{ ok: false, error: { code: "EVAL_ERROR", message: redactErrorMessage(err, redactOn) } },
|
|
1722
|
+
400
|
|
1723
|
+
);
|
|
1724
|
+
}
|
|
1725
|
+
});
|
|
1726
|
+
app6.post("/evals/runs/:evalRunId/cancel", (c) => {
|
|
1727
|
+
const evalRunId = c.req.param("evalRunId");
|
|
1728
|
+
const ac = activeRuns.get(evalRunId);
|
|
1729
|
+
if (!ac) {
|
|
1730
|
+
return c.json(
|
|
1731
|
+
{ ok: false, error: { code: "NOT_FOUND", message: "No active eval run found" } },
|
|
1732
|
+
404
|
|
1733
|
+
);
|
|
821
1734
|
}
|
|
1735
|
+
ac.abort();
|
|
1736
|
+
activeRuns.delete(evalRunId);
|
|
1737
|
+
return c.json({ ok: true, data: { cancelled: true } });
|
|
822
1738
|
});
|
|
823
1739
|
app6.post("/evals/:name/rescore", async (c) => {
|
|
824
1740
|
if (evalLoader) await evalLoader();
|
|
825
1741
|
const runtime = c.get("runtime");
|
|
1742
|
+
const redactOn = runtime.isRedactEnabled();
|
|
826
1743
|
const name = c.req.param("name");
|
|
827
1744
|
const body = await c.req.json();
|
|
828
1745
|
if (!body.resultId || typeof body.resultId !== "string") {
|
|
@@ -860,19 +1777,29 @@ function createEvalRoutes(evalLoader) {
|
|
|
860
1777
|
timestamp: Date.now(),
|
|
861
1778
|
data: result
|
|
862
1779
|
});
|
|
863
|
-
return c.json({
|
|
1780
|
+
return c.json({
|
|
1781
|
+
ok: true,
|
|
1782
|
+
data: redactEvalResult(result, redactOn)
|
|
1783
|
+
});
|
|
864
1784
|
} catch (err) {
|
|
865
|
-
|
|
866
|
-
|
|
1785
|
+
return c.json(
|
|
1786
|
+
{ ok: false, error: { code: "EVAL_ERROR", message: redactErrorMessage(err, redactOn) } },
|
|
1787
|
+
400
|
|
1788
|
+
);
|
|
867
1789
|
}
|
|
868
1790
|
});
|
|
869
1791
|
app6.post("/evals/compare", async (c) => {
|
|
870
1792
|
const runtime = c.get("runtime");
|
|
1793
|
+
const redactOn = runtime.isRedactEnabled();
|
|
871
1794
|
const body = await c.req.json();
|
|
1795
|
+
const MAX_POOLED_RUNS = 25;
|
|
872
1796
|
const validateIdParam = (v, name) => {
|
|
873
1797
|
if (typeof v === "string") return v === "" ? `${name} must be non-empty` : null;
|
|
874
1798
|
if (Array.isArray(v)) {
|
|
875
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
|
+
}
|
|
876
1803
|
for (const elem of v) {
|
|
877
1804
|
if (typeof elem !== "string" || elem === "") {
|
|
878
1805
|
return `${name} array must contain only non-empty strings`;
|
|
@@ -935,8 +1862,13 @@ function createEvalRoutes(evalLoader) {
|
|
|
935
1862
|
const result = await runtime.evalCompare(baseline, candidate, body.options);
|
|
936
1863
|
return c.json({ ok: true, data: result });
|
|
937
1864
|
} catch (err) {
|
|
938
|
-
|
|
939
|
-
|
|
1865
|
+
return c.json(
|
|
1866
|
+
{
|
|
1867
|
+
ok: false,
|
|
1868
|
+
error: { code: "COMPARE_FAILED", message: redactErrorMessage(err, redactOn) }
|
|
1869
|
+
},
|
|
1870
|
+
400
|
|
1871
|
+
);
|
|
940
1872
|
}
|
|
941
1873
|
});
|
|
942
1874
|
app6.post("/evals/import", async (c) => {
|
|
@@ -998,7 +1930,11 @@ function createEvalRoutes(evalLoader) {
|
|
|
998
1930
|
});
|
|
999
1931
|
return c.json({ ok: true, data: { id, eval: evalName, timestamp } });
|
|
1000
1932
|
});
|
|
1001
|
-
|
|
1933
|
+
function closeActiveRuns() {
|
|
1934
|
+
for (const ac of activeRuns.values()) ac.abort();
|
|
1935
|
+
activeRuns.clear();
|
|
1936
|
+
}
|
|
1937
|
+
return { app: app6, closeActiveRuns };
|
|
1002
1938
|
}
|
|
1003
1939
|
|
|
1004
1940
|
// src/server/routes/playground.ts
|
|
@@ -1032,34 +1968,50 @@ function createPlaygroundRoutes(connMgr) {
|
|
|
1032
1968
|
);
|
|
1033
1969
|
}
|
|
1034
1970
|
const sessionId = body.sessionId ?? `playground-${Date.now()}`;
|
|
1035
|
-
const executionId = `playground-${sessionId}-${Date.now()}`;
|
|
1036
1971
|
const store = runtime.getStateStore();
|
|
1037
1972
|
const history = await store.getSession(sessionId);
|
|
1038
1973
|
history.push({ role: "user", content: body.message });
|
|
1039
|
-
const
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
});
|
|
1974
|
+
const redactOn = runtime.isRedactEnabled();
|
|
1975
|
+
const ctx = runtime.createContext({ sessionHistory: history });
|
|
1976
|
+
const executionId = ctx.executionId;
|
|
1977
|
+
const traceListener = (event) => {
|
|
1978
|
+
if (event.executionId !== executionId) return;
|
|
1979
|
+
connMgr.broadcastWithWildcard(`execution:${executionId}`, redactStreamEvent(event, redactOn));
|
|
1980
|
+
};
|
|
1981
|
+
runtime.on("trace", traceListener);
|
|
1048
1982
|
(async () => {
|
|
1983
|
+
let stepCounter = Number.MAX_SAFE_INTEGER - 1;
|
|
1984
|
+
const terminalFields = () => ({
|
|
1985
|
+
executionId,
|
|
1986
|
+
step: stepCounter++,
|
|
1987
|
+
timestamp: Date.now()
|
|
1988
|
+
});
|
|
1049
1989
|
try {
|
|
1050
1990
|
const result = await ctx.ask(agent, body.message);
|
|
1051
1991
|
const resultText = typeof result === "string" ? result : JSON.stringify(result);
|
|
1052
1992
|
history.push({ role: "assistant", content: resultText });
|
|
1053
1993
|
await store.saveSession(sessionId, history);
|
|
1054
|
-
|
|
1994
|
+
const doneEvent = {
|
|
1995
|
+
...terminalFields(),
|
|
1055
1996
|
type: "done",
|
|
1056
|
-
data: resultText
|
|
1057
|
-
}
|
|
1997
|
+
data: { result: resultText }
|
|
1998
|
+
};
|
|
1999
|
+
connMgr.broadcastWithWildcard(
|
|
2000
|
+
`execution:${executionId}`,
|
|
2001
|
+
redactStreamEvent(doneEvent, redactOn)
|
|
2002
|
+
);
|
|
1058
2003
|
} catch (err) {
|
|
1059
|
-
|
|
2004
|
+
const errorEvent = {
|
|
2005
|
+
...terminalFields(),
|
|
1060
2006
|
type: "error",
|
|
1061
|
-
message: err instanceof Error ? err.message : String(err)
|
|
1062
|
-
}
|
|
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);
|
|
1063
2015
|
}
|
|
1064
2016
|
})();
|
|
1065
2017
|
return c.json({
|
|
@@ -1070,12 +2022,78 @@ function createPlaygroundRoutes(connMgr) {
|
|
|
1070
2022
|
return app6;
|
|
1071
2023
|
}
|
|
1072
2024
|
|
|
2025
|
+
// src/server/routes/eval-trends.ts
|
|
2026
|
+
var import_hono12 = require("hono");
|
|
2027
|
+
function createEvalTrendsRoutes(aggregator) {
|
|
2028
|
+
const app6 = new import_hono12.Hono();
|
|
2029
|
+
app6.get("/eval-trends", (c) => {
|
|
2030
|
+
const window = parseWindowParam(c.req.query("window"));
|
|
2031
|
+
return c.json({ ok: true, data: aggregator.getSnapshot(window) });
|
|
2032
|
+
});
|
|
2033
|
+
return app6;
|
|
2034
|
+
}
|
|
2035
|
+
|
|
2036
|
+
// src/server/routes/workflow-stats.ts
|
|
2037
|
+
var import_hono13 = require("hono");
|
|
2038
|
+
function createWorkflowStatsRoutes(aggregator) {
|
|
2039
|
+
const app6 = new import_hono13.Hono();
|
|
2040
|
+
app6.get("/workflow-stats", (c) => {
|
|
2041
|
+
const window = parseWindowParam(c.req.query("window"));
|
|
2042
|
+
return c.json({ ok: true, data: enrichWorkflowStats(aggregator.getSnapshot(window)) });
|
|
2043
|
+
});
|
|
2044
|
+
return app6;
|
|
2045
|
+
}
|
|
2046
|
+
|
|
2047
|
+
// src/server/routes/trace-stats.ts
|
|
2048
|
+
var import_hono14 = require("hono");
|
|
2049
|
+
function createTraceStatsRoutes(aggregator) {
|
|
2050
|
+
const app6 = new import_hono14.Hono();
|
|
2051
|
+
app6.get("/trace-stats", (c) => {
|
|
2052
|
+
const window = parseWindowParam(c.req.query("window"));
|
|
2053
|
+
return c.json({ ok: true, data: aggregator.getSnapshot(window) });
|
|
2054
|
+
});
|
|
2055
|
+
return app6;
|
|
2056
|
+
}
|
|
2057
|
+
|
|
1073
2058
|
// src/server/index.ts
|
|
1074
2059
|
function createServer(options) {
|
|
1075
2060
|
const { runtime, staticRoot, basePath = "", readOnly = false } = options;
|
|
1076
|
-
const app6 = new
|
|
1077
|
-
const connMgr = new ConnectionManager();
|
|
1078
|
-
const
|
|
2061
|
+
const app6 = new import_hono15.Hono();
|
|
2062
|
+
const connMgr = new ConnectionManager(options.bufferCaps);
|
|
2063
|
+
const windows = ["24h", "7d", "30d", "all"];
|
|
2064
|
+
const costAggregator = new TraceAggregator({
|
|
2065
|
+
runtime,
|
|
2066
|
+
connMgr,
|
|
2067
|
+
channel: "costs",
|
|
2068
|
+
reducer: reduceCost,
|
|
2069
|
+
emptyState: emptyCostData,
|
|
2070
|
+
windows
|
|
2071
|
+
});
|
|
2072
|
+
const workflowStatsAggregator = new ExecutionAggregator({
|
|
2073
|
+
runtime,
|
|
2074
|
+
connMgr,
|
|
2075
|
+
channel: "workflow-stats",
|
|
2076
|
+
reducer: reduceWorkflowStats,
|
|
2077
|
+
emptyState: emptyWorkflowStatsData,
|
|
2078
|
+
windows,
|
|
2079
|
+
broadcastTransform: enrichWorkflowStats
|
|
2080
|
+
});
|
|
2081
|
+
const traceStatsAggregator = new TraceAggregator({
|
|
2082
|
+
runtime,
|
|
2083
|
+
connMgr,
|
|
2084
|
+
channel: "trace-stats",
|
|
2085
|
+
reducer: reduceTraceStats,
|
|
2086
|
+
emptyState: emptyTraceStatsData,
|
|
2087
|
+
windows
|
|
2088
|
+
});
|
|
2089
|
+
const evalTrendsAggregator = new EvalAggregator({
|
|
2090
|
+
runtime,
|
|
2091
|
+
connMgr,
|
|
2092
|
+
channel: "eval-trends",
|
|
2093
|
+
reducer: reduceEvalTrends,
|
|
2094
|
+
emptyState: emptyEvalTrendData,
|
|
2095
|
+
windows
|
|
2096
|
+
});
|
|
1079
2097
|
if (options.cors !== false) {
|
|
1080
2098
|
app6.use("*", (0, import_cors.cors)());
|
|
1081
2099
|
}
|
|
@@ -1093,11 +2111,11 @@ function createServer(options) {
|
|
|
1093
2111
|
/^PUT \/api\/memory(\/|$)/,
|
|
1094
2112
|
/^DELETE \/api\/memory(\/|$)/,
|
|
1095
2113
|
/^POST \/api\/decisions(\/|$)/,
|
|
1096
|
-
/^POST \/api\/costs(\/|$)/,
|
|
1097
2114
|
/^POST \/api\/tools(\/|$)/,
|
|
1098
2115
|
/^POST \/api\/evals\/import$/,
|
|
1099
2116
|
/^POST \/api\/evals\/[^/]+\/run$/,
|
|
1100
2117
|
/^POST \/api\/evals\/[^/]+\/rescore$/,
|
|
2118
|
+
/^POST \/api\/evals\/runs\/[^/]+\/cancel$/,
|
|
1101
2119
|
/^DELETE \/api\/evals\/history\/[^/]+$/,
|
|
1102
2120
|
/^POST \/api\/playground(\/|$)/
|
|
1103
2121
|
];
|
|
@@ -1117,7 +2135,7 @@ function createServer(options) {
|
|
|
1117
2135
|
await next();
|
|
1118
2136
|
});
|
|
1119
2137
|
}
|
|
1120
|
-
const api = new
|
|
2138
|
+
const api = new import_hono15.Hono();
|
|
1121
2139
|
api.route("/", createHealthRoutes(readOnly));
|
|
1122
2140
|
api.route("/", createWorkflowRoutes(connMgr));
|
|
1123
2141
|
api.route("/", executions_default);
|
|
@@ -1127,20 +2145,37 @@ function createServer(options) {
|
|
|
1127
2145
|
api.route("/", memory_default);
|
|
1128
2146
|
api.route("/", decisions_default);
|
|
1129
2147
|
api.route("/", createCostRoutes(costAggregator));
|
|
1130
|
-
api.route("/",
|
|
2148
|
+
api.route("/", createEvalTrendsRoutes(evalTrendsAggregator));
|
|
2149
|
+
api.route("/", createWorkflowStatsRoutes(workflowStatsAggregator));
|
|
2150
|
+
api.route("/", createTraceStatsRoutes(traceStatsAggregator));
|
|
2151
|
+
const { app: evalApp, closeActiveRuns } = createEvalRoutes(connMgr, options.evalLoader);
|
|
2152
|
+
api.route("/", evalApp);
|
|
1131
2153
|
api.route("/", createPlaygroundRoutes(connMgr));
|
|
1132
2154
|
app6.route("/api", api);
|
|
1133
2155
|
const traceListener = (event) => {
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
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
|
+
);
|
|
1141
2170
|
}
|
|
1142
2171
|
};
|
|
1143
2172
|
runtime.on("trace", traceListener);
|
|
2173
|
+
const aggregatorStartPromise = Promise.all([
|
|
2174
|
+
costAggregator.start(),
|
|
2175
|
+
workflowStatsAggregator.start(),
|
|
2176
|
+
traceStatsAggregator.start(),
|
|
2177
|
+
evalTrendsAggregator.start()
|
|
2178
|
+
]).catch((err) => console.error("[axl-studio] aggregator start failed:", err));
|
|
1144
2179
|
if (staticRoot) {
|
|
1145
2180
|
const indexPath = (0, import_node_path.resolve)(staticRoot, "index.html");
|
|
1146
2181
|
let spaHtml;
|
|
@@ -1190,9 +2225,22 @@ function createServer(options) {
|
|
|
1190
2225
|
app: app6,
|
|
1191
2226
|
connMgr,
|
|
1192
2227
|
costAggregator,
|
|
2228
|
+
workflowStatsAggregator,
|
|
2229
|
+
traceStatsAggregator,
|
|
2230
|
+
evalTrendsAggregator,
|
|
2231
|
+
aggregatorStartPromise,
|
|
1193
2232
|
/** Create WS handlers. Call before registering static/SPA routes are reached. */
|
|
1194
2233
|
createWsHandlers: () => createWsHandlers(connMgr),
|
|
1195
|
-
traceListener
|
|
2234
|
+
traceListener,
|
|
2235
|
+
/** Abort all active streaming eval runs. */
|
|
2236
|
+
closeActiveRuns,
|
|
2237
|
+
/** Close all aggregators (clear intervals and unsubscribe listeners). */
|
|
2238
|
+
closeAggregators: () => {
|
|
2239
|
+
costAggregator.close();
|
|
2240
|
+
workflowStatsAggregator.close();
|
|
2241
|
+
traceStatsAggregator.close();
|
|
2242
|
+
evalTrendsAggregator.close();
|
|
2243
|
+
}
|
|
1196
2244
|
};
|
|
1197
2245
|
}
|
|
1198
2246
|
|
|
@@ -1374,7 +2422,13 @@ async function registerConditions(conditions) {
|
|
|
1374
2422
|
// src/middleware.ts
|
|
1375
2423
|
var import_meta2 = {};
|
|
1376
2424
|
function createStudioMiddleware(options) {
|
|
1377
|
-
const {
|
|
2425
|
+
const {
|
|
2426
|
+
runtime,
|
|
2427
|
+
serveClient = true,
|
|
2428
|
+
verifyUpgrade,
|
|
2429
|
+
readOnly = false,
|
|
2430
|
+
filterTraceEvent
|
|
2431
|
+
} = options;
|
|
1378
2432
|
const basePath = normalizeBasePath(options.basePath);
|
|
1379
2433
|
const staticRoot = serveClient ? resolveClientDist() : void 0;
|
|
1380
2434
|
if (serveClient && !staticRoot) {
|
|
@@ -1384,15 +2438,19 @@ function createStudioMiddleware(options) {
|
|
|
1384
2438
|
);
|
|
1385
2439
|
}
|
|
1386
2440
|
const evalLoader = options.evals ? createEvalLoader(options.evals, runtime) : void 0;
|
|
1387
|
-
const { app: app6, connMgr, traceListener } = createServer({
|
|
2441
|
+
const { app: app6, connMgr, traceListener, closeActiveRuns, closeAggregators } = createServer({
|
|
1388
2442
|
runtime,
|
|
1389
2443
|
staticRoot,
|
|
1390
2444
|
basePath,
|
|
1391
2445
|
readOnly,
|
|
1392
2446
|
cors: false,
|
|
1393
2447
|
// Host framework owns CORS policy
|
|
1394
|
-
evalLoader
|
|
2448
|
+
evalLoader,
|
|
2449
|
+
bufferCaps: options.bufferCaps
|
|
1395
2450
|
});
|
|
2451
|
+
if (filterTraceEvent) {
|
|
2452
|
+
connMgr.setFilter(filterTraceEvent);
|
|
2453
|
+
}
|
|
1396
2454
|
if (process.env.NODE_ENV === "production" && !verifyUpgrade) {
|
|
1397
2455
|
console.warn(
|
|
1398
2456
|
"[axl-studio] WARNING: Studio middleware mounted in production without verifyUpgrade. WebSocket connections are not authenticated. All registered workflows, tools, and agents are accessible. See https://axlsdk.com/docs/studio/security"
|
|
@@ -1433,7 +2491,7 @@ function createStudioMiddleware(options) {
|
|
|
1433
2491
|
}
|
|
1434
2492
|
});
|
|
1435
2493
|
}
|
|
1436
|
-
function handleWebSocket(ws) {
|
|
2494
|
+
function handleWebSocket(ws, metadata) {
|
|
1437
2495
|
if (closed) {
|
|
1438
2496
|
ws.close();
|
|
1439
2497
|
return;
|
|
@@ -1443,6 +2501,9 @@ function createStudioMiddleware(options) {
|
|
|
1443
2501
|
close: () => ws.close()
|
|
1444
2502
|
};
|
|
1445
2503
|
connMgr.add(socket);
|
|
2504
|
+
if (metadata !== void 0) {
|
|
2505
|
+
connMgr.setMetadata(socket, metadata);
|
|
2506
|
+
}
|
|
1446
2507
|
ws.on("message", (raw) => {
|
|
1447
2508
|
const reply = handleWsMessage(String(raw), socket, connMgr);
|
|
1448
2509
|
if (reply) ws.send(reply);
|
|
@@ -1465,32 +2526,42 @@ function createStudioMiddleware(options) {
|
|
|
1465
2526
|
upgradeHandler = async (req, socket, head) => {
|
|
1466
2527
|
const pathname = new URL(req.url, `http://${req.headers.host}`).pathname;
|
|
1467
2528
|
if (pathname !== wsPath) return;
|
|
2529
|
+
if (closed) {
|
|
2530
|
+
socket.destroy();
|
|
2531
|
+
return;
|
|
2532
|
+
}
|
|
2533
|
+
let connectionMetadata;
|
|
1468
2534
|
if (verifyUpgrade) {
|
|
1469
2535
|
try {
|
|
1470
|
-
const
|
|
2536
|
+
const result = await verifyUpgrade(req);
|
|
2537
|
+
const allowed = typeof result === "boolean" ? result : result.allowed;
|
|
1471
2538
|
if (!allowed) {
|
|
1472
2539
|
socket.write("HTTP/1.1 401 Unauthorized\r\n\r\n");
|
|
1473
2540
|
socket.destroy();
|
|
1474
2541
|
return;
|
|
1475
2542
|
}
|
|
2543
|
+
if (typeof result === "object" && result !== null) {
|
|
2544
|
+
connectionMetadata = result.metadata;
|
|
2545
|
+
}
|
|
1476
2546
|
} catch {
|
|
1477
2547
|
socket.write("HTTP/1.1 403 Forbidden\r\n\r\n");
|
|
1478
2548
|
socket.destroy();
|
|
1479
2549
|
return;
|
|
1480
2550
|
}
|
|
1481
2551
|
}
|
|
1482
|
-
if (!wss) {
|
|
2552
|
+
if (closed || !wss) {
|
|
1483
2553
|
socket.destroy();
|
|
1484
2554
|
return;
|
|
1485
2555
|
}
|
|
1486
2556
|
wss.handleUpgrade(req, socket, head, (ws) => {
|
|
1487
|
-
handleWebSocket(ws);
|
|
2557
|
+
handleWebSocket(ws, connectionMetadata);
|
|
1488
2558
|
});
|
|
1489
2559
|
};
|
|
1490
2560
|
server.on("upgrade", upgradeHandler);
|
|
1491
2561
|
}
|
|
1492
2562
|
function close() {
|
|
1493
2563
|
closed = true;
|
|
2564
|
+
closeActiveRuns();
|
|
1494
2565
|
connMgr.closeAll();
|
|
1495
2566
|
if (upgradeHandler && serverRef) {
|
|
1496
2567
|
serverRef.removeListener("upgrade", upgradeHandler);
|
|
@@ -1504,6 +2575,7 @@ function createStudioMiddleware(options) {
|
|
|
1504
2575
|
if (traceListener) {
|
|
1505
2576
|
runtime.removeListener("trace", traceListener);
|
|
1506
2577
|
}
|
|
2578
|
+
closeAggregators();
|
|
1507
2579
|
}
|
|
1508
2580
|
return {
|
|
1509
2581
|
handler,
|