@tanstack/ai-client 0.28.0 → 0.29.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/dist/esm/chat-client.d.ts +3 -0
- package/dist/esm/chat-client.js +33 -11
- package/dist/esm/chat-client.js.map +1 -1
- package/dist/esm/connection-adapters.js +2 -1
- package/dist/esm/connection-adapters.js.map +1 -1
- package/dist/esm/message-date-normalizer.d.ts +3 -0
- package/dist/esm/message-date-normalizer.js +35 -0
- package/dist/esm/message-date-normalizer.js.map +1 -0
- package/dist/esm/storage-adapters.js +13 -1
- package/dist/esm/storage-adapters.js.map +1 -1
- package/dist/esm/types.d.ts +5 -0
- package/dist/esm/types.js.map +1 -1
- package/package.json +3 -3
- package/src/chat-client.ts +70 -7
- package/src/connection-adapters.ts +5 -2
- package/src/message-date-normalizer.ts +29 -0
- package/src/storage-adapters.ts +13 -2
- package/src/types.ts +5 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
//#region src/message-date-normalizer.ts
|
|
2
|
+
function validDate(value) {
|
|
3
|
+
if (value instanceof Date) return Number.isNaN(value.getTime()) ? void 0 : value;
|
|
4
|
+
if (typeof value !== "string") return void 0;
|
|
5
|
+
const date = new Date(value);
|
|
6
|
+
return Number.isNaN(date.getTime()) ? void 0 : date;
|
|
7
|
+
}
|
|
8
|
+
function normalizeMessageDates(message) {
|
|
9
|
+
const messageDate = validDate(message.createdAt);
|
|
10
|
+
const parts = message.parts.map((part) => {
|
|
11
|
+
if (part.type !== "tool-result") return part;
|
|
12
|
+
const date = validDate(part.createdAt);
|
|
13
|
+
const { createdAt: _ignored, ...rest } = part;
|
|
14
|
+
return date ? {
|
|
15
|
+
...rest,
|
|
16
|
+
createdAt: date
|
|
17
|
+
} : rest;
|
|
18
|
+
});
|
|
19
|
+
const { createdAt: _ignored, ...rest } = message;
|
|
20
|
+
return messageDate ? {
|
|
21
|
+
...rest,
|
|
22
|
+
parts,
|
|
23
|
+
createdAt: messageDate
|
|
24
|
+
} : {
|
|
25
|
+
...rest,
|
|
26
|
+
parts
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function normalizeMessagesDates(messages) {
|
|
30
|
+
return messages.map(normalizeMessageDates);
|
|
31
|
+
}
|
|
32
|
+
//#endregion
|
|
33
|
+
export { normalizeMessageDates, normalizeMessagesDates };
|
|
34
|
+
|
|
35
|
+
//# sourceMappingURL=message-date-normalizer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"message-date-normalizer.js","names":[],"sources":["../../src/message-date-normalizer.ts"],"sourcesContent":["import type { UIMessage } from './types'\n\nfunction validDate(value: unknown): Date | undefined {\n if (value instanceof Date)\n return Number.isNaN(value.getTime()) ? undefined : value\n if (typeof value !== 'string') return undefined\n const date = new Date(value)\n return Number.isNaN(date.getTime()) ? undefined : date\n}\n\nexport function normalizeMessageDates(message: UIMessage): UIMessage {\n const messageDate = validDate(message.createdAt)\n const parts = message.parts.map((part) => {\n if (part.type !== 'tool-result') return part\n const date = validDate(part.createdAt)\n const { createdAt: _ignored, ...rest } = part\n return date ? { ...rest, createdAt: date } : rest\n })\n const { createdAt: _ignored, ...rest } = message\n return messageDate\n ? { ...rest, parts, createdAt: messageDate }\n : { ...rest, parts }\n}\n\nexport function normalizeMessagesDates(\n messages: Array<UIMessage>,\n): Array<UIMessage> {\n return messages.map(normalizeMessageDates)\n}\n"],"mappings":";AAEA,SAAS,UAAU,OAAkC;CACnD,IAAI,iBAAiB,MACnB,OAAO,OAAO,MAAM,MAAM,QAAQ,CAAC,IAAI,KAAA,IAAY;CACrD,IAAI,OAAO,UAAU,UAAU,OAAO,KAAA;CACtC,MAAM,OAAO,IAAI,KAAK,KAAK;CAC3B,OAAO,OAAO,MAAM,KAAK,QAAQ,CAAC,IAAI,KAAA,IAAY;AACpD;AAEA,SAAgB,sBAAsB,SAA+B;CACnE,MAAM,cAAc,UAAU,QAAQ,SAAS;CAC/C,MAAM,QAAQ,QAAQ,MAAM,KAAK,SAAS;EACxC,IAAI,KAAK,SAAS,eAAe,OAAO;EACxC,MAAM,OAAO,UAAU,KAAK,SAAS;EACrC,MAAM,EAAE,WAAW,UAAU,GAAG,SAAS;EACzC,OAAO,OAAO;GAAE,GAAG;GAAM,WAAW;EAAK,IAAI;CAC/C,CAAC;CACD,MAAM,EAAE,WAAW,UAAU,GAAG,SAAS;CACzC,OAAO,cACH;EAAE,GAAG;EAAM;EAAO,WAAW;CAAY,IACzC;EAAE,GAAG;EAAM;CAAM;AACvB;AAEA,SAAgB,uBACd,UACkB;CAClB,OAAO,SAAS,IAAI,qBAAqB;AAC3C"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { normalizeMessagesDates } from "./message-date-normalizer.js";
|
|
1
2
|
//#region src/storage-adapters.ts
|
|
2
3
|
/**
|
|
3
4
|
* Thrown by a storage adapter when its backing store is absent — most commonly
|
|
@@ -20,6 +21,17 @@ function stringifyJson(value) {
|
|
|
20
21
|
if (typeof serialized !== "string") throw new TypeError("The value is not JSON serializable.");
|
|
21
22
|
return serialized;
|
|
22
23
|
}
|
|
24
|
+
function reviveMessageCreatedAt(message) {
|
|
25
|
+
return normalizeMessagesDates([message]).at(0) ?? message;
|
|
26
|
+
}
|
|
27
|
+
function revivePersistedState(state) {
|
|
28
|
+
if (state == null || typeof state !== "object") return state;
|
|
29
|
+
if (!Array.isArray(state.messages)) return state;
|
|
30
|
+
return {
|
|
31
|
+
...state,
|
|
32
|
+
messages: state.messages.map(reviveMessageCreatedAt)
|
|
33
|
+
};
|
|
34
|
+
}
|
|
23
35
|
function createWebStoragePersistence(storageName, options) {
|
|
24
36
|
const keyPrefix = options.keyPrefix ?? "tanstack-ai:";
|
|
25
37
|
const serialize = options.serialize ?? stringifyJson;
|
|
@@ -33,7 +45,7 @@ function createWebStoragePersistence(storageName, options) {
|
|
|
33
45
|
return {
|
|
34
46
|
getItem(id) {
|
|
35
47
|
const item = getStorage().getItem(key(id));
|
|
36
|
-
return item === null ? null : deserialize(item);
|
|
48
|
+
return item === null ? null : revivePersistedState(deserialize(item));
|
|
37
49
|
},
|
|
38
50
|
setItem(id, value) {
|
|
39
51
|
getStorage().setItem(key(id), serialize(value));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"storage-adapters.js","names":[],"sources":["../../src/storage-adapters.ts"],"sourcesContent":["import type { ChatPersistedState, ChatStorageAdapter } from './types'\n\nexport interface WebStoragePersistenceOptions {\n keyPrefix?: string\n /**\n * Defaults to `JSON.stringify`. Override only for values JSON can't\n * round-trip losslessly (a `Map`, a `bigint`, a `Date` you need back as a\n * `Date` rather than an ISO string).\n */\n serialize?: (value: ChatPersistedState) => string\n /** Defaults to `JSON.parse`. */\n deserialize?: (value: string) => ChatPersistedState\n}\n\nexport interface IndexedDBPersistenceOptions {\n databaseName?: string\n objectStoreName?: string\n keyPrefix?: string\n}\n\ntype StorageName = 'localStorage' | 'sessionStorage' | 'indexedDB'\n\n/**\n * Thrown by a storage adapter when its backing store is absent — most commonly\n * during server-side rendering, where `localStorage` / `sessionStorage` /\n * `indexedDB` do not exist on `globalThis`. The adapters check availability\n * lazily, **per operation**, so constructing an adapter never throws; the error\n * surfaces from `getItem` / `setItem` / `removeItem` (rejected promise for\n * IndexedDB). The chat persistence layer treats adapter failures as best-effort\n * (storage errors never break chat setup or streaming).\n */\nexport class StorageUnavailableError extends Error {\n constructor(storageName: StorageName) {\n super(`${storageName} is not available in this environment.`)\n this.name = 'StorageUnavailableError'\n }\n}\n\nfunction stringifyJson(value: ChatPersistedState): string {\n const stringify: (input: unknown) => unknown = JSON.stringify\n const serialized = stringify(value)\n if (typeof serialized !== 'string') {\n throw new TypeError('The value is not JSON serializable.')\n }\n return serialized\n}\n\nfunction createWebStoragePersistence(\n storageName: 'localStorage' | 'sessionStorage',\n options: WebStoragePersistenceOptions,\n): ChatStorageAdapter<ChatPersistedState> {\n const keyPrefix = options.keyPrefix ?? 'tanstack-ai:'\n const serialize = options.serialize ?? stringifyJson\n const deserialize = options.deserialize ?? JSON.parse\n const key = (id: string) => `${keyPrefix}${id}`\n\n const getStorage = (): Storage => {\n const browserGlobals: {\n localStorage?: Storage\n sessionStorage?: Storage\n } = globalThis\n const storage = browserGlobals[storageName]\n if (!storage) {\n throw new StorageUnavailableError(storageName)\n }\n return storage\n }\n\n return {\n getItem(id) {\n const item = getStorage().getItem(key(id))\n return item === null ? null : deserialize(item)\n },\n setItem(id, value) {\n getStorage().setItem(key(id), serialize(value))\n },\n removeItem(id) {\n getStorage().removeItem(key(id))\n },\n }\n}\n\n/**\n * A `ChatStorageAdapter` backed by `window.localStorage` (persists across\n * reloads and browser restarts). Keys are namespaced with `keyPrefix`, which\n * defaults to `tanstack-ai:`. Every operation reads `localStorage` lazily and\n * throws {@link StorageUnavailableError} when it is absent (e.g. SSR), so the\n * adapter can be constructed safely on the server.\n *\n * The `serialize` / `deserialize` codec defaults to `JSON.stringify` /\n * `JSON.parse`, so the common case needs no codec.\n */\nexport function localStoragePersistence(\n options: WebStoragePersistenceOptions = {},\n): ChatStorageAdapter<ChatPersistedState> {\n return createWebStoragePersistence('localStorage', options)\n}\n\n/**\n * A `ChatStorageAdapter` backed by `window.sessionStorage` (scoped to the tab\n * and cleared when it closes). Identical to {@link localStoragePersistence} in\n * every other respect: the `tanstack-ai:` default `keyPrefix`, lazy\n * per-operation {@link StorageUnavailableError} on SSR, and a JSON codec that\n * defaults to `JSON.stringify` / `JSON.parse`.\n */\nexport function sessionStoragePersistence(\n options: WebStoragePersistenceOptions = {},\n): ChatStorageAdapter<ChatPersistedState> {\n return createWebStoragePersistence('sessionStorage', options)\n}\n\n/**\n * A `ChatStorageAdapter` backed by IndexedDB, for values too large for Web\n * Storage or that benefit from structured-clone storage. All operations are\n * async and the database opens lazily on first use; keys are namespaced with\n * `keyPrefix` (default `tanstack-ai:`). When IndexedDB is unavailable (e.g.\n * SSR) each operation rejects with {@link StorageUnavailableError}.\n *\n * No serialize/deserialize codec is needed or accepted — values are stored via\n * IndexedDB's native structured clone, so `Date`, `Map`, `ArrayBuffer`, etc.\n * round-trip without a JSON step.\n */\nexport function indexedDBPersistence(\n options: IndexedDBPersistenceOptions = {},\n): ChatStorageAdapter<ChatPersistedState> {\n const databaseName = options.databaseName ?? 'tanstack-ai'\n const objectStoreName = options.objectStoreName ?? 'persistence'\n const keyPrefix = options.keyPrefix ?? 'tanstack-ai:'\n let databasePromise: Promise<IDBDatabase> | undefined\n\n const openDatabase = (): Promise<IDBDatabase> => {\n if (databasePromise) {\n return databasePromise\n }\n\n databasePromise = new Promise<IDBDatabase>((resolve, reject) => {\n const browserGlobals: { indexedDB?: IDBFactory } = globalThis\n const factory = browserGlobals.indexedDB\n if (!factory) {\n reject(new StorageUnavailableError('indexedDB'))\n return\n }\n\n let request: IDBOpenDBRequest\n let openFailed = false\n try {\n request = factory.open(databaseName)\n } catch (error) {\n reject(error)\n return\n }\n\n request.onupgradeneeded = () => {\n if (!request.result.objectStoreNames.contains(objectStoreName)) {\n request.result.createObjectStore(objectStoreName)\n }\n }\n request.onerror = () => {\n openFailed = true\n reject(request.error ?? new Error(`Failed to open ${databaseName}.`))\n }\n request.onblocked = () => {\n openFailed = true\n reject(\n new Error(\n `Opening IndexedDB database \"${databaseName}\" was blocked.`,\n ),\n )\n }\n request.onsuccess = () => {\n const database = request.result\n if (openFailed) {\n database.close()\n return\n }\n database.onversionchange = () => {\n database.close()\n databasePromise = undefined\n }\n resolve(database)\n }\n }).catch((error: unknown) => {\n databasePromise = undefined\n throw error\n })\n\n return databasePromise\n }\n\n const runRequest = async <TResult>(\n mode: IDBTransactionMode,\n createRequest: (store: IDBObjectStore) => IDBRequest<TResult>,\n ): Promise<TResult> => {\n const database = await openDatabase()\n return new Promise<TResult>((resolve, reject) => {\n let request: IDBRequest<TResult>\n let result: TResult\n try {\n const transaction = database.transaction(objectStoreName, mode)\n request = createRequest(transaction.objectStore(objectStoreName))\n request.onsuccess = () => {\n result = request.result\n }\n request.onerror = () => {\n reject(request.error ?? new Error('IndexedDB request failed.'))\n }\n transaction.oncomplete = () => {\n resolve(result)\n }\n transaction.onerror = () => {\n reject(\n transaction.error ?? new Error('IndexedDB transaction failed.'),\n )\n }\n transaction.onabort = () => {\n reject(\n transaction.error ?? new Error('IndexedDB transaction aborted.'),\n )\n }\n } catch (error) {\n reject(error)\n }\n })\n }\n\n const key = (id: string) => `${keyPrefix}${id}`\n return {\n getItem(id) {\n return runRequest('readonly', (store) => store.get(key(id)))\n },\n setItem(id, value) {\n return runRequest('readwrite', (store) => store.put(value, key(id))).then(\n () => undefined,\n )\n },\n removeItem(id) {\n return runRequest('readwrite', (store) => store.delete(key(id))).then(\n () => undefined,\n )\n },\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"storage-adapters.js","names":[],"sources":["../../src/storage-adapters.ts"],"sourcesContent":["import type { ChatPersistedState, ChatStorageAdapter, UIMessage } from './types'\nimport { normalizeMessagesDates } from './message-date-normalizer'\n\nexport interface WebStoragePersistenceOptions {\n keyPrefix?: string\n /**\n * Defaults to `JSON.stringify`. Override only for values JSON can't\n * round-trip losslessly (a `Map`, a `bigint`, a `Date` you need back as a\n * `Date` rather than an ISO string).\n */\n serialize?: (value: ChatPersistedState) => string\n /** Defaults to `JSON.parse`. */\n deserialize?: (value: string) => ChatPersistedState\n}\n\nexport interface IndexedDBPersistenceOptions {\n databaseName?: string\n objectStoreName?: string\n keyPrefix?: string\n}\n\ntype StorageName = 'localStorage' | 'sessionStorage' | 'indexedDB'\n\n/**\n * Thrown by a storage adapter when its backing store is absent — most commonly\n * during server-side rendering, where `localStorage` / `sessionStorage` /\n * `indexedDB` do not exist on `globalThis`. The adapters check availability\n * lazily, **per operation**, so constructing an adapter never throws; the error\n * surfaces from `getItem` / `setItem` / `removeItem` (rejected promise for\n * IndexedDB). The chat persistence layer treats adapter failures as best-effort\n * (storage errors never break chat setup or streaming).\n */\nexport class StorageUnavailableError extends Error {\n constructor(storageName: StorageName) {\n super(`${storageName} is not available in this environment.`)\n this.name = 'StorageUnavailableError'\n }\n}\n\nfunction stringifyJson(value: ChatPersistedState): string {\n const stringify: (input: unknown) => unknown = JSON.stringify\n const serialized = stringify(value)\n if (typeof serialized !== 'string') {\n throw new TypeError('The value is not JSON serializable.')\n }\n return serialized\n}\n\nfunction reviveMessageCreatedAt(message: UIMessage): UIMessage {\n return normalizeMessagesDates([message]).at(0) ?? message\n}\n\nfunction revivePersistedState(state: ChatPersistedState): ChatPersistedState {\n if (state == null || typeof state !== 'object') return state\n if (!Array.isArray(state.messages)) return state\n return { ...state, messages: state.messages.map(reviveMessageCreatedAt) }\n}\n\nfunction createWebStoragePersistence(\n storageName: 'localStorage' | 'sessionStorage',\n options: WebStoragePersistenceOptions,\n): ChatStorageAdapter<ChatPersistedState> {\n const keyPrefix = options.keyPrefix ?? 'tanstack-ai:'\n const serialize = options.serialize ?? stringifyJson\n const deserialize = options.deserialize ?? JSON.parse\n const key = (id: string) => `${keyPrefix}${id}`\n\n const getStorage = (): Storage => {\n const browserGlobals: {\n localStorage?: Storage\n sessionStorage?: Storage\n } = globalThis\n const storage = browserGlobals[storageName]\n if (!storage) {\n throw new StorageUnavailableError(storageName)\n }\n return storage\n }\n\n return {\n getItem(id) {\n const item = getStorage().getItem(key(id))\n return item === null ? null : revivePersistedState(deserialize(item))\n },\n setItem(id, value) {\n getStorage().setItem(key(id), serialize(value))\n },\n removeItem(id) {\n getStorage().removeItem(key(id))\n },\n }\n}\n\n/**\n * A `ChatStorageAdapter` backed by `window.localStorage` (persists across\n * reloads and browser restarts). Keys are namespaced with `keyPrefix`, which\n * defaults to `tanstack-ai:`. Every operation reads `localStorage` lazily and\n * throws {@link StorageUnavailableError} when it is absent (e.g. SSR), so the\n * adapter can be constructed safely on the server.\n *\n * The `serialize` / `deserialize` codec defaults to `JSON.stringify` /\n * `JSON.parse`, so the common case needs no codec.\n */\nexport function localStoragePersistence(\n options: WebStoragePersistenceOptions = {},\n): ChatStorageAdapter<ChatPersistedState> {\n return createWebStoragePersistence('localStorage', options)\n}\n\n/**\n * A `ChatStorageAdapter` backed by `window.sessionStorage` (scoped to the tab\n * and cleared when it closes). Identical to {@link localStoragePersistence} in\n * every other respect: the `tanstack-ai:` default `keyPrefix`, lazy\n * per-operation {@link StorageUnavailableError} on SSR, and a JSON codec that\n * defaults to `JSON.stringify` / `JSON.parse`.\n */\nexport function sessionStoragePersistence(\n options: WebStoragePersistenceOptions = {},\n): ChatStorageAdapter<ChatPersistedState> {\n return createWebStoragePersistence('sessionStorage', options)\n}\n\n/**\n * A `ChatStorageAdapter` backed by IndexedDB, for values too large for Web\n * Storage or that benefit from structured-clone storage. All operations are\n * async and the database opens lazily on first use; keys are namespaced with\n * `keyPrefix` (default `tanstack-ai:`). When IndexedDB is unavailable (e.g.\n * SSR) each operation rejects with {@link StorageUnavailableError}.\n *\n * No serialize/deserialize codec is needed or accepted — values are stored via\n * IndexedDB's native structured clone, so `Date`, `Map`, `ArrayBuffer`, etc.\n * round-trip without a JSON step.\n */\nexport function indexedDBPersistence(\n options: IndexedDBPersistenceOptions = {},\n): ChatStorageAdapter<ChatPersistedState> {\n const databaseName = options.databaseName ?? 'tanstack-ai'\n const objectStoreName = options.objectStoreName ?? 'persistence'\n const keyPrefix = options.keyPrefix ?? 'tanstack-ai:'\n let databasePromise: Promise<IDBDatabase> | undefined\n\n const openDatabase = (): Promise<IDBDatabase> => {\n if (databasePromise) {\n return databasePromise\n }\n\n databasePromise = new Promise<IDBDatabase>((resolve, reject) => {\n const browserGlobals: { indexedDB?: IDBFactory } = globalThis\n const factory = browserGlobals.indexedDB\n if (!factory) {\n reject(new StorageUnavailableError('indexedDB'))\n return\n }\n\n let request: IDBOpenDBRequest\n let openFailed = false\n try {\n request = factory.open(databaseName)\n } catch (error) {\n reject(error)\n return\n }\n\n request.onupgradeneeded = () => {\n if (!request.result.objectStoreNames.contains(objectStoreName)) {\n request.result.createObjectStore(objectStoreName)\n }\n }\n request.onerror = () => {\n openFailed = true\n reject(request.error ?? new Error(`Failed to open ${databaseName}.`))\n }\n request.onblocked = () => {\n openFailed = true\n reject(\n new Error(\n `Opening IndexedDB database \"${databaseName}\" was blocked.`,\n ),\n )\n }\n request.onsuccess = () => {\n const database = request.result\n if (openFailed) {\n database.close()\n return\n }\n database.onversionchange = () => {\n database.close()\n databasePromise = undefined\n }\n resolve(database)\n }\n }).catch((error: unknown) => {\n databasePromise = undefined\n throw error\n })\n\n return databasePromise\n }\n\n const runRequest = async <TResult>(\n mode: IDBTransactionMode,\n createRequest: (store: IDBObjectStore) => IDBRequest<TResult>,\n ): Promise<TResult> => {\n const database = await openDatabase()\n return new Promise<TResult>((resolve, reject) => {\n let request: IDBRequest<TResult>\n let result: TResult\n try {\n const transaction = database.transaction(objectStoreName, mode)\n request = createRequest(transaction.objectStore(objectStoreName))\n request.onsuccess = () => {\n result = request.result\n }\n request.onerror = () => {\n reject(request.error ?? new Error('IndexedDB request failed.'))\n }\n transaction.oncomplete = () => {\n resolve(result)\n }\n transaction.onerror = () => {\n reject(\n transaction.error ?? new Error('IndexedDB transaction failed.'),\n )\n }\n transaction.onabort = () => {\n reject(\n transaction.error ?? new Error('IndexedDB transaction aborted.'),\n )\n }\n } catch (error) {\n reject(error)\n }\n })\n }\n\n const key = (id: string) => `${keyPrefix}${id}`\n return {\n getItem(id) {\n return runRequest('readonly', (store) => store.get(key(id)))\n },\n setItem(id, value) {\n return runRequest('readwrite', (store) => store.put(value, key(id))).then(\n () => undefined,\n )\n },\n removeItem(id) {\n return runRequest('readwrite', (store) => store.delete(key(id))).then(\n () => undefined,\n )\n },\n }\n}\n"],"mappings":"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"}
|
package/dist/esm/types.d.ts
CHANGED
|
@@ -407,10 +407,14 @@ export type ToolCallPart<TTools extends ReadonlyArray<AnyClientTool> = any> = [
|
|
|
407
407
|
] extends [never] ? UntypedToolCallPart : unknown extends TTools ? UntypedToolCallPart : TTools extends ReadonlyArray<infer Tool> ? Tool extends AnyClientTool ? ToolCallPartForTool<Tool> : UntypedToolCallPart : UntypedToolCallPart;
|
|
408
408
|
export interface ToolResultPart {
|
|
409
409
|
type: 'tool-result';
|
|
410
|
+
id?: string;
|
|
411
|
+
name?: string;
|
|
410
412
|
toolCallId: string;
|
|
411
413
|
content: string | Array<ContentPart>;
|
|
412
414
|
state: ToolResultState;
|
|
413
415
|
error?: string;
|
|
416
|
+
metadata?: Record<string, unknown>;
|
|
417
|
+
createdAt?: Date;
|
|
414
418
|
}
|
|
415
419
|
export interface ThinkingPart {
|
|
416
420
|
type: 'thinking';
|
|
@@ -432,6 +436,7 @@ export type MessagePart<TTools extends ReadonlyArray<AnyClientTool> = any, TData
|
|
|
432
436
|
export interface UIMessage<TTools extends ReadonlyArray<AnyClientTool> = any, TData = unknown> {
|
|
433
437
|
id: string;
|
|
434
438
|
role: 'system' | 'user' | 'assistant';
|
|
439
|
+
name?: string;
|
|
435
440
|
parts: Array<MessagePart<TTools, TData>>;
|
|
436
441
|
createdAt?: Date;
|
|
437
442
|
/**
|
package/dist/esm/types.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.js","names":[],"sources":["../../src/types.ts"],"sourcesContent":["import type {\n AnyClientTool,\n ApprovalCapabilityOf,\n ApprovalSchemaOf,\n AudioPart,\n BatchInterruptError,\n ChunkStrategy,\n ContentPart,\n DocumentPart,\n ImagePart,\n InferSchemaType,\n InterruptDefinition,\n InferToolInput,\n InferToolOutput,\n InputSchemaOf,\n Interrupt,\n InterruptBinding,\n ItemInterruptError,\n ModelMessage,\n NoSchema,\n RunAgentResumeItem,\n SchemaInput,\n StreamChunk,\n StructuredOutputPart,\n UIResourcePart,\n VideoPart,\n} from '@tanstack/ai/client'\nimport type { ByokClient } from './byok'\nimport type { ConnectionAdapter } from './connection-adapters'\nimport type { AIDevtoolsClientMetadata } from './devtools'\nimport type { ChatDevtoolsBridgeFactory } from './devtools-noop'\n\nexport type { StructuredOutputPart }\n\nexport interface ChatResumeState {\n threadId: string\n runId: string\n}\n\nexport type ChatPendingInterrupt = Interrupt\n\n/**\n * The durable pointer a chat keeps for the run it may need to rejoin, plus any\n * interrupt that run is waiting on.\n *\n * @internal\n */\nexport interface ChatResumeSnapshot {\n resumeState: ChatResumeState\n pendingInterrupts?: Array<ChatPendingInterrupt>\n}\n\nexport type InterruptItemStatus =\n | 'pending'\n | 'validating'\n | 'staged'\n | 'submitting'\n | 'error'\n\nexport interface BoundInterruptBase {\n readonly id: string\n readonly interruptId: string\n readonly reason: string\n readonly message?: string\n readonly responseSchema?: Readonly<Record<string, unknown>>\n readonly expiresAt?: string\n readonly metadata?: Readonly<Record<string, unknown>>\n readonly threadId: string\n readonly interruptedRunId: string\n readonly generation: number\n readonly status: InterruptItemStatus\n readonly errors: ReadonlyArray<ItemInterruptError>\n /** @deprecated Use `errors[0]`. */\n readonly error?: ItemInterruptError\n /**\n * Whether the binding/schema allows resolution at hydrate time.\n * Does not flip on submit/expiry — gate UI on `status`, `resuming`, and\n * `errors` for those lifecycle states.\n */\n readonly canResolve: boolean\n cancel: () => void\n clearResolution: () => void\n}\n\nexport interface GenericAGUIInterrupt extends BoundInterruptBase {\n readonly kind: 'generic'\n readonly binding: Readonly<Extract<InterruptBinding, { kind: 'generic' }>>\n resolveInterrupt: (payload: unknown) => void\n}\n\ntype InterruptResponseInput<TDefinition> =\n TDefinition extends InterruptDefinition<any, any, infer TResponseSchema, any>\n ? InferSchemaType<TResponseSchema>\n : never\n\ntype RegisteredGenericInterruptFor<\n TDefinition extends InterruptDefinition<any, any, any, any>,\n> =\n TDefinition extends InterruptDefinition<\n infer TDefinitionId,\n any,\n any,\n infer TPayload\n >\n ? BoundInterruptBase & {\n readonly kind: 'generic'\n readonly definitionId: TDefinitionId\n readonly key: string\n readonly payload: TPayload | undefined\n readonly binding: Readonly<\n Extract<InterruptBinding, { kind: 'generic' }> & {\n definitionId: TDefinitionId\n key: string\n batchIndex: number\n }\n >\n resolveInterrupt: (\n response: InterruptResponseInput<TDefinition>,\n ) => void\n }\n : never\n\nexport type RegisteredGenericInterrupt<\n TInterrupts extends ReadonlyArray<InterruptDefinition<any, any, any, any>>,\n> = TInterrupts[number] extends infer TDefinition\n ? TDefinition extends InterruptDefinition<any, any, any, any>\n ? RegisteredGenericInterruptFor<TDefinition>\n : never\n : never\n\n/** A bound generic interrupt for one `defineInterrupt()` definition. */\nexport type GenericInterrupt<\n TDefinition extends InterruptDefinition<any, any, any, any>,\n> = RegisteredGenericInterruptFor<TDefinition>\n\n/**\n * An interrupt that arrived on the stream carrying no resume binding this\n * client understands — no `tanstack:interruptBinding`, or one written at a\n * protocol version we don't recognise.\n *\n * These are surfaced rather than hidden so a UI can show that the run is\n * paused, but they are never resolvable here: something else owns them. A\n * workflow engine's durable approval projected into the same AG-UI stream\n * lands in this bucket, and resolving it through the chat resume path would\n * send an answer no one is waiting for. Render it, or route it to whatever\n * actually owns the pause.\n */\nexport interface UnboundInterrupt extends Omit<\n BoundInterruptBase,\n 'cancel' | 'clearResolution'\n> {\n readonly kind: 'unbound'\n readonly binding?: undefined\n readonly canResolve: false\n}\n\ntype ApprovalBranchSchema<TTool, TBranch extends 'approve' | 'reject'> =\n ApprovalSchemaOf<TTool> extends infer TApproval\n ? TApproval extends { approve?: SchemaInput; reject?: SchemaInput }\n ? Exclude<TApproval[TBranch], undefined>\n : TApproval extends SchemaInput\n ? TApproval\n : never\n : never\n\ntype ApprovalEdits<TTool> =\n InputSchemaOf<TTool> extends NoSchema\n ? { editedArgs?: never }\n : { editedArgs?: InferToolInput<TTool> }\n\ntype ApprovalPayload<TSchema> = [TSchema] extends [never]\n ? { payload?: never }\n : TSchema extends SchemaInput\n ? { payload: InferSchemaType<TSchema> }\n : { payload?: never }\n\ntype ApproveArguments<TTool> = [\n ApprovalBranchSchema<TTool, 'approve'>,\n] extends [never]\n ? InputSchemaOf<TTool> extends NoSchema\n ? [options?: never]\n : [options?: ApprovalEdits<TTool> & { payload?: never }]\n : [\n options: ApprovalEdits<TTool> &\n ApprovalPayload<ApprovalBranchSchema<TTool, 'approve'>>,\n ]\n\ntype RejectArguments<TTool> = [ApprovalBranchSchema<TTool, 'reject'>] extends [\n never,\n]\n ? [options?: never]\n : [\n options: { editedArgs?: never } & ApprovalPayload<\n ApprovalBranchSchema<TTool, 'reject'>\n >,\n ]\n\nexport type ToolApprovalInterrupt<TTool extends AnyClientTool = AnyClientTool> =\n TTool extends AnyClientTool\n ? BoundInterruptBase & {\n readonly kind: 'tool-approval'\n readonly binding: Readonly<\n Extract<InterruptBinding, { kind: 'tool-approval' }>\n >\n readonly toolName: TTool['name']\n readonly toolCallId: string\n readonly originalArgs: InferToolInput<TTool>\n // A single generic call signature — not two overloads. Overloads break\n // editor autocomplete: a half-typed options literal (e.g.\n // `resolveInterrupt(true, { payload: {` ) satisfies neither overload,\n // so TS resolves no signature and offers no contextual completions.\n // Making `approved` a generic discriminant lets TS infer it from the\n // first argument and pick the matching branch for the rest params, so\n // `payload` / `editedArgs` / the correct schema's fields complete\n // per-branch (a plain union-of-tuples would offer both branches'\n // fields) while still enforcing the right shape.\n resolveInterrupt: <TApproved extends boolean>(\n approved: TApproved,\n ...args: TApproved extends true\n ? ApproveArguments<TTool>\n : RejectArguments<TTool>\n ) => void\n }\n : never\n\ntype ApprovalInterrupts<TTools extends ReadonlyArray<AnyClientTool>> =\n TTools[number] extends infer TTool\n ? TTool extends AnyClientTool\n ? ApprovalCapabilityOf<TTool> extends true\n ? ToolApprovalInterrupt<TTool>\n : never\n : never\n : never\n\n// Client tools resolve through their `.client()` implementation (auto-run) or\n// `addToolResult` — never as a bound interrupt. The `client-tool-execution`\n// pause is handled internally and is intentionally absent from this public\n// union.\nexport type ChatInterrupt<\n TTools extends ReadonlyArray<AnyClientTool> = ReadonlyArray<AnyClientTool>,\n TInterrupts extends ReadonlyArray<InterruptDefinition<any, any, any, any>> =\n readonly [],\n> =\n | GenericAGUIInterrupt\n | RegisteredGenericInterrupt<TInterrupts>\n | UnboundInterrupt\n | ApprovalInterrupts<TTools>\n\nexport type ResolvableChatInterrupt<\n TTools extends ReadonlyArray<AnyClientTool> = ReadonlyArray<AnyClientTool>,\n TInterrupts extends ReadonlyArray<InterruptDefinition<any, any, any, any>> =\n readonly [],\n> =\n | GenericAGUIInterrupt\n | RegisteredGenericInterrupt<TInterrupts>\n | ApprovalInterrupts<TTools>\n\nexport type BoundInterrupts<\n TTools extends ReadonlyArray<AnyClientTool> = ReadonlyArray<AnyClientTool>,\n TInterrupts extends ReadonlyArray<InterruptDefinition<any, any, any, any>> =\n readonly [],\n> = ReadonlyArray<ChatInterrupt<TTools, TInterrupts>>\n\nexport interface ChatInterruptState<\n TTools extends ReadonlyArray<AnyClientTool> = ReadonlyArray<AnyClientTool>,\n TInterrupts extends ReadonlyArray<InterruptDefinition<any, any, any, any>> =\n readonly [],\n> {\n readonly interrupts: BoundInterrupts<TTools, TInterrupts>\n /** @deprecated Use `interrupts`. Same snapshot today. */\n readonly pendingInterrupts: BoundInterrupts<TTools, TInterrupts>\n readonly interruptErrors: ReadonlyArray<BatchInterruptError>\n readonly resuming: boolean\n}\n\n/**\n * `messages` is the full UIMessage history (not a delta). `data` is the\n * merged body — `ChatClientOptions.body` plus any per-call data passed to\n * `sendMessage(...)`. `threadId` / `runId` are the AG-UI correlation ids\n * the chat client uses to track this turn — forward them to your server\n * if it needs to correlate requests.\n */\nexport interface ChatFetcherInput {\n messages: Array<UIMessage>\n data?: Record<string, unknown>\n threadId: string\n runId: string\n parentRunId?: string\n resume?: Array<RunAgentResumeItem>\n}\n\nexport interface ChatFetcherOptions {\n /** Fires when `stop()` is called or the request is superseded. */\n signal: AbortSignal\n /** Extra request headers for this run (e.g. BYOK keys). */\n headers?: Record<string, string>\n}\n\n/**\n * Direct function that performs a chat request. Mirrors\n * `GenerationFetcher`. Returns either a `Response` (SSE body parsed by the\n * chat client) or an `AsyncIterable<StreamChunk>` (yielded directly). May\n * return the value synchronously, as a `Promise`, or as an async generator\n * (`async function*`) — the chat client awaits whichever shape is returned.\n *\n * @example\n * ```ts\n * useChat({\n * fetcher: ({ messages }, { signal }) =>\n * chatFn({ data: { messages }, signal }),\n * })\n * ```\n */\nexport type ChatFetcher = (\n input: ChatFetcherInput,\n options: ChatFetcherOptions,\n) =>\n | Response\n | AsyncIterable<StreamChunk>\n | Promise<Response | AsyncIterable<StreamChunk>>\n\n/**\n * Distributive `Omit` — applies `Omit<O, K>` per branch of a union so\n * discriminated unions survive omission. Plain `Omit` collapses unions\n * into a single object shape, which would erase the `ChatTransport` XOR\n * when framework hooks omit React-managed callbacks from\n * `ChatClientOptions`.\n */\nexport type DistributedOmit<\n TObject,\n TKeys extends keyof any,\n> = TObject extends unknown ? Omit<TObject, TKeys> : never\n\n/**\n * Discriminated union enforcing that exactly one of `connection` or\n * `fetcher` is provided. Mirrors `GenerationTransport`.\n */\nexport type ChatTransport =\n | { connection: ConnectionAdapter; fetcher?: never }\n | { fetcher: ChatFetcher; connection?: never }\n\n/**\n * Tool call states - track the lifecycle of a tool call\n */\nexport type ToolCallState =\n | 'awaiting-input' // Received start but no arguments yet\n | 'input-streaming' // Partial arguments received\n | 'input-complete' // All arguments received\n | 'approval-requested' // Waiting for user approval\n | 'approval-responded' // User has approved/denied\n | 'complete' // Result is complete\n | 'error' // Tool execution failed (terminal)\n\n/**\n * Tool result states - track the lifecycle of a tool result\n */\nexport type ToolResultState =\n | 'streaming' // Placeholder for future streamed output\n | 'complete' // Result is complete\n | 'error' // Error occurred\n\n/**\n * ChatClient state - track the lifecycle of a chat\n */\nexport type ChatClientState = 'ready' | 'submitted' | 'streaming' | 'error'\n\n/**\n * Connection lifecycle state for the subscription loop.\n */\nexport type ConnectionStatus =\n | 'disconnected'\n | 'connecting'\n | 'connected'\n | 'error'\n\n/**\n * Multimodal content input for sending messages with rich media.\n * Allows sending text, images, audio, video, and documents to the LLM.\n *\n * @example\n * ```ts\n * // Send an image with a question\n * client.sendMessage({\n * content: [\n * { type: 'text', content: 'What is in this image?' },\n * { type: 'image', source: { type: 'url', value: 'https://example.com/photo.jpg' } }\n * ],\n * id: 'custom-message-id' // optional\n * })\n * ```\n */\nexport interface MultimodalContent {\n /**\n * The content of the message.\n * Can be a simple string or an array of content parts for multimodal messages.\n */\n content: string | Array<ContentPart>\n /**\n * Optional custom ID for the message.\n * If not provided, a unique ID will be generated.\n */\n id?: string\n /**\n * Optional AG-UI metadata bag copied onto the resulting UIMessage.\n *\n * @example\n * ```ts\n * await client.sendMessage({\n * content: 'Show me failed logins',\n * metadata: { author: { id: 'user-42', name: 'Dana' } },\n * })\n * ```\n */\n metadata?: Record<string, any>\n}\n\n/**\n * Action taken when `sendMessage` is called while the client is busy\n * (streaming, claiming a send, or draining the queue).\n * - `queue`: hold the message; it auto-sends when the current run settles\n * **successfully**.\n * - `drop`: ignore the send (promise still resolves; does not throw).\n * - `interrupt`: abort the current stream and send immediately. Unlike\n * `stop()`, does **not** flush already-queued messages — they still drain\n * after the interrupting send settles successfully.\n */\nexport type WhenBusy = 'queue' | 'drop' | 'interrupt'\n\n/**\n * Why the client is busy when a {@link QueueStrategy} runs.\n * - `streaming` — an LLM stream is active (`isLoading`).\n * - `sendInFlight` — a send has claimed the client but is not yet loading.\n * - `draining` — the queue drain loop is delivering pending messages.\n */\nexport type QueueBusyReason = 'streaming' | 'sendInFlight' | 'draining'\n\n/**\n * A user message held in the send queue while a stream is active.\n * Rendered separately from `messages`; cancellable via `cancelQueued(id)`\n * until it drains.\n */\nexport interface QueuedMessage {\n id: string\n content: string | MultimodalContent\n createdAt: number\n}\n\n/**\n * Declarative queue policy.\n */\nexport interface QueueConfig {\n /**\n * Action when the client is busy (streaming, claiming a send, or draining).\n * Default `'queue'`.\n */\n whenBusy?: WhenBusy\n /**\n * How queued items leave the queue.\n * - `'fifo'`: one at a time, in order (default).\n * - `'batch'`: merge all queued items into one send when the run settles\n * successfully.\n */\n drain?: 'fifo' | 'batch'\n /** Max queued items. Unlimited when omitted. `0` means never queue. */\n maxSize?: number\n /**\n * Behavior when `maxSize` is reached. Default `'reject'`.\n * `'reject'` silently discards the new send (does not throw);\n * `'drop-oldest'` evicts the oldest queued item to make room.\n * Only meaningful when `maxSize` is set.\n */\n onOverflow?: 'reject' | 'drop-oldest'\n}\n\n/**\n * Escape hatch: decide the action for a single send. Drain stays FIFO for the\n * function form (no `batch` via function). Per-call `sendOptions.whenBusy`\n * overrides the strategy for that send.\n *\n * Actions match {@link WhenBusy}. Concurrent streams are not supported.\n * `pending.id` is the id that will be stored if the action is `'queue'`\n * (safe to pass to `cancelQueued`).\n */\nexport type QueueStrategy = (ctx: {\n pending: QueuedMessage\n busyReason: QueueBusyReason\n queued: ReadonlyArray<QueuedMessage>\n}) => { action: WhenBusy }\n\n/** A `WhenBusy` shorthand, a full config, or a strategy function. */\nexport type QueueOption = WhenBusy | QueueConfig | QueueStrategy\n\n/** Per-call overrides for `sendMessage`. */\nexport interface SendMessageOptions {\n /** Overrides the configured `whenBusy` for this one send. */\n whenBusy?: WhenBusy\n /**\n * Extra JSON merged into this request's wire `forwardedProps`.\n * Shallow merge: `{ ...chatBody, ...positionalBody, ...body }`.\n * This field wins on key collisions.\n *\n * Framework hooks (`useChat`, `injectChat`, `createChat`) expose\n * `sendMessage(content, options)` with no positional body, so this field\n * is the per-call body channel on those surfaces.\n */\n body?: Record<string, any>\n}\n\n/**\n * Message parts - building blocks of UIMessage\n */\nexport interface TextPart {\n type: 'text'\n content: string\n}\n\n/**\n * Helper type that creates a tool-call part for a specific tool.\n * This is a conditional type to enable proper distribution over union types,\n * creating a discriminated union where `name` is the discriminant.\n */\ntype ToolCallPartForTool<T> = T extends AnyClientTool\n ? {\n type: 'tool-call'\n id: string\n name: T['name']\n arguments: string // JSON string (may be incomplete)\n /** Parsed tool input (typed from inputSchema) */\n input?: InferToolInput<T>\n state: ToolCallState\n /** Tool execution output (for client tools or after approval) */\n output?: InferToolOutput<T>\n } & (NonNullable<T['needsApproval']> extends true\n ? {\n /**\n * Approval metadata — present only on tools defined with\n * `needsApproval: true`. Populated once the call reaches\n * `state: 'approval-requested'`. `needsApproval` is an optional\n * property on the tool, so we index into it (rather than\n * `T extends { needsApproval: true }`, which an optional property\n * never satisfies) and strip `undefined` before comparing to `true`.\n */\n approval?: {\n id: string // Unique approval ID\n needsApproval: boolean // Always true if present\n approved?: boolean // User's decision (undefined until responded)\n }\n }\n : // Tools without `needsApproval: true` never carry an approval field.\n // `& unknown` is a no-op intersection (adds nothing).\n unknown)\n : never\n\n/**\n * Fallback tool-call part type when tools are not typed\n */\ntype UntypedToolCallPart = {\n type: 'tool-call'\n id: string\n name: string\n arguments: string\n input?: any\n state: ToolCallState\n approval?: {\n id: string\n needsApproval: boolean\n approved?: boolean\n }\n output?: any\n}\n\n/**\n * Tool call part that creates a proper discriminated union.\n * When TTools is typed, checking `part.name === 'toolName'` will narrow\n * `part.output` to the correct type for that tool.\n *\n * The discriminant is `name`, so code like:\n * ```ts\n * if (part.name === 'recommendGuitar') {\n * // part.output is now typed to the recommendGuitar tool's output\n * }\n * ```\n */\nexport type ToolCallPart<TTools extends ReadonlyArray<AnyClientTool> = any> =\n // Check if we have a concrete tools array (not 'any' or 'never')\n [TTools] extends [never]\n ? UntypedToolCallPart\n : unknown extends TTools\n ? UntypedToolCallPart\n : TTools extends ReadonlyArray<infer Tool>\n ? Tool extends AnyClientTool\n ? ToolCallPartForTool<Tool>\n : UntypedToolCallPart\n : UntypedToolCallPart\n\nexport interface ToolResultPart {\n type: 'tool-result'\n toolCallId: string\n content: string | Array<ContentPart>\n state: ToolResultState\n error?: string // Error message if state is \"error\"\n}\n\nexport interface ThinkingPart {\n type: 'thinking'\n content: string\n}\n\nexport type MessagePart<\n TTools extends ReadonlyArray<AnyClientTool> = any,\n TData = unknown,\n> =\n | TextPart\n | ImagePart\n | AudioPart\n | VideoPart\n | DocumentPart\n | ToolCallPart<TTools>\n | ToolResultPart\n | ThinkingPart\n | StructuredOutputPart<TData>\n | UIResourcePart\n\n/**\n * UIMessage - Domain-specific message format optimized for building chat UIs\n * Contains parts that can be text, tool calls, or tool results.\n *\n * `TTools` narrows the tool-call/result part types based on the registered\n * tools. `TData` is the schema-inferred type for any `structured-output` part\n * on the message — defaulted to `unknown` so untyped consumers (the core\n * stream processor, the wire converter) don't need to thread a schema generic\n * everywhere; the hook layer (`useChat({ outputSchema })`) substitutes it on\n * the public return so `m.parts.find(p => p.type === 'structured-output').data`\n * is typed without manual casts.\n */\nexport interface UIMessage<\n TTools extends ReadonlyArray<AnyClientTool> = any,\n TData = unknown,\n> {\n id: string\n role: 'system' | 'user' | 'assistant'\n parts: Array<MessagePart<TTools, TData>>\n createdAt?: Date\n /**\n * Optional AG-UI metadata bag. TanStack writes the `tanstack` key.\n * User keys stay at the top.\n */\n metadata?: Record<string, any>\n}\n\n/**\n * A generic key/value storage adapter. `getItem` may be sync or async; the\n * chat persistence layer treats every call as best-effort. The provided\n * `localStoragePersistence` / `sessionStoragePersistence` / `indexedDBPersistence`\n * factories return one of these, and `ChatStorageAdapter<ChatPersistedState>`\n * is assignable to {@link ChatClientPersistence}.\n */\nexport interface ChatStorageAdapter<TValue> {\n getItem: (\n id: string,\n ) => TValue | null | undefined | Promise<TValue | null | undefined>\n setItem: (id: string, value: TValue) => void | Promise<void>\n removeItem: (id: string) => void | Promise<void>\n}\n\n/**\n * The single record a `ChatClientPersistence` adapter stores per chat. It folds\n * the two things that must survive a full page reload into one blob under one\n * key: the message transcript and the optional resume snapshot (which run to\n * rejoin / which interrupts to rehydrate). One adapter, one key — see\n * {@link ChatClientPersistence}.\n */\nexport interface ChatPersistedState<\n TTools extends ReadonlyArray<AnyClientTool> = any,\n> {\n messages: Array<UIMessage<TTools>>\n /** Present while a run is in flight or paused on an interrupt; absent otherwise. */\n resume?: ChatResumeSnapshot\n}\n\n/**\n * Storage adapter for durable chat state. A single adapter persists both the\n * message transcript and the resume snapshot as one {@link ChatPersistedState}\n * record, so a full page reload restores the conversation AND can rejoin an\n * in-flight run / rehydrate pending interrupts.\n *\n * For backward compatibility `getItem` may also return a bare `UIMessage[]`\n * (the legacy messages-only format); the client normalizes it to\n * `{ messages }`. `setItem` always writes the combined record.\n */\nexport interface ChatClientPersistence<\n TTools extends ReadonlyArray<AnyClientTool> = any,\n> {\n getItem: (\n id: string,\n ) =>\n | ChatPersistedState<TTools>\n | Array<UIMessage<TTools>>\n | null\n | undefined\n | Promise<\n ChatPersistedState<TTools> | Array<UIMessage<TTools>> | null | undefined\n >\n setItem: (\n id: string,\n state: ChatPersistedState<TTools>,\n ) => void | Promise<void>\n removeItem: (id: string) => void | Promise<void>\n}\n\n/**\n * The `persistence` option for a chat.\n *\n * - `false` (default): ephemeral. Messages live in memory only; a reload starts\n * from empty.\n * - `true`: server-authoritative. Nothing is cached in the browser. On mount the\n * client hydrates the thread from the server by its `threadId` (paints the\n * stored transcript and tails any run still generating), so a reload or the\n * same thread opened on another device both just resume. Requires a connection\n * with a `hydrate` handler.\n * - a {@link ChatClientPersistence} adapter: client-authoritative. The combined\n * {@link ChatPersistedState} record (transcript plus resume pointer) is cached\n * in the browser and restored on reload with no network.\n */\nexport type ChatPersistenceOption<\n TTools extends ReadonlyArray<AnyClientTool> = any,\n> = boolean | ChatClientPersistence<TTools>\n\n/**\n * The `persistence` / `threadId` pairing for `ChatClient` and the chat hooks.\n *\n * Persistence that is on (`true` or a storage adapter) requires a `threadId`.\n * A minted id changes every reload, so nothing would restore. The compiler\n * asks for the conversation id instead.\n *\n * Omit `persistence`, or set it to `false`, and `threadId` stays optional.\n * The client then mints one after mount for the wire and DevTools.\n *\n * Intersect this onto `ChatClientOptions`. Do not apply a later plain `Omit`\n * to that type: it collapses the union and the requirement disappears. Use\n * {@link DistributedOmit}.\n */\nexport type ChatPersistenceOptions<\n TTools extends ReadonlyArray<AnyClientTool> = any,\n> =\n | {\n persistence: true\n threadId: string\n }\n | {\n persistence: ChatClientPersistence<TTools>\n threadId: string\n }\n | {\n persistence?: false | undefined\n threadId?: string\n }\n\ntype IsUnknown<T> = unknown extends T\n ? [T] extends [unknown]\n ? true\n : false\n : false\n\ntype KnownContext<T> = IsUnknown<T> extends true ? never : T\n\ntype MergeContext<TLeft, TRight> = [TLeft] extends [never]\n ? TRight\n : [TRight] extends [never]\n ? TLeft\n : TLeft & TRight\n\ntype UnionToIntersection<T> = [T] extends [never]\n ? never\n : (T extends unknown ? (value: T) => void : never) extends (\n value: infer TIntersection,\n ) => void\n ? TIntersection\n : never\n\ntype DefinedContext<T> = Exclude<T, undefined>\n\ntype ContextFromExecute<T> = T extends (...args: any) => any\n ? NonNullable<Parameters<T>[1]> extends { context: infer TContext }\n ? KnownContext<TContext>\n : never\n : never\n\ntype ContextFromClientTool<T> = T extends AnyClientTool\n ? T extends { execute?: infer TExecute }\n ? ContextFromExecute<TExecute>\n : never\n : never\n\ntype RequiredContextFromClientToolUnion<T> = T extends unknown\n ? undefined extends ContextFromClientTool<T>\n ? never\n : ContextFromClientTool<T>\n : never\n\ntype ContextFromClientToolUnion<T> = [\n UnionToIntersection<DefinedContext<ContextFromClientTool<T>>>,\n] extends [never]\n ? never\n : [RequiredContextFromClientToolUnion<T>] extends [never]\n ? UnionToIntersection<DefinedContext<ContextFromClientTool<T>>> | undefined\n : UnionToIntersection<DefinedContext<ContextFromClientTool<T>>>\n\ntype ContextFromClientTools<TTools> =\n IsUnknown<TTools> extends true\n ? never\n : TTools extends readonly [infer THead, ...infer TTail]\n ? MergeContext<\n ContextFromClientTool<THead>,\n ContextFromClientTools<TTail>\n >\n : TTools extends ReadonlyArray<infer TItem>\n ? ContextFromClientToolUnion<TItem>\n : never\n\nexport type InferredClientContext<TTools> = [\n ContextFromClientTools<TTools>,\n] extends [never]\n ? unknown\n : ContextFromClientTools<TTools>\n\nexport type ClientContextOptionFromTools<TTools, TContext> = [\n ContextFromClientTools<TTools>,\n] extends [never]\n ? { context?: TContext }\n : undefined extends ContextFromClientTools<TTools>\n ? { context?: TContext & ContextFromClientTools<TTools> }\n : { context: TContext & ContextFromClientTools<TTools> }\n\n/**\n * Base options for `ChatClient`, excluding the transport (`connection` or\n * `fetcher`) which is supplied separately via `ChatTransport` so the XOR\n * is preserved when composing the final `ChatClientOptions` type.\n */\nexport interface ChatClientBaseOptions<\n TTools extends ReadonlyArray<AnyClientTool> = any,\n TContext = unknown,\n TInterrupts extends ReadonlyArray<InterruptDefinition<any, any, any, any>> =\n readonly [],\n> {\n /**\n * Initial messages to populate the chat\n */\n initialMessages?: Array<UIMessage<TTools>>\n\n /**\n * Initial resumable run state, useful when rehydrating a persisted client\n * after a full page reload. This restores the client-side interrupt\n * descriptors needed to send AG-UI resume entries.\n */\n initialResumeSnapshot?: ChatResumeSnapshot\n\n /**\n * Arbitrary client-controlled JSON forwarded to the server in the\n * AG-UI `RunAgentInput.forwardedProps` field. Use this for per-session\n * options like provider/model selection or feature flags that the\n * server endpoint should read.\n *\n * Replaces the legacy `body` option. If both are provided,\n * `forwardedProps` wins on key collision.\n */\n forwardedProps?: Record<string, any>\n\n /**\n * @deprecated Use `forwardedProps` instead. `body` continues to work\n * unchanged — its values are merged into the AG-UI\n * `RunAgentInput.forwardedProps` field on the wire and are also\n * mirrored under the legacy `data` field for servers that have not\n * migrated yet. Will be removed in a future major release.\n */\n body?: Record<string, any>\n\n /**\n * Optional BYOK keyring. On each send the client prepares the resolved\n * provider and stamps `x-byok-*` request headers. Keys never go in the body.\n */\n byok?: ByokClient\n\n /**\n * Optional provider id for this chat. If it returns a provider slug,\n * only that key is prepared and sent. Otherwise the merged `provider`\n * from `forwardedProps`, `body`, and per-call `sendMessage` `body` is\n * used. Later sources win. If no slug resolves, the send throws\n * instead of attaching every stored key.\n */\n byokProvider?: () => string | undefined\n\n /**\n * Client-local runtime context passed to client tool implementations.\n *\n * This value is not serialized to the server. Use `forwardedProps` for\n * explicit client-to-server handoff of serializable values.\n */\n context?: TContext\n\n /**\n * Callback when a response is received\n */\n onResponse?: (response?: Response) => void | Promise<void>\n\n /**\n * Callback when a stream chunk is received\n */\n onChunk?: (chunk: StreamChunk) => void\n\n /**\n * Callback when the response is finished\n */\n onFinish?: (message: UIMessage<TTools>) => void\n\n /**\n * Callback when an error occurs\n */\n onError?: (error: Error) => void\n\n /**\n * Callback when messages change\n */\n onMessagesChange?: (messages: Array<UIMessage<TTools>>) => void\n\n /**\n * Callback when loading state changes\n */\n onLoadingChange?: (isLoading: boolean) => void\n\n /**\n * Callback when error state changes\n */\n onErrorChange?: (error: Error | undefined) => void\n\n /**\n * Callback when chat status changes\n */\n onStatusChange?: (status: ChatClientState) => void\n\n /**\n * Callback when subscription lifecycle changes.\n * This is independent from request lifecycle (`isLoading`, `status`).\n */\n onSubscriptionChange?: (isSubscribed: boolean) => void\n\n /**\n * Callback when connection lifecycle changes.\n */\n onConnectionStatusChange?: (status: ConnectionStatus) => void\n\n /**\n * Callback when session generation activity changes.\n * Derived from stream run events (RUN_STARTED / RUN_FINISHED / RUN_ERROR).\n * Unlike `onLoadingChange` (request-local), this reflects shared generation\n * activity visible to all subscribers (e.g. across tabs/devices).\n */\n onSessionGeneratingChange?: (isGenerating: boolean) => void\n\n /**\n * Policy for messages sent while the client is busy (streaming, claiming\n * a send, or draining the queue). Accepts a `WhenBusy` string, a\n * `QueueConfig`, or a `QueueStrategy` function.\n * Default: `{ whenBusy: 'queue', drain: 'fifo' }`.\n * Queued items auto-send only after a **successful** settle; they are\n * discarded on error/abort, `stop()`, `clear()`, `unsubscribe()`, and\n * `reload()`.\n */\n queue?: QueueOption\n\n /**\n * Callback when the pending send queue changes (enqueue, cancel, drain,\n * or flush).\n */\n onQueueChange?: (queue: Array<QueuedMessage>) => void\n\n /**\n * Callback when resumable run state or pending interrupts change.\n */\n onResumeStateChange?: (\n resumeState: ChatResumeState | null,\n pendingInterrupts: BoundInterrupts<TTools, TInterrupts>,\n ) => void\n\n /**\n * Callback when the id of the run this client has in flight changes: the new\n * id when a run starts (a send, or a `joinRun` rejoin), `null` when it settles.\n */\n onRunIdChange?: (runId: string | null) => void\n\n /**\n * Callback when the immutable interrupt state snapshot changes.\n * Snapshot restoration passes `{ source: 'hydrate' }`; streamed and\n * client-initiated updates pass `{ source: 'live' }`.\n */\n onInterruptStateChange?: (\n state: ChatInterruptState<TTools, TInterrupts>,\n context: { source: 'hydrate' | 'live' },\n ) => void\n\n /**\n * Callback when a custom event is received from a server-side tool.\n * Custom events are emitted by tools using `context.emitCustomEvent()` during execution.\n *\n * @param eventType - The name of the custom event\n * @param data - The event payload data\n * @param context - Additional context including the toolCallId that emitted the event\n */\n onCustomEvent?: (\n eventType: string,\n data: unknown,\n context: { toolCallId?: string },\n ) => void\n\n /**\n * Client-side tools with execution logic\n * When provided, tools with execute functions will be called automatically\n */\n tools?: TTools\n\n /** First-party generic interrupts this client can type and resolve. */\n interrupts?: TInterrupts\n\n /**\n * Devtools hook metadata for this client instance.\n */\n devtools?: Partial<AIDevtoolsClientMetadata>\n\n /**\n * Factory that constructs the devtools bridge. Default is a no-op\n * factory, which keeps `@tanstack/ai-client/devtools` (the heavy\n * bridge implementation) out of the main entry's bundle. Frameworks\n * that need live devtools should pass the real factory from\n * `@tanstack/ai-client/devtools`.\n */\n devtoolsBridgeFactory?: ChatDevtoolsBridgeFactory\n\n /**\n * Stream processing options (optional)\n * Configure chunking strategy\n */\n streamProcessor?: {\n /**\n * Strategy for when to emit text updates\n * Defaults to ImmediateStrategy (every chunk)\n */\n chunkStrategy?: ChunkStrategy\n }\n}\n\n/**\n * Options for `ChatClient`. Exactly one of `connection` or `fetcher` must be\n * provided — the type-level XOR is enforced via `ChatTransport`. Persistence\n * that is on requires a `threadId` via {@link ChatPersistenceOptions}.\n */\nexport type ChatClientOptions<\n TTools extends ReadonlyArray<AnyClientTool> = any,\n TContext = InferredClientContext<TTools>,\n TInterrupts extends ReadonlyArray<InterruptDefinition<any, any, any, any>> =\n readonly [],\n> = DistributedOmit<\n ChatClientBaseOptions<TTools, TContext, TInterrupts>,\n 'context'\n> &\n ClientContextOptionFromTools<TTools, TContext> &\n ChatTransport &\n ChatPersistenceOptions<TTools>\n\nexport interface ChatRequestBody {\n messages: Array<ModelMessage>\n data?: Record<string, any>\n}\n\n/**\n * Create a typed array of client tools with proper type inference.\n * This eliminates the need for `as const` when defining tool arrays.\n *\n * @example\n * ```ts\n * const tools = clientTools(\n * myTool1.client(() => result1),\n * myTool2.client(() => result2),\n * )\n *\n * // tools is now properly typed as a tuple with literal tool names\n * // This enables type narrowing when checking part.name === 'toolName'\n * ```\n */\nexport function clientTools<const T extends Array<AnyClientTool>>(\n ...tools: T\n): T {\n return tools\n}\n\n/**\n * Helper to create typed chat client options\n * Use this to get proper type inference for messages\n *\n * @example\n * ```ts\n * const tools = clientTools(myTool1, myTool2)\n *\n * const chatOptions = createChatClientOptions({\n * connection: fetchServerSentEvents('/api/chat'),\n * tools,\n * })\n *\n * type MyMessages = InferChatMessages<typeof chatOptions>\n * ```\n */\nexport function createChatClientOptions<\n const TTools extends ReadonlyArray<AnyClientTool>,\n TContext = InferredClientContext<TTools>,\n const TInterrupts extends ReadonlyArray<\n InterruptDefinition<any, any, any, any>\n > = readonly [],\n>(\n options: ChatClientOptions<TTools, TContext, TInterrupts>,\n): ChatClientOptions<TTools, TContext, TInterrupts> {\n return options\n}\n\n/**\n * Extract the message type from chat options\n *\n * @example\n * ```ts\n * const chatOptions = createChatClientOptions({\n * connection: fetchServerSentEvents('/api/chat'),\n * tools: [myTool1, myTool2],\n * })\n *\n * type MyMessages = InferChatMessages<typeof chatOptions>\n * // MyMessages is now Array<UIMessage<[typeof myTool1, typeof myTool2]>>\n * ```\n */\nexport type InferChatMessages<T> =\n T extends ChatClientOptions<infer TTools, any>\n ? Array<UIMessage<TTools>>\n : never\n"],"mappings":";;;;;;;;;;;;;;;;AAgkCA,SAAgB,YACd,GAAG,OACA;CACH,OAAO;AACT;;;;;;;;;;;;;;;;;AAkBA,SAAgB,wBAOd,SACkD;CAClD,OAAO;AACT"}
|
|
1
|
+
{"version":3,"file":"types.js","names":[],"sources":["../../src/types.ts"],"sourcesContent":["import type {\n AnyClientTool,\n ApprovalCapabilityOf,\n ApprovalSchemaOf,\n AudioPart,\n BatchInterruptError,\n ChunkStrategy,\n ContentPart,\n DocumentPart,\n ImagePart,\n InferSchemaType,\n InterruptDefinition,\n InferToolInput,\n InferToolOutput,\n InputSchemaOf,\n Interrupt,\n InterruptBinding,\n ItemInterruptError,\n ModelMessage,\n NoSchema,\n RunAgentResumeItem,\n SchemaInput,\n StreamChunk,\n StructuredOutputPart,\n UIResourcePart,\n VideoPart,\n} from '@tanstack/ai/client'\nimport type { ByokClient } from './byok'\nimport type { ConnectionAdapter } from './connection-adapters'\nimport type { AIDevtoolsClientMetadata } from './devtools'\nimport type { ChatDevtoolsBridgeFactory } from './devtools-noop'\n\nexport type { StructuredOutputPart }\n\nexport interface ChatResumeState {\n threadId: string\n runId: string\n}\n\nexport type ChatPendingInterrupt = Interrupt\n\n/**\n * The durable pointer a chat keeps for the run it may need to rejoin, plus any\n * interrupt that run is waiting on.\n *\n * @internal\n */\nexport interface ChatResumeSnapshot {\n resumeState: ChatResumeState\n pendingInterrupts?: Array<ChatPendingInterrupt>\n}\n\nexport type InterruptItemStatus =\n | 'pending'\n | 'validating'\n | 'staged'\n | 'submitting'\n | 'error'\n\nexport interface BoundInterruptBase {\n readonly id: string\n readonly interruptId: string\n readonly reason: string\n readonly message?: string\n readonly responseSchema?: Readonly<Record<string, unknown>>\n readonly expiresAt?: string\n readonly metadata?: Readonly<Record<string, unknown>>\n readonly threadId: string\n readonly interruptedRunId: string\n readonly generation: number\n readonly status: InterruptItemStatus\n readonly errors: ReadonlyArray<ItemInterruptError>\n /** @deprecated Use `errors[0]`. */\n readonly error?: ItemInterruptError\n /**\n * Whether the binding/schema allows resolution at hydrate time.\n * Does not flip on submit/expiry — gate UI on `status`, `resuming`, and\n * `errors` for those lifecycle states.\n */\n readonly canResolve: boolean\n cancel: () => void\n clearResolution: () => void\n}\n\nexport interface GenericAGUIInterrupt extends BoundInterruptBase {\n readonly kind: 'generic'\n readonly binding: Readonly<Extract<InterruptBinding, { kind: 'generic' }>>\n resolveInterrupt: (payload: unknown) => void\n}\n\ntype InterruptResponseInput<TDefinition> =\n TDefinition extends InterruptDefinition<any, any, infer TResponseSchema, any>\n ? InferSchemaType<TResponseSchema>\n : never\n\ntype RegisteredGenericInterruptFor<\n TDefinition extends InterruptDefinition<any, any, any, any>,\n> =\n TDefinition extends InterruptDefinition<\n infer TDefinitionId,\n any,\n any,\n infer TPayload\n >\n ? BoundInterruptBase & {\n readonly kind: 'generic'\n readonly definitionId: TDefinitionId\n readonly key: string\n readonly payload: TPayload | undefined\n readonly binding: Readonly<\n Extract<InterruptBinding, { kind: 'generic' }> & {\n definitionId: TDefinitionId\n key: string\n batchIndex: number\n }\n >\n resolveInterrupt: (\n response: InterruptResponseInput<TDefinition>,\n ) => void\n }\n : never\n\nexport type RegisteredGenericInterrupt<\n TInterrupts extends ReadonlyArray<InterruptDefinition<any, any, any, any>>,\n> = TInterrupts[number] extends infer TDefinition\n ? TDefinition extends InterruptDefinition<any, any, any, any>\n ? RegisteredGenericInterruptFor<TDefinition>\n : never\n : never\n\n/** A bound generic interrupt for one `defineInterrupt()` definition. */\nexport type GenericInterrupt<\n TDefinition extends InterruptDefinition<any, any, any, any>,\n> = RegisteredGenericInterruptFor<TDefinition>\n\n/**\n * An interrupt that arrived on the stream carrying no resume binding this\n * client understands — no `tanstack:interruptBinding`, or one written at a\n * protocol version we don't recognise.\n *\n * These are surfaced rather than hidden so a UI can show that the run is\n * paused, but they are never resolvable here: something else owns them. A\n * workflow engine's durable approval projected into the same AG-UI stream\n * lands in this bucket, and resolving it through the chat resume path would\n * send an answer no one is waiting for. Render it, or route it to whatever\n * actually owns the pause.\n */\nexport interface UnboundInterrupt extends Omit<\n BoundInterruptBase,\n 'cancel' | 'clearResolution'\n> {\n readonly kind: 'unbound'\n readonly binding?: undefined\n readonly canResolve: false\n}\n\ntype ApprovalBranchSchema<TTool, TBranch extends 'approve' | 'reject'> =\n ApprovalSchemaOf<TTool> extends infer TApproval\n ? TApproval extends { approve?: SchemaInput; reject?: SchemaInput }\n ? Exclude<TApproval[TBranch], undefined>\n : TApproval extends SchemaInput\n ? TApproval\n : never\n : never\n\ntype ApprovalEdits<TTool> =\n InputSchemaOf<TTool> extends NoSchema\n ? { editedArgs?: never }\n : { editedArgs?: InferToolInput<TTool> }\n\ntype ApprovalPayload<TSchema> = [TSchema] extends [never]\n ? { payload?: never }\n : TSchema extends SchemaInput\n ? { payload: InferSchemaType<TSchema> }\n : { payload?: never }\n\ntype ApproveArguments<TTool> = [\n ApprovalBranchSchema<TTool, 'approve'>,\n] extends [never]\n ? InputSchemaOf<TTool> extends NoSchema\n ? [options?: never]\n : [options?: ApprovalEdits<TTool> & { payload?: never }]\n : [\n options: ApprovalEdits<TTool> &\n ApprovalPayload<ApprovalBranchSchema<TTool, 'approve'>>,\n ]\n\ntype RejectArguments<TTool> = [ApprovalBranchSchema<TTool, 'reject'>] extends [\n never,\n]\n ? [options?: never]\n : [\n options: { editedArgs?: never } & ApprovalPayload<\n ApprovalBranchSchema<TTool, 'reject'>\n >,\n ]\n\nexport type ToolApprovalInterrupt<TTool extends AnyClientTool = AnyClientTool> =\n TTool extends AnyClientTool\n ? BoundInterruptBase & {\n readonly kind: 'tool-approval'\n readonly binding: Readonly<\n Extract<InterruptBinding, { kind: 'tool-approval' }>\n >\n readonly toolName: TTool['name']\n readonly toolCallId: string\n readonly originalArgs: InferToolInput<TTool>\n // A single generic call signature — not two overloads. Overloads break\n // editor autocomplete: a half-typed options literal (e.g.\n // `resolveInterrupt(true, { payload: {` ) satisfies neither overload,\n // so TS resolves no signature and offers no contextual completions.\n // Making `approved` a generic discriminant lets TS infer it from the\n // first argument and pick the matching branch for the rest params, so\n // `payload` / `editedArgs` / the correct schema's fields complete\n // per-branch (a plain union-of-tuples would offer both branches'\n // fields) while still enforcing the right shape.\n resolveInterrupt: <TApproved extends boolean>(\n approved: TApproved,\n ...args: TApproved extends true\n ? ApproveArguments<TTool>\n : RejectArguments<TTool>\n ) => void\n }\n : never\n\ntype ApprovalInterrupts<TTools extends ReadonlyArray<AnyClientTool>> =\n TTools[number] extends infer TTool\n ? TTool extends AnyClientTool\n ? ApprovalCapabilityOf<TTool> extends true\n ? ToolApprovalInterrupt<TTool>\n : never\n : never\n : never\n\n// Client tools resolve through their `.client()` implementation (auto-run) or\n// `addToolResult` — never as a bound interrupt. The `client-tool-execution`\n// pause is handled internally and is intentionally absent from this public\n// union.\nexport type ChatInterrupt<\n TTools extends ReadonlyArray<AnyClientTool> = ReadonlyArray<AnyClientTool>,\n TInterrupts extends ReadonlyArray<InterruptDefinition<any, any, any, any>> =\n readonly [],\n> =\n | GenericAGUIInterrupt\n | RegisteredGenericInterrupt<TInterrupts>\n | UnboundInterrupt\n | ApprovalInterrupts<TTools>\n\nexport type ResolvableChatInterrupt<\n TTools extends ReadonlyArray<AnyClientTool> = ReadonlyArray<AnyClientTool>,\n TInterrupts extends ReadonlyArray<InterruptDefinition<any, any, any, any>> =\n readonly [],\n> =\n | GenericAGUIInterrupt\n | RegisteredGenericInterrupt<TInterrupts>\n | ApprovalInterrupts<TTools>\n\nexport type BoundInterrupts<\n TTools extends ReadonlyArray<AnyClientTool> = ReadonlyArray<AnyClientTool>,\n TInterrupts extends ReadonlyArray<InterruptDefinition<any, any, any, any>> =\n readonly [],\n> = ReadonlyArray<ChatInterrupt<TTools, TInterrupts>>\n\nexport interface ChatInterruptState<\n TTools extends ReadonlyArray<AnyClientTool> = ReadonlyArray<AnyClientTool>,\n TInterrupts extends ReadonlyArray<InterruptDefinition<any, any, any, any>> =\n readonly [],\n> {\n readonly interrupts: BoundInterrupts<TTools, TInterrupts>\n /** @deprecated Use `interrupts`. Same snapshot today. */\n readonly pendingInterrupts: BoundInterrupts<TTools, TInterrupts>\n readonly interruptErrors: ReadonlyArray<BatchInterruptError>\n readonly resuming: boolean\n}\n\n/**\n * `messages` is the full UIMessage history (not a delta). `data` is the\n * merged body — `ChatClientOptions.body` plus any per-call data passed to\n * `sendMessage(...)`. `threadId` / `runId` are the AG-UI correlation ids\n * the chat client uses to track this turn — forward them to your server\n * if it needs to correlate requests.\n */\nexport interface ChatFetcherInput {\n messages: Array<UIMessage>\n data?: Record<string, unknown>\n threadId: string\n runId: string\n parentRunId?: string\n resume?: Array<RunAgentResumeItem>\n}\n\nexport interface ChatFetcherOptions {\n /** Fires when `stop()` is called or the request is superseded. */\n signal: AbortSignal\n /** Extra request headers for this run (e.g. BYOK keys). */\n headers?: Record<string, string>\n}\n\n/**\n * Direct function that performs a chat request. Mirrors\n * `GenerationFetcher`. Returns either a `Response` (SSE body parsed by the\n * chat client) or an `AsyncIterable<StreamChunk>` (yielded directly). May\n * return the value synchronously, as a `Promise`, or as an async generator\n * (`async function*`) — the chat client awaits whichever shape is returned.\n *\n * @example\n * ```ts\n * useChat({\n * fetcher: ({ messages }, { signal }) =>\n * chatFn({ data: { messages }, signal }),\n * })\n * ```\n */\nexport type ChatFetcher = (\n input: ChatFetcherInput,\n options: ChatFetcherOptions,\n) =>\n | Response\n | AsyncIterable<StreamChunk>\n | Promise<Response | AsyncIterable<StreamChunk>>\n\n/**\n * Distributive `Omit` — applies `Omit<O, K>` per branch of a union so\n * discriminated unions survive omission. Plain `Omit` collapses unions\n * into a single object shape, which would erase the `ChatTransport` XOR\n * when framework hooks omit React-managed callbacks from\n * `ChatClientOptions`.\n */\nexport type DistributedOmit<\n TObject,\n TKeys extends keyof any,\n> = TObject extends unknown ? Omit<TObject, TKeys> : never\n\n/**\n * Discriminated union enforcing that exactly one of `connection` or\n * `fetcher` is provided. Mirrors `GenerationTransport`.\n */\nexport type ChatTransport =\n | { connection: ConnectionAdapter; fetcher?: never }\n | { fetcher: ChatFetcher; connection?: never }\n\n/**\n * Tool call states - track the lifecycle of a tool call\n */\nexport type ToolCallState =\n | 'awaiting-input' // Received start but no arguments yet\n | 'input-streaming' // Partial arguments received\n | 'input-complete' // All arguments received\n | 'approval-requested' // Waiting for user approval\n | 'approval-responded' // User has approved/denied\n | 'complete' // Result is complete\n | 'error' // Tool execution failed (terminal)\n\n/**\n * Tool result states - track the lifecycle of a tool result\n */\nexport type ToolResultState =\n | 'streaming' // Placeholder for future streamed output\n | 'complete' // Result is complete\n | 'error' // Error occurred\n\n/**\n * ChatClient state - track the lifecycle of a chat\n */\nexport type ChatClientState = 'ready' | 'submitted' | 'streaming' | 'error'\n\n/**\n * Connection lifecycle state for the subscription loop.\n */\nexport type ConnectionStatus =\n | 'disconnected'\n | 'connecting'\n | 'connected'\n | 'error'\n\n/**\n * Multimodal content input for sending messages with rich media.\n * Allows sending text, images, audio, video, and documents to the LLM.\n *\n * @example\n * ```ts\n * // Send an image with a question\n * client.sendMessage({\n * content: [\n * { type: 'text', content: 'What is in this image?' },\n * { type: 'image', source: { type: 'url', value: 'https://example.com/photo.jpg' } }\n * ],\n * id: 'custom-message-id' // optional\n * })\n * ```\n */\nexport interface MultimodalContent {\n /**\n * The content of the message.\n * Can be a simple string or an array of content parts for multimodal messages.\n */\n content: string | Array<ContentPart>\n /**\n * Optional custom ID for the message.\n * If not provided, a unique ID will be generated.\n */\n id?: string\n /**\n * Optional AG-UI metadata bag copied onto the resulting UIMessage.\n *\n * @example\n * ```ts\n * await client.sendMessage({\n * content: 'Show me failed logins',\n * metadata: { author: { id: 'user-42', name: 'Dana' } },\n * })\n * ```\n */\n metadata?: Record<string, any>\n}\n\n/**\n * Action taken when `sendMessage` is called while the client is busy\n * (streaming, claiming a send, or draining the queue).\n * - `queue`: hold the message; it auto-sends when the current run settles\n * **successfully**.\n * - `drop`: ignore the send (promise still resolves; does not throw).\n * - `interrupt`: abort the current stream and send immediately. Unlike\n * `stop()`, does **not** flush already-queued messages — they still drain\n * after the interrupting send settles successfully.\n */\nexport type WhenBusy = 'queue' | 'drop' | 'interrupt'\n\n/**\n * Why the client is busy when a {@link QueueStrategy} runs.\n * - `streaming` — an LLM stream is active (`isLoading`).\n * - `sendInFlight` — a send has claimed the client but is not yet loading.\n * - `draining` — the queue drain loop is delivering pending messages.\n */\nexport type QueueBusyReason = 'streaming' | 'sendInFlight' | 'draining'\n\n/**\n * A user message held in the send queue while a stream is active.\n * Rendered separately from `messages`; cancellable via `cancelQueued(id)`\n * until it drains.\n */\nexport interface QueuedMessage {\n id: string\n content: string | MultimodalContent\n createdAt: number\n}\n\n/**\n * Declarative queue policy.\n */\nexport interface QueueConfig {\n /**\n * Action when the client is busy (streaming, claiming a send, or draining).\n * Default `'queue'`.\n */\n whenBusy?: WhenBusy\n /**\n * How queued items leave the queue.\n * - `'fifo'`: one at a time, in order (default).\n * - `'batch'`: merge all queued items into one send when the run settles\n * successfully.\n */\n drain?: 'fifo' | 'batch'\n /** Max queued items. Unlimited when omitted. `0` means never queue. */\n maxSize?: number\n /**\n * Behavior when `maxSize` is reached. Default `'reject'`.\n * `'reject'` silently discards the new send (does not throw);\n * `'drop-oldest'` evicts the oldest queued item to make room.\n * Only meaningful when `maxSize` is set.\n */\n onOverflow?: 'reject' | 'drop-oldest'\n}\n\n/**\n * Escape hatch: decide the action for a single send. Drain stays FIFO for the\n * function form (no `batch` via function). Per-call `sendOptions.whenBusy`\n * overrides the strategy for that send.\n *\n * Actions match {@link WhenBusy}. Concurrent streams are not supported.\n * `pending.id` is the id that will be stored if the action is `'queue'`\n * (safe to pass to `cancelQueued`).\n */\nexport type QueueStrategy = (ctx: {\n pending: QueuedMessage\n busyReason: QueueBusyReason\n queued: ReadonlyArray<QueuedMessage>\n}) => { action: WhenBusy }\n\n/** A `WhenBusy` shorthand, a full config, or a strategy function. */\nexport type QueueOption = WhenBusy | QueueConfig | QueueStrategy\n\n/** Per-call overrides for `sendMessage`. */\nexport interface SendMessageOptions {\n /** Overrides the configured `whenBusy` for this one send. */\n whenBusy?: WhenBusy\n /**\n * Extra JSON merged into this request's wire `forwardedProps`.\n * Shallow merge: `{ ...chatBody, ...positionalBody, ...body }`.\n * This field wins on key collisions.\n *\n * Framework hooks (`useChat`, `injectChat`, `createChat`) expose\n * `sendMessage(content, options)` with no positional body, so this field\n * is the per-call body channel on those surfaces.\n */\n body?: Record<string, any>\n}\n\n/**\n * Message parts - building blocks of UIMessage\n */\nexport interface TextPart {\n type: 'text'\n content: string\n}\n\n/**\n * Helper type that creates a tool-call part for a specific tool.\n * This is a conditional type to enable proper distribution over union types,\n * creating a discriminated union where `name` is the discriminant.\n */\ntype ToolCallPartForTool<T> = T extends AnyClientTool\n ? {\n type: 'tool-call'\n id: string\n name: T['name']\n arguments: string // JSON string (may be incomplete)\n /** Parsed tool input (typed from inputSchema) */\n input?: InferToolInput<T>\n state: ToolCallState\n /** Tool execution output (for client tools or after approval) */\n output?: InferToolOutput<T>\n } & (NonNullable<T['needsApproval']> extends true\n ? {\n /**\n * Approval metadata — present only on tools defined with\n * `needsApproval: true`. Populated once the call reaches\n * `state: 'approval-requested'`. `needsApproval` is an optional\n * property on the tool, so we index into it (rather than\n * `T extends { needsApproval: true }`, which an optional property\n * never satisfies) and strip `undefined` before comparing to `true`.\n */\n approval?: {\n id: string // Unique approval ID\n needsApproval: boolean // Always true if present\n approved?: boolean // User's decision (undefined until responded)\n }\n }\n : // Tools without `needsApproval: true` never carry an approval field.\n // `& unknown` is a no-op intersection (adds nothing).\n unknown)\n : never\n\n/**\n * Fallback tool-call part type when tools are not typed\n */\ntype UntypedToolCallPart = {\n type: 'tool-call'\n id: string\n name: string\n arguments: string\n input?: any\n state: ToolCallState\n approval?: {\n id: string\n needsApproval: boolean\n approved?: boolean\n }\n output?: any\n}\n\n/**\n * Tool call part that creates a proper discriminated union.\n * When TTools is typed, checking `part.name === 'toolName'` will narrow\n * `part.output` to the correct type for that tool.\n *\n * The discriminant is `name`, so code like:\n * ```ts\n * if (part.name === 'recommendGuitar') {\n * // part.output is now typed to the recommendGuitar tool's output\n * }\n * ```\n */\nexport type ToolCallPart<TTools extends ReadonlyArray<AnyClientTool> = any> =\n // Check if we have a concrete tools array (not 'any' or 'never')\n [TTools] extends [never]\n ? UntypedToolCallPart\n : unknown extends TTools\n ? UntypedToolCallPart\n : TTools extends ReadonlyArray<infer Tool>\n ? Tool extends AnyClientTool\n ? ToolCallPartForTool<Tool>\n : UntypedToolCallPart\n : UntypedToolCallPart\n\nexport interface ToolResultPart {\n type: 'tool-result'\n id?: string\n name?: string\n toolCallId: string\n content: string | Array<ContentPart>\n state: ToolResultState\n error?: string // Error message if state is \"error\"\n metadata?: Record<string, unknown>\n createdAt?: Date\n}\n\nexport interface ThinkingPart {\n type: 'thinking'\n content: string\n}\n\nexport type MessagePart<\n TTools extends ReadonlyArray<AnyClientTool> = any,\n TData = unknown,\n> =\n | TextPart\n | ImagePart\n | AudioPart\n | VideoPart\n | DocumentPart\n | ToolCallPart<TTools>\n | ToolResultPart\n | ThinkingPart\n | StructuredOutputPart<TData>\n | UIResourcePart\n\n/**\n * UIMessage - Domain-specific message format optimized for building chat UIs\n * Contains parts that can be text, tool calls, or tool results.\n *\n * `TTools` narrows the tool-call/result part types based on the registered\n * tools. `TData` is the schema-inferred type for any `structured-output` part\n * on the message — defaulted to `unknown` so untyped consumers (the core\n * stream processor, the wire converter) don't need to thread a schema generic\n * everywhere; the hook layer (`useChat({ outputSchema })`) substitutes it on\n * the public return so `m.parts.find(p => p.type === 'structured-output').data`\n * is typed without manual casts.\n */\nexport interface UIMessage<\n TTools extends ReadonlyArray<AnyClientTool> = any,\n TData = unknown,\n> {\n id: string\n role: 'system' | 'user' | 'assistant'\n name?: string\n parts: Array<MessagePart<TTools, TData>>\n createdAt?: Date\n /**\n * Optional AG-UI metadata bag. TanStack writes the `tanstack` key.\n * User keys stay at the top.\n */\n metadata?: Record<string, any>\n}\n\n/**\n * A generic key/value storage adapter. `getItem` may be sync or async; the\n * chat persistence layer treats every call as best-effort. The provided\n * `localStoragePersistence` / `sessionStoragePersistence` / `indexedDBPersistence`\n * factories return one of these, and `ChatStorageAdapter<ChatPersistedState>`\n * is assignable to {@link ChatClientPersistence}.\n */\nexport interface ChatStorageAdapter<TValue> {\n getItem: (\n id: string,\n ) => TValue | null | undefined | Promise<TValue | null | undefined>\n setItem: (id: string, value: TValue) => void | Promise<void>\n removeItem: (id: string) => void | Promise<void>\n}\n\n/**\n * The single record a `ChatClientPersistence` adapter stores per chat. It folds\n * the two things that must survive a full page reload into one blob under one\n * key: the message transcript and the optional resume snapshot (which run to\n * rejoin / which interrupts to rehydrate). One adapter, one key — see\n * {@link ChatClientPersistence}.\n */\nexport interface ChatPersistedState<\n TTools extends ReadonlyArray<AnyClientTool> = any,\n> {\n messages: Array<UIMessage<TTools>>\n /** Present while a run is in flight or paused on an interrupt; absent otherwise. */\n resume?: ChatResumeSnapshot\n}\n\n/**\n * Storage adapter for durable chat state. A single adapter persists both the\n * message transcript and the resume snapshot as one {@link ChatPersistedState}\n * record, so a full page reload restores the conversation AND can rejoin an\n * in-flight run / rehydrate pending interrupts.\n *\n * For backward compatibility `getItem` may also return a bare `UIMessage[]`\n * (the legacy messages-only format); the client normalizes it to\n * `{ messages }`. `setItem` always writes the combined record.\n */\nexport interface ChatClientPersistence<\n TTools extends ReadonlyArray<AnyClientTool> = any,\n> {\n getItem: (\n id: string,\n ) =>\n | ChatPersistedState<TTools>\n | Array<UIMessage<TTools>>\n | null\n | undefined\n | Promise<\n ChatPersistedState<TTools> | Array<UIMessage<TTools>> | null | undefined\n >\n setItem: (\n id: string,\n state: ChatPersistedState<TTools>,\n ) => void | Promise<void>\n removeItem: (id: string) => void | Promise<void>\n}\n\n/**\n * The `persistence` option for a chat.\n *\n * - `false` (default): ephemeral. Messages live in memory only; a reload starts\n * from empty.\n * - `true`: server-authoritative. Nothing is cached in the browser. On mount the\n * client hydrates the thread from the server by its `threadId` (paints the\n * stored transcript and tails any run still generating), so a reload or the\n * same thread opened on another device both just resume. Requires a connection\n * with a `hydrate` handler.\n * - a {@link ChatClientPersistence} adapter: client-authoritative. The combined\n * {@link ChatPersistedState} record (transcript plus resume pointer) is cached\n * in the browser and restored on reload with no network.\n */\nexport type ChatPersistenceOption<\n TTools extends ReadonlyArray<AnyClientTool> = any,\n> = boolean | ChatClientPersistence<TTools>\n\n/**\n * The `persistence` / `threadId` pairing for `ChatClient` and the chat hooks.\n *\n * Persistence that is on (`true` or a storage adapter) requires a `threadId`.\n * A minted id changes every reload, so nothing would restore. The compiler\n * asks for the conversation id instead.\n *\n * Omit `persistence`, or set it to `false`, and `threadId` stays optional.\n * The client then mints one after mount for the wire and DevTools.\n *\n * Intersect this onto `ChatClientOptions`. Do not apply a later plain `Omit`\n * to that type: it collapses the union and the requirement disappears. Use\n * {@link DistributedOmit}.\n */\nexport type ChatPersistenceOptions<\n TTools extends ReadonlyArray<AnyClientTool> = any,\n> =\n | {\n persistence: true\n threadId: string\n }\n | {\n persistence: ChatClientPersistence<TTools>\n threadId: string\n }\n | {\n persistence?: false | undefined\n threadId?: string\n }\n\ntype IsUnknown<T> = unknown extends T\n ? [T] extends [unknown]\n ? true\n : false\n : false\n\ntype KnownContext<T> = IsUnknown<T> extends true ? never : T\n\ntype MergeContext<TLeft, TRight> = [TLeft] extends [never]\n ? TRight\n : [TRight] extends [never]\n ? TLeft\n : TLeft & TRight\n\ntype UnionToIntersection<T> = [T] extends [never]\n ? never\n : (T extends unknown ? (value: T) => void : never) extends (\n value: infer TIntersection,\n ) => void\n ? TIntersection\n : never\n\ntype DefinedContext<T> = Exclude<T, undefined>\n\ntype ContextFromExecute<T> = T extends (...args: any) => any\n ? NonNullable<Parameters<T>[1]> extends { context: infer TContext }\n ? KnownContext<TContext>\n : never\n : never\n\ntype ContextFromClientTool<T> = T extends AnyClientTool\n ? T extends { execute?: infer TExecute }\n ? ContextFromExecute<TExecute>\n : never\n : never\n\ntype RequiredContextFromClientToolUnion<T> = T extends unknown\n ? undefined extends ContextFromClientTool<T>\n ? never\n : ContextFromClientTool<T>\n : never\n\ntype ContextFromClientToolUnion<T> = [\n UnionToIntersection<DefinedContext<ContextFromClientTool<T>>>,\n] extends [never]\n ? never\n : [RequiredContextFromClientToolUnion<T>] extends [never]\n ? UnionToIntersection<DefinedContext<ContextFromClientTool<T>>> | undefined\n : UnionToIntersection<DefinedContext<ContextFromClientTool<T>>>\n\ntype ContextFromClientTools<TTools> =\n IsUnknown<TTools> extends true\n ? never\n : TTools extends readonly [infer THead, ...infer TTail]\n ? MergeContext<\n ContextFromClientTool<THead>,\n ContextFromClientTools<TTail>\n >\n : TTools extends ReadonlyArray<infer TItem>\n ? ContextFromClientToolUnion<TItem>\n : never\n\nexport type InferredClientContext<TTools> = [\n ContextFromClientTools<TTools>,\n] extends [never]\n ? unknown\n : ContextFromClientTools<TTools>\n\nexport type ClientContextOptionFromTools<TTools, TContext> = [\n ContextFromClientTools<TTools>,\n] extends [never]\n ? { context?: TContext }\n : undefined extends ContextFromClientTools<TTools>\n ? { context?: TContext & ContextFromClientTools<TTools> }\n : { context: TContext & ContextFromClientTools<TTools> }\n\n/**\n * Base options for `ChatClient`, excluding the transport (`connection` or\n * `fetcher`) which is supplied separately via `ChatTransport` so the XOR\n * is preserved when composing the final `ChatClientOptions` type.\n */\nexport interface ChatClientBaseOptions<\n TTools extends ReadonlyArray<AnyClientTool> = any,\n TContext = unknown,\n TInterrupts extends ReadonlyArray<InterruptDefinition<any, any, any, any>> =\n readonly [],\n> {\n /**\n * Initial messages to populate the chat\n */\n initialMessages?: Array<UIMessage<TTools>>\n\n /**\n * Initial resumable run state, useful when rehydrating a persisted client\n * after a full page reload. This restores the client-side interrupt\n * descriptors needed to send AG-UI resume entries.\n */\n initialResumeSnapshot?: ChatResumeSnapshot\n\n /**\n * Arbitrary client-controlled JSON forwarded to the server in the\n * AG-UI `RunAgentInput.forwardedProps` field. Use this for per-session\n * options like provider/model selection or feature flags that the\n * server endpoint should read.\n *\n * Replaces the legacy `body` option. If both are provided,\n * `forwardedProps` wins on key collision.\n */\n forwardedProps?: Record<string, any>\n\n /**\n * @deprecated Use `forwardedProps` instead. `body` continues to work\n * unchanged — its values are merged into the AG-UI\n * `RunAgentInput.forwardedProps` field on the wire and are also\n * mirrored under the legacy `data` field for servers that have not\n * migrated yet. Will be removed in a future major release.\n */\n body?: Record<string, any>\n\n /**\n * Optional BYOK keyring. On each send the client prepares the resolved\n * provider and stamps `x-byok-*` request headers. Keys never go in the body.\n */\n byok?: ByokClient\n\n /**\n * Optional provider id for this chat. If it returns a provider slug,\n * only that key is prepared and sent. Otherwise the merged `provider`\n * from `forwardedProps`, `body`, and per-call `sendMessage` `body` is\n * used. Later sources win. If no slug resolves, the send throws\n * instead of attaching every stored key.\n */\n byokProvider?: () => string | undefined\n\n /**\n * Client-local runtime context passed to client tool implementations.\n *\n * This value is not serialized to the server. Use `forwardedProps` for\n * explicit client-to-server handoff of serializable values.\n */\n context?: TContext\n\n /**\n * Callback when a response is received\n */\n onResponse?: (response?: Response) => void | Promise<void>\n\n /**\n * Callback when a stream chunk is received\n */\n onChunk?: (chunk: StreamChunk) => void\n\n /**\n * Callback when the response is finished\n */\n onFinish?: (message: UIMessage<TTools>) => void\n\n /**\n * Callback when an error occurs\n */\n onError?: (error: Error) => void\n\n /**\n * Callback when messages change\n */\n onMessagesChange?: (messages: Array<UIMessage<TTools>>) => void\n\n /**\n * Callback when loading state changes\n */\n onLoadingChange?: (isLoading: boolean) => void\n\n /**\n * Callback when error state changes\n */\n onErrorChange?: (error: Error | undefined) => void\n\n /**\n * Callback when chat status changes\n */\n onStatusChange?: (status: ChatClientState) => void\n\n /**\n * Callback when subscription lifecycle changes.\n * This is independent from request lifecycle (`isLoading`, `status`).\n */\n onSubscriptionChange?: (isSubscribed: boolean) => void\n\n /**\n * Callback when connection lifecycle changes.\n */\n onConnectionStatusChange?: (status: ConnectionStatus) => void\n\n /**\n * Callback when session generation activity changes.\n * Derived from stream run events (RUN_STARTED / RUN_FINISHED / RUN_ERROR).\n * Unlike `onLoadingChange` (request-local), this reflects shared generation\n * activity visible to all subscribers (e.g. across tabs/devices).\n */\n onSessionGeneratingChange?: (isGenerating: boolean) => void\n\n /**\n * Policy for messages sent while the client is busy (streaming, claiming\n * a send, or draining the queue). Accepts a `WhenBusy` string, a\n * `QueueConfig`, or a `QueueStrategy` function.\n * Default: `{ whenBusy: 'queue', drain: 'fifo' }`.\n * Queued items auto-send only after a **successful** settle; they are\n * discarded on error/abort, `stop()`, `clear()`, `unsubscribe()`, and\n * `reload()`.\n */\n queue?: QueueOption\n\n /**\n * Callback when the pending send queue changes (enqueue, cancel, drain,\n * or flush).\n */\n onQueueChange?: (queue: Array<QueuedMessage>) => void\n\n /**\n * Callback when resumable run state or pending interrupts change.\n */\n onResumeStateChange?: (\n resumeState: ChatResumeState | null,\n pendingInterrupts: BoundInterrupts<TTools, TInterrupts>,\n ) => void\n\n /**\n * Callback when the id of the run this client has in flight changes: the new\n * id when a run starts (a send, or a `joinRun` rejoin), `null` when it settles.\n */\n onRunIdChange?: (runId: string | null) => void\n\n /**\n * Callback when the immutable interrupt state snapshot changes.\n * Snapshot restoration passes `{ source: 'hydrate' }`; streamed and\n * client-initiated updates pass `{ source: 'live' }`.\n */\n onInterruptStateChange?: (\n state: ChatInterruptState<TTools, TInterrupts>,\n context: { source: 'hydrate' | 'live' },\n ) => void\n\n /**\n * Callback when a custom event is received from a server-side tool.\n * Custom events are emitted by tools using `context.emitCustomEvent()` during execution.\n *\n * @param eventType - The name of the custom event\n * @param data - The event payload data\n * @param context - Additional context including the toolCallId that emitted the event\n */\n onCustomEvent?: (\n eventType: string,\n data: unknown,\n context: { toolCallId?: string },\n ) => void\n\n /**\n * Client-side tools with execution logic\n * When provided, tools with execute functions will be called automatically\n */\n tools?: TTools\n\n /** First-party generic interrupts this client can type and resolve. */\n interrupts?: TInterrupts\n\n /**\n * Devtools hook metadata for this client instance.\n */\n devtools?: Partial<AIDevtoolsClientMetadata>\n\n /**\n * Factory that constructs the devtools bridge. Default is a no-op\n * factory, which keeps `@tanstack/ai-client/devtools` (the heavy\n * bridge implementation) out of the main entry's bundle. Frameworks\n * that need live devtools should pass the real factory from\n * `@tanstack/ai-client/devtools`.\n */\n devtoolsBridgeFactory?: ChatDevtoolsBridgeFactory\n\n /**\n * Stream processing options (optional)\n * Configure chunking strategy\n */\n streamProcessor?: {\n /**\n * Strategy for when to emit text updates\n * Defaults to ImmediateStrategy (every chunk)\n */\n chunkStrategy?: ChunkStrategy\n }\n}\n\n/**\n * Options for `ChatClient`. Exactly one of `connection` or `fetcher` must be\n * provided — the type-level XOR is enforced via `ChatTransport`. Persistence\n * that is on requires a `threadId` via {@link ChatPersistenceOptions}.\n */\nexport type ChatClientOptions<\n TTools extends ReadonlyArray<AnyClientTool> = any,\n TContext = InferredClientContext<TTools>,\n TInterrupts extends ReadonlyArray<InterruptDefinition<any, any, any, any>> =\n readonly [],\n> = DistributedOmit<\n ChatClientBaseOptions<TTools, TContext, TInterrupts>,\n 'context'\n> &\n ClientContextOptionFromTools<TTools, TContext> &\n ChatTransport &\n ChatPersistenceOptions<TTools>\n\nexport interface ChatRequestBody {\n messages: Array<ModelMessage>\n data?: Record<string, any>\n}\n\n/**\n * Create a typed array of client tools with proper type inference.\n * This eliminates the need for `as const` when defining tool arrays.\n *\n * @example\n * ```ts\n * const tools = clientTools(\n * myTool1.client(() => result1),\n * myTool2.client(() => result2),\n * )\n *\n * // tools is now properly typed as a tuple with literal tool names\n * // This enables type narrowing when checking part.name === 'toolName'\n * ```\n */\nexport function clientTools<const T extends Array<AnyClientTool>>(\n ...tools: T\n): T {\n return tools\n}\n\n/**\n * Helper to create typed chat client options\n * Use this to get proper type inference for messages\n *\n * @example\n * ```ts\n * const tools = clientTools(myTool1, myTool2)\n *\n * const chatOptions = createChatClientOptions({\n * connection: fetchServerSentEvents('/api/chat'),\n * tools,\n * })\n *\n * type MyMessages = InferChatMessages<typeof chatOptions>\n * ```\n */\nexport function createChatClientOptions<\n const TTools extends ReadonlyArray<AnyClientTool>,\n TContext = InferredClientContext<TTools>,\n const TInterrupts extends ReadonlyArray<\n InterruptDefinition<any, any, any, any>\n > = readonly [],\n>(\n options: ChatClientOptions<TTools, TContext, TInterrupts>,\n): ChatClientOptions<TTools, TContext, TInterrupts> {\n return options\n}\n\n/**\n * Extract the message type from chat options\n *\n * @example\n * ```ts\n * const chatOptions = createChatClientOptions({\n * connection: fetchServerSentEvents('/api/chat'),\n * tools: [myTool1, myTool2],\n * })\n *\n * type MyMessages = InferChatMessages<typeof chatOptions>\n * // MyMessages is now Array<UIMessage<[typeof myTool1, typeof myTool2]>>\n * ```\n */\nexport type InferChatMessages<T> =\n T extends ChatClientOptions<infer TTools, any>\n ? Array<UIMessage<TTools>>\n : never\n"],"mappings":";;;;;;;;;;;;;;;;AAqkCA,SAAgB,YACd,GAAG,OACA;CACH,OAAO;AACT;;;;;;;;;;;;;;;;;AAkBA,SAAgB,wBAOd,SACkD;CAClD,OAAO;AACT"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tanstack/ai-client",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.29.0",
|
|
4
4
|
"description": "Framework-agnostic headless client for TanStack AI chat, realtime sessions, streaming transports, and media generations.",
|
|
5
5
|
"author": "Tanner Linsley",
|
|
6
6
|
"license": "MIT",
|
|
@@ -53,8 +53,8 @@
|
|
|
53
53
|
"src"
|
|
54
54
|
],
|
|
55
55
|
"dependencies": {
|
|
56
|
-
"@tanstack/ai": "^0.
|
|
57
|
-
"@tanstack/ai-event-client": "^0.
|
|
56
|
+
"@tanstack/ai": "^0.50.0",
|
|
57
|
+
"@tanstack/ai-event-client": "^0.11.0",
|
|
58
58
|
"@tanstack/ai-utils": "^0.4.0"
|
|
59
59
|
},
|
|
60
60
|
"devDependencies": {
|
package/src/chat-client.ts
CHANGED
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
} from './connection-adapters'
|
|
27
27
|
import { ChatPersistor } from './client-persistor'
|
|
28
28
|
import { ClearedStreamTracker } from './cleared-stream-tracker'
|
|
29
|
+
import { normalizeMessagesDates } from './message-date-normalizer'
|
|
29
30
|
import { InterruptManager } from './interrupt-manager'
|
|
30
31
|
import type {
|
|
31
32
|
AnyClientTool,
|
|
@@ -417,6 +418,11 @@ export class ChatClient<
|
|
|
417
418
|
private processingResolve: (() => void) | null = null
|
|
418
419
|
private errorReportedGeneration: number | null = null
|
|
419
420
|
private streamGeneration = 0
|
|
421
|
+
private continuationGeneration = 0
|
|
422
|
+
// Generation of the run that opened the current stream. Public
|
|
423
|
+
// `addToolResult` must use this, not the live counter: `stop()` increments
|
|
424
|
+
// the live counter, so a post-stop call would otherwise look current.
|
|
425
|
+
private streamContinuationGeneration = 0
|
|
420
426
|
// Tracks whether a queued checkForContinuation was skipped because
|
|
421
427
|
// continuationPending was true (chained approval scenario)
|
|
422
428
|
private continuationSkipped = false
|
|
@@ -737,6 +743,7 @@ export class ChatClient<
|
|
|
737
743
|
const clientTool = clientTools.get(args.toolName)
|
|
738
744
|
const executeFunc = clientTool?.execute
|
|
739
745
|
if (executeFunc) {
|
|
746
|
+
const continuationGeneration = this.continuationGeneration
|
|
740
747
|
// Capture the run context at execution-start so a tool whose
|
|
741
748
|
// result lands AFTER the originating run finishes still reports
|
|
742
749
|
// back against the originating run, not whatever run is
|
|
@@ -763,6 +770,7 @@ export class ChatClient<
|
|
|
763
770
|
state: 'output-available',
|
|
764
771
|
},
|
|
765
772
|
clientTool,
|
|
773
|
+
continuationGeneration,
|
|
766
774
|
runEventContext,
|
|
767
775
|
)
|
|
768
776
|
} catch (error: any) {
|
|
@@ -775,6 +783,7 @@ export class ChatClient<
|
|
|
775
783
|
errorText: error.message,
|
|
776
784
|
},
|
|
777
785
|
clientTool,
|
|
786
|
+
continuationGeneration,
|
|
778
787
|
runEventContext,
|
|
779
788
|
)
|
|
780
789
|
} finally {
|
|
@@ -1017,7 +1026,7 @@ export class ChatClient<
|
|
|
1017
1026
|
// A send may have started while the fetch was in flight — don't stomp it.
|
|
1018
1027
|
if (this.isLoading || this.abortController) return
|
|
1019
1028
|
if (result.messages.length > 0) {
|
|
1020
|
-
this.processor.setMessages(result.messages)
|
|
1029
|
+
this.processor.setMessages(normalizeMessagesDates(result.messages))
|
|
1021
1030
|
}
|
|
1022
1031
|
if (result.interrupts && result.interrupts.pending.length > 0) {
|
|
1023
1032
|
// Pending interrupt = the thread is paused awaiting a human decision, so
|
|
@@ -1304,6 +1313,21 @@ export class ChatClient<
|
|
|
1304
1313
|
): Promise<boolean> {
|
|
1305
1314
|
const target = state ?? this.lastResume
|
|
1306
1315
|
if (!target) return Promise.resolve(false)
|
|
1316
|
+
return this.resumeInterruptsUnsafeForGeneration(
|
|
1317
|
+
resume,
|
|
1318
|
+
target,
|
|
1319
|
+
this.continuationGeneration,
|
|
1320
|
+
)
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
private resumeInterruptsUnsafeForGeneration(
|
|
1324
|
+
resume: Array<RunAgentResumeItem>,
|
|
1325
|
+
target: ChatResumeState,
|
|
1326
|
+
continuationGeneration: number,
|
|
1327
|
+
): Promise<boolean> {
|
|
1328
|
+
if (continuationGeneration !== this.continuationGeneration) {
|
|
1329
|
+
return Promise.resolve(false)
|
|
1330
|
+
}
|
|
1307
1331
|
// Auto-executed client tools resolve during the parent stream's
|
|
1308
1332
|
// `pendingToolExecutions` wait — while `isLoading` is still true.
|
|
1309
1333
|
// Defer the child continuation until that stream settles so we do not
|
|
@@ -1312,7 +1336,13 @@ export class ChatClient<
|
|
|
1312
1336
|
return new Promise<boolean>((resolve, reject) => {
|
|
1313
1337
|
this.queuePostStreamAction(async () => {
|
|
1314
1338
|
try {
|
|
1315
|
-
resolve(
|
|
1339
|
+
resolve(
|
|
1340
|
+
await this.resumeInterruptsUnsafeForGeneration(
|
|
1341
|
+
resume,
|
|
1342
|
+
target,
|
|
1343
|
+
continuationGeneration,
|
|
1344
|
+
),
|
|
1345
|
+
)
|
|
1316
1346
|
} catch (error) {
|
|
1317
1347
|
reject(error)
|
|
1318
1348
|
}
|
|
@@ -1336,6 +1366,7 @@ export class ChatClient<
|
|
|
1336
1366
|
private async submitInterruptBatch(
|
|
1337
1367
|
submission: InterruptManagerSubmission,
|
|
1338
1368
|
): Promise<void> {
|
|
1369
|
+
const continuationGeneration = this.continuationGeneration
|
|
1339
1370
|
this.activeInterruptSubmission = submission
|
|
1340
1371
|
this.interruptSubmissionFailure = undefined
|
|
1341
1372
|
// Reflect approval decisions in the local message tree immediately so a
|
|
@@ -1347,15 +1378,21 @@ export class ChatClient<
|
|
|
1347
1378
|
const approvalId = resolution.interruptId
|
|
1348
1379
|
this.processor.addToolApprovalResponse(approvalId, approved)
|
|
1349
1380
|
}
|
|
1350
|
-
const resumed = await this.
|
|
1381
|
+
const resumed = await this.resumeInterruptsUnsafeForGeneration(
|
|
1351
1382
|
[...submission.resolutions],
|
|
1352
1383
|
{
|
|
1353
1384
|
threadId: submission.threadId,
|
|
1354
1385
|
runId: submission.interruptedRunId,
|
|
1355
1386
|
},
|
|
1387
|
+
continuationGeneration,
|
|
1356
1388
|
).finally(() => {
|
|
1357
|
-
this
|
|
1389
|
+
// Only clear if this resume still owns the client: `stop()` may have
|
|
1390
|
+
// invalidated it while the submission was settling.
|
|
1391
|
+
if (this.activeInterruptSubmission === submission) {
|
|
1392
|
+
this.activeInterruptSubmission = undefined
|
|
1393
|
+
}
|
|
1358
1394
|
})
|
|
1395
|
+
if (continuationGeneration !== this.continuationGeneration) return
|
|
1359
1396
|
const failure = this.takeInterruptSubmissionFailure()
|
|
1360
1397
|
if (failure !== undefined) {
|
|
1361
1398
|
throw { errors: failure.errors }
|
|
@@ -1678,6 +1715,7 @@ export class ChatClient<
|
|
|
1678
1715
|
// persisted pointer with the provider id — so a SECOND reload would
|
|
1679
1716
|
// `joinRun` an id the log isn't keyed by and never re-attach.
|
|
1680
1717
|
this.lastResume = { threadId: this.threadId, runId }
|
|
1718
|
+
this.streamContinuationGeneration = this.continuationGeneration
|
|
1681
1719
|
this.setIsLoading(true)
|
|
1682
1720
|
this.setStatus('streaming')
|
|
1683
1721
|
void (async () => {
|
|
@@ -2163,6 +2201,7 @@ export class ChatClient<
|
|
|
2163
2201
|
|
|
2164
2202
|
// Track generation so a superseded stream's cleanup doesn't clobber the new one
|
|
2165
2203
|
const generation = ++this.streamGeneration
|
|
2204
|
+
this.streamContinuationGeneration = this.continuationGeneration
|
|
2166
2205
|
// Native interrupt continuation is a fresh child run. The interrupted run
|
|
2167
2206
|
// is carried as parentRunId and the complete resolution batch as resume.
|
|
2168
2207
|
const resumeThreadId = this.pendingResumeThreadId
|
|
@@ -2506,9 +2545,14 @@ export class ChatClient<
|
|
|
2506
2545
|
* Stop the current stream
|
|
2507
2546
|
*/
|
|
2508
2547
|
stop(): void {
|
|
2548
|
+
// Invalidate deferred work from the stopped continuation.
|
|
2549
|
+
this.continuationGeneration++
|
|
2509
2550
|
const hadLocalStream = this.abortController !== null
|
|
2510
2551
|
this.cancelInFlightStream({ setReadyStatus: true })
|
|
2511
2552
|
this.discardPendingSends()
|
|
2553
|
+
this.lastResume = null
|
|
2554
|
+
this.activeInterruptSubmission = undefined
|
|
2555
|
+
this.interruptManager.reset()
|
|
2512
2556
|
if (hadLocalStream) {
|
|
2513
2557
|
this.resetSessionGenerating()
|
|
2514
2558
|
}
|
|
@@ -2552,12 +2596,17 @@ export class ChatClient<
|
|
|
2552
2596
|
*/
|
|
2553
2597
|
async addToolResult(result: ClientToolResult): Promise<void> {
|
|
2554
2598
|
const clientTool = this.clientToolsRef.current.get(result.tool)
|
|
2555
|
-
await this.addToolResultForClientTool(
|
|
2599
|
+
await this.addToolResultForClientTool(
|
|
2600
|
+
result,
|
|
2601
|
+
clientTool,
|
|
2602
|
+
this.streamContinuationGeneration,
|
|
2603
|
+
)
|
|
2556
2604
|
}
|
|
2557
2605
|
|
|
2558
2606
|
private async addToolResultForClientTool(
|
|
2559
2607
|
result: ClientToolResult,
|
|
2560
2608
|
clientTool: AnyClientTool | undefined,
|
|
2609
|
+
continuationGeneration: number,
|
|
2561
2610
|
context?: ChatClientRunEventContext,
|
|
2562
2611
|
): Promise<void> {
|
|
2563
2612
|
if (clientTool && result.state !== 'output-error') {
|
|
@@ -2584,6 +2633,8 @@ export class ChatClient<
|
|
|
2584
2633
|
context,
|
|
2585
2634
|
)
|
|
2586
2635
|
|
|
2636
|
+
if (continuationGeneration !== this.continuationGeneration) return
|
|
2637
|
+
|
|
2587
2638
|
// Always update local message state so the tool-call part is terminal in
|
|
2588
2639
|
// the UI even when the AG-UI interrupt path owns server continuation.
|
|
2589
2640
|
this.processor.addToolResult(
|
|
@@ -2609,7 +2660,11 @@ export class ChatClient<
|
|
|
2609
2660
|
|
|
2610
2661
|
// If stream is in progress, queue continuation check for after it ends
|
|
2611
2662
|
if (this.isLoading) {
|
|
2612
|
-
this.queuePostStreamAction(() =>
|
|
2663
|
+
this.queuePostStreamAction(() =>
|
|
2664
|
+
continuationGeneration === this.continuationGeneration
|
|
2665
|
+
? this.checkForContinuation()
|
|
2666
|
+
: Promise.resolve(),
|
|
2667
|
+
)
|
|
2613
2668
|
return
|
|
2614
2669
|
}
|
|
2615
2670
|
|
|
@@ -2689,7 +2744,11 @@ export class ChatClient<
|
|
|
2689
2744
|
* Queue an action to be executed after the current stream ends
|
|
2690
2745
|
*/
|
|
2691
2746
|
private queuePostStreamAction(action: () => Promise<void>): void {
|
|
2692
|
-
this.
|
|
2747
|
+
const continuationGeneration = this.continuationGeneration
|
|
2748
|
+
this.postStreamActions.push(async () => {
|
|
2749
|
+
if (continuationGeneration !== this.continuationGeneration) return
|
|
2750
|
+
await action()
|
|
2751
|
+
})
|
|
2693
2752
|
}
|
|
2694
2753
|
|
|
2695
2754
|
/**
|
|
@@ -2712,6 +2771,10 @@ export class ChatClient<
|
|
|
2712
2771
|
* Check if we should continue the flow and do so if needed
|
|
2713
2772
|
*/
|
|
2714
2773
|
private async checkForContinuation(): Promise<void> {
|
|
2774
|
+
// stop() bumps continuationGeneration without opening a new stream.
|
|
2775
|
+
if (this.streamContinuationGeneration !== this.continuationGeneration) {
|
|
2776
|
+
return
|
|
2777
|
+
}
|
|
2715
2778
|
if (this.hasPendingInterrupts()) return
|
|
2716
2779
|
|
|
2717
2780
|
// Prevent duplicate continuation attempts
|
|
@@ -20,6 +20,7 @@ import type {
|
|
|
20
20
|
UIMessage,
|
|
21
21
|
} from '@tanstack/ai/client'
|
|
22
22
|
import type { ChatFetcher, ChatPendingInterrupt } from './types'
|
|
23
|
+
import { normalizeMessagesDates } from './message-date-normalizer'
|
|
23
24
|
|
|
24
25
|
/**
|
|
25
26
|
* Associates connect-wrapped chunks with the run they were produced under.
|
|
@@ -573,7 +574,9 @@ async function fetchThreadHydration(
|
|
|
573
574
|
}
|
|
574
575
|
: null
|
|
575
576
|
return {
|
|
576
|
-
messages: Array.isArray(data.messages)
|
|
577
|
+
messages: Array.isArray(data.messages)
|
|
578
|
+
? normalizeMessagesDates(data.messages)
|
|
579
|
+
: [],
|
|
577
580
|
activeRun,
|
|
578
581
|
interrupts,
|
|
579
582
|
}
|
|
@@ -1224,7 +1227,7 @@ function buildRunAgentInputBody(
|
|
|
1224
1227
|
): Record<string, unknown> {
|
|
1225
1228
|
// Precedence (later spreads win): static adapter `body` is the base,
|
|
1226
1229
|
// overridden by `runContext.forwardedProps`, overridden by per-message `data`.
|
|
1227
|
-
const wireMessages = uiMessagesToWire(messages
|
|
1230
|
+
const wireMessages = uiMessagesToWire(messages)
|
|
1228
1231
|
const forwardedProps = {
|
|
1229
1232
|
...options.body,
|
|
1230
1233
|
...(runContext?.forwardedProps ?? {}),
|