@tanstack/ai-client 0.31.0 → 0.32.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.
@@ -130,7 +130,7 @@ export interface ConnectConnectionAdapter {
130
130
  * `fetchServerSentEvents` / `fetchHttpStream`, and on `stream()` /
131
131
  * `rpcStream()` when supplied via {@link StreamConnectionHandlers}.
132
132
  */
133
- hydrate?: (threadId: string) => Promise<ChatHydrationResult>;
133
+ hydrate?: (threadId: string, options?: ChatHydrateOptions) => Promise<ChatHydrationResult>;
134
134
  }
135
135
  /**
136
136
  * Server-resolved hydration for a generation thread. `resumeSnapshot` is the
@@ -166,11 +166,22 @@ export interface GenerationHydrationResult {
166
166
  runId: string;
167
167
  } | null;
168
168
  }
169
+ /**
170
+ * Optional paging for a chat hydrate GET.
171
+ * Omit both fields to load the full thread.
172
+ */
173
+ export interface ChatHydrateOptions {
174
+ limit?: number;
175
+ before?: string;
176
+ }
169
177
  /**
170
178
  * Server-resolved hydration for a thread. `messages` is the stored transcript;
171
179
  * `activeRun` is a cursor to a run still generating for the thread (or `null`).
172
180
  * Keyed on the STABLE thread id — the client never handles a run id, so a turn
173
181
  * that spans several runs (interrupt/tool continuations) reconnects correctly.
182
+ *
183
+ * `page` is additive. Old servers omit it; treat that as a full list
184
+ * (`truncated: false`).
174
185
  */
175
186
  export interface ChatHydrationResult {
176
187
  messages: Array<UIMessage>;
@@ -186,6 +197,16 @@ export interface ChatHydrationResult {
186
197
  runId: string;
187
198
  pending: Array<ChatPendingInterrupt>;
188
199
  } | null;
200
+ /**
201
+ * Whether this window is a slice of a longer thread.
202
+ * `cursor` is set when `truncated` is true and is sent back as `before`.
203
+ */
204
+ page?: {
205
+ truncated: false;
206
+ } | {
207
+ truncated: true;
208
+ cursor: string;
209
+ };
189
210
  }
190
211
  /**
191
212
  * A {@link ConnectConnectionAdapter} that also supports joining an existing run
@@ -205,7 +226,7 @@ export interface ResumableConnectConnectionAdapter extends ConnectConnectionAdap
205
226
  * JSON GET (`?threadId`), so it is transport-agnostic regardless of how the
206
227
  * delivery stream is served.
207
228
  */
208
- hydrate?: (threadId: string) => Promise<ChatHydrationResult>;
229
+ hydrate?: (threadId: string, options?: ChatHydrateOptions) => Promise<ChatHydrationResult>;
209
230
  }
210
231
  export interface SubscribeConnectionAdapter {
211
232
  /**
@@ -228,7 +249,7 @@ export interface SubscribeConnectionAdapter {
228
249
  * cursor). Present only when the underlying connection supports it. The client
229
250
  * calls it on mount to re-hydrate without any app-side loader or prop.
230
251
  */
231
- hydrate?: (threadId: string) => Promise<ChatHydrationResult>;
252
+ hydrate?: (threadId: string, options?: ChatHydrateOptions) => Promise<ChatHydrationResult>;
232
253
  }
233
254
  /**
234
255
  * Connection adapter union.
@@ -394,7 +415,7 @@ export interface StreamConnectionHandlers {
394
415
  * Server-driven chat hydration for `persistence: true`: the stored
395
416
  * transcript for `threadId` plus a cursor to an in-flight run.
396
417
  */
397
- hydrate?: (threadId: string) => Promise<ChatHydrationResult>;
418
+ hydrate?: (threadId: string, options?: ChatHydrateOptions) => Promise<ChatHydrationResult>;
398
419
  /**
399
420
  * Server-driven generation hydration for `persistence: true`: the last
400
421
  * generation's resume snapshot for `threadId` plus a cursor to a run still
@@ -309,15 +309,33 @@ function errorFromXhrStatus(xhr) {
309
309
  }
310
310
  return /* @__PURE__ */ new Error(`XHR error! status: ${xhr.status} ${xhr.statusText}`);
311
311
  }
312
+ function chatHydrationSearchParams(threadId, options) {
313
+ const values = { threadId };
314
+ if (options?.limit !== void 0) values.limit = String(options.limit);
315
+ if (options?.before !== void 0) values.before = options.before;
316
+ return values;
317
+ }
318
+ function readChatHydrationPage(value) {
319
+ if (value === null || typeof value !== "object") return { truncated: false };
320
+ if (!("truncated" in value) || value.truncated !== true) return { truncated: false };
321
+ if (!("cursor" in value) || typeof value.cursor !== "string" || value.cursor === "") return { truncated: false };
322
+ return {
323
+ truncated: true,
324
+ cursor: value.cursor
325
+ };
326
+ }
312
327
  /**
313
328
  * GET the hydration endpoint for a thread and parse its JSON `{ messages,
314
329
  * activeRun }` body. This is the transport-agnostic reconnect probe: keyed on
315
330
  * the STABLE thread id, it returns the stored transcript and — if a run is still
316
331
  * generating — a cursor the caller tails via `joinRun`. Shared by every fetch/
317
332
  * XHR adapter so the client never has to know which transport is in use.
333
+ *
334
+ * Pass `limit` / `before` to page. Old servers that omit `page` are treated as
335
+ * a full list (`truncated: false`).
318
336
  */
319
- async function fetchThreadHydration(fetchClient, url, headers, credentials, threadId) {
320
- const response = await fetchClient(withSearchParams(url, { threadId }), {
337
+ async function fetchThreadHydration(fetchClient, url, headers, credentials, threadId, options) {
338
+ const response = await fetchClient(withSearchParams(url, chatHydrationSearchParams(threadId, options)), {
321
339
  method: "GET",
322
340
  headers: {
323
341
  Accept: "application/json",
@@ -335,7 +353,8 @@ async function fetchThreadHydration(fetchClient, url, headers, credentials, thre
335
353
  return {
336
354
  messages: Array.isArray(data.messages) ? normalizeMessagesDates(data.messages) : [],
337
355
  activeRun,
338
- interrupts
356
+ interrupts,
357
+ page: readChatHydrationPage(data.page)
339
358
  };
340
359
  }
341
360
  /**
@@ -474,6 +493,7 @@ function normalizeConnectionAdapter(connection) {
474
493
  if (!hasConnect) throw new Error("Connection adapter must provide either connect or both subscribe and send");
475
494
  let activeBuffer = [];
476
495
  let activeWaiters = [];
496
+ let activeSubscriber;
477
497
  function push(chunk, runId) {
478
498
  if (runId) chunkRunIds.set(chunk, runId);
479
499
  const waiter = activeWaiters.shift();
@@ -481,17 +501,14 @@ function normalizeConnectionAdapter(connection) {
481
501
  else activeBuffer.push(chunk);
482
502
  }
483
503
  async function waitUntilSubscriberIdle(abortSignal) {
484
- const idle = () => activeBuffer.length === 0 && (activeWaiters.length > 0 || abortSignal?.aborted);
504
+ const idle = () => activeSubscriber !== activeWaiters || activeBuffer.length === 0 && (activeWaiters.length > 0 || abortSignal?.aborted);
485
505
  for (let i = 0; i < 16 && !abortSignal?.aborted; i++) {
486
506
  if (idle()) return;
487
507
  await Promise.resolve();
488
508
  }
489
- let macrotaskWaits = 0;
490
509
  while (!abortSignal?.aborted) {
491
510
  if (idle()) return;
492
511
  await new Promise((resolve) => setTimeout(resolve, 0));
493
- macrotaskWaits++;
494
- if (activeWaiters.length === 0 && macrotaskWaits >= 32) return;
495
512
  }
496
513
  }
497
514
  return {
@@ -501,19 +518,24 @@ function normalizeConnectionAdapter(connection) {
501
518
  activeBuffer = myBuffer;
502
519
  activeWaiters = myWaiters;
503
520
  return (async function* () {
504
- while (!abortSignal?.aborted) {
505
- let chunk;
506
- const buffered = myBuffer.shift();
507
- if (buffered !== void 0) chunk = buffered;
508
- else chunk = await new Promise((resolve) => {
509
- const onAbort = () => resolve(null);
510
- myWaiters.push((c) => {
511
- abortSignal?.removeEventListener("abort", onAbort);
512
- resolve(c);
521
+ activeSubscriber = myWaiters;
522
+ try {
523
+ while (!abortSignal?.aborted) {
524
+ let chunk;
525
+ const buffered = myBuffer.shift();
526
+ if (buffered !== void 0) chunk = buffered;
527
+ else chunk = await new Promise((resolve) => {
528
+ const onAbort = () => resolve(null);
529
+ myWaiters.push((c) => {
530
+ abortSignal?.removeEventListener("abort", onAbort);
531
+ resolve(c);
532
+ });
533
+ abortSignal?.addEventListener("abort", onAbort, { once: true });
513
534
  });
514
- abortSignal?.addEventListener("abort", onAbort, { once: true });
515
- });
516
- if (chunk !== null) yield chunk;
535
+ if (chunk !== null) yield chunk;
536
+ }
537
+ } finally {
538
+ if (activeSubscriber === myWaiters) activeSubscriber = void 0;
517
539
  }
518
540
  })();
519
541
  },
@@ -556,7 +578,7 @@ function normalizeConnectionAdapter(connection) {
556
578
  ...typeof connection.joinRun === "function" ? { joinRun: (runId, abortSignal) => connection.joinRun(runId, abortSignal) } : {},
557
579
  ...(() => {
558
580
  const hydrate = connection.hydrate;
559
- return typeof hydrate === "function" ? { hydrate: (threadId) => hydrate(threadId) } : {};
581
+ return typeof hydrate === "function" ? { hydrate: (threadId, options) => hydrate(threadId, options) } : {};
560
582
  })()
561
583
  };
562
584
  }
@@ -654,10 +676,10 @@ function fetchServerSentEvents(url, options = {}) {
654
676
  credentials: resolvedOptions.credentials || "same-origin"
655
677
  }, (response, sseSignal) => responseToSSEEvents(response, sseSignal, { runId })), signal, resolvedOptions.reconnect);
656
678
  },
657
- async hydrate(threadId) {
679
+ async hydrate(threadId, hydrateOptions) {
658
680
  const resolvedUrl = typeof url === "function" ? url() : url;
659
681
  const resolvedOptions = typeof options === "function" ? await options() : options;
660
- return fetchThreadHydration(resolvedOptions.fetchClient ?? fetch, resolvedUrl, mergeHeaders(resolvedOptions.headers), resolvedOptions.credentials || "same-origin", threadId);
682
+ return fetchThreadHydration(resolvedOptions.fetchClient ?? fetch, resolvedUrl, mergeHeaders(resolvedOptions.headers), resolvedOptions.credentials || "same-origin", threadId, hydrateOptions);
661
683
  },
662
684
  async hydrateGeneration(threadId) {
663
685
  const resolvedUrl = typeof url === "function" ? url() : url;
@@ -737,10 +759,10 @@ function fetchHttpStream(url, options = {}) {
737
759
  credentials: resolvedOptions.credentials || "same-origin"
738
760
  }, responseToNdjsonEvents), signal, resolvedOptions.reconnect);
739
761
  },
740
- async hydrate(threadId) {
762
+ async hydrate(threadId, hydrateOptions) {
741
763
  const resolvedUrl = typeof url === "function" ? url() : url;
742
764
  const resolvedOptions = typeof options === "function" ? await options() : options;
743
- return fetchThreadHydration(resolvedOptions.fetchClient ?? fetch, resolvedUrl, mergeHeaders(resolvedOptions.headers), resolvedOptions.credentials || "same-origin", threadId);
765
+ return fetchThreadHydration(resolvedOptions.fetchClient ?? fetch, resolvedUrl, mergeHeaders(resolvedOptions.headers), resolvedOptions.credentials || "same-origin", threadId, hydrateOptions);
744
766
  },
745
767
  async hydrateGeneration(threadId) {
746
768
  const resolvedUrl = typeof url === "function" ? url() : url;
@@ -918,10 +940,10 @@ function xhrServerSentEvents(url, options = {}) {
918
940
  runId
919
941
  }), resolvedOptions, "GET", [], void 0, void 0, (lines) => linesToSSEEvents(lines, { runId })), signal, resolvedOptions.reconnect);
920
942
  },
921
- async hydrate(threadId) {
943
+ async hydrate(threadId, hydrateOptions) {
922
944
  const resolvedUrl = typeof url === "function" ? url() : url;
923
945
  const resolvedOptions = await resolveXhrConnectionOptions(options);
924
- return fetchThreadHydration(fetch, resolvedUrl, mergeHeaders(resolvedOptions.headers), resolvedOptions.withCredentials ? "include" : "same-origin", threadId);
946
+ return fetchThreadHydration(fetch, resolvedUrl, mergeHeaders(resolvedOptions.headers), resolvedOptions.withCredentials ? "include" : "same-origin", threadId, hydrateOptions);
925
947
  },
926
948
  async hydrateGeneration(threadId) {
927
949
  const resolvedUrl = typeof url === "function" ? url() : url;
@@ -955,10 +977,10 @@ function xhrHttpStream(url, options = {}) {
955
977
  runId
956
978
  }), resolvedOptions, "GET", [], void 0, void 0, linesToNdjsonEvents), signal, resolvedOptions.reconnect);
957
979
  },
958
- async hydrate(threadId) {
980
+ async hydrate(threadId, hydrateOptions) {
959
981
  const resolvedUrl = typeof url === "function" ? url() : url;
960
982
  const resolvedOptions = await resolveXhrConnectionOptions(options);
961
- return fetchThreadHydration(fetch, resolvedUrl, mergeHeaders(resolvedOptions.headers), resolvedOptions.withCredentials ? "include" : "same-origin", threadId);
983
+ return fetchThreadHydration(fetch, resolvedUrl, mergeHeaders(resolvedOptions.headers), resolvedOptions.withCredentials ? "include" : "same-origin", threadId, hydrateOptions);
962
984
  },
963
985
  async hydrateGeneration(threadId) {
964
986
  const resolvedUrl = typeof url === "function" ? url() : url;