@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.
- package/README.md +14 -13
- package/dist/esm/byok/passkey.js +16 -0
- package/dist/esm/byok/passkey.js.map +1 -1
- package/dist/esm/chat-client.d.ts +27 -0
- package/dist/esm/chat-client.js +100 -4
- package/dist/esm/chat-client.js.map +1 -1
- package/dist/esm/connection-adapters.d.ts +25 -4
- package/dist/esm/connection-adapters.js +50 -28
- package/dist/esm/connection-adapters.js.map +1 -1
- package/dist/esm/types.d.ts +9 -0
- package/dist/esm/types.js.map +1 -1
- package/package.json +3 -3
- package/src/byok/passkey.ts +38 -0
- package/src/chat-client.ts +130 -3
- package/src/connection-adapters.ts +115 -33
- package/src/types.ts +9 -0
|
@@ -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(
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
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?: (
|
|
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?: (
|
|
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?: (
|
|
1052
|
+
hydrate?: (
|
|
1053
|
+
threadId: string,
|
|
1054
|
+
options?: ChatHydrateOptions,
|
|
1055
|
+
) => Promise<ChatHydrationResult>
|
|
988
1056
|
}
|
|
989
1057
|
|
|
990
1058
|
/**
|
|
@@ -1042,6 +1110,7 @@ export function normalizeConnectionAdapter(
|
|
|
1042
1110
|
// Legacy connect() wrapper
|
|
1043
1111
|
let activeBuffer: Array<StreamChunk> = []
|
|
1044
1112
|
let activeWaiters: Array<(chunk: StreamChunk | null) => void> = []
|
|
1113
|
+
let activeSubscriber: typeof activeWaiters | undefined
|
|
1045
1114
|
|
|
1046
1115
|
function push(chunk: StreamChunk, runId?: string): void {
|
|
1047
1116
|
if (runId) {
|
|
@@ -1062,18 +1131,16 @@ export function normalizeConnectionAdapter(
|
|
|
1062
1131
|
// previous chunk has left processIncomingChunk. Empty waiters with an
|
|
1063
1132
|
// empty buffer is in-flight delivery, not idle.
|
|
1064
1133
|
const idle = () =>
|
|
1065
|
-
|
|
1066
|
-
(
|
|
1134
|
+
activeSubscriber !== activeWaiters ||
|
|
1135
|
+
(activeBuffer.length === 0 &&
|
|
1136
|
+
(activeWaiters.length > 0 || abortSignal?.aborted))
|
|
1067
1137
|
for (let i = 0; i < 16 && !abortSignal?.aborted; i++) {
|
|
1068
1138
|
if (idle()) return
|
|
1069
1139
|
await Promise.resolve()
|
|
1070
1140
|
}
|
|
1071
|
-
let macrotaskWaits = 0
|
|
1072
1141
|
while (!abortSignal?.aborted) {
|
|
1073
1142
|
if (idle()) return
|
|
1074
1143
|
await new Promise<void>((resolve) => setTimeout(resolve, 0))
|
|
1075
|
-
macrotaskWaits++
|
|
1076
|
-
if (activeWaiters.length === 0 && macrotaskWaits >= 32) return
|
|
1077
1144
|
}
|
|
1078
1145
|
}
|
|
1079
1146
|
|
|
@@ -1087,22 +1154,27 @@ export function normalizeConnectionAdapter(
|
|
|
1087
1154
|
activeWaiters = myWaiters
|
|
1088
1155
|
|
|
1089
1156
|
return (async function* () {
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1157
|
+
activeSubscriber = myWaiters
|
|
1158
|
+
try {
|
|
1159
|
+
while (!abortSignal?.aborted) {
|
|
1160
|
+
let chunk: StreamChunk | null
|
|
1161
|
+
const buffered = myBuffer.shift()
|
|
1162
|
+
if (buffered !== undefined) {
|
|
1163
|
+
chunk = buffered
|
|
1164
|
+
} else {
|
|
1165
|
+
chunk = await new Promise<StreamChunk | null>((resolve) => {
|
|
1166
|
+
const onAbort = () => resolve(null)
|
|
1167
|
+
myWaiters.push((c) => {
|
|
1168
|
+
abortSignal?.removeEventListener('abort', onAbort)
|
|
1169
|
+
resolve(c)
|
|
1170
|
+
})
|
|
1171
|
+
abortSignal?.addEventListener('abort', onAbort, { once: true })
|
|
1101
1172
|
})
|
|
1102
|
-
|
|
1103
|
-
|
|
1173
|
+
}
|
|
1174
|
+
if (chunk !== null) yield chunk
|
|
1104
1175
|
}
|
|
1105
|
-
|
|
1176
|
+
} finally {
|
|
1177
|
+
if (activeSubscriber === myWaiters) activeSubscriber = undefined
|
|
1106
1178
|
}
|
|
1107
1179
|
})()
|
|
1108
1180
|
},
|
|
@@ -1205,7 +1277,10 @@ export function normalizeConnectionAdapter(
|
|
|
1205
1277
|
// (no non-null assertion). Present only when the connection supports it.
|
|
1206
1278
|
const hydrate = (connection as ResumableConnectConnectionAdapter).hydrate
|
|
1207
1279
|
return typeof hydrate === 'function'
|
|
1208
|
-
? {
|
|
1280
|
+
? {
|
|
1281
|
+
hydrate: (threadId: string, options?: ChatHydrateOptions) =>
|
|
1282
|
+
hydrate(threadId, options),
|
|
1283
|
+
}
|
|
1209
1284
|
: {}
|
|
1210
1285
|
})(),
|
|
1211
1286
|
}
|
|
@@ -1419,7 +1494,7 @@ export function fetchServerSentEvents(
|
|
|
1419
1494
|
resolvedOptions.reconnect,
|
|
1420
1495
|
)
|
|
1421
1496
|
},
|
|
1422
|
-
async hydrate(threadId) {
|
|
1497
|
+
async hydrate(threadId, hydrateOptions) {
|
|
1423
1498
|
const resolvedUrl = typeof url === 'function' ? url() : url
|
|
1424
1499
|
const resolvedOptions =
|
|
1425
1500
|
typeof options === 'function' ? await options() : options
|
|
@@ -1429,6 +1504,7 @@ export function fetchServerSentEvents(
|
|
|
1429
1504
|
mergeHeaders(resolvedOptions.headers),
|
|
1430
1505
|
resolvedOptions.credentials || 'same-origin',
|
|
1431
1506
|
threadId,
|
|
1507
|
+
hydrateOptions,
|
|
1432
1508
|
)
|
|
1433
1509
|
},
|
|
1434
1510
|
async hydrateGeneration(threadId) {
|
|
@@ -1575,7 +1651,7 @@ export function fetchHttpStream(
|
|
|
1575
1651
|
resolvedOptions.reconnect,
|
|
1576
1652
|
)
|
|
1577
1653
|
},
|
|
1578
|
-
async hydrate(threadId) {
|
|
1654
|
+
async hydrate(threadId, hydrateOptions) {
|
|
1579
1655
|
const resolvedUrl = typeof url === 'function' ? url() : url
|
|
1580
1656
|
const resolvedOptions =
|
|
1581
1657
|
typeof options === 'function' ? await options() : options
|
|
@@ -1585,6 +1661,7 @@ export function fetchHttpStream(
|
|
|
1585
1661
|
mergeHeaders(resolvedOptions.headers),
|
|
1586
1662
|
resolvedOptions.credentials || 'same-origin',
|
|
1587
1663
|
threadId,
|
|
1664
|
+
hydrateOptions,
|
|
1588
1665
|
)
|
|
1589
1666
|
},
|
|
1590
1667
|
async hydrateGeneration(threadId) {
|
|
@@ -1913,7 +1990,7 @@ export function xhrServerSentEvents(
|
|
|
1913
1990
|
resolvedOptions.reconnect,
|
|
1914
1991
|
)
|
|
1915
1992
|
},
|
|
1916
|
-
async hydrate(threadId) {
|
|
1993
|
+
async hydrate(threadId, hydrateOptions) {
|
|
1917
1994
|
const resolvedUrl = typeof url === 'function' ? url() : url
|
|
1918
1995
|
const resolvedOptions = await resolveXhrConnectionOptions(options)
|
|
1919
1996
|
// Hydration is a non-streaming JSON GET, so fetch is fine even for the
|
|
@@ -1924,6 +2001,7 @@ export function xhrServerSentEvents(
|
|
|
1924
2001
|
mergeHeaders(resolvedOptions.headers),
|
|
1925
2002
|
resolvedOptions.withCredentials ? 'include' : 'same-origin',
|
|
1926
2003
|
threadId,
|
|
2004
|
+
hydrateOptions,
|
|
1927
2005
|
)
|
|
1928
2006
|
},
|
|
1929
2007
|
async hydrateGeneration(threadId) {
|
|
@@ -1996,7 +2074,7 @@ export function xhrHttpStream(
|
|
|
1996
2074
|
resolvedOptions.reconnect,
|
|
1997
2075
|
)
|
|
1998
2076
|
},
|
|
1999
|
-
async hydrate(threadId) {
|
|
2077
|
+
async hydrate(threadId, hydrateOptions) {
|
|
2000
2078
|
const resolvedUrl = typeof url === 'function' ? url() : url
|
|
2001
2079
|
const resolvedOptions = await resolveXhrConnectionOptions(options)
|
|
2002
2080
|
// Hydration is a non-streaming JSON GET, so fetch is fine even for the
|
|
@@ -2007,6 +2085,7 @@ export function xhrHttpStream(
|
|
|
2007
2085
|
mergeHeaders(resolvedOptions.headers),
|
|
2008
2086
|
resolvedOptions.withCredentials ? 'include' : 'same-origin',
|
|
2009
2087
|
threadId,
|
|
2088
|
+
hydrateOptions,
|
|
2010
2089
|
)
|
|
2011
2090
|
},
|
|
2012
2091
|
async hydrateGeneration(threadId) {
|
|
@@ -2447,7 +2526,10 @@ export interface StreamConnectionHandlers {
|
|
|
2447
2526
|
* Server-driven chat hydration for `persistence: true`: the stored
|
|
2448
2527
|
* transcript for `threadId` plus a cursor to an in-flight run.
|
|
2449
2528
|
*/
|
|
2450
|
-
hydrate?: (
|
|
2529
|
+
hydrate?: (
|
|
2530
|
+
threadId: string,
|
|
2531
|
+
options?: ChatHydrateOptions,
|
|
2532
|
+
) => Promise<ChatHydrationResult>
|
|
2451
2533
|
/**
|
|
2452
2534
|
* Server-driven generation hydration for `persistence: true`: the last
|
|
2453
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
|