libfx 0.0.15 → 0.0.17

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
- import { fetchCleanupAction, cleanupTimeoutMs, cleanupByteLimit } from "./fetch-cleanup.js";
10
+ import { fetchCleanupAction, cleanupTimeoutMs, cleanupByteLimit, retryAfterSeconds } 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) {
@@ -480,7 +471,11 @@ function createNativeCoreRuntime(addon, options) {
480
471
  });
481
472
  const response = await wait(responseTask);
482
473
  if (settled && !state.consumed) throw new DOMException("Aborted", "AbortError");
483
- const started = settled ? fetchOperationStale : addon.startCoreFetchResponse(core, state.handle, response.status);
474
+ const started = settled
475
+ ? fetchOperationStale
476
+ : typeof addon.startCoreFetchResponseWithRetryAfter === "function"
477
+ ? addon.startCoreFetchResponseWithRetryAfter(core, state.handle, response.status, retryAfterSeconds(response.headers))
478
+ : addon.startCoreFetchResponse(core, state.handle, response.status);
484
479
  if (started !== fetchOperationApplied && started !== fetchOperationStale) throw new Error(`invalid native fetch start result ${started}`);
485
480
  state.active = started === fetchOperationApplied;
486
481
  reader = response.body?.getReader();
@@ -610,7 +605,7 @@ function createNativeCoreRuntime(addon, options) {
610
605
  }
611
606
 
612
607
  function createNativeAgent(addon, options) {
613
- return createWasmAgent({
608
+ return createEngine({
614
609
  ...options,
615
610
  runtimeFactory(runtimeOptions) {
616
611
  return createNativeCoreRuntime(addon, runtimeOptions);
@@ -618,17 +613,18 @@ function createNativeAgent(addon, options) {
618
613
  });
619
614
  }
620
615
 
621
- async function createWithFallback(surface, nativeMethod, wasmFactory, defaultWasm, options) {
616
+ async function createWithFallback(surface, nativeMethod, options) {
622
617
  const { nativeAddon, backend = "auto", ...unvalidatedOptions } = options ?? {};
623
618
  if (!new Set(["auto", "native", "wasm"]).has(backend)) {
624
619
  throw new TypeError('backend must be "auto", "native", or "wasm"');
625
620
  }
626
621
  const runtimeOptions = surface === "agent" ? normalizeAgentOptions(unvalidatedOptions) : unvalidatedOptions;
627
622
 
623
+ let native = null;
628
624
  let nativeError;
629
625
  let nativeAttempted = false;
630
626
  if (backend !== "wasm") {
631
- const native = await resolveNativeBackend(nativeAddon);
627
+ native = await resolveNativeBackend(nativeAddon);
632
628
  nativeError = native.error;
633
629
  if (typeof native.backend?.[nativeMethod] === "function") {
634
630
  nativeAttempted = true;
@@ -643,7 +639,12 @@ async function createWithFallback(surface, nativeMethod, wasmFactory, defaultWas
643
639
  }
644
640
  }
645
641
  if (backend === "native") {
646
- const error = nativeError ?? new Error(`native addon does not provide ${nativeMethod}()`);
642
+ const missingPackage = native.failure === "missing" ? native.nativePackage : undefined;
643
+ const error = nativeError ?? new Error(native.backend
644
+ ? `native addon does not provide ${nativeMethod}()`
645
+ : missingPackage
646
+ ? `${sentence(nativeFailureReason(native).message)} Install ${missingPackage}, which libfx lists as an optional dependency.`
647
+ : nativeFailureReason(native).message, native.probeError ? { cause: native.probeError } : undefined);
647
648
  error.code ??= "LIBFX_NATIVE_UNAVAILABLE";
648
649
  throw error;
649
650
  }
@@ -651,10 +652,10 @@ async function createWithFallback(surface, nativeMethod, wasmFactory, defaultWas
651
652
 
652
653
  if (!supportsJspi()) {
653
654
  if (nativeAttempted) throw nativeError;
654
- throw jspiFallbackError(surface, nativeError);
655
+ throw jspiFallbackError(surface, native, nativeError);
655
656
  }
656
- const wasmSource = runtimeOptions.wasm ?? defaultWasm;
657
- return wasmFactory({ ...runtimeOptions, wasm: await wasmInput(wasmSource) });
657
+ const wasmBackend = await import("./node-wasm.js");
658
+ return surface === "agent" ? wasmBackend.createWasmEngine(runtimeOptions) : wasmBackend.createWasmTerminal(runtimeOptions);
658
659
  }
659
660
 
660
661
  /**
@@ -663,13 +664,7 @@ async function createWithFallback(surface, nativeMethod, wasmFactory, defaultWas
663
664
  * runs durable sessions on these.
664
665
  */
665
666
  export async function createFxEngine(options = {}) {
666
- return createWithFallback(
667
- "agent",
668
- "createCore",
669
- createWasmAgent,
670
- defaultCoreWasm,
671
- options,
672
- );
667
+ return createWithFallback("agent", "createCore", options);
673
668
  }
674
669
 
675
670
  // The durability a session gets when the caller names none: Vercel's World
@@ -723,5 +718,5 @@ export const createFxAgent = createDurableAgentFactory({
723
718
  });
724
719
 
725
720
  export function createFxTerminal(options = {}) {
726
- return createWithFallback("terminal", "createFxTerminal", createWasmTerminal, defaultTermWasm, options);
721
+ return createWithFallback("terminal", "createFxTerminal", options);
727
722
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "libfx",
3
- "version": "0.0.15",
3
+ "version": "0.0.17",
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.17",
101
+ "@libfx/libfx-linux-arm64-gnu": "0.0.17",
102
+ "@libfx/libfx-darwin-x64": "0.0.17",
103
+ "@libfx/libfx-darwin-arm64": "0.0.17"
104
+ }
98
105
  }
package/types/libfx.d.cts CHANGED
@@ -46,7 +46,7 @@ export interface FxModelOptions {
46
46
  effort?: FxEffort;
47
47
  /** Enables the fast lane for models that advertise one. */
48
48
  fast?: boolean;
49
- /** Requests Ultra mode, the higher-cost service tier, for models whose metadata advertises it. Disables Fast. */
49
+ /** Requests Ultrafast mode, the higher-cost service tier, for models whose metadata advertises it. Disables Fast. */
50
50
  ultrafast?: boolean;
51
51
  }
52
52
 
@@ -261,8 +261,8 @@ export interface FxUsage {
261
261
  reasoningTokens?: number;
262
262
  }
263
263
 
264
- /** Why an engine turn ended. */
265
- export type FxStopReason = "end_turn" | "max_output_tokens" | "max_model_turns" | "refused" | "cancelled";
264
+ /** Why an engine turn ended. `yielded`: it stopped at its `yieldAt` and stays open for `resume()`. */
265
+ export type FxStopReason = "end_turn" | "max_output_tokens" | "max_model_turns" | "refused" | "cancelled" | "yielded";
266
266
 
267
267
  // ---------------------------------------------------------------------------
268
268
  // Diagnostics
@@ -304,7 +304,8 @@ export type FxEngineEvent = FxEventBase & (
304
304
  | { type: "permission.request"; request: unknown }
305
305
  | { type: "permission.resolve"; optionId: unknown }
306
306
  | { type: "journal.open"; events: number; checkpoint: boolean; turns: unknown; resumable: boolean }
307
- | { type: "journal.append"; events: number }
307
+ /** `barrier`: the record's `barrier`, as `Persistence.append` receives it. */
308
+ | { type: "journal.append"; events: number; barrier: boolean }
308
309
  | { type: "journal.error"; error: string }
309
310
  | { type: "checkpoint.save"; through: string; bytes: number }
310
311
  | { type: "checkpoint.error"; error: string; message: string }
@@ -423,7 +424,7 @@ export type FxHookEvent = FxHookEnvelope & (
423
424
  | { type: "mcp.start"; data: { operationId: number | string; callId: string | null; server: string | null; tool: string; transport: string | null } }
424
425
  | { type: "mcp.request"; data: { operationId: number | string; requestId: number | string; requestIdKind: "jsonrpc" | "engine" | "correlation" } }
425
426
  | { type: "mcp.end"; data: { operationId: number | string; status: "success" | "failure" | "cancelled" | "input_required" | "unavailable" | "unknown"; error: string | null } }
426
- | { type: "delegation"; data: { callId: string; childSessionId: string; workId: string; operationId: string | null; phase: "dispatched" | "completed" | "yielded" | "replayed" | "failed" | "cancelled" | "feedback_queued" | "feedback_applied" | "feedback_not_applied" } }
427
+ | { type: "delegation"; data: { callId: string; childSessionId: string; workId: string; operationId: string | null; phase: "dispatched" | "completed" | "replayed" | "failed" | "cancelled" | "feedback_queued" | "feedback_applied" | "feedback_not_applied" } }
427
428
  /** Emitted only after append resolves and its cursor validates. */
428
429
  | { type: "journal.commit"; data: { cursor: string; firstSeq: number | null; lastSeq: number | null; eventCount: number } }
429
430
  | { type: "journal.restore"; data: { cursor: string | null; lastSeq: number; resumable: boolean; openTurnId: string | null; lastTurnId: string | null } }
@@ -669,6 +670,12 @@ export interface FxCustomProviderModel {
669
670
  maxOutputTokens?: number;
670
671
  supportsToolUse?: boolean;
671
672
  supportsVision?: boolean;
673
+ /** The request field that carries the output limit. Defaults to `"max_tokens"`. */
674
+ outputLimitField?: "max_tokens" | "max_completion_tokens";
675
+ /** Fields sent in the body of every request for the model, such as `{ temperature: 0.2 }`. A field fx writes itself, such as `messages`, is refused. */
676
+ request?: Record<string, unknown>;
677
+ /** The effort levels the model offers, in order, each with the request fields that choose it, such as `{ high: { reasoning_effort: "high" } }`. A level the model does not list is not sent. */
678
+ reasoningEfforts?: Record<string, Record<string, unknown>>;
672
679
  }
673
680
 
674
681
  /** A server that speaks OpenAI Chat Completions, such as a local model server or a router. */
@@ -679,6 +686,12 @@ export interface FxCustomProviderDefinition {
679
686
  baseUrl: string;
680
687
  /** The models it serves, by id. */
681
688
  models?: Record<string, FxCustomProviderModel>;
689
+ /** Fields for every model, including one `models` does not list, where the model sets none itself. */
690
+ modelDefaults?: FxCustomProviderModel;
691
+ /** The model that names a session from its first prompt. Without it, a session keeps the title derived from the prompt. */
692
+ titleModel?: string;
693
+ /** The model that reads images for a model that cannot. Without it, the provider has no image fallback. */
694
+ visionModel?: string;
682
695
  }
683
696
 
684
697
  /** A custom provider's API key. */
@@ -796,8 +809,8 @@ export interface FxAccepted {
796
809
  /** How a durable turn ended. */
797
810
  export interface FxTurnResult {
798
811
  messageId: string;
799
- /** `error` when the turn failed, with the reason in `error`; `unknown` when the process that ran it stopped before writing how it ended. */
800
- stopReason: FxStopReason | "error" | "unknown";
812
+ /** `error` when the turn failed, with the reason in `error`; `unknown` when the process that ran it stopped before writing how it ended. A yielded turn continues, so it never ends `yielded`. */
813
+ stopReason: Exclude<FxStopReason, "yielded"> | "error" | "unknown";
801
814
  /** Absent when the result is a failure read from the session's log rather than from the turn's end. */
802
815
  usage?: FxUsage;
803
816
  error?: FxErrorSummary;
@@ -848,7 +861,7 @@ export type FxStreamLine = (
848
861
  | {
849
862
  type: "turn_end";
850
863
  messageId: string;
851
- stopReason: FxStopReason | "error" | "unknown";
864
+ stopReason: Exclude<FxStopReason, "yielded"> | "error" | "unknown";
852
865
  usage?: FxUsage;
853
866
  error?: FxErrorSummary;
854
867
  repeated?: true;
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";