@coder/ai-sdk-agent 0.5.0 → 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 +204 -2
- package/dist/index.js +225 -9
- 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
|
/**
|
|
@@ -1543,11 +1664,15 @@ var CoderLanguageModel = class {
|
|
|
1543
1664
|
#inFlight = false;
|
|
1544
1665
|
/** The turn-lifetime event stream, retained across client-tool pauses. */
|
|
1545
1666
|
#retainedStream;
|
|
1667
|
+
/** Exception-isolated observability emitter for `segment:*` events. */
|
|
1668
|
+
#emitTransportEvent;
|
|
1669
|
+
#segmentSeq = 0;
|
|
1546
1670
|
constructor(config) {
|
|
1547
1671
|
assertSupportedAiVersion();
|
|
1548
1672
|
this.#config = config;
|
|
1549
1673
|
this.modelId = config.model ?? "chatd";
|
|
1550
1674
|
this.#chatId = config.chatId;
|
|
1675
|
+
this.#emitTransportEvent = safeTransportEmitter(config.onTransportEvent);
|
|
1551
1676
|
}
|
|
1552
1677
|
get chatId() {
|
|
1553
1678
|
return this.#chatId;
|
|
@@ -1707,7 +1832,93 @@ var CoderLanguageModel = class {
|
|
|
1707
1832
|
});
|
|
1708
1833
|
return events;
|
|
1709
1834
|
}
|
|
1710
|
-
|
|
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) {
|
|
1711
1922
|
if (this.#inFlight) {
|
|
1712
1923
|
throw new CoderAgentError(
|
|
1713
1924
|
"A generation is already in flight on this CoderAgent (single-flight). Use a separate CoderAgent instance for concurrent sessions."
|
|
@@ -1826,6 +2037,7 @@ var CoderLanguageModel = class {
|
|
|
1826
2037
|
}
|
|
1827
2038
|
const chatId = this.#chatId;
|
|
1828
2039
|
turnChatId = chatId;
|
|
2040
|
+
if (segmentChat) segmentChat.chatId = chatId;
|
|
1829
2041
|
const dynamicNames = dynamicToolNames(options.tools);
|
|
1830
2042
|
translator = new TurnTranslator({
|
|
1831
2043
|
dynamicToolNames: dynamicNames,
|
|
@@ -1968,7 +2180,8 @@ var CoderLanguageModel = class {
|
|
|
1968
2180
|
async doStream(options) {
|
|
1969
2181
|
const cancelController = new AbortController();
|
|
1970
2182
|
const abortSignal = options.abortSignal ? AbortSignal.any([options.abortSignal, cancelController.signal]) : cancelController.signal;
|
|
1971
|
-
|
|
2183
|
+
let consumerCancelled = false;
|
|
2184
|
+
const gen = this.#runTurn({ ...options, abortSignal }, () => consumerCancelled);
|
|
1972
2185
|
const stream = new ReadableStream({
|
|
1973
2186
|
async pull(controller) {
|
|
1974
2187
|
try {
|
|
@@ -1981,6 +2194,7 @@ var CoderLanguageModel = class {
|
|
|
1981
2194
|
}
|
|
1982
2195
|
},
|
|
1983
2196
|
async cancel() {
|
|
2197
|
+
consumerCancelled = !options.abortSignal?.aborted;
|
|
1984
2198
|
cancelController.abort();
|
|
1985
2199
|
await gen.return().catch(() => {
|
|
1986
2200
|
});
|
|
@@ -2131,7 +2345,8 @@ var CoderAgent = class {
|
|
|
2131
2345
|
baseUrl: settings.baseUrl,
|
|
2132
2346
|
token: settings.token,
|
|
2133
2347
|
fetch: settings.fetch,
|
|
2134
|
-
webSocketFactory: settings.webSocketFactory
|
|
2348
|
+
webSocketFactory: settings.webSocketFactory,
|
|
2349
|
+
onTransportEvent: settings.onTransportEvent
|
|
2135
2350
|
});
|
|
2136
2351
|
} else {
|
|
2137
2352
|
throw new CoderAgentError(
|
|
@@ -2157,7 +2372,8 @@ var CoderAgent = class {
|
|
|
2157
2372
|
mcpServerIds: settings.mcpServerIds,
|
|
2158
2373
|
planMode: settings.planMode,
|
|
2159
2374
|
chatId: settings.chatId,
|
|
2160
|
-
requestTimeoutMs: settings.requestTimeoutMs
|
|
2375
|
+
requestTimeoutMs: settings.requestTimeoutMs,
|
|
2376
|
+
onTransportEvent: settings.onTransportEvent
|
|
2161
2377
|
});
|
|
2162
2378
|
this.#inner = new ToolLoopAgent({
|
|
2163
2379
|
model: this.#model,
|