@axlsdk/studio 0.21.1 → 0.22.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/dist/cli.cjs CHANGED
@@ -118,6 +118,19 @@ function redactStreamEvent(event, redact) {
118
118
  if (!redact) return event;
119
119
  return (0, import_axl.redactHistoricalEvent)(event);
120
120
  }
121
+ function sanitizeRichInputFailure(event) {
122
+ const message = "Playground media input failed";
123
+ if (event.type === "ask_end" && !event.outcome.ok) {
124
+ return { ...event, outcome: { ok: false, error: message } };
125
+ }
126
+ if (event.type === "agent_call_end" && event.data.error !== void 0) {
127
+ return { ...event, data: { ...event.data, error: message } };
128
+ }
129
+ if (event.type === "error") {
130
+ return { ...event, data: { ...event.data, message } };
131
+ }
132
+ return event;
133
+ }
121
134
  function redactEvalItem(item) {
122
135
  const scrubbed = {
123
136
  ...item,
@@ -836,11 +849,11 @@ function reduceCost(acc, event) {
836
849
  byWorkflow2[event.workflow] = { ...prev, executions: prev.executions + 1 };
837
850
  return { ...acc, byWorkflow: byWorkflow2 };
838
851
  }
839
- if (event.cost == null && !event.tokens) return acc;
852
+ const unpricedLeaf = (0, import_axl2.isUnpricedLeaf)(event);
853
+ if (event.cost == null && !event.tokens && !unpricedLeaf) return acc;
840
854
  const cost = (0, import_axl2.eventCostContribution)(event);
841
855
  if (event.type === "ask_end") return acc;
842
856
  const tokens = event.tokens ?? {};
843
- const unpricedLeaf = (0, import_axl2.isUnpricedLeaf)(event);
844
857
  const totalTokens = event.type === "agent_call_end" ? {
845
858
  input: acc.totalTokens.input + finite(tokens.input),
846
859
  output: acc.totalTokens.output + finite(tokens.output),
@@ -1353,6 +1366,24 @@ var executions_default = createExecutionRoutes();
1353
1366
 
1354
1367
  // src/server/routes/sessions.ts
1355
1368
  var import_hono4 = require("hono");
1369
+ function describeStudioInput(content) {
1370
+ if (typeof content === "string") return void 0;
1371
+ return {
1372
+ parts: content.map((part) => {
1373
+ if (part.type === "text") return { type: "text", characters: part.text.length };
1374
+ const { source } = part;
1375
+ const bytes = source.type === "bytes" ? source.data.byteLength : source.type === "base64" ? Math.floor(source.data.length * 3 / 4) - (source.data.endsWith("==") ? 2 : source.data.endsWith("=") ? 1 : 0) : void 0;
1376
+ return {
1377
+ type: "image",
1378
+ source: source.type,
1379
+ ...source.mediaType ? { mediaType: source.mediaType } : {},
1380
+ ...bytes !== void 0 ? { bytes } : {}
1381
+ // URLs, provider-file references, and labels are intentionally not
1382
+ // included: this is a Studio-safe descriptor, not a replay payload.
1383
+ };
1384
+ })
1385
+ };
1386
+ }
1356
1387
  function createSessionRoutes(connMgr) {
1357
1388
  const app5 = new import_hono4.Hono();
1358
1389
  app5.get("/sessions", async (c) => {
@@ -1375,11 +1406,17 @@ function createSessionRoutes(connMgr) {
1375
1406
  const id = c.req.param("id");
1376
1407
  const history = await store.getSession(id);
1377
1408
  const handoffHistory = await store.getSessionMeta(id, "handoffHistory");
1409
+ const redact = runtime.isRedactEnabled();
1410
+ const studioHistory = history.map((message) => {
1411
+ const input = describeStudioInput(message.content);
1412
+ if (input) return { ...message, content: input };
1413
+ return redact ? redactSessionHistory([message], true)[0] : message;
1414
+ });
1378
1415
  return c.json({
1379
1416
  ok: true,
1380
1417
  data: {
1381
1418
  id,
1382
- history: redactSessionHistory(history, runtime.isRedactEnabled()),
1419
+ history: studioHistory,
1383
1420
  // HandoffRecord has no content fields (source/target/mode/
1384
1421
  // timestamp/duration) — nothing to scrub.
1385
1422
  handoffHistory: handoffHistory ?? []
@@ -15949,6 +15986,54 @@ function date4(params) {
15949
15986
  // ../../node_modules/.pnpm/zod@4.3.6/node_modules/zod/v4/classic/external.js
15950
15987
  config(en_default());
15951
15988
 
15989
+ // src/playground-image.ts
15990
+ var PLAYGROUND_IMAGE_MAX_BYTES = 5 * 1024 * 1024;
15991
+ var PLAYGROUND_IMAGE_MAX_BASE64_CHARACTERS = Math.ceil(PLAYGROUND_IMAGE_MAX_BYTES / 3) * 4;
15992
+ var PLAYGROUND_IMAGE_REQUEST_MAX_BYTES = PLAYGROUND_IMAGE_MAX_BASE64_CHARACTERS + 16 * 1024;
15993
+ var PLAYGROUND_IMAGE_MEDIA_TYPES = [
15994
+ "image/png",
15995
+ "image/jpeg",
15996
+ "image/webp",
15997
+ "image/gif"
15998
+ ];
15999
+ function isPlaygroundImageMediaType(value) {
16000
+ return typeof value === "string" && PLAYGROUND_IMAGE_MEDIA_TYPES.includes(value);
16001
+ }
16002
+ function base64ByteLength(value) {
16003
+ if (value.length === 0 || value.length % 4 !== 0 || !/^[A-Za-z0-9+/]*={0,2}$/.test(value)) {
16004
+ return void 0;
16005
+ }
16006
+ const padding = value.endsWith("==") ? 2 : value.endsWith("=") ? 1 : 0;
16007
+ return value.length / 4 * 3 - padding;
16008
+ }
16009
+ function decodedPrefix(value, length) {
16010
+ try {
16011
+ const prefix = value.slice(0, Math.ceil(length / 3) * 4);
16012
+ const decoded = atob(prefix);
16013
+ return Uint8Array.from(decoded, (character) => character.charCodeAt(0));
16014
+ } catch {
16015
+ return void 0;
16016
+ }
16017
+ }
16018
+ function hasMatchingImageSignature(mediaType, data) {
16019
+ const bytes = decodedPrefix(data, 12);
16020
+ if (!bytes) return false;
16021
+ const startsWith = (...prefix) => prefix.every((byte, index) => bytes[index] === byte);
16022
+ switch (mediaType) {
16023
+ case "image/png":
16024
+ return startsWith(137, 80, 78, 71, 13, 10, 26, 10);
16025
+ case "image/jpeg":
16026
+ return startsWith(255, 216, 255);
16027
+ case "image/gif":
16028
+ return startsWith(71, 73, 70, 56) && (bytes[4] === 55 || bytes[4] === 57) && bytes[5] === 97;
16029
+ case "image/webp":
16030
+ return startsWith(82, 73, 70, 70) && startsWithAt(bytes, 8, 87, 69, 66, 80);
16031
+ }
16032
+ }
16033
+ function startsWithAt(bytes, offset, ...prefix) {
16034
+ return prefix.every((byte, index) => bytes[offset + index] === byte);
16035
+ }
16036
+
15952
16037
  // src/server/routes/playground.ts
15953
16038
  var DEMO_SCHEMA_BY_AGENT = {
15954
16039
  "streaming-structured-agent": external_exports.object({
@@ -15958,11 +16043,74 @@ var DEMO_SCHEMA_BY_AGENT = {
15958
16043
  confidence: external_exports.number().min(0).max(1)
15959
16044
  })
15960
16045
  };
16046
+ var PlaygroundBodyTooLargeError = class extends Error {
16047
+ };
16048
+ async function readPlaygroundBody(request) {
16049
+ const contentLength = request.headers.get("content-length");
16050
+ if (contentLength !== null) {
16051
+ const bytes = Number(contentLength);
16052
+ if (!Number.isSafeInteger(bytes) || bytes < 0 || bytes > PLAYGROUND_IMAGE_REQUEST_MAX_BYTES) {
16053
+ throw new PlaygroundBodyTooLargeError();
16054
+ }
16055
+ }
16056
+ if (!request.body) throw new SyntaxError("missing body");
16057
+ const reader = request.body.getReader();
16058
+ const chunks = [];
16059
+ let total = 0;
16060
+ try {
16061
+ while (true) {
16062
+ const next = await reader.read();
16063
+ if (next.done) break;
16064
+ total += next.value.byteLength;
16065
+ if (total > PLAYGROUND_IMAGE_REQUEST_MAX_BYTES) {
16066
+ await reader.cancel();
16067
+ throw new PlaygroundBodyTooLargeError();
16068
+ }
16069
+ chunks.push(next.value);
16070
+ }
16071
+ } finally {
16072
+ reader.releaseLock();
16073
+ }
16074
+ const body = new Uint8Array(total);
16075
+ let offset = 0;
16076
+ for (const chunk of chunks) {
16077
+ body.set(chunk, offset);
16078
+ offset += chunk.byteLength;
16079
+ }
16080
+ return JSON.parse(new TextDecoder().decode(body));
16081
+ }
15961
16082
  function createPlaygroundRoutes(connMgr) {
15962
16083
  const app5 = new import_hono11.Hono();
15963
16084
  app5.post("/playground/chat", async (c) => {
15964
16085
  const runtime = c.get("runtime");
15965
- const body = await c.req.json();
16086
+ let parsedBody;
16087
+ try {
16088
+ parsedBody = await readPlaygroundBody(c.req.raw);
16089
+ } catch (error48) {
16090
+ if (error48 instanceof PlaygroundBodyTooLargeError) {
16091
+ return c.json(
16092
+ {
16093
+ ok: false,
16094
+ error: { code: "REQUEST_TOO_LARGE", message: "playground image request is too large" }
16095
+ },
16096
+ 413
16097
+ );
16098
+ }
16099
+ return c.json(
16100
+ { ok: false, error: { code: "INVALID_INPUT", message: "request body must be valid JSON" } },
16101
+ 400
16102
+ );
16103
+ }
16104
+ if (!parsedBody || typeof parsedBody !== "object" || Array.isArray(parsedBody)) {
16105
+ return c.json(
16106
+ {
16107
+ ok: false,
16108
+ error: { code: "INVALID_INPUT", message: "request body must be a JSON object" }
16109
+ },
16110
+ 400
16111
+ );
16112
+ }
16113
+ const body = parsedBody;
15966
16114
  if (!body.message || typeof body.message !== "string" || !body.message.trim()) {
15967
16115
  return c.json(
15968
16116
  {
@@ -15975,6 +16123,58 @@ function createPlaygroundRoutes(connMgr) {
15975
16123
  400
15976
16124
  );
15977
16125
  }
16126
+ const message = body.message;
16127
+ let image;
16128
+ if (body.image !== void 0) {
16129
+ const candidate = body.image;
16130
+ if (!candidate || typeof candidate !== "object" || !isPlaygroundImageMediaType(candidate.mediaType) || typeof candidate.data !== "string") {
16131
+ return c.json(
16132
+ {
16133
+ ok: false,
16134
+ error: { code: "INVALID_IMAGE", message: "image must be a supported base64 image" }
16135
+ },
16136
+ 400
16137
+ );
16138
+ }
16139
+ if (candidate.data.length > PLAYGROUND_IMAGE_MAX_BASE64_CHARACTERS) {
16140
+ return c.json(
16141
+ { ok: false, error: { code: "IMAGE_TOO_LARGE", message: "image must be at most 5 MiB" } },
16142
+ 400
16143
+ );
16144
+ }
16145
+ const bytes = base64ByteLength(candidate.data);
16146
+ if (bytes === void 0) {
16147
+ return c.json(
16148
+ {
16149
+ ok: false,
16150
+ error: { code: "INVALID_IMAGE", message: "image data must be valid base64" }
16151
+ },
16152
+ 400
16153
+ );
16154
+ }
16155
+ if (bytes > PLAYGROUND_IMAGE_MAX_BYTES) {
16156
+ return c.json(
16157
+ {
16158
+ ok: false,
16159
+ error: {
16160
+ code: "IMAGE_TOO_LARGE",
16161
+ message: `image must be at most ${PLAYGROUND_IMAGE_MAX_BYTES / 1024 / 1024} MiB`
16162
+ }
16163
+ },
16164
+ 400
16165
+ );
16166
+ }
16167
+ if (!hasMatchingImageSignature(candidate.mediaType, candidate.data)) {
16168
+ return c.json(
16169
+ {
16170
+ ok: false,
16171
+ error: { code: "INVALID_IMAGE", message: "image data does not match its media type" }
16172
+ },
16173
+ 400
16174
+ );
16175
+ }
16176
+ image = { mediaType: candidate.mediaType, data: candidate.data };
16177
+ }
15978
16178
  const agents = runtime.getAgents();
15979
16179
  const agent = body.agent ? agents.find((a) => a._name === body.agent) : agents[0];
15980
16180
  if (!agent) {
@@ -15989,14 +16189,18 @@ function createPlaygroundRoutes(connMgr) {
15989
16189
  const sessionId = body.sessionId ?? `playground-${Date.now()}`;
15990
16190
  const store = runtime.getStateStore();
15991
16191
  const history = await store.getSession(sessionId);
15992
- history.push({ role: "user", content: body.message });
16192
+ const persistedHistory = [...history, { role: "user", content: message }];
15993
16193
  const redactOn = runtime.isRedactEnabled();
15994
16194
  const ctx = runtime.createContext({ sessionHistory: history });
15995
16195
  const executionId = ctx.executionId;
15996
16196
  void ctx.events;
15997
16197
  const traceListener = (event) => {
15998
16198
  if (event.executionId !== executionId) return;
15999
- connMgr.broadcastWithWildcard(`execution:${executionId}`, redactStreamEvent(event, redactOn));
16199
+ const safeEvent = image ? sanitizeRichInputFailure(event) : event;
16200
+ connMgr.broadcastWithWildcard(
16201
+ `execution:${executionId}`,
16202
+ redactStreamEvent(safeEvent, redactOn)
16203
+ );
16000
16204
  };
16001
16205
  runtime.on("trace", traceListener);
16002
16206
  (async () => {
@@ -16009,10 +16213,17 @@ function createPlaygroundRoutes(connMgr) {
16009
16213
  });
16010
16214
  try {
16011
16215
  const schema = DEMO_SCHEMA_BY_AGENT[agent._name];
16012
- const result = await ctx.ask(agent, body.message, schema ? { schema } : void 0);
16216
+ const input = image ? [
16217
+ { type: "text", text: message },
16218
+ {
16219
+ type: "image",
16220
+ source: { type: "base64", data: image.data, mediaType: image.mediaType }
16221
+ }
16222
+ ] : message;
16223
+ const result = await ctx.ask(agent, input, schema ? { schema } : void 0);
16013
16224
  const resultText = typeof result === "string" ? result : JSON.stringify(result);
16014
- history.push({ role: "assistant", content: resultText });
16015
- await store.saveSession(sessionId, history);
16225
+ persistedHistory.push({ role: "assistant", content: resultText });
16226
+ await store.saveSession(sessionId, persistedHistory);
16016
16227
  const doneEvent = {
16017
16228
  ...terminalFields(),
16018
16229
  type: "done",
@@ -16026,7 +16237,11 @@ function createPlaygroundRoutes(connMgr) {
16026
16237
  const errorEvent = {
16027
16238
  ...terminalFields(),
16028
16239
  type: "error",
16029
- data: { message: err instanceof Error ? err.message : String(err) }
16240
+ // Media payloads can make providers echo user-controlled content in
16241
+ // errors. The manual terminal event is deliberately fixed and safe.
16242
+ data: {
16243
+ message: image ? "Playground media input failed" : err instanceof Error ? err.message : String(err)
16244
+ }
16030
16245
  };
16031
16246
  connMgr.broadcastWithWildcard(
16032
16247
  `execution:${executionId}`,
@@ -16419,7 +16634,7 @@ Tip: Use .mts for configs with top-level await or in projects without "type": "m
16419
16634
  ].join("\n");
16420
16635
  nodeModule.register(`data:text/javascript,${encodeURIComponent(hookCode)}`);
16421
16636
  } catch {
16422
- console.warn(`[axl-studio] Warning: --conditions requires Node.js 20.6+`);
16637
+ console.warn(`[axl-studio] Warning: this Node.js runtime does not support --conditions`);
16423
16638
  }
16424
16639
  }
16425
16640
  console.log(`[axl-studio] Loading config from ${configPath}`);