libfx 0.0.15 → 0.0.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/node.js CHANGED
@@ -1,28 +1,26 @@
1
- import { access, readFile } from "node:fs/promises";
1
+ import { access } from "node:fs/promises";
2
2
  import { closeSync } from "node:fs";
3
3
  import { createRequire } from "node:module";
4
4
  import { Socket } from "node:net";
5
5
  import { homedir, tmpdir } from "node:os";
6
6
  import { isAbsolute, resolve } from "node:path";
7
7
  import { fileURLToPath, pathToFileURL } from "node:url";
8
+ import { bundledAssetUrl } from "./bundled-asset.js";
8
9
  import { CoreOutput } from "./core-output.js";
9
- import { loadModule, withModuleFailure } from "./wasm-module.js";
10
10
  import { fetchCleanupAction, cleanupTimeoutMs, cleanupByteLimit } from "./fetch-cleanup.js";
11
11
  import { createDurableAgentFactory, memory, wakeHandler } from "./durable.js";
12
12
  import { fxHarness } from "./fx-harness.js";
13
13
  import {
14
- createFxEngine as createWasmAgent,
15
- createFxTerminal as createWasmTerminal,
14
+ createFxEngine as createEngine,
16
15
  createMemoryPersistence,
17
16
  FxFencedError,
18
17
  FxJournalVersionError,
19
- encodeXtermKeyEvent,
20
18
  fxSdkApiVersion,
21
19
  listModels,
22
20
  normalizeAgentOptions,
23
21
  supportsJspi,
24
- xtermAdapter,
25
- } from "./fx-sdk.js";
22
+ } from "./engine.js";
23
+ import { encodeXtermKeyEvent, xtermAdapter } from "./xterm.js";
26
24
 
27
25
  export { createMemoryPersistence, encodeXtermKeyEvent, FxFencedError, FxJournalVersionError, fxSdkApiVersion, listModels, memory, supportsJspi, wakeHandler, xtermAdapter };
28
26
  export const libfxApiVersion = 3;
@@ -33,10 +31,7 @@ const fetchOperationApplied = 1;
33
31
  const fetchOperationBackpressure = 2;
34
32
 
35
33
  const nodeRequire = createRequire(import.meta.url);
36
- const defaultCoreWasm = new URL("./fx-core.wasm", import.meta.url);
37
- const defaultTermWasm = new URL("./fx-term.wasm", import.meta.url);
38
34
  let nativeBackendPromise;
39
- const wasmFilePromises = new Map();
40
35
 
41
36
  const backendReasonCodes = {
42
37
  unsupportedPlatform: "LIBFX_UNSUPPORTED_PLATFORM",
@@ -49,25 +44,24 @@ const backendReasonCodes = {
49
44
  wasmLoad: "LIBFX_WASM_LOAD_FAILED",
50
45
  };
51
46
 
52
- function bundledAssetUrl(asset) {
53
- if (asset.protocol !== "" || typeof asset.href !== "string" ||
54
- typeof __webpack_base_uri__ === "undefined" || typeof __webpack_public_path__ !== "string" ||
55
- !asset.href.startsWith(__webpack_public_path__)) {
56
- throw new TypeError("Bundled asset URL has no filesystem mapping");
57
- }
58
- // Webpack's relative URL is a public asset address, not a Node URL instance.
59
- return new URL(asset.href.slice(__webpack_public_path__.length), __webpack_base_uri__);
47
+ function sentence(message) {
48
+ return `${message[0].toUpperCase()}${message.slice(1)}.`;
60
49
  }
61
50
 
62
- function jspiFallbackError(surface, nativeError) {
63
- const nativeDetail = nativeError ? ` Native loading failed: ${nativeError.message}.` : " No compatible native addon was found.";
51
+ function jspiFallbackError(surface, native, nativeError) {
52
+ const missingPackage = !nativeError && native?.failure === "missing" ? native.nativePackage : undefined;
53
+ const nativeDetail = nativeError ? ` Native loading failed: ${nativeError.message}.`
54
+ : missingPackage ? ` ${sentence(nativeFailureReason(native).message)}`
55
+ : " No compatible native addon was found.";
56
+ const remedy = missingPackage
57
+ ? `Install ${missingPackage}, which libfx lists as an optional dependency, or run Node with --experimental-wasm-jspi.`
58
+ : "Run Node with --experimental-wasm-jspi or install a libfx package containing a compatible native addon.";
64
59
  const error = new Error(
65
60
  `libfx could not start the ${surface} backend.${nativeDetail} ` +
66
- "The WebAssembly fallback requires JavaScript Promise Integration (JSPI). " +
67
- "Run Node with --experimental-wasm-jspi or install a libfx package containing a compatible native addon.",
61
+ `The WebAssembly fallback requires JavaScript Promise Integration (JSPI). ${remedy}`,
68
62
  );
69
63
  error.code = "LIBFX_JSPI_REQUIRED";
70
- error.cause = nativeError;
64
+ error.cause = nativeError ?? native?.probeError;
71
65
  return error;
72
66
  }
73
67
 
@@ -79,7 +73,8 @@ async function loadNativeCandidate(candidate) {
79
73
  // Bundlers trace the asset URL; Node must load the native file at runtime.
80
74
  return Reflect.apply(nodeRequire, undefined, [fileURLToPath(candidate)]);
81
75
  }
82
- const imported = await import(candidate.href);
76
+ // A module the caller names is its own file, which Node loads as is.
77
+ const imported = await import(/* webpackIgnore: true */ /* turbopackIgnore: true */ candidate.href);
83
78
  return imported.default ?? imported;
84
79
  }
85
80
  if (typeof candidate === "object") return candidate.default ?? candidate;
@@ -89,37 +84,54 @@ async function loadNativeCandidate(candidate) {
89
84
  if (candidate.endsWith(".node")) {
90
85
  return Reflect.apply(nodeRequire, undefined, [isAbsolute(candidate) ? candidate : resolve(candidate)]);
91
86
  }
92
- const imported = await import(candidate.startsWith("file:") ? candidate : pathToFileURL(candidate).href);
87
+ const imported = await import(/* webpackIgnore: true */ /* turbopackIgnore: true */ candidate.startsWith("file:") ? candidate : pathToFileURL(candidate).href);
93
88
  return imported.default ?? imported;
94
89
  }
95
90
 
96
- function defaultNativeCandidate() {
97
- // Local path bindings let deployment tracers retain these assets in the generated CommonJS entry.
98
- if (process.platform === "linux" && process.arch === "x64") {
99
- const asset = new URL("./libfx.linux-x64.node", import.meta.url);
100
- if (asset.protocol === "") return fileURLToPath(bundledAssetUrl(asset));
101
- const path = fileURLToPath(asset);
102
- return path;
103
- }
104
- if (process.platform === "linux" && process.arch === "arm64") {
105
- const asset = new URL("./libfx.linux-arm64.node", import.meta.url);
106
- if (asset.protocol === "") return fileURLToPath(bundledAssetUrl(asset));
107
- const path = fileURLToPath(asset);
108
- return path;
109
- }
110
- if (process.platform === "darwin" && process.arch === "x64") {
111
- const asset = new URL("./libfx.darwin-x64.node", import.meta.url);
112
- if (asset.protocol === "") return fileURLToPath(bundledAssetUrl(asset));
113
- const path = fileURLToPath(asset);
114
- return path;
115
- }
116
- if (process.platform === "darwin" && process.arch === "arm64") {
117
- const asset = new URL("./libfx.darwin-arm64.node", import.meta.url);
118
- if (asset.protocol === "") return fileURLToPath(bundledAssetUrl(asset));
119
- const path = fileURLToPath(asset);
120
- return path;
121
- }
122
- return null;
91
+ // Each platform's addon ships in a package of its own, which libfx lists as
92
+ // an optional dependency, so a package manager installs only the one for its
93
+ // machine. The package's index.js names the addon with a literal
94
+ // `new URL("./libfx.node", import.meta.url)`, so a bundler that bundles
95
+ // libfx copies that one addon and traces it.
96
+ const nativePackages = {
97
+ "darwin-arm64": "@libfx/libfx-darwin-arm64",
98
+ "darwin-x64": "@libfx/libfx-darwin-x64",
99
+ "linux-arm64": "@libfx/libfx-linux-arm64-gnu",
100
+ "linux-x64": "@libfx/libfx-linux-x64-gnu",
101
+ };
102
+
103
+ // Each specifier is a literal, so Node, Turbopack, and file tracers find the
104
+ // installed package. A missing one fails here at run time, and inside `try`
105
+ // it does not fail a Turbopack build. webpack folds the `typeof` test and
106
+ // sees only a context over the platform packages that are installed, so it
107
+ // does not warn about the ones that are missing.
108
+ async function importNativePackage(name) {
109
+ try {
110
+ if (typeof __webpack_require__ === "function") {
111
+ return await import(
112
+ /* webpackInclude: /[\\/]libfx-(darwin|linux)-[a-z0-9-]+[\\/]index\.js$/ */
113
+ `@libfx/libfx-${name.slice("@libfx/libfx-".length)}`
114
+ );
115
+ } else {
116
+ if (name === "@libfx/libfx-darwin-arm64") return await import("@libfx/libfx-darwin-arm64");
117
+ if (name === "@libfx/libfx-darwin-x64") return await import("@libfx/libfx-darwin-x64");
118
+ if (name === "@libfx/libfx-linux-arm64-gnu") return await import("@libfx/libfx-linux-arm64-gnu");
119
+ if (name === "@libfx/libfx-linux-x64-gnu") return await import("@libfx/libfx-linux-x64-gnu");
120
+ }
121
+ } catch (error) {
122
+ return { error };
123
+ }
124
+ return { error: new Error(`${name} is not a libfx native addon package`) };
125
+ }
126
+
127
+ // The path of the addon the platform package names, or the error that kept
128
+ // the package from loading.
129
+ async function defaultNativeCandidate(name) {
130
+ const imported = await importNativePackage(name);
131
+ if (imported.error) return imported;
132
+ const asset = imported.default;
133
+ if (!(asset instanceof URL)) return { error: new TypeError(`${name} does not export its addon's URL`) };
134
+ return { candidate: fileURLToPath(asset.protocol === "" ? bundledAssetUrl(asset) : asset) };
123
135
  }
124
136
 
125
137
  function validateNativeBackend(backend) {
@@ -180,10 +192,17 @@ async function loadAndValidateNativeCandidate(candidate, artifactMissing = false
180
192
  }
181
193
 
182
194
  async function discoverNativeBackend() {
183
- const candidate = defaultNativeCandidate();
184
- if (!candidate) {
195
+ const nativePackage = nativePackages[`${process.platform}-${process.arch}`];
196
+ if (!nativePackage) {
185
197
  return { backend: null, error: null, failure: "unsupported" };
186
198
  }
199
+ // The package's index.js only names its addon, so a package that cannot be
200
+ // found is not installed or not part of the bundle.
201
+ const { candidate, error: packageError } = await defaultNativeCandidate(nativePackage);
202
+ if (packageError) {
203
+ if (missingArtifact(packageError)) return { backend: null, error: null, probeError: packageError, failure: "missing", nativePackage };
204
+ return { backend: null, error: packageError, failure: "load" };
205
+ }
187
206
  try {
188
207
  await access(candidate);
189
208
  } catch (error) {
@@ -204,35 +223,6 @@ async function resolveNativeBackend(nativeAddon) {
204
223
  return nativeBackendPromise;
205
224
  }
206
225
 
207
- function wasmInput(input) {
208
- const path = wasmFilePath(input);
209
- if (path === null) {
210
- if (input instanceof URL) return input.href;
211
- return input;
212
- }
213
- const cached = wasmFilePromises.get(path);
214
- if (cached) return cached;
215
- const pendingRead = readFile(path);
216
- let pending;
217
- pending = withModuleFailure(pendingRead, () => {
218
- if (wasmFilePromises.get(path) === pending) wasmFilePromises.delete(path);
219
- });
220
- wasmFilePromises.set(path, pending);
221
- pendingRead.catch(() => {
222
- if (wasmFilePromises.get(path) === pending) wasmFilePromises.delete(path);
223
- });
224
- return pending;
225
- }
226
-
227
- function wasmFilePath(input) {
228
- if (input instanceof URL) {
229
- if (input.protocol === "") input = bundledAssetUrl(input);
230
- if (input.protocol === "file:") return fileURLToPath(input);
231
- }
232
- if (typeof input === "string" && !URL.canParse(input)) return resolve(input);
233
- return null;
234
- }
235
-
236
226
  function reason(code, message, error) {
237
227
  const causeCode = error?.code;
238
228
  return {
@@ -253,7 +243,9 @@ function nativeFailureReason(result) {
253
243
  case "missing":
254
244
  return reason(
255
245
  backendReasonCodes.missingArtifact,
256
- `native addon artifact was not found${detailError?.message ? `: ${detailError.message}` : ""}`,
246
+ result.nativePackage
247
+ ? `the native addon package ${result.nativePackage} for ${process.platform}-${process.arch} is not installed`
248
+ : `native addon artifact was not found${detailError?.message ? `: ${detailError.message}` : ""}`,
257
249
  detailError,
258
250
  );
259
251
  case "load":
@@ -326,15 +318,14 @@ export async function getBackendInfo(value = {}) {
326
318
  available: false,
327
319
  reason: reason(
328
320
  backendReasonCodes.jspiUnavailable,
329
- "WebAssembly backend requires JavaScript Promise Integration (JSPI)",
321
+ "WebAssembly backend requires JavaScript Promise Integration (JSPI); run Node with --experimental-wasm-jspi",
330
322
  ),
331
323
  });
332
324
  return { surface, backend: "unavailable", attempts };
333
325
  }
334
- const defaultWasm = surface === "agent" ? defaultCoreWasm : defaultTermWasm;
335
- const wasmSource = wasm ?? defaultWasm;
336
326
  try {
337
- await loadModule(await wasmInput(wasmSource));
327
+ const { probeWasm } = await import("./node-wasm.js");
328
+ await probeWasm(surface, wasm);
338
329
  attempts.push({ backend: "wasm-jspi", available: true, reason: null });
339
330
  return { surface, backend: "wasm-jspi", attempts };
340
331
  } catch (error) {
@@ -610,7 +601,7 @@ function createNativeCoreRuntime(addon, options) {
610
601
  }
611
602
 
612
603
  function createNativeAgent(addon, options) {
613
- return createWasmAgent({
604
+ return createEngine({
614
605
  ...options,
615
606
  runtimeFactory(runtimeOptions) {
616
607
  return createNativeCoreRuntime(addon, runtimeOptions);
@@ -618,17 +609,18 @@ function createNativeAgent(addon, options) {
618
609
  });
619
610
  }
620
611
 
621
- async function createWithFallback(surface, nativeMethod, wasmFactory, defaultWasm, options) {
612
+ async function createWithFallback(surface, nativeMethod, options) {
622
613
  const { nativeAddon, backend = "auto", ...unvalidatedOptions } = options ?? {};
623
614
  if (!new Set(["auto", "native", "wasm"]).has(backend)) {
624
615
  throw new TypeError('backend must be "auto", "native", or "wasm"');
625
616
  }
626
617
  const runtimeOptions = surface === "agent" ? normalizeAgentOptions(unvalidatedOptions) : unvalidatedOptions;
627
618
 
619
+ let native = null;
628
620
  let nativeError;
629
621
  let nativeAttempted = false;
630
622
  if (backend !== "wasm") {
631
- const native = await resolveNativeBackend(nativeAddon);
623
+ native = await resolveNativeBackend(nativeAddon);
632
624
  nativeError = native.error;
633
625
  if (typeof native.backend?.[nativeMethod] === "function") {
634
626
  nativeAttempted = true;
@@ -643,7 +635,12 @@ async function createWithFallback(surface, nativeMethod, wasmFactory, defaultWas
643
635
  }
644
636
  }
645
637
  if (backend === "native") {
646
- const error = nativeError ?? new Error(`native addon does not provide ${nativeMethod}()`);
638
+ const missingPackage = native.failure === "missing" ? native.nativePackage : undefined;
639
+ const error = nativeError ?? new Error(native.backend
640
+ ? `native addon does not provide ${nativeMethod}()`
641
+ : missingPackage
642
+ ? `${sentence(nativeFailureReason(native).message)} Install ${missingPackage}, which libfx lists as an optional dependency.`
643
+ : nativeFailureReason(native).message, native.probeError ? { cause: native.probeError } : undefined);
647
644
  error.code ??= "LIBFX_NATIVE_UNAVAILABLE";
648
645
  throw error;
649
646
  }
@@ -651,10 +648,10 @@ async function createWithFallback(surface, nativeMethod, wasmFactory, defaultWas
651
648
 
652
649
  if (!supportsJspi()) {
653
650
  if (nativeAttempted) throw nativeError;
654
- throw jspiFallbackError(surface, nativeError);
651
+ throw jspiFallbackError(surface, native, nativeError);
655
652
  }
656
- const wasmSource = runtimeOptions.wasm ?? defaultWasm;
657
- return wasmFactory({ ...runtimeOptions, wasm: await wasmInput(wasmSource) });
653
+ const wasmBackend = await import("./node-wasm.js");
654
+ return surface === "agent" ? wasmBackend.createWasmEngine(runtimeOptions) : wasmBackend.createWasmTerminal(runtimeOptions);
658
655
  }
659
656
 
660
657
  /**
@@ -663,13 +660,7 @@ async function createWithFallback(surface, nativeMethod, wasmFactory, defaultWas
663
660
  * runs durable sessions on these.
664
661
  */
665
662
  export async function createFxEngine(options = {}) {
666
- return createWithFallback(
667
- "agent",
668
- "createCore",
669
- createWasmAgent,
670
- defaultCoreWasm,
671
- options,
672
- );
663
+ return createWithFallback("agent", "createCore", options);
673
664
  }
674
665
 
675
666
  // The durability a session gets when the caller names none: Vercel's World
@@ -723,5 +714,5 @@ export const createFxAgent = createDurableAgentFactory({
723
714
  });
724
715
 
725
716
  export function createFxTerminal(options = {}) {
726
- return createWithFallback("terminal", "createFxTerminal", createWasmTerminal, defaultTermWasm, options);
717
+ return createWithFallback("terminal", "createFxTerminal", options);
727
718
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "libfx",
3
- "version": "0.0.15",
3
+ "version": "0.0.16",
4
4
  "description": "Embed fx agents and terminals in JavaScript hosts",
5
5
  "type": "module",
6
6
  "repository": {
@@ -87,12 +87,19 @@
87
87
  "default": "./skills-node.js"
88
88
  }
89
89
  },
90
+ "sideEffects": false,
90
91
  "engines": {
91
- "node": ">=20"
92
+ "node": "^20.19.0 || >=22.12.0"
92
93
  },
93
94
  "publishConfig": {
94
95
  "access": "public",
95
96
  "registry": "https://registry.npmjs.org/"
96
97
  },
97
- "license": "Apache-2.0"
98
+ "license": "Apache-2.0",
99
+ "optionalDependencies": {
100
+ "@libfx/libfx-linux-x64-gnu": "0.0.16",
101
+ "@libfx/libfx-linux-arm64-gnu": "0.0.16",
102
+ "@libfx/libfx-darwin-x64": "0.0.16",
103
+ "@libfx/libfx-darwin-arm64": "0.0.16"
104
+ }
98
105
  }
package/types/node.d.ts CHANGED
@@ -1,3 +1,3 @@
1
1
  // `libfx` and `libfx/node` on Node.js, for `import` (node.js). The CommonJS
2
- // entry, node.cjs, is built from node.js, so both declare the same exports.
2
+ // entry, node.cjs, exports node.js itself, so both declare the same exports.
3
3
  export * from "./node.cjs";