@coder/ai-sdk-agent 0.2.1 → 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/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
- interruptChat(chatId, signal) {
313
- return this.#request("POST", `${API_PREFIX}/${chatId}/interrupt`, void 0, signal);
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 configs = await this.listModelConfigs(signal);
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.model.toLowerCase() === model && (provider === void 0 || c.provider.toLowerCase() === provider)
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.model.toLowerCase() === model);
660
+ const byModel = pool.find((c) => modelOf(c) === model);
417
661
  if (byModel) return byModel.id;
418
- const byDisplay = pool.find((c) => c.display_name.toLowerCase().includes(lower));
662
+ const byDisplay = pool.find((c) => displayNameOf(c)?.includes(lower));
419
663
  if (byDisplay) return byDisplay.id;
420
- const byModelSubstring = pool.find((c) => c.model.toLowerCase().includes(model));
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;
@@ -667,6 +1036,26 @@ var TurnTranslator = class {
667
1036
  dynamic: true
668
1037
  });
669
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 } : {}
1057
+ });
1058
+ }
670
1059
  // --- ingest ---------------------------------------------------------------
671
1060
  ingest(ev) {
672
1061
  const out = [];
@@ -717,6 +1106,7 @@ var TurnTranslator = class {
717
1106
  switch (part.type) {
718
1107
  case "text":
719
1108
  this.#text.sawDelta = true;
1109
+ this.#deltasSinceSnapshot = true;
720
1110
  this.#openText(out);
721
1111
  if (part.text) {
722
1112
  out.push({ type: "text-delta", id: this.#text.id, delta: part.text });
@@ -725,6 +1115,7 @@ var TurnTranslator = class {
725
1115
  break;
726
1116
  case "reasoning":
727
1117
  this.#reasoning.sawDelta = true;
1118
+ this.#deltasSinceSnapshot = true;
728
1119
  this.#openReasoning(out);
729
1120
  if (part.text) {
730
1121
  out.push({ type: "reasoning-delta", id: this.#reasoning.id, delta: part.text });
@@ -739,6 +1130,9 @@ var TurnTranslator = class {
739
1130
  case "tool-result":
740
1131
  if (!this.#isClientTool(part.tool_name)) this.#emitServerToolResult(out, part);
741
1132
  break;
1133
+ case "source":
1134
+ this.#emitSource(out, part);
1135
+ break;
742
1136
  default:
743
1137
  break;
744
1138
  }
@@ -750,7 +1144,7 @@ var TurnTranslator = class {
750
1144
  }
751
1145
  const content = message.content ?? [];
752
1146
  if (message.role === "assistant") {
753
- if (this.#currentAssistantId !== void 0 && this.#currentAssistantId !== message.id) {
1147
+ if (this.#currentAssistantId !== void 0 && this.#currentAssistantId !== message.id && !this.#deltasSinceSnapshot) {
754
1148
  this.#closeText(out);
755
1149
  this.#closeReasoning(out);
756
1150
  this.#text.sawDelta = false;
@@ -770,11 +1164,14 @@ var TurnTranslator = class {
770
1164
  this.#emitServerToolCall(out, part);
771
1165
  else if (part.type === "tool-result" && !this.#isClientTool(part.tool_name))
772
1166
  this.#emitServerToolResult(out, part);
1167
+ else if (part.type === "source") this.#emitSource(out, part);
773
1168
  }
1169
+ this.#deltasSinceSnapshot = false;
774
1170
  } else if (message.role === "tool") {
775
1171
  for (const part of content) {
776
1172
  if (part.type === "tool-result" && !this.#isClientTool(part.tool_name))
777
1173
  this.#emitServerToolResult(out, part);
1174
+ else if (part.type === "source") this.#emitSource(out, part);
778
1175
  }
779
1176
  }
780
1177
  }
@@ -788,10 +1185,16 @@ var TurnTranslator = class {
788
1185
  else if (this.#clientToolCallSeen || this.#terminalStatus === "requires_action")
789
1186
  unified = "tool-calls";
790
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;
791
1193
  out.push({
792
1194
  type: "finish",
793
1195
  usage: mapUsage(this.#usage),
794
- finishReason: { unified, raw: this.#terminalStatus }
1196
+ finishReason: { unified, raw: this.#terminalStatus },
1197
+ ...Object.keys(coder).length > 0 ? { providerMetadata: { coder } } : {}
795
1198
  });
796
1199
  return out;
797
1200
  }
@@ -820,6 +1223,7 @@ var CoderLanguageModel = class {
820
1223
  #submittedToolCallIds = /* @__PURE__ */ new Set();
821
1224
  #inFlight = false;
822
1225
  constructor(config) {
1226
+ assertSupportedAiVersion();
823
1227
  this.#config = config;
824
1228
  this.modelId = config.model ?? "chatd";
825
1229
  this.#chatId = config.chatId;
@@ -1035,6 +1439,7 @@ var CoderLanguageModel = class {
1035
1439
  unified: "stop",
1036
1440
  raw: void 0
1037
1441
  };
1442
+ let providerMetadata;
1038
1443
  const warnings = [];
1039
1444
  for await (const part of this.#runTurn(options)) {
1040
1445
  switch (part.type) {
@@ -1074,6 +1479,7 @@ var CoderLanguageModel = class {
1074
1479
  case "finish":
1075
1480
  usage = part.usage;
1076
1481
  finishReason = part.finishReason;
1482
+ providerMetadata = part.providerMetadata;
1077
1483
  break;
1078
1484
  case "error":
1079
1485
  throw part.error instanceof Error ? part.error : new CoderChatError({ message: String(part.error) });
@@ -1081,7 +1487,13 @@ var CoderLanguageModel = class {
1081
1487
  break;
1082
1488
  }
1083
1489
  }
1084
- return { content, finishReason, usage, warnings };
1490
+ return {
1491
+ content,
1492
+ finishReason,
1493
+ usage,
1494
+ warnings,
1495
+ ...providerMetadata ? { providerMetadata } : {}
1496
+ };
1085
1497
  }
1086
1498
  };
1087
1499
 
@@ -1100,6 +1512,47 @@ function makeChatAttachment(file) {
1100
1512
  };
1101
1513
  }
1102
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
+ }
1103
1556
  var CoderAgent = class {
1104
1557
  version = "agent-v1";
1105
1558
  #client;
@@ -1107,7 +1560,12 @@ var CoderAgent = class {
1107
1560
  #inner;
1108
1561
  #organizationId;
1109
1562
  #workspaceFiles;
1563
+ #workspaceId;
1564
+ #workspaceApi;
1565
+ #settleDeadlineMs;
1566
+ #settleRetryDelayMs;
1110
1567
  constructor(settings) {
1568
+ assertSupportedAiVersion();
1111
1569
  if (settings.client) {
1112
1570
  this.#client = settings.client;
1113
1571
  } else if (settings.baseUrl && settings.token) {
@@ -1124,6 +1582,10 @@ var CoderAgent = class {
1124
1582
  }
1125
1583
  this.#organizationId = settings.organizationId;
1126
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);
1127
1589
  if (settings.workspaceFiles && settings.workspaceId && settings.workspaceFiles.workspaceId !== settings.workspaceId) {
1128
1590
  throw new CoderAgentError(
1129
1591
  `workspaceFiles.workspaceId (${settings.workspaceFiles.workspaceId}) does not match the agent's workspaceId (${settings.workspaceId}); the chat's tools would not see uploaded files.`
@@ -1182,22 +1644,41 @@ var CoderAgent = class {
1182
1644
  resetSession() {
1183
1645
  this.#model.resetSession();
1184
1646
  }
1185
- /** Interrupt the in-flight generation, if any. */
1186
- async interrupt() {
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) {
1187
1653
  const id = this.#model.chatId;
1188
- if (id) await this.#client.interruptChat(id);
1654
+ if (id) await this.#client.interruptChat(id, opts?.signal);
1189
1655
  }
1190
- /** Archive the underlying chat (safe cleanup; hides it from listings). */
1191
- async archive() {
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) {
1192
1666
  const id = this.#model.chatId;
1193
- if (id) await this.#client.archiveChat(id);
1667
+ if (!id) return;
1668
+ await archiveWhenSettled(this.#client, id, {
1669
+ deadlineMs: this.#settleDeadlineMs,
1670
+ retryDelayMs: this.#settleRetryDelayMs,
1671
+ signal: opts?.signal
1672
+ });
1194
1673
  }
1195
1674
  /**
1196
1675
  * Clean up the chat when the agent leaves an `await using` scope, so cleanup
1197
1676
  * rides scope exit instead of a separate call you have to remember in a
1198
- * `finally`. Interrupts any in-flight server run, then archives the chat.
1199
- * Best-effort: disposal errors are swallowed so they can't mask the scope's
1200
- * own error.
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.
1201
1682
  *
1202
1683
  * @example
1203
1684
  * ```ts
@@ -1207,12 +1688,13 @@ var CoderAgent = class {
1207
1688
  * ```
1208
1689
  */
1209
1690
  async [Symbol.asyncDispose]() {
1691
+ const deadline = AbortSignal.timeout(this.#settleDeadlineMs);
1210
1692
  try {
1211
- await this.interrupt();
1693
+ await this.interrupt({ signal: deadline });
1212
1694
  } catch {
1213
1695
  }
1214
1696
  try {
1215
- await this.archive();
1697
+ await this.archive({ signal: deadline });
1216
1698
  } catch {
1217
1699
  }
1218
1700
  }
@@ -1256,7 +1738,158 @@ var CoderAgent = class {
1256
1738
  });
1257
1739
  return { workspaceId: this.#workspaceFiles.workspaceId, path };
1258
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
+ }
1259
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
+ }
1260
1893
  export {
1261
1894
  CHAT_ATTACHMENT_MEDIA_TYPES,
1262
1895
  CODER_PROVIDER_OPTIONS,
@@ -1268,6 +1901,7 @@ export {
1268
1901
  CoderLanguageModel,
1269
1902
  MAX_CHAT_FILE_SIZE_BYTES,
1270
1903
  TurnTranslator,
1904
+ chatMessagesToUIMessages,
1271
1905
  classifyTurnAction,
1272
1906
  dataContentToFileContent,
1273
1907
  dynamicToolNames,
@@ -1275,6 +1909,7 @@ export {
1275
1909
  resolveFileContent,
1276
1910
  streamChatEvents,
1277
1911
  toolsToDynamicTools,
1278
- userContentToInputParts
1912
+ userContentToInputParts,
1913
+ watchChatEvents
1279
1914
  };
1280
1915
  //# sourceMappingURL=index.js.map