@coder/ai-sdk-agent 0.7.2 → 0.9.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 +73 -18
- package/dist/index.d.ts +172 -17
- package/dist/index.js +251 -89
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -30,6 +30,15 @@ var CoderApiError = class extends CoderAgentError {
|
|
|
30
30
|
};
|
|
31
31
|
var CoderStreamError = class extends APICallError {
|
|
32
32
|
name = "CoderStreamError";
|
|
33
|
+
/**
|
|
34
|
+
* The chatd chat id the failed turn had created or attached to, when known.
|
|
35
|
+
* A fresh-chat stream failure discards the session so a retry starts on a
|
|
36
|
+
* fresh chat (`CoderAgent.chatId` becomes `undefined`), but the failed chat
|
|
37
|
+
* still exists server-side — this field names it so callers can clean it up
|
|
38
|
+
* (`CoderAgent.archive()` also retains it as the last-known cleanup target).
|
|
39
|
+
* `undefined` when the failure happened before any chat existed.
|
|
40
|
+
*/
|
|
41
|
+
chatId;
|
|
33
42
|
constructor(args) {
|
|
34
43
|
super({
|
|
35
44
|
message: args.message,
|
|
@@ -42,6 +51,7 @@ var CoderStreamError = class extends APICallError {
|
|
|
42
51
|
isRetryable: args.isRetryable ?? true,
|
|
43
52
|
cause: args.cause
|
|
44
53
|
});
|
|
54
|
+
this.chatId = args.chatId;
|
|
45
55
|
}
|
|
46
56
|
};
|
|
47
57
|
var CoderChatError = class extends CoderAgentError {
|
|
@@ -303,6 +313,52 @@ async function* streamChatEventsLoop(options) {
|
|
|
303
313
|
abortListenerAdded = true;
|
|
304
314
|
}
|
|
305
315
|
}
|
|
316
|
+
const ingest = (next) => {
|
|
317
|
+
let progress = false;
|
|
318
|
+
let forwarded = true;
|
|
319
|
+
if (next.type === "message_part") {
|
|
320
|
+
const mp = next.message_part;
|
|
321
|
+
const hv = mp?.history_version;
|
|
322
|
+
const ga = mp?.generation_attempt;
|
|
323
|
+
const seq = mp?.seq;
|
|
324
|
+
if (hv !== void 0 && ga !== void 0 && seq !== void 0) {
|
|
325
|
+
if (hv === lastHistoryVersion && ga === lastGenerationAttempt && lastSeq !== void 0 && seq <= lastSeq) {
|
|
326
|
+
forwarded = false;
|
|
327
|
+
} else {
|
|
328
|
+
lastHistoryVersion = hv;
|
|
329
|
+
lastGenerationAttempt = ga;
|
|
330
|
+
lastSeq = seq;
|
|
331
|
+
progress = true;
|
|
332
|
+
}
|
|
333
|
+
} else {
|
|
334
|
+
sawUntrackableDelta = true;
|
|
335
|
+
}
|
|
336
|
+
} else if (next.type === "message" && next.message) {
|
|
337
|
+
const serialized = JSON.stringify(next.message);
|
|
338
|
+
if (lastMessageJson.get(next.message.id) !== serialized) {
|
|
339
|
+
lastMessageJson.set(next.message.id, serialized);
|
|
340
|
+
progress = true;
|
|
341
|
+
}
|
|
342
|
+
if (maxCommittedId === void 0 || next.message.id > maxCommittedId) {
|
|
343
|
+
maxCommittedId = next.message.id;
|
|
344
|
+
sawUntrackableDelta = false;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
if (progress) {
|
|
348
|
+
failures = 0;
|
|
349
|
+
backoffMs = STREAM_BACKOFF_INITIAL_MS;
|
|
350
|
+
}
|
|
351
|
+
emit?.({
|
|
352
|
+
type: "ws:event",
|
|
353
|
+
chatId,
|
|
354
|
+
reader,
|
|
355
|
+
attempt,
|
|
356
|
+
event: next,
|
|
357
|
+
forwarded,
|
|
358
|
+
timestamp: Date.now()
|
|
359
|
+
});
|
|
360
|
+
if (forwarded) queue.push(next);
|
|
361
|
+
};
|
|
306
362
|
const onMessage = (ev) => {
|
|
307
363
|
if (finished) return;
|
|
308
364
|
let batch;
|
|
@@ -325,27 +381,9 @@ async function* streamChatEventsLoop(options) {
|
|
|
325
381
|
return;
|
|
326
382
|
}
|
|
327
383
|
if (Array.isArray(batch)) {
|
|
328
|
-
for (const e of batch)
|
|
329
|
-
queue.push(e);
|
|
330
|
-
emit?.({
|
|
331
|
-
type: "ws:event",
|
|
332
|
-
chatId,
|
|
333
|
-
reader,
|
|
334
|
-
attempt,
|
|
335
|
-
event: e,
|
|
336
|
-
timestamp: Date.now()
|
|
337
|
-
});
|
|
338
|
-
}
|
|
384
|
+
for (const e of batch) ingest(e);
|
|
339
385
|
} else if (batch && typeof batch === "object") {
|
|
340
|
-
|
|
341
|
-
emit?.({
|
|
342
|
-
type: "ws:event",
|
|
343
|
-
chatId,
|
|
344
|
-
reader,
|
|
345
|
-
attempt,
|
|
346
|
-
event: batch,
|
|
347
|
-
timestamp: Date.now()
|
|
348
|
-
});
|
|
386
|
+
ingest(batch);
|
|
349
387
|
}
|
|
350
388
|
wake();
|
|
351
389
|
};
|
|
@@ -383,40 +421,7 @@ async function* streamChatEventsLoop(options) {
|
|
|
383
421
|
try {
|
|
384
422
|
while (true) {
|
|
385
423
|
while (queue.length > 0) {
|
|
386
|
-
|
|
387
|
-
let progress = false;
|
|
388
|
-
if (next.type === "message_part") {
|
|
389
|
-
const mp = next.message_part;
|
|
390
|
-
const hv = mp?.history_version;
|
|
391
|
-
const ga = mp?.generation_attempt;
|
|
392
|
-
const seq = mp?.seq;
|
|
393
|
-
if (hv !== void 0 && ga !== void 0 && seq !== void 0) {
|
|
394
|
-
if (hv === lastHistoryVersion && ga === lastGenerationAttempt && lastSeq !== void 0 && seq <= lastSeq) {
|
|
395
|
-
continue;
|
|
396
|
-
}
|
|
397
|
-
lastHistoryVersion = hv;
|
|
398
|
-
lastGenerationAttempt = ga;
|
|
399
|
-
lastSeq = seq;
|
|
400
|
-
progress = true;
|
|
401
|
-
} else {
|
|
402
|
-
sawUntrackableDelta = true;
|
|
403
|
-
}
|
|
404
|
-
} else if (next.type === "message" && next.message) {
|
|
405
|
-
const serialized = JSON.stringify(next.message);
|
|
406
|
-
if (lastMessageJson.get(next.message.id) !== serialized) {
|
|
407
|
-
lastMessageJson.set(next.message.id, serialized);
|
|
408
|
-
progress = true;
|
|
409
|
-
}
|
|
410
|
-
if (maxCommittedId === void 0 || next.message.id > maxCommittedId) {
|
|
411
|
-
maxCommittedId = next.message.id;
|
|
412
|
-
sawUntrackableDelta = false;
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
if (progress) {
|
|
416
|
-
failures = 0;
|
|
417
|
-
backoffMs = STREAM_BACKOFF_INITIAL_MS;
|
|
418
|
-
}
|
|
419
|
-
yield next;
|
|
424
|
+
yield queue.shift();
|
|
420
425
|
}
|
|
421
426
|
if (finished) break;
|
|
422
427
|
await new Promise((resolve) => {
|
|
@@ -446,7 +451,8 @@ async function* streamChatEventsLoop(options) {
|
|
|
446
451
|
throw new CoderStreamError({
|
|
447
452
|
message: "chat stream dropped mid-turn and cannot be resumed: this server does not stamp deltas with history_version/generation_attempt/seq, so a redial would replay them",
|
|
448
453
|
url,
|
|
449
|
-
cause: failure
|
|
454
|
+
cause: failure,
|
|
455
|
+
chatId
|
|
450
456
|
});
|
|
451
457
|
}
|
|
452
458
|
if (failure) lastFailure = failure;
|
|
@@ -455,7 +461,8 @@ async function* streamChatEventsLoop(options) {
|
|
|
455
461
|
throw new CoderStreamError({
|
|
456
462
|
message: `Coder chat stream ${path} could not be (re-)established after ${failures} consecutive connection failures without progress`,
|
|
457
463
|
url,
|
|
458
|
-
cause: lastFailure
|
|
464
|
+
cause: lastFailure,
|
|
465
|
+
chatId
|
|
459
466
|
});
|
|
460
467
|
}
|
|
461
468
|
emit?.({
|
|
@@ -667,9 +674,11 @@ var CoderChatClient = class {
|
|
|
667
674
|
/**
|
|
668
675
|
* Issue a request and return the raw `Response` on success (body unread). On a
|
|
669
676
|
* non-2xx status the body is consumed to build a {@link CoderApiError}. Shared
|
|
670
|
-
* by JSON requests and the raw upload/download endpoints.
|
|
677
|
+
* by JSON requests and the raw upload/download endpoints. `op` names the
|
|
678
|
+
* public client method on the exchange's `http:*` events (issue #112) — the
|
|
679
|
+
* emit site is shared, so each call site threads its own literal.
|
|
671
680
|
*/
|
|
672
|
-
async #send(method, path, opts) {
|
|
681
|
+
async #send(op, method, path, opts) {
|
|
673
682
|
const headers = {
|
|
674
683
|
"Coder-Session-Token": this.#token,
|
|
675
684
|
...opts?.headers
|
|
@@ -684,7 +693,7 @@ var CoderChatClient = class {
|
|
|
684
693
|
if (emit) {
|
|
685
694
|
requestId = ++this.#httpSeq;
|
|
686
695
|
startedAt = performance.now();
|
|
687
|
-
emit({ type: "http:request", id: requestId, method, path, timestamp: Date.now() });
|
|
696
|
+
emit({ type: "http:request", id: requestId, op, method, path, timestamp: Date.now() });
|
|
688
697
|
}
|
|
689
698
|
let res;
|
|
690
699
|
try {
|
|
@@ -693,6 +702,7 @@ var CoderChatClient = class {
|
|
|
693
702
|
emit?.({
|
|
694
703
|
type: "http:error",
|
|
695
704
|
id: requestId,
|
|
705
|
+
op,
|
|
696
706
|
method,
|
|
697
707
|
path,
|
|
698
708
|
message: err instanceof Error ? err.message : String(err),
|
|
@@ -704,6 +714,7 @@ var CoderChatClient = class {
|
|
|
704
714
|
emit?.({
|
|
705
715
|
type: "http:response",
|
|
706
716
|
id: requestId,
|
|
717
|
+
op,
|
|
707
718
|
method,
|
|
708
719
|
path,
|
|
709
720
|
status: res.status,
|
|
@@ -733,8 +744,8 @@ var CoderChatClient = class {
|
|
|
733
744
|
return void 0;
|
|
734
745
|
}
|
|
735
746
|
}
|
|
736
|
-
async #request(method, path, body, signal) {
|
|
737
|
-
const res = await this.#send(method, path, {
|
|
747
|
+
async #request(op, method, path, body, signal) {
|
|
748
|
+
const res = await this.#send(op, method, path, {
|
|
738
749
|
body: body === void 0 ? void 0 : JSON.stringify(body),
|
|
739
750
|
headers: body === void 0 ? void 0 : { "Content-Type": "application/json" },
|
|
740
751
|
signal
|
|
@@ -744,6 +755,7 @@ var CoderChatClient = class {
|
|
|
744
755
|
// --- REST -----------------------------------------------------------------
|
|
745
756
|
listModelConfigs(signal) {
|
|
746
757
|
return this.#request(
|
|
758
|
+
"listModelConfigs",
|
|
747
759
|
"GET",
|
|
748
760
|
`${API_PREFIX}/model-configs`,
|
|
749
761
|
void 0,
|
|
@@ -751,13 +763,14 @@ var CoderChatClient = class {
|
|
|
751
763
|
);
|
|
752
764
|
}
|
|
753
765
|
createChat(req, signal) {
|
|
754
|
-
return this.#request("POST", API_PREFIX, req, signal);
|
|
766
|
+
return this.#request("createChat", "POST", API_PREFIX, req, signal);
|
|
755
767
|
}
|
|
756
768
|
getChat(chatId, signal) {
|
|
757
|
-
return this.#request("GET", `${API_PREFIX}/${chatId}`, void 0, signal);
|
|
769
|
+
return this.#request("getChat", "GET", `${API_PREFIX}/${chatId}`, void 0, signal);
|
|
758
770
|
}
|
|
759
771
|
createChatMessage(chatId, req, signal) {
|
|
760
772
|
return this.#request(
|
|
773
|
+
"createChatMessage",
|
|
761
774
|
"POST",
|
|
762
775
|
`${API_PREFIX}/${chatId}/messages`,
|
|
763
776
|
req,
|
|
@@ -771,6 +784,7 @@ var CoderChatClient = class {
|
|
|
771
784
|
if (opts?.limit !== void 0) params.set("limit", String(opts.limit));
|
|
772
785
|
const q = params.toString();
|
|
773
786
|
return this.#request(
|
|
787
|
+
"getMessages",
|
|
774
788
|
"GET",
|
|
775
789
|
`${API_PREFIX}/${chatId}/messages${q ? `?${q}` : ""}`,
|
|
776
790
|
void 0,
|
|
@@ -778,7 +792,13 @@ var CoderChatClient = class {
|
|
|
778
792
|
);
|
|
779
793
|
}
|
|
780
794
|
submitToolResults(chatId, req, signal) {
|
|
781
|
-
return this.#request(
|
|
795
|
+
return this.#request(
|
|
796
|
+
"submitToolResults",
|
|
797
|
+
"POST",
|
|
798
|
+
`${API_PREFIX}/${chatId}/tool-results`,
|
|
799
|
+
req,
|
|
800
|
+
signal
|
|
801
|
+
);
|
|
782
802
|
}
|
|
783
803
|
/**
|
|
784
804
|
* Interrupt the chat's in-flight run (`POST /interrupt`). The server flips
|
|
@@ -797,6 +817,7 @@ var CoderChatClient = class {
|
|
|
797
817
|
const opts = optsOrSignal instanceof AbortSignal ? { signal: optsOrSignal } : optsOrSignal;
|
|
798
818
|
const query = opts?.wait ? "?wait=true" : "";
|
|
799
819
|
return this.#request(
|
|
820
|
+
"interruptChat",
|
|
800
821
|
"POST",
|
|
801
822
|
`${API_PREFIX}/${chatId}/interrupt${query}`,
|
|
802
823
|
void 0,
|
|
@@ -804,11 +825,17 @@ var CoderChatClient = class {
|
|
|
804
825
|
);
|
|
805
826
|
}
|
|
806
827
|
updateChat(chatId, req, signal) {
|
|
807
|
-
return this.#request("PATCH", `${API_PREFIX}/${chatId}`, req, signal);
|
|
828
|
+
return this.#request("updateChat", "PATCH", `${API_PREFIX}/${chatId}`, req, signal);
|
|
808
829
|
}
|
|
809
830
|
/** Convenience: archive a chat (soft-hide; safe for cleanup). */
|
|
810
831
|
archiveChat(chatId, signal) {
|
|
811
|
-
return this
|
|
832
|
+
return this.#request(
|
|
833
|
+
"archiveChat",
|
|
834
|
+
"PATCH",
|
|
835
|
+
`${API_PREFIX}/${chatId}`,
|
|
836
|
+
{ archived: true },
|
|
837
|
+
signal
|
|
838
|
+
);
|
|
812
839
|
}
|
|
813
840
|
// --- Files ----------------------------------------------------------------
|
|
814
841
|
/**
|
|
@@ -846,6 +873,7 @@ var CoderChatClient = class {
|
|
|
846
873
|
headers["Content-Disposition"] = `attachment; filename="${ascii}"; filename*=UTF-8''${utf8}`;
|
|
847
874
|
}
|
|
848
875
|
const res = await this.#send(
|
|
876
|
+
"uploadChatFile",
|
|
849
877
|
"POST",
|
|
850
878
|
`${API_PREFIX}/files?organization=${encodeURIComponent(organizationId)}`,
|
|
851
879
|
{ body: resolved.body, headers, signal }
|
|
@@ -863,7 +891,9 @@ var CoderChatClient = class {
|
|
|
863
891
|
}
|
|
864
892
|
/** Download a chat file's bytes and media type by id. */
|
|
865
893
|
async getChatFile(fileId, signal) {
|
|
866
|
-
const res = await this.#send("GET", `${API_PREFIX}/files/${fileId}`, {
|
|
894
|
+
const res = await this.#send("getChatFile", "GET", `${API_PREFIX}/files/${fileId}`, {
|
|
895
|
+
signal
|
|
896
|
+
});
|
|
867
897
|
const buf = await res.arrayBuffer();
|
|
868
898
|
return {
|
|
869
899
|
bytes: new Uint8Array(buf),
|
|
@@ -917,7 +947,13 @@ var CoderChatClient = class {
|
|
|
917
947
|
async resolveModelConfigId(hint, signal) {
|
|
918
948
|
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
919
949
|
if (UUID_RE.test(hint)) return hint;
|
|
920
|
-
const raw = await this
|
|
950
|
+
const raw = await this.#request(
|
|
951
|
+
"resolveModelConfigId",
|
|
952
|
+
"GET",
|
|
953
|
+
`${API_PREFIX}/model-configs`,
|
|
954
|
+
void 0,
|
|
955
|
+
signal
|
|
956
|
+
);
|
|
921
957
|
const configs = (Array.isArray(raw) ? raw : []).filter(
|
|
922
958
|
(c) => typeof c === "object" && c !== null
|
|
923
959
|
);
|
|
@@ -1752,6 +1788,25 @@ var CoderLanguageModel = class {
|
|
|
1752
1788
|
supportedUrls = {};
|
|
1753
1789
|
#config;
|
|
1754
1790
|
#chatId;
|
|
1791
|
+
/**
|
|
1792
|
+
* The last chat id this model created or attached to, retained as a CLEANUP
|
|
1793
|
+
* target — see {@link lastKnownChatId}. Unlike `#chatId` it survives
|
|
1794
|
+
* {@link resetSession}, and is only replaced when a later turn's chat
|
|
1795
|
+
* supersedes it or cleared by {@link noteChatCleanedUp} once cleanup
|
|
1796
|
+
* actually succeeded against it.
|
|
1797
|
+
*/
|
|
1798
|
+
#lastKnownChatId;
|
|
1799
|
+
/**
|
|
1800
|
+
* Every chat id stranded by an AUTOMATIC session discard (a stream failure
|
|
1801
|
+
* on a chat the failed turn itself created) and not yet cleaned up. A
|
|
1802
|
+
* single `#lastKnownChatId` slot is not enough here: each AI-SDK
|
|
1803
|
+
* `maxRetries` attempt strands its own fresh chat and only the final
|
|
1804
|
+
* attempt's error reaches the caller, so without this ledger every attempt
|
|
1805
|
+
* before the last would leak unnamed. Drained by {@link noteChatCleanedUp}.
|
|
1806
|
+
* Deliberate abandonment ({@link resetSession}) does NOT record here — the
|
|
1807
|
+
* caller chose to walk away and may want the chat kept.
|
|
1808
|
+
*/
|
|
1809
|
+
#strandedChatIds = /* @__PURE__ */ new Set();
|
|
1755
1810
|
#lastSeenMessageId = 0;
|
|
1756
1811
|
#resolvedModelConfigId;
|
|
1757
1812
|
#modelResolved = false;
|
|
@@ -1767,15 +1822,55 @@ var CoderLanguageModel = class {
|
|
|
1767
1822
|
this.#config = config;
|
|
1768
1823
|
this.modelId = config.model ?? "chatd";
|
|
1769
1824
|
this.#chatId = config.chatId;
|
|
1825
|
+
this.#lastKnownChatId = config.chatId;
|
|
1770
1826
|
this.#emitTransportEvent = safeTransportEmitter(config.onTransportEvent);
|
|
1771
1827
|
}
|
|
1772
1828
|
get chatId() {
|
|
1773
1829
|
return this.#chatId;
|
|
1774
1830
|
}
|
|
1831
|
+
/**
|
|
1832
|
+
* The last chat id this model created or attached to, for CLEANUP targeting
|
|
1833
|
+
* only — generation always targets {@link chatId}. It survives
|
|
1834
|
+
* {@link resetSession} (including the automatic session discard after a
|
|
1835
|
+
* fresh-chat stream failure, which clears {@link chatId} so a retry starts
|
|
1836
|
+
* fresh while the failed chat still exists server-side), is superseded when
|
|
1837
|
+
* a later turn creates or attaches to another chat, and is cleared via
|
|
1838
|
+
* {@link noteChatCleanedUp} once cleanup actually succeeded against it.
|
|
1839
|
+
*/
|
|
1840
|
+
get lastKnownChatId() {
|
|
1841
|
+
return this.#lastKnownChatId;
|
|
1842
|
+
}
|
|
1843
|
+
/**
|
|
1844
|
+
* Chat ids stranded by automatic session discards and not yet cleaned up,
|
|
1845
|
+
* oldest first. Each retryable fresh-chat stream failure strands one chat;
|
|
1846
|
+
* under AI-SDK `maxRetries` several can accumulate while only the final
|
|
1847
|
+
* attempt's error (and {@link lastKnownChatId}) names the newest —
|
|
1848
|
+
* `CoderAgent.archive()` retires all of them. Ids leave the list only via
|
|
1849
|
+
* {@link noteChatCleanedUp}.
|
|
1850
|
+
*/
|
|
1851
|
+
get strandedChatIds() {
|
|
1852
|
+
return [...this.#strandedChatIds];
|
|
1853
|
+
}
|
|
1854
|
+
/**
|
|
1855
|
+
* Forgets `chatId` as a cleanup target — call it only after cleanup
|
|
1856
|
+
* actually succeeded against that chat (e.g. `CoderAgent.archive()`
|
|
1857
|
+
* archived it), so later cleanup calls don't re-target an already-archived
|
|
1858
|
+
* chat: removes it from {@link strandedChatIds} and, when it is the
|
|
1859
|
+
* last-known id, clears {@link lastKnownChatId} (match-guarded: a stale
|
|
1860
|
+
* completion must not erase a newer chat's id). The session field
|
|
1861
|
+
* ({@link chatId}) is deliberately untouched — archiving does not reset the
|
|
1862
|
+
* session.
|
|
1863
|
+
*/
|
|
1864
|
+
noteChatCleanedUp(chatId) {
|
|
1865
|
+
this.#strandedChatIds.delete(chatId);
|
|
1866
|
+
if (this.#lastKnownChatId === chatId) this.#lastKnownChatId = void 0;
|
|
1867
|
+
}
|
|
1775
1868
|
/**
|
|
1776
1869
|
* Drops the current session so the next turn creates a fresh chat. Also
|
|
1777
1870
|
* closes the retained event stream, if any (fire-and-forget — a sync reset
|
|
1778
1871
|
* must not wait on socket teardown, and `close()` never rejects).
|
|
1872
|
+
* {@link lastKnownChatId} deliberately survives the reset: the dropped chat
|
|
1873
|
+
* still exists server-side and cleanup must still be able to target it.
|
|
1779
1874
|
*/
|
|
1780
1875
|
resetSession() {
|
|
1781
1876
|
this.#chatId = void 0;
|
|
@@ -2145,6 +2240,7 @@ var CoderLanguageModel = class {
|
|
|
2145
2240
|
}
|
|
2146
2241
|
const chatId = this.#chatId;
|
|
2147
2242
|
turnChatId = chatId;
|
|
2243
|
+
this.#lastKnownChatId = chatId;
|
|
2148
2244
|
if (segmentChat) segmentChat.chatId = chatId;
|
|
2149
2245
|
const dynamicNames = dynamicToolNames(options.tools);
|
|
2150
2246
|
translator = new TurnTranslator({
|
|
@@ -2243,7 +2339,11 @@ var CoderLanguageModel = class {
|
|
|
2243
2339
|
} catch (err) {
|
|
2244
2340
|
throwIfAborted();
|
|
2245
2341
|
if (err instanceof CoderStreamError) {
|
|
2342
|
+
if (err.chatId === void 0 && turnChatId !== void 0) {
|
|
2343
|
+
err.chatId = turnChatId;
|
|
2344
|
+
}
|
|
2246
2345
|
if (turnCreatedChat) {
|
|
2346
|
+
if (turnChatId) this.#strandedChatIds.add(turnChatId);
|
|
2247
2347
|
discardSession = true;
|
|
2248
2348
|
}
|
|
2249
2349
|
const effectful = chatHasWorkspace || chatHasMcpServers || uploadedAttachment;
|
|
@@ -2252,7 +2352,8 @@ var CoderLanguageModel = class {
|
|
|
2252
2352
|
message: `${err.message}; automatic retry is disabled because ${turnCreatedChat ? "the turn has server-side effects a replay would repeat (workspace/MCP tools that may have executed, or freshly uploaded attachments)" : "the chat has prior state (a retry would resubmit this turn's prompt as a new user turn)"}`,
|
|
2253
2353
|
url: err.url,
|
|
2254
2354
|
cause: err,
|
|
2255
|
-
isRetryable: false
|
|
2355
|
+
isRetryable: false,
|
|
2356
|
+
chatId: err.chatId
|
|
2256
2357
|
});
|
|
2257
2358
|
}
|
|
2258
2359
|
if (err instanceof CoderAgentError && !(err instanceof CoderApiError) && !(err instanceof CoderChatError)) {
|
|
@@ -2466,25 +2567,28 @@ var CoderAgent = class {
|
|
|
2466
2567
|
#settleRetryDelayMs;
|
|
2467
2568
|
constructor(settings) {
|
|
2468
2569
|
assertSupportedAiVersion();
|
|
2570
|
+
const env = globalThis.process?.env;
|
|
2571
|
+
const baseUrl = settings.baseUrl ?? env?.CODER_URL;
|
|
2572
|
+
const token = settings.token ?? env?.CODER_SESSION_TOKEN;
|
|
2469
2573
|
if (settings.client) {
|
|
2470
2574
|
this.#client = settings.client;
|
|
2471
|
-
} else if (
|
|
2575
|
+
} else if (baseUrl && token) {
|
|
2472
2576
|
this.#client = new CoderChatClient({
|
|
2473
|
-
baseUrl
|
|
2474
|
-
token
|
|
2577
|
+
baseUrl,
|
|
2578
|
+
token,
|
|
2475
2579
|
fetch: settings.fetch,
|
|
2476
2580
|
webSocketFactory: settings.webSocketFactory,
|
|
2477
2581
|
onTransportEvent: settings.onTransportEvent
|
|
2478
2582
|
});
|
|
2479
2583
|
} else {
|
|
2480
2584
|
throw new CoderAgentError(
|
|
2481
|
-
"CoderAgent requires either `client` or both `baseUrl` and `token
|
|
2585
|
+
"CoderAgent requires either `client` or both `baseUrl` and `token` (or the CODER_URL and CODER_SESSION_TOKEN environment variables)."
|
|
2482
2586
|
);
|
|
2483
2587
|
}
|
|
2484
2588
|
this.#organizationId = settings.organizationId;
|
|
2485
2589
|
this.#workspaceFiles = settings.workspaceFiles;
|
|
2486
2590
|
this.#workspaceId = settings.workspaceId;
|
|
2487
|
-
this.#workspaceApi =
|
|
2591
|
+
this.#workspaceApi = baseUrl && token ? { baseUrl, token, fetch: settings.fetch } : void 0;
|
|
2488
2592
|
this.#settleDeadlineMs = sanitizeDelayMs(settings.settleDeadlineMs, SETTLE_DEADLINE_MS);
|
|
2489
2593
|
this.#settleRetryDelayMs = sanitizeDelayMs(settings.settleRetryDelayMs, SETTLE_RETRY_DELAY_MS);
|
|
2490
2594
|
if (settings.workspaceFiles && settings.workspaceId && settings.workspaceFiles.workspaceId !== settings.workspaceId) {
|
|
@@ -2528,6 +2632,29 @@ var CoderAgent = class {
|
|
|
2528
2632
|
get chatId() {
|
|
2529
2633
|
return this.#model.chatId;
|
|
2530
2634
|
}
|
|
2635
|
+
/**
|
|
2636
|
+
* The last chat id this agent created or attached to, retained for CLEANUP
|
|
2637
|
+
* even after the session was dropped — a fresh-chat stream failure discards
|
|
2638
|
+
* the session ({@link CoderAgent.chatId} becomes `undefined`, so a retry
|
|
2639
|
+
* starts fresh), but the failed chat still exists server-side.
|
|
2640
|
+
* {@link CoderAgent.archive} and {@link CoderAgent.interrupt} fall back to
|
|
2641
|
+
* this id; generation never does. Superseded when a later turn creates or
|
|
2642
|
+
* attaches to another chat; cleared once {@link CoderAgent.archive}
|
|
2643
|
+
* succeeds against it.
|
|
2644
|
+
*/
|
|
2645
|
+
get lastKnownChatId() {
|
|
2646
|
+
return this.#model.lastKnownChatId;
|
|
2647
|
+
}
|
|
2648
|
+
/**
|
|
2649
|
+
* Chat ids stranded by automatic session discards and not yet cleaned up,
|
|
2650
|
+
* oldest first. Each retryable fresh-chat stream failure strands one chat,
|
|
2651
|
+
* so with `maxRetries` > 0 several can accumulate while only the final
|
|
2652
|
+
* attempt's error (and {@link CoderAgent.lastKnownChatId}) names the
|
|
2653
|
+
* newest. {@link CoderAgent.archive} retires all of them.
|
|
2654
|
+
*/
|
|
2655
|
+
get strandedChatIds() {
|
|
2656
|
+
return this.#model.strandedChatIds;
|
|
2657
|
+
}
|
|
2531
2658
|
generate(options) {
|
|
2532
2659
|
return this.#inner.generate(options);
|
|
2533
2660
|
}
|
|
@@ -2543,7 +2670,12 @@ var CoderAgent = class {
|
|
|
2543
2670
|
listModels(signal) {
|
|
2544
2671
|
return this.#client.listModelConfigs(signal);
|
|
2545
2672
|
}
|
|
2546
|
-
/**
|
|
2673
|
+
/**
|
|
2674
|
+
* Start a fresh chatd chat on the next turn. The dropped chat still exists
|
|
2675
|
+
* server-side: {@link CoderAgent.archive} / {@link CoderAgent.interrupt}
|
|
2676
|
+
* keep targeting it (via {@link CoderAgent.lastKnownChatId}) until a new
|
|
2677
|
+
* chat supersedes it or an archive succeeds against it.
|
|
2678
|
+
*/
|
|
2547
2679
|
resetSession() {
|
|
2548
2680
|
this.#model.resetSession();
|
|
2549
2681
|
}
|
|
@@ -2551,37 +2683,67 @@ var CoderAgent = class {
|
|
|
2551
2683
|
* Interrupt the in-flight generation, if any. Resolves as soon as the server
|
|
2552
2684
|
* acknowledges the interrupt — the run keeps winding down asynchronously for
|
|
2553
2685
|
* a few seconds afterwards (see {@link CoderAgent.archive}).
|
|
2686
|
+
*
|
|
2687
|
+
* Targets the session's chat, or — after the session was dropped (a
|
|
2688
|
+
* fresh-chat stream failure, or {@link CoderAgent.resetSession}) — the
|
|
2689
|
+
* {@link CoderAgent.lastKnownChatId}, whose run may still be winding down
|
|
2690
|
+
* server-side. With neither, there is nothing to interrupt and it resolves
|
|
2691
|
+
* `{ interrupted: false }` without a server call (inspect the result rather
|
|
2692
|
+
* than relying on a silent no-op).
|
|
2554
2693
|
*/
|
|
2555
2694
|
async interrupt(opts) {
|
|
2556
|
-
const id = this.#model.chatId;
|
|
2557
|
-
if (!id) return;
|
|
2695
|
+
const id = this.#model.chatId ?? this.#model.lastKnownChatId;
|
|
2696
|
+
if (!id) return { interrupted: false };
|
|
2558
2697
|
await this.#model.closePausedStream();
|
|
2559
2698
|
await this.#client.interruptChat(id, opts?.signal);
|
|
2699
|
+
return { interrupted: true, chatId: id };
|
|
2560
2700
|
}
|
|
2561
2701
|
/**
|
|
2562
2702
|
* Archive the underlying chat (safe cleanup; hides it from listings).
|
|
2563
2703
|
*
|
|
2704
|
+
* Targets the session's chat, or — after the session was dropped (a
|
|
2705
|
+
* fresh-chat stream failure, or {@link CoderAgent.resetSession}) — the
|
|
2706
|
+
* {@link CoderAgent.lastKnownChatId}, so the stranded chat is still cleaned
|
|
2707
|
+
* up; chats stranded by EARLIER automatic discards
|
|
2708
|
+
* ({@link CoderAgent.strandedChatIds} — `maxRetries` can accumulate several)
|
|
2709
|
+
* are retired in the same call. On success each archived id is cleared as a
|
|
2710
|
+
* cleanup target; with no id at all, there is nothing to archive and it
|
|
2711
|
+
* resolves `{ archived: false }` without a server call (inspect the result
|
|
2712
|
+
* rather than relying on a silent no-op).
|
|
2713
|
+
*
|
|
2564
2714
|
* A freshly interrupted/settled chat can keep winding down server-side for a
|
|
2565
2715
|
* few seconds, during which the server rejects archiving with a 409. Those
|
|
2566
|
-
* 409s are retried (~1s apart
|
|
2567
|
-
* `
|
|
2568
|
-
*
|
|
2716
|
+
* 409s are retried (~1s apart — see `settleRetryDelayMs`) inside ONE
|
|
2717
|
+
* `settleDeadlineMs` window (~15s) shared by the whole call — however many
|
|
2718
|
+
* chats it archives — and the last 409 is rethrown if the window closes
|
|
2719
|
+
* first; any other failure, including a caller abort, rethrows immediately.
|
|
2720
|
+
* Chats are archived oldest first, each id cleared as its archive succeeds —
|
|
2721
|
+
* a mid-list failure rethrows and leaves the remaining ids targetable by a
|
|
2722
|
+
* later retry.
|
|
2569
2723
|
*/
|
|
2570
2724
|
async archive(opts) {
|
|
2571
|
-
const
|
|
2572
|
-
if (!
|
|
2725
|
+
const primary = this.#model.chatId ?? this.#model.lastKnownChatId;
|
|
2726
|
+
if (!primary) return { archived: false };
|
|
2573
2727
|
await this.#model.closePausedStream();
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2728
|
+
const targets = [.../* @__PURE__ */ new Set([...this.#model.strandedChatIds, primary])];
|
|
2729
|
+
const deadline = Date.now() + this.#settleDeadlineMs;
|
|
2730
|
+
for (const id of targets) {
|
|
2731
|
+
await archiveWhenSettled(this.#client, id, {
|
|
2732
|
+
deadlineMs: Math.max(deadline - Date.now(), 1),
|
|
2733
|
+
retryDelayMs: this.#settleRetryDelayMs,
|
|
2734
|
+
signal: opts?.signal
|
|
2735
|
+
});
|
|
2736
|
+
this.#model.noteChatCleanedUp(id);
|
|
2737
|
+
}
|
|
2738
|
+
return { archived: true, chatId: primary, archivedChatIds: targets };
|
|
2579
2739
|
}
|
|
2580
2740
|
/**
|
|
2581
2741
|
* Clean up the chat when the agent leaves an `await using` scope, so cleanup
|
|
2582
2742
|
* rides scope exit instead of a separate call you have to remember in a
|
|
2583
2743
|
* `finally`. Interrupts any in-flight server run, then archives the chat
|
|
2584
|
-
* (retrying while the interrupted run settles)
|
|
2744
|
+
* (retrying while the interrupted run settles) — including a chat stranded
|
|
2745
|
+
* by a session discard (see {@link CoderAgent.lastKnownChatId}), which a
|
|
2746
|
+
* failed `generate()` would otherwise leak. Best-effort and bounded
|
|
2585
2747
|
* (~15s overall): disposal errors are swallowed after the bounded attempts
|
|
2586
2748
|
* so they can't mask the scope's own error — call {@link CoderAgent.archive}
|
|
2587
2749
|
* directly when you need guaranteed cleanup.
|
|
@@ -2658,7 +2820,7 @@ var CoderAgent = class {
|
|
|
2658
2820
|
}
|
|
2659
2821
|
if (!this.#workspaceApi) {
|
|
2660
2822
|
throw new CoderAgentError(
|
|
2661
|
-
`${method}() needs the deployment's REST credentials \u2014 construct the CoderAgent with \`baseUrl\` and \`token\` (they can accompany \`client\`).`
|
|
2823
|
+
`${method}() needs the deployment's REST credentials \u2014 construct the CoderAgent with \`baseUrl\` and \`token\` (they can accompany \`client\`), or set the CODER_URL and CODER_SESSION_TOKEN environment variables.`
|
|
2662
2824
|
);
|
|
2663
2825
|
}
|
|
2664
2826
|
return { conn: this.#workspaceApi, workspaceId: this.#workspaceId };
|