@axlsdk/studio 0.9.1 → 0.10.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 +174 -7
- package/dist/{chunk-EG74VI3M.js → chunk-RBTYI3TW.js} +142 -41
- package/dist/chunk-RBTYI3TW.js.map +1 -0
- package/dist/cli.cjs +152 -52
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +1 -1
- package/dist/client/assets/{index-DDlRZgfC.js → index-jeUeToM_.js} +13 -13
- package/dist/client/index.html +2 -2
- package/dist/connection-manager-DbOgO_gK.d.cts +75 -0
- package/dist/connection-manager-DbOgO_gK.d.ts +75 -0
- package/dist/middleware.cjs +1058 -0
- package/dist/middleware.cjs.map +1 -0
- package/dist/middleware.d.cts +76 -0
- package/dist/middleware.d.ts +76 -0
- package/dist/middleware.js +173 -0
- package/dist/middleware.js.map +1 -0
- package/dist/server/index.cjs +140 -40
- package/dist/server/index.cjs.map +1 -1
- package/dist/server/index.d.cts +10 -62
- package/dist/server/index.d.ts +10 -62
- package/dist/server/index.js +1 -1
- package/package.json +16 -4
- package/dist/chunk-EG74VI3M.js.map +0 -1
|
@@ -0,0 +1,1058 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/middleware.ts
|
|
21
|
+
var middleware_exports = {};
|
|
22
|
+
__export(middleware_exports, {
|
|
23
|
+
createStudioMiddleware: () => createStudioMiddleware,
|
|
24
|
+
handleWsMessage: () => handleWsMessage
|
|
25
|
+
});
|
|
26
|
+
module.exports = __toCommonJS(middleware_exports);
|
|
27
|
+
var import_node_path2 = require("path");
|
|
28
|
+
var import_node_fs2 = require("fs");
|
|
29
|
+
var import_node_url = require("url");
|
|
30
|
+
var import_node_server = require("@hono/node-server");
|
|
31
|
+
var import_ws = require("ws");
|
|
32
|
+
|
|
33
|
+
// src/server/index.ts
|
|
34
|
+
var import_node_fs = require("fs");
|
|
35
|
+
var import_node_path = require("path");
|
|
36
|
+
var import_hono12 = require("hono");
|
|
37
|
+
var import_cors = require("hono/cors");
|
|
38
|
+
var import_serve_static = require("@hono/node-server/serve-static");
|
|
39
|
+
|
|
40
|
+
// src/server/middleware/error-handler.ts
|
|
41
|
+
async function errorHandler(c, next) {
|
|
42
|
+
try {
|
|
43
|
+
await next();
|
|
44
|
+
} catch (err) {
|
|
45
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
46
|
+
const code = err.code ?? "INTERNAL_ERROR";
|
|
47
|
+
let status = 500;
|
|
48
|
+
if ("status" in err) {
|
|
49
|
+
const errStatus = err.status;
|
|
50
|
+
if (typeof errStatus === "number" && errStatus >= 400 && errStatus < 600) {
|
|
51
|
+
status = errStatus;
|
|
52
|
+
}
|
|
53
|
+
} else if (code === "NOT_FOUND" || message.includes("not found") || message.includes("not registered")) {
|
|
54
|
+
status = 404;
|
|
55
|
+
} else if (code === "VALIDATION_ERROR" || message.includes("Expected") || message.includes("invalid")) {
|
|
56
|
+
status = 400;
|
|
57
|
+
}
|
|
58
|
+
const body = {
|
|
59
|
+
ok: false,
|
|
60
|
+
error: { code, message }
|
|
61
|
+
};
|
|
62
|
+
return c.json(body, status);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// src/server/ws/connection-manager.ts
|
|
67
|
+
var ConnectionManager = class {
|
|
68
|
+
/** channel -> set of WS connections */
|
|
69
|
+
channels = /* @__PURE__ */ new Map();
|
|
70
|
+
/** ws -> set of subscribed channels (for cleanup) */
|
|
71
|
+
connections = /* @__PURE__ */ new Map();
|
|
72
|
+
maxConnections = 100;
|
|
73
|
+
/** Register a new WS connection. */
|
|
74
|
+
add(ws) {
|
|
75
|
+
if (this.connections.size >= this.maxConnections) {
|
|
76
|
+
ws.close?.();
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
this.connections.set(ws, /* @__PURE__ */ new Set());
|
|
80
|
+
}
|
|
81
|
+
/** Remove a WS connection and all its subscriptions. */
|
|
82
|
+
remove(ws) {
|
|
83
|
+
const channels = this.connections.get(ws);
|
|
84
|
+
if (channels) {
|
|
85
|
+
for (const ch of channels) {
|
|
86
|
+
this.channels.get(ch)?.delete(ws);
|
|
87
|
+
if (this.channels.get(ch)?.size === 0) {
|
|
88
|
+
this.channels.delete(ch);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
this.connections.delete(ws);
|
|
93
|
+
}
|
|
94
|
+
/** Subscribe a connection to a channel. No-op if the connection was not added. */
|
|
95
|
+
subscribe(ws, channel) {
|
|
96
|
+
if (!this.connections.has(ws)) return;
|
|
97
|
+
let subs = this.channels.get(channel);
|
|
98
|
+
if (!subs) {
|
|
99
|
+
subs = /* @__PURE__ */ new Set();
|
|
100
|
+
this.channels.set(channel, subs);
|
|
101
|
+
}
|
|
102
|
+
subs.add(ws);
|
|
103
|
+
this.connections.get(ws).add(channel);
|
|
104
|
+
}
|
|
105
|
+
/** Unsubscribe a connection from a channel. */
|
|
106
|
+
unsubscribe(ws, channel) {
|
|
107
|
+
this.channels.get(channel)?.delete(ws);
|
|
108
|
+
if (this.channels.get(channel)?.size === 0) {
|
|
109
|
+
this.channels.delete(channel);
|
|
110
|
+
}
|
|
111
|
+
this.connections.get(ws)?.delete(channel);
|
|
112
|
+
}
|
|
113
|
+
/** Broadcast data to all subscribers of a channel. */
|
|
114
|
+
broadcast(channel, data) {
|
|
115
|
+
const subs = this.channels.get(channel);
|
|
116
|
+
if (!subs || subs.size === 0) return;
|
|
117
|
+
const msg = JSON.stringify({ type: "event", channel, data });
|
|
118
|
+
for (const ws of [...subs]) {
|
|
119
|
+
try {
|
|
120
|
+
ws.send(msg);
|
|
121
|
+
} catch {
|
|
122
|
+
this.remove(ws);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
/** Broadcast to channel and all wildcard subscribers (e.g., trace:* matches trace:abc). */
|
|
127
|
+
broadcastWithWildcard(channel, data) {
|
|
128
|
+
this.broadcast(channel, data);
|
|
129
|
+
const colonIdx = channel.indexOf(":");
|
|
130
|
+
if (colonIdx > 0) {
|
|
131
|
+
const wildcardChannel = channel.substring(0, colonIdx) + ":*";
|
|
132
|
+
const subs = this.channels.get(wildcardChannel);
|
|
133
|
+
if (!subs || subs.size === 0) return;
|
|
134
|
+
const msg = JSON.stringify({ type: "event", channel, data });
|
|
135
|
+
for (const ws of [...subs]) {
|
|
136
|
+
try {
|
|
137
|
+
ws.send(msg);
|
|
138
|
+
} catch {
|
|
139
|
+
this.remove(ws);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/** Close all connections and clear all state. Used during shutdown. */
|
|
145
|
+
closeAll() {
|
|
146
|
+
for (const ws of this.connections.keys()) {
|
|
147
|
+
ws.close?.();
|
|
148
|
+
}
|
|
149
|
+
this.connections.clear();
|
|
150
|
+
this.channels.clear();
|
|
151
|
+
}
|
|
152
|
+
/** Get the number of active connections. */
|
|
153
|
+
get connectionCount() {
|
|
154
|
+
return this.connections.size;
|
|
155
|
+
}
|
|
156
|
+
/** Check if any connections are subscribed to a channel. */
|
|
157
|
+
hasSubscribers(channel) {
|
|
158
|
+
return (this.channels.get(channel)?.size ?? 0) > 0;
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
// src/server/ws/protocol.ts
|
|
163
|
+
var VALID_CHANNEL_PREFIXES = ["execution:", "trace:"];
|
|
164
|
+
var VALID_EXACT_CHANNELS = ["costs", "decisions"];
|
|
165
|
+
var MAX_CHANNEL_LENGTH = 256;
|
|
166
|
+
function handleWsMessage(raw, socket, connMgr) {
|
|
167
|
+
if (raw.length > 65536) {
|
|
168
|
+
return JSON.stringify({ type: "error", message: "Message too large" });
|
|
169
|
+
}
|
|
170
|
+
let msg;
|
|
171
|
+
try {
|
|
172
|
+
msg = JSON.parse(raw);
|
|
173
|
+
} catch {
|
|
174
|
+
return JSON.stringify({ type: "error", message: "Invalid JSON" });
|
|
175
|
+
}
|
|
176
|
+
switch (msg.type) {
|
|
177
|
+
case "subscribe": {
|
|
178
|
+
const error = validateChannel(msg.channel);
|
|
179
|
+
if (error) return JSON.stringify({ type: "error", message: error });
|
|
180
|
+
connMgr.subscribe(socket, msg.channel);
|
|
181
|
+
return JSON.stringify({ type: "subscribed", channel: msg.channel });
|
|
182
|
+
}
|
|
183
|
+
case "unsubscribe": {
|
|
184
|
+
const error = validateChannel(msg.channel);
|
|
185
|
+
if (error) return JSON.stringify({ type: "error", message: error });
|
|
186
|
+
connMgr.unsubscribe(socket, msg.channel);
|
|
187
|
+
return JSON.stringify({ type: "unsubscribed", channel: msg.channel });
|
|
188
|
+
}
|
|
189
|
+
case "ping":
|
|
190
|
+
return JSON.stringify({ type: "pong" });
|
|
191
|
+
default:
|
|
192
|
+
return JSON.stringify({ type: "error", message: "Unknown message type" });
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
function validateChannel(channel) {
|
|
196
|
+
if (typeof channel !== "string" || !channel) {
|
|
197
|
+
return "Missing or invalid channel";
|
|
198
|
+
}
|
|
199
|
+
if (channel.length > MAX_CHANNEL_LENGTH) {
|
|
200
|
+
return `Channel name exceeds ${MAX_CHANNEL_LENGTH} characters`;
|
|
201
|
+
}
|
|
202
|
+
if (!VALID_EXACT_CHANNELS.includes(channel) && !VALID_CHANNEL_PREFIXES.some((p) => channel.startsWith(p))) {
|
|
203
|
+
return `Invalid channel: ${channel}`;
|
|
204
|
+
}
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// src/server/ws/handler.ts
|
|
209
|
+
function createWsHandlers(connMgr) {
|
|
210
|
+
return {
|
|
211
|
+
onOpen(_event, ws) {
|
|
212
|
+
connMgr.add(ws);
|
|
213
|
+
},
|
|
214
|
+
onMessage(event, ws) {
|
|
215
|
+
const reply = handleWsMessage(String(event.data), ws, connMgr);
|
|
216
|
+
if (reply) ws.send(reply);
|
|
217
|
+
},
|
|
218
|
+
onClose(_event, ws) {
|
|
219
|
+
connMgr.remove(ws);
|
|
220
|
+
},
|
|
221
|
+
onError(_event, ws) {
|
|
222
|
+
connMgr.remove(ws);
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// src/server/cost-aggregator.ts
|
|
228
|
+
var CostAggregator = class {
|
|
229
|
+
constructor(connMgr) {
|
|
230
|
+
this.connMgr = connMgr;
|
|
231
|
+
}
|
|
232
|
+
data = {
|
|
233
|
+
totalCost: 0,
|
|
234
|
+
totalTokens: { input: 0, output: 0, reasoning: 0 },
|
|
235
|
+
byAgent: {},
|
|
236
|
+
byModel: {},
|
|
237
|
+
byWorkflow: {}
|
|
238
|
+
};
|
|
239
|
+
/** Process a trace event and update cost data. */
|
|
240
|
+
onTrace(event) {
|
|
241
|
+
if (!event.cost && !event.tokens) return;
|
|
242
|
+
const cost = event.cost ?? 0;
|
|
243
|
+
const tokens = event.tokens ?? {};
|
|
244
|
+
this.data.totalCost += cost;
|
|
245
|
+
this.data.totalTokens.input += tokens.input ?? 0;
|
|
246
|
+
this.data.totalTokens.output += tokens.output ?? 0;
|
|
247
|
+
this.data.totalTokens.reasoning += tokens.reasoning ?? 0;
|
|
248
|
+
if (event.agent) {
|
|
249
|
+
const entry = this.data.byAgent[event.agent] ?? { cost: 0, calls: 0 };
|
|
250
|
+
entry.cost += cost;
|
|
251
|
+
entry.calls += 1;
|
|
252
|
+
this.data.byAgent[event.agent] = entry;
|
|
253
|
+
}
|
|
254
|
+
if (event.model) {
|
|
255
|
+
const entry = this.data.byModel[event.model] ?? {
|
|
256
|
+
cost: 0,
|
|
257
|
+
calls: 0,
|
|
258
|
+
tokens: { input: 0, output: 0 }
|
|
259
|
+
};
|
|
260
|
+
entry.cost += cost;
|
|
261
|
+
entry.calls += 1;
|
|
262
|
+
entry.tokens.input += tokens.input ?? 0;
|
|
263
|
+
entry.tokens.output += tokens.output ?? 0;
|
|
264
|
+
this.data.byModel[event.model] = entry;
|
|
265
|
+
}
|
|
266
|
+
if (event.workflow) {
|
|
267
|
+
const entry = this.data.byWorkflow[event.workflow] ?? { cost: 0, executions: 0 };
|
|
268
|
+
entry.cost += cost;
|
|
269
|
+
if (event.type === "workflow_start") entry.executions += 1;
|
|
270
|
+
this.data.byWorkflow[event.workflow] = entry;
|
|
271
|
+
}
|
|
272
|
+
this.connMgr.broadcast("costs", this.data);
|
|
273
|
+
}
|
|
274
|
+
/** Get current aggregated cost data. */
|
|
275
|
+
getData() {
|
|
276
|
+
return this.data;
|
|
277
|
+
}
|
|
278
|
+
/** Reset all accumulated data. */
|
|
279
|
+
reset() {
|
|
280
|
+
this.data = {
|
|
281
|
+
totalCost: 0,
|
|
282
|
+
totalTokens: { input: 0, output: 0, reasoning: 0 },
|
|
283
|
+
byAgent: {},
|
|
284
|
+
byModel: {},
|
|
285
|
+
byWorkflow: {}
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
// src/server/routes/health.ts
|
|
291
|
+
var import_hono = require("hono");
|
|
292
|
+
var app = new import_hono.Hono();
|
|
293
|
+
app.get("/health", (c) => {
|
|
294
|
+
const runtime = c.get("runtime");
|
|
295
|
+
return c.json({
|
|
296
|
+
ok: true,
|
|
297
|
+
data: {
|
|
298
|
+
status: "healthy",
|
|
299
|
+
workflows: runtime.getWorkflowNames().length,
|
|
300
|
+
agents: runtime.getAgents().length,
|
|
301
|
+
tools: runtime.getTools().length
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
});
|
|
305
|
+
var health_default = app;
|
|
306
|
+
|
|
307
|
+
// src/server/routes/workflows.ts
|
|
308
|
+
var import_hono2 = require("hono");
|
|
309
|
+
var import_axl = require("@axlsdk/axl");
|
|
310
|
+
function createWorkflowRoutes(connMgr) {
|
|
311
|
+
const app8 = new import_hono2.Hono();
|
|
312
|
+
app8.get("/workflows", (c) => {
|
|
313
|
+
const runtime = c.get("runtime");
|
|
314
|
+
const workflows = runtime.getWorkflows().map((w) => ({
|
|
315
|
+
name: w.name,
|
|
316
|
+
hasInputSchema: !!w.inputSchema,
|
|
317
|
+
hasOutputSchema: !!w.outputSchema
|
|
318
|
+
}));
|
|
319
|
+
return c.json({ ok: true, data: workflows });
|
|
320
|
+
});
|
|
321
|
+
app8.get("/workflows/:name", (c) => {
|
|
322
|
+
const runtime = c.get("runtime");
|
|
323
|
+
const name = c.req.param("name");
|
|
324
|
+
const workflow = runtime.getWorkflow(name);
|
|
325
|
+
if (!workflow) {
|
|
326
|
+
return c.json(
|
|
327
|
+
{ ok: false, error: { code: "NOT_FOUND", message: `Workflow "${name}" not found` } },
|
|
328
|
+
404
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
return c.json({
|
|
332
|
+
ok: true,
|
|
333
|
+
data: {
|
|
334
|
+
name: workflow.name,
|
|
335
|
+
inputSchema: workflow.inputSchema ? (0, import_axl.zodToJsonSchema)(workflow.inputSchema) : null,
|
|
336
|
+
outputSchema: workflow.outputSchema ? (0, import_axl.zodToJsonSchema)(workflow.outputSchema) : null
|
|
337
|
+
}
|
|
338
|
+
});
|
|
339
|
+
});
|
|
340
|
+
app8.post("/workflows/:name/execute", async (c) => {
|
|
341
|
+
const runtime = c.get("runtime");
|
|
342
|
+
const name = c.req.param("name");
|
|
343
|
+
const workflow = runtime.getWorkflow(name);
|
|
344
|
+
if (!workflow) {
|
|
345
|
+
return c.json(
|
|
346
|
+
{ ok: false, error: { code: "NOT_FOUND", message: `Workflow "${name}" not found` } },
|
|
347
|
+
404
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
const body = await c.req.json();
|
|
351
|
+
if (body.stream) {
|
|
352
|
+
const stream = runtime.stream(name, body.input ?? {}, { metadata: body.metadata });
|
|
353
|
+
const executionId = `stream-${Date.now()}`;
|
|
354
|
+
(async () => {
|
|
355
|
+
try {
|
|
356
|
+
for await (const event of stream) {
|
|
357
|
+
connMgr.broadcastWithWildcard(`execution:${executionId}`, event);
|
|
358
|
+
}
|
|
359
|
+
} catch (err) {
|
|
360
|
+
connMgr.broadcastWithWildcard(`execution:${executionId}`, {
|
|
361
|
+
type: "error",
|
|
362
|
+
message: err instanceof Error ? err.message : "Stream error"
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
})();
|
|
366
|
+
return c.json({ ok: true, data: { executionId, streaming: true } });
|
|
367
|
+
}
|
|
368
|
+
const result = await runtime.execute(name, body.input ?? {}, { metadata: body.metadata });
|
|
369
|
+
return c.json({ ok: true, data: { result } });
|
|
370
|
+
});
|
|
371
|
+
return app8;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// src/server/routes/executions.ts
|
|
375
|
+
var import_hono3 = require("hono");
|
|
376
|
+
var app2 = new import_hono3.Hono();
|
|
377
|
+
app2.get("/executions", (c) => {
|
|
378
|
+
const runtime = c.get("runtime");
|
|
379
|
+
const executions = runtime.getExecutions();
|
|
380
|
+
return c.json({ ok: true, data: executions });
|
|
381
|
+
});
|
|
382
|
+
app2.get("/executions/:id", async (c) => {
|
|
383
|
+
const runtime = c.get("runtime");
|
|
384
|
+
const id = c.req.param("id");
|
|
385
|
+
const execution = await runtime.getExecution(id);
|
|
386
|
+
if (!execution) {
|
|
387
|
+
return c.json(
|
|
388
|
+
{ ok: false, error: { code: "NOT_FOUND", message: `Execution "${id}" not found` } },
|
|
389
|
+
404
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
return c.json({ ok: true, data: execution });
|
|
393
|
+
});
|
|
394
|
+
app2.post("/executions/:id/abort", (c) => {
|
|
395
|
+
const runtime = c.get("runtime");
|
|
396
|
+
const id = c.req.param("id");
|
|
397
|
+
runtime.abort(id);
|
|
398
|
+
return c.json({ ok: true, data: { aborted: true } });
|
|
399
|
+
});
|
|
400
|
+
var executions_default = app2;
|
|
401
|
+
|
|
402
|
+
// src/server/routes/sessions.ts
|
|
403
|
+
var import_hono4 = require("hono");
|
|
404
|
+
function createSessionRoutes(connMgr) {
|
|
405
|
+
const app8 = new import_hono4.Hono();
|
|
406
|
+
app8.get("/sessions", async (c) => {
|
|
407
|
+
const runtime = c.get("runtime");
|
|
408
|
+
const store = runtime.getStateStore();
|
|
409
|
+
if (!store.listSessions) {
|
|
410
|
+
return c.json({ ok: true, data: [] });
|
|
411
|
+
}
|
|
412
|
+
const ids = await store.listSessions();
|
|
413
|
+
const sessions = [];
|
|
414
|
+
for (const id of ids) {
|
|
415
|
+
const history = await store.getSession(id);
|
|
416
|
+
sessions.push({ id, messageCount: history.length });
|
|
417
|
+
}
|
|
418
|
+
return c.json({ ok: true, data: sessions });
|
|
419
|
+
});
|
|
420
|
+
app8.get("/sessions/:id", async (c) => {
|
|
421
|
+
const runtime = c.get("runtime");
|
|
422
|
+
const store = runtime.getStateStore();
|
|
423
|
+
const id = c.req.param("id");
|
|
424
|
+
const history = await store.getSession(id);
|
|
425
|
+
const handoffHistory = await store.getSessionMeta(id, "handoffHistory");
|
|
426
|
+
return c.json({ ok: true, data: { id, history, handoffHistory: handoffHistory ?? [] } });
|
|
427
|
+
});
|
|
428
|
+
app8.post("/sessions/:id/send", async (c) => {
|
|
429
|
+
const runtime = c.get("runtime");
|
|
430
|
+
const id = c.req.param("id");
|
|
431
|
+
const body = await c.req.json();
|
|
432
|
+
const session = runtime.session(id);
|
|
433
|
+
const result = await session.send(body.workflow, body.message);
|
|
434
|
+
return c.json({ ok: true, data: { result } });
|
|
435
|
+
});
|
|
436
|
+
app8.post("/sessions/:id/stream", async (c) => {
|
|
437
|
+
const runtime = c.get("runtime");
|
|
438
|
+
const id = c.req.param("id");
|
|
439
|
+
const body = await c.req.json();
|
|
440
|
+
const session = runtime.session(id);
|
|
441
|
+
const stream = await session.stream(body.workflow, body.message);
|
|
442
|
+
const executionId = `session-${id}-${Date.now()}`;
|
|
443
|
+
(async () => {
|
|
444
|
+
try {
|
|
445
|
+
for await (const event of stream) {
|
|
446
|
+
connMgr.broadcastWithWildcard(`execution:${executionId}`, event);
|
|
447
|
+
}
|
|
448
|
+
} catch (err) {
|
|
449
|
+
connMgr.broadcastWithWildcard(`execution:${executionId}`, {
|
|
450
|
+
type: "error",
|
|
451
|
+
message: err instanceof Error ? err.message : "Stream error"
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
})();
|
|
455
|
+
return c.json({ ok: true, data: { executionId, streaming: true } });
|
|
456
|
+
});
|
|
457
|
+
app8.delete("/sessions/:id", async (c) => {
|
|
458
|
+
const runtime = c.get("runtime");
|
|
459
|
+
const store = runtime.getStateStore();
|
|
460
|
+
const id = c.req.param("id");
|
|
461
|
+
await store.deleteSession(id);
|
|
462
|
+
return c.json({ ok: true, data: { deleted: true } });
|
|
463
|
+
});
|
|
464
|
+
return app8;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// src/server/routes/agents.ts
|
|
468
|
+
var import_hono5 = require("hono");
|
|
469
|
+
var import_axl2 = require("@axlsdk/axl");
|
|
470
|
+
var app3 = new import_hono5.Hono();
|
|
471
|
+
app3.get("/agents", (c) => {
|
|
472
|
+
const runtime = c.get("runtime");
|
|
473
|
+
const agents = runtime.getAgents().map((a) => ({
|
|
474
|
+
name: a._name,
|
|
475
|
+
model: a.resolveModel(),
|
|
476
|
+
system: a.resolveSystem(),
|
|
477
|
+
tools: a._config.tools?.map((t) => t.name) ?? [],
|
|
478
|
+
handoffs: typeof a._config.handoffs === "function" ? ["(dynamic)"] : a._config.handoffs?.map((h) => h.agent._name) ?? [],
|
|
479
|
+
maxTurns: a._config.maxTurns,
|
|
480
|
+
temperature: a._config.temperature,
|
|
481
|
+
maxTokens: a._config.maxTokens,
|
|
482
|
+
effort: a._config.effort,
|
|
483
|
+
thinkingBudget: a._config.thinkingBudget,
|
|
484
|
+
includeThoughts: a._config.includeThoughts,
|
|
485
|
+
toolChoice: a._config.toolChoice,
|
|
486
|
+
stop: a._config.stop
|
|
487
|
+
}));
|
|
488
|
+
return c.json({ ok: true, data: agents });
|
|
489
|
+
});
|
|
490
|
+
app3.get("/agents/:name", (c) => {
|
|
491
|
+
const runtime = c.get("runtime");
|
|
492
|
+
const name = c.req.param("name");
|
|
493
|
+
const agent = runtime.getAgent(name);
|
|
494
|
+
if (!agent) {
|
|
495
|
+
return c.json(
|
|
496
|
+
{ ok: false, error: { code: "NOT_FOUND", message: `Agent "${name}" not found` } },
|
|
497
|
+
404
|
|
498
|
+
);
|
|
499
|
+
}
|
|
500
|
+
const cfg = agent._config;
|
|
501
|
+
return c.json({
|
|
502
|
+
ok: true,
|
|
503
|
+
data: {
|
|
504
|
+
name: agent._name,
|
|
505
|
+
model: agent.resolveModel(),
|
|
506
|
+
system: agent.resolveSystem(),
|
|
507
|
+
tools: cfg.tools?.map((t) => ({
|
|
508
|
+
name: t.name,
|
|
509
|
+
description: t.description,
|
|
510
|
+
inputSchema: (0, import_axl2.zodToJsonSchema)(t.inputSchema)
|
|
511
|
+
})) ?? [],
|
|
512
|
+
handoffs: typeof cfg.handoffs === "function" ? [
|
|
513
|
+
{
|
|
514
|
+
agent: "(dynamic)",
|
|
515
|
+
description: "Resolved at runtime from metadata",
|
|
516
|
+
mode: "oneway"
|
|
517
|
+
}
|
|
518
|
+
] : cfg.handoffs?.map((h) => ({
|
|
519
|
+
agent: h.agent._name,
|
|
520
|
+
description: h.description,
|
|
521
|
+
mode: h.mode ?? "oneway"
|
|
522
|
+
})) ?? [],
|
|
523
|
+
maxTurns: cfg.maxTurns,
|
|
524
|
+
temperature: cfg.temperature,
|
|
525
|
+
maxTokens: cfg.maxTokens,
|
|
526
|
+
effort: cfg.effort,
|
|
527
|
+
thinkingBudget: cfg.thinkingBudget,
|
|
528
|
+
includeThoughts: cfg.includeThoughts,
|
|
529
|
+
toolChoice: cfg.toolChoice,
|
|
530
|
+
stop: cfg.stop,
|
|
531
|
+
timeout: cfg.timeout,
|
|
532
|
+
maxContext: cfg.maxContext,
|
|
533
|
+
version: cfg.version,
|
|
534
|
+
mcp: cfg.mcp,
|
|
535
|
+
mcpTools: cfg.mcpTools,
|
|
536
|
+
hasGuardrails: !!cfg.guardrails,
|
|
537
|
+
guardrails: cfg.guardrails ? {
|
|
538
|
+
hasInput: !!cfg.guardrails.input,
|
|
539
|
+
hasOutput: !!cfg.guardrails.output,
|
|
540
|
+
onBlock: cfg.guardrails.onBlock ?? "throw",
|
|
541
|
+
maxRetries: cfg.guardrails.maxRetries
|
|
542
|
+
} : null
|
|
543
|
+
}
|
|
544
|
+
});
|
|
545
|
+
});
|
|
546
|
+
var agents_default = app3;
|
|
547
|
+
|
|
548
|
+
// src/server/routes/tools.ts
|
|
549
|
+
var import_hono6 = require("hono");
|
|
550
|
+
var import_axl3 = require("@axlsdk/axl");
|
|
551
|
+
var app4 = new import_hono6.Hono();
|
|
552
|
+
app4.get("/tools", (c) => {
|
|
553
|
+
const runtime = c.get("runtime");
|
|
554
|
+
const tools = runtime.getTools().map((t) => ({
|
|
555
|
+
name: t.name,
|
|
556
|
+
description: t.description,
|
|
557
|
+
inputSchema: t.inputSchema ? (0, import_axl3.zodToJsonSchema)(t.inputSchema) : {},
|
|
558
|
+
sensitive: t.sensitive ?? false,
|
|
559
|
+
requireApproval: t.requireApproval ?? false
|
|
560
|
+
}));
|
|
561
|
+
return c.json({ ok: true, data: tools });
|
|
562
|
+
});
|
|
563
|
+
app4.get("/tools/:name", (c) => {
|
|
564
|
+
const runtime = c.get("runtime");
|
|
565
|
+
const name = c.req.param("name");
|
|
566
|
+
const tool = runtime.getTool(name);
|
|
567
|
+
if (!tool) {
|
|
568
|
+
return c.json(
|
|
569
|
+
{ ok: false, error: { code: "NOT_FOUND", message: `Tool "${name}" not found` } },
|
|
570
|
+
404
|
|
571
|
+
);
|
|
572
|
+
}
|
|
573
|
+
return c.json({
|
|
574
|
+
ok: true,
|
|
575
|
+
data: {
|
|
576
|
+
name: tool.name,
|
|
577
|
+
description: tool.description,
|
|
578
|
+
inputSchema: tool.inputSchema ? (0, import_axl3.zodToJsonSchema)(tool.inputSchema) : {},
|
|
579
|
+
sensitive: tool.sensitive,
|
|
580
|
+
requireApproval: tool.requireApproval,
|
|
581
|
+
retry: tool.retry,
|
|
582
|
+
hasHooks: !!tool.hooks,
|
|
583
|
+
hooks: tool.hooks ? {
|
|
584
|
+
hasBefore: !!tool.hooks.before,
|
|
585
|
+
hasAfter: !!tool.hooks.after
|
|
586
|
+
} : null
|
|
587
|
+
}
|
|
588
|
+
});
|
|
589
|
+
});
|
|
590
|
+
app4.post("/tools/:name/test", async (c) => {
|
|
591
|
+
const runtime = c.get("runtime");
|
|
592
|
+
const name = c.req.param("name");
|
|
593
|
+
const tool = runtime.getTool(name);
|
|
594
|
+
if (!tool) {
|
|
595
|
+
return c.json(
|
|
596
|
+
{ ok: false, error: { code: "NOT_FOUND", message: `Tool "${name}" not found` } },
|
|
597
|
+
404
|
|
598
|
+
);
|
|
599
|
+
}
|
|
600
|
+
const body = await c.req.json();
|
|
601
|
+
const ctx = runtime.createContext();
|
|
602
|
+
const result = await tool.run(ctx, body.input);
|
|
603
|
+
return c.json({ ok: true, data: { result } });
|
|
604
|
+
});
|
|
605
|
+
var tools_default = app4;
|
|
606
|
+
|
|
607
|
+
// src/server/routes/memory.ts
|
|
608
|
+
var import_hono7 = require("hono");
|
|
609
|
+
var app5 = new import_hono7.Hono();
|
|
610
|
+
app5.get("/memory/:scope", async (c) => {
|
|
611
|
+
const runtime = c.get("runtime");
|
|
612
|
+
const store = runtime.getStateStore();
|
|
613
|
+
const scope = c.req.param("scope");
|
|
614
|
+
if (!store.getAllMemory) {
|
|
615
|
+
return c.json({ ok: true, data: [] });
|
|
616
|
+
}
|
|
617
|
+
const entries = await store.getAllMemory(scope);
|
|
618
|
+
return c.json({ ok: true, data: entries });
|
|
619
|
+
});
|
|
620
|
+
app5.get("/memory/:scope/:key", async (c) => {
|
|
621
|
+
const runtime = c.get("runtime");
|
|
622
|
+
const store = runtime.getStateStore();
|
|
623
|
+
const scope = c.req.param("scope");
|
|
624
|
+
const key = c.req.param("key");
|
|
625
|
+
if (!store.getMemory) {
|
|
626
|
+
return c.json(
|
|
627
|
+
{ ok: false, error: { code: "NOT_SUPPORTED", message: "Memory not supported" } },
|
|
628
|
+
501
|
|
629
|
+
);
|
|
630
|
+
}
|
|
631
|
+
const value = await store.getMemory(scope, key);
|
|
632
|
+
if (value === null) {
|
|
633
|
+
return c.json(
|
|
634
|
+
{ ok: false, error: { code: "NOT_FOUND", message: `Memory "${scope}/${key}" not found` } },
|
|
635
|
+
404
|
|
636
|
+
);
|
|
637
|
+
}
|
|
638
|
+
return c.json({ ok: true, data: { key, value } });
|
|
639
|
+
});
|
|
640
|
+
app5.put("/memory/:scope/:key", async (c) => {
|
|
641
|
+
const runtime = c.get("runtime");
|
|
642
|
+
const store = runtime.getStateStore();
|
|
643
|
+
const scope = c.req.param("scope");
|
|
644
|
+
const key = c.req.param("key");
|
|
645
|
+
if (!store.saveMemory) {
|
|
646
|
+
return c.json(
|
|
647
|
+
{ ok: false, error: { code: "NOT_SUPPORTED", message: "Memory not supported" } },
|
|
648
|
+
501
|
|
649
|
+
);
|
|
650
|
+
}
|
|
651
|
+
const body = await c.req.json();
|
|
652
|
+
await store.saveMemory(scope, key, body.value);
|
|
653
|
+
return c.json({ ok: true, data: { saved: true } });
|
|
654
|
+
});
|
|
655
|
+
app5.delete("/memory/:scope/:key", async (c) => {
|
|
656
|
+
const runtime = c.get("runtime");
|
|
657
|
+
const store = runtime.getStateStore();
|
|
658
|
+
const scope = c.req.param("scope");
|
|
659
|
+
const key = c.req.param("key");
|
|
660
|
+
if (!store.deleteMemory) {
|
|
661
|
+
return c.json(
|
|
662
|
+
{ ok: false, error: { code: "NOT_SUPPORTED", message: "Memory not supported" } },
|
|
663
|
+
501
|
|
664
|
+
);
|
|
665
|
+
}
|
|
666
|
+
await store.deleteMemory(scope, key);
|
|
667
|
+
return c.json({ ok: true, data: { deleted: true } });
|
|
668
|
+
});
|
|
669
|
+
app5.post("/memory/search", async (c) => {
|
|
670
|
+
return c.json({
|
|
671
|
+
ok: true,
|
|
672
|
+
data: { results: [], message: "Semantic search requires MemoryManager with vector store" }
|
|
673
|
+
});
|
|
674
|
+
});
|
|
675
|
+
var memory_default = app5;
|
|
676
|
+
|
|
677
|
+
// src/server/routes/decisions.ts
|
|
678
|
+
var import_hono8 = require("hono");
|
|
679
|
+
var app6 = new import_hono8.Hono();
|
|
680
|
+
app6.get("/decisions", async (c) => {
|
|
681
|
+
const runtime = c.get("runtime");
|
|
682
|
+
const decisions = await runtime.getPendingDecisions();
|
|
683
|
+
return c.json({ ok: true, data: decisions });
|
|
684
|
+
});
|
|
685
|
+
app6.post("/decisions/:executionId/resolve", async (c) => {
|
|
686
|
+
const runtime = c.get("runtime");
|
|
687
|
+
const executionId = c.req.param("executionId");
|
|
688
|
+
const body = await c.req.json();
|
|
689
|
+
await runtime.resolveDecision(executionId, body);
|
|
690
|
+
return c.json({ ok: true, data: { resolved: true } });
|
|
691
|
+
});
|
|
692
|
+
var decisions_default = app6;
|
|
693
|
+
|
|
694
|
+
// src/server/routes/costs.ts
|
|
695
|
+
var import_hono9 = require("hono");
|
|
696
|
+
function createCostRoutes(costAggregator) {
|
|
697
|
+
const app8 = new import_hono9.Hono();
|
|
698
|
+
app8.get("/costs", (c) => {
|
|
699
|
+
return c.json({ ok: true, data: costAggregator.getData() });
|
|
700
|
+
});
|
|
701
|
+
app8.post("/costs/reset", (c) => {
|
|
702
|
+
costAggregator.reset();
|
|
703
|
+
return c.json({ ok: true, data: { reset: true } });
|
|
704
|
+
});
|
|
705
|
+
return app8;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
// src/server/routes/evals.ts
|
|
709
|
+
var import_hono10 = require("hono");
|
|
710
|
+
var app7 = new import_hono10.Hono();
|
|
711
|
+
app7.get("/evals", async (c) => {
|
|
712
|
+
const runtime = c.get("runtime");
|
|
713
|
+
const evals = runtime.getRegisteredEvals();
|
|
714
|
+
return c.json({ ok: true, data: evals });
|
|
715
|
+
});
|
|
716
|
+
app7.post("/evals/:name/run", async (c) => {
|
|
717
|
+
const runtime = c.get("runtime");
|
|
718
|
+
const name = c.req.param("name");
|
|
719
|
+
const entry = runtime.getRegisteredEval(name);
|
|
720
|
+
if (!entry) {
|
|
721
|
+
return c.json(
|
|
722
|
+
{ ok: false, error: { code: "NOT_FOUND", message: `Eval "${name}" not found` } },
|
|
723
|
+
404
|
|
724
|
+
);
|
|
725
|
+
}
|
|
726
|
+
try {
|
|
727
|
+
const result = await runtime.runRegisteredEval(name);
|
|
728
|
+
return c.json({ ok: true, data: result });
|
|
729
|
+
} catch (err) {
|
|
730
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
731
|
+
return c.json({ ok: false, error: { code: "EVAL_ERROR", message } }, 400);
|
|
732
|
+
}
|
|
733
|
+
});
|
|
734
|
+
app7.post("/evals/compare", async (c) => {
|
|
735
|
+
const runtime = c.get("runtime");
|
|
736
|
+
const body = await c.req.json();
|
|
737
|
+
try {
|
|
738
|
+
const result = await runtime.evalCompare(body.baseline, body.candidate);
|
|
739
|
+
return c.json({ ok: true, data: result });
|
|
740
|
+
} catch (err) {
|
|
741
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
742
|
+
return c.json({ ok: false, error: { code: "EVAL_ERROR", message } }, 400);
|
|
743
|
+
}
|
|
744
|
+
});
|
|
745
|
+
var evals_default = app7;
|
|
746
|
+
|
|
747
|
+
// src/server/routes/playground.ts
|
|
748
|
+
var import_hono11 = require("hono");
|
|
749
|
+
function createPlaygroundRoutes(connMgr) {
|
|
750
|
+
const app8 = new import_hono11.Hono();
|
|
751
|
+
app8.post("/playground/chat", async (c) => {
|
|
752
|
+
const runtime = c.get("runtime");
|
|
753
|
+
const body = await c.req.json();
|
|
754
|
+
const workflowName = body.workflow ?? runtime.getWorkflowNames()[0];
|
|
755
|
+
if (!workflowName) {
|
|
756
|
+
return c.json(
|
|
757
|
+
{ ok: false, error: { code: "NO_WORKFLOW", message: "No workflows registered" } },
|
|
758
|
+
400
|
|
759
|
+
);
|
|
760
|
+
}
|
|
761
|
+
const sessionId = body.sessionId ?? `playground-${Date.now()}`;
|
|
762
|
+
const session = runtime.session(sessionId);
|
|
763
|
+
const stream = await session.stream(workflowName, body.message);
|
|
764
|
+
const executionId = `playground-${sessionId}-${Date.now()}`;
|
|
765
|
+
(async () => {
|
|
766
|
+
try {
|
|
767
|
+
for await (const event of stream) {
|
|
768
|
+
connMgr.broadcastWithWildcard(`execution:${executionId}`, event);
|
|
769
|
+
}
|
|
770
|
+
} catch (err) {
|
|
771
|
+
connMgr.broadcastWithWildcard(`execution:${executionId}`, {
|
|
772
|
+
type: "error",
|
|
773
|
+
message: err instanceof Error ? err.message : "Stream error"
|
|
774
|
+
});
|
|
775
|
+
}
|
|
776
|
+
})();
|
|
777
|
+
return c.json({
|
|
778
|
+
ok: true,
|
|
779
|
+
data: { sessionId, executionId, streaming: true }
|
|
780
|
+
});
|
|
781
|
+
});
|
|
782
|
+
return app8;
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
// src/server/index.ts
|
|
786
|
+
function createServer(options) {
|
|
787
|
+
const { runtime, staticRoot, basePath = "", readOnly = false } = options;
|
|
788
|
+
const app8 = new import_hono12.Hono();
|
|
789
|
+
const connMgr = new ConnectionManager();
|
|
790
|
+
const costAggregator = new CostAggregator(connMgr);
|
|
791
|
+
if (options.cors !== false) {
|
|
792
|
+
app8.use("*", (0, import_cors.cors)());
|
|
793
|
+
}
|
|
794
|
+
app8.use("*", errorHandler);
|
|
795
|
+
app8.use("*", async (c, next) => {
|
|
796
|
+
c.set("runtime", runtime);
|
|
797
|
+
await next();
|
|
798
|
+
});
|
|
799
|
+
if (readOnly) {
|
|
800
|
+
const blocked = [
|
|
801
|
+
"POST /api/workflows",
|
|
802
|
+
"POST /api/executions",
|
|
803
|
+
"POST /api/sessions",
|
|
804
|
+
"DELETE /api/sessions",
|
|
805
|
+
"PUT /api/memory",
|
|
806
|
+
"DELETE /api/memory",
|
|
807
|
+
"POST /api/decisions",
|
|
808
|
+
"POST /api/costs",
|
|
809
|
+
"POST /api/tools",
|
|
810
|
+
"POST /api/evals",
|
|
811
|
+
"POST /api/playground"
|
|
812
|
+
];
|
|
813
|
+
app8.use("/api/*", async (c, next) => {
|
|
814
|
+
const apiIdx = c.req.path.indexOf("/api/");
|
|
815
|
+
const apiPath = apiIdx >= 0 ? c.req.path.slice(apiIdx) : c.req.path;
|
|
816
|
+
const key = `${c.req.method} ${apiPath}`;
|
|
817
|
+
if (blocked.some((b) => key.startsWith(b))) {
|
|
818
|
+
return c.json(
|
|
819
|
+
{
|
|
820
|
+
ok: false,
|
|
821
|
+
error: { code: "READ_ONLY", message: "Studio is mounted in read-only mode" }
|
|
822
|
+
},
|
|
823
|
+
405
|
|
824
|
+
);
|
|
825
|
+
}
|
|
826
|
+
await next();
|
|
827
|
+
});
|
|
828
|
+
}
|
|
829
|
+
const api = new import_hono12.Hono();
|
|
830
|
+
api.route("/", health_default);
|
|
831
|
+
api.route("/", createWorkflowRoutes(connMgr));
|
|
832
|
+
api.route("/", executions_default);
|
|
833
|
+
api.route("/", createSessionRoutes(connMgr));
|
|
834
|
+
api.route("/", agents_default);
|
|
835
|
+
api.route("/", tools_default);
|
|
836
|
+
api.route("/", memory_default);
|
|
837
|
+
api.route("/", decisions_default);
|
|
838
|
+
api.route("/", createCostRoutes(costAggregator));
|
|
839
|
+
api.route("/", evals_default);
|
|
840
|
+
api.route("/", createPlaygroundRoutes(connMgr));
|
|
841
|
+
app8.route("/api", api);
|
|
842
|
+
const traceListener = (event) => {
|
|
843
|
+
const traceEvent = event;
|
|
844
|
+
if (traceEvent.executionId) {
|
|
845
|
+
connMgr.broadcastWithWildcard(`trace:${traceEvent.executionId}`, traceEvent);
|
|
846
|
+
}
|
|
847
|
+
costAggregator.onTrace(traceEvent);
|
|
848
|
+
if (traceEvent.type === "await_human") {
|
|
849
|
+
connMgr.broadcast("decisions", traceEvent);
|
|
850
|
+
}
|
|
851
|
+
};
|
|
852
|
+
runtime.on("trace", traceListener);
|
|
853
|
+
if (staticRoot) {
|
|
854
|
+
app8.use(
|
|
855
|
+
"/*",
|
|
856
|
+
(0, import_serve_static.serveStatic)({
|
|
857
|
+
root: staticRoot,
|
|
858
|
+
rewriteRequestPath: basePath ? (path) => path.startsWith(basePath) ? path.slice(basePath.length) || "/" : path : void 0
|
|
859
|
+
})
|
|
860
|
+
);
|
|
861
|
+
if (basePath) {
|
|
862
|
+
const indexPath = (0, import_node_path.resolve)(staticRoot, "index.html");
|
|
863
|
+
if (!(0, import_node_fs.existsSync)(indexPath)) {
|
|
864
|
+
console.warn(`[axl-studio] index.html not found at ${indexPath}`);
|
|
865
|
+
} else {
|
|
866
|
+
const indexHtml = (0, import_node_fs.readFileSync)(indexPath, "utf-8");
|
|
867
|
+
const safeBasePath = JSON.stringify(basePath).replace(/</g, "\\u003c");
|
|
868
|
+
const injectedHtml = indexHtml.replace(
|
|
869
|
+
"</head>",
|
|
870
|
+
`<base href="${basePath}/">
|
|
871
|
+
<script>window.__AXL_STUDIO_BASE__=${safeBasePath}</script>
|
|
872
|
+
</head>`
|
|
873
|
+
);
|
|
874
|
+
if (injectedHtml === indexHtml) {
|
|
875
|
+
console.warn(
|
|
876
|
+
"[axl-studio] Could not inject basePath into index.html \u2014 </head> tag not found. The SPA may not route correctly."
|
|
877
|
+
);
|
|
878
|
+
}
|
|
879
|
+
app8.get("*", (c) => c.html(injectedHtml));
|
|
880
|
+
}
|
|
881
|
+
} else {
|
|
882
|
+
app8.get("*", (0, import_serve_static.serveStatic)({ root: staticRoot, path: "/index.html" }));
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
return {
|
|
886
|
+
app: app8,
|
|
887
|
+
connMgr,
|
|
888
|
+
costAggregator,
|
|
889
|
+
createWsHandlers: () => createWsHandlers(connMgr),
|
|
890
|
+
traceListener
|
|
891
|
+
};
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
// src/middleware.ts
|
|
895
|
+
var import_meta = {};
|
|
896
|
+
function createStudioMiddleware(options) {
|
|
897
|
+
const { runtime, serveClient = true, verifyUpgrade, readOnly = false } = options;
|
|
898
|
+
const basePath = normalizeBasePath(options.basePath);
|
|
899
|
+
const staticRoot = serveClient ? resolveClientDist() : void 0;
|
|
900
|
+
if (serveClient && !staticRoot) {
|
|
901
|
+
const dir = import_meta.dirname ?? (0, import_node_path2.dirname)((0, import_node_url.fileURLToPath)(import_meta.url));
|
|
902
|
+
console.warn(
|
|
903
|
+
`[axl-studio] serveClient is true but no pre-built client found at ${(0, import_node_path2.resolve)(dir, "client")}. Studio UI will not be available. Set serveClient: false to suppress this warning.`
|
|
904
|
+
);
|
|
905
|
+
}
|
|
906
|
+
const { app: app8, connMgr, traceListener } = createServer({
|
|
907
|
+
runtime,
|
|
908
|
+
staticRoot,
|
|
909
|
+
basePath,
|
|
910
|
+
readOnly,
|
|
911
|
+
cors: false
|
|
912
|
+
// Host framework owns CORS policy
|
|
913
|
+
});
|
|
914
|
+
if (process.env.NODE_ENV === "production" && !verifyUpgrade) {
|
|
915
|
+
console.warn(
|
|
916
|
+
"[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"
|
|
917
|
+
);
|
|
918
|
+
}
|
|
919
|
+
const listener = (0, import_node_server.getRequestListener)(app8.fetch, {
|
|
920
|
+
overrideGlobalObjects: false
|
|
921
|
+
});
|
|
922
|
+
let closed = false;
|
|
923
|
+
function handler(req, res) {
|
|
924
|
+
if (closed) {
|
|
925
|
+
res.writeHead(503, { "Content-Type": "application/json" });
|
|
926
|
+
res.end(
|
|
927
|
+
JSON.stringify({
|
|
928
|
+
ok: false,
|
|
929
|
+
error: { code: "CLOSED", message: "Studio middleware has been shut down" }
|
|
930
|
+
})
|
|
931
|
+
);
|
|
932
|
+
return;
|
|
933
|
+
}
|
|
934
|
+
listener(req, res).catch((err) => {
|
|
935
|
+
console.error("[axl-studio] Unhandled error in request handler:", err);
|
|
936
|
+
if (!res.headersSent) {
|
|
937
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
938
|
+
res.end(
|
|
939
|
+
JSON.stringify({
|
|
940
|
+
ok: false,
|
|
941
|
+
error: { code: "INTERNAL_ERROR", message: "Internal server error" }
|
|
942
|
+
})
|
|
943
|
+
);
|
|
944
|
+
}
|
|
945
|
+
});
|
|
946
|
+
}
|
|
947
|
+
function handleWebSocket(ws) {
|
|
948
|
+
if (closed) {
|
|
949
|
+
ws.close();
|
|
950
|
+
return;
|
|
951
|
+
}
|
|
952
|
+
const socket = {
|
|
953
|
+
send: (data) => ws.send(data),
|
|
954
|
+
close: () => ws.close()
|
|
955
|
+
};
|
|
956
|
+
connMgr.add(socket);
|
|
957
|
+
ws.on("message", (raw) => {
|
|
958
|
+
const reply = handleWsMessage(String(raw), socket, connMgr);
|
|
959
|
+
if (reply) ws.send(reply);
|
|
960
|
+
});
|
|
961
|
+
ws.on("close", () => connMgr.remove(socket));
|
|
962
|
+
ws.on("error", () => connMgr.remove(socket));
|
|
963
|
+
}
|
|
964
|
+
let wss;
|
|
965
|
+
let upgradeHandler;
|
|
966
|
+
let serverRef;
|
|
967
|
+
function upgradeWebSocket(server, path) {
|
|
968
|
+
if (wss) {
|
|
969
|
+
throw new Error(
|
|
970
|
+
"[axl-studio] upgradeWebSocket() has already been called. Call close() first if you need to re-attach."
|
|
971
|
+
);
|
|
972
|
+
}
|
|
973
|
+
const wsPath = path ?? (basePath ? `${basePath}/ws` : "/ws");
|
|
974
|
+
wss = new import_ws.WebSocketServer({ noServer: true });
|
|
975
|
+
serverRef = server;
|
|
976
|
+
upgradeHandler = async (req, socket, head) => {
|
|
977
|
+
const pathname = new URL(req.url, `http://${req.headers.host}`).pathname;
|
|
978
|
+
if (pathname !== wsPath) return;
|
|
979
|
+
if (verifyUpgrade) {
|
|
980
|
+
try {
|
|
981
|
+
const allowed = await verifyUpgrade(req);
|
|
982
|
+
if (!allowed) {
|
|
983
|
+
socket.write("HTTP/1.1 401 Unauthorized\r\n\r\n");
|
|
984
|
+
socket.destroy();
|
|
985
|
+
return;
|
|
986
|
+
}
|
|
987
|
+
} catch {
|
|
988
|
+
socket.write("HTTP/1.1 403 Forbidden\r\n\r\n");
|
|
989
|
+
socket.destroy();
|
|
990
|
+
return;
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
if (!wss) {
|
|
994
|
+
socket.destroy();
|
|
995
|
+
return;
|
|
996
|
+
}
|
|
997
|
+
wss.handleUpgrade(req, socket, head, (ws) => {
|
|
998
|
+
handleWebSocket(ws);
|
|
999
|
+
});
|
|
1000
|
+
};
|
|
1001
|
+
server.on("upgrade", upgradeHandler);
|
|
1002
|
+
}
|
|
1003
|
+
function close() {
|
|
1004
|
+
closed = true;
|
|
1005
|
+
connMgr.closeAll();
|
|
1006
|
+
if (upgradeHandler && serverRef) {
|
|
1007
|
+
serverRef.removeListener("upgrade", upgradeHandler);
|
|
1008
|
+
upgradeHandler = void 0;
|
|
1009
|
+
serverRef = void 0;
|
|
1010
|
+
}
|
|
1011
|
+
if (wss) {
|
|
1012
|
+
wss.close();
|
|
1013
|
+
wss = void 0;
|
|
1014
|
+
}
|
|
1015
|
+
if (traceListener) {
|
|
1016
|
+
runtime.removeListener("trace", traceListener);
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
return {
|
|
1020
|
+
handler,
|
|
1021
|
+
handleWebSocket,
|
|
1022
|
+
upgradeWebSocket,
|
|
1023
|
+
app: app8,
|
|
1024
|
+
connectionManager: connMgr,
|
|
1025
|
+
close
|
|
1026
|
+
};
|
|
1027
|
+
}
|
|
1028
|
+
function normalizeBasePath(raw) {
|
|
1029
|
+
if (!raw) return "";
|
|
1030
|
+
const normalized = raw.replace(/\/+$/, "");
|
|
1031
|
+
if (!normalized) return "";
|
|
1032
|
+
if (!normalized.startsWith("/")) {
|
|
1033
|
+
throw new Error(`basePath must start with '/' (got '${raw}'). Example: '/studio'`);
|
|
1034
|
+
}
|
|
1035
|
+
if (normalized.includes("..")) {
|
|
1036
|
+
throw new Error(`basePath must not contain '..' segments (got '${raw}')`);
|
|
1037
|
+
}
|
|
1038
|
+
if (normalized.includes("//")) {
|
|
1039
|
+
throw new Error(`basePath must not contain consecutive slashes (got '${raw}')`);
|
|
1040
|
+
}
|
|
1041
|
+
if (!/^\/[a-zA-Z0-9/_-]*$/.test(normalized)) {
|
|
1042
|
+
throw new Error(
|
|
1043
|
+
`basePath contains invalid characters (got '${raw}'). Only alphanumeric characters, /, _, and - are allowed.`
|
|
1044
|
+
);
|
|
1045
|
+
}
|
|
1046
|
+
return normalized;
|
|
1047
|
+
}
|
|
1048
|
+
function resolveClientDist() {
|
|
1049
|
+
const dir = import_meta.dirname ?? (0, import_node_path2.dirname)((0, import_node_url.fileURLToPath)(import_meta.url));
|
|
1050
|
+
const candidate = (0, import_node_path2.resolve)(dir, "client");
|
|
1051
|
+
return (0, import_node_fs2.existsSync)((0, import_node_path2.resolve)(candidate, "index.html")) ? candidate : void 0;
|
|
1052
|
+
}
|
|
1053
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1054
|
+
0 && (module.exports = {
|
|
1055
|
+
createStudioMiddleware,
|
|
1056
|
+
handleWsMessage
|
|
1057
|
+
});
|
|
1058
|
+
//# sourceMappingURL=middleware.cjs.map
|