libfx 0.0.14-dev.1561.g1a070af32cdf → 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/README.md +9 -6
- package/durable.js +490 -50
- package/fx-core.wasm +0 -0
- package/fx-harness.js +9 -2
- package/fx-sdk.js +237 -36
- package/fx-term.wasm +0 -0
- package/libfx.darwin-arm64.node +0 -0
- package/libfx.darwin-x64.node +0 -0
- package/libfx.linux-arm64.node +0 -0
- package/libfx.linux-x64.node +0 -0
- package/mcp.js +3 -3
- package/node.cjs +567 -75
- package/package.json +1 -1
- package/types/durable-world.d.ts +4 -1
- package/types/libfx.d.cts +10 -4
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
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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) =>
|
|
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
|
|
981
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
2513
|
+
from ??= await log.ui.length();
|
|
2514
|
+
if (aborted()) {
|
|
2241
2515
|
accept.resolve({ messageId, sessionId });
|
|
2242
|
-
|
|
2516
|
+
if (!attaching)
|
|
2517
|
+
return { settled: { messageId, stopReason: "cancelled", usage: {} } };
|
|
2518
|
+
return { log, from, attaching };
|
|
2243
2519
|
}
|
|
2244
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 :
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
|
6035
|
-
|
|
6036
|
-
|
|
6037
|
-
|
|
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 ? [] :
|
|
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, () =>
|
|
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
|
},
|