libfx 0.0.13-dev.1470.g5ec2519d98b9 → 0.0.13-dev.1472.g2c7a4f83f06f
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 +200 -106
- package/fx-term.wasm +0 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# libfx
|
|
2
2
|
|
|
3
|
-
libfx is the agent from [fx](https://github.com/vercel-labs/fx), the open source coding agent from Vercel Labs, as a JavaScript library. You give it a model, instructions and your own tools, and it runs the conversation: streaming, tool calls, retries, steering and images. It's the same Zig engine that runs fx in
|
|
3
|
+
libfx is the agent from [fx](https://github.com/vercel-labs/fx), the open source coding agent from Vercel Labs, as a JavaScript library. You give it a model, instructions and your own tools, and it runs the conversation: the model loop, streaming, tool calls, retries, steering and images. It's the same Zig engine that runs fx in a terminal, built as a Node.js addon and as WebAssembly.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
The hard part of running an agent on a server is that the server goes away. A function reaches its time limit in the middle of a reply, a deploy replaces the process, or it crashes during a tool call. libfx stores every prompt before it accepts it and every step of a turn as the turn runs, so the next process that picks up the session continues the turn where it stopped. On Vercel, that takes one route and one line of `vercel.json`.
|
|
6
6
|
|
|
7
7
|
```sh
|
|
8
8
|
npm install libfx
|
|
@@ -25,43 +25,40 @@ for await (const event of turn) {
|
|
|
25
25
|
await agent.close();
|
|
26
26
|
```
|
|
27
27
|
|
|
28
|
-
Model calls go through [AI Gateway](https://vercel.com/docs/ai-gateway), so any model it lists works
|
|
28
|
+
Set `AI_GATEWAY_API_KEY` and run it with Node.js 20 or newer. Model calls go through [AI Gateway](https://vercel.com/docs/ai-gateway), so any model it lists works, and on Vercel libfx uses the deployment's OIDC token instead of a key. libfx can also run on a user's own ChatGPT or Grok plan, or on another server through the OpenAI Chat Completions API. See [Providers](#providers).
|
|
29
29
|
|
|
30
|
-
Importing libfx
|
|
30
|
+
Importing libfx and calling `createFxAgent()` touch no network, files or processes. The engine loads when a session runs its first turn.
|
|
31
31
|
|
|
32
32
|
The full documentation is at [fx.sh/docs/lib](https://fx.sh/docs/lib).
|
|
33
33
|
|
|
34
|
-
##
|
|
34
|
+
## How it fits together
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
- Takes [images](https://fx.sh/docs/lib/images) in prompts and tool results
|
|
42
|
-
- Gives each conversation its own model, instructions and context
|
|
43
|
-
- Runs on AI Gateway, on a user's own [ChatGPT](#chatgpt-plans) or [Grok](#grok-plans) plan, or on [any OpenAI-compatible server](#custom-providers)
|
|
44
|
-
- Uses a native addon on macOS and Linux, and WebAssembly everywhere else, including browsers
|
|
45
|
-
- Puts the [fx terminal](https://fx.sh/docs/lib/webassembly#embed-the-terminal) in a web page
|
|
46
|
-
- Ships TypeScript types for every entry point, with no runtime dependencies
|
|
36
|
+
| Object | What it is | How long it lives |
|
|
37
|
+
| --- | --- | --- |
|
|
38
|
+
| Agent | Configuration: model, instructions, tools, provider and where sessions live | One per server process |
|
|
39
|
+
| Session | One conversation | As long as its store keeps it |
|
|
40
|
+
| Turn | One prompt and everything the agent does to answer it | Until it ends with a stop reason |
|
|
47
41
|
|
|
48
|
-
|
|
42
|
+
An agent is configuration, and its conversations live in its sessions. Create it once per process with the same options on every server, and open a session for each conversation. Any process with that agent can run, continue or stream any of its sessions.
|
|
43
|
+
|
|
44
|
+
Underneath the agent is `createFxEngine()`, a single conversation with no store, for when you don't want sessions. See [Without sessions](#without-sessions).
|
|
49
45
|
|
|
50
|
-
|
|
46
|
+
## Sessions
|
|
51
47
|
|
|
52
48
|
```js
|
|
53
49
|
const turn = agent.session().prompt("Plan the migration.");
|
|
54
|
-
const { sessionId } = await turn.accepted;
|
|
50
|
+
const { sessionId } = await turn.accepted; // the prompt is stored
|
|
55
51
|
|
|
56
|
-
// Later,
|
|
52
|
+
// Later, in any process that creates the same agent:
|
|
57
53
|
agent.session(sessionId).prompt("Start with the schema.");
|
|
58
54
|
```
|
|
59
55
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
56
|
+
- **One turn at a time.** A prompt sent while a turn runs waits behind it, even when it reaches a different server.
|
|
57
|
+
- **Turns finish on their own.** A turn runs to the end whether or not anyone reads its events. Stop it with `session.cancel()`, or with the `signal` you passed to `prompt()`.
|
|
58
|
+
- **Steering.** `session.steer(text)` adds guidance to the running turn at its next model request. When no turn is running, the text becomes the next turn.
|
|
59
|
+
- **Retries are safe.** Give a prompt a `messageId`, and a repeated request with the same id runs nothing new. It follows the original turn, and its result has `repeated: true`.
|
|
63
60
|
|
|
64
|
-
Each conversation can bring its own settings
|
|
61
|
+
Each conversation can bring its own settings, which apply to the turns that session object starts:
|
|
65
62
|
|
|
66
63
|
```js
|
|
67
64
|
agent.session(sessionId, {
|
|
@@ -71,9 +68,17 @@ agent.session(sessionId, {
|
|
|
71
68
|
});
|
|
72
69
|
```
|
|
73
70
|
|
|
71
|
+
A turn's result says how it ended:
|
|
72
|
+
|
|
73
|
+
```js
|
|
74
|
+
const { stopReason, usage, error } = await turn.result;
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
`stopReason` is `end_turn`, `max_output_tokens`, `max_model_turns`, `refused` or `cancelled`. It's `error` when the turn failed, with the reason in `error`, and `unknown` when the process running it stopped before recording how it ended.
|
|
78
|
+
|
|
74
79
|
### Streaming to a browser
|
|
75
80
|
|
|
76
|
-
`turn.readable` is the turn as newline-delimited JSON, ready to return from a route.
|
|
81
|
+
`turn.readable` is the turn as newline-delimited JSON, ready to return from a route. `session.stream(cursor)` returns every line after `cursor` and stays open for new ones, so a client that loses its connection reconnects to any server and carries on.
|
|
77
82
|
|
|
78
83
|
```js
|
|
79
84
|
// app/api/chat/route.js
|
|
@@ -91,22 +96,23 @@ export async function GET(request) {
|
|
|
91
96
|
const sessionId = params.get("sessionId");
|
|
92
97
|
if (!sessionId) return new Response("sessionId is required", { status: 400 });
|
|
93
98
|
// Same check as POST.
|
|
94
|
-
|
|
95
|
-
return new Response(stream);
|
|
99
|
+
return new Response(agent.session(sessionId).stream(Number(params.get("cursor") ?? 0)));
|
|
96
100
|
}
|
|
97
101
|
```
|
|
98
102
|
|
|
103
|
+
Each line is one JSON object with a `type`: the model's events, such as `text_delta` and `tool_start`, and the turn's own, `turn_start` (which carries the `sessionId`), `turn_end`, `turn_resume` and `turn_yield`. Lines from `session.stream()` carry a `cursor`. Store the last one you read and pass it back to resume after it. The `FxStreamLine` type lists every field.
|
|
104
|
+
|
|
99
105
|
## Durability
|
|
100
106
|
|
|
101
|
-
libfx stores a prompt before
|
|
107
|
+
libfx stores a prompt before `turn.accepted` resolves, and stores each step of a turn while the turn runs. If the process running a turn dies, the next process that gets work for the session continues the turn from its last stored step. The model is told the turn was interrupted, so it can check what happened before going on.
|
|
102
108
|
|
|
103
|
-
Where sessions live depends on where your code runs
|
|
109
|
+
Where sessions live depends on where your code runs. libfx picks for you:
|
|
104
110
|
|
|
105
111
|
| Where | Durability | Sessions live in |
|
|
106
112
|
| --- | --- | --- |
|
|
107
|
-
| Vercel | `vercel()` | Vercel's [Workflow World](https://workflow-sdk.dev/worlds) |
|
|
108
|
-
| Node.js | `local()` |
|
|
109
|
-
| Browsers | `memory()` |
|
|
113
|
+
| Vercel | `vercel()` | Vercel's [Workflow World](https://workflow-sdk.dev/worlds), the store and queue behind Vercel Workflow |
|
|
114
|
+
| Node.js | `local()` | Files in `FX_SESSIONS_DIR`, or in `libfx/sessions` under the system's temporary directory |
|
|
115
|
+
| Browsers | `memory()` | The page's memory |
|
|
110
116
|
|
|
111
117
|
To choose yourself, pass `durability`:
|
|
112
118
|
|
|
@@ -115,14 +121,12 @@ import { createFxAgent, memory } from "libfx";
|
|
|
115
121
|
import { local } from "libfx/durable-local";
|
|
116
122
|
|
|
117
123
|
createFxAgent({ durability: local({ dir: ".fx/sessions" }) });
|
|
118
|
-
createFxAgent({ durability: memory() }); // nothing
|
|
124
|
+
createFxAgent({ durability: memory() }); // nothing outlives the process
|
|
119
125
|
```
|
|
120
126
|
|
|
121
|
-
|
|
127
|
+
On `local()`, `vercel()` and `world()`, a session id is a Workflow run id, `wrun_` followed by letters and digits. Take it from `turn.accepted` or `await agent.newSessionId()` instead of making one up. On `memory()`, any 1 to 128 letters, digits, `.`, `_` or `-` work.
|
|
122
128
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
A session stores only the conversation. Every process passes its own model, credentials, tools, MCP clients and skills when it creates the agent.
|
|
129
|
+
A session stores the conversation and nothing else. Every process passes its own model, credentials, tools, MCP clients and skills when it creates the agent. If a session's saved checkpoint came from a different libfx version, tool set or model, the session still loads, and `onEvent` gets a `checkpoint.mismatch` event naming what changed.
|
|
126
130
|
|
|
127
131
|
### On Vercel
|
|
128
132
|
|
|
@@ -147,13 +151,64 @@ Then subscribe it to libfx's topic in `vercel.json`:
|
|
|
147
151
|
}
|
|
148
152
|
```
|
|
149
153
|
|
|
150
|
-
That's all. `prompt()` sends each turn to Vercel Queues, and this route runs it
|
|
154
|
+
That's all. `prompt()` sends each turn to Vercel Queues, and this route runs it:
|
|
155
|
+
|
|
156
|
+
- When a turn gets within 30 seconds of the function's time limit, libfx stops it, and the next invocation continues it.
|
|
157
|
+
- The function running a turn renews a 15-second lease on its session. If the function dies, the session frees when the lease runs out, and the turn continues in a new invocation.
|
|
158
|
+
- A function that only stalled and wakes up after that is refused at its next write, so two processes never write one session.
|
|
159
|
+
|
|
160
|
+
`vercel({ reserveMs, leaseMs })` from `libfx/durable-vercel` changes both timings.
|
|
161
|
+
|
|
162
|
+
### Tools that change things
|
|
163
|
+
|
|
164
|
+
When a process dies during a tool call, libfx can't know whether the call finished. So each tool says whether it's safe to run twice:
|
|
165
|
+
|
|
166
|
+
```js
|
|
167
|
+
const byId = { type: "object", properties: { id: { type: "string" } }, required: ["id"] };
|
|
168
|
+
|
|
169
|
+
const tools = [
|
|
170
|
+
{
|
|
171
|
+
name: "get_order",
|
|
172
|
+
description: "Look up an order.",
|
|
173
|
+
inputSchema: byId,
|
|
174
|
+
idempotent: true, // a lookup is harmless to repeat
|
|
175
|
+
execute: ({ id }) => orders.get(id),
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
name: "refund_order",
|
|
179
|
+
description: "Refund an order.",
|
|
180
|
+
inputSchema: byId,
|
|
181
|
+
execute: ({ id }, { executionId }) => payments.refund(id, { idempotencyKey: executionId }),
|
|
182
|
+
},
|
|
183
|
+
];
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
When the turn continues, a call to an `idempotent: true` tool runs again. A call to any other tool never runs twice on its own: the model gets an error saying the call may have partly run, and decides whether to check, try again or ask the user.
|
|
151
187
|
|
|
152
|
-
|
|
188
|
+
Each call's `executionId` stays the same when it runs again. Pass it to the service your tool calls as an idempotency key, and a service that honors idempotency keys won't refund twice.
|
|
153
189
|
|
|
154
|
-
|
|
190
|
+
### Your own store
|
|
155
191
|
|
|
156
|
-
|
|
192
|
+
`world()` from `libfx/durable-world` keeps sessions in a [Workflow World](https://workflow-sdk.dev/worlds) you create, such as Workflow's Postgres World:
|
|
193
|
+
|
|
194
|
+
```js
|
|
195
|
+
import { createWorld } from "@workflow/world-postgres";
|
|
196
|
+
import { createFxAgent } from "libfx";
|
|
197
|
+
import { world } from "libfx/durable-world";
|
|
198
|
+
|
|
199
|
+
const sessions = createWorld({
|
|
200
|
+
connectionString: process.env.DATABASE_URL,
|
|
201
|
+
namespace: "libfx",
|
|
202
|
+
jobPrefix: "libfx_",
|
|
203
|
+
});
|
|
204
|
+
await sessions.start();
|
|
205
|
+
|
|
206
|
+
export const agent = createFxAgent({
|
|
207
|
+
durability: world(sessions, { name: "postgres", queueDurable: true }),
|
|
208
|
+
});
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
A World that delivers queue messages over HTTP needs `agent.wakeHandler()` at the address it posts to. Workflow's Postgres World 5.0.2 posts every message to `/.well-known/workflow/v1/flow` on your app. `world()` takes options for what libfx can't learn from the World itself, such as how long a lease lasts and whether the queue keeps a message across crashes. The `WorldOptions` type documents each one.
|
|
157
212
|
|
|
158
213
|
## Tools
|
|
159
214
|
|
|
@@ -164,16 +219,29 @@ const agent = createFxAgent({
|
|
|
164
219
|
description: "Look up an order by id.",
|
|
165
220
|
inputSchema: { type: "object", properties: { id: { type: "string" } }, required: ["id"] },
|
|
166
221
|
idempotent: true,
|
|
167
|
-
async execute({ id }, { signal
|
|
222
|
+
async execute({ id }, { signal }) {
|
|
168
223
|
return orders.get(id, { signal });
|
|
169
224
|
},
|
|
170
225
|
}],
|
|
171
226
|
});
|
|
172
227
|
```
|
|
173
228
|
|
|
174
|
-
`execute`
|
|
229
|
+
`execute` receives the model's input and a context:
|
|
230
|
+
|
|
231
|
+
| Field | What it is |
|
|
232
|
+
| --- | --- |
|
|
233
|
+
| `signal` | Aborts when the turn is cancelled |
|
|
234
|
+
| `executionId` | The call's id, the same if the call runs again after a crash |
|
|
235
|
+
| `sessionId` | The session the call belongs to |
|
|
236
|
+
| `context` | The JSON passed as `context` to `agent.session()` |
|
|
237
|
+
|
|
238
|
+
Return a string to send it as is, or any other value to send it as JSON. A thrown error reaches the model as a failed tool result. To return images, return `{ type: "libfx.tool-result", text, images }`, with base64 image data.
|
|
239
|
+
|
|
240
|
+
On the native backend, the calls from one model response run at the same time, up to 16 at once. Mark a tool `writes: true` to keep its calls from overlapping any other. On WebAssembly, calls run one after another.
|
|
175
241
|
|
|
176
|
-
|
|
242
|
+
Validate the input and authorize the action inside `execute`. libfx orders the calls but grants nothing itself.
|
|
243
|
+
|
|
244
|
+
`tools` can also be an object keyed by tool name. Add `{ name: "web_search", providerExecuted: true }` for AI Gateway's web search, which runs at the provider instead of in your code.
|
|
177
245
|
|
|
178
246
|
### MCP and skills
|
|
179
247
|
|
|
@@ -195,23 +263,27 @@ await agent.close();
|
|
|
195
263
|
await mcp.close();
|
|
196
264
|
```
|
|
197
265
|
|
|
198
|
-
libfx doesn't own the MCP connection. You create the client, handle its auth and close it after the agent.
|
|
266
|
+
libfx doesn't own the MCP connection. You create the client, handle its auth and close it after the agent. `loadSkillFile()` reads one `SKILL.md` and nothing it references.
|
|
199
267
|
|
|
200
268
|
## Models
|
|
201
269
|
|
|
202
270
|
```js
|
|
203
|
-
createFxAgent({ model: { id: "anthropic/claude-opus-5.5", effort: "
|
|
271
|
+
createFxAgent({ model: { id: "anthropic/claude-opus-5.5", effort: "high", fast: true } });
|
|
204
272
|
```
|
|
205
273
|
|
|
206
274
|
`effort` and `fast` mean the same as the fx CLI's `--effort` and `--fast` flags. `ultrafast: true` asks for the faster, more expensive service tier on models that offer it. A setting the model doesn't support fails with an error whose `code` names it, such as `LIBFX_MODEL_UNSUPPORTED_EFFORT`.
|
|
207
275
|
|
|
208
|
-
`listModels({ apiKey })` returns the Gateway's language models. Behind a proxy that only
|
|
276
|
+
`listModels({ apiKey })` returns the ids of AI Gateway's language models. Behind a proxy that forwards only chat requests, pass the catalog yourself as `modelCatalog`, and the agent won't fetch it.
|
|
277
|
+
|
|
278
|
+
## Providers
|
|
279
|
+
|
|
280
|
+
### AI Gateway
|
|
209
281
|
|
|
210
|
-
|
|
282
|
+
AI Gateway is the default. libfx uses the first credential it finds: the `apiKey` option, then `AI_GATEWAY_API_KEY`, then on Vercel the deployment's OIDC token.
|
|
211
283
|
|
|
212
284
|
### ChatGPT plans
|
|
213
285
|
|
|
214
|
-
An agent can run on a user's ChatGPT plan through Codex
|
|
286
|
+
An agent can run on a user's ChatGPT plan through Codex. Set `provider: "codex"` and give libfx the user's credentials:
|
|
215
287
|
|
|
216
288
|
```js
|
|
217
289
|
const agent = createFxAgent({
|
|
@@ -222,15 +294,7 @@ const agent = createFxAgent({
|
|
|
222
294
|
});
|
|
223
295
|
```
|
|
224
296
|
|
|
225
|
-
libfx calls `credentials()`
|
|
226
|
-
|
|
227
|
-
Sessions in one process share each refresh. If your app runs on several servers, two of them can notice the same expiring credentials at once, and the provider accepts only the first refresh. Pass `refreshLock` so they take turns:
|
|
228
|
-
|
|
229
|
-
```js
|
|
230
|
-
refreshLock: ({ provider, sessionId }, refresh) => locks.withLock(`${provider}:${accountOf(sessionId)}`, refresh),
|
|
231
|
-
```
|
|
232
|
-
|
|
233
|
-
libfx calls it with a function to run while you hold a lock on that user's account, for example in Redis. Inside it, libfx asks `credentials()` again and uses what another server already saved. This works for Grok too.
|
|
297
|
+
libfx calls `credentials()` in whichever process runs the turn. It refreshes the credentials shortly before they expire and once after a rejected request, then passes the new ones to `onCredentials` for you to save. Codex doesn't accept requests from web pages, so these turns run on your server.
|
|
234
298
|
|
|
235
299
|
To sign a user in, use `libfx/chatgpt`:
|
|
236
300
|
|
|
@@ -240,28 +304,17 @@ import { startSignIn, finishSignIn } from "libfx/chatgpt";
|
|
|
240
304
|
// One request: send the user to signIn.url and keep signIn on the server.
|
|
241
305
|
const signIn = await startSignIn();
|
|
242
306
|
|
|
243
|
-
// A later request: the user pastes the address OpenAI opened.
|
|
307
|
+
// A later request: the user pastes back the address OpenAI opened.
|
|
244
308
|
const credentials = await finishSignIn(signIn, pastedAddress);
|
|
245
309
|
```
|
|
246
310
|
|
|
247
|
-
After the user signs in, OpenAI opens a page at `localhost:1455` that doesn't load. That's expected
|
|
311
|
+
After the user signs in, OpenAI opens a page at `localhost:1455` that doesn't load. That's expected: ask the user to copy the page's whole address and paste it back. If their account or ChatGPT workspace allows device code sign-in, `startDeviceSignIn()` returns a short code to enter on OpenAI's site instead, and `pollDeviceSignIn()` or `completeDeviceSignIn()` waits for it. Each step returns plain data, so a later request or function can finish it.
|
|
248
312
|
|
|
249
313
|
The sign-in uses the same OpenAI client as the Codex CLI, and the user's ChatGPT plan and OpenAI's terms apply to the turns.
|
|
250
314
|
|
|
251
315
|
### Grok plans
|
|
252
316
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
```js
|
|
256
|
-
const agent = createFxAgent({
|
|
257
|
-
provider: "grok",
|
|
258
|
-
model: "grok-4.5",
|
|
259
|
-
credentials: ({ provider, sessionId }) => tokens.load(provider, sessionId),
|
|
260
|
-
onCredentials: (credentials, { provider, sessionId }) => tokens.save(provider, sessionId, credentials),
|
|
261
|
-
});
|
|
262
|
-
```
|
|
263
|
-
|
|
264
|
-
libfx handles Grok credentials the same way as Codex's: it adds them to Grok requests itself, refreshes them, and checks that a refreshed token still belongs to the same xAI account. Grok doesn't accept requests from web pages either, so these turns run on your server.
|
|
317
|
+
`provider: "grok"` runs on a user's Grok plan, with the same `credentials` and `onCredentials` callbacks, called with `provider: "grok"`. libfx also checks that refreshed credentials still belong to the same xAI account. These turns run on your server too.
|
|
265
318
|
|
|
266
319
|
To sign a user in, use `libfx/grok`. Device code is the simplest:
|
|
267
320
|
|
|
@@ -271,44 +324,66 @@ import { startDeviceSignIn, completeDeviceSignIn } from "libfx/grok";
|
|
|
271
324
|
// Show device.userCode and a link to device.verificationUrl.
|
|
272
325
|
const device = await startDeviceSignIn();
|
|
273
326
|
|
|
274
|
-
//
|
|
327
|
+
// Waits until the user enters the code. pollDeviceSignIn() checks once, for a later request.
|
|
275
328
|
const credentials = await completeDeviceSignIn(device);
|
|
276
329
|
```
|
|
277
330
|
|
|
278
|
-
`startSignIn()` and `finishSignIn()` work
|
|
331
|
+
`startSignIn()` and `finishSignIn()` work as for ChatGPT, except that xAI opens a page at `127.0.0.1`. The sign-in uses the same xAI client as the fx CLI, and the user's Grok plan and xAI's terms apply to the turns.
|
|
279
332
|
|
|
280
|
-
###
|
|
333
|
+
### Several servers, one account
|
|
281
334
|
|
|
282
|
-
|
|
335
|
+
The sessions in one process share each refresh. Across servers, two can notice the same expiring credentials at once, and the provider accepts only the first refresh. Pass `refreshLock` so they take turns:
|
|
283
336
|
|
|
284
337
|
```js
|
|
285
|
-
|
|
286
|
-
provider: { name: "local", baseUrl: "http://localhost:11434/v1" },
|
|
287
|
-
model: "qwen3:8b",
|
|
288
|
-
});
|
|
338
|
+
refreshLock: ({ provider, sessionId }, refresh) => locks.withLock(`${provider}:${accountOf(sessionId)}`, refresh),
|
|
289
339
|
```
|
|
290
340
|
|
|
291
|
-
|
|
341
|
+
libfx calls it with a function to run while you hold a lock on that user's account, for example in Redis. Inside it, libfx calls `credentials()` again and uses what another server already saved.
|
|
342
|
+
|
|
343
|
+
### Custom providers
|
|
344
|
+
|
|
345
|
+
An agent can run on a server that speaks the OpenAI Chat Completions API, such as a local model server or a router:
|
|
292
346
|
|
|
293
347
|
```js
|
|
348
|
+
const router = {
|
|
349
|
+
name: "router",
|
|
350
|
+
baseUrl: "https://openrouter.ai/api/v1",
|
|
351
|
+
models: { "qwen/qwen3-coder": { contextWindow: 128000, supportsToolUse: true } },
|
|
352
|
+
};
|
|
353
|
+
|
|
294
354
|
const agent = createFxAgent({
|
|
295
|
-
provider:
|
|
296
|
-
name: "router",
|
|
297
|
-
baseUrl: "https://openrouter.ai/api/v1",
|
|
298
|
-
models: { "qwen/qwen3-coder": { contextWindow: 128000, supportsToolUse: true } },
|
|
299
|
-
},
|
|
355
|
+
provider: router,
|
|
300
356
|
model: "qwen/qwen3-coder",
|
|
301
357
|
credentials: async ({ provider, sessionId }) => ({ apiKey: await keys.load(provider, sessionId) }),
|
|
302
358
|
});
|
|
303
359
|
```
|
|
304
360
|
|
|
305
|
-
|
|
361
|
+
Requests go to `{baseUrl}/chat/completions`, which must use HTTPS, or HTTP on `localhost`. Leave out `credentials` for a server that needs no key. `models` tells libfx what it can't ask the server: each model's context window and output limit, and whether it takes tools and images. A key isn't refreshed, so a rejected one ends the turn with the server's error. In a browser, the server must accept cross-origin requests, and the page can read the key.
|
|
306
362
|
|
|
307
363
|
Saved sessions follow the agent's provider: point an agent at another base URL and its sessions continue there, history included.
|
|
308
364
|
|
|
309
|
-
|
|
365
|
+
### Where credentials go
|
|
310
366
|
|
|
311
|
-
|
|
367
|
+
libfx adds ChatGPT, Grok and custom-provider credentials to each request in JavaScript, just before it's sent, and sends them only to that provider. They never reach the engine. No credential is written to a saved session or passed to `onEvent`, and if a server quotes a key or token back in an error, libfx masks it before the error reaches the turn.
|
|
368
|
+
|
|
369
|
+
## Images
|
|
370
|
+
|
|
371
|
+
```js
|
|
372
|
+
agent.session().prompt([
|
|
373
|
+
{ type: "text", text: "What's wrong with this chart?" },
|
|
374
|
+
{ type: "image", mimeType: "image/png", data: base64Png },
|
|
375
|
+
]);
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
A session stores each prompt as JSON before it runs, so a session prompt takes image data as a base64 string. `createFxEngine()` also takes bytes, a `Blob` or a `File`. Each prompt takes up to 8 PNG, JPEG, GIF or WebP images, at most 3.75 MiB each and 6 MiB together. Pass `resizeImage` to downscale or convert prompt images before they're sent.
|
|
379
|
+
|
|
380
|
+
## Diagnostics
|
|
381
|
+
|
|
382
|
+
`onEvent` receives diagnostics apart from the model's output: request timing and retries, and session events such as `session.deadline` when a turn stops for a function's time limit. The `FxAgentEvent` type lists them all.
|
|
383
|
+
|
|
384
|
+
## Without sessions
|
|
385
|
+
|
|
386
|
+
`createFxEngine()` is the engine under the agent: one conversation in the memory of one process, with no queue and no session store. Use it for a single request, a subagent inside a tool call, or a conversation you store yourself.
|
|
312
387
|
|
|
313
388
|
```js
|
|
314
389
|
import { createFxEngine } from "libfx";
|
|
@@ -317,14 +392,22 @@ const engine = await createFxEngine({ apiKey: process.env.AI_GATEWAY_API_KEY, mo
|
|
|
317
392
|
|
|
318
393
|
const turn = engine.prompt("Summarize this file.");
|
|
319
394
|
for await (const event of turn) {
|
|
320
|
-
|
|
395
|
+
if (event.type === "text_delta") process.stdout.write(event.delta);
|
|
321
396
|
}
|
|
322
397
|
|
|
323
|
-
const checkpoint = await engine.checkpoint(); // opaque bytes
|
|
324
|
-
|
|
398
|
+
const checkpoint = await engine.checkpoint(); // opaque bytes, up to 4 MiB
|
|
399
|
+
await engine.close();
|
|
400
|
+
|
|
401
|
+
const restored = await createFxEngine({ apiKey: process.env.AI_GATEWAY_API_KEY, model: "anthropic/claude-haiku-5.5", checkpoint });
|
|
325
402
|
```
|
|
326
403
|
|
|
327
|
-
|
|
404
|
+
An engine differs from a session:
|
|
405
|
+
|
|
406
|
+
- On AI Gateway, it needs an `apiKey`.
|
|
407
|
+
- One prompt runs at a time, and breaking out of a turn's events cancels the turn.
|
|
408
|
+
- The turn itself has `cancel()` and `steer()`.
|
|
409
|
+
|
|
410
|
+
To have the engine record the conversation as it runs, pass a `persistence` store instead of a checkpoint. A new engine on the same store can then `resume()` a turn the last process left open. A store needs two methods and can add a third, and the bytes are opaque, so it never has to understand them:
|
|
328
411
|
|
|
329
412
|
```ts
|
|
330
413
|
interface Persistence {
|
|
@@ -342,11 +425,7 @@ interface JournalRecord {
|
|
|
342
425
|
}
|
|
343
426
|
```
|
|
344
427
|
|
|
345
|
-
Reject an `append()` whose `expected` isn't the last stored cursor with `FxFencedError`, so two processes can't write the same conversation
|
|
346
|
-
|
|
347
|
-
### Upgrading from 0.0.13
|
|
348
|
-
|
|
349
|
-
In libfx 0.0.13 and earlier, `createFxAgent()` created this engine. It now creates the durable agent, so rename those calls to `createFxEngine()`. Old code keeps running against the agent, with a few differences: breaking out of a turn's events no longer cancels it, turns have no `cancel()` or `steer()` (the session does), and `persistence` and `sessionId` throw because the durability holds the session.
|
|
428
|
+
Reject an `append()` whose `expected` isn't the last stored cursor with `FxFencedError`, so two processes can't write the same conversation. `createMemoryPersistence()` is a reference store for tests. When a resumed turn left a tool call running, `resume({ onAmbiguous })` lets you return `"rerun"` to run it again under the same `executionId`.
|
|
350
429
|
|
|
351
430
|
## Runtimes
|
|
352
431
|
|
|
@@ -357,7 +436,7 @@ In libfx 0.0.13 and earlier, `createFxAgent()` created this engine. It now creat
|
|
|
357
436
|
| Browsers | WebAssembly with JSPI, from `libfx/browser` |
|
|
358
437
|
| Bun | The same as Node.js. For WebAssembly, use Bun 1.4.2 or newer. |
|
|
359
438
|
|
|
360
|
-
The default `backend: "auto"` tries native first. Pass `"native"` or `"wasm"` to require one, and call `getBackendInfo()` to see what would load without creating anything.
|
|
439
|
+
The default `backend: "auto"` tries the native addon first. Pass `"native"` or `"wasm"` to require one, and call `getBackendInfo()` to see what would load without creating anything.
|
|
361
440
|
|
|
362
441
|
Next.js works in Node.js route handlers, with webpack or Turbopack, and doesn't need `serverExternalPackages`. CommonJS apps can `require("libfx")`. With TypeScript's `moduleResolution: "bundler"`, add `"node"` to `customConditions` to get the Node.js types.
|
|
363
442
|
|
|
@@ -374,25 +453,40 @@ const terminal = await createFxTerminal({
|
|
|
374
453
|
await terminal.interactive;
|
|
375
454
|
```
|
|
376
455
|
|
|
377
|
-
This is the fx CLI's interface compiled to WebAssembly. Your page supplies storage for its sessions, settings and sign-in, and can give it a workspace that runs commands. See [
|
|
456
|
+
This is the fx CLI's interface compiled to WebAssembly. Your page supplies storage for its sessions, settings and sign-in, and can give it a workspace that runs commands. See [Terminal](https://fx.sh/docs/lib/terminal).
|
|
378
457
|
|
|
379
458
|
## Entry points
|
|
380
459
|
|
|
381
460
|
| Import | What's in it |
|
|
382
461
|
| --- | --- |
|
|
383
|
-
| `libfx` | `createFxAgent`, `createFxEngine`, `
|
|
384
|
-
| `libfx/node` | The same, for Node.js explicitly |
|
|
385
|
-
| `libfx/browser` | The browser build, plus `createFxTerminal` and `xtermAdapter` |
|
|
462
|
+
| `libfx` | `createFxAgent`, `createFxEngine`, `createFxTerminal`, `memory`, `listModels`, `createMemoryPersistence`, `FxFencedError`, `FxJournalVersionError`, `supportsJspi`, `xtermAdapter`, `encodeXtermKeyEvent`, and on Node.js `getBackendInfo` |
|
|
463
|
+
| `libfx/node`, `libfx/browser` | The same, for Node.js or browsers explicitly |
|
|
386
464
|
| `libfx/wasm` | The WebAssembly SDK without packaged asset paths, so you pass `wasm` yourself |
|
|
387
465
|
| `libfx/durable-local`, `libfx/durable-vercel`, `libfx/durable-world` | `local()`, `vercel()` and `world()` |
|
|
388
|
-
| `libfx/chatgpt` |
|
|
389
|
-
| `libfx/grok` | `startSignIn`, `finishSignIn`, `startDeviceSignIn`, `pollDeviceSignIn`, `completeDeviceSignIn`, `refreshCredentials` |
|
|
466
|
+
| `libfx/chatgpt`, `libfx/grok` | Sign-in for ChatGPT and Grok plans, and `refreshCredentials` |
|
|
390
467
|
| `libfx/mcp` | `createMcpAdapter` |
|
|
391
468
|
| `libfx/skills`, `libfx/skills/node` | `createSkillsAdapter`, and `loadSkillFile` for Node.js and Bun |
|
|
392
469
|
|
|
470
|
+
Every entry point ships TypeScript types, and the package has no runtime dependencies.
|
|
471
|
+
|
|
472
|
+
## For coding agents
|
|
473
|
+
|
|
474
|
+
- **Docs as Markdown:** [fx.sh/llms.txt](https://fx.sh/llms.txt) indexes every page, and each page has a `.md` version, such as `https://fx.sh/docs/lib/durability.md`. The declarations in `types/` are the exact API.
|
|
475
|
+
- **One agent per process:** create it once at module scope, not per request, and open `agent.session(id)` for each conversation.
|
|
476
|
+
- **Session ids:** take them from `turn.accepted` or `agent.newSessionId()`. On every durability except `memory()`, an id that isn't a Workflow run id throws.
|
|
477
|
+
- **Side effects:** set `idempotent: true` only on tools that are safe to run twice, and pass `executionId` to services that take an idempotency key.
|
|
478
|
+
- **Deployments:** on Vercel, the `wakeHandler()` route and its `vercel.json` trigger are both required, or prompts are stored and never run.
|
|
479
|
+
- **Credentials:** keep keys and ChatGPT or Grok credentials on the server, and the object `startSignIn()` returns too.
|
|
480
|
+
- **Checking a setup:** `getBackendInfo()` reports which backend would load without creating anything or making requests.
|
|
481
|
+
- **Changing libfx:** the source is in the [fx repository](https://github.com/vercel-labs/fx) under `sdk/`. Its `AGENTS.md` files name the tests each change needs.
|
|
482
|
+
|
|
483
|
+
## Upgrading from 0.0.13
|
|
484
|
+
|
|
485
|
+
In libfx 0.0.13 and earlier, `createFxAgent()` created the engine. It now creates the durable agent, so rename those calls to `createFxEngine()`. Old code keeps running against the agent, with a few differences: breaking out of a turn's events no longer cancels it, turns have no `cancel()` or `steer()` (the session does), and `persistence` and `sessionId` throw because the durability holds the session.
|
|
486
|
+
|
|
393
487
|
## Security
|
|
394
488
|
|
|
395
|
-
Your tools, MCP clients and skill loaders have whatever access your code gives them. libfx
|
|
489
|
+
Your tools, MCP clients and skill loaders have whatever access your code gives them. libfx orders their calls but grants nothing itself, and the native backend doesn't turn on fx's built-in shell or file tools. Don't put long-lived keys in browser code, and treat the `nativeAddon` and `gatewayChatUrl` options as trusted configuration.
|
|
396
490
|
|
|
397
491
|
## Contributing
|
|
398
492
|
|
package/fx-term.wasm
CHANGED
|
Binary file
|