@coder/ai-sdk-agent 0.4.3 → 0.6.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 +74 -0
- package/dist/index.d.ts +247 -3
- package/dist/index.js +452 -33
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -163,6 +163,20 @@ var TERMINAL_STATUSES = /* @__PURE__ */ new Set([
|
|
|
163
163
|
|
|
164
164
|
// src/coder/ws.ts
|
|
165
165
|
import NodeWebSocket from "ws";
|
|
166
|
+
|
|
167
|
+
// src/transport-events.ts
|
|
168
|
+
function safeTransportEmitter(handler) {
|
|
169
|
+
if (!handler) return void 0;
|
|
170
|
+
return (event) => {
|
|
171
|
+
try {
|
|
172
|
+
void Promise.resolve(handler(event)).catch(() => {
|
|
173
|
+
});
|
|
174
|
+
} catch {
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// src/coder/ws.ts
|
|
166
180
|
var defaultFactory = (url, { headers }) => {
|
|
167
181
|
return new NodeWebSocket(url, { headers });
|
|
168
182
|
};
|
|
@@ -228,6 +242,8 @@ function streamChatEvents(options) {
|
|
|
228
242
|
async function* streamChatEventsLoop(options) {
|
|
229
243
|
const { baseUrl, token, chatId, signal } = options;
|
|
230
244
|
const factory = options.webSocketFactory ?? defaultFactory;
|
|
245
|
+
const emit = safeTransportEmitter(options.onTransportEvent);
|
|
246
|
+
let attempt = 0;
|
|
231
247
|
const wsBase = httpToWs(baseUrl);
|
|
232
248
|
const path = `/api/experimental/chats/${chatId}/stream`;
|
|
233
249
|
const query = options.afterId !== void 0 ? `?after_id=${options.afterId}` : "";
|
|
@@ -246,11 +262,24 @@ async function* streamChatEventsLoop(options) {
|
|
|
246
262
|
let finished = false;
|
|
247
263
|
let failure;
|
|
248
264
|
let parseFailure = false;
|
|
265
|
+
let closeEmitted = false;
|
|
249
266
|
const wake = () => {
|
|
250
267
|
resolveNext?.();
|
|
251
268
|
resolveNext = void 0;
|
|
252
269
|
};
|
|
253
|
-
|
|
270
|
+
attempt += 1;
|
|
271
|
+
emit?.({ type: "ws:dial", chatId, attempt, url, timestamp: Date.now() });
|
|
272
|
+
let ws;
|
|
273
|
+
try {
|
|
274
|
+
ws = factory(url, { headers: { "Coder-Session-Token": token } });
|
|
275
|
+
} catch (err) {
|
|
276
|
+
if (emit) {
|
|
277
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
278
|
+
emit({ type: "ws:error", chatId, attempt, message, timestamp: Date.now() });
|
|
279
|
+
emit({ type: "ws:close", chatId, attempt, timestamp: Date.now() });
|
|
280
|
+
}
|
|
281
|
+
throw err;
|
|
282
|
+
}
|
|
254
283
|
const onAbort = () => {
|
|
255
284
|
finished = true;
|
|
256
285
|
try {
|
|
@@ -274,6 +303,13 @@ async function* streamChatEventsLoop(options) {
|
|
|
274
303
|
const data = typeof ev.data === "string" ? ev.data : String(ev.data);
|
|
275
304
|
batch = JSON.parse(data);
|
|
276
305
|
} catch (err) {
|
|
306
|
+
emit?.({
|
|
307
|
+
type: "ws:error",
|
|
308
|
+
chatId,
|
|
309
|
+
attempt,
|
|
310
|
+
message: "failed to parse chat stream frame",
|
|
311
|
+
timestamp: Date.now()
|
|
312
|
+
});
|
|
277
313
|
failure = new CoderAgentError("failed to parse chat stream frame", { cause: err });
|
|
278
314
|
parseFailure = true;
|
|
279
315
|
finished = true;
|
|
@@ -281,24 +317,55 @@ async function* streamChatEventsLoop(options) {
|
|
|
281
317
|
return;
|
|
282
318
|
}
|
|
283
319
|
if (Array.isArray(batch)) {
|
|
284
|
-
for (const e of batch)
|
|
320
|
+
for (const e of batch) {
|
|
321
|
+
queue.push(e);
|
|
322
|
+
emit?.({
|
|
323
|
+
type: "ws:event",
|
|
324
|
+
chatId,
|
|
325
|
+
attempt,
|
|
326
|
+
event: e,
|
|
327
|
+
timestamp: Date.now()
|
|
328
|
+
});
|
|
329
|
+
}
|
|
285
330
|
} else if (batch && typeof batch === "object") {
|
|
286
331
|
queue.push(batch);
|
|
332
|
+
emit?.({
|
|
333
|
+
type: "ws:event",
|
|
334
|
+
chatId,
|
|
335
|
+
attempt,
|
|
336
|
+
event: batch,
|
|
337
|
+
timestamp: Date.now()
|
|
338
|
+
});
|
|
287
339
|
}
|
|
288
340
|
wake();
|
|
289
341
|
};
|
|
290
342
|
const onError = (ev) => {
|
|
291
343
|
if (finished) return;
|
|
292
344
|
const message = ev && typeof ev === "object" && "message" in ev ? String(ev.message) : "chat stream socket error";
|
|
345
|
+
emit?.({ type: "ws:error", chatId, attempt, message, timestamp: Date.now() });
|
|
293
346
|
const status = upgradeStatus(message);
|
|
294
347
|
failure = status !== void 0 && status >= 400 && status < 500 && !TRANSIENT_UPGRADE_STATUSES.has(status) ? new CoderApiError({ status, method: "GET", path, message }) : new CoderAgentError(message);
|
|
295
348
|
finished = true;
|
|
296
349
|
wake();
|
|
297
350
|
};
|
|
298
|
-
const onClose = () => {
|
|
351
|
+
const onClose = (ev) => {
|
|
352
|
+
if (emit && !closeEmitted) {
|
|
353
|
+
closeEmitted = true;
|
|
354
|
+
const closeEvent = {
|
|
355
|
+
type: "ws:close",
|
|
356
|
+
chatId,
|
|
357
|
+
attempt,
|
|
358
|
+
timestamp: Date.now()
|
|
359
|
+
};
|
|
360
|
+
if (ev?.code !== void 0) closeEvent.code = ev.code;
|
|
361
|
+
if (ev?.reason !== void 0 && ev.reason !== "") closeEvent.reason = ev.reason;
|
|
362
|
+
emit(closeEvent);
|
|
363
|
+
}
|
|
299
364
|
finished = true;
|
|
300
365
|
wake();
|
|
301
366
|
};
|
|
367
|
+
const onOpen = emit ? () => emit({ type: "ws:open", chatId, attempt, timestamp: Date.now() }) : void 0;
|
|
368
|
+
if (onOpen) ws.addEventListener("open", onOpen);
|
|
302
369
|
ws.addEventListener("message", onMessage);
|
|
303
370
|
ws.addEventListener("error", onError);
|
|
304
371
|
ws.addEventListener("close", onClose);
|
|
@@ -344,6 +411,7 @@ async function* streamChatEventsLoop(options) {
|
|
|
344
411
|
} finally {
|
|
345
412
|
finished = true;
|
|
346
413
|
if (signal && abortListenerAdded) signal.removeEventListener("abort", onAbort);
|
|
414
|
+
if (onOpen) ws.removeEventListener("open", onOpen);
|
|
347
415
|
ws.removeEventListener("message", onMessage);
|
|
348
416
|
ws.removeEventListener("error", onError);
|
|
349
417
|
ws.removeEventListener("close", onClose);
|
|
@@ -351,6 +419,10 @@ async function* streamChatEventsLoop(options) {
|
|
|
351
419
|
ws.close(1e3);
|
|
352
420
|
} catch {
|
|
353
421
|
}
|
|
422
|
+
if (emit && !closeEmitted) {
|
|
423
|
+
closeEmitted = true;
|
|
424
|
+
emit({ type: "ws:close", chatId, attempt, timestamp: Date.now() });
|
|
425
|
+
}
|
|
354
426
|
}
|
|
355
427
|
if (signal?.aborted) return;
|
|
356
428
|
if (failure instanceof CoderApiError) throw failure;
|
|
@@ -371,6 +443,15 @@ async function* streamChatEventsLoop(options) {
|
|
|
371
443
|
cause: lastFailure
|
|
372
444
|
});
|
|
373
445
|
}
|
|
446
|
+
emit?.({
|
|
447
|
+
type: "ws:redial",
|
|
448
|
+
chatId,
|
|
449
|
+
attempt,
|
|
450
|
+
consecutiveFailures: failures,
|
|
451
|
+
maxConsecutiveFailures: STREAM_MAX_CONSECUTIVE_FAILURES,
|
|
452
|
+
backoffMs,
|
|
453
|
+
timestamp: Date.now()
|
|
454
|
+
});
|
|
374
455
|
await sleep(backoffMs, signal);
|
|
375
456
|
if (signal?.aborted) return;
|
|
376
457
|
backoffMs = Math.min(backoffMs * 2, STREAM_BACKOFF_CAP_MS);
|
|
@@ -554,11 +635,18 @@ var CoderChatClient = class {
|
|
|
554
635
|
#token;
|
|
555
636
|
#fetch;
|
|
556
637
|
#webSocketFactory;
|
|
638
|
+
/** The raw subscriber, forwarded to stream readers (which wrap it themselves). */
|
|
639
|
+
#onTransportEvent;
|
|
640
|
+
/** Exception-isolated emitter for this client's own HTTP events. */
|
|
641
|
+
#emitTransportEvent;
|
|
642
|
+
#httpSeq = 0;
|
|
557
643
|
constructor(options) {
|
|
558
644
|
this.baseUrl = options.baseUrl.replace(/\/$/, "");
|
|
559
645
|
this.#token = options.token;
|
|
560
646
|
this.#fetch = options.fetch ?? globalThis.fetch.bind(globalThis);
|
|
561
647
|
this.#webSocketFactory = options.webSocketFactory;
|
|
648
|
+
this.#onTransportEvent = options.onTransportEvent;
|
|
649
|
+
this.#emitTransportEvent = safeTransportEmitter(options.onTransportEvent);
|
|
562
650
|
}
|
|
563
651
|
/**
|
|
564
652
|
* Issue a request and return the raw `Response` on success (body unread). On a
|
|
@@ -574,7 +662,39 @@ var CoderChatClient = class {
|
|
|
574
662
|
if (typeof ReadableStream !== "undefined" && opts?.body instanceof ReadableStream) {
|
|
575
663
|
init.duplex = "half";
|
|
576
664
|
}
|
|
577
|
-
const
|
|
665
|
+
const emit = this.#emitTransportEvent;
|
|
666
|
+
let requestId = 0;
|
|
667
|
+
let startedAt = 0;
|
|
668
|
+
if (emit) {
|
|
669
|
+
requestId = ++this.#httpSeq;
|
|
670
|
+
startedAt = performance.now();
|
|
671
|
+
emit({ type: "http:request", id: requestId, method, path, timestamp: Date.now() });
|
|
672
|
+
}
|
|
673
|
+
let res;
|
|
674
|
+
try {
|
|
675
|
+
res = await this.#fetch(`${this.baseUrl}${path}`, init);
|
|
676
|
+
} catch (err) {
|
|
677
|
+
emit?.({
|
|
678
|
+
type: "http:error",
|
|
679
|
+
id: requestId,
|
|
680
|
+
method,
|
|
681
|
+
path,
|
|
682
|
+
message: err instanceof Error ? err.message : String(err),
|
|
683
|
+
durationMs: performance.now() - startedAt,
|
|
684
|
+
timestamp: Date.now()
|
|
685
|
+
});
|
|
686
|
+
throw err;
|
|
687
|
+
}
|
|
688
|
+
emit?.({
|
|
689
|
+
type: "http:response",
|
|
690
|
+
id: requestId,
|
|
691
|
+
method,
|
|
692
|
+
path,
|
|
693
|
+
status: res.status,
|
|
694
|
+
ok: res.ok,
|
|
695
|
+
durationMs: performance.now() - startedAt,
|
|
696
|
+
timestamp: Date.now()
|
|
697
|
+
});
|
|
578
698
|
if (!res.ok) {
|
|
579
699
|
const errObj = await this.#json(res) ?? {};
|
|
580
700
|
throw new CoderApiError({
|
|
@@ -742,7 +862,8 @@ var CoderChatClient = class {
|
|
|
742
862
|
chatId,
|
|
743
863
|
afterId: opts?.afterId,
|
|
744
864
|
signal: opts?.signal,
|
|
745
|
-
webSocketFactory: this.#webSocketFactory
|
|
865
|
+
webSocketFactory: this.#webSocketFactory,
|
|
866
|
+
onTransportEvent: this.#onTransportEvent
|
|
746
867
|
});
|
|
747
868
|
}
|
|
748
869
|
/**
|
|
@@ -1016,6 +1137,95 @@ function dynamicToolNames(tools) {
|
|
|
1016
1137
|
return names;
|
|
1017
1138
|
}
|
|
1018
1139
|
|
|
1140
|
+
// src/model/session-stream.ts
|
|
1141
|
+
var SessionChatStream = class {
|
|
1142
|
+
chatId;
|
|
1143
|
+
#events;
|
|
1144
|
+
#controller = new AbortController();
|
|
1145
|
+
#pending;
|
|
1146
|
+
#attached = true;
|
|
1147
|
+
#reusable = false;
|
|
1148
|
+
#closePromise;
|
|
1149
|
+
constructor(opts) {
|
|
1150
|
+
this.chatId = opts.chatId;
|
|
1151
|
+
this.#events = opts.open(this.#controller.signal);
|
|
1152
|
+
}
|
|
1153
|
+
get closed() {
|
|
1154
|
+
return this.#closePromise !== void 0;
|
|
1155
|
+
}
|
|
1156
|
+
/** Whether a segment is currently consuming this stream. */
|
|
1157
|
+
get attached() {
|
|
1158
|
+
return this.#attached;
|
|
1159
|
+
}
|
|
1160
|
+
/** Whether a resume segment for `chatId` may {@link attach}. */
|
|
1161
|
+
canAttach(chatId) {
|
|
1162
|
+
return !this.closed && this.#reusable && this.chatId === chatId;
|
|
1163
|
+
}
|
|
1164
|
+
/**
|
|
1165
|
+
* The next event, memoized: repeated calls return the SAME promise until
|
|
1166
|
+
* {@link consumed} acknowledges delivery, so a read abandoned by one segment
|
|
1167
|
+
* is handed to the next instead of losing its event.
|
|
1168
|
+
*/
|
|
1169
|
+
read() {
|
|
1170
|
+
if (!this.#pending) {
|
|
1171
|
+
if (!this.#attached && !this.closed) {
|
|
1172
|
+
throw new CoderAgentError("SessionChatStream.read() without an attached segment");
|
|
1173
|
+
}
|
|
1174
|
+
this.#pending = this.#events.next();
|
|
1175
|
+
this.#pending.catch(() => {
|
|
1176
|
+
});
|
|
1177
|
+
}
|
|
1178
|
+
return this.#pending;
|
|
1179
|
+
}
|
|
1180
|
+
/**
|
|
1181
|
+
* Acknowledge that the segment received `p`'s result and the next
|
|
1182
|
+
* {@link read} should issue a fresh read. A no-op unless `p` is the
|
|
1183
|
+
* outstanding read — a raced-out segment never consumes.
|
|
1184
|
+
*/
|
|
1185
|
+
consumed(p) {
|
|
1186
|
+
if (this.#pending === p) this.#pending = void 0;
|
|
1187
|
+
}
|
|
1188
|
+
/** Attach the resume segment to a stream paused at `requires_action`. */
|
|
1189
|
+
attach() {
|
|
1190
|
+
if (!this.canAttach(this.chatId) || this.#attached) {
|
|
1191
|
+
throw new CoderAgentError("SessionChatStream.attach() on a non-reusable stream");
|
|
1192
|
+
}
|
|
1193
|
+
this.#attached = true;
|
|
1194
|
+
this.#reusable = false;
|
|
1195
|
+
}
|
|
1196
|
+
/**
|
|
1197
|
+
* Segment settled at a healthy `requires_action` pause: keep the stream for
|
|
1198
|
+
* the tool-result resume, with a read outstanding so the reader's redial
|
|
1199
|
+
* machinery stays live while the client tools execute.
|
|
1200
|
+
*/
|
|
1201
|
+
pause() {
|
|
1202
|
+
this.#attached = false;
|
|
1203
|
+
if (this.closed) return;
|
|
1204
|
+
this.#reusable = true;
|
|
1205
|
+
if (!this.#pending) {
|
|
1206
|
+
this.#pending = this.#events.next();
|
|
1207
|
+
this.#pending.catch(() => {
|
|
1208
|
+
});
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
/**
|
|
1212
|
+
* Close the underlying reader (and socket). Idempotent; safe on a stream
|
|
1213
|
+
* whose generator already ended or threw.
|
|
1214
|
+
*/
|
|
1215
|
+
close() {
|
|
1216
|
+
this.#closePromise ??= (async () => {
|
|
1217
|
+
this.#attached = false;
|
|
1218
|
+
this.#reusable = false;
|
|
1219
|
+
this.#controller.abort();
|
|
1220
|
+
try {
|
|
1221
|
+
await this.#events.return(void 0);
|
|
1222
|
+
} catch {
|
|
1223
|
+
}
|
|
1224
|
+
})();
|
|
1225
|
+
return this.#closePromise;
|
|
1226
|
+
}
|
|
1227
|
+
};
|
|
1228
|
+
|
|
1019
1229
|
// src/model/translate.ts
|
|
1020
1230
|
function addDefined(a, b) {
|
|
1021
1231
|
if (b === void 0) return a;
|
|
@@ -1075,6 +1285,7 @@ function jsonResult(value) {
|
|
|
1075
1285
|
}
|
|
1076
1286
|
var TurnTranslator = class {
|
|
1077
1287
|
#dynamicToolNames;
|
|
1288
|
+
#submittedToolCallIds;
|
|
1078
1289
|
#seq = 0;
|
|
1079
1290
|
#text = { id: void 0, len: 0, sawDelta: false };
|
|
1080
1291
|
#reasoning = { id: void 0, len: 0, sawDelta: false };
|
|
@@ -1111,6 +1322,7 @@ var TurnTranslator = class {
|
|
|
1111
1322
|
constructor(opts) {
|
|
1112
1323
|
this.#dynamicToolNames = opts.dynamicToolNames;
|
|
1113
1324
|
this.#turnCursor = opts.turnCursor ?? 0;
|
|
1325
|
+
this.#submittedToolCallIds = opts.submittedToolCallIds ?? /* @__PURE__ */ new Set();
|
|
1114
1326
|
}
|
|
1115
1327
|
get terminalStatus() {
|
|
1116
1328
|
return this.#terminalStatus;
|
|
@@ -1260,6 +1472,7 @@ var TurnTranslator = class {
|
|
|
1260
1472
|
case "action_required":
|
|
1261
1473
|
for (const tc of ev.action_required?.tool_calls ?? []) {
|
|
1262
1474
|
if (this.#clientToolCalls.has(tc.tool_call_id)) continue;
|
|
1475
|
+
if (this.#submittedToolCallIds.has(tc.tool_call_id)) continue;
|
|
1263
1476
|
this.#closeText(out);
|
|
1264
1477
|
this.#closeReasoning(out);
|
|
1265
1478
|
this.#clientToolCalls.add(tc.tool_call_id);
|
|
@@ -1281,8 +1494,12 @@ var TurnTranslator = class {
|
|
|
1281
1494
|
}
|
|
1282
1495
|
break;
|
|
1283
1496
|
case "status":
|
|
1284
|
-
if (ev.status
|
|
1285
|
-
|
|
1497
|
+
if (ev.status) {
|
|
1498
|
+
if (TERMINAL_STATUSES.has(ev.status.status)) {
|
|
1499
|
+
this.#terminalStatus = ev.status.status;
|
|
1500
|
+
} else {
|
|
1501
|
+
this.#terminalStatus = void 0;
|
|
1502
|
+
}
|
|
1286
1503
|
}
|
|
1287
1504
|
break;
|
|
1288
1505
|
default:
|
|
@@ -1416,6 +1633,7 @@ var TurnTranslator = class {
|
|
|
1416
1633
|
// src/model/language-model.ts
|
|
1417
1634
|
var ACTION_REQUIRED_GRACE_MS = 2e3;
|
|
1418
1635
|
var GRACE_EXPIRED = /* @__PURE__ */ Symbol("action-required-grace-expired");
|
|
1636
|
+
var SEGMENT_ABORTED = /* @__PURE__ */ Symbol("segment-aborted");
|
|
1419
1637
|
function startGraceTimer(ms) {
|
|
1420
1638
|
let timer;
|
|
1421
1639
|
const expired = new Promise((resolve) => {
|
|
@@ -1444,20 +1662,90 @@ var CoderLanguageModel = class {
|
|
|
1444
1662
|
#modelResolved = false;
|
|
1445
1663
|
#submittedToolCallIds = /* @__PURE__ */ new Set();
|
|
1446
1664
|
#inFlight = false;
|
|
1665
|
+
/** The turn-lifetime event stream, retained across client-tool pauses. */
|
|
1666
|
+
#retainedStream;
|
|
1667
|
+
/** Exception-isolated observability emitter for `segment:*` events. */
|
|
1668
|
+
#emitTransportEvent;
|
|
1669
|
+
#segmentSeq = 0;
|
|
1447
1670
|
constructor(config) {
|
|
1448
1671
|
assertSupportedAiVersion();
|
|
1449
1672
|
this.#config = config;
|
|
1450
1673
|
this.modelId = config.model ?? "chatd";
|
|
1451
1674
|
this.#chatId = config.chatId;
|
|
1675
|
+
this.#emitTransportEvent = safeTransportEmitter(config.onTransportEvent);
|
|
1452
1676
|
}
|
|
1453
1677
|
get chatId() {
|
|
1454
1678
|
return this.#chatId;
|
|
1455
1679
|
}
|
|
1456
|
-
/**
|
|
1680
|
+
/**
|
|
1681
|
+
* Drops the current session so the next turn creates a fresh chat. Also
|
|
1682
|
+
* closes the retained event stream, if any (fire-and-forget — a sync reset
|
|
1683
|
+
* must not wait on socket teardown, and `close()` never rejects).
|
|
1684
|
+
*/
|
|
1457
1685
|
resetSession() {
|
|
1458
1686
|
this.#chatId = void 0;
|
|
1459
1687
|
this.#lastSeenMessageId = 0;
|
|
1460
1688
|
this.#submittedToolCallIds.clear();
|
|
1689
|
+
const stream = this.#retainedStream;
|
|
1690
|
+
this.#retainedStream = void 0;
|
|
1691
|
+
if (stream) void stream.close();
|
|
1692
|
+
}
|
|
1693
|
+
/**
|
|
1694
|
+
* Closes the retained event stream, if any. The stream otherwise closes
|
|
1695
|
+
* when its turn settles or fails, or when the next turn replaces it — but a
|
|
1696
|
+
* turn that pauses for client tools and never resumes (e.g. the caller
|
|
1697
|
+
* stopped the tool loop, or aborted between segments) leaves the socket
|
|
1698
|
+
* open, so dispose (`await using`) or {@link resetSession} sessions you
|
|
1699
|
+
* abandon mid-turn.
|
|
1700
|
+
*/
|
|
1701
|
+
async [Symbol.asyncDispose]() {
|
|
1702
|
+
const stream = this.#retainedStream;
|
|
1703
|
+
this.#retainedStream = void 0;
|
|
1704
|
+
if (stream) await stream.close();
|
|
1705
|
+
}
|
|
1706
|
+
/**
|
|
1707
|
+
* Closes a stream retained by a client-tool pause. Called on explicit
|
|
1708
|
+
* interrupts ({@link CoderAgent.interrupt}): an interrupted pause is dead —
|
|
1709
|
+
* its buffer would otherwise collect the interrupt's own settle events,
|
|
1710
|
+
* which a later tool-result resume would consume as if they were the
|
|
1711
|
+
* resumed generation's (truncating the turn), and if the tool loop never
|
|
1712
|
+
* resumes the socket would outlive the explicit interrupt. A stream a
|
|
1713
|
+
* segment is actively reading is left alone: that segment observes the
|
|
1714
|
+
* settle the interrupt produces and closes the stream itself.
|
|
1715
|
+
*/
|
|
1716
|
+
async closePausedStream() {
|
|
1717
|
+
const stream = this.#retainedStream;
|
|
1718
|
+
if (!stream || stream.attached) return;
|
|
1719
|
+
this.#retainedStream = void 0;
|
|
1720
|
+
await stream.close();
|
|
1721
|
+
}
|
|
1722
|
+
/**
|
|
1723
|
+
* The stream a segment reads: a resume segment re-attaches to the stream
|
|
1724
|
+
* its predecessor left paused at `requires_action` (the whole point of
|
|
1725
|
+
* retention — no re-dial per tool round trip); everything else — a new
|
|
1726
|
+
* turn, or a pause the caller abandoned — replaces it with a fresh dial,
|
|
1727
|
+
* because a stale buffer from a settle this client never observed must not
|
|
1728
|
+
* leak into the new turn.
|
|
1729
|
+
*/
|
|
1730
|
+
#acquireStream(resume, chatId, afterId) {
|
|
1731
|
+
const retained = this.#retainedStream;
|
|
1732
|
+
if (resume && retained?.canAttach(chatId)) {
|
|
1733
|
+
retained.attach();
|
|
1734
|
+
return retained;
|
|
1735
|
+
}
|
|
1736
|
+
this.#retainedStream = void 0;
|
|
1737
|
+
if (retained) void retained.close();
|
|
1738
|
+
const created = new SessionChatStream({
|
|
1739
|
+
chatId,
|
|
1740
|
+
open: (streamSignal) => this.#config.client.streamEvents(chatId, { afterId, signal: streamSignal })
|
|
1741
|
+
});
|
|
1742
|
+
this.#retainedStream = created;
|
|
1743
|
+
return created;
|
|
1744
|
+
}
|
|
1745
|
+
/** Close `stream` and drop the retention slot if it still holds it. */
|
|
1746
|
+
async #closeStream(stream) {
|
|
1747
|
+
if (this.#retainedStream === stream) this.#retainedStream = void 0;
|
|
1748
|
+
await stream.close();
|
|
1461
1749
|
}
|
|
1462
1750
|
async #resolveModelConfigId(signal) {
|
|
1463
1751
|
if (this.#modelResolved) return this.#resolvedModelConfigId;
|
|
@@ -1544,7 +1832,93 @@ var CoderLanguageModel = class {
|
|
|
1544
1832
|
});
|
|
1545
1833
|
return events;
|
|
1546
1834
|
}
|
|
1547
|
-
|
|
1835
|
+
/**
|
|
1836
|
+
* Wraps {@link #runTurnInner} with `segment:start`/`segment:settle`
|
|
1837
|
+
* observability events. A pure pass-through without a subscriber — no
|
|
1838
|
+
* counters, clocks, or event objects on the unobserved path. The settle
|
|
1839
|
+
* event is emitted in a `finally`, so every started segment settles exactly
|
|
1840
|
+
* once: cleanly (with the terminal status carried by the `finish` part that
|
|
1841
|
+
* streamed through), with an `error` summary, or — when the consumer tears
|
|
1842
|
+
* the stream down mid-segment — with neither. Consumer teardown has two
|
|
1843
|
+
* shapes: a plain generator return (no catch runs), and a
|
|
1844
|
+
* `ReadableStream.cancel()` that {@link doStream} routes through an abort to
|
|
1845
|
+
* unblock a pending socket read — the latter surfaces here as an abort
|
|
1846
|
+
* throw, so `isConsumerCancel` (supplied by `doStream`) reclassifies it as
|
|
1847
|
+
* teardown rather than a failed segment.
|
|
1848
|
+
*
|
|
1849
|
+
* Two more classification wrinkles (review findings):
|
|
1850
|
+
* - a streamed server `error` part is captured BEFORE being yielded —
|
|
1851
|
+
* `doGenerate()` throws on receiving it and closes this generator without
|
|
1852
|
+
* another pull, so the catch below never sees that failure;
|
|
1853
|
+
* - the segment's chat id is tracked in `segmentChat` (written by
|
|
1854
|
+
* {@link #runTurnInner} once known) — a retry-safe stream failure on a
|
|
1855
|
+
* freshly created chat discards the session (clearing `this.#chatId`)
|
|
1856
|
+
* before the finally runs, and the settle must still name the chat that
|
|
1857
|
+
* the preceding `ws:*`/`http:*` events identified.
|
|
1858
|
+
*/
|
|
1859
|
+
async *#runTurn(options, isConsumerCancel) {
|
|
1860
|
+
const emit = this.#emitTransportEvent;
|
|
1861
|
+
if (!emit) {
|
|
1862
|
+
yield* this.#runTurnInner(options);
|
|
1863
|
+
return;
|
|
1864
|
+
}
|
|
1865
|
+
const segment = ++this.#segmentSeq;
|
|
1866
|
+
const startedAt = performance.now();
|
|
1867
|
+
const startEvent = {
|
|
1868
|
+
type: "segment:start",
|
|
1869
|
+
segment,
|
|
1870
|
+
timestamp: Date.now()
|
|
1871
|
+
};
|
|
1872
|
+
if (this.#chatId !== void 0) startEvent.chatId = this.#chatId;
|
|
1873
|
+
emit(startEvent);
|
|
1874
|
+
const segmentChat = {};
|
|
1875
|
+
let finishReason;
|
|
1876
|
+
let failure;
|
|
1877
|
+
let failed = false;
|
|
1878
|
+
try {
|
|
1879
|
+
for await (const part of this.#runTurnInner(options, segmentChat)) {
|
|
1880
|
+
if (part.type === "finish") finishReason = part.finishReason;
|
|
1881
|
+
else if (part.type === "error" && !failed) {
|
|
1882
|
+
failure = part.error;
|
|
1883
|
+
failed = true;
|
|
1884
|
+
}
|
|
1885
|
+
yield part;
|
|
1886
|
+
}
|
|
1887
|
+
} catch (err) {
|
|
1888
|
+
if (!isConsumerCancel?.()) {
|
|
1889
|
+
failure = err;
|
|
1890
|
+
failed = true;
|
|
1891
|
+
}
|
|
1892
|
+
throw err;
|
|
1893
|
+
} finally {
|
|
1894
|
+
const settleEvent = {
|
|
1895
|
+
type: "segment:settle",
|
|
1896
|
+
segment,
|
|
1897
|
+
durationMs: performance.now() - startedAt,
|
|
1898
|
+
timestamp: Date.now()
|
|
1899
|
+
};
|
|
1900
|
+
const chatId = segmentChat.chatId ?? this.#chatId;
|
|
1901
|
+
if (chatId !== void 0) settleEvent.chatId = chatId;
|
|
1902
|
+
if (!failed && finishReason === void 0 && options.abortSignal?.aborted) {
|
|
1903
|
+
if (!isConsumerCancel?.()) {
|
|
1904
|
+
failure = options.abortSignal.reason;
|
|
1905
|
+
failed = true;
|
|
1906
|
+
}
|
|
1907
|
+
}
|
|
1908
|
+
if (failed) {
|
|
1909
|
+
settleEvent.error = {
|
|
1910
|
+
name: failure instanceof Error ? failure.name : "Error",
|
|
1911
|
+
message: failure instanceof Error ? failure.message : String(failure)
|
|
1912
|
+
};
|
|
1913
|
+
}
|
|
1914
|
+
if (finishReason !== void 0) {
|
|
1915
|
+
if (finishReason.raw !== void 0) settleEvent.status = finishReason.raw;
|
|
1916
|
+
if (!failed) settleEvent.finishReason = finishReason.unified;
|
|
1917
|
+
}
|
|
1918
|
+
emit(settleEvent);
|
|
1919
|
+
}
|
|
1920
|
+
}
|
|
1921
|
+
async *#runTurnInner(options, segmentChat) {
|
|
1548
1922
|
if (this.#inFlight) {
|
|
1549
1923
|
throw new CoderAgentError(
|
|
1550
1924
|
"A generation is already in flight on this CoderAgent (single-flight). Use a separate CoderAgent instance for concurrent sessions."
|
|
@@ -1559,10 +1933,12 @@ var CoderLanguageModel = class {
|
|
|
1559
1933
|
);
|
|
1560
1934
|
const signal = sources.length > 0 ? AbortSignal.any(sources) : void 0;
|
|
1561
1935
|
let translator;
|
|
1936
|
+
let streamAcquired = false;
|
|
1562
1937
|
let discardSession = false;
|
|
1563
1938
|
let interruptSent = false;
|
|
1939
|
+
let turnChatId;
|
|
1564
1940
|
const interrupt = () => {
|
|
1565
|
-
const id = this.#chatId;
|
|
1941
|
+
const id = turnChatId ?? this.#chatId;
|
|
1566
1942
|
if (interruptSent || !id) return;
|
|
1567
1943
|
interruptSent = true;
|
|
1568
1944
|
void this.#config.client.interruptChat(id).catch(() => {
|
|
@@ -1660,25 +2036,49 @@ var CoderLanguageModel = class {
|
|
|
1660
2036
|
afterId = this.#lastSeenMessageId;
|
|
1661
2037
|
}
|
|
1662
2038
|
const chatId = this.#chatId;
|
|
2039
|
+
turnChatId = chatId;
|
|
2040
|
+
if (segmentChat) segmentChat.chatId = chatId;
|
|
1663
2041
|
const dynamicNames = dynamicToolNames(options.tools);
|
|
1664
2042
|
translator = new TurnTranslator({
|
|
1665
2043
|
dynamicToolNames: dynamicNames,
|
|
1666
|
-
turnCursor: afterId ?? 0
|
|
2044
|
+
turnCursor: afterId ?? 0,
|
|
2045
|
+
// The retained stream redials with the TURN's original cursor, so a
|
|
2046
|
+
// reconnect can replay an earlier segment's `action_required`
|
|
2047
|
+
// snapshot; calls this session already answered must not run again.
|
|
2048
|
+
submittedToolCallIds: this.#submittedToolCallIds
|
|
1667
2049
|
});
|
|
1668
2050
|
let sinceRequiresAction = 0;
|
|
1669
2051
|
let grace;
|
|
1670
2052
|
let recovery;
|
|
1671
2053
|
let recoveryAbort;
|
|
1672
2054
|
let recoveryAttempted = false;
|
|
1673
|
-
const stream = this.#
|
|
2055
|
+
const stream = this.#acquireStream(action.kind === "resume", chatId, afterId);
|
|
2056
|
+
streamAcquired = true;
|
|
2057
|
+
let onSegmentAbort;
|
|
2058
|
+
const segmentAborted = signal ? new Promise((resolve) => {
|
|
2059
|
+
onSegmentAbort = () => resolve(SEGMENT_ABORTED);
|
|
2060
|
+
if (signal.aborted) onSegmentAbort();
|
|
2061
|
+
else signal.addEventListener("abort", onSegmentAbort, { once: true });
|
|
2062
|
+
}) : void 0;
|
|
2063
|
+
const raceSegment = (p) => segmentAborted ? Promise.race([p, segmentAborted]) : p;
|
|
2064
|
+
let retainStream = false;
|
|
1674
2065
|
try {
|
|
1675
|
-
let next = stream.next();
|
|
1676
2066
|
for (; ; ) {
|
|
1677
|
-
|
|
2067
|
+
const next = stream.read();
|
|
1678
2068
|
const awaitingActionRequired = translator.terminalStatus === "requires_action" && !translator.clientToolCallSeen;
|
|
2069
|
+
if (!awaitingActionRequired && (grace !== void 0 || recoveryAttempted)) {
|
|
2070
|
+
grace?.cancel();
|
|
2071
|
+
grace = void 0;
|
|
2072
|
+
recoveryAbort?.abort();
|
|
2073
|
+
recoveryAbort = void 0;
|
|
2074
|
+
recovery = void 0;
|
|
2075
|
+
recoveryAttempted = false;
|
|
2076
|
+
sinceRequiresAction = 0;
|
|
2077
|
+
}
|
|
2078
|
+
let raced;
|
|
1679
2079
|
if (awaitingActionRequired && !recoveryAttempted) {
|
|
1680
2080
|
grace ??= startGraceTimer(ACTION_REQUIRED_GRACE_MS);
|
|
1681
|
-
|
|
2081
|
+
raced = await raceSegment(Promise.race([next, grace.expired]));
|
|
1682
2082
|
if (raced === GRACE_EXPIRED) {
|
|
1683
2083
|
recoveryAttempted = true;
|
|
1684
2084
|
throwIfAborted();
|
|
@@ -1691,27 +2091,25 @@ var CoderLanguageModel = class {
|
|
|
1691
2091
|
).catch(() => []);
|
|
1692
2092
|
continue;
|
|
1693
2093
|
}
|
|
1694
|
-
result = raced;
|
|
1695
2094
|
} else if (awaitingActionRequired && recovery) {
|
|
1696
|
-
|
|
2095
|
+
raced = await raceSegment(Promise.race([next, recovery]));
|
|
1697
2096
|
if (Array.isArray(raced)) {
|
|
1698
2097
|
recovery = void 0;
|
|
1699
2098
|
throwIfAborted();
|
|
1700
2099
|
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
|
-
}
|
|
2100
|
+
if (translator.clientToolCallSeen) break;
|
|
1706
2101
|
continue;
|
|
1707
2102
|
}
|
|
1708
|
-
result = raced;
|
|
1709
2103
|
} else {
|
|
1710
|
-
|
|
2104
|
+
raced = await raceSegment(next);
|
|
2105
|
+
}
|
|
2106
|
+
if (raced === SEGMENT_ABORTED) {
|
|
2107
|
+
throwIfAborted();
|
|
2108
|
+
throw new CoderAgentError("segment abort sentinel resolved without an abort");
|
|
1711
2109
|
}
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
for (const part of translator.ingest(
|
|
2110
|
+
stream.consumed(next);
|
|
2111
|
+
if (raced.done) break;
|
|
2112
|
+
for (const part of translator.ingest(raced.value)) yield part;
|
|
1715
2113
|
const status = translator.terminalStatus;
|
|
1716
2114
|
if (status) {
|
|
1717
2115
|
if (status !== "requires_action") break;
|
|
@@ -1719,6 +2117,7 @@ var CoderLanguageModel = class {
|
|
|
1719
2117
|
if (++sinceRequiresAction > 200) break;
|
|
1720
2118
|
}
|
|
1721
2119
|
}
|
|
2120
|
+
retainStream = !signal?.aborted && translator.terminalStatus === "requires_action" && translator.clientToolCallSeen && !translator.error;
|
|
1722
2121
|
} catch (err) {
|
|
1723
2122
|
throwIfAborted();
|
|
1724
2123
|
if (err instanceof CoderStreamError) {
|
|
@@ -1745,8 +2144,12 @@ var CoderLanguageModel = class {
|
|
|
1745
2144
|
} finally {
|
|
1746
2145
|
grace?.cancel();
|
|
1747
2146
|
recoveryAbort?.abort();
|
|
1748
|
-
|
|
1749
|
-
|
|
2147
|
+
if (signal && onSegmentAbort) signal.removeEventListener("abort", onSegmentAbort);
|
|
2148
|
+
if (retainStream && !signal?.aborted && !stream.closed) {
|
|
2149
|
+
stream.pause();
|
|
2150
|
+
} else {
|
|
2151
|
+
await this.#closeStream(stream);
|
|
2152
|
+
}
|
|
1750
2153
|
}
|
|
1751
2154
|
throwIfAborted();
|
|
1752
2155
|
if (!translator.terminalStatus && !translator.error) {
|
|
@@ -1765,6 +2168,11 @@ var CoderLanguageModel = class {
|
|
|
1765
2168
|
this.#lastSeenMessageId = translator.maxMessageId;
|
|
1766
2169
|
signal?.removeEventListener("abort", interrupt);
|
|
1767
2170
|
if (!translator?.terminalStatus) interrupt();
|
|
2171
|
+
if (!streamAcquired && this.#retainedStream) {
|
|
2172
|
+
const stale = this.#retainedStream;
|
|
2173
|
+
this.#retainedStream = void 0;
|
|
2174
|
+
void stale.close();
|
|
2175
|
+
}
|
|
1768
2176
|
if (discardSession) this.resetSession();
|
|
1769
2177
|
this.#inFlight = false;
|
|
1770
2178
|
}
|
|
@@ -1772,7 +2180,8 @@ var CoderLanguageModel = class {
|
|
|
1772
2180
|
async doStream(options) {
|
|
1773
2181
|
const cancelController = new AbortController();
|
|
1774
2182
|
const abortSignal = options.abortSignal ? AbortSignal.any([options.abortSignal, cancelController.signal]) : cancelController.signal;
|
|
1775
|
-
|
|
2183
|
+
let consumerCancelled = false;
|
|
2184
|
+
const gen = this.#runTurn({ ...options, abortSignal }, () => consumerCancelled);
|
|
1776
2185
|
const stream = new ReadableStream({
|
|
1777
2186
|
async pull(controller) {
|
|
1778
2187
|
try {
|
|
@@ -1785,6 +2194,7 @@ var CoderLanguageModel = class {
|
|
|
1785
2194
|
}
|
|
1786
2195
|
},
|
|
1787
2196
|
async cancel() {
|
|
2197
|
+
consumerCancelled = !options.abortSignal?.aborted;
|
|
1788
2198
|
cancelController.abort();
|
|
1789
2199
|
await gen.return().catch(() => {
|
|
1790
2200
|
});
|
|
@@ -1935,7 +2345,8 @@ var CoderAgent = class {
|
|
|
1935
2345
|
baseUrl: settings.baseUrl,
|
|
1936
2346
|
token: settings.token,
|
|
1937
2347
|
fetch: settings.fetch,
|
|
1938
|
-
webSocketFactory: settings.webSocketFactory
|
|
2348
|
+
webSocketFactory: settings.webSocketFactory,
|
|
2349
|
+
onTransportEvent: settings.onTransportEvent
|
|
1939
2350
|
});
|
|
1940
2351
|
} else {
|
|
1941
2352
|
throw new CoderAgentError(
|
|
@@ -1961,7 +2372,8 @@ var CoderAgent = class {
|
|
|
1961
2372
|
mcpServerIds: settings.mcpServerIds,
|
|
1962
2373
|
planMode: settings.planMode,
|
|
1963
2374
|
chatId: settings.chatId,
|
|
1964
|
-
requestTimeoutMs: settings.requestTimeoutMs
|
|
2375
|
+
requestTimeoutMs: settings.requestTimeoutMs,
|
|
2376
|
+
onTransportEvent: settings.onTransportEvent
|
|
1965
2377
|
});
|
|
1966
2378
|
this.#inner = new ToolLoopAgent({
|
|
1967
2379
|
model: this.#model,
|
|
@@ -2014,7 +2426,9 @@ var CoderAgent = class {
|
|
|
2014
2426
|
*/
|
|
2015
2427
|
async interrupt(opts) {
|
|
2016
2428
|
const id = this.#model.chatId;
|
|
2017
|
-
if (id)
|
|
2429
|
+
if (!id) return;
|
|
2430
|
+
await this.#model.closePausedStream();
|
|
2431
|
+
await this.#client.interruptChat(id, opts?.signal);
|
|
2018
2432
|
}
|
|
2019
2433
|
/**
|
|
2020
2434
|
* Archive the underlying chat (safe cleanup; hides it from listings).
|
|
@@ -2028,6 +2442,7 @@ var CoderAgent = class {
|
|
|
2028
2442
|
async archive(opts) {
|
|
2029
2443
|
const id = this.#model.chatId;
|
|
2030
2444
|
if (!id) return;
|
|
2445
|
+
await this.#model.closePausedStream();
|
|
2031
2446
|
await archiveWhenSettled(this.#client, id, {
|
|
2032
2447
|
deadlineMs: this.#settleDeadlineMs,
|
|
2033
2448
|
retryDelayMs: this.#settleRetryDelayMs,
|
|
@@ -2051,6 +2466,10 @@ var CoderAgent = class {
|
|
|
2051
2466
|
* ```
|
|
2052
2467
|
*/
|
|
2053
2468
|
async [Symbol.asyncDispose]() {
|
|
2469
|
+
try {
|
|
2470
|
+
await this.#model[Symbol.asyncDispose]();
|
|
2471
|
+
} catch {
|
|
2472
|
+
}
|
|
2054
2473
|
const deadline = AbortSignal.timeout(this.#settleDeadlineMs);
|
|
2055
2474
|
try {
|
|
2056
2475
|
await this.interrupt({ signal: deadline });
|