libfx #
libfx is the small fx agent kernel for JavaScript hosts. One agent is one
in-memory conversation with three operations: prompt, checkpoint, and
close.
npm install libfx
Node.js uses the native addon when available and falls back to WebAssembly. Browsers use WebAssembly with JSPI. The default package has no runtime dependencies and performs no MCP connection, skill scan, process spawn, or filesystem read when imported.
Agent #
import { createFxAgent } from "libfx";
const agent = await createFxAgent({
apiKey: process.env.AI_GATEWAY_API_KEY,
model: "google/gemini-2.5-flash-lite",
onEvent(event) {
if (event.type === "transport.response") console.log(event.elapsedMs);
},
});
const turn = agent.prompt("Explain this project.");
for await (const event of turn) {
if (event.type === "text_delta") process.stdout.write(event.delta);
}
console.log(await turn.result); // { stopReason, usage }
const checkpoint = await agent.checkpoint();
await agent.close();
apiKey is required. model is optional and defaults to fx's built-in model.
Agent configuration uses named options; env is reserved for
createFxTerminal().
The host selects the model. Agent creation does not fetch the Gateway model
catalog. Prompting can resolve model capabilities and context capacity through
the supplied fetch; fx caches that metadata for the agent.
onEvent receives runtime diagnostics separately from model output. Transport
events report request start, response status and elapsed time, safe Gateway
request metadata, and failures. Credentials and raw headers are never included.
libfx makes at most one automatic retry after a retryable transport failure and only before model output or tool effects escape. Cancellation prevents a retry.
prompt(input, { signal? }) accepts a string or text/resource blocks. It
returns an async iterable of normalized events:
text_deltareasoning_deltawhen supplied by the providertool_starttool_end
Consume the turn while it runs, then await turn.result. Output is lossless and
backpressured: a slow reader pauses production instead of growing an unlimited
event queue. Awaiting only turn.result can wait for an unread stream to drain.
If you only need the result, explicitly discard events:
const turn = agent.prompt("Update the index.");
for await (const _ of turn) {}
const result = await turn.result;
A turn has one event consumer. Breaking out of its iterator cancels the turn;
turn.cancel() and agent.close() also release blocked output. Transport or
message-decoding failures reject the result instead of returning success with
missing text.
Native transport buffers at most 8 MiB of output bytes. Unread SDK events apply backpressure at 1 MiB of encoded messages or 256 events. One message can exceed that threshold when the queue is empty; an individual encoded ACP message is limited to 64 MiB on both backends. These are transport bounds, not a total answer-size limit or a bound on retained conversation history.
Only one prompt may run at a time. checkpoint() is idle-only and returns
opaque, bounded, versioned bytes. Restore them only when creating a fresh
agent:
const restored = await createFxAgent({ apiKey, model, checkpoint });
An already-aborted prompt signal returns cancelled without a model request
or a history change. The next prompt can run normally.
The checkpoint contains conversation history and usage only. The host owns durable storage and must resupply models, credentials, instructions, tools, MCP clients, and skill records.
Models #
Model discovery is explicit and does not create an Agent or load native or Wasm artifacts:
import { listModels } from "libfx";
const models = await listModels({
apiKey: process.env.AI_GATEWAY_API_KEY,
});
listModels() performs one bounded Gateway request and returns sorted, unique
language-model IDs. It accepts the same optional fetch override as the Agent
API.
JavaScript tools and instructions #
const agent = await createFxAgent({
apiKey,
model,
instructions: "Keep answers concise.",
tools: [{
name: "lookup",
description: "Look up a value.",
inputSchema: {
type: "object",
properties: { key: { type: "string" } },
required: ["key"],
},
async execute(input, { signal }) {
return database.get(input.key, { signal });
},
}],
});
The JavaScript host is the authority for tool effects. The same descriptors,
schemas, cancellation, results, and events are used by N-API and WebAssembly.
Cancelling a prompt aborts its tools' signals and stops waiting for their
callbacks. Late results and rejections are ignored. Tools remain responsible
for stopping their own work when their signal is aborted.
Instructions are limited to 64 KiB of UTF-8 text, including text assembled by
the MCP and skills adapters. They are the complete host-owned system context:
libfx adds no hidden base prompt, and omitting instructions sends no system
message.
MCP #
libfx/mcp accepts a host-owned MCP client. Transport, authentication,
elicitation, and cleanup remain outside the kernel. The client uses the MCP
TypeScript SDK v1 signature: callTool(params, resultSchema?, options?), with
cancellation passed in options. Tool text and structured data
reach the model together. PNG, JPEG, GIF, and WebP tool images reach models that
advertise image input support; other models receive an explicit omission notice.
Images are retained in checkpoints within the existing checkpoint size limit.
Each image may contain up to 5 MiB of base64 data, with at most eight images and
an 8 MiB result frame. Ordinary host tool objects remain JSON text. Resource and
prompt options supply text instructions; non-text context has an omission notice.
Tool catalogs are paginated up to the existing 64-tool bound. Tool names are
normalized for model APIs, with collisions kept distinct and original names used
for calls to the MCP client. Each tool description and JSON schema may contain up
to 64 KiB, within the control message's 8 MiB limit.
import { createMcpAdapter } from "libfx/mcp";
const mcp = await createMcpAdapter(client, {
prefix: "github_",
resources: ["repo://instructions"],
prompts: ["review"],
});
const agent = await createFxAgent({
apiKey,
model,
tools: mcp.tools,
instructions: mcp.instructions,
});
// ...
await agent.close();
await mcp.close();
Skills #
Use libfx/skills for already-loaded records or libfx/skills/node to load a
SKILL.md explicitly in Node or Bun.
import { loadSkillFile } from "libfx/skills/node";
import { createSkillsAdapter } from "libfx/skills";
const record = await loadSkillFile("./skills/review/SKILL.md");
const skills = createSkillsAdapter([record]);
const agent = await createFxAgent({ apiKey, model, ...skills });
Backends #
await createFxAgent({ apiKey, backend: "auto" }); // native, then Wasm fallback
await createFxAgent({ apiKey, backend: "native" }); // require N-API
await createFxAgent({ apiKey, backend: "wasm" }); // require Wasm + JSPI
CommonJS applications can load the same Node API with require("libfx"). The
package chooses its generated CommonJS entry automatically and keeps asset
paths relative to the installed package.
Use getBackendInfo() to inspect backend availability without creating an
Agent or terminal:
import { getBackendInfo } from "libfx";
const info = await getBackendInfo({ surface: "agent", backend: "auto" });
// {
// surface: "agent",
// backend: "native" | "wasm-jspi" | "unavailable",
// attempts: [{ backend, available, reason }]
// }
surface is agent by default and may also be terminal. backend has the
same auto, native, and wasm selection as the factories. nativeAddon and
wasm select explicit assets with the same meanings as their factory options.
The probe loads and validates the native module or compiles the selected Wasm
asset, then stops. It does not create a core, open a runtime socket, read
credentials, start a model request, or write session state. A remote Wasm
source can perform its normal asset fetch.
Expected environmental failures resolve as structured attempts. Invalid
options reject with TypeError. The stable reason codes are:
| Code | Meaning |
|---|---|
LIBFX_UNSUPPORTED_PLATFORM |
No packaged native addon supports the current platform and architecture. |
LIBFX_NATIVE_ARTIFACT_MISSING |
The selected native addon file is absent. |
LIBFX_NATIVE_LOAD_FAILED |
Node could not load the selected native addon. |
LIBFX_NATIVE_API_MISMATCH |
The addon API version is incompatible. |
LIBFX_NATIVE_SURFACE_MISSING |
The addon does not implement the selected Agent or terminal surface. |
LIBFX_NATIVE_DISABLED |
nativeAddon: false disabled native loading. |
LIBFX_JSPI_UNAVAILABLE |
The current JavaScript runtime does not provide JSPI. |
LIBFX_WASM_LOAD_FAILED |
The selected Wasm asset could not be loaded or compiled. |
The optional causeCode field retains a Node error code such as ENOENT or
ERR_DLOPEN_FAILED when one exists. Probe success establishes backend loading
only; it does not validate credentials, a future Agent initialization, or a
model request.
Within one loaded SDK module, libfx compiles each stable Wasm source once and creates a separate WebAssembly instance for every Agent. Agent memory, history, tools, cancellation, and shutdown remain isolated. Workers and separate processes maintain their own module caches, as do the ESM and CommonJS entries.
Node factories and getBackendInfo() also accept a wasm Promise resolving
to an HTTP(S) URL string, a Response, Wasm bytes, or a compiled
WebAssembly.Module. Asset resolver failures propagate from factories and
appear as LIBFX_WASM_LOAD_FAILED in diagnostics.
Node.js 20+ is supported. Browser WebAssembly requires a JSPI-capable browser.
The Linux x64 and arm64 native addons require glibc 2.34 or newer. Native
agents do not require JSPI or experimental Node flags.
Some Node versions require --experimental-wasm-jspi.
Bun 1.4.2 is the tested recommendation for Bun's WebAssembly backend.
Bun 1.3.14 can crash when a hot WebAssembly loop resumes through JSPI during
JIT tier-up.
Next.js and Vercel #
Create agents in a server route using the Node.js runtime. Import libfx
normally; the package includes its native assets and exposes both ESM and
CommonJS Node entrypoints. Native agents support Next.js 15 with webpack and
Next.js 16 with webpack or Turbopack, without serverExternalPackages or manual
native-file inclusion. Webpack's emitted assets are resolved relative to the
server bundle, including standalone builds with a custom distDir or assetPrefix.
This native setup does not require JSPI. Explicit WebAssembly use still needs
JSPI and available Wasm assets; Next.js's standalone tracer excludes .wasm
files, so a standalone Wasm host must supply those assets separately.
import { createFxAgent } from "libfx";
export const runtime = "nodejs";
export async function POST(request) {
const { prompt } = await request.json();
const agent = await createFxAgent({ apiKey: process.env.AI_GATEWAY_API_KEY });
try {
let text = "";
const turn = agent.prompt(prompt, { signal: request.signal });
for await (const event of turn) {
if (event.type === "text_delta") text += event.delta;
}
await turn.result;
return Response.json({ text });
} finally {
await agent.close();
}
}
Use the application's normal authentication and request limits around the route. JavaScript tools and MCP clients remain host-owned and must be supplied when creating an agent, including after checkpoint restoration. The native backend does not enable the CLI's built-in shell or filesystem tools.
Interactive terminal #
createFxTerminal() remains a separate terminal harness API. In browsers,
connect it to xterm.js with xtermAdapter():
import { createFxTerminal, xtermAdapter } from "libfx/browser";
const runtime = await createFxTerminal({
terminal: xtermAdapter(term),
env: { AI_GATEWAY_API_KEY: "<short-lived credential>" },
});
await runtime.interactive;
The xterm adapter preserves browser-style composer editing for Shift+Enter, Command+A, Command+C, Command+X, Command+Z, and Command+Shift+Z. Shift+Enter inserts a newline without submitting. A click inside the visible composer moves its caret; pointer drags remain xterm terminal-output selections. When xterm already has an output selection, Command+C copies that selection instead of the composer selection.
The terminal runtime exposes interactive, exited, write, resize, and
abort. Terminal session, config, OAuth, prompt-history, clipboard, URL, and
workspace stores remain terminal-only host integrations. Clipboard copy writes
through the host clipboard.writeText(text) adapter and defaults to
navigator.clipboard.
During /compact and automatic compaction, the terminal shows a live
Compacting activity row with elapsed time. Input and cancellation remain
responsive while the summary request is pending. Compaction progress and
outcomes do not add transcript entries, including cancellation after resume.
Stored snapshots retain cancellation-origin metadata; keep them opaque and
resume with the same or a newer SDK build. Older snapshots remain readable.
Security #
Treat nativeAddon and gatewayChatUrl as trusted host
configuration. Do not embed long-lived credentials in public browser code.
Host tool functions, MCP clients, and skill loaders retain their own authority;
libfx validates and sequences them but does not grant operating-system access.