libfx 0.0.11 → 0.0.12
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/README.md +60 -32
- package/fx-core.wasm +0 -0
- package/fx-sdk.js +189 -42
- package/fx-term.wasm +0 -0
- package/libfx.darwin-arm64.node +0 -0
- package/libfx.darwin-x64.node +0 -0
- package/libfx.linux-arm64.node +0 -0
- package/libfx.linux-x64.node +0 -0
- package/node.cjs +201 -41
- package/node.js +5 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -39,25 +39,39 @@ await agent.close();
|
|
|
39
39
|
|
|
40
40
|
`apiKey` is required. `model` is optional and defaults to fx's built-in model.
|
|
41
41
|
Agent configuration uses named options; `env` is reserved for
|
|
42
|
-
`createFxTerminal()`.
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
`fast`
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
`
|
|
57
|
-
|
|
42
|
+
`createFxTerminal()`. The canonical model configuration groups the model ID
|
|
43
|
+
and model-specific options:
|
|
44
|
+
|
|
45
|
+
```js
|
|
46
|
+
const agent = await createFxAgent({
|
|
47
|
+
apiKey,
|
|
48
|
+
model: { id: "anthropic/claude-opus-5.5-fast", effort: "low", fast: true },
|
|
49
|
+
});
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
A string `model` remains supported as shorthand. Top-level `effort` and
|
|
53
|
+
`fast` are deprecated but remain supported with a string model or no model;
|
|
54
|
+
they cannot be mixed with a model object. New code should use the model object.
|
|
55
|
+
|
|
56
|
+
`model.effort` sets the reasoning effort for models that advertise effort
|
|
57
|
+
levels. It uses the same vocabulary as the fx CLI's `--effort` flag:
|
|
58
|
+
`"default"` leaves the choice to the model; named levels such as `"low"`,
|
|
59
|
+
`"medium"`, `"high"`, or `"xhigh"` request a specific level. A named level is
|
|
60
|
+
validated at creation; an unsupported level rejects with an Error carrying
|
|
61
|
+
`code: "LIBFX_MODEL_UNSUPPORTED_EFFORT"`, `model`, and
|
|
62
|
+
`capability: "effort"`. Its message names the supported set when available.
|
|
63
|
+
Omitting effort or using `"default"` leaves the model default in place.
|
|
64
|
+
|
|
65
|
+
`model.fast` enables the fast lane for models that advertise one, matching the
|
|
66
|
+
fx CLI's `--fast` flag. A model without a fast path rejects at creation with
|
|
67
|
+
`code: "LIBFX_MODEL_UNSUPPORTED_FAST"`, `model`, and `capability: "fast"`.
|
|
68
|
+
Omitting fast or setting it to `false` leaves the model default in place.
|
|
69
|
+
The new codes replace `LIBFX_UNSUPPORTED_EFFORT` and `LIBFX_UNSUPPORTED_FAST`
|
|
70
|
+
for both nested and legacy top-level settings. Callers that check the old codes
|
|
71
|
+
must update their error handling.
|
|
58
72
|
|
|
59
73
|
The host selects the model. Agent creation does not fetch the Gateway model
|
|
60
|
-
catalog unless
|
|
74
|
+
catalog unless effort requests a named level or fast is enabled. Prompting
|
|
61
75
|
can resolve model capabilities and context capacity through the supplied
|
|
62
76
|
`fetch`; fx caches that metadata for the agent.
|
|
63
77
|
|
|
@@ -96,9 +110,11 @@ const result = await turn.result;
|
|
|
96
110
|
```
|
|
97
111
|
|
|
98
112
|
A turn has one event consumer. Breaking out of its iterator cancels the turn;
|
|
99
|
-
`turn.cancel()` and `agent.close()` also release blocked output.
|
|
100
|
-
|
|
101
|
-
|
|
113
|
+
`turn.cancel()` and `agent.close()` also release blocked output. Embedded agents
|
|
114
|
+
have no implicit model-step cap, so hosts should cancel turns that exceed their
|
|
115
|
+
own budgets. The CLI's `max_agent_steps` setting does not apply to
|
|
116
|
+
`createFxAgent()`. Transport or message-decoding failures reject the result
|
|
117
|
+
instead of returning success with missing text.
|
|
102
118
|
|
|
103
119
|
Native transport buffers at most 8 MiB of output bytes. Unread SDK events apply
|
|
104
120
|
backpressure at 1 MiB of encoded messages or 256 events. One message can exceed
|
|
@@ -106,22 +122,32 @@ that threshold when the queue is empty; an individual encoded ACP message is
|
|
|
106
122
|
limited to 64 MiB on both backends. These are transport bounds, not a total
|
|
107
123
|
answer-size limit or a bound on retained conversation history.
|
|
108
124
|
|
|
109
|
-
Image blocks
|
|
110
|
-
wrapping) of a PNG,
|
|
125
|
+
Image blocks accept a `Blob` or `File` with a non-empty `type`, or the
|
|
126
|
+
existing canonical base64 (no line wrapping) and explicit `mimeType` of a PNG,
|
|
127
|
+
JPEG, GIF, or WebP payload:
|
|
111
128
|
|
|
112
129
|
```js
|
|
113
130
|
const turn = agent.prompt([
|
|
114
131
|
{ type: "text", text: "What does this screenshot show?" },
|
|
115
|
-
{ type: "image", data:
|
|
132
|
+
{ type: "image", data: file }, // File or Blob, with file.type
|
|
133
|
+
// Or: { type: "image", data: base64Png, mimeType: "image/png" }
|
|
116
134
|
]);
|
|
117
135
|
```
|
|
118
136
|
|
|
119
137
|
A prompt may contain up to 8 images, each with up to 5 MiB of base64 data,
|
|
120
|
-
with at most 8 MiB of image data per prompt
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
138
|
+
with at most 8 MiB of image data per prompt. The SDK checks Blob size before
|
|
139
|
+
reading it, encodes it for the same ACP wire format, and rejects larger input
|
|
140
|
+
with typed `RangeError`s. The total frame size is checked before reading a
|
|
141
|
+
Blob, and the actual byte count is checked before encoding it. Blob reads are
|
|
142
|
+
asynchronous: `prompt()` returns a turn, and read failures reject
|
|
143
|
+
`turn.result`. Cancelling or closing while a Blob is being read settles the
|
|
144
|
+
turn without sending its prompt. For base64 input, size errors still throw
|
|
145
|
+
synchronously from `prompt()`.
|
|
146
|
+
|
|
147
|
+
The kernel sniffs decoded bytes and compares them with the claimed MIME type
|
|
148
|
+
for both input forms; a mismatch fails the turn with
|
|
149
|
+
`Invalid image prompt block`. Images are routed only to models that advertise
|
|
150
|
+
image input; for any other model the turn fails with
|
|
125
151
|
`Image prompts are unavailable for the selected model` and no image bytes
|
|
126
152
|
leave the process. Prompt images are retained in checkpoints within the
|
|
127
153
|
existing 4 MiB checkpoint bound, so a restored agent can refer to earlier
|
|
@@ -133,8 +159,10 @@ without discarding the in-flight response or completed tool work. Steering also
|
|
|
133
159
|
accepts an array of text blocks; image and resource steering blocks are rejected.
|
|
134
160
|
Each message is limited to 64 KiB, with at most 64 queued messages and 1 MiB of
|
|
135
161
|
queued steering text. Accepted steering appears as a `user_message` event before
|
|
136
|
-
the model's continued output.
|
|
137
|
-
|
|
162
|
+
the model's continued output. For a Blob prompt, steering during the read
|
|
163
|
+
waits for the prompt to be sent; cancelling before then rejects the pending
|
|
164
|
+
steering. Calling `steer()` after the turn settles rejects with
|
|
165
|
+
`no prompt is running`.
|
|
138
166
|
|
|
139
167
|
```js
|
|
140
168
|
const turn = agent.prompt("Build the feature.");
|
|
@@ -162,8 +190,8 @@ The checkpoint contains conversation history and usage only. The host owns
|
|
|
162
190
|
durable storage and must resupply models, credentials, instructions, tools,
|
|
163
191
|
MCP clients, and skill records. Reasoning effort and fast mode are
|
|
164
192
|
agent-creation options and are not stored in a checkpoint: recreate the agent
|
|
165
|
-
with new `effort` or `fast` values to change them, the same path as
|
|
166
|
-
models.
|
|
193
|
+
with new `model.effort` or `model.fast` values to change them, the same path as
|
|
194
|
+
switching models.
|
|
167
195
|
|
|
168
196
|
## Models
|
|
169
197
|
|
package/fx-core.wasm
CHANGED
|
Binary file
|
package/fx-sdk.js
CHANGED
|
@@ -84,7 +84,7 @@ function normalizeFast(value) {
|
|
|
84
84
|
return value;
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
-
function normalizeAgentOptions(value) {
|
|
87
|
+
export function normalizeAgentOptions(value) {
|
|
88
88
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
89
89
|
throw new TypeError("createFxAgent() options must be an object");
|
|
90
90
|
}
|
|
@@ -93,9 +93,24 @@ function normalizeAgentOptions(value) {
|
|
|
93
93
|
throw new TypeError("createFxAgent() does not accept env; pass apiKey and model directly");
|
|
94
94
|
}
|
|
95
95
|
options.apiKey = boundedString(options.apiKey, "apiKey", maxApiKeyBytes, true);
|
|
96
|
-
options.model
|
|
97
|
-
|
|
98
|
-
|
|
96
|
+
if (options.model !== null && typeof options.model === "object" && !Array.isArray(options.model)) {
|
|
97
|
+
if (Object.hasOwn(options, "effort") || Object.hasOwn(options, "fast")) {
|
|
98
|
+
throw new TypeError("model options cannot be mixed with top-level effort or fast");
|
|
99
|
+
}
|
|
100
|
+
const model = options.model;
|
|
101
|
+
for (const name of Object.keys(model)) {
|
|
102
|
+
if (name !== "id" && name !== "effort" && name !== "fast") {
|
|
103
|
+
throw new TypeError(`unsupported model option: ${name}`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
options.model = boundedString(model.id, "model.id", maxModelBytes, true);
|
|
107
|
+
options.effort = normalizeEffort(model.effort);
|
|
108
|
+
options.fast = normalizeFast(model.fast);
|
|
109
|
+
} else {
|
|
110
|
+
options.model = boundedString(options.model, "model", maxModelBytes, false);
|
|
111
|
+
options.effort = normalizeEffort(options.effort);
|
|
112
|
+
options.fast = normalizeFast(options.fast);
|
|
113
|
+
}
|
|
99
114
|
validateGatewayChatUrl(options.gatewayChatUrl);
|
|
100
115
|
return options;
|
|
101
116
|
}
|
|
@@ -110,16 +125,15 @@ function agentEnvironment(options) {
|
|
|
110
125
|
};
|
|
111
126
|
}
|
|
112
127
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
(
|
|
119
|
-
error.code
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
error.code ??= "LIBFX_UNSUPPORTED_FAST";
|
|
128
|
+
function agentRpcError(response) {
|
|
129
|
+
const error = new Error(response.message);
|
|
130
|
+
const data = response.data;
|
|
131
|
+
if (data && ["LIBFX_MODEL_UNSUPPORTED_EFFORT", "LIBFX_MODEL_UNSUPPORTED_FAST"].includes(data.code) &&
|
|
132
|
+
typeof data.model === "string" &&
|
|
133
|
+
data.capability === (data.code === "LIBFX_MODEL_UNSUPPORTED_FAST" ? "fast" : "effort")) {
|
|
134
|
+
error.code = data.code;
|
|
135
|
+
error.model = data.model;
|
|
136
|
+
error.capability = data.capability;
|
|
123
137
|
}
|
|
124
138
|
return error;
|
|
125
139
|
}
|
|
@@ -1276,6 +1290,15 @@ export async function createFxTerminal(options) {
|
|
|
1276
1290
|
};
|
|
1277
1291
|
}
|
|
1278
1292
|
|
|
1293
|
+
function blobByteLength(value) {
|
|
1294
|
+
if (typeof Blob === "undefined" || value == null) return null;
|
|
1295
|
+
try {
|
|
1296
|
+
return Object.getOwnPropertyDescriptor(Blob.prototype, "size").get.call(value);
|
|
1297
|
+
} catch {
|
|
1298
|
+
return null;
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1279
1302
|
function normalizePromptInput(input) {
|
|
1280
1303
|
if (typeof input === "string") return [{ type: "text", text: input }];
|
|
1281
1304
|
if (!Array.isArray(input)) throw new TypeError("prompt input must be a string or an array of prompt blocks");
|
|
@@ -1284,24 +1307,34 @@ function normalizePromptInput(input) {
|
|
|
1284
1307
|
return input.map((block, index) => {
|
|
1285
1308
|
if (!block || typeof block !== "object") throw new TypeError(`prompt block ${index} must be an object`);
|
|
1286
1309
|
if (block.type === "image") {
|
|
1287
|
-
|
|
1288
|
-
|
|
1310
|
+
const size = blobByteLength(block.data);
|
|
1311
|
+
const blob = size !== null;
|
|
1312
|
+
if (!blob && (typeof block.data !== "string" || block.data.length === 0)) {
|
|
1313
|
+
throw new TypeError(`image prompt block ${index} requires base64 data or a Blob`);
|
|
1314
|
+
}
|
|
1315
|
+
const mimeType = blob ? block.data.type : block.mimeType;
|
|
1316
|
+
if (blob && block.mimeType !== undefined && block.mimeType !== mimeType) {
|
|
1317
|
+
throw new TypeError(`image prompt block ${index} mimeType disagrees with Blob.type`);
|
|
1289
1318
|
}
|
|
1290
|
-
if (typeof
|
|
1319
|
+
if (typeof mimeType !== "string" || mimeType.length === 0 || mimeType.length > 128) {
|
|
1291
1320
|
throw new TypeError(`image prompt block ${index} requires a mimeType`);
|
|
1292
1321
|
}
|
|
1293
|
-
if (
|
|
1322
|
+
if (blob && (!Number.isSafeInteger(size) || size <= 0)) {
|
|
1323
|
+
throw new TypeError(`image prompt block ${index} requires a non-empty Blob with a valid size`);
|
|
1324
|
+
}
|
|
1325
|
+
const encodedLength = blob ? Math.ceil(size / 3) * 4 : block.data.length;
|
|
1326
|
+
if (encodedLength > maxPromptImageDataBytes) {
|
|
1294
1327
|
throw new RangeError(`image prompt block ${index} exceeds the ${maxPromptImageDataBytes} byte per-image libfx limit`);
|
|
1295
1328
|
}
|
|
1296
1329
|
imageCount += 1;
|
|
1297
1330
|
if (imageCount > maxPromptImages) {
|
|
1298
1331
|
throw new RangeError(`prompt cannot contain more than ${maxPromptImages} images`);
|
|
1299
1332
|
}
|
|
1300
|
-
imageBytes +=
|
|
1333
|
+
imageBytes += encodedLength;
|
|
1301
1334
|
if (imageBytes > maxPromptImagesBytes) {
|
|
1302
1335
|
throw new RangeError(`prompt images exceed the ${maxPromptImagesBytes} byte libfx frame limit`);
|
|
1303
1336
|
}
|
|
1304
|
-
return { type: "image", data: block.data, mimeType
|
|
1337
|
+
return { type: "image", data: block.data, mimeType };
|
|
1305
1338
|
}
|
|
1306
1339
|
if (block.type === "text") {
|
|
1307
1340
|
if (typeof block.text !== "string") throw new TypeError(`text prompt block ${index} requires text`);
|
|
@@ -1317,6 +1350,50 @@ function normalizePromptInput(input) {
|
|
|
1317
1350
|
});
|
|
1318
1351
|
}
|
|
1319
1352
|
|
|
1353
|
+
function promptFrameSize(prompt) {
|
|
1354
|
+
let blobDataBytes = 0;
|
|
1355
|
+
const projected = prompt.map((block) => {
|
|
1356
|
+
if (block.type !== "image" || typeof block.data === "string") return block;
|
|
1357
|
+
const size = blobByteLength(block.data);
|
|
1358
|
+
if (!Number.isSafeInteger(size)) throw new TypeError("image prompt requires a valid Blob size");
|
|
1359
|
+
blobDataBytes += Math.ceil(size / 3) * 4;
|
|
1360
|
+
return { type: "image", data: "", mimeType: block.mimeType };
|
|
1361
|
+
});
|
|
1362
|
+
return encoder.encode(JSON.stringify({ sessionId: "", prompt: projected })).length + blobDataBytes + promptFrameEnvelopeBytes;
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
async function materializePromptBlobs(blocks, isCancelled) {
|
|
1366
|
+
const encoded = [];
|
|
1367
|
+
let imageBytes = 0;
|
|
1368
|
+
for (const [index, block] of blocks.entries()) {
|
|
1369
|
+
if (isCancelled()) return null;
|
|
1370
|
+
if (block.type !== "image") {
|
|
1371
|
+
encoded.push(block);
|
|
1372
|
+
continue;
|
|
1373
|
+
}
|
|
1374
|
+
if (typeof block.data === "string") {
|
|
1375
|
+
imageBytes += block.data.length;
|
|
1376
|
+
if (imageBytes > maxPromptImagesBytes) {
|
|
1377
|
+
throw new RangeError(`prompt images exceed the ${maxPromptImagesBytes} byte libfx frame limit`);
|
|
1378
|
+
}
|
|
1379
|
+
encoded.push(block);
|
|
1380
|
+
continue;
|
|
1381
|
+
}
|
|
1382
|
+
const bytes = new Uint8Array(await block.data.arrayBuffer());
|
|
1383
|
+
if (isCancelled()) return null;
|
|
1384
|
+
const encodedLength = Math.ceil(bytes.length / 3) * 4;
|
|
1385
|
+
if (encodedLength > maxPromptImageDataBytes) {
|
|
1386
|
+
throw new RangeError(`image prompt block ${index} exceeds the ${maxPromptImageDataBytes} byte per-image libfx limit`);
|
|
1387
|
+
}
|
|
1388
|
+
imageBytes += encodedLength;
|
|
1389
|
+
if (imageBytes > maxPromptImagesBytes) {
|
|
1390
|
+
throw new RangeError(`prompt images exceed the ${maxPromptImagesBytes} byte libfx frame limit`);
|
|
1391
|
+
}
|
|
1392
|
+
encoded.push({ type: "image", data: bytesToBase64(bytes), mimeType: block.mimeType });
|
|
1393
|
+
}
|
|
1394
|
+
return normalizePromptInput(encoded);
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1320
1397
|
function normalizeSteeringInput(input) {
|
|
1321
1398
|
const blocks = normalizePromptInput(input);
|
|
1322
1399
|
if (blocks.some((block) => block.type !== "text")) {
|
|
@@ -1443,6 +1520,7 @@ export async function createFxAgent(options = {}) {
|
|
|
1443
1520
|
let sessionId = null;
|
|
1444
1521
|
let activeTurn = null;
|
|
1445
1522
|
let closing = false;
|
|
1523
|
+
let coreExitError = null;
|
|
1446
1524
|
const isCurrentTurn = (turn) => turn && activeTurn === turn && !turn.cancelled && !closing;
|
|
1447
1525
|
const emit = (type, detail = {}) => {
|
|
1448
1526
|
try { options.onEvent?.({ type, timestamp: performance.now(), ...detail }); } catch {}
|
|
@@ -1572,7 +1650,9 @@ export async function createFxAgent(options = {}) {
|
|
|
1572
1650
|
const send = (message) => {
|
|
1573
1651
|
if (closing) throw new Error("fx agent is closing");
|
|
1574
1652
|
emit("acp.send", { message });
|
|
1653
|
+
if (message.method === "session/prompt" && activeTurn?.cancelled) throw new Error("Cancelled");
|
|
1575
1654
|
runtime.write(`${JSON.stringify(message)}\n`);
|
|
1655
|
+
if (message.method === "session/prompt") activeTurn?.promptWritten();
|
|
1576
1656
|
};
|
|
1577
1657
|
const request = (method, params = {}) => new Promise((resolve, reject) => {
|
|
1578
1658
|
const id = nextId++;
|
|
@@ -1580,11 +1660,13 @@ export async function createFxAgent(options = {}) {
|
|
|
1580
1660
|
try { send({ jsonrpc: "2.0", id, method, params }); } catch (error) { pending.delete(id); reject(error); }
|
|
1581
1661
|
});
|
|
1582
1662
|
runtime.exited.then((code) => {
|
|
1583
|
-
emit("runtime.exit", { code });
|
|
1584
1663
|
closing = true;
|
|
1585
1664
|
const error = runtime.error ?? new Error(`fx-core exited with code ${code} before completing the ACP request`);
|
|
1665
|
+
coreExitError = error;
|
|
1666
|
+
activeTurn?.failPendingBlob(error);
|
|
1586
1667
|
for (const waiter of pending.values()) waiter.reject(error);
|
|
1587
1668
|
pending.clear();
|
|
1669
|
+
emit("runtime.exit", { code });
|
|
1588
1670
|
});
|
|
1589
1671
|
runtime.setLineHandler((message, size) => {
|
|
1590
1672
|
emit("acp.receive", { message });
|
|
@@ -1623,7 +1705,7 @@ export async function createFxAgent(options = {}) {
|
|
|
1623
1705
|
return;
|
|
1624
1706
|
}
|
|
1625
1707
|
const waiter = pending.get(message.id); if (!waiter) return; pending.delete(message.id);
|
|
1626
|
-
if (message.error) waiter.reject(
|
|
1708
|
+
if (message.error) waiter.reject(agentRpcError(message.error)); else waiter.resolve(message.result);
|
|
1627
1709
|
}
|
|
1628
1710
|
try {
|
|
1629
1711
|
await request("initialize", {
|
|
@@ -1648,7 +1730,7 @@ export async function createFxAgent(options = {}) {
|
|
|
1648
1730
|
try { runtime.abortHostEffects(); } catch {}
|
|
1649
1731
|
try { runtime.closeStdin(); } catch {}
|
|
1650
1732
|
try { await runtime.exited; } catch {}
|
|
1651
|
-
throw
|
|
1733
|
+
throw error;
|
|
1652
1734
|
}
|
|
1653
1735
|
|
|
1654
1736
|
const agent = {
|
|
@@ -1765,7 +1847,8 @@ export async function createFxAgent(options = {}) {
|
|
|
1765
1847
|
|
|
1766
1848
|
function startTurn(input, promptOptions) {
|
|
1767
1849
|
const prompt = normalizePromptInput(input);
|
|
1768
|
-
|
|
1850
|
+
const hasBlobs = prompt.some((block) => block.type === "image" && typeof block.data !== "string");
|
|
1851
|
+
if (promptFrameSize(prompt) > maxPromptFrameBytes) {
|
|
1769
1852
|
throw new RangeError(`prompt exceeds the ${maxPromptFrameBytes} byte libfx frame limit`);
|
|
1770
1853
|
}
|
|
1771
1854
|
const signal = promptOptions.signal;
|
|
@@ -1778,6 +1861,12 @@ export async function createFxAgent(options = {}) {
|
|
|
1778
1861
|
let terminalError;
|
|
1779
1862
|
let reportedPressure = false;
|
|
1780
1863
|
let discardedBytes = 0;
|
|
1864
|
+
let cancelBlobRead = null;
|
|
1865
|
+
let rejectBlobRead = null;
|
|
1866
|
+
let resolvePromptStart = null;
|
|
1867
|
+
const promptStarted = hasBlobs ? new Promise((resolve) => { resolvePromptStart = resolve; }) : null;
|
|
1868
|
+
let pendingSteeringCount = 0;
|
|
1869
|
+
let pendingSteeringBytes = 0;
|
|
1781
1870
|
const toolControllers = new Set();
|
|
1782
1871
|
let finished = false;
|
|
1783
1872
|
let cancelled = false;
|
|
@@ -1802,34 +1891,63 @@ export async function createFxAgent(options = {}) {
|
|
|
1802
1891
|
transportBytes: 0,
|
|
1803
1892
|
lastTransportActivityAt: null,
|
|
1804
1893
|
get cancelled() { return cancelled; },
|
|
1894
|
+
failPendingBlob(error) {
|
|
1895
|
+
rejectBlobRead?.(error);
|
|
1896
|
+
rejectBlobRead = null;
|
|
1897
|
+
cancelBlobRead = null;
|
|
1898
|
+
},
|
|
1899
|
+
promptWritten() {
|
|
1900
|
+
resolvePromptStart?.(true);
|
|
1901
|
+
resolvePromptStart = null;
|
|
1902
|
+
},
|
|
1805
1903
|
steer(text) {
|
|
1806
1904
|
if (finished || cancelled || activeTurn !== turn) {
|
|
1807
1905
|
return Promise.reject(new Error("no prompt is running"));
|
|
1808
1906
|
}
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
if (
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1907
|
+
if (closing) return Promise.reject(coreExitError ?? new Error("fx agent is closing"));
|
|
1908
|
+
const apply = () => {
|
|
1909
|
+
if (finished || cancelled || activeTurn !== turn) {
|
|
1910
|
+
return Promise.reject(new Error("no prompt is running"));
|
|
1911
|
+
}
|
|
1912
|
+
if (closing) return Promise.reject(coreExitError ?? new Error("fx agent is closing"));
|
|
1913
|
+
try {
|
|
1914
|
+
if (typeof runtime.steer === "function") {
|
|
1915
|
+
runtime.steer(text);
|
|
1916
|
+
void turn.push({
|
|
1917
|
+
sessionUpdate: "user_message_chunk",
|
|
1918
|
+
content: { type: "text", text },
|
|
1919
|
+
});
|
|
1920
|
+
return Promise.resolve();
|
|
1921
|
+
}
|
|
1922
|
+
return request("libfx/steer", { sessionId, text });
|
|
1923
|
+
} catch (error) {
|
|
1924
|
+
return Promise.reject(error);
|
|
1820
1925
|
}
|
|
1821
|
-
}
|
|
1822
|
-
|
|
1926
|
+
};
|
|
1927
|
+
if (!resolvePromptStart) return apply();
|
|
1928
|
+
const bytes = encoder.encode(text).length;
|
|
1929
|
+
if (pendingSteeringCount >= maxSteeringMessages || bytes > maxSteeringQueueBytes - pendingSteeringBytes) {
|
|
1930
|
+
return Promise.reject(new Error("steering queue is full"));
|
|
1823
1931
|
}
|
|
1824
|
-
|
|
1932
|
+
pendingSteeringCount++;
|
|
1933
|
+
pendingSteeringBytes += bytes;
|
|
1934
|
+
return promptStarted.then((started) => {
|
|
1935
|
+
if (coreExitError && !cancelled) throw coreExitError;
|
|
1936
|
+
return started === true ? apply() : Promise.reject(started instanceof Error ? started : new Error("no prompt is running"));
|
|
1937
|
+
}).finally(() => { pendingSteeringCount--; pendingSteeringBytes -= bytes; });
|
|
1825
1938
|
},
|
|
1826
1939
|
cancel() {
|
|
1827
1940
|
if (finished || cancelled) return;
|
|
1828
1941
|
cancelled = true;
|
|
1942
|
+
cancelBlobRead?.();
|
|
1943
|
+
cancelBlobRead = null;
|
|
1944
|
+
rejectBlobRead = null;
|
|
1945
|
+
resolvePromptStart?.(false);
|
|
1946
|
+
resolvePromptStart = null;
|
|
1829
1947
|
runtime.closeSteering?.();
|
|
1830
1948
|
resumeOutput?.();
|
|
1831
1949
|
resumeOutput = null;
|
|
1832
|
-
send({ jsonrpc: "2.0", method: "session/cancel", params: { sessionId } });
|
|
1950
|
+
if (!closing) send({ jsonrpc: "2.0", method: "session/cancel", params: { sessionId } });
|
|
1833
1951
|
for (const controller of toolControllers) controller.abort();
|
|
1834
1952
|
runtime.abortHostEffects();
|
|
1835
1953
|
},
|
|
@@ -1854,6 +1972,8 @@ export async function createFxAgent(options = {}) {
|
|
|
1854
1972
|
},
|
|
1855
1973
|
};
|
|
1856
1974
|
if (signal?.aborted) {
|
|
1975
|
+
resolvePromptStart?.(false);
|
|
1976
|
+
resolvePromptStart = null;
|
|
1857
1977
|
finished = true;
|
|
1858
1978
|
turn.result = Promise.resolve({ stopReason: "cancelled" });
|
|
1859
1979
|
return turn;
|
|
@@ -1862,10 +1982,37 @@ export async function createFxAgent(options = {}) {
|
|
|
1862
1982
|
runtime.openSteering?.();
|
|
1863
1983
|
const abort = () => turn.cancel();
|
|
1864
1984
|
signal?.addEventListener("abort", abort, { once: true });
|
|
1865
|
-
|
|
1866
|
-
|
|
1985
|
+
const blobReadCancelled = hasBlobs ? new Promise((resolve, reject) => {
|
|
1986
|
+
cancelBlobRead = () => resolve(null);
|
|
1987
|
+
rejectBlobRead = reject;
|
|
1988
|
+
}) : null;
|
|
1989
|
+
const response = hasBlobs
|
|
1990
|
+
? Promise.race([
|
|
1991
|
+
Promise.resolve().then(() => materializePromptBlobs(prompt, () => cancelled || closing)),
|
|
1992
|
+
blobReadCancelled,
|
|
1993
|
+
]).then((encodedPrompt) => {
|
|
1994
|
+
cancelBlobRead = null;
|
|
1995
|
+
rejectBlobRead = null;
|
|
1996
|
+
if (coreExitError && !cancelled) throw coreExitError;
|
|
1997
|
+
if (encodedPrompt === null || cancelled || closing) {
|
|
1998
|
+
resolvePromptStart?.(false);
|
|
1999
|
+
resolvePromptStart = null;
|
|
2000
|
+
return { stopReason: "cancelled" };
|
|
2001
|
+
}
|
|
2002
|
+
if (promptFrameSize(encodedPrompt) > maxPromptFrameBytes) {
|
|
2003
|
+
throw new RangeError(`prompt exceeds the ${maxPromptFrameBytes} byte libfx frame limit`);
|
|
2004
|
+
}
|
|
2005
|
+
return request("session/prompt", { sessionId, prompt: encodedPrompt });
|
|
2006
|
+
})
|
|
2007
|
+
: request("session/prompt", { sessionId, prompt });
|
|
2008
|
+
turn.result = response
|
|
2009
|
+
.then((value) => ({ stopReason: cancelled ? "cancelled" : value.stopReason, usage: value.usage }))
|
|
1867
2010
|
.catch((error) => {
|
|
1868
|
-
|
|
2011
|
+
cancelBlobRead = null;
|
|
2012
|
+
rejectBlobRead = null;
|
|
2013
|
+
resolvePromptStart?.(error);
|
|
2014
|
+
resolvePromptStart = null;
|
|
2015
|
+
if (cancelled && error.message === "Cancelled") return { stopReason: "cancelled" };
|
|
1869
2016
|
terminalError = error;
|
|
1870
2017
|
throw error;
|
|
1871
2018
|
})
|
package/fx-term.wasm
CHANGED
|
Binary file
|
package/libfx.darwin-arm64.node
CHANGED
|
Binary file
|
package/libfx.darwin-x64.node
CHANGED
|
Binary file
|
package/libfx.linux-arm64.node
CHANGED
|
Binary file
|
package/libfx.linux-x64.node
CHANGED
|
Binary file
|
package/node.cjs
CHANGED
|
@@ -264,9 +264,24 @@ function normalizeAgentOptions(value) {
|
|
|
264
264
|
throw new TypeError("createFxAgent() does not accept env; pass apiKey and model directly");
|
|
265
265
|
}
|
|
266
266
|
options.apiKey = boundedString(options.apiKey, "apiKey", maxApiKeyBytes, true);
|
|
267
|
-
options.model
|
|
268
|
-
|
|
269
|
-
|
|
267
|
+
if (options.model !== null && typeof options.model === "object" && !Array.isArray(options.model)) {
|
|
268
|
+
if (Object.hasOwn(options, "effort") || Object.hasOwn(options, "fast")) {
|
|
269
|
+
throw new TypeError("model options cannot be mixed with top-level effort or fast");
|
|
270
|
+
}
|
|
271
|
+
const model = options.model;
|
|
272
|
+
for (const name of Object.keys(model)) {
|
|
273
|
+
if (name !== "id" && name !== "effort" && name !== "fast") {
|
|
274
|
+
throw new TypeError(`unsupported model option: ${name}`);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
options.model = boundedString(model.id, "model.id", maxModelBytes, true);
|
|
278
|
+
options.effort = normalizeEffort(model.effort);
|
|
279
|
+
options.fast = normalizeFast(model.fast);
|
|
280
|
+
} else {
|
|
281
|
+
options.model = boundedString(options.model, "model", maxModelBytes, false);
|
|
282
|
+
options.effort = normalizeEffort(options.effort);
|
|
283
|
+
options.fast = normalizeFast(options.fast);
|
|
284
|
+
}
|
|
270
285
|
validateGatewayChatUrl(options.gatewayChatUrl);
|
|
271
286
|
return options;
|
|
272
287
|
}
|
|
@@ -279,12 +294,13 @@ function agentEnvironment(options) {
|
|
|
279
294
|
...options.gatewayChatUrl === undefined ? {} : { FX_GATEWAY_CHAT_URL: options.gatewayChatUrl }
|
|
280
295
|
};
|
|
281
296
|
}
|
|
282
|
-
function
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
error.
|
|
297
|
+
function agentRpcError(response) {
|
|
298
|
+
const error = new Error(response.message);
|
|
299
|
+
const data = response.data;
|
|
300
|
+
if (data && ["LIBFX_MODEL_UNSUPPORTED_EFFORT", "LIBFX_MODEL_UNSUPPORTED_FAST"].includes(data.code) && typeof data.model === "string" && data.capability === (data.code === "LIBFX_MODEL_UNSUPPORTED_FAST" ? "fast" : "effort")) {
|
|
301
|
+
error.code = data.code;
|
|
302
|
+
error.model = data.model;
|
|
303
|
+
error.capability = data.capability;
|
|
288
304
|
}
|
|
289
305
|
return error;
|
|
290
306
|
}
|
|
@@ -1634,6 +1650,15 @@ async function createFxTerminal(options) {
|
|
|
1634
1650
|
}
|
|
1635
1651
|
};
|
|
1636
1652
|
}
|
|
1653
|
+
function blobByteLength(value) {
|
|
1654
|
+
if (typeof Blob === "undefined" || value == null)
|
|
1655
|
+
return null;
|
|
1656
|
+
try {
|
|
1657
|
+
return Object.getOwnPropertyDescriptor(Blob.prototype, "size").get.call(value);
|
|
1658
|
+
} catch {
|
|
1659
|
+
return null;
|
|
1660
|
+
}
|
|
1661
|
+
}
|
|
1637
1662
|
function normalizePromptInput(input) {
|
|
1638
1663
|
if (typeof input === "string")
|
|
1639
1664
|
return [{ type: "text", text: input }];
|
|
@@ -1645,24 +1670,34 @@ function normalizePromptInput(input) {
|
|
|
1645
1670
|
if (!block || typeof block !== "object")
|
|
1646
1671
|
throw new TypeError(`prompt block ${index} must be an object`);
|
|
1647
1672
|
if (block.type === "image") {
|
|
1648
|
-
|
|
1649
|
-
|
|
1673
|
+
const size = blobByteLength(block.data);
|
|
1674
|
+
const blob = size !== null;
|
|
1675
|
+
if (!blob && (typeof block.data !== "string" || block.data.length === 0)) {
|
|
1676
|
+
throw new TypeError(`image prompt block ${index} requires base64 data or a Blob`);
|
|
1677
|
+
}
|
|
1678
|
+
const mimeType = blob ? block.data.type : block.mimeType;
|
|
1679
|
+
if (blob && block.mimeType !== undefined && block.mimeType !== mimeType) {
|
|
1680
|
+
throw new TypeError(`image prompt block ${index} mimeType disagrees with Blob.type`);
|
|
1650
1681
|
}
|
|
1651
|
-
if (typeof
|
|
1682
|
+
if (typeof mimeType !== "string" || mimeType.length === 0 || mimeType.length > 128) {
|
|
1652
1683
|
throw new TypeError(`image prompt block ${index} requires a mimeType`);
|
|
1653
1684
|
}
|
|
1654
|
-
if (
|
|
1685
|
+
if (blob && (!Number.isSafeInteger(size) || size <= 0)) {
|
|
1686
|
+
throw new TypeError(`image prompt block ${index} requires a non-empty Blob with a valid size`);
|
|
1687
|
+
}
|
|
1688
|
+
const encodedLength = blob ? Math.ceil(size / 3) * 4 : block.data.length;
|
|
1689
|
+
if (encodedLength > maxPromptImageDataBytes) {
|
|
1655
1690
|
throw new RangeError(`image prompt block ${index} exceeds the ${maxPromptImageDataBytes} byte per-image libfx limit`);
|
|
1656
1691
|
}
|
|
1657
1692
|
imageCount += 1;
|
|
1658
1693
|
if (imageCount > maxPromptImages) {
|
|
1659
1694
|
throw new RangeError(`prompt cannot contain more than ${maxPromptImages} images`);
|
|
1660
1695
|
}
|
|
1661
|
-
imageBytes +=
|
|
1696
|
+
imageBytes += encodedLength;
|
|
1662
1697
|
if (imageBytes > maxPromptImagesBytes) {
|
|
1663
1698
|
throw new RangeError(`prompt images exceed the ${maxPromptImagesBytes} byte libfx frame limit`);
|
|
1664
1699
|
}
|
|
1665
|
-
return { type: "image", data: block.data, mimeType
|
|
1700
|
+
return { type: "image", data: block.data, mimeType };
|
|
1666
1701
|
}
|
|
1667
1702
|
if (block.type === "text") {
|
|
1668
1703
|
if (typeof block.text !== "string")
|
|
@@ -1680,6 +1715,52 @@ function normalizePromptInput(input) {
|
|
|
1680
1715
|
throw new TypeError(`unsupported prompt block type: ${String(block.type)}`);
|
|
1681
1716
|
});
|
|
1682
1717
|
}
|
|
1718
|
+
function promptFrameSize(prompt) {
|
|
1719
|
+
let blobDataBytes = 0;
|
|
1720
|
+
const projected = prompt.map((block) => {
|
|
1721
|
+
if (block.type !== "image" || typeof block.data === "string")
|
|
1722
|
+
return block;
|
|
1723
|
+
const size = blobByteLength(block.data);
|
|
1724
|
+
if (!Number.isSafeInteger(size))
|
|
1725
|
+
throw new TypeError("image prompt requires a valid Blob size");
|
|
1726
|
+
blobDataBytes += Math.ceil(size / 3) * 4;
|
|
1727
|
+
return { type: "image", data: "", mimeType: block.mimeType };
|
|
1728
|
+
});
|
|
1729
|
+
return encoder.encode(JSON.stringify({ sessionId: "", prompt: projected })).length + blobDataBytes + promptFrameEnvelopeBytes;
|
|
1730
|
+
}
|
|
1731
|
+
async function materializePromptBlobs(blocks, isCancelled) {
|
|
1732
|
+
const encoded = [];
|
|
1733
|
+
let imageBytes = 0;
|
|
1734
|
+
for (const [index, block] of blocks.entries()) {
|
|
1735
|
+
if (isCancelled())
|
|
1736
|
+
return null;
|
|
1737
|
+
if (block.type !== "image") {
|
|
1738
|
+
encoded.push(block);
|
|
1739
|
+
continue;
|
|
1740
|
+
}
|
|
1741
|
+
if (typeof block.data === "string") {
|
|
1742
|
+
imageBytes += block.data.length;
|
|
1743
|
+
if (imageBytes > maxPromptImagesBytes) {
|
|
1744
|
+
throw new RangeError(`prompt images exceed the ${maxPromptImagesBytes} byte libfx frame limit`);
|
|
1745
|
+
}
|
|
1746
|
+
encoded.push(block);
|
|
1747
|
+
continue;
|
|
1748
|
+
}
|
|
1749
|
+
const bytes = new Uint8Array(await block.data.arrayBuffer());
|
|
1750
|
+
if (isCancelled())
|
|
1751
|
+
return null;
|
|
1752
|
+
const encodedLength = Math.ceil(bytes.length / 3) * 4;
|
|
1753
|
+
if (encodedLength > maxPromptImageDataBytes) {
|
|
1754
|
+
throw new RangeError(`image prompt block ${index} exceeds the ${maxPromptImageDataBytes} byte per-image libfx limit`);
|
|
1755
|
+
}
|
|
1756
|
+
imageBytes += encodedLength;
|
|
1757
|
+
if (imageBytes > maxPromptImagesBytes) {
|
|
1758
|
+
throw new RangeError(`prompt images exceed the ${maxPromptImagesBytes} byte libfx frame limit`);
|
|
1759
|
+
}
|
|
1760
|
+
encoded.push({ type: "image", data: bytesToBase64(bytes), mimeType: block.mimeType });
|
|
1761
|
+
}
|
|
1762
|
+
return normalizePromptInput(encoded);
|
|
1763
|
+
}
|
|
1683
1764
|
function normalizeSteeringInput(input) {
|
|
1684
1765
|
const blocks = normalizePromptInput(input);
|
|
1685
1766
|
if (blocks.some((block) => block.type !== "text")) {
|
|
@@ -1823,6 +1904,7 @@ async function createFxAgent(options = {}) {
|
|
|
1823
1904
|
let sessionId = null;
|
|
1824
1905
|
let activeTurn = null;
|
|
1825
1906
|
let closing = false;
|
|
1907
|
+
let coreExitError = null;
|
|
1826
1908
|
const isCurrentTurn = (turn) => turn && activeTurn === turn && !turn.cancelled && !closing;
|
|
1827
1909
|
const emit = (type, detail = {}) => {
|
|
1828
1910
|
try {
|
|
@@ -1962,8 +2044,12 @@ async function createFxAgent(options = {}) {
|
|
|
1962
2044
|
if (closing)
|
|
1963
2045
|
throw new Error("fx agent is closing");
|
|
1964
2046
|
emit("acp.send", { message });
|
|
2047
|
+
if (message.method === "session/prompt" && activeTurn?.cancelled)
|
|
2048
|
+
throw new Error("Cancelled");
|
|
1965
2049
|
runtime.write(`${JSON.stringify(message)}
|
|
1966
2050
|
`);
|
|
2051
|
+
if (message.method === "session/prompt")
|
|
2052
|
+
activeTurn?.promptWritten();
|
|
1967
2053
|
};
|
|
1968
2054
|
const request = (method, params = {}) => new Promise((resolve, reject) => {
|
|
1969
2055
|
const id = nextId++;
|
|
@@ -1976,12 +2062,14 @@ async function createFxAgent(options = {}) {
|
|
|
1976
2062
|
}
|
|
1977
2063
|
});
|
|
1978
2064
|
runtime.exited.then((code) => {
|
|
1979
|
-
emit("runtime.exit", { code });
|
|
1980
2065
|
closing = true;
|
|
1981
2066
|
const error = runtime.error ?? new Error(`fx-core exited with code ${code} before completing the ACP request`);
|
|
2067
|
+
coreExitError = error;
|
|
2068
|
+
activeTurn?.failPendingBlob(error);
|
|
1982
2069
|
for (const waiter of pending.values())
|
|
1983
2070
|
waiter.reject(error);
|
|
1984
2071
|
pending.clear();
|
|
2072
|
+
emit("runtime.exit", { code });
|
|
1985
2073
|
});
|
|
1986
2074
|
runtime.setLineHandler((message, size) => {
|
|
1987
2075
|
emit("acp.receive", { message });
|
|
@@ -2028,7 +2116,7 @@ async function createFxAgent(options = {}) {
|
|
|
2028
2116
|
return;
|
|
2029
2117
|
pending.delete(message.id);
|
|
2030
2118
|
if (message.error)
|
|
2031
|
-
waiter.reject(
|
|
2119
|
+
waiter.reject(agentRpcError(message.error));
|
|
2032
2120
|
else
|
|
2033
2121
|
waiter.resolve(message.result);
|
|
2034
2122
|
}
|
|
@@ -2058,7 +2146,7 @@ async function createFxAgent(options = {}) {
|
|
|
2058
2146
|
try {
|
|
2059
2147
|
await runtime.exited;
|
|
2060
2148
|
} catch {}
|
|
2061
|
-
throw
|
|
2149
|
+
throw error;
|
|
2062
2150
|
}
|
|
2063
2151
|
const agent = {
|
|
2064
2152
|
prompt(input, promptOptions = {}) {
|
|
@@ -2201,7 +2289,8 @@ async function createFxAgent(options = {}) {
|
|
|
2201
2289
|
}
|
|
2202
2290
|
function startTurn(input, promptOptions) {
|
|
2203
2291
|
const prompt = normalizePromptInput(input);
|
|
2204
|
-
|
|
2292
|
+
const hasBlobs = prompt.some((block) => block.type === "image" && typeof block.data !== "string");
|
|
2293
|
+
if (promptFrameSize(prompt) > maxPromptFrameBytes) {
|
|
2205
2294
|
throw new RangeError(`prompt exceeds the ${maxPromptFrameBytes} byte libfx frame limit`);
|
|
2206
2295
|
}
|
|
2207
2296
|
const signal = promptOptions.signal;
|
|
@@ -2215,6 +2304,14 @@ async function createFxAgent(options = {}) {
|
|
|
2215
2304
|
let terminalError;
|
|
2216
2305
|
let reportedPressure = false;
|
|
2217
2306
|
let discardedBytes = 0;
|
|
2307
|
+
let cancelBlobRead = null;
|
|
2308
|
+
let rejectBlobRead = null;
|
|
2309
|
+
let resolvePromptStart = null;
|
|
2310
|
+
const promptStarted = hasBlobs ? new Promise((resolve) => {
|
|
2311
|
+
resolvePromptStart = resolve;
|
|
2312
|
+
}) : null;
|
|
2313
|
+
let pendingSteeringCount = 0;
|
|
2314
|
+
let pendingSteeringBytes = 0;
|
|
2218
2315
|
const toolControllers = new Set;
|
|
2219
2316
|
let finished = false;
|
|
2220
2317
|
let cancelled = false;
|
|
@@ -2251,35 +2348,72 @@ async function createFxAgent(options = {}) {
|
|
|
2251
2348
|
get cancelled() {
|
|
2252
2349
|
return cancelled;
|
|
2253
2350
|
},
|
|
2351
|
+
failPendingBlob(error) {
|
|
2352
|
+
rejectBlobRead?.(error);
|
|
2353
|
+
rejectBlobRead = null;
|
|
2354
|
+
cancelBlobRead = null;
|
|
2355
|
+
},
|
|
2356
|
+
promptWritten() {
|
|
2357
|
+
resolvePromptStart?.(true);
|
|
2358
|
+
resolvePromptStart = null;
|
|
2359
|
+
},
|
|
2254
2360
|
steer(text) {
|
|
2255
2361
|
if (finished || cancelled || activeTurn !== turn) {
|
|
2256
2362
|
return Promise.reject(new Error("no prompt is running"));
|
|
2257
2363
|
}
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2364
|
+
if (closing)
|
|
2365
|
+
return Promise.reject(coreExitError ?? new Error("fx agent is closing"));
|
|
2366
|
+
const apply = () => {
|
|
2367
|
+
if (finished || cancelled || activeTurn !== turn) {
|
|
2368
|
+
return Promise.reject(new Error("no prompt is running"));
|
|
2369
|
+
}
|
|
2370
|
+
if (closing)
|
|
2371
|
+
return Promise.reject(coreExitError ?? new Error("fx agent is closing"));
|
|
2372
|
+
try {
|
|
2373
|
+
if (typeof runtime.steer === "function") {
|
|
2374
|
+
runtime.steer(text);
|
|
2375
|
+
turn.push({
|
|
2376
|
+
sessionUpdate: "user_message_chunk",
|
|
2377
|
+
content: { type: "text", text }
|
|
2378
|
+
});
|
|
2379
|
+
return Promise.resolve();
|
|
2380
|
+
}
|
|
2381
|
+
return request("libfx/steer", { sessionId, text });
|
|
2382
|
+
} catch (error) {
|
|
2383
|
+
return Promise.reject(error);
|
|
2269
2384
|
}
|
|
2270
|
-
}
|
|
2271
|
-
|
|
2385
|
+
};
|
|
2386
|
+
if (!resolvePromptStart)
|
|
2387
|
+
return apply();
|
|
2388
|
+
const bytes = encoder.encode(text).length;
|
|
2389
|
+
if (pendingSteeringCount >= maxSteeringMessages || bytes > maxSteeringQueueBytes - pendingSteeringBytes) {
|
|
2390
|
+
return Promise.reject(new Error("steering queue is full"));
|
|
2272
2391
|
}
|
|
2273
|
-
|
|
2392
|
+
pendingSteeringCount++;
|
|
2393
|
+
pendingSteeringBytes += bytes;
|
|
2394
|
+
return promptStarted.then((started) => {
|
|
2395
|
+
if (coreExitError && !cancelled)
|
|
2396
|
+
throw coreExitError;
|
|
2397
|
+
return started === true ? apply() : Promise.reject(started instanceof Error ? started : new Error("no prompt is running"));
|
|
2398
|
+
}).finally(() => {
|
|
2399
|
+
pendingSteeringCount--;
|
|
2400
|
+
pendingSteeringBytes -= bytes;
|
|
2401
|
+
});
|
|
2274
2402
|
},
|
|
2275
2403
|
cancel() {
|
|
2276
2404
|
if (finished || cancelled)
|
|
2277
2405
|
return;
|
|
2278
2406
|
cancelled = true;
|
|
2407
|
+
cancelBlobRead?.();
|
|
2408
|
+
cancelBlobRead = null;
|
|
2409
|
+
rejectBlobRead = null;
|
|
2410
|
+
resolvePromptStart?.(false);
|
|
2411
|
+
resolvePromptStart = null;
|
|
2279
2412
|
runtime.closeSteering?.();
|
|
2280
2413
|
resumeOutput?.();
|
|
2281
2414
|
resumeOutput = null;
|
|
2282
|
-
|
|
2415
|
+
if (!closing)
|
|
2416
|
+
send({ jsonrpc: "2.0", method: "session/cancel", params: { sessionId } });
|
|
2283
2417
|
for (const controller of toolControllers)
|
|
2284
2418
|
controller.abort();
|
|
2285
2419
|
runtime.abortHostEffects();
|
|
@@ -2311,6 +2445,8 @@ async function createFxAgent(options = {}) {
|
|
|
2311
2445
|
}
|
|
2312
2446
|
};
|
|
2313
2447
|
if (signal?.aborted) {
|
|
2448
|
+
resolvePromptStart?.(false);
|
|
2449
|
+
resolvePromptStart = null;
|
|
2314
2450
|
finished = true;
|
|
2315
2451
|
turn.result = Promise.resolve({ stopReason: "cancelled" });
|
|
2316
2452
|
return turn;
|
|
@@ -2319,8 +2455,34 @@ async function createFxAgent(options = {}) {
|
|
|
2319
2455
|
runtime.openSteering?.();
|
|
2320
2456
|
const abort = () => turn.cancel();
|
|
2321
2457
|
signal?.addEventListener("abort", abort, { once: true });
|
|
2322
|
-
|
|
2323
|
-
|
|
2458
|
+
const blobReadCancelled = hasBlobs ? new Promise((resolve, reject) => {
|
|
2459
|
+
cancelBlobRead = () => resolve(null);
|
|
2460
|
+
rejectBlobRead = reject;
|
|
2461
|
+
}) : null;
|
|
2462
|
+
const response = hasBlobs ? Promise.race([
|
|
2463
|
+
Promise.resolve().then(() => materializePromptBlobs(prompt, () => cancelled || closing)),
|
|
2464
|
+
blobReadCancelled
|
|
2465
|
+
]).then((encodedPrompt) => {
|
|
2466
|
+
cancelBlobRead = null;
|
|
2467
|
+
rejectBlobRead = null;
|
|
2468
|
+
if (coreExitError && !cancelled)
|
|
2469
|
+
throw coreExitError;
|
|
2470
|
+
if (encodedPrompt === null || cancelled || closing) {
|
|
2471
|
+
resolvePromptStart?.(false);
|
|
2472
|
+
resolvePromptStart = null;
|
|
2473
|
+
return { stopReason: "cancelled" };
|
|
2474
|
+
}
|
|
2475
|
+
if (promptFrameSize(encodedPrompt) > maxPromptFrameBytes) {
|
|
2476
|
+
throw new RangeError(`prompt exceeds the ${maxPromptFrameBytes} byte libfx frame limit`);
|
|
2477
|
+
}
|
|
2478
|
+
return request("session/prompt", { sessionId, prompt: encodedPrompt });
|
|
2479
|
+
}) : request("session/prompt", { sessionId, prompt });
|
|
2480
|
+
turn.result = response.then((value) => ({ stopReason: cancelled ? "cancelled" : value.stopReason, usage: value.usage })).catch((error) => {
|
|
2481
|
+
cancelBlobRead = null;
|
|
2482
|
+
rejectBlobRead = null;
|
|
2483
|
+
resolvePromptStart?.(error);
|
|
2484
|
+
resolvePromptStart = null;
|
|
2485
|
+
if (cancelled && error.message === "Cancelled")
|
|
2324
2486
|
return { stopReason: "cancelled" };
|
|
2325
2487
|
terminalError = error;
|
|
2326
2488
|
throw error;
|
|
@@ -2848,10 +3010,11 @@ function createNativeAgent(addon, options) {
|
|
|
2848
3010
|
});
|
|
2849
3011
|
}
|
|
2850
3012
|
async function createWithFallback(surface, nativeMethod, wasmFactory, defaultWasm, options) {
|
|
2851
|
-
const { nativeAddon, backend = "auto", ...
|
|
3013
|
+
const { nativeAddon, backend = "auto", ...unvalidatedOptions } = options ?? {};
|
|
2852
3014
|
if (!new Set(["auto", "native", "wasm"]).has(backend)) {
|
|
2853
3015
|
throw new TypeError('backend must be "auto", "native", or "wasm"');
|
|
2854
3016
|
}
|
|
3017
|
+
const runtimeOptions = surface === "agent" ? normalizeAgentOptions(unvalidatedOptions) : unvalidatedOptions;
|
|
2855
3018
|
let nativeError;
|
|
2856
3019
|
let nativeAttempted = false;
|
|
2857
3020
|
if (backend !== "wasm") {
|
|
@@ -2865,7 +3028,7 @@ async function createWithFallback(surface, nativeMethod, wasmFactory, defaultWas
|
|
|
2865
3028
|
return await native.backend[nativeMethod](runtimeOptions);
|
|
2866
3029
|
} catch (error) {
|
|
2867
3030
|
nativeError = error;
|
|
2868
|
-
if (backend === "native")
|
|
3031
|
+
if (backend === "native" || error?.code === "LIBFX_MODEL_UNSUPPORTED_FAST" || error?.code === "LIBFX_MODEL_UNSUPPORTED_EFFORT")
|
|
2869
3032
|
throw error;
|
|
2870
3033
|
}
|
|
2871
3034
|
}
|
|
@@ -2884,9 +3047,6 @@ async function createWithFallback(surface, nativeMethod, wasmFactory, defaultWas
|
|
|
2884
3047
|
return wasmFactory({ ...runtimeOptions, wasm: await wasmInput(wasmSource) });
|
|
2885
3048
|
}
|
|
2886
3049
|
async function createFxAgent2(options = {}) {
|
|
2887
|
-
if (options != null && Object.hasOwn(Object(options), "env")) {
|
|
2888
|
-
throw new TypeError("createFxAgent() does not accept env; pass apiKey and model directly");
|
|
2889
|
-
}
|
|
2890
3050
|
return createWithFallback("agent", "createCore", createFxAgent, defaultCoreWasm, options);
|
|
2891
3051
|
}
|
|
2892
3052
|
function createFxTerminal2(options = {}) {
|
package/node.js
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
encodeXtermKeyEvent,
|
|
14
14
|
fxSdkApiVersion,
|
|
15
15
|
listModels,
|
|
16
|
+
normalizeAgentOptions,
|
|
16
17
|
supportsJspi,
|
|
17
18
|
xtermAdapter,
|
|
18
19
|
} from "./fx-sdk.js";
|
|
@@ -513,10 +514,11 @@ function createNativeAgent(addon, options) {
|
|
|
513
514
|
}
|
|
514
515
|
|
|
515
516
|
async function createWithFallback(surface, nativeMethod, wasmFactory, defaultWasm, options) {
|
|
516
|
-
const { nativeAddon, backend = "auto", ...
|
|
517
|
+
const { nativeAddon, backend = "auto", ...unvalidatedOptions } = options ?? {};
|
|
517
518
|
if (!new Set(["auto", "native", "wasm"]).has(backend)) {
|
|
518
519
|
throw new TypeError('backend must be "auto", "native", or "wasm"');
|
|
519
520
|
}
|
|
521
|
+
const runtimeOptions = surface === "agent" ? normalizeAgentOptions(unvalidatedOptions) : unvalidatedOptions;
|
|
520
522
|
|
|
521
523
|
let nativeError;
|
|
522
524
|
let nativeAttempted = false;
|
|
@@ -530,7 +532,8 @@ async function createWithFallback(surface, nativeMethod, wasmFactory, defaultWas
|
|
|
530
532
|
return await native.backend[nativeMethod](runtimeOptions);
|
|
531
533
|
} catch (error) {
|
|
532
534
|
nativeError = error;
|
|
533
|
-
if (backend === "native"
|
|
535
|
+
if (backend === "native" || error?.code === "LIBFX_MODEL_UNSUPPORTED_FAST" ||
|
|
536
|
+
error?.code === "LIBFX_MODEL_UNSUPPORTED_EFFORT") throw error;
|
|
534
537
|
}
|
|
535
538
|
}
|
|
536
539
|
if (backend === "native") {
|
|
@@ -549,9 +552,6 @@ async function createWithFallback(surface, nativeMethod, wasmFactory, defaultWas
|
|
|
549
552
|
}
|
|
550
553
|
|
|
551
554
|
export async function createFxAgent(options = {}) {
|
|
552
|
-
if (options != null && Object.hasOwn(Object(options), "env")) {
|
|
553
|
-
throw new TypeError("createFxAgent() does not accept env; pass apiKey and model directly");
|
|
554
|
-
}
|
|
555
555
|
return createWithFallback(
|
|
556
556
|
"agent",
|
|
557
557
|
"createCore",
|