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/node.cjs CHANGED
@@ -198,7 +198,12 @@ var maxUnreadEventBytes = 1024 * 1024;
198
198
  var maxUnreadEvents = 256;
199
199
  var maxPromptImages = 8;
200
200
  var maxPromptImageDataBytes = 5 * 1024 * 1024;
201
- var maxPromptImagesBytes = 8 * 1024 * 1024;
201
+ var maxPromptImagesDataBytes = 8 * 1024 * 1024;
202
+ var maxPromptImageBytes = maxPromptImageDataBytes / 4 * 3;
203
+ var maxPromptImagesBytes = maxPromptImagesDataBytes / 4 * 3;
204
+ var maxPendingAttachments = 8;
205
+ var maxOutboundAttachments = 4;
206
+ var maxCheckpointBytes = 4 * 1024 * 1024;
202
207
  var maxPromptFrameBytes = 8 * 1024 * 1024;
203
208
  var promptFrameEnvelopeBytes = 128;
204
209
  var maxSteeringMessageBytes = 64 * 1024;
@@ -255,6 +260,14 @@ function normalizeFast(value) {
255
260
  }
256
261
  return value;
257
262
  }
263
+ function normalizeUltrafast(value) {
264
+ if (value === undefined)
265
+ return;
266
+ if (typeof value !== "boolean") {
267
+ throw new TypeError("ultrafast must be a boolean");
268
+ }
269
+ return value;
270
+ }
258
271
  function normalizeAgentOptions(value) {
259
272
  if (!value || typeof value !== "object" || Array.isArray(value)) {
260
273
  throw new TypeError("createFxAgent() options must be an object");
@@ -264,10 +277,30 @@ function normalizeAgentOptions(value) {
264
277
  throw new TypeError("createFxAgent() does not accept env; pass apiKey and model directly");
265
278
  }
266
279
  options.apiKey = boundedString(options.apiKey, "apiKey", maxApiKeyBytes, true);
267
- options.model = boundedString(options.model, "model", maxModelBytes, false);
268
- options.effort = normalizeEffort(options.effort);
269
- options.fast = normalizeFast(options.fast);
280
+ if (options.model !== null && typeof options.model === "object" && !Array.isArray(options.model)) {
281
+ if (Object.hasOwn(options, "effort") || Object.hasOwn(options, "fast") || Object.hasOwn(options, "ultrafast")) {
282
+ throw new TypeError("model options cannot be mixed with top-level effort, fast, or ultrafast");
283
+ }
284
+ const model = options.model;
285
+ for (const name of Object.keys(model)) {
286
+ if (name !== "id" && name !== "effort" && name !== "fast" && name !== "ultrafast") {
287
+ throw new TypeError(`unsupported model option: ${name}`);
288
+ }
289
+ }
290
+ options.model = boundedString(model.id, "model.id", maxModelBytes, true);
291
+ options.effort = normalizeEffort(model.effort);
292
+ options.fast = normalizeFast(model.fast);
293
+ options.ultrafast = normalizeUltrafast(model.ultrafast);
294
+ } else {
295
+ options.model = boundedString(options.model, "model", maxModelBytes, false);
296
+ options.effort = normalizeEffort(options.effort);
297
+ options.fast = normalizeFast(options.fast);
298
+ options.ultrafast = normalizeUltrafast(options.ultrafast);
299
+ }
270
300
  validateGatewayChatUrl(options.gatewayChatUrl);
301
+ if (options.resizeImage !== undefined && typeof options.resizeImage !== "function") {
302
+ throw new TypeError("resizeImage must be a function");
303
+ }
271
304
  return options;
272
305
  }
273
306
  function agentEnvironment(options) {
@@ -276,15 +309,17 @@ function agentEnvironment(options) {
276
309
  ...options.model === undefined ? {} : { FX_MODEL: options.model },
277
310
  ...options.effort === undefined ? {} : { FX_EFFORT: options.effort },
278
311
  ...options.fast === undefined ? {} : { FX_FAST: options.fast ? "true" : "false" },
312
+ ...options.ultrafast === undefined ? {} : { FX_ULTRAFAST: options.ultrafast ? "true" : "false" },
279
313
  ...options.gatewayChatUrl === undefined ? {} : { FX_GATEWAY_CHAT_URL: options.gatewayChatUrl }
280
314
  };
281
315
  }
282
- function agentBootstrapError(error) {
283
- if (error instanceof Error && (error.message === "Invalid reasoning effort" || error.message.startsWith("Reasoning effort"))) {
284
- error.code ??= "LIBFX_UNSUPPORTED_EFFORT";
285
- }
286
- if (error instanceof Error && error.message.startsWith("Fast mode")) {
287
- error.code ??= "LIBFX_UNSUPPORTED_FAST";
316
+ function agentRpcError(response) {
317
+ const error = new Error(response.message);
318
+ const data = response.data;
319
+ if (data && ["LIBFX_MODEL_UNSUPPORTED_EFFORT", "LIBFX_MODEL_UNSUPPORTED_FAST", "LIBFX_MODEL_UNSUPPORTED_ULTRAFAST"].includes(data.code) && typeof data.model === "string" && data.capability === (data.code === "LIBFX_MODEL_UNSUPPORTED_FAST" ? "fast" : data.code === "LIBFX_MODEL_UNSUPPORTED_ULTRAFAST" ? "ultrafast" : "effort")) {
320
+ error.code = data.code;
321
+ error.model = data.model;
322
+ error.capability = data.capability;
288
323
  }
289
324
  return error;
290
325
  }
@@ -644,6 +679,9 @@ function createRuntime(options) {
644
679
  const steering = [];
645
680
  let steeringBytes = 0;
646
681
  let steeringOpen = false;
682
+ const inboundAttachments = new Map;
683
+ const outboundAttachments = new Map;
684
+ let nextOutboundAttachment = 1;
647
685
  const workspaceExecs = new Set;
648
686
  const workspace = prepareWorkspaceAdapter(options.workspace);
649
687
  const args = ["fx", ...options.args || []];
@@ -967,6 +1005,37 @@ function createRuntime(options) {
967
1005
  steeringBytes -= value.length;
968
1006
  return value.length;
969
1007
  }
1008
+ function writeAttachment(id, data) {
1009
+ if (inboundAttachments.size >= maxPendingAttachments)
1010
+ throw new Error("attachment table is full");
1011
+ inboundAttachments.set(id, data.slice());
1012
+ }
1013
+ function attachmentSize(id) {
1014
+ return inboundAttachments.get(id >>> 0)?.length ?? -1;
1015
+ }
1016
+ function attachmentTake(id, outputPtr, outputCap) {
1017
+ const key = id >>> 0;
1018
+ const value = inboundAttachments.get(key);
1019
+ if (!value)
1020
+ return -1;
1021
+ if (value.length > 0) {
1022
+ const output = checkedBytes(outputPtr, outputCap);
1023
+ if (!output || value.length > output.length)
1024
+ return -1;
1025
+ output.set(value);
1026
+ }
1027
+ inboundAttachments.delete(key);
1028
+ return value.length;
1029
+ }
1030
+ function attachmentPut(inputPtr, inputLen) {
1031
+ const input = checkedBytes(inputPtr, inputLen);
1032
+ if (!input || outboundAttachments.size >= maxOutboundAttachments)
1033
+ return -1;
1034
+ const id = nextOutboundAttachment;
1035
+ nextOutboundAttachment = id === 2147483647 ? 1 : id + 1;
1036
+ outboundAttachments.set(id, input.slice());
1037
+ return id;
1038
+ }
970
1039
  let pendingHostToolResult = null;
971
1040
  function hostToolCall(namePtr, nameLen, argumentsPtr, argumentsLen, outputPtr, outputCap, statusPtr) {
972
1041
  pendingHostToolResult = null;
@@ -1430,6 +1499,9 @@ function createRuntime(options) {
1430
1499
  steeringOpen = false;
1431
1500
  clearSteering();
1432
1501
  },
1502
+ fx_attachment_size: attachmentSize,
1503
+ fx_attachment_take: attachmentTake,
1504
+ fx_attachment_put: attachmentPut,
1433
1505
  fx_open_url: new WebAssembly.Suspending(openUrl),
1434
1506
  fx_oauth_session_load: new WebAssembly.Suspending(oauthSessionLoad),
1435
1507
  fx_oauth_session_commit: new WebAssembly.Suspending(oauthSessionCommit),
@@ -1460,6 +1532,15 @@ function createRuntime(options) {
1460
1532
  write(data) {
1461
1533
  stdin.push(typeof data === "string" ? encoder.encode(data) : data);
1462
1534
  },
1535
+ writeAttachment,
1536
+ takeAttachment(id) {
1537
+ const value = outboundAttachments.get(id) ?? null;
1538
+ outboundAttachments.delete(id);
1539
+ return value;
1540
+ },
1541
+ discardAttachments() {
1542
+ inboundAttachments.clear();
1543
+ },
1463
1544
  wake() {
1464
1545
  stdin.wake();
1465
1546
  },
@@ -1634,35 +1715,131 @@ async function createFxTerminal(options) {
1634
1715
  }
1635
1716
  };
1636
1717
  }
1637
- function normalizePromptInput(input) {
1718
+ function blobByteLength(value) {
1719
+ if (typeof Blob === "undefined" || value == null)
1720
+ return null;
1721
+ try {
1722
+ return Object.getOwnPropertyDescriptor(Blob.prototype, "size").get.call(value);
1723
+ } catch {
1724
+ return null;
1725
+ }
1726
+ }
1727
+ function normalizeImageSourceRef(value, name) {
1728
+ boundedString(value, `${name} sourceRef`, 512, false);
1729
+ if (value !== undefined && /[\x00-\x1f\x7f\uD800-\uDFFF]/u.test(value)) {
1730
+ throw new TypeError(`${name} sourceRef must be valid UTF-8 without ASCII controls`);
1731
+ }
1732
+ return value;
1733
+ }
1734
+ function omitReferencedImageData(blocks) {
1735
+ return blocks.map((block) => block.type !== "image" || block.sourceRef === undefined ? block : {
1736
+ type: "image",
1737
+ mimeType: block.mimeType,
1738
+ sourceRef: block.sourceRef
1739
+ });
1740
+ }
1741
+ function promptImageDataBytes(prompt) {
1742
+ return prompt.reduce((total, block) => {
1743
+ if (block.type !== "image")
1744
+ return total;
1745
+ if (typeof block.data === "string")
1746
+ return total + block.data.length;
1747
+ const byteLength = block.bytes?.byteLength ?? block.byteLength ?? blobByteLength(block.data) ?? 0;
1748
+ return total + Math.ceil(byteLength / 3) * 4;
1749
+ }, 0);
1750
+ }
1751
+ function byteView(value) {
1752
+ if (value instanceof ArrayBuffer)
1753
+ return new Uint8Array(value);
1754
+ if (ArrayBuffer.isView(value))
1755
+ return new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
1756
+ return null;
1757
+ }
1758
+ var base64Alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
1759
+ function canonicalBase64ByteLength(value) {
1760
+ if (value.length === 0 || value.length % 4 !== 0 || !/^[A-Za-z0-9+/]*={0,2}$/.test(value))
1761
+ return -1;
1762
+ const groups = value.length / 4;
1763
+ if (value.endsWith("==")) {
1764
+ return (base64Alphabet.indexOf(value[value.length - 3]) & 15) === 0 ? groups * 3 - 2 : -1;
1765
+ }
1766
+ if (value.endsWith("=")) {
1767
+ return (base64Alphabet.indexOf(value[value.length - 2]) & 3) === 0 ? groups * 3 - 1 : -1;
1768
+ }
1769
+ return groups * 3;
1770
+ }
1771
+ function requireImageMimeType(mimeType, index) {
1772
+ if (typeof mimeType !== "string" || mimeType.length === 0 || mimeType.length > 128) {
1773
+ throw new TypeError(`image prompt block ${index} requires a mimeType`);
1774
+ }
1775
+ }
1776
+ function checkImageByteLength(byteLength, index) {
1777
+ if (byteLength > maxPromptImageBytes) {
1778
+ throw new RangeError(`image prompt block ${index} exceeds the ${maxPromptImageBytes} byte per-image libfx limit`);
1779
+ }
1780
+ }
1781
+ function checkPromptImagesByteLength(byteLength) {
1782
+ if (byteLength > maxPromptImagesBytes) {
1783
+ throw new RangeError(`prompt images exceed the ${maxPromptImagesBytes} byte libfx limit`);
1784
+ }
1785
+ }
1786
+ function normalizePromptImage(block, index) {
1787
+ const sourceRef = normalizeImageSourceRef(block.sourceRef, `image prompt block ${index}`);
1788
+ const reference = sourceRef === undefined ? {} : { sourceRef };
1789
+ if (block.data === undefined && sourceRef !== undefined) {
1790
+ requireImageMimeType(block.mimeType, index);
1791
+ return { type: "image", mimeType: block.mimeType, ...reference };
1792
+ }
1793
+ const size = blobByteLength(block.data);
1794
+ if (size !== null) {
1795
+ const mimeType = block.data.type;
1796
+ if (block.mimeType !== undefined && block.mimeType !== mimeType) {
1797
+ throw new TypeError(`image prompt block ${index} mimeType disagrees with Blob.type`);
1798
+ }
1799
+ requireImageMimeType(mimeType, index);
1800
+ if (!Number.isSafeInteger(size) || size <= 0) {
1801
+ throw new TypeError(`image prompt block ${index} requires a non-empty Blob with a valid size`);
1802
+ }
1803
+ return { type: "image", source: "blob", data: block.data, mimeType, byteLength: size, ...reference };
1804
+ }
1805
+ if (typeof block.data === "string" && block.data.length > 0) {
1806
+ requireImageMimeType(block.mimeType, index);
1807
+ const byteLength = canonicalBase64ByteLength(block.data);
1808
+ if (byteLength <= 0)
1809
+ throw new TypeError(`image prompt block ${index} requires canonical base64 data`);
1810
+ return { type: "image", source: "base64", data: block.data, mimeType: block.mimeType, byteLength, ...reference };
1811
+ }
1812
+ const bytes = typeof block.data === "string" ? null : byteView(block.data);
1813
+ if (!bytes || bytes.byteLength === 0) {
1814
+ throw new TypeError(`image prompt block ${index} requires base64 data, bytes, or a Blob`);
1815
+ }
1816
+ requireImageMimeType(block.mimeType, index);
1817
+ return { type: "image", source: "bytes", data: bytes, mimeType: block.mimeType, byteLength: bytes.byteLength, ...reference };
1818
+ }
1819
+ function normalizePromptInput(input, { deferImageLimits = false } = {}) {
1638
1820
  if (typeof input === "string")
1639
1821
  return [{ type: "text", text: input }];
1640
1822
  if (!Array.isArray(input))
1641
1823
  throw new TypeError("prompt input must be a string or an array of prompt blocks");
1642
1824
  let imageCount = 0;
1643
1825
  let imageBytes = 0;
1644
- return input.map((block, index) => {
1826
+ let prompt = input.map((block, index) => {
1645
1827
  if (!block || typeof block !== "object")
1646
1828
  throw new TypeError(`prompt block ${index} must be an object`);
1647
1829
  if (block.type === "image") {
1648
- if (typeof block.data !== "string" || block.data.length === 0) {
1649
- throw new TypeError(`image prompt block ${index} requires base64 data`);
1650
- }
1651
- if (typeof block.mimeType !== "string" || block.mimeType.length === 0 || block.mimeType.length > 128) {
1652
- throw new TypeError(`image prompt block ${index} requires a mimeType`);
1653
- }
1654
- if (block.data.length > maxPromptImageDataBytes) {
1655
- throw new RangeError(`image prompt block ${index} exceeds the ${maxPromptImageDataBytes} byte per-image libfx limit`);
1656
- }
1830
+ const image = normalizePromptImage(block, index);
1657
1831
  imageCount += 1;
1658
1832
  if (imageCount > maxPromptImages) {
1659
1833
  throw new RangeError(`prompt cannot contain more than ${maxPromptImages} images`);
1660
1834
  }
1661
- imageBytes += block.data.length;
1662
- if (imageBytes > maxPromptImagesBytes) {
1663
- throw new RangeError(`prompt images exceed the ${maxPromptImagesBytes} byte libfx frame limit`);
1835
+ if (!deferImageLimits) {
1836
+ if (image.byteLength > maxPromptImageBytes && image.sourceRef !== undefined) {
1837
+ return omitReferencedImageData([image])[0];
1838
+ }
1839
+ checkImageByteLength(image.byteLength ?? 0, index);
1840
+ imageBytes += image.byteLength ?? 0;
1664
1841
  }
1665
- return { type: "image", data: block.data, mimeType: block.mimeType };
1842
+ return image;
1666
1843
  }
1667
1844
  if (block.type === "text") {
1668
1845
  if (typeof block.text !== "string")
@@ -1679,6 +1856,89 @@ function normalizePromptInput(input) {
1679
1856
  }
1680
1857
  throw new TypeError(`unsupported prompt block type: ${String(block.type)}`);
1681
1858
  });
1859
+ if (!deferImageLimits) {
1860
+ if (imageBytes > maxPromptImagesBytes) {
1861
+ prompt = omitReferencedImageData(prompt);
1862
+ imageBytes = prompt.reduce((total, block) => total + (block.type === "image" ? block.byteLength ?? 0 : 0), 0);
1863
+ }
1864
+ checkPromptImagesByteLength(imageBytes);
1865
+ if (promptFrameSize(prompt) > maxPromptFrameBytes)
1866
+ prompt = omitReferencedImageData(prompt);
1867
+ }
1868
+ return prompt;
1869
+ }
1870
+ function promptFrameSize(prompt, countImages = true) {
1871
+ const encodedImageBytes = countImages ? promptImageDataBytes(prompt) : 0;
1872
+ const projected = prompt.map((block) => block.type !== "image" ? block : {
1873
+ type: "image",
1874
+ mimeType: block.mimeType,
1875
+ ...block.sourceRef === undefined ? {} : { sourceRef: block.sourceRef },
1876
+ ...block.data === undefined && block.bytes === undefined ? {} : { _meta: { fx: { attachment: 4294967295 } } }
1877
+ });
1878
+ return encoder.encode(JSON.stringify({ sessionId: "", prompt: projected })).length + encodedImageBytes + promptFrameEnvelopeBytes;
1879
+ }
1880
+ function checkPromptFrameSize(prompt, countImages) {
1881
+ if (promptFrameSize(prompt, countImages) > maxPromptFrameBytes) {
1882
+ throw new RangeError(`prompt exceeds the ${maxPromptFrameBytes} byte libfx frame limit`);
1883
+ }
1884
+ }
1885
+ function preparePromptImages(blocks) {
1886
+ return blocks.map((block) => block.type !== "image" || block.data === undefined ? block : {
1887
+ type: "image",
1888
+ mimeType: block.mimeType,
1889
+ bytes: block.source === "base64" ? base64ToBytes(block.data) : block.data,
1890
+ ...block.sourceRef === undefined ? {} : { sourceRef: block.sourceRef }
1891
+ });
1892
+ }
1893
+ function resizedPromptImage(value, index) {
1894
+ const bytes = byteView(value?.bytes);
1895
+ if (!bytes || bytes.byteLength === 0 || typeof value.mimeType !== "string" || value.mimeType.length === 0 || value.mimeType.length > 128) {
1896
+ throw new TypeError(`resizeImage must return non-empty bytes and a mimeType for image prompt block ${index}`);
1897
+ }
1898
+ return { bytes: bytes.slice(), mimeType: value.mimeType };
1899
+ }
1900
+ async function materializePromptImages(blocks, isCancelled, resizeImage) {
1901
+ let prepared = [];
1902
+ let imageBytes = 0;
1903
+ for (let index = 0;index < blocks.length; index++) {
1904
+ if (isCancelled())
1905
+ return null;
1906
+ const block = blocks[index];
1907
+ if (block.type !== "image" || block.data === undefined) {
1908
+ prepared.push(block);
1909
+ continue;
1910
+ }
1911
+ let image = block.source === "blob" ? { mimeType: block.mimeType, bytes: new Uint8Array(await block.data.arrayBuffer()) } : preparePromptImages([block])[0];
1912
+ if (isCancelled())
1913
+ return null;
1914
+ if (resizeImage) {
1915
+ image = resizedPromptImage(await resizeImage({ bytes: image.bytes, mimeType: image.mimeType }), index);
1916
+ if (isCancelled())
1917
+ return null;
1918
+ }
1919
+ image = {
1920
+ type: "image",
1921
+ mimeType: image.mimeType,
1922
+ bytes: image.bytes,
1923
+ ...block.sourceRef === undefined ? {} : { sourceRef: block.sourceRef }
1924
+ };
1925
+ if (image.bytes.byteLength > maxPromptImageBytes && image.sourceRef !== undefined) {
1926
+ image = omitReferencedImageData([image])[0];
1927
+ }
1928
+ checkImageByteLength(image.bytes?.byteLength ?? 0, index);
1929
+ imageBytes += image.bytes?.byteLength ?? 0;
1930
+ prepared.push(image);
1931
+ if (imageBytes > maxPromptImagesBytes) {
1932
+ prepared = omitReferencedImageData(prepared);
1933
+ blocks = omitReferencedImageData(blocks);
1934
+ imageBytes = prepared.reduce((total, entry) => total + (entry.bytes?.byteLength ?? 0), 0);
1935
+ }
1936
+ checkPromptImagesByteLength(imageBytes);
1937
+ }
1938
+ if (promptFrameSize(prepared) > maxPromptFrameBytes)
1939
+ prepared = omitReferencedImageData(prepared);
1940
+ checkPromptFrameSize(prepared, true);
1941
+ return prepared;
1682
1942
  }
1683
1943
  function normalizeSteeringInput(input) {
1684
1944
  const blocks = normalizePromptInput(input);
@@ -1765,18 +2025,31 @@ function hostToolContent(value) {
1765
2025
  if (typeof value.text !== "string" || !Array.isArray(value.images) || value.images.length > 8) {
1766
2026
  throw new TypeError("invalid typed tool result");
1767
2027
  }
1768
- let imageBytes = 0;
1769
- const images = value.images.map((image) => {
1770
- if (image?.type !== "image" || typeof image.data !== "string" || typeof image.mimeType !== "string" || image.mimeType.length > 128 || image.data.length > 5 * 1024 * 1024) {
2028
+ let images = value.images.map((image) => {
2029
+ if (image?.type !== "image")
2030
+ throw new TypeError("invalid tool image");
2031
+ const sourceRef = normalizeImageSourceRef(image.sourceRef, "tool image");
2032
+ const referenceOnly = image.data === undefined && sourceRef !== undefined;
2033
+ if (!referenceOnly && typeof image.data !== "string" || typeof image.mimeType !== "string" || image.mimeType.length === 0 || image.mimeType.length > 128 || image.data?.length > maxPromptImageDataBytes && sourceRef === undefined) {
1771
2034
  throw new TypeError("invalid tool image");
1772
2035
  }
1773
- imageBytes += image.data.length;
1774
- if (imageBytes > 8 * 1024 * 1024)
1775
- throw new RangeError("tool images exceed the result limit");
1776
- return { type: "image", data: image.data, mimeType: image.mimeType };
2036
+ return {
2037
+ type: "image",
2038
+ ...!referenceOnly && image.data.length <= maxPromptImageDataBytes ? { data: image.data } : {},
2039
+ mimeType: image.mimeType,
2040
+ ...sourceRef === undefined ? {} : { sourceRef }
2041
+ };
1777
2042
  });
1778
- const content = JSON.stringify({ text: value.text, images });
1779
- if (new TextEncoder().encode(content).length > 8 * 1024 * 1024)
2043
+ if (promptImageDataBytes(images) > maxPromptImagesDataBytes)
2044
+ images = omitReferencedImageData(images);
2045
+ if (promptImageDataBytes(images) > maxPromptImagesDataBytes)
2046
+ throw new RangeError("tool images exceed the result limit");
2047
+ let content = JSON.stringify({ text: value.text, images });
2048
+ if (encoder.encode(content).length > maxPromptImagesDataBytes) {
2049
+ images = omitReferencedImageData(images);
2050
+ content = JSON.stringify({ text: value.text, images });
2051
+ }
2052
+ if (encoder.encode(content).length > maxPromptImagesDataBytes)
1780
2053
  throw new RangeError("typed tool result exceeds the result limit");
1781
2054
  return { content, rich: true, isError: value.isError === true };
1782
2055
  }
@@ -1799,13 +2072,6 @@ function checkpointBytes(value) {
1799
2072
  }
1800
2073
  throw new TypeError("checkpoint must be an ArrayBuffer or typed array");
1801
2074
  }
1802
- function bytesToBase64(value) {
1803
- let binary = "";
1804
- for (let offset = 0;offset < value.length; offset += 32768) {
1805
- binary += String.fromCharCode(...value.subarray(offset, offset + 32768));
1806
- }
1807
- return btoa(binary);
1808
- }
1809
2075
  function base64ToBytes(value) {
1810
2076
  const binary = atob(value);
1811
2077
  const bytes = new Uint8Array(binary.length);
@@ -1818,11 +2084,15 @@ async function createFxAgent(options = {}) {
1818
2084
  const hostTools = normalizeHostTools(options.tools);
1819
2085
  const instructions = normalizeInstructions(options.instructions);
1820
2086
  const initialCheckpoint = checkpointBytes(options.checkpoint);
2087
+ if (initialCheckpoint && initialCheckpoint.byteLength > maxCheckpointBytes) {
2088
+ throw new Error("libfx checkpoint is too large");
2089
+ }
1821
2090
  const pending = new Map;
1822
2091
  let nextId = 1;
1823
2092
  let sessionId = null;
1824
2093
  let activeTurn = null;
1825
2094
  let closing = false;
2095
+ let coreExitError = null;
1826
2096
  const isCurrentTurn = (turn) => turn && activeTurn === turn && !turn.cancelled && !closing;
1827
2097
  const emit = (type, detail = {}) => {
1828
2098
  try {
@@ -1962,8 +2232,12 @@ async function createFxAgent(options = {}) {
1962
2232
  if (closing)
1963
2233
  throw new Error("fx agent is closing");
1964
2234
  emit("acp.send", { message });
2235
+ if (message.method === "session/prompt" && activeTurn?.cancelled)
2236
+ throw new Error("Cancelled");
1965
2237
  runtime.write(`${JSON.stringify(message)}
1966
2238
  `);
2239
+ if (message.method === "session/prompt")
2240
+ activeTurn?.promptWritten();
1967
2241
  };
1968
2242
  const request = (method, params = {}) => new Promise((resolve, reject) => {
1969
2243
  const id = nextId++;
@@ -1975,13 +2249,53 @@ async function createFxAgent(options = {}) {
1975
2249
  reject(error);
1976
2250
  }
1977
2251
  });
2252
+ let nextAttachmentId = 1;
2253
+ const attachBytes = (payloads) => {
2254
+ if (typeof runtime.writeAttachment !== "function")
2255
+ throw new Error("fx runtime does not accept binary attachments");
2256
+ runtime.discardAttachments?.();
2257
+ return payloads.map((bytes) => {
2258
+ const id = nextAttachmentId;
2259
+ nextAttachmentId = id === 2147483647 ? 1 : id + 1;
2260
+ runtime.writeAttachment(id, bytes);
2261
+ return id;
2262
+ });
2263
+ };
2264
+ let checkpointTail = null;
2265
+ let pendingCheckpoints = 0;
2266
+ async function takeCheckpoint() {
2267
+ if (closing)
2268
+ throw new Error("fx agent is closed");
2269
+ if (activeTurn)
2270
+ throw new Error("cannot checkpoint while a prompt is active");
2271
+ const response = await request("libfx/checkpoint", { sessionId });
2272
+ const id = response?.checkpointAttachment;
2273
+ const bytes = Number.isSafeInteger(id) && id > 0 ? runtime.takeAttachment?.(id) : null;
2274
+ if (!(bytes instanceof Uint8Array) || bytes.byteLength === 0)
2275
+ throw new Error("fx returned an invalid checkpoint");
2276
+ return new Uint8Array(bytes.buffer, bytes.byteOffset, bytes.byteLength);
2277
+ }
2278
+ const sendPrompt = (blocks) => {
2279
+ const images = blocks.filter((block) => block.type === "image" && block.bytes !== undefined);
2280
+ const ids = images.length ? attachBytes(images.map((block) => block.bytes)) : [];
2281
+ let next = 0;
2282
+ const prompt = blocks.map((block) => block.type !== "image" ? block : {
2283
+ type: "image",
2284
+ mimeType: block.mimeType,
2285
+ ...block.sourceRef === undefined ? {} : { sourceRef: block.sourceRef },
2286
+ ...block.bytes === undefined ? {} : { _meta: { fx: { attachment: ids[next++] } } }
2287
+ });
2288
+ return request("session/prompt", { sessionId, prompt });
2289
+ };
1978
2290
  runtime.exited.then((code) => {
1979
- emit("runtime.exit", { code });
1980
2291
  closing = true;
1981
2292
  const error = runtime.error ?? new Error(`fx-core exited with code ${code} before completing the ACP request`);
2293
+ coreExitError = error;
2294
+ activeTurn?.failImagePrep(error);
1982
2295
  for (const waiter of pending.values())
1983
2296
  waiter.reject(error);
1984
2297
  pending.clear();
2298
+ emit("runtime.exit", { code });
1985
2299
  });
1986
2300
  runtime.setLineHandler((message, size) => {
1987
2301
  emit("acp.receive", { message });
@@ -2017,6 +2331,10 @@ async function createFxAgent(options = {}) {
2017
2331
  if (cancelled || closing)
2018
2332
  return;
2019
2333
  const response = { jsonrpc: "2.0", id: message.id, result: { content, isError, ...rich ? { contentType: "rich" } : {} } };
2334
+ if (rich && encoder.encode(JSON.stringify(response)).length + 1 > maxPromptFrameBytes) {
2335
+ const result = JSON.parse(content);
2336
+ response.result.content = JSON.stringify({ ...result, images: omitReferencedImageData(result.images) });
2337
+ }
2020
2338
  if (encoder.encode(JSON.stringify(response)).length + 1 > 8 * 1024 * 1024) {
2021
2339
  response.result = { content: "Host tool result exceeded the response frame limit", isError: true };
2022
2340
  }
@@ -2028,7 +2346,7 @@ async function createFxAgent(options = {}) {
2028
2346
  return;
2029
2347
  pending.delete(message.id);
2030
2348
  if (message.error)
2031
- waiter.reject(new Error(message.error.message));
2349
+ waiter.reject(agentRpcError(message.error));
2032
2350
  else
2033
2351
  waiter.resolve(message.result);
2034
2352
  }
@@ -2042,10 +2360,8 @@ async function createFxAgent(options = {}) {
2042
2360
  const sessionResult = await request("libfx/new");
2043
2361
  sessionId = sessionResult.sessionId;
2044
2362
  if (initialCheckpoint) {
2045
- await request("libfx/restore", {
2046
- sessionId,
2047
- checkpoint: bytesToBase64(initialCheckpoint)
2048
- });
2363
+ const [checkpointAttachment] = attachBytes([initialCheckpoint]);
2364
+ await request("libfx/restore", { sessionId, checkpointAttachment });
2049
2365
  }
2050
2366
  } catch (error) {
2051
2367
  closing = true;
@@ -2058,7 +2374,7 @@ async function createFxAgent(options = {}) {
2058
2374
  try {
2059
2375
  await runtime.exited;
2060
2376
  } catch {}
2061
- throw agentBootstrapError(error);
2377
+ throw error;
2062
2378
  }
2063
2379
  const agent = {
2064
2380
  prompt(input, promptOptions = {}) {
@@ -2068,15 +2384,20 @@ async function createFxAgent(options = {}) {
2068
2384
  throw new Error("a prompt is already in progress for this session");
2069
2385
  return normalizeTurn(startTurn(input, promptOptions));
2070
2386
  },
2071
- async checkpoint() {
2072
- if (closing)
2073
- throw new Error("fx agent is closed");
2074
- if (activeTurn)
2075
- throw new Error("cannot checkpoint while a prompt is active");
2076
- const response = await request("libfx/checkpoint", { sessionId });
2077
- if (typeof response?.checkpoint !== "string")
2078
- throw new Error("fx returned an invalid checkpoint");
2079
- return base64ToBytes(response.checkpoint);
2387
+ checkpoint() {
2388
+ const previous = pendingCheckpoints > 0 ? checkpointTail : null;
2389
+ pendingCheckpoints++;
2390
+ let release;
2391
+ checkpointTail = new Promise((resolve) => {
2392
+ release = resolve;
2393
+ });
2394
+ const run = previous ? previous.then(takeCheckpoint) : takeCheckpoint();
2395
+ const settle = () => {
2396
+ pendingCheckpoints--;
2397
+ release();
2398
+ };
2399
+ run.then(settle, settle);
2400
+ return run;
2080
2401
  },
2081
2402
  async close() {
2082
2403
  if (closing) {
@@ -2200,10 +2521,12 @@ async function createFxAgent(options = {}) {
2200
2521
  return { inputTruncated: true, inputPreview: decoder.decode(utf8Prefix(bytes, maxToolStartInputBytes)) };
2201
2522
  }
2202
2523
  function startTurn(input, promptOptions) {
2203
- const prompt = normalizePromptInput(input);
2204
- if (encoder.encode(JSON.stringify({ sessionId: "", prompt })).length + promptFrameEnvelopeBytes > maxPromptFrameBytes) {
2205
- throw new RangeError(`prompt exceeds the ${maxPromptFrameBytes} byte libfx frame limit`);
2206
- }
2524
+ const resizeImage = options.resizeImage;
2525
+ const normalized = normalizePromptInput(input, { deferImageLimits: resizeImage !== undefined });
2526
+ const hasPixels = normalized.some((block) => block.type === "image" && block.data !== undefined);
2527
+ const asyncImages = hasPixels && (resizeImage !== undefined || normalized.some((block) => block.type === "image" && block.source === "blob"));
2528
+ checkPromptFrameSize(normalized, resizeImage === undefined);
2529
+ const prompt = asyncImages ? normalized.map((block) => block.type === "image" && block.source === "bytes" ? { ...block, data: block.data.slice() } : block) : preparePromptImages(normalized);
2207
2530
  const signal = promptOptions.signal;
2208
2531
  if (signal !== undefined && (typeof signal?.addEventListener !== "function" || typeof signal?.removeEventListener !== "function"))
2209
2532
  throw new TypeError("prompt signal must be an AbortSignal");
@@ -2215,6 +2538,14 @@ async function createFxAgent(options = {}) {
2215
2538
  let terminalError;
2216
2539
  let reportedPressure = false;
2217
2540
  let discardedBytes = 0;
2541
+ let cancelImagePrep = null;
2542
+ let rejectImagePrep = null;
2543
+ let resolvePromptStart = null;
2544
+ const promptStarted = asyncImages ? new Promise((resolve) => {
2545
+ resolvePromptStart = resolve;
2546
+ }) : null;
2547
+ let pendingSteeringCount = 0;
2548
+ let pendingSteeringBytes = 0;
2218
2549
  const toolControllers = new Set;
2219
2550
  let finished = false;
2220
2551
  let cancelled = false;
@@ -2251,35 +2582,72 @@ async function createFxAgent(options = {}) {
2251
2582
  get cancelled() {
2252
2583
  return cancelled;
2253
2584
  },
2585
+ failImagePrep(error) {
2586
+ rejectImagePrep?.(error);
2587
+ rejectImagePrep = null;
2588
+ cancelImagePrep = null;
2589
+ },
2590
+ promptWritten() {
2591
+ resolvePromptStart?.(true);
2592
+ resolvePromptStart = null;
2593
+ },
2254
2594
  steer(text) {
2255
2595
  if (finished || cancelled || activeTurn !== turn) {
2256
2596
  return Promise.reject(new Error("no prompt is running"));
2257
2597
  }
2258
- let accepted;
2259
- try {
2260
- if (typeof runtime.steer === "function") {
2261
- runtime.steer(text);
2262
- turn.push({
2263
- sessionUpdate: "user_message_chunk",
2264
- content: { type: "text", text }
2265
- });
2266
- accepted = Promise.resolve();
2267
- } else {
2268
- accepted = request("libfx/steer", { sessionId, text });
2598
+ if (closing)
2599
+ return Promise.reject(coreExitError ?? new Error("fx agent is closing"));
2600
+ const apply = () => {
2601
+ if (finished || cancelled || activeTurn !== turn) {
2602
+ return Promise.reject(new Error("no prompt is running"));
2603
+ }
2604
+ if (closing)
2605
+ return Promise.reject(coreExitError ?? new Error("fx agent is closing"));
2606
+ try {
2607
+ if (typeof runtime.steer === "function") {
2608
+ runtime.steer(text);
2609
+ turn.push({
2610
+ sessionUpdate: "user_message_chunk",
2611
+ content: { type: "text", text }
2612
+ });
2613
+ return Promise.resolve();
2614
+ }
2615
+ return request("libfx/steer", { sessionId, text });
2616
+ } catch (error) {
2617
+ return Promise.reject(error);
2269
2618
  }
2270
- } catch (error) {
2271
- return Promise.reject(error);
2619
+ };
2620
+ if (!resolvePromptStart)
2621
+ return apply();
2622
+ const bytes = encoder.encode(text).length;
2623
+ if (pendingSteeringCount >= maxSteeringMessages || bytes > maxSteeringQueueBytes - pendingSteeringBytes) {
2624
+ return Promise.reject(new Error("steering queue is full"));
2272
2625
  }
2273
- return accepted;
2626
+ pendingSteeringCount++;
2627
+ pendingSteeringBytes += bytes;
2628
+ return promptStarted.then((started) => {
2629
+ if (coreExitError && !cancelled)
2630
+ throw coreExitError;
2631
+ return started === true ? apply() : Promise.reject(started instanceof Error ? started : new Error("no prompt is running"));
2632
+ }).finally(() => {
2633
+ pendingSteeringCount--;
2634
+ pendingSteeringBytes -= bytes;
2635
+ });
2274
2636
  },
2275
2637
  cancel() {
2276
2638
  if (finished || cancelled)
2277
2639
  return;
2278
2640
  cancelled = true;
2641
+ cancelImagePrep?.();
2642
+ cancelImagePrep = null;
2643
+ rejectImagePrep = null;
2644
+ resolvePromptStart?.(false);
2645
+ resolvePromptStart = null;
2279
2646
  runtime.closeSteering?.();
2280
2647
  resumeOutput?.();
2281
2648
  resumeOutput = null;
2282
- send({ jsonrpc: "2.0", method: "session/cancel", params: { sessionId } });
2649
+ if (!closing)
2650
+ send({ jsonrpc: "2.0", method: "session/cancel", params: { sessionId } });
2283
2651
  for (const controller of toolControllers)
2284
2652
  controller.abort();
2285
2653
  runtime.abortHostEffects();
@@ -2311,6 +2679,8 @@ async function createFxAgent(options = {}) {
2311
2679
  }
2312
2680
  };
2313
2681
  if (signal?.aborted) {
2682
+ resolvePromptStart?.(false);
2683
+ resolvePromptStart = null;
2314
2684
  finished = true;
2315
2685
  turn.result = Promise.resolve({ stopReason: "cancelled" });
2316
2686
  return turn;
@@ -2319,8 +2689,40 @@ async function createFxAgent(options = {}) {
2319
2689
  runtime.openSteering?.();
2320
2690
  const abort = () => turn.cancel();
2321
2691
  signal?.addEventListener("abort", abort, { once: true });
2322
- turn.result = request("session/prompt", { sessionId, prompt }).then((response) => ({ stopReason: cancelled ? "cancelled" : response.stopReason, usage: response.usage })).catch((error) => {
2323
- if (error.message === "Cancelled")
2692
+ const imagePrepCancelled = asyncImages ? new Promise((resolve, reject) => {
2693
+ cancelImagePrep = () => resolve(null);
2694
+ rejectImagePrep = reject;
2695
+ }) : null;
2696
+ let response;
2697
+ if (asyncImages) {
2698
+ response = Promise.race([
2699
+ Promise.resolve().then(() => materializePromptImages(prompt, () => cancelled || closing, resizeImage)),
2700
+ imagePrepCancelled
2701
+ ]).then((prepared) => {
2702
+ cancelImagePrep = null;
2703
+ rejectImagePrep = null;
2704
+ if (coreExitError && !cancelled)
2705
+ throw coreExitError;
2706
+ if (prepared === null || cancelled || closing) {
2707
+ resolvePromptStart?.(false);
2708
+ resolvePromptStart = null;
2709
+ return { stopReason: "cancelled" };
2710
+ }
2711
+ return sendPrompt(prepared);
2712
+ });
2713
+ } else {
2714
+ try {
2715
+ response = sendPrompt(prompt);
2716
+ } catch (error) {
2717
+ response = Promise.reject(error);
2718
+ }
2719
+ }
2720
+ turn.result = response.then((value) => ({ stopReason: cancelled ? "cancelled" : value.stopReason, usage: value.usage })).catch((error) => {
2721
+ cancelImagePrep = null;
2722
+ rejectImagePrep = null;
2723
+ resolvePromptStart?.(error);
2724
+ resolvePromptStart = null;
2725
+ if (cancelled && error.message === "Cancelled")
2324
2726
  return { stopReason: "cancelled" };
2325
2727
  terminalError = error;
2326
2728
  throw error;
@@ -2351,7 +2753,7 @@ async function createFxAgent(options = {}) {
2351
2753
 
2352
2754
  // sdk/node.js
2353
2755
  var libfxApiVersion = 2;
2354
- var nativeCoreApiVersion = 3;
2756
+ var nativeCoreApiVersion = 4;
2355
2757
  var fetchOperationStale = 0;
2356
2758
  var fetchOperationApplied = 1;
2357
2759
  var fetchOperationBackpressure = 2;
@@ -2642,7 +3044,12 @@ async function getBackendInfo(value = {}) {
2642
3044
  }
2643
3045
  }
2644
3046
  function createNativeCoreRuntime(addon, options) {
2645
- const { apiKey, model, effort, fast, gatewayChatUrl } = options;
3047
+ const { apiKey, model, effort, fast, ultrafast, gatewayChatUrl } = options;
3048
+ if (ultrafast !== undefined && addon.supportsUltrafast !== true) {
3049
+ const error = new Error("native addon does not support the ultrafast option");
3050
+ error.code = "LIBFX_NATIVE_CAPABILITY_UNAVAILABLE";
3051
+ throw error;
3052
+ }
2646
3053
  const core = addon.createCore({
2647
3054
  apiKey,
2648
3055
  home: options.home ?? import_node_os.homedir(),
@@ -2650,6 +3057,7 @@ function createNativeCoreRuntime(addon, options) {
2650
3057
  ...model === undefined ? {} : { model },
2651
3058
  ...effort === undefined ? {} : { effort },
2652
3059
  ...fast === undefined ? {} : { fast },
3060
+ ...ultrafast === undefined ? {} : { ultrafast },
2653
3061
  ...gatewayChatUrl === undefined ? {} : { gatewayChatUrl }
2654
3062
  });
2655
3063
  let readyFd;
@@ -2696,7 +3104,7 @@ function createNativeCoreRuntime(addon, options) {
2696
3104
  readySocket.destroy();
2697
3105
  readyClosed.then(() => exitedResolve(code));
2698
3106
  };
2699
- const pumpFetch = async (request) => {
3107
+ const pumpFetch = async (request, body) => {
2700
3108
  const controller = new AbortController;
2701
3109
  const state = { handle: request.handle, controller };
2702
3110
  fetchState = state;
@@ -2704,7 +3112,7 @@ function createNativeCoreRuntime(addon, options) {
2704
3112
  const response = await (options.fetch ?? globalThis.fetch)(request.url, {
2705
3113
  method: request.method,
2706
3114
  headers: new Headers(JSON.parse(request.headers).map(({ name, value }) => [name, value])),
2707
- body: request.body?.length ? Buffer.from(request.body, "base64") : undefined,
3115
+ body: body.length ? body : undefined,
2708
3116
  signal: controller.signal
2709
3117
  });
2710
3118
  const started = addon.startCoreFetchResponse(core, state.handle, response.status);
@@ -2761,7 +3169,7 @@ function createNativeCoreRuntime(addon, options) {
2761
3169
  } else {
2762
3170
  const fetchRequest = addon.takeCoreFetch(core);
2763
3171
  if (fetchRequest)
2764
- pumpFetch(JSON.parse(fetchRequest.toString("utf8")));
3172
+ pumpFetch(JSON.parse(fetchRequest.request.toString("utf8")), fetchRequest.body);
2765
3173
  }
2766
3174
  if (addon.coreExitCode(core) !== 0) {
2767
3175
  finish(1, new Error("native output delivery failed"));
@@ -2822,6 +3230,15 @@ function createNativeCoreRuntime(addon, options) {
2822
3230
  write(data) {
2823
3231
  addon.writeCore(core, Buffer.from(data));
2824
3232
  },
3233
+ writeAttachment(id, bytes) {
3234
+ addon.writeCoreAttachment(core, id, Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength));
3235
+ },
3236
+ takeAttachment(id) {
3237
+ return addon.takeCoreAttachment(core, id);
3238
+ },
3239
+ discardAttachments() {
3240
+ addon.discardCoreAttachments(core);
3241
+ },
2825
3242
  closeStdin() {
2826
3243
  addon.closeCore(core);
2827
3244
  },
@@ -2848,10 +3265,11 @@ function createNativeAgent(addon, options) {
2848
3265
  });
2849
3266
  }
2850
3267
  async function createWithFallback(surface, nativeMethod, wasmFactory, defaultWasm, options) {
2851
- const { nativeAddon, backend = "auto", ...runtimeOptions } = options ?? {};
3268
+ const { nativeAddon, backend = "auto", ...unvalidatedOptions } = options ?? {};
2852
3269
  if (!new Set(["auto", "native", "wasm"]).has(backend)) {
2853
3270
  throw new TypeError('backend must be "auto", "native", or "wasm"');
2854
3271
  }
3272
+ const runtimeOptions = surface === "agent" ? normalizeAgentOptions(unvalidatedOptions) : unvalidatedOptions;
2855
3273
  let nativeError;
2856
3274
  let nativeAttempted = false;
2857
3275
  if (backend !== "wasm") {
@@ -2865,7 +3283,7 @@ async function createWithFallback(surface, nativeMethod, wasmFactory, defaultWas
2865
3283
  return await native.backend[nativeMethod](runtimeOptions);
2866
3284
  } catch (error) {
2867
3285
  nativeError = error;
2868
- if (backend === "native")
3286
+ if (backend === "native" || error?.code === "LIBFX_MODEL_UNSUPPORTED_FAST" || error?.code === "LIBFX_MODEL_UNSUPPORTED_EFFORT")
2869
3287
  throw error;
2870
3288
  }
2871
3289
  }
@@ -2884,9 +3302,6 @@ async function createWithFallback(surface, nativeMethod, wasmFactory, defaultWas
2884
3302
  return wasmFactory({ ...runtimeOptions, wasm: await wasmInput(wasmSource) });
2885
3303
  }
2886
3304
  async function createFxAgent2(options = {}) {
2887
- if (options != null && Object.hasOwn(Object(options), "env")) {
2888
- throw new TypeError("createFxAgent() does not accept env; pass apiKey and model directly");
2889
- }
2890
3305
  return createWithFallback("agent", "createCore", createFxAgent, defaultCoreWasm, options);
2891
3306
  }
2892
3307
  function createFxTerminal2(options = {}) {