@coder/ai-sdk-agent 0.6.3 → 0.7.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 +24 -14
- package/dist/index.d.ts +36 -1
- package/dist/index.js +32 -9
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -324,19 +324,19 @@ for (const ev of events) {
|
|
|
324
324
|
`timestamp` (`Date.now()` at observation — comparable to server‑side timestamps
|
|
325
325
|
such as a message's `created_at`, for delivery‑lag measurements):
|
|
326
326
|
|
|
327
|
-
| event | when | payload (besides `timestamp`)
|
|
328
|
-
| ---------------- | --------------------------------------------------- |
|
|
329
|
-
| `http:request` | a REST request is sent | `id` (correlates the pair), `method`, `path`
|
|
330
|
-
| `http:response` | response headers arrive (incl. non‑2xx, `ok:false`) | `id`, `method`, `path`, `status`, `ok`, `durationMs`
|
|
331
|
-
| `http:error` | the fetch itself rejects (network failure, abort) | `id`, `method`, `path`, `message`, `durationMs`
|
|
332
|
-
| `ws:dial` | a stream connection attempt starts | `chatId`, `attempt` (1‑based, increments per redial), `url`
|
|
333
|
-
| `ws:open` | the WebSocket handshake completes | `chatId`, `attempt`
|
|
334
|
-
| `ws:event` | a decoded stream event arrives | `chatId`, `attempt`, `event` (the decoded `ChatStreamEvent`, by reference — don't mutate)
|
|
335
|
-
| `ws:close` | the connection ends (exactly one per dial) | `chatId`, `attempt`, `code`/`reason` when the server/network closed it; absent when the reader closed it (settle, teardown, redial)
|
|
336
|
-
| `ws:error` | a socket error or unparseable frame | `chatId`, `attempt`, `message`
|
|
337
|
-
| `ws:redial` | a dropped connection is about to be redialed | `chatId`, `attempt` (the ended connection), `consecutiveFailures`, `maxConsecutiveFailures`, `backoffMs`
|
|
338
|
-
| `segment:start` | a turn segment (one model round‑trip) starts | `segment` (1‑based per model instance), `chatId` (absent before the first turn creates the chat)
|
|
339
|
-
| `segment:settle` | the segment ends (exactly one per start) | `segment`, `chatId`, `durationMs`, and: `status` + `finishReason` on a clean settle, `error` (`{name, message}`, plus `status` if the run still settled terminally) on failure, neither on a teardown |
|
|
327
|
+
| event | when | payload (besides `timestamp`) |
|
|
328
|
+
| ---------------- | --------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
329
|
+
| `http:request` | a REST request is sent | `id` (correlates the pair), `method`, `path` |
|
|
330
|
+
| `http:response` | response headers arrive (incl. non‑2xx, `ok:false`) | `id`, `method`, `path`, `status`, `ok`, `durationMs` |
|
|
331
|
+
| `http:error` | the fetch itself rejects (network failure, abort) | `id`, `method`, `path`, `message`, `durationMs` |
|
|
332
|
+
| `ws:dial` | a stream connection attempt starts | `chatId`, `reader` (identifies the `streamChatEvents` call), `attempt` (1‑based per reader, increments per redial), `url` |
|
|
333
|
+
| `ws:open` | the WebSocket handshake completes | `chatId`, `reader`, `attempt` |
|
|
334
|
+
| `ws:event` | a decoded stream event arrives | `chatId`, `reader`, `attempt`, `event` (the decoded `ChatStreamEvent`, by reference — don't mutate) |
|
|
335
|
+
| `ws:close` | the connection ends (exactly one per dial) | `chatId`, `reader`, `attempt`, `code`/`reason` when the server/network closed it; absent when the reader closed it (settle, teardown, redial) |
|
|
336
|
+
| `ws:error` | a socket error or unparseable frame | `chatId`, `reader`, `attempt`, `message` |
|
|
337
|
+
| `ws:redial` | a dropped connection is about to be redialed | `chatId`, `reader`, `attempt` (the ended connection), `consecutiveFailures`, `maxConsecutiveFailures`, `backoffMs` |
|
|
338
|
+
| `segment:start` | a turn segment (one model round‑trip) starts | `segment` (1‑based per model instance), `chatId` (absent before the first turn creates the chat) |
|
|
339
|
+
| `segment:settle` | the segment ends (exactly one per start) | `segment`, `chatId`, `reader` (the reader that served the segment; absent if none was acquired), `durationMs`, and: `status` + `finishReason` on a clean settle, `error` (`{name, message}`, plus `status` if the run still settled terminally) on failure, neither on a teardown |
|
|
340
340
|
|
|
341
341
|
Semantics worth knowing:
|
|
342
342
|
|
|
@@ -348,8 +348,18 @@ Semantics worth knowing:
|
|
|
348
348
|
`Coder-Session-Token` header, which is deliberately excluded); `path`/`url`
|
|
349
349
|
never contain credentials.
|
|
350
350
|
- `ws:event` fires at arrival, **before** replay dedup: after a redial, chatd's
|
|
351
|
-
replay of the in‑progress episode is visible here (correlate with `attempt`)
|
|
351
|
+
replay of the in‑progress episode is visible here (correlate with `reader`/`attempt`)
|
|
352
352
|
even though the reader suppresses the duplicates it forwards to the turn.
|
|
353
|
+
- Every `ws:*` event carries `reader` — a monotonic id for the
|
|
354
|
+
`streamChatEvents` call (reader) behind the connection, allocated from one
|
|
355
|
+
process‑wide counter so it stays unique across model and client instances.
|
|
356
|
+
`attempt` restarts at 1 per reader, so identify a connection as
|
|
357
|
+
`(chatId, reader, attempt)`, never `(chatId, attempt)` alone: a client‑tool
|
|
358
|
+
pause the caller abandons is closed fire‑and‑forget when the next turn dials
|
|
359
|
+
its replacement, and the superseded reader's late `ws:close` (or a raced‑in
|
|
360
|
+
frame) can emit after the new reader's `ws:dial` — the reader id is what
|
|
361
|
+
tells them apart. `segment:settle` names the reader that served the segment;
|
|
362
|
+
`segment:start` predates stream acquisition and carries none.
|
|
353
363
|
- A multi‑step turn that drives client tools emits one `segment:start`/
|
|
354
364
|
`segment:settle` pair per round‑trip, all riding **one** `ws:dial`ed
|
|
355
365
|
connection — the stream is retained across `requires_action` pauses. A pause
|
package/dist/index.d.ts
CHANGED
|
@@ -382,9 +382,24 @@ interface HttpErrorTransportEvent extends HttpTransportEventBase {
|
|
|
382
382
|
}
|
|
383
383
|
interface StreamTransportEventBase extends TransportEventBase {
|
|
384
384
|
chatId: string;
|
|
385
|
+
/**
|
|
386
|
+
* Identifies the reader (one `streamChatEvents` call) this event belongs
|
|
387
|
+
* to: monotonically increasing, allocated per call from one module-global
|
|
388
|
+
* (process-wide) counter — NOT per model or client instance, so
|
|
389
|
+
* `(chatId, reader, attempt)` stays unique for the lifetime of any
|
|
390
|
+
* subscriber, even one shared across instances (issue #94). Readers on
|
|
391
|
+
* other chats/clients advance the same counter, so ids are ordered but not
|
|
392
|
+
* dense per chat. Use it to tell a superseded reader's late events apart
|
|
393
|
+
* from the live reader's: a client-tool pause abandoned by the caller is
|
|
394
|
+
* closed fire-and-forget when the next turn dials its replacement, and the
|
|
395
|
+
* old reader's `ws:close` (or a raced-in frame) can emit after the new
|
|
396
|
+
* reader's `ws:dial` — both would otherwise be ambiguous at `attempt` 1.
|
|
397
|
+
*/
|
|
398
|
+
reader: number;
|
|
385
399
|
/**
|
|
386
400
|
* 1-based connection attempt within one reader (`streamChatEvents` call —
|
|
387
|
-
* with stream retention (#44), one per turn). Increments on every redial
|
|
401
|
+
* with stream retention (#44), one per turn). Increments on every redial
|
|
402
|
+
* and restarts at 1 for each new reader.
|
|
388
403
|
*/
|
|
389
404
|
attempt: number;
|
|
390
405
|
}
|
|
@@ -446,6 +461,17 @@ interface SegmentTransportEventBase extends TransportEventBase {
|
|
|
446
461
|
segment: number;
|
|
447
462
|
/** Undefined on a first turn's start until the chat is created. */
|
|
448
463
|
chatId?: string;
|
|
464
|
+
/**
|
|
465
|
+
* The reader (`streamChatEvents` call — the `reader` on `ws:*` events) that
|
|
466
|
+
* served this segment, correlating it with the connection's `ws:*` events.
|
|
467
|
+
* Absent on `segment:start`, which is emitted before the segment acquires
|
|
468
|
+
* its stream (a fresh turn dials only once the chat exists, and even a
|
|
469
|
+
* stream retained by a client-tool pause can still be replaced — stamping a
|
|
470
|
+
* predicted reader could lie), and on a settle when the segment failed
|
|
471
|
+
* before acquiring one. Segments of one turn share the reader: the stream
|
|
472
|
+
* is retained across `requires_action` pauses (#44).
|
|
473
|
+
*/
|
|
474
|
+
reader?: number;
|
|
449
475
|
}
|
|
450
476
|
/**
|
|
451
477
|
* A segment (one model round-trip: submission plus the server run until it
|
|
@@ -531,6 +557,13 @@ interface StreamChatEventsOptions {
|
|
|
531
557
|
* listeners are registered. See {@link CoderTransportEvent}.
|
|
532
558
|
*/
|
|
533
559
|
onTransportEvent?: TransportEventHandler;
|
|
560
|
+
/**
|
|
561
|
+
* Reader id stamped on this call's `ws:*` events, for callers that need to
|
|
562
|
+
* know it (the model records it to stamp `segment:settle` with the reader
|
|
563
|
+
* that served the segment). Must come from {@link nextStreamReaderId};
|
|
564
|
+
* allocated automatically when absent.
|
|
565
|
+
*/
|
|
566
|
+
reader?: number;
|
|
534
567
|
}
|
|
535
568
|
/**
|
|
536
569
|
* Opens the chatd `/stream` WebSocket and yields decoded {@link ChatStreamEvent}s
|
|
@@ -678,6 +711,8 @@ declare class CoderChatClient {
|
|
|
678
711
|
streamEvents(chatId: string, opts?: {
|
|
679
712
|
afterId?: number;
|
|
680
713
|
signal?: AbortSignal;
|
|
714
|
+
/** Pre-allocated reader id for `ws:*` events — see {@link streamChatEvents}. */
|
|
715
|
+
reader?: number;
|
|
681
716
|
}): AsyncGenerator<ChatStreamEvent, void, void>;
|
|
682
717
|
/**
|
|
683
718
|
* Watch lifecycle events (status/title changes, creation, deletion, …) for
|
package/dist/index.js
CHANGED
|
@@ -185,6 +185,11 @@ function httpToWs(baseUrl) {
|
|
|
185
185
|
if (baseUrl.startsWith("http://")) return `ws://${baseUrl.slice("http://".length)}`;
|
|
186
186
|
return baseUrl;
|
|
187
187
|
}
|
|
188
|
+
var readerSeq = 0;
|
|
189
|
+
function nextStreamReaderId() {
|
|
190
|
+
readerSeq += 1;
|
|
191
|
+
return readerSeq;
|
|
192
|
+
}
|
|
188
193
|
var STREAM_BACKOFF_INITIAL_MS = 1e3;
|
|
189
194
|
var STREAM_BACKOFF_CAP_MS = 3e4;
|
|
190
195
|
var STREAM_MAX_CONSECUTIVE_FAILURES = 5;
|
|
@@ -197,7 +202,8 @@ function streamChatEvents(options) {
|
|
|
197
202
|
if (external.aborted) chain();
|
|
198
203
|
else external.addEventListener("abort", chain, { once: true });
|
|
199
204
|
}
|
|
200
|
-
const
|
|
205
|
+
const reader = options.reader ?? nextStreamReaderId();
|
|
206
|
+
const inner = streamChatEventsLoop({ ...options, reader, signal: controller.signal });
|
|
201
207
|
const detach = () => external?.removeEventListener("abort", chain);
|
|
202
208
|
const finish = async () => {
|
|
203
209
|
controller.abort();
|
|
@@ -240,7 +246,7 @@ function streamChatEvents(options) {
|
|
|
240
246
|
};
|
|
241
247
|
}
|
|
242
248
|
async function* streamChatEventsLoop(options) {
|
|
243
|
-
const { baseUrl, token, chatId, signal } = options;
|
|
249
|
+
const { baseUrl, token, chatId, signal, reader } = options;
|
|
244
250
|
const factory = options.webSocketFactory ?? defaultFactory;
|
|
245
251
|
const emit = safeTransportEmitter(options.onTransportEvent);
|
|
246
252
|
let attempt = 0;
|
|
@@ -269,15 +275,15 @@ async function* streamChatEventsLoop(options) {
|
|
|
269
275
|
resolveNext = void 0;
|
|
270
276
|
};
|
|
271
277
|
attempt += 1;
|
|
272
|
-
emit?.({ type: "ws:dial", chatId, attempt, url, timestamp: Date.now() });
|
|
278
|
+
emit?.({ type: "ws:dial", chatId, reader, attempt, url, timestamp: Date.now() });
|
|
273
279
|
let ws;
|
|
274
280
|
try {
|
|
275
281
|
ws = factory(url, { headers: { "Coder-Session-Token": token } });
|
|
276
282
|
} catch (err) {
|
|
277
283
|
if (emit) {
|
|
278
284
|
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() });
|
|
285
|
+
emit({ type: "ws:error", chatId, reader, attempt, message, timestamp: Date.now() });
|
|
286
|
+
emit({ type: "ws:close", chatId, reader, attempt, timestamp: Date.now() });
|
|
281
287
|
}
|
|
282
288
|
throw err;
|
|
283
289
|
}
|
|
@@ -307,6 +313,7 @@ async function* streamChatEventsLoop(options) {
|
|
|
307
313
|
emit?.({
|
|
308
314
|
type: "ws:error",
|
|
309
315
|
chatId,
|
|
316
|
+
reader,
|
|
310
317
|
attempt,
|
|
311
318
|
message: "failed to parse chat stream frame",
|
|
312
319
|
timestamp: Date.now()
|
|
@@ -323,6 +330,7 @@ async function* streamChatEventsLoop(options) {
|
|
|
323
330
|
emit?.({
|
|
324
331
|
type: "ws:event",
|
|
325
332
|
chatId,
|
|
333
|
+
reader,
|
|
326
334
|
attempt,
|
|
327
335
|
event: e,
|
|
328
336
|
timestamp: Date.now()
|
|
@@ -333,6 +341,7 @@ async function* streamChatEventsLoop(options) {
|
|
|
333
341
|
emit?.({
|
|
334
342
|
type: "ws:event",
|
|
335
343
|
chatId,
|
|
344
|
+
reader,
|
|
336
345
|
attempt,
|
|
337
346
|
event: batch,
|
|
338
347
|
timestamp: Date.now()
|
|
@@ -343,7 +352,7 @@ async function* streamChatEventsLoop(options) {
|
|
|
343
352
|
const onError = (ev) => {
|
|
344
353
|
if (finished) return;
|
|
345
354
|
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() });
|
|
355
|
+
emit?.({ type: "ws:error", chatId, reader, attempt, message, timestamp: Date.now() });
|
|
347
356
|
const status = upgradeStatus(message);
|
|
348
357
|
failure = status !== void 0 && status >= 400 && status < 500 && !TRANSIENT_UPGRADE_STATUSES.has(status) ? new CoderApiError({ status, method: "GET", path, message }) : new CoderAgentError(message);
|
|
349
358
|
finished = true;
|
|
@@ -355,6 +364,7 @@ async function* streamChatEventsLoop(options) {
|
|
|
355
364
|
const closeEvent = {
|
|
356
365
|
type: "ws:close",
|
|
357
366
|
chatId,
|
|
367
|
+
reader,
|
|
358
368
|
attempt,
|
|
359
369
|
timestamp: Date.now()
|
|
360
370
|
};
|
|
@@ -365,7 +375,7 @@ async function* streamChatEventsLoop(options) {
|
|
|
365
375
|
finished = true;
|
|
366
376
|
wake();
|
|
367
377
|
};
|
|
368
|
-
const onOpen = emit ? () => emit({ type: "ws:open", chatId, attempt, timestamp: Date.now() }) : void 0;
|
|
378
|
+
const onOpen = emit ? () => emit({ type: "ws:open", chatId, reader, attempt, timestamp: Date.now() }) : void 0;
|
|
369
379
|
if (onOpen) ws.addEventListener("open", onOpen);
|
|
370
380
|
ws.addEventListener("message", onMessage);
|
|
371
381
|
ws.addEventListener("error", onError);
|
|
@@ -426,7 +436,7 @@ async function* streamChatEventsLoop(options) {
|
|
|
426
436
|
}
|
|
427
437
|
if (emit && !closeEmitted) {
|
|
428
438
|
closeEmitted = true;
|
|
429
|
-
emit({ type: "ws:close", chatId, attempt, timestamp: Date.now() });
|
|
439
|
+
emit({ type: "ws:close", chatId, reader, attempt, timestamp: Date.now() });
|
|
430
440
|
}
|
|
431
441
|
}
|
|
432
442
|
if (signal?.aborted) return;
|
|
@@ -451,6 +461,7 @@ async function* streamChatEventsLoop(options) {
|
|
|
451
461
|
emit?.({
|
|
452
462
|
type: "ws:redial",
|
|
453
463
|
chatId,
|
|
464
|
+
reader,
|
|
454
465
|
attempt,
|
|
455
466
|
consecutiveFailures: failures,
|
|
456
467
|
maxConsecutiveFailures: STREAM_MAX_CONSECUTIVE_FAILURES,
|
|
@@ -867,6 +878,7 @@ var CoderChatClient = class {
|
|
|
867
878
|
chatId,
|
|
868
879
|
afterId: opts?.afterId,
|
|
869
880
|
signal: opts?.signal,
|
|
881
|
+
reader: opts?.reader,
|
|
870
882
|
webSocketFactory: this.#webSocketFactory,
|
|
871
883
|
onTransportEvent: this.#onTransportEvent
|
|
872
884
|
});
|
|
@@ -1145,6 +1157,12 @@ function dynamicToolNames(tools) {
|
|
|
1145
1157
|
// src/model/session-stream.ts
|
|
1146
1158
|
var SessionChatStream = class {
|
|
1147
1159
|
chatId;
|
|
1160
|
+
/**
|
|
1161
|
+
* The reader id stamped on the wrapped reader's `ws:*` transport events —
|
|
1162
|
+
* the model stamps it on `segment:settle` so segments correlate with the
|
|
1163
|
+
* connection that served them (issue #94).
|
|
1164
|
+
*/
|
|
1165
|
+
reader;
|
|
1148
1166
|
#events;
|
|
1149
1167
|
#controller = new AbortController();
|
|
1150
1168
|
#pending;
|
|
@@ -1153,6 +1171,7 @@ var SessionChatStream = class {
|
|
|
1153
1171
|
#closePromise;
|
|
1154
1172
|
constructor(opts) {
|
|
1155
1173
|
this.chatId = opts.chatId;
|
|
1174
|
+
this.reader = opts.reader;
|
|
1156
1175
|
this.#events = opts.open(this.#controller.signal);
|
|
1157
1176
|
}
|
|
1158
1177
|
get closed() {
|
|
@@ -1811,9 +1830,11 @@ var CoderLanguageModel = class {
|
|
|
1811
1830
|
}
|
|
1812
1831
|
this.#retainedStream = void 0;
|
|
1813
1832
|
if (retained) void retained.close();
|
|
1833
|
+
const reader = nextStreamReaderId();
|
|
1814
1834
|
const created = new SessionChatStream({
|
|
1815
1835
|
chatId,
|
|
1816
|
-
|
|
1836
|
+
reader,
|
|
1837
|
+
open: (streamSignal) => this.#config.client.streamEvents(chatId, { afterId, signal: streamSignal, reader })
|
|
1817
1838
|
});
|
|
1818
1839
|
this.#retainedStream = created;
|
|
1819
1840
|
return created;
|
|
@@ -1977,6 +1998,7 @@ var CoderLanguageModel = class {
|
|
|
1977
1998
|
};
|
|
1978
1999
|
const chatId = segmentChat.chatId ?? this.#chatId;
|
|
1979
2000
|
if (chatId !== void 0) settleEvent.chatId = chatId;
|
|
2001
|
+
if (segmentChat.reader !== void 0) settleEvent.reader = segmentChat.reader;
|
|
1980
2002
|
if (!failed && finishReason === void 0) {
|
|
1981
2003
|
if (segmentChat.timeoutError) {
|
|
1982
2004
|
failure = segmentChat.timeoutError;
|
|
@@ -2140,6 +2162,7 @@ var CoderLanguageModel = class {
|
|
|
2140
2162
|
let recoveryAttempted = false;
|
|
2141
2163
|
const stream = this.#acquireStream(action.kind === "resume", chatId, afterId);
|
|
2142
2164
|
streamAcquired = true;
|
|
2165
|
+
if (segmentChat) segmentChat.reader = stream.reader;
|
|
2143
2166
|
let onSegmentAbort;
|
|
2144
2167
|
const segmentAborted = signal ? new Promise((resolve) => {
|
|
2145
2168
|
onSegmentAbort = () => resolve(SEGMENT_ABORTED);
|