@coder/ai-sdk-agent 0.2.0 → 0.3.0
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 +280 -14
- package/dist/index.d.ts +249 -16
- package/dist/index.js +672 -26
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
package/dist/index.js
CHANGED
|
@@ -42,6 +42,32 @@ var CoderChatError = class extends CoderAgentError {
|
|
|
42
42
|
}
|
|
43
43
|
};
|
|
44
44
|
|
|
45
|
+
// src/ai-version.ts
|
|
46
|
+
var SUPPORTED_AI_MAJOR = 6;
|
|
47
|
+
function unsupportedAiVersionMessage(version) {
|
|
48
|
+
const major = Number(/^v?(\d+)(?:[.+-]|$)/.exec(version.trim())?.[1]);
|
|
49
|
+
if (!Number.isSafeInteger(major) || major === SUPPORTED_AI_MAJOR) return void 0;
|
|
50
|
+
return `@coder/ai-sdk-agent supports the \`ai\` package v${SUPPORTED_AI_MAJOR} only (peer dependency \`ai@^${SUPPORTED_AI_MAJOR}\`), but ai@${version} is installed. Other majors fail at runtime in confusing ways (brand-check TypeErrors, silently empty streams). Install a compatible version, e.g. \`pnpm add ai@^${SUPPORTED_AI_MAJOR}\`.`;
|
|
51
|
+
}
|
|
52
|
+
function resolveInstalledAiVersion() {
|
|
53
|
+
try {
|
|
54
|
+
const nodeModule = typeof process !== "undefined" && typeof process.getBuiltinModule === "function" ? process.getBuiltinModule("node:module") : void 0;
|
|
55
|
+
const requireFromHere = nodeModule?.createRequire(import.meta.url);
|
|
56
|
+
const pkg = requireFromHere?.("ai/package.json");
|
|
57
|
+
return typeof pkg?.version === "string" ? pkg.version : void 0;
|
|
58
|
+
} catch {
|
|
59
|
+
return void 0;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
var cachedMessage;
|
|
63
|
+
function assertSupportedAiVersion(resolveVersion = resolveInstalledAiVersion) {
|
|
64
|
+
if (cachedMessage === void 0) {
|
|
65
|
+
const version = resolveVersion();
|
|
66
|
+
cachedMessage = version === void 0 ? null : unsupportedAiVersionMessage(version) ?? null;
|
|
67
|
+
}
|
|
68
|
+
if (cachedMessage !== null) throw new CoderAgentError(cachedMessage);
|
|
69
|
+
}
|
|
70
|
+
|
|
45
71
|
// src/files.ts
|
|
46
72
|
function isBlob(v) {
|
|
47
73
|
return typeof Blob !== "undefined" && v instanceof Blob;
|
|
@@ -211,6 +237,176 @@ async function* streamChatEvents(options) {
|
|
|
211
237
|
}
|
|
212
238
|
}
|
|
213
239
|
}
|
|
240
|
+
var WATCH_PATH = "/api/experimental/chats/watch";
|
|
241
|
+
var WATCH_BACKOFF_INITIAL_MS = 1e3;
|
|
242
|
+
var WATCH_BACKOFF_CAP_MS = 3e4;
|
|
243
|
+
function sleep(ms, signal) {
|
|
244
|
+
return new Promise((resolve) => {
|
|
245
|
+
if (signal?.aborted) {
|
|
246
|
+
resolve();
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
const onAbort = () => {
|
|
250
|
+
clearTimeout(timer);
|
|
251
|
+
resolve();
|
|
252
|
+
};
|
|
253
|
+
const timer = setTimeout(() => {
|
|
254
|
+
signal?.removeEventListener("abort", onAbort);
|
|
255
|
+
resolve();
|
|
256
|
+
}, ms);
|
|
257
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
function upgradeStatus(message) {
|
|
261
|
+
const match = /unexpected server response: (\d{3})$/i.exec(message.trim());
|
|
262
|
+
return match ? Number(match[1]) : void 0;
|
|
263
|
+
}
|
|
264
|
+
function watchChatEvents(options) {
|
|
265
|
+
const controller = new AbortController();
|
|
266
|
+
const external = options.signal;
|
|
267
|
+
const chain = () => controller.abort(external?.reason);
|
|
268
|
+
if (external) {
|
|
269
|
+
if (external.aborted) chain();
|
|
270
|
+
else external.addEventListener("abort", chain, { once: true });
|
|
271
|
+
}
|
|
272
|
+
const inner = watchChatEventsLoop({ ...options, signal: controller.signal });
|
|
273
|
+
const detach = () => external?.removeEventListener("abort", chain);
|
|
274
|
+
const finish = async () => {
|
|
275
|
+
controller.abort();
|
|
276
|
+
detach();
|
|
277
|
+
try {
|
|
278
|
+
await inner.return();
|
|
279
|
+
} catch {
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
return {
|
|
283
|
+
// Detach the signal chain once the loop settles for good (done, or the
|
|
284
|
+
// terminal 4xx rejection) — the iteration protocol never calls return()
|
|
285
|
+
// on a failed iterator, and the {once} listener would otherwise pile up
|
|
286
|
+
// on a long-lived signal across re-created watchers.
|
|
287
|
+
async next() {
|
|
288
|
+
try {
|
|
289
|
+
const result = await inner.next();
|
|
290
|
+
if (result.done) detach();
|
|
291
|
+
return result;
|
|
292
|
+
} catch (err) {
|
|
293
|
+
detach();
|
|
294
|
+
throw err;
|
|
295
|
+
}
|
|
296
|
+
},
|
|
297
|
+
async return() {
|
|
298
|
+
await finish();
|
|
299
|
+
return { done: true, value: void 0 };
|
|
300
|
+
},
|
|
301
|
+
async throw(err) {
|
|
302
|
+
await finish();
|
|
303
|
+
throw err;
|
|
304
|
+
},
|
|
305
|
+
[Symbol.asyncIterator]() {
|
|
306
|
+
return this;
|
|
307
|
+
},
|
|
308
|
+
// Native async generators are async-disposable; the wrapper must be too,
|
|
309
|
+
// so `await using events = client.watchChats(…)` keeps tearing down.
|
|
310
|
+
async [Symbol.asyncDispose]() {
|
|
311
|
+
await finish();
|
|
312
|
+
}
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
async function* watchChatEventsLoop(options) {
|
|
316
|
+
const { baseUrl, token, signal } = options;
|
|
317
|
+
const factory = options.webSocketFactory ?? defaultFactory;
|
|
318
|
+
const url = `${httpToWs(baseUrl)}${WATCH_PATH}`;
|
|
319
|
+
let backoffMs = WATCH_BACKOFF_INITIAL_MS;
|
|
320
|
+
while (!signal?.aborted) {
|
|
321
|
+
const queue = [];
|
|
322
|
+
let resolveNext;
|
|
323
|
+
let finished = false;
|
|
324
|
+
let failure;
|
|
325
|
+
const wake = () => {
|
|
326
|
+
resolveNext?.();
|
|
327
|
+
resolveNext = void 0;
|
|
328
|
+
};
|
|
329
|
+
const ws = factory(url, { headers: { "Coder-Session-Token": token } });
|
|
330
|
+
const onAbort = () => {
|
|
331
|
+
finished = true;
|
|
332
|
+
try {
|
|
333
|
+
ws.close(1e3);
|
|
334
|
+
} catch {
|
|
335
|
+
}
|
|
336
|
+
wake();
|
|
337
|
+
};
|
|
338
|
+
let abortListenerAdded = false;
|
|
339
|
+
if (signal) {
|
|
340
|
+
if (signal.aborted) onAbort();
|
|
341
|
+
else {
|
|
342
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
343
|
+
abortListenerAdded = true;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
const onMessage = (ev) => {
|
|
347
|
+
if (finished) return;
|
|
348
|
+
let payload;
|
|
349
|
+
try {
|
|
350
|
+
const data = typeof ev.data === "string" ? ev.data : String(ev.data);
|
|
351
|
+
payload = JSON.parse(data);
|
|
352
|
+
} catch (err) {
|
|
353
|
+
failure = new CoderAgentError("failed to parse chat watch frame", { cause: err });
|
|
354
|
+
finished = true;
|
|
355
|
+
wake();
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
if (Array.isArray(payload)) {
|
|
359
|
+
for (const e of payload) queue.push(e);
|
|
360
|
+
} else if (payload && typeof payload === "object") {
|
|
361
|
+
queue.push(payload);
|
|
362
|
+
}
|
|
363
|
+
wake();
|
|
364
|
+
};
|
|
365
|
+
const onError = (ev) => {
|
|
366
|
+
if (finished) return;
|
|
367
|
+
const message = ev && typeof ev === "object" && "message" in ev ? String(ev.message) : "chat watch socket error";
|
|
368
|
+
const status = upgradeStatus(message);
|
|
369
|
+
failure = status !== void 0 && status >= 400 && status < 500 ? new CoderApiError({ status, method: "GET", path: WATCH_PATH, message }) : new CoderAgentError(message);
|
|
370
|
+
finished = true;
|
|
371
|
+
wake();
|
|
372
|
+
};
|
|
373
|
+
const onClose = () => {
|
|
374
|
+
finished = true;
|
|
375
|
+
wake();
|
|
376
|
+
};
|
|
377
|
+
ws.addEventListener("message", onMessage);
|
|
378
|
+
ws.addEventListener("error", onError);
|
|
379
|
+
ws.addEventListener("close", onClose);
|
|
380
|
+
try {
|
|
381
|
+
while (true) {
|
|
382
|
+
while (queue.length > 0) {
|
|
383
|
+
const next = queue.shift();
|
|
384
|
+
backoffMs = WATCH_BACKOFF_INITIAL_MS;
|
|
385
|
+
yield next;
|
|
386
|
+
}
|
|
387
|
+
if (finished) break;
|
|
388
|
+
await new Promise((resolve) => {
|
|
389
|
+
resolveNext = resolve;
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
} finally {
|
|
393
|
+
finished = true;
|
|
394
|
+
if (signal && abortListenerAdded) signal.removeEventListener("abort", onAbort);
|
|
395
|
+
ws.removeEventListener("message", onMessage);
|
|
396
|
+
ws.removeEventListener("error", onError);
|
|
397
|
+
ws.removeEventListener("close", onClose);
|
|
398
|
+
try {
|
|
399
|
+
ws.close(1e3);
|
|
400
|
+
} catch {
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
if (signal?.aborted) return;
|
|
404
|
+
if (failure instanceof CoderApiError) throw failure;
|
|
405
|
+
await sleep(backoffMs, signal);
|
|
406
|
+
if (signal?.aborted) return;
|
|
407
|
+
backoffMs = Math.min(backoffMs * 2, WATCH_BACKOFF_CAP_MS);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
214
410
|
|
|
215
411
|
// src/coder/client.ts
|
|
216
412
|
var API_PREFIX = "/api/experimental/chats";
|
|
@@ -309,8 +505,28 @@ var CoderChatClient = class {
|
|
|
309
505
|
submitToolResults(chatId, req, signal) {
|
|
310
506
|
return this.#request("POST", `${API_PREFIX}/${chatId}/tool-results`, req, signal);
|
|
311
507
|
}
|
|
312
|
-
|
|
313
|
-
|
|
508
|
+
/**
|
|
509
|
+
* Interrupt the chat's in-flight run (`POST /interrupt`). The server flips
|
|
510
|
+
* an active run to `interrupting` and responds immediately with the updated
|
|
511
|
+
* chat — it does not wait for the run to actually stop.
|
|
512
|
+
*
|
|
513
|
+
* Pass `{ wait: true }` to send `?wait=true`, asking the server to hold the
|
|
514
|
+
* response until the run has stopped. Coder servers without that param
|
|
515
|
+
* ignore the unknown query harmlessly and still return immediately, so
|
|
516
|
+
* callers should be prepared to confirm completion via the event stream.
|
|
517
|
+
*
|
|
518
|
+
* The second parameter also accepts a bare `AbortSignal` (the historical
|
|
519
|
+
* signature).
|
|
520
|
+
*/
|
|
521
|
+
interruptChat(chatId, optsOrSignal) {
|
|
522
|
+
const opts = optsOrSignal instanceof AbortSignal ? { signal: optsOrSignal } : optsOrSignal;
|
|
523
|
+
const query = opts?.wait ? "?wait=true" : "";
|
|
524
|
+
return this.#request(
|
|
525
|
+
"POST",
|
|
526
|
+
`${API_PREFIX}/${chatId}/interrupt${query}`,
|
|
527
|
+
void 0,
|
|
528
|
+
opts?.signal
|
|
529
|
+
);
|
|
314
530
|
}
|
|
315
531
|
updateChat(chatId, req, signal) {
|
|
316
532
|
return this.#request("PATCH", `${API_PREFIX}/${chatId}`, req, signal);
|
|
@@ -390,6 +606,22 @@ var CoderChatClient = class {
|
|
|
390
606
|
webSocketFactory: this.#webSocketFactory
|
|
391
607
|
});
|
|
392
608
|
}
|
|
609
|
+
/**
|
|
610
|
+
* Watch lifecycle events (status/title changes, creation, deletion, …) for
|
|
611
|
+
* every chat visible to the authenticated user via the `/chats/watch`
|
|
612
|
+
* WebSocket. Dropped connections are redialed with exponential backoff; the
|
|
613
|
+
* iteration ends only when `opts.signal` aborts, or with a terminal
|
|
614
|
+
* {@link CoderApiError} when the server rejects the upgrade with a 4xx
|
|
615
|
+
* (bad/expired token, or an older Coder server without the endpoint → 404).
|
|
616
|
+
*/
|
|
617
|
+
watchChats(opts) {
|
|
618
|
+
return watchChatEvents({
|
|
619
|
+
baseUrl: this.baseUrl,
|
|
620
|
+
token: this.#token,
|
|
621
|
+
signal: opts?.signal,
|
|
622
|
+
webSocketFactory: this.#webSocketFactory
|
|
623
|
+
});
|
|
624
|
+
}
|
|
393
625
|
// --- Helpers --------------------------------------------------------------
|
|
394
626
|
/**
|
|
395
627
|
* Resolves a user-friendly model hint to a model-config UUID.
|
|
@@ -398,30 +630,159 @@ var CoderChatClient = class {
|
|
|
398
630
|
* (e.g. `anthropic:claude-haiku-4-5-20251001`), a bare model id, or a
|
|
399
631
|
* display-name substring (case-insensitive). Returns `undefined` if no
|
|
400
632
|
* match is found, in which case the caller should let chatd pick the default.
|
|
633
|
+
*
|
|
634
|
+
* Tolerates partial payloads from older/newer servers: a malformed listing
|
|
635
|
+
* (empty body, non-array JSON, null entries) resolves as no match, and
|
|
636
|
+
* entries missing `provider`, `model`, or `display_name` are matched on the
|
|
637
|
+
* fields they do carry — an entry without `provider` still matches by its
|
|
638
|
+
* model id, one without `model` only by display name.
|
|
401
639
|
*/
|
|
402
640
|
async resolveModelConfigId(hint, signal) {
|
|
403
641
|
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
404
642
|
if (UUID_RE.test(hint)) return hint;
|
|
405
|
-
const
|
|
643
|
+
const raw = await this.listModelConfigs(signal);
|
|
644
|
+
const configs = (Array.isArray(raw) ? raw : []).filter(
|
|
645
|
+
(c) => typeof c === "object" && c !== null
|
|
646
|
+
);
|
|
406
647
|
const lower = hint.toLowerCase();
|
|
407
648
|
const colon = hint.indexOf(":");
|
|
408
649
|
const provider = colon >= 0 ? hint.slice(0, colon).toLowerCase() : void 0;
|
|
409
650
|
const model = colon >= 0 ? hint.slice(colon + 1).toLowerCase() : lower;
|
|
410
651
|
const candidates = configs.filter((c) => c.enabled !== false);
|
|
411
652
|
const pool = candidates.length > 0 ? candidates : configs;
|
|
653
|
+
const modelOf = (c) => typeof c.model === "string" ? c.model.toLowerCase() : void 0;
|
|
654
|
+
const providerOf = (c) => typeof c.provider === "string" ? c.provider.toLowerCase() : void 0;
|
|
655
|
+
const displayNameOf = (c) => typeof c.display_name === "string" ? c.display_name.toLowerCase() : void 0;
|
|
412
656
|
const exact = pool.find(
|
|
413
|
-
(c) => c
|
|
657
|
+
(c) => modelOf(c) === model && (provider === void 0 || providerOf(c) === provider)
|
|
414
658
|
);
|
|
415
659
|
if (exact) return exact.id;
|
|
416
|
-
const byModel = pool.find((c) => c
|
|
660
|
+
const byModel = pool.find((c) => modelOf(c) === model);
|
|
417
661
|
if (byModel) return byModel.id;
|
|
418
|
-
const byDisplay = pool.find((c) => c
|
|
662
|
+
const byDisplay = pool.find((c) => displayNameOf(c)?.includes(lower));
|
|
419
663
|
if (byDisplay) return byDisplay.id;
|
|
420
|
-
const byModelSubstring = pool.find((c) => c
|
|
664
|
+
const byModelSubstring = pool.find((c) => modelOf(c)?.includes(model));
|
|
421
665
|
return byModelSubstring?.id;
|
|
422
666
|
}
|
|
423
667
|
};
|
|
424
668
|
|
|
669
|
+
// src/coder/workspaces.ts
|
|
670
|
+
async function request(conn, method, path, body, signal) {
|
|
671
|
+
const fetchFn = conn.fetch ?? globalThis.fetch.bind(globalThis);
|
|
672
|
+
const headers = { "Coder-Session-Token": conn.token };
|
|
673
|
+
if (body !== void 0) headers["Content-Type"] = "application/json";
|
|
674
|
+
const res = await fetchFn(`${conn.baseUrl.replace(/\/$/, "")}${path}`, {
|
|
675
|
+
method,
|
|
676
|
+
headers,
|
|
677
|
+
body: body === void 0 ? void 0 : JSON.stringify(body),
|
|
678
|
+
signal
|
|
679
|
+
});
|
|
680
|
+
const text = await res.text().catch(() => "");
|
|
681
|
+
let parsed;
|
|
682
|
+
try {
|
|
683
|
+
parsed = text.length > 0 ? JSON.parse(text) : void 0;
|
|
684
|
+
} catch {
|
|
685
|
+
parsed = void 0;
|
|
686
|
+
}
|
|
687
|
+
if (!res.ok) {
|
|
688
|
+
const errObj = parsed ?? {};
|
|
689
|
+
throw new CoderApiError({
|
|
690
|
+
status: res.status,
|
|
691
|
+
method,
|
|
692
|
+
path,
|
|
693
|
+
message: errObj.message ?? res.statusText ?? "request failed",
|
|
694
|
+
detail: errObj.detail
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
return parsed;
|
|
698
|
+
}
|
|
699
|
+
function getWorkspace(conn, workspaceId, signal) {
|
|
700
|
+
return request(
|
|
701
|
+
conn,
|
|
702
|
+
"GET",
|
|
703
|
+
`/api/v2/workspaces/${workspaceId}`,
|
|
704
|
+
void 0,
|
|
705
|
+
signal
|
|
706
|
+
);
|
|
707
|
+
}
|
|
708
|
+
function getAppsHost(conn, signal) {
|
|
709
|
+
return request(conn, "GET", "/api/v2/applications/host", void 0, signal);
|
|
710
|
+
}
|
|
711
|
+
async function upsertPortShare(conn, workspaceId, req, signal) {
|
|
712
|
+
const path = `/api/v2/workspaces/${workspaceId}/port-share`;
|
|
713
|
+
try {
|
|
714
|
+
return await request(conn, "POST", path, req, signal);
|
|
715
|
+
} catch (err) {
|
|
716
|
+
if (err instanceof CoderApiError && err.status === 404) {
|
|
717
|
+
throw new CoderApiError({
|
|
718
|
+
status: 404,
|
|
719
|
+
method: "POST",
|
|
720
|
+
path,
|
|
721
|
+
message: "port sharing is unavailable",
|
|
722
|
+
detail: "the workspace was not found, or this Coder server predates port sharing (added in Coder v2.9)"
|
|
723
|
+
});
|
|
724
|
+
}
|
|
725
|
+
throw err;
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
function defaultAgentName(workspace) {
|
|
729
|
+
const agents = (workspace.latest_build?.resources ?? []).flatMap((r) => r.agents ?? []);
|
|
730
|
+
const first = agents[0];
|
|
731
|
+
if (agents.length === 1 && first) return first.name;
|
|
732
|
+
if (agents.length === 0) {
|
|
733
|
+
throw new CoderAgentError(
|
|
734
|
+
`Workspace "${workspace.name}" has no agents in its latest build (is it running?); pass agentName explicitly.`
|
|
735
|
+
);
|
|
736
|
+
}
|
|
737
|
+
throw new CoderAgentError(
|
|
738
|
+
`Workspace "${workspace.name}" has ${agents.length} agents (${agents.map((a) => a.name).join(", ")}); pass agentName to pick one.`
|
|
739
|
+
);
|
|
740
|
+
}
|
|
741
|
+
function assertValidPort(port) {
|
|
742
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
743
|
+
throw new CoderAgentError(`Invalid port ${port}: expected an integer between 1 and 65535.`);
|
|
744
|
+
}
|
|
745
|
+
if (port < 1e3) {
|
|
746
|
+
throw new CoderAgentError(
|
|
747
|
+
`Port ${port} cannot be previewed: Coder subdomain app URLs only encode 4-5 digit ports (1000-65535). Serve the preview on a higher port instead.`
|
|
748
|
+
);
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
async function resolveWorkspacePreview(conn, opts, signal) {
|
|
752
|
+
assertValidPort(opts.port);
|
|
753
|
+
const [workspace, appHost] = await Promise.all([
|
|
754
|
+
getWorkspace(conn, opts.workspaceId, signal),
|
|
755
|
+
getAppsHost(conn, signal)
|
|
756
|
+
]);
|
|
757
|
+
if (!appHost.host) {
|
|
758
|
+
throw new CoderAgentError(
|
|
759
|
+
"This Coder deployment has no wildcard app host configured (--wildcard-access-url), so subdomain preview URLs are unavailable."
|
|
760
|
+
);
|
|
761
|
+
}
|
|
762
|
+
const agentName = opts.agentName ?? defaultAgentName(workspace);
|
|
763
|
+
const suffix = opts.protocol === "https" ? "s" : "";
|
|
764
|
+
const label = `${opts.port}${suffix}--${agentName}--${workspace.name}--${workspace.owner_name}`;
|
|
765
|
+
const scheme = new URL(conn.baseUrl).protocol;
|
|
766
|
+
return { url: `${scheme}//${appHost.host.replace(/\*/g, label)}`, agentName };
|
|
767
|
+
}
|
|
768
|
+
async function shareWorkspacePreview(conn, opts, signal) {
|
|
769
|
+
const requested = opts.shareLevel ?? "authenticated";
|
|
770
|
+
const { url, agentName } = await resolveWorkspacePreview(conn, opts, signal);
|
|
771
|
+
const share = await upsertPortShare(
|
|
772
|
+
conn,
|
|
773
|
+
opts.workspaceId,
|
|
774
|
+
{
|
|
775
|
+
agent_name: agentName,
|
|
776
|
+
port: opts.port,
|
|
777
|
+
share_level: requested,
|
|
778
|
+
protocol: opts.protocol ?? "http"
|
|
779
|
+
},
|
|
780
|
+
signal
|
|
781
|
+
);
|
|
782
|
+
const level = share?.share_level && share.share_level !== "owner" ? share.share_level : requested;
|
|
783
|
+
return { url, shareLevel: level };
|
|
784
|
+
}
|
|
785
|
+
|
|
425
786
|
// src/model/prompt.ts
|
|
426
787
|
var CODER_PROVIDER_OPTIONS = "coder";
|
|
427
788
|
function extractSystemPrompt(prompt) {
|
|
@@ -529,7 +890,10 @@ function mapUsage(u) {
|
|
|
529
890
|
total: u?.output_tokens,
|
|
530
891
|
text: void 0,
|
|
531
892
|
reasoning: u?.reasoning_tokens
|
|
532
|
-
}
|
|
893
|
+
},
|
|
894
|
+
// Preserve the verbatim wire usage (snake_case) so callers can reach fields
|
|
895
|
+
// the normalized shape has no slot for (context_limit, cost, runtime, …).
|
|
896
|
+
...u ? { raw: u } : {}
|
|
533
897
|
};
|
|
534
898
|
}
|
|
535
899
|
function jsonResult(value) {
|
|
@@ -541,10 +905,15 @@ var TurnTranslator = class {
|
|
|
541
905
|
#text = { id: void 0, len: 0, sawDelta: false };
|
|
542
906
|
#reasoning = { id: void 0, len: 0, sawDelta: false };
|
|
543
907
|
#currentAssistantId;
|
|
908
|
+
// Whether text/reasoning deltas arrived since the last assistant `message`
|
|
909
|
+
// snapshot — i.e. the next assistant snapshot is the trailing snapshot of the
|
|
910
|
+
// message those deltas streamed (deltas carry no message id of their own).
|
|
911
|
+
#deltasSinceSnapshot = false;
|
|
544
912
|
#serverToolCalls = /* @__PURE__ */ new Set();
|
|
545
913
|
#serverToolResults = /* @__PURE__ */ new Set();
|
|
546
914
|
#clientToolCalls = /* @__PURE__ */ new Set();
|
|
547
915
|
#clientToolCallSeen = false;
|
|
916
|
+
#sources = /* @__PURE__ */ new Set();
|
|
548
917
|
#usage;
|
|
549
918
|
#error;
|
|
550
919
|
#terminalStatus;
|
|
@@ -631,14 +1000,21 @@ var TurnTranslator = class {
|
|
|
631
1000
|
if (part.args === void 0) return;
|
|
632
1001
|
if (this.#serverToolCalls.has(id)) return;
|
|
633
1002
|
this.#serverToolCalls.add(id);
|
|
634
|
-
out.push({
|
|
1003
|
+
out.push({
|
|
1004
|
+
type: "tool-input-start",
|
|
1005
|
+
id,
|
|
1006
|
+
toolName: name,
|
|
1007
|
+
providerExecuted: true,
|
|
1008
|
+
dynamic: true
|
|
1009
|
+
});
|
|
635
1010
|
out.push({ type: "tool-input-end", id });
|
|
636
1011
|
out.push({
|
|
637
1012
|
type: "tool-call",
|
|
638
1013
|
toolCallId: id,
|
|
639
1014
|
toolName: name,
|
|
640
1015
|
input: typeof part.args === "string" ? part.args : JSON.stringify(part.args),
|
|
641
|
-
providerExecuted: true
|
|
1016
|
+
providerExecuted: true,
|
|
1017
|
+
dynamic: true
|
|
642
1018
|
});
|
|
643
1019
|
}
|
|
644
1020
|
#emitServerToolResult(out, part) {
|
|
@@ -647,13 +1023,37 @@ var TurnTranslator = class {
|
|
|
647
1023
|
if (!id || !name) return;
|
|
648
1024
|
if (part.result === void 0) return;
|
|
649
1025
|
if (this.#serverToolResults.has(id)) return;
|
|
1026
|
+
if (!this.#serverToolCalls.has(id)) return;
|
|
650
1027
|
this.#serverToolResults.add(id);
|
|
651
1028
|
out.push({
|
|
652
1029
|
type: "tool-result",
|
|
653
1030
|
toolCallId: id,
|
|
654
1031
|
toolName: name,
|
|
655
1032
|
result: jsonResult(part.result),
|
|
656
|
-
isError: part.is_error ?? false
|
|
1033
|
+
isError: part.is_error ?? false,
|
|
1034
|
+
// Mirror the call's `dynamic: true` so call and result land in the same
|
|
1035
|
+
// bucket (steps[*].dynamicToolCalls / dynamicToolResults, UI streams).
|
|
1036
|
+
dynamic: true
|
|
1037
|
+
});
|
|
1038
|
+
}
|
|
1039
|
+
/**
|
|
1040
|
+
* Emits a chatd `source` part as a standalone V3 url source (no text-block
|
|
1041
|
+
* bracketing needed). Deduped by the emitted id so a part streamed as a
|
|
1042
|
+
* `message_part` isn't re-emitted by its trailing `message` snapshot, while
|
|
1043
|
+
* snapshot-only turns still emit theirs.
|
|
1044
|
+
*/
|
|
1045
|
+
#emitSource(out, part) {
|
|
1046
|
+
const url = part.url;
|
|
1047
|
+
if (!url) return;
|
|
1048
|
+
const id = part.source_id || url;
|
|
1049
|
+
if (this.#sources.has(id)) return;
|
|
1050
|
+
this.#sources.add(id);
|
|
1051
|
+
out.push({
|
|
1052
|
+
type: "source",
|
|
1053
|
+
sourceType: "url",
|
|
1054
|
+
id,
|
|
1055
|
+
url,
|
|
1056
|
+
...part.title !== void 0 ? { title: part.title } : {}
|
|
657
1057
|
});
|
|
658
1058
|
}
|
|
659
1059
|
// --- ingest ---------------------------------------------------------------
|
|
@@ -706,6 +1106,7 @@ var TurnTranslator = class {
|
|
|
706
1106
|
switch (part.type) {
|
|
707
1107
|
case "text":
|
|
708
1108
|
this.#text.sawDelta = true;
|
|
1109
|
+
this.#deltasSinceSnapshot = true;
|
|
709
1110
|
this.#openText(out);
|
|
710
1111
|
if (part.text) {
|
|
711
1112
|
out.push({ type: "text-delta", id: this.#text.id, delta: part.text });
|
|
@@ -714,6 +1115,7 @@ var TurnTranslator = class {
|
|
|
714
1115
|
break;
|
|
715
1116
|
case "reasoning":
|
|
716
1117
|
this.#reasoning.sawDelta = true;
|
|
1118
|
+
this.#deltasSinceSnapshot = true;
|
|
717
1119
|
this.#openReasoning(out);
|
|
718
1120
|
if (part.text) {
|
|
719
1121
|
out.push({ type: "reasoning-delta", id: this.#reasoning.id, delta: part.text });
|
|
@@ -728,6 +1130,9 @@ var TurnTranslator = class {
|
|
|
728
1130
|
case "tool-result":
|
|
729
1131
|
if (!this.#isClientTool(part.tool_name)) this.#emitServerToolResult(out, part);
|
|
730
1132
|
break;
|
|
1133
|
+
case "source":
|
|
1134
|
+
this.#emitSource(out, part);
|
|
1135
|
+
break;
|
|
731
1136
|
default:
|
|
732
1137
|
break;
|
|
733
1138
|
}
|
|
@@ -739,7 +1144,7 @@ var TurnTranslator = class {
|
|
|
739
1144
|
}
|
|
740
1145
|
const content = message.content ?? [];
|
|
741
1146
|
if (message.role === "assistant") {
|
|
742
|
-
if (this.#currentAssistantId !== void 0 && this.#currentAssistantId !== message.id) {
|
|
1147
|
+
if (this.#currentAssistantId !== void 0 && this.#currentAssistantId !== message.id && !this.#deltasSinceSnapshot) {
|
|
743
1148
|
this.#closeText(out);
|
|
744
1149
|
this.#closeReasoning(out);
|
|
745
1150
|
this.#text.sawDelta = false;
|
|
@@ -759,11 +1164,14 @@ var TurnTranslator = class {
|
|
|
759
1164
|
this.#emitServerToolCall(out, part);
|
|
760
1165
|
else if (part.type === "tool-result" && !this.#isClientTool(part.tool_name))
|
|
761
1166
|
this.#emitServerToolResult(out, part);
|
|
1167
|
+
else if (part.type === "source") this.#emitSource(out, part);
|
|
762
1168
|
}
|
|
1169
|
+
this.#deltasSinceSnapshot = false;
|
|
763
1170
|
} else if (message.role === "tool") {
|
|
764
1171
|
for (const part of content) {
|
|
765
1172
|
if (part.type === "tool-result" && !this.#isClientTool(part.tool_name))
|
|
766
1173
|
this.#emitServerToolResult(out, part);
|
|
1174
|
+
else if (part.type === "source") this.#emitSource(out, part);
|
|
767
1175
|
}
|
|
768
1176
|
}
|
|
769
1177
|
}
|
|
@@ -777,10 +1185,16 @@ var TurnTranslator = class {
|
|
|
777
1185
|
else if (this.#clientToolCallSeen || this.#terminalStatus === "requires_action")
|
|
778
1186
|
unified = "tool-calls";
|
|
779
1187
|
else unified = "stop";
|
|
1188
|
+
const coder = {};
|
|
1189
|
+
if (this.#usage?.total_cost_micros !== void 0)
|
|
1190
|
+
coder.total_cost_micros = this.#usage.total_cost_micros;
|
|
1191
|
+
if (this.#usage?.total_runtime_ms !== void 0)
|
|
1192
|
+
coder.total_runtime_ms = this.#usage.total_runtime_ms;
|
|
780
1193
|
out.push({
|
|
781
1194
|
type: "finish",
|
|
782
1195
|
usage: mapUsage(this.#usage),
|
|
783
|
-
finishReason: { unified, raw: this.#terminalStatus }
|
|
1196
|
+
finishReason: { unified, raw: this.#terminalStatus },
|
|
1197
|
+
...Object.keys(coder).length > 0 ? { providerMetadata: { coder } } : {}
|
|
784
1198
|
});
|
|
785
1199
|
return out;
|
|
786
1200
|
}
|
|
@@ -809,6 +1223,7 @@ var CoderLanguageModel = class {
|
|
|
809
1223
|
#submittedToolCallIds = /* @__PURE__ */ new Set();
|
|
810
1224
|
#inFlight = false;
|
|
811
1225
|
constructor(config) {
|
|
1226
|
+
assertSupportedAiVersion();
|
|
812
1227
|
this.#config = config;
|
|
813
1228
|
this.modelId = config.model ?? "chatd";
|
|
814
1229
|
this.#chatId = config.chatId;
|
|
@@ -1024,6 +1439,7 @@ var CoderLanguageModel = class {
|
|
|
1024
1439
|
unified: "stop",
|
|
1025
1440
|
raw: void 0
|
|
1026
1441
|
};
|
|
1442
|
+
let providerMetadata;
|
|
1027
1443
|
const warnings = [];
|
|
1028
1444
|
for await (const part of this.#runTurn(options)) {
|
|
1029
1445
|
switch (part.type) {
|
|
@@ -1063,6 +1479,7 @@ var CoderLanguageModel = class {
|
|
|
1063
1479
|
case "finish":
|
|
1064
1480
|
usage = part.usage;
|
|
1065
1481
|
finishReason = part.finishReason;
|
|
1482
|
+
providerMetadata = part.providerMetadata;
|
|
1066
1483
|
break;
|
|
1067
1484
|
case "error":
|
|
1068
1485
|
throw part.error instanceof Error ? part.error : new CoderChatError({ message: String(part.error) });
|
|
@@ -1070,7 +1487,13 @@ var CoderLanguageModel = class {
|
|
|
1070
1487
|
break;
|
|
1071
1488
|
}
|
|
1072
1489
|
}
|
|
1073
|
-
return {
|
|
1490
|
+
return {
|
|
1491
|
+
content,
|
|
1492
|
+
finishReason,
|
|
1493
|
+
usage,
|
|
1494
|
+
warnings,
|
|
1495
|
+
...providerMetadata ? { providerMetadata } : {}
|
|
1496
|
+
};
|
|
1074
1497
|
}
|
|
1075
1498
|
};
|
|
1076
1499
|
|
|
@@ -1089,6 +1512,47 @@ function makeChatAttachment(file) {
|
|
|
1089
1512
|
};
|
|
1090
1513
|
}
|
|
1091
1514
|
var DEFAULT_STOP = stepCountIs(64);
|
|
1515
|
+
var SETTLE_DEADLINE_MS = 15e3;
|
|
1516
|
+
var SETTLE_RETRY_DELAY_MS = 1e3;
|
|
1517
|
+
function sanitizeDelayMs(value, fallback) {
|
|
1518
|
+
const MAX_TIMER_MS = 2 ** 31 - 1;
|
|
1519
|
+
if (value === void 0 || Number.isNaN(value)) return fallback;
|
|
1520
|
+
if (value === Number.POSITIVE_INFINITY) return MAX_TIMER_MS;
|
|
1521
|
+
if (!Number.isFinite(value) || value < 0) return fallback;
|
|
1522
|
+
return Math.min(Math.floor(value), MAX_TIMER_MS);
|
|
1523
|
+
}
|
|
1524
|
+
function sleep2(ms, signal) {
|
|
1525
|
+
return new Promise((resolve, reject) => {
|
|
1526
|
+
if (signal?.aborted) {
|
|
1527
|
+
reject(signal.reason);
|
|
1528
|
+
return;
|
|
1529
|
+
}
|
|
1530
|
+
const onAbort = () => {
|
|
1531
|
+
clearTimeout(timer);
|
|
1532
|
+
reject(signal?.reason);
|
|
1533
|
+
};
|
|
1534
|
+
const timer = setTimeout(() => {
|
|
1535
|
+
signal?.removeEventListener("abort", onAbort);
|
|
1536
|
+
resolve();
|
|
1537
|
+
}, ms);
|
|
1538
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
1539
|
+
});
|
|
1540
|
+
}
|
|
1541
|
+
async function archiveWhenSettled(client, chatId, opts) {
|
|
1542
|
+
const timeout = AbortSignal.timeout(opts.deadlineMs);
|
|
1543
|
+
const signal = opts.signal ? AbortSignal.any([opts.signal, timeout]) : timeout;
|
|
1544
|
+
const deadline = Date.now() + opts.deadlineMs;
|
|
1545
|
+
for (; ; ) {
|
|
1546
|
+
try {
|
|
1547
|
+
await client.archiveChat(chatId, signal);
|
|
1548
|
+
return;
|
|
1549
|
+
} catch (err) {
|
|
1550
|
+
if (!(err instanceof CoderApiError) || err.status !== 409) throw err;
|
|
1551
|
+
if (Date.now() + opts.retryDelayMs > deadline) throw err;
|
|
1552
|
+
await sleep2(opts.retryDelayMs, opts.signal);
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1092
1556
|
var CoderAgent = class {
|
|
1093
1557
|
version = "agent-v1";
|
|
1094
1558
|
#client;
|
|
@@ -1096,7 +1560,12 @@ var CoderAgent = class {
|
|
|
1096
1560
|
#inner;
|
|
1097
1561
|
#organizationId;
|
|
1098
1562
|
#workspaceFiles;
|
|
1563
|
+
#workspaceId;
|
|
1564
|
+
#workspaceApi;
|
|
1565
|
+
#settleDeadlineMs;
|
|
1566
|
+
#settleRetryDelayMs;
|
|
1099
1567
|
constructor(settings) {
|
|
1568
|
+
assertSupportedAiVersion();
|
|
1100
1569
|
if (settings.client) {
|
|
1101
1570
|
this.#client = settings.client;
|
|
1102
1571
|
} else if (settings.baseUrl && settings.token) {
|
|
@@ -1113,6 +1582,10 @@ var CoderAgent = class {
|
|
|
1113
1582
|
}
|
|
1114
1583
|
this.#organizationId = settings.organizationId;
|
|
1115
1584
|
this.#workspaceFiles = settings.workspaceFiles;
|
|
1585
|
+
this.#workspaceId = settings.workspaceId;
|
|
1586
|
+
this.#workspaceApi = settings.baseUrl && settings.token ? { baseUrl: settings.baseUrl, token: settings.token, fetch: settings.fetch } : void 0;
|
|
1587
|
+
this.#settleDeadlineMs = sanitizeDelayMs(settings.settleDeadlineMs, SETTLE_DEADLINE_MS);
|
|
1588
|
+
this.#settleRetryDelayMs = sanitizeDelayMs(settings.settleRetryDelayMs, SETTLE_RETRY_DELAY_MS);
|
|
1116
1589
|
if (settings.workspaceFiles && settings.workspaceId && settings.workspaceFiles.workspaceId !== settings.workspaceId) {
|
|
1117
1590
|
throw new CoderAgentError(
|
|
1118
1591
|
`workspaceFiles.workspaceId (${settings.workspaceFiles.workspaceId}) does not match the agent's workspaceId (${settings.workspaceId}); the chat's tools would not see uploaded files.`
|
|
@@ -1171,22 +1644,41 @@ var CoderAgent = class {
|
|
|
1171
1644
|
resetSession() {
|
|
1172
1645
|
this.#model.resetSession();
|
|
1173
1646
|
}
|
|
1174
|
-
/**
|
|
1175
|
-
|
|
1647
|
+
/**
|
|
1648
|
+
* Interrupt the in-flight generation, if any. Resolves as soon as the server
|
|
1649
|
+
* acknowledges the interrupt — the run keeps winding down asynchronously for
|
|
1650
|
+
* a few seconds afterwards (see {@link CoderAgent.archive}).
|
|
1651
|
+
*/
|
|
1652
|
+
async interrupt(opts) {
|
|
1176
1653
|
const id = this.#model.chatId;
|
|
1177
|
-
if (id) await this.#client.interruptChat(id);
|
|
1654
|
+
if (id) await this.#client.interruptChat(id, opts?.signal);
|
|
1178
1655
|
}
|
|
1179
|
-
/**
|
|
1180
|
-
|
|
1656
|
+
/**
|
|
1657
|
+
* Archive the underlying chat (safe cleanup; hides it from listings).
|
|
1658
|
+
*
|
|
1659
|
+
* A freshly interrupted/settled chat can keep winding down server-side for a
|
|
1660
|
+
* few seconds, during which the server rejects archiving with a 409. Those
|
|
1661
|
+
* 409s are retried (~1s apart, ~15s overall — see `settleDeadlineMs` /
|
|
1662
|
+
* `settleRetryDelayMs`) and the last one is rethrown if the chat never
|
|
1663
|
+
* settles; any other failure, including a caller abort, rethrows immediately.
|
|
1664
|
+
*/
|
|
1665
|
+
async archive(opts) {
|
|
1181
1666
|
const id = this.#model.chatId;
|
|
1182
|
-
if (id)
|
|
1667
|
+
if (!id) return;
|
|
1668
|
+
await archiveWhenSettled(this.#client, id, {
|
|
1669
|
+
deadlineMs: this.#settleDeadlineMs,
|
|
1670
|
+
retryDelayMs: this.#settleRetryDelayMs,
|
|
1671
|
+
signal: opts?.signal
|
|
1672
|
+
});
|
|
1183
1673
|
}
|
|
1184
1674
|
/**
|
|
1185
1675
|
* Clean up the chat when the agent leaves an `await using` scope, so cleanup
|
|
1186
1676
|
* rides scope exit instead of a separate call you have to remember in a
|
|
1187
|
-
* `finally`. Interrupts any in-flight server run, then archives the chat
|
|
1188
|
-
*
|
|
1189
|
-
*
|
|
1677
|
+
* `finally`. Interrupts any in-flight server run, then archives the chat
|
|
1678
|
+
* (retrying while the interrupted run settles). Best-effort and bounded
|
|
1679
|
+
* (~15s overall): disposal errors are swallowed after the bounded attempts
|
|
1680
|
+
* so they can't mask the scope's own error — call {@link CoderAgent.archive}
|
|
1681
|
+
* directly when you need guaranteed cleanup.
|
|
1190
1682
|
*
|
|
1191
1683
|
* @example
|
|
1192
1684
|
* ```ts
|
|
@@ -1196,12 +1688,13 @@ var CoderAgent = class {
|
|
|
1196
1688
|
* ```
|
|
1197
1689
|
*/
|
|
1198
1690
|
async [Symbol.asyncDispose]() {
|
|
1691
|
+
const deadline = AbortSignal.timeout(this.#settleDeadlineMs);
|
|
1199
1692
|
try {
|
|
1200
|
-
await this.interrupt();
|
|
1693
|
+
await this.interrupt({ signal: deadline });
|
|
1201
1694
|
} catch {
|
|
1202
1695
|
}
|
|
1203
1696
|
try {
|
|
1204
|
-
await this.archive();
|
|
1697
|
+
await this.archive({ signal: deadline });
|
|
1205
1698
|
} catch {
|
|
1206
1699
|
}
|
|
1207
1700
|
}
|
|
@@ -1245,7 +1738,158 @@ var CoderAgent = class {
|
|
|
1245
1738
|
});
|
|
1246
1739
|
return { workspaceId: this.#workspaceFiles.workspaceId, path };
|
|
1247
1740
|
}
|
|
1741
|
+
// --- previews ---------------------------------------------------------------
|
|
1742
|
+
/** The v2 connection + workspace id the preview helpers need, or a clear error. */
|
|
1743
|
+
#previewContext(method) {
|
|
1744
|
+
if (!this.#workspaceId) {
|
|
1745
|
+
throw new CoderAgentError(
|
|
1746
|
+
`${method}() requires the agent to be bound to a workspace \u2014 construct the CoderAgent with the \`workspaceId\` setting.`
|
|
1747
|
+
);
|
|
1748
|
+
}
|
|
1749
|
+
if (!this.#workspaceApi) {
|
|
1750
|
+
throw new CoderAgentError(
|
|
1751
|
+
`${method}() needs the deployment's REST credentials \u2014 construct the CoderAgent with \`baseUrl\` and \`token\` (they can accompany \`client\`).`
|
|
1752
|
+
);
|
|
1753
|
+
}
|
|
1754
|
+
return { conn: this.#workspaceApi, workspaceId: this.#workspaceId };
|
|
1755
|
+
}
|
|
1756
|
+
/**
|
|
1757
|
+
* Resolve the browser URL where a port on the bound workspace can be
|
|
1758
|
+
* previewed, e.g. `https://3000--main--dev--alice.apps.example.com`.
|
|
1759
|
+
* Composed from stable v2 endpoints (workspace lookup + wildcard app host),
|
|
1760
|
+
* so it works against old Coder servers. Requires the `workspaceId` setting,
|
|
1761
|
+
* `baseUrl`/`token` credentials, and a deployment with a wildcard access URL
|
|
1762
|
+
* configured.
|
|
1763
|
+
*
|
|
1764
|
+
* The URL honors the port's current share level — private to the workspace
|
|
1765
|
+
* owner unless shared (see {@link CoderAgent.sharePreview}).
|
|
1766
|
+
*/
|
|
1767
|
+
async getPreview(opts) {
|
|
1768
|
+
const { conn, workspaceId } = this.#previewContext("getPreview");
|
|
1769
|
+
const { url } = await resolveWorkspacePreview(
|
|
1770
|
+
conn,
|
|
1771
|
+
{ workspaceId, port: opts.port, agentName: opts.agentName, protocol: opts.protocol },
|
|
1772
|
+
opts.signal
|
|
1773
|
+
);
|
|
1774
|
+
return { url };
|
|
1775
|
+
}
|
|
1776
|
+
/**
|
|
1777
|
+
* Share a port on the bound workspace and return its preview URL. Upserts
|
|
1778
|
+
* the port's share level — `authenticated` by default — so teammates (or,
|
|
1779
|
+
* with `public`, anyone) can open what the agent is serving; re-invoking
|
|
1780
|
+
* updates the level in place. On Coder servers that predate port sharing
|
|
1781
|
+
* (< v2.9) this fails with a 404 {@link CoderApiError} saying so.
|
|
1782
|
+
*/
|
|
1783
|
+
async sharePreview(opts) {
|
|
1784
|
+
const { conn, workspaceId } = this.#previewContext("sharePreview");
|
|
1785
|
+
return shareWorkspacePreview(
|
|
1786
|
+
conn,
|
|
1787
|
+
{
|
|
1788
|
+
workspaceId,
|
|
1789
|
+
port: opts.port,
|
|
1790
|
+
agentName: opts.agentName,
|
|
1791
|
+
protocol: opts.protocol,
|
|
1792
|
+
shareLevel: opts.shareLevel
|
|
1793
|
+
},
|
|
1794
|
+
opts.signal
|
|
1795
|
+
);
|
|
1796
|
+
}
|
|
1248
1797
|
};
|
|
1798
|
+
|
|
1799
|
+
// src/model/history.ts
|
|
1800
|
+
function toolErrorText(result) {
|
|
1801
|
+
if (typeof result === "string") return result;
|
|
1802
|
+
if (result === void 0 || result === null) return "";
|
|
1803
|
+
return JSON.stringify(result) ?? "";
|
|
1804
|
+
}
|
|
1805
|
+
function toDynamicToolPart(call, result) {
|
|
1806
|
+
const toolCallId = call.tool_call_id;
|
|
1807
|
+
const toolName = call.tool_name;
|
|
1808
|
+
if (!toolCallId || !toolName) return void 0;
|
|
1809
|
+
const providerExecuted = call.provider_executed ?? result?.provider_executed;
|
|
1810
|
+
const base = {
|
|
1811
|
+
type: "dynamic-tool",
|
|
1812
|
+
toolName,
|
|
1813
|
+
toolCallId,
|
|
1814
|
+
...providerExecuted !== void 0 ? { providerExecuted } : {}
|
|
1815
|
+
};
|
|
1816
|
+
if (!result) return { ...base, state: "input-available", input: call.args };
|
|
1817
|
+
if (result.is_error) {
|
|
1818
|
+
return {
|
|
1819
|
+
...base,
|
|
1820
|
+
state: "output-error",
|
|
1821
|
+
input: call.args,
|
|
1822
|
+
errorText: toolErrorText(result.result)
|
|
1823
|
+
};
|
|
1824
|
+
}
|
|
1825
|
+
return { ...base, state: "output-available", input: call.args, output: result.result };
|
|
1826
|
+
}
|
|
1827
|
+
function toUIParts(content, resultsByCallId, opts) {
|
|
1828
|
+
const parts = [];
|
|
1829
|
+
for (const part of content) {
|
|
1830
|
+
switch (part.type) {
|
|
1831
|
+
case "text":
|
|
1832
|
+
parts.push({ type: "text", text: part.text ?? "", state: "done" });
|
|
1833
|
+
break;
|
|
1834
|
+
case "reasoning":
|
|
1835
|
+
parts.push({ type: "reasoning", text: part.text ?? "", state: "done" });
|
|
1836
|
+
break;
|
|
1837
|
+
case "tool-call": {
|
|
1838
|
+
const result = part.tool_call_id ? resultsByCallId.get(part.tool_call_id) : void 0;
|
|
1839
|
+
const ui = toDynamicToolPart(part, result);
|
|
1840
|
+
if (ui) parts.push(ui);
|
|
1841
|
+
break;
|
|
1842
|
+
}
|
|
1843
|
+
case "source":
|
|
1844
|
+
if (part.url) {
|
|
1845
|
+
parts.push({
|
|
1846
|
+
type: "source-url",
|
|
1847
|
+
sourceId: part.source_id || part.url,
|
|
1848
|
+
url: part.url,
|
|
1849
|
+
...part.title ? { title: part.title } : {}
|
|
1850
|
+
});
|
|
1851
|
+
}
|
|
1852
|
+
break;
|
|
1853
|
+
case "file": {
|
|
1854
|
+
const url = opts?.fileUrl?.(part) ?? part.url;
|
|
1855
|
+
if (url) {
|
|
1856
|
+
parts.push({
|
|
1857
|
+
type: "file",
|
|
1858
|
+
mediaType: part.media_type ?? "application/octet-stream",
|
|
1859
|
+
...part.name ? { filename: part.name } : {},
|
|
1860
|
+
url
|
|
1861
|
+
});
|
|
1862
|
+
}
|
|
1863
|
+
break;
|
|
1864
|
+
}
|
|
1865
|
+
case "tool-result":
|
|
1866
|
+
break;
|
|
1867
|
+
default:
|
|
1868
|
+
break;
|
|
1869
|
+
}
|
|
1870
|
+
}
|
|
1871
|
+
return parts;
|
|
1872
|
+
}
|
|
1873
|
+
function chatMessagesToUIMessages(messages, opts) {
|
|
1874
|
+
const ordered = [...messages].sort((a, b) => a.id - b.id);
|
|
1875
|
+
const resultsByCallId = /* @__PURE__ */ new Map();
|
|
1876
|
+
for (const message of ordered) {
|
|
1877
|
+
for (const part of message.content ?? []) {
|
|
1878
|
+
if (part.type !== "tool-result" || !part.tool_call_id) continue;
|
|
1879
|
+
if (!resultsByCallId.has(part.tool_call_id)) resultsByCallId.set(part.tool_call_id, part);
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1882
|
+
const out = [];
|
|
1883
|
+
for (const message of ordered) {
|
|
1884
|
+
if (message.role === "tool") continue;
|
|
1885
|
+
out.push({
|
|
1886
|
+
id: String(message.id),
|
|
1887
|
+
role: message.role,
|
|
1888
|
+
parts: toUIParts(message.content ?? [], resultsByCallId, opts)
|
|
1889
|
+
});
|
|
1890
|
+
}
|
|
1891
|
+
return out;
|
|
1892
|
+
}
|
|
1249
1893
|
export {
|
|
1250
1894
|
CHAT_ATTACHMENT_MEDIA_TYPES,
|
|
1251
1895
|
CODER_PROVIDER_OPTIONS,
|
|
@@ -1257,6 +1901,7 @@ export {
|
|
|
1257
1901
|
CoderLanguageModel,
|
|
1258
1902
|
MAX_CHAT_FILE_SIZE_BYTES,
|
|
1259
1903
|
TurnTranslator,
|
|
1904
|
+
chatMessagesToUIMessages,
|
|
1260
1905
|
classifyTurnAction,
|
|
1261
1906
|
dataContentToFileContent,
|
|
1262
1907
|
dynamicToolNames,
|
|
@@ -1264,6 +1909,7 @@ export {
|
|
|
1264
1909
|
resolveFileContent,
|
|
1265
1910
|
streamChatEvents,
|
|
1266
1911
|
toolsToDynamicTools,
|
|
1267
|
-
userContentToInputParts
|
|
1912
|
+
userContentToInputParts,
|
|
1913
|
+
watchChatEvents
|
|
1268
1914
|
};
|
|
1269
1915
|
//# sourceMappingURL=index.js.map
|