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/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 lineBuffer = "";
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) emitStdout(chunk);
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
- return options.fetch(text(urlPtr, urlLen), {
573
- method: text(methodPtr, methodLen),
574
- headers: headersFromJson(headersPtr, headersLen),
575
- body: bodyLen ? bytes(bodyPtr, bodyLen).slice() : undefined,
576
- }).then(async (response) => {
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
- if (output.length > outputCap) return -3;
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
- streams.forEach((state) => state.controller.abort());
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
- options.stdout = (chunk) => {
961
- lineBuffer += decoder.decode(chunk, { stream: true });
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
- () => runtime.markExited(0),
990
+ () => {
991
+ runtime.setInstance(null);
992
+ try { runtime.finishOutput(); runtime.markExited(0); }
993
+ catch (error) { runtime.abort(error); }
994
+ },
982
995
  (error) => {
983
- if (!String(error).includes("proc_exit")) console.error(error);
984
- runtime.markExited(runtime.aborted ? 130 : 1);
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 (typeof value === "string") return value;
1123
- if (value === undefined) return "null";
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?.toolControllers.add(controller);
1216
- let content;
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
- content = hostToolContent(await execute(input, { signal: controller.signal }));
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
- content = error instanceof Error ? error.message : String(error);
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
- turn?.toolControllers.delete(controller);
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
- const error = new Error(`fx-core exited with code ${code} before completing the ACP request`);
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(async (message) => {
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
- send({ jsonrpc: "2.0", id: message.id, result: { content, isError } });
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
- async *[Symbol.asyncIterator]() {
1374
- for await (const update of rawTurn) {
1375
- const event = eventFor(update);
1376
- if (event) yield event;
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: rawTurn.result.then((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) { const waiter = waiters.shift(); if (waiter) waiter({ value: update, done: false }); else queue.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]() { return { next() { if (queue.length) return Promise.resolve({ value: queue.shift(), done: false }); if (finished) return Promise.resolve({ done: true }); return new Promise((resolve) => waiters.push(resolve)); } }; },
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
- waiters.splice(0).forEach((resolve) => resolve({ done: true }));
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
Binary file
Binary file
Binary file
Binary file