libfx 0.0.13-dev.1415.g607b1632fff6 → 0.0.13-dev.1431.g1df91f0b4ebf
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 +624 -43
- package/browser.js +17 -3
- package/durable/local.mjs +190 -0
- package/durable/vercel.mjs +268 -0
- package/durable/world.mjs +1 -0
- package/durable.js +2063 -0
- package/fx-core.wasm +0 -0
- package/fx-harness.js +198 -0
- package/fx-sdk.js +989 -86
- 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.cjs +2990 -158
- package/node.js +46 -4
- package/package.json +58 -13
- package/types/browser.d.ts +125 -0
- package/types/durable-local.d.ts +18 -0
- package/types/durable-vercel.d.ts +16 -0
- package/types/durable-world.d.ts +73 -0
- package/types/libfx.d.cts +782 -0
- package/types/mcp.d.ts +40 -0
- package/types/node.d.cts +143 -0
- package/types/node.d.ts +3 -0
- package/types/skills-node.d.ts +25 -0
- package/types/skills.d.ts +27 -0
- package/types/wasm.d.ts +91 -0
package/README.md
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
# libfx
|
|
2
2
|
|
|
3
|
-
`libfx` is the
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
`libfx` is the fx agent kernel for JavaScript hosts. You create an agent once
|
|
4
|
+
and open sessions on it. libfx saves each session as it runs, so a crash, a
|
|
5
|
+
function timeout, or a redeploy continues the conversation instead of losing
|
|
6
|
+
it. The same code runs on your machine and on Vercel.
|
|
6
7
|
|
|
7
8
|
```sh
|
|
8
9
|
npm install libfx
|
|
@@ -13,45 +14,339 @@ Browsers use WebAssembly with JSPI. The default package has no runtime
|
|
|
13
14
|
dependencies and performs no MCP connection, skill scan, process spawn, or
|
|
14
15
|
filesystem read when imported.
|
|
15
16
|
|
|
17
|
+
libfx ships TypeScript declarations for every entry point. Under
|
|
18
|
+
`moduleResolution: "bundler"`, TypeScript reads the browser declarations for
|
|
19
|
+
`libfx` unless `customConditions` includes `"node"`.
|
|
20
|
+
|
|
16
21
|
## Agent
|
|
17
22
|
|
|
18
23
|
```js
|
|
19
24
|
import { createFxAgent } from "libfx";
|
|
20
25
|
|
|
21
|
-
const agent =
|
|
22
|
-
apiKey: process.env.AI_GATEWAY_API_KEY,
|
|
26
|
+
const agent = createFxAgent({
|
|
23
27
|
model: "google/gemini-2.5-flash-lite",
|
|
24
|
-
|
|
25
|
-
if (event.type === "transport.response") console.log(event.elapsedMs);
|
|
26
|
-
},
|
|
28
|
+
instructions: "Answer in one paragraph.",
|
|
27
29
|
});
|
|
28
30
|
|
|
29
|
-
const turn = agent.prompt("Explain this project.");
|
|
31
|
+
const turn = agent.session().prompt("Explain this project.");
|
|
30
32
|
|
|
31
33
|
for await (const event of turn) {
|
|
32
34
|
if (event.type === "text_delta") process.stdout.write(event.delta);
|
|
33
35
|
}
|
|
34
36
|
|
|
35
|
-
console.log(await turn.result); // { stopReason, usage }
|
|
36
|
-
const checkpoint = await agent.checkpoint();
|
|
37
|
+
console.log(await turn.result); // { messageId, stopReason, usage }
|
|
37
38
|
await agent.close();
|
|
38
39
|
```
|
|
39
40
|
|
|
40
|
-
`
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
and
|
|
41
|
+
`createFxAgent()` returns at once and does no I/O until a session runs a turn.
|
|
42
|
+
`apiKey` is optional. Without it, libfx uses `AI_GATEWAY_API_KEY`, then on
|
|
43
|
+
Vercel the deployment's OIDC token. `model` defaults to fx's built-in model.
|
|
44
|
+
Tools, MCP clients, and skills are agent options, described in
|
|
45
|
+
[JavaScript tools and instructions](#javascript-tools-and-instructions) and the
|
|
46
|
+
sections after it.
|
|
47
|
+
|
|
48
|
+
Because nothing loads before a session runs, an option the backend rejects,
|
|
49
|
+
such as an effort level the model does not support, fails that session's
|
|
50
|
+
first turn rather than the `createFxAgent()` call. The turn's `result` has the
|
|
51
|
+
stop reason `error` and the reason in `error`, and `onEvent` receives
|
|
52
|
+
`session.error`.
|
|
53
|
+
|
|
54
|
+
An agent is the configuration its sessions run under. Every process that
|
|
55
|
+
creates an agent with the same options can run the same sessions, so create
|
|
56
|
+
one agent for your server and open a session per conversation.
|
|
57
|
+
`agent.close()` waits for the turns this process is running, then lets their
|
|
58
|
+
sessions go.
|
|
59
|
+
|
|
60
|
+
### Sessions
|
|
61
|
+
|
|
62
|
+
`agent.session(id)` opens the session `id`, and `agent.session()` starts a new
|
|
63
|
+
one. Any other value, `null` included, throws. Opening a session reads
|
|
64
|
+
nothing; its first prompt does. A new session's `id` is set once that prompt is
|
|
65
|
+
accepted, so read it from `turn.accepted`:
|
|
44
66
|
|
|
45
67
|
```js
|
|
46
|
-
const
|
|
47
|
-
|
|
68
|
+
const turn = agent.session().prompt("Plan the migration.");
|
|
69
|
+
const { sessionId } = await turn.accepted;
|
|
70
|
+
|
|
71
|
+
// Later, in any process:
|
|
72
|
+
const followUp = agent.session(sessionId).prompt("Start with the schema.");
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
To know the id before the first prompt, for example so a retried first
|
|
76
|
+
request with the same `messageId` reaches the same session, get one from
|
|
77
|
+
`await agent.newSessionId()` and open it with `agent.session(id)`.
|
|
78
|
+
|
|
79
|
+
On `memory()`, a session id is 1 to 128 letters, digits, `.`, `_`, or `-`. A
|
|
80
|
+
World durability, `local()`, `vercel()`, or `world()`, keeps each session as a
|
|
81
|
+
World run, so the id is a run id: `wrun_` followed by 10 to 64 letters or
|
|
82
|
+
digits. Take it from `agent.newSessionId()` or `turn.accepted`. Vercel's World
|
|
83
|
+
only creates a run whose id was minted in the last 24 hours, so send a new
|
|
84
|
+
id's first prompt within a day. `agent.prompt()` and `agent.checkpoint()` use
|
|
85
|
+
a session the agent opens for itself, for code that holds one conversation.
|
|
86
|
+
|
|
87
|
+
One process runs a session's turns at a time, one turn after another. A
|
|
88
|
+
second process that receives a prompt for a busy session queues it behind the
|
|
89
|
+
running turn. Pass `context` to hand JSON to the session's tools:
|
|
90
|
+
`agent.session(id, { context: { userId } })` gives every tool call
|
|
91
|
+
`context` beside its `executionId` and `sessionId`. Each turn runs with the context of the
|
|
92
|
+
`session()` call whose prompt or steer started it, and a call without
|
|
93
|
+
`context` gives its turns none.
|
|
94
|
+
|
|
95
|
+
A conversation can also choose its own model and instructions.
|
|
96
|
+
`agent.session(id, { model, instructions })` runs the turns it starts with
|
|
97
|
+
that model, a model ID or a model object such as `{ id, effort, fast }`, in
|
|
98
|
+
place of the agent's whole model choice, and with those instructions in place
|
|
99
|
+
of the agent's. They follow the rule `context` follows: a turn keeps the
|
|
100
|
+
settings of the `session()` call that started it, also when another process
|
|
101
|
+
continues it, and a call without them runs with the agent's. Pass a
|
|
102
|
+
conversation's settings on each call; a change applies from its next turn.
|
|
103
|
+
When a session's next turn runs under another model than the one its last
|
|
104
|
+
checkpoint was saved under, the session continues and `onEvent` receives a
|
|
105
|
+
`checkpoint.mismatch` event.
|
|
106
|
+
|
|
107
|
+
### Turns
|
|
108
|
+
|
|
109
|
+
`session.prompt(input)` queues `input` as the session's next turn and returns
|
|
110
|
+
a view of that turn. Prompt input is a string or an array of content blocks,
|
|
111
|
+
as described in [prompt input](#prompt-input), except that image data must be a
|
|
112
|
+
base64 string, because the prompt is stored as JSON before it runs. A turn
|
|
113
|
+
view has:
|
|
114
|
+
|
|
115
|
+
- `messageId`: the turn's id, which libfx chooses unless you pass one.
|
|
116
|
+
- `accepted`: resolves to `{ messageId, sessionId }` once the prompt is stored,
|
|
117
|
+
or at once for a prompt whose `signal` had already aborted, which is never
|
|
118
|
+
stored.
|
|
119
|
+
- `result`: resolves to `{ messageId, stopReason, usage }` when the turn ends.
|
|
120
|
+
- Async iteration over the turn's events, as
|
|
121
|
+
[model and tool events](#prompt-input) describes.
|
|
122
|
+
- `readable`: the same turn as an NDJSON `ReadableStream` that a route can
|
|
123
|
+
return as its response body. Each line is an event with a `cursor`.
|
|
124
|
+
|
|
125
|
+
Dropping a turn view cancels nothing: the turn runs to its end whether or not
|
|
126
|
+
anyone reads it. Call `session.cancel()` to stop it. A prompt's `signal`
|
|
127
|
+
cancels that prompt's turn when it aborts, whether the turn is running or still
|
|
128
|
+
waiting behind another, and never a later turn. A prompt whose `signal` had
|
|
129
|
+
already aborted is not stored: its `result` resolves with the stop reason
|
|
130
|
+
`cancelled`.
|
|
131
|
+
|
|
132
|
+
Give a prompt a `messageId` when the same request can reach your server more
|
|
133
|
+
than once, such as a client retry. A second `prompt()` with an id the session
|
|
134
|
+
already accepted runs nothing: while that turn runs, the view follows it, and
|
|
135
|
+
after it ends, `result` resolves to its outcome with `repeated: true`. When
|
|
136
|
+
that turn failed and its end never reached the stream, `result` resolves with
|
|
137
|
+
the failure from the session's log, without `repeated` if the failure could be
|
|
138
|
+
this request's own run. When the process that ran the turn stopped before
|
|
139
|
+
writing how it ended, `result` resolves with the stop reason `unknown` after
|
|
140
|
+
30 seconds. An id follows the session id rule.
|
|
141
|
+
|
|
142
|
+
`session.stream(cursor)` returns every event the session has produced from
|
|
143
|
+
`cursor` on, as NDJSON, and stays open for the events after them. Any process
|
|
144
|
+
can serve it, so a browser that loses its connection reconnects with the last
|
|
145
|
+
`cursor` it saw and continues where it stopped. Beside the turn events, the
|
|
146
|
+
stream marks each turn with `turn_start` and `turn_end`, a continued turn with
|
|
147
|
+
`turn_resume`, and a turn that stopped for a function deadline with
|
|
148
|
+
`turn_yield`. `turn_start` and `turn_resume` carry the `sessionId`, so a client
|
|
149
|
+
that started a new session learns its id from the first line of
|
|
150
|
+
`turn.readable`.
|
|
151
|
+
|
|
152
|
+
Each line carries the `epoch` of the lease its writer held, and epochs only
|
|
153
|
+
rise over a session's life. A worker that was replaced while it ran can still land lines late, because the World accepts
|
|
154
|
+
them. A worker that takes a turn over writes its `turn_resume` line before it
|
|
155
|
+
goes on, and every reader hides a line whose epoch is lower than one before
|
|
156
|
+
it, so the replaced worker's later lines never show and every reader, on any
|
|
157
|
+
server and after any refresh, shows the same lines. A replaced worker's line
|
|
158
|
+
that lands in the moment between its successor's claim and that `turn_resume`
|
|
159
|
+
line still shows; only a World that refuses stale stream writes closes that
|
|
160
|
+
gap.
|
|
161
|
+
|
|
162
|
+
`session.steer(text)` adds guidance to the running turn at its next model
|
|
163
|
+
request, or to the next turn when none is running. `session.resume()`
|
|
164
|
+
continues a turn a stopped process left open and returns its view, whose
|
|
165
|
+
`result` has the stop reason `idle` when there was none. A prompt also
|
|
166
|
+
continues an open turn before it runs, so most hosts never call `resume()`.
|
|
167
|
+
|
|
168
|
+
### Durability
|
|
169
|
+
|
|
170
|
+
The `durability` option chooses where sessions live. Without it, libfx picks
|
|
171
|
+
one from the environment:
|
|
172
|
+
|
|
173
|
+
| Environment | Durability | Sessions live in |
|
|
174
|
+
| --- | --- | --- |
|
|
175
|
+
| Vercel, where `VERCEL` is set | `vercel()` | Vercel's World, the store and queue behind Vercel Workflow |
|
|
176
|
+
| Node.js elsewhere | `local()` | files in `FX_SESSIONS_DIR`, or `$TMPDIR/libfx/sessions` |
|
|
177
|
+
| Browsers | `memory()` | this page's memory |
|
|
178
|
+
|
|
179
|
+
```js
|
|
180
|
+
import { createFxAgent, memory } from "libfx";
|
|
181
|
+
import { local } from "libfx/durable-local";
|
|
182
|
+
|
|
183
|
+
createFxAgent({ durability: local({ dir: ".fx/sessions" }) });
|
|
184
|
+
createFxAgent({ durability: memory() }); // nothing survives the process
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
`libfx/durable-local` and `libfx/durable-vercel` carry their World inside
|
|
188
|
+
them, and libfx loads one only when a session first needs it, so your app
|
|
189
|
+
installs no other package and an app with no sessions loads neither.
|
|
190
|
+
|
|
191
|
+
To keep sessions in a World your app creates, such as Workflow's Postgres
|
|
192
|
+
World, pass it to `world()` from `libfx/durable-world`:
|
|
193
|
+
|
|
194
|
+
```js
|
|
195
|
+
import { createFxAgent } from "libfx";
|
|
196
|
+
import { world } from "libfx/durable-world";
|
|
197
|
+
import { createWorld } from "@workflow/world-postgres";
|
|
198
|
+
|
|
199
|
+
// libfx's own queue topics and jobs, apart from the app's workflows.
|
|
200
|
+
const sessions = createWorld({ namespace: "libfx", jobPrefix: "libfx_" });
|
|
201
|
+
await sessions.start();
|
|
202
|
+
|
|
203
|
+
const agent = createFxAgent({ durability: world(sessions, { queueDurable: true }) });
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
A World you pass in stays yours. libfx never starts or closes it, so start it
|
|
207
|
+
before the agent's first prompt and close it after `agent.close()`. When the
|
|
208
|
+
World delivers in your process, agents given the same World take its
|
|
209
|
+
deliveries in turn, as servers sharing a queue do, and a delivery that arrives
|
|
210
|
+
while no agent is listening fails, so the queue delivers it again. Pass a
|
|
211
|
+
function that returns a World instead, and libfx creates, starts, and closes
|
|
212
|
+
it with the agent. `local()` and `vercel()` are `world()` with the World each
|
|
213
|
+
one bundles.
|
|
214
|
+
|
|
215
|
+
`world()` takes what libfx can't learn from the World itself:
|
|
216
|
+
|
|
217
|
+
| Option | Default | Meaning |
|
|
218
|
+
| --- | --- | --- |
|
|
219
|
+
| `queueDurable` | `false` | The queue keeps a message until a delivery acknowledges it, across crashes. When `false`, `prompt()` stores the prompt in the session before it queues it. |
|
|
220
|
+
| `livenessKnown`, `alive(lease)`, `holderInfo()` | `false` | `alive(lease)` answers whether the holder of a lease still runs, from what `holderInfo()` put in it, so a crashed worker's session frees at once. Without it, a worker renews its lease while it runs, and the session of one that died frees `leaseMs` after its last renewal. |
|
|
221
|
+
| `leaseMs` | 15000 | Without liveness, how long a lease lasts after its worker took or last renewed it. The worker renews it every third of that, and never past its deadline. |
|
|
222
|
+
| `maxDurationMs`, `reserveMs` | no limit, 30000 | Each delivery stops `reserveMs` before `maxDurationMs`, or before the World's `getRuntimeDeadline()` when it has one. |
|
|
223
|
+
| `pollMs` | 1000 | How often a waiting worker reads the session again. |
|
|
224
|
+
| `gatewayKey()` | none | The AI Gateway credential when the agent has no `apiKey`. |
|
|
225
|
+
| `name` | `"world"` | The durability's name in errors. |
|
|
226
|
+
|
|
227
|
+
libfx uses the World's `events.create` and `events.list`, its streams, its
|
|
228
|
+
queue, and `createQueueHandler`. It reads an event's position in its run from
|
|
229
|
+
the event's id, which must be lowercase letters, an underscore, and that
|
|
230
|
+
position as 26 digits, as in Workflow's Worlds; a session whose World returns
|
|
231
|
+
another id fails. It sends every message on the topic
|
|
232
|
+
`__libfx_wkf_workflow_session` and handles the prefix
|
|
233
|
+
`__libfx_wkf_workflow_`, which is what a Workflow World's `namespace: "libfx"`
|
|
234
|
+
gives, so a World serving the app's own workflows on the default namespace
|
|
235
|
+
needs a second instance for libfx. A World with `registerHandler` delivers to
|
|
236
|
+
the agent in its process. Any other World delivers over HTTP, so mount
|
|
237
|
+
`agent.wakeHandler()` where it delivers, as [Next.js and
|
|
238
|
+
Vercel](#nextjs-and-vercel) shows.
|
|
239
|
+
|
|
240
|
+
libfx records each step of a turn as it happens. A prompt is stored before
|
|
241
|
+
the caller hears it was accepted, and a tool call with effects is stored
|
|
242
|
+
before it runs, so a crash never loses an accepted prompt and never repeats a
|
|
243
|
+
tool call silently. When a session has no turn in progress, the process that
|
|
244
|
+
takes it sends the next turn's first model request while it writes its claim
|
|
245
|
+
on the session, and it writes, shows, and runs nothing for that turn until
|
|
246
|
+
the claim lands. If another process's claim lands first, the late claim
|
|
247
|
+
counts for nothing and its model request is cancelled. Other records are
|
|
248
|
+
written while the model works, without holding up the turn. A session also
|
|
249
|
+
saves a checkpoint at the end of every turn and wherever a turn yields, so the
|
|
250
|
+
process that continues it reads the checkpoint and the few records after it,
|
|
251
|
+
not the whole history.
|
|
252
|
+
|
|
253
|
+
When the process running a turn stops, the next process to receive work for
|
|
254
|
+
the session continues the turn from its last record:
|
|
255
|
+
|
|
256
|
+
- On `local()`, a session held by a process that died is free at once.
|
|
257
|
+
- On `vercel()`, where libfx cannot tell whether a function still runs, a
|
|
258
|
+
worker renews a 15 second lease while it runs. When its function dies, its
|
|
259
|
+
session frees 15 seconds after the last renewal, and the next delivery for
|
|
260
|
+
it continues the turn. When
|
|
261
|
+
the deadline is near, libfx stops before the next model request and saves
|
|
262
|
+
the turn, and the next invocation continues it. A model request or tool
|
|
263
|
+
call still running shortly before the deadline is cut off: `onEvent`
|
|
264
|
+
receives `session.deadline`, and the next invocation continues the turn,
|
|
265
|
+
running a cut-off idempotent call again and telling the model about any
|
|
266
|
+
other. The event's `cutoffs` counts the times in a row the deadline cut
|
|
267
|
+
off the same step. After 3, the next invocation does not run that step
|
|
268
|
+
again: a call is answered as possibly run, so the model decides what to do
|
|
269
|
+
next, and a model request cancels the turn. So a process never runs on
|
|
270
|
+
after the deadline its hold ends at. A
|
|
271
|
+
process that freezes, or whose renewals stall for a lease, and wakes after
|
|
272
|
+
another took its session over stops at its next write, a renewal included,
|
|
273
|
+
and `onEvent` receives `session.fenced`. Until then it
|
|
274
|
+
can finish a model request or a call to an idempotent tool; it never starts
|
|
275
|
+
a tool with effects, because that waits for its record to be stored. A
|
|
276
|
+
process that started a new turn on an idle session and was taken over
|
|
277
|
+
while still claiming the session runs nothing for that turn and reads the
|
|
278
|
+
session's log again instead, so the prompt it brought is not lost.
|
|
279
|
+
- When a write fails for any other reason, or the engine running a turn stops,
|
|
280
|
+
`onEvent` receives `session.error`, and the same queue message runs the
|
|
281
|
+
session again with a new engine. A turn whose engine stops 3 times is
|
|
282
|
+
cancelled, and the prompts behind it run; one that never got started ends
|
|
283
|
+
with an error instead. When the engine stops twice more, even to cancel the
|
|
284
|
+
turn, the session can run no more turns: the turn and every prompt sent to
|
|
285
|
+
the session end with an error. A turn whose engine stops after storing its
|
|
286
|
+
end resolves with the stop reason `unknown`.
|
|
287
|
+
- When an engine cannot open, the turn waiting on it ends with an error, and
|
|
288
|
+
each later engine that cannot open ends the next waiting prompt the same
|
|
289
|
+
way. A started turn that ended this way stays open in the session, and
|
|
290
|
+
`session.resume()` answers `idle` for it. The next prompt's engine first
|
|
291
|
+
continues it, or cancels it when it was cancelled or its engine kept
|
|
292
|
+
stopping, and then runs that prompt.
|
|
293
|
+
- On `memory()`, sessions end with the process.
|
|
294
|
+
|
|
295
|
+
The model is told when its turn was interrupted, so it can check what
|
|
296
|
+
happened before going on. A turn that stopped at a model request for a
|
|
297
|
+
deadline continues without that notice; one whose call was cut off gets it.
|
|
298
|
+
|
|
299
|
+
A durability holds conversation history only. Every process supplies the
|
|
300
|
+
model, credentials, instructions, tools, MCP clients, and skills when it
|
|
301
|
+
creates the agent. Every checkpoint records the libfx version, tool set, and
|
|
302
|
+
model that saved it. When a saved session, or a checkpoint passed as
|
|
303
|
+
`checkpoint`, was saved under different ones than the agent now has, it still
|
|
304
|
+
loads, and `onEvent` receives a `checkpoint.mismatch` event naming what
|
|
305
|
+
changed.
|
|
306
|
+
|
|
307
|
+
`session.checkpoint()` returns the session's history as opaque bytes, read
|
|
308
|
+
without changing the session. Pass them as `checkpoint` to start a new agent's
|
|
309
|
+
own session from them, for example to move a conversation between
|
|
310
|
+
durabilities.
|
|
311
|
+
|
|
312
|
+
### Tools with effects
|
|
313
|
+
|
|
314
|
+
When a process stops while a tool call runs, libfx cannot know whether the
|
|
315
|
+
call finished. Mark a tool `idempotent: true` when running it twice is safe,
|
|
316
|
+
such as a lookup:
|
|
317
|
+
|
|
318
|
+
```js
|
|
319
|
+
const tools = [
|
|
320
|
+
{ name: "get_order", description: "Looks up an order", idempotent: true, inputSchema, async execute(input, { executionId }) { /* ... */ } },
|
|
321
|
+
{ name: "refund_order", description: "Refunds an order", inputSchema, async execute(input, { executionId }) { /* ... */ } },
|
|
322
|
+
];
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
An idempotent call that was running runs again when the turn continues. Any
|
|
326
|
+
other call never runs again on its own: the turn continues at once, and the
|
|
327
|
+
model receives that call's result as an error saying it may have partly run,
|
|
328
|
+
so it can check the call's effects before calling it again or ask the user.
|
|
329
|
+
|
|
330
|
+
`executionId` is the same for a call each time it runs, including after a
|
|
331
|
+
crash. Pass it to the service the tool calls, as an idempotency key, so a call
|
|
332
|
+
that runs again changes nothing twice. Calls to idempotent tools also start
|
|
333
|
+
without waiting for their record to be stored.
|
|
334
|
+
|
|
335
|
+
## Model options
|
|
336
|
+
|
|
337
|
+
Model configuration groups the model ID and model-specific options:
|
|
338
|
+
|
|
339
|
+
```js
|
|
340
|
+
const agent = createFxAgent({
|
|
48
341
|
model: { id: "anthropic/claude-opus-5.5-fast", effort: "low", fast: true },
|
|
49
342
|
});
|
|
50
343
|
```
|
|
51
344
|
|
|
52
|
-
|
|
53
|
-
`
|
|
54
|
-
|
|
345
|
+
Agent configuration uses named options; `env` is reserved for
|
|
346
|
+
`createFxTerminal()`. A string `model` remains supported as shorthand.
|
|
347
|
+
Top-level `effort` and `fast` are deprecated but remain supported with a
|
|
348
|
+
string model or no model; they cannot be mixed with a model object. New code
|
|
349
|
+
should use the model object.
|
|
55
350
|
|
|
56
351
|
`model.effort` sets the reasoning effort for models that advertise effort
|
|
57
352
|
levels. It uses the same vocabulary as the fx CLI's `--effort` flag:
|
|
@@ -98,6 +393,42 @@ included.
|
|
|
98
393
|
libfx makes at most one automatic retry after a retryable transport failure and
|
|
99
394
|
only before model output or tool effects escape. Cancellation prevents a retry.
|
|
100
395
|
|
|
396
|
+
## A single engine
|
|
397
|
+
|
|
398
|
+
`createFxEngine()` is the kernel under `createFxAgent()`: one conversation,
|
|
399
|
+
held in the memory of the process that runs it, with no queue and no session
|
|
400
|
+
store. Use it to run turns inside one request and keep nothing, or to keep a
|
|
401
|
+
conversation in storage of your own through [persistence](#persistence). It
|
|
402
|
+
takes the same model, tool, MCP, and skill options, `apiKey` is required, and
|
|
403
|
+
it resolves once its backend has loaded. In this section, `agent` is an engine:
|
|
404
|
+
|
|
405
|
+
```js
|
|
406
|
+
import { createFxEngine } from "libfx";
|
|
407
|
+
|
|
408
|
+
const agent = await createFxEngine({ apiKey: process.env.AI_GATEWAY_API_KEY, model });
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
### Code written for libfx 0.0.12
|
|
412
|
+
|
|
413
|
+
In libfx 0.0.12, `createFxAgent()` created this engine. It now creates the
|
|
414
|
+
durable agent, so rename those calls to `createFxEngine()`. Code that still
|
|
415
|
+
calls `createFxAgent()` keeps running, with these differences:
|
|
416
|
+
|
|
417
|
+
- `await createFxAgent(...)` resolves to the durable agent, because awaiting a
|
|
418
|
+
value that is not a promise returns the value.
|
|
419
|
+
- Breaking out of a turn's events no longer cancels the turn, which runs to
|
|
420
|
+
its end. Cancel it with the prompt's `signal` or `session.cancel()`.
|
|
421
|
+
- Turns have no `cancel()` or `steer()`, and the agent has no `followUp()` or
|
|
422
|
+
`resume()`. A session has `steer()`, `cancel()`, and `resume()`.
|
|
423
|
+
- `persistence` and `sessionId` throw, because the durability holds the
|
|
424
|
+
session.
|
|
425
|
+
|
|
426
|
+
On the native backend, host tools now run alongside each other unless they
|
|
427
|
+
are marked `writes: true`, in `createFxEngine()` as well. WebAssembly runs
|
|
428
|
+
them one at a time.
|
|
429
|
+
|
|
430
|
+
### Prompt input
|
|
431
|
+
|
|
101
432
|
`prompt(input, { signal? })` accepts a string or text, image, and resource
|
|
102
433
|
blocks. It returns an async iterable of normalized events:
|
|
103
434
|
|
|
@@ -124,7 +455,7 @@ A turn has one event consumer. Breaking out of its iterator cancels the turn;
|
|
|
124
455
|
`turn.cancel()` and `agent.close()` also release blocked output. Embedded agents
|
|
125
456
|
have no implicit model-step cap, so hosts should cancel turns that exceed their
|
|
126
457
|
own budgets. The CLI's `max_agent_steps` setting does not apply to
|
|
127
|
-
`
|
|
458
|
+
`createFxEngine()`. Transport or message-decoding failures reject the result
|
|
128
459
|
instead of returning success with missing text.
|
|
129
460
|
|
|
130
461
|
Native transport buffers at most 8 MiB of output bytes. Unread SDK events apply
|
|
@@ -191,7 +522,7 @@ For a Node.js host that already uses `sharp`:
|
|
|
191
522
|
```js
|
|
192
523
|
import sharp from "sharp";
|
|
193
524
|
|
|
194
|
-
const agent = await
|
|
525
|
+
const agent = await createFxEngine({
|
|
195
526
|
apiKey,
|
|
196
527
|
model,
|
|
197
528
|
async resizeImage({ bytes }) {
|
|
@@ -250,7 +581,7 @@ const prepareImage = {
|
|
|
250
581
|
};
|
|
251
582
|
},
|
|
252
583
|
};
|
|
253
|
-
// Supply prepareImage in
|
|
584
|
+
// Supply prepareImage in createFxEngine({ tools: [prepareImage], ... }).
|
|
254
585
|
```
|
|
255
586
|
|
|
256
587
|
`imageStore` is application code, not a libfx API. It must authorize references,
|
|
@@ -285,9 +616,36 @@ for await (const event of turn) {
|
|
|
285
616
|
}
|
|
286
617
|
```
|
|
287
618
|
|
|
619
|
+
`steer()` resolves to `{ id }`, and the returned promise carries the same `id`
|
|
620
|
+
at once. Until the model sees a steer, `turn.withdraw(id)` takes it back and
|
|
621
|
+
resolves to `withdrawn`; after that it resolves to `already_placed`. With
|
|
622
|
+
[persistence](#persistence), a steer resolves only once its acceptance is stored, a model request
|
|
623
|
+
carries it only after that, and a withdrawal resolves once it is stored, so a
|
|
624
|
+
crash never loses a steer the call reported as accepted: `agent.resume()`
|
|
625
|
+
delivers one the model had not seen yet. The web core accepts a steer when it
|
|
626
|
+
takes it at the next boundary, so there `steer()` resolves then, and a steer
|
|
627
|
+
still queued when the turn ends rejects.
|
|
628
|
+
|
|
629
|
+
`agent.followUp(input)` queues text to run as its own turn once the current
|
|
630
|
+
turn ends, or at once when none is running, instead of failing with
|
|
631
|
+
`a prompt is already in progress`. It returns a promise for that turn; the
|
|
632
|
+
promise carries the follow-up's `id` and `accepted`, which resolves `{ id }`
|
|
633
|
+
once it is stored. With persistence, a follow-up survives a crash. After a
|
|
634
|
+
restore, the follow-ups the session held have no caller, so each waits for
|
|
635
|
+
`agent.resume()`: every call continues the open turn first, then starts the
|
|
636
|
+
next held follow-up, and returns `null` once none is left. Follow-ups this
|
|
637
|
+
agent queues run on their own and do not wait behind held ones. Call `resume()`
|
|
638
|
+
until it returns `null` whenever a session opens, before this agent queues
|
|
639
|
+
follow-ups of its own: one queued during a resumed turn starts when that turn
|
|
640
|
+
ends, and `resume()` throws while it runs. A follow-up whose turn ends before
|
|
641
|
+
libfx records its first `turn_progress` is withdrawn, and one that ends after
|
|
642
|
+
it is recorded like any other turn, so a follow-up never runs twice. `close()`
|
|
643
|
+
rejects follow-ups that have not started; the session's records still hold them.
|
|
644
|
+
|
|
288
645
|
Cancelling a steered turn drops any guidance that has not reached a safe
|
|
289
646
|
boundary and releases its queue. Applied guidance is part of the same history
|
|
290
647
|
turn, so an idle `checkpoint()` includes the full steered conversation.
|
|
648
|
+
|
|
291
649
|
`checkpoint()` returns opaque, bounded, versioned bytes. Concurrent calls run
|
|
292
650
|
one at a time, and a call still waiting for an earlier one fails the same way a
|
|
293
651
|
direct call would if a prompt starts or the agent closes first. A newer libfx
|
|
@@ -295,7 +653,7 @@ restores checkpoints from older versions, but an older libfx cannot restore one
|
|
|
295
653
|
written by a newer version. Restore them only when creating a fresh agent:
|
|
296
654
|
|
|
297
655
|
```js
|
|
298
|
-
const restored = await
|
|
656
|
+
const restored = await createFxEngine({ apiKey, model, checkpoint });
|
|
299
657
|
```
|
|
300
658
|
|
|
301
659
|
An already-aborted prompt signal returns `cancelled` without a model request
|
|
@@ -308,6 +666,148 @@ agent-creation options and are not stored in a checkpoint: recreate the agent
|
|
|
308
666
|
with new `model.effort`, `model.fast`, or `model.ultrafast` values to change
|
|
309
667
|
them, the same path as switching models.
|
|
310
668
|
|
|
669
|
+
### Persistence
|
|
670
|
+
|
|
671
|
+
Pass a store as `persistence`, and libfx records the session as it runs, so a
|
|
672
|
+
new agent can continue it after the process running it stops. libfx writes
|
|
673
|
+
opaque records and checkpoints to the store and reads them back. The store
|
|
674
|
+
never parses them, and libfx never sees how they are stored.
|
|
675
|
+
|
|
676
|
+
```js
|
|
677
|
+
import { createFxEngine, createMemoryPersistence } from "libfx";
|
|
678
|
+
|
|
679
|
+
const persistence = createMemoryPersistence();
|
|
680
|
+
const agent = await createFxEngine({ apiKey, model, persistence, sessionId: "support-42" });
|
|
681
|
+
```
|
|
682
|
+
|
|
683
|
+
A store is an object with these methods:
|
|
684
|
+
|
|
685
|
+
```ts
|
|
686
|
+
interface Persistence {
|
|
687
|
+
load(): Promise<{
|
|
688
|
+
checkpoint?: { data: Uint8Array; through: string };
|
|
689
|
+
journal?: Iterable<JournalRecord> | AsyncIterable<JournalRecord>;
|
|
690
|
+
}>;
|
|
691
|
+
append(input: { expected: string | null; idempotencyKey: string; data: Uint8Array }): Promise<{ cursor: string }>;
|
|
692
|
+
saveCheckpoint?(input: { through: string; data: Uint8Array }): Promise<void>;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
interface JournalRecord {
|
|
696
|
+
cursor: string;
|
|
697
|
+
data: Uint8Array;
|
|
698
|
+
}
|
|
699
|
+
```
|
|
700
|
+
|
|
701
|
+
`append()` stores one record and resolves to its cursor, a string the store
|
|
702
|
+
chooses that places the record after the ones before it. The session's records
|
|
703
|
+
are its journal. libfx sends records one at a time, in order, and `expected`
|
|
704
|
+
is the cursor of the last record the agent knows of, or `null` for a new
|
|
705
|
+
session. Reject the append with an `FxFencedError` when `expected` is not the
|
|
706
|
+
cursor of the last stored record, so that a second agent writing the same
|
|
707
|
+
session stops the first instead of interleaving with it. `idempotencyKey` is
|
|
708
|
+
unique to each write, so a store that retries a write can return the stored
|
|
709
|
+
record's cursor instead of storing it twice.
|
|
710
|
+
|
|
711
|
+
`createFxEngine()` calls `load()` once. Resolve to the latest checkpoint, if any,
|
|
712
|
+
and the records stored after it, oldest first, each with its cursor. Resolve to
|
|
713
|
+
`{}` for a session with nothing stored. libfx skips any record the checkpoint
|
|
714
|
+
already covers.
|
|
715
|
+
|
|
716
|
+
libfx waits for the store only where a crash could otherwise lose work or
|
|
717
|
+
repeat it: the first record of each turn, which holds the prompt, is stored
|
|
718
|
+
before the model sees it, and a response's tool calls are stored before any of
|
|
719
|
+
them starts. Other appends are not waited for, so a turn's `result` can settle
|
|
720
|
+
before its last records land, and `agent.close()` waits for every append. If
|
|
721
|
+
`append` rejects, libfx stops the current turn at once: it starts no further
|
|
722
|
+
model request or tool call and writes nothing more. The turn fails with an
|
|
723
|
+
error whose `code` is `FX_JOURNAL_APPEND_FAILED` and whose `cause` is your
|
|
724
|
+
error, and the agent refuses later prompts. When no turn was left to report
|
|
725
|
+
the failure, `close()` rejects with it. A model request the turn started while
|
|
726
|
+
the failed append was in flight can still complete.
|
|
727
|
+
|
|
728
|
+
When the store has `saveCheckpoint()`, libfx saves a checkpoint after a turn
|
|
729
|
+
ends or yields once the records stored since the last checkpoint reach
|
|
730
|
+
`checkpointAfterBytes` (default 1 MiB). A checkpoint where a turn yielded
|
|
731
|
+
holds the open turn, and the next load continues it. `through` is the cursor of the last
|
|
732
|
+
record the checkpoint covers, so `load()` can return that checkpoint and only
|
|
733
|
+
the records after it, and the cost of opening a session stays bounded as its
|
|
734
|
+
history grows. libfx takes a checkpoint only while no turn is running. A saved
|
|
735
|
+
checkpoint emits a `checkpoint.save` event. A failed save emits
|
|
736
|
+
`checkpoint.error` and changes nothing: the records still restore the session,
|
|
737
|
+
and the next turn's end tries again. Deleting the records a checkpoint covers
|
|
738
|
+
is up to the store.
|
|
739
|
+
|
|
740
|
+
The records a load reads, after libfx drops the older progress of an open
|
|
741
|
+
turn, must fit in 4 MiB of JSON, a checkpoint in 4 MiB, and the history in
|
|
742
|
+
1,024 turns. Otherwise `createFxEngine()` rejects with an error whose `code` is
|
|
743
|
+
`FX_JOURNAL_TOO_LARGE`. libfx refuses records that repeat, skip, or do not
|
|
744
|
+
parse, and `createFxEngine()` rejects with the reason and the `code`
|
|
745
|
+
`FX_JOURNAL_INVALID`. For records written by a newer libfx, it rejects with an
|
|
746
|
+
`FxJournalVersionError`, whose `code` is `FX_JOURNAL_VERSION`. Each libfx
|
|
747
|
+
release resumes the sessions and checkpoints that the release before it saved,
|
|
748
|
+
so upgrade the processes that read a session before the ones that write it.
|
|
749
|
+
|
|
750
|
+
AI Gateway keys session affinity and prompt caching to the session's id. libfx
|
|
751
|
+
picks a new id for each agent unless you pass `sessionId`, so give a restored
|
|
752
|
+
session the id it had before. An id is 1 to 255 letters, digits, `.`, `_`, or
|
|
753
|
+
`-`. `agent.sessionId` is the id the agent uses.
|
|
754
|
+
|
|
755
|
+
If the last process stopped during a turn, `agent.resume()` continues that
|
|
756
|
+
turn with no new input and returns it, like `prompt()`. The model is told
|
|
757
|
+
"Resuming from unexpected session interruption.". A tool call that was
|
|
758
|
+
running does not run again: it comes back answered with an error saying it may
|
|
759
|
+
have partly run, and the model decides whether to check its effects, call it
|
|
760
|
+
again, or ask the user. A turn that fails or is cancelled ends in the journal
|
|
761
|
+
as it does in the agent, so only a stopped process leaves one to resume.
|
|
762
|
+
`resume()` returns `null` when the session holds no open turn and no held
|
|
763
|
+
follow-up, so a host can call it until it does every time it opens a session:
|
|
764
|
+
|
|
765
|
+
```js
|
|
766
|
+
const agent = await createFxEngine({ apiKey, model, persistence, sessionId, tools });
|
|
767
|
+
for (let turn = agent.resume(); turn; turn = agent.resume()) {
|
|
768
|
+
for await (const event of turn) console.log(event);
|
|
769
|
+
await turn.result;
|
|
770
|
+
}
|
|
771
|
+
```
|
|
772
|
+
|
|
773
|
+
When your host knows a call never ran, such as one it handed to another
|
|
774
|
+
process before the call did anything, pass `resume({ onAmbiguous })`. libfx
|
|
775
|
+
calls `onAmbiguous({ executionId, name, input })` once for each call the turn
|
|
776
|
+
left running, and each call it returns `"rerun"` for runs again under the same
|
|
777
|
+
`executionId` before the turn continues. Every other call keeps the answer that it may
|
|
778
|
+
have partly run.
|
|
779
|
+
|
|
780
|
+
Calling `prompt()` instead ends the open turn as interrupted and starts a new
|
|
781
|
+
one.
|
|
782
|
+
|
|
783
|
+
Give a prompt a `turnId` when the same request can reach libfx more than once,
|
|
784
|
+
such as from a queue that delivers it again after a crash. When the open turn
|
|
785
|
+
has that id, `prompt(input, { turnId })` continues it as `resume()` does, and
|
|
786
|
+
takes `onAmbiguous` the same way. When
|
|
787
|
+
the session's last turn ended with that id, it returns a turn that has already
|
|
788
|
+
ended, with the stop reason `end_turn`, without calling the model. Any other id
|
|
789
|
+
starts a new turn. A turn id is 1 to 128 letters, digits, `.`, `_`, or `-`.
|
|
790
|
+
Without one, libfx names the turn. `turn.id` is the turn's id.
|
|
791
|
+
|
|
792
|
+
To move a running session to another process, call
|
|
793
|
+
`turn.cancel({ reason: "handoff" })`. The turn stops at once, like any
|
|
794
|
+
cancellation, but libfx stores nothing more, so the journal keeps the turn
|
|
795
|
+
open and the agent that opens the session next continues it with `resume()`.
|
|
796
|
+
The handed-off agent then refuses `prompt()`, `resume()`, and `followUp()`;
|
|
797
|
+
close it. A handoff needs persistence.
|
|
798
|
+
|
|
799
|
+
Pass `persistence` instead of a `checkpoint` option; the two cannot be
|
|
800
|
+
combined. Like a checkpoint, persistence holds conversation history only:
|
|
801
|
+
resupply models, credentials, instructions, tools, MCP clients, and skill
|
|
802
|
+
records when you create the agent. A resumed turn continues under the tools the
|
|
803
|
+
new agent has. `createMemoryPersistence()` keeps the records and the latest
|
|
804
|
+
checkpoint in memory as `persistence.records` and `persistence.checkpoint`,
|
|
805
|
+
which suits tests and hosts that copy them to their own storage. It fences an
|
|
806
|
+
append whose `expected` cursor is not its last record's, and returns the stored
|
|
807
|
+
cursor for a repeated `idempotencyKey`. `FxFencedError` is exported by `libfx`;
|
|
808
|
+
its `code` is `FX_FENCED`, which a store of your own can use for the same case.
|
|
809
|
+
`FxJournalVersionError` is exported by `libfx` too.
|
|
810
|
+
|
|
311
811
|
## Models
|
|
312
812
|
|
|
313
813
|
Model discovery is explicit and does not create an Agent or load native or Wasm
|
|
@@ -325,10 +825,25 @@ const models = await listModels({
|
|
|
325
825
|
language-model IDs. It accepts the same optional `fetch` override as the Agent
|
|
326
826
|
API.
|
|
327
827
|
|
|
828
|
+
An agent reads the AI Gateway model catalog to learn what its model supports,
|
|
829
|
+
such as image input, reasoning effort, and output limits, and those details
|
|
830
|
+
shape every request. It fetches the catalog through your `fetch`. If that
|
|
831
|
+
request fails, for example behind a proxy that only forwards chat requests,
|
|
832
|
+
the agent cannot confirm the model's capabilities and refuses image prompts.
|
|
833
|
+
Pass `modelCatalog` to supply the catalog instead: the entries from
|
|
834
|
+
`https://ai-gateway.vercel.sh/coding-agent/v1/models`, either the response's
|
|
835
|
+
`{ data }` or the array, or only the entries for the models you use. The agent
|
|
836
|
+
then makes no catalog request, so every process that receives the same
|
|
837
|
+
entries builds the same requests:
|
|
838
|
+
|
|
839
|
+
```js
|
|
840
|
+
const agent = createFxAgent({ apiKey, model, modelCatalog });
|
|
841
|
+
```
|
|
842
|
+
|
|
328
843
|
## JavaScript tools and instructions
|
|
329
844
|
|
|
330
845
|
```js
|
|
331
|
-
const agent =
|
|
846
|
+
const agent = createFxAgent({
|
|
332
847
|
apiKey,
|
|
333
848
|
model,
|
|
334
849
|
instructions: "Keep answers concise.",
|
|
@@ -351,7 +866,7 @@ Gateway web search can run at the provider instead of in the JavaScript host.
|
|
|
351
866
|
Mark its canonical tool name with `providerExecuted: true` and omit `execute`:
|
|
352
867
|
|
|
353
868
|
```js
|
|
354
|
-
const agent =
|
|
869
|
+
const agent = createFxAgent({
|
|
355
870
|
apiKey,
|
|
356
871
|
tools: [{ name: "web_search", providerExecuted: true }],
|
|
357
872
|
});
|
|
@@ -365,6 +880,25 @@ call host code or require a separate provider key; their `tool_start` and
|
|
|
365
880
|
Their built-in permission policy is enforced when the request is projected, as
|
|
366
881
|
there is no local call-time effect to approve.
|
|
367
882
|
|
|
883
|
+
`tools` may also be an object keyed by tool name, such as
|
|
884
|
+
`tools: { lookup, save }`, where each value is a descriptor without `name`.
|
|
885
|
+
A descriptor that does include `name` must match its key.
|
|
886
|
+
|
|
887
|
+
When one model response calls several tools, the native backend runs them at
|
|
888
|
+
the same time and returns their results to the model in the order it called
|
|
889
|
+
them. Mark a tool `writes: true` when its calls must not overlap others: it
|
|
890
|
+
starts after every earlier call in the response finishes, and later calls
|
|
891
|
+
wait for it. WebAssembly runs calls one at a time. Every call still goes
|
|
892
|
+
through its own permission check before any of them starts.
|
|
893
|
+
|
|
894
|
+
`execute` receives `{ signal, executionId, sessionId }`, plus `context` when
|
|
895
|
+
the turn has one. `executionId` is the model's id for the call and stays the same when
|
|
896
|
+
the call runs again after a crash, so a tool with an external effect can pass
|
|
897
|
+
it on as an idempotency key. [Tools with effects](#tools-with-effects)
|
|
898
|
+
describes which calls run again.
|
|
899
|
+
When a tool rejects with an empty message, the model receives a non-empty error
|
|
900
|
+
so the provider does not refuse the conversation.
|
|
901
|
+
|
|
368
902
|
For ordinary tools, the JavaScript host is the authority for effects. The same
|
|
369
903
|
descriptors, schemas, cancellation, results, and events are used by N-API and
|
|
370
904
|
WebAssembly.
|
|
@@ -430,7 +964,7 @@ const mcp = await createMcpAdapter(client, {
|
|
|
430
964
|
prompts: ["review"],
|
|
431
965
|
});
|
|
432
966
|
|
|
433
|
-
const agent =
|
|
967
|
+
const agent = createFxAgent({
|
|
434
968
|
apiKey,
|
|
435
969
|
model,
|
|
436
970
|
tools: mcp.tools,
|
|
@@ -453,15 +987,15 @@ import { createSkillsAdapter } from "libfx/skills";
|
|
|
453
987
|
|
|
454
988
|
const record = await loadSkillFile("./skills/review/SKILL.md");
|
|
455
989
|
const skills = createSkillsAdapter([record]);
|
|
456
|
-
const agent =
|
|
990
|
+
const agent = createFxAgent({ apiKey, model, ...skills });
|
|
457
991
|
```
|
|
458
992
|
|
|
459
993
|
## Backends
|
|
460
994
|
|
|
461
995
|
```js
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
996
|
+
createFxAgent({ apiKey, backend: "auto" }); // native, then Wasm fallback
|
|
997
|
+
createFxAgent({ apiKey, backend: "native" }); // require N-API
|
|
998
|
+
createFxAgent({ apiKey, backend: "wasm" }); // require Wasm + JSPI
|
|
465
999
|
```
|
|
466
1000
|
|
|
467
1001
|
CommonJS applications can load the same Node API with `require("libfx")`. The
|
|
@@ -540,28 +1074,75 @@ This native setup does not require JSPI. Explicit WebAssembly use still needs
|
|
|
540
1074
|
JSPI and available Wasm assets; Next.js's standalone tracer excludes `.wasm`
|
|
541
1075
|
files, so a standalone Wasm host must supply those assets separately.
|
|
542
1076
|
|
|
1077
|
+
Create one agent for the server and open a session per request. The route
|
|
1078
|
+
returns the turn's stream and never runs the agent itself:
|
|
1079
|
+
|
|
543
1080
|
```js
|
|
1081
|
+
// lib/agent.js
|
|
544
1082
|
import { createFxAgent } from "libfx";
|
|
545
1083
|
|
|
546
|
-
export const
|
|
1084
|
+
export const agent = createFxAgent({ model: "anthropic/claude-haiku-4.5", tools });
|
|
1085
|
+
```
|
|
1086
|
+
|
|
1087
|
+
```js
|
|
1088
|
+
// app/api/chat/route.js
|
|
1089
|
+
import { agent } from "@/lib/agent";
|
|
547
1090
|
|
|
548
1091
|
export async function POST(request) {
|
|
549
|
-
const
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
1092
|
+
const sessionId = new URL(request.url).searchParams.get("sessionId") ?? undefined;
|
|
1093
|
+
// Check that the caller may use this session.
|
|
1094
|
+
const turn = agent.session(sessionId).prompt(await request.text());
|
|
1095
|
+
return new Response(turn.readable);
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
// Reconnects from any server.
|
|
1099
|
+
export async function GET(request) {
|
|
1100
|
+
const params = new URL(request.url).searchParams;
|
|
1101
|
+
const sessionId = params.get("sessionId");
|
|
1102
|
+
if (!sessionId) return new Response("sessionId is required", { status: 400 });
|
|
1103
|
+
// The same check as POST.
|
|
1104
|
+
const stream = agent.session(sessionId).stream(Number(params.get("cursor") ?? 0));
|
|
1105
|
+
return new Response(stream);
|
|
1106
|
+
}
|
|
1107
|
+
```
|
|
1108
|
+
|
|
1109
|
+
On your machine, the server runs each turn in its own process and keeps the
|
|
1110
|
+
session with `local()`. On Vercel, `prompt()` sends the prompt to Vercel Queues, and a separate
|
|
1111
|
+
invocation runs the turn while the `POST` response streams it. If the route's
|
|
1112
|
+
response ends first, the turn keeps running, and the client reconnects with
|
|
1113
|
+
the last `cursor` it read.
|
|
1114
|
+
|
|
1115
|
+
Until Vercel's World can deliver queue messages to the function itself, mount
|
|
1116
|
+
the agent's delivery route and subscribe it to libfx's queue topic in
|
|
1117
|
+
`vercel.json`:
|
|
1118
|
+
|
|
1119
|
+
```js
|
|
1120
|
+
// app/api/libfx/route.js
|
|
1121
|
+
import { agent } from "@/lib/agent";
|
|
1122
|
+
|
|
1123
|
+
export const POST = agent.wakeHandler();
|
|
1124
|
+
```
|
|
1125
|
+
|
|
1126
|
+
```json
|
|
1127
|
+
{
|
|
1128
|
+
"functions": {
|
|
1129
|
+
"app/api/libfx/route.js": {
|
|
1130
|
+
"experimentalTriggers": [{ "type": "queue/v2beta", "topic": "__libfx_wkf_workflow_session" }]
|
|
556
1131
|
}
|
|
557
|
-
await turn.result;
|
|
558
|
-
return Response.json({ text });
|
|
559
|
-
} finally {
|
|
560
|
-
await agent.close();
|
|
561
1132
|
}
|
|
562
1133
|
}
|
|
563
1134
|
```
|
|
564
1135
|
|
|
1136
|
+
The delivery route's maximum duration bounds each invocation. libfx stops a
|
|
1137
|
+
turn 30 seconds before the deadline, at its next model request, and cuts off a
|
|
1138
|
+
call still running 2 seconds before it; either way the same queue message runs
|
|
1139
|
+
the rest of the turn in a new invocation. Pass
|
|
1140
|
+
`durability: vercel({ reserveMs })` from `libfx/durable-vercel` to change that
|
|
1141
|
+
margin. A function that dies mid-turn holds its session for at most
|
|
1142
|
+
`leaseMs` after its last renewal, 15 seconds unless you pass
|
|
1143
|
+
`vercel({ leaseMs })`. libfx authenticates to the AI Gateway with the deployment's OIDC token
|
|
1144
|
+
unless you pass `apiKey` or set `AI_GATEWAY_API_KEY`.
|
|
1145
|
+
|
|
565
1146
|
Use the application's normal authentication and request limits around the
|
|
566
1147
|
route. JavaScript tools and MCP clients remain host-owned and must be supplied
|
|
567
1148
|
when creating an agent, including after checkpoint restoration. The native
|