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/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");
@@ -265,24 +278,29 @@ function normalizeAgentOptions(value) {
265
278
  }
266
279
  options.apiKey = boundedString(options.apiKey, "apiKey", maxApiKeyBytes, true);
267
280
  if (options.model !== null && typeof options.model === "object" && !Array.isArray(options.model)) {
268
- if (Object.hasOwn(options, "effort") || Object.hasOwn(options, "fast")) {
269
- throw new TypeError("model options cannot be mixed with top-level effort or fast");
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");
270
283
  }
271
284
  const model = options.model;
272
285
  for (const name of Object.keys(model)) {
273
- if (name !== "id" && name !== "effort" && name !== "fast") {
286
+ if (name !== "id" && name !== "effort" && name !== "fast" && name !== "ultrafast") {
274
287
  throw new TypeError(`unsupported model option: ${name}`);
275
288
  }
276
289
  }
277
290
  options.model = boundedString(model.id, "model.id", maxModelBytes, true);
278
291
  options.effort = normalizeEffort(model.effort);
279
292
  options.fast = normalizeFast(model.fast);
293
+ options.ultrafast = normalizeUltrafast(model.ultrafast);
280
294
  } else {
281
295
  options.model = boundedString(options.model, "model", maxModelBytes, false);
282
296
  options.effort = normalizeEffort(options.effort);
283
297
  options.fast = normalizeFast(options.fast);
298
+ options.ultrafast = normalizeUltrafast(options.ultrafast);
284
299
  }
285
300
  validateGatewayChatUrl(options.gatewayChatUrl);
301
+ if (options.resizeImage !== undefined && typeof options.resizeImage !== "function") {
302
+ throw new TypeError("resizeImage must be a function");
303
+ }
286
304
  return options;
287
305
  }
288
306
  function agentEnvironment(options) {
@@ -291,13 +309,14 @@ function agentEnvironment(options) {
291
309
  ...options.model === undefined ? {} : { FX_MODEL: options.model },
292
310
  ...options.effort === undefined ? {} : { FX_EFFORT: options.effort },
293
311
  ...options.fast === undefined ? {} : { FX_FAST: options.fast ? "true" : "false" },
312
+ ...options.ultrafast === undefined ? {} : { FX_ULTRAFAST: options.ultrafast ? "true" : "false" },
294
313
  ...options.gatewayChatUrl === undefined ? {} : { FX_GATEWAY_CHAT_URL: options.gatewayChatUrl }
295
314
  };
296
315
  }
297
316
  function agentRpcError(response) {
298
317
  const error = new Error(response.message);
299
318
  const data = response.data;
300
- if (data && ["LIBFX_MODEL_UNSUPPORTED_EFFORT", "LIBFX_MODEL_UNSUPPORTED_FAST"].includes(data.code) && typeof data.model === "string" && data.capability === (data.code === "LIBFX_MODEL_UNSUPPORTED_FAST" ? "fast" : "effort")) {
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")) {
301
320
  error.code = data.code;
302
321
  error.model = data.model;
303
322
  error.capability = data.capability;
@@ -660,6 +679,9 @@ function createRuntime(options) {
660
679
  const steering = [];
661
680
  let steeringBytes = 0;
662
681
  let steeringOpen = false;
682
+ const inboundAttachments = new Map;
683
+ const outboundAttachments = new Map;
684
+ let nextOutboundAttachment = 1;
663
685
  const workspaceExecs = new Set;
664
686
  const workspace = prepareWorkspaceAdapter(options.workspace);
665
687
  const args = ["fx", ...options.args || []];
@@ -983,6 +1005,37 @@ function createRuntime(options) {
983
1005
  steeringBytes -= value.length;
984
1006
  return value.length;
985
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
+ }
986
1039
  let pendingHostToolResult = null;
987
1040
  function hostToolCall(namePtr, nameLen, argumentsPtr, argumentsLen, outputPtr, outputCap, statusPtr) {
988
1041
  pendingHostToolResult = null;
@@ -1446,6 +1499,9 @@ function createRuntime(options) {
1446
1499
  steeringOpen = false;
1447
1500
  clearSteering();
1448
1501
  },
1502
+ fx_attachment_size: attachmentSize,
1503
+ fx_attachment_take: attachmentTake,
1504
+ fx_attachment_put: attachmentPut,
1449
1505
  fx_open_url: new WebAssembly.Suspending(openUrl),
1450
1506
  fx_oauth_session_load: new WebAssembly.Suspending(oauthSessionLoad),
1451
1507
  fx_oauth_session_commit: new WebAssembly.Suspending(oauthSessionCommit),
@@ -1476,6 +1532,15 @@ function createRuntime(options) {
1476
1532
  write(data) {
1477
1533
  stdin.push(typeof data === "string" ? encoder.encode(data) : data);
1478
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
+ },
1479
1544
  wake() {
1480
1545
  stdin.wake();
1481
1546
  },
@@ -1659,45 +1724,122 @@ function blobByteLength(value) {
1659
1724
  return null;
1660
1725
  }
1661
1726
  }
1662
- function normalizePromptInput(input) {
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 } = {}) {
1663
1820
  if (typeof input === "string")
1664
1821
  return [{ type: "text", text: input }];
1665
1822
  if (!Array.isArray(input))
1666
1823
  throw new TypeError("prompt input must be a string or an array of prompt blocks");
1667
1824
  let imageCount = 0;
1668
1825
  let imageBytes = 0;
1669
- return input.map((block, index) => {
1826
+ let prompt = input.map((block, index) => {
1670
1827
  if (!block || typeof block !== "object")
1671
1828
  throw new TypeError(`prompt block ${index} must be an object`);
1672
1829
  if (block.type === "image") {
1673
- const size = blobByteLength(block.data);
1674
- const blob = size !== null;
1675
- if (!blob && (typeof block.data !== "string" || block.data.length === 0)) {
1676
- throw new TypeError(`image prompt block ${index} requires base64 data or a Blob`);
1677
- }
1678
- const mimeType = blob ? block.data.type : block.mimeType;
1679
- if (blob && block.mimeType !== undefined && block.mimeType !== mimeType) {
1680
- throw new TypeError(`image prompt block ${index} mimeType disagrees with Blob.type`);
1681
- }
1682
- if (typeof mimeType !== "string" || mimeType.length === 0 || mimeType.length > 128) {
1683
- throw new TypeError(`image prompt block ${index} requires a mimeType`);
1684
- }
1685
- if (blob && (!Number.isSafeInteger(size) || size <= 0)) {
1686
- throw new TypeError(`image prompt block ${index} requires a non-empty Blob with a valid size`);
1687
- }
1688
- const encodedLength = blob ? Math.ceil(size / 3) * 4 : block.data.length;
1689
- if (encodedLength > maxPromptImageDataBytes) {
1690
- throw new RangeError(`image prompt block ${index} exceeds the ${maxPromptImageDataBytes} byte per-image libfx limit`);
1691
- }
1830
+ const image = normalizePromptImage(block, index);
1692
1831
  imageCount += 1;
1693
1832
  if (imageCount > maxPromptImages) {
1694
1833
  throw new RangeError(`prompt cannot contain more than ${maxPromptImages} images`);
1695
1834
  }
1696
- imageBytes += encodedLength;
1697
- if (imageBytes > maxPromptImagesBytes) {
1698
- 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;
1699
1841
  }
1700
- return { type: "image", data: block.data, mimeType };
1842
+ return image;
1701
1843
  }
1702
1844
  if (block.type === "text") {
1703
1845
  if (typeof block.text !== "string")
@@ -1714,52 +1856,89 @@ function normalizePromptInput(input) {
1714
1856
  }
1715
1857
  throw new TypeError(`unsupported prompt block type: ${String(block.type)}`);
1716
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
+ }
1717
1884
  }
1718
- function promptFrameSize(prompt) {
1719
- let blobDataBytes = 0;
1720
- const projected = prompt.map((block) => {
1721
- if (block.type !== "image" || typeof block.data === "string")
1722
- return block;
1723
- const size = blobByteLength(block.data);
1724
- if (!Number.isSafeInteger(size))
1725
- throw new TypeError("image prompt requires a valid Blob size");
1726
- blobDataBytes += Math.ceil(size / 3) * 4;
1727
- return { type: "image", data: "", mimeType: block.mimeType };
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 }
1728
1891
  });
1729
- return encoder.encode(JSON.stringify({ sessionId: "", prompt: projected })).length + blobDataBytes + promptFrameEnvelopeBytes;
1730
1892
  }
1731
- async function materializePromptBlobs(blocks, isCancelled) {
1732
- const encoded = [];
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 = [];
1733
1902
  let imageBytes = 0;
1734
- for (const [index, block] of blocks.entries()) {
1903
+ for (let index = 0;index < blocks.length; index++) {
1735
1904
  if (isCancelled())
1736
1905
  return null;
1737
- if (block.type !== "image") {
1738
- encoded.push(block);
1739
- continue;
1740
- }
1741
- if (typeof block.data === "string") {
1742
- imageBytes += block.data.length;
1743
- if (imageBytes > maxPromptImagesBytes) {
1744
- throw new RangeError(`prompt images exceed the ${maxPromptImagesBytes} byte libfx frame limit`);
1745
- }
1746
- encoded.push(block);
1906
+ const block = blocks[index];
1907
+ if (block.type !== "image" || block.data === undefined) {
1908
+ prepared.push(block);
1747
1909
  continue;
1748
1910
  }
1749
- const bytes = new Uint8Array(await block.data.arrayBuffer());
1911
+ let image = block.source === "blob" ? { mimeType: block.mimeType, bytes: new Uint8Array(await block.data.arrayBuffer()) } : preparePromptImages([block])[0];
1750
1912
  if (isCancelled())
1751
1913
  return null;
1752
- const encodedLength = Math.ceil(bytes.length / 3) * 4;
1753
- if (encodedLength > maxPromptImageDataBytes) {
1754
- throw new RangeError(`image prompt block ${index} exceeds the ${maxPromptImageDataBytes} byte per-image libfx limit`);
1914
+ if (resizeImage) {
1915
+ image = resizedPromptImage(await resizeImage({ bytes: image.bytes, mimeType: image.mimeType }), index);
1916
+ if (isCancelled())
1917
+ return null;
1755
1918
  }
1756
- imageBytes += encodedLength;
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);
1757
1931
  if (imageBytes > maxPromptImagesBytes) {
1758
- throw new RangeError(`prompt images exceed the ${maxPromptImagesBytes} byte libfx frame limit`);
1932
+ prepared = omitReferencedImageData(prepared);
1933
+ blocks = omitReferencedImageData(blocks);
1934
+ imageBytes = prepared.reduce((total, entry) => total + (entry.bytes?.byteLength ?? 0), 0);
1759
1935
  }
1760
- encoded.push({ type: "image", data: bytesToBase64(bytes), mimeType: block.mimeType });
1936
+ checkPromptImagesByteLength(imageBytes);
1761
1937
  }
1762
- return normalizePromptInput(encoded);
1938
+ if (promptFrameSize(prepared) > maxPromptFrameBytes)
1939
+ prepared = omitReferencedImageData(prepared);
1940
+ checkPromptFrameSize(prepared, true);
1941
+ return prepared;
1763
1942
  }
1764
1943
  function normalizeSteeringInput(input) {
1765
1944
  const blocks = normalizePromptInput(input);
@@ -1846,18 +2025,31 @@ function hostToolContent(value) {
1846
2025
  if (typeof value.text !== "string" || !Array.isArray(value.images) || value.images.length > 8) {
1847
2026
  throw new TypeError("invalid typed tool result");
1848
2027
  }
1849
- let imageBytes = 0;
1850
- const images = value.images.map((image) => {
1851
- 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) {
1852
2034
  throw new TypeError("invalid tool image");
1853
2035
  }
1854
- imageBytes += image.data.length;
1855
- if (imageBytes > 8 * 1024 * 1024)
1856
- throw new RangeError("tool images exceed the result limit");
1857
- 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
+ };
1858
2042
  });
1859
- const content = JSON.stringify({ text: value.text, images });
1860
- 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)
1861
2053
  throw new RangeError("typed tool result exceeds the result limit");
1862
2054
  return { content, rich: true, isError: value.isError === true };
1863
2055
  }
@@ -1880,13 +2072,6 @@ function checkpointBytes(value) {
1880
2072
  }
1881
2073
  throw new TypeError("checkpoint must be an ArrayBuffer or typed array");
1882
2074
  }
1883
- function bytesToBase64(value) {
1884
- let binary = "";
1885
- for (let offset = 0;offset < value.length; offset += 32768) {
1886
- binary += String.fromCharCode(...value.subarray(offset, offset + 32768));
1887
- }
1888
- return btoa(binary);
1889
- }
1890
2075
  function base64ToBytes(value) {
1891
2076
  const binary = atob(value);
1892
2077
  const bytes = new Uint8Array(binary.length);
@@ -1899,6 +2084,9 @@ async function createFxAgent(options = {}) {
1899
2084
  const hostTools = normalizeHostTools(options.tools);
1900
2085
  const instructions = normalizeInstructions(options.instructions);
1901
2086
  const initialCheckpoint = checkpointBytes(options.checkpoint);
2087
+ if (initialCheckpoint && initialCheckpoint.byteLength > maxCheckpointBytes) {
2088
+ throw new Error("libfx checkpoint is too large");
2089
+ }
1902
2090
  const pending = new Map;
1903
2091
  let nextId = 1;
1904
2092
  let sessionId = null;
@@ -2061,11 +2249,49 @@ async function createFxAgent(options = {}) {
2061
2249
  reject(error);
2062
2250
  }
2063
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
+ };
2064
2290
  runtime.exited.then((code) => {
2065
2291
  closing = true;
2066
2292
  const error = runtime.error ?? new Error(`fx-core exited with code ${code} before completing the ACP request`);
2067
2293
  coreExitError = error;
2068
- activeTurn?.failPendingBlob(error);
2294
+ activeTurn?.failImagePrep(error);
2069
2295
  for (const waiter of pending.values())
2070
2296
  waiter.reject(error);
2071
2297
  pending.clear();
@@ -2105,6 +2331,10 @@ async function createFxAgent(options = {}) {
2105
2331
  if (cancelled || closing)
2106
2332
  return;
2107
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
+ }
2108
2338
  if (encoder.encode(JSON.stringify(response)).length + 1 > 8 * 1024 * 1024) {
2109
2339
  response.result = { content: "Host tool result exceeded the response frame limit", isError: true };
2110
2340
  }
@@ -2130,10 +2360,8 @@ async function createFxAgent(options = {}) {
2130
2360
  const sessionResult = await request("libfx/new");
2131
2361
  sessionId = sessionResult.sessionId;
2132
2362
  if (initialCheckpoint) {
2133
- await request("libfx/restore", {
2134
- sessionId,
2135
- checkpoint: bytesToBase64(initialCheckpoint)
2136
- });
2363
+ const [checkpointAttachment] = attachBytes([initialCheckpoint]);
2364
+ await request("libfx/restore", { sessionId, checkpointAttachment });
2137
2365
  }
2138
2366
  } catch (error) {
2139
2367
  closing = true;
@@ -2156,15 +2384,20 @@ async function createFxAgent(options = {}) {
2156
2384
  throw new Error("a prompt is already in progress for this session");
2157
2385
  return normalizeTurn(startTurn(input, promptOptions));
2158
2386
  },
2159
- async checkpoint() {
2160
- if (closing)
2161
- throw new Error("fx agent is closed");
2162
- if (activeTurn)
2163
- throw new Error("cannot checkpoint while a prompt is active");
2164
- const response = await request("libfx/checkpoint", { sessionId });
2165
- if (typeof response?.checkpoint !== "string")
2166
- throw new Error("fx returned an invalid checkpoint");
2167
- 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;
2168
2401
  },
2169
2402
  async close() {
2170
2403
  if (closing) {
@@ -2288,11 +2521,12 @@ async function createFxAgent(options = {}) {
2288
2521
  return { inputTruncated: true, inputPreview: decoder.decode(utf8Prefix(bytes, maxToolStartInputBytes)) };
2289
2522
  }
2290
2523
  function startTurn(input, promptOptions) {
2291
- const prompt = normalizePromptInput(input);
2292
- const hasBlobs = prompt.some((block) => block.type === "image" && typeof block.data !== "string");
2293
- if (promptFrameSize(prompt) > maxPromptFrameBytes) {
2294
- throw new RangeError(`prompt exceeds the ${maxPromptFrameBytes} byte libfx frame limit`);
2295
- }
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);
2296
2530
  const signal = promptOptions.signal;
2297
2531
  if (signal !== undefined && (typeof signal?.addEventListener !== "function" || typeof signal?.removeEventListener !== "function"))
2298
2532
  throw new TypeError("prompt signal must be an AbortSignal");
@@ -2304,10 +2538,10 @@ async function createFxAgent(options = {}) {
2304
2538
  let terminalError;
2305
2539
  let reportedPressure = false;
2306
2540
  let discardedBytes = 0;
2307
- let cancelBlobRead = null;
2308
- let rejectBlobRead = null;
2541
+ let cancelImagePrep = null;
2542
+ let rejectImagePrep = null;
2309
2543
  let resolvePromptStart = null;
2310
- const promptStarted = hasBlobs ? new Promise((resolve) => {
2544
+ const promptStarted = asyncImages ? new Promise((resolve) => {
2311
2545
  resolvePromptStart = resolve;
2312
2546
  }) : null;
2313
2547
  let pendingSteeringCount = 0;
@@ -2348,10 +2582,10 @@ async function createFxAgent(options = {}) {
2348
2582
  get cancelled() {
2349
2583
  return cancelled;
2350
2584
  },
2351
- failPendingBlob(error) {
2352
- rejectBlobRead?.(error);
2353
- rejectBlobRead = null;
2354
- cancelBlobRead = null;
2585
+ failImagePrep(error) {
2586
+ rejectImagePrep?.(error);
2587
+ rejectImagePrep = null;
2588
+ cancelImagePrep = null;
2355
2589
  },
2356
2590
  promptWritten() {
2357
2591
  resolvePromptStart?.(true);
@@ -2404,9 +2638,9 @@ async function createFxAgent(options = {}) {
2404
2638
  if (finished || cancelled)
2405
2639
  return;
2406
2640
  cancelled = true;
2407
- cancelBlobRead?.();
2408
- cancelBlobRead = null;
2409
- rejectBlobRead = null;
2641
+ cancelImagePrep?.();
2642
+ cancelImagePrep = null;
2643
+ rejectImagePrep = null;
2410
2644
  resolvePromptStart?.(false);
2411
2645
  resolvePromptStart = null;
2412
2646
  runtime.closeSteering?.();
@@ -2455,31 +2689,37 @@ async function createFxAgent(options = {}) {
2455
2689
  runtime.openSteering?.();
2456
2690
  const abort = () => turn.cancel();
2457
2691
  signal?.addEventListener("abort", abort, { once: true });
2458
- const blobReadCancelled = hasBlobs ? new Promise((resolve, reject) => {
2459
- cancelBlobRead = () => resolve(null);
2460
- rejectBlobRead = reject;
2692
+ const imagePrepCancelled = asyncImages ? new Promise((resolve, reject) => {
2693
+ cancelImagePrep = () => resolve(null);
2694
+ rejectImagePrep = reject;
2461
2695
  }) : null;
2462
- const response = hasBlobs ? Promise.race([
2463
- Promise.resolve().then(() => materializePromptBlobs(prompt, () => cancelled || closing)),
2464
- blobReadCancelled
2465
- ]).then((encodedPrompt) => {
2466
- cancelBlobRead = null;
2467
- rejectBlobRead = null;
2468
- if (coreExitError && !cancelled)
2469
- throw coreExitError;
2470
- if (encodedPrompt === null || cancelled || closing) {
2471
- resolvePromptStart?.(false);
2472
- resolvePromptStart = null;
2473
- return { stopReason: "cancelled" };
2474
- }
2475
- if (promptFrameSize(encodedPrompt) > maxPromptFrameBytes) {
2476
- throw new RangeError(`prompt exceeds the ${maxPromptFrameBytes} byte libfx frame limit`);
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);
2477
2718
  }
2478
- return request("session/prompt", { sessionId, prompt: encodedPrompt });
2479
- }) : request("session/prompt", { sessionId, prompt });
2719
+ }
2480
2720
  turn.result = response.then((value) => ({ stopReason: cancelled ? "cancelled" : value.stopReason, usage: value.usage })).catch((error) => {
2481
- cancelBlobRead = null;
2482
- rejectBlobRead = null;
2721
+ cancelImagePrep = null;
2722
+ rejectImagePrep = null;
2483
2723
  resolvePromptStart?.(error);
2484
2724
  resolvePromptStart = null;
2485
2725
  if (cancelled && error.message === "Cancelled")
@@ -2513,7 +2753,7 @@ async function createFxAgent(options = {}) {
2513
2753
 
2514
2754
  // sdk/node.js
2515
2755
  var libfxApiVersion = 2;
2516
- var nativeCoreApiVersion = 3;
2756
+ var nativeCoreApiVersion = 4;
2517
2757
  var fetchOperationStale = 0;
2518
2758
  var fetchOperationApplied = 1;
2519
2759
  var fetchOperationBackpressure = 2;
@@ -2804,7 +3044,12 @@ async function getBackendInfo(value = {}) {
2804
3044
  }
2805
3045
  }
2806
3046
  function createNativeCoreRuntime(addon, options) {
2807
- 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
+ }
2808
3053
  const core = addon.createCore({
2809
3054
  apiKey,
2810
3055
  home: options.home ?? import_node_os.homedir(),
@@ -2812,6 +3057,7 @@ function createNativeCoreRuntime(addon, options) {
2812
3057
  ...model === undefined ? {} : { model },
2813
3058
  ...effort === undefined ? {} : { effort },
2814
3059
  ...fast === undefined ? {} : { fast },
3060
+ ...ultrafast === undefined ? {} : { ultrafast },
2815
3061
  ...gatewayChatUrl === undefined ? {} : { gatewayChatUrl }
2816
3062
  });
2817
3063
  let readyFd;
@@ -2858,7 +3104,7 @@ function createNativeCoreRuntime(addon, options) {
2858
3104
  readySocket.destroy();
2859
3105
  readyClosed.then(() => exitedResolve(code));
2860
3106
  };
2861
- const pumpFetch = async (request) => {
3107
+ const pumpFetch = async (request, body) => {
2862
3108
  const controller = new AbortController;
2863
3109
  const state = { handle: request.handle, controller };
2864
3110
  fetchState = state;
@@ -2866,7 +3112,7 @@ function createNativeCoreRuntime(addon, options) {
2866
3112
  const response = await (options.fetch ?? globalThis.fetch)(request.url, {
2867
3113
  method: request.method,
2868
3114
  headers: new Headers(JSON.parse(request.headers).map(({ name, value }) => [name, value])),
2869
- body: request.body?.length ? Buffer.from(request.body, "base64") : undefined,
3115
+ body: body.length ? body : undefined,
2870
3116
  signal: controller.signal
2871
3117
  });
2872
3118
  const started = addon.startCoreFetchResponse(core, state.handle, response.status);
@@ -2923,7 +3169,7 @@ function createNativeCoreRuntime(addon, options) {
2923
3169
  } else {
2924
3170
  const fetchRequest = addon.takeCoreFetch(core);
2925
3171
  if (fetchRequest)
2926
- pumpFetch(JSON.parse(fetchRequest.toString("utf8")));
3172
+ pumpFetch(JSON.parse(fetchRequest.request.toString("utf8")), fetchRequest.body);
2927
3173
  }
2928
3174
  if (addon.coreExitCode(core) !== 0) {
2929
3175
  finish(1, new Error("native output delivery failed"));
@@ -2984,6 +3230,15 @@ function createNativeCoreRuntime(addon, options) {
2984
3230
  write(data) {
2985
3231
  addon.writeCore(core, Buffer.from(data));
2986
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
+ },
2987
3242
  closeStdin() {
2988
3243
  addon.closeCore(core);
2989
3244
  },