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/README.md +240 -275
- package/browser.js +2 -1
- package/core-output.js +58 -0
- package/fx-core.wasm +0 -0
- package/fx-sdk.js +658 -208
- 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 +2542 -0
- package/node.js +331 -88
- 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,18 @@ 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")) 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
|
-
|
|
934
|
-
|
|
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
|
|
939
|
-
let
|
|
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 = {
|
|
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
|
-
|
|
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(
|
|
1323
|
+
runtime.setLineHandler((message, size) => {
|
|
968
1324
|
emit("acp.receive", { message });
|
|
969
1325
|
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);
|
|
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
|
-
|
|
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
|
-
|
|
990
|
-
|
|
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)
|
|
993
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
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
|
|
1032
|
-
|
|
1033
|
-
|
|
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
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
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
|
-
|
|
1509
|
+
return capacity.then(() => turn.push(update, size));
|
|
1051
1510
|
}
|
|
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;
|
|
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
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
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
|
-
|
|
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();
|
|
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
|
-
|
|
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 (
|
|
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
|
-
}
|
|
1547
|
+
if (signal?.aborted) {
|
|
1548
|
+
finished = true;
|
|
1549
|
+
turn.result = Promise.resolve({ stopReason: "cancelled" });
|
|
1550
|
+
return turn;
|
|
1125
1551
|
}
|
|
1126
|
-
|
|
1127
|
-
|
|
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
|
}
|