mirror of
https://github.com/vercel-labs/fx.git
synced 2026-10-28 10:15:17 +08:00
- `createFxAgent()` returns the agent at once instead of a promise, and does no I/O until a session runs a turn. - `agent.session(id)` opens a session, and `session.prompt()` returns a turn view with `accepted`, `result`, async iteration over its events, and an NDJSON `readable`. - Sessions are durable by default: `vercel()` on Vercel, `local()` files in Node.js elsewhere, and `memory()` in browsers. The `durability` option picks one. - `libfx/durable-local` and `libfx/durable-vercel` bundle their World, so an app installs no other package. - A turn whose process crashes, redeploys, or reaches its function deadline continues from its last record in the next process that gets work for the session. - On Vercel, a turn stops at its next model request 30 seconds before the function deadline and continues in a new invocation from the same queue message. - A model request or tool call still running 2 seconds before the deadline is cut off, and `onEvent` receives `session.deadline`. - One process runs a session's turns at a time, and a prompt for a busy session queues behind the running turn. - A process that froze and was replaced stops at its next write and never starts a tool with effects, and `onEvent` receives `session.fenced`. - After an interruption, a call to an `idempotent: true` tool runs again. Any other call never reruns, and the model gets an error saying it may have partly run. - `executionId` is the same each time a call runs, including after a crash. - A prompt sent again with the same `messageId` starts nothing new: its view follows the original turn, or resolves with `repeated: true`. - `session.stream(cursor)` replays a session's events from any cursor on any server, with `turn_start`, `turn_end`, `turn_resume`, and `turn_yield` lines. - Every reader of a session's stream shows the same lines, because lines a replaced worker lands late are hidden. - `session.steer()` and `session.cancel()` reach the session's running turn, and `session.resume()` continues a turn a stopped process left open. - Every turn end and yield saves a checkpoint, so continuing a session reads one checkpoint and the records after it. - Checkpoints record the libfx version, tool set, and model. A session or checkpoint saved under different ones still loads, and `onEvent` receives `checkpoint.mismatch`. - `session.checkpoint()` exports a session's history, and the `checkpoint` option starts a new agent's session from it. - `agent.wakeHandler()` is the queue delivery route an app mounts on Vercel and subscribes to `__libfx_wkf_workflow_session` in `vercel.json`. - On Vercel, libfx authenticates to the AI Gateway with the deployment's OIDC token when neither `apiKey` nor `AI_GATEWAY_API_KEY` is set. - `createFxEngine()` accepts a `persistence` store, and `createMemoryPersistence()` provides one in memory. A store that rejects a stale append with `FxFencedError` stops a second writer. - A turn whose store append fails stops at once with `FX_JOURNAL_APPEND_FAILED`, and the agent refuses later prompts.
44 lines
1.9 KiB
JavaScript
44 lines
1.9 KiB
JavaScript
// Bundles each durability module with the World it wraps into one
|
|
// self-contained ES module, so an app installs nothing more and never loads a
|
|
// World of its own. Run with Bun: `bun sdk/scripts/bundle-durable.mjs <out>`.
|
|
import { resolve } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
const [outputDir] = process.argv.slice(2);
|
|
if (!outputDir) throw new Error("usage: bun sdk/scripts/bundle-durable.mjs <output-dir>");
|
|
const worlds = new URL("../durable/node_modules/@workflow/", import.meta.url);
|
|
const localWorld = await Bun.file(new URL("world-local/package.json", worlds)).json();
|
|
|
|
// world-local versions its data directory from the package.json beside its
|
|
// own module, which a bundle no longer has, so the version is pinned here.
|
|
const marker = "export async function getPackageInfo() {";
|
|
const pinWorldVersion = {
|
|
name: "pin-world-local-version",
|
|
setup(build) {
|
|
build.onLoad({ filter: /[\\/]world-local[\\/]dist[\\/]init\.js$/ }, async ({ path }) => {
|
|
const source = await Bun.file(path).text();
|
|
if (source.split(marker).length !== 2) throw new Error("world-local's getPackageInfo() changed; update the version pin");
|
|
const pinned = JSON.stringify({ name: localWorld.name, version: localWorld.version });
|
|
return { contents: source.replace(marker, `${marker}\n return ${pinned};`), loader: "js" };
|
|
});
|
|
},
|
|
};
|
|
|
|
for (const name of ["local", "vercel"]) {
|
|
const result = await Bun.build({
|
|
entrypoints: [fileURLToPath(new URL(`../durable/${name}.mjs`, import.meta.url))],
|
|
outdir: resolve(outputDir, "durable"),
|
|
naming: `${name}.mjs`,
|
|
target: "node",
|
|
format: "esm",
|
|
minify: true,
|
|
// cbor-x prefers an optional native addon and falls back without it.
|
|
external: ["cbor-extract"],
|
|
plugins: [pinWorldVersion],
|
|
});
|
|
if (!result.success) {
|
|
for (const log of result.logs) console.error(log);
|
|
process.exit(1);
|
|
}
|
|
}
|