libfx 0.0.7-dev.632.g9fe8a30c19a7 → 0.0.8-dev.820.g1d9d3b63d6ea
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 +130 -5
- package/core-output.js +58 -0
- package/fx-core.wasm +0 -0
- package/fx-sdk.js +234 -92
- 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/mcp.js +47 -18
- package/node.cjs +2542 -0
- package/node.js +302 -61
- package/package.json +9 -3
- package/wasm-module.js +50 -0
package/fx-sdk.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { CoreOutput, maxCoreMessageBytes } from "./core-output.js";
|
|
2
|
+
import { loadModule } from "./wasm-module.js";
|
|
3
|
+
|
|
1
4
|
const encoder = new TextEncoder();
|
|
2
5
|
const decoder = new TextDecoder();
|
|
3
6
|
const strictDecoder = new TextDecoder("utf-8", { fatal: true });
|
|
@@ -11,6 +14,8 @@ const maxUrlBytes = 16 * 1024;
|
|
|
11
14
|
const maxModelCatalogBytes = 4 * 1024 * 1024;
|
|
12
15
|
const maxModelCatalogEntries = 10_000;
|
|
13
16
|
const streamReadsPerTaskYield = 32;
|
|
17
|
+
const maxUnreadEventBytes = 1024 * 1024;
|
|
18
|
+
const maxUnreadEvents = 256;
|
|
14
19
|
|
|
15
20
|
function boundedString(value, name, maxBytes, required) {
|
|
16
21
|
if (value === undefined && !required) return undefined;
|
|
@@ -285,43 +290,6 @@ class ByteQueue {
|
|
|
285
290
|
}
|
|
286
291
|
}
|
|
287
292
|
|
|
288
|
-
const modulePromisesBySource = new Map();
|
|
289
|
-
const modulePromisesByObject = new WeakMap();
|
|
290
|
-
|
|
291
|
-
async function compileModule(input) {
|
|
292
|
-
if (input instanceof WebAssembly.Module) return input;
|
|
293
|
-
if (typeof input === "string") input = fetch(input);
|
|
294
|
-
if (input instanceof Promise) input = await input;
|
|
295
|
-
if (input instanceof WebAssembly.Module) return input;
|
|
296
|
-
if (input instanceof Response) {
|
|
297
|
-
const contentType = input.headers.get("content-type")?.split(";", 1)[0].trim().toLowerCase();
|
|
298
|
-
if (contentType === "application/wasm" && typeof WebAssembly.compileStreaming === "function") {
|
|
299
|
-
return WebAssembly.compileStreaming(input);
|
|
300
|
-
}
|
|
301
|
-
const bytes = await input.arrayBuffer();
|
|
302
|
-
return WebAssembly.compile(bytes);
|
|
303
|
-
}
|
|
304
|
-
if (input instanceof ArrayBuffer || ArrayBuffer.isView(input)) {
|
|
305
|
-
return WebAssembly.compile(input);
|
|
306
|
-
}
|
|
307
|
-
throw new TypeError("wasm must be a URL, Response, ArrayBuffer, typed array, or WebAssembly.Module");
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
function loadModule(input) {
|
|
311
|
-
if (input instanceof WebAssembly.Module) return Promise.resolve(input);
|
|
312
|
-
const isString = typeof input === "string";
|
|
313
|
-
if (!isString && (typeof input !== "object" || input === null)) return compileModule(input);
|
|
314
|
-
const cache = isString ? modulePromisesBySource : modulePromisesByObject;
|
|
315
|
-
const cached = cache.get(input);
|
|
316
|
-
if (cached) return cached;
|
|
317
|
-
const pending = compileModule(input);
|
|
318
|
-
cache.set(input, pending);
|
|
319
|
-
pending.catch(() => {
|
|
320
|
-
if (cache.get(input) === pending) cache.delete(input);
|
|
321
|
-
});
|
|
322
|
-
return pending;
|
|
323
|
-
}
|
|
324
|
-
|
|
325
293
|
function raceWithTimeout(promise, timeoutMs, timeoutValue) {
|
|
326
294
|
let timer;
|
|
327
295
|
return new Promise((resolve, reject) => {
|
|
@@ -341,8 +309,11 @@ function yieldToHostTask() {
|
|
|
341
309
|
}
|
|
342
310
|
|
|
343
311
|
function createRuntime(options) {
|
|
312
|
+
// Creating this inside a Wasm call would retain that instance through the error stack.
|
|
313
|
+
const abortReason = new DOMException("This operation was aborted", "AbortError");
|
|
344
314
|
const stdin = new ByteQueue();
|
|
345
315
|
const streams = new Map();
|
|
316
|
+
const httpRequests = new Set();
|
|
346
317
|
const workspaceExecs = new Set();
|
|
347
318
|
const workspace = prepareWorkspaceAdapter(options.workspace);
|
|
348
319
|
const args = ["fx", ...(options.args || [])];
|
|
@@ -352,7 +323,8 @@ function createRuntime(options) {
|
|
|
352
323
|
let exitedResolve;
|
|
353
324
|
let exitCode = null;
|
|
354
325
|
let aborted = false;
|
|
355
|
-
let
|
|
326
|
+
let coreOutput;
|
|
327
|
+
let outputError;
|
|
356
328
|
const exited = new Promise((resolve) => { exitedResolve = resolve; });
|
|
357
329
|
const markExited = (code) => {
|
|
358
330
|
if (exitCode !== null) return;
|
|
@@ -382,7 +354,7 @@ function createRuntime(options) {
|
|
|
382
354
|
}
|
|
383
355
|
|
|
384
356
|
function emitStdout(chunk) {
|
|
385
|
-
if (options.stdout) options.stdout(chunk);
|
|
357
|
+
if (options.stdout) return options.stdout(chunk);
|
|
386
358
|
}
|
|
387
359
|
|
|
388
360
|
function fdWrite(fd, iovs, count, nwritten) {
|
|
@@ -392,6 +364,7 @@ function createRuntime(options) {
|
|
|
392
364
|
for (let index = 0; index < count; index++) {
|
|
393
365
|
total += view.getUint32(iovs + index * 8 + 4, true);
|
|
394
366
|
}
|
|
367
|
+
if (coreOutput && total > maxCoreMessageBytes) throw new RangeError("core output message exceeds 64 MiB");
|
|
395
368
|
if (fd === 1 || fd === 2) {
|
|
396
369
|
const chunk = new Uint8Array(total);
|
|
397
370
|
let offset = 0;
|
|
@@ -401,7 +374,13 @@ function createRuntime(options) {
|
|
|
401
374
|
chunk.set(bytes(ptr, len), offset);
|
|
402
375
|
offset += len;
|
|
403
376
|
}
|
|
404
|
-
if (fd === 1)
|
|
377
|
+
if (fd === 1) {
|
|
378
|
+
const pending = emitStdout(chunk);
|
|
379
|
+
if (coreOutput && pending) return Promise.resolve(pending).then(() => {
|
|
380
|
+
writeU32(nwritten, total);
|
|
381
|
+
return 0;
|
|
382
|
+
});
|
|
383
|
+
}
|
|
405
384
|
else if (typeof options.stderr === "function") options.stderr(chunk);
|
|
406
385
|
else console.warn(decoder.decode(chunk));
|
|
407
386
|
}
|
|
@@ -569,20 +548,40 @@ function createRuntime(options) {
|
|
|
569
548
|
}
|
|
570
549
|
|
|
571
550
|
function httpRequest(methodPtr, methodLen, urlPtr, urlLen, headersPtr, headersLen, bodyPtr, bodyLen, statusOut, responsePtr, responseCap) {
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
551
|
+
const controller = new AbortController();
|
|
552
|
+
httpRequests.add(controller);
|
|
553
|
+
let onAbort;
|
|
554
|
+
const cancelled = new Promise((resolve) => {
|
|
555
|
+
onAbort = () => resolve(-1);
|
|
556
|
+
controller.signal.addEventListener("abort", onAbort, { once: true });
|
|
557
|
+
});
|
|
558
|
+
const request = (async () => {
|
|
559
|
+
const response = await options.fetch(text(urlPtr, urlLen), {
|
|
560
|
+
method: text(methodPtr, methodLen),
|
|
561
|
+
headers: headersFromJson(headersPtr, headersLen),
|
|
562
|
+
body: bodyLen ? bytes(bodyPtr, bodyLen).slice() : undefined,
|
|
563
|
+
signal: controller.signal,
|
|
564
|
+
});
|
|
565
|
+
if (controller.signal.aborted) {
|
|
566
|
+
void cancelResponseBody(response);
|
|
567
|
+
return -1;
|
|
568
|
+
}
|
|
577
569
|
const body = new Uint8Array(await response.arrayBuffer());
|
|
570
|
+
if (controller.signal.aborted) return -1;
|
|
578
571
|
new DataView(memory().buffer).setUint16(statusOut, response.status, true);
|
|
579
572
|
if (body.length > responseCap) return -2;
|
|
580
573
|
bytes(responsePtr, body.length).set(body);
|
|
581
574
|
return body.length;
|
|
582
|
-
}).catch(() => -1);
|
|
575
|
+
})().catch(() => -1);
|
|
576
|
+
return Promise.race([request, cancelled]).finally(() => {
|
|
577
|
+
controller.signal.removeEventListener("abort", onAbort);
|
|
578
|
+
httpRequests.delete(controller);
|
|
579
|
+
});
|
|
583
580
|
}
|
|
584
581
|
|
|
582
|
+
let pendingHostToolResult = null;
|
|
585
583
|
function hostToolCall(namePtr, nameLen, argumentsPtr, argumentsLen, outputPtr, outputCap, statusPtr) {
|
|
584
|
+
pendingHostToolResult = null;
|
|
586
585
|
if (typeof options.hostToolExecutor !== "function") return -1;
|
|
587
586
|
if (options.traceWasi) console.error("fx host tool call start");
|
|
588
587
|
let input;
|
|
@@ -591,9 +590,13 @@ function createRuntime(options) {
|
|
|
591
590
|
if (options.traceWasi) console.error("fx host tool call settled", result.cancelled, result.isError);
|
|
592
591
|
if (result.cancelled) return -2;
|
|
593
592
|
const output = encoder.encode(result.content);
|
|
594
|
-
|
|
593
|
+
bytes(statusPtr, 1)[0] = (result.isError ? 1 : 0) + (result.rich ? 2 : 0);
|
|
594
|
+
if (output.length > outputCap) {
|
|
595
|
+
if (!result.rich || output.length > 8 * 1024 * 1024) return -3;
|
|
596
|
+
pendingHostToolResult = output;
|
|
597
|
+
return output.length;
|
|
598
|
+
}
|
|
595
599
|
bytes(outputPtr, output.length).set(output);
|
|
596
|
-
bytes(statusPtr, 1)[0] = result.isError ? 1 : 0;
|
|
597
600
|
return output.length;
|
|
598
601
|
}).catch(() => -1);
|
|
599
602
|
}
|
|
@@ -857,7 +860,9 @@ function createRuntime(options) {
|
|
|
857
860
|
}
|
|
858
861
|
|
|
859
862
|
function abortHostEffects() {
|
|
860
|
-
|
|
863
|
+
pendingHostToolResult = null;
|
|
864
|
+
streams.forEach((state) => state.controller.abort(abortReason));
|
|
865
|
+
httpRequests.forEach((controller) => controller.abort(abortReason));
|
|
861
866
|
workspaceExecs.forEach((state) => state.abort(-3));
|
|
862
867
|
}
|
|
863
868
|
|
|
@@ -867,7 +872,7 @@ function createRuntime(options) {
|
|
|
867
872
|
args_get(ptrs, data) { if (options.traceWasi) console.error("wasi args_get"); writeVector(args, ptrs, data); return 0; },
|
|
868
873
|
environ_sizes_get(count, size) { if (options.traceWasi) console.error("wasi environ_sizes_get"); writeU32(count, env.length); writeU32(size, env.reduce((n, v) => n + encoder.encode(v).length + 1, 0)); return 0; },
|
|
869
874
|
environ_get(ptrs, data) { if (options.traceWasi) console.error("wasi environ_get"); writeVector(env, ptrs, data); return 0; },
|
|
870
|
-
fd_write: fdWrite,
|
|
875
|
+
fd_write: options.args?.[0] === "acp" ? new WebAssembly.Suspending(fdWrite) : fdWrite,
|
|
871
876
|
fd_read: new WebAssembly.Suspending(fdRead),
|
|
872
877
|
fd_close() { return 0; },
|
|
873
878
|
fd_fdstat_get(fd, out) {
|
|
@@ -915,9 +920,16 @@ function createRuntime(options) {
|
|
|
915
920
|
fx_http_stream_open: streamOpen,
|
|
916
921
|
fx_http_stream_status: new WebAssembly.Suspending(streamStatus),
|
|
917
922
|
fx_http_stream_next: new WebAssembly.Suspending(streamNext),
|
|
918
|
-
fx_http_stream_close(handle) { const state = streams.get(handle); state?.controller.abort(); streams.delete(handle); },
|
|
923
|
+
fx_http_stream_close(handle) { const state = streams.get(handle); state?.controller.abort(abortReason); streams.delete(handle); },
|
|
919
924
|
fx_http_request: new WebAssembly.Suspending(httpRequest),
|
|
920
925
|
fx_host_tool_call: new WebAssembly.Suspending(hostToolCall),
|
|
926
|
+
fx_host_tool_result_read(offset, ptr, cap) {
|
|
927
|
+
if (!pendingHostToolResult || offset < 0 || offset > pendingHostToolResult.length) return -1;
|
|
928
|
+
const chunk = pendingHostToolResult.subarray(offset, offset + cap);
|
|
929
|
+
bytes(ptr, chunk.length).set(chunk);
|
|
930
|
+
return chunk.length;
|
|
931
|
+
},
|
|
932
|
+
fx_host_tool_result_release() { pendingHostToolResult = null; },
|
|
921
933
|
fx_open_url: new WebAssembly.Suspending(openUrl),
|
|
922
934
|
fx_oauth_session_load: new WebAssembly.Suspending(oauthSessionLoad),
|
|
923
935
|
fx_oauth_session_commit: new WebAssembly.Suspending(oauthSessionCommit),
|
|
@@ -947,8 +959,10 @@ function createRuntime(options) {
|
|
|
947
959
|
wake() { stdin.wake(); },
|
|
948
960
|
closeStdin() { stdin.close(); },
|
|
949
961
|
abortHostEffects,
|
|
950
|
-
abort() {
|
|
962
|
+
abort(error) {
|
|
951
963
|
aborted = true;
|
|
964
|
+
outputError = error;
|
|
965
|
+
coreOutput?.close();
|
|
952
966
|
abortHostEffects();
|
|
953
967
|
stdin.close();
|
|
954
968
|
markExited(130);
|
|
@@ -956,17 +970,12 @@ function createRuntime(options) {
|
|
|
956
970
|
markExited,
|
|
957
971
|
get aborted() { return aborted; },
|
|
958
972
|
get exitCode() { return exitCode; },
|
|
973
|
+
get error() { return outputError; },
|
|
959
974
|
setLineHandler(handler) {
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
for (;;) {
|
|
963
|
-
const newline = lineBuffer.indexOf("\n");
|
|
964
|
-
if (newline < 0) break;
|
|
965
|
-
const line = lineBuffer.slice(0, newline); lineBuffer = lineBuffer.slice(newline + 1);
|
|
966
|
-
if (line) handler(JSON.parse(line));
|
|
967
|
-
}
|
|
968
|
-
};
|
|
975
|
+
coreOutput = new CoreOutput(handler);
|
|
976
|
+
options.stdout = (chunk) => coreOutput.write(chunk);
|
|
969
977
|
},
|
|
978
|
+
finishOutput() { coreOutput?.finish(); },
|
|
970
979
|
};
|
|
971
980
|
}
|
|
972
981
|
|
|
@@ -978,10 +987,18 @@ async function instantiate(options) {
|
|
|
978
987
|
runtime.setInstance(instance);
|
|
979
988
|
const start = WebAssembly.promising(instance.exports._start);
|
|
980
989
|
start().then(
|
|
981
|
-
() =>
|
|
990
|
+
() => {
|
|
991
|
+
runtime.setInstance(null);
|
|
992
|
+
try { runtime.finishOutput(); runtime.markExited(0); }
|
|
993
|
+
catch (error) { runtime.abort(error); }
|
|
994
|
+
},
|
|
982
995
|
(error) => {
|
|
983
|
-
|
|
984
|
-
|
|
996
|
+
runtime.setInstance(null);
|
|
997
|
+
if (options.args?.[0] === "acp" && !String(error).includes("proc_exit")) runtime.abort(error);
|
|
998
|
+
else {
|
|
999
|
+
if (!String(error).includes("proc_exit")) console.error(error);
|
|
1000
|
+
runtime.markExited(runtime.aborted ? 130 : 1);
|
|
1001
|
+
}
|
|
985
1002
|
},
|
|
986
1003
|
);
|
|
987
1004
|
return runtime;
|
|
@@ -1119,10 +1136,27 @@ function normalizeInstructions(value) {
|
|
|
1119
1136
|
}
|
|
1120
1137
|
|
|
1121
1138
|
function hostToolContent(value) {
|
|
1122
|
-
if (
|
|
1123
|
-
|
|
1139
|
+
if (value?.type === "libfx.tool-result") {
|
|
1140
|
+
if (typeof value.text !== "string" || !Array.isArray(value.images) || value.images.length > 8) {
|
|
1141
|
+
throw new TypeError("invalid typed tool result");
|
|
1142
|
+
}
|
|
1143
|
+
let imageBytes = 0;
|
|
1144
|
+
const images = value.images.map((image) => {
|
|
1145
|
+
if (image?.type !== "image" || typeof image.data !== "string" || typeof image.mimeType !== "string" || image.mimeType.length > 128 || image.data.length > 5 * 1024 * 1024) {
|
|
1146
|
+
throw new TypeError("invalid tool image");
|
|
1147
|
+
}
|
|
1148
|
+
imageBytes += image.data.length;
|
|
1149
|
+
if (imageBytes > 8 * 1024 * 1024) throw new RangeError("tool images exceed the result limit");
|
|
1150
|
+
return { type: "image", data: image.data, mimeType: image.mimeType };
|
|
1151
|
+
});
|
|
1152
|
+
const content = JSON.stringify({ text: value.text, images });
|
|
1153
|
+
if (new TextEncoder().encode(content).length > 8 * 1024 * 1024) throw new RangeError("typed tool result exceeds the result limit");
|
|
1154
|
+
return { content, rich: true, isError: value.isError === true };
|
|
1155
|
+
}
|
|
1156
|
+
if (typeof value === "string") return { content: value, rich: false };
|
|
1157
|
+
if (value === undefined) return { content: "null", rich: false };
|
|
1124
1158
|
const encoded = JSON.stringify(value);
|
|
1125
|
-
return encoded === undefined ? "null" : encoded;
|
|
1159
|
+
return { content: encoded === undefined ? "null" : encoded, rich: false };
|
|
1126
1160
|
}
|
|
1127
1161
|
|
|
1128
1162
|
function checkpointBytes(value) {
|
|
@@ -1160,6 +1194,7 @@ export async function createFxAgent(options = {}) {
|
|
|
1160
1194
|
let sessionId = null;
|
|
1161
1195
|
let activeTurn = null;
|
|
1162
1196
|
let closing = false;
|
|
1197
|
+
const isCurrentTurn = (turn) => turn && activeTurn === turn && !turn.cancelled && !closing;
|
|
1163
1198
|
const emit = (type, detail = {}) => {
|
|
1164
1199
|
try { options.onEvent?.({ type, timestamp: performance.now(), ...detail }); } catch {}
|
|
1165
1200
|
};
|
|
@@ -1176,6 +1211,10 @@ export async function createFxAgent(options = {}) {
|
|
|
1176
1211
|
const attempt = activeTurn ? ++activeTurn.transportAttempts : attemptIndex + 1;
|
|
1177
1212
|
emit("transport.start", { attempt, method, endpoint, model: options.model });
|
|
1178
1213
|
try {
|
|
1214
|
+
if (activeTurn?.cancelled) {
|
|
1215
|
+
runtime.abortHostEffects();
|
|
1216
|
+
throw new DOMException("Aborted", "AbortError");
|
|
1217
|
+
}
|
|
1179
1218
|
if (!hostFetch) throw new TypeError("fetch is unavailable");
|
|
1180
1219
|
const response = await hostFetch(input, init);
|
|
1181
1220
|
const headers = response.headers;
|
|
@@ -1211,20 +1250,46 @@ export async function createFxAgent(options = {}) {
|
|
|
1211
1250
|
const turn = requestedSessionId === undefined || requestedSessionId === sessionId
|
|
1212
1251
|
? activeTurn
|
|
1213
1252
|
: null;
|
|
1253
|
+
if (!isCurrentTurn(turn)) return { content: "", isError: true, cancelled: true };
|
|
1214
1254
|
const controller = new AbortController();
|
|
1215
|
-
turn
|
|
1216
|
-
let
|
|
1255
|
+
turn.toolControllers.add(controller);
|
|
1256
|
+
let onAbort;
|
|
1257
|
+
const aborted = new Promise((resolve) => { onAbort = () => resolve(); });
|
|
1258
|
+
controller.signal.addEventListener("abort", onAbort, { once: true });
|
|
1259
|
+
let content = "";
|
|
1260
|
+
let rich = false;
|
|
1217
1261
|
let isError = false;
|
|
1218
1262
|
try {
|
|
1219
1263
|
if (!execute) throw new Error(`unknown host tool: ${String(name)}`);
|
|
1220
|
-
|
|
1264
|
+
const execution = Promise.resolve().then(() => {
|
|
1265
|
+
if (controller.signal.aborted || !isCurrentTurn(turn)) return;
|
|
1266
|
+
return execute(input, { signal: controller.signal });
|
|
1267
|
+
});
|
|
1268
|
+
const value = await Promise.race([execution, aborted]);
|
|
1269
|
+
if (!controller.signal.aborted) {
|
|
1270
|
+
const normalized = hostToolContent(value);
|
|
1271
|
+
content = normalized.content;
|
|
1272
|
+
rich = normalized.rich;
|
|
1273
|
+
isError = normalized.isError === true;
|
|
1274
|
+
}
|
|
1221
1275
|
} catch (error) {
|
|
1222
1276
|
isError = true;
|
|
1223
|
-
|
|
1277
|
+
if (error?.toolResult?.type === "libfx.tool-result") {
|
|
1278
|
+
try {
|
|
1279
|
+
const normalized = hostToolContent(error.toolResult);
|
|
1280
|
+
content = normalized.content;
|
|
1281
|
+
rich = normalized.rich;
|
|
1282
|
+
} catch {
|
|
1283
|
+
content = error instanceof Error ? error.message : String(error);
|
|
1284
|
+
}
|
|
1285
|
+
} else {
|
|
1286
|
+
content = error instanceof Error ? error.message : String(error);
|
|
1287
|
+
}
|
|
1224
1288
|
} finally {
|
|
1225
|
-
|
|
1289
|
+
controller.signal.removeEventListener("abort", onAbort);
|
|
1290
|
+
turn.toolControllers.delete(controller);
|
|
1226
1291
|
}
|
|
1227
|
-
return { content, isError, cancelled: controller.signal.aborted };
|
|
1292
|
+
return { content, isError, rich, cancelled: controller.signal.aborted || !isCurrentTurn(turn) };
|
|
1228
1293
|
};
|
|
1229
1294
|
emit("runtime.start");
|
|
1230
1295
|
const runtimeOptions = {
|
|
@@ -1250,37 +1315,50 @@ export async function createFxAgent(options = {}) {
|
|
|
1250
1315
|
});
|
|
1251
1316
|
runtime.exited.then((code) => {
|
|
1252
1317
|
emit("runtime.exit", { code });
|
|
1253
|
-
|
|
1318
|
+
closing = true;
|
|
1319
|
+
const error = runtime.error ?? new Error(`fx-core exited with code ${code} before completing the ACP request`);
|
|
1254
1320
|
for (const waiter of pending.values()) waiter.reject(error);
|
|
1255
1321
|
pending.clear();
|
|
1256
1322
|
});
|
|
1257
|
-
runtime.setLineHandler(
|
|
1323
|
+
runtime.setLineHandler((message, size) => {
|
|
1258
1324
|
emit("acp.receive", { message });
|
|
1259
1325
|
if (message.method === "session/update") {
|
|
1260
|
-
if (message.params.sessionId === sessionId) activeTurn?.push(message.params.update);
|
|
1326
|
+
if (message.params.sessionId === sessionId) return activeTurn?.push(message.params.update, size);
|
|
1261
1327
|
return;
|
|
1262
1328
|
}
|
|
1329
|
+
void handleControlMessage(message).catch((error) => runtime.abort(error));
|
|
1330
|
+
});
|
|
1331
|
+
async function handleControlMessage(message) {
|
|
1263
1332
|
if (message.method === "session/request_permission") {
|
|
1333
|
+
const turn = activeTurn;
|
|
1334
|
+
if (!isCurrentTurn(turn)) return;
|
|
1264
1335
|
emit("permission.request", { request: message.params });
|
|
1336
|
+
if (!isCurrentTurn(turn)) return;
|
|
1265
1337
|
let optionId = null;
|
|
1266
1338
|
try { optionId = await options.onPermission?.(message.params); } catch {}
|
|
1339
|
+
if (!isCurrentTurn(turn)) return;
|
|
1267
1340
|
emit("permission.resolve", { optionId });
|
|
1341
|
+
if (!isCurrentTurn(turn)) return;
|
|
1268
1342
|
send({ jsonrpc: "2.0", id: message.id, result: optionId ? { outcome: { outcome: "selected", optionId } } : { outcome: { outcome: "cancelled" } } });
|
|
1269
1343
|
return;
|
|
1270
1344
|
}
|
|
1271
1345
|
if (message.method === "libfx/tool_call") {
|
|
1272
|
-
const { content, isError, cancelled } = await executeHostTool(
|
|
1346
|
+
const { content, isError, rich, cancelled } = await executeHostTool(
|
|
1273
1347
|
message.params?.name,
|
|
1274
1348
|
message.params?.input,
|
|
1275
1349
|
message.params?.sessionId,
|
|
1276
1350
|
);
|
|
1277
1351
|
if (cancelled || closing) return;
|
|
1278
|
-
|
|
1352
|
+
const response = { jsonrpc: "2.0", id: message.id, result: { content, isError, ...(rich ? { contentType: "rich" } : {}) } };
|
|
1353
|
+
if (encoder.encode(JSON.stringify(response)).length + 1 > 8 * 1024 * 1024) {
|
|
1354
|
+
response.result = { content: "Host tool result exceeded the response frame limit", isError: true };
|
|
1355
|
+
}
|
|
1356
|
+
send(response);
|
|
1279
1357
|
return;
|
|
1280
1358
|
}
|
|
1281
1359
|
const waiter = pending.get(message.id); if (!waiter) return; pending.delete(message.id);
|
|
1282
1360
|
if (message.error) waiter.reject(new Error(message.error.message)); else waiter.resolve(message.result);
|
|
1283
|
-
}
|
|
1361
|
+
}
|
|
1284
1362
|
try {
|
|
1285
1363
|
await request("initialize", {
|
|
1286
1364
|
protocolVersion: 1,
|
|
@@ -1368,18 +1446,28 @@ export async function createFxAgent(options = {}) {
|
|
|
1368
1446
|
}
|
|
1369
1447
|
return null;
|
|
1370
1448
|
};
|
|
1449
|
+
const result = rawTurn.result.then((value) => ({
|
|
1450
|
+
stopReason: value.stopReason,
|
|
1451
|
+
usage: normalizeTurnUsage(value.usage),
|
|
1452
|
+
}));
|
|
1453
|
+
void result.catch(() => {});
|
|
1371
1454
|
return {
|
|
1372
1455
|
cancel() { rawTurn.cancel(); },
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
const
|
|
1376
|
-
|
|
1377
|
-
|
|
1456
|
+
[Symbol.asyncIterator]() {
|
|
1457
|
+
const iterator = (async function* () {
|
|
1458
|
+
for await (const update of rawTurn) {
|
|
1459
|
+
const event = eventFor(update);
|
|
1460
|
+
if (event) yield event;
|
|
1461
|
+
}
|
|
1462
|
+
})();
|
|
1463
|
+
return {
|
|
1464
|
+
next(value) { return iterator.next(value); },
|
|
1465
|
+
return(value) { rawTurn.cancel(); return iterator.return(value); },
|
|
1466
|
+
throw(error) { rawTurn.cancel(); return iterator.throw(error); },
|
|
1467
|
+
[Symbol.asyncIterator]() { return this; },
|
|
1468
|
+
};
|
|
1378
1469
|
},
|
|
1379
|
-
result
|
|
1380
|
-
stopReason: result.stopReason,
|
|
1381
|
-
usage: normalizeTurnUsage(result.usage),
|
|
1382
|
-
})),
|
|
1470
|
+
result,
|
|
1383
1471
|
};
|
|
1384
1472
|
}
|
|
1385
1473
|
|
|
@@ -1399,39 +1487,93 @@ export async function createFxAgent(options = {}) {
|
|
|
1399
1487
|
if (signal !== undefined && (typeof signal?.addEventListener !== "function" || typeof signal?.removeEventListener !== "function")) throw new TypeError("prompt signal must be an AbortSignal");
|
|
1400
1488
|
const queue = [];
|
|
1401
1489
|
const waiters = [];
|
|
1490
|
+
let queuedBytes = 0;
|
|
1491
|
+
let resumeOutput;
|
|
1492
|
+
let iteratorTaken = false;
|
|
1493
|
+
let terminalError;
|
|
1494
|
+
let reportedPressure = false;
|
|
1495
|
+
let discardedBytes = 0;
|
|
1402
1496
|
const toolControllers = new Set();
|
|
1403
1497
|
let finished = false;
|
|
1404
1498
|
let cancelled = false;
|
|
1405
1499
|
const turn = {
|
|
1406
|
-
push(update
|
|
1500
|
+
push(update, size = encoder.encode(JSON.stringify(update)).length) {
|
|
1501
|
+
if (cancelled || finished) { discardedBytes += size; return; }
|
|
1502
|
+
if (size > maxCoreMessageBytes) throw new RangeError("core output message exceeds 64 MiB");
|
|
1503
|
+
if (queue.length && (queue.length >= maxUnreadEvents || size > maxUnreadEventBytes - queuedBytes)) {
|
|
1504
|
+
const capacity = new Promise((resolveCapacity) => { resumeOutput = resolveCapacity; });
|
|
1505
|
+
if (!reportedPressure) {
|
|
1506
|
+
reportedPressure = true;
|
|
1507
|
+
emit("output.backpressure", { bufferedBytes: queuedBytes, bufferedEvents: queue.length });
|
|
1508
|
+
}
|
|
1509
|
+
return capacity.then(() => turn.push(update, size));
|
|
1510
|
+
}
|
|
1511
|
+
const waiter = waiters.shift();
|
|
1512
|
+
if (waiter) waiter.resolve({ value: update, done: false });
|
|
1513
|
+
else { queue.push({ update, size }); queuedBytes += size; }
|
|
1514
|
+
},
|
|
1407
1515
|
toolControllers,
|
|
1408
1516
|
transportAttempts: 0,
|
|
1517
|
+
get cancelled() { return cancelled; },
|
|
1409
1518
|
cancel() {
|
|
1410
1519
|
if (finished || cancelled) return;
|
|
1411
1520
|
cancelled = true;
|
|
1521
|
+
resumeOutput?.();
|
|
1522
|
+
resumeOutput = null;
|
|
1412
1523
|
send({ jsonrpc: "2.0", method: "session/cancel", params: { sessionId } });
|
|
1413
1524
|
for (const controller of toolControllers) controller.abort();
|
|
1414
1525
|
runtime.abortHostEffects();
|
|
1415
1526
|
},
|
|
1416
|
-
[Symbol.asyncIterator]() {
|
|
1527
|
+
[Symbol.asyncIterator]() {
|
|
1528
|
+
if (iteratorTaken) throw new Error("a turn has only one event consumer");
|
|
1529
|
+
iteratorTaken = true;
|
|
1530
|
+
return {
|
|
1531
|
+
next() {
|
|
1532
|
+
if (queue.length) {
|
|
1533
|
+
const { update, size } = queue.shift();
|
|
1534
|
+
queuedBytes -= size;
|
|
1535
|
+
resumeOutput?.();
|
|
1536
|
+
resumeOutput = null;
|
|
1537
|
+
return Promise.resolve({ value: update, done: false });
|
|
1538
|
+
}
|
|
1539
|
+
if (terminalError) return Promise.reject(terminalError);
|
|
1540
|
+
if (finished) return Promise.resolve({ done: true });
|
|
1541
|
+
return new Promise((resolve, reject) => waiters.push({ resolve, reject }));
|
|
1542
|
+
},
|
|
1543
|
+
return() { turn.cancel(); return Promise.resolve({ done: true }); },
|
|
1544
|
+
};
|
|
1545
|
+
},
|
|
1417
1546
|
};
|
|
1547
|
+
if (signal?.aborted) {
|
|
1548
|
+
finished = true;
|
|
1549
|
+
turn.result = Promise.resolve({ stopReason: "cancelled" });
|
|
1550
|
+
return turn;
|
|
1551
|
+
}
|
|
1418
1552
|
activeTurn = turn;
|
|
1419
1553
|
const abort = () => turn.cancel();
|
|
1420
1554
|
signal?.addEventListener("abort", abort, { once: true });
|
|
1421
1555
|
turn.result = request("session/prompt", { sessionId, prompt })
|
|
1422
|
-
.then((response) => ({ stopReason: response.stopReason, usage: response.usage }))
|
|
1556
|
+
.then((response) => ({ stopReason: cancelled ? "cancelled" : response.stopReason, usage: response.usage }))
|
|
1423
1557
|
.catch((error) => {
|
|
1424
1558
|
if (error.message === "Cancelled") return { stopReason: "cancelled" };
|
|
1559
|
+
terminalError = error;
|
|
1425
1560
|
throw error;
|
|
1426
1561
|
})
|
|
1427
1562
|
.finally(() => {
|
|
1428
1563
|
finished = true;
|
|
1564
|
+
resumeOutput?.();
|
|
1565
|
+
resumeOutput = null;
|
|
1429
1566
|
signal?.removeEventListener("abort", abort);
|
|
1430
1567
|
if (activeTurn === turn) activeTurn = null;
|
|
1431
1568
|
toolControllers.clear();
|
|
1432
|
-
|
|
1569
|
+
if (discardedBytes) emit("output.discarded", { reason: "cancelled", bytes: discardedBytes });
|
|
1570
|
+
for (const waiter of waiters.splice(0)) {
|
|
1571
|
+
if (terminalError) waiter.reject(terminalError);
|
|
1572
|
+
else waiter.resolve({ done: true });
|
|
1573
|
+
}
|
|
1433
1574
|
});
|
|
1434
1575
|
if (signal?.aborted) turn.cancel();
|
|
1576
|
+
void turn.result.catch(() => {});
|
|
1435
1577
|
return turn;
|
|
1436
1578
|
}
|
|
1437
1579
|
}
|
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
|