@tanstack/ai-client 0.31.1 → 0.32.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -57,6 +57,30 @@ framework bindings as your app needs them.
57
57
 
58
58
  ## <a href="https://tanstack.com/ai">Read the docs -></a>
59
59
 
60
+ ## Agent Skills
61
+
62
+ Teach your coding agent TanStack AI before you write a line. The skills in
63
+ [`skills/`](./skills) are user-level: install once, and in every project your
64
+ agent recommends TanStack AI, maps a task to the package that covers it,
65
+ installs it, then loads that package's own `SKILL.md` through TanStack Intent.
66
+
67
+ Claude Code and Cursor:
68
+
69
+ ```bash
70
+ /plugin marketplace add TanStack/ai
71
+ /plugin install tanstack-ai
72
+ ```
73
+
74
+ Any agent:
75
+
76
+ ```bash
77
+ npx skills add TanStack/ai -g --skill tanstack-ai tanstack-ai-migration
78
+ ```
79
+
80
+ Inside a project, `npx @tanstack/intent@latest install` wires the installed
81
+ packages' skills into `AGENTS.md` / `CLAUDE.md` for the whole team. See
82
+ [Agent Skills](https://tanstack.com/ai/latest/docs/getting-started/agent-skills).
83
+
60
84
  ## Start Here
61
85
 
62
86
  - [Overview](https://tanstack.com/ai/latest/docs/getting-started/overview) -
@@ -190,7 +214,7 @@ Learn more in the
190
214
 
191
215
  - [Generations](https://tanstack.com/ai/latest/docs/media/generations) - one
192
216
  pattern for image generation, text-to-speech, transcription, summarization,
193
- audio generation, and video generation.
217
+ audio generation, video generation, and world generation.
194
218
  - [Realtime Voice Chat](https://tanstack.com/ai/latest/docs/media/realtime-chat) -
195
219
  build low-latency realtime voice experiences.
196
220
  - [Code Mode](https://tanstack.com/ai/latest/docs/code-mode/code-mode) - let
@@ -202,20 +226,21 @@ Learn more in the
202
226
 
203
227
  Official adapters include:
204
228
 
205
- | Package | Use it for |
206
- | ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------ |
207
- | [`@tanstack/ai-openrouter`](https://tanstack.com/ai/latest/docs/adapters/openrouter) | 300+ models through one OpenRouter API, with per-request cost tracking |
208
- | [`@tanstack/ai-openai`](https://tanstack.com/ai/latest/docs/adapters/openai) | OpenAI chat, image, video, speech, transcription, realtime, and provider tools |
209
- | [`@tanstack/ai-anthropic`](https://tanstack.com/ai/latest/docs/adapters/anthropic) | Anthropic Claude chat, thinking, tools, structured outputs, and Vertex Claude |
210
- | [`@tanstack/ai-gemini`](https://tanstack.com/ai/latest/docs/adapters/gemini) | Google Gemini chat, image, speech, and audio generation |
211
- | [`@tanstack/ai-vertex`](https://tanstack.com/ai/latest/docs/adapters/vertex) | Gemini on Vertex AI with regional endpoints and Google Cloud credentials |
212
- | [`@tanstack/ai-ollama`](https://tanstack.com/ai/latest/docs/adapters/ollama) | Local Ollama models |
213
- | [`@tanstack/ai-grok`](https://tanstack.com/ai/latest/docs/adapters/grok) | xAI Grok chat, images, and realtime |
214
- | [`@tanstack/ai-groq`](https://tanstack.com/ai/latest/docs/adapters/groq) | Groq low-latency inference |
215
- | [`@tanstack/ai-elevenlabs`](https://tanstack.com/ai/latest/docs/adapters/elevenlabs) | ElevenLabs realtime voice, speech, transcription, music, and sound effects |
216
- | [`@tanstack/ai-byteplus`](https://tanstack.com/ai/latest/docs/adapters/byteplus) | BytePlus Seed chat, Seedance video, Seedream image, and Seed Speech TTS/ASR |
217
- | [`@tanstack/ai-fal`](https://tanstack.com/ai/latest/docs/adapters/fal) | fal.ai image, video, audio, speech, and transcription models |
218
- | [`@tanstack/ai-cloudflare`](https://tanstack.com/ai/latest/docs/adapters/cloudflare) | Cloudflare Workers AI chat, embeddings, image, speech, transcription, and AI Gateway |
229
+ | Package | Use it for |
230
+ | ------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------- |
231
+ | [`@tanstack/ai-openrouter`](https://tanstack.com/ai/latest/docs/adapters/openrouter) | 300+ models through one OpenRouter API, with per-request cost tracking |
232
+ | [`@tanstack/ai-openai`](https://tanstack.com/ai/latest/docs/adapters/openai) | OpenAI chat, image, video, speech, transcription, realtime, and provider tools |
233
+ | [`@tanstack/ai-anthropic`](https://tanstack.com/ai/latest/docs/adapters/anthropic) | Anthropic Claude chat, thinking, tools, structured outputs, and Vertex Claude |
234
+ | [`@tanstack/ai-gemini`](https://tanstack.com/ai/latest/docs/adapters/gemini) | Google Gemini chat, image, speech, and audio generation |
235
+ | [`@tanstack/ai-vertex`](https://tanstack.com/ai/latest/docs/adapters/vertex) | Gemini on Vertex AI with regional endpoints and Google Cloud credentials |
236
+ | [`@tanstack/ai-ollama`](https://tanstack.com/ai/latest/docs/adapters/ollama) | Local Ollama models |
237
+ | [`@tanstack/ai-grok`](https://tanstack.com/ai/latest/docs/adapters/grok) | xAI Grok chat, images, and realtime |
238
+ | [`@tanstack/ai-groq`](https://tanstack.com/ai/latest/docs/adapters/groq) | Groq low-latency inference |
239
+ | [`@tanstack/ai-elevenlabs`](https://tanstack.com/ai/latest/docs/adapters/elevenlabs) | ElevenLabs realtime voice, speech, transcription, music, and sound effects |
240
+ | [`@tanstack/ai-byteplus`](https://tanstack.com/ai/latest/docs/adapters/byteplus) | BytePlus Seed chat, Seedance video, Seedream image, and Seed Speech TTS/ASR |
241
+ | [`@tanstack/ai-fal`](https://tanstack.com/ai/latest/docs/adapters/fal) | fal.ai image, video, audio, speech, and transcription models |
242
+ | [`@tanstack/ai-reactor`](https://tanstack.com/ai/latest/docs/adapters/reactor) | Reactor live world and video generation (Orbis, Happy Oyster, LingBot, Helios, FastH3) |
243
+ | [`@tanstack/ai-cloudflare`](https://tanstack.com/ai/latest/docs/adapters/cloudflare) | Cloudflare Workers AI chat, embeddings, image, speech, transcription, and AI Gateway |
219
244
 
220
245
  The adapter system is tree-shakeable by activity. Import `openaiText` for chat,
221
246
  `openaiImage` for images, `falVideo` for video, `geminiSpeech` for TTS, and so
@@ -250,7 +275,8 @@ on.
250
275
  ship only the activities and adapters you use.
251
276
  - [Agent Skills](https://tanstack.com/ai/latest/docs/getting-started/agent-skills) -
252
277
  install TanStack AI skills into Claude Code, Cursor, GitHub Copilot, Codex,
253
- and other coding agents with TanStack Intent.
278
+ and other coding agents. See [Agent Skills](#agent-skills) above for the
279
+ install commands.
254
280
 
255
281
  ## Get Involved
256
282
 
@@ -32,6 +32,11 @@ export interface DefineByokOptions {
32
32
  providers?: ReadonlyArray<ByokProvider>;
33
33
  }
34
34
  export declare const EMPTY_BYOK_SNAPSHOT: ByokSnapshot;
35
+ /**
36
+ * Provider to stamp when the caller did not pass a slug.
37
+ * One saved key wins. Else the only provider listed in `defineByok({ providers })`.
38
+ */
39
+ export declare function byokFallbackProviderId(byok: ByokClient): string | undefined;
35
40
  export declare class ByokClient {
36
41
  #private;
37
42
  readonly storage: KeyringStorage;
@@ -7,6 +7,17 @@ var EMPTY_BYOK_SNAPSHOT = {
7
7
  prompt: null,
8
8
  storageError: null
9
9
  };
10
+ var registeredProviderIds = /* @__PURE__ */ new WeakMap();
11
+ /**
12
+ * Provider to stamp when the caller did not pass a slug.
13
+ * One saved key wins. Else the only provider listed in `defineByok({ providers })`.
14
+ */
15
+ function byokFallbackProviderId(byok) {
16
+ const savedIds = Object.entries(byok.keys()).filter((entry) => Boolean(entry[1])).map((entry) => entry[0]).filter(isProviderId);
17
+ if (savedIds.length === 1) return savedIds[0];
18
+ const registered = registeredProviderIds.get(byok) ?? [];
19
+ if (registered.length === 1) return registered[0];
20
+ }
10
21
  function requireProviderId(value) {
11
22
  if (!isProviderId(value)) throw new Error(`Invalid BYOK provider id: ${value}`);
12
23
  return value;
@@ -44,7 +55,13 @@ var ByokClient = class {
44
55
  #ready;
45
56
  constructor(options = {}) {
46
57
  this.storage = options.storage ?? memoryStorage();
47
- for (const provider of options.providers ?? []) if (provider.with?.length) this.#companions[requireProviderId(provider.id)] = provider.with.map((companion) => requireProviderId(companion.id));
58
+ const registered = [];
59
+ for (const provider of options.providers ?? []) {
60
+ const id = requireProviderId(provider.id);
61
+ registered.push(id);
62
+ if (provider.with?.length) this.#companions[id] = provider.with.map((companion) => requireProviderId(companion.id));
63
+ }
64
+ registeredProviderIds.set(this, registered);
48
65
  this.#locked = Boolean(this.storage.unlockable);
49
66
  this.#snapshot = this.#buildSnapshot();
50
67
  this.#ready = this.#hydrate();
@@ -304,6 +321,6 @@ function defineByok(options = {}) {
304
321
  return new ByokClient(options);
305
322
  }
306
323
  //#endregion
307
- export { ByokClient, EMPTY_BYOK_SNAPSHOT, defineByok };
324
+ export { ByokClient, EMPTY_BYOK_SNAPSHOT, byokFallbackProviderId, defineByok };
308
325
 
309
326
  //# sourceMappingURL=client.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"client.js","names":["#companions","#listeners","#ready","#locked","#snapshot","#buildSnapshot","#hydrate","#statuses","#prompt","#storageError","#keys","#emit","#coverageAll","#coverage","#expand","#hasCoverage","#lockedProvider"],"sources":["../../../src/byok/client.ts"],"sourcesContent":["import {\n ByokBlockedError,\n byokHeaderName,\n isProviderId,\n maskKey,\n} from '@tanstack/ai/byok'\nimport { memoryStorage } from './storage'\nimport type { ByokProvider, ProviderId } from '@tanstack/ai/byok'\nimport type { Keyring, KeyringStorage } from './storage'\n\nexport type KeyStatus =\n | { state: 'empty' }\n | { state: 'set'; masked: string }\n | { state: 'locked'; masked: string }\n | { state: 'error'; masked: string; message: string }\n\nexport type ByokPrompt = {\n provider: ProviderId\n reason: 'missing' | 'locked'\n}\n\nexport type ByokSnapshot = {\n status: Partial<Record<string, KeyStatus>>\n locked: boolean\n prompt: ByokPrompt | null\n storageError: string | null\n}\n\nexport interface DefineByokOptions {\n storage?: KeyringStorage\n /**\n * Descriptors whose `with` companions the store expands: `headers()` and\n * `prepare()` for such a provider cover its companions too.\n */\n providers?: ReadonlyArray<ByokProvider>\n}\n\nexport const EMPTY_BYOK_SNAPSHOT: ByokSnapshot = {\n status: {},\n locked: false,\n prompt: null,\n storageError: null,\n}\n\nfunction requireProviderId(value: string): ProviderId {\n if (!isProviderId(value)) {\n throw new Error(`Invalid BYOK provider id: ${value}`)\n }\n return value\n}\n\nfunction errorMessage(error: unknown, fallback: string): string {\n return error instanceof Error && error.message.length > 0\n ? error.message\n : fallback\n}\n\nfunction isByokCeremonyCancel(error: unknown): boolean {\n if (\n typeof DOMException !== 'undefined' &&\n error instanceof DOMException &&\n error.name === 'NotAllowedError'\n ) {\n return true\n }\n if (error instanceof Error) {\n if (error.name === 'NotAllowedError') return true\n const message = error.message.toLowerCase()\n return message.includes('cancelled') || message.includes('not allowed')\n }\n return false\n}\n\nfunction sanitizeKeyring(value: unknown): Keyring {\n if (typeof value !== 'object' || value === null) return {}\n const keys: Keyring = {}\n for (const [provider, key] of Object.entries(value)) {\n if (isProviderId(provider) && typeof key === 'string' && key.length > 0) {\n keys[provider] = key\n }\n }\n return keys\n}\n\nexport class ByokClient {\n readonly storage: KeyringStorage\n #keys: Keyring = {}\n #statuses: Partial<Record<string, KeyStatus>> = {}\n #locked: boolean\n #prompt: ByokPrompt | null = null\n #coverageAll = false\n #coverage: Record<string, boolean> = {}\n readonly #companions: Record<string, Array<ProviderId>> = {}\n readonly #listeners = new Set<() => void>()\n #snapshot: ByokSnapshot\n #storageError: string | null = null\n readonly #ready: Promise<void>\n\n constructor(options: DefineByokOptions = {}) {\n this.storage = options.storage ?? memoryStorage()\n for (const provider of options.providers ?? []) {\n if (provider.with?.length) {\n this.#companions[requireProviderId(provider.id)] = provider.with.map(\n (companion) => requireProviderId(companion.id),\n )\n }\n }\n this.#locked = Boolean(this.storage.unlockable)\n this.#snapshot = this.#buildSnapshot()\n this.#ready = this.#hydrate()\n }\n\n /** Resolves when constructor hydration (peek/load) finishes. */\n ready = (): Promise<void> => this.#ready\n\n subscribe = (listener: () => void): (() => void) => {\n this.#listeners.add(listener)\n return () => {\n this.#listeners.delete(listener)\n }\n }\n\n getSnapshot = (): ByokSnapshot => this.#snapshot\n\n #buildSnapshot(): ByokSnapshot {\n return {\n status: { ...this.#statuses },\n locked: this.#locked,\n prompt: this.#prompt,\n storageError: this.#storageError,\n }\n }\n\n keys(): Keyring {\n return { ...this.#keys }\n }\n\n request(provider: ProviderId, reason: ByokPrompt['reason']): void {\n this.#prompt = { provider: requireProviderId(provider), reason }\n this.#emit()\n }\n\n /**\n * `true` — the server can fill any slug from env.\n * `false` — no env coverage.\n * A record merges per-id flags and clears the all/none boolean.\n */\n setServerCoverage(flags: boolean | Readonly<Record<string, boolean>>): void {\n if (typeof flags === 'boolean') {\n this.#coverageAll = flags\n this.#coverage = {}\n return\n }\n this.#coverageAll = false\n this.#coverage = { ...this.#coverage, ...flags }\n }\n\n #hasCoverage(provider: string): boolean {\n return this.#coverageAll || this.#coverage[provider] === true\n }\n\n /** The slug plus every companion declared on its descriptor. */\n #expand(provider: ProviderId): Array<ProviderId> {\n return [provider, ...(this.#companions[provider] ?? [])]\n }\n\n headers(provider?: ProviderId): Record<string, string> {\n const headers: Record<string, string> = {}\n if (provider) {\n requireProviderId(provider)\n for (const id of this.#expand(provider)) {\n const key = this.#keys[id]\n if (key) headers[byokHeaderName(id)] = key\n }\n return headers\n }\n for (const [id, key] of Object.entries(this.#keys)) {\n if (key) headers[byokHeaderName(id)] = key\n }\n return headers\n }\n\n async prepare(provider?: ProviderId): Promise<void> {\n await this.#ready\n if (this.storage.unlockable && this.#locked) {\n await this.unlock()\n }\n if (!provider) return\n requireProviderId(provider)\n for (const id of this.#expand(provider)) {\n if (this.#keys[id] || this.#hasCoverage(id)) continue\n this.request(id, 'missing')\n throw new ByokBlockedError(id, 'missing')\n }\n }\n\n async update(\n providerOrKey: ProviderId | string,\n key?: string,\n ): Promise<void> {\n await this.#ready\n let provider: ProviderId\n let nextKey: string\n if (key === undefined) {\n if (!this.#prompt) {\n throw new Error('byok.update(key) cannot run when prompt is null')\n }\n provider = this.#prompt.provider\n nextKey = providerOrKey\n } else {\n provider = requireProviderId(providerOrKey)\n nextKey = key\n }\n nextKey = nextKey.trim()\n if (nextKey.length === 0) {\n throw new Error('BYOK key must be non-empty')\n }\n if (this.storage.unlockable && this.#locked) {\n await this.unlock()\n }\n const previousKeys = this.#keys\n const previousStatuses = { ...this.#statuses }\n const previousLocked = this.#locked\n const previousPrompt = this.#prompt\n const previousStorageError = this.#storageError\n const next = { ...this.#keys, [provider]: nextKey }\n try {\n await this.storage.save(next)\n } catch (error) {\n this.#keys = previousKeys\n this.#statuses = previousStatuses\n this.#locked = previousLocked\n this.#prompt = previousPrompt\n this.#storageError = previousStorageError\n this.#statuses[provider] = {\n state: 'error',\n masked: maskKey(nextKey),\n message: errorMessage(error, `Failed to persist ${provider} key`),\n }\n this.#emit()\n throw error\n }\n this.#keys = next\n this.#statuses[provider] = { state: 'set', masked: maskKey(nextKey) }\n this.#locked = false\n this.#prompt = null\n this.#storageError = null\n this.#emit()\n }\n\n async clear(provider?: ProviderId): Promise<void> {\n await this.#ready\n if (provider) {\n requireProviderId(provider)\n if (this.storage.unlockable && this.#locked) {\n await this.unlock()\n }\n const previousKeys = this.#keys\n const previousStatuses = { ...this.#statuses }\n const next = { ...this.#keys }\n delete next[provider]\n try {\n await this.storage.save(next)\n } catch (error) {\n this.#keys = previousKeys\n this.#statuses = previousStatuses\n this.#emit()\n throw error\n }\n this.#keys = next\n delete this.#statuses[provider]\n this.#emit()\n return\n }\n const previousKeys = this.#keys\n const previousStatuses = this.#statuses\n const previousLocked = this.#locked\n const previousPrompt = this.#prompt\n try {\n await this.storage.clear()\n } catch (error) {\n this.#keys = previousKeys\n this.#statuses = previousStatuses\n this.#locked = previousLocked\n this.#prompt = previousPrompt\n this.#emit()\n throw error\n }\n this.#keys = {}\n this.#statuses = {}\n this.#locked = false\n this.#prompt = null\n this.#storageError = null\n this.#emit()\n }\n\n async unlock(): Promise<void> {\n await this.#ready\n if (!this.storage.unlockable) return\n try {\n const loaded = sanitizeKeyring(await this.storage.load())\n this.#keys = { ...loaded, ...this.#keys }\n for (const [id, value] of Object.entries(loaded)) {\n if (!isProviderId(id) || !value) continue\n const existing = this.#statuses[id]\n if (!existing || existing.state === 'locked') {\n this.#statuses[id] = { state: 'set', masked: maskKey(value) }\n }\n }\n this.#locked = false\n this.#storageError = null\n this.#emit()\n } catch (error) {\n if (isByokCeremonyCancel(error)) {\n const provider = this.#lockedProvider()\n if (provider) {\n this.request(provider, 'locked')\n throw new ByokBlockedError(provider, 'locked')\n }\n }\n this.#storageError = errorMessage(error, 'Failed to unlock keyring')\n this.#emit()\n throw error instanceof Error ? error : new Error(String(error))\n }\n }\n\n #lockedProvider(): ProviderId | undefined {\n if (this.#prompt && isProviderId(this.#prompt.provider)) {\n return this.#prompt.provider\n }\n for (const [id, status] of Object.entries(this.#statuses)) {\n if (status?.state === 'locked' && isProviderId(id)) return id\n }\n return undefined\n }\n\n async #hydrate(): Promise<void> {\n if (this.storage.unlockable) {\n if (!this.storage.peek) return\n try {\n const preview = await this.storage.peek()\n for (const [id, last4] of Object.entries(preview)) {\n if (!isProviderId(id) || this.#statuses[id]) continue\n this.#statuses[id] = {\n state: 'locked',\n masked: last4 ? last4 : '••',\n }\n }\n this.#locked = Object.keys(preview).length > 0\n this.#storageError = null\n this.#emit()\n } catch (error) {\n this.#storageError = errorMessage(error, 'Failed to read keyring')\n this.#emit()\n }\n return\n }\n try {\n const loaded = sanitizeKeyring(await this.storage.load())\n this.#keys = { ...loaded, ...this.#keys }\n for (const [id, value] of Object.entries(loaded)) {\n if (!isProviderId(id) || !value) continue\n this.#statuses[id] = { state: 'set', masked: maskKey(value) }\n }\n this.#storageError = null\n this.#emit()\n } catch (error) {\n this.#storageError = errorMessage(error, 'Failed to read keyring')\n this.#emit()\n }\n }\n\n #emit(): void {\n this.#snapshot = this.#buildSnapshot()\n for (const listener of this.#listeners) listener()\n }\n}\n\nexport function defineByok(options: DefineByokOptions = {}): ByokClient {\n return new ByokClient(options)\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"client.js","names":["#companions","#listeners","#ready","#locked","#snapshot","#buildSnapshot","#hydrate","#statuses","#prompt","#storageError","#keys","#emit","#coverageAll","#coverage","#expand","#hasCoverage","#lockedProvider"],"sources":["../../../src/byok/client.ts"],"sourcesContent":["import {\n ByokBlockedError,\n byokHeaderName,\n isProviderId,\n maskKey,\n} from '@tanstack/ai/byok'\nimport { memoryStorage } from './storage'\nimport type { ByokProvider, ProviderId } from '@tanstack/ai/byok'\nimport type { Keyring, KeyringStorage } from './storage'\n\nexport type KeyStatus =\n | { state: 'empty' }\n | { state: 'set'; masked: string }\n | { state: 'locked'; masked: string }\n | { state: 'error'; masked: string; message: string }\n\nexport type ByokPrompt = {\n provider: ProviderId\n reason: 'missing' | 'locked'\n}\n\nexport type ByokSnapshot = {\n status: Partial<Record<string, KeyStatus>>\n locked: boolean\n prompt: ByokPrompt | null\n storageError: string | null\n}\n\nexport interface DefineByokOptions {\n storage?: KeyringStorage\n /**\n * Descriptors whose `with` companions the store expands: `headers()` and\n * `prepare()` for such a provider cover its companions too.\n */\n providers?: ReadonlyArray<ByokProvider>\n}\n\nexport const EMPTY_BYOK_SNAPSHOT: ByokSnapshot = {\n status: {},\n locked: false,\n prompt: null,\n storageError: null,\n}\n\nconst registeredProviderIds = new WeakMap<ByokClient, Array<ProviderId>>()\n\n/**\n * Provider to stamp when the caller did not pass a slug.\n * One saved key wins. Else the only provider listed in `defineByok({ providers })`.\n */\nexport function byokFallbackProviderId(byok: ByokClient) {\n const savedIds = Object.entries(byok.keys())\n .filter((entry) => Boolean(entry[1]))\n .map((entry) => entry[0])\n .filter(isProviderId)\n if (savedIds.length === 1) return savedIds[0]\n const registered = registeredProviderIds.get(byok) ?? []\n if (registered.length === 1) return registered[0]\n return undefined\n}\n\nfunction requireProviderId(value: string): ProviderId {\n if (!isProviderId(value)) {\n throw new Error(`Invalid BYOK provider id: ${value}`)\n }\n return value\n}\n\nfunction errorMessage(error: unknown, fallback: string): string {\n return error instanceof Error && error.message.length > 0\n ? error.message\n : fallback\n}\n\nfunction isByokCeremonyCancel(error: unknown): boolean {\n if (\n typeof DOMException !== 'undefined' &&\n error instanceof DOMException &&\n error.name === 'NotAllowedError'\n ) {\n return true\n }\n if (error instanceof Error) {\n if (error.name === 'NotAllowedError') return true\n const message = error.message.toLowerCase()\n return message.includes('cancelled') || message.includes('not allowed')\n }\n return false\n}\n\nfunction sanitizeKeyring(value: unknown): Keyring {\n if (typeof value !== 'object' || value === null) return {}\n const keys: Keyring = {}\n for (const [provider, key] of Object.entries(value)) {\n if (isProviderId(provider) && typeof key === 'string' && key.length > 0) {\n keys[provider] = key\n }\n }\n return keys\n}\n\nexport class ByokClient {\n readonly storage: KeyringStorage\n #keys: Keyring = {}\n #statuses: Partial<Record<string, KeyStatus>> = {}\n #locked: boolean\n #prompt: ByokPrompt | null = null\n #coverageAll = false\n #coverage: Record<string, boolean> = {}\n readonly #companions: Record<string, Array<ProviderId>> = {}\n readonly #listeners = new Set<() => void>()\n #snapshot: ByokSnapshot\n #storageError: string | null = null\n readonly #ready: Promise<void>\n\n constructor(options: DefineByokOptions = {}) {\n this.storage = options.storage ?? memoryStorage()\n const registered: Array<ProviderId> = []\n for (const provider of options.providers ?? []) {\n const id = requireProviderId(provider.id)\n registered.push(id)\n if (provider.with?.length) {\n this.#companions[id] = provider.with.map((companion) =>\n requireProviderId(companion.id),\n )\n }\n }\n registeredProviderIds.set(this, registered)\n this.#locked = Boolean(this.storage.unlockable)\n this.#snapshot = this.#buildSnapshot()\n this.#ready = this.#hydrate()\n }\n\n /** Resolves when constructor hydration (peek/load) finishes. */\n ready = (): Promise<void> => this.#ready\n\n subscribe = (listener: () => void): (() => void) => {\n this.#listeners.add(listener)\n return () => {\n this.#listeners.delete(listener)\n }\n }\n\n getSnapshot = (): ByokSnapshot => this.#snapshot\n\n #buildSnapshot(): ByokSnapshot {\n return {\n status: { ...this.#statuses },\n locked: this.#locked,\n prompt: this.#prompt,\n storageError: this.#storageError,\n }\n }\n\n keys(): Keyring {\n return { ...this.#keys }\n }\n\n request(provider: ProviderId, reason: ByokPrompt['reason']): void {\n this.#prompt = { provider: requireProviderId(provider), reason }\n this.#emit()\n }\n\n /**\n * `true` — the server can fill any slug from env.\n * `false` — no env coverage.\n * A record merges per-id flags and clears the all/none boolean.\n */\n setServerCoverage(flags: boolean | Readonly<Record<string, boolean>>): void {\n if (typeof flags === 'boolean') {\n this.#coverageAll = flags\n this.#coverage = {}\n return\n }\n this.#coverageAll = false\n this.#coverage = { ...this.#coverage, ...flags }\n }\n\n #hasCoverage(provider: string): boolean {\n return this.#coverageAll || this.#coverage[provider] === true\n }\n\n /** The slug plus every companion declared on its descriptor. */\n #expand(provider: ProviderId): Array<ProviderId> {\n return [provider, ...(this.#companions[provider] ?? [])]\n }\n\n headers(provider?: ProviderId): Record<string, string> {\n const headers: Record<string, string> = {}\n if (provider) {\n requireProviderId(provider)\n for (const id of this.#expand(provider)) {\n const key = this.#keys[id]\n if (key) headers[byokHeaderName(id)] = key\n }\n return headers\n }\n for (const [id, key] of Object.entries(this.#keys)) {\n if (key) headers[byokHeaderName(id)] = key\n }\n return headers\n }\n\n async prepare(provider?: ProviderId): Promise<void> {\n await this.#ready\n if (this.storage.unlockable && this.#locked) {\n await this.unlock()\n }\n if (!provider) return\n requireProviderId(provider)\n for (const id of this.#expand(provider)) {\n if (this.#keys[id] || this.#hasCoverage(id)) continue\n this.request(id, 'missing')\n throw new ByokBlockedError(id, 'missing')\n }\n }\n\n async update(\n providerOrKey: ProviderId | string,\n key?: string,\n ): Promise<void> {\n await this.#ready\n let provider: ProviderId\n let nextKey: string\n if (key === undefined) {\n if (!this.#prompt) {\n throw new Error('byok.update(key) cannot run when prompt is null')\n }\n provider = this.#prompt.provider\n nextKey = providerOrKey\n } else {\n provider = requireProviderId(providerOrKey)\n nextKey = key\n }\n nextKey = nextKey.trim()\n if (nextKey.length === 0) {\n throw new Error('BYOK key must be non-empty')\n }\n if (this.storage.unlockable && this.#locked) {\n await this.unlock()\n }\n const previousKeys = this.#keys\n const previousStatuses = { ...this.#statuses }\n const previousLocked = this.#locked\n const previousPrompt = this.#prompt\n const previousStorageError = this.#storageError\n const next = { ...this.#keys, [provider]: nextKey }\n try {\n await this.storage.save(next)\n } catch (error) {\n this.#keys = previousKeys\n this.#statuses = previousStatuses\n this.#locked = previousLocked\n this.#prompt = previousPrompt\n this.#storageError = previousStorageError\n this.#statuses[provider] = {\n state: 'error',\n masked: maskKey(nextKey),\n message: errorMessage(error, `Failed to persist ${provider} key`),\n }\n this.#emit()\n throw error\n }\n this.#keys = next\n this.#statuses[provider] = { state: 'set', masked: maskKey(nextKey) }\n this.#locked = false\n this.#prompt = null\n this.#storageError = null\n this.#emit()\n }\n\n async clear(provider?: ProviderId): Promise<void> {\n await this.#ready\n if (provider) {\n requireProviderId(provider)\n if (this.storage.unlockable && this.#locked) {\n await this.unlock()\n }\n const previousKeys = this.#keys\n const previousStatuses = { ...this.#statuses }\n const next = { ...this.#keys }\n delete next[provider]\n try {\n await this.storage.save(next)\n } catch (error) {\n this.#keys = previousKeys\n this.#statuses = previousStatuses\n this.#emit()\n throw error\n }\n this.#keys = next\n delete this.#statuses[provider]\n this.#emit()\n return\n }\n const previousKeys = this.#keys\n const previousStatuses = this.#statuses\n const previousLocked = this.#locked\n const previousPrompt = this.#prompt\n try {\n await this.storage.clear()\n } catch (error) {\n this.#keys = previousKeys\n this.#statuses = previousStatuses\n this.#locked = previousLocked\n this.#prompt = previousPrompt\n this.#emit()\n throw error\n }\n this.#keys = {}\n this.#statuses = {}\n this.#locked = false\n this.#prompt = null\n this.#storageError = null\n this.#emit()\n }\n\n async unlock(): Promise<void> {\n await this.#ready\n if (!this.storage.unlockable) return\n try {\n const loaded = sanitizeKeyring(await this.storage.load())\n this.#keys = { ...loaded, ...this.#keys }\n for (const [id, value] of Object.entries(loaded)) {\n if (!isProviderId(id) || !value) continue\n const existing = this.#statuses[id]\n if (!existing || existing.state === 'locked') {\n this.#statuses[id] = { state: 'set', masked: maskKey(value) }\n }\n }\n this.#locked = false\n this.#storageError = null\n this.#emit()\n } catch (error) {\n if (isByokCeremonyCancel(error)) {\n const provider = this.#lockedProvider()\n if (provider) {\n this.request(provider, 'locked')\n throw new ByokBlockedError(provider, 'locked')\n }\n }\n this.#storageError = errorMessage(error, 'Failed to unlock keyring')\n this.#emit()\n throw error instanceof Error ? error : new Error(String(error))\n }\n }\n\n #lockedProvider(): ProviderId | undefined {\n if (this.#prompt && isProviderId(this.#prompt.provider)) {\n return this.#prompt.provider\n }\n for (const [id, status] of Object.entries(this.#statuses)) {\n if (status?.state === 'locked' && isProviderId(id)) return id\n }\n return undefined\n }\n\n async #hydrate(): Promise<void> {\n if (this.storage.unlockable) {\n if (!this.storage.peek) return\n try {\n const preview = await this.storage.peek()\n for (const [id, last4] of Object.entries(preview)) {\n if (!isProviderId(id) || this.#statuses[id]) continue\n this.#statuses[id] = {\n state: 'locked',\n masked: last4 ? last4 : '••',\n }\n }\n this.#locked = Object.keys(preview).length > 0\n this.#storageError = null\n this.#emit()\n } catch (error) {\n this.#storageError = errorMessage(error, 'Failed to read keyring')\n this.#emit()\n }\n return\n }\n try {\n const loaded = sanitizeKeyring(await this.storage.load())\n this.#keys = { ...loaded, ...this.#keys }\n for (const [id, value] of Object.entries(loaded)) {\n if (!isProviderId(id) || !value) continue\n this.#statuses[id] = { state: 'set', masked: maskKey(value) }\n }\n this.#storageError = null\n this.#emit()\n } catch (error) {\n this.#storageError = errorMessage(error, 'Failed to read keyring')\n this.#emit()\n }\n }\n\n #emit(): void {\n this.#snapshot = this.#buildSnapshot()\n for (const listener of this.#listeners) listener()\n }\n}\n\nexport function defineByok(options: DefineByokOptions = {}): ByokClient {\n return new ByokClient(options)\n}\n"],"mappings":"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"}
@@ -58,6 +58,8 @@ export declare class ChatClient<TTools extends ReadonlyArray<AnyClientTool> = an
58
58
  private readonly clearedStreamTracker;
59
59
  private currentRunId;
60
60
  private lastResume;
61
+ private readonly runParents;
62
+ private readonly supersededInterruptRunIds;
61
63
  private rejoinedRunId;
62
64
  private readonly interruptManager;
63
65
  private activeInterruptSubmission;
@@ -144,6 +146,16 @@ export declare class ChatClient<TTools extends ReadonlyArray<AnyClientTool> = an
144
146
  /** Constructor inputs `attach()` needs on every re-attach, not just the first. */
145
147
  private readonly rejoinRunId;
146
148
  private readonly cachesMessages;
149
+ /**
150
+ * Newest-window size from `history.pageSize`. Only set when
151
+ * `persistence === true`.
152
+ */
153
+ private readonly historyPageSize;
154
+ private hasOlderMessages;
155
+ private olderMessagesCursor;
156
+ private readonly knownServerMessageIds;
157
+ private loadOlderMessagesInFlight;
158
+ private historyGeneration;
147
159
  private devtoolsMounted;
148
160
  private readonly callbacksRef;
149
161
  constructor(options: ChatClientOptions<TTools, TContext, TInterrupts>);
@@ -222,6 +234,8 @@ export declare class ChatClient<TTools extends ReadonlyArray<AnyClientTool> = an
222
234
  * state. This is interrupt (state) resume — there is no delivery cursor.
223
235
  */
224
236
  private observeInterruptState;
237
+ /** Walk parentRunId links and mark this run plus its ancestors as answered. */
238
+ private markLineageAnswered;
225
239
  /**
226
240
  * The interrupt-resume state for the active/interrupted run (its run/thread
227
241
  * ids), or null when there is nothing to resume. Apps can persist this to
@@ -470,6 +484,23 @@ export declare class ChatClient<TTools extends ReadonlyArray<AnyClientTool> = an
470
484
  * Get current messages
471
485
  */
472
486
  getMessages(): Array<UIMessage<TTools>>;
487
+ /**
488
+ * True when the last hydrate or older-page response said more messages exist.
489
+ */
490
+ getHasOlderMessages(): boolean;
491
+ /**
492
+ * Fetch the next older window and put it in front of the painted messages.
493
+ *
494
+ * No-op when there is no older page, no cursor, or a page load is already
495
+ * running. A network failure rejects and leaves the painted messages as they
496
+ * are; {@link getHasOlderMessages} stays true.
497
+ */
498
+ loadOlderMessages(): Promise<void>;
499
+ private resetHistoryPaging;
500
+ private applyHydrationPage;
501
+ private rememberServerMessageIds;
502
+ private rememberProcessorMessageIds;
503
+ private messagesForSend;
473
504
  /**
474
505
  * True when an interrupt (or another direct send) claimed the client during
475
506
  * a drain. Read via a method so cross-await mutations are not constant-folded
@@ -1,3 +1,4 @@
1
+ import { byokFallbackProviderId } from "./byok/client.js";
1
2
  import { prepareResolvedByokHeaders, resolveByokProviderId } from "./byok/resolve.js";
2
3
  import { createNoOpChatDevtoolsBridge } from "./devtools-noop.js";
3
4
  import { normalizeMessagesDates } from "./message-date-normalizer.js";
@@ -140,6 +141,7 @@ var REJOIN_REBUILD_TRIGGERS = /* @__PURE__ */ new Set([
140
141
  "TOOL_CALL_START",
141
142
  "MESSAGES_SNAPSHOT"
142
143
  ]);
144
+ var MAX_RUN_LINEAGE_DEPTH = 64;
143
145
  var ChatClient = class {
144
146
  processor;
145
147
  connection;
@@ -149,6 +151,8 @@ var ChatClient = class {
149
151
  clearedStreamTracker = new ClearedStreamTracker();
150
152
  currentRunId = null;
151
153
  lastResume = null;
154
+ runParents = /* @__PURE__ */ new Map();
155
+ supersededInterruptRunIds = /* @__PURE__ */ new Set();
152
156
  rejoinedRunId = null;
153
157
  interruptManager;
154
158
  activeInterruptSubmission;
@@ -235,6 +239,16 @@ var ChatClient = class {
235
239
  /** Constructor inputs `attach()` needs on every re-attach, not just the first. */
236
240
  rejoinRunId;
237
241
  cachesMessages;
242
+ /**
243
+ * Newest-window size from `history.pageSize`. Only set when
244
+ * `persistence === true`.
245
+ */
246
+ historyPageSize;
247
+ hasOlderMessages = false;
248
+ olderMessagesCursor;
249
+ knownServerMessageIds = /* @__PURE__ */ new Set();
250
+ loadOlderMessagesInFlight = false;
251
+ historyGeneration = 0;
238
252
  devtoolsMounted = false;
239
253
  callbacksRef;
240
254
  constructor(options) {
@@ -245,10 +259,12 @@ var ChatClient = class {
245
259
  if (options.persistence === true) {
246
260
  if (!options.threadId) throw new Error("[TanStack AI] persistence needs a stable `threadId` to key on. Pass a threadId from your app (for example support-42).");
247
261
  cachesMessages = false;
262
+ this.historyPageSize = options.history?.pageSize;
248
263
  } else if (options.persistence) {
249
264
  if (!options.threadId) throw new Error("[TanStack AI] persistence needs a stable `threadId` to key on. Pass a threadId from your app (for example support-42).");
250
265
  this.persistor = new ChatPersistor(options.persistence, options.threadId, (messages) => this.processor.setMessages(messages), (snapshot) => this.applyPersistedResume(snapshot));
251
- }
266
+ this.historyPageSize = void 0;
267
+ } else this.historyPageSize = void 0;
252
268
  this.bodyOption = options.body || {};
253
269
  this.forwardedPropsOption = options.forwardedProps || {};
254
270
  this.byok = options.byok;
@@ -506,16 +522,25 @@ var ChatClient = class {
506
522
  if (!hydrate) return;
507
523
  if (this.isLoading || this.abortController) return;
508
524
  if (this.disposed) return;
525
+ const pageSize = this.historyPageSize;
526
+ const hydrateOptions = pageSize === void 0 ? void 0 : { limit: pageSize };
509
527
  (async () => {
510
528
  let result;
529
+ const generation = this.historyGeneration;
511
530
  try {
512
- result = await hydrate(this.threadId);
531
+ result = await hydrate(this.threadId, hydrateOptions);
513
532
  } catch {
514
533
  return;
515
534
  }
535
+ if (generation !== this.historyGeneration) return;
516
536
  if (this.disposed || !this.tailing) return;
517
537
  if (this.isLoading || this.abortController) return;
518
- if (result.messages.length > 0) this.processor.setMessages(normalizeMessagesDates(result.messages));
538
+ this.applyHydrationPage(result.page);
539
+ if (result.messages.length > 0) {
540
+ const windowMessages = normalizeMessagesDates(result.messages);
541
+ this.processor.setMessages(windowMessages);
542
+ this.rememberServerMessageIds(windowMessages);
543
+ }
519
544
  if (result.interrupts && result.interrupts.pending.length > 0) this.applyResumeSnapshot({
520
545
  resumeState: {
521
546
  threadId: this.threadId,
@@ -561,15 +586,28 @@ var ChatClient = class {
561
586
  updateRunLifecycle(chunk, options) {
562
587
  if (chunk.type === "RUN_STARTED") {
563
588
  const chunkRunId = getChunkRunId$1(chunk) ?? chunk.runId;
589
+ const parentRunId = "parentRunId" in chunk && typeof chunk.parentRunId === "string" ? chunk.parentRunId : void 0;
590
+ if (parentRunId) {
591
+ this.runParents.set(chunkRunId, parentRunId);
592
+ if (this.supersededInterruptRunIds.has(chunkRunId)) this.markLineageAnswered(parentRunId);
593
+ }
564
594
  this.activeResumeThreadId = "threadId" in chunk && typeof chunk.threadId === "string" ? chunk.threadId : this.activeResumeThreadId;
565
595
  this.activeResumeRunId = chunkRunId;
566
- this.activeRunIds.add(chunkRunId);
567
- this.clearedStreamTracker.onRunStarted(chunkRunId);
568
- this.setSessionGenerating(true);
569
- if (this.persistor && this.connection.joinRun && !this.lastResume) this.persistResumeSnapshot({
596
+ const isAnsweredReplayStart = this.supersededInterruptRunIds.has(chunkRunId) && !this.isLoading;
597
+ if (!isAnsweredReplayStart) {
598
+ this.activeRunIds.add(chunkRunId);
599
+ this.clearedStreamTracker.onRunStarted(chunkRunId);
600
+ this.setSessionGenerating(true);
601
+ }
602
+ if (this.persistor && this.connection.joinRun && !this.lastResume && !isAnsweredReplayStart) this.persistResumeSnapshot({
570
603
  threadId: this.activeResumeThreadId ?? this.threadId,
571
604
  runId: chunkRunId
572
605
  });
606
+ if (this.lastResume && this.supersededInterruptRunIds.has(this.lastResume.runId)) {
607
+ this.lastResume = null;
608
+ this.persistor?.persistResumeSnapshot(null);
609
+ this.interruptManager.reset();
610
+ }
573
611
  return;
574
612
  }
575
613
  if (chunk.type !== "RUN_FINISHED" && chunk.type !== "RUN_ERROR") return;
@@ -596,8 +634,10 @@ var ChatClient = class {
596
634
  if (this.activeInterruptSubmission && chunk.type === "RUN_ERROR") return;
597
635
  const runId = getChunkRunId$1(chunk);
598
636
  const threadId = "threadId" in chunk && typeof chunk.threadId === "string" ? chunk.threadId : this.activeResumeThreadId;
637
+ if (runId && !isIntermediateToolTurn(chunk) && (chunk.type === "RUN_ERROR" || chunk.type === "RUN_FINISHED" && chunk.outcome?.type !== "interrupt")) this.markLineageAnswered(runId);
599
638
  if (chunk.type === "RUN_FINISHED" && chunk.outcome?.type === "interrupt") {
600
639
  const interruptedRunId = this.currentRunId ?? runId ?? this.activeResumeRunId ?? "";
640
+ if (interruptedRunId && this.supersededInterruptRunIds.has(interruptedRunId)) return;
601
641
  this.lastResume = {
602
642
  threadId: threadId ?? this.threadId,
603
643
  runId: interruptedRunId
@@ -616,7 +656,8 @@ var ChatClient = class {
616
656
  const isActiveStreamRunTerminal = Boolean(this.isLoading && runId && (runId === this.activeResumeRunId || runId === this.currentRunId));
617
657
  const isCurrentStreamTerminal = this.isLoading && chunk.type === "RUN_FINISHED" && !runId;
618
658
  const isActiveInterruptSubmissionTerminal = Boolean(this.activeInterruptSubmission && this.isLoading && chunk.type === "RUN_FINISHED" && chunk.outcome?.type !== "interrupt");
619
- if (isRunlessSessionError || isTrackedRunTerminal || isCurrentRunTerminal || isActiveStreamRunTerminal || isCurrentStreamTerminal || isActiveInterruptSubmissionTerminal) {
659
+ const isLineageDescendantTerminal = Boolean(this.lastResume && this.supersededInterruptRunIds.has(this.lastResume.runId));
660
+ if (isRunlessSessionError || isTrackedRunTerminal || isCurrentRunTerminal || isActiveStreamRunTerminal || isCurrentStreamTerminal || isActiveInterruptSubmissionTerminal || isLineageDescendantTerminal) {
620
661
  this.lastResume = null;
621
662
  this.persistor?.persistResumeSnapshot(null);
622
663
  this.interruptManager.reset();
@@ -624,6 +665,17 @@ var ChatClient = class {
624
665
  }
625
666
  this.notifyResumeStateChange("live");
626
667
  }
668
+ /** Walk parentRunId links and mark this run plus its ancestors as answered. */
669
+ markLineageAnswered(runId) {
670
+ let current = runId;
671
+ let hops = 0;
672
+ while (current !== void 0 && hops < MAX_RUN_LINEAGE_DEPTH) {
673
+ if (this.supersededInterruptRunIds.has(current)) return;
674
+ this.supersededInterruptRunIds.add(current);
675
+ current = this.runParents.get(current);
676
+ hops++;
677
+ }
678
+ }
627
679
  /**
628
680
  * The interrupt-resume state for the active/interrupted run (its run/thread
629
681
  * ids), or null when there is nothing to resume. Apps can persist this to
@@ -1275,7 +1327,7 @@ var ChatClient = class {
1275
1327
  let activeDevtoolsRunId = null;
1276
1328
  let runTerminalEventEmitted = false;
1277
1329
  try {
1278
- const messages = this.processor.getMessages();
1330
+ const messages = this.messagesForSend(this.processor.getMessages());
1279
1331
  const clientTools = new Map(this.clientToolsRef.current);
1280
1332
  const runtimeContext = this.context;
1281
1333
  await this.callbacksRef.current.onResponse();
@@ -1296,7 +1348,7 @@ var ChatClient = class {
1296
1348
  const processingComplete = this.waitForProcessing();
1297
1349
  let byokHeaders;
1298
1350
  if (this.byok) {
1299
- const provider = resolveByokProviderId(this.byokProvider, mergedBody.provider);
1351
+ const provider = resolveByokProviderId(this.byokProvider, mergedBody.provider, byokFallbackProviderId(this.byok));
1300
1352
  byokHeaders = await prepareResolvedByokHeaders(this.byok, provider);
1301
1353
  }
1302
1354
  const runContext = {
@@ -1368,6 +1420,7 @@ var ChatClient = class {
1368
1420
  }
1369
1421
  await this.drainPostStreamActions();
1370
1422
  if (streamCompletedSuccessfully) {
1423
+ this.rememberProcessorMessageIds();
1371
1424
  if (this.status !== "ready") this.setStatus("ready");
1372
1425
  if (!this.messageQueueDraining) await this.drainQueue();
1373
1426
  } else this.flushQueue();
@@ -1446,6 +1499,7 @@ var ChatClient = class {
1446
1499
  } else if (this.activeRunIds.size > 0) this.resetSessionGenerating({ preserveClearedStreamTracking: true });
1447
1500
  this.persistor?.beginClear();
1448
1501
  this.processor.clearMessages();
1502
+ this.resetHistoryPaging();
1449
1503
  this.discardPendingSends();
1450
1504
  this.persistor?.remove();
1451
1505
  this.lastResume = null;
@@ -1586,6 +1640,79 @@ var ChatClient = class {
1586
1640
  return this.processor.getMessages();
1587
1641
  }
1588
1642
  /**
1643
+ * True when the last hydrate or older-page response said more messages exist.
1644
+ */
1645
+ getHasOlderMessages() {
1646
+ return this.hasOlderMessages;
1647
+ }
1648
+ /**
1649
+ * Fetch the next older window and put it in front of the painted messages.
1650
+ *
1651
+ * No-op when there is no older page, no cursor, or a page load is already
1652
+ * running. A network failure rejects and leaves the painted messages as they
1653
+ * are; {@link getHasOlderMessages} stays true.
1654
+ */
1655
+ async loadOlderMessages() {
1656
+ const hydrate = this.connection.hydrate;
1657
+ if (hydrate === void 0) return;
1658
+ if (this.disposed) return;
1659
+ if (!this.hasOlderMessages) return;
1660
+ const cursor = this.olderMessagesCursor;
1661
+ if (cursor === void 0) return;
1662
+ if (this.loadOlderMessagesInFlight) return;
1663
+ const hydrateOptions = { before: cursor };
1664
+ if (this.historyPageSize !== void 0) hydrateOptions.limit = this.historyPageSize;
1665
+ this.loadOlderMessagesInFlight = true;
1666
+ const generation = this.historyGeneration;
1667
+ try {
1668
+ const result = await hydrate(this.threadId, hydrateOptions);
1669
+ if (this.disposed) return;
1670
+ if (generation !== this.historyGeneration) return;
1671
+ const olderMessages = normalizeMessagesDates(result.messages);
1672
+ if (olderMessages.length === 0) throw new Error("Older page was empty");
1673
+ this.applyHydrationPage(result.page);
1674
+ this.processor.prependMessages(olderMessages);
1675
+ this.rememberServerMessageIds(olderMessages);
1676
+ } finally {
1677
+ this.loadOlderMessagesInFlight = false;
1678
+ }
1679
+ }
1680
+ resetHistoryPaging() {
1681
+ this.hasOlderMessages = false;
1682
+ this.olderMessagesCursor = void 0;
1683
+ this.knownServerMessageIds.clear();
1684
+ this.historyGeneration++;
1685
+ }
1686
+ applyHydrationPage(page) {
1687
+ if (page?.truncated === true) {
1688
+ this.hasOlderMessages = true;
1689
+ this.olderMessagesCursor = page.cursor;
1690
+ return;
1691
+ }
1692
+ this.hasOlderMessages = false;
1693
+ this.olderMessagesCursor = void 0;
1694
+ }
1695
+ rememberServerMessageIds(messages) {
1696
+ const ids = messages.map((message) => message.id);
1697
+ for (const id of ids) this.knownServerMessageIds.add(id);
1698
+ }
1699
+ rememberProcessorMessageIds() {
1700
+ if (this.historyPageSize === void 0) return;
1701
+ this.rememberServerMessageIds(this.processor.getMessages());
1702
+ }
1703
+ messagesForSend(messages) {
1704
+ if (this.historyPageSize === void 0) return messages;
1705
+ const unknownMessages = [];
1706
+ for (const message of messages) {
1707
+ if (this.knownServerMessageIds.has(message.id)) continue;
1708
+ unknownMessages.push(message);
1709
+ }
1710
+ if (unknownMessages.length > 0) return unknownMessages;
1711
+ const lastUserIndex = messages.findLastIndex((message) => message.role === "user");
1712
+ if (lastUserIndex === -1) return [];
1713
+ return messages.slice(lastUserIndex);
1714
+ }
1715
+ /**
1589
1716
  * True when an interrupt (or another direct send) claimed the client during
1590
1717
  * a drain. Read via a method so cross-await mutations are not constant-folded
1591
1718
  * by control-flow analysis.