libfx 0.0.14 → 0.0.15

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/node.cjs CHANGED
@@ -225,14 +225,103 @@ function leaseEnd(backend, deadline) {
225
225
  }
226
226
  var recentIdsKept = 256;
227
227
  var maxBackstopSeconds = 300;
228
+ var base64Letters = encoder.encode("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/");
228
229
  var toBase64 = (bytes) => {
229
- let text = "";
230
- for (let index = 0;index < bytes.length; index += 32768) {
231
- text += String.fromCharCode(...bytes.subarray(index, index + 32768));
232
- }
233
- return btoa(text);
230
+ if (typeof bytes.toBase64 === "function")
231
+ return bytes.toBase64();
232
+ const letters = new Uint8Array(Math.ceil(bytes.length / 3) * 4);
233
+ const whole = bytes.length - bytes.length % 3;
234
+ let at = 0;
235
+ for (let index = 0;index < whole; index += 3) {
236
+ const group = bytes[index] << 16 | bytes[index + 1] << 8 | bytes[index + 2];
237
+ letters[at] = base64Letters[group >> 18];
238
+ letters[at + 1] = base64Letters[group >> 12 & 63];
239
+ letters[at + 2] = base64Letters[group >> 6 & 63];
240
+ letters[at + 3] = base64Letters[group & 63];
241
+ at += 4;
242
+ }
243
+ if (whole < bytes.length) {
244
+ const two = whole + 1 < bytes.length;
245
+ const group = bytes[whole] << 16 | (two ? bytes[whole + 1] << 8 : 0);
246
+ letters[at] = base64Letters[group >> 18];
247
+ letters[at + 1] = base64Letters[group >> 12 & 63];
248
+ letters[at + 2] = two ? base64Letters[group >> 6 & 63] : 61;
249
+ letters[at + 3] = 61;
250
+ }
251
+ return decoder.decode(letters);
234
252
  };
235
- var fromBase64 = (text) => Uint8Array.from(atob(text), (char) => char.charCodeAt(0));
253
+ var fromBase64 = (text) => {
254
+ if (typeof Uint8Array.fromBase64 === "function")
255
+ return Uint8Array.fromBase64(text);
256
+ const binary = atob(text);
257
+ const bytes = new Uint8Array(binary.length);
258
+ for (let index = 0;index < binary.length; index += 1)
259
+ bytes[index] = binary.charCodeAt(index);
260
+ return bytes;
261
+ };
262
+ async function imageName(bytes) {
263
+ const digest = new Uint8Array(await globalThis.crypto.subtle.digest("SHA-256", bytes));
264
+ let name = "";
265
+ for (const byte of digest)
266
+ name += byte.toString(16).padStart(2, "0");
267
+ return name;
268
+ }
269
+ var base64url = (text) => toBase64(encoder.encode(text)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
270
+ var persistenceImages = Symbol.for("libfx.persistenceImages");
271
+ var byteView = (value) => value instanceof ArrayBuffer ? new Uint8Array(value) : ArrayBuffer.isView(value) ? new Uint8Array(value.buffer, value.byteOffset, value.byteLength) : null;
272
+ function promptInputForLog(input, { named = false } = {}) {
273
+ let images = false;
274
+ const blocks = [];
275
+ for (let index = 0;index < input.length; index += 1) {
276
+ const block = input[index];
277
+ if (block?.type !== "image") {
278
+ blocks.push(block);
279
+ continue;
280
+ }
281
+ const image = { ...block };
282
+ if (image.data !== undefined && typeof image.data !== "string")
283
+ throw new TypeError("a durable prompt carries image data as a base64 string");
284
+ if (typeof image.data === "string")
285
+ images = named;
286
+ blocks.push(image);
287
+ }
288
+ return { input: blocks, images };
289
+ }
290
+ async function storePromptImages(images, input) {
291
+ const blocks = [];
292
+ for (const block of input) {
293
+ if (block?.type !== "image" || block.data === undefined) {
294
+ blocks.push(block);
295
+ continue;
296
+ }
297
+ const { data, ...rest } = block;
298
+ const bytes = fromBase64(data);
299
+ const sha256 = await imageName(bytes);
300
+ if (images)
301
+ await images.put(sha256, bytes);
302
+ blocks.push({ ...rest, data: { sha256 } });
303
+ }
304
+ return blocks;
305
+ }
306
+ async function resolvePromptImages(images, input) {
307
+ if (!Array.isArray(input))
308
+ return { input };
309
+ const blocks = [];
310
+ for (const block of input) {
311
+ const name = block?.type === "image" ? block.data?.sha256 : undefined;
312
+ if (typeof name !== "string") {
313
+ blocks.push(block);
314
+ continue;
315
+ }
316
+ const bytes = await images.get(name);
317
+ if (!bytes)
318
+ return { missing: name };
319
+ if (await imageName(bytes) !== name)
320
+ return { missing: name, corrupt: true };
321
+ blocks.push({ ...block, data: toBase64(bytes) });
322
+ }
323
+ return { input: blocks };
324
+ }
236
325
  function memory({ maxDurationMs, reserveMs } = {}) {
237
326
  checkDurations("memory", { maxDurationMs, reserveMs });
238
327
  let backend = null;
@@ -519,7 +608,7 @@ function createMemoryBackend({ maxDurationMs, reserveMs } = {}) {
519
608
  const sessionFor = (id) => {
520
609
  let session = sessions.get(id);
521
610
  if (!session) {
522
- session = { entries: [], head: null, ui: [], uiWaiters: new Set, watchers: new Set };
611
+ session = { entries: [], head: null, ui: [], uiWaiters: new Set, watchers: new Set, images: new Map };
523
612
  sessions.set(id, session);
524
613
  }
525
614
  return session;
@@ -605,6 +694,15 @@ function createMemoryBackend({ maxDurationMs, reserveMs } = {}) {
605
694
  session.watchers.add(fn);
606
695
  return () => session.watchers.delete(fn);
607
696
  },
697
+ images: {
698
+ async put(name, bytes) {
699
+ if (!session.images.has(name))
700
+ session.images.set(name, bytes.slice());
701
+ },
702
+ async get(name) {
703
+ return session.images.get(name)?.slice();
704
+ }
705
+ },
608
706
  ui: {
609
707
  async write(lines) {
610
708
  session.ui.push(...lines);
@@ -660,6 +758,7 @@ function createMemoryBackend({ maxDurationMs, reserveMs } = {}) {
660
758
  }
661
759
  var logStep = "fx.log";
662
760
  var uiStreamOf = (runId) => `${runId.replace(/^wrun_/, "strm_")}_user_bGliZngtdWk`;
761
+ var imageStreamOf = (runId, name) => `${runId.replace(/^wrun_/, "strm_")}_user_${base64url(`libfx-image-${name}`)}`;
663
762
  var queuePrefix = "__libfx_wkf_workflow_";
664
763
  var defaultAgentName = "default";
665
764
  var agentNameRule = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
@@ -705,6 +804,31 @@ var missingRun = (error) => error?.status === 404 || /not found|does not exist|E
705
804
  var streamReadRetries = 5;
706
805
  var missingStreamPollMs = 50;
707
806
  var missingStreamFastMs = 2000;
807
+ var maxLogViews = 32;
808
+ var maxLogViewChars = 32 * 1024 * 1024;
809
+ var gapless = (read) => read.listed === 0 || read.newest - read.lowest + 1 === read.listed;
810
+ var firstEventSlot = 1;
811
+ var listedDownTo = (read, floor) => read.lowest === floor || read.through !== null && read.lowest <= read.through;
812
+ function extendLogView(view, newer) {
813
+ const combined = [...view.entries, ...newer];
814
+ let through = view.through;
815
+ for (let index = combined.length - 1;index >= 0; index -= 1) {
816
+ const entry = combined[index].entry;
817
+ if (entry?.k === "checkpoint") {
818
+ through = entry.through === null || entry.through === undefined ? 0 : Number(entry.through);
819
+ break;
820
+ }
821
+ }
822
+ if (through < view.through)
823
+ return null;
824
+ const entries = combined.filter((item) => Number(item.cursor) > through);
825
+ let head = null;
826
+ for (let index = entries.length - 1;index >= 0 && head === null; index -= 1) {
827
+ if (chainedKind(entries[index].entry?.k))
828
+ head = entries[index].cursor;
829
+ }
830
+ return { entries, through, head: head ?? (through ? String(through) : "null") };
831
+ }
708
832
  var firstPageEvents = 10;
709
833
  var laterPageEvents = 100;
710
834
  var listenersOf = new WeakMap;
@@ -804,6 +928,32 @@ function createWorldBackend(world, durability) {
804
928
  for (const watcher of watchers.get(runId) ?? [])
805
929
  watcher();
806
930
  };
931
+ const logViews = new Map;
932
+ let logViewChars = 0;
933
+ const forgetView = (runId) => {
934
+ const view = logViews.get(runId);
935
+ if (!view)
936
+ return;
937
+ logViewChars -= view.text.length;
938
+ logViews.delete(runId);
939
+ };
940
+ const rememberView = (runId, entries, known, through) => {
941
+ forgetView(runId);
942
+ const text = JSON.stringify(entries);
943
+ if (text.length > maxLogViewChars)
944
+ return;
945
+ logViews.set(runId, { text, known, through });
946
+ logViewChars += text.length;
947
+ for (const oldest of logViews.keys()) {
948
+ if (logViews.size <= maxLogViews && logViewChars <= maxLogViewChars)
949
+ break;
950
+ forgetView(oldest);
951
+ }
952
+ };
953
+ const viewOf = (runId) => {
954
+ const view = logViews.get(runId);
955
+ return view && { entries: JSON.parse(view.text), known: view.known, through: view.through };
956
+ };
807
957
  let handler = null;
808
958
  const route = typeof world.registerHandler !== "function";
809
959
  const listEvents = (runId, cursor, limit) => world.events.list({
@@ -885,6 +1035,7 @@ function createWorldBackend(world, durability) {
885
1035
  },
886
1036
  async session(runId) {
887
1037
  let known = 0;
1038
+ const storedImages = new Set;
888
1039
  const inBandAt = new Map;
889
1040
  let inBand = world.capabilities?.inBandFence === true;
890
1041
  const noteInBand = (cursor, count) => {
@@ -935,9 +1086,12 @@ function createWorldBackend(world, durability) {
935
1086
  let through = null;
936
1087
  let snapshot;
937
1088
  let newestChained;
1089
+ let newest = stopBelow;
1090
+ let lowest = Infinity;
1091
+ let listed = 0;
938
1092
  const done = (head) => {
939
1093
  noteSnapshot(snapshot, newestChained ?? head);
940
- return { events, through };
1094
+ return { events, through, snapshot, newest, lowest, listed };
941
1095
  };
942
1096
  for (let cursor, limit = firstPageEvents;; limit = laterPageEvents) {
943
1097
  let page;
@@ -948,7 +1102,7 @@ function createWorldBackend(world, durability) {
948
1102
  throw error;
949
1103
  if (stopBelow === 0)
950
1104
  noteSnapshot({ seq: 0, seqInBand: inBandAtRunCreation }, "null");
951
- return { events, through };
1105
+ return { events, through, newest, lowest, listed, missing: true };
952
1106
  }
953
1107
  snapshot ??= page.snapshot;
954
1108
  for (const event of page.data) {
@@ -957,6 +1111,11 @@ function createWorldBackend(world, durability) {
957
1111
  known = slot;
958
1112
  if (slot <= stopBelow)
959
1113
  return done(undefined);
1114
+ if (slot > newest)
1115
+ newest = slot;
1116
+ if (slot < lowest)
1117
+ lowest = slot;
1118
+ listed += 1;
960
1119
  if (through !== null && slot <= through)
961
1120
  return done(through === 0 ? "null" : String(through));
962
1121
  const entry = logEntryOf(event);
@@ -975,10 +1134,41 @@ function createWorldBackend(world, durability) {
975
1134
  cursor = page.cursor;
976
1135
  }
977
1136
  };
1137
+ let covered = null;
1138
+ const fullRead = async () => {
1139
+ const read = await newestFirst(0, true);
1140
+ const entries = read.events.reverse();
1141
+ if (!read.missing && listedDownTo(read, firstEventSlot) && gapless(read)) {
1142
+ rememberView(runId, entries, read.newest, read.through ?? 0);
1143
+ covered = read.newest;
1144
+ } else {
1145
+ forgetView(runId);
1146
+ covered = null;
1147
+ }
1148
+ return entries;
1149
+ };
978
1150
  return {
1151
+ newest: () => covered,
979
1152
  async read() {
980
- const { events } = await newestFirst(0, true);
981
- return events.reverse();
1153
+ const view = viewOf(runId);
1154
+ if (!view)
1155
+ return fullRead();
1156
+ if (view.known > known)
1157
+ known = view.known;
1158
+ const since = await newestFirst(view.known, true);
1159
+ if (since.missing) {
1160
+ forgetView(runId);
1161
+ covered = null;
1162
+ return [];
1163
+ }
1164
+ const reaches = since.listed === 0 || listedDownTo(since, view.known + 1);
1165
+ const next = reaches && gapless(since) ? extendLogView(view, since.events.reverse()) : null;
1166
+ if (!next)
1167
+ return fullRead();
1168
+ noteSnapshot(since.snapshot, next.head);
1169
+ rememberView(runId, next.entries, since.newest, next.through);
1170
+ covered = since.newest;
1171
+ return next.entries;
982
1172
  },
983
1173
  async readSince(cursor) {
984
1174
  const { events } = await newestFirst(Number(cursor), false);
@@ -1055,6 +1245,44 @@ function createWorldBackend(world, durability) {
1055
1245
  set.add(fn);
1056
1246
  return () => set.delete(fn);
1057
1247
  },
1248
+ images: {
1249
+ async put(name, bytes) {
1250
+ if (storedImages.has(name))
1251
+ return;
1252
+ if (typeof world.streams.getChunks !== "function")
1253
+ throw new TypeError("a World needs streams.getChunks() to keep a session's images");
1254
+ await ensureRun();
1255
+ const stream = imageStreamOf(runId, name);
1256
+ let present = false;
1257
+ try {
1258
+ present = Number((await world.streams.getInfo(runId, stream))?.tailIndex ?? -1) >= 0;
1259
+ } catch (error) {
1260
+ if (!missingRun(error))
1261
+ throw error;
1262
+ }
1263
+ if (!present)
1264
+ await world.streams.write(runId, stream, bytes);
1265
+ storedImages.add(name);
1266
+ },
1267
+ async get(name) {
1268
+ let page;
1269
+ try {
1270
+ page = await world.streams.getChunks(runId, imageStreamOf(runId, name), { limit: 8 });
1271
+ } catch (error) {
1272
+ if (missingRun(error))
1273
+ return;
1274
+ throw error;
1275
+ }
1276
+ for (const chunk of page?.data ?? []) {
1277
+ const bytes = byteView(chunk?.data);
1278
+ if (bytes && bytes.byteLength > 0 && await imageName(bytes) === name) {
1279
+ storedImages.add(name);
1280
+ return bytes.slice();
1281
+ }
1282
+ }
1283
+ return;
1284
+ }
1285
+ },
1058
1286
  ui: {
1059
1287
  async write(lines) {
1060
1288
  await ensureRun();
@@ -1253,9 +1481,19 @@ async function recordMessage(log, state, message) {
1253
1481
  if (idInUse(state, message))
1254
1482
  throw messageIdInUse(message);
1255
1483
  const entries = messageEntries(state, message);
1484
+ const appended = [];
1256
1485
  for (const entry of entries)
1257
- await log.append(entry);
1258
- return entries.length;
1486
+ appended.push({ cursor: await log.append(entry), entry });
1487
+ return appended;
1488
+ }
1489
+ function handoffEntries(read, newestSeen, appended) {
1490
+ if (!Number.isSafeInteger(newestSeen) || appended.length === 0)
1491
+ return null;
1492
+ for (const [index, item] of appended.entries()) {
1493
+ if (Number(item.cursor) !== newestSeen + index + 1)
1494
+ return null;
1495
+ }
1496
+ return [...read, ...appended.map(({ cursor, entry }) => ({ cursor, entry: JSON.parse(JSON.stringify(entry)) }))];
1259
1497
  }
1260
1498
  function earlyStart(entries, state, message, fold) {
1261
1499
  const writes = messageEntries(state, message);
@@ -1301,9 +1539,9 @@ class SessionWorker {
1301
1539
  fold(entries, delivery = null) {
1302
1540
  return foldSessionLog(entries, { now: Date.now(), alive: (lease) => this.backend.alive(lease, delivery) });
1303
1541
  }
1304
- async consume(message, delivery = null) {
1305
- const log = await this.backend.session(this.sessionId);
1306
- let entries = await log.read();
1542
+ async consume(message, delivery = null, handoff = null) {
1543
+ const log = handoff?.log ?? await this.backend.session(this.sessionId);
1544
+ let entries = handoff?.entries ?? await log.read();
1307
1545
  let state = this.fold(entries, delivery);
1308
1546
  if (!knownMessage(state, message)) {
1309
1547
  this.agent.emit("session.refused", { sessionId: this.sessionId, messageId: message.messageId, reason: "unknown message" });
@@ -1467,6 +1705,7 @@ class SessionWorker {
1467
1705
  }
1468
1706
  const ui = uiWriter(log, (error) => agent.emit("ui.error", { sessionId: this.sessionId, error: error?.name ?? "Error" }), lease.epoch);
1469
1707
  let leaseExpiresAt = lease.expiresAt;
1708
+ let imageReadFailed = null;
1470
1709
  const store = {
1471
1710
  load: async () => ({
1472
1711
  ...state.checkpoint?.data ? { checkpoint: { through: state.checkpoint.through ?? "0", data: fromBase64(state.checkpoint.data) } } : {},
@@ -1474,6 +1713,17 @@ class SessionWorker {
1474
1713
  head: head ?? state.head
1475
1714
  }),
1476
1715
  append: ({ idempotencyKey, data, marks }) => chained({ k: "record", key: idempotencyKey, data: toBase64(data), marks }).then((cursor) => ({ cursor })),
1716
+ [persistenceImages]: {
1717
+ put: (name, bytes) => log.images.put(name, bytes),
1718
+ get: async (name) => {
1719
+ try {
1720
+ return await log.images.get(name);
1721
+ } catch (error) {
1722
+ imageReadFailed = error;
1723
+ throw error;
1724
+ }
1725
+ }
1726
+ },
1477
1727
  saveCheckpoint: async ({ through, data }) => {
1478
1728
  const now = this.fold(await log.read());
1479
1729
  const kept = [...now.unconsumed, ...now.held].filter((input) => input.cursor <= Number(through)).sort((a, b) => a.cursor - b.cursor);
@@ -1545,6 +1795,8 @@ class SessionWorker {
1545
1795
  harnessRun = runOf(state.openTurn ?? state.pending[0]);
1546
1796
  harness = await agent.openHarness(this.sessionId, store, harnessRun, ready);
1547
1797
  } catch (error) {
1798
+ if (imageReadFailed)
1799
+ throw imageReadFailed;
1548
1800
  const waiting = state.openTurn && !state.openFailed ? state.openTurn.id : state.pending[0]?.messageId;
1549
1801
  if (waiting) {
1550
1802
  const summary = errorSummary(error);
@@ -1607,8 +1859,17 @@ class SessionWorker {
1607
1859
  harness = await agent.openHarness(this.sessionId, store, wanted, ready);
1608
1860
  harnessRun = wanted;
1609
1861
  }
1862
+ const withImages = await resolvePromptImages(log.images, next.input);
1863
+ if (withImages.missing) {
1864
+ await log.append({ k: "ended", messageId });
1865
+ const message = withImages.corrupt ? `the session's image store holds other bytes under image ${withImages.missing}` : `the session's image store does not hold image ${withImages.missing}`;
1866
+ ui.push({ type: "turn_end", messageId, stopReason: "error", usage: {}, error: { name: "Error", message } });
1867
+ await ui.settle();
1868
+ state = this.fold(await log.read());
1869
+ continue;
1870
+ }
1610
1871
  try {
1611
- turn = harness.prompt(next.input ?? "", { turnId: messageId, yieldAt });
1872
+ turn = harness.prompt(withImages.input ?? "", { turnId: messageId, yieldAt });
1612
1873
  } catch (error) {
1613
1874
  await log.append({ k: "stopped", messageId });
1614
1875
  return unusable(error);
@@ -2114,14 +2375,14 @@ function createDurableAgentFactory({ harness, defaultDurability, defaultShared =
2114
2375
  const member = { open: backend, durability: durabilityOf, definition: definitionOf(harnessOptions), store: durability ? storeOf(durability) : unresolved, defaults: durability ? null : unresolved };
2115
2376
  claimName(agentName, member);
2116
2377
  const registered = joinGroup(agentsByName, agentName, member);
2117
- const receive = (created, message, delivery, { inRequest = false } = {}) => {
2378
+ const receive = (created, message, delivery, { inRequest = false, handoff = null } = {}) => {
2118
2379
  let worker = workers.get(message.sessionId);
2119
2380
  if (!worker) {
2120
2381
  worker = new SessionWorker(agent, created, message.sessionId);
2121
2382
  workers.set(message.sessionId, worker);
2122
2383
  }
2123
2384
  worker.deliveries += 1;
2124
- let work = worker.consume(message, delivery);
2385
+ let work = worker.consume(message, delivery, handoff);
2125
2386
  if (inRequest) {
2126
2387
  work = work.then(async (result) => {
2127
2388
  if (Number.isFinite(result?.timeoutSeconds)) {
@@ -2178,7 +2439,9 @@ function createDurableAgentFactory({ harness, defaultDurability, defaultShared =
2178
2439
  };
2179
2440
  const full = { ...message, ...extra, agent: agentName, sessionId };
2180
2441
  const log = await created.session(sessionId);
2181
- await recordMessage(log, foldSessionLog(await log.read()), full);
2442
+ const read = await log.read();
2443
+ const newestSeen = log.newest?.();
2444
+ const appended = await recordMessage(log, foldSessionLog(read), full);
2182
2445
  const queued = queuedMessage(full, created.store);
2183
2446
  const idempotencyKey = `${sessionId}:${message.type}:${message.messageId}`;
2184
2447
  if (inRequest && created.runInRequest) {
@@ -2189,7 +2452,9 @@ function createDurableAgentFactory({ harness, defaultDurability, defaultShared =
2189
2452
  const { messageId: backstop } = await created.queue.send(queued, { idempotencyKey, delaySeconds }) ?? {};
2190
2453
  seeded = true;
2191
2454
  const delivery = backstop ? { messageId: backstop, attempt: 0 } : null;
2192
- const work = receive(created, queued, delivery, { inRequest: true }).then(() => {}, () => {});
2455
+ const entries = handoffEntries(read, newestSeen, appended);
2456
+ const handoff = entries ? { log, entries } : null;
2457
+ const work = receive(created, queued, delivery, { inRequest: true, handoff }).then(() => {}, () => {});
2193
2458
  created.waitUntil?.(work);
2194
2459
  return;
2195
2460
  }
@@ -2197,7 +2462,8 @@ function createDurableAgentFactory({ harness, defaultDurability, defaultShared =
2197
2462
  await created.queue.send(queued, { idempotencyKey });
2198
2463
  seeded = true;
2199
2464
  };
2200
- const startTurn = (type, messageId, input, retried = false, aborted = false) => {
2465
+ let storing = null;
2466
+ const startTurn = (type, messageId, input, retried = false, aborted = () => false, ready = null, progress = null) => {
2201
2467
  let accept;
2202
2468
  const accepted = new Promise((resolve, reject) => {
2203
2469
  accept = { resolve, reject };
@@ -2208,15 +2474,23 @@ function createDurableAgentFactory({ harness, defaultDurability, defaultShared =
2208
2474
  resumeRequest: type === "resume" ? messageId : null,
2209
2475
  start: async () => {
2210
2476
  try {
2477
+ const read = input === undefined || ready === null || aborted() ? input : await ready();
2478
+ const message = { type, messageId, ...input === undefined ? {} : { input: read } };
2479
+ const queue = () => {
2480
+ if (progress)
2481
+ progress.sent = true;
2482
+ return send(message, { inRequest: type === "prompt" });
2483
+ };
2211
2484
  const { created, id: sessionId } = await resolved;
2212
2485
  const log = await created.session(sessionId);
2213
2486
  let attaching = type === "resume";
2214
2487
  let from = null;
2215
- if (retried && !aborted) {
2488
+ if (retried)
2216
2489
  from = await log.ui.length();
2490
+ if (retried && !aborted()) {
2217
2491
  const [state] = await Promise.all([
2218
2492
  log.read().then(foldSessionLog),
2219
- send({ type, messageId, ...input === undefined ? {} : { input } }, { inRequest: type === "prompt" }).then(() => accept.resolve({ messageId, sessionId }))
2493
+ queue().then(() => accept.resolve({ messageId, sessionId }))
2220
2494
  ]);
2221
2495
  const outcome = await retryOutcome(log, state, messageId, from, { sent: true });
2222
2496
  if (outcome.settled)
@@ -2226,7 +2500,6 @@ function createDurableAgentFactory({ harness, defaultDurability, defaultShared =
2226
2500
  return { log, from, attaching: outcome.follow === true };
2227
2501
  }
2228
2502
  if (retried) {
2229
- from = await log.ui.length();
2230
2503
  const outcome = await retryOutcome(log, foldSessionLog(await log.read()), messageId, from, { sent: false });
2231
2504
  if (outcome.follow)
2232
2505
  attaching = true;
@@ -2237,12 +2510,14 @@ function createDurableAgentFactory({ harness, defaultDurability, defaultShared =
2237
2510
  return { log, from, attaching: true, endWithinMs: retryEndWaitMs };
2238
2511
  }
2239
2512
  }
2240
- if (aborted && !attaching) {
2513
+ from ??= await log.ui.length();
2514
+ if (aborted()) {
2241
2515
  accept.resolve({ messageId, sessionId });
2242
- return { settled: { messageId, stopReason: "cancelled", usage: {} } };
2516
+ if (!attaching)
2517
+ return { settled: { messageId, stopReason: "cancelled", usage: {} } };
2518
+ return { log, from, attaching };
2243
2519
  }
2244
- from ??= await log.ui.length();
2245
- await send({ type, messageId, ...input === undefined ? {} : { input } }, { inRequest: type === "prompt" });
2520
+ await queue();
2246
2521
  accept.resolve({ messageId, sessionId });
2247
2522
  return { log, from, attaching };
2248
2523
  } catch (error) {
@@ -2265,17 +2540,64 @@ function createDurableAgentFactory({ harness, defaultDurability, defaultShared =
2265
2540
  throw new TypeError(`messageId must be ${idRule}`);
2266
2541
  if (typeof input !== "string" && !Array.isArray(input))
2267
2542
  throw new TypeError("prompt input must be a string or an array of content blocks");
2268
- if (Array.isArray(input) && input.some((block) => block?.type === "image" && block.data !== undefined && typeof block.data !== "string")) {
2269
- throw new TypeError("a durable prompt carries image data as a base64 string");
2270
- }
2271
- admit(input);
2272
2543
  const signal = promptOptions.signal;
2273
2544
  if (signal !== undefined && typeof signal?.addEventListener !== "function")
2274
2545
  throw new TypeError("prompt signal must be an AbortSignal");
2275
- const turn = startTurn("prompt", messageId, input, promptOptions.messageId !== undefined, signal?.aborted === true);
2546
+ const aborted = signal?.aborted === true;
2547
+ const retried = promptOptions.messageId !== undefined;
2548
+ const named = !aborted;
2549
+ const stored = Array.isArray(input) ? promptInputForLog(input, { named }) : { input, images: false };
2550
+ admit(stored.input);
2551
+ plugged.checkPrompt?.(stored.input);
2552
+ const before = storing;
2553
+ const ready = before || stored.images ? async () => {
2554
+ let release = () => {};
2555
+ const abortion = signal === undefined ? null : new Promise((resolve) => {
2556
+ signal.addEventListener("abort", resolve, { once: true });
2557
+ release = () => signal.removeEventListener("abort", resolve);
2558
+ });
2559
+ const unlessAborted = (promise) => {
2560
+ if (abortion === null)
2561
+ return promise;
2562
+ promise.catch(() => {});
2563
+ return Promise.race([promise, abortion]);
2564
+ };
2565
+ try {
2566
+ await unlessAborted(before ?? Promise.resolve());
2567
+ if (signal?.aborted)
2568
+ return null;
2569
+ let read = stored.input;
2570
+ if (stored.images) {
2571
+ const { created, id: sessionId } = await resolved;
2572
+ const log = await created.session(sessionId);
2573
+ const known = retried && knownMessage(foldSessionLog(await log.read()), { type: "prompt", messageId });
2574
+ if (signal?.aborted)
2575
+ return null;
2576
+ read = await unlessAborted(storePromptImages(known ? null : log.images, read));
2577
+ if (signal?.aborted)
2578
+ return null;
2579
+ }
2580
+ if (closed)
2581
+ throw new Error(`${label} is closed`);
2582
+ return read;
2583
+ } finally {
2584
+ release();
2585
+ }
2586
+ } : null;
2587
+ const progress = { sent: false };
2588
+ const turn = startTurn("prompt", messageId, stored.input, retried, () => signal?.aborted === true, ready, progress);
2589
+ if (stored.images && !aborted) {
2590
+ const settled = Promise.all([before, turn.accepted.then(() => {}, () => {})]).then(() => {});
2591
+ storing = settled;
2592
+ settled.then(() => {
2593
+ if (storing === settled)
2594
+ storing = null;
2595
+ });
2596
+ }
2276
2597
  if (signal !== undefined && !signal.aborted) {
2277
2598
  const stop = () => {
2278
- send({ type: "cancel", messageId: newId("cnl"), target: messageId }).catch(() => {});
2599
+ if (progress.sent)
2600
+ send({ type: "cancel", messageId: newId("cnl"), target: messageId }).catch(() => {});
2279
2601
  };
2280
2602
  signal.addEventListener("abort", stop, { once: true });
2281
2603
  turn.result.finally(() => signal.removeEventListener("abort", stop)).catch(() => {});
@@ -2373,7 +2695,14 @@ function createDurableAgentFactory({ harness, defaultDurability, defaultShared =
2373
2695
  if (!validId(messageId))
2374
2696
  throw new TypeError(`messageId must be ${idRule}`);
2375
2697
  admit(input);
2376
- await send({ type: "steer", messageId, input });
2698
+ let named = input;
2699
+ if (Array.isArray(input) && input.some((block) => block?.type === "image" && typeof block.data === "string")) {
2700
+ const { created, id: sessionId } = await resolved;
2701
+ named = await storePromptImages((await created.session(sessionId)).images, input);
2702
+ if (closed)
2703
+ throw new Error(`${label} is closed`);
2704
+ }
2705
+ await send({ type: "steer", messageId, input: named });
2377
2706
  return { messageId };
2378
2707
  },
2379
2708
  async cancel() {
@@ -2391,6 +2720,12 @@ function createDurableAgentFactory({ harness, defaultDurability, defaultShared =
2391
2720
  }),
2392
2721
  append: async () => {
2393
2722
  throw new Error("a checkpoint reads the session without changing it");
2723
+ },
2724
+ [persistenceImages]: {
2725
+ get: (name) => log.images.get(name),
2726
+ put: async () => {
2727
+ throw new Error("a checkpoint reads the session without changing it");
2728
+ }
2394
2729
  }
2395
2730
  };
2396
2731
  const harness = await agent.openHarness(sessionId, readOnly, null);
@@ -3102,6 +3437,7 @@ var maxPromptImagesDataBytes = 8 * 1024 * 1024;
3102
3437
  var maxPromptImageBytes = maxPromptImageDataBytes / 4 * 3;
3103
3438
  var maxPromptImagesBytes = maxPromptImagesDataBytes / 4 * 3;
3104
3439
  var maxPendingAttachments = 8;
3440
+ var maxPendingAttachmentBytes = 8 * 1024 * 1024;
3105
3441
  var maxOutboundAttachments = 4;
3106
3442
  var maxCheckpointBytes = 4 * 1024 * 1024;
3107
3443
  var maxJournalBytes = 4 * 1024 * 1024;
@@ -3249,6 +3585,9 @@ function normalizeAgentOptions(value) {
3249
3585
  Object.assign(options, normalizeModelChoice(options));
3250
3586
  if (custom && options.model === undefined)
3251
3587
  throw new TypeError("a custom provider needs a model");
3588
+ if (subscription && options.model === undefined) {
3589
+ throw new TypeError(`provider "${options.provider}" needs a model; fx's default model is an AI Gateway id`);
3590
+ }
3252
3591
  validateGatewayChatUrl(options.gatewayChatUrl);
3253
3592
  if (options.resizeImage !== undefined && typeof options.resizeImage !== "function") {
3254
3593
  throw new TypeError("resizeImage must be a function");
@@ -3278,6 +3617,8 @@ function normalizeAgentOptions(value) {
3278
3617
  }
3279
3618
  var engineInternals = Symbol.for("libfx.engineInternals");
3280
3619
  var journalMarksWanted = Symbol.for("libfx.journalMarks");
3620
+ var persistenceImages2 = Symbol.for("libfx.persistenceImages");
3621
+ var imageWindowLimits = Symbol.for("libfx.imageWindow");
3281
3622
  var coreAnswered = Symbol.for("libfx.coreAnswered");
3282
3623
 
3283
3624
  class FxFencedError extends Error {
@@ -3304,8 +3645,23 @@ var journalOpenErrorCodes = new Map([
3304
3645
  ["Invalid libfx journal", "FX_JOURNAL_INVALID"],
3305
3646
  ["libfx journal events are out of order", "FX_JOURNAL_INVALID"],
3306
3647
  ["libfx journal is too large", "FX_JOURNAL_TOO_LARGE"],
3307
- ["libfx journal holds more than 1024 turns", "FX_JOURNAL_TOO_LARGE"]
3648
+ ["libfx journal holds more than 1024 turns", "FX_JOURNAL_TOO_LARGE"],
3649
+ ["libfx journal images are too large", "FX_JOURNAL_TOO_LARGE"],
3650
+ ["Invalid libfx journal images", "FX_JOURNAL_INVALID"],
3651
+ ["libfx image does not match its name", "FX_JOURNAL_IMAGE_MISSING"]
3308
3652
  ]);
3653
+ async function imageName2(bytes) {
3654
+ const digest = new Uint8Array(await globalThis.crypto.subtle.digest("SHA-256", bytes));
3655
+ let name = "";
3656
+ for (const byte of digest)
3657
+ name += byte.toString(16).padStart(2, "0");
3658
+ return name;
3659
+ }
3660
+ function imageMissingError(name, cause) {
3661
+ const error = journalLoadError(`libfx journal names an image the store does not hold: ${name}`, "FX_JOURNAL_IMAGE_MISSING", cause);
3662
+ error.image = name;
3663
+ return error;
3664
+ }
3309
3665
  function createMemoryPersistence() {
3310
3666
  const records = [];
3311
3667
  let checkpoint = null;
@@ -3341,6 +3697,7 @@ function createMemoryPersistence() {
3341
3697
  var resumeTurn = Symbol("libfx.resume");
3342
3698
  var followUpInput = Symbol("libfx.followUp");
3343
3699
  var rerunCallIds = Symbol("libfx.rerun");
3700
+ var resumeOutputShown = Symbol.for("libfx.resumeOutputShown");
3344
3701
  function journalAppendError(cause) {
3345
3702
  const error = new Error("libfx journal append failed", { cause });
3346
3703
  error.code = "FX_JOURNAL_APPEND_FAILED";
@@ -3466,6 +3823,7 @@ async function loadPersistence(persistence) {
3466
3823
  }
3467
3824
  return { checkpoint, events, head, tailBytes: bytes, lastSeq: events.at(-1)?.seq ?? covered };
3468
3825
  }
3826
+ var waitedForEvents = new Set(["input_accepted", "input_withdrawn", "turn_committed", "turn_progress_cleared", "turn_yielded"]);
3469
3827
  function journalMarks(events) {
3470
3828
  const marks = [];
3471
3829
  for (const event of events) {
@@ -5027,7 +5385,7 @@ function promptImageDataBytes(prompt) {
5027
5385
  return total + Math.ceil(byteLength / 3) * 4;
5028
5386
  }, 0);
5029
5387
  }
5030
- function byteView(value) {
5388
+ function byteView2(value) {
5031
5389
  if (value instanceof ArrayBuffer)
5032
5390
  return new Uint8Array(value);
5033
5391
  if (ArrayBuffer.isView(value))
@@ -5088,7 +5446,7 @@ function normalizePromptImage(block, index) {
5088
5446
  throw new TypeError(`image prompt block ${index} requires canonical base64 data`);
5089
5447
  return { type: "image", source: "base64", data: block.data, mimeType: block.mimeType, byteLength, ...reference };
5090
5448
  }
5091
- const bytes = typeof block.data === "string" ? null : byteView(block.data);
5449
+ const bytes = typeof block.data === "string" ? null : byteView2(block.data);
5092
5450
  if (!bytes || bytes.byteLength === 0) {
5093
5451
  throw new TypeError(`image prompt block ${index} requires base64 data, bytes, or a Blob`);
5094
5452
  }
@@ -5146,6 +5504,52 @@ function normalizePromptInput(input, { deferImageLimits = false } = {}) {
5146
5504
  }
5147
5505
  return prompt;
5148
5506
  }
5507
+ var imageSignatures = [
5508
+ ["image/png", 0, [137, 80, 78, 71, 13, 10, 26, 10]],
5509
+ ["image/jpeg", 0, [255, 216, 255]],
5510
+ ["image/gif", 0, [71, 73, 70, 56, 55, 97]],
5511
+ ["image/gif", 0, [71, 73, 70, 56, 57, 97]],
5512
+ ["image/webp", 0, [82, 73, 70, 70]]
5513
+ ];
5514
+ var imageTypes = new Set(imageSignatures.map(([type]) => type));
5515
+ var webpMarker = [87, 69, 66, 80];
5516
+ function imageTypeOf(bytes) {
5517
+ const startsWith = (offset, signature) => bytes.length >= offset + signature.length && signature.every((byte, index) => bytes[offset + index] === byte);
5518
+ for (const [type, offset, signature] of imageSignatures) {
5519
+ if (!startsWith(offset, signature))
5520
+ continue;
5521
+ if (type === "image/webp" && !startsWith(8, webpMarker))
5522
+ continue;
5523
+ return type;
5524
+ }
5525
+ return null;
5526
+ }
5527
+ function checkImageBytesType(bytes, mimeType, index) {
5528
+ const type = imageTypeOf(bytes);
5529
+ if (type === null)
5530
+ throw new TypeError(`image prompt block ${index} is not a PNG, JPEG, GIF or WebP image`);
5531
+ if (type !== mimeType)
5532
+ throw new TypeError(`image prompt block ${index} holds ${type} data, not ${mimeType}`);
5533
+ }
5534
+ function checkPromptInput(input, { resizeImage } = {}) {
5535
+ const resized = resizeImage !== undefined;
5536
+ const normalized = normalizePromptInput(input, { deferImageLimits: resized });
5537
+ if (!normalized.some((block) => block.type === "image" || block.type === "text" && block.text.length > 0)) {
5538
+ throw new TypeError("prompt is empty");
5539
+ }
5540
+ checkPromptFrameSize(normalized, !resized);
5541
+ normalized.forEach((block, index) => {
5542
+ if (block.type !== "image" || resized && block.data !== undefined)
5543
+ return;
5544
+ if (!imageTypes.has(block.mimeType))
5545
+ throw new TypeError(`image prompt block ${index} mimeType must be image/png, image/jpeg, image/gif or image/webp`);
5546
+ if (block.source === "bytes")
5547
+ checkImageBytesType(block.data, block.mimeType, index);
5548
+ else if (block.source === "base64")
5549
+ checkImageBytesType(base64ToBytes(block.data.slice(0, 16)), block.mimeType, index);
5550
+ });
5551
+ return normalized;
5552
+ }
5149
5553
  function promptFrameSize(prompt, countImages = true) {
5150
5554
  const encodedImageBytes = countImages ? promptImageDataBytes(prompt) : 0;
5151
5555
  const projected = prompt.map((block) => block.type !== "image" ? block : {
@@ -5170,7 +5574,7 @@ function preparePromptImages(blocks) {
5170
5574
  });
5171
5575
  }
5172
5576
  function resizedPromptImage(value, index) {
5173
- const bytes = byteView(value?.bytes);
5577
+ const bytes = byteView2(value?.bytes);
5174
5578
  if (!bytes || bytes.byteLength === 0 || typeof value.mimeType !== "string" || value.mimeType.length === 0 || value.mimeType.length > 128) {
5175
5579
  throw new TypeError(`resizeImage must return non-empty bytes and a mimeType for image prompt block ${index}`);
5176
5580
  }
@@ -5217,6 +5621,10 @@ async function materializePromptImages(blocks, isCancelled, resizeImage) {
5217
5621
  if (promptFrameSize(prepared) > maxPromptFrameBytes)
5218
5622
  prepared = omitReferencedImageData(prepared);
5219
5623
  checkPromptFrameSize(prepared, true);
5624
+ prepared.forEach((image, index) => {
5625
+ if (image.type === "image" && image.bytes !== undefined)
5626
+ checkImageBytesType(image.bytes, image.mimeType, index);
5627
+ });
5220
5628
  return prepared;
5221
5629
  }
5222
5630
  function normalizeSteeringInput(input) {
@@ -5257,8 +5665,6 @@ function normalizeHostTools(value) {
5257
5665
  } else {
5258
5666
  throw new TypeError("tools must be an array or an object");
5259
5667
  }
5260
- if (entries.length > 64)
5261
- throw new RangeError("tools cannot contain more than 64 entries");
5262
5668
  const descriptors = [];
5263
5669
  const executors = new Map;
5264
5670
  const names = new Set;
@@ -5351,7 +5757,7 @@ function normalizeInstructions(value) {
5351
5757
  }
5352
5758
  return instructions;
5353
5759
  }
5354
- function hostToolContent(value) {
5760
+ function hostToolContent(value, stored = null) {
5355
5761
  if (value?.type === "libfx.tool-result") {
5356
5762
  if (typeof value.text !== "string" || !Array.isArray(value.images) || value.images.length > 8) {
5357
5763
  throw new TypeError("invalid typed tool result");
@@ -5364,11 +5770,13 @@ function hostToolContent(value) {
5364
5770
  if (!referenceOnly && typeof image.data !== "string" || typeof image.mimeType !== "string" || image.mimeType.length === 0 || image.mimeType.length > 128 || image.data?.length > maxPromptImageDataBytes && sourceRef === undefined) {
5365
5771
  throw new TypeError("invalid tool image");
5366
5772
  }
5773
+ const data = !referenceOnly && image.data.length <= maxPromptImageDataBytes ? image.data : undefined;
5367
5774
  return {
5368
5775
  type: "image",
5369
- ...!referenceOnly && image.data.length <= maxPromptImageDataBytes ? { data: image.data } : {},
5776
+ ...data === undefined ? {} : { data },
5370
5777
  mimeType: image.mimeType,
5371
- ...sourceRef === undefined ? {} : { sourceRef }
5778
+ ...sourceRef === undefined ? {} : { sourceRef },
5779
+ ...data !== undefined && stored?.has(data) ? { stored: stored.get(data) } : {}
5372
5780
  };
5373
5781
  });
5374
5782
  if (promptImageDataBytes(images) > maxPromptImagesDataBytes)
@@ -5382,7 +5790,7 @@ function hostToolContent(value) {
5382
5790
  }
5383
5791
  if (encoder4.encode(content).length > maxPromptImagesDataBytes)
5384
5792
  throw new RangeError("typed tool result exceeds the result limit");
5385
- return { content, rich: true, isError: value.isError === true };
5793
+ return { content, rich: true, isError: value.isError === true, images };
5386
5794
  }
5387
5795
  if (typeof value === "string")
5388
5796
  return { content: value, rich: false };
@@ -5483,6 +5891,8 @@ async function createFxEngine(options = {}) {
5483
5891
  let coreExitError = null;
5484
5892
  const isCurrentTurn = (turn) => turn && activeTurn === turn && !turn.cancelled && !closing;
5485
5893
  const persistence = options.persistence ?? null;
5894
+ const imageStorage = persistence?.[persistenceImages2] ?? null;
5895
+ const imageStore = imageStorage !== null;
5486
5896
  const wantsMarks = persistence?.marks === true || persistence?.[journalMarksWanted] === true;
5487
5897
  const checkpointAfterBytes = options.checkpointAfterBytes ?? defaultCheckpointAfterBytes;
5488
5898
  let journalHead = null;
@@ -5593,7 +6003,7 @@ async function createFxEngine(options = {}) {
5593
6003
  return;
5594
6004
  }
5595
6005
  journalQueue.push(...events);
5596
- if (barrier || events.some((event) => event?.type === "input_accepted" || event?.type === "input_withdrawn"))
6006
+ if (barrier || events.some((event) => waitedForEvents.has(event?.type)))
5597
6007
  journalBarrier = true;
5598
6008
  if (events.every((event) => event?.type === "session_config"))
5599
6009
  return;
@@ -5759,6 +6169,28 @@ async function createFxEngine(options = {}) {
5759
6169
  }
5760
6170
  throw new Error("transport retry exhausted");
5761
6171
  };
6172
+ const storedToolContent = async (value) => {
6173
+ const normalized = hostToolContent(value);
6174
+ if (!imageStore || !normalized.images?.some((image) => image.data !== undefined))
6175
+ return normalized;
6176
+ const stored = new Map;
6177
+ for (const image of normalized.images) {
6178
+ if (image.data === undefined || stored.has(image.data))
6179
+ continue;
6180
+ let bytes;
6181
+ try {
6182
+ bytes = base64ToBytes(image.data);
6183
+ } catch {
6184
+ continue;
6185
+ }
6186
+ const name = await imageName2(bytes);
6187
+ try {
6188
+ await imageStorage.put(name, bytes);
6189
+ stored.set(image.data, name);
6190
+ } catch {}
6191
+ }
6192
+ return stored.size ? hostToolContent(value, stored) : normalized;
6193
+ };
5762
6194
  const executeHostTool = async (name, input, requestedSessionId, toolCallId) => {
5763
6195
  const execute = hostTools.executors.get(name);
5764
6196
  const issuerSessionId = requestedSessionId ?? sessionId;
@@ -5792,7 +6224,7 @@ async function createFxEngine(options = {}) {
5792
6224
  });
5793
6225
  const value = await Promise.race([execution, aborted]);
5794
6226
  if (!controller.signal.aborted) {
5795
- const normalized = hostToolContent(value);
6227
+ const normalized = await storedToolContent(value);
5796
6228
  content = normalized.content;
5797
6229
  rich = normalized.rich;
5798
6230
  isError = normalized.isError === true;
@@ -5801,7 +6233,7 @@ async function createFxEngine(options = {}) {
5801
6233
  isError = true;
5802
6234
  if (error?.toolResult?.type === "libfx.tool-result") {
5803
6235
  try {
5804
- const normalized = hostToolContent(error.toolResult);
6236
+ const normalized = await storedToolContent(error.toolResult);
5805
6237
  content = normalized.content;
5806
6238
  rich = normalized.rich;
5807
6239
  } catch {
@@ -5891,10 +6323,27 @@ async function createFxEngine(options = {}) {
5891
6323
  throw new Error("fx returned an invalid checkpoint");
5892
6324
  return new Uint8Array(bytes.buffer, bytes.byteOffset, bytes.byteLength);
5893
6325
  }
5894
- const sendPrompt = (blocks, resuming = false, followUp = null, turnId = null, rerun = [], yieldAt = undefined) => {
6326
+ async function storePromptImages(blocks, stopped) {
6327
+ const named = [];
6328
+ for (const block of blocks) {
6329
+ if (block.type !== "image" || block.bytes === undefined) {
6330
+ named.push(block);
6331
+ continue;
6332
+ }
6333
+ const name = await imageName2(block.bytes);
6334
+ if (stopped())
6335
+ return null;
6336
+ await imageStorage.put(name, block.bytes);
6337
+ if (stopped())
6338
+ return null;
6339
+ named.push({ ...block, stored: name });
6340
+ }
6341
+ return named;
6342
+ }
6343
+ const sendPrompt = (blocks, resuming = false, followUp = null, turnId = null, rerun = [], yieldAt = undefined, outputShown = false) => {
5895
6344
  const stopAt = journal && yieldAt !== undefined ? { yieldAt } : {};
5896
6345
  if (resuming) {
5897
- return request("session/prompt", { sessionId, prompt: [], _meta: { fx: { continueRecovery: true, ...rerun.length ? { rerun } : {}, ...stopAt } } });
6346
+ return request("session/prompt", { sessionId, prompt: [], _meta: { fx: { continueRecovery: true, ...outputShown ? {} : { freshOutput: true }, ...rerun.length ? { rerun } : {}, ...stopAt } } });
5898
6347
  }
5899
6348
  const images = blocks.filter((block) => block.type === "image" && block.bytes !== undefined);
5900
6349
  const ids = images.length ? attachBytes(images.map((block) => block.bytes)) : [];
@@ -5903,7 +6352,7 @@ async function createFxEngine(options = {}) {
5903
6352
  type: "image",
5904
6353
  mimeType: block.mimeType,
5905
6354
  ...block.sourceRef === undefined ? {} : { sourceRef: block.sourceRef },
5906
- ...block.bytes === undefined ? {} : { _meta: { fx: { attachment: ids[next++] } } }
6355
+ ...block.bytes === undefined ? {} : { _meta: { fx: { attachment: ids[next++], ...block.stored === undefined ? {} : { stored: block.stored } } } }
5907
6356
  });
5908
6357
  const fx = {
5909
6358
  ...followUp ? { inputId: followUp.id, inputAccepted: followUp.accepted } : {},
@@ -6031,21 +6480,10 @@ async function createFxEngine(options = {}) {
6031
6480
  }
6032
6481
  if (journal) {
6033
6482
  const { events, checkpoint } = loaded;
6034
- const params = { sessionId, ...options.inputsDurable === true ? { inputsDurable: true } : {} };
6035
- const attachments = [];
6036
- if (events.length > 0) {
6037
- const bytes = encoder4.encode(JSON.stringify(withoutSupersededProgress(events)));
6038
- if (bytes.byteLength > maxJournalBytes)
6039
- throw journalLoadError("libfx journal is too large", "FX_JOURNAL_TOO_LARGE");
6040
- attachments.push(["journalAttachment", bytes]);
6041
- }
6042
- if (checkpoint)
6043
- attachments.push(["snapshotAttachment", checkpoint]);
6044
- const ids = attachments.length ? attachBytes(attachments.map(([, bytes]) => bytes)) : [];
6045
- attachments.forEach(([name], index) => {
6046
- params[name] = ids[index];
6047
- });
6048
- const opened = await request("libfx/journal_open", params).catch((error) => {
6483
+ const journalBytes = events.length > 0 ? encoder4.encode(JSON.stringify(withoutSupersededProgress(events))) : null;
6484
+ if (journalBytes && journalBytes.byteLength > maxJournalBytes)
6485
+ throw journalLoadError("libfx journal is too large", "FX_JOURNAL_TOO_LARGE");
6486
+ const openFailed = (error) => {
6049
6487
  const message = error?.message ?? "";
6050
6488
  if (/newer fx/.test(message))
6051
6489
  throw new FxJournalVersionError(message);
@@ -6053,7 +6491,56 @@ async function createFxEngine(options = {}) {
6053
6491
  if (code)
6054
6492
  throw journalLoadError(message, code, error);
6055
6493
  throw error;
6056
- });
6494
+ };
6495
+ const openOnce = () => {
6496
+ const params = {
6497
+ sessionId,
6498
+ ...options.inputsDurable === true ? { inputsDurable: true } : {},
6499
+ ...imageStore ? { imageStore: true } : {},
6500
+ ...imageStore && options[imageWindowLimits] ? { imageWindow: options[imageWindowLimits] } : {}
6501
+ };
6502
+ const attachments = [];
6503
+ if (journalBytes)
6504
+ attachments.push(["journalAttachment", journalBytes]);
6505
+ if (checkpoint)
6506
+ attachments.push(["snapshotAttachment", checkpoint]);
6507
+ const ids = attachments.length ? attachBytes(attachments.map(([, bytes]) => bytes)) : [];
6508
+ attachments.forEach(([name], index) => {
6509
+ params[name] = ids[index];
6510
+ });
6511
+ return request("libfx/journal_open", params).catch(openFailed);
6512
+ };
6513
+ const stageImages = async (names) => {
6514
+ let batch = [];
6515
+ let batchBytes = 0;
6516
+ const flush = async () => {
6517
+ if (batch.length === 0)
6518
+ return;
6519
+ const ids = attachBytes(batch.map(({ bytes }) => bytes));
6520
+ const images = batch.map(({ name }, index) => ({ name, attachment: ids[index] }));
6521
+ batch = [];
6522
+ batchBytes = 0;
6523
+ await request("libfx/journal_images", { sessionId, images }).catch(openFailed);
6524
+ };
6525
+ for (const name of new Set(names.map(String))) {
6526
+ const stored = imageStore ? await imageStorage.get(name) : undefined;
6527
+ if (stored === undefined || stored === null)
6528
+ throw imageMissingError(name);
6529
+ const bytes = persistenceBytes(stored, "an image the image store returns");
6530
+ if (batch.length === maxPendingAttachments || batchBytes + bytes.byteLength > maxPendingAttachmentBytes)
6531
+ await flush();
6532
+ batch.push({ name, bytes });
6533
+ batchBytes += bytes.byteLength;
6534
+ }
6535
+ await flush();
6536
+ };
6537
+ let opened = await openOnce();
6538
+ if (Array.isArray(opened?.imagesNeeded)) {
6539
+ await stageImages(opened.imagesNeeded);
6540
+ opened = await openOnce();
6541
+ if (Array.isArray(opened?.imagesNeeded))
6542
+ throw imageMissingError(String(opened.imagesNeeded[0]));
6543
+ }
6057
6544
  compareCheckpointMeta(opened?.checkpointMeta);
6058
6545
  resumable = opened?.resumable === true;
6059
6546
  openTurnId = resumable && validTurnId(opened?.openTurnId) ? opened.openTurnId : null;
@@ -6375,10 +6862,9 @@ async function createFxEngine(options = {}) {
6375
6862
  const resizeImage = options.resizeImage;
6376
6863
  const resuming = promptOptions[resumeTurn] === true;
6377
6864
  const followUp = promptOptions[followUpInput] ?? null;
6378
- const normalized = resuming ? [] : normalizePromptInput(input, { deferImageLimits: resizeImage !== undefined });
6865
+ const normalized = resuming ? [] : checkPromptInput(input, { resizeImage });
6379
6866
  const hasPixels = normalized.some((block) => block.type === "image" && block.data !== undefined);
6380
- const asyncImages = hasPixels && (resizeImage !== undefined || normalized.some((block) => block.type === "image" && block.source === "blob"));
6381
- checkPromptFrameSize(normalized, resizeImage === undefined);
6867
+ const asyncImages = hasPixels && (resizeImage !== undefined || imageStore || normalized.some((block) => block.type === "image" && block.source === "blob"));
6382
6868
  const prompt = asyncImages ? normalized.map((block) => block.type === "image" && block.source === "bytes" ? { ...block, data: block.data.slice() } : block) : preparePromptImages(normalized);
6383
6869
  const signal = promptOptions.signal;
6384
6870
  if (signal !== undefined && (typeof signal?.addEventListener !== "function" || typeof signal?.removeEventListener !== "function"))
@@ -6611,8 +7097,9 @@ async function createFxEngine(options = {}) {
6611
7097
  }) : null;
6612
7098
  let response;
6613
7099
  if (asyncImages) {
7100
+ const stopped = () => cancelled || closing;
6614
7101
  response = Promise.race([
6615
- Promise.resolve().then(() => materializePromptImages(prompt, () => cancelled || closing, resizeImage)),
7102
+ Promise.resolve().then(() => materializePromptImages(prompt, stopped, resizeImage)).then((prepared) => prepared && imageStore ? storePromptImages(prepared, stopped) : prepared),
6616
7103
  imagePrepCancelled
6617
7104
  ]).then((prepared) => {
6618
7105
  cancelImagePrep = null;
@@ -6628,12 +7115,14 @@ async function createFxEngine(options = {}) {
6628
7115
  });
6629
7116
  } else {
6630
7117
  try {
6631
- response = sendPrompt(prompt, resuming, followUp, turn.id, promptOptions[rerunCallIds] ?? [], yieldAt);
7118
+ response = sendPrompt(prompt, resuming, followUp, turn.id, promptOptions[rerunCallIds] ?? [], yieldAt, promptOptions[resumeOutputShown] === true);
6632
7119
  } catch (error) {
6633
7120
  response = Promise.reject(error);
6634
7121
  }
6635
7122
  }
6636
- turn.result = response.then((value) => {
7123
+ turn.result = response.then(async (value) => {
7124
+ if (journal)
7125
+ await journalSettled().catch(() => {});
6637
7126
  if (journalFailure)
6638
7127
  throw reportJournalFailure();
6639
7128
  if (value.stopReason === "yielded" && !cancelled) {
@@ -6713,6 +7202,9 @@ function fxHarness({ createEngine, defaultApiKey = async () => {
6713
7202
  return {
6714
7203
  ...checkpoint === undefined ? {} : { seed: snapshotOf(checkpointBytesOf(checkpoint)) },
6715
7204
  settings: sessionSettings,
7205
+ checkPrompt(input) {
7206
+ checkPromptInput(input, { resizeImage: engineOptions.resizeImage });
7207
+ },
6716
7208
  async open({ sessionId, store, context, settings, durability, ready }) {
6717
7209
  const apiKey = engineOptions.apiKey ?? await defaultApiKey(durability);
6718
7210
  const engine = await createEngine({
@@ -6811,7 +7303,7 @@ function fxSession(engine, idempotent) {
6811
7303
  throw error;
6812
7304
  }
6813
7305
  },
6814
- resume: ({ rerun = true, ...options } = {}) => fxTurn(engine.resume({ ...options, onAmbiguous: (call) => rerun && idempotent.has(call.name) ? "rerun" : undefined }), internals.exited),
7306
+ resume: ({ rerun = true, ...options } = {}) => fxTurn(engine.resume({ ...options, [resumeOutputShown]: true, onAmbiguous: (call) => rerun && idempotent.has(call.name) ? "rerun" : undefined }), internals.exited),
6815
7307
  get openTurn() {
6816
7308
  return internals.openTurn;
6817
7309
  },