libfx 0.0.7-dev.609.g1f98d14929a0 → 0.0.7-dev.630.gc07d0e40d42d

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
@@ -19,7 +19,11 @@ filesystem read when imported.
19
19
  import { createFxAgent } from "libfx";
20
20
 
21
21
  const agent = await createFxAgent({
22
- env: { AI_GATEWAY_API_KEY: process.env.AI_GATEWAY_API_KEY },
22
+ apiKey: process.env.AI_GATEWAY_API_KEY,
23
+ model: "google/gemini-2.5-flash-lite",
24
+ onEvent(event) {
25
+ if (event.type === "transport.response") console.log(event.elapsedMs);
26
+ },
23
27
  });
24
28
 
25
29
  const turn = agent.prompt("Explain this project.");
@@ -33,6 +37,20 @@ const checkpoint = await agent.checkpoint();
33
37
  await agent.close();
34
38
  ```
35
39
 
40
+ `apiKey` is required. `model` is optional and defaults to fx's built-in model.
41
+ Agent configuration uses named options; `env` is reserved for
42
+ `createFxTerminal()`.
43
+
44
+ The host selects the model. Agent creation and prompting do not fetch the
45
+ Gateway model catalog.
46
+
47
+ `onEvent` receives runtime diagnostics separately from model output. Transport
48
+ events report request start, response status and elapsed time, safe Gateway
49
+ request metadata, and failures. Credentials and raw headers are never included.
50
+
51
+ libfx makes at most one automatic retry after a retryable transport failure and
52
+ only before model output or tool effects escape. Cancellation prevents a retry.
53
+
36
54
  `prompt(input, { signal? })` accepts a string or text/resource blocks. It
37
55
  returns an async iterable of normalized events:
38
56
 
@@ -46,17 +64,36 @@ opaque, bounded, versioned bytes. Restore them only when creating a fresh
46
64
  agent:
47
65
 
48
66
  ```js
49
- const restored = await createFxAgent({ checkpoint, env });
67
+ const restored = await createFxAgent({ apiKey, model, checkpoint });
50
68
  ```
51
69
 
52
70
  The checkpoint contains conversation history and usage only. The host owns
53
71
  durable storage and must resupply models, credentials, instructions, tools,
54
72
  MCP clients, and skill records.
55
73
 
74
+ ## Models
75
+
76
+ Model discovery is explicit and does not create an Agent or load native or Wasm
77
+ artifacts:
78
+
79
+ ```js
80
+ import { listModels } from "libfx";
81
+
82
+ const models = await listModels({
83
+ apiKey: process.env.AI_GATEWAY_API_KEY,
84
+ });
85
+ ```
86
+
87
+ `listModels()` performs one bounded Gateway request and returns sorted, unique
88
+ language-model IDs. It accepts the same optional `fetch` override as the Agent
89
+ API.
90
+
56
91
  ## JavaScript tools and instructions
57
92
 
58
93
  ```js
59
94
  const agent = await createFxAgent({
95
+ apiKey,
96
+ model,
60
97
  instructions: "Keep answers concise.",
61
98
  tools: [{
62
99
  name: "lookup",
@@ -70,14 +107,15 @@ const agent = await createFxAgent({
70
107
  return database.get(input.key, { signal });
71
108
  },
72
109
  }],
73
- env,
74
110
  });
75
111
  ```
76
112
 
77
113
  The JavaScript host is the authority for tool effects. The same descriptors,
78
114
  schemas, cancellation, results, and events are used by N-API and WebAssembly.
79
115
  Instructions are limited to 64 KiB of UTF-8 text, including text assembled by
80
- the MCP and skills adapters.
116
+ the MCP and skills adapters. They are the complete host-owned system context:
117
+ libfx adds no hidden base prompt, and omitting `instructions` sends no system
118
+ message.
81
119
 
82
120
  ## MCP
83
121
 
@@ -94,9 +132,10 @@ const mcp = await createMcpAdapter(client, {
94
132
  });
95
133
 
96
134
  const agent = await createFxAgent({
135
+ apiKey,
136
+ model,
97
137
  tools: mcp.tools,
98
138
  instructions: mcp.instructions,
99
- env,
100
139
  });
101
140
 
102
141
  // ...
@@ -115,15 +154,15 @@ import { createSkillsAdapter } from "libfx/skills";
115
154
 
116
155
  const record = await loadSkillFile("./skills/review/SKILL.md");
117
156
  const skills = createSkillsAdapter([record]);
118
- const agent = await createFxAgent({ ...skills, env });
157
+ const agent = await createFxAgent({ apiKey, model, ...skills });
119
158
  ```
120
159
 
121
160
  ## Backends
122
161
 
123
162
  ```js
124
- await createFxAgent({ backend: "auto" }); // native, then Wasm fallback
125
- await createFxAgent({ backend: "native" }); // require N-API
126
- await createFxAgent({ backend: "wasm" }); // require Wasm + JSPI
163
+ await createFxAgent({ apiKey, backend: "auto" }); // native, then Wasm fallback
164
+ await createFxAgent({ apiKey, backend: "native" }); // require N-API
165
+ await createFxAgent({ apiKey, backend: "wasm" }); // require Wasm + JSPI
127
166
  ```
128
167
 
129
168
  Within one JavaScript realm, libfx compiles each stable Wasm source once and
@@ -156,7 +195,7 @@ stores remain terminal-only host integrations.
156
195
 
157
196
  ## Security
158
197
 
159
- Treat `nativeAddon` and `env.FX_GATEWAY_CHAT_URL` as trusted host
198
+ Treat `nativeAddon` and `gatewayChatUrl` as trusted host
160
199
  configuration. Do not embed long-lived credentials in public browser code.
161
200
  Host tool functions, MCP clients, and skill loaders retain their own authority;
162
201
  libfx validates and sequences them but does not grant operating-system access.
package/browser.js CHANGED
@@ -3,11 +3,12 @@ import {
3
3
  createFxTerminal as createWasmTerminal,
4
4
  encodeXtermKeyEvent,
5
5
  fxSdkApiVersion,
6
+ listModels,
6
7
  supportsJspi,
7
8
  xtermAdapter,
8
9
  } from "./fx-sdk.js";
9
10
 
10
- export { encodeXtermKeyEvent, fxSdkApiVersion, supportsJspi, xtermAdapter };
11
+ export { encodeXtermKeyEvent, fxSdkApiVersion, listModels, supportsJspi, xtermAdapter };
11
12
  export const libfxApiVersion = 2;
12
13
 
13
14
  const defaultCoreWasm = new URL("./fx-core.wasm", import.meta.url).href;
package/fx-core.wasm CHANGED
Binary file
package/fx-sdk.js CHANGED
@@ -5,8 +5,145 @@ const workspaceInfoLimit = 4 * 1024;
5
5
  const workspaceCommandLimit = 64 * 1024;
6
6
  const workspaceOutputLimit = 64 * 1024;
7
7
  const maxInstructionsBytes = 64 * 1024;
8
+ const maxApiKeyBytes = 64 * 1024;
9
+ const maxModelBytes = 1024;
10
+ const maxUrlBytes = 16 * 1024;
11
+ const maxModelCatalogBytes = 4 * 1024 * 1024;
12
+ const maxModelCatalogEntries = 10_000;
8
13
  const streamReadsPerTaskYield = 32;
9
14
 
15
+ function boundedString(value, name, maxBytes, required) {
16
+ if (value === undefined && !required) return undefined;
17
+ if (typeof value !== "string" || value.length === 0) {
18
+ throw new TypeError(`${name} ${required ? "is required and " : ""}must be a non-empty string`);
19
+ }
20
+ if (encoder.encode(value).length > maxBytes) {
21
+ throw new RangeError(`${name} exceeds the ${maxBytes} byte libfx limit`);
22
+ }
23
+ return value;
24
+ }
25
+
26
+ function validateGatewayChatUrl(value) {
27
+ if (value === undefined) return;
28
+ boundedString(value, "gatewayChatUrl", maxUrlBytes, false);
29
+ let url;
30
+ try { url = new URL(value); } catch { throw new TypeError("gatewayChatUrl must be a valid URL"); }
31
+ if (url.username || url.password || url.hash) {
32
+ throw new TypeError("gatewayChatUrl must not contain credentials or a fragment");
33
+ }
34
+ if (url.href === "https://ai-gateway.vercel.sh/v3/ai/language-model") return;
35
+ const loopback = url.hostname === "127.0.0.1" || url.hostname === "[::1]" || url.hostname === "localhost";
36
+ if (url.protocol !== "http:" || !loopback || !url.port) {
37
+ throw new TypeError("gatewayChatUrl must use the canonical Gateway or explicit loopback HTTP");
38
+ }
39
+ }
40
+
41
+ function normalizeAgentOptions(value) {
42
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
43
+ throw new TypeError("createFxAgent() options must be an object");
44
+ }
45
+ const options = { ...value };
46
+ if (Object.hasOwn(options, "env")) {
47
+ throw new TypeError("createFxAgent() does not accept env; pass apiKey and model directly");
48
+ }
49
+ options.apiKey = boundedString(options.apiKey, "apiKey", maxApiKeyBytes, true);
50
+ options.model = boundedString(options.model, "model", maxModelBytes, false);
51
+ validateGatewayChatUrl(options.gatewayChatUrl);
52
+ return options;
53
+ }
54
+
55
+ function agentEnvironment(options) {
56
+ return {
57
+ AI_GATEWAY_API_KEY: options.apiKey,
58
+ ...(options.model === undefined ? {} : { FX_MODEL: options.model }),
59
+ ...(options.gatewayChatUrl === undefined ? {} : { FX_GATEWAY_CHAT_URL: options.gatewayChatUrl }),
60
+ };
61
+ }
62
+
63
+ async function cancelResponseBody(response) {
64
+ try {
65
+ await response.body?.cancel();
66
+ } catch {}
67
+ }
68
+
69
+ async function readBoundedResponseText(response, limit) {
70
+ const declared = Number(response.headers.get("content-length"));
71
+ if (Number.isFinite(declared) && declared > limit) {
72
+ await cancelResponseBody(response);
73
+ throw new RangeError(`model catalog exceeds the ${limit} byte libfx limit`);
74
+ }
75
+ if (!response.body) {
76
+ const bytes = new Uint8Array(await response.arrayBuffer());
77
+ if (bytes.length > limit) throw new RangeError(`model catalog exceeds the ${limit} byte libfx limit`);
78
+ return strictDecoder.decode(bytes);
79
+ }
80
+
81
+ const reader = response.body.getReader();
82
+ const chunks = [];
83
+ let total = 0;
84
+ for (;;) {
85
+ const { done, value } = await reader.read();
86
+ if (done) break;
87
+ if (!value?.length) continue;
88
+ total += value.length;
89
+ if (total > limit) {
90
+ try {
91
+ await reader.cancel();
92
+ } catch {}
93
+ throw new RangeError(`model catalog exceeds the ${limit} byte libfx limit`);
94
+ }
95
+ chunks.push(value);
96
+ }
97
+ const bytes = new Uint8Array(total);
98
+ let offset = 0;
99
+ for (const chunk of chunks) {
100
+ bytes.set(chunk, offset);
101
+ offset += chunk.length;
102
+ }
103
+ return strictDecoder.decode(bytes);
104
+ }
105
+
106
+ export async function listModels(options = {}) {
107
+ if (!options || typeof options !== "object" || Array.isArray(options)) {
108
+ throw new TypeError("listModels() options must be an object");
109
+ }
110
+ const apiKey = boundedString(options.apiKey, "apiKey", maxApiKeyBytes, true);
111
+ const fetchModels = options.fetch ?? globalThis.fetch?.bind(globalThis);
112
+ if (typeof fetchModels !== "function") throw new TypeError("fetch is unavailable");
113
+ const response = await fetchModels("https://ai-gateway.vercel.sh/coding-agent/v1/models", {
114
+ method: "GET",
115
+ headers: { authorization: `Bearer ${apiKey}` },
116
+ });
117
+ if (!response.ok) {
118
+ await cancelResponseBody(response);
119
+ throw new Error(`model catalog request failed with HTTP ${response.status}`);
120
+ }
121
+
122
+ let catalog;
123
+ try {
124
+ catalog = JSON.parse(await readBoundedResponseText(response, maxModelCatalogBytes));
125
+ } catch (error) {
126
+ if (error instanceof RangeError) throw error;
127
+ throw new TypeError("model catalog response is malformed");
128
+ }
129
+ if (!catalog || typeof catalog !== "object" || !Array.isArray(catalog.data)) {
130
+ throw new TypeError("model catalog response is malformed");
131
+ }
132
+ if (catalog.data.length > maxModelCatalogEntries) {
133
+ throw new RangeError(`model catalog exceeds the ${maxModelCatalogEntries} entry libfx limit`);
134
+ }
135
+
136
+ const ids = new Set();
137
+ for (const entry of catalog.data) {
138
+ if (!entry || typeof entry !== "object") continue;
139
+ if (typeof entry.type === "string" && entry.type.toLowerCase() !== "language") continue;
140
+ if (typeof entry.id !== "string" || entry.id.length === 0) continue;
141
+ if (encoder.encode(entry.id).length > maxModelBytes) continue;
142
+ ids.add(entry.id);
143
+ }
144
+ return [...ids].sort();
145
+ }
146
+
10
147
  function validWorkspacePath(path) {
11
148
  if (typeof path !== "string" || !path.startsWith("/") || path.includes("\0")) return false;
12
149
  if (strictDecoder.decode(encoder.encode(path)) !== path) return false;
@@ -1014,7 +1151,7 @@ function base64ToBytes(value) {
1014
1151
  }
1015
1152
 
1016
1153
  export async function createFxAgent(options = {}) {
1017
- options = { ...options };
1154
+ options = normalizeAgentOptions(options);
1018
1155
  const hostTools = normalizeHostTools(options.tools);
1019
1156
  const instructions = normalizeInstructions(options.instructions);
1020
1157
  const initialCheckpoint = checkpointBytes(options.checkpoint);
@@ -1026,6 +1163,49 @@ export async function createFxAgent(options = {}) {
1026
1163
  const emit = (type, detail = {}) => {
1027
1164
  try { options.onEvent?.({ type, timestamp: performance.now(), ...detail }); } catch {}
1028
1165
  };
1166
+ const hostFetch = options.fetch ?? globalThis.fetch?.bind(globalThis);
1167
+ const transportFetch = async (input, init = {}) => {
1168
+ const method = String(init.method ?? input?.method ?? "GET").toUpperCase();
1169
+ let endpoint = String(input?.url ?? input);
1170
+ try {
1171
+ const url = new URL(endpoint);
1172
+ endpoint = `${url.origin}${url.pathname}`;
1173
+ } catch {}
1174
+ for (let attemptIndex = 0; attemptIndex < 2; attemptIndex++) {
1175
+ const startedAt = performance.now();
1176
+ const attempt = activeTurn ? ++activeTurn.transportAttempts : attemptIndex + 1;
1177
+ emit("transport.start", { attempt, method, endpoint, model: options.model });
1178
+ try {
1179
+ if (!hostFetch) throw new TypeError("fetch is unavailable");
1180
+ const response = await hostFetch(input, init);
1181
+ const headers = response.headers;
1182
+ emit("transport.response", {
1183
+ attempt,
1184
+ status: response.status,
1185
+ elapsedMs: performance.now() - startedAt,
1186
+ requestId: headers.get("x-vercel-id"),
1187
+ generationId: headers.get("x-generation-id"),
1188
+ model: headers.get("x-model-id") ?? options.model,
1189
+ provider: headers.get("x-vercel-ai-gateway-provider") ?? headers.get("x-ai-gateway-provider"),
1190
+ });
1191
+ return response;
1192
+ } catch (error) {
1193
+ const errorName = error instanceof Error ? error.name : "Error";
1194
+ const elapsedMs = performance.now() - startedAt;
1195
+ emit("transport.error", { attempt, elapsedMs, error: errorName });
1196
+ if (init.signal?.aborted) throw new DOMException("Aborted", "AbortError");
1197
+ if (attemptIndex === 1) throw error;
1198
+ emit("transport.retry", {
1199
+ attempt,
1200
+ nextAttempt: attempt + 1,
1201
+ elapsedMs,
1202
+ error: errorName,
1203
+ });
1204
+ if (init.signal?.aborted) throw new DOMException("Aborted", "AbortError");
1205
+ }
1206
+ }
1207
+ throw new Error("transport retry exhausted");
1208
+ };
1029
1209
  const executeHostTool = async (name, input, requestedSessionId) => {
1030
1210
  const execute = hostTools.executors.get(name);
1031
1211
  const turn = requestedSessionId === undefined || requestedSessionId === sessionId
@@ -1047,7 +1227,13 @@ export async function createFxAgent(options = {}) {
1047
1227
  return { content, isError, cancelled: controller.signal.aborted };
1048
1228
  };
1049
1229
  emit("runtime.start");
1050
- const runtimeOptions = { ...options, args: ["acp"], hostToolExecutor: executeHostTool };
1230
+ const runtimeOptions = {
1231
+ ...options,
1232
+ fetch: transportFetch,
1233
+ args: ["acp"],
1234
+ env: agentEnvironment(options),
1235
+ hostToolExecutor: executeHostTool,
1236
+ };
1051
1237
  const runtime = options.runtimeFactory
1052
1238
  ? await options.runtimeFactory(runtimeOptions)
1053
1239
  : await instantiate(runtimeOptions);
@@ -1219,6 +1405,7 @@ export async function createFxAgent(options = {}) {
1219
1405
  const turn = {
1220
1406
  push(update) { const waiter = waiters.shift(); if (waiter) waiter({ value: update, done: false }); else queue.push(update); },
1221
1407
  toolControllers,
1408
+ transportAttempts: 0,
1222
1409
  cancel() {
1223
1410
  if (finished || cancelled) return;
1224
1411
  cancelled = true;
package/fx-term.wasm CHANGED
Binary file
Binary file
Binary file
Binary file
Binary file
package/node.js CHANGED
@@ -8,11 +8,12 @@ import {
8
8
  createFxTerminal as createWasmTerminal,
9
9
  encodeXtermKeyEvent,
10
10
  fxSdkApiVersion,
11
+ listModels,
11
12
  supportsJspi,
12
13
  xtermAdapter,
13
14
  } from "./fx-sdk.js";
14
15
 
15
- export { encodeXtermKeyEvent, fxSdkApiVersion, supportsJspi, xtermAdapter };
16
+ export { encodeXtermKeyEvent, fxSdkApiVersion, listModels, supportsJspi, xtermAdapter };
16
17
  export const libfxApiVersion = 2;
17
18
 
18
19
  const fetchOperationStale = 0;
@@ -67,9 +68,8 @@ function validateNativeBackend(backend) {
67
68
  const actualVersion = backend.libfxApiVersion ?? "missing";
68
69
  throw new Error(`native addon API version ${actualVersion} is incompatible with libfx API version ${libfxApiVersion}`);
69
70
  }
70
- if (typeof backend.createFxAgent !== "function" && typeof backend.createCore !== "function" &&
71
- typeof backend.createFxTerminal !== "function") {
72
- throw new Error("native addon must export createFxAgent(), createCore(), or createFxTerminal()");
71
+ if (typeof backend.createCore !== "function" && typeof backend.createFxTerminal !== "function") {
72
+ throw new Error("native addon must export createCore() or createFxTerminal()");
73
73
  }
74
74
  return backend;
75
75
  }
@@ -120,26 +120,8 @@ function wasmBytes(input) {
120
120
  return pending;
121
121
  }
122
122
 
123
- function validateGatewayChatUrl(value) {
124
- if (value === undefined) return;
125
- if (typeof value !== "string") throw new TypeError("FX_GATEWAY_CHAT_URL must be a string");
126
- let url;
127
- try { url = new URL(value); } catch { throw new TypeError("FX_GATEWAY_CHAT_URL must be a valid URL"); }
128
- if (url.username || url.password || url.hash) {
129
- throw new TypeError("FX_GATEWAY_CHAT_URL must not contain credentials or a fragment");
130
- }
131
- if (url.href === "https://ai-gateway.vercel.sh/v3/ai/language-model") return;
132
- const loopback = url.hostname === "127.0.0.1" || url.hostname === "[::1]" || url.hostname === "localhost";
133
- if (url.protocol !== "http:" || !loopback || !url.port) {
134
- throw new TypeError("FX_GATEWAY_CHAT_URL must use the canonical Gateway or explicit loopback HTTP");
135
- }
136
- }
137
-
138
123
  function createNativeCoreRuntime(addon, options) {
139
- const apiKey = options.env?.AI_GATEWAY_API_KEY;
140
- const model = options.env?.FX_MODEL;
141
- const gatewayChatUrl = options.env?.FX_GATEWAY_CHAT_URL;
142
- validateGatewayChatUrl(gatewayChatUrl);
124
+ const { apiKey, model, gatewayChatUrl } = options;
143
125
  const core = addon.createCore({
144
126
  apiKey,
145
127
  home: options.home ?? homedir(),
@@ -258,7 +240,6 @@ function createNativeAgent(addon, options) {
258
240
 
259
241
  async function createWithFallback(surface, nativeMethod, wasmFactory, defaultWasm, options) {
260
242
  const { nativeAddon, backend = "auto", ...runtimeOptions } = options ?? {};
261
- validateGatewayChatUrl(runtimeOptions.env?.FX_GATEWAY_CHAT_URL);
262
243
  if (!new Set(["auto", "native", "wasm"]).has(backend)) {
263
244
  throw new TypeError('backend must be "auto", "native", or "wasm"');
264
245
  }
@@ -268,14 +249,11 @@ async function createWithFallback(surface, nativeMethod, wasmFactory, defaultWas
268
249
  if (backend !== "wasm") {
269
250
  const native = await resolveNativeBackend(nativeAddon);
270
251
  nativeError = native.error;
271
- if (typeof native.backend?.[nativeMethod] === "function" ||
272
- (surface === "agent" && typeof native.backend?.createCore === "function")) {
252
+ if (typeof native.backend?.[nativeMethod] === "function") {
273
253
  nativeAttempted = true;
274
254
  try {
275
- if (typeof native.backend?.[nativeMethod] === "function") {
276
- return await native.backend[nativeMethod](runtimeOptions);
277
- }
278
- return await createNativeAgent(native.backend, runtimeOptions);
255
+ if (surface === "agent") return await createNativeAgent(native.backend, runtimeOptions);
256
+ return await native.backend[nativeMethod](runtimeOptions);
279
257
  } catch (error) {
280
258
  nativeError = error;
281
259
  if (backend === "native") throw error;
@@ -298,8 +276,17 @@ async function createWithFallback(surface, nativeMethod, wasmFactory, defaultWas
298
276
  });
299
277
  }
300
278
 
301
- export function createFxAgent(options = {}) {
302
- return createWithFallback("agent", "createFxAgent", createWasmAgent, defaultCoreWasm, options);
279
+ export async function createFxAgent(options = {}) {
280
+ if (options != null && Object.hasOwn(Object(options), "env")) {
281
+ throw new TypeError("createFxAgent() does not accept env; pass apiKey and model directly");
282
+ }
283
+ return createWithFallback(
284
+ "agent",
285
+ "createCore",
286
+ createWasmAgent,
287
+ defaultCoreWasm,
288
+ options,
289
+ );
303
290
  }
304
291
 
305
292
  export function createFxTerminal(options = {}) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "libfx",
3
- "version": "0.0.7-dev.609.g1f98d14929a0",
3
+ "version": "0.0.7-dev.630.gc07d0e40d42d",
4
4
  "description": "Embed fx agents and terminals in JavaScript hosts",
5
5
  "type": "module",
6
6
  "repository": {