@coder/ai-sdk-agent 0.9.2 → 0.9.4
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 +10 -1
- package/dist/index.d.ts +30 -3
- package/dist/index.js +55 -10
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1067,7 +1067,16 @@ What resuming does (and doesn't do):
|
|
|
1067
1067
|
`requestTimeoutMs` waiting — it attributes nothing of the live run's output
|
|
1068
1068
|
to itself (its window opens only when its own queued prompt materializes),
|
|
1069
1069
|
and if it times out first, it withdraws its queued prompt rather than
|
|
1070
|
-
leaving it to run unattended.
|
|
1070
|
+
leaving it to run unattended. The live run's turn is guarded symmetrically:
|
|
1071
|
+
the server starts the queued prompt's run **without ever emitting a
|
|
1072
|
+
terminal settle for the finished one**, so the SDK settles the finishing
|
|
1073
|
+
turn the moment the promoted prompt appears on its stream — nothing of the
|
|
1074
|
+
next turn's output or usage leaks into it. That settle reports
|
|
1075
|
+
`finishReason: "stop"` as a documented approximation: the wire doesn't
|
|
1076
|
+
distinguish a run that completed into a promotion from one interrupted by
|
|
1077
|
+
`busy_behavior: "interrupt"`, so an interrupted turn also settles `"stop"`
|
|
1078
|
+
(with whatever partial output it committed) rather than surfacing an
|
|
1079
|
+
interruption.
|
|
1071
1080
|
|
|
1072
1081
|
### Let the SDK absorb drops — and handle what it re‑throws
|
|
1073
1082
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1404,6 +1404,17 @@ interface TurnTranslatorOptions {
|
|
|
1404
1404
|
* the queue entry materializes as this turn's own user message, every event
|
|
1405
1405
|
* belongs to the chat's concurrent run and is withheld (see
|
|
1406
1406
|
* {@link QueuedSubmissionGate} and {@link TurnTranslatorOptions.queuedSubmission}).
|
|
1407
|
+
*
|
|
1408
|
+
* The mirror direction is the PROMOTION SETTLE BOUNDARY: a user-role
|
|
1409
|
+
* `message` snapshot past the turn's effective cursor settles this turn
|
|
1410
|
+
* mid-run and freezes the translator, because chatd starts the chat's next
|
|
1411
|
+
* run without ever emitting a terminal status for the finished one (see
|
|
1412
|
+
* {@link settledAtPromotionBoundary}, issue #120). Both behaviors key off
|
|
1413
|
+
* the SAME promoted user snapshot — it is the submitting turn's anchor
|
|
1414
|
+
* (turn start) and the finishing turn's boundary (turn end) — and stay
|
|
1415
|
+
* independent because each keys strictly off its OWN turn's cursor: the
|
|
1416
|
+
* anchor advances the submitter's cursor TO the snapshot's id, while the
|
|
1417
|
+
* boundary requires an id strictly PAST the finisher's cursor.
|
|
1407
1418
|
*/
|
|
1408
1419
|
declare class TurnTranslator {
|
|
1409
1420
|
#private;
|
|
@@ -1415,9 +1426,10 @@ declare class TurnTranslator {
|
|
|
1415
1426
|
get error(): ChatErrorPayload | undefined;
|
|
1416
1427
|
/**
|
|
1417
1428
|
* The turn's EFFECTIVE attribution cursor: the constructor's `turnCursor`
|
|
1418
|
-
* until a queued submission anchors
|
|
1419
|
-
* after
|
|
1420
|
-
* the
|
|
1429
|
+
* until a queued submission anchors (the materialized user message's id
|
|
1430
|
+
* after) or a zero-cursor turn adopts its own prompt's id (see
|
|
1431
|
+
* #ingestMessage). Distinct from the stream's redial cursor, which
|
|
1432
|
+
* deliberately stays the turn's original `after_id` (see `streamChatEvents`).
|
|
1421
1433
|
*/
|
|
1422
1434
|
get turnCursor(): number;
|
|
1423
1435
|
/**
|
|
@@ -1433,6 +1445,21 @@ declare class TurnTranslator {
|
|
|
1433
1445
|
* fails fast instead of hanging until timeout/abort.
|
|
1434
1446
|
*/
|
|
1435
1447
|
get queuedSubmissionDeadEnd(): ChatErrorPayload | undefined;
|
|
1448
|
+
/**
|
|
1449
|
+
* Whether the turn settled at a PROMOTION BOUNDARY (issue #120): a
|
|
1450
|
+
* user-role `message` snapshot with an id past the turn's effective cursor
|
|
1451
|
+
* arrived mid-run. chatd commits a mid-run user message only when it starts
|
|
1452
|
+
* the chat's NEXT run — `FinishTurn`/`FinishInterruption` promote the queue
|
|
1453
|
+
* head atomically with the previous run's finish (the chat transitions
|
|
1454
|
+
* `running → running` and NO terminal status is ever emitted for the
|
|
1455
|
+
* finished run), and interrupting a `requires_action` pause queues and then
|
|
1456
|
+
* promotes the same way — so the snapshot is proof this turn's own run is
|
|
1457
|
+
* over even though no terminal status was (or ever will be) sent for it.
|
|
1458
|
+
* The segment loop settles the turn here instead of absorbing the promoted
|
|
1459
|
+
* run's content, usage, and settle events; the successor run stays alive
|
|
1460
|
+
* and belongs to whoever submitted it, so teardown must NOT interrupt it.
|
|
1461
|
+
*/
|
|
1462
|
+
get settledAtPromotionBoundary(): boolean;
|
|
1436
1463
|
ingest(ev: ChatStreamEvent): LanguageModelV4StreamPart[];
|
|
1437
1464
|
finish(): LanguageModelV4StreamPart[];
|
|
1438
1465
|
}
|
package/dist/index.js
CHANGED
|
@@ -1426,15 +1426,27 @@ var TurnTranslator = class {
|
|
|
1426
1426
|
* initial sync when resuming a chat, or a mid-turn `history_reset` re-send
|
|
1427
1427
|
* of the full history. Ingest skips them entirely so a replay can neither
|
|
1428
1428
|
* re-emit earlier turns' content nor inflate this turn's usage. Mutable for
|
|
1429
|
-
* exactly
|
|
1429
|
+
* exactly two transitions, both of which pin the turn's OWN user message as
|
|
1430
|
+
* the effective floor: a QUEUED submission starts at the pre-turn cursor
|
|
1430
1431
|
* and advances to the materialized user message's id when the gate anchors
|
|
1431
|
-
* (see {@link QueuedSubmissionGate})
|
|
1432
|
-
*
|
|
1433
|
-
*
|
|
1432
|
+
* (see {@link QueuedSubmissionGate}), and a ZERO cursor (this turn created
|
|
1433
|
+
* the chat) adopts the replay's first user snapshot — this turn's own
|
|
1434
|
+
* prompt — as the cursor (see #ingestMessage). Either way, replayed
|
|
1435
|
+
* pre-cursor tail messages then stay filtered on every redial, which reuses
|
|
1436
|
+
* the turn's ORIGINAL `after_id` and therefore re-delivers them.
|
|
1434
1437
|
*/
|
|
1435
1438
|
#turnCursor;
|
|
1436
1439
|
/** The queued-submission attribution gate; undefined once anchored (#114). */
|
|
1437
1440
|
#queuedGate;
|
|
1441
|
+
/**
|
|
1442
|
+
* Set when the turn settled at a PROMOTION BOUNDARY (issue #120): a
|
|
1443
|
+
* user-role `message` snapshot with an id past the turn's effective cursor
|
|
1444
|
+
* arrived mid-run. Once set, the translator is FROZEN — every later event
|
|
1445
|
+
* is the successor run's traffic and ingests as a no-op — so nothing past
|
|
1446
|
+
* the boundary can leak into this turn's content, usage, or settle state.
|
|
1447
|
+
* See {@link settledAtPromotionBoundary} for why the snapshot is reliable.
|
|
1448
|
+
*/
|
|
1449
|
+
#promotionBoundary = false;
|
|
1438
1450
|
constructor(opts) {
|
|
1439
1451
|
this.#dynamicToolNames = opts.dynamicToolNames;
|
|
1440
1452
|
this.#turnCursor = opts.turnCursor ?? 0;
|
|
@@ -1465,9 +1477,10 @@ var TurnTranslator = class {
|
|
|
1465
1477
|
}
|
|
1466
1478
|
/**
|
|
1467
1479
|
* The turn's EFFECTIVE attribution cursor: the constructor's `turnCursor`
|
|
1468
|
-
* until a queued submission anchors
|
|
1469
|
-
* after
|
|
1470
|
-
* the
|
|
1480
|
+
* until a queued submission anchors (the materialized user message's id
|
|
1481
|
+
* after) or a zero-cursor turn adopts its own prompt's id (see
|
|
1482
|
+
* #ingestMessage). Distinct from the stream's redial cursor, which
|
|
1483
|
+
* deliberately stays the turn's original `after_id` (see `streamChatEvents`).
|
|
1471
1484
|
*/
|
|
1472
1485
|
get turnCursor() {
|
|
1473
1486
|
return this.#turnCursor;
|
|
@@ -1489,6 +1502,23 @@ var TurnTranslator = class {
|
|
|
1489
1502
|
get queuedSubmissionDeadEnd() {
|
|
1490
1503
|
return this.#queuedGate?.deadEnd;
|
|
1491
1504
|
}
|
|
1505
|
+
/**
|
|
1506
|
+
* Whether the turn settled at a PROMOTION BOUNDARY (issue #120): a
|
|
1507
|
+
* user-role `message` snapshot with an id past the turn's effective cursor
|
|
1508
|
+
* arrived mid-run. chatd commits a mid-run user message only when it starts
|
|
1509
|
+
* the chat's NEXT run — `FinishTurn`/`FinishInterruption` promote the queue
|
|
1510
|
+
* head atomically with the previous run's finish (the chat transitions
|
|
1511
|
+
* `running → running` and NO terminal status is ever emitted for the
|
|
1512
|
+
* finished run), and interrupting a `requires_action` pause queues and then
|
|
1513
|
+
* promotes the same way — so the snapshot is proof this turn's own run is
|
|
1514
|
+
* over even though no terminal status was (or ever will be) sent for it.
|
|
1515
|
+
* The segment loop settles the turn here instead of absorbing the promoted
|
|
1516
|
+
* run's content, usage, and settle events; the successor run stays alive
|
|
1517
|
+
* and belongs to whoever submitted it, so teardown must NOT interrupt it.
|
|
1518
|
+
*/
|
|
1519
|
+
get settledAtPromotionBoundary() {
|
|
1520
|
+
return this.#promotionBoundary;
|
|
1521
|
+
}
|
|
1492
1522
|
// --- block helpers --------------------------------------------------------
|
|
1493
1523
|
#openText(out) {
|
|
1494
1524
|
if (this.#reasoning.id) this.#closeReasoning(out);
|
|
@@ -1631,6 +1661,7 @@ var TurnTranslator = class {
|
|
|
1631
1661
|
}
|
|
1632
1662
|
// --- ingest ---------------------------------------------------------------
|
|
1633
1663
|
ingest(ev) {
|
|
1664
|
+
if (this.#promotionBoundary) return [];
|
|
1634
1665
|
const gate = this.#queuedGate;
|
|
1635
1666
|
if (gate) return this.#ingestGated(gate, ev);
|
|
1636
1667
|
const out = [];
|
|
@@ -1794,8 +1825,18 @@ var TurnTranslator = class {
|
|
|
1794
1825
|
}
|
|
1795
1826
|
}
|
|
1796
1827
|
#ingestMessage(out, message) {
|
|
1828
|
+
if (this.#promotionBoundary) return;
|
|
1797
1829
|
if (message.id > this.#maxMessageId) this.#maxMessageId = message.id;
|
|
1798
1830
|
if (message.id <= this.#turnCursor) return;
|
|
1831
|
+
if (message.role === "user") {
|
|
1832
|
+
if (this.#turnCursor === 0) {
|
|
1833
|
+
this.#turnCursor = message.id;
|
|
1834
|
+
return;
|
|
1835
|
+
}
|
|
1836
|
+
this.#promotionBoundary = true;
|
|
1837
|
+
this.#terminalStatus = void 0;
|
|
1838
|
+
return;
|
|
1839
|
+
}
|
|
1799
1840
|
if (message.usage && message.role === "assistant") {
|
|
1800
1841
|
this.#usageByMessageId.set(message.id, message.usage);
|
|
1801
1842
|
}
|
|
@@ -1892,7 +1933,9 @@ var TurnTranslator = class {
|
|
|
1892
1933
|
this.#closeReasoning(out);
|
|
1893
1934
|
let unified;
|
|
1894
1935
|
if (this.#error || this.#terminalStatus === "error") unified = "error";
|
|
1895
|
-
else if (this.#
|
|
1936
|
+
else if (this.#promotionBoundary) {
|
|
1937
|
+
unified = "stop";
|
|
1938
|
+
} else if (this.#clientToolCallSeen || this.#terminalStatus === "requires_action")
|
|
1896
1939
|
unified = "tool-calls";
|
|
1897
1940
|
else unified = "stop";
|
|
1898
1941
|
const usage = accumulateUsage(this.#usageByMessageId);
|
|
@@ -2315,6 +2358,7 @@ var CoderLanguageModel = class {
|
|
|
2315
2358
|
const interrupt = () => {
|
|
2316
2359
|
const id = turnChatId ?? this.#chatId;
|
|
2317
2360
|
if (interruptSent || !id) return;
|
|
2361
|
+
if (translator?.settledAtPromotionBoundary) return;
|
|
2318
2362
|
interruptSent = true;
|
|
2319
2363
|
if (queuedTurn && (translator === void 0 || translator.awaitingQueuedSubmission)) {
|
|
2320
2364
|
const entryId = queuedEntry?.id;
|
|
@@ -2532,6 +2576,7 @@ var CoderLanguageModel = class {
|
|
|
2532
2576
|
}
|
|
2533
2577
|
}
|
|
2534
2578
|
for (const part of parts) yield part;
|
|
2579
|
+
if (translator.settledAtPromotionBoundary) break;
|
|
2535
2580
|
const status = translator.terminalStatus;
|
|
2536
2581
|
if (status) {
|
|
2537
2582
|
if (status !== "requires_action") break;
|
|
@@ -2579,7 +2624,7 @@ var CoderLanguageModel = class {
|
|
|
2579
2624
|
}
|
|
2580
2625
|
}
|
|
2581
2626
|
throwIfAborted();
|
|
2582
|
-
if (!translator.terminalStatus && !translator.error) {
|
|
2627
|
+
if (!translator.terminalStatus && !translator.error && !translator.settledAtPromotionBoundary) {
|
|
2583
2628
|
throw new CoderChatError({
|
|
2584
2629
|
message: "Coder chat stream ended before the turn settled (connection closed or the server ended the stream without a terminal status).",
|
|
2585
2630
|
kind: "stream_closed",
|
|
@@ -2600,7 +2645,7 @@ var CoderLanguageModel = class {
|
|
|
2600
2645
|
if (timeoutSignal?.aborted) segmentChat.timeoutError = timeoutError();
|
|
2601
2646
|
}
|
|
2602
2647
|
signal?.removeEventListener("abort", interrupt);
|
|
2603
|
-
if (!translator?.terminalStatus) interrupt();
|
|
2648
|
+
if (!translator?.terminalStatus && !translator?.settledAtPromotionBoundary) interrupt();
|
|
2604
2649
|
if (!streamAcquired && this.#retainedStream) {
|
|
2605
2650
|
const stale = this.#retainedStream;
|
|
2606
2651
|
this.#retainedStream = void 0;
|