@coder/ai-sdk-agent 0.1.0 → 0.2.1

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,7 +42,70 @@ var CoderChatError = class extends CoderAgentError {
42
42
  }
43
43
  };
44
44
 
45
+ // src/files.ts
46
+ function isBlob(v) {
47
+ return typeof Blob !== "undefined" && v instanceof Blob;
48
+ }
49
+ function isReadableStream(v) {
50
+ return typeof ReadableStream !== "undefined" && v instanceof ReadableStream;
51
+ }
52
+ function resolveFileContent(content, opts) {
53
+ let mediaType = opts?.mediaType;
54
+ let name = opts?.name;
55
+ let body;
56
+ let size;
57
+ if (isBlob(content)) {
58
+ body = content;
59
+ size = content.size;
60
+ if (!mediaType && content.type) mediaType = content.type;
61
+ const maybeName = content.name;
62
+ if (!name && typeof maybeName === "string") name = maybeName;
63
+ } else if (isReadableStream(content)) {
64
+ body = content;
65
+ size = void 0;
66
+ } else if (content instanceof Uint8Array || content instanceof ArrayBuffer) {
67
+ size = content.byteLength;
68
+ body = new Blob([content]);
69
+ } else {
70
+ throw new CoderAgentError(
71
+ "Unsupported file content: expected a Blob/File, ReadableStream, Uint8Array, or ArrayBuffer."
72
+ );
73
+ }
74
+ if (!mediaType) {
75
+ throw new CoderAgentError(
76
+ "Could not determine the file's media type. Pass `mediaType`, or provide a Blob/File that carries its own `type`."
77
+ );
78
+ }
79
+ return { body, mediaType, name, size };
80
+ }
81
+ function base64ToBytes(b64) {
82
+ if (typeof Buffer !== "undefined") return Buffer.from(b64, "base64");
83
+ const bin = atob(b64);
84
+ const bytes = new Uint8Array(bin.length);
85
+ for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
86
+ return bytes;
87
+ }
88
+ function dataContentToFileContent(data) {
89
+ if (data instanceof Uint8Array) return data;
90
+ if (typeof data === "string") return base64ToBytes(data);
91
+ throw new CoderAgentError(
92
+ "File parts referencing a URL are not supported; provide the file bytes instead."
93
+ );
94
+ }
95
+
45
96
  // src/coder/types.ts
97
+ var CHAT_ATTACHMENT_MEDIA_TYPES = /* @__PURE__ */ new Set([
98
+ "application/json",
99
+ "application/pdf",
100
+ "image/gif",
101
+ "image/jpeg",
102
+ "image/png",
103
+ "image/webp",
104
+ "text/csv",
105
+ "text/markdown",
106
+ "text/plain"
107
+ ]);
108
+ var MAX_CHAT_FILE_SIZE_BYTES = 10 * 1024 * 1024;
46
109
  var TERMINAL_STATUSES = /* @__PURE__ */ new Set([
47
110
  "waiting",
48
111
  "completed",
@@ -162,24 +225,23 @@ var CoderChatClient = class {
162
225
  this.#fetch = options.fetch ?? globalThis.fetch.bind(globalThis);
163
226
  this.#webSocketFactory = options.webSocketFactory;
164
227
  }
165
- async #request(method, path, body, signal) {
166
- const headers = { "Coder-Session-Token": this.#token };
167
- if (body !== void 0) headers["Content-Type"] = "application/json";
168
- const res = await this.#fetch(`${this.baseUrl}${path}`, {
169
- method,
170
- headers,
171
- body: body === void 0 ? void 0 : JSON.stringify(body),
172
- signal
173
- });
174
- const text = await res.text();
175
- let parsed;
176
- try {
177
- parsed = text.length > 0 ? JSON.parse(text) : void 0;
178
- } catch {
179
- parsed = void 0;
228
+ /**
229
+ * Issue a request and return the raw `Response` on success (body unread). On a
230
+ * non-2xx status the body is consumed to build a {@link CoderApiError}. Shared
231
+ * by JSON requests and the raw upload/download endpoints.
232
+ */
233
+ async #send(method, path, opts) {
234
+ const headers = {
235
+ "Coder-Session-Token": this.#token,
236
+ ...opts?.headers
237
+ };
238
+ const init = { method, headers, body: opts?.body, signal: opts?.signal };
239
+ if (typeof ReadableStream !== "undefined" && opts?.body instanceof ReadableStream) {
240
+ init.duplex = "half";
180
241
  }
242
+ const res = await this.#fetch(`${this.baseUrl}${path}`, init);
181
243
  if (!res.ok) {
182
- const errObj = parsed ?? {};
244
+ const errObj = await this.#json(res) ?? {};
183
245
  throw new CoderApiError({
184
246
  status: res.status,
185
247
  method,
@@ -188,7 +250,25 @@ var CoderChatClient = class {
188
250
  detail: errObj.detail
189
251
  });
190
252
  }
191
- return parsed;
253
+ return res;
254
+ }
255
+ /** Read a response body as JSON, tolerating an unreadable/empty/malformed body (→ undefined). */
256
+ async #json(res) {
257
+ const text = await res.text().catch(() => "");
258
+ if (text.length === 0) return void 0;
259
+ try {
260
+ return JSON.parse(text);
261
+ } catch {
262
+ return void 0;
263
+ }
264
+ }
265
+ async #request(method, path, body, signal) {
266
+ const res = await this.#send(method, path, {
267
+ body: body === void 0 ? void 0 : JSON.stringify(body),
268
+ headers: body === void 0 ? void 0 : { "Content-Type": "application/json" },
269
+ signal
270
+ });
271
+ return await this.#json(res);
192
272
  }
193
273
  // --- REST -----------------------------------------------------------------
194
274
  listModelConfigs(signal) {
@@ -239,6 +319,66 @@ var CoderChatClient = class {
239
319
  archiveChat(chatId, signal) {
240
320
  return this.updateChat(chatId, { archived: true }, signal);
241
321
  }
322
+ // --- Files ----------------------------------------------------------------
323
+ /**
324
+ * Upload a file as a durable chat attachment, returning its id (referenced
325
+ * from message content via a `file` input part). The server enforces a narrow
326
+ * media-type allowlist and a 10 MiB cap; both are checked client-side first so
327
+ * unsupported files fail fast with a clear error instead of an opaque 4xx.
328
+ *
329
+ * The body is sent raw (not multipart): `Content-Type` carries the media type
330
+ * and `Content-Disposition` the filename, mirroring `codersdk`.
331
+ */
332
+ async uploadChatFile(organizationId, file, signal) {
333
+ const resolved = resolveFileContent(file.content, {
334
+ mediaType: file.mediaType,
335
+ name: file.name
336
+ });
337
+ const mediaType = (resolved.mediaType.split(";")[0] ?? resolved.mediaType).trim().toLowerCase();
338
+ if (!CHAT_ATTACHMENT_MEDIA_TYPES.has(mediaType)) {
339
+ throw new CoderAgentError(
340
+ `Media type "${mediaType}" is not allowed for chat attachments (allowed: ${[...CHAT_ATTACHMENT_MEDIA_TYPES].join(", ")}). Write other file types to a workspace instead.`
341
+ );
342
+ }
343
+ if (resolved.size !== void 0 && resolved.size > MAX_CHAT_FILE_SIZE_BYTES) {
344
+ throw new CoderAgentError(
345
+ `File is ${resolved.size} bytes, over the ${MAX_CHAT_FILE_SIZE_BYTES}-byte (10 MiB) chat attachment limit. Write large files to a workspace instead.`
346
+ );
347
+ }
348
+ const headers = { "Content-Type": mediaType };
349
+ if (resolved.name) {
350
+ const ascii = resolved.name.replace(/[^\x20-\x7e]/g, "_").replace(/(["\\])/g, "\\$1");
351
+ const utf8 = encodeURIComponent(resolved.name).replace(
352
+ /['()*]/g,
353
+ (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`
354
+ );
355
+ headers["Content-Disposition"] = `attachment; filename="${ascii}"; filename*=UTF-8''${utf8}`;
356
+ }
357
+ const res = await this.#send(
358
+ "POST",
359
+ `${API_PREFIX}/files?organization=${encodeURIComponent(organizationId)}`,
360
+ { body: resolved.body, headers, signal }
361
+ );
362
+ const parsed = await this.#json(res);
363
+ if (!parsed?.id) {
364
+ throw new CoderApiError({
365
+ status: res.status,
366
+ method: "POST",
367
+ path: `${API_PREFIX}/files`,
368
+ message: "upload succeeded but the response contained no file id"
369
+ });
370
+ }
371
+ return { id: parsed.id, mediaType, name: resolved.name };
372
+ }
373
+ /** Download a chat file's bytes and media type by id. */
374
+ async getChatFile(fileId, signal) {
375
+ const res = await this.#send("GET", `${API_PREFIX}/files/${fileId}`, { signal });
376
+ const buf = await res.arrayBuffer();
377
+ return {
378
+ bytes: new Uint8Array(buf),
379
+ mediaType: res.headers.get("Content-Type") ?? "application/octet-stream"
380
+ };
381
+ }
242
382
  // --- Streaming ------------------------------------------------------------
243
383
  streamEvents(chatId, opts) {
244
384
  return streamChatEvents({
@@ -283,6 +423,7 @@ var CoderChatClient = class {
283
423
  };
284
424
 
285
425
  // src/model/prompt.ts
426
+ var CODER_PROVIDER_OPTIONS = "coder";
286
427
  function extractSystemPrompt(prompt) {
287
428
  const parts = prompt.filter(
288
429
  (m) => m.role === "system"
@@ -291,14 +432,28 @@ function extractSystemPrompt(prompt) {
291
432
  const joined = parts.map((m) => m.content).join("\n\n").trim();
292
433
  return joined.length > 0 ? joined : void 0;
293
434
  }
294
- function userContentToInputParts(message) {
295
- const out = [];
296
- for (const part of message.content) {
297
- if (part.type === "text" && part.text.length > 0) {
298
- out.push({ type: "text", text: part.text });
299
- }
300
- }
301
- return out;
435
+ function coderFileId(providerOptions) {
436
+ const ns = providerOptions?.[CODER_PROVIDER_OPTIONS];
437
+ return typeof ns?.fileId === "string" && ns.fileId.length > 0 ? ns.fileId : void 0;
438
+ }
439
+ async function userContentToInputParts(content, uploadFile) {
440
+ const parts = await Promise.all(
441
+ content.map(async (part) => {
442
+ if (part.type === "text") {
443
+ return part.text.length > 0 ? { type: "text", text: part.text } : null;
444
+ }
445
+ if (part.type === "file") {
446
+ const fileId = coderFileId(part.providerOptions) ?? await uploadFile({
447
+ data: part.data,
448
+ mediaType: part.mediaType,
449
+ filename: part.filename
450
+ });
451
+ return { type: "file", file_id: fileId };
452
+ }
453
+ return null;
454
+ })
455
+ );
456
+ return parts.filter((p) => p !== null);
302
457
  }
303
458
  function toolResultOutputToChatd(output) {
304
459
  switch (output.type) {
@@ -336,7 +491,7 @@ function classifyTurnAction(prompt) {
336
491
  return { kind: "resume", toolResults };
337
492
  }
338
493
  if (last.role === "user") {
339
- return { kind: "new-turn", content: userContentToInputParts(last) };
494
+ return { kind: "new-turn", content: last.content };
340
495
  }
341
496
  return { kind: "noop" };
342
497
  }
@@ -476,14 +631,21 @@ var TurnTranslator = class {
476
631
  if (part.args === void 0) return;
477
632
  if (this.#serverToolCalls.has(id)) return;
478
633
  this.#serverToolCalls.add(id);
479
- out.push({ type: "tool-input-start", id, toolName: name, providerExecuted: true });
634
+ out.push({
635
+ type: "tool-input-start",
636
+ id,
637
+ toolName: name,
638
+ providerExecuted: true,
639
+ dynamic: true
640
+ });
480
641
  out.push({ type: "tool-input-end", id });
481
642
  out.push({
482
643
  type: "tool-call",
483
644
  toolCallId: id,
484
645
  toolName: name,
485
646
  input: typeof part.args === "string" ? part.args : JSON.stringify(part.args),
486
- providerExecuted: true
647
+ providerExecuted: true,
648
+ dynamic: true
487
649
  });
488
650
  }
489
651
  #emitServerToolResult(out, part) {
@@ -492,13 +654,17 @@ var TurnTranslator = class {
492
654
  if (!id || !name) return;
493
655
  if (part.result === void 0) return;
494
656
  if (this.#serverToolResults.has(id)) return;
657
+ if (!this.#serverToolCalls.has(id)) return;
495
658
  this.#serverToolResults.add(id);
496
659
  out.push({
497
660
  type: "tool-result",
498
661
  toolCallId: id,
499
662
  toolName: name,
500
663
  result: jsonResult(part.result),
501
- isError: part.is_error ?? false
664
+ isError: part.is_error ?? false,
665
+ // Mirror the call's `dynamic: true` so call and result land in the same
666
+ // bucket (steps[*].dynamicToolCalls / dynamicToolResults, UI streams).
667
+ dynamic: true
502
668
  });
503
669
  }
504
670
  // --- ingest ---------------------------------------------------------------
@@ -678,6 +844,23 @@ var CoderLanguageModel = class {
678
844
  this.#modelResolved = true;
679
845
  return this.#resolvedModelConfigId;
680
846
  }
847
+ /**
848
+ * Resolve a user message's content to chatd input parts, uploading any file
849
+ * parts to chat-file storage (the upload endpoint needs only the organization
850
+ * id, so this runs before the chat exists). Pre-uploaded files carried via
851
+ * `providerOptions.coder.fileId` are referenced without re-uploading.
852
+ */
853
+ #buildContent(content, signal) {
854
+ const uploadFile = async (f) => {
855
+ const uploaded = await this.#config.client.uploadChatFile(
856
+ this.#config.organizationId,
857
+ { content: dataContentToFileContent(f.data), mediaType: f.mediaType, name: f.filename },
858
+ signal
859
+ );
860
+ return uploaded.id;
861
+ };
862
+ return userContentToInputParts(content, uploadFile);
863
+ }
681
864
  async *#runTurn(options) {
682
865
  if (this.#inFlight) {
683
866
  throw new CoderAgentError(
@@ -685,23 +868,62 @@ var CoderLanguageModel = class {
685
868
  );
686
869
  }
687
870
  this.#inFlight = true;
871
+ const externalSignal = options.abortSignal;
872
+ const timeoutMs = this.#config.requestTimeoutMs;
873
+ const timeoutSignal = timeoutMs !== void 0 && timeoutMs > 0 ? AbortSignal.timeout(timeoutMs) : void 0;
874
+ const sources = [externalSignal, timeoutSignal].filter(
875
+ (s) => s !== void 0
876
+ );
877
+ const signal = sources.length > 0 ? AbortSignal.any(sources) : void 0;
878
+ const translator = new TurnTranslator({ dynamicToolNames: dynamicToolNames(options.tools) });
879
+ let interruptSent = false;
880
+ const interrupt = () => {
881
+ const id = this.#chatId;
882
+ if (interruptSent || !id) return;
883
+ interruptSent = true;
884
+ void this.#config.client.interruptChat(id).catch(() => {
885
+ });
886
+ };
887
+ signal?.addEventListener("abort", interrupt, { once: true });
888
+ const throwIfAborted = () => {
889
+ if (timeoutSignal?.aborted) {
890
+ throw new CoderChatError({
891
+ message: `Coder Agent turn exceeded its ${timeoutMs}ms requestTimeoutMs budget.`,
892
+ kind: "timeout",
893
+ retryable: true
894
+ });
895
+ }
896
+ if (signal?.aborted) {
897
+ throw externalSignal?.reason ?? new DOMException("The operation was aborted.", "AbortError");
898
+ }
899
+ };
688
900
  try {
689
- const { prompt, abortSignal: signal } = options;
690
- yield { type: "stream-start", warnings: [] };
901
+ const { prompt } = options;
902
+ const warnings = [];
903
+ if (options.responseFormat?.type === "json") {
904
+ warnings.push({
905
+ type: "unsupported",
906
+ feature: "responseFormat",
907
+ details: "Coder Agents does not enforce a JSON schema server-side, so structured output is best-effort (not schema-constrained). For reliable structured output, use @coder/ai-sdk-provider (createCoder) with generateObject / Output.object."
908
+ });
909
+ }
910
+ yield { type: "stream-start", warnings };
691
911
  const action = classifyTurnAction(prompt);
692
912
  if (action.kind === "noop") {
693
913
  throw new CoderAgentError(
694
914
  "CoderAgent received a prompt with no user message or tool results to act on."
695
915
  );
696
916
  }
697
- const translator = new TurnTranslator({ dynamicToolNames: dynamicToolNames(options.tools) });
698
917
  let afterId;
699
918
  if (action.kind === "new-turn") {
700
- const modelConfigId = await this.#resolveModelConfigId(signal);
919
+ const [modelConfigId, content] = await Promise.all([
920
+ this.#resolveModelConfigId(signal),
921
+ this.#buildContent(action.content, signal)
922
+ ]);
701
923
  if (!this.#chatId) {
702
924
  const req = {
703
925
  organization_id: this.#config.organizationId,
704
- content: action.content,
926
+ content,
705
927
  client_type: "api"
706
928
  };
707
929
  const system = extractSystemPrompt(prompt);
@@ -719,7 +941,7 @@ var CoderLanguageModel = class {
719
941
  const resp = await this.#config.client.createChatMessage(
720
942
  this.#chatId,
721
943
  {
722
- content: action.content,
944
+ content,
723
945
  ...modelConfigId ? { model_config_id: modelConfigId } : {}
724
946
  },
725
947
  signal
@@ -740,27 +962,51 @@ var CoderLanguageModel = class {
740
962
  }
741
963
  const chatId = this.#chatId;
742
964
  let sinceRequiresAction = 0;
743
- for await (const ev of this.#config.client.streamEvents(chatId, { afterId, signal })) {
744
- for (const part of translator.ingest(ev)) yield part;
745
- const status = translator.terminalStatus;
746
- if (status) {
747
- if (status !== "requires_action") break;
748
- if (translator.clientToolCallSeen) break;
749
- if (++sinceRequiresAction > 200) break;
965
+ try {
966
+ for await (const ev of this.#config.client.streamEvents(chatId, { afterId, signal })) {
967
+ for (const part of translator.ingest(ev)) yield part;
968
+ const status = translator.terminalStatus;
969
+ if (status) {
970
+ if (status !== "requires_action") break;
971
+ if (translator.clientToolCallSeen) break;
972
+ if (++sinceRequiresAction > 200) break;
973
+ }
974
+ }
975
+ } catch (err) {
976
+ throwIfAborted();
977
+ if (err instanceof CoderAgentError && !(err instanceof CoderApiError) && !(err instanceof CoderChatError)) {
978
+ throw new CoderChatError({
979
+ message: `Coder chat stream failed mid-turn: ${err.message}`,
980
+ kind: "stream_closed",
981
+ retryable: true
982
+ });
750
983
  }
984
+ throw err;
751
985
  }
752
- if (signal?.aborted && !translator.terminalStatus) {
753
- throw signal.reason ?? new DOMException("The operation was aborted.", "AbortError");
986
+ throwIfAborted();
987
+ if (!translator.terminalStatus && !translator.error) {
988
+ throw new CoderChatError({
989
+ message: "Coder chat stream ended before the turn settled (connection closed or the server ended the stream without a terminal status).",
990
+ kind: "stream_closed",
991
+ retryable: true
992
+ });
754
993
  }
755
994
  for (const part of translator.finish()) yield part;
995
+ } catch (err) {
996
+ throwIfAborted();
997
+ throw err;
998
+ } finally {
756
999
  if (translator.maxMessageId > this.#lastSeenMessageId)
757
1000
  this.#lastSeenMessageId = translator.maxMessageId;
758
- } finally {
1001
+ signal?.removeEventListener("abort", interrupt);
1002
+ if (!translator.terminalStatus) interrupt();
759
1003
  this.#inFlight = false;
760
1004
  }
761
1005
  }
762
1006
  async doStream(options) {
763
- const gen = this.#runTurn(options);
1007
+ const cancelController = new AbortController();
1008
+ const abortSignal = options.abortSignal ? AbortSignal.any([options.abortSignal, cancelController.signal]) : cancelController.signal;
1009
+ const gen = this.#runTurn({ ...options, abortSignal });
764
1010
  const stream = new ReadableStream({
765
1011
  async pull(controller) {
766
1012
  try {
@@ -768,11 +1014,14 @@ var CoderLanguageModel = class {
768
1014
  if (done) controller.close();
769
1015
  else controller.enqueue(value);
770
1016
  } catch (err) {
771
- controller.error(err);
1017
+ if (cancelController.signal.aborted) controller.close();
1018
+ else controller.error(err);
772
1019
  }
773
1020
  },
774
1021
  async cancel() {
775
- await gen.return();
1022
+ cancelController.abort();
1023
+ await gen.return().catch(() => {
1024
+ });
776
1025
  }
777
1026
  });
778
1027
  return { stream };
@@ -837,12 +1086,27 @@ var CoderLanguageModel = class {
837
1086
  };
838
1087
 
839
1088
  // src/agent/coder-agent.ts
1089
+ function makeChatAttachment(file) {
1090
+ return {
1091
+ ...file,
1092
+ toFilePart: () => ({
1093
+ type: "file",
1094
+ // Bytes are unused: the reference travels in providerOptions.coder.fileId.
1095
+ data: "",
1096
+ mediaType: file.mediaType,
1097
+ ...file.name ? { filename: file.name } : {},
1098
+ providerOptions: { [CODER_PROVIDER_OPTIONS]: { fileId: file.id } }
1099
+ })
1100
+ };
1101
+ }
840
1102
  var DEFAULT_STOP = stepCountIs(64);
841
1103
  var CoderAgent = class {
842
1104
  version = "agent-v1";
843
1105
  #client;
844
1106
  #model;
845
1107
  #inner;
1108
+ #organizationId;
1109
+ #workspaceFiles;
846
1110
  constructor(settings) {
847
1111
  if (settings.client) {
848
1112
  this.#client = settings.client;
@@ -858,6 +1122,13 @@ var CoderAgent = class {
858
1122
  "CoderAgent requires either `client` or both `baseUrl` and `token`."
859
1123
  );
860
1124
  }
1125
+ this.#organizationId = settings.organizationId;
1126
+ this.#workspaceFiles = settings.workspaceFiles;
1127
+ if (settings.workspaceFiles && settings.workspaceId && settings.workspaceFiles.workspaceId !== settings.workspaceId) {
1128
+ throw new CoderAgentError(
1129
+ `workspaceFiles.workspaceId (${settings.workspaceFiles.workspaceId}) does not match the agent's workspaceId (${settings.workspaceId}); the chat's tools would not see uploaded files.`
1130
+ );
1131
+ }
861
1132
  this.#model = new CoderLanguageModel({
862
1133
  client: this.#client,
863
1134
  organizationId: settings.organizationId,
@@ -865,7 +1136,8 @@ var CoderAgent = class {
865
1136
  workspaceId: settings.workspaceId,
866
1137
  mcpServerIds: settings.mcpServerIds,
867
1138
  planMode: settings.planMode,
868
- chatId: settings.chatId
1139
+ chatId: settings.chatId,
1140
+ requestTimeoutMs: settings.requestTimeoutMs
869
1141
  });
870
1142
  this.#inner = new ToolLoopAgent({
871
1143
  model: this.#model,
@@ -898,6 +1170,14 @@ var CoderAgent = class {
898
1170
  return this.#inner.stream(options);
899
1171
  }
900
1172
  // --- session helpers ------------------------------------------------------
1173
+ /**
1174
+ * List the model configs available on the deployment. Use this to discover
1175
+ * valid values for the `model` hint instead of guessing ids — match on
1176
+ * `id`, `provider`/`model`, or `display_name`.
1177
+ */
1178
+ listModels(signal) {
1179
+ return this.#client.listModelConfigs(signal);
1180
+ }
901
1181
  /** Start a fresh chatd chat on the next turn. */
902
1182
  resetSession() {
903
1183
  this.#model.resetSession();
@@ -912,19 +1192,89 @@ var CoderAgent = class {
912
1192
  const id = this.#model.chatId;
913
1193
  if (id) await this.#client.archiveChat(id);
914
1194
  }
1195
+ /**
1196
+ * Clean up the chat when the agent leaves an `await using` scope, so cleanup
1197
+ * 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.
1201
+ *
1202
+ * @example
1203
+ * ```ts
1204
+ * await using agent = new CoderAgent({ ... });
1205
+ * const { text } = await agent.generate({ prompt: "…" });
1206
+ * // agent.interrupt() + agent.archive() run automatically here.
1207
+ * ```
1208
+ */
1209
+ async [Symbol.asyncDispose]() {
1210
+ try {
1211
+ await this.interrupt();
1212
+ } catch {
1213
+ }
1214
+ try {
1215
+ await this.archive();
1216
+ } catch {
1217
+ }
1218
+ }
1219
+ // --- files ----------------------------------------------------------------
1220
+ /**
1221
+ * Upload a file as a chat attachment — content for the model to *read* (a PDF,
1222
+ * image, CSV, …). Returns a handle you can reference from a later turn via
1223
+ * {@link ChatAttachment.toFilePart} (no re-upload). Validated against the chat
1224
+ * allowlist and 10 MiB cap up front.
1225
+ *
1226
+ * You usually don't need this: a native AI SDK `file` part dropped into a
1227
+ * message's `content` is uploaded transparently. Reach for `attach()` to
1228
+ * pre-upload, to reuse one file across turns, or to validate before sending.
1229
+ *
1230
+ * For large or non-allowlisted files (zips, datasets, binaries) — material for
1231
+ * the agent to *operate on* — use {@link CoderAgent.uploadToWorkspace}.
1232
+ */
1233
+ async attach(file, signal) {
1234
+ const uploaded = await this.#client.uploadChatFile(this.#organizationId, file, signal);
1235
+ return makeChatAttachment(uploaded);
1236
+ }
1237
+ /**
1238
+ * Write a file onto the bound workspace's filesystem for the agent to operate
1239
+ * on (extract, build, read with its tools). Requires a {@link WorkspaceFileStore}
1240
+ * via the `workspaceFiles` setting. The file is written as-is — no unpacking;
1241
+ * instruct the agent to `unzip`/`tar -x`, or extract over your own connection.
1242
+ *
1243
+ * The chat should be bound to the same workspace (`workspaceId`) so the agent's
1244
+ * server-side tools can see the file.
1245
+ */
1246
+ async uploadToWorkspace(file) {
1247
+ if (!this.#workspaceFiles) {
1248
+ throw new CoderAgentError(
1249
+ "uploadToWorkspace requires a `workspaceFiles` adapter. Construct the agent with `workspaceFiles` (e.g. backed by a @coder/ai-sdk-eve-sandbox session) to enable it."
1250
+ );
1251
+ }
1252
+ const { path } = await this.#workspaceFiles.write({
1253
+ content: file.content,
1254
+ path: file.path,
1255
+ signal: file.signal
1256
+ });
1257
+ return { workspaceId: this.#workspaceFiles.workspaceId, path };
1258
+ }
915
1259
  };
916
1260
  export {
1261
+ CHAT_ATTACHMENT_MEDIA_TYPES,
1262
+ CODER_PROVIDER_OPTIONS,
917
1263
  CoderAgent,
918
1264
  CoderAgentError,
919
1265
  CoderApiError,
920
1266
  CoderChatClient,
921
1267
  CoderChatError,
922
1268
  CoderLanguageModel,
1269
+ MAX_CHAT_FILE_SIZE_BYTES,
923
1270
  TurnTranslator,
924
1271
  classifyTurnAction,
1272
+ dataContentToFileContent,
925
1273
  dynamicToolNames,
926
1274
  extractSystemPrompt,
1275
+ resolveFileContent,
927
1276
  streamChatEvents,
928
- toolsToDynamicTools
1277
+ toolsToDynamicTools,
1278
+ userContentToInputParts
929
1279
  };
930
1280
  //# sourceMappingURL=index.js.map