libfx 0.0.7-dev.617.g49fc251a51aa → 0.0.7-dev.632.g9fe8a30c19a7

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
@@ -21,6 +21,9 @@ import { createFxAgent } from "libfx";
21
21
  const agent = await createFxAgent({
22
22
  apiKey: process.env.AI_GATEWAY_API_KEY,
23
23
  model: "google/gemini-2.5-flash-lite",
24
+ onEvent(event) {
25
+ if (event.type === "transport.response") console.log(event.elapsedMs);
26
+ },
24
27
  });
25
28
 
26
29
  const turn = agent.prompt("Explain this project.");
@@ -38,6 +41,16 @@ await agent.close();
38
41
  Agent configuration uses named options; `env` is reserved for
39
42
  `createFxTerminal()`.
40
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
+
41
54
  `prompt(input, { signal? })` accepts a string or text/resource blocks. It
42
55
  returns an async iterable of normalized events:
43
56
 
@@ -58,6 +71,23 @@ The checkpoint contains conversation history and usage only. The host owns
58
71
  durable storage and must resupply models, credentials, instructions, tools,
59
72
  MCP clients, and skill records.
60
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
+
61
91
  ## JavaScript tools and instructions
62
92
 
63
93
  ```js
@@ -83,7 +113,9 @@ const agent = await createFxAgent({
83
113
  The JavaScript host is the authority for tool effects. The same descriptors,
84
114
  schemas, cancellation, results, and events are used by N-API and WebAssembly.
85
115
  Instructions are limited to 64 KiB of UTF-8 text, including text assembled by
86
- 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.
87
119
 
88
120
  ## MCP
89
121
 
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
@@ -8,6 +8,8 @@ const maxInstructionsBytes = 64 * 1024;
8
8
  const maxApiKeyBytes = 64 * 1024;
9
9
  const maxModelBytes = 1024;
10
10
  const maxUrlBytes = 16 * 1024;
11
+ const maxModelCatalogBytes = 4 * 1024 * 1024;
12
+ const maxModelCatalogEntries = 10_000;
11
13
  const streamReadsPerTaskYield = 32;
12
14
 
13
15
  function boundedString(value, name, maxBytes, required) {
@@ -58,6 +60,90 @@ function agentEnvironment(options) {
58
60
  };
59
61
  }
60
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
+
61
147
  function validWorkspacePath(path) {
62
148
  if (typeof path !== "string" || !path.startsWith("/") || path.includes("\0")) return false;
63
149
  if (strictDecoder.decode(encoder.encode(path)) !== path) return false;
@@ -1077,6 +1163,49 @@ export async function createFxAgent(options = {}) {
1077
1163
  const emit = (type, detail = {}) => {
1078
1164
  try { options.onEvent?.({ type, timestamp: performance.now(), ...detail }); } catch {}
1079
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
+ };
1080
1209
  const executeHostTool = async (name, input, requestedSessionId) => {
1081
1210
  const execute = hostTools.executors.get(name);
1082
1211
  const turn = requestedSessionId === undefined || requestedSessionId === sessionId
@@ -1100,6 +1229,7 @@ export async function createFxAgent(options = {}) {
1100
1229
  emit("runtime.start");
1101
1230
  const runtimeOptions = {
1102
1231
  ...options,
1232
+ fetch: transportFetch,
1103
1233
  args: ["acp"],
1104
1234
  env: agentEnvironment(options),
1105
1235
  hostToolExecutor: executeHostTool,
@@ -1275,6 +1405,7 @@ export async function createFxAgent(options = {}) {
1275
1405
  const turn = {
1276
1406
  push(update) { const waiter = waiters.shift(); if (waiter) waiter({ value: update, done: false }); else queue.push(update); },
1277
1407
  toolControllers,
1408
+ transportAttempts: 0,
1278
1409
  cancel() {
1279
1410
  if (finished || cancelled) return;
1280
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "libfx",
3
- "version": "0.0.7-dev.617.g49fc251a51aa",
3
+ "version": "0.0.7-dev.632.g9fe8a30c19a7",
4
4
  "description": "Embed fx agents and terminals in JavaScript hosts",
5
5
  "type": "module",
6
6
  "repository": {