npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@appport/fx

v0.1.2

Published

Embed fx agents and terminals in JavaScript hosts

Readme

libfx

libfx is the small fx agent kernel for JavaScript hosts. One agent is one in-memory conversation with prompt, checkpoint, and close operations, plus mid-turn steering on each running turn.

npm install @appport/fx

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 "@appport/fx";

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();

With a string model or no model, the agent uses the Vercel AI Gateway: apiKey is required and model defaults to fx's built-in model. To use Ollama or another OpenAI-compatible provider instead, pass a createFxModel() handle as model; no Gateway account, key, or URL is involved. See Local and direct providers. Agent configuration uses named options; env is reserved for createFxTerminal(). The canonical model configuration groups the model ID and model-specific options:

const agent = await createFxAgent({
  apiKey,
  model: { id: "anthropic/claude-opus-5.5-fast", effort: "low", fast: true },
});

A string model remains supported as shorthand. Top-level effort and fast are deprecated but remain supported with a string model or no model; they cannot be mixed with a model object. New code should use the model object.

model.effort sets the reasoning effort for models that advertise effort levels. It uses the same vocabulary as the fx CLI's --effort flag: "default" leaves the choice to the model; named levels such as "low", "medium", "high", or "xhigh" request a specific level. A named level is validated at creation; an unsupported level rejects with an Error carrying code: "LIBFX_MODEL_UNSUPPORTED_EFFORT", model, and capability: "effort". Its message names the supported set when available. Omitting effort or using "default" leaves the model default in place.

model.fast enables the fast lane for models that advertise one, matching the fx CLI's --fast flag. A model without a fast path rejects at creation with code: "LIBFX_MODEL_UNSUPPORTED_FAST", model, and capability: "fast". Omitting fast or setting it to false leaves the model default in place.

model.ultrafast requests Ultra mode. It is off by default and maps to openai.serviceTier: "ultrafast" through the Vercel AI Gateway for models whose metadata advertises Ultra eligibility. It uses the higher-cost service tier. Set it to true only after the host has selected an eligible model; an unsupported request rejects with code: "LIBFX_MODEL_UNSUPPORTED_ULTRAFAST", model, and capability: "ultrafast". Set it to false to explicitly disable an inherited request. Ultra and Fast are mutually exclusive, so ultrafast: true disables Fast for the agent. Gateway metadata currently marks Astra eligible; libfx does not select Ultra automatically.

The new codes replace LIBFX_UNSUPPORTED_EFFORT and LIBFX_UNSUPPORTED_FAST for both nested and legacy top-level settings. Callers that check the old codes must update their error handling.

The host selects the model. Agent creation does not fetch the Gateway model catalog unless effort requests a named level, fast is enabled, or ultrafast is enabled. 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, failures, and throttled transport.activity liveness while a response body is streaming. Activity events include the attempt, current chunk bytes, and cumulative response bytes. They call the host directly at most once per 250 ms rather than entering the normalized turn queue, so an unread turn cannot accumulate heartbeat events. 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, image, and resource blocks. It returns an async iterable of normalized events:

  • text_delta
  • reasoning_delta when supplied by the provider
  • tool_start with id, name, and the tool's input object. Inputs over 64 KiB of JSON arrive as an inputPreview string prefix plus inputTruncated: true instead; the tool still receives complete arguments.
  • tool_end
  • user_message with text when accepted mid-turn steering enters the turn

Consume the turn while it runs, then await turn.result. Streamed text and tool results are 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. Embedded agents have no implicit model-step cap, so hosts should cancel turns that exceed their own budgets. The CLI's max_agent_steps setting does not apply to createFxAgent(). 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.

Image blocks accept a Blob or File with a non-empty type, raw bytes (Uint8Array, Buffer, ArrayBuffer, or another typed array) with an explicit mimeType, or canonical base64 (no line wrapping) with an explicit mimeType. The payload must be PNG, JPEG, GIF, or WebP. For a Blob, the MIME type is inferred from Blob.type; an empty type or a conflicting explicit mimeType rejects the input:

const turn = agent.prompt([
  { type: "text", text: "What does this screenshot show?" },
  { type: "image", data: file, sourceRef: "uploads:screenshot-1" }, // File or Blob, with file.type
  // Or: { type: "image", data: pngBytes, mimeType: "image/png" }
  // Or: { type: "image", data: base64Png, mimeType: "image/png" }
]);

An optional sourceRef identifies a host-owned original. It must be a non-empty UTF-8 string of at most 512 bytes, without ASCII control characters (0–31 or DEL). With a reference, you may omit data entirely:

agent.prompt([
  { type: "image", mimeType: "image/png", sourceRef: "uploads:screenshot-1" },
]);

Image bytes reach the agent core beside the ACP prompt message rather than inside it. Base64 input is decoded before transfer; prompt image bytes are encoded as base64 only for the model request. A prompt may contain up to 8 images, including reference-only blocks. Each image may contain up to 3.75 MiB (3,932,160 bytes) of raw data, with at most 6 MiB of raw image data per prompt. Once encoded for the model request, those limits are 5 MiB per image and 8 MiB per prompt. The prompt's text, encoded images, and metadata must fit within the same 8 MiB frame budget.

Without resizeImage, the SDK checks Blob size before reading it and the actual byte count after reading it. If referenced image data would exceed a per-image, aggregate image, or combined frame limit, the SDK sends its MIME type and reference without image data. Referenced Blobs known to exceed those limits remain unread and host-owned. A reference does not increase the limits; unreferenced oversized input rejects with typed RangeErrors.

Base64 that is not canonical throws a TypeError from prompt(). Raw bytes are copied before prompt() returns, so the caller can reuse its buffer. Blob reads are asynchronous: prompt() returns a turn, and read failures reject turn.result. Cancelling or closing during a Blob read settles the turn without sending its prompt. Without resizeImage, size errors for unreferenced base64 and raw-byte input throw synchronously from prompt().

To downscale or convert prompt images before they are sent, pass the optional resizeImage hook when creating the agent. It receives { bytes, mimeType }, where bytes is a Uint8Array, and returns { bytes, mimeType } directly or as a promise. Returned bytes may be any typed array or ArrayBuffer. For a Node.js host that already uses sharp:

import sharp from "sharp";

const agent = await createFxAgent({
  apiKey,
  model,
  async resizeImage({ bytes }) {
    const png = await sharp(bytes).resize({ width: 1568, withoutEnlargement: true }).png().toBuffer();
    return { bytes: png, mimeType: "image/png" };
  },
});

In a browser, return the ArrayBuffer from Blob.arrayBuffer(), including a Blob produced by OffscreenCanvas.convertToBlob(). The returned bytes are copied, so the hook may reuse its buffer. With resizeImage, image size limits apply to its output rather than its input; image count and frame metadata limits still apply. Image prompts with data are prepared asynchronously like a Blob prompt, and a failure inside the hook rejects turn.result. This opt-in preprocessing reads supplied Blob data even when its original size exceeds the limits. Reference-only blocks have no bytes and do not call the hook or fetch the original. The hook is host-provided, not a built-in image converter or a libfx runtime dependency.

The kernel sniffs the final bytes and compares them with the claimed MIME type for every input form that carries data; a mismatch fails the turn with Invalid image prompt block. Eligible images reach the model unchanged unless your hook changes them. Images are routed only to models that advertise image input; for any other model the turn fails with Image prompts are unavailable for the selected model and no image bytes leave the process. Images outside the request's pixel or encoded-size limits, and reference-only originals, are withheld with model-visible recovery feedback that includes their sourceRef when supplied.

A source reference is metadata, not an access grant or an automatic fetch. Your host owns the original's lifetime, reference resolution, and authorization. Expose preparation or retrieval through your existing tools' execute() callbacks if the agent needs a smaller copy. libfx has no built-in image resizer, shell, global converter, or source store, and a reference grants no native filesystem authority. If a tool is absent or fails, the original remains withheld rather than being sent anyway.

For example, a tool can delegate to your app's authorized image store:

const prepareImage = {
  name: "prepare_image",
  description: "Return a new smaller copy of a host-owned image. Use the request limit for maxSide.",
  inputSchema: {
    type: "object",
    properties: { sourceRef: { type: "string" }, maxSide: { type: "integer" } },
    required: ["sourceRef", "maxSide"],
  },
  async execute({ sourceRef, maxSide }, { signal }) {
    const copy = await imageStore.prepareCopy(sourceRef, { maxSide, signal });
    return {
      type: "libfx.tool-result",
      text: "Prepared a new copy; original unchanged.",
      images: [{ type: "image", data: copy.base64, mimeType: copy.mimeType, sourceRef }],
    };
  },
};
// Supply prepareImage in createFxAgent({ tools: [prepareImage], ... }).

imageStore is application code, not a libfx API. It must authorize references, validate the requested dimensions, and stop conversion when signal aborts. The returned copy is checked again by the kernel before model submission. Typed host-tool images still use base64 data; resizeImage prepares prompt images only.

Version 2 checkpoints retain prompt images as raw image blobs and preserve source refs within the existing 4 MiB checkpoint bound on both backends. A checkpoint does not store a source file or host-owned original for a reference-only block. On restoration, resupply your tools and restore the sources those refs identify.

Only one top-level prompt may run at a time. While it runs, await turn.steer(text) appends guidance at the next safe model boundary without discarding the in-flight response or completed tool work. Steering also accepts an array of text blocks; image and resource steering blocks are rejected. Each message is limited to 64 KiB, with at most 64 queued messages and 1 MiB of queued steering text. Accepted steering appears as a user_message event before the model's continued output. For a Blob prompt or one prepared by resizeImage, steering during that preparation waits for the prompt to be sent; cancelling before then rejects the pending steering. Calling steer() after the turn settles rejects with no prompt is running.

const turn = agent.prompt("Build the feature.");
for await (const event of turn) {
  if (event.type === "tool_end" && event.name === "read_file") {
    await turn.steer("Keep the public API backward compatible.");
  }
}

Cancelling a steered turn drops any guidance that has not reached a safe boundary and releases its queue. Applied guidance is part of the same history turn, so an idle checkpoint() includes the full steered conversation. checkpoint() returns opaque, bounded, versioned bytes. Concurrent calls run one at a time, and a call still waiting for an earlier one fails the same way a direct call would if a prompt starts or the agent closes first. A newer libfx restores checkpoints from older versions, but an older libfx cannot restore one written by a newer version. 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. Reasoning effort, Fast mode, and Ultra mode are agent-creation options and are not stored in a checkpoint: recreate the agent with new model.effort, model.fast, or model.ultrafast values to change them, the same path as switching models.

Models

Model discovery is explicit and does not create an Agent or load native or Wasm artifacts:

import { listModels } from "@appport/fx";

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.

Local and direct providers

createFxAgent() selects its provider from model:

| model | Provider | Credential | | --- | --- | --- | | A string, a { id, effort, fast, ultrafast } object, or omitted | Vercel AI Gateway (the default) | apiKey is required | | A createFxModel() handle | The endpoint you configured, called directly | None from fx; use apiKeyEnv on the model if the endpoint needs one |

import { createFxAgent, createFxModel } from "@appport/fx";
import { createMcpAdapter } from "@appport/fx/mcp";

const model = await createFxModel({
  baseUrl: "http://localhost:11434/v1", // Ollama
  model: "qwen3-coder",
});
const mcp = await createMcpAdapter(mcpClient); // for example the AppBoundry MCP server
const agent = await createFxAgent({ model, tools: mcp.tools, instructions: mcp.instructions });

const turn = agent.prompt("Which capabilities are enabled?");
for await (const event of turn) {
  if (event.type === "text_delta") process.stdout.write(event.delta);
}
console.log(await turn.result); // { stopReason, usage }

With a model handle, fx runs the agent loop in JavaScript: it streams one model request, executes the structured tool calls through your tools (including MCP tools), returns each result to the model, and repeats until the model answers without a tool call. Events, turn.result, steer(), cancellation, and checkpoint() behave as documented above, with these differences:

  • No request goes to the Gateway, and fetch is not used. The model handle's native provider makes the requests, so this path needs the Node addon.
  • apiKey, gatewayChatUrl, fetch, and the effort, fast, and ultrafast options are rejected, so a stale Gateway setting is never silently ignored. Provider-executed tools are also rejected.
  • maxSteps (default 100) bounds model requests per prompt; reaching it ends the turn with stopReason: "max_turn_requests". maxOutputTokens is optional.
  • stopReason is end_turn, cancelled, or max_turn_requests. A provider failure rejects the turn: an HTTP error or a model that writes a tool call as plain text rejects with code: "LIBFX_MODEL_FAILED" and the failure kind (for example server_error, rate_limited, malformed_tool_call), and failures without a provider response, including a truncated or filtered output, an unadvertised tool name, or invalid tool-argument JSON, reject with the model's LIBFX_MODEL_REQUEST_FAILED error. The failed prompt is not added to history. A hand-written model object that reports finish_reason length or content_filter ends the turn with max_tokens or refused.
  • A tool that throws, returns an error, or is not registered returns an error result to the model, and the turn continues.
  • breaking out of the event loop, turn.cancel(), an aborted signal, and agent.close() abort the in-flight provider request and settle turn.result as cancelled; a cancelled turn is removed from history.
  • onEvent receives model.request and model.response diagnostics. The transport events of the Gateway agent do not exist on this path.
  • checkpoint() rejects while a prompt is running.
  • Prompts are text only (image blocks throw), and tool results reach the model as text.
  • A checkpoint made by a direct agent restores only into a direct agent. It holds the model-visible messages, not Gateway session state.
  • Verification: the protocol and loop are covered by deterministic tests with a scripted provider; real-model tool quality and live MCP endpoints are not. See the certification report.
  • The loop does not run fx's permission review or built-in tools; your tools carry their own authority, as for the Gateway agent.

Direct model calls

createFxModel() sends chat requests to an OpenAI-compatible endpoint through the native fx provider without creating an Agent. It runs no agent loop, tool execution, or permission review. When the model asks for a tool, the result contains the tool call and your code decides what to run. The model API requires the native Node addon; it has no Wasm or browser implementation.

import { createFxModel } from "@appport/fx";

const model = await createFxModel({
  baseUrl: "http://localhost:11434/v1",
  model: "qwen3-coder",
});

const result = await model.chat({
  messages: [
    { role: "system", content: "Answer briefly." },
    { role: "user", content: "What is 2 + 2?" },
  ],
});

if (result.completed) console.log(result.completed.content);
else console.error(result.failed.kind, result.failed.detail);

| Option | Description | | --- | --- | | baseUrl | Endpoint prefix. fx posts to <baseUrl>/chat/completions. Plain http is accepted only for localhost, 127.0.0.1, and [::1]. | | model | Model ID sent with every request. | | apiKeyEnv | Optional environment variable name. fx reads the variable on each call and sends its value as a bearer token; the key never passes through JavaScript. Without it, requests carry no Authorization header. | | toolChoiceMode | "omit" (default) or "send". With "omit", tool_choice is enforced on the response but not sent, because many local servers reject the field. | | id | Provider identifier. Defaults to "openai-compatible". | | nativeAddon | Optional explicit addon, with the same meaning as for createFxAgent(). |

A request contains messages and, optionally, tools, tool_choice ("auto", "none", or "required"), and max_output_tokens. Each message has a role of system, user, assistant, or tool and string content. An assistant message that requested tools carries tool_calls, and a tool result carries the matching tool_call_id:

const result = await model.chat({
  messages: [
    { role: "user", content: "Weather in Oslo?" },
    {
      role: "assistant",
      content: null,
      tool_calls: [{ id: "call_1", name: "weather", arguments_json: '{"city":"Oslo"}' }],
    },
    { role: "tool", tool_call_id: "call_1", content: '{"celsius":3}' },
  ],
  tools: [{
    name: "weather",
    description: "Current weather for a city",
    input_schema: { type: "object", properties: { city: { type: "string" } }, required: ["city"] },
  }],
});

chat() resolves to one of two shapes:

  • { completed } contains content, tool_calls (each with id, name, and arguments_json), finish_reason (stop, tool_calls, length, content_filter, or null), response_id, and usage with input_tokens, output_tokens, cache_read_tokens, cache_write_tokens, and reasoning_tokens. A field the provider did not report is null.
  • { failed } reports a response that cannot be used. For an HTTP error it contains kind (such as rate_limited or server_error), the response body as detail with any credential redacted, and retry_after_seconds from a Retry-After header or null. The kind malformed_tool_call means the model wrote a tool call as plain text instead of returning a structured one; see below.

A tool call is real only when it arrives in completed.tool_calls. When tools are advertised and tool_choice is not "none", a normal stop with no structured calls whose text opens <function=NAME> on its own line for an advertised tool, followed by <parameter= and </function>, outside a fenced code block, resolves { failed: { kind: "malformed_tool_call", detail } }. detail is the model's original text. fx never parses or runs it, and does not retry. Text that only mentions the syntax, fenced examples, and replies with structured tool calls are unaffected.

chat() rejects with an Error whose code identifies failures that produced no provider response: LIBFX_MODEL_CANCELLED (also named AbortError), LIBFX_MODEL_TIMEOUT, LIBFX_MODEL_CREDENTIAL_MISSING, LIBFX_MODEL_CREDENTIAL_INVALID, or LIBFX_MODEL_REQUEST_FAILED. The message of a LIBFX_MODEL_REQUEST_FAILED error names the native cause, such as ConnectionRefused, or RequiredToolMissing when tool_choice: "required" produced no tool call. createFxModel() rejects an unusable baseUrl with LIBFX_INVALID_MODEL_CONFIG.

Streaming

stream() returns an async iterator of events as the provider produces them:

for await (const event of model.stream(request)) {
  if (event.type === "text_delta") process.stdout.write(event.text);
  if (event.type === "completion") console.log(event.completion.finish_reason);
  if (event.type === "failure") console.error(event.failure.kind);
}

text_delta and reasoning_delta events carry text in provider order. The stream then ends with exactly one completion event, whose completion has the same shape as chat()'s completed, or one failure event. Tool calls arrive complete in completion.tool_calls, because the OpenAI-compatible stream reducer does not emit partial tool-call events. Cancellation and request errors are thrown from the iterator with the same codes as chat(). Leaving the loop early cancels the request.

Cancellation and concurrency

Both methods accept { signal }. Aborting the signal cancels the active HTTP request and closes its connection. chat() then rejects, or the stream throws, with LIBFX_MODEL_CANCELLED. An already-aborted signal fails the call before any request is sent.

Each call runs on its own native thread, so the event loop stays responsive and calls on one or several models run concurrently without occupying the libuv thread pool that Node's file system, DNS, and crypto work share. A process can have up to 256 calls in flight; beyond that, a new call fails with LIBFX_MODEL_CALL_LIMIT until another finishes. Terminating a worker thread cancels the calls it started.

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 });
    },
  }],
});

Gateway web search can run at the provider instead of in the JavaScript host. Mark its canonical tool name with providerExecuted: true and omit execute:

const agent = await createFxAgent({
  apiKey,
  tools: [{ name: "web_search", providerExecuted: true }],
});

The kernel supplies the canonical schema and Gateway advertisement. Currently web_search is the supported provider-executed descriptor; unknown or local names reject agent creation. Provider-executed tools do not call host code or require a separate provider key; their tool_start and tool_end events, results, and checkpoint history use the same turn contract. Their built-in permission policy is enforced when the request is projected, as there is no local call-time effect to approve.

For ordinary tools, the JavaScript host is the authority for 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.

A host tool may use any name, including the kernel's builtin names such as write_file and edit_file: the kernel routes by the registered executor, so a host-defined write_file calls the host's execute() rather than the builtin file mutation.

Ordinary objects returned by tools are JSON text. To return rich image content, use the typed result:

return {
  type: "libfx.tool-result",
  text: "Original is available through the host image tools.",
  images: [
    { type: "image", mimeType: "image/png", sourceRef: "uploads:screenshot-1" },
    // A prepared copy may also include data: base64Png.
  ],
};

Tool images require mimeType and accept base64 data, or a sourceRef with no data. They do not accept raw bytes or Blobs and do not run resizeImage. References use the same validation, ownership, recovery feedback, and checkpoint rules as prompt images. A result may contain up to 8 images, each with up to 5 MiB of base64-encoded data. Its text, encoded images, and metadata must fit within the 8 MiB result/frame bound. Referenced data is omitted if those bounds would overflow; an unreferenced oversized result remains a tool error.

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 "@appport/fx/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 "@appport/fx/skills/node";
import { createSkillsAdapter } from "@appport/fx/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("@appport/fx"). 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 "@appport/fx";

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. The npm package ships native addons for darwin-arm64, darwin-x64, linux-arm64, and linux-x64. The packed tarball is installed and exercised from ESM and CommonJS by tests/test-packed-consumer.mjs on every release, on the platform that builds it; the other addons are built and checked in the release but not loaded there. Windows has no native addon. 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 "@appport/fx";

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 "@appport/fx/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.