@coder/ai-sdk-agent 0.4.2 → 0.5.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 +33 -5
- package/dist/index.d.ts +98 -6
- package/dist/index.js +595 -92
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
} from "ai";
|
|
6
6
|
|
|
7
7
|
// src/errors.ts
|
|
8
|
+
import { APICallError } from "@ai-sdk/provider";
|
|
8
9
|
var CoderAgentError = class extends Error {
|
|
9
10
|
name = "CoderAgentError";
|
|
10
11
|
constructor(message, options) {
|
|
@@ -27,6 +28,22 @@ var CoderApiError = class extends CoderAgentError {
|
|
|
27
28
|
this.path = args.path;
|
|
28
29
|
}
|
|
29
30
|
};
|
|
31
|
+
var CoderStreamError = class extends APICallError {
|
|
32
|
+
name = "CoderStreamError";
|
|
33
|
+
constructor(args) {
|
|
34
|
+
super({
|
|
35
|
+
message: args.message,
|
|
36
|
+
url: args.url,
|
|
37
|
+
requestBodyValues: void 0,
|
|
38
|
+
// Retries re-invoke the model with the SAME prompt, so the model layer
|
|
39
|
+
// downgrades this to false whenever the failed chat has prior state a
|
|
40
|
+
// re-submission would corrupt, or server-side tooling (workspace/MCP)
|
|
41
|
+
// whose effects a replay would duplicate (see #runTurn).
|
|
42
|
+
isRetryable: args.isRetryable ?? true,
|
|
43
|
+
cause: args.cause
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
};
|
|
30
47
|
var CoderChatError = class extends CoderAgentError {
|
|
31
48
|
name = "CoderChatError";
|
|
32
49
|
kind;
|
|
@@ -154,92 +171,209 @@ function httpToWs(baseUrl) {
|
|
|
154
171
|
if (baseUrl.startsWith("http://")) return `ws://${baseUrl.slice("http://".length)}`;
|
|
155
172
|
return baseUrl;
|
|
156
173
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
const
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
|
|
174
|
+
var STREAM_BACKOFF_INITIAL_MS = 1e3;
|
|
175
|
+
var STREAM_BACKOFF_CAP_MS = 3e4;
|
|
176
|
+
var STREAM_MAX_CONSECUTIVE_FAILURES = 5;
|
|
177
|
+
var TRANSIENT_UPGRADE_STATUSES = /* @__PURE__ */ new Set([408, 425, 429]);
|
|
178
|
+
function streamChatEvents(options) {
|
|
179
|
+
const controller = new AbortController();
|
|
180
|
+
const external = options.signal;
|
|
181
|
+
const chain = () => controller.abort(external?.reason);
|
|
182
|
+
if (external) {
|
|
183
|
+
if (external.aborted) chain();
|
|
184
|
+
else external.addEventListener("abort", chain, { once: true });
|
|
185
|
+
}
|
|
186
|
+
const inner = streamChatEventsLoop({ ...options, signal: controller.signal });
|
|
187
|
+
const detach = () => external?.removeEventListener("abort", chain);
|
|
188
|
+
const finish = async () => {
|
|
189
|
+
controller.abort();
|
|
190
|
+
detach();
|
|
174
191
|
try {
|
|
175
|
-
|
|
192
|
+
await inner.return();
|
|
176
193
|
} catch {
|
|
177
194
|
}
|
|
178
|
-
wake();
|
|
179
195
|
};
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
196
|
+
return {
|
|
197
|
+
// Detach the signal chain once the loop settles for good (done, or a
|
|
198
|
+
// terminal failure) — the iteration protocol never calls return() on a
|
|
199
|
+
// failed iterator, and the {once} listener would otherwise pile up on a
|
|
200
|
+
// long-lived signal across re-created readers.
|
|
201
|
+
async next() {
|
|
202
|
+
try {
|
|
203
|
+
const result = await inner.next();
|
|
204
|
+
if (result.done) detach();
|
|
205
|
+
return result;
|
|
206
|
+
} catch (err) {
|
|
207
|
+
detach();
|
|
208
|
+
throw err;
|
|
209
|
+
}
|
|
210
|
+
},
|
|
211
|
+
async return() {
|
|
212
|
+
await finish();
|
|
213
|
+
return { done: true, value: void 0 };
|
|
214
|
+
},
|
|
215
|
+
async throw(err) {
|
|
216
|
+
await finish();
|
|
217
|
+
throw err;
|
|
218
|
+
},
|
|
219
|
+
[Symbol.asyncIterator]() {
|
|
220
|
+
return this;
|
|
221
|
+
},
|
|
222
|
+
// Native async generators are async-disposable; the wrapper must be too.
|
|
223
|
+
async [Symbol.asyncDispose]() {
|
|
224
|
+
await finish();
|
|
186
225
|
}
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
async function* streamChatEventsLoop(options) {
|
|
229
|
+
const { baseUrl, token, chatId, signal } = options;
|
|
230
|
+
const factory = options.webSocketFactory ?? defaultFactory;
|
|
231
|
+
const wsBase = httpToWs(baseUrl);
|
|
232
|
+
const path = `/api/experimental/chats/${chatId}/stream`;
|
|
233
|
+
const query = options.afterId !== void 0 ? `?after_id=${options.afterId}` : "";
|
|
234
|
+
const url = `${wsBase}${path}${query}`;
|
|
235
|
+
let maxCommittedId = options.afterId;
|
|
236
|
+
let lastHistoryVersion;
|
|
237
|
+
let lastGenerationAttempt;
|
|
238
|
+
let lastSeq;
|
|
239
|
+
let sawUntrackableDelta = false;
|
|
240
|
+
let backoffMs = STREAM_BACKOFF_INITIAL_MS;
|
|
241
|
+
let failures = 0;
|
|
242
|
+
let lastFailure;
|
|
243
|
+
while (!signal?.aborted) {
|
|
244
|
+
const queue = [];
|
|
245
|
+
let resolveNext;
|
|
246
|
+
let finished = false;
|
|
247
|
+
let failure;
|
|
248
|
+
let parseFailure = false;
|
|
249
|
+
const wake = () => {
|
|
250
|
+
resolveNext?.();
|
|
251
|
+
resolveNext = void 0;
|
|
252
|
+
};
|
|
253
|
+
const ws = factory(url, { headers: { "Coder-Session-Token": token } });
|
|
254
|
+
const onAbort = () => {
|
|
196
255
|
finished = true;
|
|
256
|
+
try {
|
|
257
|
+
ws.close(1e3);
|
|
258
|
+
} catch {
|
|
259
|
+
}
|
|
197
260
|
wake();
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
if (
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
261
|
+
};
|
|
262
|
+
let abortListenerAdded = false;
|
|
263
|
+
if (signal) {
|
|
264
|
+
if (signal.aborted) onAbort();
|
|
265
|
+
else {
|
|
266
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
267
|
+
abortListenerAdded = true;
|
|
268
|
+
}
|
|
204
269
|
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
while (true) {
|
|
223
|
-
while (queue.length > 0) {
|
|
224
|
-
const next = queue.shift();
|
|
225
|
-
yield next;
|
|
270
|
+
const onMessage = (ev) => {
|
|
271
|
+
if (finished) return;
|
|
272
|
+
let batch;
|
|
273
|
+
try {
|
|
274
|
+
const data = typeof ev.data === "string" ? ev.data : String(ev.data);
|
|
275
|
+
batch = JSON.parse(data);
|
|
276
|
+
} catch (err) {
|
|
277
|
+
failure = new CoderAgentError("failed to parse chat stream frame", { cause: err });
|
|
278
|
+
parseFailure = true;
|
|
279
|
+
finished = true;
|
|
280
|
+
wake();
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
if (Array.isArray(batch)) {
|
|
284
|
+
for (const e of batch) queue.push(e);
|
|
285
|
+
} else if (batch && typeof batch === "object") {
|
|
286
|
+
queue.push(batch);
|
|
226
287
|
}
|
|
227
|
-
|
|
288
|
+
wake();
|
|
289
|
+
};
|
|
290
|
+
const onError = (ev) => {
|
|
228
291
|
if (finished) return;
|
|
229
|
-
|
|
230
|
-
|
|
292
|
+
const message = ev && typeof ev === "object" && "message" in ev ? String(ev.message) : "chat stream socket error";
|
|
293
|
+
const status = upgradeStatus(message);
|
|
294
|
+
failure = status !== void 0 && status >= 400 && status < 500 && !TRANSIENT_UPGRADE_STATUSES.has(status) ? new CoderApiError({ status, method: "GET", path, message }) : new CoderAgentError(message);
|
|
295
|
+
finished = true;
|
|
296
|
+
wake();
|
|
297
|
+
};
|
|
298
|
+
const onClose = () => {
|
|
299
|
+
finished = true;
|
|
300
|
+
wake();
|
|
301
|
+
};
|
|
302
|
+
ws.addEventListener("message", onMessage);
|
|
303
|
+
ws.addEventListener("error", onError);
|
|
304
|
+
ws.addEventListener("close", onClose);
|
|
305
|
+
try {
|
|
306
|
+
while (true) {
|
|
307
|
+
while (queue.length > 0) {
|
|
308
|
+
const next = queue.shift();
|
|
309
|
+
let progress = false;
|
|
310
|
+
if (next.type === "message_part") {
|
|
311
|
+
const mp = next.message_part;
|
|
312
|
+
const hv = mp?.history_version;
|
|
313
|
+
const ga = mp?.generation_attempt;
|
|
314
|
+
const seq = mp?.seq;
|
|
315
|
+
if (hv !== void 0 && ga !== void 0 && seq !== void 0) {
|
|
316
|
+
if (hv === lastHistoryVersion && ga === lastGenerationAttempt && lastSeq !== void 0 && seq <= lastSeq) {
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
lastHistoryVersion = hv;
|
|
320
|
+
lastGenerationAttempt = ga;
|
|
321
|
+
lastSeq = seq;
|
|
322
|
+
progress = true;
|
|
323
|
+
} else {
|
|
324
|
+
sawUntrackableDelta = true;
|
|
325
|
+
}
|
|
326
|
+
} else if (next.type === "message" && next.message) {
|
|
327
|
+
if (maxCommittedId === void 0 || next.message.id > maxCommittedId) {
|
|
328
|
+
maxCommittedId = next.message.id;
|
|
329
|
+
progress = true;
|
|
330
|
+
sawUntrackableDelta = false;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
if (progress) {
|
|
334
|
+
failures = 0;
|
|
335
|
+
backoffMs = STREAM_BACKOFF_INITIAL_MS;
|
|
336
|
+
}
|
|
337
|
+
yield next;
|
|
338
|
+
}
|
|
339
|
+
if (finished) break;
|
|
340
|
+
await new Promise((resolve) => {
|
|
341
|
+
resolveNext = resolve;
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
} finally {
|
|
345
|
+
finished = true;
|
|
346
|
+
if (signal && abortListenerAdded) signal.removeEventListener("abort", onAbort);
|
|
347
|
+
ws.removeEventListener("message", onMessage);
|
|
348
|
+
ws.removeEventListener("error", onError);
|
|
349
|
+
ws.removeEventListener("close", onClose);
|
|
350
|
+
try {
|
|
351
|
+
ws.close(1e3);
|
|
352
|
+
} catch {
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
if (signal?.aborted) return;
|
|
356
|
+
if (failure instanceof CoderApiError) throw failure;
|
|
357
|
+
if (parseFailure && failure) throw failure;
|
|
358
|
+
if (sawUntrackableDelta) {
|
|
359
|
+
throw new CoderStreamError({
|
|
360
|
+
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",
|
|
361
|
+
url,
|
|
362
|
+
cause: failure
|
|
231
363
|
});
|
|
232
364
|
}
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
if (
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
} catch {
|
|
365
|
+
if (failure) lastFailure = failure;
|
|
366
|
+
failures += 1;
|
|
367
|
+
if (failures >= STREAM_MAX_CONSECUTIVE_FAILURES) {
|
|
368
|
+
throw new CoderStreamError({
|
|
369
|
+
message: `Coder chat stream ${path} could not be (re-)established after ${failures} consecutive connection failures without progress`,
|
|
370
|
+
url,
|
|
371
|
+
cause: lastFailure
|
|
372
|
+
});
|
|
242
373
|
}
|
|
374
|
+
await sleep(backoffMs, signal);
|
|
375
|
+
if (signal?.aborted) return;
|
|
376
|
+
backoffMs = Math.min(backoffMs * 2, STREAM_BACKOFF_CAP_MS);
|
|
243
377
|
}
|
|
244
378
|
}
|
|
245
379
|
var WATCH_PATH = "/api/experimental/chats/watch";
|
|
@@ -882,6 +1016,95 @@ function dynamicToolNames(tools) {
|
|
|
882
1016
|
return names;
|
|
883
1017
|
}
|
|
884
1018
|
|
|
1019
|
+
// src/model/session-stream.ts
|
|
1020
|
+
var SessionChatStream = class {
|
|
1021
|
+
chatId;
|
|
1022
|
+
#events;
|
|
1023
|
+
#controller = new AbortController();
|
|
1024
|
+
#pending;
|
|
1025
|
+
#attached = true;
|
|
1026
|
+
#reusable = false;
|
|
1027
|
+
#closePromise;
|
|
1028
|
+
constructor(opts) {
|
|
1029
|
+
this.chatId = opts.chatId;
|
|
1030
|
+
this.#events = opts.open(this.#controller.signal);
|
|
1031
|
+
}
|
|
1032
|
+
get closed() {
|
|
1033
|
+
return this.#closePromise !== void 0;
|
|
1034
|
+
}
|
|
1035
|
+
/** Whether a segment is currently consuming this stream. */
|
|
1036
|
+
get attached() {
|
|
1037
|
+
return this.#attached;
|
|
1038
|
+
}
|
|
1039
|
+
/** Whether a resume segment for `chatId` may {@link attach}. */
|
|
1040
|
+
canAttach(chatId) {
|
|
1041
|
+
return !this.closed && this.#reusable && this.chatId === chatId;
|
|
1042
|
+
}
|
|
1043
|
+
/**
|
|
1044
|
+
* The next event, memoized: repeated calls return the SAME promise until
|
|
1045
|
+
* {@link consumed} acknowledges delivery, so a read abandoned by one segment
|
|
1046
|
+
* is handed to the next instead of losing its event.
|
|
1047
|
+
*/
|
|
1048
|
+
read() {
|
|
1049
|
+
if (!this.#pending) {
|
|
1050
|
+
if (!this.#attached && !this.closed) {
|
|
1051
|
+
throw new CoderAgentError("SessionChatStream.read() without an attached segment");
|
|
1052
|
+
}
|
|
1053
|
+
this.#pending = this.#events.next();
|
|
1054
|
+
this.#pending.catch(() => {
|
|
1055
|
+
});
|
|
1056
|
+
}
|
|
1057
|
+
return this.#pending;
|
|
1058
|
+
}
|
|
1059
|
+
/**
|
|
1060
|
+
* Acknowledge that the segment received `p`'s result and the next
|
|
1061
|
+
* {@link read} should issue a fresh read. A no-op unless `p` is the
|
|
1062
|
+
* outstanding read — a raced-out segment never consumes.
|
|
1063
|
+
*/
|
|
1064
|
+
consumed(p) {
|
|
1065
|
+
if (this.#pending === p) this.#pending = void 0;
|
|
1066
|
+
}
|
|
1067
|
+
/** Attach the resume segment to a stream paused at `requires_action`. */
|
|
1068
|
+
attach() {
|
|
1069
|
+
if (!this.canAttach(this.chatId) || this.#attached) {
|
|
1070
|
+
throw new CoderAgentError("SessionChatStream.attach() on a non-reusable stream");
|
|
1071
|
+
}
|
|
1072
|
+
this.#attached = true;
|
|
1073
|
+
this.#reusable = false;
|
|
1074
|
+
}
|
|
1075
|
+
/**
|
|
1076
|
+
* Segment settled at a healthy `requires_action` pause: keep the stream for
|
|
1077
|
+
* the tool-result resume, with a read outstanding so the reader's redial
|
|
1078
|
+
* machinery stays live while the client tools execute.
|
|
1079
|
+
*/
|
|
1080
|
+
pause() {
|
|
1081
|
+
this.#attached = false;
|
|
1082
|
+
if (this.closed) return;
|
|
1083
|
+
this.#reusable = true;
|
|
1084
|
+
if (!this.#pending) {
|
|
1085
|
+
this.#pending = this.#events.next();
|
|
1086
|
+
this.#pending.catch(() => {
|
|
1087
|
+
});
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
/**
|
|
1091
|
+
* Close the underlying reader (and socket). Idempotent; safe on a stream
|
|
1092
|
+
* whose generator already ended or threw.
|
|
1093
|
+
*/
|
|
1094
|
+
close() {
|
|
1095
|
+
this.#closePromise ??= (async () => {
|
|
1096
|
+
this.#attached = false;
|
|
1097
|
+
this.#reusable = false;
|
|
1098
|
+
this.#controller.abort();
|
|
1099
|
+
try {
|
|
1100
|
+
await this.#events.return(void 0);
|
|
1101
|
+
} catch {
|
|
1102
|
+
}
|
|
1103
|
+
})();
|
|
1104
|
+
return this.#closePromise;
|
|
1105
|
+
}
|
|
1106
|
+
};
|
|
1107
|
+
|
|
885
1108
|
// src/model/translate.ts
|
|
886
1109
|
function addDefined(a, b) {
|
|
887
1110
|
if (b === void 0) return a;
|
|
@@ -941,6 +1164,7 @@ function jsonResult(value) {
|
|
|
941
1164
|
}
|
|
942
1165
|
var TurnTranslator = class {
|
|
943
1166
|
#dynamicToolNames;
|
|
1167
|
+
#submittedToolCallIds;
|
|
944
1168
|
#seq = 0;
|
|
945
1169
|
#text = { id: void 0, len: 0, sawDelta: false };
|
|
946
1170
|
#reasoning = { id: void 0, len: 0, sawDelta: false };
|
|
@@ -958,6 +1182,11 @@ var TurnTranslator = class {
|
|
|
958
1182
|
// the same message replaces its earlier entry instead of double-counting
|
|
959
1183
|
// (chatd re-sends full snapshots on revision bumps and history resets).
|
|
960
1184
|
#usageByMessageId = /* @__PURE__ */ new Map();
|
|
1185
|
+
// Assistant snapshots already processed once. A redial replays the turn
|
|
1186
|
+
// from its original cursor, so earlier messages arrive again — without this,
|
|
1187
|
+
// re-running the boundary logic on them would reset the emitted-length
|
|
1188
|
+
// cursors and re-emit their entire text (see #ingestMessage).
|
|
1189
|
+
#snapshotSeen = /* @__PURE__ */ new Set();
|
|
961
1190
|
#error;
|
|
962
1191
|
#terminalStatus;
|
|
963
1192
|
#maxMessageId = 0;
|
|
@@ -972,6 +1201,7 @@ var TurnTranslator = class {
|
|
|
972
1201
|
constructor(opts) {
|
|
973
1202
|
this.#dynamicToolNames = opts.dynamicToolNames;
|
|
974
1203
|
this.#turnCursor = opts.turnCursor ?? 0;
|
|
1204
|
+
this.#submittedToolCallIds = opts.submittedToolCallIds ?? /* @__PURE__ */ new Set();
|
|
975
1205
|
}
|
|
976
1206
|
get terminalStatus() {
|
|
977
1207
|
return this.#terminalStatus;
|
|
@@ -1121,6 +1351,7 @@ var TurnTranslator = class {
|
|
|
1121
1351
|
case "action_required":
|
|
1122
1352
|
for (const tc of ev.action_required?.tool_calls ?? []) {
|
|
1123
1353
|
if (this.#clientToolCalls.has(tc.tool_call_id)) continue;
|
|
1354
|
+
if (this.#submittedToolCallIds.has(tc.tool_call_id)) continue;
|
|
1124
1355
|
this.#closeText(out);
|
|
1125
1356
|
this.#closeReasoning(out);
|
|
1126
1357
|
this.#clientToolCalls.add(tc.tool_call_id);
|
|
@@ -1142,8 +1373,12 @@ var TurnTranslator = class {
|
|
|
1142
1373
|
}
|
|
1143
1374
|
break;
|
|
1144
1375
|
case "status":
|
|
1145
|
-
if (ev.status
|
|
1146
|
-
|
|
1376
|
+
if (ev.status) {
|
|
1377
|
+
if (TERMINAL_STATUSES.has(ev.status.status)) {
|
|
1378
|
+
this.#terminalStatus = ev.status.status;
|
|
1379
|
+
} else {
|
|
1380
|
+
this.#terminalStatus = void 0;
|
|
1381
|
+
}
|
|
1147
1382
|
}
|
|
1148
1383
|
break;
|
|
1149
1384
|
default:
|
|
@@ -1197,6 +1432,17 @@ var TurnTranslator = class {
|
|
|
1197
1432
|
}
|
|
1198
1433
|
const content = message.content ?? [];
|
|
1199
1434
|
if (message.role === "assistant") {
|
|
1435
|
+
if (this.#snapshotSeen.has(message.id) && (message.id !== this.#currentAssistantId || this.#deltasSinceSnapshot)) {
|
|
1436
|
+
for (const part of content) {
|
|
1437
|
+
if (part.type === "tool-call" && !this.#isClientTool(part.tool_name))
|
|
1438
|
+
this.#emitServerToolCall(out, part);
|
|
1439
|
+
else if (part.type === "tool-result" && !this.#isClientTool(part.tool_name))
|
|
1440
|
+
this.#emitServerToolResult(out, part);
|
|
1441
|
+
else if (part.type === "source") this.#emitSource(out, part);
|
|
1442
|
+
}
|
|
1443
|
+
return;
|
|
1444
|
+
}
|
|
1445
|
+
this.#snapshotSeen.add(message.id);
|
|
1200
1446
|
if (this.#currentAssistantId !== void 0 && this.#currentAssistantId !== message.id && !this.#deltasSinceSnapshot) {
|
|
1201
1447
|
this.#closeText(out);
|
|
1202
1448
|
this.#closeReasoning(out);
|
|
@@ -1204,13 +1450,24 @@ var TurnTranslator = class {
|
|
|
1204
1450
|
this.#reasoning.sawDelta = false;
|
|
1205
1451
|
}
|
|
1206
1452
|
this.#currentAssistantId = message.id;
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1453
|
+
const emitSnapshotText = () => {
|
|
1454
|
+
if (!this.#text.sawDelta || this.#text.id) {
|
|
1455
|
+
const full = content.filter((p) => p.type === "text").map((p) => p.text ?? "").join("");
|
|
1456
|
+
if (full.length > 0) this.#emitTextUpTo(out, full);
|
|
1457
|
+
}
|
|
1458
|
+
};
|
|
1459
|
+
const emitSnapshotReasoning = () => {
|
|
1460
|
+
if (!this.#reasoning.sawDelta || this.#reasoning.id) {
|
|
1461
|
+
const full = content.filter((p) => p.type === "reasoning").map((p) => p.text ?? "").join("");
|
|
1462
|
+
if (full.length > 0) this.#emitReasoningUpTo(out, full);
|
|
1463
|
+
}
|
|
1464
|
+
};
|
|
1465
|
+
if (this.#reasoning.id) {
|
|
1466
|
+
emitSnapshotReasoning();
|
|
1467
|
+
emitSnapshotText();
|
|
1468
|
+
} else {
|
|
1469
|
+
emitSnapshotText();
|
|
1470
|
+
emitSnapshotReasoning();
|
|
1214
1471
|
}
|
|
1215
1472
|
for (const part of content) {
|
|
1216
1473
|
if (part.type === "tool-call" && !this.#isClientTool(part.tool_name))
|
|
@@ -1253,6 +1510,16 @@ var TurnTranslator = class {
|
|
|
1253
1510
|
};
|
|
1254
1511
|
|
|
1255
1512
|
// src/model/language-model.ts
|
|
1513
|
+
var ACTION_REQUIRED_GRACE_MS = 2e3;
|
|
1514
|
+
var GRACE_EXPIRED = /* @__PURE__ */ Symbol("action-required-grace-expired");
|
|
1515
|
+
var SEGMENT_ABORTED = /* @__PURE__ */ Symbol("segment-aborted");
|
|
1516
|
+
function startGraceTimer(ms) {
|
|
1517
|
+
let timer;
|
|
1518
|
+
const expired = new Promise((resolve) => {
|
|
1519
|
+
timer = setTimeout(() => resolve(GRACE_EXPIRED), ms);
|
|
1520
|
+
});
|
|
1521
|
+
return { expired, cancel: () => clearTimeout(timer) };
|
|
1522
|
+
}
|
|
1256
1523
|
var EMPTY_USAGE = {
|
|
1257
1524
|
inputTokens: {
|
|
1258
1525
|
total: void 0,
|
|
@@ -1274,6 +1541,8 @@ var CoderLanguageModel = class {
|
|
|
1274
1541
|
#modelResolved = false;
|
|
1275
1542
|
#submittedToolCallIds = /* @__PURE__ */ new Set();
|
|
1276
1543
|
#inFlight = false;
|
|
1544
|
+
/** The turn-lifetime event stream, retained across client-tool pauses. */
|
|
1545
|
+
#retainedStream;
|
|
1277
1546
|
constructor(config) {
|
|
1278
1547
|
assertSupportedAiVersion();
|
|
1279
1548
|
this.#config = config;
|
|
@@ -1283,11 +1552,75 @@ var CoderLanguageModel = class {
|
|
|
1283
1552
|
get chatId() {
|
|
1284
1553
|
return this.#chatId;
|
|
1285
1554
|
}
|
|
1286
|
-
/**
|
|
1555
|
+
/**
|
|
1556
|
+
* Drops the current session so the next turn creates a fresh chat. Also
|
|
1557
|
+
* closes the retained event stream, if any (fire-and-forget — a sync reset
|
|
1558
|
+
* must not wait on socket teardown, and `close()` never rejects).
|
|
1559
|
+
*/
|
|
1287
1560
|
resetSession() {
|
|
1288
1561
|
this.#chatId = void 0;
|
|
1289
1562
|
this.#lastSeenMessageId = 0;
|
|
1290
1563
|
this.#submittedToolCallIds.clear();
|
|
1564
|
+
const stream = this.#retainedStream;
|
|
1565
|
+
this.#retainedStream = void 0;
|
|
1566
|
+
if (stream) void stream.close();
|
|
1567
|
+
}
|
|
1568
|
+
/**
|
|
1569
|
+
* Closes the retained event stream, if any. The stream otherwise closes
|
|
1570
|
+
* when its turn settles or fails, or when the next turn replaces it — but a
|
|
1571
|
+
* turn that pauses for client tools and never resumes (e.g. the caller
|
|
1572
|
+
* stopped the tool loop, or aborted between segments) leaves the socket
|
|
1573
|
+
* open, so dispose (`await using`) or {@link resetSession} sessions you
|
|
1574
|
+
* abandon mid-turn.
|
|
1575
|
+
*/
|
|
1576
|
+
async [Symbol.asyncDispose]() {
|
|
1577
|
+
const stream = this.#retainedStream;
|
|
1578
|
+
this.#retainedStream = void 0;
|
|
1579
|
+
if (stream) await stream.close();
|
|
1580
|
+
}
|
|
1581
|
+
/**
|
|
1582
|
+
* Closes a stream retained by a client-tool pause. Called on explicit
|
|
1583
|
+
* interrupts ({@link CoderAgent.interrupt}): an interrupted pause is dead —
|
|
1584
|
+
* its buffer would otherwise collect the interrupt's own settle events,
|
|
1585
|
+
* which a later tool-result resume would consume as if they were the
|
|
1586
|
+
* resumed generation's (truncating the turn), and if the tool loop never
|
|
1587
|
+
* resumes the socket would outlive the explicit interrupt. A stream a
|
|
1588
|
+
* segment is actively reading is left alone: that segment observes the
|
|
1589
|
+
* settle the interrupt produces and closes the stream itself.
|
|
1590
|
+
*/
|
|
1591
|
+
async closePausedStream() {
|
|
1592
|
+
const stream = this.#retainedStream;
|
|
1593
|
+
if (!stream || stream.attached) return;
|
|
1594
|
+
this.#retainedStream = void 0;
|
|
1595
|
+
await stream.close();
|
|
1596
|
+
}
|
|
1597
|
+
/**
|
|
1598
|
+
* The stream a segment reads: a resume segment re-attaches to the stream
|
|
1599
|
+
* its predecessor left paused at `requires_action` (the whole point of
|
|
1600
|
+
* retention — no re-dial per tool round trip); everything else — a new
|
|
1601
|
+
* turn, or a pause the caller abandoned — replaces it with a fresh dial,
|
|
1602
|
+
* because a stale buffer from a settle this client never observed must not
|
|
1603
|
+
* leak into the new turn.
|
|
1604
|
+
*/
|
|
1605
|
+
#acquireStream(resume, chatId, afterId) {
|
|
1606
|
+
const retained = this.#retainedStream;
|
|
1607
|
+
if (resume && retained?.canAttach(chatId)) {
|
|
1608
|
+
retained.attach();
|
|
1609
|
+
return retained;
|
|
1610
|
+
}
|
|
1611
|
+
this.#retainedStream = void 0;
|
|
1612
|
+
if (retained) void retained.close();
|
|
1613
|
+
const created = new SessionChatStream({
|
|
1614
|
+
chatId,
|
|
1615
|
+
open: (streamSignal) => this.#config.client.streamEvents(chatId, { afterId, signal: streamSignal })
|
|
1616
|
+
});
|
|
1617
|
+
this.#retainedStream = created;
|
|
1618
|
+
return created;
|
|
1619
|
+
}
|
|
1620
|
+
/** Close `stream` and drop the retention slot if it still holds it. */
|
|
1621
|
+
async #closeStream(stream) {
|
|
1622
|
+
if (this.#retainedStream === stream) this.#retainedStream = void 0;
|
|
1623
|
+
await stream.close();
|
|
1291
1624
|
}
|
|
1292
1625
|
async #resolveModelConfigId(signal) {
|
|
1293
1626
|
if (this.#modelResolved) return this.#resolvedModelConfigId;
|
|
@@ -1306,17 +1639,74 @@ var CoderLanguageModel = class {
|
|
|
1306
1639
|
* id, so this runs before the chat exists). Pre-uploaded files carried via
|
|
1307
1640
|
* `providerOptions.coder.fileId` are referenced without re-uploading.
|
|
1308
1641
|
*/
|
|
1309
|
-
#buildContent(content, signal) {
|
|
1642
|
+
#buildContent(content, signal, onUpload) {
|
|
1310
1643
|
const uploadFile = async (f) => {
|
|
1311
1644
|
const uploaded = await this.#config.client.uploadChatFile(
|
|
1312
1645
|
this.#config.organizationId,
|
|
1313
1646
|
{ content: dataContentToFileContent(f.data), mediaType: f.mediaType, name: f.filename },
|
|
1314
1647
|
signal
|
|
1315
1648
|
);
|
|
1649
|
+
onUpload?.();
|
|
1316
1650
|
return uploaded.id;
|
|
1317
1651
|
};
|
|
1318
1652
|
return userContentToInputParts(content, uploadFile);
|
|
1319
1653
|
}
|
|
1654
|
+
/**
|
|
1655
|
+
* REST fallback for a `requires_action` segment whose `action_required`
|
|
1656
|
+
* event never arrived: recover the turn's pending client tool calls from
|
|
1657
|
+
* committed chat history (`GET /chats/{id}/messages`) — the same history
|
|
1658
|
+
* chatd itself derives `action_required` from server-side. Returns synthetic
|
|
1659
|
+
* stream events for the translator: the turn's message snapshots first (so
|
|
1660
|
+
* text, usage, and the message cursor stay consistent even if the stream
|
|
1661
|
+
* also missed a commit — the translator's id-keyed dedup makes re-ingesting
|
|
1662
|
+
* already-delivered ones a no-op), then one `action_required` event carrying
|
|
1663
|
+
* the unresolved dynamic tool calls. Returns `[]` when history shows no
|
|
1664
|
+
* pending calls — nothing to recover, the caller keeps waiting on the
|
|
1665
|
+
* stream, bounded by `requestTimeoutMs`/abort.
|
|
1666
|
+
*/
|
|
1667
|
+
async #recoverRequiresAction(chatId, dynamicNames, afterId, signal) {
|
|
1668
|
+
const { messages } = await this.#config.client.getMessages(chatId, { limit: 200 }, signal);
|
|
1669
|
+
const turnMessages = messages.filter((m) => m.id > (afterId ?? 0)).sort((a, b) => a.id - b.id);
|
|
1670
|
+
const lastAssistant = turnMessages.findLast((m) => m.role === "assistant");
|
|
1671
|
+
if (!lastAssistant) return [];
|
|
1672
|
+
const handled = /* @__PURE__ */ new Set();
|
|
1673
|
+
for (const message of turnMessages) {
|
|
1674
|
+
if (message.id <= lastAssistant.id) continue;
|
|
1675
|
+
for (const part of message.content ?? []) {
|
|
1676
|
+
if (part.type === "tool-result" && part.tool_call_id) handled.add(part.tool_call_id);
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
const toolCalls = [];
|
|
1680
|
+
for (const part of lastAssistant.content ?? []) {
|
|
1681
|
+
if (part.type !== "tool-call" || part.provider_executed) continue;
|
|
1682
|
+
if (!part.tool_call_id || !part.tool_name) continue;
|
|
1683
|
+
if (!dynamicNames.has(part.tool_name)) continue;
|
|
1684
|
+
if (handled.has(part.tool_call_id)) continue;
|
|
1685
|
+
if (this.#submittedToolCallIds.has(part.tool_call_id)) continue;
|
|
1686
|
+
toolCalls.push({
|
|
1687
|
+
tool_call_id: part.tool_call_id,
|
|
1688
|
+
tool_name: part.tool_name,
|
|
1689
|
+
// `action_required` carries args as their raw JSON text (chatd sends
|
|
1690
|
+
// `string(part.Args)` of the history part's json.RawMessage);
|
|
1691
|
+
// re-encoding the snapshot's parsed JSON value reconstructs it. A
|
|
1692
|
+
// JSON `null` is a legitimate argument value and must survive; only
|
|
1693
|
+
// an ABSENT args field falls back to empty-object args.
|
|
1694
|
+
args: JSON.stringify(part.args === void 0 ? {} : part.args)
|
|
1695
|
+
});
|
|
1696
|
+
}
|
|
1697
|
+
if (toolCalls.length === 0) return [];
|
|
1698
|
+
const events = turnMessages.map((message) => ({
|
|
1699
|
+
type: "message",
|
|
1700
|
+
chat_id: chatId,
|
|
1701
|
+
message
|
|
1702
|
+
}));
|
|
1703
|
+
events.push({
|
|
1704
|
+
type: "action_required",
|
|
1705
|
+
chat_id: chatId,
|
|
1706
|
+
action_required: { tool_calls: toolCalls }
|
|
1707
|
+
});
|
|
1708
|
+
return events;
|
|
1709
|
+
}
|
|
1320
1710
|
async *#runTurn(options) {
|
|
1321
1711
|
if (this.#inFlight) {
|
|
1322
1712
|
throw new CoderAgentError(
|
|
@@ -1332,9 +1722,12 @@ var CoderLanguageModel = class {
|
|
|
1332
1722
|
);
|
|
1333
1723
|
const signal = sources.length > 0 ? AbortSignal.any(sources) : void 0;
|
|
1334
1724
|
let translator;
|
|
1725
|
+
let streamAcquired = false;
|
|
1726
|
+
let discardSession = false;
|
|
1335
1727
|
let interruptSent = false;
|
|
1728
|
+
let turnChatId;
|
|
1336
1729
|
const interrupt = () => {
|
|
1337
|
-
const id = this.#chatId;
|
|
1730
|
+
const id = turnChatId ?? this.#chatId;
|
|
1338
1731
|
if (interruptSent || !id) return;
|
|
1339
1732
|
interruptSent = true;
|
|
1340
1733
|
void this.#config.client.interruptChat(id).catch(() => {
|
|
@@ -1371,6 +1764,9 @@ var CoderLanguageModel = class {
|
|
|
1371
1764
|
);
|
|
1372
1765
|
}
|
|
1373
1766
|
let afterId;
|
|
1767
|
+
let turnCreatedChat = false;
|
|
1768
|
+
let uploadedAttachment = false;
|
|
1769
|
+
let chatHasWorkspace = Boolean(this.#config.workspaceId);
|
|
1374
1770
|
if (this.#chatId && this.#lastSeenMessageId === 0) {
|
|
1375
1771
|
const { messages } = await this.#config.client.getMessages(
|
|
1376
1772
|
this.#chatId,
|
|
@@ -1382,7 +1778,9 @@ var CoderLanguageModel = class {
|
|
|
1382
1778
|
if (action.kind === "new-turn") {
|
|
1383
1779
|
const [modelConfigId, content] = await Promise.all([
|
|
1384
1780
|
this.#resolveModelConfigId(signal),
|
|
1385
|
-
this.#buildContent(action.content, signal)
|
|
1781
|
+
this.#buildContent(action.content, signal, () => {
|
|
1782
|
+
uploadedAttachment = true;
|
|
1783
|
+
})
|
|
1386
1784
|
]);
|
|
1387
1785
|
if (!this.#chatId) {
|
|
1388
1786
|
const req = {
|
|
@@ -1400,6 +1798,8 @@ var CoderLanguageModel = class {
|
|
|
1400
1798
|
if (this.#config.planMode) req.plan_mode = this.#config.planMode;
|
|
1401
1799
|
const chat = await this.#config.client.createChat(req, signal);
|
|
1402
1800
|
this.#chatId = chat.id;
|
|
1801
|
+
turnCreatedChat = true;
|
|
1802
|
+
if (chat.workspace_id) chatHasWorkspace = true;
|
|
1403
1803
|
afterId = this.#lastSeenMessageId > 0 ? this.#lastSeenMessageId : void 0;
|
|
1404
1804
|
} else {
|
|
1405
1805
|
const resp = await this.#config.client.createChatMessage(
|
|
@@ -1425,14 +1825,79 @@ var CoderLanguageModel = class {
|
|
|
1425
1825
|
afterId = this.#lastSeenMessageId;
|
|
1426
1826
|
}
|
|
1427
1827
|
const chatId = this.#chatId;
|
|
1828
|
+
turnChatId = chatId;
|
|
1829
|
+
const dynamicNames = dynamicToolNames(options.tools);
|
|
1428
1830
|
translator = new TurnTranslator({
|
|
1429
|
-
dynamicToolNames:
|
|
1430
|
-
turnCursor: afterId ?? 0
|
|
1831
|
+
dynamicToolNames: dynamicNames,
|
|
1832
|
+
turnCursor: afterId ?? 0,
|
|
1833
|
+
// The retained stream redials with the TURN's original cursor, so a
|
|
1834
|
+
// reconnect can replay an earlier segment's `action_required`
|
|
1835
|
+
// snapshot; calls this session already answered must not run again.
|
|
1836
|
+
submittedToolCallIds: this.#submittedToolCallIds
|
|
1431
1837
|
});
|
|
1432
1838
|
let sinceRequiresAction = 0;
|
|
1839
|
+
let grace;
|
|
1840
|
+
let recovery;
|
|
1841
|
+
let recoveryAbort;
|
|
1842
|
+
let recoveryAttempted = false;
|
|
1843
|
+
const stream = this.#acquireStream(action.kind === "resume", chatId, afterId);
|
|
1844
|
+
streamAcquired = true;
|
|
1845
|
+
let onSegmentAbort;
|
|
1846
|
+
const segmentAborted = signal ? new Promise((resolve) => {
|
|
1847
|
+
onSegmentAbort = () => resolve(SEGMENT_ABORTED);
|
|
1848
|
+
if (signal.aborted) onSegmentAbort();
|
|
1849
|
+
else signal.addEventListener("abort", onSegmentAbort, { once: true });
|
|
1850
|
+
}) : void 0;
|
|
1851
|
+
const raceSegment = (p) => segmentAborted ? Promise.race([p, segmentAborted]) : p;
|
|
1852
|
+
let retainStream = false;
|
|
1433
1853
|
try {
|
|
1434
|
-
for
|
|
1435
|
-
|
|
1854
|
+
for (; ; ) {
|
|
1855
|
+
const next = stream.read();
|
|
1856
|
+
const awaitingActionRequired = translator.terminalStatus === "requires_action" && !translator.clientToolCallSeen;
|
|
1857
|
+
if (!awaitingActionRequired && (grace !== void 0 || recoveryAttempted)) {
|
|
1858
|
+
grace?.cancel();
|
|
1859
|
+
grace = void 0;
|
|
1860
|
+
recoveryAbort?.abort();
|
|
1861
|
+
recoveryAbort = void 0;
|
|
1862
|
+
recovery = void 0;
|
|
1863
|
+
recoveryAttempted = false;
|
|
1864
|
+
sinceRequiresAction = 0;
|
|
1865
|
+
}
|
|
1866
|
+
let raced;
|
|
1867
|
+
if (awaitingActionRequired && !recoveryAttempted) {
|
|
1868
|
+
grace ??= startGraceTimer(ACTION_REQUIRED_GRACE_MS);
|
|
1869
|
+
raced = await raceSegment(Promise.race([next, grace.expired]));
|
|
1870
|
+
if (raced === GRACE_EXPIRED) {
|
|
1871
|
+
recoveryAttempted = true;
|
|
1872
|
+
throwIfAborted();
|
|
1873
|
+
recoveryAbort = new AbortController();
|
|
1874
|
+
recovery = this.#recoverRequiresAction(
|
|
1875
|
+
chatId,
|
|
1876
|
+
dynamicNames,
|
|
1877
|
+
afterId,
|
|
1878
|
+
signal ? AbortSignal.any([signal, recoveryAbort.signal]) : recoveryAbort.signal
|
|
1879
|
+
).catch(() => []);
|
|
1880
|
+
continue;
|
|
1881
|
+
}
|
|
1882
|
+
} else if (awaitingActionRequired && recovery) {
|
|
1883
|
+
raced = await raceSegment(Promise.race([next, recovery]));
|
|
1884
|
+
if (Array.isArray(raced)) {
|
|
1885
|
+
recovery = void 0;
|
|
1886
|
+
throwIfAborted();
|
|
1887
|
+
for (const ev of raced) for (const part of translator.ingest(ev)) yield part;
|
|
1888
|
+
if (translator.clientToolCallSeen) break;
|
|
1889
|
+
continue;
|
|
1890
|
+
}
|
|
1891
|
+
} else {
|
|
1892
|
+
raced = await raceSegment(next);
|
|
1893
|
+
}
|
|
1894
|
+
if (raced === SEGMENT_ABORTED) {
|
|
1895
|
+
throwIfAborted();
|
|
1896
|
+
throw new CoderAgentError("segment abort sentinel resolved without an abort");
|
|
1897
|
+
}
|
|
1898
|
+
stream.consumed(next);
|
|
1899
|
+
if (raced.done) break;
|
|
1900
|
+
for (const part of translator.ingest(raced.value)) yield part;
|
|
1436
1901
|
const status = translator.terminalStatus;
|
|
1437
1902
|
if (status) {
|
|
1438
1903
|
if (status !== "requires_action") break;
|
|
@@ -1440,8 +1905,22 @@ var CoderLanguageModel = class {
|
|
|
1440
1905
|
if (++sinceRequiresAction > 200) break;
|
|
1441
1906
|
}
|
|
1442
1907
|
}
|
|
1908
|
+
retainStream = !signal?.aborted && translator.terminalStatus === "requires_action" && translator.clientToolCallSeen && !translator.error;
|
|
1443
1909
|
} catch (err) {
|
|
1444
1910
|
throwIfAborted();
|
|
1911
|
+
if (err instanceof CoderStreamError) {
|
|
1912
|
+
if (turnCreatedChat) {
|
|
1913
|
+
discardSession = true;
|
|
1914
|
+
}
|
|
1915
|
+
const effectful = chatHasWorkspace || Boolean(this.#config.mcpServerIds?.length) || uploadedAttachment;
|
|
1916
|
+
if (turnCreatedChat && !effectful) throw err;
|
|
1917
|
+
throw new CoderStreamError({
|
|
1918
|
+
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)"}`,
|
|
1919
|
+
url: err.url,
|
|
1920
|
+
cause: err,
|
|
1921
|
+
isRetryable: false
|
|
1922
|
+
});
|
|
1923
|
+
}
|
|
1445
1924
|
if (err instanceof CoderAgentError && !(err instanceof CoderApiError) && !(err instanceof CoderChatError)) {
|
|
1446
1925
|
throw new CoderChatError({
|
|
1447
1926
|
message: `Coder chat stream failed mid-turn: ${err.message}`,
|
|
@@ -1450,6 +1929,15 @@ var CoderLanguageModel = class {
|
|
|
1450
1929
|
});
|
|
1451
1930
|
}
|
|
1452
1931
|
throw err;
|
|
1932
|
+
} finally {
|
|
1933
|
+
grace?.cancel();
|
|
1934
|
+
recoveryAbort?.abort();
|
|
1935
|
+
if (signal && onSegmentAbort) signal.removeEventListener("abort", onSegmentAbort);
|
|
1936
|
+
if (retainStream && !signal?.aborted && !stream.closed) {
|
|
1937
|
+
stream.pause();
|
|
1938
|
+
} else {
|
|
1939
|
+
await this.#closeStream(stream);
|
|
1940
|
+
}
|
|
1453
1941
|
}
|
|
1454
1942
|
throwIfAborted();
|
|
1455
1943
|
if (!translator.terminalStatus && !translator.error) {
|
|
@@ -1468,6 +1956,12 @@ var CoderLanguageModel = class {
|
|
|
1468
1956
|
this.#lastSeenMessageId = translator.maxMessageId;
|
|
1469
1957
|
signal?.removeEventListener("abort", interrupt);
|
|
1470
1958
|
if (!translator?.terminalStatus) interrupt();
|
|
1959
|
+
if (!streamAcquired && this.#retainedStream) {
|
|
1960
|
+
const stale = this.#retainedStream;
|
|
1961
|
+
this.#retainedStream = void 0;
|
|
1962
|
+
void stale.close();
|
|
1963
|
+
}
|
|
1964
|
+
if (discardSession) this.resetSession();
|
|
1471
1965
|
this.#inFlight = false;
|
|
1472
1966
|
}
|
|
1473
1967
|
}
|
|
@@ -1716,7 +2210,9 @@ var CoderAgent = class {
|
|
|
1716
2210
|
*/
|
|
1717
2211
|
async interrupt(opts) {
|
|
1718
2212
|
const id = this.#model.chatId;
|
|
1719
|
-
if (id)
|
|
2213
|
+
if (!id) return;
|
|
2214
|
+
await this.#model.closePausedStream();
|
|
2215
|
+
await this.#client.interruptChat(id, opts?.signal);
|
|
1720
2216
|
}
|
|
1721
2217
|
/**
|
|
1722
2218
|
* Archive the underlying chat (safe cleanup; hides it from listings).
|
|
@@ -1730,6 +2226,7 @@ var CoderAgent = class {
|
|
|
1730
2226
|
async archive(opts) {
|
|
1731
2227
|
const id = this.#model.chatId;
|
|
1732
2228
|
if (!id) return;
|
|
2229
|
+
await this.#model.closePausedStream();
|
|
1733
2230
|
await archiveWhenSettled(this.#client, id, {
|
|
1734
2231
|
deadlineMs: this.#settleDeadlineMs,
|
|
1735
2232
|
retryDelayMs: this.#settleRetryDelayMs,
|
|
@@ -1753,6 +2250,10 @@ var CoderAgent = class {
|
|
|
1753
2250
|
* ```
|
|
1754
2251
|
*/
|
|
1755
2252
|
async [Symbol.asyncDispose]() {
|
|
2253
|
+
try {
|
|
2254
|
+
await this.#model[Symbol.asyncDispose]();
|
|
2255
|
+
} catch {
|
|
2256
|
+
}
|
|
1756
2257
|
const deadline = AbortSignal.timeout(this.#settleDeadlineMs);
|
|
1757
2258
|
try {
|
|
1758
2259
|
await this.interrupt({ signal: deadline });
|
|
@@ -1793,7 +2294,7 @@ var CoderAgent = class {
|
|
|
1793
2294
|
async uploadToWorkspace(file) {
|
|
1794
2295
|
if (!this.#workspaceFiles) {
|
|
1795
2296
|
throw new CoderAgentError(
|
|
1796
|
-
"uploadToWorkspace requires a `workspaceFiles` adapter. Construct the agent with `workspaceFiles` (e.g. backed by a @coder/ai-sdk-
|
|
2297
|
+
"uploadToWorkspace requires a `workspaceFiles` adapter. Construct the agent with `workspaceFiles` (e.g. backed by a @coder/ai-sdk-sandbox workspace session) to enable it."
|
|
1797
2298
|
);
|
|
1798
2299
|
}
|
|
1799
2300
|
const { path } = await this.#workspaceFiles.write({
|
|
@@ -1964,7 +2465,9 @@ export {
|
|
|
1964
2465
|
CoderChatClient,
|
|
1965
2466
|
CoderChatError,
|
|
1966
2467
|
CoderLanguageModel,
|
|
2468
|
+
CoderStreamError,
|
|
1967
2469
|
MAX_CHAT_FILE_SIZE_BYTES,
|
|
2470
|
+
TERMINAL_STATUSES,
|
|
1968
2471
|
TurnTranslator,
|
|
1969
2472
|
chatMessagesToUIMessages,
|
|
1970
2473
|
classifyTurnAction,
|