libfx 0.0.9 → 0.0.10-dev.1207.g8578e5771980

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
@@ -41,9 +41,25 @@ await agent.close();
41
41
  Agent configuration uses named options; `env` is reserved for
42
42
  `createFxTerminal()`.
43
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.
58
+
44
59
  The host selects the model. Agent creation does not fetch the Gateway model
45
- catalog. Prompting can resolve model capabilities and context capacity through
46
- the supplied `fetch`; fx caches that metadata for the agent.
60
+ catalog unless `effort` requests a named level or `fast` is enabled. Prompting
61
+ can resolve model capabilities and context capacity through the supplied
62
+ `fetch`; fx caches that metadata for the agent.
47
63
 
48
64
  `onEvent` receives runtime diagnostics separately from model output. Transport
49
65
  events report request start, response status and elapsed time, safe Gateway
@@ -52,8 +68,8 @@ request metadata, and failures. Credentials and raw headers are never included.
52
68
  libfx makes at most one automatic retry after a retryable transport failure and
53
69
  only before model output or tool effects escape. Cancellation prevents a retry.
54
70
 
55
- `prompt(input, { signal? })` accepts a string or text/resource blocks. It
56
- returns an async iterable of normalized events:
71
+ `prompt(input, { signal? })` accepts a string or text, image, and resource
72
+ blocks. It returns an async iterable of normalized events:
57
73
 
58
74
  - `text_delta`
59
75
  - `reasoning_delta` when supplied by the provider
@@ -82,6 +98,27 @@ that threshold when the queue is empty; an individual encoded ACP message is
82
98
  limited to 64 MiB on both backends. These are transport bounds, not a total
83
99
  answer-size limit or a bound on retained conversation history.
84
100
 
101
+ Image blocks use ACP's content shape and carry canonical base64 (no line
102
+ wrapping) of a PNG, JPEG, GIF, or WebP payload:
103
+
104
+ ```js
105
+ const turn = agent.prompt([
106
+ { type: "text", text: "What does this screenshot show?" },
107
+ { type: "image", data: base64Png, mimeType: "image/png" },
108
+ ]);
109
+ ```
110
+
111
+ A prompt may contain up to 8 images, each with up to 5 MiB of base64 data,
112
+ with at most 8 MiB of image data per prompt; the SDK rejects larger input with
113
+ typed `RangeError`s before any request. The kernel then validates the decoded
114
+ bytes against the declared `mimeType` and fails the turn with
115
+ `Invalid image prompt block` on a mismatch. Images are routed only to models
116
+ that advertise image input; for any other model the turn fails with
117
+ `Image prompts are unavailable for the selected model` and no image bytes
118
+ leave the process. Prompt images are retained in checkpoints within the
119
+ existing 4 MiB checkpoint bound, so a restored agent can refer to earlier
120
+ images on either backend.
121
+
85
122
  Only one prompt may run at a time. `checkpoint()` is idle-only and returns
86
123
  opaque, bounded, versioned bytes. Restore them only when creating a fresh
87
124
  agent:
@@ -95,7 +132,10 @@ or a history change. The next prompt can run normally.
95
132
 
96
133
  The checkpoint contains conversation history and usage only. The host owns
97
134
  durable storage and must resupply models, credentials, instructions, tools,
98
- MCP clients, and skill records.
135
+ MCP clients, and skill records. Reasoning effort and fast mode are
136
+ agent-creation options and are not stored in a checkpoint: recreate the agent
137
+ with new `effort` or `fast` values to change them, the same path as switching
138
+ models.
99
139
 
100
140
  ## Models
101
141
 
package/fx-core.wasm CHANGED
Binary file
package/fx-sdk.js CHANGED
@@ -10,12 +10,24 @@ const workspaceOutputLimit = 64 * 1024;
10
10
  const maxInstructionsBytes = 64 * 1024;
11
11
  const maxApiKeyBytes = 64 * 1024;
12
12
  const maxModelBytes = 1024;
13
+ // Matches the kernel's ReasoningEffort.max_name_bytes.
14
+ const maxEffortBytes = 64;
13
15
  const maxUrlBytes = 16 * 1024;
14
16
  const maxModelCatalogBytes = 4 * 1024 * 1024;
15
17
  const maxModelCatalogEntries = 10_000;
16
18
  const streamReadsPerTaskYield = 32;
17
19
  const maxUnreadEventBytes = 1024 * 1024;
18
20
  const maxUnreadEvents = 256;
21
+ // Prompt image limits mirror the host tool result image contract: the kernel
22
+ // validates content, the SDK bounds the frame before it reaches the core.
23
+ const maxPromptImages = 8;
24
+ const maxPromptImageDataBytes = 5 * 1024 * 1024;
25
+ const maxPromptImagesBytes = 8 * 1024 * 1024;
26
+ // The core's ACP reader drops frames over 8 MiB without a request id to answer
27
+ // (jsonrpc frame_resource_byte_limit), so the SDK must never emit one. The
28
+ // envelope allowance covers the method key and request id.
29
+ const maxPromptFrameBytes = 8 * 1024 * 1024;
30
+ const promptFrameEnvelopeBytes = 128;
19
31
 
20
32
  function boundedString(value, name, maxBytes, required) {
21
33
  if (value === undefined && !required) return undefined;
@@ -44,6 +56,27 @@ function validateGatewayChatUrl(value) {
44
56
  }
45
57
  }
46
58
 
59
+ // Mirrors the kernel's ReasoningEffort.parse: "auto"/"adaptive"/"default" pick
60
+ // the model default; anything else must be a bounded effort name.
61
+ function normalizeEffort(value) {
62
+ if (value === undefined) return undefined;
63
+ boundedString(value, "effort", maxEffortBytes, false);
64
+ if (!/^[A-Za-z0-9._-]+$/.test(value)) {
65
+ throw new TypeError('effort must use only letters, digits, ".", "-", or "_"');
66
+ }
67
+ return value;
68
+ }
69
+
70
+ // Mirrors the CLI's --fast/--no-fast toggle: a strict boolean, with undefined
71
+ // leaving the model default in place.
72
+ function normalizeFast(value) {
73
+ if (value === undefined) return undefined;
74
+ if (typeof value !== "boolean") {
75
+ throw new TypeError("fast must be a boolean");
76
+ }
77
+ return value;
78
+ }
79
+
47
80
  function normalizeAgentOptions(value) {
48
81
  if (!value || typeof value !== "object" || Array.isArray(value)) {
49
82
  throw new TypeError("createFxAgent() options must be an object");
@@ -54,6 +87,8 @@ function normalizeAgentOptions(value) {
54
87
  }
55
88
  options.apiKey = boundedString(options.apiKey, "apiKey", maxApiKeyBytes, true);
56
89
  options.model = boundedString(options.model, "model", maxModelBytes, false);
90
+ options.effort = normalizeEffort(options.effort);
91
+ options.fast = normalizeFast(options.fast);
57
92
  validateGatewayChatUrl(options.gatewayChatUrl);
58
93
  return options;
59
94
  }
@@ -62,10 +97,26 @@ function agentEnvironment(options) {
62
97
  return {
63
98
  AI_GATEWAY_API_KEY: options.apiKey,
64
99
  ...(options.model === undefined ? {} : { FX_MODEL: options.model }),
100
+ ...(options.effort === undefined ? {} : { FX_EFFORT: options.effort }),
101
+ ...(options.fast === undefined ? {} : { FX_FAST: options.fast ? "true" : "false" }),
65
102
  ...(options.gatewayChatUrl === undefined ? {} : { FX_GATEWAY_CHAT_URL: options.gatewayChatUrl }),
66
103
  };
67
104
  }
68
105
 
106
+ // The kernel rejects an unsupported effort or fast lane during initialize;
107
+ // these messages originate only from that validation, so the rejection is
108
+ // safe to retype.
109
+ function agentBootstrapError(error) {
110
+ if (error instanceof Error &&
111
+ (error.message === "Invalid reasoning effort" || error.message.startsWith("Reasoning effort"))) {
112
+ error.code ??= "LIBFX_UNSUPPORTED_EFFORT";
113
+ }
114
+ if (error instanceof Error && error.message.startsWith("Fast mode")) {
115
+ error.code ??= "LIBFX_UNSUPPORTED_FAST";
116
+ }
117
+ return error;
118
+ }
119
+
69
120
  async function cancelResponseBody(response) {
70
121
  try {
71
122
  await response.body?.cancel();
@@ -1182,9 +1233,30 @@ export async function createFxTerminal(options) {
1182
1233
  function normalizePromptInput(input) {
1183
1234
  if (typeof input === "string") return [{ type: "text", text: input }];
1184
1235
  if (!Array.isArray(input)) throw new TypeError("prompt input must be a string or an array of prompt blocks");
1236
+ let imageCount = 0;
1237
+ let imageBytes = 0;
1185
1238
  return input.map((block, index) => {
1186
1239
  if (!block || typeof block !== "object") throw new TypeError(`prompt block ${index} must be an object`);
1187
- if (block.type === "image") throw new TypeError("image prompt blocks are unsupported");
1240
+ if (block.type === "image") {
1241
+ if (typeof block.data !== "string" || block.data.length === 0) {
1242
+ throw new TypeError(`image prompt block ${index} requires base64 data`);
1243
+ }
1244
+ if (typeof block.mimeType !== "string" || block.mimeType.length === 0 || block.mimeType.length > 128) {
1245
+ throw new TypeError(`image prompt block ${index} requires a mimeType`);
1246
+ }
1247
+ if (block.data.length > maxPromptImageDataBytes) {
1248
+ throw new RangeError(`image prompt block ${index} exceeds the ${maxPromptImageDataBytes} byte per-image libfx limit`);
1249
+ }
1250
+ imageCount += 1;
1251
+ if (imageCount > maxPromptImages) {
1252
+ throw new RangeError(`prompt cannot contain more than ${maxPromptImages} images`);
1253
+ }
1254
+ imageBytes += block.data.length;
1255
+ if (imageBytes > maxPromptImagesBytes) {
1256
+ throw new RangeError(`prompt images exceed the ${maxPromptImagesBytes} byte libfx frame limit`);
1257
+ }
1258
+ return { type: "image", data: block.data, mimeType: block.mimeType };
1259
+ }
1188
1260
  if (block.type === "text") {
1189
1261
  if (typeof block.text !== "string") throw new TypeError(`text prompt block ${index} requires text`);
1190
1262
  return { type: "text", text: block.text };
@@ -1490,7 +1562,7 @@ export async function createFxAgent(options = {}) {
1490
1562
  try { runtime.abortHostEffects(); } catch {}
1491
1563
  try { runtime.closeStdin(); } catch {}
1492
1564
  try { await runtime.exited; } catch {}
1493
- throw error;
1565
+ throw agentBootstrapError(error);
1494
1566
  }
1495
1567
 
1496
1568
  const agent = {
@@ -1591,6 +1663,9 @@ export async function createFxAgent(options = {}) {
1591
1663
 
1592
1664
  function startTurn(input, promptOptions) {
1593
1665
  const prompt = normalizePromptInput(input);
1666
+ if (encoder.encode(JSON.stringify({ sessionId: "", prompt })).length + promptFrameEnvelopeBytes > maxPromptFrameBytes) {
1667
+ throw new RangeError(`prompt exceeds the ${maxPromptFrameBytes} byte libfx frame limit`);
1668
+ }
1594
1669
  const signal = promptOptions.signal;
1595
1670
  if (signal !== undefined && (typeof signal?.addEventListener !== "function" || typeof signal?.removeEventListener !== "function")) throw new TypeError("prompt signal must be an AbortSignal");
1596
1671
  const queue = [];
package/fx-term.wasm CHANGED
Binary file
Binary file
Binary file
Binary file
Binary file
package/node.cjs CHANGED
@@ -188,12 +188,18 @@ var workspaceOutputLimit = 64 * 1024;
188
188
  var maxInstructionsBytes = 64 * 1024;
189
189
  var maxApiKeyBytes = 64 * 1024;
190
190
  var maxModelBytes = 1024;
191
+ var maxEffortBytes = 64;
191
192
  var maxUrlBytes = 16 * 1024;
192
193
  var maxModelCatalogBytes = 4 * 1024 * 1024;
193
194
  var maxModelCatalogEntries = 1e4;
194
195
  var streamReadsPerTaskYield = 32;
195
196
  var maxUnreadEventBytes = 1024 * 1024;
196
197
  var maxUnreadEvents = 256;
198
+ var maxPromptImages = 8;
199
+ var maxPromptImageDataBytes = 5 * 1024 * 1024;
200
+ var maxPromptImagesBytes = 8 * 1024 * 1024;
201
+ var maxPromptFrameBytes = 8 * 1024 * 1024;
202
+ var promptFrameEnvelopeBytes = 128;
197
203
  function boundedString(value, name, maxBytes, required) {
198
204
  if (value === undefined && !required)
199
205
  return;
@@ -227,6 +233,23 @@ function validateGatewayChatUrl(value) {
227
233
  throw new TypeError("gatewayChatUrl must use the canonical Gateway or explicit loopback HTTP");
228
234
  }
229
235
  }
236
+ function normalizeEffort(value) {
237
+ if (value === undefined)
238
+ return;
239
+ boundedString(value, "effort", maxEffortBytes, false);
240
+ if (!/^[A-Za-z0-9._-]+$/.test(value)) {
241
+ throw new TypeError('effort must use only letters, digits, ".", "-", or "_"');
242
+ }
243
+ return value;
244
+ }
245
+ function normalizeFast(value) {
246
+ if (value === undefined)
247
+ return;
248
+ if (typeof value !== "boolean") {
249
+ throw new TypeError("fast must be a boolean");
250
+ }
251
+ return value;
252
+ }
230
253
  function normalizeAgentOptions(value) {
231
254
  if (!value || typeof value !== "object" || Array.isArray(value)) {
232
255
  throw new TypeError("createFxAgent() options must be an object");
@@ -237,6 +260,8 @@ function normalizeAgentOptions(value) {
237
260
  }
238
261
  options.apiKey = boundedString(options.apiKey, "apiKey", maxApiKeyBytes, true);
239
262
  options.model = boundedString(options.model, "model", maxModelBytes, false);
263
+ options.effort = normalizeEffort(options.effort);
264
+ options.fast = normalizeFast(options.fast);
240
265
  validateGatewayChatUrl(options.gatewayChatUrl);
241
266
  return options;
242
267
  }
@@ -244,9 +269,20 @@ function agentEnvironment(options) {
244
269
  return {
245
270
  AI_GATEWAY_API_KEY: options.apiKey,
246
271
  ...options.model === undefined ? {} : { FX_MODEL: options.model },
272
+ ...options.effort === undefined ? {} : { FX_EFFORT: options.effort },
273
+ ...options.fast === undefined ? {} : { FX_FAST: options.fast ? "true" : "false" },
247
274
  ...options.gatewayChatUrl === undefined ? {} : { FX_GATEWAY_CHAT_URL: options.gatewayChatUrl }
248
275
  };
249
276
  }
277
+ function agentBootstrapError(error) {
278
+ if (error instanceof Error && (error.message === "Invalid reasoning effort" || error.message.startsWith("Reasoning effort"))) {
279
+ error.code ??= "LIBFX_UNSUPPORTED_EFFORT";
280
+ }
281
+ if (error instanceof Error && error.message.startsWith("Fast mode")) {
282
+ error.code ??= "LIBFX_UNSUPPORTED_FAST";
283
+ }
284
+ return error;
285
+ }
250
286
  async function cancelResponseBody(response) {
251
287
  try {
252
288
  await response.body?.cancel();
@@ -1547,11 +1583,31 @@ function normalizePromptInput(input) {
1547
1583
  return [{ type: "text", text: input }];
1548
1584
  if (!Array.isArray(input))
1549
1585
  throw new TypeError("prompt input must be a string or an array of prompt blocks");
1586
+ let imageCount = 0;
1587
+ let imageBytes = 0;
1550
1588
  return input.map((block, index) => {
1551
1589
  if (!block || typeof block !== "object")
1552
1590
  throw new TypeError(`prompt block ${index} must be an object`);
1553
- if (block.type === "image")
1554
- throw new TypeError("image prompt blocks are unsupported");
1591
+ if (block.type === "image") {
1592
+ if (typeof block.data !== "string" || block.data.length === 0) {
1593
+ throw new TypeError(`image prompt block ${index} requires base64 data`);
1594
+ }
1595
+ if (typeof block.mimeType !== "string" || block.mimeType.length === 0 || block.mimeType.length > 128) {
1596
+ throw new TypeError(`image prompt block ${index} requires a mimeType`);
1597
+ }
1598
+ if (block.data.length > maxPromptImageDataBytes) {
1599
+ throw new RangeError(`image prompt block ${index} exceeds the ${maxPromptImageDataBytes} byte per-image libfx limit`);
1600
+ }
1601
+ imageCount += 1;
1602
+ if (imageCount > maxPromptImages) {
1603
+ throw new RangeError(`prompt cannot contain more than ${maxPromptImages} images`);
1604
+ }
1605
+ imageBytes += block.data.length;
1606
+ if (imageBytes > maxPromptImagesBytes) {
1607
+ throw new RangeError(`prompt images exceed the ${maxPromptImagesBytes} byte libfx frame limit`);
1608
+ }
1609
+ return { type: "image", data: block.data, mimeType: block.mimeType };
1610
+ }
1555
1611
  if (block.type === "text") {
1556
1612
  if (typeof block.text !== "string")
1557
1613
  throw new TypeError(`text prompt block ${index} requires text`);
@@ -1902,7 +1958,7 @@ async function createFxAgent(options = {}) {
1902
1958
  try {
1903
1959
  await runtime.exited;
1904
1960
  } catch {}
1905
- throw error;
1961
+ throw agentBootstrapError(error);
1906
1962
  }
1907
1963
  const agent = {
1908
1964
  prompt(input, promptOptions = {}) {
@@ -2027,6 +2083,9 @@ async function createFxAgent(options = {}) {
2027
2083
  }
2028
2084
  function startTurn(input, promptOptions) {
2029
2085
  const prompt = normalizePromptInput(input);
2086
+ if (encoder.encode(JSON.stringify({ sessionId: "", prompt })).length + promptFrameEnvelopeBytes > maxPromptFrameBytes) {
2087
+ throw new RangeError(`prompt exceeds the ${maxPromptFrameBytes} byte libfx frame limit`);
2088
+ }
2030
2089
  const signal = promptOptions.signal;
2031
2090
  if (signal !== undefined && (typeof signal?.addEventListener !== "function" || typeof signal?.removeEventListener !== "function"))
2032
2091
  throw new TypeError("prompt signal must be an AbortSignal");
@@ -2439,12 +2498,14 @@ async function getBackendInfo(value = {}) {
2439
2498
  }
2440
2499
  }
2441
2500
  function createNativeCoreRuntime(addon, options) {
2442
- const { apiKey, model, gatewayChatUrl } = options;
2501
+ const { apiKey, model, effort, fast, gatewayChatUrl } = options;
2443
2502
  const core = addon.createCore({
2444
2503
  apiKey,
2445
2504
  home: options.home ?? import_node_os.homedir(),
2446
2505
  workspaceRoot: options.workspaceRoot ?? process.cwd(),
2447
2506
  ...model === undefined ? {} : { model },
2507
+ ...effort === undefined ? {} : { effort },
2508
+ ...fast === undefined ? {} : { fast },
2448
2509
  ...gatewayChatUrl === undefined ? {} : { gatewayChatUrl }
2449
2510
  });
2450
2511
  let readyFd;
package/node.js CHANGED
@@ -345,12 +345,14 @@ export async function getBackendInfo(value = {}) {
345
345
  }
346
346
 
347
347
  function createNativeCoreRuntime(addon, options) {
348
- const { apiKey, model, gatewayChatUrl } = options;
348
+ const { apiKey, model, effort, fast, gatewayChatUrl } = options;
349
349
  const core = addon.createCore({
350
350
  apiKey,
351
351
  home: options.home ?? homedir(),
352
352
  workspaceRoot: options.workspaceRoot ?? process.cwd(),
353
353
  ...(model === undefined ? {} : { model }),
354
+ ...(effort === undefined ? {} : { effort }),
355
+ ...(fast === undefined ? {} : { fast }),
354
356
  ...(gatewayChatUrl === undefined ? {} : { gatewayChatUrl }),
355
357
  });
356
358
  let readyFd;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "libfx",
3
- "version": "0.0.9",
3
+ "version": "0.0.10-dev.1207.g8578e5771980",
4
4
  "description": "Embed fx agents and terminals in JavaScript hosts",
5
5
  "type": "module",
6
6
  "repository": {