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.
Files changed (3) hide show
  1. package/README.md +200 -106
  2. package/fx-term.wasm +0 -0
  3. 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 your terminal, built as a native addon and as WebAssembly.
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
- Sessions are durable. libfx records each step of a turn as it happens, so when a server crashes, a function hits its time limit or you redeploy in the middle of a reply, the next process picks the turn up where it stopped. On Vercel, that takes one route and one line of `vercel.json`.
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. Set `AI_GATEWAY_API_KEY` on your machine. On Vercel, libfx uses the deployment's OIDC token and there's no key to manage.
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 doesn't touch the network, read files or start processes, and `createFxAgent()` returns right away. Nothing loads until a session runs its first turn.
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
- ## Features
34
+ ## How it fits together
35
35
 
36
- - [Streams](https://fx.sh/docs/lib/prompts) text, reasoning and tool calls as the agent works
37
- - Runs your JavaScript functions as [tools](https://fx.sh/docs/lib/tools), along with [MCP](https://fx.sh/docs/lib/mcp) clients and [skills](https://fx.sh/docs/lib/skills)
38
- - Keeps [sessions](https://fx.sh/docs/lib/durability) going through crashes, deploys and function timeouts
39
- - Reconnects to a running turn from any server, starting at the last event you saw
40
- - [Steers](https://fx.sh/docs/lib/steering) a turn while it runs
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
- ## Sessions
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
- An agent is configuration. Create one per server and open a session per conversation:
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, from any process that creates the same agent:
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
- A session runs one turn at a time. A prompt that arrives while a turn is running waits behind it, even when it reaches a different server. A turn runs to the end whether or not anyone reads it; stop it with `session.cancel()` or the prompt's `signal`. `session.steer(text)` adds guidance to the running turn at its next model request.
61
-
62
- If a client might send the same request twice, give the prompt a `messageId`. A prompt with an id the session already accepted runs nothing and follows the original turn.
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. They apply to the turns that call starts:
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. Every line has a `cursor`. `session.stream(cursor)` replays everything from that point and stays open, so a client that loses its connection can reconnect to any server and carry on.
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
- const stream = agent.session(sessionId).stream(Number(params.get("cursor") ?? 0));
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 it reports the prompt as accepted, and it stores each step of a turn while the turn runs. If the process running a turn dies, the next process that gets work for that session continues the turn from its last step. The model is told the turn was interrupted, so it can check what happened before going on.
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, and libfx picks for you:
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()` | files in `$TMPDIR/libfx/sessions`, or `FX_SESSIONS_DIR` |
109
- | Browsers | `memory()` | the page's 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 survives the process
124
+ createFxAgent({ durability: memory() }); // nothing outlives the process
119
125
  ```
120
126
 
121
- To keep sessions in your own database, create a World such as Workflow's Postgres World and pass it to `world()` from `libfx/durable-world`. The [durability guide](https://fx.sh/docs/lib/durability) shows how.
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
- With `local()` and `vercel()`, a session id is a Workflow run id. Take it from `turn.accepted` or `await agent.newSessionId()` instead of making one up.
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. When a turn gets close to the function's time limit, libfx saves it and the next invocation continues it. When a function dies mid-turn, its session frees about 15 seconds after it last checked in, and the turn carries on in a new invocation. `vercel({ reserveMs, leaseMs })` from `libfx/durable-vercel` changes both timings.
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
- ### Tools with side effects
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
- When a process dies during a tool call, libfx can't know whether the call finished. Mark a tool `idempotent: true` if running it twice is harmless, like a lookup, and it runs again when the turn continues. 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.
190
+ ### Your own store
155
191
 
156
- Each call also gets an `executionId` that stays the same when the call runs again. Pass it to the service your tool calls as an idempotency key, and a repeated call won't change anything twice.
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, executionId, context }) {
222
+ async execute({ id }, { signal }) {
168
223
  return orders.get(id, { signal });
169
224
  },
170
225
  }],
171
226
  });
172
227
  ```
173
228
 
174
- `execute` gets a `signal` that aborts when the turn is cancelled, the call's `executionId`, and the session's `context`. Return any JSON value, or `{ type: "libfx.tool-result", text, images }` to return images.
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
- On the native backend, the calls from one model response run at the same time. Mark a tool `writes: true` to keep it from overlapping with others. Add `{ name: "web_search", providerExecuted: true }` for AI Gateway's web search, which runs at the provider instead of in your code.
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: "low", fast: true } });
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 forwards chat requests, pass the catalog yourself as `modelCatalog`, and the agent won't fetch it.
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
- `onEvent` receives diagnostics apart from the model's output: request timing, and session events such as `session.deadline` and `checkpoint.mismatch`. Credentials never appear in them.
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 instead of AI Gateway. Set `provider: "codex"` and hand libfx the user's ChatGPT credentials:
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()` before Codex requests, in whichever process runs the turn, and adds the credentials to those requests itself, so they never reach the engine or a saved session. It refreshes them 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.
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. 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()` and `pollDeviceSignIn()` show a short code to enter on OpenAI's site instead. Each step returns plain data, so it can finish in another request or function.
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
- An agent can also run on a user's Grok plan. Set `provider: "grok"` and pass the same `credentials` callback, which libfx calls with `provider: "grok"`:
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
- // Wait until the user enters the code. pollDeviceSignIn() checks once, for a later request.
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 like ChatGPT's: after the user signs in, xAI opens a page at `127.0.0.1` that doesn't load, and the user pastes its address back. 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.
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
- ### Custom providers
333
+ ### Several servers, one account
281
334
 
282
- An agent can also run on any server that speaks the OpenAI Chat Completions API, such as a local model server or a router. Pass the provider as an object and name the model it serves:
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
- const agent = createFxAgent({
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
- Requests go to `{baseUrl}/chat/completions`, which must use HTTPS, or HTTP on `localhost`. A server that needs a key gets it from the same `credentials` callback, which libfx calls with the provider's `name`:
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
- libfx sends the key as a bearer token to that chat URL only, and the key never reaches the engine or a saved session. A key isn't refreshed, so a rejected one ends the turn with the server's error. `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. Tool calls work when the model and server support them. In a browser, the server must accept cross-origin requests, and the page can read the key.
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
- ## One conversation, no sessions
365
+ ### Where credentials go
310
366
 
311
- `createFxEngine()` is the engine under the agent: one conversation in the memory of the process that runs it, with no queue and no session store. Use it for a single request, or to keep the conversation in storage of your own.
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
- const restored = await createFxEngine({ apiKey, model, checkpoint });
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
- Pass a `persistence` store instead, and the engine records the conversation as it runs, so a new engine can `resume()` a turn the last process left open. A store needs two methods and can add a third. The bytes are opaque, so it never has to understand them:
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 at once. `createMemoryPersistence()` is a reference store for tests. [Save and restore](https://fx.sh/docs/lib/checkpoints) has the details.
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 [WebAssembly](https://fx.sh/docs/lib/webassembly).
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`, `memory`, `listModels`, `getBackendInfo`, `createMemoryPersistence`, `FxFencedError` |
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` | `startSignIn`, `finishSignIn`, `startDeviceSignIn`, `pollDeviceSignIn`, `completeDeviceSignIn`, `refreshCredentials` |
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 validates and 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. Keep ChatGPT credentials, and the `signIn` object `startSignIn()` returns, on the server.
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "libfx",
3
- "version": "0.0.13-dev.1470.g5ec2519d98b9",
3
+ "version": "0.0.13-dev.1472.g2c7a4f83f06f",
4
4
  "description": "Embed fx agents and terminals in JavaScript hosts",
5
5
  "type": "module",
6
6
  "repository": {