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 +33 -1
- package/browser.js +2 -1
- package/fx-core.wasm +0 -0
- package/fx-sdk.js +131 -0
- package/fx-term.wasm +0 -0
- package/libfx.darwin-arm64.node +0 -0
- package/libfx.darwin-x64.node +0 -0
- package/libfx.linux-arm64.node +0 -0
- package/libfx.linux-x64.node +0 -0
- package/node.js +2 -1
- package/package.json +1 -1
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
|
package/libfx.darwin-arm64.node
CHANGED
|
Binary file
|
package/libfx.darwin-x64.node
CHANGED
|
Binary file
|
package/libfx.linux-arm64.node
CHANGED
|
Binary file
|
package/libfx.linux-x64.node
CHANGED
|
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;
|