@coder/ai-sdk-agent 0.4.3 → 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/dist/index.d.ts +43 -1
- package/dist/index.js +227 -24
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -873,6 +873,15 @@ interface CoderLanguageModelConfig {
|
|
|
873
873
|
*
|
|
874
874
|
* NOTE: a single model instance is single-flight — do not run concurrent
|
|
875
875
|
* generations against the same instance/session.
|
|
876
|
+
*
|
|
877
|
+
* The session retains ONE event stream per turn (issue #44): a segment that
|
|
878
|
+
* pauses for client tools leaves the WebSocket open, and the tool-result
|
|
879
|
+
* resume segment keeps reading it — no re-dial per tool round trip. The
|
|
880
|
+
* socket closes when its segment ends any other way (terminal settle, error,
|
|
881
|
+
* abort, timeout), when the next turn replaces it, or on session teardown
|
|
882
|
+
* (`resetSession()` / `[Symbol.asyncDispose]()`). A turn that pauses for
|
|
883
|
+
* client tools but never resumes leaves the socket open until teardown, so
|
|
884
|
+
* dispose or reset sessions you abandon mid-turn.
|
|
876
885
|
*/
|
|
877
886
|
declare class CoderLanguageModel implements LanguageModelV4 {
|
|
878
887
|
#private;
|
|
@@ -882,8 +891,32 @@ declare class CoderLanguageModel implements LanguageModelV4 {
|
|
|
882
891
|
readonly supportedUrls: Record<string, RegExp[]>;
|
|
883
892
|
constructor(config: CoderLanguageModelConfig);
|
|
884
893
|
get chatId(): string | undefined;
|
|
885
|
-
/**
|
|
894
|
+
/**
|
|
895
|
+
* Drops the current session so the next turn creates a fresh chat. Also
|
|
896
|
+
* closes the retained event stream, if any (fire-and-forget — a sync reset
|
|
897
|
+
* must not wait on socket teardown, and `close()` never rejects).
|
|
898
|
+
*/
|
|
886
899
|
resetSession(): void;
|
|
900
|
+
/**
|
|
901
|
+
* Closes the retained event stream, if any. The stream otherwise closes
|
|
902
|
+
* when its turn settles or fails, or when the next turn replaces it — but a
|
|
903
|
+
* turn that pauses for client tools and never resumes (e.g. the caller
|
|
904
|
+
* stopped the tool loop, or aborted between segments) leaves the socket
|
|
905
|
+
* open, so dispose (`await using`) or {@link resetSession} sessions you
|
|
906
|
+
* abandon mid-turn.
|
|
907
|
+
*/
|
|
908
|
+
[Symbol.asyncDispose](): Promise<void>;
|
|
909
|
+
/**
|
|
910
|
+
* Closes a stream retained by a client-tool pause. Called on explicit
|
|
911
|
+
* interrupts ({@link CoderAgent.interrupt}): an interrupted pause is dead —
|
|
912
|
+
* its buffer would otherwise collect the interrupt's own settle events,
|
|
913
|
+
* which a later tool-result resume would consume as if they were the
|
|
914
|
+
* resumed generation's (truncating the turn), and if the tool loop never
|
|
915
|
+
* resumes the socket would outlive the explicit interrupt. A stream a
|
|
916
|
+
* segment is actively reading is left alone: that segment observes the
|
|
917
|
+
* settle the interrupt produces and closes the stream itself.
|
|
918
|
+
*/
|
|
919
|
+
closePausedStream(): Promise<void>;
|
|
887
920
|
doStream(options: LanguageModelV4CallOptions): Promise<LanguageModelV4StreamResult>;
|
|
888
921
|
doGenerate(options: LanguageModelV4CallOptions): Promise<LanguageModelV4GenerateResult>;
|
|
889
922
|
}
|
|
@@ -911,6 +944,15 @@ declare class TurnTranslator {
|
|
|
911
944
|
constructor(opts: {
|
|
912
945
|
dynamicToolNames: ReadonlySet<string>;
|
|
913
946
|
turnCursor?: number;
|
|
947
|
+
/**
|
|
948
|
+
* Tool-call ids whose results the session already submitted. With the
|
|
949
|
+
* stream retained across segments (and redialed with the turn's original
|
|
950
|
+
* cursor), a reconnect can replay a previous segment's `action_required`
|
|
951
|
+
* snapshot; re-emitting an already-answered call would make the AI SDK
|
|
952
|
+
* execute the tool a second time. A live reference — the set grows as
|
|
953
|
+
* later segments submit results.
|
|
954
|
+
*/
|
|
955
|
+
submittedToolCallIds?: ReadonlySet<string>;
|
|
914
956
|
});
|
|
915
957
|
get terminalStatus(): ChatStatus | undefined;
|
|
916
958
|
/** Whether a client (custom) tool call has been emitted this turn. */
|
package/dist/index.js
CHANGED
|
@@ -1016,6 +1016,95 @@ function dynamicToolNames(tools) {
|
|
|
1016
1016
|
return names;
|
|
1017
1017
|
}
|
|
1018
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
|
+
|
|
1019
1108
|
// src/model/translate.ts
|
|
1020
1109
|
function addDefined(a, b) {
|
|
1021
1110
|
if (b === void 0) return a;
|
|
@@ -1075,6 +1164,7 @@ function jsonResult(value) {
|
|
|
1075
1164
|
}
|
|
1076
1165
|
var TurnTranslator = class {
|
|
1077
1166
|
#dynamicToolNames;
|
|
1167
|
+
#submittedToolCallIds;
|
|
1078
1168
|
#seq = 0;
|
|
1079
1169
|
#text = { id: void 0, len: 0, sawDelta: false };
|
|
1080
1170
|
#reasoning = { id: void 0, len: 0, sawDelta: false };
|
|
@@ -1111,6 +1201,7 @@ var TurnTranslator = class {
|
|
|
1111
1201
|
constructor(opts) {
|
|
1112
1202
|
this.#dynamicToolNames = opts.dynamicToolNames;
|
|
1113
1203
|
this.#turnCursor = opts.turnCursor ?? 0;
|
|
1204
|
+
this.#submittedToolCallIds = opts.submittedToolCallIds ?? /* @__PURE__ */ new Set();
|
|
1114
1205
|
}
|
|
1115
1206
|
get terminalStatus() {
|
|
1116
1207
|
return this.#terminalStatus;
|
|
@@ -1260,6 +1351,7 @@ var TurnTranslator = class {
|
|
|
1260
1351
|
case "action_required":
|
|
1261
1352
|
for (const tc of ev.action_required?.tool_calls ?? []) {
|
|
1262
1353
|
if (this.#clientToolCalls.has(tc.tool_call_id)) continue;
|
|
1354
|
+
if (this.#submittedToolCallIds.has(tc.tool_call_id)) continue;
|
|
1263
1355
|
this.#closeText(out);
|
|
1264
1356
|
this.#closeReasoning(out);
|
|
1265
1357
|
this.#clientToolCalls.add(tc.tool_call_id);
|
|
@@ -1281,8 +1373,12 @@ var TurnTranslator = class {
|
|
|
1281
1373
|
}
|
|
1282
1374
|
break;
|
|
1283
1375
|
case "status":
|
|
1284
|
-
if (ev.status
|
|
1285
|
-
|
|
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
|
+
}
|
|
1286
1382
|
}
|
|
1287
1383
|
break;
|
|
1288
1384
|
default:
|
|
@@ -1416,6 +1512,7 @@ var TurnTranslator = class {
|
|
|
1416
1512
|
// src/model/language-model.ts
|
|
1417
1513
|
var ACTION_REQUIRED_GRACE_MS = 2e3;
|
|
1418
1514
|
var GRACE_EXPIRED = /* @__PURE__ */ Symbol("action-required-grace-expired");
|
|
1515
|
+
var SEGMENT_ABORTED = /* @__PURE__ */ Symbol("segment-aborted");
|
|
1419
1516
|
function startGraceTimer(ms) {
|
|
1420
1517
|
let timer;
|
|
1421
1518
|
const expired = new Promise((resolve) => {
|
|
@@ -1444,6 +1541,8 @@ var CoderLanguageModel = class {
|
|
|
1444
1541
|
#modelResolved = false;
|
|
1445
1542
|
#submittedToolCallIds = /* @__PURE__ */ new Set();
|
|
1446
1543
|
#inFlight = false;
|
|
1544
|
+
/** The turn-lifetime event stream, retained across client-tool pauses. */
|
|
1545
|
+
#retainedStream;
|
|
1447
1546
|
constructor(config) {
|
|
1448
1547
|
assertSupportedAiVersion();
|
|
1449
1548
|
this.#config = config;
|
|
@@ -1453,11 +1552,75 @@ var CoderLanguageModel = class {
|
|
|
1453
1552
|
get chatId() {
|
|
1454
1553
|
return this.#chatId;
|
|
1455
1554
|
}
|
|
1456
|
-
/**
|
|
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
|
+
*/
|
|
1457
1560
|
resetSession() {
|
|
1458
1561
|
this.#chatId = void 0;
|
|
1459
1562
|
this.#lastSeenMessageId = 0;
|
|
1460
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();
|
|
1461
1624
|
}
|
|
1462
1625
|
async #resolveModelConfigId(signal) {
|
|
1463
1626
|
if (this.#modelResolved) return this.#resolvedModelConfigId;
|
|
@@ -1559,10 +1722,12 @@ var CoderLanguageModel = class {
|
|
|
1559
1722
|
);
|
|
1560
1723
|
const signal = sources.length > 0 ? AbortSignal.any(sources) : void 0;
|
|
1561
1724
|
let translator;
|
|
1725
|
+
let streamAcquired = false;
|
|
1562
1726
|
let discardSession = false;
|
|
1563
1727
|
let interruptSent = false;
|
|
1728
|
+
let turnChatId;
|
|
1564
1729
|
const interrupt = () => {
|
|
1565
|
-
const id = this.#chatId;
|
|
1730
|
+
const id = turnChatId ?? this.#chatId;
|
|
1566
1731
|
if (interruptSent || !id) return;
|
|
1567
1732
|
interruptSent = true;
|
|
1568
1733
|
void this.#config.client.interruptChat(id).catch(() => {
|
|
@@ -1660,25 +1825,48 @@ var CoderLanguageModel = class {
|
|
|
1660
1825
|
afterId = this.#lastSeenMessageId;
|
|
1661
1826
|
}
|
|
1662
1827
|
const chatId = this.#chatId;
|
|
1828
|
+
turnChatId = chatId;
|
|
1663
1829
|
const dynamicNames = dynamicToolNames(options.tools);
|
|
1664
1830
|
translator = new TurnTranslator({
|
|
1665
1831
|
dynamicToolNames: dynamicNames,
|
|
1666
|
-
turnCursor: afterId ?? 0
|
|
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
|
|
1667
1837
|
});
|
|
1668
1838
|
let sinceRequiresAction = 0;
|
|
1669
1839
|
let grace;
|
|
1670
1840
|
let recovery;
|
|
1671
1841
|
let recoveryAbort;
|
|
1672
1842
|
let recoveryAttempted = false;
|
|
1673
|
-
const stream = this.#
|
|
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;
|
|
1674
1853
|
try {
|
|
1675
|
-
let next = stream.next();
|
|
1676
1854
|
for (; ; ) {
|
|
1677
|
-
|
|
1855
|
+
const next = stream.read();
|
|
1678
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;
|
|
1679
1867
|
if (awaitingActionRequired && !recoveryAttempted) {
|
|
1680
1868
|
grace ??= startGraceTimer(ACTION_REQUIRED_GRACE_MS);
|
|
1681
|
-
|
|
1869
|
+
raced = await raceSegment(Promise.race([next, grace.expired]));
|
|
1682
1870
|
if (raced === GRACE_EXPIRED) {
|
|
1683
1871
|
recoveryAttempted = true;
|
|
1684
1872
|
throwIfAborted();
|
|
@@ -1691,27 +1879,25 @@ var CoderLanguageModel = class {
|
|
|
1691
1879
|
).catch(() => []);
|
|
1692
1880
|
continue;
|
|
1693
1881
|
}
|
|
1694
|
-
result = raced;
|
|
1695
1882
|
} else if (awaitingActionRequired && recovery) {
|
|
1696
|
-
|
|
1883
|
+
raced = await raceSegment(Promise.race([next, recovery]));
|
|
1697
1884
|
if (Array.isArray(raced)) {
|
|
1698
1885
|
recovery = void 0;
|
|
1699
1886
|
throwIfAborted();
|
|
1700
1887
|
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
|
-
}
|
|
1888
|
+
if (translator.clientToolCallSeen) break;
|
|
1706
1889
|
continue;
|
|
1707
1890
|
}
|
|
1708
|
-
result = raced;
|
|
1709
1891
|
} else {
|
|
1710
|
-
|
|
1892
|
+
raced = await raceSegment(next);
|
|
1893
|
+
}
|
|
1894
|
+
if (raced === SEGMENT_ABORTED) {
|
|
1895
|
+
throwIfAborted();
|
|
1896
|
+
throw new CoderAgentError("segment abort sentinel resolved without an abort");
|
|
1711
1897
|
}
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
for (const part of translator.ingest(
|
|
1898
|
+
stream.consumed(next);
|
|
1899
|
+
if (raced.done) break;
|
|
1900
|
+
for (const part of translator.ingest(raced.value)) yield part;
|
|
1715
1901
|
const status = translator.terminalStatus;
|
|
1716
1902
|
if (status) {
|
|
1717
1903
|
if (status !== "requires_action") break;
|
|
@@ -1719,6 +1905,7 @@ var CoderLanguageModel = class {
|
|
|
1719
1905
|
if (++sinceRequiresAction > 200) break;
|
|
1720
1906
|
}
|
|
1721
1907
|
}
|
|
1908
|
+
retainStream = !signal?.aborted && translator.terminalStatus === "requires_action" && translator.clientToolCallSeen && !translator.error;
|
|
1722
1909
|
} catch (err) {
|
|
1723
1910
|
throwIfAborted();
|
|
1724
1911
|
if (err instanceof CoderStreamError) {
|
|
@@ -1745,8 +1932,12 @@ var CoderLanguageModel = class {
|
|
|
1745
1932
|
} finally {
|
|
1746
1933
|
grace?.cancel();
|
|
1747
1934
|
recoveryAbort?.abort();
|
|
1748
|
-
|
|
1749
|
-
|
|
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
|
+
}
|
|
1750
1941
|
}
|
|
1751
1942
|
throwIfAborted();
|
|
1752
1943
|
if (!translator.terminalStatus && !translator.error) {
|
|
@@ -1765,6 +1956,11 @@ var CoderLanguageModel = class {
|
|
|
1765
1956
|
this.#lastSeenMessageId = translator.maxMessageId;
|
|
1766
1957
|
signal?.removeEventListener("abort", interrupt);
|
|
1767
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
|
+
}
|
|
1768
1964
|
if (discardSession) this.resetSession();
|
|
1769
1965
|
this.#inFlight = false;
|
|
1770
1966
|
}
|
|
@@ -2014,7 +2210,9 @@ var CoderAgent = class {
|
|
|
2014
2210
|
*/
|
|
2015
2211
|
async interrupt(opts) {
|
|
2016
2212
|
const id = this.#model.chatId;
|
|
2017
|
-
if (id)
|
|
2213
|
+
if (!id) return;
|
|
2214
|
+
await this.#model.closePausedStream();
|
|
2215
|
+
await this.#client.interruptChat(id, opts?.signal);
|
|
2018
2216
|
}
|
|
2019
2217
|
/**
|
|
2020
2218
|
* Archive the underlying chat (safe cleanup; hides it from listings).
|
|
@@ -2028,6 +2226,7 @@ var CoderAgent = class {
|
|
|
2028
2226
|
async archive(opts) {
|
|
2029
2227
|
const id = this.#model.chatId;
|
|
2030
2228
|
if (!id) return;
|
|
2229
|
+
await this.#model.closePausedStream();
|
|
2031
2230
|
await archiveWhenSettled(this.#client, id, {
|
|
2032
2231
|
deadlineMs: this.#settleDeadlineMs,
|
|
2033
2232
|
retryDelayMs: this.#settleRetryDelayMs,
|
|
@@ -2051,6 +2250,10 @@ var CoderAgent = class {
|
|
|
2051
2250
|
* ```
|
|
2052
2251
|
*/
|
|
2053
2252
|
async [Symbol.asyncDispose]() {
|
|
2253
|
+
try {
|
|
2254
|
+
await this.#model[Symbol.asyncDispose]();
|
|
2255
|
+
} catch {
|
|
2256
|
+
}
|
|
2054
2257
|
const deadline = AbortSignal.timeout(this.#settleDeadlineMs);
|
|
2055
2258
|
try {
|
|
2056
2259
|
await this.interrupt({ signal: deadline });
|