libfx 0.0.12 → 0.0.13
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 +168 -30
- package/fx-core.wasm +0 -0
- package/fx-sdk.js +395 -124
- 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/node.cjs +389 -134
- package/node.js +18 -5
- package/package.json +1 -1
package/fx-sdk.js
CHANGED
|
@@ -19,11 +19,20 @@ const streamReadsPerTaskYield = 32;
|
|
|
19
19
|
const transportActivityIntervalMs = 250;
|
|
20
20
|
const maxUnreadEventBytes = 1024 * 1024;
|
|
21
21
|
const maxUnreadEvents = 256;
|
|
22
|
-
// Prompt
|
|
23
|
-
//
|
|
22
|
+
// Prompt images travel as raw bytes beside the ACP frame and are base64
|
|
23
|
+
// encoded only in the model request. An image may use 5 MiB of encoded
|
|
24
|
+
// request data, and a prompt's images 8 MiB, so the raw limits are 3/4 of that.
|
|
25
|
+
// The kernel still validates content and media type.
|
|
24
26
|
const maxPromptImages = 8;
|
|
25
27
|
const maxPromptImageDataBytes = 5 * 1024 * 1024;
|
|
26
|
-
const
|
|
28
|
+
const maxPromptImagesDataBytes = 8 * 1024 * 1024;
|
|
29
|
+
const maxPromptImageBytes = (maxPromptImageDataBytes / 4) * 3;
|
|
30
|
+
const maxPromptImagesBytes = (maxPromptImagesDataBytes / 4) * 3;
|
|
31
|
+
// Matches the native attachment table: one prompt's images or one checkpoint.
|
|
32
|
+
const maxPendingAttachments = 8;
|
|
33
|
+
const maxOutboundAttachments = 4;
|
|
34
|
+
// Matches the core's kernel checkpoint limit (max_checkpoint_bytes).
|
|
35
|
+
const maxCheckpointBytes = 4 * 1024 * 1024;
|
|
27
36
|
// The core's ACP reader drops frames over 8 MiB without a request id to answer
|
|
28
37
|
// (jsonrpc frame_resource_byte_limit), so the SDK must never emit one. The
|
|
29
38
|
// envelope allowance covers the method key and request id.
|
|
@@ -84,6 +93,14 @@ function normalizeFast(value) {
|
|
|
84
93
|
return value;
|
|
85
94
|
}
|
|
86
95
|
|
|
96
|
+
function normalizeUltrafast(value) {
|
|
97
|
+
if (value === undefined) return undefined;
|
|
98
|
+
if (typeof value !== "boolean") {
|
|
99
|
+
throw new TypeError("ultrafast must be a boolean");
|
|
100
|
+
}
|
|
101
|
+
return value;
|
|
102
|
+
}
|
|
103
|
+
|
|
87
104
|
export function normalizeAgentOptions(value) {
|
|
88
105
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
89
106
|
throw new TypeError("createFxAgent() options must be an object");
|
|
@@ -94,24 +111,29 @@ export function normalizeAgentOptions(value) {
|
|
|
94
111
|
}
|
|
95
112
|
options.apiKey = boundedString(options.apiKey, "apiKey", maxApiKeyBytes, true);
|
|
96
113
|
if (options.model !== null && typeof options.model === "object" && !Array.isArray(options.model)) {
|
|
97
|
-
if (Object.hasOwn(options, "effort") || Object.hasOwn(options, "fast")) {
|
|
98
|
-
throw new TypeError("model options cannot be mixed with top-level effort or
|
|
114
|
+
if (Object.hasOwn(options, "effort") || Object.hasOwn(options, "fast") || Object.hasOwn(options, "ultrafast")) {
|
|
115
|
+
throw new TypeError("model options cannot be mixed with top-level effort, fast, or ultrafast");
|
|
99
116
|
}
|
|
100
117
|
const model = options.model;
|
|
101
118
|
for (const name of Object.keys(model)) {
|
|
102
|
-
if (name !== "id" && name !== "effort" && name !== "fast") {
|
|
119
|
+
if (name !== "id" && name !== "effort" && name !== "fast" && name !== "ultrafast") {
|
|
103
120
|
throw new TypeError(`unsupported model option: ${name}`);
|
|
104
121
|
}
|
|
105
122
|
}
|
|
106
123
|
options.model = boundedString(model.id, "model.id", maxModelBytes, true);
|
|
107
124
|
options.effort = normalizeEffort(model.effort);
|
|
108
125
|
options.fast = normalizeFast(model.fast);
|
|
126
|
+
options.ultrafast = normalizeUltrafast(model.ultrafast);
|
|
109
127
|
} else {
|
|
110
128
|
options.model = boundedString(options.model, "model", maxModelBytes, false);
|
|
111
129
|
options.effort = normalizeEffort(options.effort);
|
|
112
130
|
options.fast = normalizeFast(options.fast);
|
|
131
|
+
options.ultrafast = normalizeUltrafast(options.ultrafast);
|
|
113
132
|
}
|
|
114
133
|
validateGatewayChatUrl(options.gatewayChatUrl);
|
|
134
|
+
if (options.resizeImage !== undefined && typeof options.resizeImage !== "function") {
|
|
135
|
+
throw new TypeError("resizeImage must be a function");
|
|
136
|
+
}
|
|
115
137
|
return options;
|
|
116
138
|
}
|
|
117
139
|
|
|
@@ -121,6 +143,7 @@ function agentEnvironment(options) {
|
|
|
121
143
|
...(options.model === undefined ? {} : { FX_MODEL: options.model }),
|
|
122
144
|
...(options.effort === undefined ? {} : { FX_EFFORT: options.effort }),
|
|
123
145
|
...(options.fast === undefined ? {} : { FX_FAST: options.fast ? "true" : "false" }),
|
|
146
|
+
...(options.ultrafast === undefined ? {} : { FX_ULTRAFAST: options.ultrafast ? "true" : "false" }),
|
|
124
147
|
...(options.gatewayChatUrl === undefined ? {} : { FX_GATEWAY_CHAT_URL: options.gatewayChatUrl }),
|
|
125
148
|
};
|
|
126
149
|
}
|
|
@@ -128,9 +151,9 @@ function agentEnvironment(options) {
|
|
|
128
151
|
function agentRpcError(response) {
|
|
129
152
|
const error = new Error(response.message);
|
|
130
153
|
const data = response.data;
|
|
131
|
-
if (data && ["LIBFX_MODEL_UNSUPPORTED_EFFORT", "LIBFX_MODEL_UNSUPPORTED_FAST"].includes(data.code) &&
|
|
154
|
+
if (data && ["LIBFX_MODEL_UNSUPPORTED_EFFORT", "LIBFX_MODEL_UNSUPPORTED_FAST", "LIBFX_MODEL_UNSUPPORTED_ULTRAFAST"].includes(data.code) &&
|
|
132
155
|
typeof data.model === "string" &&
|
|
133
|
-
data.capability === (data.code === "LIBFX_MODEL_UNSUPPORTED_FAST" ? "fast" : "effort")) {
|
|
156
|
+
data.capability === (data.code === "LIBFX_MODEL_UNSUPPORTED_FAST" ? "fast" : data.code === "LIBFX_MODEL_UNSUPPORTED_ULTRAFAST" ? "ultrafast" : "effort")) {
|
|
134
157
|
error.code = data.code;
|
|
135
158
|
error.model = data.model;
|
|
136
159
|
error.capability = data.capability;
|
|
@@ -465,6 +488,11 @@ function createRuntime(options) {
|
|
|
465
488
|
const steering = [];
|
|
466
489
|
let steeringBytes = 0;
|
|
467
490
|
let steeringOpen = false;
|
|
491
|
+
// Raw payloads beside ACP frames: the core copies inbound bytes into its
|
|
492
|
+
// own memory and publishes outbound bytes for the agent to take.
|
|
493
|
+
const inboundAttachments = new Map();
|
|
494
|
+
const outboundAttachments = new Map();
|
|
495
|
+
let nextOutboundAttachment = 1;
|
|
468
496
|
const workspaceExecs = new Set();
|
|
469
497
|
const workspace = prepareWorkspaceAdapter(options.workspace);
|
|
470
498
|
const args = ["fx", ...(options.args || [])];
|
|
@@ -761,6 +789,38 @@ function createRuntime(options) {
|
|
|
761
789
|
return value.length;
|
|
762
790
|
}
|
|
763
791
|
|
|
792
|
+
function writeAttachment(id, data) {
|
|
793
|
+
if (inboundAttachments.size >= maxPendingAttachments) throw new Error("attachment table is full");
|
|
794
|
+
inboundAttachments.set(id, data.slice());
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
function attachmentSize(id) {
|
|
798
|
+
return inboundAttachments.get(id >>> 0)?.length ?? -1;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
function attachmentTake(id, outputPtr, outputCap) {
|
|
802
|
+
const key = id >>> 0;
|
|
803
|
+
const value = inboundAttachments.get(key);
|
|
804
|
+
if (!value) return -1;
|
|
805
|
+
// An empty payload needs no output buffer, whose pointer may be arbitrary.
|
|
806
|
+
if (value.length > 0) {
|
|
807
|
+
const output = checkedBytes(outputPtr, outputCap);
|
|
808
|
+
if (!output || value.length > output.length) return -1;
|
|
809
|
+
output.set(value);
|
|
810
|
+
}
|
|
811
|
+
inboundAttachments.delete(key);
|
|
812
|
+
return value.length;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
function attachmentPut(inputPtr, inputLen) {
|
|
816
|
+
const input = checkedBytes(inputPtr, inputLen);
|
|
817
|
+
if (!input || outboundAttachments.size >= maxOutboundAttachments) return -1;
|
|
818
|
+
const id = nextOutboundAttachment;
|
|
819
|
+
nextOutboundAttachment = id === 0x7fffffff ? 1 : id + 1;
|
|
820
|
+
outboundAttachments.set(id, input.slice());
|
|
821
|
+
return id;
|
|
822
|
+
}
|
|
823
|
+
|
|
764
824
|
let pendingHostToolResult = null;
|
|
765
825
|
function hostToolCall(namePtr, nameLen, argumentsPtr, argumentsLen, outputPtr, outputCap, statusPtr) {
|
|
766
826
|
pendingHostToolResult = null;
|
|
@@ -1132,6 +1192,9 @@ function createRuntime(options) {
|
|
|
1132
1192
|
fx_host_tool_result_release() { pendingHostToolResult = null; },
|
|
1133
1193
|
fx_steering_take: steeringTake,
|
|
1134
1194
|
fx_steering_close() { steeringOpen = false; clearSteering(); },
|
|
1195
|
+
fx_attachment_size: attachmentSize,
|
|
1196
|
+
fx_attachment_take: attachmentTake,
|
|
1197
|
+
fx_attachment_put: attachmentPut,
|
|
1135
1198
|
fx_open_url: new WebAssembly.Suspending(openUrl),
|
|
1136
1199
|
fx_oauth_session_load: new WebAssembly.Suspending(oauthSessionLoad),
|
|
1137
1200
|
fx_oauth_session_commit: new WebAssembly.Suspending(oauthSessionCommit),
|
|
@@ -1158,6 +1221,13 @@ function createRuntime(options) {
|
|
|
1158
1221
|
imports: { wasi_snapshot_preview1: wasi, fx }, exited,
|
|
1159
1222
|
setInstance(value) { instance = value; },
|
|
1160
1223
|
write(data) { stdin.push(typeof data === "string" ? encoder.encode(data) : data); },
|
|
1224
|
+
writeAttachment,
|
|
1225
|
+
takeAttachment(id) {
|
|
1226
|
+
const value = outboundAttachments.get(id) ?? null;
|
|
1227
|
+
outboundAttachments.delete(id);
|
|
1228
|
+
return value;
|
|
1229
|
+
},
|
|
1230
|
+
discardAttachments() { inboundAttachments.clear(); },
|
|
1161
1231
|
wake() { stdin.wake(); },
|
|
1162
1232
|
closeStdin() { steeringOpen = false; clearSteering(); stdin.close(); },
|
|
1163
1233
|
openSteering() { clearSteering(); steeringOpen = true; },
|
|
@@ -1299,42 +1369,128 @@ function blobByteLength(value) {
|
|
|
1299
1369
|
}
|
|
1300
1370
|
}
|
|
1301
1371
|
|
|
1302
|
-
function
|
|
1372
|
+
function normalizeImageSourceRef(value, name) {
|
|
1373
|
+
boundedString(value, `${name} sourceRef`, 512, false);
|
|
1374
|
+
if (value !== undefined && /[\x00-\x1f\x7f\uD800-\uDFFF]/u.test(value)) {
|
|
1375
|
+
throw new TypeError(`${name} sourceRef must be valid UTF-8 without ASCII controls`);
|
|
1376
|
+
}
|
|
1377
|
+
return value;
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
function omitReferencedImageData(blocks) {
|
|
1381
|
+
return blocks.map((block) => block.type !== "image" || block.sourceRef === undefined ? block : {
|
|
1382
|
+
type: "image",
|
|
1383
|
+
mimeType: block.mimeType,
|
|
1384
|
+
sourceRef: block.sourceRef,
|
|
1385
|
+
});
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
function promptImageDataBytes(prompt) {
|
|
1389
|
+
return prompt.reduce((total, block) => {
|
|
1390
|
+
if (block.type !== "image") return total;
|
|
1391
|
+
if (typeof block.data === "string") return total + block.data.length;
|
|
1392
|
+
const byteLength = block.bytes?.byteLength ?? block.byteLength ?? blobByteLength(block.data) ?? 0;
|
|
1393
|
+
return total + Math.ceil(byteLength / 3) * 4;
|
|
1394
|
+
}, 0);
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
// Returns a Uint8Array view of an ArrayBuffer or typed array, or null.
|
|
1398
|
+
function byteView(value) {
|
|
1399
|
+
if (value instanceof ArrayBuffer) return new Uint8Array(value);
|
|
1400
|
+
if (ArrayBuffer.isView(value)) return new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
|
|
1401
|
+
return null;
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
const base64Alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
1405
|
+
|
|
1406
|
+
// Returns the decoded length of canonical, unwrapped base64, or -1.
|
|
1407
|
+
function canonicalBase64ByteLength(value) {
|
|
1408
|
+
if (value.length === 0 || value.length % 4 !== 0 || !/^[A-Za-z0-9+/]*={0,2}$/.test(value)) return -1;
|
|
1409
|
+
const groups = value.length / 4;
|
|
1410
|
+
if (value.endsWith("==")) {
|
|
1411
|
+
return (base64Alphabet.indexOf(value[value.length - 3]) & 0x0f) === 0 ? groups * 3 - 2 : -1;
|
|
1412
|
+
}
|
|
1413
|
+
if (value.endsWith("=")) {
|
|
1414
|
+
return (base64Alphabet.indexOf(value[value.length - 2]) & 0x03) === 0 ? groups * 3 - 1 : -1;
|
|
1415
|
+
}
|
|
1416
|
+
return groups * 3;
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
function requireImageMimeType(mimeType, index) {
|
|
1420
|
+
if (typeof mimeType !== "string" || mimeType.length === 0 || mimeType.length > 128) {
|
|
1421
|
+
throw new TypeError(`image prompt block ${index} requires a mimeType`);
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
function checkImageByteLength(byteLength, index) {
|
|
1426
|
+
if (byteLength > maxPromptImageBytes) {
|
|
1427
|
+
throw new RangeError(`image prompt block ${index} exceeds the ${maxPromptImageBytes} byte per-image libfx limit`);
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
function checkPromptImagesByteLength(byteLength) {
|
|
1432
|
+
if (byteLength > maxPromptImagesBytes) {
|
|
1433
|
+
throw new RangeError(`prompt images exceed the ${maxPromptImagesBytes} byte libfx limit`);
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
// Pixel inputs carry a private source and byteLength until preparation;
|
|
1438
|
+
// reference-only inputs remain public image descriptors without pixel data.
|
|
1439
|
+
function normalizePromptImage(block, index) {
|
|
1440
|
+
const sourceRef = normalizeImageSourceRef(block.sourceRef, `image prompt block ${index}`);
|
|
1441
|
+
const reference = sourceRef === undefined ? {} : { sourceRef };
|
|
1442
|
+
if (block.data === undefined && sourceRef !== undefined) {
|
|
1443
|
+
requireImageMimeType(block.mimeType, index);
|
|
1444
|
+
return { type: "image", mimeType: block.mimeType, ...reference };
|
|
1445
|
+
}
|
|
1446
|
+
const size = blobByteLength(block.data);
|
|
1447
|
+
if (size !== null) {
|
|
1448
|
+
const mimeType = block.data.type;
|
|
1449
|
+
if (block.mimeType !== undefined && block.mimeType !== mimeType) {
|
|
1450
|
+
throw new TypeError(`image prompt block ${index} mimeType disagrees with Blob.type`);
|
|
1451
|
+
}
|
|
1452
|
+
requireImageMimeType(mimeType, index);
|
|
1453
|
+
if (!Number.isSafeInteger(size) || size <= 0) {
|
|
1454
|
+
throw new TypeError(`image prompt block ${index} requires a non-empty Blob with a valid size`);
|
|
1455
|
+
}
|
|
1456
|
+
return { type: "image", source: "blob", data: block.data, mimeType, byteLength: size, ...reference };
|
|
1457
|
+
}
|
|
1458
|
+
if (typeof block.data === "string" && block.data.length > 0) {
|
|
1459
|
+
requireImageMimeType(block.mimeType, index);
|
|
1460
|
+
const byteLength = canonicalBase64ByteLength(block.data);
|
|
1461
|
+
if (byteLength <= 0) throw new TypeError(`image prompt block ${index} requires canonical base64 data`);
|
|
1462
|
+
return { type: "image", source: "base64", data: block.data, mimeType: block.mimeType, byteLength, ...reference };
|
|
1463
|
+
}
|
|
1464
|
+
const bytes = typeof block.data === "string" ? null : byteView(block.data);
|
|
1465
|
+
if (!bytes || bytes.byteLength === 0) {
|
|
1466
|
+
throw new TypeError(`image prompt block ${index} requires base64 data, bytes, or a Blob`);
|
|
1467
|
+
}
|
|
1468
|
+
requireImageMimeType(block.mimeType, index);
|
|
1469
|
+
return { type: "image", source: "bytes", data: bytes, mimeType: block.mimeType, byteLength: bytes.byteLength, ...reference };
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
// With deferImageLimits, byte limits apply after resizeImage instead.
|
|
1473
|
+
function normalizePromptInput(input, { deferImageLimits = false } = {}) {
|
|
1303
1474
|
if (typeof input === "string") return [{ type: "text", text: input }];
|
|
1304
1475
|
if (!Array.isArray(input)) throw new TypeError("prompt input must be a string or an array of prompt blocks");
|
|
1305
1476
|
let imageCount = 0;
|
|
1306
1477
|
let imageBytes = 0;
|
|
1307
|
-
|
|
1478
|
+
let prompt = input.map((block, index) => {
|
|
1308
1479
|
if (!block || typeof block !== "object") throw new TypeError(`prompt block ${index} must be an object`);
|
|
1309
1480
|
if (block.type === "image") {
|
|
1310
|
-
const
|
|
1311
|
-
const blob = size !== null;
|
|
1312
|
-
if (!blob && (typeof block.data !== "string" || block.data.length === 0)) {
|
|
1313
|
-
throw new TypeError(`image prompt block ${index} requires base64 data or a Blob`);
|
|
1314
|
-
}
|
|
1315
|
-
const mimeType = blob ? block.data.type : block.mimeType;
|
|
1316
|
-
if (blob && block.mimeType !== undefined && block.mimeType !== mimeType) {
|
|
1317
|
-
throw new TypeError(`image prompt block ${index} mimeType disagrees with Blob.type`);
|
|
1318
|
-
}
|
|
1319
|
-
if (typeof mimeType !== "string" || mimeType.length === 0 || mimeType.length > 128) {
|
|
1320
|
-
throw new TypeError(`image prompt block ${index} requires a mimeType`);
|
|
1321
|
-
}
|
|
1322
|
-
if (blob && (!Number.isSafeInteger(size) || size <= 0)) {
|
|
1323
|
-
throw new TypeError(`image prompt block ${index} requires a non-empty Blob with a valid size`);
|
|
1324
|
-
}
|
|
1325
|
-
const encodedLength = blob ? Math.ceil(size / 3) * 4 : block.data.length;
|
|
1326
|
-
if (encodedLength > maxPromptImageDataBytes) {
|
|
1327
|
-
throw new RangeError(`image prompt block ${index} exceeds the ${maxPromptImageDataBytes} byte per-image libfx limit`);
|
|
1328
|
-
}
|
|
1481
|
+
const image = normalizePromptImage(block, index);
|
|
1329
1482
|
imageCount += 1;
|
|
1330
1483
|
if (imageCount > maxPromptImages) {
|
|
1331
1484
|
throw new RangeError(`prompt cannot contain more than ${maxPromptImages} images`);
|
|
1332
1485
|
}
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1486
|
+
if (!deferImageLimits) {
|
|
1487
|
+
if (image.byteLength > maxPromptImageBytes && image.sourceRef !== undefined) {
|
|
1488
|
+
return omitReferencedImageData([image])[0];
|
|
1489
|
+
}
|
|
1490
|
+
checkImageByteLength(image.byteLength ?? 0, index);
|
|
1491
|
+
imageBytes += image.byteLength ?? 0;
|
|
1336
1492
|
}
|
|
1337
|
-
return
|
|
1493
|
+
return image;
|
|
1338
1494
|
}
|
|
1339
1495
|
if (block.type === "text") {
|
|
1340
1496
|
if (typeof block.text !== "string") throw new TypeError(`text prompt block ${index} requires text`);
|
|
@@ -1348,50 +1504,103 @@ function normalizePromptInput(input) {
|
|
|
1348
1504
|
}
|
|
1349
1505
|
throw new TypeError(`unsupported prompt block type: ${String(block.type)}`);
|
|
1350
1506
|
});
|
|
1507
|
+
if (!deferImageLimits) {
|
|
1508
|
+
if (imageBytes > maxPromptImagesBytes) {
|
|
1509
|
+
prompt = omitReferencedImageData(prompt);
|
|
1510
|
+
imageBytes = prompt.reduce((total, block) => total + (block.type === "image" ? block.byteLength ?? 0 : 0), 0);
|
|
1511
|
+
}
|
|
1512
|
+
checkPromptImagesByteLength(imageBytes);
|
|
1513
|
+
if (promptFrameSize(prompt) > maxPromptFrameBytes) prompt = omitReferencedImageData(prompt);
|
|
1514
|
+
}
|
|
1515
|
+
return prompt;
|
|
1351
1516
|
}
|
|
1352
1517
|
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1518
|
+
// Image bytes travel beside the frame as attachment references, but the model
|
|
1519
|
+
// request carries them base64 encoded and the native host caps that request at
|
|
1520
|
+
// 8 MiB. Images therefore count at their encoded size, the same budget they
|
|
1521
|
+
// had inside the frame. With countImages false, only the frame counts.
|
|
1522
|
+
function promptFrameSize(prompt, countImages = true) {
|
|
1523
|
+
const encodedImageBytes = countImages ? promptImageDataBytes(prompt) : 0;
|
|
1524
|
+
const projected = prompt.map((block) => block.type !== "image" ? block : {
|
|
1525
|
+
type: "image",
|
|
1526
|
+
mimeType: block.mimeType,
|
|
1527
|
+
...(block.sourceRef === undefined ? {} : { sourceRef: block.sourceRef }),
|
|
1528
|
+
...(block.data === undefined && block.bytes === undefined ? {} : { _meta: { fx: { attachment: 0xffffffff } } }),
|
|
1361
1529
|
});
|
|
1362
|
-
return encoder.encode(JSON.stringify({ sessionId: "", prompt: projected })).length +
|
|
1530
|
+
return encoder.encode(JSON.stringify({ sessionId: "", prompt: projected })).length +
|
|
1531
|
+
encodedImageBytes + promptFrameEnvelopeBytes;
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1534
|
+
function checkPromptFrameSize(prompt, countImages) {
|
|
1535
|
+
if (promptFrameSize(prompt, countImages) > maxPromptFrameBytes) {
|
|
1536
|
+
throw new RangeError(`prompt exceeds the ${maxPromptFrameBytes} byte libfx frame limit`);
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
// Returns prompt blocks whose images carry { mimeType, bytes }. Synchronous
|
|
1541
|
+
// sources only: base64 is decoded and caller bytes are used in place.
|
|
1542
|
+
function preparePromptImages(blocks) {
|
|
1543
|
+
return blocks.map((block) => block.type !== "image" || block.data === undefined ? block : {
|
|
1544
|
+
type: "image",
|
|
1545
|
+
mimeType: block.mimeType,
|
|
1546
|
+
bytes: block.source === "base64" ? base64ToBytes(block.data) : block.data,
|
|
1547
|
+
...(block.sourceRef === undefined ? {} : { sourceRef: block.sourceRef }),
|
|
1548
|
+
});
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
function resizedPromptImage(value, index) {
|
|
1552
|
+
const bytes = byteView(value?.bytes);
|
|
1553
|
+
if (!bytes || bytes.byteLength === 0 || typeof value.mimeType !== "string" ||
|
|
1554
|
+
value.mimeType.length === 0 || value.mimeType.length > 128) {
|
|
1555
|
+
throw new TypeError(`resizeImage must return non-empty bytes and a mimeType for image prompt block ${index}`);
|
|
1556
|
+
}
|
|
1557
|
+
// Copied because a hook may reuse its output buffer for the next image.
|
|
1558
|
+
return { bytes: bytes.slice(), mimeType: value.mimeType };
|
|
1363
1559
|
}
|
|
1364
1560
|
|
|
1365
|
-
|
|
1366
|
-
|
|
1561
|
+
// Reads Blob images and applies resizeImage, then checks the final byte and
|
|
1562
|
+
// frame limits. Returns null when the turn is cancelled first.
|
|
1563
|
+
async function materializePromptImages(blocks, isCancelled, resizeImage) {
|
|
1564
|
+
let prepared = [];
|
|
1367
1565
|
let imageBytes = 0;
|
|
1368
|
-
for (
|
|
1566
|
+
for (let index = 0; index < blocks.length; index++) {
|
|
1369
1567
|
if (isCancelled()) return null;
|
|
1370
|
-
|
|
1371
|
-
|
|
1568
|
+
const block = blocks[index];
|
|
1569
|
+
if (block.type !== "image" || block.data === undefined) {
|
|
1570
|
+
prepared.push(block);
|
|
1372
1571
|
continue;
|
|
1373
1572
|
}
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
throw new RangeError(`prompt images exceed the ${maxPromptImagesBytes} byte libfx frame limit`);
|
|
1378
|
-
}
|
|
1379
|
-
encoded.push(block);
|
|
1380
|
-
continue;
|
|
1381
|
-
}
|
|
1382
|
-
const bytes = new Uint8Array(await block.data.arrayBuffer());
|
|
1573
|
+
let image = block.source === "blob"
|
|
1574
|
+
? { mimeType: block.mimeType, bytes: new Uint8Array(await block.data.arrayBuffer()) }
|
|
1575
|
+
: preparePromptImages([block])[0];
|
|
1383
1576
|
if (isCancelled()) return null;
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1577
|
+
if (resizeImage) {
|
|
1578
|
+
image = resizedPromptImage(await resizeImage({ bytes: image.bytes, mimeType: image.mimeType }), index);
|
|
1579
|
+
if (isCancelled()) return null;
|
|
1387
1580
|
}
|
|
1388
|
-
|
|
1581
|
+
image = {
|
|
1582
|
+
type: "image",
|
|
1583
|
+
mimeType: image.mimeType,
|
|
1584
|
+
bytes: image.bytes,
|
|
1585
|
+
...(block.sourceRef === undefined ? {} : { sourceRef: block.sourceRef }),
|
|
1586
|
+
};
|
|
1587
|
+
if (image.bytes.byteLength > maxPromptImageBytes && image.sourceRef !== undefined) {
|
|
1588
|
+
image = omitReferencedImageData([image])[0];
|
|
1589
|
+
}
|
|
1590
|
+
checkImageByteLength(image.bytes?.byteLength ?? 0, index);
|
|
1591
|
+
imageBytes += image.bytes?.byteLength ?? 0;
|
|
1592
|
+
prepared.push(image);
|
|
1389
1593
|
if (imageBytes > maxPromptImagesBytes) {
|
|
1390
|
-
|
|
1594
|
+
prepared = omitReferencedImageData(prepared);
|
|
1595
|
+
// Do not read more referenced Blobs after the actual aggregate overflows.
|
|
1596
|
+
blocks = omitReferencedImageData(blocks);
|
|
1597
|
+
imageBytes = prepared.reduce((total, entry) => total + (entry.bytes?.byteLength ?? 0), 0);
|
|
1391
1598
|
}
|
|
1392
|
-
|
|
1599
|
+
checkPromptImagesByteLength(imageBytes);
|
|
1393
1600
|
}
|
|
1394
|
-
|
|
1601
|
+
if (promptFrameSize(prepared) > maxPromptFrameBytes) prepared = omitReferencedImageData(prepared);
|
|
1602
|
+
checkPromptFrameSize(prepared, true);
|
|
1603
|
+
return prepared;
|
|
1395
1604
|
}
|
|
1396
1605
|
|
|
1397
1606
|
function normalizeSteeringInput(input) {
|
|
@@ -1466,17 +1675,28 @@ function hostToolContent(value) {
|
|
|
1466
1675
|
if (typeof value.text !== "string" || !Array.isArray(value.images) || value.images.length > 8) {
|
|
1467
1676
|
throw new TypeError("invalid typed tool result");
|
|
1468
1677
|
}
|
|
1469
|
-
let
|
|
1470
|
-
|
|
1471
|
-
|
|
1678
|
+
let images = value.images.map((image) => {
|
|
1679
|
+
if (image?.type !== "image") throw new TypeError("invalid tool image");
|
|
1680
|
+
const sourceRef = normalizeImageSourceRef(image.sourceRef, "tool image");
|
|
1681
|
+
const referenceOnly = image.data === undefined && sourceRef !== undefined;
|
|
1682
|
+
if ((!referenceOnly && typeof image.data !== "string") || typeof image.mimeType !== "string" || image.mimeType.length === 0 || image.mimeType.length > 128 || (image.data?.length > maxPromptImageDataBytes && sourceRef === undefined)) {
|
|
1472
1683
|
throw new TypeError("invalid tool image");
|
|
1473
1684
|
}
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1685
|
+
return {
|
|
1686
|
+
type: "image",
|
|
1687
|
+
...(!referenceOnly && image.data.length <= maxPromptImageDataBytes ? { data: image.data } : {}),
|
|
1688
|
+
mimeType: image.mimeType,
|
|
1689
|
+
...(sourceRef === undefined ? {} : { sourceRef }),
|
|
1690
|
+
};
|
|
1477
1691
|
});
|
|
1478
|
-
|
|
1479
|
-
if (
|
|
1692
|
+
if (promptImageDataBytes(images) > maxPromptImagesDataBytes) images = omitReferencedImageData(images);
|
|
1693
|
+
if (promptImageDataBytes(images) > maxPromptImagesDataBytes) throw new RangeError("tool images exceed the result limit");
|
|
1694
|
+
let content = JSON.stringify({ text: value.text, images });
|
|
1695
|
+
if (encoder.encode(content).length > maxPromptImagesDataBytes) {
|
|
1696
|
+
images = omitReferencedImageData(images);
|
|
1697
|
+
content = JSON.stringify({ text: value.text, images });
|
|
1698
|
+
}
|
|
1699
|
+
if (encoder.encode(content).length > maxPromptImagesDataBytes) throw new RangeError("typed tool result exceeds the result limit");
|
|
1480
1700
|
return { content, rich: true, isError: value.isError === true };
|
|
1481
1701
|
}
|
|
1482
1702
|
if (typeof value === "string") return { content: value, rich: false };
|
|
@@ -1495,14 +1715,6 @@ function checkpointBytes(value) {
|
|
|
1495
1715
|
throw new TypeError("checkpoint must be an ArrayBuffer or typed array");
|
|
1496
1716
|
}
|
|
1497
1717
|
|
|
1498
|
-
function bytesToBase64(value) {
|
|
1499
|
-
let binary = "";
|
|
1500
|
-
for (let offset = 0; offset < value.length; offset += 0x8000) {
|
|
1501
|
-
binary += String.fromCharCode(...value.subarray(offset, offset + 0x8000));
|
|
1502
|
-
}
|
|
1503
|
-
return btoa(binary);
|
|
1504
|
-
}
|
|
1505
|
-
|
|
1506
1718
|
function base64ToBytes(value) {
|
|
1507
1719
|
const binary = atob(value);
|
|
1508
1720
|
const bytes = new Uint8Array(binary.length);
|
|
@@ -1515,6 +1727,11 @@ export async function createFxAgent(options = {}) {
|
|
|
1515
1727
|
const hostTools = normalizeHostTools(options.tools);
|
|
1516
1728
|
const instructions = normalizeInstructions(options.instructions);
|
|
1517
1729
|
const initialCheckpoint = checkpointBytes(options.checkpoint);
|
|
1730
|
+
// Checked before the core starts, and with the core's message, so both
|
|
1731
|
+
// backends report it the same way.
|
|
1732
|
+
if (initialCheckpoint && initialCheckpoint.byteLength > maxCheckpointBytes) {
|
|
1733
|
+
throw new Error("libfx checkpoint is too large");
|
|
1734
|
+
}
|
|
1518
1735
|
const pending = new Map();
|
|
1519
1736
|
let nextId = 1;
|
|
1520
1737
|
let sessionId = null;
|
|
@@ -1659,11 +1876,47 @@ export async function createFxAgent(options = {}) {
|
|
|
1659
1876
|
pending.set(id, { resolve, reject });
|
|
1660
1877
|
try { send({ jsonrpc: "2.0", id, method, params }); } catch (error) { pending.delete(id); reject(error); }
|
|
1661
1878
|
});
|
|
1879
|
+
// Raw payloads ride beside the next frame instead of inside it. Payloads
|
|
1880
|
+
// left by an earlier frame that never reached the core are dropped first.
|
|
1881
|
+
let nextAttachmentId = 1;
|
|
1882
|
+
const attachBytes = (payloads) => {
|
|
1883
|
+
if (typeof runtime.writeAttachment !== "function") throw new Error("fx runtime does not accept binary attachments");
|
|
1884
|
+
runtime.discardAttachments?.();
|
|
1885
|
+
return payloads.map((bytes) => {
|
|
1886
|
+
const id = nextAttachmentId;
|
|
1887
|
+
nextAttachmentId = id === 0x7fffffff ? 1 : id + 1;
|
|
1888
|
+
runtime.writeAttachment(id, bytes);
|
|
1889
|
+
return id;
|
|
1890
|
+
});
|
|
1891
|
+
};
|
|
1892
|
+
let checkpointTail = null;
|
|
1893
|
+
let pendingCheckpoints = 0;
|
|
1894
|
+
async function takeCheckpoint() {
|
|
1895
|
+
if (closing) throw new Error("fx agent is closed");
|
|
1896
|
+
if (activeTurn) throw new Error("cannot checkpoint while a prompt is active");
|
|
1897
|
+
const response = await request("libfx/checkpoint", { sessionId });
|
|
1898
|
+
const id = response?.checkpointAttachment;
|
|
1899
|
+
const bytes = Number.isSafeInteger(id) && id > 0 ? runtime.takeAttachment?.(id) : null;
|
|
1900
|
+
if (!(bytes instanceof Uint8Array) || bytes.byteLength === 0) throw new Error("fx returned an invalid checkpoint");
|
|
1901
|
+
return new Uint8Array(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
1902
|
+
}
|
|
1903
|
+
const sendPrompt = (blocks) => {
|
|
1904
|
+
const images = blocks.filter((block) => block.type === "image" && block.bytes !== undefined);
|
|
1905
|
+
const ids = images.length ? attachBytes(images.map((block) => block.bytes)) : [];
|
|
1906
|
+
let next = 0;
|
|
1907
|
+
const prompt = blocks.map((block) => block.type !== "image" ? block : {
|
|
1908
|
+
type: "image",
|
|
1909
|
+
mimeType: block.mimeType,
|
|
1910
|
+
...(block.sourceRef === undefined ? {} : { sourceRef: block.sourceRef }),
|
|
1911
|
+
...(block.bytes === undefined ? {} : { _meta: { fx: { attachment: ids[next++] } } }),
|
|
1912
|
+
});
|
|
1913
|
+
return request("session/prompt", { sessionId, prompt });
|
|
1914
|
+
};
|
|
1662
1915
|
runtime.exited.then((code) => {
|
|
1663
1916
|
closing = true;
|
|
1664
1917
|
const error = runtime.error ?? new Error(`fx-core exited with code ${code} before completing the ACP request`);
|
|
1665
1918
|
coreExitError = error;
|
|
1666
|
-
activeTurn?.
|
|
1919
|
+
activeTurn?.failImagePrep(error);
|
|
1667
1920
|
for (const waiter of pending.values()) waiter.reject(error);
|
|
1668
1921
|
pending.clear();
|
|
1669
1922
|
emit("runtime.exit", { code });
|
|
@@ -1698,6 +1951,10 @@ export async function createFxAgent(options = {}) {
|
|
|
1698
1951
|
);
|
|
1699
1952
|
if (cancelled || closing) return;
|
|
1700
1953
|
const response = { jsonrpc: "2.0", id: message.id, result: { content, isError, ...(rich ? { contentType: "rich" } : {}) } };
|
|
1954
|
+
if (rich && encoder.encode(JSON.stringify(response)).length + 1 > maxPromptFrameBytes) {
|
|
1955
|
+
const result = JSON.parse(content);
|
|
1956
|
+
response.result.content = JSON.stringify({ ...result, images: omitReferencedImageData(result.images) });
|
|
1957
|
+
}
|
|
1701
1958
|
if (encoder.encode(JSON.stringify(response)).length + 1 > 8 * 1024 * 1024) {
|
|
1702
1959
|
response.result = { content: "Host tool result exceeded the response frame limit", isError: true };
|
|
1703
1960
|
}
|
|
@@ -1720,10 +1977,8 @@ export async function createFxAgent(options = {}) {
|
|
|
1720
1977
|
const sessionResult = await request("libfx/new");
|
|
1721
1978
|
sessionId = sessionResult.sessionId;
|
|
1722
1979
|
if (initialCheckpoint) {
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
checkpoint: bytesToBase64(initialCheckpoint),
|
|
1726
|
-
});
|
|
1980
|
+
const [checkpointAttachment] = attachBytes([initialCheckpoint]);
|
|
1981
|
+
await request("libfx/restore", { sessionId, checkpointAttachment });
|
|
1727
1982
|
}
|
|
1728
1983
|
} catch (error) {
|
|
1729
1984
|
closing = true;
|
|
@@ -1739,12 +1994,21 @@ export async function createFxAgent(options = {}) {
|
|
|
1739
1994
|
if (activeTurn) throw new Error("a prompt is already in progress for this session");
|
|
1740
1995
|
return normalizeTurn(startTurn(input, promptOptions));
|
|
1741
1996
|
},
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1997
|
+
checkpoint() {
|
|
1998
|
+
// One checkpoint runs at a time: each holds an outbound attachment until
|
|
1999
|
+
// it is taken, and the native table holds only a few. An idle call still
|
|
2000
|
+
// sends its request before returning, ahead of a later prompt(). The slot
|
|
2001
|
+
// is claimed before that send, so a call from an event handler during it
|
|
2002
|
+
// still waits. The count drops before the caller's own reaction to `run`,
|
|
2003
|
+
// so a call made right after awaiting the previous one is idle.
|
|
2004
|
+
const previous = pendingCheckpoints > 0 ? checkpointTail : null;
|
|
2005
|
+
pendingCheckpoints++;
|
|
2006
|
+
let release;
|
|
2007
|
+
checkpointTail = new Promise((resolve) => { release = resolve; });
|
|
2008
|
+
const run = previous ? previous.then(takeCheckpoint) : takeCheckpoint();
|
|
2009
|
+
const settle = () => { pendingCheckpoints--; release(); };
|
|
2010
|
+
run.then(settle, settle);
|
|
2011
|
+
return run;
|
|
1748
2012
|
},
|
|
1749
2013
|
async close() {
|
|
1750
2014
|
if (closing) { await runtime.exited; return; }
|
|
@@ -1846,11 +2110,18 @@ export async function createFxAgent(options = {}) {
|
|
|
1846
2110
|
}
|
|
1847
2111
|
|
|
1848
2112
|
function startTurn(input, promptOptions) {
|
|
1849
|
-
const
|
|
1850
|
-
const
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
2113
|
+
const resizeImage = options.resizeImage;
|
|
2114
|
+
const normalized = normalizePromptInput(input, { deferImageLimits: resizeImage !== undefined });
|
|
2115
|
+
const hasPixels = normalized.some((block) => block.type === "image" && block.data !== undefined);
|
|
2116
|
+
// Blob reads and resizeImage run before the prompt frame is sent.
|
|
2117
|
+
const asyncImages = hasPixels && (resizeImage !== undefined ||
|
|
2118
|
+
normalized.some((block) => block.type === "image" && block.source === "blob"));
|
|
2119
|
+
// resizeImage decides the final image sizes, so they are counted after it runs.
|
|
2120
|
+
checkPromptFrameSize(normalized, resizeImage === undefined);
|
|
2121
|
+
// Snapshot caller-owned bytes, which could change before an async send.
|
|
2122
|
+
const prompt = asyncImages
|
|
2123
|
+
? normalized.map((block) => block.type === "image" && block.source === "bytes" ? { ...block, data: block.data.slice() } : block)
|
|
2124
|
+
: preparePromptImages(normalized);
|
|
1854
2125
|
const signal = promptOptions.signal;
|
|
1855
2126
|
if (signal !== undefined && (typeof signal?.addEventListener !== "function" || typeof signal?.removeEventListener !== "function")) throw new TypeError("prompt signal must be an AbortSignal");
|
|
1856
2127
|
const queue = [];
|
|
@@ -1861,10 +2132,10 @@ export async function createFxAgent(options = {}) {
|
|
|
1861
2132
|
let terminalError;
|
|
1862
2133
|
let reportedPressure = false;
|
|
1863
2134
|
let discardedBytes = 0;
|
|
1864
|
-
let
|
|
1865
|
-
let
|
|
2135
|
+
let cancelImagePrep = null;
|
|
2136
|
+
let rejectImagePrep = null;
|
|
1866
2137
|
let resolvePromptStart = null;
|
|
1867
|
-
const promptStarted =
|
|
2138
|
+
const promptStarted = asyncImages ? new Promise((resolve) => { resolvePromptStart = resolve; }) : null;
|
|
1868
2139
|
let pendingSteeringCount = 0;
|
|
1869
2140
|
let pendingSteeringBytes = 0;
|
|
1870
2141
|
const toolControllers = new Set();
|
|
@@ -1891,10 +2162,10 @@ export async function createFxAgent(options = {}) {
|
|
|
1891
2162
|
transportBytes: 0,
|
|
1892
2163
|
lastTransportActivityAt: null,
|
|
1893
2164
|
get cancelled() { return cancelled; },
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
2165
|
+
failImagePrep(error) {
|
|
2166
|
+
rejectImagePrep?.(error);
|
|
2167
|
+
rejectImagePrep = null;
|
|
2168
|
+
cancelImagePrep = null;
|
|
1898
2169
|
},
|
|
1899
2170
|
promptWritten() {
|
|
1900
2171
|
resolvePromptStart?.(true);
|
|
@@ -1939,9 +2210,9 @@ export async function createFxAgent(options = {}) {
|
|
|
1939
2210
|
cancel() {
|
|
1940
2211
|
if (finished || cancelled) return;
|
|
1941
2212
|
cancelled = true;
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
2213
|
+
cancelImagePrep?.();
|
|
2214
|
+
cancelImagePrep = null;
|
|
2215
|
+
rejectImagePrep = null;
|
|
1945
2216
|
resolvePromptStart?.(false);
|
|
1946
2217
|
resolvePromptStart = null;
|
|
1947
2218
|
runtime.closeSteering?.();
|
|
@@ -1982,34 +2253,34 @@ export async function createFxAgent(options = {}) {
|
|
|
1982
2253
|
runtime.openSteering?.();
|
|
1983
2254
|
const abort = () => turn.cancel();
|
|
1984
2255
|
signal?.addEventListener("abort", abort, { once: true });
|
|
1985
|
-
const
|
|
1986
|
-
|
|
1987
|
-
|
|
2256
|
+
const imagePrepCancelled = asyncImages ? new Promise((resolve, reject) => {
|
|
2257
|
+
cancelImagePrep = () => resolve(null);
|
|
2258
|
+
rejectImagePrep = reject;
|
|
1988
2259
|
}) : null;
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
2260
|
+
let response;
|
|
2261
|
+
if (asyncImages) {
|
|
2262
|
+
response = Promise.race([
|
|
2263
|
+
Promise.resolve().then(() => materializePromptImages(prompt, () => cancelled || closing, resizeImage)),
|
|
2264
|
+
imagePrepCancelled,
|
|
2265
|
+
]).then((prepared) => {
|
|
2266
|
+
cancelImagePrep = null;
|
|
2267
|
+
rejectImagePrep = null;
|
|
1996
2268
|
if (coreExitError && !cancelled) throw coreExitError;
|
|
1997
|
-
if (
|
|
2269
|
+
if (prepared === null || cancelled || closing) {
|
|
1998
2270
|
resolvePromptStart?.(false);
|
|
1999
2271
|
resolvePromptStart = null;
|
|
2000
2272
|
return { stopReason: "cancelled" };
|
|
2001
2273
|
}
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
: request("session/prompt", { sessionId, prompt });
|
|
2274
|
+
return sendPrompt(prepared);
|
|
2275
|
+
});
|
|
2276
|
+
} else {
|
|
2277
|
+
try { response = sendPrompt(prompt); } catch (error) { response = Promise.reject(error); }
|
|
2278
|
+
}
|
|
2008
2279
|
turn.result = response
|
|
2009
2280
|
.then((value) => ({ stopReason: cancelled ? "cancelled" : value.stopReason, usage: value.usage }))
|
|
2010
2281
|
.catch((error) => {
|
|
2011
|
-
|
|
2012
|
-
|
|
2282
|
+
cancelImagePrep = null;
|
|
2283
|
+
rejectImagePrep = null;
|
|
2013
2284
|
resolvePromptStart?.(error);
|
|
2014
2285
|
resolvePromptStart = null;
|
|
2015
2286
|
if (cancelled && error.message === "Cancelled") return { stopReason: "cancelled" };
|