libfx 0.0.10 → 0.0.11
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 +109 -17
- package/fx-core.wasm +0 -0
- package/fx-sdk.js +208 -5
- 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 +214 -8
- package/node.js +4 -1
- 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
|
|
@@ -41,28 +41,52 @@ await agent.close();
|
|
|
41
41
|
Agent configuration uses named options; `env` is reserved for
|
|
42
42
|
`createFxTerminal()`.
|
|
43
43
|
|
|
44
|
+
`effort` sets the reasoning effort for models that advertise effort levels. It
|
|
45
|
+
uses the same vocabulary as the fx CLI's `--effort` flag: `"default"` leaves
|
|
46
|
+
the choice to the model, and named levels such as `"low"`, `"medium"`,
|
|
47
|
+
`"high"`, or `"xhigh"` request a specific level. A named level is validated
|
|
48
|
+
against the selected model's advertised levels at creation; an unsupported
|
|
49
|
+
level rejects with an error (code `LIBFX_UNSUPPORTED_EFFORT`) naming the
|
|
50
|
+
supported set. When `effort` is omitted or `"default"`, the model default
|
|
51
|
+
applies.
|
|
52
|
+
|
|
53
|
+
`fast` enables the fast lane for models that advertise one, matching the fx
|
|
54
|
+
CLI's `--fast` flag. Enabling it is validated against the selected model at
|
|
55
|
+
creation; a model without a fast path rejects with an error (code
|
|
56
|
+
`LIBFX_UNSUPPORTED_FAST`). When `fast` is omitted or `false`, the model
|
|
57
|
+
default applies.
|
|
58
|
+
|
|
44
59
|
The host selects the model. Agent creation does not fetch the Gateway model
|
|
45
|
-
catalog
|
|
46
|
-
|
|
60
|
+
catalog unless `effort` requests a named level or `fast` is enabled. Prompting
|
|
61
|
+
can resolve model capabilities and context capacity through the supplied
|
|
62
|
+
`fetch`; fx caches that metadata for the agent.
|
|
47
63
|
|
|
48
64
|
`onEvent` receives runtime diagnostics separately from model output. Transport
|
|
49
65
|
events report request start, response status and elapsed time, safe Gateway
|
|
50
|
-
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.
|
|
51
72
|
|
|
52
73
|
libfx makes at most one automatic retry after a retryable transport failure and
|
|
53
74
|
only before model output or tool effects escape. Cancellation prevents a retry.
|
|
54
75
|
|
|
55
|
-
`prompt(input, { signal? })` accepts a string or text
|
|
56
|
-
returns an async iterable of normalized events:
|
|
76
|
+
`prompt(input, { signal? })` accepts a string or text, image, and resource
|
|
77
|
+
blocks. It returns an async iterable of normalized events:
|
|
57
78
|
|
|
58
79
|
- `text_delta`
|
|
59
80
|
- `reasoning_delta` when supplied by the provider
|
|
60
|
-
- `tool_start`
|
|
81
|
+
- `tool_start` with `id`, `name`, and the tool's `input` object. Inputs over
|
|
82
|
+
64 KiB of JSON arrive as an `inputPreview` string prefix plus
|
|
83
|
+
`inputTruncated: true` instead; the tool still receives complete arguments.
|
|
61
84
|
- `tool_end`
|
|
85
|
+
- `user_message` with `text` when accepted mid-turn steering enters the turn
|
|
62
86
|
|
|
63
|
-
Consume the turn while it runs, then await `turn.result`.
|
|
64
|
-
backpressured: a slow reader pauses production
|
|
65
|
-
event queue. Awaiting only `turn.result` can wait for an unread stream to drain.
|
|
87
|
+
Consume the turn while it runs, then await `turn.result`. Streamed text and
|
|
88
|
+
tool results are lossless and backpressured: a slow reader pauses production
|
|
89
|
+
instead of growing an unlimited event queue. Awaiting only `turn.result` can wait for an unread stream to drain.
|
|
66
90
|
If you only need the result, explicitly discard events:
|
|
67
91
|
|
|
68
92
|
```js
|
|
@@ -82,9 +106,50 @@ that threshold when the queue is empty; an individual encoded ACP message is
|
|
|
82
106
|
limited to 64 MiB on both backends. These are transport bounds, not a total
|
|
83
107
|
answer-size limit or a bound on retained conversation history.
|
|
84
108
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
109
|
+
Image blocks use ACP's content shape and carry canonical base64 (no line
|
|
110
|
+
wrapping) of a PNG, JPEG, GIF, or WebP payload:
|
|
111
|
+
|
|
112
|
+
```js
|
|
113
|
+
const turn = agent.prompt([
|
|
114
|
+
{ type: "text", text: "What does this screenshot show?" },
|
|
115
|
+
{ type: "image", data: base64Png, mimeType: "image/png" },
|
|
116
|
+
]);
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
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; the SDK rejects larger input with
|
|
121
|
+
typed `RangeError`s before any request. The kernel then validates the decoded
|
|
122
|
+
bytes against the declared `mimeType` and fails the turn with
|
|
123
|
+
`Invalid image prompt block` on a mismatch. Images are routed only to models
|
|
124
|
+
that advertise image input; for any other model the turn fails with
|
|
125
|
+
`Image prompts are unavailable for the selected model` and no image bytes
|
|
126
|
+
leave the process. Prompt images are retained in checkpoints within the
|
|
127
|
+
existing 4 MiB checkpoint bound, so a restored agent can refer to earlier
|
|
128
|
+
images on either backend.
|
|
129
|
+
|
|
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:
|
|
88
153
|
|
|
89
154
|
```js
|
|
90
155
|
const restored = await createFxAgent({ apiKey, model, checkpoint });
|
|
@@ -95,7 +160,10 @@ or a history change. The next prompt can run normally.
|
|
|
95
160
|
|
|
96
161
|
The checkpoint contains conversation history and usage only. The host owns
|
|
97
162
|
durable storage and must resupply models, credentials, instructions, tools,
|
|
98
|
-
MCP clients, and skill records.
|
|
163
|
+
MCP clients, and skill records. Reasoning effort and fast mode are
|
|
164
|
+
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 switching
|
|
166
|
+
models.
|
|
99
167
|
|
|
100
168
|
## Models
|
|
101
169
|
|
|
@@ -136,11 +204,35 @@ const agent = await createFxAgent({
|
|
|
136
204
|
});
|
|
137
205
|
```
|
|
138
206
|
|
|
139
|
-
|
|
140
|
-
|
|
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.
|
|
141
228
|
Cancelling a prompt aborts its tools' signals and stops waiting for their
|
|
142
229
|
callbacks. Late results and rejections are ignored. Tools remain responsible
|
|
143
230
|
for stopping their own work when their signal is aborted.
|
|
231
|
+
|
|
232
|
+
A host tool may use any name, including the kernel's builtin names such as
|
|
233
|
+
`write_file` and `edit_file`: the kernel routes by the registered executor, so
|
|
234
|
+
a host-defined `write_file` calls the host's `execute()` rather than the
|
|
235
|
+
builtin file mutation.
|
|
144
236
|
Instructions are limited to 64 KiB of UTF-8 text, including text assembled by
|
|
145
237
|
the MCP and skills adapters. They are the complete host-owned system context:
|
|
146
238
|
libfx adds no hidden base prompt, and omitting `instructions` sends no system
|
package/fx-core.wasm
CHANGED
|
Binary file
|
package/fx-sdk.js
CHANGED
|
@@ -10,12 +10,31 @@ const workspaceOutputLimit = 64 * 1024;
|
|
|
10
10
|
const maxInstructionsBytes = 64 * 1024;
|
|
11
11
|
const maxApiKeyBytes = 64 * 1024;
|
|
12
12
|
const maxModelBytes = 1024;
|
|
13
|
+
// Matches the kernel's ReasoningEffort.max_name_bytes.
|
|
14
|
+
const maxEffortBytes = 64;
|
|
13
15
|
const maxUrlBytes = 16 * 1024;
|
|
14
16
|
const maxModelCatalogBytes = 4 * 1024 * 1024;
|
|
15
17
|
const maxModelCatalogEntries = 10_000;
|
|
16
18
|
const streamReadsPerTaskYield = 32;
|
|
19
|
+
const transportActivityIntervalMs = 250;
|
|
17
20
|
const maxUnreadEventBytes = 1024 * 1024;
|
|
18
21
|
const maxUnreadEvents = 256;
|
|
22
|
+
// Prompt image limits mirror the host tool result image contract: the kernel
|
|
23
|
+
// validates content, the SDK bounds the frame before it reaches the core.
|
|
24
|
+
const maxPromptImages = 8;
|
|
25
|
+
const maxPromptImageDataBytes = 5 * 1024 * 1024;
|
|
26
|
+
const maxPromptImagesBytes = 8 * 1024 * 1024;
|
|
27
|
+
// The core's ACP reader drops frames over 8 MiB without a request id to answer
|
|
28
|
+
// (jsonrpc frame_resource_byte_limit), so the SDK must never emit one. The
|
|
29
|
+
// envelope allowance covers the method key and request id.
|
|
30
|
+
const maxPromptFrameBytes = 8 * 1024 * 1024;
|
|
31
|
+
const promptFrameEnvelopeBytes = 128;
|
|
32
|
+
const maxSteeringMessageBytes = 64 * 1024;
|
|
33
|
+
const maxSteeringMessages = 64;
|
|
34
|
+
const maxSteeringQueueBytes = 1024 * 1024;
|
|
35
|
+
// tool_start events carry a bounded preview of the tool input; larger inputs
|
|
36
|
+
// are marked truncated instead of dropped or sent whole.
|
|
37
|
+
const maxToolStartInputBytes = 64 * 1024;
|
|
19
38
|
|
|
20
39
|
function boundedString(value, name, maxBytes, required) {
|
|
21
40
|
if (value === undefined && !required) return undefined;
|
|
@@ -44,6 +63,27 @@ function validateGatewayChatUrl(value) {
|
|
|
44
63
|
}
|
|
45
64
|
}
|
|
46
65
|
|
|
66
|
+
// Mirrors the kernel's ReasoningEffort.parse: "auto"/"adaptive"/"default" pick
|
|
67
|
+
// the model default; anything else must be a bounded effort name.
|
|
68
|
+
function normalizeEffort(value) {
|
|
69
|
+
if (value === undefined) return undefined;
|
|
70
|
+
boundedString(value, "effort", maxEffortBytes, false);
|
|
71
|
+
if (!/^[A-Za-z0-9._-]+$/.test(value)) {
|
|
72
|
+
throw new TypeError('effort must use only letters, digits, ".", "-", or "_"');
|
|
73
|
+
}
|
|
74
|
+
return value;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Mirrors the CLI's --fast/--no-fast toggle: a strict boolean, with undefined
|
|
78
|
+
// leaving the model default in place.
|
|
79
|
+
function normalizeFast(value) {
|
|
80
|
+
if (value === undefined) return undefined;
|
|
81
|
+
if (typeof value !== "boolean") {
|
|
82
|
+
throw new TypeError("fast must be a boolean");
|
|
83
|
+
}
|
|
84
|
+
return value;
|
|
85
|
+
}
|
|
86
|
+
|
|
47
87
|
function normalizeAgentOptions(value) {
|
|
48
88
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
49
89
|
throw new TypeError("createFxAgent() options must be an object");
|
|
@@ -54,6 +94,8 @@ function normalizeAgentOptions(value) {
|
|
|
54
94
|
}
|
|
55
95
|
options.apiKey = boundedString(options.apiKey, "apiKey", maxApiKeyBytes, true);
|
|
56
96
|
options.model = boundedString(options.model, "model", maxModelBytes, false);
|
|
97
|
+
options.effort = normalizeEffort(options.effort);
|
|
98
|
+
options.fast = normalizeFast(options.fast);
|
|
57
99
|
validateGatewayChatUrl(options.gatewayChatUrl);
|
|
58
100
|
return options;
|
|
59
101
|
}
|
|
@@ -62,10 +104,26 @@ function agentEnvironment(options) {
|
|
|
62
104
|
return {
|
|
63
105
|
AI_GATEWAY_API_KEY: options.apiKey,
|
|
64
106
|
...(options.model === undefined ? {} : { FX_MODEL: options.model }),
|
|
107
|
+
...(options.effort === undefined ? {} : { FX_EFFORT: options.effort }),
|
|
108
|
+
...(options.fast === undefined ? {} : { FX_FAST: options.fast ? "true" : "false" }),
|
|
65
109
|
...(options.gatewayChatUrl === undefined ? {} : { FX_GATEWAY_CHAT_URL: options.gatewayChatUrl }),
|
|
66
110
|
};
|
|
67
111
|
}
|
|
68
112
|
|
|
113
|
+
// The kernel rejects an unsupported effort or fast lane during initialize;
|
|
114
|
+
// these messages originate only from that validation, so the rejection is
|
|
115
|
+
// safe to retype.
|
|
116
|
+
function agentBootstrapError(error) {
|
|
117
|
+
if (error instanceof Error &&
|
|
118
|
+
(error.message === "Invalid reasoning effort" || error.message.startsWith("Reasoning effort"))) {
|
|
119
|
+
error.code ??= "LIBFX_UNSUPPORTED_EFFORT";
|
|
120
|
+
}
|
|
121
|
+
if (error instanceof Error && error.message.startsWith("Fast mode")) {
|
|
122
|
+
error.code ??= "LIBFX_UNSUPPORTED_FAST";
|
|
123
|
+
}
|
|
124
|
+
return error;
|
|
125
|
+
}
|
|
126
|
+
|
|
69
127
|
async function cancelResponseBody(response) {
|
|
70
128
|
try {
|
|
71
129
|
await response.body?.cancel();
|
|
@@ -390,6 +448,9 @@ function createRuntime(options) {
|
|
|
390
448
|
const stdin = new ByteQueue();
|
|
391
449
|
const streams = new Map();
|
|
392
450
|
const httpRequests = new Set();
|
|
451
|
+
const steering = [];
|
|
452
|
+
let steeringBytes = 0;
|
|
453
|
+
let steeringOpen = false;
|
|
393
454
|
const workspaceExecs = new Set();
|
|
394
455
|
const workspace = prepareWorkspaceAdapter(options.workspace);
|
|
395
456
|
const args = ["fx", ...(options.args || [])];
|
|
@@ -602,6 +663,7 @@ function createRuntime(options) {
|
|
|
602
663
|
state.readResult = null;
|
|
603
664
|
if (done) return 0;
|
|
604
665
|
if (!value?.length) return null;
|
|
666
|
+
options.onTransportChunk?.(value.length);
|
|
605
667
|
return copy(value);
|
|
606
668
|
};
|
|
607
669
|
const immediate = consume();
|
|
@@ -655,6 +717,36 @@ function createRuntime(options) {
|
|
|
655
717
|
});
|
|
656
718
|
}
|
|
657
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
|
+
|
|
658
750
|
let pendingHostToolResult = null;
|
|
659
751
|
function hostToolCall(namePtr, nameLen, argumentsPtr, argumentsLen, outputPtr, outputCap, statusPtr) {
|
|
660
752
|
pendingHostToolResult = null;
|
|
@@ -1024,6 +1116,8 @@ function createRuntime(options) {
|
|
|
1024
1116
|
return chunk.length;
|
|
1025
1117
|
},
|
|
1026
1118
|
fx_host_tool_result_release() { pendingHostToolResult = null; },
|
|
1119
|
+
fx_steering_take: steeringTake,
|
|
1120
|
+
fx_steering_close() { steeringOpen = false; clearSteering(); },
|
|
1027
1121
|
fx_open_url: new WebAssembly.Suspending(openUrl),
|
|
1028
1122
|
fx_oauth_session_load: new WebAssembly.Suspending(oauthSessionLoad),
|
|
1029
1123
|
fx_oauth_session_commit: new WebAssembly.Suspending(oauthSessionCommit),
|
|
@@ -1051,7 +1145,10 @@ function createRuntime(options) {
|
|
|
1051
1145
|
setInstance(value) { instance = value; },
|
|
1052
1146
|
write(data) { stdin.push(typeof data === "string" ? encoder.encode(data) : data); },
|
|
1053
1147
|
wake() { stdin.wake(); },
|
|
1054
|
-
closeStdin() { stdin.close(); },
|
|
1148
|
+
closeStdin() { steeringOpen = false; clearSteering(); stdin.close(); },
|
|
1149
|
+
openSteering() { clearSteering(); steeringOpen = true; },
|
|
1150
|
+
steer: queueSteering,
|
|
1151
|
+
closeSteering() { steeringOpen = false; clearSteering(); },
|
|
1055
1152
|
abortHostEffects,
|
|
1056
1153
|
abort(error) {
|
|
1057
1154
|
aborted = true;
|
|
@@ -1182,9 +1279,30 @@ export async function createFxTerminal(options) {
|
|
|
1182
1279
|
function normalizePromptInput(input) {
|
|
1183
1280
|
if (typeof input === "string") return [{ type: "text", text: input }];
|
|
1184
1281
|
if (!Array.isArray(input)) throw new TypeError("prompt input must be a string or an array of prompt blocks");
|
|
1282
|
+
let imageCount = 0;
|
|
1283
|
+
let imageBytes = 0;
|
|
1185
1284
|
return input.map((block, index) => {
|
|
1186
1285
|
if (!block || typeof block !== "object") throw new TypeError(`prompt block ${index} must be an object`);
|
|
1187
|
-
if (block.type === "image")
|
|
1286
|
+
if (block.type === "image") {
|
|
1287
|
+
if (typeof block.data !== "string" || block.data.length === 0) {
|
|
1288
|
+
throw new TypeError(`image prompt block ${index} requires base64 data`);
|
|
1289
|
+
}
|
|
1290
|
+
if (typeof block.mimeType !== "string" || block.mimeType.length === 0 || block.mimeType.length > 128) {
|
|
1291
|
+
throw new TypeError(`image prompt block ${index} requires a mimeType`);
|
|
1292
|
+
}
|
|
1293
|
+
if (block.data.length > maxPromptImageDataBytes) {
|
|
1294
|
+
throw new RangeError(`image prompt block ${index} exceeds the ${maxPromptImageDataBytes} byte per-image libfx limit`);
|
|
1295
|
+
}
|
|
1296
|
+
imageCount += 1;
|
|
1297
|
+
if (imageCount > maxPromptImages) {
|
|
1298
|
+
throw new RangeError(`prompt cannot contain more than ${maxPromptImages} images`);
|
|
1299
|
+
}
|
|
1300
|
+
imageBytes += block.data.length;
|
|
1301
|
+
if (imageBytes > maxPromptImagesBytes) {
|
|
1302
|
+
throw new RangeError(`prompt images exceed the ${maxPromptImagesBytes} byte libfx frame limit`);
|
|
1303
|
+
}
|
|
1304
|
+
return { type: "image", data: block.data, mimeType: block.mimeType };
|
|
1305
|
+
}
|
|
1188
1306
|
if (block.type === "text") {
|
|
1189
1307
|
if (typeof block.text !== "string") throw new TypeError(`text prompt block ${index} requires text`);
|
|
1190
1308
|
return { type: "text", text: block.text };
|
|
@@ -1199,19 +1317,42 @@ function normalizePromptInput(input) {
|
|
|
1199
1317
|
});
|
|
1200
1318
|
}
|
|
1201
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
|
+
|
|
1202
1333
|
function normalizeHostTools(value) {
|
|
1203
1334
|
if (value === undefined) return { descriptors: [], executors: new Map() };
|
|
1204
1335
|
if (!Array.isArray(value)) throw new TypeError("tools must be an array");
|
|
1205
1336
|
if (value.length > 64) throw new RangeError("tools cannot contain more than 64 entries");
|
|
1206
1337
|
const descriptors = [];
|
|
1207
1338
|
const executors = new Map();
|
|
1339
|
+
const names = new Set();
|
|
1208
1340
|
for (const [index, tool] of value.entries()) {
|
|
1209
1341
|
if (!tool || typeof tool !== "object") throw new TypeError(`tool ${index} must be an object`);
|
|
1210
|
-
const { name, description, inputSchema, execute } = tool;
|
|
1342
|
+
const { name, description, inputSchema, execute, providerExecuted } = tool;
|
|
1211
1343
|
if (typeof name !== "string" || !/^[A-Za-z0-9_-]{1,64}$/.test(name)) {
|
|
1212
1344
|
throw new TypeError(`tool ${index} has an invalid name`);
|
|
1213
1345
|
}
|
|
1214
|
-
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
|
+
}
|
|
1215
1356
|
if (typeof description !== "string") throw new TypeError(`tool ${name} requires a description`);
|
|
1216
1357
|
if (typeof execute !== "function") throw new TypeError(`tool ${name} requires execute()`);
|
|
1217
1358
|
if (!inputSchema || typeof inputSchema !== "object" || Array.isArray(inputSchema)) {
|
|
@@ -1317,6 +1458,10 @@ export async function createFxAgent(options = {}) {
|
|
|
1317
1458
|
for (let attemptIndex = 0; attemptIndex < 2; attemptIndex++) {
|
|
1318
1459
|
const startedAt = performance.now();
|
|
1319
1460
|
const attempt = activeTurn ? ++activeTurn.transportAttempts : attemptIndex + 1;
|
|
1461
|
+
if (activeTurn) {
|
|
1462
|
+
activeTurn.transportBytes = 0;
|
|
1463
|
+
activeTurn.lastTransportActivityAt = null;
|
|
1464
|
+
}
|
|
1320
1465
|
emit("transport.start", { attempt, method, endpoint, model: options.model });
|
|
1321
1466
|
try {
|
|
1322
1467
|
if (activeTurn?.cancelled) {
|
|
@@ -1406,6 +1551,19 @@ export async function createFxAgent(options = {}) {
|
|
|
1406
1551
|
args: ["acp"],
|
|
1407
1552
|
env: agentEnvironment(options),
|
|
1408
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
|
+
},
|
|
1409
1567
|
};
|
|
1410
1568
|
const runtime = options.runtimeFactory
|
|
1411
1569
|
? await options.runtimeFactory(runtimeOptions)
|
|
@@ -1490,7 +1648,7 @@ export async function createFxAgent(options = {}) {
|
|
|
1490
1648
|
try { runtime.abortHostEffects(); } catch {}
|
|
1491
1649
|
try { runtime.closeStdin(); } catch {}
|
|
1492
1650
|
try { await runtime.exited; } catch {}
|
|
1493
|
-
throw error;
|
|
1651
|
+
throw agentBootstrapError(error);
|
|
1494
1652
|
}
|
|
1495
1653
|
|
|
1496
1654
|
const agent = {
|
|
@@ -1531,6 +1689,9 @@ export async function createFxAgent(options = {}) {
|
|
|
1531
1689
|
const delta = update.content?.text;
|
|
1532
1690
|
return delta ? { type: "reasoning_delta", delta } : null;
|
|
1533
1691
|
}
|
|
1692
|
+
if (update.sessionUpdate === "user_message_chunk" && update.content?.type === "text") {
|
|
1693
|
+
return { type: "user_message", text: update.content.text };
|
|
1694
|
+
}
|
|
1534
1695
|
if (update.sessionUpdate === "tool_call") {
|
|
1535
1696
|
toolNames.set(update.toolCallId, update.name || update.toolName || update.title || "tool");
|
|
1536
1697
|
if (started.has(update.toolCallId)) return null;
|
|
@@ -1539,6 +1700,7 @@ export async function createFxAgent(options = {}) {
|
|
|
1539
1700
|
type: "tool_start",
|
|
1540
1701
|
id: update.toolCallId,
|
|
1541
1702
|
name: toolNames.get(update.toolCallId),
|
|
1703
|
+
...toolStartInput(update.rawInput),
|
|
1542
1704
|
};
|
|
1543
1705
|
}
|
|
1544
1706
|
if (update.sessionUpdate === "tool_call_update" &&
|
|
@@ -1561,6 +1723,7 @@ export async function createFxAgent(options = {}) {
|
|
|
1561
1723
|
void result.catch(() => {});
|
|
1562
1724
|
return {
|
|
1563
1725
|
cancel() { rawTurn.cancel(); },
|
|
1726
|
+
steer(input) { return rawTurn.steer(normalizeSteeringInput(input)); },
|
|
1564
1727
|
[Symbol.asyncIterator]() {
|
|
1565
1728
|
const iterator = (async function* () {
|
|
1566
1729
|
for await (const update of rawTurn) {
|
|
@@ -1589,8 +1752,22 @@ export async function createFxAgent(options = {}) {
|
|
|
1589
1752
|
return result;
|
|
1590
1753
|
}
|
|
1591
1754
|
|
|
1755
|
+
// The input object rides the event when it fits the preview budget; larger
|
|
1756
|
+
// inputs become a bounded JSON prefix plus an explicit marker.
|
|
1757
|
+
function toolStartInput(rawInput) {
|
|
1758
|
+
if (rawInput === undefined || rawInput === null) return {};
|
|
1759
|
+
const serialized = JSON.stringify(rawInput);
|
|
1760
|
+
if (serialized === undefined) return {};
|
|
1761
|
+
const bytes = encoder.encode(serialized);
|
|
1762
|
+
if (bytes.length <= maxToolStartInputBytes) return { input: rawInput };
|
|
1763
|
+
return { inputTruncated: true, inputPreview: decoder.decode(utf8Prefix(bytes, maxToolStartInputBytes)) };
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1592
1766
|
function startTurn(input, promptOptions) {
|
|
1593
1767
|
const prompt = normalizePromptInput(input);
|
|
1768
|
+
if (encoder.encode(JSON.stringify({ sessionId: "", prompt })).length + promptFrameEnvelopeBytes > maxPromptFrameBytes) {
|
|
1769
|
+
throw new RangeError(`prompt exceeds the ${maxPromptFrameBytes} byte libfx frame limit`);
|
|
1770
|
+
}
|
|
1594
1771
|
const signal = promptOptions.signal;
|
|
1595
1772
|
if (signal !== undefined && (typeof signal?.addEventListener !== "function" || typeof signal?.removeEventListener !== "function")) throw new TypeError("prompt signal must be an AbortSignal");
|
|
1596
1773
|
const queue = [];
|
|
@@ -1622,10 +1799,34 @@ export async function createFxAgent(options = {}) {
|
|
|
1622
1799
|
},
|
|
1623
1800
|
toolControllers,
|
|
1624
1801
|
transportAttempts: 0,
|
|
1802
|
+
transportBytes: 0,
|
|
1803
|
+
lastTransportActivityAt: null,
|
|
1625
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
|
+
},
|
|
1626
1826
|
cancel() {
|
|
1627
1827
|
if (finished || cancelled) return;
|
|
1628
1828
|
cancelled = true;
|
|
1829
|
+
runtime.closeSteering?.();
|
|
1629
1830
|
resumeOutput?.();
|
|
1630
1831
|
resumeOutput = null;
|
|
1631
1832
|
send({ jsonrpc: "2.0", method: "session/cancel", params: { sessionId } });
|
|
@@ -1658,6 +1859,7 @@ export async function createFxAgent(options = {}) {
|
|
|
1658
1859
|
return turn;
|
|
1659
1860
|
}
|
|
1660
1861
|
activeTurn = turn;
|
|
1862
|
+
runtime.openSteering?.();
|
|
1661
1863
|
const abort = () => turn.cancel();
|
|
1662
1864
|
signal?.addEventListener("abort", abort, { once: true });
|
|
1663
1865
|
turn.result = request("session/prompt", { sessionId, prompt })
|
|
@@ -1673,6 +1875,7 @@ export async function createFxAgent(options = {}) {
|
|
|
1673
1875
|
resumeOutput = null;
|
|
1674
1876
|
signal?.removeEventListener("abort", abort);
|
|
1675
1877
|
if (activeTurn === turn) activeTurn = null;
|
|
1878
|
+
runtime.closeSteering?.();
|
|
1676
1879
|
toolControllers.clear();
|
|
1677
1880
|
if (discardedBytes) emit("output.discarded", { reason: "cancelled", bytes: discardedBytes });
|
|
1678
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
|
@@ -188,12 +188,23 @@ var workspaceOutputLimit = 64 * 1024;
|
|
|
188
188
|
var maxInstructionsBytes = 64 * 1024;
|
|
189
189
|
var maxApiKeyBytes = 64 * 1024;
|
|
190
190
|
var maxModelBytes = 1024;
|
|
191
|
+
var maxEffortBytes = 64;
|
|
191
192
|
var maxUrlBytes = 16 * 1024;
|
|
192
193
|
var maxModelCatalogBytes = 4 * 1024 * 1024;
|
|
193
194
|
var maxModelCatalogEntries = 1e4;
|
|
194
195
|
var streamReadsPerTaskYield = 32;
|
|
196
|
+
var transportActivityIntervalMs = 250;
|
|
195
197
|
var maxUnreadEventBytes = 1024 * 1024;
|
|
196
198
|
var maxUnreadEvents = 256;
|
|
199
|
+
var maxPromptImages = 8;
|
|
200
|
+
var maxPromptImageDataBytes = 5 * 1024 * 1024;
|
|
201
|
+
var maxPromptImagesBytes = 8 * 1024 * 1024;
|
|
202
|
+
var maxPromptFrameBytes = 8 * 1024 * 1024;
|
|
203
|
+
var promptFrameEnvelopeBytes = 128;
|
|
204
|
+
var maxSteeringMessageBytes = 64 * 1024;
|
|
205
|
+
var maxSteeringMessages = 64;
|
|
206
|
+
var maxSteeringQueueBytes = 1024 * 1024;
|
|
207
|
+
var maxToolStartInputBytes = 64 * 1024;
|
|
197
208
|
function boundedString(value, name, maxBytes, required) {
|
|
198
209
|
if (value === undefined && !required)
|
|
199
210
|
return;
|
|
@@ -227,6 +238,23 @@ function validateGatewayChatUrl(value) {
|
|
|
227
238
|
throw new TypeError("gatewayChatUrl must use the canonical Gateway or explicit loopback HTTP");
|
|
228
239
|
}
|
|
229
240
|
}
|
|
241
|
+
function normalizeEffort(value) {
|
|
242
|
+
if (value === undefined)
|
|
243
|
+
return;
|
|
244
|
+
boundedString(value, "effort", maxEffortBytes, false);
|
|
245
|
+
if (!/^[A-Za-z0-9._-]+$/.test(value)) {
|
|
246
|
+
throw new TypeError('effort must use only letters, digits, ".", "-", or "_"');
|
|
247
|
+
}
|
|
248
|
+
return value;
|
|
249
|
+
}
|
|
250
|
+
function normalizeFast(value) {
|
|
251
|
+
if (value === undefined)
|
|
252
|
+
return;
|
|
253
|
+
if (typeof value !== "boolean") {
|
|
254
|
+
throw new TypeError("fast must be a boolean");
|
|
255
|
+
}
|
|
256
|
+
return value;
|
|
257
|
+
}
|
|
230
258
|
function normalizeAgentOptions(value) {
|
|
231
259
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
232
260
|
throw new TypeError("createFxAgent() options must be an object");
|
|
@@ -237,6 +265,8 @@ function normalizeAgentOptions(value) {
|
|
|
237
265
|
}
|
|
238
266
|
options.apiKey = boundedString(options.apiKey, "apiKey", maxApiKeyBytes, true);
|
|
239
267
|
options.model = boundedString(options.model, "model", maxModelBytes, false);
|
|
268
|
+
options.effort = normalizeEffort(options.effort);
|
|
269
|
+
options.fast = normalizeFast(options.fast);
|
|
240
270
|
validateGatewayChatUrl(options.gatewayChatUrl);
|
|
241
271
|
return options;
|
|
242
272
|
}
|
|
@@ -244,9 +274,20 @@ function agentEnvironment(options) {
|
|
|
244
274
|
return {
|
|
245
275
|
AI_GATEWAY_API_KEY: options.apiKey,
|
|
246
276
|
...options.model === undefined ? {} : { FX_MODEL: options.model },
|
|
277
|
+
...options.effort === undefined ? {} : { FX_EFFORT: options.effort },
|
|
278
|
+
...options.fast === undefined ? {} : { FX_FAST: options.fast ? "true" : "false" },
|
|
247
279
|
...options.gatewayChatUrl === undefined ? {} : { FX_GATEWAY_CHAT_URL: options.gatewayChatUrl }
|
|
248
280
|
};
|
|
249
281
|
}
|
|
282
|
+
function agentBootstrapError(error) {
|
|
283
|
+
if (error instanceof Error && (error.message === "Invalid reasoning effort" || error.message.startsWith("Reasoning effort"))) {
|
|
284
|
+
error.code ??= "LIBFX_UNSUPPORTED_EFFORT";
|
|
285
|
+
}
|
|
286
|
+
if (error instanceof Error && error.message.startsWith("Fast mode")) {
|
|
287
|
+
error.code ??= "LIBFX_UNSUPPORTED_FAST";
|
|
288
|
+
}
|
|
289
|
+
return error;
|
|
290
|
+
}
|
|
250
291
|
async function cancelResponseBody(response) {
|
|
251
292
|
try {
|
|
252
293
|
await response.body?.cancel();
|
|
@@ -600,6 +641,9 @@ function createRuntime(options) {
|
|
|
600
641
|
const stdin = new ByteQueue;
|
|
601
642
|
const streams = new Map;
|
|
602
643
|
const httpRequests = new Set;
|
|
644
|
+
const steering = [];
|
|
645
|
+
let steeringBytes = 0;
|
|
646
|
+
let steeringOpen = false;
|
|
603
647
|
const workspaceExecs = new Set;
|
|
604
648
|
const workspace = prepareWorkspaceAdapter(options.workspace);
|
|
605
649
|
const args = ["fx", ...options.args || []];
|
|
@@ -835,6 +879,7 @@ function createRuntime(options) {
|
|
|
835
879
|
return 0;
|
|
836
880
|
if (!value?.length)
|
|
837
881
|
return null;
|
|
882
|
+
options.onTransportChunk?.(value.length);
|
|
838
883
|
return copy(value);
|
|
839
884
|
};
|
|
840
885
|
const immediate = consume();
|
|
@@ -891,6 +936,37 @@ function createRuntime(options) {
|
|
|
891
936
|
httpRequests.delete(controller);
|
|
892
937
|
});
|
|
893
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
|
+
}
|
|
894
970
|
let pendingHostToolResult = null;
|
|
895
971
|
function hostToolCall(namePtr, nameLen, argumentsPtr, argumentsLen, outputPtr, outputCap, statusPtr) {
|
|
896
972
|
pendingHostToolResult = null;
|
|
@@ -1349,6 +1425,11 @@ function createRuntime(options) {
|
|
|
1349
1425
|
fx_host_tool_result_release() {
|
|
1350
1426
|
pendingHostToolResult = null;
|
|
1351
1427
|
},
|
|
1428
|
+
fx_steering_take: steeringTake,
|
|
1429
|
+
fx_steering_close() {
|
|
1430
|
+
steeringOpen = false;
|
|
1431
|
+
clearSteering();
|
|
1432
|
+
},
|
|
1352
1433
|
fx_open_url: new WebAssembly.Suspending(openUrl),
|
|
1353
1434
|
fx_oauth_session_load: new WebAssembly.Suspending(oauthSessionLoad),
|
|
1354
1435
|
fx_oauth_session_commit: new WebAssembly.Suspending(oauthSessionCommit),
|
|
@@ -1383,8 +1464,19 @@ function createRuntime(options) {
|
|
|
1383
1464
|
stdin.wake();
|
|
1384
1465
|
},
|
|
1385
1466
|
closeStdin() {
|
|
1467
|
+
steeringOpen = false;
|
|
1468
|
+
clearSteering();
|
|
1386
1469
|
stdin.close();
|
|
1387
1470
|
},
|
|
1471
|
+
openSteering() {
|
|
1472
|
+
clearSteering();
|
|
1473
|
+
steeringOpen = true;
|
|
1474
|
+
},
|
|
1475
|
+
steer: queueSteering,
|
|
1476
|
+
closeSteering() {
|
|
1477
|
+
steeringOpen = false;
|
|
1478
|
+
clearSteering();
|
|
1479
|
+
},
|
|
1388
1480
|
abortHostEffects,
|
|
1389
1481
|
abort(error) {
|
|
1390
1482
|
aborted = true;
|
|
@@ -1547,11 +1639,31 @@ function normalizePromptInput(input) {
|
|
|
1547
1639
|
return [{ type: "text", text: input }];
|
|
1548
1640
|
if (!Array.isArray(input))
|
|
1549
1641
|
throw new TypeError("prompt input must be a string or an array of prompt blocks");
|
|
1642
|
+
let imageCount = 0;
|
|
1643
|
+
let imageBytes = 0;
|
|
1550
1644
|
return input.map((block, index) => {
|
|
1551
1645
|
if (!block || typeof block !== "object")
|
|
1552
1646
|
throw new TypeError(`prompt block ${index} must be an object`);
|
|
1553
|
-
if (block.type === "image")
|
|
1554
|
-
|
|
1647
|
+
if (block.type === "image") {
|
|
1648
|
+
if (typeof block.data !== "string" || block.data.length === 0) {
|
|
1649
|
+
throw new TypeError(`image prompt block ${index} requires base64 data`);
|
|
1650
|
+
}
|
|
1651
|
+
if (typeof block.mimeType !== "string" || block.mimeType.length === 0 || block.mimeType.length > 128) {
|
|
1652
|
+
throw new TypeError(`image prompt block ${index} requires a mimeType`);
|
|
1653
|
+
}
|
|
1654
|
+
if (block.data.length > maxPromptImageDataBytes) {
|
|
1655
|
+
throw new RangeError(`image prompt block ${index} exceeds the ${maxPromptImageDataBytes} byte per-image libfx limit`);
|
|
1656
|
+
}
|
|
1657
|
+
imageCount += 1;
|
|
1658
|
+
if (imageCount > maxPromptImages) {
|
|
1659
|
+
throw new RangeError(`prompt cannot contain more than ${maxPromptImages} images`);
|
|
1660
|
+
}
|
|
1661
|
+
imageBytes += block.data.length;
|
|
1662
|
+
if (imageBytes > maxPromptImagesBytes) {
|
|
1663
|
+
throw new RangeError(`prompt images exceed the ${maxPromptImagesBytes} byte libfx frame limit`);
|
|
1664
|
+
}
|
|
1665
|
+
return { type: "image", data: block.data, mimeType: block.mimeType };
|
|
1666
|
+
}
|
|
1555
1667
|
if (block.type === "text") {
|
|
1556
1668
|
if (typeof block.text !== "string")
|
|
1557
1669
|
throw new TypeError(`text prompt block ${index} requires text`);
|
|
@@ -1568,6 +1680,20 @@ function normalizePromptInput(input) {
|
|
|
1568
1680
|
throw new TypeError(`unsupported prompt block type: ${String(block.type)}`);
|
|
1569
1681
|
});
|
|
1570
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
|
+
}
|
|
1571
1697
|
function normalizeHostTools(value) {
|
|
1572
1698
|
if (value === undefined)
|
|
1573
1699
|
return { descriptors: [], executors: new Map };
|
|
@@ -1577,15 +1703,26 @@ function normalizeHostTools(value) {
|
|
|
1577
1703
|
throw new RangeError("tools cannot contain more than 64 entries");
|
|
1578
1704
|
const descriptors = [];
|
|
1579
1705
|
const executors = new Map;
|
|
1706
|
+
const names = new Set;
|
|
1580
1707
|
for (const [index, tool] of value.entries()) {
|
|
1581
1708
|
if (!tool || typeof tool !== "object")
|
|
1582
1709
|
throw new TypeError(`tool ${index} must be an object`);
|
|
1583
|
-
const { name, description, inputSchema, execute } = tool;
|
|
1710
|
+
const { name, description, inputSchema, execute, providerExecuted } = tool;
|
|
1584
1711
|
if (typeof name !== "string" || !/^[A-Za-z0-9_-]{1,64}$/.test(name)) {
|
|
1585
1712
|
throw new TypeError(`tool ${index} has an invalid name`);
|
|
1586
1713
|
}
|
|
1587
|
-
if (
|
|
1714
|
+
if (names.has(name))
|
|
1588
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
|
+
}
|
|
1589
1726
|
if (typeof description !== "string")
|
|
1590
1727
|
throw new TypeError(`tool ${name} requires a description`);
|
|
1591
1728
|
if (typeof execute !== "function")
|
|
@@ -1703,6 +1840,10 @@ async function createFxAgent(options = {}) {
|
|
|
1703
1840
|
for (let attemptIndex = 0;attemptIndex < 2; attemptIndex++) {
|
|
1704
1841
|
const startedAt = performance.now();
|
|
1705
1842
|
const attempt = activeTurn ? ++activeTurn.transportAttempts : attemptIndex + 1;
|
|
1843
|
+
if (activeTurn) {
|
|
1844
|
+
activeTurn.transportBytes = 0;
|
|
1845
|
+
activeTurn.lastTransportActivityAt = null;
|
|
1846
|
+
}
|
|
1706
1847
|
emit("transport.start", { attempt, method, endpoint, model: options.model });
|
|
1707
1848
|
try {
|
|
1708
1849
|
if (activeTurn?.cancelled) {
|
|
@@ -1798,7 +1939,22 @@ async function createFxAgent(options = {}) {
|
|
|
1798
1939
|
fetch: transportFetch,
|
|
1799
1940
|
args: ["acp"],
|
|
1800
1941
|
env: agentEnvironment(options),
|
|
1801
|
-
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
|
+
}
|
|
1802
1958
|
};
|
|
1803
1959
|
const runtime = options.runtimeFactory ? await options.runtimeFactory(runtimeOptions) : await instantiate(runtimeOptions);
|
|
1804
1960
|
emit("runtime.ready");
|
|
@@ -1902,7 +2058,7 @@ async function createFxAgent(options = {}) {
|
|
|
1902
2058
|
try {
|
|
1903
2059
|
await runtime.exited;
|
|
1904
2060
|
} catch {}
|
|
1905
|
-
throw error;
|
|
2061
|
+
throw agentBootstrapError(error);
|
|
1906
2062
|
}
|
|
1907
2063
|
const agent = {
|
|
1908
2064
|
prompt(input, promptOptions = {}) {
|
|
@@ -1951,6 +2107,9 @@ async function createFxAgent(options = {}) {
|
|
|
1951
2107
|
const delta = update.content?.text;
|
|
1952
2108
|
return delta ? { type: "reasoning_delta", delta } : null;
|
|
1953
2109
|
}
|
|
2110
|
+
if (update.sessionUpdate === "user_message_chunk" && update.content?.type === "text") {
|
|
2111
|
+
return { type: "user_message", text: update.content.text };
|
|
2112
|
+
}
|
|
1954
2113
|
if (update.sessionUpdate === "tool_call") {
|
|
1955
2114
|
toolNames.set(update.toolCallId, update.name || update.toolName || update.title || "tool");
|
|
1956
2115
|
if (started.has(update.toolCallId))
|
|
@@ -1959,7 +2118,8 @@ async function createFxAgent(options = {}) {
|
|
|
1959
2118
|
return {
|
|
1960
2119
|
type: "tool_start",
|
|
1961
2120
|
id: update.toolCallId,
|
|
1962
|
-
name: toolNames.get(update.toolCallId)
|
|
2121
|
+
name: toolNames.get(update.toolCallId),
|
|
2122
|
+
...toolStartInput(update.rawInput)
|
|
1963
2123
|
};
|
|
1964
2124
|
}
|
|
1965
2125
|
if (update.sessionUpdate === "tool_call_update" && (update.status === "completed" || update.status === "failed")) {
|
|
@@ -1983,6 +2143,9 @@ async function createFxAgent(options = {}) {
|
|
|
1983
2143
|
cancel() {
|
|
1984
2144
|
rawTurn.cancel();
|
|
1985
2145
|
},
|
|
2146
|
+
steer(input) {
|
|
2147
|
+
return rawTurn.steer(normalizeSteeringInput(input));
|
|
2148
|
+
},
|
|
1986
2149
|
[Symbol.asyncIterator]() {
|
|
1987
2150
|
const iterator = async function* () {
|
|
1988
2151
|
for await (const update of rawTurn) {
|
|
@@ -2025,8 +2188,22 @@ async function createFxAgent(options = {}) {
|
|
|
2025
2188
|
result.reasoningTokens = usage.reasoningTokens;
|
|
2026
2189
|
return result;
|
|
2027
2190
|
}
|
|
2191
|
+
function toolStartInput(rawInput) {
|
|
2192
|
+
if (rawInput === undefined || rawInput === null)
|
|
2193
|
+
return {};
|
|
2194
|
+
const serialized = JSON.stringify(rawInput);
|
|
2195
|
+
if (serialized === undefined)
|
|
2196
|
+
return {};
|
|
2197
|
+
const bytes = encoder.encode(serialized);
|
|
2198
|
+
if (bytes.length <= maxToolStartInputBytes)
|
|
2199
|
+
return { input: rawInput };
|
|
2200
|
+
return { inputTruncated: true, inputPreview: decoder.decode(utf8Prefix(bytes, maxToolStartInputBytes)) };
|
|
2201
|
+
}
|
|
2028
2202
|
function startTurn(input, promptOptions) {
|
|
2029
2203
|
const prompt = normalizePromptInput(input);
|
|
2204
|
+
if (encoder.encode(JSON.stringify({ sessionId: "", prompt })).length + promptFrameEnvelopeBytes > maxPromptFrameBytes) {
|
|
2205
|
+
throw new RangeError(`prompt exceeds the ${maxPromptFrameBytes} byte libfx frame limit`);
|
|
2206
|
+
}
|
|
2030
2207
|
const signal = promptOptions.signal;
|
|
2031
2208
|
if (signal !== undefined && (typeof signal?.addEventListener !== "function" || typeof signal?.removeEventListener !== "function"))
|
|
2032
2209
|
throw new TypeError("prompt signal must be an AbortSignal");
|
|
@@ -2069,13 +2246,37 @@ async function createFxAgent(options = {}) {
|
|
|
2069
2246
|
},
|
|
2070
2247
|
toolControllers,
|
|
2071
2248
|
transportAttempts: 0,
|
|
2249
|
+
transportBytes: 0,
|
|
2250
|
+
lastTransportActivityAt: null,
|
|
2072
2251
|
get cancelled() {
|
|
2073
2252
|
return cancelled;
|
|
2074
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
|
+
},
|
|
2075
2275
|
cancel() {
|
|
2076
2276
|
if (finished || cancelled)
|
|
2077
2277
|
return;
|
|
2078
2278
|
cancelled = true;
|
|
2279
|
+
runtime.closeSteering?.();
|
|
2079
2280
|
resumeOutput?.();
|
|
2080
2281
|
resumeOutput = null;
|
|
2081
2282
|
send({ jsonrpc: "2.0", method: "session/cancel", params: { sessionId } });
|
|
@@ -2115,6 +2316,7 @@ async function createFxAgent(options = {}) {
|
|
|
2115
2316
|
return turn;
|
|
2116
2317
|
}
|
|
2117
2318
|
activeTurn = turn;
|
|
2319
|
+
runtime.openSteering?.();
|
|
2118
2320
|
const abort = () => turn.cancel();
|
|
2119
2321
|
signal?.addEventListener("abort", abort, { once: true });
|
|
2120
2322
|
turn.result = request("session/prompt", { sessionId, prompt }).then((response) => ({ stopReason: cancelled ? "cancelled" : response.stopReason, usage: response.usage })).catch((error) => {
|
|
@@ -2129,6 +2331,7 @@ async function createFxAgent(options = {}) {
|
|
|
2129
2331
|
signal?.removeEventListener("abort", abort);
|
|
2130
2332
|
if (activeTurn === turn)
|
|
2131
2333
|
activeTurn = null;
|
|
2334
|
+
runtime.closeSteering?.();
|
|
2132
2335
|
toolControllers.clear();
|
|
2133
2336
|
if (discardedBytes)
|
|
2134
2337
|
emit("output.discarded", { reason: "cancelled", bytes: discardedBytes });
|
|
@@ -2439,12 +2642,14 @@ async function getBackendInfo(value = {}) {
|
|
|
2439
2642
|
}
|
|
2440
2643
|
}
|
|
2441
2644
|
function createNativeCoreRuntime(addon, options) {
|
|
2442
|
-
const { apiKey, model, gatewayChatUrl } = options;
|
|
2645
|
+
const { apiKey, model, effort, fast, gatewayChatUrl } = options;
|
|
2443
2646
|
const core = addon.createCore({
|
|
2444
2647
|
apiKey,
|
|
2445
2648
|
home: options.home ?? import_node_os.homedir(),
|
|
2446
2649
|
workspaceRoot: options.workspaceRoot ?? process.cwd(),
|
|
2447
2650
|
...model === undefined ? {} : { model },
|
|
2651
|
+
...effort === undefined ? {} : { effort },
|
|
2652
|
+
...fast === undefined ? {} : { fast },
|
|
2448
2653
|
...gatewayChatUrl === undefined ? {} : { gatewayChatUrl }
|
|
2449
2654
|
});
|
|
2450
2655
|
let readyFd;
|
|
@@ -2510,6 +2715,7 @@ function createNativeCoreRuntime(addon, options) {
|
|
|
2510
2715
|
if (response.body) {
|
|
2511
2716
|
for await (const chunk of response.body) {
|
|
2512
2717
|
const buffer = Buffer.from(chunk);
|
|
2718
|
+
options.onTransportChunk?.(buffer.length);
|
|
2513
2719
|
let offset = 0;
|
|
2514
2720
|
while (offset < buffer.length) {
|
|
2515
2721
|
const end = Math.min(offset + 65536, buffer.length);
|
package/node.js
CHANGED
|
@@ -345,12 +345,14 @@ export async function getBackendInfo(value = {}) {
|
|
|
345
345
|
}
|
|
346
346
|
|
|
347
347
|
function createNativeCoreRuntime(addon, options) {
|
|
348
|
-
const { apiKey, model, gatewayChatUrl } = options;
|
|
348
|
+
const { apiKey, model, effort, fast, gatewayChatUrl } = options;
|
|
349
349
|
const core = addon.createCore({
|
|
350
350
|
apiKey,
|
|
351
351
|
home: options.home ?? homedir(),
|
|
352
352
|
workspaceRoot: options.workspaceRoot ?? process.cwd(),
|
|
353
353
|
...(model === undefined ? {} : { model }),
|
|
354
|
+
...(effort === undefined ? {} : { effort }),
|
|
355
|
+
...(fast === undefined ? {} : { fast }),
|
|
354
356
|
...(gatewayChatUrl === undefined ? {} : { gatewayChatUrl }),
|
|
355
357
|
});
|
|
356
358
|
let readyFd;
|
|
@@ -405,6 +407,7 @@ function createNativeCoreRuntime(addon, options) {
|
|
|
405
407
|
if (response.body) {
|
|
406
408
|
for await (const chunk of response.body) {
|
|
407
409
|
const buffer = Buffer.from(chunk);
|
|
410
|
+
options.onTransportChunk?.(buffer.length);
|
|
408
411
|
let offset = 0;
|
|
409
412
|
while (offset < buffer.length) {
|
|
410
413
|
const end = Math.min(offset + 64 * 1024, buffer.length);
|