@coder/ai-sdk-agent 0.1.0 → 0.2.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 +227 -17
- package/dist/index.d.ts +264 -4
- package/dist/index.js +386 -47
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
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
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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 =
|
|
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
|
|
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
|
|
295
|
-
const
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
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:
|
|
494
|
+
return { kind: "new-turn", content: last.content };
|
|
340
495
|
}
|
|
341
496
|
return { kind: "noop" };
|
|
342
497
|
}
|
|
@@ -678,6 +833,23 @@ var CoderLanguageModel = class {
|
|
|
678
833
|
this.#modelResolved = true;
|
|
679
834
|
return this.#resolvedModelConfigId;
|
|
680
835
|
}
|
|
836
|
+
/**
|
|
837
|
+
* Resolve a user message's content to chatd input parts, uploading any file
|
|
838
|
+
* parts to chat-file storage (the upload endpoint needs only the organization
|
|
839
|
+
* id, so this runs before the chat exists). Pre-uploaded files carried via
|
|
840
|
+
* `providerOptions.coder.fileId` are referenced without re-uploading.
|
|
841
|
+
*/
|
|
842
|
+
#buildContent(content, signal) {
|
|
843
|
+
const uploadFile = async (f) => {
|
|
844
|
+
const uploaded = await this.#config.client.uploadChatFile(
|
|
845
|
+
this.#config.organizationId,
|
|
846
|
+
{ content: dataContentToFileContent(f.data), mediaType: f.mediaType, name: f.filename },
|
|
847
|
+
signal
|
|
848
|
+
);
|
|
849
|
+
return uploaded.id;
|
|
850
|
+
};
|
|
851
|
+
return userContentToInputParts(content, uploadFile);
|
|
852
|
+
}
|
|
681
853
|
async *#runTurn(options) {
|
|
682
854
|
if (this.#inFlight) {
|
|
683
855
|
throw new CoderAgentError(
|
|
@@ -685,23 +857,62 @@ var CoderLanguageModel = class {
|
|
|
685
857
|
);
|
|
686
858
|
}
|
|
687
859
|
this.#inFlight = true;
|
|
860
|
+
const externalSignal = options.abortSignal;
|
|
861
|
+
const timeoutMs = this.#config.requestTimeoutMs;
|
|
862
|
+
const timeoutSignal = timeoutMs !== void 0 && timeoutMs > 0 ? AbortSignal.timeout(timeoutMs) : void 0;
|
|
863
|
+
const sources = [externalSignal, timeoutSignal].filter(
|
|
864
|
+
(s) => s !== void 0
|
|
865
|
+
);
|
|
866
|
+
const signal = sources.length > 0 ? AbortSignal.any(sources) : void 0;
|
|
867
|
+
const translator = new TurnTranslator({ dynamicToolNames: dynamicToolNames(options.tools) });
|
|
868
|
+
let interruptSent = false;
|
|
869
|
+
const interrupt = () => {
|
|
870
|
+
const id = this.#chatId;
|
|
871
|
+
if (interruptSent || !id) return;
|
|
872
|
+
interruptSent = true;
|
|
873
|
+
void this.#config.client.interruptChat(id).catch(() => {
|
|
874
|
+
});
|
|
875
|
+
};
|
|
876
|
+
signal?.addEventListener("abort", interrupt, { once: true });
|
|
877
|
+
const throwIfAborted = () => {
|
|
878
|
+
if (timeoutSignal?.aborted) {
|
|
879
|
+
throw new CoderChatError({
|
|
880
|
+
message: `Coder Agent turn exceeded its ${timeoutMs}ms requestTimeoutMs budget.`,
|
|
881
|
+
kind: "timeout",
|
|
882
|
+
retryable: true
|
|
883
|
+
});
|
|
884
|
+
}
|
|
885
|
+
if (signal?.aborted) {
|
|
886
|
+
throw externalSignal?.reason ?? new DOMException("The operation was aborted.", "AbortError");
|
|
887
|
+
}
|
|
888
|
+
};
|
|
688
889
|
try {
|
|
689
|
-
const { prompt
|
|
690
|
-
|
|
890
|
+
const { prompt } = options;
|
|
891
|
+
const warnings = [];
|
|
892
|
+
if (options.responseFormat?.type === "json") {
|
|
893
|
+
warnings.push({
|
|
894
|
+
type: "unsupported",
|
|
895
|
+
feature: "responseFormat",
|
|
896
|
+
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."
|
|
897
|
+
});
|
|
898
|
+
}
|
|
899
|
+
yield { type: "stream-start", warnings };
|
|
691
900
|
const action = classifyTurnAction(prompt);
|
|
692
901
|
if (action.kind === "noop") {
|
|
693
902
|
throw new CoderAgentError(
|
|
694
903
|
"CoderAgent received a prompt with no user message or tool results to act on."
|
|
695
904
|
);
|
|
696
905
|
}
|
|
697
|
-
const translator = new TurnTranslator({ dynamicToolNames: dynamicToolNames(options.tools) });
|
|
698
906
|
let afterId;
|
|
699
907
|
if (action.kind === "new-turn") {
|
|
700
|
-
const modelConfigId = await
|
|
908
|
+
const [modelConfigId, content] = await Promise.all([
|
|
909
|
+
this.#resolveModelConfigId(signal),
|
|
910
|
+
this.#buildContent(action.content, signal)
|
|
911
|
+
]);
|
|
701
912
|
if (!this.#chatId) {
|
|
702
913
|
const req = {
|
|
703
914
|
organization_id: this.#config.organizationId,
|
|
704
|
-
content
|
|
915
|
+
content,
|
|
705
916
|
client_type: "api"
|
|
706
917
|
};
|
|
707
918
|
const system = extractSystemPrompt(prompt);
|
|
@@ -719,7 +930,7 @@ var CoderLanguageModel = class {
|
|
|
719
930
|
const resp = await this.#config.client.createChatMessage(
|
|
720
931
|
this.#chatId,
|
|
721
932
|
{
|
|
722
|
-
content
|
|
933
|
+
content,
|
|
723
934
|
...modelConfigId ? { model_config_id: modelConfigId } : {}
|
|
724
935
|
},
|
|
725
936
|
signal
|
|
@@ -740,27 +951,51 @@ var CoderLanguageModel = class {
|
|
|
740
951
|
}
|
|
741
952
|
const chatId = this.#chatId;
|
|
742
953
|
let sinceRequiresAction = 0;
|
|
743
|
-
|
|
744
|
-
for (const
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
if (status
|
|
748
|
-
|
|
749
|
-
|
|
954
|
+
try {
|
|
955
|
+
for await (const ev of this.#config.client.streamEvents(chatId, { afterId, signal })) {
|
|
956
|
+
for (const part of translator.ingest(ev)) yield part;
|
|
957
|
+
const status = translator.terminalStatus;
|
|
958
|
+
if (status) {
|
|
959
|
+
if (status !== "requires_action") break;
|
|
960
|
+
if (translator.clientToolCallSeen) break;
|
|
961
|
+
if (++sinceRequiresAction > 200) break;
|
|
962
|
+
}
|
|
750
963
|
}
|
|
964
|
+
} catch (err) {
|
|
965
|
+
throwIfAborted();
|
|
966
|
+
if (err instanceof CoderAgentError && !(err instanceof CoderApiError) && !(err instanceof CoderChatError)) {
|
|
967
|
+
throw new CoderChatError({
|
|
968
|
+
message: `Coder chat stream failed mid-turn: ${err.message}`,
|
|
969
|
+
kind: "stream_closed",
|
|
970
|
+
retryable: true
|
|
971
|
+
});
|
|
972
|
+
}
|
|
973
|
+
throw err;
|
|
751
974
|
}
|
|
752
|
-
|
|
753
|
-
|
|
975
|
+
throwIfAborted();
|
|
976
|
+
if (!translator.terminalStatus && !translator.error) {
|
|
977
|
+
throw new CoderChatError({
|
|
978
|
+
message: "Coder chat stream ended before the turn settled (connection closed or the server ended the stream without a terminal status).",
|
|
979
|
+
kind: "stream_closed",
|
|
980
|
+
retryable: true
|
|
981
|
+
});
|
|
754
982
|
}
|
|
755
983
|
for (const part of translator.finish()) yield part;
|
|
984
|
+
} catch (err) {
|
|
985
|
+
throwIfAborted();
|
|
986
|
+
throw err;
|
|
987
|
+
} finally {
|
|
756
988
|
if (translator.maxMessageId > this.#lastSeenMessageId)
|
|
757
989
|
this.#lastSeenMessageId = translator.maxMessageId;
|
|
758
|
-
|
|
990
|
+
signal?.removeEventListener("abort", interrupt);
|
|
991
|
+
if (!translator.terminalStatus) interrupt();
|
|
759
992
|
this.#inFlight = false;
|
|
760
993
|
}
|
|
761
994
|
}
|
|
762
995
|
async doStream(options) {
|
|
763
|
-
const
|
|
996
|
+
const cancelController = new AbortController();
|
|
997
|
+
const abortSignal = options.abortSignal ? AbortSignal.any([options.abortSignal, cancelController.signal]) : cancelController.signal;
|
|
998
|
+
const gen = this.#runTurn({ ...options, abortSignal });
|
|
764
999
|
const stream = new ReadableStream({
|
|
765
1000
|
async pull(controller) {
|
|
766
1001
|
try {
|
|
@@ -768,11 +1003,14 @@ var CoderLanguageModel = class {
|
|
|
768
1003
|
if (done) controller.close();
|
|
769
1004
|
else controller.enqueue(value);
|
|
770
1005
|
} catch (err) {
|
|
771
|
-
controller.
|
|
1006
|
+
if (cancelController.signal.aborted) controller.close();
|
|
1007
|
+
else controller.error(err);
|
|
772
1008
|
}
|
|
773
1009
|
},
|
|
774
1010
|
async cancel() {
|
|
775
|
-
|
|
1011
|
+
cancelController.abort();
|
|
1012
|
+
await gen.return().catch(() => {
|
|
1013
|
+
});
|
|
776
1014
|
}
|
|
777
1015
|
});
|
|
778
1016
|
return { stream };
|
|
@@ -837,12 +1075,27 @@ var CoderLanguageModel = class {
|
|
|
837
1075
|
};
|
|
838
1076
|
|
|
839
1077
|
// src/agent/coder-agent.ts
|
|
1078
|
+
function makeChatAttachment(file) {
|
|
1079
|
+
return {
|
|
1080
|
+
...file,
|
|
1081
|
+
toFilePart: () => ({
|
|
1082
|
+
type: "file",
|
|
1083
|
+
// Bytes are unused: the reference travels in providerOptions.coder.fileId.
|
|
1084
|
+
data: "",
|
|
1085
|
+
mediaType: file.mediaType,
|
|
1086
|
+
...file.name ? { filename: file.name } : {},
|
|
1087
|
+
providerOptions: { [CODER_PROVIDER_OPTIONS]: { fileId: file.id } }
|
|
1088
|
+
})
|
|
1089
|
+
};
|
|
1090
|
+
}
|
|
840
1091
|
var DEFAULT_STOP = stepCountIs(64);
|
|
841
1092
|
var CoderAgent = class {
|
|
842
1093
|
version = "agent-v1";
|
|
843
1094
|
#client;
|
|
844
1095
|
#model;
|
|
845
1096
|
#inner;
|
|
1097
|
+
#organizationId;
|
|
1098
|
+
#workspaceFiles;
|
|
846
1099
|
constructor(settings) {
|
|
847
1100
|
if (settings.client) {
|
|
848
1101
|
this.#client = settings.client;
|
|
@@ -858,6 +1111,13 @@ var CoderAgent = class {
|
|
|
858
1111
|
"CoderAgent requires either `client` or both `baseUrl` and `token`."
|
|
859
1112
|
);
|
|
860
1113
|
}
|
|
1114
|
+
this.#organizationId = settings.organizationId;
|
|
1115
|
+
this.#workspaceFiles = settings.workspaceFiles;
|
|
1116
|
+
if (settings.workspaceFiles && settings.workspaceId && settings.workspaceFiles.workspaceId !== settings.workspaceId) {
|
|
1117
|
+
throw new CoderAgentError(
|
|
1118
|
+
`workspaceFiles.workspaceId (${settings.workspaceFiles.workspaceId}) does not match the agent's workspaceId (${settings.workspaceId}); the chat's tools would not see uploaded files.`
|
|
1119
|
+
);
|
|
1120
|
+
}
|
|
861
1121
|
this.#model = new CoderLanguageModel({
|
|
862
1122
|
client: this.#client,
|
|
863
1123
|
organizationId: settings.organizationId,
|
|
@@ -865,7 +1125,8 @@ var CoderAgent = class {
|
|
|
865
1125
|
workspaceId: settings.workspaceId,
|
|
866
1126
|
mcpServerIds: settings.mcpServerIds,
|
|
867
1127
|
planMode: settings.planMode,
|
|
868
|
-
chatId: settings.chatId
|
|
1128
|
+
chatId: settings.chatId,
|
|
1129
|
+
requestTimeoutMs: settings.requestTimeoutMs
|
|
869
1130
|
});
|
|
870
1131
|
this.#inner = new ToolLoopAgent({
|
|
871
1132
|
model: this.#model,
|
|
@@ -898,6 +1159,14 @@ var CoderAgent = class {
|
|
|
898
1159
|
return this.#inner.stream(options);
|
|
899
1160
|
}
|
|
900
1161
|
// --- session helpers ------------------------------------------------------
|
|
1162
|
+
/**
|
|
1163
|
+
* List the model configs available on the deployment. Use this to discover
|
|
1164
|
+
* valid values for the `model` hint instead of guessing ids — match on
|
|
1165
|
+
* `id`, `provider`/`model`, or `display_name`.
|
|
1166
|
+
*/
|
|
1167
|
+
listModels(signal) {
|
|
1168
|
+
return this.#client.listModelConfigs(signal);
|
|
1169
|
+
}
|
|
901
1170
|
/** Start a fresh chatd chat on the next turn. */
|
|
902
1171
|
resetSession() {
|
|
903
1172
|
this.#model.resetSession();
|
|
@@ -912,19 +1181,89 @@ var CoderAgent = class {
|
|
|
912
1181
|
const id = this.#model.chatId;
|
|
913
1182
|
if (id) await this.#client.archiveChat(id);
|
|
914
1183
|
}
|
|
1184
|
+
/**
|
|
1185
|
+
* Clean up the chat when the agent leaves an `await using` scope, so cleanup
|
|
1186
|
+
* 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
|
+
* Best-effort: disposal errors are swallowed so they can't mask the scope's
|
|
1189
|
+
* own error.
|
|
1190
|
+
*
|
|
1191
|
+
* @example
|
|
1192
|
+
* ```ts
|
|
1193
|
+
* await using agent = new CoderAgent({ ... });
|
|
1194
|
+
* const { text } = await agent.generate({ prompt: "…" });
|
|
1195
|
+
* // agent.interrupt() + agent.archive() run automatically here.
|
|
1196
|
+
* ```
|
|
1197
|
+
*/
|
|
1198
|
+
async [Symbol.asyncDispose]() {
|
|
1199
|
+
try {
|
|
1200
|
+
await this.interrupt();
|
|
1201
|
+
} catch {
|
|
1202
|
+
}
|
|
1203
|
+
try {
|
|
1204
|
+
await this.archive();
|
|
1205
|
+
} catch {
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
// --- files ----------------------------------------------------------------
|
|
1209
|
+
/**
|
|
1210
|
+
* Upload a file as a chat attachment — content for the model to *read* (a PDF,
|
|
1211
|
+
* image, CSV, …). Returns a handle you can reference from a later turn via
|
|
1212
|
+
* {@link ChatAttachment.toFilePart} (no re-upload). Validated against the chat
|
|
1213
|
+
* allowlist and 10 MiB cap up front.
|
|
1214
|
+
*
|
|
1215
|
+
* You usually don't need this: a native AI SDK `file` part dropped into a
|
|
1216
|
+
* message's `content` is uploaded transparently. Reach for `attach()` to
|
|
1217
|
+
* pre-upload, to reuse one file across turns, or to validate before sending.
|
|
1218
|
+
*
|
|
1219
|
+
* For large or non-allowlisted files (zips, datasets, binaries) — material for
|
|
1220
|
+
* the agent to *operate on* — use {@link CoderAgent.uploadToWorkspace}.
|
|
1221
|
+
*/
|
|
1222
|
+
async attach(file, signal) {
|
|
1223
|
+
const uploaded = await this.#client.uploadChatFile(this.#organizationId, file, signal);
|
|
1224
|
+
return makeChatAttachment(uploaded);
|
|
1225
|
+
}
|
|
1226
|
+
/**
|
|
1227
|
+
* Write a file onto the bound workspace's filesystem for the agent to operate
|
|
1228
|
+
* on (extract, build, read with its tools). Requires a {@link WorkspaceFileStore}
|
|
1229
|
+
* via the `workspaceFiles` setting. The file is written as-is — no unpacking;
|
|
1230
|
+
* instruct the agent to `unzip`/`tar -x`, or extract over your own connection.
|
|
1231
|
+
*
|
|
1232
|
+
* The chat should be bound to the same workspace (`workspaceId`) so the agent's
|
|
1233
|
+
* server-side tools can see the file.
|
|
1234
|
+
*/
|
|
1235
|
+
async uploadToWorkspace(file) {
|
|
1236
|
+
if (!this.#workspaceFiles) {
|
|
1237
|
+
throw new CoderAgentError(
|
|
1238
|
+
"uploadToWorkspace requires a `workspaceFiles` adapter. Construct the agent with `workspaceFiles` (e.g. backed by a @coder/ai-sdk-eve-sandbox session) to enable it."
|
|
1239
|
+
);
|
|
1240
|
+
}
|
|
1241
|
+
const { path } = await this.#workspaceFiles.write({
|
|
1242
|
+
content: file.content,
|
|
1243
|
+
path: file.path,
|
|
1244
|
+
signal: file.signal
|
|
1245
|
+
});
|
|
1246
|
+
return { workspaceId: this.#workspaceFiles.workspaceId, path };
|
|
1247
|
+
}
|
|
915
1248
|
};
|
|
916
1249
|
export {
|
|
1250
|
+
CHAT_ATTACHMENT_MEDIA_TYPES,
|
|
1251
|
+
CODER_PROVIDER_OPTIONS,
|
|
917
1252
|
CoderAgent,
|
|
918
1253
|
CoderAgentError,
|
|
919
1254
|
CoderApiError,
|
|
920
1255
|
CoderChatClient,
|
|
921
1256
|
CoderChatError,
|
|
922
1257
|
CoderLanguageModel,
|
|
1258
|
+
MAX_CHAT_FILE_SIZE_BYTES,
|
|
923
1259
|
TurnTranslator,
|
|
924
1260
|
classifyTurnAction,
|
|
1261
|
+
dataContentToFileContent,
|
|
925
1262
|
dynamicToolNames,
|
|
926
1263
|
extractSystemPrompt,
|
|
1264
|
+
resolveFileContent,
|
|
927
1265
|
streamChatEvents,
|
|
928
|
-
toolsToDynamicTools
|
|
1266
|
+
toolsToDynamicTools,
|
|
1267
|
+
userContentToInputParts
|
|
929
1268
|
};
|
|
930
1269
|
//# sourceMappingURL=index.js.map
|