libfx 0.0.11 → 0.0.12

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 CHANGED
@@ -39,25 +39,39 @@ await agent.close();
39
39
 
40
40
  `apiKey` is required. `model` is optional and defaults to fx's built-in model.
41
41
  Agent configuration uses named options; `env` is reserved for
42
- `createFxTerminal()`.
43
-
44
- `effort` sets the reasoning effort for models that advertise effort levels. It
45
- uses the same vocabulary as the fx CLI's `--effort` flag: `"default"` leaves
46
- the choice to the model, and named levels such as `"low"`, `"medium"`,
47
- `"high"`, or `"xhigh"` request a specific level. A named level is validated
48
- against the selected model's advertised levels at creation; an unsupported
49
- level rejects with an error (code `LIBFX_UNSUPPORTED_EFFORT`) naming the
50
- supported set. When `effort` is omitted or `"default"`, the model default
51
- applies.
52
-
53
- `fast` enables the fast lane for models that advertise one, matching the fx
54
- CLI's `--fast` flag. Enabling it is validated against the selected model at
55
- creation; a model without a fast path rejects with an error (code
56
- `LIBFX_UNSUPPORTED_FAST`). When `fast` is omitted or `false`, the model
57
- default applies.
42
+ `createFxTerminal()`. The canonical model configuration groups the model ID
43
+ and model-specific options:
44
+
45
+ ```js
46
+ const agent = await createFxAgent({
47
+ apiKey,
48
+ model: { id: "anthropic/claude-opus-5.5-fast", effort: "low", fast: true },
49
+ });
50
+ ```
51
+
52
+ A string `model` remains supported as shorthand. Top-level `effort` and
53
+ `fast` are deprecated but remain supported with a string model or no model;
54
+ they cannot be mixed with a model object. New code should use the model object.
55
+
56
+ `model.effort` sets the reasoning effort for models that advertise effort
57
+ levels. It uses the same vocabulary as the fx CLI's `--effort` flag:
58
+ `"default"` leaves the choice to the model; named levels such as `"low"`,
59
+ `"medium"`, `"high"`, or `"xhigh"` request a specific level. A named level is
60
+ validated at creation; an unsupported level rejects with an Error carrying
61
+ `code: "LIBFX_MODEL_UNSUPPORTED_EFFORT"`, `model`, and
62
+ `capability: "effort"`. Its message names the supported set when available.
63
+ Omitting effort or using `"default"` leaves the model default in place.
64
+
65
+ `model.fast` enables the fast lane for models that advertise one, matching the
66
+ fx CLI's `--fast` flag. A model without a fast path rejects at creation with
67
+ `code: "LIBFX_MODEL_UNSUPPORTED_FAST"`, `model`, and `capability: "fast"`.
68
+ Omitting fast or setting it to `false` leaves the model default in place.
69
+ The new codes replace `LIBFX_UNSUPPORTED_EFFORT` and `LIBFX_UNSUPPORTED_FAST`
70
+ for both nested and legacy top-level settings. Callers that check the old codes
71
+ must update their error handling.
58
72
 
59
73
  The host selects the model. Agent creation does not fetch the Gateway model
60
- catalog unless `effort` requests a named level or `fast` is enabled. Prompting
74
+ catalog unless effort requests a named level or fast is enabled. Prompting
61
75
  can resolve model capabilities and context capacity through the supplied
62
76
  `fetch`; fx caches that metadata for the agent.
63
77
 
@@ -96,9 +110,11 @@ const result = await turn.result;
96
110
  ```
97
111
 
98
112
  A turn has one event consumer. Breaking out of its iterator cancels the turn;
99
- `turn.cancel()` and `agent.close()` also release blocked output. Transport or
100
- message-decoding failures reject the result instead of returning success with
101
- missing text.
113
+ `turn.cancel()` and `agent.close()` also release blocked output. Embedded agents
114
+ have no implicit model-step cap, so hosts should cancel turns that exceed their
115
+ own budgets. The CLI's `max_agent_steps` setting does not apply to
116
+ `createFxAgent()`. Transport or message-decoding failures reject the result
117
+ instead of returning success with missing text.
102
118
 
103
119
  Native transport buffers at most 8 MiB of output bytes. Unread SDK events apply
104
120
  backpressure at 1 MiB of encoded messages or 256 events. One message can exceed
@@ -106,22 +122,32 @@ that threshold when the queue is empty; an individual encoded ACP message is
106
122
  limited to 64 MiB on both backends. These are transport bounds, not a total
107
123
  answer-size limit or a bound on retained conversation history.
108
124
 
109
- Image blocks use ACP's content shape and carry canonical base64 (no line
110
- wrapping) of a PNG, JPEG, GIF, or WebP payload:
125
+ Image blocks accept a `Blob` or `File` with a non-empty `type`, or the
126
+ existing canonical base64 (no line wrapping) and explicit `mimeType` of a PNG,
127
+ JPEG, GIF, or WebP payload:
111
128
 
112
129
  ```js
113
130
  const turn = agent.prompt([
114
131
  { type: "text", text: "What does this screenshot show?" },
115
- { type: "image", data: base64Png, mimeType: "image/png" },
132
+ { type: "image", data: file }, // File or Blob, with file.type
133
+ // Or: { type: "image", data: base64Png, mimeType: "image/png" }
116
134
  ]);
117
135
  ```
118
136
 
119
137
  A prompt may contain up to 8 images, each with up to 5 MiB of base64 data,
120
- with at most 8 MiB of image data per prompt; the SDK rejects larger input with
121
- typed `RangeError`s before any request. The kernel then validates the decoded
122
- bytes against the declared `mimeType` and fails the turn with
123
- `Invalid image prompt block` on a mismatch. Images are routed only to models
124
- that advertise image input; for any other model the turn fails with
138
+ with at most 8 MiB of image data per prompt. The SDK checks Blob size before
139
+ reading it, encodes it for the same ACP wire format, and rejects larger input
140
+ with typed `RangeError`s. The total frame size is checked before reading a
141
+ Blob, and the actual byte count is checked before encoding it. Blob reads are
142
+ asynchronous: `prompt()` returns a turn, and read failures reject
143
+ `turn.result`. Cancelling or closing while a Blob is being read settles the
144
+ turn without sending its prompt. For base64 input, size errors still throw
145
+ synchronously from `prompt()`.
146
+
147
+ The kernel sniffs decoded bytes and compares them with the claimed MIME type
148
+ for both input forms; a mismatch fails the turn with
149
+ `Invalid image prompt block`. Images are routed only to models that advertise
150
+ image input; for any other model the turn fails with
125
151
  `Image prompts are unavailable for the selected model` and no image bytes
126
152
  leave the process. Prompt images are retained in checkpoints within the
127
153
  existing 4 MiB checkpoint bound, so a restored agent can refer to earlier
@@ -133,8 +159,10 @@ without discarding the in-flight response or completed tool work. Steering also
133
159
  accepts an array of text blocks; image and resource steering blocks are rejected.
134
160
  Each message is limited to 64 KiB, with at most 64 queued messages and 1 MiB of
135
161
  queued steering text. Accepted steering appears as a `user_message` event before
136
- the model's continued output. Calling `steer()` after the turn settles rejects
137
- with `no prompt is running`.
162
+ the model's continued output. For a Blob prompt, steering during the read
163
+ waits for the prompt to be sent; cancelling before then rejects the pending
164
+ steering. Calling `steer()` after the turn settles rejects with
165
+ `no prompt is running`.
138
166
 
139
167
  ```js
140
168
  const turn = agent.prompt("Build the feature.");
@@ -162,8 +190,8 @@ The checkpoint contains conversation history and usage only. The host owns
162
190
  durable storage and must resupply models, credentials, instructions, tools,
163
191
  MCP clients, and skill records. Reasoning effort and fast mode are
164
192
  agent-creation options and are not stored in a checkpoint: recreate the agent
165
- with new `effort` or `fast` values to change them, the same path as switching
166
- models.
193
+ with new `model.effort` or `model.fast` values to change them, the same path as
194
+ switching models.
167
195
 
168
196
  ## Models
169
197
 
package/fx-core.wasm CHANGED
Binary file
package/fx-sdk.js CHANGED
@@ -84,7 +84,7 @@ function normalizeFast(value) {
84
84
  return value;
85
85
  }
86
86
 
87
- function normalizeAgentOptions(value) {
87
+ export function normalizeAgentOptions(value) {
88
88
  if (!value || typeof value !== "object" || Array.isArray(value)) {
89
89
  throw new TypeError("createFxAgent() options must be an object");
90
90
  }
@@ -93,9 +93,24 @@ function normalizeAgentOptions(value) {
93
93
  throw new TypeError("createFxAgent() does not accept env; pass apiKey and model directly");
94
94
  }
95
95
  options.apiKey = boundedString(options.apiKey, "apiKey", maxApiKeyBytes, true);
96
- options.model = boundedString(options.model, "model", maxModelBytes, false);
97
- options.effort = normalizeEffort(options.effort);
98
- options.fast = normalizeFast(options.fast);
96
+ if (options.model !== null && typeof options.model === "object" && !Array.isArray(options.model)) {
97
+ if (Object.hasOwn(options, "effort") || Object.hasOwn(options, "fast")) {
98
+ throw new TypeError("model options cannot be mixed with top-level effort or fast");
99
+ }
100
+ const model = options.model;
101
+ for (const name of Object.keys(model)) {
102
+ if (name !== "id" && name !== "effort" && name !== "fast") {
103
+ throw new TypeError(`unsupported model option: ${name}`);
104
+ }
105
+ }
106
+ options.model = boundedString(model.id, "model.id", maxModelBytes, true);
107
+ options.effort = normalizeEffort(model.effort);
108
+ options.fast = normalizeFast(model.fast);
109
+ } else {
110
+ options.model = boundedString(options.model, "model", maxModelBytes, false);
111
+ options.effort = normalizeEffort(options.effort);
112
+ options.fast = normalizeFast(options.fast);
113
+ }
99
114
  validateGatewayChatUrl(options.gatewayChatUrl);
100
115
  return options;
101
116
  }
@@ -110,16 +125,15 @@ function agentEnvironment(options) {
110
125
  };
111
126
  }
112
127
 
113
- // The kernel rejects an unsupported effort or fast lane during initialize;
114
- // these messages originate only from that validation, so the rejection is
115
- // safe to retype.
116
- function agentBootstrapError(error) {
117
- if (error instanceof Error &&
118
- (error.message === "Invalid reasoning effort" || error.message.startsWith("Reasoning effort"))) {
119
- error.code ??= "LIBFX_UNSUPPORTED_EFFORT";
120
- }
121
- if (error instanceof Error && error.message.startsWith("Fast mode")) {
122
- error.code ??= "LIBFX_UNSUPPORTED_FAST";
128
+ function agentRpcError(response) {
129
+ const error = new Error(response.message);
130
+ const data = response.data;
131
+ if (data && ["LIBFX_MODEL_UNSUPPORTED_EFFORT", "LIBFX_MODEL_UNSUPPORTED_FAST"].includes(data.code) &&
132
+ typeof data.model === "string" &&
133
+ data.capability === (data.code === "LIBFX_MODEL_UNSUPPORTED_FAST" ? "fast" : "effort")) {
134
+ error.code = data.code;
135
+ error.model = data.model;
136
+ error.capability = data.capability;
123
137
  }
124
138
  return error;
125
139
  }
@@ -1276,6 +1290,15 @@ export async function createFxTerminal(options) {
1276
1290
  };
1277
1291
  }
1278
1292
 
1293
+ function blobByteLength(value) {
1294
+ if (typeof Blob === "undefined" || value == null) return null;
1295
+ try {
1296
+ return Object.getOwnPropertyDescriptor(Blob.prototype, "size").get.call(value);
1297
+ } catch {
1298
+ return null;
1299
+ }
1300
+ }
1301
+
1279
1302
  function normalizePromptInput(input) {
1280
1303
  if (typeof input === "string") return [{ type: "text", text: input }];
1281
1304
  if (!Array.isArray(input)) throw new TypeError("prompt input must be a string or an array of prompt blocks");
@@ -1284,24 +1307,34 @@ function normalizePromptInput(input) {
1284
1307
  return input.map((block, index) => {
1285
1308
  if (!block || typeof block !== "object") throw new TypeError(`prompt block ${index} must be an object`);
1286
1309
  if (block.type === "image") {
1287
- if (typeof block.data !== "string" || block.data.length === 0) {
1288
- throw new TypeError(`image prompt block ${index} requires base64 data`);
1310
+ const size = blobByteLength(block.data);
1311
+ const blob = size !== null;
1312
+ if (!blob && (typeof block.data !== "string" || block.data.length === 0)) {
1313
+ throw new TypeError(`image prompt block ${index} requires base64 data or a Blob`);
1314
+ }
1315
+ const mimeType = blob ? block.data.type : block.mimeType;
1316
+ if (blob && block.mimeType !== undefined && block.mimeType !== mimeType) {
1317
+ throw new TypeError(`image prompt block ${index} mimeType disagrees with Blob.type`);
1289
1318
  }
1290
- if (typeof block.mimeType !== "string" || block.mimeType.length === 0 || block.mimeType.length > 128) {
1319
+ if (typeof mimeType !== "string" || mimeType.length === 0 || mimeType.length > 128) {
1291
1320
  throw new TypeError(`image prompt block ${index} requires a mimeType`);
1292
1321
  }
1293
- if (block.data.length > maxPromptImageDataBytes) {
1322
+ if (blob && (!Number.isSafeInteger(size) || size <= 0)) {
1323
+ throw new TypeError(`image prompt block ${index} requires a non-empty Blob with a valid size`);
1324
+ }
1325
+ const encodedLength = blob ? Math.ceil(size / 3) * 4 : block.data.length;
1326
+ if (encodedLength > maxPromptImageDataBytes) {
1294
1327
  throw new RangeError(`image prompt block ${index} exceeds the ${maxPromptImageDataBytes} byte per-image libfx limit`);
1295
1328
  }
1296
1329
  imageCount += 1;
1297
1330
  if (imageCount > maxPromptImages) {
1298
1331
  throw new RangeError(`prompt cannot contain more than ${maxPromptImages} images`);
1299
1332
  }
1300
- imageBytes += block.data.length;
1333
+ imageBytes += encodedLength;
1301
1334
  if (imageBytes > maxPromptImagesBytes) {
1302
1335
  throw new RangeError(`prompt images exceed the ${maxPromptImagesBytes} byte libfx frame limit`);
1303
1336
  }
1304
- return { type: "image", data: block.data, mimeType: block.mimeType };
1337
+ return { type: "image", data: block.data, mimeType };
1305
1338
  }
1306
1339
  if (block.type === "text") {
1307
1340
  if (typeof block.text !== "string") throw new TypeError(`text prompt block ${index} requires text`);
@@ -1317,6 +1350,50 @@ function normalizePromptInput(input) {
1317
1350
  });
1318
1351
  }
1319
1352
 
1353
+ function promptFrameSize(prompt) {
1354
+ let blobDataBytes = 0;
1355
+ const projected = prompt.map((block) => {
1356
+ if (block.type !== "image" || typeof block.data === "string") return block;
1357
+ const size = blobByteLength(block.data);
1358
+ if (!Number.isSafeInteger(size)) throw new TypeError("image prompt requires a valid Blob size");
1359
+ blobDataBytes += Math.ceil(size / 3) * 4;
1360
+ return { type: "image", data: "", mimeType: block.mimeType };
1361
+ });
1362
+ return encoder.encode(JSON.stringify({ sessionId: "", prompt: projected })).length + blobDataBytes + promptFrameEnvelopeBytes;
1363
+ }
1364
+
1365
+ async function materializePromptBlobs(blocks, isCancelled) {
1366
+ const encoded = [];
1367
+ let imageBytes = 0;
1368
+ for (const [index, block] of blocks.entries()) {
1369
+ if (isCancelled()) return null;
1370
+ if (block.type !== "image") {
1371
+ encoded.push(block);
1372
+ continue;
1373
+ }
1374
+ if (typeof block.data === "string") {
1375
+ imageBytes += block.data.length;
1376
+ if (imageBytes > maxPromptImagesBytes) {
1377
+ throw new RangeError(`prompt images exceed the ${maxPromptImagesBytes} byte libfx frame limit`);
1378
+ }
1379
+ encoded.push(block);
1380
+ continue;
1381
+ }
1382
+ const bytes = new Uint8Array(await block.data.arrayBuffer());
1383
+ if (isCancelled()) return null;
1384
+ const encodedLength = Math.ceil(bytes.length / 3) * 4;
1385
+ if (encodedLength > maxPromptImageDataBytes) {
1386
+ throw new RangeError(`image prompt block ${index} exceeds the ${maxPromptImageDataBytes} byte per-image libfx limit`);
1387
+ }
1388
+ imageBytes += encodedLength;
1389
+ if (imageBytes > maxPromptImagesBytes) {
1390
+ throw new RangeError(`prompt images exceed the ${maxPromptImagesBytes} byte libfx frame limit`);
1391
+ }
1392
+ encoded.push({ type: "image", data: bytesToBase64(bytes), mimeType: block.mimeType });
1393
+ }
1394
+ return normalizePromptInput(encoded);
1395
+ }
1396
+
1320
1397
  function normalizeSteeringInput(input) {
1321
1398
  const blocks = normalizePromptInput(input);
1322
1399
  if (blocks.some((block) => block.type !== "text")) {
@@ -1443,6 +1520,7 @@ export async function createFxAgent(options = {}) {
1443
1520
  let sessionId = null;
1444
1521
  let activeTurn = null;
1445
1522
  let closing = false;
1523
+ let coreExitError = null;
1446
1524
  const isCurrentTurn = (turn) => turn && activeTurn === turn && !turn.cancelled && !closing;
1447
1525
  const emit = (type, detail = {}) => {
1448
1526
  try { options.onEvent?.({ type, timestamp: performance.now(), ...detail }); } catch {}
@@ -1572,7 +1650,9 @@ export async function createFxAgent(options = {}) {
1572
1650
  const send = (message) => {
1573
1651
  if (closing) throw new Error("fx agent is closing");
1574
1652
  emit("acp.send", { message });
1653
+ if (message.method === "session/prompt" && activeTurn?.cancelled) throw new Error("Cancelled");
1575
1654
  runtime.write(`${JSON.stringify(message)}\n`);
1655
+ if (message.method === "session/prompt") activeTurn?.promptWritten();
1576
1656
  };
1577
1657
  const request = (method, params = {}) => new Promise((resolve, reject) => {
1578
1658
  const id = nextId++;
@@ -1580,11 +1660,13 @@ export async function createFxAgent(options = {}) {
1580
1660
  try { send({ jsonrpc: "2.0", id, method, params }); } catch (error) { pending.delete(id); reject(error); }
1581
1661
  });
1582
1662
  runtime.exited.then((code) => {
1583
- emit("runtime.exit", { code });
1584
1663
  closing = true;
1585
1664
  const error = runtime.error ?? new Error(`fx-core exited with code ${code} before completing the ACP request`);
1665
+ coreExitError = error;
1666
+ activeTurn?.failPendingBlob(error);
1586
1667
  for (const waiter of pending.values()) waiter.reject(error);
1587
1668
  pending.clear();
1669
+ emit("runtime.exit", { code });
1588
1670
  });
1589
1671
  runtime.setLineHandler((message, size) => {
1590
1672
  emit("acp.receive", { message });
@@ -1623,7 +1705,7 @@ export async function createFxAgent(options = {}) {
1623
1705
  return;
1624
1706
  }
1625
1707
  const waiter = pending.get(message.id); if (!waiter) return; pending.delete(message.id);
1626
- if (message.error) waiter.reject(new Error(message.error.message)); else waiter.resolve(message.result);
1708
+ if (message.error) waiter.reject(agentRpcError(message.error)); else waiter.resolve(message.result);
1627
1709
  }
1628
1710
  try {
1629
1711
  await request("initialize", {
@@ -1648,7 +1730,7 @@ export async function createFxAgent(options = {}) {
1648
1730
  try { runtime.abortHostEffects(); } catch {}
1649
1731
  try { runtime.closeStdin(); } catch {}
1650
1732
  try { await runtime.exited; } catch {}
1651
- throw agentBootstrapError(error);
1733
+ throw error;
1652
1734
  }
1653
1735
 
1654
1736
  const agent = {
@@ -1765,7 +1847,8 @@ export async function createFxAgent(options = {}) {
1765
1847
 
1766
1848
  function startTurn(input, promptOptions) {
1767
1849
  const prompt = normalizePromptInput(input);
1768
- if (encoder.encode(JSON.stringify({ sessionId: "", prompt })).length + promptFrameEnvelopeBytes > maxPromptFrameBytes) {
1850
+ const hasBlobs = prompt.some((block) => block.type === "image" && typeof block.data !== "string");
1851
+ if (promptFrameSize(prompt) > maxPromptFrameBytes) {
1769
1852
  throw new RangeError(`prompt exceeds the ${maxPromptFrameBytes} byte libfx frame limit`);
1770
1853
  }
1771
1854
  const signal = promptOptions.signal;
@@ -1778,6 +1861,12 @@ export async function createFxAgent(options = {}) {
1778
1861
  let terminalError;
1779
1862
  let reportedPressure = false;
1780
1863
  let discardedBytes = 0;
1864
+ let cancelBlobRead = null;
1865
+ let rejectBlobRead = null;
1866
+ let resolvePromptStart = null;
1867
+ const promptStarted = hasBlobs ? new Promise((resolve) => { resolvePromptStart = resolve; }) : null;
1868
+ let pendingSteeringCount = 0;
1869
+ let pendingSteeringBytes = 0;
1781
1870
  const toolControllers = new Set();
1782
1871
  let finished = false;
1783
1872
  let cancelled = false;
@@ -1802,34 +1891,63 @@ export async function createFxAgent(options = {}) {
1802
1891
  transportBytes: 0,
1803
1892
  lastTransportActivityAt: null,
1804
1893
  get cancelled() { return cancelled; },
1894
+ failPendingBlob(error) {
1895
+ rejectBlobRead?.(error);
1896
+ rejectBlobRead = null;
1897
+ cancelBlobRead = null;
1898
+ },
1899
+ promptWritten() {
1900
+ resolvePromptStart?.(true);
1901
+ resolvePromptStart = null;
1902
+ },
1805
1903
  steer(text) {
1806
1904
  if (finished || cancelled || activeTurn !== turn) {
1807
1905
  return Promise.reject(new Error("no prompt is running"));
1808
1906
  }
1809
- let accepted;
1810
- try {
1811
- if (typeof runtime.steer === "function") {
1812
- runtime.steer(text);
1813
- void turn.push({
1814
- sessionUpdate: "user_message_chunk",
1815
- content: { type: "text", text },
1816
- });
1817
- accepted = Promise.resolve();
1818
- } else {
1819
- accepted = request("libfx/steer", { sessionId, text });
1907
+ if (closing) return Promise.reject(coreExitError ?? new Error("fx agent is closing"));
1908
+ const apply = () => {
1909
+ if (finished || cancelled || activeTurn !== turn) {
1910
+ return Promise.reject(new Error("no prompt is running"));
1911
+ }
1912
+ if (closing) return Promise.reject(coreExitError ?? new Error("fx agent is closing"));
1913
+ try {
1914
+ if (typeof runtime.steer === "function") {
1915
+ runtime.steer(text);
1916
+ void turn.push({
1917
+ sessionUpdate: "user_message_chunk",
1918
+ content: { type: "text", text },
1919
+ });
1920
+ return Promise.resolve();
1921
+ }
1922
+ return request("libfx/steer", { sessionId, text });
1923
+ } catch (error) {
1924
+ return Promise.reject(error);
1820
1925
  }
1821
- } catch (error) {
1822
- return Promise.reject(error);
1926
+ };
1927
+ if (!resolvePromptStart) return apply();
1928
+ const bytes = encoder.encode(text).length;
1929
+ if (pendingSteeringCount >= maxSteeringMessages || bytes > maxSteeringQueueBytes - pendingSteeringBytes) {
1930
+ return Promise.reject(new Error("steering queue is full"));
1823
1931
  }
1824
- return accepted;
1932
+ pendingSteeringCount++;
1933
+ pendingSteeringBytes += bytes;
1934
+ return promptStarted.then((started) => {
1935
+ if (coreExitError && !cancelled) throw coreExitError;
1936
+ return started === true ? apply() : Promise.reject(started instanceof Error ? started : new Error("no prompt is running"));
1937
+ }).finally(() => { pendingSteeringCount--; pendingSteeringBytes -= bytes; });
1825
1938
  },
1826
1939
  cancel() {
1827
1940
  if (finished || cancelled) return;
1828
1941
  cancelled = true;
1942
+ cancelBlobRead?.();
1943
+ cancelBlobRead = null;
1944
+ rejectBlobRead = null;
1945
+ resolvePromptStart?.(false);
1946
+ resolvePromptStart = null;
1829
1947
  runtime.closeSteering?.();
1830
1948
  resumeOutput?.();
1831
1949
  resumeOutput = null;
1832
- send({ jsonrpc: "2.0", method: "session/cancel", params: { sessionId } });
1950
+ if (!closing) send({ jsonrpc: "2.0", method: "session/cancel", params: { sessionId } });
1833
1951
  for (const controller of toolControllers) controller.abort();
1834
1952
  runtime.abortHostEffects();
1835
1953
  },
@@ -1854,6 +1972,8 @@ export async function createFxAgent(options = {}) {
1854
1972
  },
1855
1973
  };
1856
1974
  if (signal?.aborted) {
1975
+ resolvePromptStart?.(false);
1976
+ resolvePromptStart = null;
1857
1977
  finished = true;
1858
1978
  turn.result = Promise.resolve({ stopReason: "cancelled" });
1859
1979
  return turn;
@@ -1862,10 +1982,37 @@ export async function createFxAgent(options = {}) {
1862
1982
  runtime.openSteering?.();
1863
1983
  const abort = () => turn.cancel();
1864
1984
  signal?.addEventListener("abort", abort, { once: true });
1865
- turn.result = request("session/prompt", { sessionId, prompt })
1866
- .then((response) => ({ stopReason: cancelled ? "cancelled" : response.stopReason, usage: response.usage }))
1985
+ const blobReadCancelled = hasBlobs ? new Promise((resolve, reject) => {
1986
+ cancelBlobRead = () => resolve(null);
1987
+ rejectBlobRead = reject;
1988
+ }) : null;
1989
+ const response = hasBlobs
1990
+ ? Promise.race([
1991
+ Promise.resolve().then(() => materializePromptBlobs(prompt, () => cancelled || closing)),
1992
+ blobReadCancelled,
1993
+ ]).then((encodedPrompt) => {
1994
+ cancelBlobRead = null;
1995
+ rejectBlobRead = null;
1996
+ if (coreExitError && !cancelled) throw coreExitError;
1997
+ if (encodedPrompt === null || cancelled || closing) {
1998
+ resolvePromptStart?.(false);
1999
+ resolvePromptStart = null;
2000
+ return { stopReason: "cancelled" };
2001
+ }
2002
+ if (promptFrameSize(encodedPrompt) > maxPromptFrameBytes) {
2003
+ throw new RangeError(`prompt exceeds the ${maxPromptFrameBytes} byte libfx frame limit`);
2004
+ }
2005
+ return request("session/prompt", { sessionId, prompt: encodedPrompt });
2006
+ })
2007
+ : request("session/prompt", { sessionId, prompt });
2008
+ turn.result = response
2009
+ .then((value) => ({ stopReason: cancelled ? "cancelled" : value.stopReason, usage: value.usage }))
1867
2010
  .catch((error) => {
1868
- if (error.message === "Cancelled") return { stopReason: "cancelled" };
2011
+ cancelBlobRead = null;
2012
+ rejectBlobRead = null;
2013
+ resolvePromptStart?.(error);
2014
+ resolvePromptStart = null;
2015
+ if (cancelled && error.message === "Cancelled") return { stopReason: "cancelled" };
1869
2016
  terminalError = error;
1870
2017
  throw error;
1871
2018
  })
package/fx-term.wasm CHANGED
Binary file
Binary file
Binary file
Binary file
Binary file
package/node.cjs CHANGED
@@ -264,9 +264,24 @@ function normalizeAgentOptions(value) {
264
264
  throw new TypeError("createFxAgent() does not accept env; pass apiKey and model directly");
265
265
  }
266
266
  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);
267
+ 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");
270
+ }
271
+ const model = options.model;
272
+ for (const name of Object.keys(model)) {
273
+ if (name !== "id" && name !== "effort" && name !== "fast") {
274
+ throw new TypeError(`unsupported model option: ${name}`);
275
+ }
276
+ }
277
+ options.model = boundedString(model.id, "model.id", maxModelBytes, true);
278
+ options.effort = normalizeEffort(model.effort);
279
+ options.fast = normalizeFast(model.fast);
280
+ } else {
281
+ options.model = boundedString(options.model, "model", maxModelBytes, false);
282
+ options.effort = normalizeEffort(options.effort);
283
+ options.fast = normalizeFast(options.fast);
284
+ }
270
285
  validateGatewayChatUrl(options.gatewayChatUrl);
271
286
  return options;
272
287
  }
@@ -279,12 +294,13 @@ function agentEnvironment(options) {
279
294
  ...options.gatewayChatUrl === undefined ? {} : { FX_GATEWAY_CHAT_URL: options.gatewayChatUrl }
280
295
  };
281
296
  }
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";
297
+ function agentRpcError(response) {
298
+ const error = new Error(response.message);
299
+ 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")) {
301
+ error.code = data.code;
302
+ error.model = data.model;
303
+ error.capability = data.capability;
288
304
  }
289
305
  return error;
290
306
  }
@@ -1634,6 +1650,15 @@ async function createFxTerminal(options) {
1634
1650
  }
1635
1651
  };
1636
1652
  }
1653
+ function blobByteLength(value) {
1654
+ if (typeof Blob === "undefined" || value == null)
1655
+ return null;
1656
+ try {
1657
+ return Object.getOwnPropertyDescriptor(Blob.prototype, "size").get.call(value);
1658
+ } catch {
1659
+ return null;
1660
+ }
1661
+ }
1637
1662
  function normalizePromptInput(input) {
1638
1663
  if (typeof input === "string")
1639
1664
  return [{ type: "text", text: input }];
@@ -1645,24 +1670,34 @@ function normalizePromptInput(input) {
1645
1670
  if (!block || typeof block !== "object")
1646
1671
  throw new TypeError(`prompt block ${index} must be an object`);
1647
1672
  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`);
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`);
1650
1681
  }
1651
- if (typeof block.mimeType !== "string" || block.mimeType.length === 0 || block.mimeType.length > 128) {
1682
+ if (typeof mimeType !== "string" || mimeType.length === 0 || mimeType.length > 128) {
1652
1683
  throw new TypeError(`image prompt block ${index} requires a mimeType`);
1653
1684
  }
1654
- if (block.data.length > maxPromptImageDataBytes) {
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) {
1655
1690
  throw new RangeError(`image prompt block ${index} exceeds the ${maxPromptImageDataBytes} byte per-image libfx limit`);
1656
1691
  }
1657
1692
  imageCount += 1;
1658
1693
  if (imageCount > maxPromptImages) {
1659
1694
  throw new RangeError(`prompt cannot contain more than ${maxPromptImages} images`);
1660
1695
  }
1661
- imageBytes += block.data.length;
1696
+ imageBytes += encodedLength;
1662
1697
  if (imageBytes > maxPromptImagesBytes) {
1663
1698
  throw new RangeError(`prompt images exceed the ${maxPromptImagesBytes} byte libfx frame limit`);
1664
1699
  }
1665
- return { type: "image", data: block.data, mimeType: block.mimeType };
1700
+ return { type: "image", data: block.data, mimeType };
1666
1701
  }
1667
1702
  if (block.type === "text") {
1668
1703
  if (typeof block.text !== "string")
@@ -1680,6 +1715,52 @@ function normalizePromptInput(input) {
1680
1715
  throw new TypeError(`unsupported prompt block type: ${String(block.type)}`);
1681
1716
  });
1682
1717
  }
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 };
1728
+ });
1729
+ return encoder.encode(JSON.stringify({ sessionId: "", prompt: projected })).length + blobDataBytes + promptFrameEnvelopeBytes;
1730
+ }
1731
+ async function materializePromptBlobs(blocks, isCancelled) {
1732
+ const encoded = [];
1733
+ let imageBytes = 0;
1734
+ for (const [index, block] of blocks.entries()) {
1735
+ if (isCancelled())
1736
+ 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);
1747
+ continue;
1748
+ }
1749
+ const bytes = new Uint8Array(await block.data.arrayBuffer());
1750
+ if (isCancelled())
1751
+ 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`);
1755
+ }
1756
+ imageBytes += encodedLength;
1757
+ if (imageBytes > maxPromptImagesBytes) {
1758
+ throw new RangeError(`prompt images exceed the ${maxPromptImagesBytes} byte libfx frame limit`);
1759
+ }
1760
+ encoded.push({ type: "image", data: bytesToBase64(bytes), mimeType: block.mimeType });
1761
+ }
1762
+ return normalizePromptInput(encoded);
1763
+ }
1683
1764
  function normalizeSteeringInput(input) {
1684
1765
  const blocks = normalizePromptInput(input);
1685
1766
  if (blocks.some((block) => block.type !== "text")) {
@@ -1823,6 +1904,7 @@ async function createFxAgent(options = {}) {
1823
1904
  let sessionId = null;
1824
1905
  let activeTurn = null;
1825
1906
  let closing = false;
1907
+ let coreExitError = null;
1826
1908
  const isCurrentTurn = (turn) => turn && activeTurn === turn && !turn.cancelled && !closing;
1827
1909
  const emit = (type, detail = {}) => {
1828
1910
  try {
@@ -1962,8 +2044,12 @@ async function createFxAgent(options = {}) {
1962
2044
  if (closing)
1963
2045
  throw new Error("fx agent is closing");
1964
2046
  emit("acp.send", { message });
2047
+ if (message.method === "session/prompt" && activeTurn?.cancelled)
2048
+ throw new Error("Cancelled");
1965
2049
  runtime.write(`${JSON.stringify(message)}
1966
2050
  `);
2051
+ if (message.method === "session/prompt")
2052
+ activeTurn?.promptWritten();
1967
2053
  };
1968
2054
  const request = (method, params = {}) => new Promise((resolve, reject) => {
1969
2055
  const id = nextId++;
@@ -1976,12 +2062,14 @@ async function createFxAgent(options = {}) {
1976
2062
  }
1977
2063
  });
1978
2064
  runtime.exited.then((code) => {
1979
- emit("runtime.exit", { code });
1980
2065
  closing = true;
1981
2066
  const error = runtime.error ?? new Error(`fx-core exited with code ${code} before completing the ACP request`);
2067
+ coreExitError = error;
2068
+ activeTurn?.failPendingBlob(error);
1982
2069
  for (const waiter of pending.values())
1983
2070
  waiter.reject(error);
1984
2071
  pending.clear();
2072
+ emit("runtime.exit", { code });
1985
2073
  });
1986
2074
  runtime.setLineHandler((message, size) => {
1987
2075
  emit("acp.receive", { message });
@@ -2028,7 +2116,7 @@ async function createFxAgent(options = {}) {
2028
2116
  return;
2029
2117
  pending.delete(message.id);
2030
2118
  if (message.error)
2031
- waiter.reject(new Error(message.error.message));
2119
+ waiter.reject(agentRpcError(message.error));
2032
2120
  else
2033
2121
  waiter.resolve(message.result);
2034
2122
  }
@@ -2058,7 +2146,7 @@ async function createFxAgent(options = {}) {
2058
2146
  try {
2059
2147
  await runtime.exited;
2060
2148
  } catch {}
2061
- throw agentBootstrapError(error);
2149
+ throw error;
2062
2150
  }
2063
2151
  const agent = {
2064
2152
  prompt(input, promptOptions = {}) {
@@ -2201,7 +2289,8 @@ async function createFxAgent(options = {}) {
2201
2289
  }
2202
2290
  function startTurn(input, promptOptions) {
2203
2291
  const prompt = normalizePromptInput(input);
2204
- if (encoder.encode(JSON.stringify({ sessionId: "", prompt })).length + promptFrameEnvelopeBytes > maxPromptFrameBytes) {
2292
+ const hasBlobs = prompt.some((block) => block.type === "image" && typeof block.data !== "string");
2293
+ if (promptFrameSize(prompt) > maxPromptFrameBytes) {
2205
2294
  throw new RangeError(`prompt exceeds the ${maxPromptFrameBytes} byte libfx frame limit`);
2206
2295
  }
2207
2296
  const signal = promptOptions.signal;
@@ -2215,6 +2304,14 @@ async function createFxAgent(options = {}) {
2215
2304
  let terminalError;
2216
2305
  let reportedPressure = false;
2217
2306
  let discardedBytes = 0;
2307
+ let cancelBlobRead = null;
2308
+ let rejectBlobRead = null;
2309
+ let resolvePromptStart = null;
2310
+ const promptStarted = hasBlobs ? new Promise((resolve) => {
2311
+ resolvePromptStart = resolve;
2312
+ }) : null;
2313
+ let pendingSteeringCount = 0;
2314
+ let pendingSteeringBytes = 0;
2218
2315
  const toolControllers = new Set;
2219
2316
  let finished = false;
2220
2317
  let cancelled = false;
@@ -2251,35 +2348,72 @@ async function createFxAgent(options = {}) {
2251
2348
  get cancelled() {
2252
2349
  return cancelled;
2253
2350
  },
2351
+ failPendingBlob(error) {
2352
+ rejectBlobRead?.(error);
2353
+ rejectBlobRead = null;
2354
+ cancelBlobRead = null;
2355
+ },
2356
+ promptWritten() {
2357
+ resolvePromptStart?.(true);
2358
+ resolvePromptStart = null;
2359
+ },
2254
2360
  steer(text) {
2255
2361
  if (finished || cancelled || activeTurn !== turn) {
2256
2362
  return Promise.reject(new Error("no prompt is running"));
2257
2363
  }
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 });
2364
+ if (closing)
2365
+ return Promise.reject(coreExitError ?? new Error("fx agent is closing"));
2366
+ const apply = () => {
2367
+ if (finished || cancelled || activeTurn !== turn) {
2368
+ return Promise.reject(new Error("no prompt is running"));
2369
+ }
2370
+ if (closing)
2371
+ return Promise.reject(coreExitError ?? new Error("fx agent is closing"));
2372
+ try {
2373
+ if (typeof runtime.steer === "function") {
2374
+ runtime.steer(text);
2375
+ turn.push({
2376
+ sessionUpdate: "user_message_chunk",
2377
+ content: { type: "text", text }
2378
+ });
2379
+ return Promise.resolve();
2380
+ }
2381
+ return request("libfx/steer", { sessionId, text });
2382
+ } catch (error) {
2383
+ return Promise.reject(error);
2269
2384
  }
2270
- } catch (error) {
2271
- return Promise.reject(error);
2385
+ };
2386
+ if (!resolvePromptStart)
2387
+ return apply();
2388
+ const bytes = encoder.encode(text).length;
2389
+ if (pendingSteeringCount >= maxSteeringMessages || bytes > maxSteeringQueueBytes - pendingSteeringBytes) {
2390
+ return Promise.reject(new Error("steering queue is full"));
2272
2391
  }
2273
- return accepted;
2392
+ pendingSteeringCount++;
2393
+ pendingSteeringBytes += bytes;
2394
+ return promptStarted.then((started) => {
2395
+ if (coreExitError && !cancelled)
2396
+ throw coreExitError;
2397
+ return started === true ? apply() : Promise.reject(started instanceof Error ? started : new Error("no prompt is running"));
2398
+ }).finally(() => {
2399
+ pendingSteeringCount--;
2400
+ pendingSteeringBytes -= bytes;
2401
+ });
2274
2402
  },
2275
2403
  cancel() {
2276
2404
  if (finished || cancelled)
2277
2405
  return;
2278
2406
  cancelled = true;
2407
+ cancelBlobRead?.();
2408
+ cancelBlobRead = null;
2409
+ rejectBlobRead = null;
2410
+ resolvePromptStart?.(false);
2411
+ resolvePromptStart = null;
2279
2412
  runtime.closeSteering?.();
2280
2413
  resumeOutput?.();
2281
2414
  resumeOutput = null;
2282
- send({ jsonrpc: "2.0", method: "session/cancel", params: { sessionId } });
2415
+ if (!closing)
2416
+ send({ jsonrpc: "2.0", method: "session/cancel", params: { sessionId } });
2283
2417
  for (const controller of toolControllers)
2284
2418
  controller.abort();
2285
2419
  runtime.abortHostEffects();
@@ -2311,6 +2445,8 @@ async function createFxAgent(options = {}) {
2311
2445
  }
2312
2446
  };
2313
2447
  if (signal?.aborted) {
2448
+ resolvePromptStart?.(false);
2449
+ resolvePromptStart = null;
2314
2450
  finished = true;
2315
2451
  turn.result = Promise.resolve({ stopReason: "cancelled" });
2316
2452
  return turn;
@@ -2319,8 +2455,34 @@ async function createFxAgent(options = {}) {
2319
2455
  runtime.openSteering?.();
2320
2456
  const abort = () => turn.cancel();
2321
2457
  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")
2458
+ const blobReadCancelled = hasBlobs ? new Promise((resolve, reject) => {
2459
+ cancelBlobRead = () => resolve(null);
2460
+ rejectBlobRead = reject;
2461
+ }) : 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`);
2477
+ }
2478
+ return request("session/prompt", { sessionId, prompt: encodedPrompt });
2479
+ }) : request("session/prompt", { sessionId, prompt });
2480
+ turn.result = response.then((value) => ({ stopReason: cancelled ? "cancelled" : value.stopReason, usage: value.usage })).catch((error) => {
2481
+ cancelBlobRead = null;
2482
+ rejectBlobRead = null;
2483
+ resolvePromptStart?.(error);
2484
+ resolvePromptStart = null;
2485
+ if (cancelled && error.message === "Cancelled")
2324
2486
  return { stopReason: "cancelled" };
2325
2487
  terminalError = error;
2326
2488
  throw error;
@@ -2848,10 +3010,11 @@ function createNativeAgent(addon, options) {
2848
3010
  });
2849
3011
  }
2850
3012
  async function createWithFallback(surface, nativeMethod, wasmFactory, defaultWasm, options) {
2851
- const { nativeAddon, backend = "auto", ...runtimeOptions } = options ?? {};
3013
+ const { nativeAddon, backend = "auto", ...unvalidatedOptions } = options ?? {};
2852
3014
  if (!new Set(["auto", "native", "wasm"]).has(backend)) {
2853
3015
  throw new TypeError('backend must be "auto", "native", or "wasm"');
2854
3016
  }
3017
+ const runtimeOptions = surface === "agent" ? normalizeAgentOptions(unvalidatedOptions) : unvalidatedOptions;
2855
3018
  let nativeError;
2856
3019
  let nativeAttempted = false;
2857
3020
  if (backend !== "wasm") {
@@ -2865,7 +3028,7 @@ async function createWithFallback(surface, nativeMethod, wasmFactory, defaultWas
2865
3028
  return await native.backend[nativeMethod](runtimeOptions);
2866
3029
  } catch (error) {
2867
3030
  nativeError = error;
2868
- if (backend === "native")
3031
+ if (backend === "native" || error?.code === "LIBFX_MODEL_UNSUPPORTED_FAST" || error?.code === "LIBFX_MODEL_UNSUPPORTED_EFFORT")
2869
3032
  throw error;
2870
3033
  }
2871
3034
  }
@@ -2884,9 +3047,6 @@ async function createWithFallback(surface, nativeMethod, wasmFactory, defaultWas
2884
3047
  return wasmFactory({ ...runtimeOptions, wasm: await wasmInput(wasmSource) });
2885
3048
  }
2886
3049
  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
3050
  return createWithFallback("agent", "createCore", createFxAgent, defaultCoreWasm, options);
2891
3051
  }
2892
3052
  function createFxTerminal2(options = {}) {
package/node.js CHANGED
@@ -13,6 +13,7 @@ import {
13
13
  encodeXtermKeyEvent,
14
14
  fxSdkApiVersion,
15
15
  listModels,
16
+ normalizeAgentOptions,
16
17
  supportsJspi,
17
18
  xtermAdapter,
18
19
  } from "./fx-sdk.js";
@@ -513,10 +514,11 @@ function createNativeAgent(addon, options) {
513
514
  }
514
515
 
515
516
  async function createWithFallback(surface, nativeMethod, wasmFactory, defaultWasm, options) {
516
- const { nativeAddon, backend = "auto", ...runtimeOptions } = options ?? {};
517
+ const { nativeAddon, backend = "auto", ...unvalidatedOptions } = options ?? {};
517
518
  if (!new Set(["auto", "native", "wasm"]).has(backend)) {
518
519
  throw new TypeError('backend must be "auto", "native", or "wasm"');
519
520
  }
521
+ const runtimeOptions = surface === "agent" ? normalizeAgentOptions(unvalidatedOptions) : unvalidatedOptions;
520
522
 
521
523
  let nativeError;
522
524
  let nativeAttempted = false;
@@ -530,7 +532,8 @@ async function createWithFallback(surface, nativeMethod, wasmFactory, defaultWas
530
532
  return await native.backend[nativeMethod](runtimeOptions);
531
533
  } catch (error) {
532
534
  nativeError = error;
533
- if (backend === "native") throw error;
535
+ if (backend === "native" || error?.code === "LIBFX_MODEL_UNSUPPORTED_FAST" ||
536
+ error?.code === "LIBFX_MODEL_UNSUPPORTED_EFFORT") throw error;
534
537
  }
535
538
  }
536
539
  if (backend === "native") {
@@ -549,9 +552,6 @@ async function createWithFallback(surface, nativeMethod, wasmFactory, defaultWas
549
552
  }
550
553
 
551
554
  export async function createFxAgent(options = {}) {
552
- if (options != null && Object.hasOwn(Object(options), "env")) {
553
- throw new TypeError("createFxAgent() does not accept env; pass apiKey and model directly");
554
- }
555
555
  return createWithFallback(
556
556
  "agent",
557
557
  "createCore",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "libfx",
3
- "version": "0.0.11",
3
+ "version": "0.0.12",
4
4
  "description": "Embed fx agents and terminals in JavaScript hosts",
5
5
  "type": "module",
6
6
  "repository": {