@tanstack/ai-client 0.31.1 → 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.
@@ -531,12 +531,48 @@ function errorFromXhrStatus(xhr: XMLHttpRequest): Error {
531
531
  return new Error(`XHR error! status: ${xhr.status} ${xhr.statusText}`)
532
532
  }
533
533
 
534
+ function chatHydrationSearchParams(
535
+ threadId: string,
536
+ options?: ChatHydrateOptions,
537
+ ): Record<string, string> {
538
+ const values: Record<string, string> = { threadId }
539
+ if (options?.limit !== undefined) {
540
+ values.limit = String(options.limit)
541
+ }
542
+ if (options?.before !== undefined) {
543
+ values.before = options.before
544
+ }
545
+ return values
546
+ }
547
+
548
+ function readChatHydrationPage(
549
+ value: unknown,
550
+ ): { truncated: false } | { truncated: true; cursor: string } {
551
+ if (value === null || typeof value !== 'object') {
552
+ return { truncated: false }
553
+ }
554
+ if (!('truncated' in value) || value.truncated !== true) {
555
+ return { truncated: false }
556
+ }
557
+ if (
558
+ !('cursor' in value) ||
559
+ typeof value.cursor !== 'string' ||
560
+ value.cursor === ''
561
+ ) {
562
+ return { truncated: false }
563
+ }
564
+ return { truncated: true, cursor: value.cursor }
565
+ }
566
+
534
567
  /**
535
568
  * GET the hydration endpoint for a thread and parse its JSON `{ messages,
536
569
  * activeRun }` body. This is the transport-agnostic reconnect probe: keyed on
537
570
  * the STABLE thread id, it returns the stored transcript and — if a run is still
538
571
  * generating — a cursor the caller tails via `joinRun`. Shared by every fetch/
539
572
  * XHR adapter so the client never has to know which transport is in use.
573
+ *
574
+ * Pass `limit` / `before` to page. Old servers that omit `page` are treated as
575
+ * a full list (`truncated: false`).
540
576
  */
541
577
  async function fetchThreadHydration(
542
578
  fetchClient: typeof globalThis.fetch,
@@ -544,12 +580,16 @@ async function fetchThreadHydration(
544
580
  headers: Record<string, string>,
545
581
  credentials: RequestCredentials,
546
582
  threadId: string,
583
+ options?: ChatHydrateOptions,
547
584
  ): Promise<ChatHydrationResult> {
548
- const response = await fetchClient(withSearchParams(url, { threadId }), {
549
- method: 'GET',
550
- headers: { Accept: 'application/json', ...headers },
551
- credentials,
552
- })
585
+ const response = await fetchClient(
586
+ withSearchParams(url, chatHydrationSearchParams(threadId, options)),
587
+ {
588
+ method: 'GET',
589
+ headers: { Accept: 'application/json', ...headers },
590
+ credentials,
591
+ },
592
+ )
553
593
  await assertResponseOk(response)
554
594
  const data = (await response.json()) as {
555
595
  messages?: Array<UIMessage>
@@ -558,6 +598,7 @@ async function fetchThreadHydration(
558
598
  runId?: unknown
559
599
  pending?: unknown
560
600
  } | null
601
+ page?: unknown
561
602
  }
562
603
  const activeRun =
563
604
  data.activeRun && typeof data.activeRun.runId === 'string'
@@ -579,6 +620,7 @@ async function fetchThreadHydration(
579
620
  : [],
580
621
  activeRun,
581
622
  interrupts,
623
+ page: readChatHydrationPage(data.page),
582
624
  }
583
625
  }
584
626
 
@@ -881,7 +923,10 @@ export interface ConnectConnectionAdapter {
881
923
  * `fetchServerSentEvents` / `fetchHttpStream`, and on `stream()` /
882
924
  * `rpcStream()` when supplied via {@link StreamConnectionHandlers}.
883
925
  */
884
- hydrate?: (threadId: string) => Promise<ChatHydrationResult>
926
+ hydrate?: (
927
+ threadId: string,
928
+ options?: ChatHydrateOptions,
929
+ ) => Promise<ChatHydrationResult>
885
930
  }
886
931
 
887
932
  /**
@@ -911,11 +956,23 @@ export interface GenerationHydrationResult {
911
956
  activeRun: { runId: string } | null
912
957
  }
913
958
 
959
+ /**
960
+ * Optional paging for a chat hydrate GET.
961
+ * Omit both fields to load the full thread.
962
+ */
963
+ export interface ChatHydrateOptions {
964
+ limit?: number
965
+ before?: string
966
+ }
967
+
914
968
  /**
915
969
  * Server-resolved hydration for a thread. `messages` is the stored transcript;
916
970
  * `activeRun` is a cursor to a run still generating for the thread (or `null`).
917
971
  * Keyed on the STABLE thread id — the client never handles a run id, so a turn
918
972
  * that spans several runs (interrupt/tool continuations) reconnects correctly.
973
+ *
974
+ * `page` is additive. Old servers omit it; treat that as a full list
975
+ * (`truncated: false`).
919
976
  */
920
977
  export interface ChatHydrationResult {
921
978
  messages: Array<UIMessage>
@@ -929,6 +986,11 @@ export interface ChatHydrationResult {
929
986
  runId: string
930
987
  pending: Array<ChatPendingInterrupt>
931
988
  } | null
989
+ /**
990
+ * Whether this window is a slice of a longer thread.
991
+ * `cursor` is set when `truncated` is true and is sent back as `before`.
992
+ */
993
+ page?: { truncated: false } | { truncated: true; cursor: string }
932
994
  }
933
995
 
934
996
  /**
@@ -952,7 +1014,10 @@ export interface ResumableConnectConnectionAdapter extends ConnectConnectionAdap
952
1014
  * JSON GET (`?threadId`), so it is transport-agnostic regardless of how the
953
1015
  * delivery stream is served.
954
1016
  */
955
- hydrate?: (threadId: string) => Promise<ChatHydrationResult>
1017
+ hydrate?: (
1018
+ threadId: string,
1019
+ options?: ChatHydrateOptions,
1020
+ ) => Promise<ChatHydrationResult>
956
1021
  }
957
1022
 
958
1023
  export interface SubscribeConnectionAdapter {
@@ -984,7 +1049,10 @@ export interface SubscribeConnectionAdapter {
984
1049
  * cursor). Present only when the underlying connection supports it. The client
985
1050
  * calls it on mount to re-hydrate without any app-side loader or prop.
986
1051
  */
987
- hydrate?: (threadId: string) => Promise<ChatHydrationResult>
1052
+ hydrate?: (
1053
+ threadId: string,
1054
+ options?: ChatHydrateOptions,
1055
+ ) => Promise<ChatHydrationResult>
988
1056
  }
989
1057
 
990
1058
  /**
@@ -1209,7 +1277,10 @@ export function normalizeConnectionAdapter(
1209
1277
  // (no non-null assertion). Present only when the connection supports it.
1210
1278
  const hydrate = (connection as ResumableConnectConnectionAdapter).hydrate
1211
1279
  return typeof hydrate === 'function'
1212
- ? { hydrate: (threadId: string) => hydrate(threadId) }
1280
+ ? {
1281
+ hydrate: (threadId: string, options?: ChatHydrateOptions) =>
1282
+ hydrate(threadId, options),
1283
+ }
1213
1284
  : {}
1214
1285
  })(),
1215
1286
  }
@@ -1423,7 +1494,7 @@ export function fetchServerSentEvents(
1423
1494
  resolvedOptions.reconnect,
1424
1495
  )
1425
1496
  },
1426
- async hydrate(threadId) {
1497
+ async hydrate(threadId, hydrateOptions) {
1427
1498
  const resolvedUrl = typeof url === 'function' ? url() : url
1428
1499
  const resolvedOptions =
1429
1500
  typeof options === 'function' ? await options() : options
@@ -1433,6 +1504,7 @@ export function fetchServerSentEvents(
1433
1504
  mergeHeaders(resolvedOptions.headers),
1434
1505
  resolvedOptions.credentials || 'same-origin',
1435
1506
  threadId,
1507
+ hydrateOptions,
1436
1508
  )
1437
1509
  },
1438
1510
  async hydrateGeneration(threadId) {
@@ -1579,7 +1651,7 @@ export function fetchHttpStream(
1579
1651
  resolvedOptions.reconnect,
1580
1652
  )
1581
1653
  },
1582
- async hydrate(threadId) {
1654
+ async hydrate(threadId, hydrateOptions) {
1583
1655
  const resolvedUrl = typeof url === 'function' ? url() : url
1584
1656
  const resolvedOptions =
1585
1657
  typeof options === 'function' ? await options() : options
@@ -1589,6 +1661,7 @@ export function fetchHttpStream(
1589
1661
  mergeHeaders(resolvedOptions.headers),
1590
1662
  resolvedOptions.credentials || 'same-origin',
1591
1663
  threadId,
1664
+ hydrateOptions,
1592
1665
  )
1593
1666
  },
1594
1667
  async hydrateGeneration(threadId) {
@@ -1917,7 +1990,7 @@ export function xhrServerSentEvents(
1917
1990
  resolvedOptions.reconnect,
1918
1991
  )
1919
1992
  },
1920
- async hydrate(threadId) {
1993
+ async hydrate(threadId, hydrateOptions) {
1921
1994
  const resolvedUrl = typeof url === 'function' ? url() : url
1922
1995
  const resolvedOptions = await resolveXhrConnectionOptions(options)
1923
1996
  // Hydration is a non-streaming JSON GET, so fetch is fine even for the
@@ -1928,6 +2001,7 @@ export function xhrServerSentEvents(
1928
2001
  mergeHeaders(resolvedOptions.headers),
1929
2002
  resolvedOptions.withCredentials ? 'include' : 'same-origin',
1930
2003
  threadId,
2004
+ hydrateOptions,
1931
2005
  )
1932
2006
  },
1933
2007
  async hydrateGeneration(threadId) {
@@ -2000,7 +2074,7 @@ export function xhrHttpStream(
2000
2074
  resolvedOptions.reconnect,
2001
2075
  )
2002
2076
  },
2003
- async hydrate(threadId) {
2077
+ async hydrate(threadId, hydrateOptions) {
2004
2078
  const resolvedUrl = typeof url === 'function' ? url() : url
2005
2079
  const resolvedOptions = await resolveXhrConnectionOptions(options)
2006
2080
  // Hydration is a non-streaming JSON GET, so fetch is fine even for the
@@ -2011,6 +2085,7 @@ export function xhrHttpStream(
2011
2085
  mergeHeaders(resolvedOptions.headers),
2012
2086
  resolvedOptions.withCredentials ? 'include' : 'same-origin',
2013
2087
  threadId,
2088
+ hydrateOptions,
2014
2089
  )
2015
2090
  },
2016
2091
  async hydrateGeneration(threadId) {
@@ -2451,7 +2526,10 @@ export interface StreamConnectionHandlers {
2451
2526
  * Server-driven chat hydration for `persistence: true`: the stored
2452
2527
  * transcript for `threadId` plus a cursor to an in-flight run.
2453
2528
  */
2454
- hydrate?: (threadId: string) => Promise<ChatHydrationResult>
2529
+ hydrate?: (
2530
+ threadId: string,
2531
+ options?: ChatHydrateOptions,
2532
+ ) => Promise<ChatHydrationResult>
2455
2533
  /**
2456
2534
  * Server-driven generation hydration for `persistence: true`: the last
2457
2535
  * generation's resume snapshot for `threadId` plus a cursor to a run still
package/src/types.ts CHANGED
@@ -751,14 +751,23 @@ export type ChatPersistenceOptions<
751
751
  | {
752
752
  persistence: true
753
753
  threadId: string
754
+ /**
755
+ * Newest-window size for server hydrate. Only with `persistence: true`.
756
+ * Without this, hydrate still loads the full thread.
757
+ */
758
+ history?: {
759
+ pageSize: number
760
+ }
754
761
  }
755
762
  | {
756
763
  persistence: ChatClientPersistence<TTools>
757
764
  threadId: string
765
+ history?: never
758
766
  }
759
767
  | {
760
768
  persistence?: false | undefined
761
769
  threadId?: string
770
+ history?: never
762
771
  }
763
772
 
764
773
  type IsUnknown<T> = unknown extends T