@coder/ai-sdk-agent 0.5.0 → 0.6.1
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 +80 -2
- package/dist/index.d.ts +216 -11
- package/dist/index.js +372 -85
- 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,11 +242,14 @@ 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}` : "";
|
|
234
250
|
const url = `${wsBase}${path}${query}`;
|
|
235
251
|
let maxCommittedId = options.afterId;
|
|
252
|
+
const lastMessageJson = /* @__PURE__ */ new Map();
|
|
236
253
|
let lastHistoryVersion;
|
|
237
254
|
let lastGenerationAttempt;
|
|
238
255
|
let lastSeq;
|
|
@@ -246,11 +263,24 @@ async function* streamChatEventsLoop(options) {
|
|
|
246
263
|
let finished = false;
|
|
247
264
|
let failure;
|
|
248
265
|
let parseFailure = false;
|
|
266
|
+
let closeEmitted = false;
|
|
249
267
|
const wake = () => {
|
|
250
268
|
resolveNext?.();
|
|
251
269
|
resolveNext = void 0;
|
|
252
270
|
};
|
|
253
|
-
|
|
271
|
+
attempt += 1;
|
|
272
|
+
emit?.({ type: "ws:dial", chatId, attempt, url, timestamp: Date.now() });
|
|
273
|
+
let ws;
|
|
274
|
+
try {
|
|
275
|
+
ws = factory(url, { headers: { "Coder-Session-Token": token } });
|
|
276
|
+
} catch (err) {
|
|
277
|
+
if (emit) {
|
|
278
|
+
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() });
|
|
281
|
+
}
|
|
282
|
+
throw err;
|
|
283
|
+
}
|
|
254
284
|
const onAbort = () => {
|
|
255
285
|
finished = true;
|
|
256
286
|
try {
|
|
@@ -274,6 +304,13 @@ async function* streamChatEventsLoop(options) {
|
|
|
274
304
|
const data = typeof ev.data === "string" ? ev.data : String(ev.data);
|
|
275
305
|
batch = JSON.parse(data);
|
|
276
306
|
} catch (err) {
|
|
307
|
+
emit?.({
|
|
308
|
+
type: "ws:error",
|
|
309
|
+
chatId,
|
|
310
|
+
attempt,
|
|
311
|
+
message: "failed to parse chat stream frame",
|
|
312
|
+
timestamp: Date.now()
|
|
313
|
+
});
|
|
277
314
|
failure = new CoderAgentError("failed to parse chat stream frame", { cause: err });
|
|
278
315
|
parseFailure = true;
|
|
279
316
|
finished = true;
|
|
@@ -281,24 +318,55 @@ async function* streamChatEventsLoop(options) {
|
|
|
281
318
|
return;
|
|
282
319
|
}
|
|
283
320
|
if (Array.isArray(batch)) {
|
|
284
|
-
for (const e of batch)
|
|
321
|
+
for (const e of batch) {
|
|
322
|
+
queue.push(e);
|
|
323
|
+
emit?.({
|
|
324
|
+
type: "ws:event",
|
|
325
|
+
chatId,
|
|
326
|
+
attempt,
|
|
327
|
+
event: e,
|
|
328
|
+
timestamp: Date.now()
|
|
329
|
+
});
|
|
330
|
+
}
|
|
285
331
|
} else if (batch && typeof batch === "object") {
|
|
286
332
|
queue.push(batch);
|
|
333
|
+
emit?.({
|
|
334
|
+
type: "ws:event",
|
|
335
|
+
chatId,
|
|
336
|
+
attempt,
|
|
337
|
+
event: batch,
|
|
338
|
+
timestamp: Date.now()
|
|
339
|
+
});
|
|
287
340
|
}
|
|
288
341
|
wake();
|
|
289
342
|
};
|
|
290
343
|
const onError = (ev) => {
|
|
291
344
|
if (finished) return;
|
|
292
345
|
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() });
|
|
293
347
|
const status = upgradeStatus(message);
|
|
294
348
|
failure = status !== void 0 && status >= 400 && status < 500 && !TRANSIENT_UPGRADE_STATUSES.has(status) ? new CoderApiError({ status, method: "GET", path, message }) : new CoderAgentError(message);
|
|
295
349
|
finished = true;
|
|
296
350
|
wake();
|
|
297
351
|
};
|
|
298
|
-
const onClose = () => {
|
|
352
|
+
const onClose = (ev) => {
|
|
353
|
+
if (emit && !closeEmitted) {
|
|
354
|
+
closeEmitted = true;
|
|
355
|
+
const closeEvent = {
|
|
356
|
+
type: "ws:close",
|
|
357
|
+
chatId,
|
|
358
|
+
attempt,
|
|
359
|
+
timestamp: Date.now()
|
|
360
|
+
};
|
|
361
|
+
if (ev?.code !== void 0) closeEvent.code = ev.code;
|
|
362
|
+
if (ev?.reason !== void 0 && ev.reason !== "") closeEvent.reason = ev.reason;
|
|
363
|
+
emit(closeEvent);
|
|
364
|
+
}
|
|
299
365
|
finished = true;
|
|
300
366
|
wake();
|
|
301
367
|
};
|
|
368
|
+
const onOpen = emit ? () => emit({ type: "ws:open", chatId, attempt, timestamp: Date.now() }) : void 0;
|
|
369
|
+
if (onOpen) ws.addEventListener("open", onOpen);
|
|
302
370
|
ws.addEventListener("message", onMessage);
|
|
303
371
|
ws.addEventListener("error", onError);
|
|
304
372
|
ws.addEventListener("close", onClose);
|
|
@@ -324,9 +392,13 @@ async function* streamChatEventsLoop(options) {
|
|
|
324
392
|
sawUntrackableDelta = true;
|
|
325
393
|
}
|
|
326
394
|
} else if (next.type === "message" && next.message) {
|
|
395
|
+
const serialized = JSON.stringify(next.message);
|
|
396
|
+
if (lastMessageJson.get(next.message.id) !== serialized) {
|
|
397
|
+
lastMessageJson.set(next.message.id, serialized);
|
|
398
|
+
progress = true;
|
|
399
|
+
}
|
|
327
400
|
if (maxCommittedId === void 0 || next.message.id > maxCommittedId) {
|
|
328
401
|
maxCommittedId = next.message.id;
|
|
329
|
-
progress = true;
|
|
330
402
|
sawUntrackableDelta = false;
|
|
331
403
|
}
|
|
332
404
|
}
|
|
@@ -344,6 +416,7 @@ async function* streamChatEventsLoop(options) {
|
|
|
344
416
|
} finally {
|
|
345
417
|
finished = true;
|
|
346
418
|
if (signal && abortListenerAdded) signal.removeEventListener("abort", onAbort);
|
|
419
|
+
if (onOpen) ws.removeEventListener("open", onOpen);
|
|
347
420
|
ws.removeEventListener("message", onMessage);
|
|
348
421
|
ws.removeEventListener("error", onError);
|
|
349
422
|
ws.removeEventListener("close", onClose);
|
|
@@ -351,6 +424,10 @@ async function* streamChatEventsLoop(options) {
|
|
|
351
424
|
ws.close(1e3);
|
|
352
425
|
} catch {
|
|
353
426
|
}
|
|
427
|
+
if (emit && !closeEmitted) {
|
|
428
|
+
closeEmitted = true;
|
|
429
|
+
emit({ type: "ws:close", chatId, attempt, timestamp: Date.now() });
|
|
430
|
+
}
|
|
354
431
|
}
|
|
355
432
|
if (signal?.aborted) return;
|
|
356
433
|
if (failure instanceof CoderApiError) throw failure;
|
|
@@ -371,6 +448,15 @@ async function* streamChatEventsLoop(options) {
|
|
|
371
448
|
cause: lastFailure
|
|
372
449
|
});
|
|
373
450
|
}
|
|
451
|
+
emit?.({
|
|
452
|
+
type: "ws:redial",
|
|
453
|
+
chatId,
|
|
454
|
+
attempt,
|
|
455
|
+
consecutiveFailures: failures,
|
|
456
|
+
maxConsecutiveFailures: STREAM_MAX_CONSECUTIVE_FAILURES,
|
|
457
|
+
backoffMs,
|
|
458
|
+
timestamp: Date.now()
|
|
459
|
+
});
|
|
374
460
|
await sleep(backoffMs, signal);
|
|
375
461
|
if (signal?.aborted) return;
|
|
376
462
|
backoffMs = Math.min(backoffMs * 2, STREAM_BACKOFF_CAP_MS);
|
|
@@ -554,11 +640,18 @@ var CoderChatClient = class {
|
|
|
554
640
|
#token;
|
|
555
641
|
#fetch;
|
|
556
642
|
#webSocketFactory;
|
|
643
|
+
/** The raw subscriber, forwarded to stream readers (which wrap it themselves). */
|
|
644
|
+
#onTransportEvent;
|
|
645
|
+
/** Exception-isolated emitter for this client's own HTTP events. */
|
|
646
|
+
#emitTransportEvent;
|
|
647
|
+
#httpSeq = 0;
|
|
557
648
|
constructor(options) {
|
|
558
649
|
this.baseUrl = options.baseUrl.replace(/\/$/, "");
|
|
559
650
|
this.#token = options.token;
|
|
560
651
|
this.#fetch = options.fetch ?? globalThis.fetch.bind(globalThis);
|
|
561
652
|
this.#webSocketFactory = options.webSocketFactory;
|
|
653
|
+
this.#onTransportEvent = options.onTransportEvent;
|
|
654
|
+
this.#emitTransportEvent = safeTransportEmitter(options.onTransportEvent);
|
|
562
655
|
}
|
|
563
656
|
/**
|
|
564
657
|
* Issue a request and return the raw `Response` on success (body unread). On a
|
|
@@ -574,7 +667,39 @@ var CoderChatClient = class {
|
|
|
574
667
|
if (typeof ReadableStream !== "undefined" && opts?.body instanceof ReadableStream) {
|
|
575
668
|
init.duplex = "half";
|
|
576
669
|
}
|
|
577
|
-
const
|
|
670
|
+
const emit = this.#emitTransportEvent;
|
|
671
|
+
let requestId = 0;
|
|
672
|
+
let startedAt = 0;
|
|
673
|
+
if (emit) {
|
|
674
|
+
requestId = ++this.#httpSeq;
|
|
675
|
+
startedAt = performance.now();
|
|
676
|
+
emit({ type: "http:request", id: requestId, method, path, timestamp: Date.now() });
|
|
677
|
+
}
|
|
678
|
+
let res;
|
|
679
|
+
try {
|
|
680
|
+
res = await this.#fetch(`${this.baseUrl}${path}`, init);
|
|
681
|
+
} catch (err) {
|
|
682
|
+
emit?.({
|
|
683
|
+
type: "http:error",
|
|
684
|
+
id: requestId,
|
|
685
|
+
method,
|
|
686
|
+
path,
|
|
687
|
+
message: err instanceof Error ? err.message : String(err),
|
|
688
|
+
durationMs: performance.now() - startedAt,
|
|
689
|
+
timestamp: Date.now()
|
|
690
|
+
});
|
|
691
|
+
throw err;
|
|
692
|
+
}
|
|
693
|
+
emit?.({
|
|
694
|
+
type: "http:response",
|
|
695
|
+
id: requestId,
|
|
696
|
+
method,
|
|
697
|
+
path,
|
|
698
|
+
status: res.status,
|
|
699
|
+
ok: res.ok,
|
|
700
|
+
durationMs: performance.now() - startedAt,
|
|
701
|
+
timestamp: Date.now()
|
|
702
|
+
});
|
|
578
703
|
if (!res.ok) {
|
|
579
704
|
const errObj = await this.#json(res) ?? {};
|
|
580
705
|
throw new CoderApiError({
|
|
@@ -742,7 +867,8 @@ var CoderChatClient = class {
|
|
|
742
867
|
chatId,
|
|
743
868
|
afterId: opts?.afterId,
|
|
744
869
|
signal: opts?.signal,
|
|
745
|
-
webSocketFactory: this.#webSocketFactory
|
|
870
|
+
webSocketFactory: this.#webSocketFactory,
|
|
871
|
+
onTransportEvent: this.#onTransportEvent
|
|
746
872
|
});
|
|
747
873
|
}
|
|
748
874
|
/**
|
|
@@ -1162,12 +1288,21 @@ function accumulateUsage(byMessageId) {
|
|
|
1162
1288
|
function jsonResult(value) {
|
|
1163
1289
|
return value ?? {};
|
|
1164
1290
|
}
|
|
1291
|
+
function joinContent(content, kind) {
|
|
1292
|
+
let full = "";
|
|
1293
|
+
for (const p of content) if (p.type === kind) full += p.text ?? "";
|
|
1294
|
+
return full;
|
|
1295
|
+
}
|
|
1165
1296
|
var TurnTranslator = class {
|
|
1166
1297
|
#dynamicToolNames;
|
|
1167
1298
|
#submittedToolCallIds;
|
|
1168
1299
|
#seq = 0;
|
|
1169
|
-
|
|
1170
|
-
|
|
1300
|
+
// `pending` accumulates delta content that has been emitted but not yet
|
|
1301
|
+
// attributed to a message id (deltas carry none); the next assistant
|
|
1302
|
+
// snapshot claims it. It survives block closes — a closed block loses its
|
|
1303
|
+
// stream id, never the record of what was emitted.
|
|
1304
|
+
#text = { id: void 0, pending: "" };
|
|
1305
|
+
#reasoning = { id: void 0, pending: "" };
|
|
1171
1306
|
#currentAssistantId;
|
|
1172
1307
|
// Whether text/reasoning deltas arrived since the last assistant `message`
|
|
1173
1308
|
// snapshot — i.e. the next assistant snapshot is the trailing snapshot of the
|
|
@@ -1182,11 +1317,22 @@ var TurnTranslator = class {
|
|
|
1182
1317
|
// the same message replaces its earlier entry instead of double-counting
|
|
1183
1318
|
// (chatd re-sends full snapshots on revision bumps and history resets).
|
|
1184
1319
|
#usageByMessageId = /* @__PURE__ */ new Map();
|
|
1185
|
-
//
|
|
1186
|
-
//
|
|
1187
|
-
//
|
|
1188
|
-
//
|
|
1189
|
-
|
|
1320
|
+
// The emitted-content ledger: the text/reasoning this translator has emitted
|
|
1321
|
+
// for each processed assistant message id. A redial replays the turn from
|
|
1322
|
+
// its original cursor and chatd re-sends full snapshots on revision bumps,
|
|
1323
|
+
// so already-processed messages arrive again — the ledger is what lets a
|
|
1324
|
+
// byte-identical replay reconcile to a no-op while an APPENDING revision (or
|
|
1325
|
+
// a commit-during-disconnect snapshot) yields exactly its missing suffix
|
|
1326
|
+
// (see #ingestMessage). `substantive` records whether any processed snapshot
|
|
1327
|
+
// of the message carried content (text, reasoning, tools, or sources) —
|
|
1328
|
+
// false only for announce-style empty snapshots, whose in-flight deltas are
|
|
1329
|
+
// still claimable by a same-id commit.
|
|
1330
|
+
#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.
|
|
1335
|
+
#deferredRevisions = /* @__PURE__ */ new Map();
|
|
1190
1336
|
#error;
|
|
1191
1337
|
#terminalStatus;
|
|
1192
1338
|
#maxMessageId = 0;
|
|
@@ -1221,7 +1367,6 @@ var TurnTranslator = class {
|
|
|
1221
1367
|
if (this.#reasoning.id) this.#closeReasoning(out);
|
|
1222
1368
|
if (!this.#text.id) {
|
|
1223
1369
|
this.#text.id = `text-${++this.#seq}`;
|
|
1224
|
-
this.#text.len = 0;
|
|
1225
1370
|
out.push({ type: "text-start", id: this.#text.id });
|
|
1226
1371
|
}
|
|
1227
1372
|
}
|
|
@@ -1229,14 +1374,12 @@ var TurnTranslator = class {
|
|
|
1229
1374
|
if (this.#text.id) {
|
|
1230
1375
|
out.push({ type: "text-end", id: this.#text.id });
|
|
1231
1376
|
this.#text.id = void 0;
|
|
1232
|
-
this.#text.len = 0;
|
|
1233
1377
|
}
|
|
1234
1378
|
}
|
|
1235
1379
|
#openReasoning(out) {
|
|
1236
1380
|
if (this.#text.id) this.#closeText(out);
|
|
1237
1381
|
if (!this.#reasoning.id) {
|
|
1238
1382
|
this.#reasoning.id = `reasoning-${++this.#seq}`;
|
|
1239
|
-
this.#reasoning.len = 0;
|
|
1240
1383
|
out.push({ type: "reasoning-start", id: this.#reasoning.id });
|
|
1241
1384
|
}
|
|
1242
1385
|
}
|
|
@@ -1244,31 +1387,40 @@ var TurnTranslator = class {
|
|
|
1244
1387
|
if (this.#reasoning.id) {
|
|
1245
1388
|
out.push({ type: "reasoning-end", id: this.#reasoning.id });
|
|
1246
1389
|
this.#reasoning.id = void 0;
|
|
1247
|
-
this.#reasoning.len = 0;
|
|
1248
1390
|
}
|
|
1249
1391
|
}
|
|
1250
|
-
#
|
|
1251
|
-
if (full.length <= this.#text.len && this.#text.id) return;
|
|
1392
|
+
#emitTextDelta(out, delta) {
|
|
1252
1393
|
this.#openText(out);
|
|
1253
|
-
|
|
1254
|
-
out.push({
|
|
1255
|
-
type: "text-delta",
|
|
1256
|
-
id: this.#text.id,
|
|
1257
|
-
delta: full.slice(this.#text.len)
|
|
1258
|
-
});
|
|
1259
|
-
this.#text.len = full.length;
|
|
1260
|
-
}
|
|
1394
|
+
out.push({ type: "text-delta", id: this.#text.id, delta });
|
|
1261
1395
|
}
|
|
1262
|
-
#
|
|
1263
|
-
if (full.length <= this.#reasoning.len && this.#reasoning.id) return;
|
|
1396
|
+
#emitReasoningDelta(out, delta) {
|
|
1264
1397
|
this.#openReasoning(out);
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1398
|
+
out.push({ type: "reasoning-delta", id: this.#reasoning.id, delta });
|
|
1399
|
+
}
|
|
1400
|
+
/**
|
|
1401
|
+
* Reconciles a replayed/revised snapshot of an EARLIER (non-current)
|
|
1402
|
+
* assistant message against its ledger entry: an APPENDING revision emits
|
|
1403
|
+
* its suffix, anything else is a no-op. The suffix is bracketed in its own
|
|
1404
|
+
* block(s) — an open block belongs to the CURRENT step, and splicing another
|
|
1405
|
+
* message's content into it would corrupt both — and the interrupted step's
|
|
1406
|
+
* next delta simply opens a fresh block (its pending content is tracked
|
|
1407
|
+
* independently of blocks).
|
|
1408
|
+
*/
|
|
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;
|
|
1413
|
+
this.#closeText(out);
|
|
1414
|
+
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);
|
|
1272
1424
|
}
|
|
1273
1425
|
}
|
|
1274
1426
|
// --- tool helpers ---------------------------------------------------------
|
|
@@ -1392,21 +1544,19 @@ var TurnTranslator = class {
|
|
|
1392
1544
|
const part = mp.part;
|
|
1393
1545
|
switch (part.type) {
|
|
1394
1546
|
case "text":
|
|
1395
|
-
this.#text.sawDelta = true;
|
|
1396
1547
|
this.#deltasSinceSnapshot = true;
|
|
1397
1548
|
this.#openText(out);
|
|
1398
1549
|
if (part.text) {
|
|
1399
1550
|
out.push({ type: "text-delta", id: this.#text.id, delta: part.text });
|
|
1400
|
-
this.#text.
|
|
1551
|
+
this.#text.pending += part.text;
|
|
1401
1552
|
}
|
|
1402
1553
|
break;
|
|
1403
1554
|
case "reasoning":
|
|
1404
|
-
this.#reasoning.sawDelta = true;
|
|
1405
1555
|
this.#deltasSinceSnapshot = true;
|
|
1406
1556
|
this.#openReasoning(out);
|
|
1407
1557
|
if (part.text) {
|
|
1408
1558
|
out.push({ type: "reasoning-delta", id: this.#reasoning.id, delta: part.text });
|
|
1409
|
-
this.#reasoning.
|
|
1559
|
+
this.#reasoning.pending += part.text;
|
|
1410
1560
|
}
|
|
1411
1561
|
break;
|
|
1412
1562
|
case "tool-call":
|
|
@@ -1432,42 +1582,56 @@ var TurnTranslator = class {
|
|
|
1432
1582
|
}
|
|
1433
1583
|
const content = message.content ?? [];
|
|
1434
1584
|
if (message.role === "assistant") {
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1585
|
+
const fullText = joinContent(content, "text");
|
|
1586
|
+
const fullReasoning = joinContent(content, "reasoning");
|
|
1587
|
+
const known = this.#emittedByMessageId.get(message.id);
|
|
1588
|
+
if (known && message.id !== this.#currentAssistantId) {
|
|
1589
|
+
this.#reconcileRevision(out, known, fullText, fullReasoning);
|
|
1590
|
+
} else if (known && this.#deltasSinceSnapshot && known.substantive) {
|
|
1591
|
+
this.#deferredRevisions.set(message.id, { text: fullText, reasoning: fullReasoning });
|
|
1592
|
+
} else {
|
|
1593
|
+
if (this.#currentAssistantId !== void 0 && this.#currentAssistantId !== message.id && !this.#deltasSinceSnapshot) {
|
|
1594
|
+
this.#closeText(out);
|
|
1595
|
+
this.#closeReasoning(out);
|
|
1442
1596
|
}
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1597
|
+
this.#currentAssistantId = message.id;
|
|
1598
|
+
const rec = known ?? { text: "", reasoning: "", substantive: false };
|
|
1599
|
+
const textAttributed = rec.text + this.#text.pending;
|
|
1600
|
+
const reasoningAttributed = rec.reasoning + this.#reasoning.pending;
|
|
1601
|
+
const textOk = fullText.startsWith(textAttributed);
|
|
1602
|
+
const reasoningOk = fullReasoning.startsWith(reasoningAttributed);
|
|
1603
|
+
let seenText = 0;
|
|
1604
|
+
let seenReasoning = 0;
|
|
1605
|
+
for (const p of content) {
|
|
1606
|
+
if (p.type === "text") {
|
|
1607
|
+
const end = seenText + (p.text ?? "").length;
|
|
1608
|
+
const from = Math.max(seenText, textAttributed.length);
|
|
1609
|
+
if (textOk && end > from) this.#emitTextDelta(out, fullText.slice(from, end));
|
|
1610
|
+
seenText = end;
|
|
1611
|
+
} else if (p.type === "reasoning") {
|
|
1612
|
+
const end = seenReasoning + (p.text ?? "").length;
|
|
1613
|
+
const from = Math.max(seenReasoning, reasoningAttributed.length);
|
|
1614
|
+
if (reasoningOk && end > from)
|
|
1615
|
+
this.#emitReasoningDelta(out, fullReasoning.slice(from, end));
|
|
1616
|
+
seenReasoning = end;
|
|
1617
|
+
}
|
|
1457
1618
|
}
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1619
|
+
if (textOk) rec.text = fullText;
|
|
1620
|
+
if (reasoningOk) rec.reasoning = fullReasoning;
|
|
1621
|
+
this.#text.pending = "";
|
|
1622
|
+
this.#reasoning.pending = "";
|
|
1623
|
+
rec.substantive ||= content.some(
|
|
1624
|
+
(p) => p.type === "text" || p.type === "reasoning" ? (p.text ?? "").length > 0 : true
|
|
1625
|
+
);
|
|
1626
|
+
this.#emittedByMessageId.set(message.id, rec);
|
|
1627
|
+
this.#deltasSinceSnapshot = false;
|
|
1628
|
+
for (const [id, deferred] of this.#deferredRevisions) {
|
|
1629
|
+
if (id === message.id) continue;
|
|
1630
|
+
const deferredRec = this.#emittedByMessageId.get(id);
|
|
1631
|
+
if (deferredRec)
|
|
1632
|
+
this.#reconcileRevision(out, deferredRec, deferred.text, deferred.reasoning);
|
|
1633
|
+
this.#deferredRevisions.delete(id);
|
|
1463
1634
|
}
|
|
1464
|
-
};
|
|
1465
|
-
if (this.#reasoning.id) {
|
|
1466
|
-
emitSnapshotReasoning();
|
|
1467
|
-
emitSnapshotText();
|
|
1468
|
-
} else {
|
|
1469
|
-
emitSnapshotText();
|
|
1470
|
-
emitSnapshotReasoning();
|
|
1471
1635
|
}
|
|
1472
1636
|
for (const part of content) {
|
|
1473
1637
|
if (part.type === "tool-call" && !this.#isClientTool(part.tool_name))
|
|
@@ -1476,7 +1640,6 @@ var TurnTranslator = class {
|
|
|
1476
1640
|
this.#emitServerToolResult(out, part);
|
|
1477
1641
|
else if (part.type === "source") this.#emitSource(out, part);
|
|
1478
1642
|
}
|
|
1479
|
-
this.#deltasSinceSnapshot = false;
|
|
1480
1643
|
} else if (message.role === "tool") {
|
|
1481
1644
|
for (const part of content) {
|
|
1482
1645
|
if (part.type === "tool-result" && !this.#isClientTool(part.tool_name))
|
|
@@ -1543,11 +1706,15 @@ var CoderLanguageModel = class {
|
|
|
1543
1706
|
#inFlight = false;
|
|
1544
1707
|
/** The turn-lifetime event stream, retained across client-tool pauses. */
|
|
1545
1708
|
#retainedStream;
|
|
1709
|
+
/** Exception-isolated observability emitter for `segment:*` events. */
|
|
1710
|
+
#emitTransportEvent;
|
|
1711
|
+
#segmentSeq = 0;
|
|
1546
1712
|
constructor(config) {
|
|
1547
1713
|
assertSupportedAiVersion();
|
|
1548
1714
|
this.#config = config;
|
|
1549
1715
|
this.modelId = config.model ?? "chatd";
|
|
1550
1716
|
this.#chatId = config.chatId;
|
|
1717
|
+
this.#emitTransportEvent = safeTransportEmitter(config.onTransportEvent);
|
|
1551
1718
|
}
|
|
1552
1719
|
get chatId() {
|
|
1553
1720
|
return this.#chatId;
|
|
@@ -1707,7 +1874,100 @@ var CoderLanguageModel = class {
|
|
|
1707
1874
|
});
|
|
1708
1875
|
return events;
|
|
1709
1876
|
}
|
|
1710
|
-
|
|
1877
|
+
/**
|
|
1878
|
+
* Wraps {@link #runTurnInner} with `segment:start`/`segment:settle`
|
|
1879
|
+
* observability events. A pure pass-through without a subscriber — no
|
|
1880
|
+
* counters, clocks, or event objects on the unobserved path. The settle
|
|
1881
|
+
* event is emitted in a `finally`, so every started segment settles exactly
|
|
1882
|
+
* once: cleanly (with the terminal status carried by the `finish` part that
|
|
1883
|
+
* streamed through), with an `error` summary, or — when the consumer tears
|
|
1884
|
+
* the stream down mid-segment — with neither. Consumer teardown has two
|
|
1885
|
+
* shapes: a plain generator return (no catch runs), and a
|
|
1886
|
+
* `ReadableStream.cancel()` that {@link doStream} routes through an abort to
|
|
1887
|
+
* unblock a pending socket read — the latter surfaces here as an abort
|
|
1888
|
+
* throw, so `isConsumerCancel` (supplied by `doStream`) reclassifies it as
|
|
1889
|
+
* teardown rather than a failed segment.
|
|
1890
|
+
*
|
|
1891
|
+
* Two more classification wrinkles (review findings):
|
|
1892
|
+
* - a streamed server `error` part is captured BEFORE being yielded —
|
|
1893
|
+
* `doGenerate()` throws on receiving it and closes this generator without
|
|
1894
|
+
* another pull, so the catch below never sees that failure;
|
|
1895
|
+
* - the segment's chat id is tracked in `segmentChat` (written by
|
|
1896
|
+
* {@link #runTurnInner} once known) — a retry-safe stream failure on a
|
|
1897
|
+
* freshly created chat discards the session (clearing `this.#chatId`)
|
|
1898
|
+
* before the finally runs, and the settle must still name the chat that
|
|
1899
|
+
* the preceding `ws:*`/`http:*` events identified. The same out-param
|
|
1900
|
+
* carries the translator's terminal status and the internal-timeout
|
|
1901
|
+
* marker for generator-return exits (see {@link SegmentChatRef}).
|
|
1902
|
+
*/
|
|
1903
|
+
async *#runTurn(options, isConsumerCancel) {
|
|
1904
|
+
const emit = this.#emitTransportEvent;
|
|
1905
|
+
if (!emit) {
|
|
1906
|
+
yield* this.#runTurnInner(options);
|
|
1907
|
+
return;
|
|
1908
|
+
}
|
|
1909
|
+
const segment = ++this.#segmentSeq;
|
|
1910
|
+
const startedAt = performance.now();
|
|
1911
|
+
const startEvent = {
|
|
1912
|
+
type: "segment:start",
|
|
1913
|
+
segment,
|
|
1914
|
+
timestamp: Date.now()
|
|
1915
|
+
};
|
|
1916
|
+
if (this.#chatId !== void 0) startEvent.chatId = this.#chatId;
|
|
1917
|
+
emit(startEvent);
|
|
1918
|
+
const segmentChat = {};
|
|
1919
|
+
let finishReason;
|
|
1920
|
+
let failure;
|
|
1921
|
+
let failed = false;
|
|
1922
|
+
try {
|
|
1923
|
+
for await (const part of this.#runTurnInner(options, segmentChat)) {
|
|
1924
|
+
if (part.type === "finish") finishReason = part.finishReason;
|
|
1925
|
+
else if (part.type === "error" && !failed) {
|
|
1926
|
+
failure = part.error;
|
|
1927
|
+
failed = true;
|
|
1928
|
+
}
|
|
1929
|
+
yield part;
|
|
1930
|
+
}
|
|
1931
|
+
} catch (err) {
|
|
1932
|
+
if (!isConsumerCancel?.()) {
|
|
1933
|
+
failure = err;
|
|
1934
|
+
failed = true;
|
|
1935
|
+
}
|
|
1936
|
+
throw err;
|
|
1937
|
+
} finally {
|
|
1938
|
+
const settleEvent = {
|
|
1939
|
+
type: "segment:settle",
|
|
1940
|
+
segment,
|
|
1941
|
+
durationMs: performance.now() - startedAt,
|
|
1942
|
+
timestamp: Date.now()
|
|
1943
|
+
};
|
|
1944
|
+
const chatId = segmentChat.chatId ?? this.#chatId;
|
|
1945
|
+
if (chatId !== void 0) settleEvent.chatId = chatId;
|
|
1946
|
+
if (!failed && finishReason === void 0) {
|
|
1947
|
+
if (segmentChat.timeoutError) {
|
|
1948
|
+
failure = segmentChat.timeoutError;
|
|
1949
|
+
failed = true;
|
|
1950
|
+
} else if (options.abortSignal?.aborted && !isConsumerCancel?.()) {
|
|
1951
|
+
failure = options.abortSignal.reason;
|
|
1952
|
+
failed = true;
|
|
1953
|
+
}
|
|
1954
|
+
}
|
|
1955
|
+
if (failed) {
|
|
1956
|
+
settleEvent.error = {
|
|
1957
|
+
name: failure instanceof Error ? failure.name : "Error",
|
|
1958
|
+
message: failure instanceof Error ? failure.message : String(failure)
|
|
1959
|
+
};
|
|
1960
|
+
}
|
|
1961
|
+
if (finishReason !== void 0) {
|
|
1962
|
+
if (finishReason.raw !== void 0) settleEvent.status = finishReason.raw;
|
|
1963
|
+
if (!failed) settleEvent.finishReason = finishReason.unified;
|
|
1964
|
+
} else if (segmentChat.terminalStatus !== void 0) {
|
|
1965
|
+
settleEvent.status = segmentChat.terminalStatus;
|
|
1966
|
+
}
|
|
1967
|
+
emit(settleEvent);
|
|
1968
|
+
}
|
|
1969
|
+
}
|
|
1970
|
+
async *#runTurnInner(options, segmentChat) {
|
|
1711
1971
|
if (this.#inFlight) {
|
|
1712
1972
|
throw new CoderAgentError(
|
|
1713
1973
|
"A generation is already in flight on this CoderAgent (single-flight). Use a separate CoderAgent instance for concurrent sessions."
|
|
@@ -1734,13 +1994,14 @@ var CoderLanguageModel = class {
|
|
|
1734
1994
|
});
|
|
1735
1995
|
};
|
|
1736
1996
|
signal?.addEventListener("abort", interrupt, { once: true });
|
|
1997
|
+
const timeoutError = () => new CoderChatError({
|
|
1998
|
+
message: `Coder Agent turn exceeded its ${timeoutMs}ms requestTimeoutMs budget.`,
|
|
1999
|
+
kind: "timeout",
|
|
2000
|
+
retryable: true
|
|
2001
|
+
});
|
|
1737
2002
|
const throwIfAborted = () => {
|
|
1738
2003
|
if (timeoutSignal?.aborted) {
|
|
1739
|
-
throw
|
|
1740
|
-
message: `Coder Agent turn exceeded its ${timeoutMs}ms requestTimeoutMs budget.`,
|
|
1741
|
-
kind: "timeout",
|
|
1742
|
-
retryable: true
|
|
1743
|
-
});
|
|
2004
|
+
throw timeoutError();
|
|
1744
2005
|
}
|
|
1745
2006
|
if (signal?.aborted) {
|
|
1746
2007
|
throw externalSignal?.reason ?? new DOMException("The operation was aborted.", "AbortError");
|
|
@@ -1767,6 +2028,7 @@ var CoderLanguageModel = class {
|
|
|
1767
2028
|
let turnCreatedChat = false;
|
|
1768
2029
|
let uploadedAttachment = false;
|
|
1769
2030
|
let chatHasWorkspace = Boolean(this.#config.workspaceId);
|
|
2031
|
+
let chatHasMcpServers = Boolean(this.#config.mcpServerIds?.length);
|
|
1770
2032
|
if (this.#chatId && this.#lastSeenMessageId === 0) {
|
|
1771
2033
|
const { messages } = await this.#config.client.getMessages(
|
|
1772
2034
|
this.#chatId,
|
|
@@ -1800,6 +2062,7 @@ var CoderLanguageModel = class {
|
|
|
1800
2062
|
this.#chatId = chat.id;
|
|
1801
2063
|
turnCreatedChat = true;
|
|
1802
2064
|
if (chat.workspace_id) chatHasWorkspace = true;
|
|
2065
|
+
if (chat.mcp_server_ids?.length) chatHasMcpServers = true;
|
|
1803
2066
|
afterId = this.#lastSeenMessageId > 0 ? this.#lastSeenMessageId : void 0;
|
|
1804
2067
|
} else {
|
|
1805
2068
|
const resp = await this.#config.client.createChatMessage(
|
|
@@ -1826,6 +2089,7 @@ var CoderLanguageModel = class {
|
|
|
1826
2089
|
}
|
|
1827
2090
|
const chatId = this.#chatId;
|
|
1828
2091
|
turnChatId = chatId;
|
|
2092
|
+
if (segmentChat) segmentChat.chatId = chatId;
|
|
1829
2093
|
const dynamicNames = dynamicToolNames(options.tools);
|
|
1830
2094
|
translator = new TurnTranslator({
|
|
1831
2095
|
dynamicToolNames: dynamicNames,
|
|
@@ -1897,7 +2161,20 @@ var CoderLanguageModel = class {
|
|
|
1897
2161
|
}
|
|
1898
2162
|
stream.consumed(next);
|
|
1899
2163
|
if (raced.done) break;
|
|
1900
|
-
|
|
2164
|
+
const parts = translator.ingest(raced.value);
|
|
2165
|
+
if (raced.value.type === "error" && translator.error && !translator.terminalStatus) {
|
|
2166
|
+
const peek = stream.read();
|
|
2167
|
+
const bound = startGraceTimer(0);
|
|
2168
|
+
const peeked = await raceSegment(Promise.race([peek, bound.expired])).catch(
|
|
2169
|
+
() => GRACE_EXPIRED
|
|
2170
|
+
);
|
|
2171
|
+
bound.cancel();
|
|
2172
|
+
if (peeked !== GRACE_EXPIRED && peeked !== SEGMENT_ABORTED && !peeked.done) {
|
|
2173
|
+
stream.consumed(peek);
|
|
2174
|
+
parts.push(...translator.ingest(peeked.value));
|
|
2175
|
+
}
|
|
2176
|
+
}
|
|
2177
|
+
for (const part of parts) yield part;
|
|
1901
2178
|
const status = translator.terminalStatus;
|
|
1902
2179
|
if (status) {
|
|
1903
2180
|
if (status !== "requires_action") break;
|
|
@@ -1912,7 +2189,7 @@ var CoderLanguageModel = class {
|
|
|
1912
2189
|
if (turnCreatedChat) {
|
|
1913
2190
|
discardSession = true;
|
|
1914
2191
|
}
|
|
1915
|
-
const effectful = chatHasWorkspace ||
|
|
2192
|
+
const effectful = chatHasWorkspace || chatHasMcpServers || uploadedAttachment;
|
|
1916
2193
|
if (turnCreatedChat && !effectful) throw err;
|
|
1917
2194
|
throw new CoderStreamError({
|
|
1918
2195
|
message: `${err.message}; automatic retry is disabled because ${turnCreatedChat ? "the turn has server-side effects a replay would repeat (workspace/MCP tools that may have executed, or freshly uploaded attachments)" : "the chat has prior state (a retry would resubmit this turn's prompt as a new user turn)"}`,
|
|
@@ -1954,6 +2231,12 @@ var CoderLanguageModel = class {
|
|
|
1954
2231
|
} finally {
|
|
1955
2232
|
if (translator && translator.maxMessageId > this.#lastSeenMessageId)
|
|
1956
2233
|
this.#lastSeenMessageId = translator.maxMessageId;
|
|
2234
|
+
if (segmentChat) {
|
|
2235
|
+
if (translator?.terminalStatus !== void 0) {
|
|
2236
|
+
segmentChat.terminalStatus = translator.terminalStatus;
|
|
2237
|
+
}
|
|
2238
|
+
if (timeoutSignal?.aborted) segmentChat.timeoutError = timeoutError();
|
|
2239
|
+
}
|
|
1957
2240
|
signal?.removeEventListener("abort", interrupt);
|
|
1958
2241
|
if (!translator?.terminalStatus) interrupt();
|
|
1959
2242
|
if (!streamAcquired && this.#retainedStream) {
|
|
@@ -1968,7 +2251,8 @@ var CoderLanguageModel = class {
|
|
|
1968
2251
|
async doStream(options) {
|
|
1969
2252
|
const cancelController = new AbortController();
|
|
1970
2253
|
const abortSignal = options.abortSignal ? AbortSignal.any([options.abortSignal, cancelController.signal]) : cancelController.signal;
|
|
1971
|
-
|
|
2254
|
+
let consumerCancelled = false;
|
|
2255
|
+
const gen = this.#runTurn({ ...options, abortSignal }, () => consumerCancelled);
|
|
1972
2256
|
const stream = new ReadableStream({
|
|
1973
2257
|
async pull(controller) {
|
|
1974
2258
|
try {
|
|
@@ -1981,6 +2265,7 @@ var CoderLanguageModel = class {
|
|
|
1981
2265
|
}
|
|
1982
2266
|
},
|
|
1983
2267
|
async cancel() {
|
|
2268
|
+
consumerCancelled = !options.abortSignal?.aborted;
|
|
1984
2269
|
cancelController.abort();
|
|
1985
2270
|
await gen.return().catch(() => {
|
|
1986
2271
|
});
|
|
@@ -2131,7 +2416,8 @@ var CoderAgent = class {
|
|
|
2131
2416
|
baseUrl: settings.baseUrl,
|
|
2132
2417
|
token: settings.token,
|
|
2133
2418
|
fetch: settings.fetch,
|
|
2134
|
-
webSocketFactory: settings.webSocketFactory
|
|
2419
|
+
webSocketFactory: settings.webSocketFactory,
|
|
2420
|
+
onTransportEvent: settings.onTransportEvent
|
|
2135
2421
|
});
|
|
2136
2422
|
} else {
|
|
2137
2423
|
throw new CoderAgentError(
|
|
@@ -2157,7 +2443,8 @@ var CoderAgent = class {
|
|
|
2157
2443
|
mcpServerIds: settings.mcpServerIds,
|
|
2158
2444
|
planMode: settings.planMode,
|
|
2159
2445
|
chatId: settings.chatId,
|
|
2160
|
-
requestTimeoutMs: settings.requestTimeoutMs
|
|
2446
|
+
requestTimeoutMs: settings.requestTimeoutMs,
|
|
2447
|
+
onTransportEvent: settings.onTransportEvent
|
|
2161
2448
|
});
|
|
2162
2449
|
this.#inner = new ToolLoopAgent({
|
|
2163
2450
|
model: this.#model,
|