Durability

A libfx session outlives the process that runs it. libfx saves each prompt and each step of a turn as it happens, so when a server crashes, redeploys, or reaches a function's time limit, the next process continues the turn where it stopped. Your code stays the same.

Available in libfx@dev

Durable sessions are in the dev release of libfx until 0.0.14. Install it with npm install libfx@dev.

Use a session

Create one agent for your server, and open a session for each conversation:

import { createFxAgent } from 'libfx'

export const agent = createFxAgent({ model: 'anthropic/claude-opus-5.5' })
const turn = agent.session().prompt('Plan the migration.')
const { sessionId } = await turn.accepted

// Later, from any process:
agent.session(sessionId).prompt('Start with the schema.')

turn.accepted resolves once the prompt is saved, so an accepted prompt is never lost. To show a conversation, return agent.session(sessionId).stream(cursor) from a route. Any server can serve it, and a client that loses its connection reconnects from the last cursor it read.

One process runs a session's turns at a time. A process that was replaced while it ran is refused at its next write, so a session's history never mixes two writers.

Where sessions live

libfx chooses where to keep sessions from the environment, so there is nothing to configure:

EnvironmentSessions live in
VercelVercel's World, the store and queue behind Vercel Workflow
Node.jsFiles in FX_SESSIONS_DIR, or in the system's temporary directory
BrowsersThe page's memory, for the life of the page

On Vercel

Add one route, app/api/libfx/route.js:

import { agent } from '@/lib/agent'

export const POST = agent.wakeHandler()

Then subscribe it to libfx's queue in vercel.json:

{
  "functions": {
    "app/api/libfx/route.js": {
      "experimentalTriggers": [{ "type": "queue/v2beta", "topic": "__libfx_wkf_workflow_session" }]
    }
  }
}

That's all. prompt() sends each turn to Vercel Queues, and this route runs it. A turn whose function crashes continues about 15 seconds later, and a turn longer than the function's time limit continues in the next invocation. libfx reaches AI Gateway with the deployment's OIDC token, so there is no key to set.

Tools with effects

A turn can stop while one of its tool calls runs. Mark the tools that are safe to run twice with idempotent: true, and pass executionId to the services your other tools call:

const byId = { type: 'object', properties: { id: { type: 'string' } }, required: ['id'] }

const tools = [
  {
    name: 'get_order',
    description: 'Look up an order.',
    idempotent: true,
    inputSchema: byId,
    execute: ({ id }) => orders.get(id),
  },
  {
    name: 'refund_order',
    description: 'Refund an order.',
    inputSchema: byId,
    execute: ({ id }, { executionId }) =>
      payments.refund(id, { idempotencyKey: executionId }),
  },
]

When the turn continues, an idempotent call runs again. Any other call never runs again on its own: the model hears that it may have partly run, so it can check before trying again or ask the user. executionId stays the same each time a call runs, so a payment service that uses it as an idempotency key never refunds twice.

Your own store

To keep sessions somewhere else, such as Workflow's Postgres World, pass a World to world() from libfx/durable-world. The libfx README covers its options.