libfx 0.0.7 → 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,10 +1,153 @@
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 });
4
7
  const workspaceInfoLimit = 4 * 1024;
5
8
  const workspaceCommandLimit = 64 * 1024;
6
9
  const workspaceOutputLimit = 64 * 1024;
10
+ const maxInstructionsBytes = 64 * 1024;
11
+ const maxApiKeyBytes = 64 * 1024;
12
+ const maxModelBytes = 1024;
13
+ const maxUrlBytes = 16 * 1024;
14
+ const maxModelCatalogBytes = 4 * 1024 * 1024;
15
+ const maxModelCatalogEntries = 10_000;
7
16
  const streamReadsPerTaskYield = 32;
17
+ const maxUnreadEventBytes = 1024 * 1024;
18
+ const maxUnreadEvents = 256;
19
+
20
+ function boundedString(value, name, maxBytes, required) {
21
+ if (value === undefined && !required) return undefined;
22
+ if (typeof value !== "string" || value.length === 0) {
23
+ throw new TypeError(`${name} ${required ? "is required and " : ""}must be a non-empty string`);
24
+ }
25
+ if (encoder.encode(value).length > maxBytes) {
26
+ throw new RangeError(`${name} exceeds the ${maxBytes} byte libfx limit`);
27
+ }
28
+ return value;
29
+ }
30
+
31
+ function validateGatewayChatUrl(value) {
32
+ if (value === undefined) return;
33
+ boundedString(value, "gatewayChatUrl", maxUrlBytes, false);
34
+ let url;
35
+ try { url = new URL(value); } catch { throw new TypeError("gatewayChatUrl must be a valid URL"); }
36
+ if (url.username || url.password || url.hash) {
37
+ throw new TypeError("gatewayChatUrl must not contain credentials or a fragment");
38
+ }
39
+ if (url.href === "https://ai-gateway.vercel.sh/v3/ai/language-model") return;
40
+ const loopback = url.hostname === "127.0.0.1" || url.hostname === "[::1]" || url.hostname === "localhost";
41
+ if (url.protocol !== "http:" || !loopback || !url.port) {
42
+ throw new TypeError("gatewayChatUrl must use the canonical Gateway or explicit loopback HTTP");
43
+ }
44
+ }
45
+
46
+ function normalizeAgentOptions(value) {
47
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
48
+ throw new TypeError("createFxAgent() options must be an object");
49
+ }
50
+ const options = { ...value };
51
+ if (Object.hasOwn(options, "env")) {
52
+ throw new TypeError("createFxAgent() does not accept env; pass apiKey and model directly");
53
+ }
54
+ options.apiKey = boundedString(options.apiKey, "apiKey", maxApiKeyBytes, true);
55
+ options.model = boundedString(options.model, "model", maxModelBytes, false);
56
+ validateGatewayChatUrl(options.gatewayChatUrl);
57
+ return options;
58
+ }
59
+
60
+ function agentEnvironment(options) {
61
+ return {
62
+ AI_GATEWAY_API_KEY: options.apiKey,
63
+ ...(options.model === undefined ? {} : { FX_MODEL: options.model }),
64
+ ...(options.gatewayChatUrl === undefined ? {} : { FX_GATEWAY_CHAT_URL: options.gatewayChatUrl }),
65
+ };
66
+ }
67
+
68
+ async function cancelResponseBody(response) {
69
+ try {
70
+ await response.body?.cancel();
71
+ } catch {}
72
+ }
73
+
74
+ async function readBoundedResponseText(response, limit) {
75
+ const declared = Number(response.headers.get("content-length"));
76
+ if (Number.isFinite(declared) && declared > limit) {
77
+ await cancelResponseBody(response);
78
+ throw new RangeError(`model catalog exceeds the ${limit} byte libfx limit`);
79
+ }
80
+ if (!response.body) {
81
+ const bytes = new Uint8Array(await response.arrayBuffer());
82
+ if (bytes.length > limit) throw new RangeError(`model catalog exceeds the ${limit} byte libfx limit`);
83
+ return strictDecoder.decode(bytes);
84
+ }
85
+
86
+ const reader = response.body.getReader();
87
+ const chunks = [];
88
+ let total = 0;
89
+ for (;;) {
90
+ const { done, value } = await reader.read();
91
+ if (done) break;
92
+ if (!value?.length) continue;
93
+ total += value.length;
94
+ if (total > limit) {
95
+ try {
96
+ await reader.cancel();
97
+ } catch {}
98
+ throw new RangeError(`model catalog exceeds the ${limit} byte libfx limit`);
99
+ }
100
+ chunks.push(value);
101
+ }
102
+ const bytes = new Uint8Array(total);
103
+ let offset = 0;
104
+ for (const chunk of chunks) {
105
+ bytes.set(chunk, offset);
106
+ offset += chunk.length;
107
+ }
108
+ return strictDecoder.decode(bytes);
109
+ }
110
+
111
+ export async function listModels(options = {}) {
112
+ if (!options || typeof options !== "object" || Array.isArray(options)) {
113
+ throw new TypeError("listModels() options must be an object");
114
+ }
115
+ const apiKey = boundedString(options.apiKey, "apiKey", maxApiKeyBytes, true);
116
+ const fetchModels = options.fetch ?? globalThis.fetch?.bind(globalThis);
117
+ if (typeof fetchModels !== "function") throw new TypeError("fetch is unavailable");
118
+ const response = await fetchModels("https://ai-gateway.vercel.sh/coding-agent/v1/models", {
119
+ method: "GET",
120
+ headers: { authorization: `Bearer ${apiKey}` },
121
+ });
122
+ if (!response.ok) {
123
+ await cancelResponseBody(response);
124
+ throw new Error(`model catalog request failed with HTTP ${response.status}`);
125
+ }
126
+
127
+ let catalog;
128
+ try {
129
+ catalog = JSON.parse(await readBoundedResponseText(response, maxModelCatalogBytes));
130
+ } catch (error) {
131
+ if (error instanceof RangeError) throw error;
132
+ throw new TypeError("model catalog response is malformed");
133
+ }
134
+ if (!catalog || typeof catalog !== "object" || !Array.isArray(catalog.data)) {
135
+ throw new TypeError("model catalog response is malformed");
136
+ }
137
+ if (catalog.data.length > maxModelCatalogEntries) {
138
+ throw new RangeError(`model catalog exceeds the ${maxModelCatalogEntries} entry libfx limit`);
139
+ }
140
+
141
+ const ids = new Set();
142
+ for (const entry of catalog.data) {
143
+ if (!entry || typeof entry !== "object") continue;
144
+ if (typeof entry.type === "string" && entry.type.toLowerCase() !== "language") continue;
145
+ if (typeof entry.id !== "string" || entry.id.length === 0) continue;
146
+ if (encoder.encode(entry.id).length > maxModelBytes) continue;
147
+ ids.add(entry.id);
148
+ }
149
+ return [...ids].sort();
150
+ }
8
151
 
9
152
  function validWorkspacePath(path) {
10
153
  if (typeof path !== "string" || !path.startsWith("/") || path.includes("\0")) return false;
@@ -50,7 +193,7 @@ function utf8Prefix(value, limit) {
50
193
  return value.subarray(0, end);
51
194
  }
52
195
 
53
- export const fxSdkApiVersion = 1;
196
+ export const fxSdkApiVersion = 2;
54
197
 
55
198
  export function supportsJspi() {
56
199
  return typeof WebAssembly.Suspending === "function" &&
@@ -92,35 +235,6 @@ export function xtermAdapter(term) {
92
235
  };
93
236
  }
94
237
 
95
- function createMemorySessionStore() {
96
- const records = new Map();
97
- let nextRevision = 1;
98
- return {
99
- async load(id) {
100
- const record = records.get(id);
101
- return record ? { bytes: record.bytes.slice(), revision: record.revision } : null;
102
- },
103
- async commit(id, bytes, expectedRevision) {
104
- const current = records.get(id);
105
- if ((current?.revision) !== expectedRevision) throw revisionConflict();
106
- const revision = String(nextRevision++);
107
- records.set(id, { bytes: bytes.slice(), revision, updatedAtMs: Date.now() });
108
- return { revision };
109
- },
110
- async list() {
111
- return [...records.entries()].map(([id, record]) => ({ id, updatedAtMs: record.updatedAtMs }))
112
- .sort((a, b) => b.updatedAtMs - a.updatedAtMs);
113
- },
114
- async remove(id) { records.delete(id); },
115
- };
116
- }
117
-
118
- function revisionConflict() {
119
- const error = new Error("session revision conflict");
120
- error.code = "FX_SESSION_REVISION_CONFLICT";
121
- return error;
122
- }
123
-
124
238
  class ByteQueue {
125
239
  chunks = [];
126
240
  waiters = [];
@@ -176,25 +290,6 @@ class ByteQueue {
176
290
  }
177
291
  }
178
292
 
179
- async function loadModule(input) {
180
- if (input instanceof WebAssembly.Module) return input;
181
- if (typeof input === "string") input = fetch(input);
182
- if (input instanceof Promise) input = await input;
183
- if (input instanceof WebAssembly.Module) return input;
184
- if (input instanceof Response) {
185
- const contentType = input.headers.get("content-type")?.split(";", 1)[0].trim().toLowerCase();
186
- if (contentType === "application/wasm" && typeof WebAssembly.compileStreaming === "function") {
187
- return WebAssembly.compileStreaming(input);
188
- }
189
- const bytes = await input.arrayBuffer();
190
- return WebAssembly.compile(bytes);
191
- }
192
- if (input instanceof ArrayBuffer || ArrayBuffer.isView(input)) {
193
- return WebAssembly.compile(input);
194
- }
195
- throw new TypeError("wasm must be a URL, Response, ArrayBuffer, typed array, or WebAssembly.Module");
196
- }
197
-
198
293
  function raceWithTimeout(promise, timeoutMs, timeoutValue) {
199
294
  let timer;
200
295
  return new Promise((resolve, reject) => {
@@ -214,8 +309,11 @@ function yieldToHostTask() {
214
309
  }
215
310
 
216
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");
217
314
  const stdin = new ByteQueue();
218
315
  const streams = new Map();
316
+ const httpRequests = new Set();
219
317
  const workspaceExecs = new Set();
220
318
  const workspace = prepareWorkspaceAdapter(options.workspace);
221
319
  const args = ["fx", ...(options.args || [])];
@@ -225,7 +323,8 @@ function createRuntime(options) {
225
323
  let exitedResolve;
226
324
  let exitCode = null;
227
325
  let aborted = false;
228
- let lineBuffer = "";
326
+ let coreOutput;
327
+ let outputError;
229
328
  const exited = new Promise((resolve) => { exitedResolve = resolve; });
230
329
  const markExited = (code) => {
231
330
  if (exitCode !== null) return;
@@ -255,7 +354,7 @@ function createRuntime(options) {
255
354
  }
256
355
 
257
356
  function emitStdout(chunk) {
258
- if (options.stdout) options.stdout(chunk);
357
+ if (options.stdout) return options.stdout(chunk);
259
358
  }
260
359
 
261
360
  function fdWrite(fd, iovs, count, nwritten) {
@@ -265,6 +364,7 @@ function createRuntime(options) {
265
364
  for (let index = 0; index < count; index++) {
266
365
  total += view.getUint32(iovs + index * 8 + 4, true);
267
366
  }
367
+ if (coreOutput && total > maxCoreMessageBytes) throw new RangeError("core output message exceeds 64 MiB");
268
368
  if (fd === 1 || fd === 2) {
269
369
  const chunk = new Uint8Array(total);
270
370
  let offset = 0;
@@ -274,7 +374,13 @@ function createRuntime(options) {
274
374
  chunk.set(bytes(ptr, len), offset);
275
375
  offset += len;
276
376
  }
277
- 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
+ }
278
384
  else if (typeof options.stderr === "function") options.stderr(chunk);
279
385
  else console.warn(decoder.decode(chunk));
280
386
  }
@@ -442,16 +548,56 @@ function createRuntime(options) {
442
548
  }
443
549
 
444
550
  function httpRequest(methodPtr, methodLen, urlPtr, urlLen, headersPtr, headersLen, bodyPtr, bodyLen, statusOut, responsePtr, responseCap) {
445
- return options.fetch(text(urlPtr, urlLen), {
446
- method: text(methodPtr, methodLen),
447
- headers: headersFromJson(headersPtr, headersLen),
448
- body: bodyLen ? bytes(bodyPtr, bodyLen).slice() : undefined,
449
- }).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
+ }
450
569
  const body = new Uint8Array(await response.arrayBuffer());
570
+ if (controller.signal.aborted) return -1;
451
571
  new DataView(memory().buffer).setUint16(statusOut, response.status, true);
452
572
  if (body.length > responseCap) return -2;
453
573
  bytes(responsePtr, body.length).set(body);
454
574
  return body.length;
575
+ })().catch(() => -1);
576
+ return Promise.race([request, cancelled]).finally(() => {
577
+ controller.signal.removeEventListener("abort", onAbort);
578
+ httpRequests.delete(controller);
579
+ });
580
+ }
581
+
582
+ let pendingHostToolResult = null;
583
+ function hostToolCall(namePtr, nameLen, argumentsPtr, argumentsLen, outputPtr, outputCap, statusPtr) {
584
+ pendingHostToolResult = null;
585
+ if (typeof options.hostToolExecutor !== "function") return -1;
586
+ if (options.traceWasi) console.error("fx host tool call start");
587
+ let input;
588
+ try { input = JSON.parse(text(argumentsPtr, argumentsLen)); } catch { return -1; }
589
+ return Promise.resolve(options.hostToolExecutor(text(namePtr, nameLen), input)).then((result) => {
590
+ if (options.traceWasi) console.error("fx host tool call settled", result.cancelled, result.isError);
591
+ if (result.cancelled) return -2;
592
+ const output = encoder.encode(result.content);
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
+ }
599
+ bytes(outputPtr, output.length).set(output);
600
+ return output.length;
455
601
  }).catch(() => -1);
456
602
  }
457
603
 
@@ -714,7 +860,9 @@ function createRuntime(options) {
714
860
  }
715
861
 
716
862
  function abortHostEffects() {
717
- streams.forEach((state) => state.controller.abort());
863
+ pendingHostToolResult = null;
864
+ streams.forEach((state) => state.controller.abort(abortReason));
865
+ httpRequests.forEach((controller) => controller.abort(abortReason));
718
866
  workspaceExecs.forEach((state) => state.abort(-3));
719
867
  }
720
868
 
@@ -724,7 +872,7 @@ function createRuntime(options) {
724
872
  args_get(ptrs, data) { if (options.traceWasi) console.error("wasi args_get"); writeVector(args, ptrs, data); return 0; },
725
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; },
726
874
  environ_get(ptrs, data) { if (options.traceWasi) console.error("wasi environ_get"); writeVector(env, ptrs, data); return 0; },
727
- fd_write: fdWrite,
875
+ fd_write: options.args?.[0] === "acp" ? new WebAssembly.Suspending(fdWrite) : fdWrite,
728
876
  fd_read: new WebAssembly.Suspending(fdRead),
729
877
  fd_close() { return 0; },
730
878
  fd_fdstat_get(fd, out) {
@@ -772,8 +920,16 @@ function createRuntime(options) {
772
920
  fx_http_stream_open: streamOpen,
773
921
  fx_http_stream_status: new WebAssembly.Suspending(streamStatus),
774
922
  fx_http_stream_next: new WebAssembly.Suspending(streamNext),
775
- 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); },
776
924
  fx_http_request: new WebAssembly.Suspending(httpRequest),
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; },
777
933
  fx_open_url: new WebAssembly.Suspending(openUrl),
778
934
  fx_oauth_session_load: new WebAssembly.Suspending(oauthSessionLoad),
779
935
  fx_oauth_session_commit: new WebAssembly.Suspending(oauthSessionCommit),
@@ -803,8 +959,10 @@ function createRuntime(options) {
803
959
  wake() { stdin.wake(); },
804
960
  closeStdin() { stdin.close(); },
805
961
  abortHostEffects,
806
- abort() {
962
+ abort(error) {
807
963
  aborted = true;
964
+ outputError = error;
965
+ coreOutput?.close();
808
966
  abortHostEffects();
809
967
  stdin.close();
810
968
  markExited(130);
@@ -812,17 +970,12 @@ function createRuntime(options) {
812
970
  markExited,
813
971
  get aborted() { return aborted; },
814
972
  get exitCode() { return exitCode; },
973
+ get error() { return outputError; },
815
974
  setLineHandler(handler) {
816
- options.stdout = (chunk) => {
817
- lineBuffer += decoder.decode(chunk, { stream: true });
818
- for (;;) {
819
- const newline = lineBuffer.indexOf("\n");
820
- if (newline < 0) break;
821
- const line = lineBuffer.slice(0, newline); lineBuffer = lineBuffer.slice(newline + 1);
822
- if (line) handler(JSON.parse(line));
823
- }
824
- };
975
+ coreOutput = new CoreOutput(handler);
976
+ options.stdout = (chunk) => coreOutput.write(chunk);
825
977
  },
978
+ finishOutput() { coreOutput?.finish(); },
826
979
  };
827
980
  }
828
981
 
@@ -834,10 +987,18 @@ async function instantiate(options) {
834
987
  runtime.setInstance(instance);
835
988
  const start = WebAssembly.promising(instance.exports._start);
836
989
  start().then(
837
- () => runtime.markExited(0),
990
+ () => {
991
+ runtime.setInstance(null);
992
+ try { runtime.finishOutput(); runtime.markExited(0); }
993
+ catch (error) { runtime.abort(error); }
994
+ },
838
995
  (error) => {
839
- if (!String(error).includes("proc_exit")) console.error(error);
840
- 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
+ }
841
1002
  },
842
1003
  );
843
1004
  return runtime;
@@ -930,20 +1091,214 @@ function normalizePromptInput(input) {
930
1091
  });
931
1092
  }
932
1093
 
933
- export async function createFxAgent(options) {
934
- options = { ...options, sessionStore: options.sessionStore || createMemorySessionStore() };
1094
+ function normalizeHostTools(value) {
1095
+ if (value === undefined) return { descriptors: [], executors: new Map() };
1096
+ if (!Array.isArray(value)) throw new TypeError("tools must be an array");
1097
+ if (value.length > 64) throw new RangeError("tools cannot contain more than 64 entries");
1098
+ const descriptors = [];
1099
+ const executors = new Map();
1100
+ for (const [index, tool] of value.entries()) {
1101
+ if (!tool || typeof tool !== "object") throw new TypeError(`tool ${index} must be an object`);
1102
+ const { name, description, inputSchema, execute } = tool;
1103
+ if (typeof name !== "string" || !/^[A-Za-z0-9_-]{1,64}$/.test(name)) {
1104
+ throw new TypeError(`tool ${index} has an invalid name`);
1105
+ }
1106
+ if (executors.has(name)) throw new TypeError(`duplicate tool name: ${name}`);
1107
+ if (typeof description !== "string") throw new TypeError(`tool ${name} requires a description`);
1108
+ if (typeof execute !== "function") throw new TypeError(`tool ${name} requires execute()`);
1109
+ if (!inputSchema || typeof inputSchema !== "object" || Array.isArray(inputSchema)) {
1110
+ throw new TypeError(`tool ${name} requires an object inputSchema`);
1111
+ }
1112
+ let schema;
1113
+ try { schema = JSON.parse(JSON.stringify(inputSchema)); } catch {
1114
+ throw new TypeError(`tool ${name} inputSchema must be JSON-serializable`);
1115
+ }
1116
+ descriptors.push({ name, description, inputSchema: schema });
1117
+ executors.set(name, execute);
1118
+ }
1119
+ return { descriptors, executors };
1120
+ }
1121
+
1122
+ function normalizeInstructions(value) {
1123
+ let instructions;
1124
+ if (value === undefined) instructions = "";
1125
+ else if (typeof value === "string") instructions = value;
1126
+ if (Array.isArray(value) && value.every((entry) => typeof entry === "string")) {
1127
+ instructions = value.filter(Boolean).join("\n\n");
1128
+ }
1129
+ if (instructions === undefined) {
1130
+ throw new TypeError("instructions must be a string or an array of strings");
1131
+ }
1132
+ if (encoder.encode(instructions).length > maxInstructionsBytes) {
1133
+ throw new RangeError(`instructions exceed the ${maxInstructionsBytes} byte libfx limit`);
1134
+ }
1135
+ return instructions;
1136
+ }
1137
+
1138
+ function hostToolContent(value) {
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 };
1158
+ const encoded = JSON.stringify(value);
1159
+ return { content: encoded === undefined ? "null" : encoded, rich: false };
1160
+ }
1161
+
1162
+ function checkpointBytes(value) {
1163
+ if (value === undefined) return null;
1164
+ if (value instanceof Uint8Array) return value.slice();
1165
+ if (value instanceof ArrayBuffer) return new Uint8Array(value.slice(0));
1166
+ if (ArrayBuffer.isView(value)) {
1167
+ return new Uint8Array(value.buffer.slice(value.byteOffset, value.byteOffset + value.byteLength));
1168
+ }
1169
+ throw new TypeError("checkpoint must be an ArrayBuffer or typed array");
1170
+ }
1171
+
1172
+ function bytesToBase64(value) {
1173
+ let binary = "";
1174
+ for (let offset = 0; offset < value.length; offset += 0x8000) {
1175
+ binary += String.fromCharCode(...value.subarray(offset, offset + 0x8000));
1176
+ }
1177
+ return btoa(binary);
1178
+ }
1179
+
1180
+ function base64ToBytes(value) {
1181
+ const binary = atob(value);
1182
+ const bytes = new Uint8Array(binary.length);
1183
+ for (let index = 0; index < binary.length; index++) bytes[index] = binary.charCodeAt(index);
1184
+ return bytes;
1185
+ }
1186
+
1187
+ export async function createFxAgent(options = {}) {
1188
+ options = normalizeAgentOptions(options);
1189
+ const hostTools = normalizeHostTools(options.tools);
1190
+ const instructions = normalizeInstructions(options.instructions);
1191
+ const initialCheckpoint = checkpointBytes(options.checkpoint);
935
1192
  const pending = new Map();
936
- const turns = new Map();
937
1193
  let nextId = 1;
938
- let activeSession = null;
939
- let loadingSessionId = null;
940
- let loadingUpdates = [];
1194
+ let sessionId = null;
1195
+ let activeTurn = null;
941
1196
  let closing = false;
1197
+ const isCurrentTurn = (turn) => turn && activeTurn === turn && !turn.cancelled && !closing;
942
1198
  const emit = (type, detail = {}) => {
943
1199
  try { options.onEvent?.({ type, timestamp: performance.now(), ...detail }); } catch {}
944
1200
  };
1201
+ const hostFetch = options.fetch ?? globalThis.fetch?.bind(globalThis);
1202
+ const transportFetch = async (input, init = {}) => {
1203
+ const method = String(init.method ?? input?.method ?? "GET").toUpperCase();
1204
+ let endpoint = String(input?.url ?? input);
1205
+ try {
1206
+ const url = new URL(endpoint);
1207
+ endpoint = `${url.origin}${url.pathname}`;
1208
+ } catch {}
1209
+ for (let attemptIndex = 0; attemptIndex < 2; attemptIndex++) {
1210
+ const startedAt = performance.now();
1211
+ const attempt = activeTurn ? ++activeTurn.transportAttempts : attemptIndex + 1;
1212
+ emit("transport.start", { attempt, method, endpoint, model: options.model });
1213
+ try {
1214
+ if (activeTurn?.cancelled) {
1215
+ runtime.abortHostEffects();
1216
+ throw new DOMException("Aborted", "AbortError");
1217
+ }
1218
+ if (!hostFetch) throw new TypeError("fetch is unavailable");
1219
+ const response = await hostFetch(input, init);
1220
+ const headers = response.headers;
1221
+ emit("transport.response", {
1222
+ attempt,
1223
+ status: response.status,
1224
+ elapsedMs: performance.now() - startedAt,
1225
+ requestId: headers.get("x-vercel-id"),
1226
+ generationId: headers.get("x-generation-id"),
1227
+ model: headers.get("x-model-id") ?? options.model,
1228
+ provider: headers.get("x-vercel-ai-gateway-provider") ?? headers.get("x-ai-gateway-provider"),
1229
+ });
1230
+ return response;
1231
+ } catch (error) {
1232
+ const errorName = error instanceof Error ? error.name : "Error";
1233
+ const elapsedMs = performance.now() - startedAt;
1234
+ emit("transport.error", { attempt, elapsedMs, error: errorName });
1235
+ if (init.signal?.aborted) throw new DOMException("Aborted", "AbortError");
1236
+ if (attemptIndex === 1) throw error;
1237
+ emit("transport.retry", {
1238
+ attempt,
1239
+ nextAttempt: attempt + 1,
1240
+ elapsedMs,
1241
+ error: errorName,
1242
+ });
1243
+ if (init.signal?.aborted) throw new DOMException("Aborted", "AbortError");
1244
+ }
1245
+ }
1246
+ throw new Error("transport retry exhausted");
1247
+ };
1248
+ const executeHostTool = async (name, input, requestedSessionId) => {
1249
+ const execute = hostTools.executors.get(name);
1250
+ const turn = requestedSessionId === undefined || requestedSessionId === sessionId
1251
+ ? activeTurn
1252
+ : null;
1253
+ if (!isCurrentTurn(turn)) return { content: "", isError: true, cancelled: true };
1254
+ const controller = new AbortController();
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;
1261
+ let isError = false;
1262
+ try {
1263
+ if (!execute) throw new Error(`unknown host tool: ${String(name)}`);
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
+ }
1275
+ } catch (error) {
1276
+ isError = true;
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
+ }
1288
+ } finally {
1289
+ controller.signal.removeEventListener("abort", onAbort);
1290
+ turn.toolControllers.delete(controller);
1291
+ }
1292
+ return { content, isError, rich, cancelled: controller.signal.aborted || !isCurrentTurn(turn) };
1293
+ };
945
1294
  emit("runtime.start");
946
- const runtimeOptions = { ...options, args: ["acp"] };
1295
+ const runtimeOptions = {
1296
+ ...options,
1297
+ fetch: transportFetch,
1298
+ args: ["acp"],
1299
+ env: agentEnvironment(options),
1300
+ hostToolExecutor: executeHostTool,
1301
+ };
947
1302
  const runtime = options.runtimeFactory
948
1303
  ? await options.runtimeFactory(runtimeOptions)
949
1304
  : await instantiate(runtimeOptions);
@@ -960,170 +1315,265 @@ export async function createFxAgent(options) {
960
1315
  });
961
1316
  runtime.exited.then((code) => {
962
1317
  emit("runtime.exit", { code });
963
- 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`);
964
1320
  for (const waiter of pending.values()) waiter.reject(error);
965
1321
  pending.clear();
966
1322
  });
967
- runtime.setLineHandler(async (message) => {
1323
+ runtime.setLineHandler((message, size) => {
968
1324
  emit("acp.receive", { message });
969
1325
  if (message.method === "session/update") {
970
- const turn = turns.get(message.params.sessionId);
971
- if (turn) turn.push(message.params.update);
972
- else if (loadingSessionId === message.params.sessionId) loadingUpdates.push(message.params.update);
1326
+ if (message.params.sessionId === sessionId) return activeTurn?.push(message.params.update, size);
973
1327
  return;
974
1328
  }
1329
+ void handleControlMessage(message).catch((error) => runtime.abort(error));
1330
+ });
1331
+ async function handleControlMessage(message) {
975
1332
  if (message.method === "session/request_permission") {
1333
+ const turn = activeTurn;
1334
+ if (!isCurrentTurn(turn)) return;
976
1335
  emit("permission.request", { request: message.params });
1336
+ if (!isCurrentTurn(turn)) return;
977
1337
  let optionId = null;
978
1338
  try { optionId = await options.onPermission?.(message.params); } catch {}
1339
+ if (!isCurrentTurn(turn)) return;
979
1340
  emit("permission.resolve", { optionId });
1341
+ if (!isCurrentTurn(turn)) return;
980
1342
  send({ jsonrpc: "2.0", id: message.id, result: optionId ? { outcome: { outcome: "selected", optionId } } : { outcome: { outcome: "cancelled" } } });
981
1343
  return;
982
1344
  }
1345
+ if (message.method === "libfx/tool_call") {
1346
+ const { content, isError, rich, cancelled } = await executeHostTool(
1347
+ message.params?.name,
1348
+ message.params?.input,
1349
+ message.params?.sessionId,
1350
+ );
1351
+ if (cancelled || closing) return;
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);
1357
+ return;
1358
+ }
983
1359
  const waiter = pending.get(message.id); if (!waiter) return; pending.delete(message.id);
984
1360
  if (message.error) waiter.reject(new Error(message.error.message)); else waiter.resolve(message.result);
985
- });
986
- await request("initialize", { protocolVersion: 1, clientCapabilities: {} });
1361
+ }
1362
+ try {
1363
+ await request("initialize", {
1364
+ protocolVersion: 1,
1365
+ clientCapabilities: {
1366
+ ...(hostTools.descriptors.length || instructions
1367
+ ? { libfx: { tools: hostTools.descriptors, instructions } }
1368
+ : {}),
1369
+ },
1370
+ });
1371
+
1372
+ const sessionResult = await request("libfx/new");
1373
+ sessionId = sessionResult.sessionId;
1374
+ if (initialCheckpoint) {
1375
+ await request("libfx/restore", {
1376
+ sessionId,
1377
+ checkpoint: bytesToBase64(initialCheckpoint),
1378
+ });
1379
+ }
1380
+ } catch (error) {
1381
+ closing = true;
1382
+ try { runtime.abortHostEffects(); } catch {}
1383
+ try { runtime.closeStdin(); } catch {}
1384
+ try { await runtime.exited; } catch {}
1385
+ throw error;
1386
+ }
987
1387
 
988
1388
  const agent = {
989
- exited: runtime.exited,
990
- abort() { closing = true; runtime.abort(); },
1389
+ prompt(input, promptOptions = {}) {
1390
+ if (closing) throw new Error("fx agent is closed");
1391
+ if (activeTurn) throw new Error("a prompt is already in progress for this session");
1392
+ return normalizeTurn(startTurn(input, promptOptions));
1393
+ },
1394
+ async checkpoint() {
1395
+ if (closing) throw new Error("fx agent is closed");
1396
+ if (activeTurn) throw new Error("cannot checkpoint while a prompt is active");
1397
+ const response = await request("libfx/checkpoint", { sessionId });
1398
+ if (typeof response?.checkpoint !== "string") throw new Error("fx returned an invalid checkpoint");
1399
+ return base64ToBytes(response.checkpoint);
1400
+ },
991
1401
  async close() {
992
- if (closing) return runtime.exited;
993
- if (activeSession) await activeSession.close();
1402
+ if (closing) { await runtime.exited; return; }
1403
+ const turn = activeTurn;
1404
+ turn?.cancel();
1405
+ if (turn) await turn.result.catch(() => {});
994
1406
  closing = true;
995
1407
  runtime.closeStdin();
996
- return runtime.exited;
997
- },
998
- async createSession() {
999
- if (activeSession) await activeSession.close();
1000
- const result = await request("session/new");
1001
- activeSession = await makeSession(result);
1002
- return activeSession;
1003
- },
1004
- async listSessions() {
1005
- return (await request("session/list")).sessions || [];
1006
- },
1007
- async openSession(id) {
1008
- if (activeSession) await activeSession.close();
1009
- loadingSessionId = id;
1010
- loadingUpdates = [];
1011
- try {
1012
- const result = await request("session/load", { sessionId: id });
1013
- activeSession = await makeSession({ sessionId: id, history: loadingUpdates, ...result });
1014
- return activeSession;
1015
- } finally {
1016
- loadingSessionId = null;
1017
- loadingUpdates = [];
1018
- }
1408
+ await runtime.exited;
1019
1409
  },
1020
1410
  };
1021
1411
  return agent;
1022
1412
 
1023
- async function makeSession(result) {
1024
- let configOptions = result.configOptions || [];
1025
- let closed = false;
1026
- let activeTurn = null;
1027
- const assertOpen = () => {
1028
- if (closed) throw new Error("fx session is closed");
1029
- if (activeSession !== session) throw new Error("fx session is no longer active");
1413
+ function normalizeTurn(rawTurn) {
1414
+ const toolNames = new Map();
1415
+ const started = new Set();
1416
+ const eventFor = (update) => {
1417
+ if (update.sessionUpdate === "agent_message_chunk") {
1418
+ const delta = update.content?.text;
1419
+ if (!delta || delta.startsWith("[context]")) return null;
1420
+ return { type: "text_delta", delta };
1421
+ }
1422
+ if (update.sessionUpdate === "agent_thought_chunk") {
1423
+ const delta = update.content?.text;
1424
+ return delta ? { type: "reasoning_delta", delta } : null;
1425
+ }
1426
+ if (update.sessionUpdate === "tool_call") {
1427
+ toolNames.set(update.toolCallId, update.name || update.toolName || update.title || "tool");
1428
+ if (started.has(update.toolCallId)) return null;
1429
+ started.add(update.toolCallId);
1430
+ return {
1431
+ type: "tool_start",
1432
+ id: update.toolCallId,
1433
+ name: toolNames.get(update.toolCallId),
1434
+ };
1435
+ }
1436
+ if (update.sessionUpdate === "tool_call_update" &&
1437
+ (update.status === "completed" || update.status === "failed")) {
1438
+ const content = update.content?.find((entry) => entry.content?.type === "text")?.content?.text;
1439
+ return {
1440
+ type: "tool_end",
1441
+ id: update.toolCallId,
1442
+ name: toolNames.get(update.toolCallId) || "tool",
1443
+ ...(content === undefined ? {} : { content }),
1444
+ isError: update.status === "failed",
1445
+ };
1446
+ }
1447
+ return null;
1030
1448
  };
1031
- const updateConfig = (response) => {
1032
- configOptions = response.configOptions || configOptions;
1033
- return configOptions;
1449
+ const result = rawTurn.result.then((value) => ({
1450
+ stopReason: value.stopReason,
1451
+ usage: normalizeTurnUsage(value.usage),
1452
+ }));
1453
+ void result.catch(() => {});
1454
+ return {
1455
+ cancel() { rawTurn.cancel(); },
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
+ };
1469
+ },
1470
+ result,
1034
1471
  };
1035
- const session = {
1036
- id: result.sessionId,
1037
- modes: result.modes,
1038
- history: result.history || [],
1039
- get configOptions() { return configOptions; },
1040
- async setConfigOption(configId, value, source = "sdk") {
1041
- assertOpen();
1042
- const previousValue = configOptions.find((option) => option.id === configId)?.currentValue;
1043
- const updated = updateConfig(await request("session/set_config_option", { sessionId: result.sessionId, configId, value }));
1044
- const accepted = updated.find((option) => option.id === configId)?.currentValue;
1045
- if (configId === "mode" && accepted) this.modes.currentModeId = accepted;
1046
- if (accepted === value) {
1047
- if (options.configStore?.set) {
1048
- try { await options.configStore.set(configId, value); } catch (error) { emit("config.persist_error", { configId, error }); }
1472
+ }
1473
+
1474
+ function normalizeTurnUsage(usage) {
1475
+ const result = {};
1476
+ if (Number.isSafeInteger(usage?.inputTokens)) result.inputTokens = usage.inputTokens;
1477
+ if (Number.isSafeInteger(usage?.outputTokens)) result.outputTokens = usage.outputTokens;
1478
+ if (Number.isSafeInteger(usage?.cacheReadTokens)) result.cacheReadTokens = usage.cacheReadTokens;
1479
+ if (Number.isSafeInteger(usage?.cacheWriteTokens)) result.cacheWriteTokens = usage.cacheWriteTokens;
1480
+ if (Number.isSafeInteger(usage?.reasoningTokens)) result.reasoningTokens = usage.reasoningTokens;
1481
+ return result;
1482
+ }
1483
+
1484
+ function startTurn(input, promptOptions) {
1485
+ const prompt = normalizePromptInput(input);
1486
+ const signal = promptOptions.signal;
1487
+ if (signal !== undefined && (typeof signal?.addEventListener !== "function" || typeof signal?.removeEventListener !== "function")) throw new TypeError("prompt signal must be an AbortSignal");
1488
+ const queue = [];
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;
1496
+ const toolControllers = new Set();
1497
+ let finished = false;
1498
+ let cancelled = false;
1499
+ const turn = {
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 });
1049
1508
  }
1050
- emit("config.changed", { configId, previousValue, value: accepted, source });
1509
+ return capacity.then(() => turn.push(update, size));
1051
1510
  }
1052
- return updated;
1053
- },
1054
- setModel(value) { return this.setConfigOption("model", value); },
1055
- setMode(value) { return this.setConfigOption("mode", value); },
1056
- async setConfig(config) {
1057
- for (const [key, value] of Object.entries(config)) await this.setConfigOption(key, value);
1058
- return configOptions;
1511
+ const waiter = waiters.shift();
1512
+ if (waiter) waiter.resolve({ value: update, done: false });
1513
+ else { queue.push({ update, size }); queuedBytes += size; }
1059
1514
  },
1060
- async close() {
1061
- if (closed) return;
1062
- activeTurn?.cancel();
1063
- if (activeTurn) await activeTurn.result.catch(() => {});
1064
- closed = true;
1065
- activeTurn = null;
1066
- if (activeSession === session) activeSession = null;
1515
+ toolControllers,
1516
+ transportAttempts: 0,
1517
+ get cancelled() { return cancelled; },
1518
+ cancel() {
1519
+ if (finished || cancelled) return;
1520
+ cancelled = true;
1521
+ resumeOutput?.();
1522
+ resumeOutput = null;
1523
+ send({ jsonrpc: "2.0", method: "session/cancel", params: { sessionId } });
1524
+ for (const controller of toolControllers) controller.abort();
1525
+ runtime.abortHostEffects();
1067
1526
  },
1068
- async remove() {
1069
- if (activeTurn) throw new Error("cannot remove a session while a prompt is active");
1070
- await request("session/remove", { sessionId: result.sessionId });
1071
- closed = true;
1072
- if (activeSession === session) activeSession = null;
1073
- },
1074
- prompt(input, promptOptions = {}) {
1075
- assertOpen();
1076
- if (activeTurn) throw new Error("a prompt is already in progress for this session");
1077
- const prompt = normalizePromptInput(input);
1078
- const signal = promptOptions.signal;
1079
- if (signal !== undefined && (typeof signal?.addEventListener !== "function" || typeof signal?.removeEventListener !== "function")) throw new TypeError("prompt signal must be an AbortSignal");
1080
- const queue = [];
1081
- const waiters = [];
1082
- let finished = false;
1083
- let cancelled = false;
1084
- const turn = {
1085
- push(update) { const waiter = waiters.shift(); if (waiter) waiter({ value: update, done: false }); else queue.push(update); },
1086
- cancel() {
1087
- if (finished || cancelled) return;
1088
- cancelled = true;
1089
- send({ jsonrpc: "2.0", method: "session/cancel", params: { sessionId: result.sessionId } });
1090
- runtime.abortHostEffects();
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 }));
1091
1542
  },
1092
- [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)); } }; },
1543
+ return() { turn.cancel(); return Promise.resolve({ done: true }); },
1093
1544
  };
1094
- turns.set(result.sessionId, turn);
1095
- activeTurn = turn;
1096
- const abort = () => turn.cancel();
1097
- signal?.addEventListener("abort", abort, { once: true });
1098
- turn.result = request("session/prompt", { sessionId: result.sessionId, prompt })
1099
- .then((response) => ({ stopReason: response.stopReason }))
1100
- .catch((error) => {
1101
- if (error.message === "Cancelled") return { stopReason: "cancelled" };
1102
- throw error;
1103
- })
1104
- .finally(() => {
1105
- finished = true;
1106
- signal?.removeEventListener("abort", abort);
1107
- turns.delete(result.sessionId);
1108
- if (activeTurn === turn) activeTurn = null;
1109
- waiters.splice(0).forEach((resolve) => resolve({ done: true }));
1110
- });
1111
- turn.stopReason = turn.result.then((turnResult) => turnResult.stopReason);
1112
- void turn.stopReason.catch(() => {});
1113
- if (signal?.aborted) turn.cancel();
1114
- return turn;
1115
1545
  },
1116
1546
  };
1117
- if (options.configStore?.get) {
1118
- activeSession = session;
1119
- for (const config of [...configOptions]) {
1120
- let value;
1121
- try { value = await options.configStore.get(config.id); } catch (error) { emit("config.restore_error", { configId: config.id, error }); continue; }
1122
- if (typeof value !== "string" || value === config.currentValue) continue;
1123
- try { await session.setConfigOption(config.id, value, "restore"); } catch (error) { emit("config.restore_error", { configId: config.id, error }); }
1124
- }
1547
+ if (signal?.aborted) {
1548
+ finished = true;
1549
+ turn.result = Promise.resolve({ stopReason: "cancelled" });
1550
+ return turn;
1125
1551
  }
1126
- session.modes.currentModeId = configOptions.find((option) => option.id === "mode")?.currentValue || session.modes.currentModeId;
1127
- return session;
1552
+ activeTurn = turn;
1553
+ const abort = () => turn.cancel();
1554
+ signal?.addEventListener("abort", abort, { once: true });
1555
+ turn.result = request("session/prompt", { sessionId, prompt })
1556
+ .then((response) => ({ stopReason: cancelled ? "cancelled" : response.stopReason, usage: response.usage }))
1557
+ .catch((error) => {
1558
+ if (error.message === "Cancelled") return { stopReason: "cancelled" };
1559
+ terminalError = error;
1560
+ throw error;
1561
+ })
1562
+ .finally(() => {
1563
+ finished = true;
1564
+ resumeOutput?.();
1565
+ resumeOutput = null;
1566
+ signal?.removeEventListener("abort", abort);
1567
+ if (activeTurn === turn) activeTurn = null;
1568
+ toolControllers.clear();
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
+ }
1574
+ });
1575
+ if (signal?.aborted) turn.cancel();
1576
+ void turn.result.catch(() => {});
1577
+ return turn;
1128
1578
  }
1129
1579
  }