@cubos/agent-sdk 0.0.1140467 → 0.0.1140658

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 CHANGED
@@ -437,7 +437,20 @@ api_keys are exempt from all of this.
437
437
 
438
438
  Transient stream failures do not end a subscription: the SDK reconnects with
439
439
  exponential backoff and reports them through `onError`, for logging or a
440
- "reconnecting" hint. A 4xx on a stream is final and closes it.
440
+ "reconnecting" hint.
441
+
442
+ Two failures that look like silence get handled rather than reported. A **401**
443
+ buys one immediate reconnect with a forced token refresh — a short-lived token
444
+ expiring under a stream that outlives it is the expected case, not a
445
+ misconfiguration — and only a second rejection is final. A connection that stops
446
+ sending **anything**, keep-alive comments included, is dropped and reopened after
447
+ `idleTimeoutMs` (30s, twice the server's keep-alive interval): a half-open socket
448
+ otherwise leaves the reader waiting forever with no error to react to.
449
+
450
+ What remains final — a 4xx that survived the refresh, a deleted conversation —
451
+ closes the subscription for good and fires **`onFatal`** (`onError` sees it too).
452
+ That is the one a UI has to surface: everything still renders, and nothing will
453
+ ever arrive again.
441
454
 
442
455
  ## Also in the package
443
456
 
package/dist/client.d.ts CHANGED
@@ -81,9 +81,20 @@ export interface ConversationHandlers {
81
81
  * as `since` on the next connect — that is what lets a cache pick up where it
82
82
  * left off instead of replaying. */
83
83
  onCursor?: (changeSeq: number) => void;
84
- /** Transient stream failures. The SDK is already reconnecting; this is for
85
- * logging or a "reconnecting…" hint, not for recovery. */
84
+ /** Every stream failure, including the one that ends the subscription. The
85
+ * SDK is already reconnecting for all but that one, so this is for logging or
86
+ * a "reconnecting…" hint — watch `onFatal` for the end. */
86
87
  onError?: (err: unknown) => void;
88
+ /**
89
+ * The subscription has stopped and will not come back: the credential was
90
+ * rejected twice (once after a forced refresh), the conversation is gone, or
91
+ * the server refused the stream outright. Fires at most once.
92
+ *
93
+ * Separate from `onError` because the two need opposite treatment — a failed
94
+ * reconnect is normal and shows at most a hint, while this is a dead view
95
+ * that has to tell the user, and only a fresh `subscribe` revives it.
96
+ */
97
+ onFatal?: (err: unknown) => void;
87
98
  }
88
99
  /** What `loadHistory` resolves to: a `MessagePage` plus where it came from. */
89
100
  export interface HistoryStart {
@@ -105,6 +116,8 @@ export interface HistoryStart {
105
116
  export interface ListHandlers {
106
117
  onConversation: (conversation: Conversation) => void;
107
118
  onError?: (err: unknown) => void;
119
+ /** As on `ConversationHandlers`: the stream is gone for good. */
120
+ onFatal?: (err: unknown) => void;
108
121
  }
109
122
  export declare class AgentClient {
110
123
  #private;
package/dist/http.d.ts CHANGED
@@ -27,8 +27,10 @@ interface RequestOptions {
27
27
  export interface Transport {
28
28
  request<T>(method: string, path: string, opts?: RequestOptions): Promise<T>;
29
29
  fetchRaw(method: string, path: string, opts?: RequestOptions): Promise<Response>;
30
- /** Headers for a stream connect, refreshed per (re)connect attempt. */
31
- streamHeaders(): Promise<Record<string, string>>;
30
+ /** Headers for a stream connect, refreshed per (re)connect attempt.
31
+ * `forceRefresh` is the stream's counterpart to the 401 retry `request` does
32
+ * on its own: a `getToken` that caches must mint a new one when it is set. */
33
+ streamHeaders(forceRefresh?: boolean): Promise<Record<string, string>>;
32
34
  url(path: string, query?: RequestOptions["query"]): string;
33
35
  fetchImpl: FetchLike;
34
36
  }
package/dist/index.d.ts CHANGED
@@ -11,6 +11,6 @@ export type { Auth, TokenSource, Transport } from "./http.js";
11
11
  export { DEFAULT_MAX_RETRIES, DEFAULT_TIMEOUT_MS } from "./http.js";
12
12
  export { mergeToolActivities, mergeVoiceMessages } from "./mapping.js";
13
13
  export type { Schemas } from "./schemas.js";
14
- export type { FetchLike } from "./sse.js";
15
- export { readSse } from "./sse.js";
14
+ export type { FetchLike, SseOptions } from "./sse.js";
15
+ export { DEFAULT_IDLE_TIMEOUT_MS, readSse, SseIdleTimeout } from "./sse.js";
16
16
  export type { Activity, Attachment, AttachmentKind, Block, ClientToolCall, Conversation, ConversationEvent, CreateConversationOptions, EnabledComponents, EventPage, Identity, ListConversationsOptions, Message, MessagePage, MessageRole, Page, PlanSnapshot, Todo, TodoStatus, ToolActivity, WorkspaceDir, WorkspaceEntry, } from "./types.js";
package/dist/index.js CHANGED
@@ -79,6 +79,7 @@ async function raiseForStatus(res, fallback) {
79
79
 
80
80
  // src/sse.ts
81
81
  var MAX_BACKOFF_MS = 30000;
82
+ var DEFAULT_IDLE_TIMEOUT_MS = 30000;
82
83
 
83
84
  class Fatal extends Error {
84
85
  cause;
@@ -87,25 +88,54 @@ class Fatal extends Error {
87
88
  this.cause = cause;
88
89
  }
89
90
  }
91
+
92
+ class SseIdleTimeout extends Error {
93
+ constructor(url, ms) {
94
+ super(`Stream ${url} sent nothing for ${ms}ms — reconnecting.`);
95
+ this.name = "SseIdleTimeout";
96
+ }
97
+ }
90
98
  async function readSse(opts) {
91
99
  const doFetch = opts.fetchImpl ?? globalThis.fetch;
100
+ const idleMs = opts.idleTimeoutMs ?? DEFAULT_IDLE_TIMEOUT_MS;
92
101
  let lastEventId = opts.lastEventId;
93
102
  let attempt = 0;
103
+ let refreshing = false;
94
104
  while (!opts.signal.aborted) {
95
105
  let madeProgress = false;
106
+ const connection = new AbortController;
107
+ const unlink = forward(opts.signal, connection);
108
+ let idleTimer;
109
+ let wentIdle = false;
110
+ const armIdle = () => {
111
+ if (idleMs <= 0)
112
+ return;
113
+ clearTimeout(idleTimer);
114
+ idleTimer = setTimeout(() => {
115
+ wentIdle = true;
116
+ connection.abort();
117
+ }, idleMs);
118
+ };
96
119
  try {
97
120
  const headers = {
98
- ...await opts.headers?.(),
121
+ ...await opts.headers?.({ forceRefresh: refreshing }),
99
122
  Accept: "text/event-stream"
100
123
  };
101
124
  if (lastEventId !== undefined)
102
125
  headers["Last-Event-ID"] = lastEventId;
103
- const res = await doFetch(opts.url, { headers, signal: opts.signal });
126
+ armIdle();
127
+ const res = await doFetch(opts.url, { headers, signal: connection.signal });
104
128
  if (opts.signal.aborted)
105
129
  return;
106
- if (res.ok && res.body)
130
+ if (res.ok && res.body) {
131
+ refreshing = false;
107
132
  opts.onOpen?.();
133
+ }
108
134
  if (!res.ok || !res.body) {
135
+ if (res.status === 401 && opts.headers && !refreshing) {
136
+ refreshing = true;
137
+ continue;
138
+ }
109
139
  if (res.status >= 400 && res.status < 500) {
110
140
  await raiseForStatus(res, `Could not open ${opts.url}.`).catch((err) => {
111
141
  throw new Fatal(err);
@@ -114,7 +144,7 @@ async function readSse(opts) {
114
144
  }
115
145
  throw new Error(`stream open failed with HTTP ${res.status}`);
116
146
  }
117
- for await (const frame of frames(res.body, opts.signal)) {
147
+ for await (const frame of frames(res.body, connection.signal, armIdle)) {
118
148
  const parsed = parseFrame(frame, opts.event);
119
149
  if (parsed === null)
120
150
  continue;
@@ -123,12 +153,18 @@ async function readSse(opts) {
123
153
  opts.onEvent(parsed.data, parsed.event);
124
154
  madeProgress = true;
125
155
  }
156
+ if (wentIdle)
157
+ throw new SseIdleTimeout(opts.url, idleMs);
126
158
  } catch (err) {
127
159
  if (opts.signal.aborted)
128
160
  return;
129
161
  if (err instanceof Fatal)
130
162
  throw err.cause;
131
- opts.onError?.(err);
163
+ opts.onError?.(wentIdle ? new SseIdleTimeout(opts.url, idleMs) : err);
164
+ } finally {
165
+ clearTimeout(idleTimer);
166
+ unlink();
167
+ connection.abort();
132
168
  }
133
169
  if (opts.signal.aborted)
134
170
  return;
@@ -139,7 +175,7 @@ async function readSse(opts) {
139
175
  await sleep(delayMs, opts.signal);
140
176
  }
141
177
  }
142
- async function* frames(body, signal) {
178
+ async function* frames(body, signal, onRead) {
143
179
  const reader = body.getReader();
144
180
  const decoder = new TextDecoder;
145
181
  let buffer = "";
@@ -148,6 +184,7 @@ async function* frames(body, signal) {
148
184
  const { value, done } = await reader.read();
149
185
  if (done)
150
186
  return;
187
+ onRead();
151
188
  buffer += decoder.decode(value, { stream: true });
152
189
  for (;; ) {
153
190
  const sep = buffer.indexOf(`
@@ -200,6 +237,14 @@ function sleep(ms, signal) {
200
237
  signal.addEventListener("abort", onAbort, { once: true });
201
238
  });
202
239
  }
240
+ function forward(outer, child) {
241
+ const onAbort = () => child.abort();
242
+ if (outer.aborted)
243
+ child.abort();
244
+ else
245
+ outer.addEventListener("abort", onAbort, { once: true });
246
+ return () => outer.removeEventListener("abort", onAbort);
247
+ }
203
248
 
204
249
  // src/http.ts
205
250
  var DEFAULT_TIMEOUT_MS = 30000;
@@ -326,8 +371,8 @@ function createTransport(baseUrl, auth, fetchImpl = globalThis.fetch, timeoutMs
326
371
  return {
327
372
  fetchImpl,
328
373
  url: (path, query) => `${root}${path}${buildQuery(query)}`,
329
- async streamHeaders() {
330
- return { Authorization: await authHeader(false) };
374
+ async streamHeaders(forceRefresh = false) {
375
+ return { Authorization: await authHeader(forceRefresh) };
331
376
  },
332
377
  fetchRaw: once,
333
378
  async request(method, path, opts = {}) {
@@ -508,7 +553,7 @@ function serveClientTools(t, conversationPath, conversationId, options) {
508
553
  await readSse({
509
554
  url: t.url(`${base}/events/stream`),
510
555
  event: "conversation_event",
511
- headers: () => t.streamHeaders(),
556
+ headers: ({ forceRefresh }) => t.streamHeaders(forceRefresh),
512
557
  fetchImpl: t.fetchImpl,
513
558
  signal: controller.signal,
514
559
  onError: options.onError,
@@ -1736,7 +1781,7 @@ class AgentClient {
1736
1781
  url: this.#transport.url(`${base}/${id}/events/stream`),
1737
1782
  event: ["conversation_event", "conversation_status"],
1738
1783
  lastEventId: opts.since === undefined ? undefined : String(opts.since),
1739
- headers: () => this.#transport.streamHeaders(),
1784
+ headers: ({ forceRefresh }) => this.#transport.streamHeaders(forceRefresh),
1740
1785
  fetchImpl: this.#transport.fetchImpl,
1741
1786
  signal: controller.signal,
1742
1787
  onError: handlers.onError,
@@ -1768,18 +1813,24 @@ class AgentClient {
1768
1813
  handlers.onToolActivity?.(withToolArguments(activity, streamedArgs));
1769
1814
  handlers.onCursor?.(raw.change_seq);
1770
1815
  }
1771
- }).catch((err) => handlers.onError?.(err));
1816
+ }).catch((err) => {
1817
+ handlers.onError?.(err);
1818
+ handlers.onFatal?.(err);
1819
+ });
1772
1820
  }
1773
1821
  if (handlers.onConversation) {
1774
1822
  readSse({
1775
1823
  url: this.#transport.url(`${base}/${id}/meta/stream`),
1776
1824
  event: "conversation_meta",
1777
- headers: () => this.#transport.streamHeaders(),
1825
+ headers: ({ forceRefresh }) => this.#transport.streamHeaders(forceRefresh),
1778
1826
  fetchImpl: this.#transport.fetchImpl,
1779
1827
  signal: controller.signal,
1780
1828
  onError: handlers.onError,
1781
1829
  onEvent: (raw) => handlers.onConversation?.(toConversation(raw))
1782
- }).catch((err) => handlers.onError?.(err));
1830
+ }).catch((err) => {
1831
+ handlers.onError?.(err);
1832
+ handlers.onFatal?.(err);
1833
+ });
1783
1834
  }
1784
1835
  });
1785
1836
  return { close: () => controller.abort() };
@@ -1792,12 +1843,15 @@ class AgentClient {
1792
1843
  readSse({
1793
1844
  url: this.#transport.url(`${base}/stream`, { origin: "interactive" }),
1794
1845
  event: "conversation",
1795
- headers: () => this.#transport.streamHeaders(),
1846
+ headers: ({ forceRefresh }) => this.#transport.streamHeaders(forceRefresh),
1796
1847
  fetchImpl: this.#transport.fetchImpl,
1797
1848
  signal: controller.signal,
1798
1849
  onError: handlers.onError,
1799
1850
  onEvent: (raw) => handlers.onConversation(toConversation(raw))
1800
- }).catch((err) => handlers.onError?.(err));
1851
+ }).catch((err) => {
1852
+ handlers.onError?.(err);
1853
+ handlers.onFatal?.(err);
1854
+ });
1801
1855
  });
1802
1856
  return { close: () => controller.abort() };
1803
1857
  }
@@ -1812,9 +1866,11 @@ export {
1812
1866
  mergeToolActivities,
1813
1867
  createUserClient,
1814
1868
  createAdminClient,
1869
+ SseIdleTimeout,
1815
1870
  MemoryConversationCache,
1816
1871
  DEFAULT_TIMEOUT_MS,
1817
1872
  DEFAULT_MAX_RETRIES,
1873
+ DEFAULT_IDLE_TIMEOUT_MS,
1818
1874
  AgentNetworkError,
1819
1875
  AgentError,
1820
1876
  AgentConfigError,
@@ -1822,5 +1878,5 @@ export {
1822
1878
  AgentApiError
1823
1879
  };
1824
1880
 
1825
- //# debugId=135CEC176CA70F7564756E2164756E21
1881
+ //# debugId=609F372BC6CA145264756E2164756E21
1826
1882
  //# sourceMappingURL=index.js.map