@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/README.md CHANGED
@@ -20,7 +20,7 @@ Or run directly with npx (no install needed):
20
20
  npx @axlsdk/studio
21
21
  ```
22
22
 
23
- **Requirements:** Node.js 20+, an existing Axl project with `@axlsdk/axl` installed.
23
+ **Requirements:** Node.js 22+, an existing Axl project with `@axlsdk/axl` installed.
24
24
 
25
25
  ## Quick Start
26
26
 
@@ -89,6 +89,19 @@ function redactStreamEvent(event, redact) {
89
89
  if (!redact) return event;
90
90
  return redactHistoricalEvent(event);
91
91
  }
92
+ function sanitizeRichInputFailure(event) {
93
+ const message = "Playground media input failed";
94
+ if (event.type === "ask_end" && !event.outcome.ok) {
95
+ return { ...event, outcome: { ok: false, error: message } };
96
+ }
97
+ if (event.type === "agent_call_end" && event.data.error !== void 0) {
98
+ return { ...event, data: { ...event.data, error: message } };
99
+ }
100
+ if (event.type === "error") {
101
+ return { ...event, data: { ...event.data, message } };
102
+ }
103
+ return event;
104
+ }
92
105
  function redactEvalItem(item) {
93
106
  const scrubbed = {
94
107
  ...item,
@@ -807,11 +820,11 @@ function reduceCost(acc, event) {
807
820
  byWorkflow2[event.workflow] = { ...prev, executions: prev.executions + 1 };
808
821
  return { ...acc, byWorkflow: byWorkflow2 };
809
822
  }
810
- if (event.cost == null && !event.tokens) return acc;
823
+ const unpricedLeaf = isUnpricedLeaf(event);
824
+ if (event.cost == null && !event.tokens && !unpricedLeaf) return acc;
811
825
  const cost = eventCostContribution(event);
812
826
  if (event.type === "ask_end") return acc;
813
827
  const tokens = event.tokens ?? {};
814
- const unpricedLeaf = isUnpricedLeaf(event);
815
828
  const totalTokens = event.type === "agent_call_end" ? {
816
829
  input: acc.totalTokens.input + finite(tokens.input),
817
830
  output: acc.totalTokens.output + finite(tokens.output),
@@ -1324,6 +1337,24 @@ var executions_default = createExecutionRoutes();
1324
1337
 
1325
1338
  // src/server/routes/sessions.ts
1326
1339
  import { Hono as Hono4 } from "hono";
1340
+ function describeStudioInput(content) {
1341
+ if (typeof content === "string") return void 0;
1342
+ return {
1343
+ parts: content.map((part) => {
1344
+ if (part.type === "text") return { type: "text", characters: part.text.length };
1345
+ const { source } = part;
1346
+ 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;
1347
+ return {
1348
+ type: "image",
1349
+ source: source.type,
1350
+ ...source.mediaType ? { mediaType: source.mediaType } : {},
1351
+ ...bytes !== void 0 ? { bytes } : {}
1352
+ // URLs, provider-file references, and labels are intentionally not
1353
+ // included: this is a Studio-safe descriptor, not a replay payload.
1354
+ };
1355
+ })
1356
+ };
1357
+ }
1327
1358
  function createSessionRoutes(connMgr) {
1328
1359
  const app5 = new Hono4();
1329
1360
  app5.get("/sessions", async (c) => {
@@ -1346,11 +1377,17 @@ function createSessionRoutes(connMgr) {
1346
1377
  const id = c.req.param("id");
1347
1378
  const history = await store.getSession(id);
1348
1379
  const handoffHistory = await store.getSessionMeta(id, "handoffHistory");
1380
+ const redact = runtime.isRedactEnabled();
1381
+ const studioHistory = history.map((message) => {
1382
+ const input = describeStudioInput(message.content);
1383
+ if (input) return { ...message, content: input };
1384
+ return redact ? redactSessionHistory([message], true)[0] : message;
1385
+ });
1349
1386
  return c.json({
1350
1387
  ok: true,
1351
1388
  data: {
1352
1389
  id,
1353
- history: redactSessionHistory(history, runtime.isRedactEnabled()),
1390
+ history: studioHistory,
1354
1391
  // HandoffRecord has no content fields (source/target/mode/
1355
1392
  // timestamp/duration) — nothing to scrub.
1356
1393
  handoffHistory: handoffHistory ?? []
@@ -15920,6 +15957,54 @@ function date4(params) {
15920
15957
  // ../../node_modules/.pnpm/zod@4.3.6/node_modules/zod/v4/classic/external.js
15921
15958
  config(en_default());
15922
15959
 
15960
+ // src/playground-image.ts
15961
+ var PLAYGROUND_IMAGE_MAX_BYTES = 5 * 1024 * 1024;
15962
+ var PLAYGROUND_IMAGE_MAX_BASE64_CHARACTERS = Math.ceil(PLAYGROUND_IMAGE_MAX_BYTES / 3) * 4;
15963
+ var PLAYGROUND_IMAGE_REQUEST_MAX_BYTES = PLAYGROUND_IMAGE_MAX_BASE64_CHARACTERS + 16 * 1024;
15964
+ var PLAYGROUND_IMAGE_MEDIA_TYPES = [
15965
+ "image/png",
15966
+ "image/jpeg",
15967
+ "image/webp",
15968
+ "image/gif"
15969
+ ];
15970
+ function isPlaygroundImageMediaType(value) {
15971
+ return typeof value === "string" && PLAYGROUND_IMAGE_MEDIA_TYPES.includes(value);
15972
+ }
15973
+ function base64ByteLength(value) {
15974
+ if (value.length === 0 || value.length % 4 !== 0 || !/^[A-Za-z0-9+/]*={0,2}$/.test(value)) {
15975
+ return void 0;
15976
+ }
15977
+ const padding = value.endsWith("==") ? 2 : value.endsWith("=") ? 1 : 0;
15978
+ return value.length / 4 * 3 - padding;
15979
+ }
15980
+ function decodedPrefix(value, length) {
15981
+ try {
15982
+ const prefix = value.slice(0, Math.ceil(length / 3) * 4);
15983
+ const decoded = atob(prefix);
15984
+ return Uint8Array.from(decoded, (character) => character.charCodeAt(0));
15985
+ } catch {
15986
+ return void 0;
15987
+ }
15988
+ }
15989
+ function hasMatchingImageSignature(mediaType, data) {
15990
+ const bytes = decodedPrefix(data, 12);
15991
+ if (!bytes) return false;
15992
+ const startsWith = (...prefix) => prefix.every((byte, index) => bytes[index] === byte);
15993
+ switch (mediaType) {
15994
+ case "image/png":
15995
+ return startsWith(137, 80, 78, 71, 13, 10, 26, 10);
15996
+ case "image/jpeg":
15997
+ return startsWith(255, 216, 255);
15998
+ case "image/gif":
15999
+ return startsWith(71, 73, 70, 56) && (bytes[4] === 55 || bytes[4] === 57) && bytes[5] === 97;
16000
+ case "image/webp":
16001
+ return startsWith(82, 73, 70, 70) && startsWithAt(bytes, 8, 87, 69, 66, 80);
16002
+ }
16003
+ }
16004
+ function startsWithAt(bytes, offset, ...prefix) {
16005
+ return prefix.every((byte, index) => bytes[offset + index] === byte);
16006
+ }
16007
+
15923
16008
  // src/server/routes/playground.ts
15924
16009
  var DEMO_SCHEMA_BY_AGENT = {
15925
16010
  "streaming-structured-agent": external_exports.object({
@@ -15929,11 +16014,74 @@ var DEMO_SCHEMA_BY_AGENT = {
15929
16014
  confidence: external_exports.number().min(0).max(1)
15930
16015
  })
15931
16016
  };
16017
+ var PlaygroundBodyTooLargeError = class extends Error {
16018
+ };
16019
+ async function readPlaygroundBody(request) {
16020
+ const contentLength = request.headers.get("content-length");
16021
+ if (contentLength !== null) {
16022
+ const bytes = Number(contentLength);
16023
+ if (!Number.isSafeInteger(bytes) || bytes < 0 || bytes > PLAYGROUND_IMAGE_REQUEST_MAX_BYTES) {
16024
+ throw new PlaygroundBodyTooLargeError();
16025
+ }
16026
+ }
16027
+ if (!request.body) throw new SyntaxError("missing body");
16028
+ const reader = request.body.getReader();
16029
+ const chunks = [];
16030
+ let total = 0;
16031
+ try {
16032
+ while (true) {
16033
+ const next = await reader.read();
16034
+ if (next.done) break;
16035
+ total += next.value.byteLength;
16036
+ if (total > PLAYGROUND_IMAGE_REQUEST_MAX_BYTES) {
16037
+ await reader.cancel();
16038
+ throw new PlaygroundBodyTooLargeError();
16039
+ }
16040
+ chunks.push(next.value);
16041
+ }
16042
+ } finally {
16043
+ reader.releaseLock();
16044
+ }
16045
+ const body = new Uint8Array(total);
16046
+ let offset = 0;
16047
+ for (const chunk of chunks) {
16048
+ body.set(chunk, offset);
16049
+ offset += chunk.byteLength;
16050
+ }
16051
+ return JSON.parse(new TextDecoder().decode(body));
16052
+ }
15932
16053
  function createPlaygroundRoutes(connMgr) {
15933
16054
  const app5 = new Hono11();
15934
16055
  app5.post("/playground/chat", async (c) => {
15935
16056
  const runtime = c.get("runtime");
15936
- const body = await c.req.json();
16057
+ let parsedBody;
16058
+ try {
16059
+ parsedBody = await readPlaygroundBody(c.req.raw);
16060
+ } catch (error48) {
16061
+ if (error48 instanceof PlaygroundBodyTooLargeError) {
16062
+ return c.json(
16063
+ {
16064
+ ok: false,
16065
+ error: { code: "REQUEST_TOO_LARGE", message: "playground image request is too large" }
16066
+ },
16067
+ 413
16068
+ );
16069
+ }
16070
+ return c.json(
16071
+ { ok: false, error: { code: "INVALID_INPUT", message: "request body must be valid JSON" } },
16072
+ 400
16073
+ );
16074
+ }
16075
+ if (!parsedBody || typeof parsedBody !== "object" || Array.isArray(parsedBody)) {
16076
+ return c.json(
16077
+ {
16078
+ ok: false,
16079
+ error: { code: "INVALID_INPUT", message: "request body must be a JSON object" }
16080
+ },
16081
+ 400
16082
+ );
16083
+ }
16084
+ const body = parsedBody;
15937
16085
  if (!body.message || typeof body.message !== "string" || !body.message.trim()) {
15938
16086
  return c.json(
15939
16087
  {
@@ -15946,6 +16094,58 @@ function createPlaygroundRoutes(connMgr) {
15946
16094
  400
15947
16095
  );
15948
16096
  }
16097
+ const message = body.message;
16098
+ let image;
16099
+ if (body.image !== void 0) {
16100
+ const candidate = body.image;
16101
+ if (!candidate || typeof candidate !== "object" || !isPlaygroundImageMediaType(candidate.mediaType) || typeof candidate.data !== "string") {
16102
+ return c.json(
16103
+ {
16104
+ ok: false,
16105
+ error: { code: "INVALID_IMAGE", message: "image must be a supported base64 image" }
16106
+ },
16107
+ 400
16108
+ );
16109
+ }
16110
+ if (candidate.data.length > PLAYGROUND_IMAGE_MAX_BASE64_CHARACTERS) {
16111
+ return c.json(
16112
+ { ok: false, error: { code: "IMAGE_TOO_LARGE", message: "image must be at most 5 MiB" } },
16113
+ 400
16114
+ );
16115
+ }
16116
+ const bytes = base64ByteLength(candidate.data);
16117
+ if (bytes === void 0) {
16118
+ return c.json(
16119
+ {
16120
+ ok: false,
16121
+ error: { code: "INVALID_IMAGE", message: "image data must be valid base64" }
16122
+ },
16123
+ 400
16124
+ );
16125
+ }
16126
+ if (bytes > PLAYGROUND_IMAGE_MAX_BYTES) {
16127
+ return c.json(
16128
+ {
16129
+ ok: false,
16130
+ error: {
16131
+ code: "IMAGE_TOO_LARGE",
16132
+ message: `image must be at most ${PLAYGROUND_IMAGE_MAX_BYTES / 1024 / 1024} MiB`
16133
+ }
16134
+ },
16135
+ 400
16136
+ );
16137
+ }
16138
+ if (!hasMatchingImageSignature(candidate.mediaType, candidate.data)) {
16139
+ return c.json(
16140
+ {
16141
+ ok: false,
16142
+ error: { code: "INVALID_IMAGE", message: "image data does not match its media type" }
16143
+ },
16144
+ 400
16145
+ );
16146
+ }
16147
+ image = { mediaType: candidate.mediaType, data: candidate.data };
16148
+ }
15949
16149
  const agents = runtime.getAgents();
15950
16150
  const agent = body.agent ? agents.find((a) => a._name === body.agent) : agents[0];
15951
16151
  if (!agent) {
@@ -15960,14 +16160,18 @@ function createPlaygroundRoutes(connMgr) {
15960
16160
  const sessionId = body.sessionId ?? `playground-${Date.now()}`;
15961
16161
  const store = runtime.getStateStore();
15962
16162
  const history = await store.getSession(sessionId);
15963
- history.push({ role: "user", content: body.message });
16163
+ const persistedHistory = [...history, { role: "user", content: message }];
15964
16164
  const redactOn = runtime.isRedactEnabled();
15965
16165
  const ctx = runtime.createContext({ sessionHistory: history });
15966
16166
  const executionId = ctx.executionId;
15967
16167
  void ctx.events;
15968
16168
  const traceListener = (event) => {
15969
16169
  if (event.executionId !== executionId) return;
15970
- connMgr.broadcastWithWildcard(`execution:${executionId}`, redactStreamEvent(event, redactOn));
16170
+ const safeEvent = image ? sanitizeRichInputFailure(event) : event;
16171
+ connMgr.broadcastWithWildcard(
16172
+ `execution:${executionId}`,
16173
+ redactStreamEvent(safeEvent, redactOn)
16174
+ );
15971
16175
  };
15972
16176
  runtime.on("trace", traceListener);
15973
16177
  (async () => {
@@ -15980,10 +16184,17 @@ function createPlaygroundRoutes(connMgr) {
15980
16184
  });
15981
16185
  try {
15982
16186
  const schema = DEMO_SCHEMA_BY_AGENT[agent._name];
15983
- const result = await ctx.ask(agent, body.message, schema ? { schema } : void 0);
16187
+ const input = image ? [
16188
+ { type: "text", text: message },
16189
+ {
16190
+ type: "image",
16191
+ source: { type: "base64", data: image.data, mediaType: image.mediaType }
16192
+ }
16193
+ ] : message;
16194
+ const result = await ctx.ask(agent, input, schema ? { schema } : void 0);
15984
16195
  const resultText = typeof result === "string" ? result : JSON.stringify(result);
15985
- history.push({ role: "assistant", content: resultText });
15986
- await store.saveSession(sessionId, history);
16196
+ persistedHistory.push({ role: "assistant", content: resultText });
16197
+ await store.saveSession(sessionId, persistedHistory);
15987
16198
  const doneEvent = {
15988
16199
  ...terminalFields(),
15989
16200
  type: "done",
@@ -15997,7 +16208,11 @@ function createPlaygroundRoutes(connMgr) {
15997
16208
  const errorEvent = {
15998
16209
  ...terminalFields(),
15999
16210
  type: "error",
16000
- data: { message: err instanceof Error ? err.message : String(err) }
16211
+ // Media payloads can make providers echo user-controlled content in
16212
+ // errors. The manual terminal event is deliberately fixed and safe.
16213
+ data: {
16214
+ message: image ? "Playground media input failed" : err instanceof Error ? err.message : String(err)
16215
+ }
16001
16216
  };
16002
16217
  connMgr.broadcastWithWildcard(
16003
16218
  `execution:${executionId}`,
@@ -16246,4 +16461,4 @@ export {
16246
16461
  EvalAggregator,
16247
16462
  createServer
16248
16463
  };
16249
- //# sourceMappingURL=chunk-YGLWYDQ3.js.map
16464
+ //# sourceMappingURL=chunk-Z374DI7D.js.map