@coder/ai-sdk-agent 0.4.2 → 0.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -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
- async function* streamChatEvents(options) {
158
- const { baseUrl, token, chatId, afterId, signal } = options;
159
- const factory = options.webSocketFactory ?? defaultFactory;
160
- const wsBase = httpToWs(baseUrl);
161
- const query = afterId !== void 0 ? `?after_id=${afterId}` : "";
162
- const url = `${wsBase}/api/experimental/chats/${chatId}/stream${query}`;
163
- const queue = [];
164
- let resolveNext;
165
- let finished = false;
166
- let failure;
167
- const wake = () => {
168
- resolveNext?.();
169
- resolveNext = void 0;
170
- };
171
- const ws = factory(url, { headers: { "Coder-Session-Token": token } });
172
- const onAbort = () => {
173
- finished = true;
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
- ws.close(1e3);
192
+ await inner.return();
176
193
  } catch {
177
194
  }
178
- wake();
179
195
  };
180
- let abortListenerAdded = false;
181
- if (signal) {
182
- if (signal.aborted) onAbort();
183
- else {
184
- signal.addEventListener("abort", onAbort, { once: true });
185
- abortListenerAdded = true;
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
- const onMessage = (ev) => {
189
- if (finished) return;
190
- let batch;
191
- try {
192
- const data = typeof ev.data === "string" ? ev.data : String(ev.data);
193
- batch = JSON.parse(data);
194
- } catch (err) {
195
- failure = new CoderAgentError("failed to parse chat stream frame", { cause: err });
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
- return;
199
- }
200
- if (Array.isArray(batch)) {
201
- for (const e of batch) queue.push(e);
202
- } else if (batch && typeof batch === "object") {
203
- queue.push(batch);
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
- wake();
206
- };
207
- const onError = (ev) => {
208
- if (finished) return;
209
- const message = ev && typeof ev === "object" && "message" in ev ? String(ev.message) : "chat stream socket error";
210
- failure = new CoderAgentError(message);
211
- finished = true;
212
- wake();
213
- };
214
- const onClose = () => {
215
- finished = true;
216
- wake();
217
- };
218
- ws.addEventListener("message", onMessage);
219
- ws.addEventListener("error", onError);
220
- ws.addEventListener("close", onClose);
221
- try {
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
- if (failure) throw failure;
288
+ wake();
289
+ };
290
+ const onError = (ev) => {
228
291
  if (finished) return;
229
- await new Promise((resolve) => {
230
- resolveNext = resolve;
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
- } finally {
234
- finished = true;
235
- if (signal && abortListenerAdded) signal.removeEventListener("abort", onAbort);
236
- ws.removeEventListener("message", onMessage);
237
- ws.removeEventListener("error", onError);
238
- ws.removeEventListener("close", onClose);
239
- try {
240
- ws.close(1e3);
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";
@@ -958,6 +1092,11 @@ var TurnTranslator = class {
958
1092
  // the same message replaces its earlier entry instead of double-counting
959
1093
  // (chatd re-sends full snapshots on revision bumps and history resets).
960
1094
  #usageByMessageId = /* @__PURE__ */ new Map();
1095
+ // Assistant snapshots already processed once. A redial replays the turn
1096
+ // from its original cursor, so earlier messages arrive again — without this,
1097
+ // re-running the boundary logic on them would reset the emitted-length
1098
+ // cursors and re-emit their entire text (see #ingestMessage).
1099
+ #snapshotSeen = /* @__PURE__ */ new Set();
961
1100
  #error;
962
1101
  #terminalStatus;
963
1102
  #maxMessageId = 0;
@@ -1197,6 +1336,17 @@ var TurnTranslator = class {
1197
1336
  }
1198
1337
  const content = message.content ?? [];
1199
1338
  if (message.role === "assistant") {
1339
+ if (this.#snapshotSeen.has(message.id) && (message.id !== this.#currentAssistantId || this.#deltasSinceSnapshot)) {
1340
+ for (const part of content) {
1341
+ if (part.type === "tool-call" && !this.#isClientTool(part.tool_name))
1342
+ this.#emitServerToolCall(out, part);
1343
+ else if (part.type === "tool-result" && !this.#isClientTool(part.tool_name))
1344
+ this.#emitServerToolResult(out, part);
1345
+ else if (part.type === "source") this.#emitSource(out, part);
1346
+ }
1347
+ return;
1348
+ }
1349
+ this.#snapshotSeen.add(message.id);
1200
1350
  if (this.#currentAssistantId !== void 0 && this.#currentAssistantId !== message.id && !this.#deltasSinceSnapshot) {
1201
1351
  this.#closeText(out);
1202
1352
  this.#closeReasoning(out);
@@ -1204,13 +1354,24 @@ var TurnTranslator = class {
1204
1354
  this.#reasoning.sawDelta = false;
1205
1355
  }
1206
1356
  this.#currentAssistantId = message.id;
1207
- if (!this.#text.sawDelta) {
1208
- const full = content.filter((p) => p.type === "text").map((p) => p.text ?? "").join("");
1209
- if (full.length > 0) this.#emitTextUpTo(out, full);
1210
- }
1211
- if (!this.#reasoning.sawDelta) {
1212
- const full = content.filter((p) => p.type === "reasoning").map((p) => p.text ?? "").join("");
1213
- if (full.length > 0) this.#emitReasoningUpTo(out, full);
1357
+ const emitSnapshotText = () => {
1358
+ if (!this.#text.sawDelta || this.#text.id) {
1359
+ const full = content.filter((p) => p.type === "text").map((p) => p.text ?? "").join("");
1360
+ if (full.length > 0) this.#emitTextUpTo(out, full);
1361
+ }
1362
+ };
1363
+ const emitSnapshotReasoning = () => {
1364
+ if (!this.#reasoning.sawDelta || this.#reasoning.id) {
1365
+ const full = content.filter((p) => p.type === "reasoning").map((p) => p.text ?? "").join("");
1366
+ if (full.length > 0) this.#emitReasoningUpTo(out, full);
1367
+ }
1368
+ };
1369
+ if (this.#reasoning.id) {
1370
+ emitSnapshotReasoning();
1371
+ emitSnapshotText();
1372
+ } else {
1373
+ emitSnapshotText();
1374
+ emitSnapshotReasoning();
1214
1375
  }
1215
1376
  for (const part of content) {
1216
1377
  if (part.type === "tool-call" && !this.#isClientTool(part.tool_name))
@@ -1253,6 +1414,15 @@ var TurnTranslator = class {
1253
1414
  };
1254
1415
 
1255
1416
  // src/model/language-model.ts
1417
+ var ACTION_REQUIRED_GRACE_MS = 2e3;
1418
+ var GRACE_EXPIRED = /* @__PURE__ */ Symbol("action-required-grace-expired");
1419
+ function startGraceTimer(ms) {
1420
+ let timer;
1421
+ const expired = new Promise((resolve) => {
1422
+ timer = setTimeout(() => resolve(GRACE_EXPIRED), ms);
1423
+ });
1424
+ return { expired, cancel: () => clearTimeout(timer) };
1425
+ }
1256
1426
  var EMPTY_USAGE = {
1257
1427
  inputTokens: {
1258
1428
  total: void 0,
@@ -1306,17 +1476,74 @@ var CoderLanguageModel = class {
1306
1476
  * id, so this runs before the chat exists). Pre-uploaded files carried via
1307
1477
  * `providerOptions.coder.fileId` are referenced without re-uploading.
1308
1478
  */
1309
- #buildContent(content, signal) {
1479
+ #buildContent(content, signal, onUpload) {
1310
1480
  const uploadFile = async (f) => {
1311
1481
  const uploaded = await this.#config.client.uploadChatFile(
1312
1482
  this.#config.organizationId,
1313
1483
  { content: dataContentToFileContent(f.data), mediaType: f.mediaType, name: f.filename },
1314
1484
  signal
1315
1485
  );
1486
+ onUpload?.();
1316
1487
  return uploaded.id;
1317
1488
  };
1318
1489
  return userContentToInputParts(content, uploadFile);
1319
1490
  }
1491
+ /**
1492
+ * REST fallback for a `requires_action` segment whose `action_required`
1493
+ * event never arrived: recover the turn's pending client tool calls from
1494
+ * committed chat history (`GET /chats/{id}/messages`) — the same history
1495
+ * chatd itself derives `action_required` from server-side. Returns synthetic
1496
+ * stream events for the translator: the turn's message snapshots first (so
1497
+ * text, usage, and the message cursor stay consistent even if the stream
1498
+ * also missed a commit — the translator's id-keyed dedup makes re-ingesting
1499
+ * already-delivered ones a no-op), then one `action_required` event carrying
1500
+ * the unresolved dynamic tool calls. Returns `[]` when history shows no
1501
+ * pending calls — nothing to recover, the caller keeps waiting on the
1502
+ * stream, bounded by `requestTimeoutMs`/abort.
1503
+ */
1504
+ async #recoverRequiresAction(chatId, dynamicNames, afterId, signal) {
1505
+ const { messages } = await this.#config.client.getMessages(chatId, { limit: 200 }, signal);
1506
+ const turnMessages = messages.filter((m) => m.id > (afterId ?? 0)).sort((a, b) => a.id - b.id);
1507
+ const lastAssistant = turnMessages.findLast((m) => m.role === "assistant");
1508
+ if (!lastAssistant) return [];
1509
+ const handled = /* @__PURE__ */ new Set();
1510
+ for (const message of turnMessages) {
1511
+ if (message.id <= lastAssistant.id) continue;
1512
+ for (const part of message.content ?? []) {
1513
+ if (part.type === "tool-result" && part.tool_call_id) handled.add(part.tool_call_id);
1514
+ }
1515
+ }
1516
+ const toolCalls = [];
1517
+ for (const part of lastAssistant.content ?? []) {
1518
+ if (part.type !== "tool-call" || part.provider_executed) continue;
1519
+ if (!part.tool_call_id || !part.tool_name) continue;
1520
+ if (!dynamicNames.has(part.tool_name)) continue;
1521
+ if (handled.has(part.tool_call_id)) continue;
1522
+ if (this.#submittedToolCallIds.has(part.tool_call_id)) continue;
1523
+ toolCalls.push({
1524
+ tool_call_id: part.tool_call_id,
1525
+ tool_name: part.tool_name,
1526
+ // `action_required` carries args as their raw JSON text (chatd sends
1527
+ // `string(part.Args)` of the history part's json.RawMessage);
1528
+ // re-encoding the snapshot's parsed JSON value reconstructs it. A
1529
+ // JSON `null` is a legitimate argument value and must survive; only
1530
+ // an ABSENT args field falls back to empty-object args.
1531
+ args: JSON.stringify(part.args === void 0 ? {} : part.args)
1532
+ });
1533
+ }
1534
+ if (toolCalls.length === 0) return [];
1535
+ const events = turnMessages.map((message) => ({
1536
+ type: "message",
1537
+ chat_id: chatId,
1538
+ message
1539
+ }));
1540
+ events.push({
1541
+ type: "action_required",
1542
+ chat_id: chatId,
1543
+ action_required: { tool_calls: toolCalls }
1544
+ });
1545
+ return events;
1546
+ }
1320
1547
  async *#runTurn(options) {
1321
1548
  if (this.#inFlight) {
1322
1549
  throw new CoderAgentError(
@@ -1332,6 +1559,7 @@ var CoderLanguageModel = class {
1332
1559
  );
1333
1560
  const signal = sources.length > 0 ? AbortSignal.any(sources) : void 0;
1334
1561
  let translator;
1562
+ let discardSession = false;
1335
1563
  let interruptSent = false;
1336
1564
  const interrupt = () => {
1337
1565
  const id = this.#chatId;
@@ -1371,6 +1599,9 @@ var CoderLanguageModel = class {
1371
1599
  );
1372
1600
  }
1373
1601
  let afterId;
1602
+ let turnCreatedChat = false;
1603
+ let uploadedAttachment = false;
1604
+ let chatHasWorkspace = Boolean(this.#config.workspaceId);
1374
1605
  if (this.#chatId && this.#lastSeenMessageId === 0) {
1375
1606
  const { messages } = await this.#config.client.getMessages(
1376
1607
  this.#chatId,
@@ -1382,7 +1613,9 @@ var CoderLanguageModel = class {
1382
1613
  if (action.kind === "new-turn") {
1383
1614
  const [modelConfigId, content] = await Promise.all([
1384
1615
  this.#resolveModelConfigId(signal),
1385
- this.#buildContent(action.content, signal)
1616
+ this.#buildContent(action.content, signal, () => {
1617
+ uploadedAttachment = true;
1618
+ })
1386
1619
  ]);
1387
1620
  if (!this.#chatId) {
1388
1621
  const req = {
@@ -1400,6 +1633,8 @@ var CoderLanguageModel = class {
1400
1633
  if (this.#config.planMode) req.plan_mode = this.#config.planMode;
1401
1634
  const chat = await this.#config.client.createChat(req, signal);
1402
1635
  this.#chatId = chat.id;
1636
+ turnCreatedChat = true;
1637
+ if (chat.workspace_id) chatHasWorkspace = true;
1403
1638
  afterId = this.#lastSeenMessageId > 0 ? this.#lastSeenMessageId : void 0;
1404
1639
  } else {
1405
1640
  const resp = await this.#config.client.createChatMessage(
@@ -1425,14 +1660,58 @@ var CoderLanguageModel = class {
1425
1660
  afterId = this.#lastSeenMessageId;
1426
1661
  }
1427
1662
  const chatId = this.#chatId;
1663
+ const dynamicNames = dynamicToolNames(options.tools);
1428
1664
  translator = new TurnTranslator({
1429
- dynamicToolNames: dynamicToolNames(options.tools),
1665
+ dynamicToolNames: dynamicNames,
1430
1666
  turnCursor: afterId ?? 0
1431
1667
  });
1432
1668
  let sinceRequiresAction = 0;
1669
+ let grace;
1670
+ let recovery;
1671
+ let recoveryAbort;
1672
+ let recoveryAttempted = false;
1673
+ const stream = this.#config.client.streamEvents(chatId, { afterId, signal });
1433
1674
  try {
1434
- for await (const ev of this.#config.client.streamEvents(chatId, { afterId, signal })) {
1435
- for (const part of translator.ingest(ev)) yield part;
1675
+ let next = stream.next();
1676
+ for (; ; ) {
1677
+ let result;
1678
+ const awaitingActionRequired = translator.terminalStatus === "requires_action" && !translator.clientToolCallSeen;
1679
+ if (awaitingActionRequired && !recoveryAttempted) {
1680
+ grace ??= startGraceTimer(ACTION_REQUIRED_GRACE_MS);
1681
+ const raced = await Promise.race([next, grace.expired]);
1682
+ if (raced === GRACE_EXPIRED) {
1683
+ recoveryAttempted = true;
1684
+ throwIfAborted();
1685
+ recoveryAbort = new AbortController();
1686
+ recovery = this.#recoverRequiresAction(
1687
+ chatId,
1688
+ dynamicNames,
1689
+ afterId,
1690
+ signal ? AbortSignal.any([signal, recoveryAbort.signal]) : recoveryAbort.signal
1691
+ ).catch(() => []);
1692
+ continue;
1693
+ }
1694
+ result = raced;
1695
+ } else if (awaitingActionRequired && recovery) {
1696
+ const raced = await Promise.race([next, recovery]);
1697
+ if (Array.isArray(raced)) {
1698
+ recovery = void 0;
1699
+ throwIfAborted();
1700
+ for (const ev of raced) for (const part of translator.ingest(ev)) yield part;
1701
+ if (translator.clientToolCallSeen) {
1702
+ void next.catch(() => {
1703
+ });
1704
+ break;
1705
+ }
1706
+ continue;
1707
+ }
1708
+ result = raced;
1709
+ } else {
1710
+ result = await next;
1711
+ }
1712
+ if (result.done) break;
1713
+ next = stream.next();
1714
+ for (const part of translator.ingest(result.value)) yield part;
1436
1715
  const status = translator.terminalStatus;
1437
1716
  if (status) {
1438
1717
  if (status !== "requires_action") break;
@@ -1442,6 +1721,19 @@ var CoderLanguageModel = class {
1442
1721
  }
1443
1722
  } catch (err) {
1444
1723
  throwIfAborted();
1724
+ if (err instanceof CoderStreamError) {
1725
+ if (turnCreatedChat) {
1726
+ discardSession = true;
1727
+ }
1728
+ const effectful = chatHasWorkspace || Boolean(this.#config.mcpServerIds?.length) || uploadedAttachment;
1729
+ if (turnCreatedChat && !effectful) throw err;
1730
+ throw new CoderStreamError({
1731
+ 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)"}`,
1732
+ url: err.url,
1733
+ cause: err,
1734
+ isRetryable: false
1735
+ });
1736
+ }
1445
1737
  if (err instanceof CoderAgentError && !(err instanceof CoderApiError) && !(err instanceof CoderChatError)) {
1446
1738
  throw new CoderChatError({
1447
1739
  message: `Coder chat stream failed mid-turn: ${err.message}`,
@@ -1450,6 +1742,11 @@ var CoderLanguageModel = class {
1450
1742
  });
1451
1743
  }
1452
1744
  throw err;
1745
+ } finally {
1746
+ grace?.cancel();
1747
+ recoveryAbort?.abort();
1748
+ await stream.return(void 0).catch(() => {
1749
+ });
1453
1750
  }
1454
1751
  throwIfAborted();
1455
1752
  if (!translator.terminalStatus && !translator.error) {
@@ -1468,6 +1765,7 @@ var CoderLanguageModel = class {
1468
1765
  this.#lastSeenMessageId = translator.maxMessageId;
1469
1766
  signal?.removeEventListener("abort", interrupt);
1470
1767
  if (!translator?.terminalStatus) interrupt();
1768
+ if (discardSession) this.resetSession();
1471
1769
  this.#inFlight = false;
1472
1770
  }
1473
1771
  }
@@ -1793,7 +2091,7 @@ var CoderAgent = class {
1793
2091
  async uploadToWorkspace(file) {
1794
2092
  if (!this.#workspaceFiles) {
1795
2093
  throw new CoderAgentError(
1796
- "uploadToWorkspace requires a `workspaceFiles` adapter. Construct the agent with `workspaceFiles` (e.g. backed by a @coder/ai-sdk-eve-sandbox session) to enable it."
2094
+ "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
2095
  );
1798
2096
  }
1799
2097
  const { path } = await this.#workspaceFiles.write({
@@ -1964,7 +2262,9 @@ export {
1964
2262
  CoderChatClient,
1965
2263
  CoderChatError,
1966
2264
  CoderLanguageModel,
2265
+ CoderStreamError,
1967
2266
  MAX_CHAT_FILE_SIZE_BYTES,
2267
+ TERMINAL_STATUSES,
1968
2268
  TurnTranslator,
1969
2269
  chatMessagesToUIMessages,
1970
2270
  classifyTurnAction,