@coder/ai-sdk-agent 0.6.2 → 0.7.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/dist/index.js CHANGED
@@ -185,6 +185,11 @@ function httpToWs(baseUrl) {
185
185
  if (baseUrl.startsWith("http://")) return `ws://${baseUrl.slice("http://".length)}`;
186
186
  return baseUrl;
187
187
  }
188
+ var readerSeq = 0;
189
+ function nextStreamReaderId() {
190
+ readerSeq += 1;
191
+ return readerSeq;
192
+ }
188
193
  var STREAM_BACKOFF_INITIAL_MS = 1e3;
189
194
  var STREAM_BACKOFF_CAP_MS = 3e4;
190
195
  var STREAM_MAX_CONSECUTIVE_FAILURES = 5;
@@ -197,7 +202,8 @@ function streamChatEvents(options) {
197
202
  if (external.aborted) chain();
198
203
  else external.addEventListener("abort", chain, { once: true });
199
204
  }
200
- const inner = streamChatEventsLoop({ ...options, signal: controller.signal });
205
+ const reader = options.reader ?? nextStreamReaderId();
206
+ const inner = streamChatEventsLoop({ ...options, reader, signal: controller.signal });
201
207
  const detach = () => external?.removeEventListener("abort", chain);
202
208
  const finish = async () => {
203
209
  controller.abort();
@@ -240,7 +246,7 @@ function streamChatEvents(options) {
240
246
  };
241
247
  }
242
248
  async function* streamChatEventsLoop(options) {
243
- const { baseUrl, token, chatId, signal } = options;
249
+ const { baseUrl, token, chatId, signal, reader } = options;
244
250
  const factory = options.webSocketFactory ?? defaultFactory;
245
251
  const emit = safeTransportEmitter(options.onTransportEvent);
246
252
  let attempt = 0;
@@ -269,15 +275,15 @@ async function* streamChatEventsLoop(options) {
269
275
  resolveNext = void 0;
270
276
  };
271
277
  attempt += 1;
272
- emit?.({ type: "ws:dial", chatId, attempt, url, timestamp: Date.now() });
278
+ emit?.({ type: "ws:dial", chatId, reader, attempt, url, timestamp: Date.now() });
273
279
  let ws;
274
280
  try {
275
281
  ws = factory(url, { headers: { "Coder-Session-Token": token } });
276
282
  } catch (err) {
277
283
  if (emit) {
278
284
  const message = err instanceof Error ? err.message : String(err);
279
- emit({ type: "ws:error", chatId, attempt, message, timestamp: Date.now() });
280
- emit({ type: "ws:close", chatId, attempt, timestamp: Date.now() });
285
+ emit({ type: "ws:error", chatId, reader, attempt, message, timestamp: Date.now() });
286
+ emit({ type: "ws:close", chatId, reader, attempt, timestamp: Date.now() });
281
287
  }
282
288
  throw err;
283
289
  }
@@ -307,6 +313,7 @@ async function* streamChatEventsLoop(options) {
307
313
  emit?.({
308
314
  type: "ws:error",
309
315
  chatId,
316
+ reader,
310
317
  attempt,
311
318
  message: "failed to parse chat stream frame",
312
319
  timestamp: Date.now()
@@ -323,6 +330,7 @@ async function* streamChatEventsLoop(options) {
323
330
  emit?.({
324
331
  type: "ws:event",
325
332
  chatId,
333
+ reader,
326
334
  attempt,
327
335
  event: e,
328
336
  timestamp: Date.now()
@@ -333,6 +341,7 @@ async function* streamChatEventsLoop(options) {
333
341
  emit?.({
334
342
  type: "ws:event",
335
343
  chatId,
344
+ reader,
336
345
  attempt,
337
346
  event: batch,
338
347
  timestamp: Date.now()
@@ -343,7 +352,7 @@ async function* streamChatEventsLoop(options) {
343
352
  const onError = (ev) => {
344
353
  if (finished) return;
345
354
  const message = ev && typeof ev === "object" && "message" in ev ? String(ev.message) : "chat stream socket error";
346
- emit?.({ type: "ws:error", chatId, attempt, message, timestamp: Date.now() });
355
+ emit?.({ type: "ws:error", chatId, reader, attempt, message, timestamp: Date.now() });
347
356
  const status = upgradeStatus(message);
348
357
  failure = status !== void 0 && status >= 400 && status < 500 && !TRANSIENT_UPGRADE_STATUSES.has(status) ? new CoderApiError({ status, method: "GET", path, message }) : new CoderAgentError(message);
349
358
  finished = true;
@@ -355,6 +364,7 @@ async function* streamChatEventsLoop(options) {
355
364
  const closeEvent = {
356
365
  type: "ws:close",
357
366
  chatId,
367
+ reader,
358
368
  attempt,
359
369
  timestamp: Date.now()
360
370
  };
@@ -365,7 +375,7 @@ async function* streamChatEventsLoop(options) {
365
375
  finished = true;
366
376
  wake();
367
377
  };
368
- const onOpen = emit ? () => emit({ type: "ws:open", chatId, attempt, timestamp: Date.now() }) : void 0;
378
+ const onOpen = emit ? () => emit({ type: "ws:open", chatId, reader, attempt, timestamp: Date.now() }) : void 0;
369
379
  if (onOpen) ws.addEventListener("open", onOpen);
370
380
  ws.addEventListener("message", onMessage);
371
381
  ws.addEventListener("error", onError);
@@ -426,7 +436,7 @@ async function* streamChatEventsLoop(options) {
426
436
  }
427
437
  if (emit && !closeEmitted) {
428
438
  closeEmitted = true;
429
- emit({ type: "ws:close", chatId, attempt, timestamp: Date.now() });
439
+ emit({ type: "ws:close", chatId, reader, attempt, timestamp: Date.now() });
430
440
  }
431
441
  }
432
442
  if (signal?.aborted) return;
@@ -451,6 +461,7 @@ async function* streamChatEventsLoop(options) {
451
461
  emit?.({
452
462
  type: "ws:redial",
453
463
  chatId,
464
+ reader,
454
465
  attempt,
455
466
  consecutiveFailures: failures,
456
467
  maxConsecutiveFailures: STREAM_MAX_CONSECUTIVE_FAILURES,
@@ -867,6 +878,7 @@ var CoderChatClient = class {
867
878
  chatId,
868
879
  afterId: opts?.afterId,
869
880
  signal: opts?.signal,
881
+ reader: opts?.reader,
870
882
  webSocketFactory: this.#webSocketFactory,
871
883
  onTransportEvent: this.#onTransportEvent
872
884
  });
@@ -1145,6 +1157,12 @@ function dynamicToolNames(tools) {
1145
1157
  // src/model/session-stream.ts
1146
1158
  var SessionChatStream = class {
1147
1159
  chatId;
1160
+ /**
1161
+ * The reader id stamped on the wrapped reader's `ws:*` transport events —
1162
+ * the model stamps it on `segment:settle` so segments correlate with the
1163
+ * connection that served them (issue #94).
1164
+ */
1165
+ reader;
1148
1166
  #events;
1149
1167
  #controller = new AbortController();
1150
1168
  #pending;
@@ -1153,6 +1171,7 @@ var SessionChatStream = class {
1153
1171
  #closePromise;
1154
1172
  constructor(opts) {
1155
1173
  this.chatId = opts.chatId;
1174
+ this.reader = opts.reader;
1156
1175
  this.#events = opts.open(this.#controller.signal);
1157
1176
  }
1158
1177
  get closed() {
@@ -1328,10 +1347,12 @@ var TurnTranslator = class {
1328
1347
  // false only for announce-style empty snapshots, whose in-flight deltas are
1329
1348
  // still claimable by a same-id commit.
1330
1349
  #emittedByMessageId = /* @__PURE__ */ new Map();
1331
- // Same-id revision snapshots that arrived while the NEXT step's deltas were
1332
- // open (latest wins). chatd sends each revision once on a healthy
1333
- // connection, so the suffix cannot wait for a replay — it is reconciled as
1334
- // soon as the next step's snapshot settles ownership of the pending deltas.
1350
+ // Same-id revision snapshots (their full wire content) that arrived while
1351
+ // the NEXT step's deltas were open (latest wins). chatd sends each revision
1352
+ // once on a healthy connection, so the suffix cannot wait for a replay — it
1353
+ // is reconciled as soon as the next step's snapshot settles ownership of
1354
+ // the pending deltas, or flushed by finish() when the segment ends without
1355
+ // one (see #78).
1335
1356
  #deferredRevisions = /* @__PURE__ */ new Map();
1336
1357
  #error;
1337
1358
  #terminalStatus;
@@ -1406,22 +1427,34 @@ var TurnTranslator = class {
1406
1427
  * next delta simply opens a fresh block (its pending content is tracked
1407
1428
  * independently of blocks).
1408
1429
  */
1409
- #reconcileRevision(out, rec, fullText, fullReasoning) {
1410
- const textSuffix = fullText.length > rec.text.length && fullText.startsWith(rec.text) ? fullText.slice(rec.text.length) : "";
1411
- const reasoningSuffix = fullReasoning.length > rec.reasoning.length && fullReasoning.startsWith(rec.reasoning) ? fullReasoning.slice(rec.reasoning.length) : "";
1412
- if (!textSuffix && !reasoningSuffix) return;
1430
+ #reconcileRevision(out, rec, content) {
1431
+ const fullText = joinContent(content, "text");
1432
+ const fullReasoning = joinContent(content, "reasoning");
1433
+ const textOk = fullText.length > rec.text.length && fullText.startsWith(rec.text);
1434
+ const reasoningOk = fullReasoning.length > rec.reasoning.length && fullReasoning.startsWith(rec.reasoning);
1435
+ if (!textOk && !reasoningOk) return;
1413
1436
  this.#closeText(out);
1414
1437
  this.#closeReasoning(out);
1415
- if (reasoningSuffix) {
1416
- this.#emitReasoningDelta(out, reasoningSuffix);
1417
- rec.reasoning = fullReasoning;
1418
- this.#closeReasoning(out);
1419
- }
1420
- if (textSuffix) {
1421
- this.#emitTextDelta(out, textSuffix);
1422
- rec.text = fullText;
1423
- this.#closeText(out);
1438
+ let seenText = 0;
1439
+ let seenReasoning = 0;
1440
+ for (const p of content) {
1441
+ if (p.type === "text") {
1442
+ const end = seenText + (p.text ?? "").length;
1443
+ const from = Math.max(seenText, rec.text.length);
1444
+ if (textOk && end > from) this.#emitTextDelta(out, fullText.slice(from, end));
1445
+ seenText = end;
1446
+ } else if (p.type === "reasoning") {
1447
+ const end = seenReasoning + (p.text ?? "").length;
1448
+ const from = Math.max(seenReasoning, rec.reasoning.length);
1449
+ if (reasoningOk && end > from)
1450
+ this.#emitReasoningDelta(out, fullReasoning.slice(from, end));
1451
+ seenReasoning = end;
1452
+ }
1424
1453
  }
1454
+ if (textOk) rec.text = fullText;
1455
+ if (reasoningOk) rec.reasoning = fullReasoning;
1456
+ this.#closeText(out);
1457
+ this.#closeReasoning(out);
1425
1458
  }
1426
1459
  // --- tool helpers ---------------------------------------------------------
1427
1460
  #isClientTool(name) {
@@ -1504,6 +1537,7 @@ var TurnTranslator = class {
1504
1537
  for (const tc of ev.action_required?.tool_calls ?? []) {
1505
1538
  if (this.#clientToolCalls.has(tc.tool_call_id)) continue;
1506
1539
  if (this.#submittedToolCallIds.has(tc.tool_call_id)) continue;
1540
+ this.#flushDeferredRevisions(out);
1507
1541
  this.#closeText(out);
1508
1542
  this.#closeReasoning(out);
1509
1543
  this.#clientToolCalls.add(tc.tool_call_id);
@@ -1521,6 +1555,7 @@ var TurnTranslator = class {
1521
1555
  case "error":
1522
1556
  if (ev.error) {
1523
1557
  this.#error = ev.error;
1558
+ this.#flushDeferredRevisions(out);
1524
1559
  out.push({ type: "error", error: new CoderChatError(ev.error) });
1525
1560
  }
1526
1561
  break;
@@ -1586,9 +1621,9 @@ var TurnTranslator = class {
1586
1621
  const fullReasoning = joinContent(content, "reasoning");
1587
1622
  const known = this.#emittedByMessageId.get(message.id);
1588
1623
  if (known && message.id !== this.#currentAssistantId) {
1589
- this.#reconcileRevision(out, known, fullText, fullReasoning);
1624
+ this.#reconcileRevision(out, known, content);
1590
1625
  } else if (known && this.#deltasSinceSnapshot && known.substantive) {
1591
- this.#deferredRevisions.set(message.id, { text: fullText, reasoning: fullReasoning });
1626
+ this.#deferredRevisions.set(message.id, content);
1592
1627
  } else {
1593
1628
  if (this.#currentAssistantId !== void 0 && this.#currentAssistantId !== message.id && !this.#deltasSinceSnapshot) {
1594
1629
  this.#closeText(out);
@@ -1628,8 +1663,7 @@ var TurnTranslator = class {
1628
1663
  for (const [id, deferred] of this.#deferredRevisions) {
1629
1664
  if (id === message.id) continue;
1630
1665
  const deferredRec = this.#emittedByMessageId.get(id);
1631
- if (deferredRec)
1632
- this.#reconcileRevision(out, deferredRec, deferred.text, deferred.reasoning);
1666
+ if (deferredRec) this.#reconcileRevision(out, deferredRec, deferred);
1633
1667
  this.#deferredRevisions.delete(id);
1634
1668
  }
1635
1669
  }
@@ -1649,8 +1683,27 @@ var TurnTranslator = class {
1649
1683
  }
1650
1684
  }
1651
1685
  // --- finish ---------------------------------------------------------------
1686
+ /**
1687
+ * Flushes revisions still deferred behind a delta-ownership race that never
1688
+ * settled — the racing step ended terminally without committing a snapshot,
1689
+ * so the drain in #ingestMessage never ran. chatd sent each revision once;
1690
+ * this is the segment's last chance to emit its suffix (#78). Attribution
1691
+ * care: the pending deltas' owner never committed, so they attribute to no
1692
+ * one — reconciliation is against the ledger alone, and only
1693
+ * ledger-extension suffixes may emit. Runs at finish() and, because
1694
+ * doGenerate() throws on a streamed error part and closes the generator
1695
+ * before finish() (issue #72), ahead of the error part in ingest().
1696
+ */
1697
+ #flushDeferredRevisions(out) {
1698
+ for (const [id, deferred] of this.#deferredRevisions) {
1699
+ const rec = this.#emittedByMessageId.get(id);
1700
+ if (rec) this.#reconcileRevision(out, rec, deferred);
1701
+ }
1702
+ this.#deferredRevisions.clear();
1703
+ }
1652
1704
  finish() {
1653
1705
  const out = [];
1706
+ this.#flushDeferredRevisions(out);
1654
1707
  this.#closeText(out);
1655
1708
  this.#closeReasoning(out);
1656
1709
  let unified;
@@ -1777,9 +1830,11 @@ var CoderLanguageModel = class {
1777
1830
  }
1778
1831
  this.#retainedStream = void 0;
1779
1832
  if (retained) void retained.close();
1833
+ const reader = nextStreamReaderId();
1780
1834
  const created = new SessionChatStream({
1781
1835
  chatId,
1782
- open: (streamSignal) => this.#config.client.streamEvents(chatId, { afterId, signal: streamSignal })
1836
+ reader,
1837
+ open: (streamSignal) => this.#config.client.streamEvents(chatId, { afterId, signal: streamSignal, reader })
1783
1838
  });
1784
1839
  this.#retainedStream = created;
1785
1840
  return created;
@@ -1943,6 +1998,7 @@ var CoderLanguageModel = class {
1943
1998
  };
1944
1999
  const chatId = segmentChat.chatId ?? this.#chatId;
1945
2000
  if (chatId !== void 0) settleEvent.chatId = chatId;
2001
+ if (segmentChat.reader !== void 0) settleEvent.reader = segmentChat.reader;
1946
2002
  if (!failed && finishReason === void 0) {
1947
2003
  if (segmentChat.timeoutError) {
1948
2004
  failure = segmentChat.timeoutError;
@@ -2106,6 +2162,7 @@ var CoderLanguageModel = class {
2106
2162
  let recoveryAttempted = false;
2107
2163
  const stream = this.#acquireStream(action.kind === "resume", chatId, afterId);
2108
2164
  streamAcquired = true;
2165
+ if (segmentChat) segmentChat.reader = stream.reader;
2109
2166
  let onSegmentAbort;
2110
2167
  const segmentAborted = signal ? new Promise((resolve) => {
2111
2168
  onSegmentAbort = () => resolve(SEGMENT_ABORTED);