libfx 0.0.7 → 0.0.8-dev.857.gba60fd94fa57
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 +251 -275
- package/browser.js +2 -1
- package/core-output.js +58 -0
- package/fx-core.wasm +0 -0
- package/fx-sdk.js +675 -211
- 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 +118 -0
- package/node.cjs +2583 -0
- package/node.js +358 -87
- package/package.json +13 -4
- package/skills-node.js +29 -0
- package/skills.js +44 -0
- package/wasm-module.js +50 -0
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 =
|
|
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
|
|
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)
|
|
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
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
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
|
-
|
|
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
|
-
|
|
817
|
-
|
|
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,21 @@ async function instantiate(options) {
|
|
|
834
987
|
runtime.setInstance(instance);
|
|
835
988
|
const start = WebAssembly.promising(instance.exports._start);
|
|
836
989
|
start().then(
|
|
837
|
-
() =>
|
|
990
|
+
() => {
|
|
991
|
+
runtime.setInstance(null);
|
|
992
|
+
try { runtime.finishOutput(); runtime.markExited(0); }
|
|
993
|
+
catch (error) { runtime.abort(error); }
|
|
994
|
+
},
|
|
838
995
|
(error) => {
|
|
839
|
-
|
|
840
|
-
|
|
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")) {
|
|
1000
|
+
runtime.abortHostEffects();
|
|
1001
|
+
console.error(error);
|
|
1002
|
+
}
|
|
1003
|
+
runtime.markExited(runtime.aborted ? 130 : 1);
|
|
1004
|
+
}
|
|
841
1005
|
},
|
|
842
1006
|
);
|
|
843
1007
|
return runtime;
|
|
@@ -879,13 +1043,13 @@ export async function createFxTerminal(options) {
|
|
|
879
1043
|
if (interruptKey && data.includes(interruptKey)) runtime.abortHostEffects();
|
|
880
1044
|
runtime.write(data);
|
|
881
1045
|
};
|
|
882
|
-
const unsubscribeData = options.terminal.onData(forwardData);
|
|
883
|
-
const unsubscribeKeyData = options.terminal.onKeyData?.(forwardData) ?? (() => {});
|
|
884
1046
|
const signalResize = () => {
|
|
885
1047
|
emit("terminal.resize", { cols: options.terminal.cols, rows: options.terminal.rows });
|
|
886
1048
|
runtime.wake();
|
|
887
1049
|
};
|
|
888
|
-
|
|
1050
|
+
let unsubscribeData;
|
|
1051
|
+
let unsubscribeKeyData;
|
|
1052
|
+
let unsubscribeResize;
|
|
889
1053
|
let subscriptionsReleased = false;
|
|
890
1054
|
const releaseSubscriptions = () => {
|
|
891
1055
|
if (subscriptionsReleased) return;
|
|
@@ -898,6 +1062,17 @@ export async function createFxTerminal(options) {
|
|
|
898
1062
|
releaseSubscriptions();
|
|
899
1063
|
emit("runtime.exit", { surface: "terminal", code });
|
|
900
1064
|
});
|
|
1065
|
+
try {
|
|
1066
|
+
unsubscribeData = options.terminal.onData(forwardData);
|
|
1067
|
+
unsubscribeKeyData = options.terminal.onKeyData?.(forwardData);
|
|
1068
|
+
unsubscribeResize = options.terminal.onResize(signalResize);
|
|
1069
|
+
} catch (error) {
|
|
1070
|
+
// The rejected factory never transfers this promise to a caller.
|
|
1071
|
+
interactive.catch(() => {});
|
|
1072
|
+
releaseSubscriptions();
|
|
1073
|
+
runtime.abort();
|
|
1074
|
+
throw error;
|
|
1075
|
+
}
|
|
901
1076
|
return {
|
|
902
1077
|
interactive,
|
|
903
1078
|
exited: runtime.exited,
|
|
@@ -930,20 +1105,214 @@ function normalizePromptInput(input) {
|
|
|
930
1105
|
});
|
|
931
1106
|
}
|
|
932
1107
|
|
|
933
|
-
|
|
934
|
-
|
|
1108
|
+
function normalizeHostTools(value) {
|
|
1109
|
+
if (value === undefined) return { descriptors: [], executors: new Map() };
|
|
1110
|
+
if (!Array.isArray(value)) throw new TypeError("tools must be an array");
|
|
1111
|
+
if (value.length > 64) throw new RangeError("tools cannot contain more than 64 entries");
|
|
1112
|
+
const descriptors = [];
|
|
1113
|
+
const executors = new Map();
|
|
1114
|
+
for (const [index, tool] of value.entries()) {
|
|
1115
|
+
if (!tool || typeof tool !== "object") throw new TypeError(`tool ${index} must be an object`);
|
|
1116
|
+
const { name, description, inputSchema, execute } = tool;
|
|
1117
|
+
if (typeof name !== "string" || !/^[A-Za-z0-9_-]{1,64}$/.test(name)) {
|
|
1118
|
+
throw new TypeError(`tool ${index} has an invalid name`);
|
|
1119
|
+
}
|
|
1120
|
+
if (executors.has(name)) throw new TypeError(`duplicate tool name: ${name}`);
|
|
1121
|
+
if (typeof description !== "string") throw new TypeError(`tool ${name} requires a description`);
|
|
1122
|
+
if (typeof execute !== "function") throw new TypeError(`tool ${name} requires execute()`);
|
|
1123
|
+
if (!inputSchema || typeof inputSchema !== "object" || Array.isArray(inputSchema)) {
|
|
1124
|
+
throw new TypeError(`tool ${name} requires an object inputSchema`);
|
|
1125
|
+
}
|
|
1126
|
+
let schema;
|
|
1127
|
+
try { schema = JSON.parse(JSON.stringify(inputSchema)); } catch {
|
|
1128
|
+
throw new TypeError(`tool ${name} inputSchema must be JSON-serializable`);
|
|
1129
|
+
}
|
|
1130
|
+
descriptors.push({ name, description, inputSchema: schema });
|
|
1131
|
+
executors.set(name, execute);
|
|
1132
|
+
}
|
|
1133
|
+
return { descriptors, executors };
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
function normalizeInstructions(value) {
|
|
1137
|
+
let instructions;
|
|
1138
|
+
if (value === undefined) instructions = "";
|
|
1139
|
+
else if (typeof value === "string") instructions = value;
|
|
1140
|
+
if (Array.isArray(value) && value.every((entry) => typeof entry === "string")) {
|
|
1141
|
+
instructions = value.filter(Boolean).join("\n\n");
|
|
1142
|
+
}
|
|
1143
|
+
if (instructions === undefined) {
|
|
1144
|
+
throw new TypeError("instructions must be a string or an array of strings");
|
|
1145
|
+
}
|
|
1146
|
+
if (encoder.encode(instructions).length > maxInstructionsBytes) {
|
|
1147
|
+
throw new RangeError(`instructions exceed the ${maxInstructionsBytes} byte libfx limit`);
|
|
1148
|
+
}
|
|
1149
|
+
return instructions;
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
function hostToolContent(value) {
|
|
1153
|
+
if (value?.type === "libfx.tool-result") {
|
|
1154
|
+
if (typeof value.text !== "string" || !Array.isArray(value.images) || value.images.length > 8) {
|
|
1155
|
+
throw new TypeError("invalid typed tool result");
|
|
1156
|
+
}
|
|
1157
|
+
let imageBytes = 0;
|
|
1158
|
+
const images = value.images.map((image) => {
|
|
1159
|
+
if (image?.type !== "image" || typeof image.data !== "string" || typeof image.mimeType !== "string" || image.mimeType.length > 128 || image.data.length > 5 * 1024 * 1024) {
|
|
1160
|
+
throw new TypeError("invalid tool image");
|
|
1161
|
+
}
|
|
1162
|
+
imageBytes += image.data.length;
|
|
1163
|
+
if (imageBytes > 8 * 1024 * 1024) throw new RangeError("tool images exceed the result limit");
|
|
1164
|
+
return { type: "image", data: image.data, mimeType: image.mimeType };
|
|
1165
|
+
});
|
|
1166
|
+
const content = JSON.stringify({ text: value.text, images });
|
|
1167
|
+
if (new TextEncoder().encode(content).length > 8 * 1024 * 1024) throw new RangeError("typed tool result exceeds the result limit");
|
|
1168
|
+
return { content, rich: true, isError: value.isError === true };
|
|
1169
|
+
}
|
|
1170
|
+
if (typeof value === "string") return { content: value, rich: false };
|
|
1171
|
+
if (value === undefined) return { content: "null", rich: false };
|
|
1172
|
+
const encoded = JSON.stringify(value);
|
|
1173
|
+
return { content: encoded === undefined ? "null" : encoded, rich: false };
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
function checkpointBytes(value) {
|
|
1177
|
+
if (value === undefined) return null;
|
|
1178
|
+
if (value instanceof Uint8Array) return value.slice();
|
|
1179
|
+
if (value instanceof ArrayBuffer) return new Uint8Array(value.slice(0));
|
|
1180
|
+
if (ArrayBuffer.isView(value)) {
|
|
1181
|
+
return new Uint8Array(value.buffer.slice(value.byteOffset, value.byteOffset + value.byteLength));
|
|
1182
|
+
}
|
|
1183
|
+
throw new TypeError("checkpoint must be an ArrayBuffer or typed array");
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
function bytesToBase64(value) {
|
|
1187
|
+
let binary = "";
|
|
1188
|
+
for (let offset = 0; offset < value.length; offset += 0x8000) {
|
|
1189
|
+
binary += String.fromCharCode(...value.subarray(offset, offset + 0x8000));
|
|
1190
|
+
}
|
|
1191
|
+
return btoa(binary);
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
function base64ToBytes(value) {
|
|
1195
|
+
const binary = atob(value);
|
|
1196
|
+
const bytes = new Uint8Array(binary.length);
|
|
1197
|
+
for (let index = 0; index < binary.length; index++) bytes[index] = binary.charCodeAt(index);
|
|
1198
|
+
return bytes;
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
export async function createFxAgent(options = {}) {
|
|
1202
|
+
options = normalizeAgentOptions(options);
|
|
1203
|
+
const hostTools = normalizeHostTools(options.tools);
|
|
1204
|
+
const instructions = normalizeInstructions(options.instructions);
|
|
1205
|
+
const initialCheckpoint = checkpointBytes(options.checkpoint);
|
|
935
1206
|
const pending = new Map();
|
|
936
|
-
const turns = new Map();
|
|
937
1207
|
let nextId = 1;
|
|
938
|
-
let
|
|
939
|
-
let
|
|
940
|
-
let loadingUpdates = [];
|
|
1208
|
+
let sessionId = null;
|
|
1209
|
+
let activeTurn = null;
|
|
941
1210
|
let closing = false;
|
|
1211
|
+
const isCurrentTurn = (turn) => turn && activeTurn === turn && !turn.cancelled && !closing;
|
|
942
1212
|
const emit = (type, detail = {}) => {
|
|
943
1213
|
try { options.onEvent?.({ type, timestamp: performance.now(), ...detail }); } catch {}
|
|
944
1214
|
};
|
|
1215
|
+
const hostFetch = options.fetch ?? globalThis.fetch?.bind(globalThis);
|
|
1216
|
+
const transportFetch = async (input, init = {}) => {
|
|
1217
|
+
const method = String(init.method ?? input?.method ?? "GET").toUpperCase();
|
|
1218
|
+
let endpoint = String(input?.url ?? input);
|
|
1219
|
+
try {
|
|
1220
|
+
const url = new URL(endpoint);
|
|
1221
|
+
endpoint = `${url.origin}${url.pathname}`;
|
|
1222
|
+
} catch {}
|
|
1223
|
+
for (let attemptIndex = 0; attemptIndex < 2; attemptIndex++) {
|
|
1224
|
+
const startedAt = performance.now();
|
|
1225
|
+
const attempt = activeTurn ? ++activeTurn.transportAttempts : attemptIndex + 1;
|
|
1226
|
+
emit("transport.start", { attempt, method, endpoint, model: options.model });
|
|
1227
|
+
try {
|
|
1228
|
+
if (activeTurn?.cancelled) {
|
|
1229
|
+
runtime.abortHostEffects();
|
|
1230
|
+
throw new DOMException("Aborted", "AbortError");
|
|
1231
|
+
}
|
|
1232
|
+
if (!hostFetch) throw new TypeError("fetch is unavailable");
|
|
1233
|
+
const response = await hostFetch(input, init);
|
|
1234
|
+
const headers = response.headers;
|
|
1235
|
+
emit("transport.response", {
|
|
1236
|
+
attempt,
|
|
1237
|
+
status: response.status,
|
|
1238
|
+
elapsedMs: performance.now() - startedAt,
|
|
1239
|
+
requestId: headers.get("x-vercel-id"),
|
|
1240
|
+
generationId: headers.get("x-generation-id"),
|
|
1241
|
+
model: headers.get("x-model-id") ?? options.model,
|
|
1242
|
+
provider: headers.get("x-vercel-ai-gateway-provider") ?? headers.get("x-ai-gateway-provider"),
|
|
1243
|
+
});
|
|
1244
|
+
return response;
|
|
1245
|
+
} catch (error) {
|
|
1246
|
+
const errorName = error instanceof Error ? error.name : "Error";
|
|
1247
|
+
const elapsedMs = performance.now() - startedAt;
|
|
1248
|
+
emit("transport.error", { attempt, elapsedMs, error: errorName });
|
|
1249
|
+
if (init.signal?.aborted) throw new DOMException("Aborted", "AbortError");
|
|
1250
|
+
if (attemptIndex === 1) throw error;
|
|
1251
|
+
emit("transport.retry", {
|
|
1252
|
+
attempt,
|
|
1253
|
+
nextAttempt: attempt + 1,
|
|
1254
|
+
elapsedMs,
|
|
1255
|
+
error: errorName,
|
|
1256
|
+
});
|
|
1257
|
+
if (init.signal?.aborted) throw new DOMException("Aborted", "AbortError");
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
throw new Error("transport retry exhausted");
|
|
1261
|
+
};
|
|
1262
|
+
const executeHostTool = async (name, input, requestedSessionId) => {
|
|
1263
|
+
const execute = hostTools.executors.get(name);
|
|
1264
|
+
const turn = requestedSessionId === undefined || requestedSessionId === sessionId
|
|
1265
|
+
? activeTurn
|
|
1266
|
+
: null;
|
|
1267
|
+
if (!isCurrentTurn(turn)) return { content: "", isError: true, cancelled: true };
|
|
1268
|
+
const controller = new AbortController();
|
|
1269
|
+
turn.toolControllers.add(controller);
|
|
1270
|
+
let onAbort;
|
|
1271
|
+
const aborted = new Promise((resolve) => { onAbort = () => resolve(); });
|
|
1272
|
+
controller.signal.addEventListener("abort", onAbort, { once: true });
|
|
1273
|
+
let content = "";
|
|
1274
|
+
let rich = false;
|
|
1275
|
+
let isError = false;
|
|
1276
|
+
try {
|
|
1277
|
+
if (!execute) throw new Error(`unknown host tool: ${String(name)}`);
|
|
1278
|
+
const execution = Promise.resolve().then(() => {
|
|
1279
|
+
if (controller.signal.aborted || !isCurrentTurn(turn)) return;
|
|
1280
|
+
return execute(input, { signal: controller.signal });
|
|
1281
|
+
});
|
|
1282
|
+
const value = await Promise.race([execution, aborted]);
|
|
1283
|
+
if (!controller.signal.aborted) {
|
|
1284
|
+
const normalized = hostToolContent(value);
|
|
1285
|
+
content = normalized.content;
|
|
1286
|
+
rich = normalized.rich;
|
|
1287
|
+
isError = normalized.isError === true;
|
|
1288
|
+
}
|
|
1289
|
+
} catch (error) {
|
|
1290
|
+
isError = true;
|
|
1291
|
+
if (error?.toolResult?.type === "libfx.tool-result") {
|
|
1292
|
+
try {
|
|
1293
|
+
const normalized = hostToolContent(error.toolResult);
|
|
1294
|
+
content = normalized.content;
|
|
1295
|
+
rich = normalized.rich;
|
|
1296
|
+
} catch {
|
|
1297
|
+
content = error instanceof Error ? error.message : String(error);
|
|
1298
|
+
}
|
|
1299
|
+
} else {
|
|
1300
|
+
content = error instanceof Error ? error.message : String(error);
|
|
1301
|
+
}
|
|
1302
|
+
} finally {
|
|
1303
|
+
controller.signal.removeEventListener("abort", onAbort);
|
|
1304
|
+
turn.toolControllers.delete(controller);
|
|
1305
|
+
}
|
|
1306
|
+
return { content, isError, rich, cancelled: controller.signal.aborted || !isCurrentTurn(turn) };
|
|
1307
|
+
};
|
|
945
1308
|
emit("runtime.start");
|
|
946
|
-
const runtimeOptions = {
|
|
1309
|
+
const runtimeOptions = {
|
|
1310
|
+
...options,
|
|
1311
|
+
fetch: transportFetch,
|
|
1312
|
+
args: ["acp"],
|
|
1313
|
+
env: agentEnvironment(options),
|
|
1314
|
+
hostToolExecutor: executeHostTool,
|
|
1315
|
+
};
|
|
947
1316
|
const runtime = options.runtimeFactory
|
|
948
1317
|
? await options.runtimeFactory(runtimeOptions)
|
|
949
1318
|
: await instantiate(runtimeOptions);
|
|
@@ -960,170 +1329,265 @@ export async function createFxAgent(options) {
|
|
|
960
1329
|
});
|
|
961
1330
|
runtime.exited.then((code) => {
|
|
962
1331
|
emit("runtime.exit", { code });
|
|
963
|
-
|
|
1332
|
+
closing = true;
|
|
1333
|
+
const error = runtime.error ?? new Error(`fx-core exited with code ${code} before completing the ACP request`);
|
|
964
1334
|
for (const waiter of pending.values()) waiter.reject(error);
|
|
965
1335
|
pending.clear();
|
|
966
1336
|
});
|
|
967
|
-
runtime.setLineHandler(
|
|
1337
|
+
runtime.setLineHandler((message, size) => {
|
|
968
1338
|
emit("acp.receive", { message });
|
|
969
1339
|
if (message.method === "session/update") {
|
|
970
|
-
|
|
971
|
-
if (turn) turn.push(message.params.update);
|
|
972
|
-
else if (loadingSessionId === message.params.sessionId) loadingUpdates.push(message.params.update);
|
|
1340
|
+
if (message.params.sessionId === sessionId) return activeTurn?.push(message.params.update, size);
|
|
973
1341
|
return;
|
|
974
1342
|
}
|
|
1343
|
+
void handleControlMessage(message).catch((error) => runtime.abort(error));
|
|
1344
|
+
});
|
|
1345
|
+
async function handleControlMessage(message) {
|
|
975
1346
|
if (message.method === "session/request_permission") {
|
|
1347
|
+
const turn = activeTurn;
|
|
1348
|
+
if (!isCurrentTurn(turn)) return;
|
|
976
1349
|
emit("permission.request", { request: message.params });
|
|
1350
|
+
if (!isCurrentTurn(turn)) return;
|
|
977
1351
|
let optionId = null;
|
|
978
1352
|
try { optionId = await options.onPermission?.(message.params); } catch {}
|
|
1353
|
+
if (!isCurrentTurn(turn)) return;
|
|
979
1354
|
emit("permission.resolve", { optionId });
|
|
1355
|
+
if (!isCurrentTurn(turn)) return;
|
|
980
1356
|
send({ jsonrpc: "2.0", id: message.id, result: optionId ? { outcome: { outcome: "selected", optionId } } : { outcome: { outcome: "cancelled" } } });
|
|
981
1357
|
return;
|
|
982
1358
|
}
|
|
1359
|
+
if (message.method === "libfx/tool_call") {
|
|
1360
|
+
const { content, isError, rich, cancelled } = await executeHostTool(
|
|
1361
|
+
message.params?.name,
|
|
1362
|
+
message.params?.input,
|
|
1363
|
+
message.params?.sessionId,
|
|
1364
|
+
);
|
|
1365
|
+
if (cancelled || closing) return;
|
|
1366
|
+
const response = { jsonrpc: "2.0", id: message.id, result: { content, isError, ...(rich ? { contentType: "rich" } : {}) } };
|
|
1367
|
+
if (encoder.encode(JSON.stringify(response)).length + 1 > 8 * 1024 * 1024) {
|
|
1368
|
+
response.result = { content: "Host tool result exceeded the response frame limit", isError: true };
|
|
1369
|
+
}
|
|
1370
|
+
send(response);
|
|
1371
|
+
return;
|
|
1372
|
+
}
|
|
983
1373
|
const waiter = pending.get(message.id); if (!waiter) return; pending.delete(message.id);
|
|
984
1374
|
if (message.error) waiter.reject(new Error(message.error.message)); else waiter.resolve(message.result);
|
|
985
|
-
}
|
|
986
|
-
|
|
1375
|
+
}
|
|
1376
|
+
try {
|
|
1377
|
+
await request("initialize", {
|
|
1378
|
+
protocolVersion: 1,
|
|
1379
|
+
clientCapabilities: {
|
|
1380
|
+
...(hostTools.descriptors.length || instructions
|
|
1381
|
+
? { libfx: { tools: hostTools.descriptors, instructions } }
|
|
1382
|
+
: {}),
|
|
1383
|
+
},
|
|
1384
|
+
});
|
|
1385
|
+
|
|
1386
|
+
const sessionResult = await request("libfx/new");
|
|
1387
|
+
sessionId = sessionResult.sessionId;
|
|
1388
|
+
if (initialCheckpoint) {
|
|
1389
|
+
await request("libfx/restore", {
|
|
1390
|
+
sessionId,
|
|
1391
|
+
checkpoint: bytesToBase64(initialCheckpoint),
|
|
1392
|
+
});
|
|
1393
|
+
}
|
|
1394
|
+
} catch (error) {
|
|
1395
|
+
closing = true;
|
|
1396
|
+
try { runtime.abortHostEffects(); } catch {}
|
|
1397
|
+
try { runtime.closeStdin(); } catch {}
|
|
1398
|
+
try { await runtime.exited; } catch {}
|
|
1399
|
+
throw error;
|
|
1400
|
+
}
|
|
987
1401
|
|
|
988
1402
|
const agent = {
|
|
989
|
-
|
|
990
|
-
|
|
1403
|
+
prompt(input, promptOptions = {}) {
|
|
1404
|
+
if (closing) throw new Error("fx agent is closed");
|
|
1405
|
+
if (activeTurn) throw new Error("a prompt is already in progress for this session");
|
|
1406
|
+
return normalizeTurn(startTurn(input, promptOptions));
|
|
1407
|
+
},
|
|
1408
|
+
async checkpoint() {
|
|
1409
|
+
if (closing) throw new Error("fx agent is closed");
|
|
1410
|
+
if (activeTurn) throw new Error("cannot checkpoint while a prompt is active");
|
|
1411
|
+
const response = await request("libfx/checkpoint", { sessionId });
|
|
1412
|
+
if (typeof response?.checkpoint !== "string") throw new Error("fx returned an invalid checkpoint");
|
|
1413
|
+
return base64ToBytes(response.checkpoint);
|
|
1414
|
+
},
|
|
991
1415
|
async close() {
|
|
992
|
-
if (closing)
|
|
993
|
-
|
|
1416
|
+
if (closing) { await runtime.exited; return; }
|
|
1417
|
+
const turn = activeTurn;
|
|
1418
|
+
turn?.cancel();
|
|
1419
|
+
if (turn) await turn.result.catch(() => {});
|
|
994
1420
|
closing = true;
|
|
995
1421
|
runtime.closeStdin();
|
|
996
|
-
|
|
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
|
-
}
|
|
1422
|
+
await runtime.exited;
|
|
1019
1423
|
},
|
|
1020
1424
|
};
|
|
1021
1425
|
return agent;
|
|
1022
1426
|
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1427
|
+
function normalizeTurn(rawTurn) {
|
|
1428
|
+
const toolNames = new Map();
|
|
1429
|
+
const started = new Set();
|
|
1430
|
+
const eventFor = (update) => {
|
|
1431
|
+
if (update.sessionUpdate === "agent_message_chunk") {
|
|
1432
|
+
const delta = update.content?.text;
|
|
1433
|
+
if (!delta || delta.startsWith("[context]")) return null;
|
|
1434
|
+
return { type: "text_delta", delta };
|
|
1435
|
+
}
|
|
1436
|
+
if (update.sessionUpdate === "agent_thought_chunk") {
|
|
1437
|
+
const delta = update.content?.text;
|
|
1438
|
+
return delta ? { type: "reasoning_delta", delta } : null;
|
|
1439
|
+
}
|
|
1440
|
+
if (update.sessionUpdate === "tool_call") {
|
|
1441
|
+
toolNames.set(update.toolCallId, update.name || update.toolName || update.title || "tool");
|
|
1442
|
+
if (started.has(update.toolCallId)) return null;
|
|
1443
|
+
started.add(update.toolCallId);
|
|
1444
|
+
return {
|
|
1445
|
+
type: "tool_start",
|
|
1446
|
+
id: update.toolCallId,
|
|
1447
|
+
name: toolNames.get(update.toolCallId),
|
|
1448
|
+
};
|
|
1449
|
+
}
|
|
1450
|
+
if (update.sessionUpdate === "tool_call_update" &&
|
|
1451
|
+
(update.status === "completed" || update.status === "failed")) {
|
|
1452
|
+
const content = update.content?.find((entry) => entry.content?.type === "text")?.content?.text;
|
|
1453
|
+
return {
|
|
1454
|
+
type: "tool_end",
|
|
1455
|
+
id: update.toolCallId,
|
|
1456
|
+
name: toolNames.get(update.toolCallId) || "tool",
|
|
1457
|
+
...(content === undefined ? {} : { content }),
|
|
1458
|
+
isError: update.status === "failed",
|
|
1459
|
+
};
|
|
1460
|
+
}
|
|
1461
|
+
return null;
|
|
1030
1462
|
};
|
|
1031
|
-
const
|
|
1032
|
-
|
|
1033
|
-
|
|
1463
|
+
const result = rawTurn.result.then((value) => ({
|
|
1464
|
+
stopReason: value.stopReason,
|
|
1465
|
+
usage: normalizeTurnUsage(value.usage),
|
|
1466
|
+
}));
|
|
1467
|
+
void result.catch(() => {});
|
|
1468
|
+
return {
|
|
1469
|
+
cancel() { rawTurn.cancel(); },
|
|
1470
|
+
[Symbol.asyncIterator]() {
|
|
1471
|
+
const iterator = (async function* () {
|
|
1472
|
+
for await (const update of rawTurn) {
|
|
1473
|
+
const event = eventFor(update);
|
|
1474
|
+
if (event) yield event;
|
|
1475
|
+
}
|
|
1476
|
+
})();
|
|
1477
|
+
return {
|
|
1478
|
+
next(value) { return iterator.next(value); },
|
|
1479
|
+
return(value) { rawTurn.cancel(); return iterator.return(value); },
|
|
1480
|
+
throw(error) { rawTurn.cancel(); return iterator.throw(error); },
|
|
1481
|
+
[Symbol.asyncIterator]() { return this; },
|
|
1482
|
+
};
|
|
1483
|
+
},
|
|
1484
|
+
result,
|
|
1034
1485
|
};
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
function normalizeTurnUsage(usage) {
|
|
1489
|
+
const result = {};
|
|
1490
|
+
if (Number.isSafeInteger(usage?.inputTokens)) result.inputTokens = usage.inputTokens;
|
|
1491
|
+
if (Number.isSafeInteger(usage?.outputTokens)) result.outputTokens = usage.outputTokens;
|
|
1492
|
+
if (Number.isSafeInteger(usage?.cacheReadTokens)) result.cacheReadTokens = usage.cacheReadTokens;
|
|
1493
|
+
if (Number.isSafeInteger(usage?.cacheWriteTokens)) result.cacheWriteTokens = usage.cacheWriteTokens;
|
|
1494
|
+
if (Number.isSafeInteger(usage?.reasoningTokens)) result.reasoningTokens = usage.reasoningTokens;
|
|
1495
|
+
return result;
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
function startTurn(input, promptOptions) {
|
|
1499
|
+
const prompt = normalizePromptInput(input);
|
|
1500
|
+
const signal = promptOptions.signal;
|
|
1501
|
+
if (signal !== undefined && (typeof signal?.addEventListener !== "function" || typeof signal?.removeEventListener !== "function")) throw new TypeError("prompt signal must be an AbortSignal");
|
|
1502
|
+
const queue = [];
|
|
1503
|
+
const waiters = [];
|
|
1504
|
+
let queuedBytes = 0;
|
|
1505
|
+
let resumeOutput;
|
|
1506
|
+
let iteratorTaken = false;
|
|
1507
|
+
let terminalError;
|
|
1508
|
+
let reportedPressure = false;
|
|
1509
|
+
let discardedBytes = 0;
|
|
1510
|
+
const toolControllers = new Set();
|
|
1511
|
+
let finished = false;
|
|
1512
|
+
let cancelled = false;
|
|
1513
|
+
const turn = {
|
|
1514
|
+
push(update, size = encoder.encode(JSON.stringify(update)).length) {
|
|
1515
|
+
if (cancelled || finished) { discardedBytes += size; return; }
|
|
1516
|
+
if (size > maxCoreMessageBytes) throw new RangeError("core output message exceeds 64 MiB");
|
|
1517
|
+
if (queue.length && (queue.length >= maxUnreadEvents || size > maxUnreadEventBytes - queuedBytes)) {
|
|
1518
|
+
const capacity = new Promise((resolveCapacity) => { resumeOutput = resolveCapacity; });
|
|
1519
|
+
if (!reportedPressure) {
|
|
1520
|
+
reportedPressure = true;
|
|
1521
|
+
emit("output.backpressure", { bufferedBytes: queuedBytes, bufferedEvents: queue.length });
|
|
1049
1522
|
}
|
|
1050
|
-
|
|
1523
|
+
return capacity.then(() => turn.push(update, size));
|
|
1051
1524
|
}
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
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;
|
|
1525
|
+
const waiter = waiters.shift();
|
|
1526
|
+
if (waiter) waiter.resolve({ value: update, done: false });
|
|
1527
|
+
else { queue.push({ update, size }); queuedBytes += size; }
|
|
1059
1528
|
},
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1529
|
+
toolControllers,
|
|
1530
|
+
transportAttempts: 0,
|
|
1531
|
+
get cancelled() { return cancelled; },
|
|
1532
|
+
cancel() {
|
|
1533
|
+
if (finished || cancelled) return;
|
|
1534
|
+
cancelled = true;
|
|
1535
|
+
resumeOutput?.();
|
|
1536
|
+
resumeOutput = null;
|
|
1537
|
+
send({ jsonrpc: "2.0", method: "session/cancel", params: { sessionId } });
|
|
1538
|
+
for (const controller of toolControllers) controller.abort();
|
|
1539
|
+
runtime.abortHostEffects();
|
|
1067
1540
|
},
|
|
1068
|
-
|
|
1069
|
-
if (
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
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();
|
|
1541
|
+
[Symbol.asyncIterator]() {
|
|
1542
|
+
if (iteratorTaken) throw new Error("a turn has only one event consumer");
|
|
1543
|
+
iteratorTaken = true;
|
|
1544
|
+
return {
|
|
1545
|
+
next() {
|
|
1546
|
+
if (queue.length) {
|
|
1547
|
+
const { update, size } = queue.shift();
|
|
1548
|
+
queuedBytes -= size;
|
|
1549
|
+
resumeOutput?.();
|
|
1550
|
+
resumeOutput = null;
|
|
1551
|
+
return Promise.resolve({ value: update, done: false });
|
|
1552
|
+
}
|
|
1553
|
+
if (terminalError) return Promise.reject(terminalError);
|
|
1554
|
+
if (finished) return Promise.resolve({ done: true });
|
|
1555
|
+
return new Promise((resolve, reject) => waiters.push({ resolve, reject }));
|
|
1091
1556
|
},
|
|
1092
|
-
|
|
1557
|
+
return() { turn.cancel(); return Promise.resolve({ done: true }); },
|
|
1093
1558
|
};
|
|
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
1559
|
},
|
|
1116
1560
|
};
|
|
1117
|
-
if (
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
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
|
-
}
|
|
1561
|
+
if (signal?.aborted) {
|
|
1562
|
+
finished = true;
|
|
1563
|
+
turn.result = Promise.resolve({ stopReason: "cancelled" });
|
|
1564
|
+
return turn;
|
|
1125
1565
|
}
|
|
1126
|
-
|
|
1127
|
-
|
|
1566
|
+
activeTurn = turn;
|
|
1567
|
+
const abort = () => turn.cancel();
|
|
1568
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
1569
|
+
turn.result = request("session/prompt", { sessionId, prompt })
|
|
1570
|
+
.then((response) => ({ stopReason: cancelled ? "cancelled" : response.stopReason, usage: response.usage }))
|
|
1571
|
+
.catch((error) => {
|
|
1572
|
+
if (error.message === "Cancelled") return { stopReason: "cancelled" };
|
|
1573
|
+
terminalError = error;
|
|
1574
|
+
throw error;
|
|
1575
|
+
})
|
|
1576
|
+
.finally(() => {
|
|
1577
|
+
finished = true;
|
|
1578
|
+
resumeOutput?.();
|
|
1579
|
+
resumeOutput = null;
|
|
1580
|
+
signal?.removeEventListener("abort", abort);
|
|
1581
|
+
if (activeTurn === turn) activeTurn = null;
|
|
1582
|
+
toolControllers.clear();
|
|
1583
|
+
if (discardedBytes) emit("output.discarded", { reason: "cancelled", bytes: discardedBytes });
|
|
1584
|
+
for (const waiter of waiters.splice(0)) {
|
|
1585
|
+
if (terminalError) waiter.reject(terminalError);
|
|
1586
|
+
else waiter.resolve({ done: true });
|
|
1587
|
+
}
|
|
1588
|
+
});
|
|
1589
|
+
if (signal?.aborted) turn.cancel();
|
|
1590
|
+
void turn.result.catch(() => {});
|
|
1591
|
+
return turn;
|
|
1128
1592
|
}
|
|
1129
1593
|
}
|