libfx 0.0.10-dev.1213.g4c99a4b2ea67 → 0.0.10-dev.1217.g707afac508e1
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 +53 -8
- package/fx-core.wasm +0 -0
- package/fx-sdk.js +116 -3
- 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 +135 -3
- package/node.js +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# libfx
|
|
2
2
|
|
|
3
3
|
`libfx` is the small fx agent kernel for JavaScript hosts. One agent is one
|
|
4
|
-
in-memory conversation with
|
|
5
|
-
|
|
4
|
+
in-memory conversation with `prompt`, `checkpoint`, and `close` operations,
|
|
5
|
+
plus mid-turn steering on each running turn.
|
|
6
6
|
|
|
7
7
|
```sh
|
|
8
8
|
npm install libfx
|
|
@@ -63,7 +63,12 @@ can resolve model capabilities and context capacity through the supplied
|
|
|
63
63
|
|
|
64
64
|
`onEvent` receives runtime diagnostics separately from model output. Transport
|
|
65
65
|
events report request start, response status and elapsed time, safe Gateway
|
|
66
|
-
request metadata,
|
|
66
|
+
request metadata, failures, and throttled `transport.activity` liveness while a
|
|
67
|
+
response body is streaming. Activity events include the attempt, current chunk
|
|
68
|
+
bytes, and cumulative response bytes. They call the host directly at most once
|
|
69
|
+
per 250 ms rather than entering the normalized turn queue, so an unread turn
|
|
70
|
+
cannot accumulate heartbeat events. Credentials and raw headers are never
|
|
71
|
+
included.
|
|
67
72
|
|
|
68
73
|
libfx makes at most one automatic retry after a retryable transport failure and
|
|
69
74
|
only before model output or tool effects escape. Cancellation prevents a retry.
|
|
@@ -77,6 +82,7 @@ blocks. It returns an async iterable of normalized events:
|
|
|
77
82
|
64 KiB of JSON arrive as an `inputPreview` string prefix plus
|
|
78
83
|
`inputTruncated: true` instead; the tool still receives complete arguments.
|
|
79
84
|
- `tool_end`
|
|
85
|
+
- `user_message` with `text` when accepted mid-turn steering enters the turn
|
|
80
86
|
|
|
81
87
|
Consume the turn while it runs, then await `turn.result`. Streamed text and
|
|
82
88
|
tool results are lossless and backpressured: a slow reader pauses production
|
|
@@ -121,9 +127,29 @@ leave the process. Prompt images are retained in checkpoints within the
|
|
|
121
127
|
existing 4 MiB checkpoint bound, so a restored agent can refer to earlier
|
|
122
128
|
images on either backend.
|
|
123
129
|
|
|
124
|
-
Only one prompt may run at a time.
|
|
125
|
-
|
|
126
|
-
|
|
130
|
+
Only one top-level prompt may run at a time. While it runs,
|
|
131
|
+
`await turn.steer(text)` appends guidance at the next safe model boundary
|
|
132
|
+
without discarding the in-flight response or completed tool work. Steering also
|
|
133
|
+
accepts an array of text blocks; image and resource steering blocks are rejected.
|
|
134
|
+
Each message is limited to 64 KiB, with at most 64 queued messages and 1 MiB of
|
|
135
|
+
queued steering text. Accepted steering appears as a `user_message` event before
|
|
136
|
+
the model's continued output. Calling `steer()` after the turn settles rejects
|
|
137
|
+
with `no prompt is running`.
|
|
138
|
+
|
|
139
|
+
```js
|
|
140
|
+
const turn = agent.prompt("Build the feature.");
|
|
141
|
+
for await (const event of turn) {
|
|
142
|
+
if (event.type === "tool_end" && event.name === "read_file") {
|
|
143
|
+
await turn.steer("Keep the public API backward compatible.");
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Cancelling a steered turn drops any guidance that has not reached a safe
|
|
149
|
+
boundary and releases its queue. Applied guidance is part of the same history
|
|
150
|
+
turn, so an idle `checkpoint()` includes the full steered conversation.
|
|
151
|
+
`checkpoint()` returns opaque, bounded, versioned bytes. Restore them only when
|
|
152
|
+
creating a fresh agent:
|
|
127
153
|
|
|
128
154
|
```js
|
|
129
155
|
const restored = await createFxAgent({ apiKey, model, checkpoint });
|
|
@@ -178,8 +204,27 @@ const agent = await createFxAgent({
|
|
|
178
204
|
});
|
|
179
205
|
```
|
|
180
206
|
|
|
181
|
-
|
|
182
|
-
|
|
207
|
+
Gateway web search can run at the provider instead of in the JavaScript host.
|
|
208
|
+
Mark its canonical tool name with `providerExecuted: true` and omit `execute`:
|
|
209
|
+
|
|
210
|
+
```js
|
|
211
|
+
const agent = await createFxAgent({
|
|
212
|
+
apiKey,
|
|
213
|
+
tools: [{ name: "web_search", providerExecuted: true }],
|
|
214
|
+
});
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
The kernel supplies the canonical schema and Gateway advertisement. Currently
|
|
218
|
+
`web_search` is the supported provider-executed descriptor; unknown or local
|
|
219
|
+
names reject agent creation. Provider-executed tools do not
|
|
220
|
+
call host code or require a separate provider key; their `tool_start` and
|
|
221
|
+
`tool_end` events, results, and checkpoint history use the same turn contract.
|
|
222
|
+
Their built-in permission policy is enforced when the request is projected, as
|
|
223
|
+
there is no local call-time effect to approve.
|
|
224
|
+
|
|
225
|
+
For ordinary tools, the JavaScript host is the authority for effects. The same
|
|
226
|
+
descriptors, schemas, cancellation, results, and events are used by N-API and
|
|
227
|
+
WebAssembly.
|
|
183
228
|
Cancelling a prompt aborts its tools' signals and stops waiting for their
|
|
184
229
|
callbacks. Late results and rejections are ignored. Tools remain responsible
|
|
185
230
|
for stopping their own work when their signal is aborted.
|
package/fx-core.wasm
CHANGED
|
Binary file
|
package/fx-sdk.js
CHANGED
|
@@ -16,6 +16,7 @@ const maxUrlBytes = 16 * 1024;
|
|
|
16
16
|
const maxModelCatalogBytes = 4 * 1024 * 1024;
|
|
17
17
|
const maxModelCatalogEntries = 10_000;
|
|
18
18
|
const streamReadsPerTaskYield = 32;
|
|
19
|
+
const transportActivityIntervalMs = 250;
|
|
19
20
|
const maxUnreadEventBytes = 1024 * 1024;
|
|
20
21
|
const maxUnreadEvents = 256;
|
|
21
22
|
// Prompt image limits mirror the host tool result image contract: the kernel
|
|
@@ -28,6 +29,9 @@ const maxPromptImagesBytes = 8 * 1024 * 1024;
|
|
|
28
29
|
// envelope allowance covers the method key and request id.
|
|
29
30
|
const maxPromptFrameBytes = 8 * 1024 * 1024;
|
|
30
31
|
const promptFrameEnvelopeBytes = 128;
|
|
32
|
+
const maxSteeringMessageBytes = 64 * 1024;
|
|
33
|
+
const maxSteeringMessages = 64;
|
|
34
|
+
const maxSteeringQueueBytes = 1024 * 1024;
|
|
31
35
|
// tool_start events carry a bounded preview of the tool input; larger inputs
|
|
32
36
|
// are marked truncated instead of dropped or sent whole.
|
|
33
37
|
const maxToolStartInputBytes = 64 * 1024;
|
|
@@ -444,6 +448,9 @@ function createRuntime(options) {
|
|
|
444
448
|
const stdin = new ByteQueue();
|
|
445
449
|
const streams = new Map();
|
|
446
450
|
const httpRequests = new Set();
|
|
451
|
+
const steering = [];
|
|
452
|
+
let steeringBytes = 0;
|
|
453
|
+
let steeringOpen = false;
|
|
447
454
|
const workspaceExecs = new Set();
|
|
448
455
|
const workspace = prepareWorkspaceAdapter(options.workspace);
|
|
449
456
|
const args = ["fx", ...(options.args || [])];
|
|
@@ -656,6 +663,7 @@ function createRuntime(options) {
|
|
|
656
663
|
state.readResult = null;
|
|
657
664
|
if (done) return 0;
|
|
658
665
|
if (!value?.length) return null;
|
|
666
|
+
options.onTransportChunk?.(value.length);
|
|
659
667
|
return copy(value);
|
|
660
668
|
};
|
|
661
669
|
const immediate = consume();
|
|
@@ -709,6 +717,36 @@ function createRuntime(options) {
|
|
|
709
717
|
});
|
|
710
718
|
}
|
|
711
719
|
|
|
720
|
+
function clearSteering() {
|
|
721
|
+
steering.length = 0;
|
|
722
|
+
steeringBytes = 0;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
function queueSteering(text) {
|
|
726
|
+
if (!steeringOpen) throw new Error("no prompt is running");
|
|
727
|
+
const value = encoder.encode(text);
|
|
728
|
+
if (value.length === 0) throw new TypeError("steering text cannot be empty");
|
|
729
|
+
if (value.length > maxSteeringMessageBytes) {
|
|
730
|
+
throw new RangeError(`steering text exceeds the ${maxSteeringMessageBytes} byte libfx limit`);
|
|
731
|
+
}
|
|
732
|
+
if (steering.length >= maxSteeringMessages || value.length > maxSteeringQueueBytes - steeringBytes) {
|
|
733
|
+
throw new Error("steering queue is full");
|
|
734
|
+
}
|
|
735
|
+
steering.push(value);
|
|
736
|
+
steeringBytes += value.length;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
function steeringTake(outputPtr, outputCap) {
|
|
740
|
+
const value = steering[0];
|
|
741
|
+
if (!value) return 0;
|
|
742
|
+
const output = checkedBytes(outputPtr, outputCap);
|
|
743
|
+
if (!output || value.length > output.length) return -1;
|
|
744
|
+
output.subarray(0, value.length).set(value);
|
|
745
|
+
steering.shift();
|
|
746
|
+
steeringBytes -= value.length;
|
|
747
|
+
return value.length;
|
|
748
|
+
}
|
|
749
|
+
|
|
712
750
|
let pendingHostToolResult = null;
|
|
713
751
|
function hostToolCall(namePtr, nameLen, argumentsPtr, argumentsLen, outputPtr, outputCap, statusPtr) {
|
|
714
752
|
pendingHostToolResult = null;
|
|
@@ -1078,6 +1116,8 @@ function createRuntime(options) {
|
|
|
1078
1116
|
return chunk.length;
|
|
1079
1117
|
},
|
|
1080
1118
|
fx_host_tool_result_release() { pendingHostToolResult = null; },
|
|
1119
|
+
fx_steering_take: steeringTake,
|
|
1120
|
+
fx_steering_close() { steeringOpen = false; clearSteering(); },
|
|
1081
1121
|
fx_open_url: new WebAssembly.Suspending(openUrl),
|
|
1082
1122
|
fx_oauth_session_load: new WebAssembly.Suspending(oauthSessionLoad),
|
|
1083
1123
|
fx_oauth_session_commit: new WebAssembly.Suspending(oauthSessionCommit),
|
|
@@ -1105,7 +1145,10 @@ function createRuntime(options) {
|
|
|
1105
1145
|
setInstance(value) { instance = value; },
|
|
1106
1146
|
write(data) { stdin.push(typeof data === "string" ? encoder.encode(data) : data); },
|
|
1107
1147
|
wake() { stdin.wake(); },
|
|
1108
|
-
closeStdin() { stdin.close(); },
|
|
1148
|
+
closeStdin() { steeringOpen = false; clearSteering(); stdin.close(); },
|
|
1149
|
+
openSteering() { clearSteering(); steeringOpen = true; },
|
|
1150
|
+
steer: queueSteering,
|
|
1151
|
+
closeSteering() { steeringOpen = false; clearSteering(); },
|
|
1109
1152
|
abortHostEffects,
|
|
1110
1153
|
abort(error) {
|
|
1111
1154
|
aborted = true;
|
|
@@ -1274,19 +1317,42 @@ function normalizePromptInput(input) {
|
|
|
1274
1317
|
});
|
|
1275
1318
|
}
|
|
1276
1319
|
|
|
1320
|
+
function normalizeSteeringInput(input) {
|
|
1321
|
+
const blocks = normalizePromptInput(input);
|
|
1322
|
+
if (blocks.some((block) => block.type !== "text")) {
|
|
1323
|
+
throw new TypeError("steering accepts only text blocks");
|
|
1324
|
+
}
|
|
1325
|
+
const text = blocks.map((block) => block.text).join("\n");
|
|
1326
|
+
if (text.length === 0) throw new TypeError("steering text cannot be empty");
|
|
1327
|
+
if (encoder.encode(text).length > maxSteeringMessageBytes) {
|
|
1328
|
+
throw new RangeError(`steering text exceeds the ${maxSteeringMessageBytes} byte libfx limit`);
|
|
1329
|
+
}
|
|
1330
|
+
return text;
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1277
1333
|
function normalizeHostTools(value) {
|
|
1278
1334
|
if (value === undefined) return { descriptors: [], executors: new Map() };
|
|
1279
1335
|
if (!Array.isArray(value)) throw new TypeError("tools must be an array");
|
|
1280
1336
|
if (value.length > 64) throw new RangeError("tools cannot contain more than 64 entries");
|
|
1281
1337
|
const descriptors = [];
|
|
1282
1338
|
const executors = new Map();
|
|
1339
|
+
const names = new Set();
|
|
1283
1340
|
for (const [index, tool] of value.entries()) {
|
|
1284
1341
|
if (!tool || typeof tool !== "object") throw new TypeError(`tool ${index} must be an object`);
|
|
1285
|
-
const { name, description, inputSchema, execute } = tool;
|
|
1342
|
+
const { name, description, inputSchema, execute, providerExecuted } = tool;
|
|
1286
1343
|
if (typeof name !== "string" || !/^[A-Za-z0-9_-]{1,64}$/.test(name)) {
|
|
1287
1344
|
throw new TypeError(`tool ${index} has an invalid name`);
|
|
1288
1345
|
}
|
|
1289
|
-
if (
|
|
1346
|
+
if (names.has(name)) throw new TypeError(`duplicate tool name: ${name}`);
|
|
1347
|
+
names.add(name);
|
|
1348
|
+
if (providerExecuted !== undefined && typeof providerExecuted !== "boolean") {
|
|
1349
|
+
throw new TypeError(`tool ${name} providerExecuted must be a boolean`);
|
|
1350
|
+
}
|
|
1351
|
+
if (providerExecuted === true) {
|
|
1352
|
+
if (execute !== undefined) throw new TypeError(`provider-executed tool ${name} must not define execute()`);
|
|
1353
|
+
descriptors.push({ name, providerExecuted: true });
|
|
1354
|
+
continue;
|
|
1355
|
+
}
|
|
1290
1356
|
if (typeof description !== "string") throw new TypeError(`tool ${name} requires a description`);
|
|
1291
1357
|
if (typeof execute !== "function") throw new TypeError(`tool ${name} requires execute()`);
|
|
1292
1358
|
if (!inputSchema || typeof inputSchema !== "object" || Array.isArray(inputSchema)) {
|
|
@@ -1392,6 +1458,10 @@ export async function createFxAgent(options = {}) {
|
|
|
1392
1458
|
for (let attemptIndex = 0; attemptIndex < 2; attemptIndex++) {
|
|
1393
1459
|
const startedAt = performance.now();
|
|
1394
1460
|
const attempt = activeTurn ? ++activeTurn.transportAttempts : attemptIndex + 1;
|
|
1461
|
+
if (activeTurn) {
|
|
1462
|
+
activeTurn.transportBytes = 0;
|
|
1463
|
+
activeTurn.lastTransportActivityAt = null;
|
|
1464
|
+
}
|
|
1395
1465
|
emit("transport.start", { attempt, method, endpoint, model: options.model });
|
|
1396
1466
|
try {
|
|
1397
1467
|
if (activeTurn?.cancelled) {
|
|
@@ -1481,6 +1551,19 @@ export async function createFxAgent(options = {}) {
|
|
|
1481
1551
|
args: ["acp"],
|
|
1482
1552
|
env: agentEnvironment(options),
|
|
1483
1553
|
hostToolExecutor: executeHostTool,
|
|
1554
|
+
onTransportChunk(byteLength) {
|
|
1555
|
+
const turn = activeTurn;
|
|
1556
|
+
if (!isCurrentTurn(turn) || !Number.isSafeInteger(byteLength) || byteLength <= 0) return;
|
|
1557
|
+
turn.transportBytes += byteLength;
|
|
1558
|
+
const now = performance.now();
|
|
1559
|
+
if (turn.lastTransportActivityAt !== null && now - turn.lastTransportActivityAt < transportActivityIntervalMs) return;
|
|
1560
|
+
turn.lastTransportActivityAt = now;
|
|
1561
|
+
emit("transport.activity", {
|
|
1562
|
+
attempt: turn.transportAttempts,
|
|
1563
|
+
chunkBytes: byteLength,
|
|
1564
|
+
totalBytes: turn.transportBytes,
|
|
1565
|
+
});
|
|
1566
|
+
},
|
|
1484
1567
|
};
|
|
1485
1568
|
const runtime = options.runtimeFactory
|
|
1486
1569
|
? await options.runtimeFactory(runtimeOptions)
|
|
@@ -1606,6 +1689,9 @@ export async function createFxAgent(options = {}) {
|
|
|
1606
1689
|
const delta = update.content?.text;
|
|
1607
1690
|
return delta ? { type: "reasoning_delta", delta } : null;
|
|
1608
1691
|
}
|
|
1692
|
+
if (update.sessionUpdate === "user_message_chunk" && update.content?.type === "text") {
|
|
1693
|
+
return { type: "user_message", text: update.content.text };
|
|
1694
|
+
}
|
|
1609
1695
|
if (update.sessionUpdate === "tool_call") {
|
|
1610
1696
|
toolNames.set(update.toolCallId, update.name || update.toolName || update.title || "tool");
|
|
1611
1697
|
if (started.has(update.toolCallId)) return null;
|
|
@@ -1637,6 +1723,7 @@ export async function createFxAgent(options = {}) {
|
|
|
1637
1723
|
void result.catch(() => {});
|
|
1638
1724
|
return {
|
|
1639
1725
|
cancel() { rawTurn.cancel(); },
|
|
1726
|
+
steer(input) { return rawTurn.steer(normalizeSteeringInput(input)); },
|
|
1640
1727
|
[Symbol.asyncIterator]() {
|
|
1641
1728
|
const iterator = (async function* () {
|
|
1642
1729
|
for await (const update of rawTurn) {
|
|
@@ -1712,10 +1799,34 @@ export async function createFxAgent(options = {}) {
|
|
|
1712
1799
|
},
|
|
1713
1800
|
toolControllers,
|
|
1714
1801
|
transportAttempts: 0,
|
|
1802
|
+
transportBytes: 0,
|
|
1803
|
+
lastTransportActivityAt: null,
|
|
1715
1804
|
get cancelled() { return cancelled; },
|
|
1805
|
+
steer(text) {
|
|
1806
|
+
if (finished || cancelled || activeTurn !== turn) {
|
|
1807
|
+
return Promise.reject(new Error("no prompt is running"));
|
|
1808
|
+
}
|
|
1809
|
+
let accepted;
|
|
1810
|
+
try {
|
|
1811
|
+
if (typeof runtime.steer === "function") {
|
|
1812
|
+
runtime.steer(text);
|
|
1813
|
+
void turn.push({
|
|
1814
|
+
sessionUpdate: "user_message_chunk",
|
|
1815
|
+
content: { type: "text", text },
|
|
1816
|
+
});
|
|
1817
|
+
accepted = Promise.resolve();
|
|
1818
|
+
} else {
|
|
1819
|
+
accepted = request("libfx/steer", { sessionId, text });
|
|
1820
|
+
}
|
|
1821
|
+
} catch (error) {
|
|
1822
|
+
return Promise.reject(error);
|
|
1823
|
+
}
|
|
1824
|
+
return accepted;
|
|
1825
|
+
},
|
|
1716
1826
|
cancel() {
|
|
1717
1827
|
if (finished || cancelled) return;
|
|
1718
1828
|
cancelled = true;
|
|
1829
|
+
runtime.closeSteering?.();
|
|
1719
1830
|
resumeOutput?.();
|
|
1720
1831
|
resumeOutput = null;
|
|
1721
1832
|
send({ jsonrpc: "2.0", method: "session/cancel", params: { sessionId } });
|
|
@@ -1748,6 +1859,7 @@ export async function createFxAgent(options = {}) {
|
|
|
1748
1859
|
return turn;
|
|
1749
1860
|
}
|
|
1750
1861
|
activeTurn = turn;
|
|
1862
|
+
runtime.openSteering?.();
|
|
1751
1863
|
const abort = () => turn.cancel();
|
|
1752
1864
|
signal?.addEventListener("abort", abort, { once: true });
|
|
1753
1865
|
turn.result = request("session/prompt", { sessionId, prompt })
|
|
@@ -1763,6 +1875,7 @@ export async function createFxAgent(options = {}) {
|
|
|
1763
1875
|
resumeOutput = null;
|
|
1764
1876
|
signal?.removeEventListener("abort", abort);
|
|
1765
1877
|
if (activeTurn === turn) activeTurn = null;
|
|
1878
|
+
runtime.closeSteering?.();
|
|
1766
1879
|
toolControllers.clear();
|
|
1767
1880
|
if (discardedBytes) emit("output.discarded", { reason: "cancelled", bytes: discardedBytes });
|
|
1768
1881
|
for (const waiter of waiters.splice(0)) {
|
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
|
@@ -193,6 +193,7 @@ var maxUrlBytes = 16 * 1024;
|
|
|
193
193
|
var maxModelCatalogBytes = 4 * 1024 * 1024;
|
|
194
194
|
var maxModelCatalogEntries = 1e4;
|
|
195
195
|
var streamReadsPerTaskYield = 32;
|
|
196
|
+
var transportActivityIntervalMs = 250;
|
|
196
197
|
var maxUnreadEventBytes = 1024 * 1024;
|
|
197
198
|
var maxUnreadEvents = 256;
|
|
198
199
|
var maxPromptImages = 8;
|
|
@@ -200,6 +201,9 @@ var maxPromptImageDataBytes = 5 * 1024 * 1024;
|
|
|
200
201
|
var maxPromptImagesBytes = 8 * 1024 * 1024;
|
|
201
202
|
var maxPromptFrameBytes = 8 * 1024 * 1024;
|
|
202
203
|
var promptFrameEnvelopeBytes = 128;
|
|
204
|
+
var maxSteeringMessageBytes = 64 * 1024;
|
|
205
|
+
var maxSteeringMessages = 64;
|
|
206
|
+
var maxSteeringQueueBytes = 1024 * 1024;
|
|
203
207
|
var maxToolStartInputBytes = 64 * 1024;
|
|
204
208
|
function boundedString(value, name, maxBytes, required) {
|
|
205
209
|
if (value === undefined && !required)
|
|
@@ -637,6 +641,9 @@ function createRuntime(options) {
|
|
|
637
641
|
const stdin = new ByteQueue;
|
|
638
642
|
const streams = new Map;
|
|
639
643
|
const httpRequests = new Set;
|
|
644
|
+
const steering = [];
|
|
645
|
+
let steeringBytes = 0;
|
|
646
|
+
let steeringOpen = false;
|
|
640
647
|
const workspaceExecs = new Set;
|
|
641
648
|
const workspace = prepareWorkspaceAdapter(options.workspace);
|
|
642
649
|
const args = ["fx", ...options.args || []];
|
|
@@ -872,6 +879,7 @@ function createRuntime(options) {
|
|
|
872
879
|
return 0;
|
|
873
880
|
if (!value?.length)
|
|
874
881
|
return null;
|
|
882
|
+
options.onTransportChunk?.(value.length);
|
|
875
883
|
return copy(value);
|
|
876
884
|
};
|
|
877
885
|
const immediate = consume();
|
|
@@ -928,6 +936,37 @@ function createRuntime(options) {
|
|
|
928
936
|
httpRequests.delete(controller);
|
|
929
937
|
});
|
|
930
938
|
}
|
|
939
|
+
function clearSteering() {
|
|
940
|
+
steering.length = 0;
|
|
941
|
+
steeringBytes = 0;
|
|
942
|
+
}
|
|
943
|
+
function queueSteering(text) {
|
|
944
|
+
if (!steeringOpen)
|
|
945
|
+
throw new Error("no prompt is running");
|
|
946
|
+
const value = encoder.encode(text);
|
|
947
|
+
if (value.length === 0)
|
|
948
|
+
throw new TypeError("steering text cannot be empty");
|
|
949
|
+
if (value.length > maxSteeringMessageBytes) {
|
|
950
|
+
throw new RangeError(`steering text exceeds the ${maxSteeringMessageBytes} byte libfx limit`);
|
|
951
|
+
}
|
|
952
|
+
if (steering.length >= maxSteeringMessages || value.length > maxSteeringQueueBytes - steeringBytes) {
|
|
953
|
+
throw new Error("steering queue is full");
|
|
954
|
+
}
|
|
955
|
+
steering.push(value);
|
|
956
|
+
steeringBytes += value.length;
|
|
957
|
+
}
|
|
958
|
+
function steeringTake(outputPtr, outputCap) {
|
|
959
|
+
const value = steering[0];
|
|
960
|
+
if (!value)
|
|
961
|
+
return 0;
|
|
962
|
+
const output = checkedBytes(outputPtr, outputCap);
|
|
963
|
+
if (!output || value.length > output.length)
|
|
964
|
+
return -1;
|
|
965
|
+
output.subarray(0, value.length).set(value);
|
|
966
|
+
steering.shift();
|
|
967
|
+
steeringBytes -= value.length;
|
|
968
|
+
return value.length;
|
|
969
|
+
}
|
|
931
970
|
let pendingHostToolResult = null;
|
|
932
971
|
function hostToolCall(namePtr, nameLen, argumentsPtr, argumentsLen, outputPtr, outputCap, statusPtr) {
|
|
933
972
|
pendingHostToolResult = null;
|
|
@@ -1386,6 +1425,11 @@ function createRuntime(options) {
|
|
|
1386
1425
|
fx_host_tool_result_release() {
|
|
1387
1426
|
pendingHostToolResult = null;
|
|
1388
1427
|
},
|
|
1428
|
+
fx_steering_take: steeringTake,
|
|
1429
|
+
fx_steering_close() {
|
|
1430
|
+
steeringOpen = false;
|
|
1431
|
+
clearSteering();
|
|
1432
|
+
},
|
|
1389
1433
|
fx_open_url: new WebAssembly.Suspending(openUrl),
|
|
1390
1434
|
fx_oauth_session_load: new WebAssembly.Suspending(oauthSessionLoad),
|
|
1391
1435
|
fx_oauth_session_commit: new WebAssembly.Suspending(oauthSessionCommit),
|
|
@@ -1420,8 +1464,19 @@ function createRuntime(options) {
|
|
|
1420
1464
|
stdin.wake();
|
|
1421
1465
|
},
|
|
1422
1466
|
closeStdin() {
|
|
1467
|
+
steeringOpen = false;
|
|
1468
|
+
clearSteering();
|
|
1423
1469
|
stdin.close();
|
|
1424
1470
|
},
|
|
1471
|
+
openSteering() {
|
|
1472
|
+
clearSteering();
|
|
1473
|
+
steeringOpen = true;
|
|
1474
|
+
},
|
|
1475
|
+
steer: queueSteering,
|
|
1476
|
+
closeSteering() {
|
|
1477
|
+
steeringOpen = false;
|
|
1478
|
+
clearSteering();
|
|
1479
|
+
},
|
|
1425
1480
|
abortHostEffects,
|
|
1426
1481
|
abort(error) {
|
|
1427
1482
|
aborted = true;
|
|
@@ -1625,6 +1680,20 @@ function normalizePromptInput(input) {
|
|
|
1625
1680
|
throw new TypeError(`unsupported prompt block type: ${String(block.type)}`);
|
|
1626
1681
|
});
|
|
1627
1682
|
}
|
|
1683
|
+
function normalizeSteeringInput(input) {
|
|
1684
|
+
const blocks = normalizePromptInput(input);
|
|
1685
|
+
if (blocks.some((block) => block.type !== "text")) {
|
|
1686
|
+
throw new TypeError("steering accepts only text blocks");
|
|
1687
|
+
}
|
|
1688
|
+
const text = blocks.map((block) => block.text).join(`
|
|
1689
|
+
`);
|
|
1690
|
+
if (text.length === 0)
|
|
1691
|
+
throw new TypeError("steering text cannot be empty");
|
|
1692
|
+
if (encoder.encode(text).length > maxSteeringMessageBytes) {
|
|
1693
|
+
throw new RangeError(`steering text exceeds the ${maxSteeringMessageBytes} byte libfx limit`);
|
|
1694
|
+
}
|
|
1695
|
+
return text;
|
|
1696
|
+
}
|
|
1628
1697
|
function normalizeHostTools(value) {
|
|
1629
1698
|
if (value === undefined)
|
|
1630
1699
|
return { descriptors: [], executors: new Map };
|
|
@@ -1634,15 +1703,26 @@ function normalizeHostTools(value) {
|
|
|
1634
1703
|
throw new RangeError("tools cannot contain more than 64 entries");
|
|
1635
1704
|
const descriptors = [];
|
|
1636
1705
|
const executors = new Map;
|
|
1706
|
+
const names = new Set;
|
|
1637
1707
|
for (const [index, tool] of value.entries()) {
|
|
1638
1708
|
if (!tool || typeof tool !== "object")
|
|
1639
1709
|
throw new TypeError(`tool ${index} must be an object`);
|
|
1640
|
-
const { name, description, inputSchema, execute } = tool;
|
|
1710
|
+
const { name, description, inputSchema, execute, providerExecuted } = tool;
|
|
1641
1711
|
if (typeof name !== "string" || !/^[A-Za-z0-9_-]{1,64}$/.test(name)) {
|
|
1642
1712
|
throw new TypeError(`tool ${index} has an invalid name`);
|
|
1643
1713
|
}
|
|
1644
|
-
if (
|
|
1714
|
+
if (names.has(name))
|
|
1645
1715
|
throw new TypeError(`duplicate tool name: ${name}`);
|
|
1716
|
+
names.add(name);
|
|
1717
|
+
if (providerExecuted !== undefined && typeof providerExecuted !== "boolean") {
|
|
1718
|
+
throw new TypeError(`tool ${name} providerExecuted must be a boolean`);
|
|
1719
|
+
}
|
|
1720
|
+
if (providerExecuted === true) {
|
|
1721
|
+
if (execute !== undefined)
|
|
1722
|
+
throw new TypeError(`provider-executed tool ${name} must not define execute()`);
|
|
1723
|
+
descriptors.push({ name, providerExecuted: true });
|
|
1724
|
+
continue;
|
|
1725
|
+
}
|
|
1646
1726
|
if (typeof description !== "string")
|
|
1647
1727
|
throw new TypeError(`tool ${name} requires a description`);
|
|
1648
1728
|
if (typeof execute !== "function")
|
|
@@ -1760,6 +1840,10 @@ async function createFxAgent(options = {}) {
|
|
|
1760
1840
|
for (let attemptIndex = 0;attemptIndex < 2; attemptIndex++) {
|
|
1761
1841
|
const startedAt = performance.now();
|
|
1762
1842
|
const attempt = activeTurn ? ++activeTurn.transportAttempts : attemptIndex + 1;
|
|
1843
|
+
if (activeTurn) {
|
|
1844
|
+
activeTurn.transportBytes = 0;
|
|
1845
|
+
activeTurn.lastTransportActivityAt = null;
|
|
1846
|
+
}
|
|
1763
1847
|
emit("transport.start", { attempt, method, endpoint, model: options.model });
|
|
1764
1848
|
try {
|
|
1765
1849
|
if (activeTurn?.cancelled) {
|
|
@@ -1855,7 +1939,22 @@ async function createFxAgent(options = {}) {
|
|
|
1855
1939
|
fetch: transportFetch,
|
|
1856
1940
|
args: ["acp"],
|
|
1857
1941
|
env: agentEnvironment(options),
|
|
1858
|
-
hostToolExecutor: executeHostTool
|
|
1942
|
+
hostToolExecutor: executeHostTool,
|
|
1943
|
+
onTransportChunk(byteLength) {
|
|
1944
|
+
const turn = activeTurn;
|
|
1945
|
+
if (!isCurrentTurn(turn) || !Number.isSafeInteger(byteLength) || byteLength <= 0)
|
|
1946
|
+
return;
|
|
1947
|
+
turn.transportBytes += byteLength;
|
|
1948
|
+
const now = performance.now();
|
|
1949
|
+
if (turn.lastTransportActivityAt !== null && now - turn.lastTransportActivityAt < transportActivityIntervalMs)
|
|
1950
|
+
return;
|
|
1951
|
+
turn.lastTransportActivityAt = now;
|
|
1952
|
+
emit("transport.activity", {
|
|
1953
|
+
attempt: turn.transportAttempts,
|
|
1954
|
+
chunkBytes: byteLength,
|
|
1955
|
+
totalBytes: turn.transportBytes
|
|
1956
|
+
});
|
|
1957
|
+
}
|
|
1859
1958
|
};
|
|
1860
1959
|
const runtime = options.runtimeFactory ? await options.runtimeFactory(runtimeOptions) : await instantiate(runtimeOptions);
|
|
1861
1960
|
emit("runtime.ready");
|
|
@@ -2008,6 +2107,9 @@ async function createFxAgent(options = {}) {
|
|
|
2008
2107
|
const delta = update.content?.text;
|
|
2009
2108
|
return delta ? { type: "reasoning_delta", delta } : null;
|
|
2010
2109
|
}
|
|
2110
|
+
if (update.sessionUpdate === "user_message_chunk" && update.content?.type === "text") {
|
|
2111
|
+
return { type: "user_message", text: update.content.text };
|
|
2112
|
+
}
|
|
2011
2113
|
if (update.sessionUpdate === "tool_call") {
|
|
2012
2114
|
toolNames.set(update.toolCallId, update.name || update.toolName || update.title || "tool");
|
|
2013
2115
|
if (started.has(update.toolCallId))
|
|
@@ -2041,6 +2143,9 @@ async function createFxAgent(options = {}) {
|
|
|
2041
2143
|
cancel() {
|
|
2042
2144
|
rawTurn.cancel();
|
|
2043
2145
|
},
|
|
2146
|
+
steer(input) {
|
|
2147
|
+
return rawTurn.steer(normalizeSteeringInput(input));
|
|
2148
|
+
},
|
|
2044
2149
|
[Symbol.asyncIterator]() {
|
|
2045
2150
|
const iterator = async function* () {
|
|
2046
2151
|
for await (const update of rawTurn) {
|
|
@@ -2141,13 +2246,37 @@ async function createFxAgent(options = {}) {
|
|
|
2141
2246
|
},
|
|
2142
2247
|
toolControllers,
|
|
2143
2248
|
transportAttempts: 0,
|
|
2249
|
+
transportBytes: 0,
|
|
2250
|
+
lastTransportActivityAt: null,
|
|
2144
2251
|
get cancelled() {
|
|
2145
2252
|
return cancelled;
|
|
2146
2253
|
},
|
|
2254
|
+
steer(text) {
|
|
2255
|
+
if (finished || cancelled || activeTurn !== turn) {
|
|
2256
|
+
return Promise.reject(new Error("no prompt is running"));
|
|
2257
|
+
}
|
|
2258
|
+
let accepted;
|
|
2259
|
+
try {
|
|
2260
|
+
if (typeof runtime.steer === "function") {
|
|
2261
|
+
runtime.steer(text);
|
|
2262
|
+
turn.push({
|
|
2263
|
+
sessionUpdate: "user_message_chunk",
|
|
2264
|
+
content: { type: "text", text }
|
|
2265
|
+
});
|
|
2266
|
+
accepted = Promise.resolve();
|
|
2267
|
+
} else {
|
|
2268
|
+
accepted = request("libfx/steer", { sessionId, text });
|
|
2269
|
+
}
|
|
2270
|
+
} catch (error) {
|
|
2271
|
+
return Promise.reject(error);
|
|
2272
|
+
}
|
|
2273
|
+
return accepted;
|
|
2274
|
+
},
|
|
2147
2275
|
cancel() {
|
|
2148
2276
|
if (finished || cancelled)
|
|
2149
2277
|
return;
|
|
2150
2278
|
cancelled = true;
|
|
2279
|
+
runtime.closeSteering?.();
|
|
2151
2280
|
resumeOutput?.();
|
|
2152
2281
|
resumeOutput = null;
|
|
2153
2282
|
send({ jsonrpc: "2.0", method: "session/cancel", params: { sessionId } });
|
|
@@ -2187,6 +2316,7 @@ async function createFxAgent(options = {}) {
|
|
|
2187
2316
|
return turn;
|
|
2188
2317
|
}
|
|
2189
2318
|
activeTurn = turn;
|
|
2319
|
+
runtime.openSteering?.();
|
|
2190
2320
|
const abort = () => turn.cancel();
|
|
2191
2321
|
signal?.addEventListener("abort", abort, { once: true });
|
|
2192
2322
|
turn.result = request("session/prompt", { sessionId, prompt }).then((response) => ({ stopReason: cancelled ? "cancelled" : response.stopReason, usage: response.usage })).catch((error) => {
|
|
@@ -2201,6 +2331,7 @@ async function createFxAgent(options = {}) {
|
|
|
2201
2331
|
signal?.removeEventListener("abort", abort);
|
|
2202
2332
|
if (activeTurn === turn)
|
|
2203
2333
|
activeTurn = null;
|
|
2334
|
+
runtime.closeSteering?.();
|
|
2204
2335
|
toolControllers.clear();
|
|
2205
2336
|
if (discardedBytes)
|
|
2206
2337
|
emit("output.discarded", { reason: "cancelled", bytes: discardedBytes });
|
|
@@ -2584,6 +2715,7 @@ function createNativeCoreRuntime(addon, options) {
|
|
|
2584
2715
|
if (response.body) {
|
|
2585
2716
|
for await (const chunk of response.body) {
|
|
2586
2717
|
const buffer = Buffer.from(chunk);
|
|
2718
|
+
options.onTransportChunk?.(buffer.length);
|
|
2587
2719
|
let offset = 0;
|
|
2588
2720
|
while (offset < buffer.length) {
|
|
2589
2721
|
const end = Math.min(offset + 65536, buffer.length);
|
package/node.js
CHANGED
|
@@ -407,6 +407,7 @@ function createNativeCoreRuntime(addon, options) {
|
|
|
407
407
|
if (response.body) {
|
|
408
408
|
for await (const chunk of response.body) {
|
|
409
409
|
const buffer = Buffer.from(chunk);
|
|
410
|
+
options.onTransportChunk?.(buffer.length);
|
|
410
411
|
let offset = 0;
|
|
411
412
|
while (offset < buffer.length) {
|
|
412
413
|
const end = Math.min(offset + 64 * 1024, buffer.length);
|