@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/README.md +377 -23
- package/dist/index.d.ts +36 -1
- package/dist/index.js +87 -30
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
|
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
|
|
1332
|
-
// open (latest wins). chatd sends each revision
|
|
1333
|
-
// connection, so the suffix cannot wait for a replay — it
|
|
1334
|
-
// soon as the next step's snapshot settles ownership of
|
|
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,
|
|
1410
|
-
const
|
|
1411
|
-
const
|
|
1412
|
-
|
|
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
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
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,
|
|
1624
|
+
this.#reconcileRevision(out, known, content);
|
|
1590
1625
|
} else if (known && this.#deltasSinceSnapshot && known.substantive) {
|
|
1591
|
-
this.#deferredRevisions.set(message.id,
|
|
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
|
-
|
|
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);
|