libfx 0.0.11 → 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 +205 -39
- package/fx-core.wasm +0 -0
- package/fx-sdk.js +501 -83
- 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 +503 -88
- package/node.js +23 -10
- 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,7 +93,15 @@ function normalizeFast(value) {
|
|
|
84
93
|
return value;
|
|
85
94
|
}
|
|
86
95
|
|
|
87
|
-
function
|
|
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
|
+
|
|
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");
|
|
90
107
|
}
|
|
@@ -93,10 +110,30 @@ function normalizeAgentOptions(value) {
|
|
|
93
110
|
throw new TypeError("createFxAgent() does not accept env; pass apiKey and model directly");
|
|
94
111
|
}
|
|
95
112
|
options.apiKey = boundedString(options.apiKey, "apiKey", maxApiKeyBytes, true);
|
|
96
|
-
options.model
|
|
97
|
-
|
|
98
|
-
|
|
113
|
+
if (options.model !== null && typeof options.model === "object" && !Array.isArray(options.model)) {
|
|
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");
|
|
116
|
+
}
|
|
117
|
+
const model = options.model;
|
|
118
|
+
for (const name of Object.keys(model)) {
|
|
119
|
+
if (name !== "id" && name !== "effort" && name !== "fast" && name !== "ultrafast") {
|
|
120
|
+
throw new TypeError(`unsupported model option: ${name}`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
options.model = boundedString(model.id, "model.id", maxModelBytes, true);
|
|
124
|
+
options.effort = normalizeEffort(model.effort);
|
|
125
|
+
options.fast = normalizeFast(model.fast);
|
|
126
|
+
options.ultrafast = normalizeUltrafast(model.ultrafast);
|
|
127
|
+
} else {
|
|
128
|
+
options.model = boundedString(options.model, "model", maxModelBytes, false);
|
|
129
|
+
options.effort = normalizeEffort(options.effort);
|
|
130
|
+
options.fast = normalizeFast(options.fast);
|
|
131
|
+
options.ultrafast = normalizeUltrafast(options.ultrafast);
|
|
132
|
+
}
|
|
99
133
|
validateGatewayChatUrl(options.gatewayChatUrl);
|
|
134
|
+
if (options.resizeImage !== undefined && typeof options.resizeImage !== "function") {
|
|
135
|
+
throw new TypeError("resizeImage must be a function");
|
|
136
|
+
}
|
|
100
137
|
return options;
|
|
101
138
|
}
|
|
102
139
|
|
|
@@ -106,20 +143,20 @@ function agentEnvironment(options) {
|
|
|
106
143
|
...(options.model === undefined ? {} : { FX_MODEL: options.model }),
|
|
107
144
|
...(options.effort === undefined ? {} : { FX_EFFORT: options.effort }),
|
|
108
145
|
...(options.fast === undefined ? {} : { FX_FAST: options.fast ? "true" : "false" }),
|
|
146
|
+
...(options.ultrafast === undefined ? {} : { FX_ULTRAFAST: options.ultrafast ? "true" : "false" }),
|
|
109
147
|
...(options.gatewayChatUrl === undefined ? {} : { FX_GATEWAY_CHAT_URL: options.gatewayChatUrl }),
|
|
110
148
|
};
|
|
111
149
|
}
|
|
112
150
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
(
|
|
119
|
-
error.code
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
error.code ??= "LIBFX_UNSUPPORTED_FAST";
|
|
151
|
+
function agentRpcError(response) {
|
|
152
|
+
const error = new Error(response.message);
|
|
153
|
+
const data = response.data;
|
|
154
|
+
if (data && ["LIBFX_MODEL_UNSUPPORTED_EFFORT", "LIBFX_MODEL_UNSUPPORTED_FAST", "LIBFX_MODEL_UNSUPPORTED_ULTRAFAST"].includes(data.code) &&
|
|
155
|
+
typeof data.model === "string" &&
|
|
156
|
+
data.capability === (data.code === "LIBFX_MODEL_UNSUPPORTED_FAST" ? "fast" : data.code === "LIBFX_MODEL_UNSUPPORTED_ULTRAFAST" ? "ultrafast" : "effort")) {
|
|
157
|
+
error.code = data.code;
|
|
158
|
+
error.model = data.model;
|
|
159
|
+
error.capability = data.capability;
|
|
123
160
|
}
|
|
124
161
|
return error;
|
|
125
162
|
}
|
|
@@ -451,6 +488,11 @@ function createRuntime(options) {
|
|
|
451
488
|
const steering = [];
|
|
452
489
|
let steeringBytes = 0;
|
|
453
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;
|
|
454
496
|
const workspaceExecs = new Set();
|
|
455
497
|
const workspace = prepareWorkspaceAdapter(options.workspace);
|
|
456
498
|
const args = ["fx", ...(options.args || [])];
|
|
@@ -747,6 +789,38 @@ function createRuntime(options) {
|
|
|
747
789
|
return value.length;
|
|
748
790
|
}
|
|
749
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
|
+
|
|
750
824
|
let pendingHostToolResult = null;
|
|
751
825
|
function hostToolCall(namePtr, nameLen, argumentsPtr, argumentsLen, outputPtr, outputCap, statusPtr) {
|
|
752
826
|
pendingHostToolResult = null;
|
|
@@ -1118,6 +1192,9 @@ function createRuntime(options) {
|
|
|
1118
1192
|
fx_host_tool_result_release() { pendingHostToolResult = null; },
|
|
1119
1193
|
fx_steering_take: steeringTake,
|
|
1120
1194
|
fx_steering_close() { steeringOpen = false; clearSteering(); },
|
|
1195
|
+
fx_attachment_size: attachmentSize,
|
|
1196
|
+
fx_attachment_take: attachmentTake,
|
|
1197
|
+
fx_attachment_put: attachmentPut,
|
|
1121
1198
|
fx_open_url: new WebAssembly.Suspending(openUrl),
|
|
1122
1199
|
fx_oauth_session_load: new WebAssembly.Suspending(oauthSessionLoad),
|
|
1123
1200
|
fx_oauth_session_commit: new WebAssembly.Suspending(oauthSessionCommit),
|
|
@@ -1144,6 +1221,13 @@ function createRuntime(options) {
|
|
|
1144
1221
|
imports: { wasi_snapshot_preview1: wasi, fx }, exited,
|
|
1145
1222
|
setInstance(value) { instance = value; },
|
|
1146
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(); },
|
|
1147
1231
|
wake() { stdin.wake(); },
|
|
1148
1232
|
closeStdin() { steeringOpen = false; clearSteering(); stdin.close(); },
|
|
1149
1233
|
openSteering() { clearSteering(); steeringOpen = true; },
|
|
@@ -1276,32 +1360,137 @@ export async function createFxTerminal(options) {
|
|
|
1276
1360
|
};
|
|
1277
1361
|
}
|
|
1278
1362
|
|
|
1279
|
-
function
|
|
1363
|
+
function blobByteLength(value) {
|
|
1364
|
+
if (typeof Blob === "undefined" || value == null) return null;
|
|
1365
|
+
try {
|
|
1366
|
+
return Object.getOwnPropertyDescriptor(Blob.prototype, "size").get.call(value);
|
|
1367
|
+
} catch {
|
|
1368
|
+
return null;
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
|
|
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 } = {}) {
|
|
1280
1474
|
if (typeof input === "string") return [{ type: "text", text: input }];
|
|
1281
1475
|
if (!Array.isArray(input)) throw new TypeError("prompt input must be a string or an array of prompt blocks");
|
|
1282
1476
|
let imageCount = 0;
|
|
1283
1477
|
let imageBytes = 0;
|
|
1284
|
-
|
|
1478
|
+
let prompt = input.map((block, index) => {
|
|
1285
1479
|
if (!block || typeof block !== "object") throw new TypeError(`prompt block ${index} must be an object`);
|
|
1286
1480
|
if (block.type === "image") {
|
|
1287
|
-
|
|
1288
|
-
throw new TypeError(`image prompt block ${index} requires base64 data`);
|
|
1289
|
-
}
|
|
1290
|
-
if (typeof block.mimeType !== "string" || block.mimeType.length === 0 || block.mimeType.length > 128) {
|
|
1291
|
-
throw new TypeError(`image prompt block ${index} requires a mimeType`);
|
|
1292
|
-
}
|
|
1293
|
-
if (block.data.length > maxPromptImageDataBytes) {
|
|
1294
|
-
throw new RangeError(`image prompt block ${index} exceeds the ${maxPromptImageDataBytes} byte per-image libfx limit`);
|
|
1295
|
-
}
|
|
1481
|
+
const image = normalizePromptImage(block, index);
|
|
1296
1482
|
imageCount += 1;
|
|
1297
1483
|
if (imageCount > maxPromptImages) {
|
|
1298
1484
|
throw new RangeError(`prompt cannot contain more than ${maxPromptImages} images`);
|
|
1299
1485
|
}
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
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;
|
|
1303
1492
|
}
|
|
1304
|
-
return
|
|
1493
|
+
return image;
|
|
1305
1494
|
}
|
|
1306
1495
|
if (block.type === "text") {
|
|
1307
1496
|
if (typeof block.text !== "string") throw new TypeError(`text prompt block ${index} requires text`);
|
|
@@ -1315,6 +1504,103 @@ function normalizePromptInput(input) {
|
|
|
1315
1504
|
}
|
|
1316
1505
|
throw new TypeError(`unsupported prompt block type: ${String(block.type)}`);
|
|
1317
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;
|
|
1516
|
+
}
|
|
1517
|
+
|
|
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 } } }),
|
|
1529
|
+
});
|
|
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 };
|
|
1559
|
+
}
|
|
1560
|
+
|
|
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 = [];
|
|
1565
|
+
let imageBytes = 0;
|
|
1566
|
+
for (let index = 0; index < blocks.length; index++) {
|
|
1567
|
+
if (isCancelled()) return null;
|
|
1568
|
+
const block = blocks[index];
|
|
1569
|
+
if (block.type !== "image" || block.data === undefined) {
|
|
1570
|
+
prepared.push(block);
|
|
1571
|
+
continue;
|
|
1572
|
+
}
|
|
1573
|
+
let image = block.source === "blob"
|
|
1574
|
+
? { mimeType: block.mimeType, bytes: new Uint8Array(await block.data.arrayBuffer()) }
|
|
1575
|
+
: preparePromptImages([block])[0];
|
|
1576
|
+
if (isCancelled()) return null;
|
|
1577
|
+
if (resizeImage) {
|
|
1578
|
+
image = resizedPromptImage(await resizeImage({ bytes: image.bytes, mimeType: image.mimeType }), index);
|
|
1579
|
+
if (isCancelled()) return null;
|
|
1580
|
+
}
|
|
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);
|
|
1593
|
+
if (imageBytes > maxPromptImagesBytes) {
|
|
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);
|
|
1598
|
+
}
|
|
1599
|
+
checkPromptImagesByteLength(imageBytes);
|
|
1600
|
+
}
|
|
1601
|
+
if (promptFrameSize(prepared) > maxPromptFrameBytes) prepared = omitReferencedImageData(prepared);
|
|
1602
|
+
checkPromptFrameSize(prepared, true);
|
|
1603
|
+
return prepared;
|
|
1318
1604
|
}
|
|
1319
1605
|
|
|
1320
1606
|
function normalizeSteeringInput(input) {
|
|
@@ -1389,17 +1675,28 @@ function hostToolContent(value) {
|
|
|
1389
1675
|
if (typeof value.text !== "string" || !Array.isArray(value.images) || value.images.length > 8) {
|
|
1390
1676
|
throw new TypeError("invalid typed tool result");
|
|
1391
1677
|
}
|
|
1392
|
-
let
|
|
1393
|
-
|
|
1394
|
-
|
|
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)) {
|
|
1395
1683
|
throw new TypeError("invalid tool image");
|
|
1396
1684
|
}
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1685
|
+
return {
|
|
1686
|
+
type: "image",
|
|
1687
|
+
...(!referenceOnly && image.data.length <= maxPromptImageDataBytes ? { data: image.data } : {}),
|
|
1688
|
+
mimeType: image.mimeType,
|
|
1689
|
+
...(sourceRef === undefined ? {} : { sourceRef }),
|
|
1690
|
+
};
|
|
1400
1691
|
});
|
|
1401
|
-
|
|
1402
|
-
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");
|
|
1403
1700
|
return { content, rich: true, isError: value.isError === true };
|
|
1404
1701
|
}
|
|
1405
1702
|
if (typeof value === "string") return { content: value, rich: false };
|
|
@@ -1418,14 +1715,6 @@ function checkpointBytes(value) {
|
|
|
1418
1715
|
throw new TypeError("checkpoint must be an ArrayBuffer or typed array");
|
|
1419
1716
|
}
|
|
1420
1717
|
|
|
1421
|
-
function bytesToBase64(value) {
|
|
1422
|
-
let binary = "";
|
|
1423
|
-
for (let offset = 0; offset < value.length; offset += 0x8000) {
|
|
1424
|
-
binary += String.fromCharCode(...value.subarray(offset, offset + 0x8000));
|
|
1425
|
-
}
|
|
1426
|
-
return btoa(binary);
|
|
1427
|
-
}
|
|
1428
|
-
|
|
1429
1718
|
function base64ToBytes(value) {
|
|
1430
1719
|
const binary = atob(value);
|
|
1431
1720
|
const bytes = new Uint8Array(binary.length);
|
|
@@ -1438,11 +1727,17 @@ export async function createFxAgent(options = {}) {
|
|
|
1438
1727
|
const hostTools = normalizeHostTools(options.tools);
|
|
1439
1728
|
const instructions = normalizeInstructions(options.instructions);
|
|
1440
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
|
+
}
|
|
1441
1735
|
const pending = new Map();
|
|
1442
1736
|
let nextId = 1;
|
|
1443
1737
|
let sessionId = null;
|
|
1444
1738
|
let activeTurn = null;
|
|
1445
1739
|
let closing = false;
|
|
1740
|
+
let coreExitError = null;
|
|
1446
1741
|
const isCurrentTurn = (turn) => turn && activeTurn === turn && !turn.cancelled && !closing;
|
|
1447
1742
|
const emit = (type, detail = {}) => {
|
|
1448
1743
|
try { options.onEvent?.({ type, timestamp: performance.now(), ...detail }); } catch {}
|
|
@@ -1572,19 +1867,59 @@ export async function createFxAgent(options = {}) {
|
|
|
1572
1867
|
const send = (message) => {
|
|
1573
1868
|
if (closing) throw new Error("fx agent is closing");
|
|
1574
1869
|
emit("acp.send", { message });
|
|
1870
|
+
if (message.method === "session/prompt" && activeTurn?.cancelled) throw new Error("Cancelled");
|
|
1575
1871
|
runtime.write(`${JSON.stringify(message)}\n`);
|
|
1872
|
+
if (message.method === "session/prompt") activeTurn?.promptWritten();
|
|
1576
1873
|
};
|
|
1577
1874
|
const request = (method, params = {}) => new Promise((resolve, reject) => {
|
|
1578
1875
|
const id = nextId++;
|
|
1579
1876
|
pending.set(id, { resolve, reject });
|
|
1580
1877
|
try { send({ jsonrpc: "2.0", id, method, params }); } catch (error) { pending.delete(id); reject(error); }
|
|
1581
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
|
+
};
|
|
1582
1915
|
runtime.exited.then((code) => {
|
|
1583
|
-
emit("runtime.exit", { code });
|
|
1584
1916
|
closing = true;
|
|
1585
1917
|
const error = runtime.error ?? new Error(`fx-core exited with code ${code} before completing the ACP request`);
|
|
1918
|
+
coreExitError = error;
|
|
1919
|
+
activeTurn?.failImagePrep(error);
|
|
1586
1920
|
for (const waiter of pending.values()) waiter.reject(error);
|
|
1587
1921
|
pending.clear();
|
|
1922
|
+
emit("runtime.exit", { code });
|
|
1588
1923
|
});
|
|
1589
1924
|
runtime.setLineHandler((message, size) => {
|
|
1590
1925
|
emit("acp.receive", { message });
|
|
@@ -1616,6 +1951,10 @@ export async function createFxAgent(options = {}) {
|
|
|
1616
1951
|
);
|
|
1617
1952
|
if (cancelled || closing) return;
|
|
1618
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
|
+
}
|
|
1619
1958
|
if (encoder.encode(JSON.stringify(response)).length + 1 > 8 * 1024 * 1024) {
|
|
1620
1959
|
response.result = { content: "Host tool result exceeded the response frame limit", isError: true };
|
|
1621
1960
|
}
|
|
@@ -1623,7 +1962,7 @@ export async function createFxAgent(options = {}) {
|
|
|
1623
1962
|
return;
|
|
1624
1963
|
}
|
|
1625
1964
|
const waiter = pending.get(message.id); if (!waiter) return; pending.delete(message.id);
|
|
1626
|
-
if (message.error) waiter.reject(
|
|
1965
|
+
if (message.error) waiter.reject(agentRpcError(message.error)); else waiter.resolve(message.result);
|
|
1627
1966
|
}
|
|
1628
1967
|
try {
|
|
1629
1968
|
await request("initialize", {
|
|
@@ -1638,17 +1977,15 @@ export async function createFxAgent(options = {}) {
|
|
|
1638
1977
|
const sessionResult = await request("libfx/new");
|
|
1639
1978
|
sessionId = sessionResult.sessionId;
|
|
1640
1979
|
if (initialCheckpoint) {
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
checkpoint: bytesToBase64(initialCheckpoint),
|
|
1644
|
-
});
|
|
1980
|
+
const [checkpointAttachment] = attachBytes([initialCheckpoint]);
|
|
1981
|
+
await request("libfx/restore", { sessionId, checkpointAttachment });
|
|
1645
1982
|
}
|
|
1646
1983
|
} catch (error) {
|
|
1647
1984
|
closing = true;
|
|
1648
1985
|
try { runtime.abortHostEffects(); } catch {}
|
|
1649
1986
|
try { runtime.closeStdin(); } catch {}
|
|
1650
1987
|
try { await runtime.exited; } catch {}
|
|
1651
|
-
throw
|
|
1988
|
+
throw error;
|
|
1652
1989
|
}
|
|
1653
1990
|
|
|
1654
1991
|
const agent = {
|
|
@@ -1657,12 +1994,21 @@ export async function createFxAgent(options = {}) {
|
|
|
1657
1994
|
if (activeTurn) throw new Error("a prompt is already in progress for this session");
|
|
1658
1995
|
return normalizeTurn(startTurn(input, promptOptions));
|
|
1659
1996
|
},
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
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;
|
|
1666
2012
|
},
|
|
1667
2013
|
async close() {
|
|
1668
2014
|
if (closing) { await runtime.exited; return; }
|
|
@@ -1764,10 +2110,18 @@ export async function createFxAgent(options = {}) {
|
|
|
1764
2110
|
}
|
|
1765
2111
|
|
|
1766
2112
|
function startTurn(input, promptOptions) {
|
|
1767
|
-
const
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
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);
|
|
1771
2125
|
const signal = promptOptions.signal;
|
|
1772
2126
|
if (signal !== undefined && (typeof signal?.addEventListener !== "function" || typeof signal?.removeEventListener !== "function")) throw new TypeError("prompt signal must be an AbortSignal");
|
|
1773
2127
|
const queue = [];
|
|
@@ -1778,6 +2132,12 @@ export async function createFxAgent(options = {}) {
|
|
|
1778
2132
|
let terminalError;
|
|
1779
2133
|
let reportedPressure = false;
|
|
1780
2134
|
let discardedBytes = 0;
|
|
2135
|
+
let cancelImagePrep = null;
|
|
2136
|
+
let rejectImagePrep = null;
|
|
2137
|
+
let resolvePromptStart = null;
|
|
2138
|
+
const promptStarted = asyncImages ? new Promise((resolve) => { resolvePromptStart = resolve; }) : null;
|
|
2139
|
+
let pendingSteeringCount = 0;
|
|
2140
|
+
let pendingSteeringBytes = 0;
|
|
1781
2141
|
const toolControllers = new Set();
|
|
1782
2142
|
let finished = false;
|
|
1783
2143
|
let cancelled = false;
|
|
@@ -1802,34 +2162,63 @@ export async function createFxAgent(options = {}) {
|
|
|
1802
2162
|
transportBytes: 0,
|
|
1803
2163
|
lastTransportActivityAt: null,
|
|
1804
2164
|
get cancelled() { return cancelled; },
|
|
2165
|
+
failImagePrep(error) {
|
|
2166
|
+
rejectImagePrep?.(error);
|
|
2167
|
+
rejectImagePrep = null;
|
|
2168
|
+
cancelImagePrep = null;
|
|
2169
|
+
},
|
|
2170
|
+
promptWritten() {
|
|
2171
|
+
resolvePromptStart?.(true);
|
|
2172
|
+
resolvePromptStart = null;
|
|
2173
|
+
},
|
|
1805
2174
|
steer(text) {
|
|
1806
2175
|
if (finished || cancelled || activeTurn !== turn) {
|
|
1807
2176
|
return Promise.reject(new Error("no prompt is running"));
|
|
1808
2177
|
}
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
if (
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
2178
|
+
if (closing) return Promise.reject(coreExitError ?? new Error("fx agent is closing"));
|
|
2179
|
+
const apply = () => {
|
|
2180
|
+
if (finished || cancelled || activeTurn !== turn) {
|
|
2181
|
+
return Promise.reject(new Error("no prompt is running"));
|
|
2182
|
+
}
|
|
2183
|
+
if (closing) return Promise.reject(coreExitError ?? new Error("fx agent is closing"));
|
|
2184
|
+
try {
|
|
2185
|
+
if (typeof runtime.steer === "function") {
|
|
2186
|
+
runtime.steer(text);
|
|
2187
|
+
void turn.push({
|
|
2188
|
+
sessionUpdate: "user_message_chunk",
|
|
2189
|
+
content: { type: "text", text },
|
|
2190
|
+
});
|
|
2191
|
+
return Promise.resolve();
|
|
2192
|
+
}
|
|
2193
|
+
return request("libfx/steer", { sessionId, text });
|
|
2194
|
+
} catch (error) {
|
|
2195
|
+
return Promise.reject(error);
|
|
1820
2196
|
}
|
|
1821
|
-
}
|
|
1822
|
-
|
|
2197
|
+
};
|
|
2198
|
+
if (!resolvePromptStart) return apply();
|
|
2199
|
+
const bytes = encoder.encode(text).length;
|
|
2200
|
+
if (pendingSteeringCount >= maxSteeringMessages || bytes > maxSteeringQueueBytes - pendingSteeringBytes) {
|
|
2201
|
+
return Promise.reject(new Error("steering queue is full"));
|
|
1823
2202
|
}
|
|
1824
|
-
|
|
2203
|
+
pendingSteeringCount++;
|
|
2204
|
+
pendingSteeringBytes += bytes;
|
|
2205
|
+
return promptStarted.then((started) => {
|
|
2206
|
+
if (coreExitError && !cancelled) throw coreExitError;
|
|
2207
|
+
return started === true ? apply() : Promise.reject(started instanceof Error ? started : new Error("no prompt is running"));
|
|
2208
|
+
}).finally(() => { pendingSteeringCount--; pendingSteeringBytes -= bytes; });
|
|
1825
2209
|
},
|
|
1826
2210
|
cancel() {
|
|
1827
2211
|
if (finished || cancelled) return;
|
|
1828
2212
|
cancelled = true;
|
|
2213
|
+
cancelImagePrep?.();
|
|
2214
|
+
cancelImagePrep = null;
|
|
2215
|
+
rejectImagePrep = null;
|
|
2216
|
+
resolvePromptStart?.(false);
|
|
2217
|
+
resolvePromptStart = null;
|
|
1829
2218
|
runtime.closeSteering?.();
|
|
1830
2219
|
resumeOutput?.();
|
|
1831
2220
|
resumeOutput = null;
|
|
1832
|
-
send({ jsonrpc: "2.0", method: "session/cancel", params: { sessionId } });
|
|
2221
|
+
if (!closing) send({ jsonrpc: "2.0", method: "session/cancel", params: { sessionId } });
|
|
1833
2222
|
for (const controller of toolControllers) controller.abort();
|
|
1834
2223
|
runtime.abortHostEffects();
|
|
1835
2224
|
},
|
|
@@ -1854,6 +2243,8 @@ export async function createFxAgent(options = {}) {
|
|
|
1854
2243
|
},
|
|
1855
2244
|
};
|
|
1856
2245
|
if (signal?.aborted) {
|
|
2246
|
+
resolvePromptStart?.(false);
|
|
2247
|
+
resolvePromptStart = null;
|
|
1857
2248
|
finished = true;
|
|
1858
2249
|
turn.result = Promise.resolve({ stopReason: "cancelled" });
|
|
1859
2250
|
return turn;
|
|
@@ -1862,10 +2253,37 @@ export async function createFxAgent(options = {}) {
|
|
|
1862
2253
|
runtime.openSteering?.();
|
|
1863
2254
|
const abort = () => turn.cancel();
|
|
1864
2255
|
signal?.addEventListener("abort", abort, { once: true });
|
|
1865
|
-
|
|
1866
|
-
|
|
2256
|
+
const imagePrepCancelled = asyncImages ? new Promise((resolve, reject) => {
|
|
2257
|
+
cancelImagePrep = () => resolve(null);
|
|
2258
|
+
rejectImagePrep = reject;
|
|
2259
|
+
}) : null;
|
|
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;
|
|
2268
|
+
if (coreExitError && !cancelled) throw coreExitError;
|
|
2269
|
+
if (prepared === null || cancelled || closing) {
|
|
2270
|
+
resolvePromptStart?.(false);
|
|
2271
|
+
resolvePromptStart = null;
|
|
2272
|
+
return { stopReason: "cancelled" };
|
|
2273
|
+
}
|
|
2274
|
+
return sendPrompt(prepared);
|
|
2275
|
+
});
|
|
2276
|
+
} else {
|
|
2277
|
+
try { response = sendPrompt(prompt); } catch (error) { response = Promise.reject(error); }
|
|
2278
|
+
}
|
|
2279
|
+
turn.result = response
|
|
2280
|
+
.then((value) => ({ stopReason: cancelled ? "cancelled" : value.stopReason, usage: value.usage }))
|
|
1867
2281
|
.catch((error) => {
|
|
1868
|
-
|
|
2282
|
+
cancelImagePrep = null;
|
|
2283
|
+
rejectImagePrep = null;
|
|
2284
|
+
resolvePromptStart?.(error);
|
|
2285
|
+
resolvePromptStart = null;
|
|
2286
|
+
if (cancelled && error.message === "Cancelled") return { stopReason: "cancelled" };
|
|
1869
2287
|
terminalError = error;
|
|
1870
2288
|
throw error;
|
|
1871
2289
|
})
|