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