@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/README.md +304 -17
- package/dist/index.d.ts +264 -4
- package/dist/index.js +400 -50
- package/dist/index.js.map +1 -1
- package/package.json +4 -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
|
}
|
|
@@ -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({
|
|
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
|
|
690
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
744
|
-
for (const
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
if (status
|
|
748
|
-
|
|
749
|
-
|
|
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
|
-
|
|
753
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
1017
|
+
if (cancelController.signal.aborted) controller.close();
|
|
1018
|
+
else controller.error(err);
|
|
772
1019
|
}
|
|
773
1020
|
},
|
|
774
1021
|
async cancel() {
|
|
775
|
-
|
|
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
|