136 Commits
Author SHA1 Message Date
Pranit 6d8193f2c6 Require every shell for the parity tests in the Linux unit job
The Linux unit test job installs zsh, so it now sets `FX_SHELL_PARITY_ALL_SHELLS` and the shell parity tests fail there instead of skipping when sh, dash, bash, or zsh is missing.
2026-10-22 11:54:35 -04:00
Pranit 2f2a0e7679 Run the ThreadSanitizer tests in ReleaseSafe with step time limits 2026-10-22 11:52:55 -04:00
Pranit 37e79dcdd5 Add a nightly ThreadSanitizer run of the unit tests 2026-10-22 11:52:55 -04:00
Pranit 33c99d68f9 Let the shell parity tests skip a missing shell
- The shell parity tests skip a missing shell unless `FX_SHELL_PARITY_ALL_SHELLS` is set. The nightly Fuzz workflow installs zsh and sets it.
- The direct-write audit counts the shell parity diagnostics as test output.
2026-10-22 11:52:55 -04:00
Pranit 7ae4d4107b Add a fuzz build step, nightly fuzzing, and shell parity tests
- `zig build fuzz` builds only the fuzz tests listed in `tests/fuzz/targets.json`. `-Dfuzz-target=<name>` picks one target, and `--fuzz=N` searches for new failing inputs.
- `zig build test` replays each target's saved inputs from `tests/fuzz/corpus/<target>/` on every pull request.
- A nightly Fuzz workflow fuzzes every target for about 20 minutes, uploads any failing input, and opens or updates an issue for that target. A pull request that changes the fuzzing setup fuzzes each target for two minutes.
- The shell parity tests run every command form fx runs without review through sh, dash, bash, and zsh, with and without `extendedglob`, and check that each one runs exactly as fx parsed it. The `shell_parity` target checks generated commands the same way.
2026-10-22 11:52:23 -04:00
Pranit cd9ec12c11 Keep the zsh snapshot when startup files replace a default alias
- zsh starts with default aliases such as `run-help`. When startup files replace one with a function, as Debian's and Ubuntu's global zshrc do, restoring the snapshot failed, so every command reran the startup files and fx showed the snapshot fallback notice. The restore now removes every alias before it defines functions, then restores the user's aliases.
- The Linux unit test job installs zsh, so the zsh user profile tests run there too.
2026-10-21 18:10:20 -04:00
Pranit de19db0153 Merge fazxes/update-mcp-tla-models into fazxes/remove-mcp-v1 2026-10-19 10:33:08 -04:00
Pranit ef8e31bfed Merge fazxes/mcp-reliability-fixes into fazxes/update-mcp-tla-models 2026-10-19 09:36:28 -04:00
Pranit 7e130af6e1 Port the MCP end-to-end tests to the new engine
Tests of v1-only behavior are removed, including the 14 that drove the v1
stdio dispatcher through a Zig fixture. The rest follow the new engine's
wire, verbs and wording. The fixtures follow the spec: stdio request state is
a string, and the legacy stdio fixture defaults to 2025-03-26, the oldest
version fx speaks.

The conformance client signs in with fx mcp login and runs the request-state
scenario through fx like the others, so the conformance job no longer sets up
Zig.
2026-10-17 16:51:51 -04:00
Pranit dc007ce1f8 Merge origin/main into fazxes/save-session-images-once 2026-10-17 01:11:43 -04:00
Pranit 974d880ce5 Bring the MCP TLA+ models and drivers up to date and run them in CI
The MCP models and drivers in tests/tla were a 14 Oct snapshot. They now
match the MCP code: the refused refresh token is never sent again, a
disconnect answers a waiting tool list, and the byte budget and the exit
reaper have models of their own. The next-version gating is gone, so
engine_lab builds against src/mcp and its scenarios run.

The host model said the status line shows a server count when nothing
needs attention. It now says nothing about MCP then, as fx does, and the
host-lifecycle tests expect that.

CI gets a job that runs each MCP model's smoke config, the host models,
and both MCP drivers, and Build & Test requires it. The planted bugs stay
a local check.
2026-10-16 18:46:44 -04:00
Pranit c196733550 Copy libfx session context when a session opens
- A libfx session keeps a copy of its `context` and session settings, taken when `agent.session()` is called. Changing the object afterward no longer changes what later prompts and steers store or what their tool calls receive.
- The 8 MiB input limit now covers the context a prompt or steer actually stores, so an app can no longer get a larger input stored by growing the context object after opening the session.
- Remote durable tests stop before running when the Vercel access token or project settings are missing, instead of reporting every test as failed.
- The steering tests check how much of the reply the model sent rather than wall-clock time, so they also pass against Vercel's World.
2026-10-16 02:08:36 -04:00
Pranit 4e1056dd24 Keep tenants apart when libfx agents share a process
- A queue delivery runs only an input its session already stored. A delivery the session's log doesn't hold, such as one posted straight to the queue route, runs nothing, writes nothing, and emits `session.refused`.
- Every durability, including `vercel()`, stores a prompt, steer, cancel or resume with its caller's context and settings before queueing it. The queued message carries only the input's id, session, agent name and store.
- A steer that reuses a prompt's id is rejected with `FX_MESSAGE_ID_IN_USE` on every durability. On `vercel()` the worker used to drop it with a `session.error`.
- Creating an agent under a name another agent already has on the same store, with different options, throws `FX_AGENT_NAME_IN_USE`. Agents created with the same options still share that name's sessions.
- `wakeHandler()` delivers each message to the agent of its name on the store the message names, and answers `FX_NO_AGENT` when the process has none. It no longer hands the message to an agent of that name on another store.
- `world()` takes a `store` option naming the store its sessions live in, defaulting to its name. `local()` treats each folder as its own store.
- A prompt or steer whose input, context and settings come to more than 8 MiB of JSON is rejected with `FX_INPUT_TOO_LARGE` before anything is stored.
- The Node native addon reads the environment from libc on every lookup, so an app that adds environment variables after the first engine starts no longer crashes the process on macOS.
- Prompts queued by an earlier libfx and still undelivered when this version deploys are refused instead of run.
2026-10-15 15:40:41 -04:00
Pranit 7513b2cc86 Merge origin/main into fazxes/save-session-images-once
main moved the TLA+ models under tests/tla and their Bun drivers under
tests/tla/drivers. The ImageStore and PromptIntake specs and configs
follow to tests/tla/sdk, and drive-images.mjs and drive-intake.mjs to
tests/tla/drivers, reading their graphs from tests/tla/sdk/graphs. CI,
sdk/AGENTS.md and tests/tla/README.md name the new paths.
2026-10-15 14:59:27 -04:00
Chad Hietala 460142bf26 Expose semantic execution hooks to libfx hosts
Signed-off-by: Chad Hietala <chad.hietala@vercel.com>
2026-10-15 09:43:27 -04:00
Pranit 1ca17681b6 Merge origin/main into fazxes/save-session-images-once
Take main's steering, which lets a durable steer carry images. A steer
still sends its image data as base64 strings, so it keeps its own input
type: the prompt input this branch widened to bytes, Blob and File no
longer types a steer.
2026-10-13 21:10:22 -04:00
Pranit 9534bf74d2 Move the TLA+ Bun drivers into tests/tla/drivers
- Move `drive.mjs`, `drive-agents.mjs`, `agent-process.mjs`, and
  `graph.mjs` from `tests/tla/sdk/` to `tests/tla/drivers/`. They read the
  state graphs that `tests/tla/sdk/check.sh` writes.
- Point CI, `AGENTS.md`, `sdk/AGENTS.md`, and the `tests/tla` notes at the
  new paths.
2026-10-13 20:46:43 -04:00
Pranit 542f00bd37 Keep every TLA+ model under tests/tla
- Move the session manager specs, trace checkers and design record, the
  indexer refresh model, and the libfx durable session model and its Bun
  drivers into `tests/tla/`, so `src/` and `sdk/` hold no TLA+ files.
- Add `tests/tla/indexer/check.sh`, which runs every refresh config and
  requires its expected verdict.
- Point code comments, CI, `AGENTS.md`, `sdk/AGENTS.md`, and
  `CONTRIBUTING.md` at the new paths, and document the models in
  `tests/tla/README.md`.
2026-10-13 20:46:43 -04:00
Pranit d0fcacb76c Merge remote-tracking branch 'origin/main' into fazxes/libfx-steer-like-cli
# Conflicts:
#	.github/workflows/ci.yml
#	sdk/AGENTS.md
#	sdk/tests/model/check.sh
#	sdk/tests/model/drive.mjs
#	sdk/tests/model/graph.mjs
2026-10-13 05:11:51 -04:00
Pranit 1b02dd1b9c Merge remote-tracking branch 'origin/main' into fazxes/save-session-images-once
# Conflicts:
#	.github/workflows/ci.yml
#	sdk/AGENTS.md
#	sdk/tests/model/check.sh
#	sdk/tests/model/drive.mjs
#	sdk/tests/model/graph.mjs
2026-10-13 04:58:25 -04:00
Pranit f666cca076 Merge pull request #1254 from vercel-labs/fazxes/route-turns-by-agent-name
Run each durable session's turns on the agent that created it
2026-10-13 04:42:59 -04:00
Pranit 1a19ec9950 Merge pull request #1223 from vercel-labs/fazxes/review-readme-changes
Request review for README and contributor guide changes
2026-10-13 04:40:52 -04:00
Pranit f3bc7e6bf4 Merge pull request #1196 from vercel-labs/fazxes/native-workspace-indexer
Index and search workspace files without running git
2026-10-13 02:03:49 -04:00
Pranit 871e93d845 Run each durable session's turns on the agent that created it
- `createFxAgent()` takes a `name`, which defaults to `"default"`, and `agent.name` returns it. A name that isn't 1 to 64 letters, digits, `.`, `_` or `-` throws.
- A session's turns run only on agents with the name of the agent that created it, so one app can run several durable agents with different models, instructions and tools.
- A prompt sent through another agent, such as `coding.session(supportSessionId).prompt()`, runs on the session's own agent, and the sending agent's `onEvent` gets a `session.forwarded` event.
- A process with no agent of a turn's name refuses the delivery with an `FX_NO_AGENT` error that names it, and the queue delivers it again.
- `wakeHandler` from `libfx` returns one queue route for every agent in the process. `agent.wakeHandler()` also routes by name.
- Agents created without a durability share one for each sessions location, or one on Vercel, so they can reach each other's sessions.
- Sessions stored before this change belong to the `default` agent.

Closes #1246.
2026-10-13 01:57:35 -04:00
Pranit e73bc45a8f Merge remote-tracking branch 'origin/main' into fazxes/libfx-steer-like-cli
# Conflicts:
#	sdk/AGENTS.md
#	sdk/README.md
#	sdk/fx-sdk.js
2026-10-13 01:17:43 -04:00
Pranit 6063702fdf Merge remote-tracking branch 'origin/main' into fazxes/save-session-images-once
# Conflicts:
#	sdk/AGENTS.md
#	sdk/README.md
#	sdk/durable.js
#	sdk/fx-sdk.js
#	sdk/types/libfx.d.cts
#	src/acp/libfx_journal.zig
#	src/core/agent/runtime/journal.zig
#	src/core/session/session_codec.zig
2026-10-12 21:30:35 -04:00
Pranit 354a33248a Merge remote-tracking branch 'origin/main' into fazxes/libfx-steer-like-cli
# Conflicts:
#	.github/workflows/ci.yml
#	sdk/AGENTS.md
#	sdk/tests/model/check.sh
2026-10-12 20:02:42 -04:00
Pranit 503ff10f64 Merge remote-tracking branch 'origin/main' into fazxes/save-session-images-once
# Conflicts:
#	.github/workflows/ci.yml
#	sdk/AGENTS.md
#	sdk/tests/model/check.sh
2026-10-12 19:24:56 -04:00
Pranit 9e64707c02 Save session images once, and take them as bytes, Blobs and Files
Durable session prompts now take image data as bytes, a Blob or a File,
as engine prompts do. libfx reads and encodes each image before the
prompt is stored, copies each image block once, and encodes base64 with
the runtime's own encoder where it has one.

Each image is saved once, named by the SHA-256 of its bytes, before the
prompt that names it. The session's log, records and checkpoints hold
only the name, so a session no longer stops working once its images pass
the 4 MiB a checkpoint or a journal load holds. An engine's persistence
can add putImage() and getImage() to do the same; without them images
stay inline. Records that name images are journal v2, and snapshots and
checkpoints that do are FXSN and FXCP v3. Older sessions still open, and
an older libfx refuses the new ones as written by a newer fx.

A prompt whose image can't be stored is rejected with
FX_IMAGE_STORE_FAILED before anything is recorded. An engine whose store
lost an image, or holds other bytes under its name, fails to open with
FX_JOURNAL_IMAGE_MISSING, and a durable turn ends with an error naming
the image. A durable retry of a prompt the log holds is answered without
writing its images again. A checkpoint the core refuses is reported with
a checkpoint.skipped event.

PromptIntake.tla and ImageStore.tla model the intake and the store, and
drive-intake.mjs and drive-images.mjs walk their state graphs against the
real code in CI.
2026-10-12 19:23:43 -04:00
Pranit b7a14442fb Steer libfx turns the way the fx CLI does
- A libfx steer sent while the model streams a reply cuts the reply short, and the turn continues with the steer and the text written so far, on the native addon and on WebAssembly.
- A steer sent while the model streams a tool call or a tool runs waits for the tool, then joins the same turn.
- A durable steer sent while a turn waits to start joins that turn instead of running after it.
- Cancelling a turn keeps the steers it never placed, and they run as the next turn instead of being dropped.
- session.steer() takes a messageId. A retry with the same id adds nothing, and a steer that reuses a prompt's id is rejected with FX_MESSAGE_ID_IN_USE.
- session.steer() resolves with { messageId }, and the user_message event carries the steer's id. It is sent when a model request carries the steer, not when the steer is queued.
- A durable steer with images ends the running turn at its next safe point and runs as the next turn. Steers sent after it join its turn.
2026-10-12 18:34:53 -04:00
Pranit 66870e58aa Require idempotent on libfx tools and speed up durable turns
- createFxAgent refuses a tool that doesn't set idempotent, and the error names both choices. MCP tools are idempotent: false.
- A tool with idempotent: true that doesn't write starts as soon as the model calls it, without waiting for the call to be saved.
- writes defaults to the opposite of idempotent, so only tools that are safe to repeat run in parallel.
- When a crash lands between a model response and the save of its tool calls, the resumed turn asks the model again with its tools instead of going on without their results.
- Each tool call's result is saved as soon as that call finishes, so a crash leaves no finished call in doubt.
- Events that arrive while a save is in flight go out together in the next record, and append() says with barrier whether the engine waits for it.
- Each progress record holds only what's new since the previous one.
- A session saves a checkpoint at a turn's end or yield only after 64 KiB of new records, instead of at every turn's end.
- The next prompt starts as soon as the previous turn ends, instead of waiting up to a second for the old worker to notice.
- On Vercel, prompt() runs the turn in the request that called it when no other invocation holds the session. Vercel Queues keeps the prompt until after that request's time limit and continues the turn if the request dies. world() takes runInRequest and waitUntil.
- On a World with Workflow's in-band writer fence, the World refuses a claim or record from a worker another has replaced, so none of it is stored.
- stream(cursor) starts reading at the cursor's line instead of the stream's start.
- A checkpoint option written by a newer libfx throws FxJournalVersionError.
- libfxApiVersion is 3.
- The SDK README explains when to set idempotent: false, and documents yieldAt, load()'s head and retryAfterSeconds in the Vercel trigger.
2026-10-12 15:53:20 -04:00
Pranay PrakashandClaude Opus 5.5 a7c4ab9adb Hold sessions on Vercel by their queue message, without lease renewals
- vercel() stops renewing a lease in the session's log while a turn runs.
  Vercel Queues keeps a delivery's message from every other delivery while
  the function handles it, so a worker holds its lease to its function's
  deadline, and the next delivery of the message takes over a session whose
  function died.
- world() takes `queueLeases` for any World whose queue holds and redelivers
  messages this way. A worker with no deadline still renews its lease.
- A worker whose write failed under such a lease has the queue deliver its
  own message again instead of sending a new one, so a lease it could not
  release is taken back at once rather than at its deadline.
- vercel({ leaseMs }) keeps the renewed lease, for a faster takeover after
  a silent crash.
- Model the queue-held lease in DurableSession.tla (QueueHeld): only the
  delivery the queue hands a frozen holder's message to may take its
  session over. Add the queue graph to check.sh and drive.mjs, run the core
  tests on a World with that queue, and add both to CI.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-12 01:05:25 -07:00
Pranit 0a76c1ee38 Merge origin/main and port the indexer to Zig 0.17
main moved to Zig 0.17. The branch keeps its deletion of
file_index_cache.zig, ignored_dirs.zig and workspace_files.zig, whose
only changes on main were 0.17 renames, and keeps its own side of
grep_search.zig and glob_files.zig. AGENTS.md takes main's refresh plus
the indexer and safe git notes.

The branch's code moves to the 0.17 names: std.Io.Dir.path,
Allocator.print, std.mem.print, builtin.target and @backingInt. The
wildmatch test builds its repeated strings with a local helper because
the indexer cannot import text_utils, and two folder selection test
calls gain the try that 0.17 requires.
2026-10-12 02:20:21 -04:00
Pranit fc93c230cd Request review for README and contributor guide changes
Add a CODEOWNERS file so any pull request that changes README.md,
CONTRIBUTING.md, or the libfx npm README in sdk/README.md requests a
review from @fazxes before it merges.
2026-10-12 02:13:36 -04:00
Pranit 72f352153f Merge remote-tracking branch 'origin/main' into fazxes/zig-0.17-upgrade
# Conflicts:
#	src/core/execution/command_runner.zig
#	src/gateway/host_model_catalog.zig
#	src/gateway/host_stream_provider.zig
#	src/gateway/js_host_model_catalog.zig
2026-10-11 22:13:10 -04:00
Pranit 28fd1a00ed Merge origin/main into fazxes/native-workspace-indexer 2026-10-11 05:59:04 -04:00
Pranit 1f510c93b1 Run libfx agents on a custom OpenAI-compatible provider
- Run a libfx agent on any server that speaks OpenAI Chat Completions by passing `provider: { name, baseUrl, models }`. Requests go to `{baseUrl}/chat/completions` through the host's `fetch`, with the CLI's own request and reply handling.
- Supply the provider's API key through the shared `credentials({ provider, sessionId })` callback, or omit it for a server that needs none. libfx adds the key only to that chat URL, so it never reaches the engine or a saved session.
- Check a custom provider's name, base URL and model details when the agent is created, require a `model`, and reject `onCredentials`, since a key is never refreshed.
- Continue a durable session on a custom provider in a new process. The crash test now covers Codex, Grok and a custom provider.
2026-10-11 04:20:12 -04:00
Pranit dabb6f2294 Run libfx agents on a Grok plan
- Run a libfx agent on Grok with `provider: "grok"`: model requests and the model list go to xAI's Grok endpoints through the host's `fetch` instead of AI Gateway.
- Supply Grok credentials through the shared `credentials({ provider, sessionId })` callback. libfx adds the token and account id only to Grok requests, so they never reach the engine or a saved session.
- Refresh Grok tokens five minutes before they expire and once after a 401, refuse a refreshed token for a different account, and pass new tokens to `onCredentials`.
- Continue a durable Grok session in a new process, which asks `credentials` for that session's credentials.
- Sign in with a Grok plan from `libfx/grok` with device code, or by pasting the callback address, and refresh tokens from there.
2026-10-11 02:42:37 -04:00
Pranit eb5100b7f1 Merge remote-tracking branch 'origin/main' into fazxes/zig-0.17-upgrade
# Conflicts:
#	README.md
#	src/gateway/host_stream_provider.zig
2026-10-11 01:39:52 -04:00
Pranit 9afca4ae0c Include libfx/chatgpt in the libfx publish check
- Publish libfx with `libfx/chatgpt` and its types, which the package file check now expects.
2026-10-10 20:58:10 -04:00
Pranit b9be2de170 Run libfx agents on Codex with ChatGPT sign-in
- Run a libfx agent on Codex with `provider: "codex"`: model requests and the model list go to ChatGPT's Codex endpoints through the host's `fetch` instead of AI Gateway.
- Supply each session's ChatGPT credentials through a `chatgpt` callback. libfx adds them only to Codex requests, so tokens never reach the engine or a saved session.
- Refresh ChatGPT tokens five minutes before they expire and once after a 401, and pass the new tokens to `onChatgptCredentials`.
- Continue a durable Codex session in a new process, which asks `chatgpt` for that session's credentials.
- Sign in with ChatGPT from `libfx/chatgpt` by pasting the callback address, or with device code where the workspace allows it, and refresh tokens from there.
2026-10-10 20:22:57 -04:00
Pranit d6b9be0885 Merge remote-tracking branch 'origin/main' into fazxes/zig-0.17-upgrade
# Conflicts:
#	src/builtins/mcp.zig
#	src/core/mcp/mcp_runtime.zig
#	src/core/mcp/project_config.zig
#	src/core/mcp/tool_search.zig
2026-10-10 18:38:05 -04:00
Pranit 9fdbeb4590 Take over a crashed durable session after a short lease
On a World that cannot tell whether a worker still runs, such as Vercel's,
a worker held its session's lease until its function's deadline, so a turn
whose function died waited for that deadline before another delivery
continued it. A worker now renews a short lease (leaseMs, 15 seconds by
default) every third of it while it runs, never past its deadline and never
after its release, so the session of a function that died frees once that
lease runs out. A renewal that another worker fenced out stops the running
turn at once. world() and vercel() accept leaseMs.
2026-10-09 07:49:30 -04:00
Pranit 041bd470b9 Merge remote-tracking branch 'origin/main' into fazxes/zig-0.17-upgrade
# Conflicts:
#	src/core/agent/runtime/parallel_execution.zig
2026-10-09 02:34:18 -04:00
Pranit 8de5bf9e9f Accept an app's World, ship types, and bound repeated deadline cut-offs
- `world()` from `libfx/durable-world` keeps sessions in a World the app
  creates, such as Workflow's Postgres World. `local()` and `vercel()` are
  built on it. libfx never starts or closes a World it was given, and agents
  that share one take its deliveries in turn instead of the last one taking
  over.
- `agent.newSessionId()` gives an id before the first prompt, every tool
  call receives `sessionId`, and `session(null)` throws like any other
  invalid id. The README states the id rule for each durability.
- TypeScript declarations for every entry point, wired through `exports` and
  checked under node16 and bundler resolution against the runtime exports.
- `createFxEngine()` names itself in its option errors, `createFxAgent()`
  refuses `env` when created, and the README lists what code written for
  0.0.12 now does differently.
- A step the deadline cuts off 3 times in a row is not run again: a cut-off
  call is answered as possibly run, and a model request cancels its turn.
  Records carry the step their turn is at, so continuing a cut-off model
  request counts as the same step. `session.deadline` reports `cutoffs`.
  The TLA+ model bounds it, a witness shows the endless rerun without the
  bound, and a new walk drives the code to the bound.
2026-10-08 22:27:30 -04:00
Pranit 8925f00b74 Add the native workspace indexer foundation
- Add src/core/indexer/, which reads gitignore rules, repository layout, and git config as files and never starts a process.
- Match git's ignore decisions for nested .gitignore files, info/exclude, and the global excludes file, including include and includeIf config.
- Report the filter drivers a repository's own config defines, and fail closed when that config cannot be read.
- Check in CI that outside code imports only indexer.zig and that the indexer imports nothing that can start a process.
2026-10-08 13:15:40 -04:00
Pranit 0475752342 Handle refused, failed, and cancelled turns in durable sessions
- A prompt the harness refuses or throws on ends its turn once with stop reason `error`, and the session's later prompts run.
- A turn cancelled or refused before it writes a record does not run again.
- A storage write that fails for any reason other than a takeover emits `session.error`, and the queue delivers the message again instead of reporting `session.fenced`.
- A prompt's `signal` cancels only that prompt's turn, whether it runs or still waits, and a signal that already aborted stores nothing.
- A retried `messageId` resolves to the original turn's stop reason, usage, and error with `repeated: true`.
- A session stream that fails to read for a reason other than not existing yet fails its views after a few tries instead of waiting forever.
- Each `agent.session(id, { context })` call uses its own context, and a session's worker is dropped once its deliveries end.
- A queue delivery that throws emits `session.error`.
- A resumed turn that ends but stays open stops its worker with `session.error` instead of running again.
2026-10-07 03:37:13 -04:00
Pranit 89de1f9db7 Check the durable modules in the published libfx package
- Publishing libfx accepts the package with `durable.js`, `fx-harness.js`, `durable/local.mjs`, and `durable/vercel.mjs`, and rejects one missing any of them.
- The crash matrix finds the bundled Worlds in `sdk/durable` by default.
2026-10-07 01:58:43 -04:00
Pranit b0d83eb894 Make libfx sessions durable
- `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.
2026-10-07 01:47:00 -04:00
Pranit 08ee83b6a8 Merge remote-tracking branch 'origin/main' into fazxes/zig-0.17-upgrade
# Conflicts:
#	src/core/images/png_downscale.zig
2026-10-05 21:52:33 -04:00