@tanstack/ai-client 0.31.0 → 0.31.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
@@ -202,19 +202,20 @@ Learn more in the
202
202
 
203
203
  Official adapters include:
204
204
 
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 |
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 |
218
219
 
219
220
  The adapter system is tree-shakeable by activity. Import `openaiText` for chat,
220
221
  `openaiImage` for images, `falVideo` for video, `geminiSpeech` for TTS, and so
@@ -43,6 +43,10 @@ function isPasskeyStorageSupported() {
43
43
  }
44
44
  /** Derive a non-extractable AES-256-GCM key from a 32-byte PRF output. */
45
45
  async function deriveAesKey(prfOutput) {
46
+ if (Array.isArray(prfOutput)) {
47
+ if (prfOutput.length !== 32 || !Array.from(prfOutput).every((byte) => typeof byte === "number" && Number.isInteger(byte) && byte >= 0 && byte <= 255)) throw new Error("Invalid passkey PRF byte array");
48
+ prfOutput = new Uint8Array(prfOutput);
49
+ }
46
50
  const base = await crypto.subtle.importKey("raw", prfOutput, "HKDF", false, ["deriveKey"]);
47
51
  return crypto.subtle.deriveKey({
48
52
  name: "HKDF",
@@ -111,6 +115,17 @@ function requirePublicKeyCredential(credential, action) {
111
115
  if (!(credential instanceof PublicKeyCredential)) throw new Error(`Unexpected credential type during ${action}`);
112
116
  return credential;
113
117
  }
118
+ /**
119
+ * Some browsers (Dia, Safari) silently suppress `navigator.credentials.get()`
120
+ * — no prompt, never resolves — when it runs without transient user activation.
121
+ * The activation from a click expires (~5s) and is lost across enough async
122
+ * work, so an unlock buried deep in a send pipeline just hangs. Fail fast with
123
+ * a clear, catchable error so the app can re-run it from a fresh gesture.
124
+ */
125
+ function requireUserActivation(action) {
126
+ const activation = globalThis.navigator?.userActivation;
127
+ if (activation && activation.isActive === false) throw new Error(`Passkey ${action} needs a fresh user action (e.g. a click). Run it directly from the event handler, before awaiting other work.`);
128
+ }
114
129
  async function registerPasskey(rpName, userName, rpId) {
115
130
  const salt = crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(32));
116
131
  const credential = requirePublicKeyCredential(await navigator.credentials.create({ publicKey: {
@@ -178,6 +193,7 @@ function passkeyStorage(options = {}) {
178
193
  };
179
194
  const existing = await idbGet(dbName);
180
195
  if (existing) {
196
+ requireUserActivation("unlock");
181
197
  cachedKey = await deriveAesKey(await evaluatePrf(existing.credentialId, existing.salt));
182
198
  cachedMeta = {
183
199
  credentialId: existing.credentialId,
@@ -1 +1 @@
1
- {"version":3,"file":"passkey.js","names":[],"sources":["../../../src/byok/passkey.ts"],"sourcesContent":["import { isProviderId } from '@tanstack/ai/byok'\nimport { memoryStorage } from './storage'\nimport type { KeyPreview, Keyring, KeyringStorage } from './storage'\n\n/**\n * Passkey-encrypted keyring storage (WebAuthn PRF → HKDF → AES-256-GCM).\n *\n * The keyring is encrypted at rest in IndexedDB with an AES-256-GCM key derived\n * from a passkey's PRF output, unwrapped on demand with a biometric/PIN tap.\n * Decryption happens entirely client-side with the user present — no server,\n * no custodian.\n *\n * Honest scope: this protects against at-rest theft (stolen device,\n * storage-dumping extension, backups). It does NOT defeat live in-page XSS —\n * an attacker running JS in the origin after the user unlocks can read the\n * decrypted keys from memory.\n */\n\nconst STORE_NAME = 'keyring'\nconst RECORD_ID = 'default'\nconst HKDF_INFO = 'byok:keyring:v1'\nconst DEFAULT_DB = 'byok'\n\ninterface StoredRecord {\n id: string\n /** The passkey's raw credential id, replayed in the unlock ceremony. */\n credentialId: ArrayBuffer\n /** Fixed per-install PRF evaluation input (not secret). */\n salt: ArrayBuffer\n /** AES-GCM initialization vector for this ciphertext. */\n iv: ArrayBuffer\n /** Encrypted keyring JSON. */\n ciphertext: ArrayBuffer\n /**\n * Unencrypted presence metadata (`provider → last 4`). Non-sensitive, so it\n * can be read via {@link KeyringStorage.peek} without an unlock ceremony to\n * show saved keys as \"locked\" after a refresh.\n */\n preview: KeyPreview\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\n/** Build the non-sensitive `provider → last 4` preview from a keyring. */\nfunction previewOf(keys: Keyring): KeyPreview {\n const preview: KeyPreview = {}\n for (const [provider, key] of Object.entries(keys)) {\n if (!key || !isProviderId(provider)) continue\n // Keys of length ≤ 4 would make last-4 the whole secret — store presence only.\n preview[provider] = key.length > 4 ? key.slice(-4) : ''\n }\n return preview\n}\n\n/**\n * Whether the current environment exposes WebAuthn. Actual PRF support can\n * only be confirmed during registration; `passkeyStorage` throws if the\n * chosen authenticator does not support PRF.\n */\nexport function isPasskeyStorageSupported(): boolean {\n return (\n typeof globalThis !== 'undefined' &&\n typeof globalThis.PublicKeyCredential !== 'undefined' &&\n typeof globalThis.navigator !== 'undefined' &&\n typeof globalThis.navigator.credentials.create === 'function'\n )\n}\n\n// ---------------------------------------------------------------------------\n// Crypto (exported for testing; the WebAuthn ceremony below feeds `deriveAesKey`)\n// ---------------------------------------------------------------------------\n\n/** Derive a non-extractable AES-256-GCM key from a 32-byte PRF output. */\nexport async function deriveAesKey(\n prfOutput: BufferSource,\n): Promise<CryptoKey> {\n const base = await crypto.subtle.importKey('raw', prfOutput, 'HKDF', false, [\n 'deriveKey',\n ])\n return crypto.subtle.deriveKey(\n {\n name: 'HKDF',\n hash: 'SHA-256',\n salt: new Uint8Array(0),\n info: new TextEncoder().encode(HKDF_INFO),\n },\n base,\n { name: 'AES-GCM', length: 256 },\n false,\n ['encrypt', 'decrypt'],\n )\n}\n\nexport async function encryptKeyring(\n key: CryptoKey,\n keys: Keyring,\n): Promise<{ iv: ArrayBuffer; ciphertext: ArrayBuffer }> {\n const iv = crypto.getRandomValues(new Uint8Array(12))\n const plaintext = new TextEncoder().encode(JSON.stringify(keys))\n const ciphertext = await crypto.subtle.encrypt(\n { name: 'AES-GCM', iv },\n key,\n plaintext,\n )\n return { iv: iv.buffer, ciphertext }\n}\n\nexport async function decryptKeyring(\n key: CryptoKey,\n iv: BufferSource,\n ciphertext: BufferSource,\n): Promise<Keyring> {\n const plaintext = await crypto.subtle.decrypt(\n { name: 'AES-GCM', iv },\n key,\n ciphertext,\n )\n const parsed: unknown = JSON.parse(new TextDecoder().decode(plaintext))\n return sanitizeKeyring(parsed)\n}\n\n// ---------------------------------------------------------------------------\n// IndexedDB\n// ---------------------------------------------------------------------------\n\nfunction openDb(dbName: string): Promise<IDBDatabase> {\n return new Promise((resolve, reject) => {\n const request = indexedDB.open(dbName, 1)\n request.onupgradeneeded = () => {\n request.result.createObjectStore(STORE_NAME, { keyPath: 'id' })\n }\n request.onsuccess = () => resolve(request.result)\n request.onerror = () => reject(request.error)\n })\n}\n\nfunction idbGet(dbName: string): Promise<StoredRecord | null> {\n return openDb(dbName).then(\n (db) =>\n new Promise((resolve, reject) => {\n const request = db\n .transaction(STORE_NAME, 'readonly')\n .objectStore(STORE_NAME)\n .get(RECORD_ID)\n request.onsuccess = () => resolve(request.result ?? null)\n request.onerror = () => reject(request.error)\n }),\n )\n}\n\nfunction idbPut(dbName: string, record: StoredRecord): Promise<void> {\n return openDb(dbName).then(\n (db) =>\n new Promise((resolve, reject) => {\n const tx = db.transaction(STORE_NAME, 'readwrite')\n tx.objectStore(STORE_NAME).put(record)\n tx.oncomplete = () => resolve()\n tx.onerror = () => reject(tx.error)\n }),\n )\n}\n\nfunction idbClear(dbName: string): Promise<void> {\n return openDb(dbName).then(\n (db) =>\n new Promise((resolve, reject) => {\n const tx = db.transaction(STORE_NAME, 'readwrite')\n tx.objectStore(STORE_NAME).delete(RECORD_ID)\n tx.oncomplete = () => resolve()\n tx.onerror = () => reject(tx.error)\n }),\n )\n}\n\n// ---------------------------------------------------------------------------\n// WebAuthn ceremonies\n// ---------------------------------------------------------------------------\n\nfunction requirePublicKeyCredential(\n credential: Credential | null,\n action: string,\n): PublicKeyCredential {\n if (!credential) throw new Error(`Passkey ${action} was cancelled`)\n if (!(credential instanceof PublicKeyCredential)) {\n throw new Error(`Unexpected credential type during ${action}`)\n }\n return credential\n}\n\nasync function registerPasskey(\n rpName: string,\n userName: string,\n rpId?: string,\n): Promise<{\n credentialId: ArrayBuffer\n salt: Uint8Array<ArrayBuffer>\n prf?: BufferSource\n}> {\n const salt = crypto.getRandomValues(new Uint8Array(32))\n const credential = requirePublicKeyCredential(\n await navigator.credentials.create({\n publicKey: {\n challenge: crypto.getRandomValues(new Uint8Array(32)),\n // Omit `id` to let the browser bind the passkey to the current origin's\n // effective domain; set it to scope across subdomains of a self-host.\n rp: rpId ? { name: rpName, id: rpId } : { name: rpName },\n user: {\n id: crypto.getRandomValues(new Uint8Array(16)),\n name: userName,\n displayName: userName,\n },\n pubKeyCredParams: [\n { type: 'public-key', alg: -7 },\n { type: 'public-key', alg: -257 },\n ],\n authenticatorSelection: {\n residentKey: 'required',\n userVerification: 'required',\n },\n extensions: { prf: { eval: { first: salt } } },\n },\n }),\n 'registration',\n )\n\n const prf = credential.getClientExtensionResults().prf\n if (!prf?.enabled) {\n throw new Error(\n 'This authenticator does not support the WebAuthn PRF extension',\n )\n }\n return { credentialId: credential.rawId, salt, prf: prf.results?.first }\n}\n\nasync function evaluatePrf(\n credentialId: BufferSource,\n salt: BufferSource,\n): Promise<BufferSource> {\n const credential = requirePublicKeyCredential(\n await navigator.credentials.get({\n publicKey: {\n challenge: crypto.getRandomValues(new Uint8Array(32)),\n allowCredentials: [{ type: 'public-key', id: credentialId }],\n userVerification: 'required',\n extensions: { prf: { eval: { first: salt } } },\n },\n }),\n 'unlock',\n )\n const result = credential.getClientExtensionResults().prf?.results?.first\n if (!result) {\n throw new Error('Authenticator did not return a PRF result')\n }\n return result\n}\n\n// ---------------------------------------------------------------------------\n// Storage strategy\n// ---------------------------------------------------------------------------\n\nexport interface PasskeyStorageOptions {\n /** Relying-party name shown in the passkey prompt. */\n rpName?: string\n /** Username label attached to the created passkey. */\n userName?: string\n /**\n * WebAuthn Relying Party ID. Omit to bind the passkey to the current origin's\n * effective domain (the default — no central/hardcoded domain). Set it to a\n * registrable parent domain to share the credential across subdomains of your\n * own deployment. The encrypted keyring is never portable across unrelated\n * domains.\n */\n rpId?: string\n /** IndexedDB database name. Defaults to `byok`. */\n dbName?: string\n}\n\n/**\n * Passkey-encrypted persistence. `ByokClient` treats this as `unlockable`, so\n * nothing is decrypted until the user calls `unlock()` (or saves a key, which\n * registers a passkey on first use). The derived key is cached in memory for\n * the session so repeated saves don't re-prompt.\n */\nexport function passkeyStorage(\n options: PasskeyStorageOptions = {},\n): KeyringStorage {\n const rpName = options.rpName ?? 'BYOK'\n const userName = options.userName ?? 'byok-keyring'\n const { rpId } = options\n const dbName = options.dbName ?? DEFAULT_DB\n\n let cachedKey: CryptoKey | null = null\n let cachedMeta: {\n credentialId: ArrayBuffer\n salt: Uint8Array<ArrayBuffer>\n } | null = null\n\n // Obtain the AES key, running exactly one WebAuthn ceremony if it isn't\n // already cached for this session (unlock if a passkey exists, else register).\n async function ensureKey(): Promise<{\n key: CryptoKey\n credentialId: ArrayBuffer\n salt: Uint8Array<ArrayBuffer>\n }> {\n if (cachedKey && cachedMeta) {\n return { key: cachedKey, ...cachedMeta }\n }\n const existing = await idbGet(dbName)\n if (existing) {\n const prf = await evaluatePrf(existing.credentialId, existing.salt)\n cachedKey = await deriveAesKey(prf)\n cachedMeta = {\n credentialId: existing.credentialId,\n salt: new Uint8Array(existing.salt),\n }\n } else {\n const reg = await registerPasskey(rpName, userName, rpId)\n const prf = reg.prf ?? (await evaluatePrf(reg.credentialId, reg.salt))\n cachedKey = await deriveAesKey(prf)\n cachedMeta = { credentialId: reg.credentialId, salt: reg.salt }\n }\n return { key: cachedKey, ...cachedMeta }\n }\n\n return {\n id: 'passkey',\n label: 'Passkey-encrypted (this device)',\n persistent: true,\n unlockable: true,\n warning:\n 'Keys are encrypted with your passkey and unlocked with biometrics. ' +\n 'This protects saved keys if your device is stolen, but not against code ' +\n 'running on this page after you unlock.',\n peek: async () => {\n // Unencrypted last-4 sidecar — no key material, no unlock ceremony.\n const existing = await idbGet(dbName)\n return existing?.preview ?? {}\n },\n load: async () => {\n const existing = await idbGet(dbName)\n if (!existing) return {}\n const { key } = await ensureKey()\n return decryptKeyring(key, existing.iv, existing.ciphertext)\n },\n save: async (keys) => {\n const existing = await idbGet(dbName)\n const hasKeys = Object.values(keys).some(Boolean)\n // First save with an empty keyring is a no-op — avoids a passkey ceremony\n // when another storage tier writes an empty ring.\n if (!hasKeys && !existing) return\n\n const { key, credentialId, salt } = await ensureKey()\n const { iv, ciphertext } = await encryptKeyring(key, keys)\n await idbPut(dbName, {\n id: RECORD_ID,\n credentialId,\n salt: salt.buffer,\n iv,\n ciphertext,\n preview: previewOf(keys),\n })\n },\n clear: async () => {\n cachedKey = null\n cachedMeta = null\n await idbClear(dbName)\n },\n }\n}\n\n/**\n * Passkey-encrypted storage when WebAuthn is available in a secure context.\n * Otherwise session memory, with a warning — this is not an automatic PRF\n * fallback. First save still throws if the authenticator lacks PRF.\n */\nexport function defaultByokStorage(\n options?: PasskeyStorageOptions,\n): KeyringStorage {\n const secure =\n typeof globalThis.isSecureContext !== 'boolean' ||\n globalThis.isSecureContext\n if (!isPasskeyStorageSupported() || !secure) {\n return {\n ...memoryStorage(),\n warning:\n 'Passkeys are unavailable in this context. Keys stay in memory for this tab only.',\n }\n }\n return passkeyStorage(options)\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"passkey.js","names":[],"sources":["../../../src/byok/passkey.ts"],"sourcesContent":["import { isProviderId } from '@tanstack/ai/byok'\nimport { memoryStorage } from './storage'\nimport type { KeyPreview, Keyring, KeyringStorage } from './storage'\n\n/**\n * Passkey-encrypted keyring storage (WebAuthn PRF → HKDF → AES-256-GCM).\n *\n * The keyring is encrypted at rest in IndexedDB with an AES-256-GCM key derived\n * from a passkey's PRF output, unwrapped on demand with a biometric/PIN tap.\n * Decryption happens entirely client-side with the user present — no server,\n * no custodian.\n *\n * Honest scope: this protects against at-rest theft (stolen device,\n * storage-dumping extension, backups). It does NOT defeat live in-page XSS —\n * an attacker running JS in the origin after the user unlocks can read the\n * decrypted keys from memory.\n */\n\nconst STORE_NAME = 'keyring'\nconst RECORD_ID = 'default'\nconst HKDF_INFO = 'byok:keyring:v1'\nconst DEFAULT_DB = 'byok'\n\ninterface StoredRecord {\n id: string\n /** The passkey's raw credential id, replayed in the unlock ceremony. */\n credentialId: ArrayBuffer\n /** Fixed per-install PRF evaluation input (not secret). */\n salt: ArrayBuffer\n /** AES-GCM initialization vector for this ciphertext. */\n iv: ArrayBuffer\n /** Encrypted keyring JSON. */\n ciphertext: ArrayBuffer\n /**\n * Unencrypted presence metadata (`provider → last 4`). Non-sensitive, so it\n * can be read via {@link KeyringStorage.peek} without an unlock ceremony to\n * show saved keys as \"locked\" after a refresh.\n */\n preview: KeyPreview\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\n/** Build the non-sensitive `provider → last 4` preview from a keyring. */\nfunction previewOf(keys: Keyring): KeyPreview {\n const preview: KeyPreview = {}\n for (const [provider, key] of Object.entries(keys)) {\n if (!key || !isProviderId(provider)) continue\n // Keys of length ≤ 4 would make last-4 the whole secret — store presence only.\n preview[provider] = key.length > 4 ? key.slice(-4) : ''\n }\n return preview\n}\n\n/**\n * Whether the current environment exposes WebAuthn. Actual PRF support can\n * only be confirmed during registration; `passkeyStorage` throws if the\n * chosen authenticator does not support PRF.\n */\nexport function isPasskeyStorageSupported(): boolean {\n return (\n typeof globalThis !== 'undefined' &&\n typeof globalThis.PublicKeyCredential !== 'undefined' &&\n typeof globalThis.navigator !== 'undefined' &&\n typeof globalThis.navigator.credentials.create === 'function'\n )\n}\n\n// ---------------------------------------------------------------------------\n// Crypto (exported for testing; the WebAuthn ceremony below feeds `deriveAesKey`)\n// ---------------------------------------------------------------------------\n\n/** Derive a non-extractable AES-256-GCM key from a 32-byte PRF output. */\nexport async function deriveAesKey(\n prfOutput: BufferSource,\n): Promise<CryptoKey> {\n // Some passkey providers return a plain byte array instead of BufferSource.\n if (Array.isArray(prfOutput)) {\n if (\n prfOutput.length !== 32 ||\n !Array.from(prfOutput).every(\n (byte: unknown) =>\n typeof byte === 'number' &&\n Number.isInteger(byte) &&\n byte >= 0 &&\n byte <= 255,\n )\n ) {\n throw new Error('Invalid passkey PRF byte array')\n }\n prfOutput = new Uint8Array(prfOutput)\n }\n const base = await crypto.subtle.importKey('raw', prfOutput, 'HKDF', false, [\n 'deriveKey',\n ])\n return crypto.subtle.deriveKey(\n {\n name: 'HKDF',\n hash: 'SHA-256',\n salt: new Uint8Array(0),\n info: new TextEncoder().encode(HKDF_INFO),\n },\n base,\n { name: 'AES-GCM', length: 256 },\n false,\n ['encrypt', 'decrypt'],\n )\n}\n\nexport async function encryptKeyring(\n key: CryptoKey,\n keys: Keyring,\n): Promise<{ iv: ArrayBuffer; ciphertext: ArrayBuffer }> {\n const iv = crypto.getRandomValues(new Uint8Array(12))\n const plaintext = new TextEncoder().encode(JSON.stringify(keys))\n const ciphertext = await crypto.subtle.encrypt(\n { name: 'AES-GCM', iv },\n key,\n plaintext,\n )\n return { iv: iv.buffer, ciphertext }\n}\n\nexport async function decryptKeyring(\n key: CryptoKey,\n iv: BufferSource,\n ciphertext: BufferSource,\n): Promise<Keyring> {\n const plaintext = await crypto.subtle.decrypt(\n { name: 'AES-GCM', iv },\n key,\n ciphertext,\n )\n const parsed: unknown = JSON.parse(new TextDecoder().decode(plaintext))\n return sanitizeKeyring(parsed)\n}\n\n// ---------------------------------------------------------------------------\n// IndexedDB\n// ---------------------------------------------------------------------------\n\nfunction openDb(dbName: string): Promise<IDBDatabase> {\n return new Promise((resolve, reject) => {\n const request = indexedDB.open(dbName, 1)\n request.onupgradeneeded = () => {\n request.result.createObjectStore(STORE_NAME, { keyPath: 'id' })\n }\n request.onsuccess = () => resolve(request.result)\n request.onerror = () => reject(request.error)\n })\n}\n\nfunction idbGet(dbName: string): Promise<StoredRecord | null> {\n return openDb(dbName).then(\n (db) =>\n new Promise((resolve, reject) => {\n const request = db\n .transaction(STORE_NAME, 'readonly')\n .objectStore(STORE_NAME)\n .get(RECORD_ID)\n request.onsuccess = () => resolve(request.result ?? null)\n request.onerror = () => reject(request.error)\n }),\n )\n}\n\nfunction idbPut(dbName: string, record: StoredRecord): Promise<void> {\n return openDb(dbName).then(\n (db) =>\n new Promise((resolve, reject) => {\n const tx = db.transaction(STORE_NAME, 'readwrite')\n tx.objectStore(STORE_NAME).put(record)\n tx.oncomplete = () => resolve()\n tx.onerror = () => reject(tx.error)\n }),\n )\n}\n\nfunction idbClear(dbName: string): Promise<void> {\n return openDb(dbName).then(\n (db) =>\n new Promise((resolve, reject) => {\n const tx = db.transaction(STORE_NAME, 'readwrite')\n tx.objectStore(STORE_NAME).delete(RECORD_ID)\n tx.oncomplete = () => resolve()\n tx.onerror = () => reject(tx.error)\n }),\n )\n}\n\n// ---------------------------------------------------------------------------\n// WebAuthn ceremonies\n// ---------------------------------------------------------------------------\n\nfunction requirePublicKeyCredential(\n credential: Credential | null,\n action: string,\n): PublicKeyCredential {\n if (!credential) throw new Error(`Passkey ${action} was cancelled`)\n if (!(credential instanceof PublicKeyCredential)) {\n throw new Error(`Unexpected credential type during ${action}`)\n }\n return credential\n}\n\n/**\n * Some browsers (Dia, Safari) silently suppress `navigator.credentials.get()`\n * — no prompt, never resolves — when it runs without transient user activation.\n * The activation from a click expires (~5s) and is lost across enough async\n * work, so an unlock buried deep in a send pipeline just hangs. Fail fast with\n * a clear, catchable error so the app can re-run it from a fresh gesture.\n */\nfunction requireUserActivation(action: string): void {\n // ponytail: only enforce where the API exists; absent means \"can't tell\", so\n // fall through rather than block a ceremony that would have worked.\n const activation = globalThis.navigator?.userActivation\n if (activation && activation.isActive === false) {\n throw new Error(\n `Passkey ${action} needs a fresh user action (e.g. a click). Run it ` +\n 'directly from the event handler, before awaiting other work.',\n )\n }\n}\n\nasync function registerPasskey(\n rpName: string,\n userName: string,\n rpId?: string,\n): Promise<{\n credentialId: ArrayBuffer\n salt: Uint8Array<ArrayBuffer>\n prf?: BufferSource\n}> {\n const salt = crypto.getRandomValues(new Uint8Array(32))\n const credential = requirePublicKeyCredential(\n await navigator.credentials.create({\n publicKey: {\n challenge: crypto.getRandomValues(new Uint8Array(32)),\n // Omit `id` to let the browser bind the passkey to the current origin's\n // effective domain; set it to scope across subdomains of a self-host.\n rp: rpId ? { name: rpName, id: rpId } : { name: rpName },\n user: {\n id: crypto.getRandomValues(new Uint8Array(16)),\n name: userName,\n displayName: userName,\n },\n pubKeyCredParams: [\n { type: 'public-key', alg: -7 },\n { type: 'public-key', alg: -257 },\n ],\n authenticatorSelection: {\n residentKey: 'required',\n userVerification: 'required',\n },\n extensions: { prf: { eval: { first: salt } } },\n },\n }),\n 'registration',\n )\n\n const prf = credential.getClientExtensionResults().prf\n if (!prf?.enabled) {\n throw new Error(\n 'This authenticator does not support the WebAuthn PRF extension',\n )\n }\n return { credentialId: credential.rawId, salt, prf: prf.results?.first }\n}\n\nasync function evaluatePrf(\n credentialId: BufferSource,\n salt: BufferSource,\n): Promise<BufferSource> {\n const credential = requirePublicKeyCredential(\n await navigator.credentials.get({\n publicKey: {\n challenge: crypto.getRandomValues(new Uint8Array(32)),\n allowCredentials: [{ type: 'public-key', id: credentialId }],\n userVerification: 'required',\n extensions: { prf: { eval: { first: salt } } },\n },\n }),\n 'unlock',\n )\n const result = credential.getClientExtensionResults().prf?.results?.first\n if (!result) {\n throw new Error('Authenticator did not return a PRF result')\n }\n return result\n}\n\n// ---------------------------------------------------------------------------\n// Storage strategy\n// ---------------------------------------------------------------------------\n\nexport interface PasskeyStorageOptions {\n /** Relying-party name shown in the passkey prompt. */\n rpName?: string\n /** Username label attached to the created passkey. */\n userName?: string\n /**\n * WebAuthn Relying Party ID. Omit to bind the passkey to the current origin's\n * effective domain (the default — no central/hardcoded domain). Set it to a\n * registrable parent domain to share the credential across subdomains of your\n * own deployment. The encrypted keyring is never portable across unrelated\n * domains.\n */\n rpId?: string\n /** IndexedDB database name. Defaults to `byok`. */\n dbName?: string\n}\n\n/**\n * Passkey-encrypted persistence. `ByokClient` treats this as `unlockable`, so\n * nothing is decrypted until the user calls `unlock()` (or saves a key, which\n * registers a passkey on first use). The derived key is cached in memory for\n * the session so repeated saves don't re-prompt.\n */\nexport function passkeyStorage(\n options: PasskeyStorageOptions = {},\n): KeyringStorage {\n const rpName = options.rpName ?? 'BYOK'\n const userName = options.userName ?? 'byok-keyring'\n const { rpId } = options\n const dbName = options.dbName ?? DEFAULT_DB\n\n let cachedKey: CryptoKey | null = null\n let cachedMeta: {\n credentialId: ArrayBuffer\n salt: Uint8Array<ArrayBuffer>\n } | null = null\n\n // Obtain the AES key, running exactly one WebAuthn ceremony if it isn't\n // already cached for this session (unlock if a passkey exists, else register).\n async function ensureKey(): Promise<{\n key: CryptoKey\n credentialId: ArrayBuffer\n salt: Uint8Array<ArrayBuffer>\n }> {\n if (cachedKey && cachedMeta) {\n return { key: cachedKey, ...cachedMeta }\n }\n const existing = await idbGet(dbName)\n if (existing) {\n requireUserActivation('unlock')\n const prf = await evaluatePrf(existing.credentialId, existing.salt)\n cachedKey = await deriveAesKey(prf)\n cachedMeta = {\n credentialId: existing.credentialId,\n salt: new Uint8Array(existing.salt),\n }\n } else {\n const reg = await registerPasskey(rpName, userName, rpId)\n // Registration can consume activation. Let the browser handle its\n // follow-up PRF ceremony rather than rejecting a valid new-key save.\n const prf = reg.prf ?? (await evaluatePrf(reg.credentialId, reg.salt))\n cachedKey = await deriveAesKey(prf)\n cachedMeta = { credentialId: reg.credentialId, salt: reg.salt }\n }\n return { key: cachedKey, ...cachedMeta }\n }\n\n return {\n id: 'passkey',\n label: 'Passkey-encrypted (this device)',\n persistent: true,\n unlockable: true,\n warning:\n 'Keys are encrypted with your passkey and unlocked with biometrics. ' +\n 'This protects saved keys if your device is stolen, but not against code ' +\n 'running on this page after you unlock.',\n peek: async () => {\n // Unencrypted last-4 sidecar — no key material, no unlock ceremony.\n const existing = await idbGet(dbName)\n return existing?.preview ?? {}\n },\n load: async () => {\n const existing = await idbGet(dbName)\n if (!existing) return {}\n const { key } = await ensureKey()\n return decryptKeyring(key, existing.iv, existing.ciphertext)\n },\n save: async (keys) => {\n const existing = await idbGet(dbName)\n const hasKeys = Object.values(keys).some(Boolean)\n // First save with an empty keyring is a no-op — avoids a passkey ceremony\n // when another storage tier writes an empty ring.\n if (!hasKeys && !existing) return\n\n const { key, credentialId, salt } = await ensureKey()\n const { iv, ciphertext } = await encryptKeyring(key, keys)\n await idbPut(dbName, {\n id: RECORD_ID,\n credentialId,\n salt: salt.buffer,\n iv,\n ciphertext,\n preview: previewOf(keys),\n })\n },\n clear: async () => {\n cachedKey = null\n cachedMeta = null\n await idbClear(dbName)\n },\n }\n}\n\n/**\n * Passkey-encrypted storage when WebAuthn is available in a secure context.\n * Otherwise session memory, with a warning — this is not an automatic PRF\n * fallback. First save still throws if the authenticator lacks PRF.\n */\nexport function defaultByokStorage(\n options?: PasskeyStorageOptions,\n): KeyringStorage {\n const secure =\n typeof globalThis.isSecureContext !== 'boolean' ||\n globalThis.isSecureContext\n if (!isPasskeyStorageSupported() || !secure) {\n return {\n ...memoryStorage(),\n warning:\n 'Passkeys are unavailable in this context. Keys stay in memory for this tab only.',\n }\n }\n return passkeyStorage(options)\n}\n"],"mappings":"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"}
@@ -474,6 +474,7 @@ function normalizeConnectionAdapter(connection) {
474
474
  if (!hasConnect) throw new Error("Connection adapter must provide either connect or both subscribe and send");
475
475
  let activeBuffer = [];
476
476
  let activeWaiters = [];
477
+ let activeSubscriber;
477
478
  function push(chunk, runId) {
478
479
  if (runId) chunkRunIds.set(chunk, runId);
479
480
  const waiter = activeWaiters.shift();
@@ -481,17 +482,14 @@ function normalizeConnectionAdapter(connection) {
481
482
  else activeBuffer.push(chunk);
482
483
  }
483
484
  async function waitUntilSubscriberIdle(abortSignal) {
484
- const idle = () => activeBuffer.length === 0 && (activeWaiters.length > 0 || abortSignal?.aborted);
485
+ const idle = () => activeSubscriber !== activeWaiters || activeBuffer.length === 0 && (activeWaiters.length > 0 || abortSignal?.aborted);
485
486
  for (let i = 0; i < 16 && !abortSignal?.aborted; i++) {
486
487
  if (idle()) return;
487
488
  await Promise.resolve();
488
489
  }
489
- let macrotaskWaits = 0;
490
490
  while (!abortSignal?.aborted) {
491
491
  if (idle()) return;
492
492
  await new Promise((resolve) => setTimeout(resolve, 0));
493
- macrotaskWaits++;
494
- if (activeWaiters.length === 0 && macrotaskWaits >= 32) return;
495
493
  }
496
494
  }
497
495
  return {
@@ -501,19 +499,24 @@ function normalizeConnectionAdapter(connection) {
501
499
  activeBuffer = myBuffer;
502
500
  activeWaiters = myWaiters;
503
501
  return (async function* () {
504
- while (!abortSignal?.aborted) {
505
- let chunk;
506
- const buffered = myBuffer.shift();
507
- if (buffered !== void 0) chunk = buffered;
508
- else chunk = await new Promise((resolve) => {
509
- const onAbort = () => resolve(null);
510
- myWaiters.push((c) => {
511
- abortSignal?.removeEventListener("abort", onAbort);
512
- resolve(c);
502
+ activeSubscriber = myWaiters;
503
+ try {
504
+ while (!abortSignal?.aborted) {
505
+ let chunk;
506
+ const buffered = myBuffer.shift();
507
+ if (buffered !== void 0) chunk = buffered;
508
+ else chunk = await new Promise((resolve) => {
509
+ const onAbort = () => resolve(null);
510
+ myWaiters.push((c) => {
511
+ abortSignal?.removeEventListener("abort", onAbort);
512
+ resolve(c);
513
+ });
514
+ abortSignal?.addEventListener("abort", onAbort, { once: true });
513
515
  });
514
- abortSignal?.addEventListener("abort", onAbort, { once: true });
515
- });
516
- if (chunk !== null) yield chunk;
516
+ if (chunk !== null) yield chunk;
517
+ }
518
+ } finally {
519
+ if (activeSubscriber === myWaiters) activeSubscriber = void 0;
517
520
  }
518
521
  })();
519
522
  },
@@ -1 +1 @@
1
- {"version":3,"file":"connection-adapters.js","names":[],"sources":["../../src/connection-adapters.ts"],"sourcesContent":["import {\n EventType,\n getChunkRunId as getNormalizedChunkRunId,\n restoreInboundChunk,\n tanstackMetadata,\n uiMessagesToWire,\n withTanstackMetadata,\n} from '@tanstack/ai/client'\nimport { ByokMissingError, isByokMissingBody } from '@tanstack/ai/byok'\nimport {\n createResponseStreamTextDecoder,\n getResponseStreamReader,\n} from './response-stream'\nimport { parseSseDataLine } from './sse-utils'\nimport type {\n ModelMessage,\n RunAgentResumeItem,\n RunErrorEvent,\n StreamChunk,\n UIMessage,\n} from '@tanstack/ai/client'\nimport type { ChatFetcher, ChatPendingInterrupt } from './types'\nimport { normalizeMessagesDates } from './message-date-normalizer'\n\n/**\n * Associates connect-wrapped chunks with the run they were produced under.\n * Content events (TEXT_MESSAGE_CONTENT, TOOL_CALL_*, …) carry no `runId` of\n * their own, so the connect wrapper stamps the caller's run id here. Lets\n * run-scoped consumers (e.g. clear-during-stream suppression) attribute those\n * otherwise-runless chunks to their originating request.\n */\nconst chunkRunIds = new WeakMap<StreamChunk, string>()\n\n/**\n * Resolve a chunk's run id, preferring the value on the chunk itself\n * (RUN_STARTED / RUN_FINISHED / RUN_ERROR carry one) and falling back to the\n * run the connect wrapper stamped it with.\n */\nexport function getChunkRunId(chunk: StreamChunk): string | undefined {\n // Prefer the client's request run id (stamped in `chunkRunIds`) over a\n // provider-assigned `chunk.runId`. Interrupt continuation correlation needs\n // the client's run identity to win when a provider stamps its own id; for\n // resumable reconnect/join the two ids match, so precedence is moot there.\n const requestRunId = chunkRunIds.get(chunk)\n return requestRunId ?? getNormalizedChunkRunId(chunk)\n}\n\n/**\n * Thrown when an SSE/HTTP stream ends with a non-empty unterminated buffer.\n * Indicates the connection was cut mid-line (server crash, dropped TCP, proxy\n * timeout) so the partial content cannot be safely parsed.\n */\nexport class StreamTruncatedError extends Error {\n constructor() {\n super(\n 'Stream ended with unterminated trailing data — connection was likely cut short.',\n )\n this.name = 'StreamTruncatedError'\n }\n}\n\nclass StreamReadError extends Error {\n constructor(cause: unknown) {\n super('Stream response body read failed', { cause })\n this.name = 'StreamReadError'\n }\n}\n\n/**\n * Thrown when a durable (id-tagged) run's stream ends with no terminal event\n * and a reconnect makes no forward progress — the run cannot complete, so the\n * consumer must not be left silently hanging on a stream that just stops.\n */\nexport class DurableStreamIncompleteError extends Error {\n constructor() {\n super(\n 'Durable run ended without a terminal event and could not resume — the run did not complete.',\n )\n this.name = 'DurableStreamIncompleteError'\n }\n}\n\n/**\n * Thrown when a durable run exceeds its reconnect ceiling. Bounds the\n * otherwise-unbounded reconnect loop so a flapping producer (or a proxy that\n * rolls the socket after every event) surfaces a failure instead of\n * reconnecting without end.\n */\nexport class StreamReconnectLimitError extends Error {\n constructor(attempts: number) {\n super(\n `Durable run exceeded its reconnect ceiling of ${attempts} attempts — giving up.`,\n )\n this.name = 'StreamReconnectLimitError'\n }\n}\n\n/**\n * Reconnect bounding for resumable streams. A constant throttle delay prevents a\n * hot loop against the origin, and the ceiling bounds a pathologically failing\n * run — but only counts CONSECUTIVE reconnects that made no forward progress.\n */\nexport interface ReconnectOptions {\n /**\n * Ceiling on the number of CONSECUTIVE reconnects that deliver no new events,\n * before failing with {@link StreamReconnectLimitError}. The counter resets to\n * zero whenever a reconnect makes forward progress, so a healthy long run —\n * even one behind a proxy that rolls the socket after every event — never\n * approaches it; the ceiling only fires when the run is genuinely stuck\n * (reconnecting repeatedly without receiving anything new). Default 5.\n */\n maxAttempts?: number\n /** Delay between reconnect attempts, in ms, to avoid hammering. Default 250. */\n delayMs?: number\n}\n\ninterface ResolvedReconnectOptions {\n maxAttempts: number\n delayMs: number\n}\n\nfunction resolveReconnectOptions(\n options: ReconnectOptions | undefined,\n): ResolvedReconnectOptions {\n const maxAttempts = options?.maxAttempts ?? 5\n const delayMs = options?.delayMs ?? 250\n // Reject non-finite / negative bounds up front: a NaN or Infinity maxAttempts\n // would make the ceiling ineffective (unbounded reconnects), and a non-finite\n // delayMs would remove throttling. Fail loudly on misconfiguration.\n if (!Number.isInteger(maxAttempts) || maxAttempts < 0) {\n throw new Error(\n `Invalid reconnect.maxAttempts: ${maxAttempts}. Must be a non-negative integer.`,\n )\n }\n if (!Number.isFinite(delayMs) || delayMs < 0) {\n throw new Error(\n `Invalid reconnect.delayMs: ${delayMs}. Must be a non-negative finite number.`,\n )\n }\n return { maxAttempts, delayMs }\n}\n\n/**\n * Reconnect bookkeeping shared by every resumable-stream driver: de-dupes\n * offsets, tracks the last acknowledged offset, honors the SSE empty-id reset\n * convention, and bounds consecutive no-progress reconnects behind a\n * throttling delay. Extracted out of {@link resumableStream} so a WebSocket\n * reconnect driver can reuse the exact same semantics.\n */\nexport interface ReconnectTracker {\n /** The most recently accepted (non-duplicate, non-empty) offset, if any. */\n readonly lastEventId: string | undefined\n /**\n * Record an incoming offset. Returns `'reset'` for an empty id (SSE's\n * resume-cursor reset — clears the de-dupe set and `lastEventId`),\n * `'duplicate'` for an already-seen id, and `'new'` otherwise (including\n * `undefined`, which is untracked — no offset to remember).\n */\n note: (id: string | undefined) => 'new' | 'duplicate' | 'reset'\n /**\n * Throttle before a reconnect attempt. Resets the no-progress counter when\n * `madeProgress` is true; otherwise increments it and throws\n * {@link StreamReconnectLimitError} once it exceeds the configured ceiling.\n */\n waitBeforeReconnect: (\n madeProgress: boolean,\n signal?: AbortSignal,\n ) => Promise<void>\n}\n\n/** Create a {@link ReconnectTracker} bound to the given reconnect bounds. */\nexport function createReconnectTracker(\n options?: ReconnectOptions,\n): ReconnectTracker {\n const reconnect = resolveReconnectOptions(options)\n // Retains every delivered offset for the run's lifetime. Intentionally\n // bounded by run length (not evicted): a conforming server replays strictly\n // after the acknowledged offset, so this only needs to catch the single\n // boundary event on reconnect, but keeping the full set keeps de-dup\n // correct even if a server replays a wider overlap.\n const seen = new Set<string>()\n let lastEventId: string | undefined\n let reconnectAttempts = 0\n return {\n get lastEventId() {\n return lastEventId\n },\n note(id) {\n if (id === undefined) return 'new'\n if (id === '') {\n // SSE spec: an empty `id:` resets the resume cursor. Drop the last\n // offset and clear the de-dupe set; the chunk itself still delivers.\n lastEventId = undefined\n seen.clear()\n return 'reset'\n }\n if (seen.has(id)) return 'duplicate'\n seen.add(id)\n lastEventId = id\n return 'new'\n },\n // Bound only CONSECUTIVE no-progress reconnects. A reconnect that made\n // forward progress resets the counter, so a healthy long run (even one\n // whose socket rolls after every event) never approaches the ceiling; it\n // fires only when the run is genuinely stuck — reconnecting repeatedly\n // with nothing new.\n async waitBeforeReconnect(madeProgress, signal) {\n if (madeProgress) {\n reconnectAttempts = 0\n } else {\n reconnectAttempts += 1\n if (reconnectAttempts > reconnect.maxAttempts) {\n throw new StreamReconnectLimitError(reconnect.maxAttempts)\n }\n }\n await abortableDelay(reconnect.delayMs, signal)\n },\n }\n}\n\n/** Resolve after `ms`, or immediately once `signal` aborts. Never rejects. */\nfunction abortableDelay(ms: number, signal?: AbortSignal): Promise<void> {\n if (ms <= 0 || signal?.aborted) return Promise.resolve()\n return new Promise((resolve) => {\n const onAbort = () => {\n clearTimeout(timer)\n resolve()\n }\n const timer = setTimeout(() => {\n signal?.removeEventListener('abort', onAbort)\n resolve()\n }, ms)\n signal?.addEventListener('abort', onAbort, { once: true })\n })\n}\n\nfunction generateRunId(prefix: string): string {\n return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`\n}\n\n/**\n * Asserts an id is present when synthesizing a terminal event. The chat\n * client always supplies `runContext.threadId` / `runContext.runId`, so an\n * absent id at this layer indicates the adapter was wired up by a caller\n * that bypassed that contract — surface it rather than fabricating one.\n */\nfunction requireSyntheticId(\n value: string | undefined,\n field: 'threadId' | 'runId',\n): string {\n if (!value) {\n throw new Error(\n `Cannot synthesize terminal event: ${field} not supplied via runContext and not observed in the upstream stream.`,\n )\n }\n return value\n}\n\n/**\n * Merge custom headers into request headers\n */\nfunction mergeHeaders(\n customHeaders?: Record<string, string> | Headers,\n): Record<string, string> {\n if (!customHeaders) {\n return {}\n }\n if (customHeaders instanceof Headers) {\n const result: Record<string, string> = {}\n customHeaders.forEach((value, key) => {\n result[key] = value\n })\n return result\n }\n return customHeaders\n}\n\n/**\n * Request header carrying the client-chosen run id to a delivery-durability\n * sink. The durable log is then keyed by the SAME id the client already holds,\n * so a later join/resume can address the run without first reading back a\n * server-generated id. Sent as a header — NOT a query param — so the POST URL\n * stays byte-identical to a plain, non-durable request; a server that isn't\n * durable simply ignores the header. (The GET join path keeps `?runId` in the\n * query, since a GET has no body/handler contract to disturb.)\n */\nconst RUN_ID_HEADER = 'X-Run-Id'\n\nfunction runIdHeader(runId: string | undefined): Record<string, string> {\n return runId === undefined ? {} : { [RUN_ID_HEADER]: runId }\n}\n\nfunction withSearchParams(url: string, values: Record<string, string>): string {\n const hashIndex = url.indexOf('#')\n const hash = hashIndex === -1 ? '' : url.slice(hashIndex)\n const withoutHash = hashIndex === -1 ? url : url.slice(0, hashIndex)\n const queryIndex = withoutHash.indexOf('?')\n const base =\n queryIndex === -1 ? withoutHash : withoutHash.slice(0, queryIndex)\n const search = new URLSearchParams(\n queryIndex === -1 ? '' : withoutHash.slice(queryIndex + 1),\n )\n for (const [key, value] of Object.entries(values)) search.set(key, value)\n const query = search.toString()\n return `${base}${query.length === 0 ? '' : `?${query}`}${hash}`\n}\n\n/**\n * Read lines from a stream (newline-delimited)\n */\nasync function* readStreamLines(\n reader: ReadableStreamDefaultReader<Uint8Array>,\n abortSignal?: AbortSignal,\n): AsyncGenerator<string> {\n try {\n const decoder = createResponseStreamTextDecoder()\n let buffer = ''\n\n while (!abortSignal?.aborted) {\n let result: ReadableStreamReadResult<Uint8Array>\n try {\n result = await reader.read()\n } catch (error) {\n if (abortSignal?.aborted) return\n throw new StreamReadError(error)\n }\n const { done, value } = result\n if (done) break\n\n buffer += decoder.decode(value, { stream: true })\n const lines = buffer.split('\\n')\n\n // Keep the last incomplete line in the buffer\n buffer = lines.pop() || ''\n\n for (const line of lines) {\n // Strip a trailing CR so a CRLF stream matches the LF path (and the\n // XHR reader). Without this an exact-equality check like the `[DONE]`\n // sentinel in linesToSSEEvents would miss `data: [DONE]\\r`.\n const normalized = line.endsWith('\\r') ? line.slice(0, -1) : line\n if (normalized.trim()) {\n yield normalized\n }\n }\n }\n\n // Flush the decoder: a connection cut mid-multibyte-character leaves bytes\n // held inside the streaming TextDecoder. Draining them here (as U+FFFD)\n // makes the trailing-buffer check below see the incomplete tail and report\n // truncation instead of silently swallowing it.\n buffer += decoder.decode()\n\n // A non-empty trailing buffer means the connection was cut mid-line.\n // Surface this as an error so the chat client transitions to 'error'\n // state instead of silently presenting a partial stream as success.\n // Skip when the consumer aborted — a user-initiated stop() interrupting\n // mid-line is expected, not a truncation bug.\n if (buffer.trim() && !abortSignal?.aborted) {\n throw new StreamTruncatedError()\n }\n } finally {\n reader.releaseLock()\n }\n}\n\n/** A parsed stream chunk paired with its adapter-owned delivery offset (if any). */\ninterface StreamEvent {\n chunk: StreamChunk\n id?: string\n}\n\n/**\n * Type guard for a durable NDJSON envelope `{ id, chunk }`. NDJSON has no\n * native event-id field, so durability rides the offset inside the payload.\n * A bare `StreamChunk` always has a top-level `type`, and the envelope never\n * does, so the two forms are unambiguous — a non-durable line stays bare.\n */\nfunction isNdjsonEnvelope(\n value: unknown,\n): value is { id: string; chunk: StreamChunk } {\n return (\n typeof value === 'object' &&\n value !== null &&\n 'chunk' in value &&\n 'id' in value &&\n typeof (value as { id: unknown }).id === 'string' &&\n !('type' in value)\n )\n}\n\n/** Rebuild pre-wire extras after SSE/NDJSON ingest. */\nfunction restoreInboundUsage(chunk: StreamChunk): StreamChunk {\n return restoreInboundChunk(chunk)\n}\n\nfunction sseChunkModel(chunk: StreamChunk): string | undefined {\n const tanstackModel = tanstackMetadata(chunk)?.model\n if (typeof tanstackModel === 'string') return tanstackModel\n const usage = 'usage' in chunk ? chunk.usage : undefined\n if (Array.isArray(usage)) {\n const model = (usage[0] as { model?: unknown } | undefined)?.model\n if (typeof model === 'string') return model\n }\n return undefined\n}\n\n/**\n * Parse SSE-format lines into stream events, pairing each chunk with the `id:`\n * offset of the event it arrived on. Shared by the fetch- and XHR-backed SSE\n * adapters so both track delivery offsets identically.\n *\n * Accepts either `data: {...}` lines or bare JSON lines. Skips comments\n * starting with `:` (proxies and CDNs inject these as keepalives) and the\n * `event:` / `retry:` SSE control fields. A `[DONE]` sentinel is treated as a\n * terminal event: a synthesized RUN_FINISHED is yielded using the most recent\n * upstream `threadId` / `runId` (falling back to `fallbackIds`), so the\n * consumer sees a clean terminal event with real correlation ids.\n *\n * A JSON parse failure throws — the consumer surfaces it as an error.\n */\nasync function* linesToSSEEvents(\n lines: AsyncIterable<string>,\n fallbackIds?: { threadId?: string; runId?: string },\n): AsyncGenerator<StreamEvent> {\n let lastThreadId: string | undefined\n let lastRunId: string | undefined\n let lastModel: string | undefined\n let pendingId: string | undefined\n for await (const line of lines) {\n if (line === 'id' || line.startsWith('id:')) {\n // SSE spec: strip a single leading space after the colon, preserve the\n // rest verbatim so an opaque adapter offset round-trips exactly (do NOT\n // trim, which would mangle a legitimate offset). An empty value is kept as\n // '' and resets the resume cursor downstream (see resumableStream).\n const rawId = line === 'id' ? '' : line.slice(3)\n pendingId = rawId.startsWith(' ') ? rawId.slice(1) : rawId\n continue\n }\n // Assumes the durability wire emits one `id:` immediately followed by one\n // `data:` per event (both shipped sinks do). `pendingId` attaches to the\n // next data line and is cleared after it; blank-line event boundaries are\n // stripped upstream, so a hand-rolled server that emits an id-only event or\n // a persistent `id:` across events is not supported here.\n if (\n line.startsWith(':') ||\n line.startsWith('event:') ||\n line.startsWith('retry:')\n ) {\n continue\n }\n const data = parseSseDataLine(line)\n if (data === '[DONE]') {\n yield {\n chunk: withTanstackMetadata(\n {\n type: EventType.RUN_FINISHED,\n threadId: lastThreadId ?? fallbackIds?.threadId ?? '',\n runId: lastRunId ?? fallbackIds?.runId ?? '',\n timestamp: Date.now(),\n },\n {\n finishReason: 'stop',\n ...(lastModel !== undefined ? { model: lastModel } : {}),\n },\n ) as StreamChunk,\n }\n return\n }\n const chunk = restoreInboundUsage(JSON.parse(data) as StreamChunk)\n if ('threadId' in chunk && typeof chunk.threadId === 'string') {\n lastThreadId = chunk.threadId\n }\n if ('runId' in chunk && typeof chunk.runId === 'string') {\n lastRunId = chunk.runId\n }\n const model = sseChunkModel(chunk)\n if (model !== undefined) lastModel = model\n const id = pendingId\n pendingId = undefined\n yield { chunk, ...(id !== undefined ? { id } : {}) }\n }\n}\n\n/**\n * Parse NDJSON-format lines into stream events. Durable streams emit each line\n * as an `{ id, chunk }` envelope carrying the delivery offset; non-durable\n * streams emit bare chunks. Both are auto-detected (see {@link isNdjsonEnvelope}),\n * so an untagged stream behaves exactly as a plain single fetch used to.\n */\nasync function* linesToNdjsonEvents(\n lines: AsyncIterable<string>,\n): AsyncGenerator<StreamEvent> {\n for await (const line of lines) {\n const parsed = JSON.parse(line) as unknown\n if (isNdjsonEnvelope(parsed)) {\n yield { chunk: restoreInboundUsage(parsed.chunk), id: parsed.id }\n } else {\n yield { chunk: restoreInboundUsage(parsed as StreamChunk) }\n }\n }\n}\n\nasync function assertResponseOk(response: Response): Promise<void> {\n if (response.ok) return\n if (response.status === 401) {\n const body: unknown = await response\n .clone()\n .json()\n .catch(() => null)\n if (isByokMissingBody(body)) {\n throw new ByokMissingError(body.error.provider)\n }\n }\n throw new Error(\n `HTTP error! status: ${response.status} ${response.statusText}`,\n )\n}\n\nfunction errorFromXhrStatus(xhr: XMLHttpRequest): Error {\n if (xhr.status === 401) {\n let parsed: unknown = null\n try {\n parsed = JSON.parse(xhr.responseText)\n } catch {\n parsed = null\n }\n if (isByokMissingBody(parsed)) {\n return new ByokMissingError(parsed.error.provider)\n }\n }\n return new Error(`XHR error! status: ${xhr.status} ${xhr.statusText}`)\n}\n\n/**\n * GET the hydration endpoint for a thread and parse its JSON `{ messages,\n * activeRun }` body. This is the transport-agnostic reconnect probe: keyed on\n * the STABLE thread id, it returns the stored transcript and — if a run is still\n * generating — a cursor the caller tails via `joinRun`. Shared by every fetch/\n * XHR adapter so the client never has to know which transport is in use.\n */\nasync function fetchThreadHydration(\n fetchClient: typeof globalThis.fetch,\n url: string,\n headers: Record<string, string>,\n credentials: RequestCredentials,\n threadId: string,\n): Promise<ChatHydrationResult> {\n const response = await fetchClient(withSearchParams(url, { threadId }), {\n method: 'GET',\n headers: { Accept: 'application/json', ...headers },\n credentials,\n })\n await assertResponseOk(response)\n const data = (await response.json()) as {\n messages?: Array<UIMessage>\n activeRun?: { runId?: unknown } | null\n interrupts?: {\n runId?: unknown\n pending?: unknown\n } | null\n }\n const activeRun =\n data.activeRun && typeof data.activeRun.runId === 'string'\n ? { runId: data.activeRun.runId }\n : null\n const interrupts =\n data.interrupts &&\n typeof data.interrupts.runId === 'string' &&\n Array.isArray(data.interrupts.pending) &&\n data.interrupts.pending.length > 0\n ? {\n runId: data.interrupts.runId,\n pending: data.interrupts.pending as Array<ChatPendingInterrupt>,\n }\n : null\n return {\n messages: Array.isArray(data.messages)\n ? normalizeMessagesDates(data.messages)\n : [],\n activeRun,\n interrupts,\n }\n}\n\n/**\n * GET the hydration endpoint for a generation thread and parse its JSON\n * `{ resumeSnapshot, activeRun }` body. Mirrors {@link fetchThreadHydration} for\n * the generation clients: keyed on the stable thread id, it returns the last\n * generation's resume snapshot (re-validated client-side before adoption) and —\n * if a run is still generating — a cursor. Shared by every fetch/XHR adapter.\n */\nasync function fetchGenerationHydration(\n fetchClient: typeof globalThis.fetch,\n url: string,\n headers: Record<string, string>,\n credentials: RequestCredentials,\n threadId: string,\n): Promise<GenerationHydrationResult> {\n const response = await fetchClient(withSearchParams(url, { threadId }), {\n method: 'GET',\n headers: { Accept: 'application/json', ...headers },\n credentials,\n })\n await assertResponseOk(response)\n const raw: unknown = await response.json()\n // A 200 carrying `null` is a legitimate hydration miss — the server has no\n // record for this thread — and reading `.activeRun` off `null` would throw.\n if (raw === null) {\n return { resumeSnapshot: null, activeRun: null }\n }\n // Any OTHER non-object body is a broken endpoint, not an empty thread.\n // Reporting it as a miss would present a misconfigured route as a fresh\n // thread; the client surfaces this through its own error channel instead.\n if (typeof raw !== 'object' || Array.isArray(raw)) {\n throw new Error(\n `Generation hydration expected a JSON object from ${url}, received ${Array.isArray(raw) ? 'an array' : typeof raw}.`,\n )\n }\n const data = raw as {\n resumeSnapshot?: GenerationHydrationResult['resumeSnapshot']\n activeRun?: { runId?: unknown } | null\n }\n const activeRun =\n data.activeRun && typeof data.activeRun.runId === 'string'\n ? { runId: data.activeRun.runId }\n : null\n return {\n resumeSnapshot: data.resumeSnapshot ?? null,\n activeRun,\n }\n}\n\n/** Yield SSE stream events (chunk + offset) from a fetch Response body. */\nasync function* responseToSSEEvents(\n response: Response,\n abortSignal?: AbortSignal,\n fallbackIds?: { threadId?: string; runId?: string },\n): AsyncGenerator<StreamEvent> {\n await assertResponseOk(response)\n const reader = getResponseStreamReader(response)\n yield* linesToSSEEvents(readStreamLines(reader, abortSignal), fallbackIds)\n}\n\n/** Yield NDJSON stream events (chunk + offset) from a fetch Response body. */\nasync function* responseToNdjsonEvents(\n response: Response,\n abortSignal?: AbortSignal,\n): AsyncGenerator<StreamEvent> {\n await assertResponseOk(response)\n const reader = getResponseStreamReader(response)\n yield* linesToNdjsonEvents(readStreamLines(reader, abortSignal))\n}\n\nasync function* responseToSSEChunks(\n response: Response,\n abortSignal?: AbortSignal,\n): AsyncGenerator<StreamChunk> {\n for await (const { chunk } of responseToSSEEvents(response, abortSignal)) {\n yield chunk\n }\n}\n\n/**\n * A re-issuable event source. Given extra headers (a `Last-Event-ID` on a\n * reconnect) and an abort signal, it opens the transport and yields stream\n * events. {@link resumableStream} calls it once per attempt, so each call MUST\n * open a fresh underlying request (a new fetch or a new XHR).\n */\ntype StreamEventSource = (\n extraHeaders: Record<string, string>,\n abortSignal?: AbortSignal,\n) => AsyncIterable<StreamEvent>\n\n/**\n * Build a fetch-backed {@link StreamEventSource}. `parseResponse` decodes the\n * body into events (SSE or NDJSON) — the reconnect engine is identical for both.\n */\nfunction fetchEventSource(\n fetchClient: typeof globalThis.fetch,\n url: string,\n requestInit: RequestInit,\n parseResponse: (\n response: Response,\n abortSignal?: AbortSignal,\n ) => AsyncIterable<StreamEvent>,\n): StreamEventSource {\n return async function* (extraHeaders, abortSignal) {\n let response: Response\n try {\n response = await fetchClient(url, {\n ...requestInit,\n headers: {\n ...(requestInit.headers as Record<string, string> | undefined),\n ...extraHeaders,\n },\n ...(abortSignal ? { signal: abortSignal } : {}),\n })\n } catch (error) {\n // A fetch REJECTION (device offline, DNS blip, connection refused) is a\n // recoverable transport failure, not a fatal one — surface it as\n // StreamReadError so resumableStream retries from the last offset, mirroring\n // the XHR path (whose onerror wraps the same way). On a genuine abort this\n // wraps the AbortError too, but that's harmless: resumableStream checks\n // `abortSignal.aborted` first and returns, so the wrapped error's type is\n // never inspected. Without an offset (initial connect / non-durable), it\n // still surfaces as a hard failure.\n throw new StreamReadError(error)\n }\n yield* parseResponse(response, abortSignal)\n }\n}\n\n/**\n * Drive a {@link StreamEventSource} with native-style resumability. Each event's\n * adapter-owned delivery offset (its `id`) is remembered; if the connection\n * drops or ends before a terminal event, the source is re-opened with a\n * `Last-Event-ID` header so the server replays strictly after the last offset.\n * Already-seen offsets are de-duped, so an overlapping replay is safe.\n *\n * When the server does NOT tag events (no durability), no offset is ever seen,\n * so no reconnect happens — behaviour is identical to a plain single request.\n * This engine is transport-agnostic: fetch/XHR × SSE/NDJSON all share it, the\n * only difference being the {@link StreamEventSource} they pass in.\n */\nasync function* resumableStream(\n openEventSource: StreamEventSource,\n abortSignal?: AbortSignal,\n reconnectOptions?: ReconnectOptions,\n): AsyncGenerator<StreamChunk> {\n const tracker = createReconnectTracker(reconnectOptions)\n\n for (;;) {\n if (abortSignal?.aborted) return\n const extraHeaders: Record<string, string> =\n tracker.lastEventId !== undefined\n ? { 'Last-Event-ID': tracker.lastEventId }\n : {}\n\n let sawTerminal = false\n let progressed = false\n try {\n for await (const { chunk, id } of openEventSource(\n extraHeaders,\n abortSignal,\n )) {\n if (tracker.note(id) === 'duplicate') continue\n progressed = true\n if (chunk.type === 'RUN_FINISHED' || chunk.type === 'RUN_ERROR') {\n sawTerminal = true\n }\n yield chunk\n // Do NOT stop on a terminal mid-source: an agent loop emits one\n // RUN_STARTED/RUN_FINISHED pair PER turn, so a tool-calling run carries\n // several RUN_FINISHED events before the run is truly done. Returning on\n // the first one would drop every subsequent turn (the tool result and\n // the final answer). Instead, drain the event source to its natural end\n // — the server closes the response only when the run is actually\n // complete — and use `sawTerminal` below to decide done-vs-reconnect.\n }\n } catch (error) {\n if (abortSignal?.aborted) return\n // A transport drop is resumable once we hold an offset — retry from it,\n // even if THIS attempt made no new progress. A caught-up run whose parked\n // long-poll socket drops (or a proxy that drops just after replaying the\n // de-duped overlap) is transient, not fatal; the consecutive-no-progress\n // ceiling in waitBeforeReconnect already bounds a genuinely stuck flapper,\n // so a per-attempt progress requirement here would only convert\n // recoverable drops into hard failures on flaky (mobile/edge) networks.\n // Without an offset (a non-durable stream), surface the failure.\n if (\n (error instanceof StreamTruncatedError ||\n error instanceof StreamReadError) &&\n tracker.lastEventId !== undefined\n ) {\n await tracker.waitBeforeReconnect(progressed, abortSignal)\n continue\n }\n throw error\n }\n\n if (abortSignal?.aborted) return\n\n // The source ended after delivering a terminal event: the run is genuinely\n // finished (for an agentic run this is the LAST turn's terminal, since we no\n // longer stop on intermediate ones). Stop — reconnecting a durable run here\n // would re-open past the final offset and see an empty window.\n if (sawTerminal) return\n\n if (tracker.lastEventId !== undefined) {\n // A durable (id-tagged) run.\n if (progressed) {\n // Clean end WITHOUT a terminal event but we advanced — the producer is\n // still going (or the socket rolled over). Reconnect from the last\n // offset (backing off to avoid a hot loop against the origin). Progress\n // resets the no-progress ceiling.\n await tracker.waitBeforeReconnect(true, abortSignal)\n continue\n }\n // Ended without a terminal event AND made no forward progress on this\n // pass: the run cannot complete. Surface an error rather than returning\n // silently, which would leave the consumer with neither a terminal event\n // nor a failure.\n //\n // Invariant this relies on: a durable transport must never surface an\n // empty long-poll window as a CLEAN end while the producer is still\n // alive. Both shipped backends honor it — memoryStream parks until data\n // or completion, and durableStream keeps one continuous response across\n // windows — so this fires only on a genuinely complete-but-unterminated\n // log. A custom StreamDurability transport that ends a response empty\n // mid-run would trip this; keep the response open until data or terminal.\n throw new DurableStreamIncompleteError()\n }\n\n // A non-durable (untagged) stream that ended cleanly. Legitimate — the\n // upper layer synthesizes a terminal event. Stop.\n return\n }\n}\n\n/**\n * Per-send context provided by the chat client to the connection adapter.\n * The adapter combines this with serialized messages to build a full\n * AG-UI `RunAgentInput` payload.\n */\nexport interface RunAgentInputContext {\n threadId: string\n runId: string\n parentRunId?: string\n /** AG-UI interrupt resume entries returned to the server on a follow-up run. */\n resume?: Array<RunAgentResumeItem>\n /** Client-declared tools to advertise in the request payload. */\n clientTools?: Array<{\n name: string\n description: string\n parameters: unknown\n }>\n /** Arbitrary user-controlled passthrough data. */\n forwardedProps?: Record<string, unknown>\n /** Extra request headers for this run (e.g. BYOK keys). POST only. */\n headers?: Record<string, string>\n}\n\nexport interface ConnectConnectionAdapter {\n /**\n * Connect and return an async iterable of StreamChunks.\n */\n connect: (\n messages: Array<UIMessage> | Array<ModelMessage>,\n data?: Record<string, any>,\n abortSignal?: AbortSignal,\n runContext?: RunAgentInputContext,\n ) => AsyncIterable<StreamChunk>\n /**\n * Fetch server-driven hydration for a generation `threadId`: the last\n * generation's resume snapshot, plus a cursor to a run still generating if\n * one exists. The generation client calls this itself on mount when\n * `persistence: true` (no loader/prop) and repaints the snapshot — it never\n * auto-starts a run. Read-only JSON GET (`?threadId`), so it is\n * transport-agnostic. Optional and feature-detected exactly like the chat\n * `hydrate` handler.\n */\n hydrateGeneration?: (threadId: string) => Promise<GenerationHydrationResult>\n /**\n * Re-attach to a run that is still generating and replay it from the start\n * (read-only `?offset=-1&runId` against the delivery-durability log). The\n * generation client tails this on mount when hydration reports a run still in\n * flight, so a dropped connection or a full reload finishes the generation in\n * place — the same durability replay the chat client uses. Optional and\n * feature-detected; present on `fetchServerSentEvents` / `fetchHttpStream`.\n */\n joinRun?: (\n runId: string,\n abortSignal?: AbortSignal,\n ) => AsyncIterable<StreamChunk>\n /**\n * Fetch server-driven hydration for a chat `threadId`: the stored transcript\n * plus a cursor to an in-flight run and any pending interrupts. The chat\n * client calls this itself on mount when `persistence: true` (no loader/prop)\n * and repaints it — it never auto-sends. Read-only JSON GET (`?threadId`), so\n * it is transport-agnostic. Optional and feature-detected; present on\n * `fetchServerSentEvents` / `fetchHttpStream`, and on `stream()` /\n * `rpcStream()` when supplied via {@link StreamConnectionHandlers}.\n */\n hydrate?: (threadId: string) => Promise<ChatHydrationResult>\n}\n\n/**\n * Server-resolved hydration for a generation thread. `resumeSnapshot` is the\n * last generation's lightweight snapshot (validated client-side before it is\n * adopted); `activeRun` is a cursor to a run still generating for the thread\n * (or `null`).\n *\n * Field-for-field compatible with `@tanstack/ai-persistence`'s\n * `ReconstructedGeneration` (the body `reconstructGeneration` returns) — the\n * client never imports that package, so this is a structural contract, not a\n * shared type. Two deliberate widenings on this side: `schemaVersion` is\n * optional (the server always writes `1`, but a hand-written fixture need not),\n * and `status` also admits `'idle'`, which the server's mapper never emits.\n * Only a client-local snapshot reaches it, when `stop()` retires a cancelled\n * run.\n */\nexport interface GenerationHydrationResult {\n resumeSnapshot: {\n schemaVersion?: 1\n resumeState: { threadId: string; runId: string } | null\n status: 'idle' | 'running' | 'complete' | 'error'\n result?: unknown\n error?: { message: string; code?: string }\n activity?: string\n } | null\n activeRun: { runId: string } | null\n}\n\n/**\n * Server-resolved hydration for a thread. `messages` is the stored transcript;\n * `activeRun` is a cursor to a run still generating for the thread (or `null`).\n * Keyed on the STABLE thread id — the client never handles a run id, so a turn\n * that spans several runs (interrupt/tool continuations) reconnects correctly.\n */\nexport interface ChatHydrationResult {\n messages: Array<UIMessage>\n activeRun: { runId: string } | null\n /**\n * Pending human-in-the-loop interrupts for the thread and the run they paused,\n * so a reload (or another device) re-prompts the approval from the server. The\n * client restores them exactly as a persisted resume snapshot would.\n */\n interrupts: {\n runId: string\n pending: Array<ChatPendingInterrupt>\n } | null\n}\n\n/**\n * A {@link ConnectConnectionAdapter} that also supports joining an existing run\n * (a second tab, or re-attaching after a full reload) via `joinRun`, replaying\n * the ordered stream from the start off the server's delivery-durability sink.\n */\nexport interface ResumableConnectConnectionAdapter extends ConnectConnectionAdapter {\n /**\n * Join an in-flight or finished run by id, replaying from the start\n * (`?offset=-1`). Read-only — sends no messages.\n */\n joinRun: (\n runId: string,\n abortSignal?: AbortSignal,\n ) => AsyncIterable<StreamChunk>\n /**\n * Fetch server-authoritative hydration for `threadId`: the stored transcript,\n * and a cursor to an in-flight run if one exists. The client calls this itself\n * on mount (no loader/prop), then tails `activeRun` via `joinRun`. Read-only\n * JSON GET (`?threadId`), so it is transport-agnostic regardless of how the\n * delivery stream is served.\n */\n hydrate?: (threadId: string) => Promise<ChatHydrationResult>\n}\n\nexport interface SubscribeConnectionAdapter {\n /**\n * Subscribe to stream chunks.\n */\n subscribe: (abortSignal?: AbortSignal) => AsyncIterable<StreamChunk>\n /**\n * Send a request; chunks arrive through subscribe().\n */\n send: (\n messages: Array<UIMessage> | Array<ModelMessage>,\n data?: Record<string, any>,\n abortSignal?: AbortSignal,\n runContext?: RunAgentInputContext,\n ) => Promise<void>\n /**\n * Re-attach to an existing run by id, replaying its stream from the start off\n * the server's delivery-durability sink. Present only when the underlying\n * connection is resumable (a `ResumableConnectConnectionAdapter`). Used to\n * rejoin an in-flight run after a full page reload.\n */\n joinRun?: (\n runId: string,\n abortSignal?: AbortSignal,\n ) => AsyncIterable<StreamChunk>\n /**\n * Server-authoritative hydration for a thread (transcript + in-flight-run\n * cursor). Present only when the underlying connection supports it. The client\n * calls it on mount to re-hydrate without any app-side loader or prop.\n */\n hydrate?: (threadId: string) => Promise<ChatHydrationResult>\n}\n\n/**\n * Connection adapter union.\n * Provide either `connect`, or `subscribe` + `send`.\n */\nexport type ConnectionAdapter =\n | ConnectConnectionAdapter\n | SubscribeConnectionAdapter\n\n/**\n * Normalize a ConnectionAdapter to subscribe/send operations.\n *\n * If a connection provides native subscribe/send, that mode is used.\n * Otherwise, connect() is wrapped using an async queue.\n */\nexport function normalizeConnectionAdapter(\n connection: ConnectionAdapter | undefined,\n): SubscribeConnectionAdapter {\n if (!connection) {\n throw new Error('Connection adapter is required')\n }\n\n const hasConnect = 'connect' in connection\n const hasSubscribe = 'subscribe' in connection\n const hasSend = 'send' in connection\n\n if (hasConnect && (hasSubscribe || hasSend)) {\n throw new Error(\n 'Connection adapter must provide either connect or both subscribe and send, not both modes',\n )\n }\n\n if (hasSubscribe && hasSend) {\n const joinRun = (connection as SubscribeConnectionAdapter).joinRun?.bind(\n connection,\n )\n const hydrate = (connection as SubscribeConnectionAdapter).hydrate?.bind(\n connection,\n )\n return {\n subscribe: connection.subscribe.bind(connection),\n send: connection.send.bind(connection),\n ...(joinRun ? { joinRun } : {}),\n ...(hydrate ? { hydrate } : {}),\n }\n }\n\n if (!hasConnect) {\n throw new Error(\n 'Connection adapter must provide either connect or both subscribe and send',\n )\n }\n\n // Legacy connect() wrapper\n let activeBuffer: Array<StreamChunk> = []\n let activeWaiters: Array<(chunk: StreamChunk | null) => void> = []\n\n function push(chunk: StreamChunk, runId?: string): void {\n if (runId) {\n chunkRunIds.set(chunk, runId)\n }\n const waiter = activeWaiters.shift()\n if (waiter) {\n waiter(chunk)\n } else {\n activeBuffer.push(chunk)\n }\n }\n\n async function waitUntilSubscriberIdle(\n abortSignal?: AbortSignal,\n ): Promise<void> {\n // Idle means the subscriber is waiting for the next chunk, so the\n // previous chunk has left processIncomingChunk. Empty waiters with an\n // empty buffer is in-flight delivery, not idle.\n const idle = () =>\n activeBuffer.length === 0 &&\n (activeWaiters.length > 0 || abortSignal?.aborted)\n for (let i = 0; i < 16 && !abortSignal?.aborted; i++) {\n if (idle()) return\n await Promise.resolve()\n }\n let macrotaskWaits = 0\n while (!abortSignal?.aborted) {\n if (idle()) return\n await new Promise<void>((resolve) => setTimeout(resolve, 0))\n macrotaskWaits++\n if (activeWaiters.length === 0 && macrotaskWaits >= 32) return\n }\n }\n\n return {\n subscribe(abortSignal?: AbortSignal): AsyncIterable<StreamChunk> {\n // Transfer ownership to the latest subscriber so only one active\n // subscribe() call receives chunks from the shared connect-wrapper queue.\n const myBuffer: Array<StreamChunk> = activeBuffer.splice(0)\n const myWaiters: Array<(chunk: StreamChunk | null) => void> = []\n activeBuffer = myBuffer\n activeWaiters = myWaiters\n\n return (async function* () {\n while (!abortSignal?.aborted) {\n let chunk: StreamChunk | null\n const buffered = myBuffer.shift()\n if (buffered !== undefined) {\n chunk = buffered\n } else {\n chunk = await new Promise<StreamChunk | null>((resolve) => {\n const onAbort = () => resolve(null)\n myWaiters.push((c) => {\n abortSignal?.removeEventListener('abort', onAbort)\n resolve(c)\n })\n abortSignal?.addEventListener('abort', onAbort, { once: true })\n })\n }\n if (chunk !== null) yield chunk\n }\n })()\n },\n async send(messages, data, abortSignal, runContext) {\n let hasTerminalEvent = false\n let upstreamThreadId: string | undefined\n let upstreamRunId: string | undefined\n try {\n const stream = connection.connect(\n messages,\n data,\n abortSignal,\n runContext,\n )\n for await (const chunk of stream) {\n if ('threadId' in chunk && typeof chunk.threadId === 'string') {\n upstreamThreadId = chunk.threadId\n }\n if ('runId' in chunk && typeof chunk.runId === 'string') {\n upstreamRunId = chunk.runId\n }\n if (chunk.type === 'RUN_FINISHED' || chunk.type === 'RUN_ERROR') {\n hasTerminalEvent = true\n }\n push(chunk, runContext?.runId)\n }\n\n // If the connect stream ended cleanly without a terminal event,\n // synthesize RUN_FINISHED so request-scoped consumers can complete.\n // The event payload may carry an upstream/provider runId when one was\n // observed, but stamp the caller's request runId so getChunkRunId()\n // correlates to activeRunIds / currentRunId (same as real stream chunks).\n if (!abortSignal?.aborted && !hasTerminalEvent) {\n push(\n withTanstackMetadata(\n {\n type: EventType.RUN_FINISHED,\n threadId: requireSyntheticId(\n upstreamThreadId ?? runContext?.threadId,\n 'threadId',\n ),\n runId: requireSyntheticId(\n upstreamRunId ?? runContext?.runId,\n 'runId',\n ),\n timestamp: Date.now(),\n },\n { finishReason: 'stop', model: 'connect-wrapper' },\n ) as StreamChunk,\n runContext?.runId,\n )\n }\n } catch (err) {\n if (!abortSignal?.aborted && !hasTerminalEvent) {\n // Guard synthesis: requireSyntheticId throws when no id is available,\n // and that must not replace the original `err` we are about to\n // rethrow. If we can't synthesize a terminal, the real failure still\n // surfaces below.\n try {\n const message =\n err instanceof Error ? err.message : 'Unknown error in connect()'\n const synthetic: RunErrorEvent = {\n type: EventType.RUN_ERROR,\n threadId: requireSyntheticId(\n upstreamThreadId ?? runContext?.threadId,\n 'threadId',\n ),\n runId: requireSyntheticId(\n upstreamRunId ?? runContext?.runId,\n 'runId',\n ),\n timestamp: Date.now(),\n message,\n }\n push(synthetic, runContext?.runId)\n } catch {\n // fall through to rethrow the original error\n }\n }\n throw err\n }\n await waitUntilSubscriberIdle(abortSignal)\n },\n // Expose joinRun only when the underlying connection is resumable. Require\n // a real function — `'joinRun' in connection` is true for\n // `{ joinRun: undefined }`, which would wrap a non-callable and throw on\n // rehydration rejoin.\n ...(typeof (connection as ResumableConnectConnectionAdapter).joinRun ===\n 'function'\n ? {\n joinRun: (runId: string, abortSignal?: AbortSignal) =>\n (connection as ResumableConnectConnectionAdapter).joinRun(\n runId,\n abortSignal,\n ),\n }\n : {}),\n ...(() => {\n // Capture under the typeof guard so `hydrate` narrows to the function type\n // (no non-null assertion). Present only when the connection supports it.\n const hydrate = (connection as ResumableConnectConnectionAdapter).hydrate\n return typeof hydrate === 'function'\n ? { hydrate: (threadId: string) => hydrate(threadId) }\n : {}\n })(),\n }\n}\n\n/**\n * Options for fetch-based connection adapters\n */\nexport interface FetchConnectionOptions {\n headers?: Record<string, string> | Headers\n credentials?: RequestCredentials\n signal?: AbortSignal\n body?: Record<string, any>\n fetchClient?: typeof globalThis.fetch\n /** Bounding for resumable-SSE reconnection (throttle delay + attempt ceiling). */\n reconnect?: ReconnectOptions\n}\n\n/**\n * Options for XHR-based connection adapters.\n */\nexport interface XhrConnectionOptions {\n headers?: Record<string, string> | Headers\n withCredentials?: boolean\n signal?: AbortSignal\n body?: Record<string, any>\n xhrFactory?: () => XMLHttpRequest\n /** Bounding for resumable reconnection (throttle delay + attempt ceiling). */\n reconnect?: ReconnectOptions\n}\n\ntype ResolvedConnectionOptions = Pick<\n FetchConnectionOptions,\n 'body' | 'headers'\n>\n\nfunction buildRunAgentInputBody(\n messages: Array<UIMessage> | Array<ModelMessage>,\n data: Record<string, any> | undefined,\n runContext: RunAgentInputContext | undefined,\n options: ResolvedConnectionOptions,\n): Record<string, unknown> {\n // Precedence (later spreads win): static adapter `body` is the base,\n // overridden by `runContext.forwardedProps`, overridden by per-message `data`.\n const wireMessages = uiMessagesToWire(messages)\n const forwardedProps = {\n ...options.body,\n ...(runContext?.forwardedProps ?? {}),\n ...data,\n }\n\n return {\n threadId: runContext?.threadId ?? generateRunId('thread'),\n runId: runContext?.runId ?? generateRunId('run'),\n ...(runContext?.parentRunId !== undefined && {\n parentRunId: runContext.parentRunId,\n }),\n ...(runContext?.resume !== undefined && { resume: runContext.resume }),\n state: {},\n messages: wireMessages,\n tools: runContext?.clientTools ?? [],\n context: [],\n forwardedProps,\n // Backward-compat mirror of `forwardedProps` under the legacy field name.\n data: { ...forwardedProps },\n }\n}\n\n/**\n * Create a Server-Sent Events connection adapter\n *\n * @param url - The API endpoint URL (or a function that returns the URL)\n * @param options - Fetch options (headers, credentials, body, etc.) or a function that returns options (can be async)\n * @returns A connection adapter for SSE streams\n *\n * @example\n * ```typescript\n * // Static URL\n * const connection = fetchServerSentEvents('/api/chat');\n *\n * // Dynamic URL\n * const connection = fetchServerSentEvents(() => `/api/chat?user=${userId}`);\n *\n * // With options\n * const connection = fetchServerSentEvents('/api/chat', {\n * headers: { 'Authorization': 'Bearer token' }\n * });\n *\n * // With dynamic options\n * const connection = fetchServerSentEvents('/api/chat', () => ({\n * headers: { 'Authorization': `Bearer ${getToken()}` }\n * }));\n *\n * // With additional body data\n * const connection = fetchServerSentEvents('/api/chat', async () => ({\n * body: {\n * provider: 'openai',\n * model: 'gpt-5.5',\n * }\n * }));\n * ```\n */\nexport function fetchServerSentEvents(\n url: string | (() => string),\n options:\n | FetchConnectionOptions\n | (() => FetchConnectionOptions | Promise<FetchConnectionOptions>) = {},\n): ResumableConnectConnectionAdapter {\n return {\n async *connect(messages, data, abortSignal, runContext) {\n // Resolve URL and options if they are functions\n const resolvedUrl = typeof url === 'function' ? url() : url\n const resolvedOptions =\n typeof options === 'function' ? await options() : options\n\n const requestHeaders: Record<string, string> = {\n 'Content-Type': 'application/json',\n ...mergeHeaders(resolvedOptions.headers),\n ...mergeHeaders(runContext?.headers),\n ...runIdHeader(runContext?.runId),\n }\n\n // Build AG-UI RunAgentInput payload.\n //\n // Precedence (later spreads win): static adapter `body` is the base,\n // overridden by `runContext.forwardedProps` (constructor body /\n // forwardedProps options), overridden by per-message `data` passed\n // to `connection.send`. Runtime values win over static config —\n // this matches the documented \"forwardedProps wins\" semantic.\n const requestBody = buildRunAgentInputBody(\n messages,\n data,\n runContext,\n resolvedOptions,\n )\n\n const fetchClient = resolvedOptions.fetchClient ?? fetch\n // `RequestInit.signal` is typed `AbortSignal | null` (no `undefined`\n // under `exactOptionalPropertyTypes`), so spread it conditionally\n // rather than passing `undefined` explicitly.\n const signal = abortSignal || resolvedOptions.signal\n // POST URL is byte-identical to a plain request; the run id (when set)\n // rides in the X-Run-Id header so durability can key the log by it\n // without changing the request URL existing clients rely on.\n const requestUrl = resolvedUrl\n\n // Resumable SSE: if the server tags events with `id:` offsets (delivery\n // durability), a dropped/rolled-over connection auto-reconnects with a\n // `Last-Event-ID` header and de-dupes the replayed prefix. With no tags,\n // this is a single plain fetch.\n yield* resumableStream(\n fetchEventSource(\n fetchClient,\n requestUrl,\n {\n method: 'POST',\n headers: requestHeaders,\n body: JSON.stringify(requestBody),\n credentials: resolvedOptions.credentials || 'same-origin',\n },\n // Thread the run's ids so a `[DONE]`-terminating server that doesn't\n // stamp them onto events still yields a correlated terminal (parity\n // with the XHR adapter's xhrSSEParser).\n (response, sseSignal) =>\n responseToSSEEvents(response, sseSignal, {\n ...(runContext?.threadId !== undefined\n ? { threadId: runContext.threadId }\n : {}),\n ...(runContext?.runId !== undefined\n ? { runId: runContext.runId }\n : {}),\n }),\n ),\n signal,\n resolvedOptions.reconnect,\n )\n },\n async *joinRun(runId, abortSignal) {\n // Read an in-flight or finished run from the start. `?offset=-1` tells the\n // server's delivery-durability sink to replay from the beginning; `runId`\n // identifies which run. This is a read-only GET — no messages are sent.\n const resolvedUrl = typeof url === 'function' ? url() : url\n const resolvedOptions =\n typeof options === 'function' ? await options() : options\n\n const joinUrl = withSearchParams(resolvedUrl, {\n offset: '-1',\n runId,\n })\n\n const requestHeaders: Record<string, string> = {\n ...mergeHeaders(resolvedOptions.headers),\n }\n const fetchClient = resolvedOptions.fetchClient ?? fetch\n const signal = abortSignal || resolvedOptions.signal\n\n yield* resumableStream(\n fetchEventSource(\n fetchClient,\n joinUrl,\n {\n method: 'GET',\n headers: requestHeaders,\n credentials: resolvedOptions.credentials || 'same-origin',\n },\n // A `[DONE]` during a join correlates to the joined run id.\n (response, sseSignal) =>\n responseToSSEEvents(response, sseSignal, { runId }),\n ),\n signal,\n resolvedOptions.reconnect,\n )\n },\n async hydrate(threadId) {\n const resolvedUrl = typeof url === 'function' ? url() : url\n const resolvedOptions =\n typeof options === 'function' ? await options() : options\n return fetchThreadHydration(\n resolvedOptions.fetchClient ?? fetch,\n resolvedUrl,\n mergeHeaders(resolvedOptions.headers),\n resolvedOptions.credentials || 'same-origin',\n threadId,\n )\n },\n async hydrateGeneration(threadId) {\n const resolvedUrl = typeof url === 'function' ? url() : url\n const resolvedOptions =\n typeof options === 'function' ? await options() : options\n return fetchGenerationHydration(\n resolvedOptions.fetchClient ?? fetch,\n resolvedUrl,\n mergeHeaders(resolvedOptions.headers),\n resolvedOptions.credentials || 'same-origin',\n threadId,\n )\n },\n }\n}\n\n/**\n * Create an HTTP streaming connection adapter (for raw streaming without SSE format)\n *\n * @param url - The API endpoint URL (or a function that returns the URL)\n * @param options - Fetch options (headers, credentials, body, etc.) or a function that returns options (can be async)\n * @returns A connection adapter for HTTP streams\n *\n * @example\n * ```typescript\n * // Static URL\n * const connection = fetchHttpStream('/api/chat');\n *\n * // Dynamic URL\n * const connection = fetchHttpStream(() => `/api/chat?user=${userId}`);\n *\n * // With options\n * const connection = fetchHttpStream('/api/chat', {\n * headers: { 'Authorization': 'Bearer token' }\n * });\n *\n * // With dynamic options\n * const connection = fetchHttpStream('/api/chat', () => ({\n * headers: { 'Authorization': `Bearer ${getToken()}` }\n * }));\n *\n * // With additional body data\n * const connection = fetchHttpStream('/api/chat', async () => ({\n * body: {\n * provider: 'openai',\n * model: 'gpt-5.5',\n * }\n * }));\n * ```\n */\nexport function fetchHttpStream(\n url: string | (() => string),\n options:\n | FetchConnectionOptions\n | (() => FetchConnectionOptions | Promise<FetchConnectionOptions>) = {},\n): ResumableConnectConnectionAdapter {\n return {\n async *connect(messages, data, abortSignal, runContext) {\n // Resolve URL and options if they are functions\n const resolvedUrl = typeof url === 'function' ? url() : url\n const resolvedOptions =\n typeof options === 'function' ? await options() : options\n\n const requestHeaders: Record<string, string> = {\n 'Content-Type': 'application/json',\n ...mergeHeaders(resolvedOptions.headers),\n ...mergeHeaders(runContext?.headers),\n ...runIdHeader(runContext?.runId),\n }\n\n // Build AG-UI RunAgentInput payload.\n //\n // Precedence (later spreads win): static adapter `body` is the base,\n // overridden by `runContext.forwardedProps` (constructor body /\n // forwardedProps options), overridden by per-message `data` passed\n // to `connection.send`. Runtime values win over static config —\n // this matches the documented \"forwardedProps wins\" semantic.\n const requestBody = buildRunAgentInputBody(\n messages,\n data,\n runContext,\n resolvedOptions,\n )\n\n const fetchClient = resolvedOptions.fetchClient ?? fetch\n // `RequestInit.signal` is typed `AbortSignal | null` (no `undefined`\n // under `exactOptionalPropertyTypes`), so spread it conditionally\n // rather than passing `undefined` explicitly.\n const signal = abortSignal || resolvedOptions.signal\n // POST URL is byte-identical to a plain request; the run id (when set)\n // rides in the X-Run-Id header so durability can key the log by it\n // without changing the request URL existing clients rely on.\n const requestUrl = resolvedUrl\n\n // Resumable NDJSON: if the server envelopes each line with an\n // `{ id, chunk }` offset (delivery durability), a dropped/rolled-over\n // connection auto-reconnects with a `Last-Event-ID` header and de-dupes\n // the replayed prefix. With bare lines (no durability), this is a single\n // plain fetch — identical to before.\n yield* resumableStream(\n fetchEventSource(\n fetchClient,\n requestUrl,\n {\n method: 'POST',\n headers: requestHeaders,\n body: JSON.stringify(requestBody),\n credentials: resolvedOptions.credentials || 'same-origin',\n },\n responseToNdjsonEvents,\n ),\n signal,\n resolvedOptions.reconnect,\n )\n },\n async *joinRun(runId, abortSignal) {\n // Read an in-flight or finished run from the start. `?offset=-1` tells the\n // server's delivery-durability sink to replay from the beginning; `runId`\n // identifies which run. This is a read-only GET — no messages are sent.\n const resolvedUrl = typeof url === 'function' ? url() : url\n const resolvedOptions =\n typeof options === 'function' ? await options() : options\n\n const joinUrl = withSearchParams(resolvedUrl, { offset: '-1', runId })\n const requestHeaders: Record<string, string> = {\n ...mergeHeaders(resolvedOptions.headers),\n }\n const fetchClient = resolvedOptions.fetchClient ?? fetch\n const signal = abortSignal || resolvedOptions.signal\n\n yield* resumableStream(\n fetchEventSource(\n fetchClient,\n joinUrl,\n {\n method: 'GET',\n headers: requestHeaders,\n credentials: resolvedOptions.credentials || 'same-origin',\n },\n responseToNdjsonEvents,\n ),\n signal,\n resolvedOptions.reconnect,\n )\n },\n async hydrate(threadId) {\n const resolvedUrl = typeof url === 'function' ? url() : url\n const resolvedOptions =\n typeof options === 'function' ? await options() : options\n return fetchThreadHydration(\n resolvedOptions.fetchClient ?? fetch,\n resolvedUrl,\n mergeHeaders(resolvedOptions.headers),\n resolvedOptions.credentials || 'same-origin',\n threadId,\n )\n },\n async hydrateGeneration(threadId) {\n const resolvedUrl = typeof url === 'function' ? url() : url\n const resolvedOptions =\n typeof options === 'function' ? await options() : options\n return fetchGenerationHydration(\n resolvedOptions.fetchClient ?? fetch,\n resolvedUrl,\n mergeHeaders(resolvedOptions.headers),\n resolvedOptions.credentials || 'same-origin',\n threadId,\n )\n },\n }\n}\n\ntype XhrConnectionOptionsResolver =\n | XhrConnectionOptions\n | (() => XhrConnectionOptions | Promise<XhrConnectionOptions>)\n\nfunction createDefaultXMLHttpRequest(): XMLHttpRequest {\n if (typeof globalThis.XMLHttpRequest !== 'function') {\n throw new Error('XMLHttpRequest is not available in this runtime')\n }\n\n return new globalThis.XMLHttpRequest()\n}\n\nfunction cleanupXhr(\n xhr: XMLHttpRequest,\n abortSignal: AbortSignal | undefined,\n onAbort: (() => void) | undefined,\n): void {\n xhr.onprogress = null\n xhr.onload = null\n xhr.onerror = null\n xhr.onabort = null\n xhr.onloadend = null\n\n if (abortSignal && onAbort) {\n abortSignal.removeEventListener('abort', onAbort)\n }\n}\n\nfunction readXhrLines(\n xhr: XMLHttpRequest,\n abortSignal?: AbortSignal,\n): AsyncGenerator<string> {\n let offset = 0\n let buffer = ''\n const lines: Array<string> = []\n const waiters: Array<() => void> = []\n let done = false\n let aborted = false\n let error: unknown\n let onAbort: (() => void) | undefined\n\n const wake = () => {\n const waiter = waiters.shift()\n waiter?.()\n }\n\n const enqueueDelta = () => {\n if (xhr.status !== 0 && (xhr.status < 200 || xhr.status >= 300)) {\n error = errorFromXhrStatus(xhr)\n done = true\n return\n }\n\n const responseText = xhr.responseText\n if (responseText.length <= offset) {\n return\n }\n\n buffer += responseText.slice(offset)\n offset = responseText.length\n const splitLines = buffer.split('\\n')\n buffer = splitLines.pop() ?? ''\n\n for (const line of splitLines) {\n const normalized = line.endsWith('\\r') ? line.slice(0, -1) : line\n if (normalized.trim()) {\n lines.push(normalized)\n }\n }\n }\n\n const finish = () => {\n enqueueDelta()\n // Tolerate a transient status === 0 (matches enqueueDelta): a real non-2xx\n // is an error, but status 0 here is not — treat the trailing buffer as a\n // truncation check instead of fabricating a bogus \"status: 0\" error.\n if (xhr.status !== 0 && (xhr.status < 200 || xhr.status >= 300)) {\n error = errorFromXhrStatus(xhr)\n } else if (buffer.trim() && !aborted) {\n error = new StreamTruncatedError()\n }\n done = true\n wake()\n }\n\n xhr.onprogress = () => {\n enqueueDelta()\n wake()\n }\n xhr.onload = finish\n xhr.onerror = () => {\n // Surface as StreamReadError so a durable (id-tagged) run whose socket\n // drops mid-stream is eligible for auto-reconnect, matching the fetch path.\n // A non-durable run has no offset, so resumableStream rethrows it as-is.\n error = new StreamReadError(new Error('XHR request failed'))\n done = true\n wake()\n }\n xhr.onabort = () => {\n aborted = true\n done = true\n wake()\n }\n xhr.onloadend = () => {\n if (!done) {\n finish()\n }\n }\n\n if (abortSignal) {\n onAbort = () => {\n aborted = true\n xhr.abort()\n }\n if (abortSignal.aborted) {\n onAbort()\n } else {\n abortSignal.addEventListener('abort', onAbort, { once: true })\n }\n }\n\n return (async function* () {\n try {\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n while (true) {\n const line = lines.shift()\n if (line !== undefined) {\n yield line\n continue\n }\n\n if (error) {\n throw error\n }\n\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n if (done || abortSignal?.aborted) {\n return\n }\n\n await new Promise<void>((resolve) => {\n waiters.push(resolve)\n })\n }\n } finally {\n cleanupXhr(xhr, abortSignal, onAbort)\n }\n })()\n}\n\ninterface ConfiguredXhrRequest {\n xhr: XMLHttpRequest\n body: string\n}\n\nfunction createConfiguredXhrRequest(\n url: string,\n options: XhrConnectionOptions,\n messages: Array<UIMessage> | Array<ModelMessage>,\n data: Record<string, any> | undefined,\n runContext: RunAgentInputContext | undefined,\n method: string = 'POST',\n extraHeaders: Record<string, string> = {},\n): ConfiguredXhrRequest {\n const xhr = options.xhrFactory?.() ?? createDefaultXMLHttpRequest()\n xhr.open(method, url)\n if (options.withCredentials !== undefined) {\n xhr.withCredentials = options.withCredentials\n }\n\n const requestHeaders: Record<string, string> = {\n 'Content-Type': 'application/json',\n ...mergeHeaders(options.headers),\n ...mergeHeaders(method === 'POST' ? runContext?.headers : undefined),\n // Client-chosen run id for durability (POST only; the GET join carries it\n // in the query instead).\n ...(method === 'POST' ? runIdHeader(runContext?.runId) : {}),\n // Reconnect offset (`Last-Event-ID`) wins over static headers.\n ...extraHeaders,\n }\n\n for (const [name, value] of Object.entries(requestHeaders)) {\n xhr.setRequestHeader(name, value)\n }\n\n const requestBody = buildRunAgentInputBody(\n messages,\n data,\n runContext,\n options,\n )\n\n return { xhr, body: JSON.stringify(requestBody) }\n}\n\nasync function resolveXhrConnectionOptions(\n options: XhrConnectionOptionsResolver,\n): Promise<XhrConnectionOptions> {\n return typeof options === 'function' ? await options() : options\n}\n\n/**\n * Build an XHR-backed {@link StreamEventSource}. `parseLines` decodes the raw\n * newline-delimited body into events (SSE or NDJSON); the reconnect engine is\n * shared with the fetch adapters. A fresh XHR is opened per attempt, so a\n * `Last-Event-ID` reconnect header (via `extraHeaders`) is applied at open time.\n */\nfunction xhrEventSource(\n url: string,\n options: XhrConnectionOptions,\n method: string,\n messages: Array<UIMessage> | Array<ModelMessage>,\n data: Record<string, any> | undefined,\n runContext: RunAgentInputContext | undefined,\n parseLines: (lines: AsyncIterable<string>) => AsyncIterable<StreamEvent>,\n): StreamEventSource {\n return async function* (extraHeaders, abortSignal) {\n const request = createConfiguredXhrRequest(\n url,\n options,\n messages,\n data,\n runContext,\n method,\n extraHeaders,\n )\n const lines = readXhrLines(request.xhr, abortSignal)\n if (abortSignal?.aborted) {\n await lines.next()\n return\n }\n // A read-only join is a bodyless GET; a run POSTs the RunAgentInput payload.\n request.xhr.send(method === 'GET' ? null : request.body)\n try {\n yield* parseLines(lines)\n } finally {\n // Tear the socket down on an early exit (terminal reached or reconnect\n // break) so late bytes stop downloading. When the abort signal fired,\n // `readXhrLines` already aborted — skip here to avoid a double abort().\n if (!abortSignal?.aborted) request.xhr.abort()\n }\n }\n}\n\n/** SSE line parser bound to the run's ids for a `[DONE]` fallback. */\nfunction xhrSSEParser(runContext: RunAgentInputContext | undefined) {\n const fallbackIds: { threadId?: string; runId?: string } = {\n ...(runContext?.threadId !== undefined\n ? { threadId: runContext.threadId }\n : {}),\n ...(runContext?.runId !== undefined ? { runId: runContext.runId } : {}),\n }\n return (lines: AsyncIterable<string>) => linesToSSEEvents(lines, fallbackIds)\n}\n\n/**\n * Create an XMLHttpRequest-backed Server-Sent Events connection adapter.\n *\n * Resumable: against a durable (`id:`-tagged) server response, a dropped socket\n * auto-reconnects with `Last-Event-ID` and de-dupes the replayed prefix, and\n * `joinRun` attaches to an existing run from the start. A non-durable response\n * is a single plain request, exactly as before.\n */\nexport function xhrServerSentEvents(\n url: string | (() => string),\n options: XhrConnectionOptionsResolver = {},\n): ResumableConnectConnectionAdapter {\n return {\n async *connect(messages, data, abortSignal, runContext) {\n const resolvedUrl = typeof url === 'function' ? url() : url\n const resolvedOptions = await resolveXhrConnectionOptions(options)\n const signal = abortSignal || resolvedOptions.signal\n // POST URL is byte-identical to a plain request; the run id (when set)\n // rides in the X-Run-Id header so durability can key the log by it\n // without changing the request URL existing clients rely on.\n const requestUrl = resolvedUrl\n yield* resumableStream(\n xhrEventSource(\n requestUrl,\n resolvedOptions,\n 'POST',\n messages,\n data,\n runContext,\n xhrSSEParser(runContext),\n ),\n signal,\n resolvedOptions.reconnect,\n )\n },\n async *joinRun(runId, abortSignal) {\n const resolvedUrl = typeof url === 'function' ? url() : url\n const resolvedOptions = await resolveXhrConnectionOptions(options)\n const signal = abortSignal || resolvedOptions.signal\n const joinUrl = withSearchParams(resolvedUrl, { offset: '-1', runId })\n yield* resumableStream(\n xhrEventSource(\n joinUrl,\n resolvedOptions,\n 'GET',\n [],\n undefined,\n undefined,\n // A `[DONE]` during a join correlates to the joined run id (parity\n // with fetchServerSentEvents.joinRun).\n (lines) => linesToSSEEvents(lines, { runId }),\n ),\n signal,\n resolvedOptions.reconnect,\n )\n },\n async hydrate(threadId) {\n const resolvedUrl = typeof url === 'function' ? url() : url\n const resolvedOptions = await resolveXhrConnectionOptions(options)\n // Hydration is a non-streaming JSON GET, so fetch is fine even for the\n // XHR-backed streaming adapter.\n return fetchThreadHydration(\n fetch,\n resolvedUrl,\n mergeHeaders(resolvedOptions.headers),\n resolvedOptions.withCredentials ? 'include' : 'same-origin',\n threadId,\n )\n },\n async hydrateGeneration(threadId) {\n const resolvedUrl = typeof url === 'function' ? url() : url\n const resolvedOptions = await resolveXhrConnectionOptions(options)\n // Hydration is a non-streaming JSON GET, so fetch is fine even for the\n // XHR-backed streaming adapter.\n return fetchGenerationHydration(\n fetch,\n resolvedUrl,\n mergeHeaders(resolvedOptions.headers),\n resolvedOptions.withCredentials ? 'include' : 'same-origin',\n threadId,\n )\n },\n }\n}\n\n/**\n * Create an XMLHttpRequest-backed newline-delimited JSON stream adapter.\n *\n * Resumable: against a durable (envelope-tagged) server response, a dropped\n * socket auto-reconnects with `Last-Event-ID` and de-dupes the replayed prefix,\n * and `joinRun` attaches to an existing run from the start. A non-durable\n * (bare-line) response is a single plain request, exactly as before.\n */\nexport function xhrHttpStream(\n url: string | (() => string),\n options: XhrConnectionOptionsResolver = {},\n): ResumableConnectConnectionAdapter {\n return {\n async *connect(messages, data, abortSignal, runContext) {\n const resolvedUrl = typeof url === 'function' ? url() : url\n const resolvedOptions = await resolveXhrConnectionOptions(options)\n const signal = abortSignal || resolvedOptions.signal\n // POST URL is byte-identical to a plain request; the run id (when set)\n // rides in the X-Run-Id header so durability can key the log by it\n // without changing the request URL existing clients rely on.\n const requestUrl = resolvedUrl\n yield* resumableStream(\n xhrEventSource(\n requestUrl,\n resolvedOptions,\n 'POST',\n messages,\n data,\n runContext,\n linesToNdjsonEvents,\n ),\n signal,\n resolvedOptions.reconnect,\n )\n },\n async *joinRun(runId, abortSignal) {\n const resolvedUrl = typeof url === 'function' ? url() : url\n const resolvedOptions = await resolveXhrConnectionOptions(options)\n const signal = abortSignal || resolvedOptions.signal\n const joinUrl = withSearchParams(resolvedUrl, { offset: '-1', runId })\n yield* resumableStream(\n xhrEventSource(\n joinUrl,\n resolvedOptions,\n 'GET',\n [],\n undefined,\n undefined,\n linesToNdjsonEvents,\n ),\n signal,\n resolvedOptions.reconnect,\n )\n },\n async hydrate(threadId) {\n const resolvedUrl = typeof url === 'function' ? url() : url\n const resolvedOptions = await resolveXhrConnectionOptions(options)\n // Hydration is a non-streaming JSON GET, so fetch is fine even for the\n // XHR-backed streaming adapter.\n return fetchThreadHydration(\n fetch,\n resolvedUrl,\n mergeHeaders(resolvedOptions.headers),\n resolvedOptions.withCredentials ? 'include' : 'same-origin',\n threadId,\n )\n },\n async hydrateGeneration(threadId) {\n const resolvedUrl = typeof url === 'function' ? url() : url\n const resolvedOptions = await resolveXhrConnectionOptions(options)\n // Hydration is a non-streaming JSON GET, so fetch is fine even for the\n // XHR-backed streaming adapter.\n return fetchGenerationHydration(\n fetch,\n resolvedUrl,\n mergeHeaders(resolvedOptions.headers),\n resolvedOptions.withCredentials ? 'include' : 'same-origin',\n threadId,\n )\n },\n }\n}\n\nexport interface WebSocketConnectionOptions {\n protocols?: string | Array<string>\n body?: Record<string, unknown>\n reconnect?: ReconnectOptions\n /** Override the WebSocket implementation (tests / non-browser runtimes). */\n WebSocketImpl?: typeof WebSocket\n}\n\nfunction runIdQuery(url: string, runId: string | undefined): string {\n return runId ? withSearchParams(url, { runId }) : url\n}\n\nfunction isPingFrame(parsed: unknown): boolean {\n return (\n typeof parsed === 'object' &&\n parsed !== null &&\n (parsed as { type?: unknown }).type === 'ping'\n )\n}\n\n/** A subscribe() consumer's registration: receives chunks or a fatal error. */\ninterface WebSocketChunkSink {\n push: (chunk: StreamChunk) => void\n fail: (error: unknown) => void\n}\n\n/**\n * A push→pull bridge from socket callbacks to an async iterable: chunks queue\n * until the consumer pulls, a recorded failure rejects the iterator, and\n * `end()` (or the abort signal) finishes it cleanly. Shared by `webSocket()`'s\n * `subscribe()` and `joinRun()`.\n */\nfunction createChunkPipe(\n abortSignal: AbortSignal | undefined,\n onFinally: () => void,\n): {\n push: (chunk: StreamChunk) => void\n fail: (error: unknown) => void\n end: () => void\n iterable: AsyncIterable<StreamChunk>\n} {\n const queue: Array<StreamChunk> = []\n const waiters: Array<(c: StreamChunk | null) => void> = []\n let failure: unknown\n let ended = false\n const wake = () => waiters.shift()?.(null)\n const push = (chunk: StreamChunk) => {\n const w = waiters.shift()\n if (w) w(chunk)\n else queue.push(chunk)\n }\n const fail = (error: unknown) => {\n failure = error\n wake()\n }\n const end = () => {\n ended = true\n wake()\n }\n const onAbort = () => wake()\n abortSignal?.addEventListener('abort', onAbort)\n const iterable = (async function* () {\n try {\n while (!abortSignal?.aborted) {\n // Drain buffered chunks before ever awaiting a new promise — a\n // fatal drop that lands while chunks are still queued (fail()\n // finds no pending waiter, since the consumer hasn't caught up\n // to its buffer yet) must not be lost.\n const buffered = queue.shift()\n if (buffered !== undefined) {\n yield buffered\n continue\n }\n // Buffer exhausted: surface a failure recorded while we were\n // draining, rather than awaiting a promise that will never\n // resolve (the connection is dead — no future push/fail).\n if (failure !== undefined) throw failure\n if (ended) return\n const chunk = await new Promise<StreamChunk | null>((r) =>\n waiters.push(r),\n )\n // The wait resolved because fail() woke us — surface the error\n // instead of treating the null sentinel as a clean end. TS narrows\n // `failure` to `undefined` from the check above and doesn't know\n // the `fail()` closure can reassign it while we were awaiting —\n // this check is very much still reachable.\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n if (failure !== undefined) throw failure\n if (chunk === null) return\n yield chunk\n }\n } finally {\n abortSignal?.removeEventListener('abort', onAbort)\n onFinally()\n }\n })()\n return { push, fail, end, iterable }\n}\n\n/**\n * The send()-driven run currently owning auto-reconnect for a `webSocket()`\n * connection: reconnect is scoped to the run `send()` is driving, so a drop\n * with no established run is surfaced to subscribers rather than auto-resumed.\n */\ninterface WebSocketRunSession {\n runId: string | undefined\n readonly tracker: ReconnectTracker\n sawTerminal: boolean\n /** Made forward progress (a new, non-duplicate chunk) since the last (re)connect. */\n progressed: boolean\n signal: AbortSignal | undefined\n}\n\n/**\n * Full-duplex, conversation-scoped WebSocket connection adapter. Pairs with the\n * server `toWebSocketResponse` / `toWebSocketStream`. `send()` writes a\n * RunAgentInput frame; `subscribe()` yields inbound chunks.\n *\n * Resumable: `send()` establishes a run session backed by a\n * {@link createReconnectTracker}. If the socket closes before a terminal\n * (`RUN_FINISHED`/`RUN_ERROR`) chunk is seen and the run is durable\n * (offset-tagged `{ id, chunk }` envelopes), the socket is reopened at\n * `?runId=&offset=<lastEventId>`, de-duping the replayed boundary. A drop with\n * no offset ever observed (non-durable) surfaces {@link StreamReadError}\n * instead of reconnecting — there is nothing to resume from.\n */\nexport function webSocket(\n url: string | (() => string),\n options: WebSocketConnectionOptions = {},\n): SubscribeConnectionAdapter & {\n joinRun: (\n runId: string,\n abortSignal?: AbortSignal,\n ) => AsyncIterable<StreamChunk>\n} {\n const Impl = options.WebSocketImpl ?? WebSocket\n let socket: WebSocket | undefined\n // Whether the current socket is the conversation socket ('run') or a\n // read-only replay connection opened by a reconnect ('resume'). Only the\n // conversation socket accepts run frames server-side.\n let socketMode: 'run' | 'resume' | undefined\n // Memoized per-socket open promise. `openOnce` sets `onopen`/`onerror`\n // exactly ONCE, at socket-creation time, and stores the resulting promise\n // here. Without this, `waitOpen` assigning `onopen`/`onerror` on every call\n // would clobber a still-pending prior caller's handlers: `openOnce` reuses\n // the same in-flight socket for concurrent callers (`readyState <= 1`), so a\n // second `send()` issued before the handshake completes would overwrite the\n // first call's handlers and leave its promise permanently unresolved.\n let openPromise: Promise<void> | undefined\n const listeners = new Set<WebSocketChunkSink>()\n let currentSession: WebSocketRunSession | undefined\n\n function failAll(error: unknown): void {\n for (const l of listeners) l.fail(error)\n }\n\n function openOnce(target: string, mode: 'run' | 'resume'): WebSocket {\n // Only the conversation socket is reused — it multiplexes many turns. A\n // 'resume' handshake carries ?offset and must reach the server as its own\n // connection (reusing any open socket would discard that query, so no\n // replay would ever be requested), and a run frame must never be written\n // to a read-only resume socket (the server registers no message listener\n // there, so the frame would be silently ignored).\n if (\n socket &&\n socket.readyState <= 1 &&\n mode === 'run' &&\n socketMode === 'run'\n ) {\n return socket\n }\n const prior = socket\n const ws = options.protocols\n ? new Impl(target, options.protocols)\n : new Impl(target)\n socket = ws\n socketMode = mode\n openPromise = new Promise<void>((resolve, reject) => {\n ws.onopen = () => resolve()\n ws.onerror = (e) => reject(new StreamReadError(e))\n })\n // Attach a no-op handler so a socket nobody awaits can't raise an\n // unhandled rejection if it errors. Awaiters of openPromise still see the rejection.\n openPromise.catch(() => {})\n ws.onmessage = (event: MessageEvent) => {\n // A retired socket (a newer connection took over below) must not keep\n // feeding the shared listeners.\n if (ws !== socket) return\n let parsed: unknown\n try {\n parsed = JSON.parse(String(event.data))\n } catch (error) {\n failAll(new StreamReadError(error))\n return\n }\n if (isPingFrame(parsed)) return\n const envelopeId = isNdjsonEnvelope(parsed) ? parsed.id : undefined\n const chunk = restoreInboundUsage(\n isNdjsonEnvelope(parsed) ? parsed.chunk : (parsed as StreamChunk),\n )\n\n // Thread durable chunks through the active run session's tracker (if\n // any) so a later reconnect knows the last offset and can skip a\n // replayed boundary. A socket with no active session dispatches chunks\n // as-is.\n const session = currentSession\n if (session) {\n if (session.tracker.note(envelopeId) === 'duplicate') return\n session.progressed = true\n if (session.runId === undefined) {\n session.runId = getChunkRunId(chunk)\n }\n if (chunk.type === 'RUN_FINISHED' || chunk.type === 'RUN_ERROR') {\n session.sawTerminal = true\n }\n }\n for (const l of listeners) l.push(chunk)\n }\n ws.onclose = () => {\n // Retired deliberately in favor of a newer connection — not a drop.\n if (ws !== socket) return\n const session = currentSession\n if (!session) {\n // No run session (never established, or cleared by a prior failure).\n // Surface the drop so subscribers do not stay parked on a dead socket.\n failAll(new StreamReadError(new Error('WebSocket connection closed')))\n return\n }\n if (session.signal?.aborted || session.sawTerminal) return\n const lastEventId = session.tracker.lastEventId\n if (lastEventId === undefined) {\n // Non-durable run (no offset ever observed) — nothing to resume\n // from. Surface a hard failure rather than silently reconnecting\n // forever against a server that never tags its events.\n currentSession = undefined\n failAll(new StreamReadError(new Error('WebSocket connection closed')))\n return\n }\n void reconnect(session, lastEventId)\n }\n // Retire a superseded socket (e.g. a lingering resume socket when send()\n // opens the next conversation socket) so two sockets never feed the\n // shared listeners at once. Its handlers see it is no longer current and\n // ignore the close.\n if (prior && prior.readyState <= 1) prior.close()\n return ws\n }\n\n async function reconnect(\n session: WebSocketRunSession,\n offset: string,\n ): Promise<void> {\n try {\n // Bounded by the shared tracker's consecutive-no-progress ceiling —\n // mirrors resumableStream so a flapping server can't reconnect forever.\n await session.tracker.waitBeforeReconnect(\n session.progressed,\n session.signal,\n )\n } catch (error) {\n if (currentSession === session) currentSession = undefined\n failAll(error)\n return\n }\n if (session.signal?.aborted) return\n // A send() issued during the backoff supersedes this resume: a newer run\n // (or a resubmit of this one) already owns a fresh conversation socket,\n // and its turn re-delivers from the durability log — the tracker de-dupes\n // any overlap. Opening the resume socket anyway would retire that live\n // conversation socket.\n if (currentSession !== session) return\n if (socket && socket.readyState <= 1) return\n session.progressed = false\n const base = typeof url === 'function' ? url() : url\n const target = withSearchParams(base, {\n ...(session.runId !== undefined ? { runId: session.runId } : {}),\n offset,\n })\n openOnce(target, 'resume')\n }\n\n function waitOpen(ws: WebSocket): Promise<void> {\n if (ws.readyState === 1) return Promise.resolve()\n // Concurrent callers awaiting the SAME in-flight socket share the SAME\n // memoized promise (set once in `openOnce`), so none of them clobber\n // another's onopen/onerror handler.\n return openPromise ?? Promise.resolve()\n }\n\n return {\n subscribe(abortSignal?: AbortSignal): AsyncIterable<StreamChunk> {\n const pipe = createChunkPipe(abortSignal, () => listeners.delete(sink))\n const sink: WebSocketChunkSink = { push: pipe.push, fail: pipe.fail }\n listeners.add(sink)\n return pipe.iterable\n },\n async send(messages, data, abortSignal, runContext) {\n const target = typeof url === 'function' ? url() : url\n const ws = openOnce(runIdQuery(target, runContext?.runId), 'run')\n await waitOpen(ws)\n // Establish (or continue) the run session this socket is driving, so\n // an unterminated drop can auto-resume it. A distinct runId starts a\n // fresh tracker (a new run's offsets are unrelated to the last one's);\n // the same runId reuses the tracker so a repeat send() on an\n // already-tracked run doesn't lose its de-dupe/offset state.\n if (!currentSession || currentSession.runId !== runContext?.runId) {\n currentSession = {\n runId: runContext?.runId,\n tracker: createReconnectTracker(options.reconnect),\n sawTerminal: false,\n progressed: false,\n signal: abortSignal,\n }\n } else {\n // Same-runId resubmit (e.g. a client-tool continuation): keep the\n // tracker, but this is a NEW turn — with the previous turn's\n // `sawTerminal` left set, a drop during the resubmitted turn would\n // neither reconnect nor surface an error.\n currentSession.signal = abortSignal\n currentSession.sawTerminal = false\n currentSession.progressed = false\n }\n const session = currentSession\n // stop() must reach the server: the conversation socket outlives the\n // turn, so without an abort frame the model keeps generating (and\n // billing) server-side. The frame aborts only this run's turn.\n abortSignal?.addEventListener(\n 'abort',\n () => {\n const abortRunId = session.runId\n const live = socket\n if (\n abortRunId === undefined ||\n session.sawTerminal ||\n socketMode !== 'run' ||\n live === undefined ||\n live.readyState !== 1\n ) {\n return\n }\n try {\n live.send(JSON.stringify({ type: 'abort', runId: abortRunId }))\n } catch {\n // Socket is CLOSING/CLOSED — the server aborts the turn on close.\n }\n },\n { once: true },\n )\n const body = buildRunAgentInputBody(messages, data, runContext, {\n body: options.body,\n })\n ws.send(JSON.stringify(body))\n },\n joinRun(runId, abortSignal): AsyncIterable<StreamChunk> {\n const target = withSearchParams(typeof url === 'function' ? url() : url, {\n offset: '-1',\n runId,\n })\n // A replay handshake must reach the server as its own connection:\n // reusing the conversation socket would discard the ?offset query (no\n // replay ever requested), and the conversation socket must not be\n // replaced by a read-only replay socket. So joinRun owns a dedicated\n // socket and never touches the shared socket or run session.\n const ws = options.protocols\n ? new Impl(target, options.protocols)\n : new Impl(target)\n const pipe = createChunkPipe(abortSignal, () => {\n if (ws.readyState <= 1) ws.close()\n })\n ws.onmessage = (event: MessageEvent) => {\n let parsed: unknown\n try {\n parsed = JSON.parse(String(event.data))\n } catch (error) {\n pipe.fail(new StreamReadError(error))\n return\n }\n if (isPingFrame(parsed)) return\n pipe.push(\n restoreInboundUsage(\n isNdjsonEnvelope(parsed) ? parsed.chunk : (parsed as StreamChunk),\n ),\n )\n }\n ws.onclose = (event?: CloseEvent) => {\n // 1000 = the server finished replaying the log and closed cleanly.\n // Anything else is a drop or a policy refusal (e.g. 1008 \"no resume\n // offset\") and must surface — a joinRun socket never auto-reconnects.\n if (event?.code === 1000) {\n pipe.end()\n return\n }\n const detail = event\n ? `${event.code}${event.reason ? `: ${event.reason}` : ''}`\n : 'unknown'\n pipe.fail(\n new StreamReadError(\n new Error(`WebSocket connection closed (${detail})`),\n ),\n )\n }\n return pipe.iterable\n },\n }\n}\n\n/**\n * Optional persistence handlers for the lightweight adapters (`stream()`,\n * `rpcStream()`). These are one-shot, request-scoped calls with no built-in\n * GET endpoint or second channel, so hydration and run-rejoin only exist if\n * the app supplies them — typically thin wrappers over TanStack Start server\n * functions backed by `@tanstack/ai-persistence` (`getGenerationHydration`)\n * and a delivery-durability log (`memoryStream` / `replayRunStream`).\n *\n * Each handler is spread onto the returned adapter only when defined, so\n * feature detection (`connection.hydrateGeneration` etc.) keeps working.\n */\nexport interface StreamConnectionHandlers {\n /**\n * Server-driven chat hydration for `persistence: true`: the stored\n * transcript for `threadId` plus a cursor to an in-flight run.\n */\n hydrate?: (threadId: string) => Promise<ChatHydrationResult>\n /**\n * Server-driven generation hydration for `persistence: true`: the last\n * generation's resume snapshot for `threadId` plus a cursor to a run still\n * generating. See {@link ConnectConnectionAdapter.hydrateGeneration}.\n */\n hydrateGeneration?: (threadId: string) => Promise<GenerationHydrationResult>\n /**\n * Re-attach to a run still generating and replay it from the start. See\n * {@link ConnectConnectionAdapter.joinRun}.\n */\n joinRun?: (\n runId: string,\n abortSignal?: AbortSignal,\n ) => AsyncIterable<StreamChunk>\n}\n\n/**\n * Create a direct stream connection adapter (for server functions or direct streams)\n *\n * @param streamFactory - A function that returns an async iterable of StreamChunks\n * @param handlers - Optional persistence handlers (`hydrate`,\n * `hydrateGeneration`, `joinRun`) that let server-driven persistence work\n * without an HTTP endpoint — each is usually a one-line server-function call\n * @returns A connection adapter for direct streams\n *\n * @example\n * ```typescript\n * // With TanStack Start server function\n * const connection = stream(() => serverFunction({ messages }));\n *\n * const client = new ChatClient({ connection });\n *\n * // With generation persistence over server functions\n * const connection = stream(\n * () => generateImageFn({ data: input }),\n * {\n * hydrateGeneration: (threadId) => getImageHydrationFn({ data: threadId }),\n * joinRun: (runId) => joinImageRunFn({ data: runId }),\n * },\n * );\n * ```\n */\nexport function stream(\n streamFactory: (\n messages: Array<UIMessage> | Array<ModelMessage>,\n data?: Record<string, any>,\n abortSignal?: AbortSignal,\n ) => AsyncIterable<StreamChunk>,\n handlers?: StreamConnectionHandlers,\n): ConnectConnectionAdapter {\n return {\n async *connect(messages, data, abortSignal) {\n // Pass messages as-is (UIMessages with parts preserved)\n // Server-side chat() handles conversion to ModelMessages\n yield* streamFactory(messages, data, abortSignal)\n },\n ...(handlers?.hydrate ? { hydrate: handlers.hydrate } : {}),\n ...(handlers?.hydrateGeneration\n ? { hydrateGeneration: handlers.hydrateGeneration }\n : {}),\n ...(handlers?.joinRun ? { joinRun: handlers.joinRun } : {}),\n }\n}\n\n/**\n * Wrap a `ChatFetcher` as a `ConnectConnectionAdapter` so the chat client can\n * consume it through the same `subscribe`/`send` plumbing used for SSE /\n * HTTP-stream / RPC connections. May return either a `Response` (parsed as\n * SSE) or an `AsyncIterable<StreamChunk>` (yielded directly).\n *\n * @internal\n */\nexport function fetcherToConnectionAdapter(\n fetcher: ChatFetcher,\n): ConnectConnectionAdapter {\n return {\n async *connect(messages, data, abortSignal, runContext) {\n if (!abortSignal) {\n throw new Error(\n 'fetcherToConnectionAdapter requires an AbortSignal — the chat client always supplies one.',\n )\n }\n if (!runContext) {\n throw new Error(\n 'fetcherToConnectionAdapter requires a RunAgentInputContext — the chat client always supplies one.',\n )\n }\n const uiMessages = messages as Array<UIMessage>\n const result = await fetcher(\n {\n messages: uiMessages,\n data,\n threadId: runContext.threadId,\n runId: runContext.runId,\n ...(runContext.parentRunId !== undefined\n ? { parentRunId: runContext.parentRunId }\n : {}),\n ...(runContext.resume !== undefined\n ? { resume: runContext.resume }\n : {}),\n },\n { signal: abortSignal, headers: runContext.headers },\n )\n if (result instanceof Response) {\n yield* responseToSSEChunks(result, abortSignal)\n } else {\n yield* abortableIterable(result, abortSignal)\n }\n },\n }\n}\n\n/**\n * Wrap an AsyncIterable so iteration aborts when `signal` fires. Without\n * this, a fetcher that returns a generator ignoring its signal would leave\n * the for-await loop hanging until the iterable naturally ends.\n */\nasync function* abortableIterable<T>(\n iterable: AsyncIterable<T>,\n signal: AbortSignal,\n): AsyncGenerator<T> {\n if (signal.aborted) return\n const iterator = iterable[Symbol.asyncIterator]()\n const abortPromise = new Promise<{ done: true; value: undefined }>(\n (resolve) => {\n signal.addEventListener(\n 'abort',\n () => resolve({ done: true, value: undefined }),\n { once: true },\n )\n },\n )\n try {\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n while (true) {\n const result = await Promise.race([iterator.next(), abortPromise])\n if (result.done) return\n yield result.value\n }\n } finally {\n await iterator.return?.()\n }\n}\n\n/**\n * Create an RPC stream connection adapter (for RPC-based streaming like Cap'n Web RPC)\n *\n * @param rpcCall - A function that accepts messages and returns an async iterable of StreamChunks\n * @param handlers - Optional persistence handlers (`hydrate`,\n * `hydrateGeneration`, `joinRun`) that let server-driven persistence work\n * without an HTTP endpoint — each is usually a one-line RPC call\n * @returns A connection adapter for RPC streams\n *\n * @example\n * ```typescript\n * // With Cap'n Web RPC\n * const connection = rpcStream((messages, data) =>\n * api.streamMurfResponse(messages, data)\n * );\n *\n * const client = new ChatClient({ connection });\n *\n * // With generation persistence over RPC\n * const connection = rpcStream(\n * (messages, data) => api.streamMurfResponse(messages, data),\n * {\n * hydrateGeneration: (threadId) => api.getGenerationHydration(threadId),\n * joinRun: (runId) => api.replayRun(runId),\n * },\n * );\n * ```\n */\nexport function rpcStream(\n rpcCall: (\n messages: Array<UIMessage> | Array<ModelMessage>,\n data?: Record<string, any>,\n abortSignal?: AbortSignal,\n ) => AsyncIterable<StreamChunk>,\n handlers?: StreamConnectionHandlers,\n): ConnectConnectionAdapter {\n return {\n async *connect(messages, data, abortSignal) {\n // Pass messages as-is (UIMessages with parts preserved)\n // Server-side chat() handles conversion to ModelMessages\n yield* rpcCall(messages, data, abortSignal)\n },\n ...(handlers?.hydrate ? { hydrate: handlers.hydrate } : {}),\n ...(handlers?.hydrateGeneration\n ? { hydrateGeneration: handlers.hydrateGeneration }\n : {}),\n ...(handlers?.joinRun ? { joinRun: handlers.joinRun } : {}),\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"connection-adapters.js","names":[],"sources":["../../src/connection-adapters.ts"],"sourcesContent":["import {\n EventType,\n getChunkRunId as getNormalizedChunkRunId,\n restoreInboundChunk,\n tanstackMetadata,\n uiMessagesToWire,\n withTanstackMetadata,\n} from '@tanstack/ai/client'\nimport { ByokMissingError, isByokMissingBody } from '@tanstack/ai/byok'\nimport {\n createResponseStreamTextDecoder,\n getResponseStreamReader,\n} from './response-stream'\nimport { parseSseDataLine } from './sse-utils'\nimport type {\n ModelMessage,\n RunAgentResumeItem,\n RunErrorEvent,\n StreamChunk,\n UIMessage,\n} from '@tanstack/ai/client'\nimport type { ChatFetcher, ChatPendingInterrupt } from './types'\nimport { normalizeMessagesDates } from './message-date-normalizer'\n\n/**\n * Associates connect-wrapped chunks with the run they were produced under.\n * Content events (TEXT_MESSAGE_CONTENT, TOOL_CALL_*, …) carry no `runId` of\n * their own, so the connect wrapper stamps the caller's run id here. Lets\n * run-scoped consumers (e.g. clear-during-stream suppression) attribute those\n * otherwise-runless chunks to their originating request.\n */\nconst chunkRunIds = new WeakMap<StreamChunk, string>()\n\n/**\n * Resolve a chunk's run id, preferring the value on the chunk itself\n * (RUN_STARTED / RUN_FINISHED / RUN_ERROR carry one) and falling back to the\n * run the connect wrapper stamped it with.\n */\nexport function getChunkRunId(chunk: StreamChunk): string | undefined {\n // Prefer the client's request run id (stamped in `chunkRunIds`) over a\n // provider-assigned `chunk.runId`. Interrupt continuation correlation needs\n // the client's run identity to win when a provider stamps its own id; for\n // resumable reconnect/join the two ids match, so precedence is moot there.\n const requestRunId = chunkRunIds.get(chunk)\n return requestRunId ?? getNormalizedChunkRunId(chunk)\n}\n\n/**\n * Thrown when an SSE/HTTP stream ends with a non-empty unterminated buffer.\n * Indicates the connection was cut mid-line (server crash, dropped TCP, proxy\n * timeout) so the partial content cannot be safely parsed.\n */\nexport class StreamTruncatedError extends Error {\n constructor() {\n super(\n 'Stream ended with unterminated trailing data — connection was likely cut short.',\n )\n this.name = 'StreamTruncatedError'\n }\n}\n\nclass StreamReadError extends Error {\n constructor(cause: unknown) {\n super('Stream response body read failed', { cause })\n this.name = 'StreamReadError'\n }\n}\n\n/**\n * Thrown when a durable (id-tagged) run's stream ends with no terminal event\n * and a reconnect makes no forward progress — the run cannot complete, so the\n * consumer must not be left silently hanging on a stream that just stops.\n */\nexport class DurableStreamIncompleteError extends Error {\n constructor() {\n super(\n 'Durable run ended without a terminal event and could not resume — the run did not complete.',\n )\n this.name = 'DurableStreamIncompleteError'\n }\n}\n\n/**\n * Thrown when a durable run exceeds its reconnect ceiling. Bounds the\n * otherwise-unbounded reconnect loop so a flapping producer (or a proxy that\n * rolls the socket after every event) surfaces a failure instead of\n * reconnecting without end.\n */\nexport class StreamReconnectLimitError extends Error {\n constructor(attempts: number) {\n super(\n `Durable run exceeded its reconnect ceiling of ${attempts} attempts — giving up.`,\n )\n this.name = 'StreamReconnectLimitError'\n }\n}\n\n/**\n * Reconnect bounding for resumable streams. A constant throttle delay prevents a\n * hot loop against the origin, and the ceiling bounds a pathologically failing\n * run — but only counts CONSECUTIVE reconnects that made no forward progress.\n */\nexport interface ReconnectOptions {\n /**\n * Ceiling on the number of CONSECUTIVE reconnects that deliver no new events,\n * before failing with {@link StreamReconnectLimitError}. The counter resets to\n * zero whenever a reconnect makes forward progress, so a healthy long run —\n * even one behind a proxy that rolls the socket after every event — never\n * approaches it; the ceiling only fires when the run is genuinely stuck\n * (reconnecting repeatedly without receiving anything new). Default 5.\n */\n maxAttempts?: number\n /** Delay between reconnect attempts, in ms, to avoid hammering. Default 250. */\n delayMs?: number\n}\n\ninterface ResolvedReconnectOptions {\n maxAttempts: number\n delayMs: number\n}\n\nfunction resolveReconnectOptions(\n options: ReconnectOptions | undefined,\n): ResolvedReconnectOptions {\n const maxAttempts = options?.maxAttempts ?? 5\n const delayMs = options?.delayMs ?? 250\n // Reject non-finite / negative bounds up front: a NaN or Infinity maxAttempts\n // would make the ceiling ineffective (unbounded reconnects), and a non-finite\n // delayMs would remove throttling. Fail loudly on misconfiguration.\n if (!Number.isInteger(maxAttempts) || maxAttempts < 0) {\n throw new Error(\n `Invalid reconnect.maxAttempts: ${maxAttempts}. Must be a non-negative integer.`,\n )\n }\n if (!Number.isFinite(delayMs) || delayMs < 0) {\n throw new Error(\n `Invalid reconnect.delayMs: ${delayMs}. Must be a non-negative finite number.`,\n )\n }\n return { maxAttempts, delayMs }\n}\n\n/**\n * Reconnect bookkeeping shared by every resumable-stream driver: de-dupes\n * offsets, tracks the last acknowledged offset, honors the SSE empty-id reset\n * convention, and bounds consecutive no-progress reconnects behind a\n * throttling delay. Extracted out of {@link resumableStream} so a WebSocket\n * reconnect driver can reuse the exact same semantics.\n */\nexport interface ReconnectTracker {\n /** The most recently accepted (non-duplicate, non-empty) offset, if any. */\n readonly lastEventId: string | undefined\n /**\n * Record an incoming offset. Returns `'reset'` for an empty id (SSE's\n * resume-cursor reset — clears the de-dupe set and `lastEventId`),\n * `'duplicate'` for an already-seen id, and `'new'` otherwise (including\n * `undefined`, which is untracked — no offset to remember).\n */\n note: (id: string | undefined) => 'new' | 'duplicate' | 'reset'\n /**\n * Throttle before a reconnect attempt. Resets the no-progress counter when\n * `madeProgress` is true; otherwise increments it and throws\n * {@link StreamReconnectLimitError} once it exceeds the configured ceiling.\n */\n waitBeforeReconnect: (\n madeProgress: boolean,\n signal?: AbortSignal,\n ) => Promise<void>\n}\n\n/** Create a {@link ReconnectTracker} bound to the given reconnect bounds. */\nexport function createReconnectTracker(\n options?: ReconnectOptions,\n): ReconnectTracker {\n const reconnect = resolveReconnectOptions(options)\n // Retains every delivered offset for the run's lifetime. Intentionally\n // bounded by run length (not evicted): a conforming server replays strictly\n // after the acknowledged offset, so this only needs to catch the single\n // boundary event on reconnect, but keeping the full set keeps de-dup\n // correct even if a server replays a wider overlap.\n const seen = new Set<string>()\n let lastEventId: string | undefined\n let reconnectAttempts = 0\n return {\n get lastEventId() {\n return lastEventId\n },\n note(id) {\n if (id === undefined) return 'new'\n if (id === '') {\n // SSE spec: an empty `id:` resets the resume cursor. Drop the last\n // offset and clear the de-dupe set; the chunk itself still delivers.\n lastEventId = undefined\n seen.clear()\n return 'reset'\n }\n if (seen.has(id)) return 'duplicate'\n seen.add(id)\n lastEventId = id\n return 'new'\n },\n // Bound only CONSECUTIVE no-progress reconnects. A reconnect that made\n // forward progress resets the counter, so a healthy long run (even one\n // whose socket rolls after every event) never approaches the ceiling; it\n // fires only when the run is genuinely stuck — reconnecting repeatedly\n // with nothing new.\n async waitBeforeReconnect(madeProgress, signal) {\n if (madeProgress) {\n reconnectAttempts = 0\n } else {\n reconnectAttempts += 1\n if (reconnectAttempts > reconnect.maxAttempts) {\n throw new StreamReconnectLimitError(reconnect.maxAttempts)\n }\n }\n await abortableDelay(reconnect.delayMs, signal)\n },\n }\n}\n\n/** Resolve after `ms`, or immediately once `signal` aborts. Never rejects. */\nfunction abortableDelay(ms: number, signal?: AbortSignal): Promise<void> {\n if (ms <= 0 || signal?.aborted) return Promise.resolve()\n return new Promise((resolve) => {\n const onAbort = () => {\n clearTimeout(timer)\n resolve()\n }\n const timer = setTimeout(() => {\n signal?.removeEventListener('abort', onAbort)\n resolve()\n }, ms)\n signal?.addEventListener('abort', onAbort, { once: true })\n })\n}\n\nfunction generateRunId(prefix: string): string {\n return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`\n}\n\n/**\n * Asserts an id is present when synthesizing a terminal event. The chat\n * client always supplies `runContext.threadId` / `runContext.runId`, so an\n * absent id at this layer indicates the adapter was wired up by a caller\n * that bypassed that contract — surface it rather than fabricating one.\n */\nfunction requireSyntheticId(\n value: string | undefined,\n field: 'threadId' | 'runId',\n): string {\n if (!value) {\n throw new Error(\n `Cannot synthesize terminal event: ${field} not supplied via runContext and not observed in the upstream stream.`,\n )\n }\n return value\n}\n\n/**\n * Merge custom headers into request headers\n */\nfunction mergeHeaders(\n customHeaders?: Record<string, string> | Headers,\n): Record<string, string> {\n if (!customHeaders) {\n return {}\n }\n if (customHeaders instanceof Headers) {\n const result: Record<string, string> = {}\n customHeaders.forEach((value, key) => {\n result[key] = value\n })\n return result\n }\n return customHeaders\n}\n\n/**\n * Request header carrying the client-chosen run id to a delivery-durability\n * sink. The durable log is then keyed by the SAME id the client already holds,\n * so a later join/resume can address the run without first reading back a\n * server-generated id. Sent as a header — NOT a query param — so the POST URL\n * stays byte-identical to a plain, non-durable request; a server that isn't\n * durable simply ignores the header. (The GET join path keeps `?runId` in the\n * query, since a GET has no body/handler contract to disturb.)\n */\nconst RUN_ID_HEADER = 'X-Run-Id'\n\nfunction runIdHeader(runId: string | undefined): Record<string, string> {\n return runId === undefined ? {} : { [RUN_ID_HEADER]: runId }\n}\n\nfunction withSearchParams(url: string, values: Record<string, string>): string {\n const hashIndex = url.indexOf('#')\n const hash = hashIndex === -1 ? '' : url.slice(hashIndex)\n const withoutHash = hashIndex === -1 ? url : url.slice(0, hashIndex)\n const queryIndex = withoutHash.indexOf('?')\n const base =\n queryIndex === -1 ? withoutHash : withoutHash.slice(0, queryIndex)\n const search = new URLSearchParams(\n queryIndex === -1 ? '' : withoutHash.slice(queryIndex + 1),\n )\n for (const [key, value] of Object.entries(values)) search.set(key, value)\n const query = search.toString()\n return `${base}${query.length === 0 ? '' : `?${query}`}${hash}`\n}\n\n/**\n * Read lines from a stream (newline-delimited)\n */\nasync function* readStreamLines(\n reader: ReadableStreamDefaultReader<Uint8Array>,\n abortSignal?: AbortSignal,\n): AsyncGenerator<string> {\n try {\n const decoder = createResponseStreamTextDecoder()\n let buffer = ''\n\n while (!abortSignal?.aborted) {\n let result: ReadableStreamReadResult<Uint8Array>\n try {\n result = await reader.read()\n } catch (error) {\n if (abortSignal?.aborted) return\n throw new StreamReadError(error)\n }\n const { done, value } = result\n if (done) break\n\n buffer += decoder.decode(value, { stream: true })\n const lines = buffer.split('\\n')\n\n // Keep the last incomplete line in the buffer\n buffer = lines.pop() || ''\n\n for (const line of lines) {\n // Strip a trailing CR so a CRLF stream matches the LF path (and the\n // XHR reader). Without this an exact-equality check like the `[DONE]`\n // sentinel in linesToSSEEvents would miss `data: [DONE]\\r`.\n const normalized = line.endsWith('\\r') ? line.slice(0, -1) : line\n if (normalized.trim()) {\n yield normalized\n }\n }\n }\n\n // Flush the decoder: a connection cut mid-multibyte-character leaves bytes\n // held inside the streaming TextDecoder. Draining them here (as U+FFFD)\n // makes the trailing-buffer check below see the incomplete tail and report\n // truncation instead of silently swallowing it.\n buffer += decoder.decode()\n\n // A non-empty trailing buffer means the connection was cut mid-line.\n // Surface this as an error so the chat client transitions to 'error'\n // state instead of silently presenting a partial stream as success.\n // Skip when the consumer aborted — a user-initiated stop() interrupting\n // mid-line is expected, not a truncation bug.\n if (buffer.trim() && !abortSignal?.aborted) {\n throw new StreamTruncatedError()\n }\n } finally {\n reader.releaseLock()\n }\n}\n\n/** A parsed stream chunk paired with its adapter-owned delivery offset (if any). */\ninterface StreamEvent {\n chunk: StreamChunk\n id?: string\n}\n\n/**\n * Type guard for a durable NDJSON envelope `{ id, chunk }`. NDJSON has no\n * native event-id field, so durability rides the offset inside the payload.\n * A bare `StreamChunk` always has a top-level `type`, and the envelope never\n * does, so the two forms are unambiguous — a non-durable line stays bare.\n */\nfunction isNdjsonEnvelope(\n value: unknown,\n): value is { id: string; chunk: StreamChunk } {\n return (\n typeof value === 'object' &&\n value !== null &&\n 'chunk' in value &&\n 'id' in value &&\n typeof (value as { id: unknown }).id === 'string' &&\n !('type' in value)\n )\n}\n\n/** Rebuild pre-wire extras after SSE/NDJSON ingest. */\nfunction restoreInboundUsage(chunk: StreamChunk): StreamChunk {\n return restoreInboundChunk(chunk)\n}\n\nfunction sseChunkModel(chunk: StreamChunk): string | undefined {\n const tanstackModel = tanstackMetadata(chunk)?.model\n if (typeof tanstackModel === 'string') return tanstackModel\n const usage = 'usage' in chunk ? chunk.usage : undefined\n if (Array.isArray(usage)) {\n const model = (usage[0] as { model?: unknown } | undefined)?.model\n if (typeof model === 'string') return model\n }\n return undefined\n}\n\n/**\n * Parse SSE-format lines into stream events, pairing each chunk with the `id:`\n * offset of the event it arrived on. Shared by the fetch- and XHR-backed SSE\n * adapters so both track delivery offsets identically.\n *\n * Accepts either `data: {...}` lines or bare JSON lines. Skips comments\n * starting with `:` (proxies and CDNs inject these as keepalives) and the\n * `event:` / `retry:` SSE control fields. A `[DONE]` sentinel is treated as a\n * terminal event: a synthesized RUN_FINISHED is yielded using the most recent\n * upstream `threadId` / `runId` (falling back to `fallbackIds`), so the\n * consumer sees a clean terminal event with real correlation ids.\n *\n * A JSON parse failure throws — the consumer surfaces it as an error.\n */\nasync function* linesToSSEEvents(\n lines: AsyncIterable<string>,\n fallbackIds?: { threadId?: string; runId?: string },\n): AsyncGenerator<StreamEvent> {\n let lastThreadId: string | undefined\n let lastRunId: string | undefined\n let lastModel: string | undefined\n let pendingId: string | undefined\n for await (const line of lines) {\n if (line === 'id' || line.startsWith('id:')) {\n // SSE spec: strip a single leading space after the colon, preserve the\n // rest verbatim so an opaque adapter offset round-trips exactly (do NOT\n // trim, which would mangle a legitimate offset). An empty value is kept as\n // '' and resets the resume cursor downstream (see resumableStream).\n const rawId = line === 'id' ? '' : line.slice(3)\n pendingId = rawId.startsWith(' ') ? rawId.slice(1) : rawId\n continue\n }\n // Assumes the durability wire emits one `id:` immediately followed by one\n // `data:` per event (both shipped sinks do). `pendingId` attaches to the\n // next data line and is cleared after it; blank-line event boundaries are\n // stripped upstream, so a hand-rolled server that emits an id-only event or\n // a persistent `id:` across events is not supported here.\n if (\n line.startsWith(':') ||\n line.startsWith('event:') ||\n line.startsWith('retry:')\n ) {\n continue\n }\n const data = parseSseDataLine(line)\n if (data === '[DONE]') {\n yield {\n chunk: withTanstackMetadata(\n {\n type: EventType.RUN_FINISHED,\n threadId: lastThreadId ?? fallbackIds?.threadId ?? '',\n runId: lastRunId ?? fallbackIds?.runId ?? '',\n timestamp: Date.now(),\n },\n {\n finishReason: 'stop',\n ...(lastModel !== undefined ? { model: lastModel } : {}),\n },\n ) as StreamChunk,\n }\n return\n }\n const chunk = restoreInboundUsage(JSON.parse(data) as StreamChunk)\n if ('threadId' in chunk && typeof chunk.threadId === 'string') {\n lastThreadId = chunk.threadId\n }\n if ('runId' in chunk && typeof chunk.runId === 'string') {\n lastRunId = chunk.runId\n }\n const model = sseChunkModel(chunk)\n if (model !== undefined) lastModel = model\n const id = pendingId\n pendingId = undefined\n yield { chunk, ...(id !== undefined ? { id } : {}) }\n }\n}\n\n/**\n * Parse NDJSON-format lines into stream events. Durable streams emit each line\n * as an `{ id, chunk }` envelope carrying the delivery offset; non-durable\n * streams emit bare chunks. Both are auto-detected (see {@link isNdjsonEnvelope}),\n * so an untagged stream behaves exactly as a plain single fetch used to.\n */\nasync function* linesToNdjsonEvents(\n lines: AsyncIterable<string>,\n): AsyncGenerator<StreamEvent> {\n for await (const line of lines) {\n const parsed = JSON.parse(line) as unknown\n if (isNdjsonEnvelope(parsed)) {\n yield { chunk: restoreInboundUsage(parsed.chunk), id: parsed.id }\n } else {\n yield { chunk: restoreInboundUsage(parsed as StreamChunk) }\n }\n }\n}\n\nasync function assertResponseOk(response: Response): Promise<void> {\n if (response.ok) return\n if (response.status === 401) {\n const body: unknown = await response\n .clone()\n .json()\n .catch(() => null)\n if (isByokMissingBody(body)) {\n throw new ByokMissingError(body.error.provider)\n }\n }\n throw new Error(\n `HTTP error! status: ${response.status} ${response.statusText}`,\n )\n}\n\nfunction errorFromXhrStatus(xhr: XMLHttpRequest): Error {\n if (xhr.status === 401) {\n let parsed: unknown = null\n try {\n parsed = JSON.parse(xhr.responseText)\n } catch {\n parsed = null\n }\n if (isByokMissingBody(parsed)) {\n return new ByokMissingError(parsed.error.provider)\n }\n }\n return new Error(`XHR error! status: ${xhr.status} ${xhr.statusText}`)\n}\n\n/**\n * GET the hydration endpoint for a thread and parse its JSON `{ messages,\n * activeRun }` body. This is the transport-agnostic reconnect probe: keyed on\n * the STABLE thread id, it returns the stored transcript and — if a run is still\n * generating — a cursor the caller tails via `joinRun`. Shared by every fetch/\n * XHR adapter so the client never has to know which transport is in use.\n */\nasync function fetchThreadHydration(\n fetchClient: typeof globalThis.fetch,\n url: string,\n headers: Record<string, string>,\n credentials: RequestCredentials,\n threadId: string,\n): Promise<ChatHydrationResult> {\n const response = await fetchClient(withSearchParams(url, { threadId }), {\n method: 'GET',\n headers: { Accept: 'application/json', ...headers },\n credentials,\n })\n await assertResponseOk(response)\n const data = (await response.json()) as {\n messages?: Array<UIMessage>\n activeRun?: { runId?: unknown } | null\n interrupts?: {\n runId?: unknown\n pending?: unknown\n } | null\n }\n const activeRun =\n data.activeRun && typeof data.activeRun.runId === 'string'\n ? { runId: data.activeRun.runId }\n : null\n const interrupts =\n data.interrupts &&\n typeof data.interrupts.runId === 'string' &&\n Array.isArray(data.interrupts.pending) &&\n data.interrupts.pending.length > 0\n ? {\n runId: data.interrupts.runId,\n pending: data.interrupts.pending as Array<ChatPendingInterrupt>,\n }\n : null\n return {\n messages: Array.isArray(data.messages)\n ? normalizeMessagesDates(data.messages)\n : [],\n activeRun,\n interrupts,\n }\n}\n\n/**\n * GET the hydration endpoint for a generation thread and parse its JSON\n * `{ resumeSnapshot, activeRun }` body. Mirrors {@link fetchThreadHydration} for\n * the generation clients: keyed on the stable thread id, it returns the last\n * generation's resume snapshot (re-validated client-side before adoption) and —\n * if a run is still generating — a cursor. Shared by every fetch/XHR adapter.\n */\nasync function fetchGenerationHydration(\n fetchClient: typeof globalThis.fetch,\n url: string,\n headers: Record<string, string>,\n credentials: RequestCredentials,\n threadId: string,\n): Promise<GenerationHydrationResult> {\n const response = await fetchClient(withSearchParams(url, { threadId }), {\n method: 'GET',\n headers: { Accept: 'application/json', ...headers },\n credentials,\n })\n await assertResponseOk(response)\n const raw: unknown = await response.json()\n // A 200 carrying `null` is a legitimate hydration miss — the server has no\n // record for this thread — and reading `.activeRun` off `null` would throw.\n if (raw === null) {\n return { resumeSnapshot: null, activeRun: null }\n }\n // Any OTHER non-object body is a broken endpoint, not an empty thread.\n // Reporting it as a miss would present a misconfigured route as a fresh\n // thread; the client surfaces this through its own error channel instead.\n if (typeof raw !== 'object' || Array.isArray(raw)) {\n throw new Error(\n `Generation hydration expected a JSON object from ${url}, received ${Array.isArray(raw) ? 'an array' : typeof raw}.`,\n )\n }\n const data = raw as {\n resumeSnapshot?: GenerationHydrationResult['resumeSnapshot']\n activeRun?: { runId?: unknown } | null\n }\n const activeRun =\n data.activeRun && typeof data.activeRun.runId === 'string'\n ? { runId: data.activeRun.runId }\n : null\n return {\n resumeSnapshot: data.resumeSnapshot ?? null,\n activeRun,\n }\n}\n\n/** Yield SSE stream events (chunk + offset) from a fetch Response body. */\nasync function* responseToSSEEvents(\n response: Response,\n abortSignal?: AbortSignal,\n fallbackIds?: { threadId?: string; runId?: string },\n): AsyncGenerator<StreamEvent> {\n await assertResponseOk(response)\n const reader = getResponseStreamReader(response)\n yield* linesToSSEEvents(readStreamLines(reader, abortSignal), fallbackIds)\n}\n\n/** Yield NDJSON stream events (chunk + offset) from a fetch Response body. */\nasync function* responseToNdjsonEvents(\n response: Response,\n abortSignal?: AbortSignal,\n): AsyncGenerator<StreamEvent> {\n await assertResponseOk(response)\n const reader = getResponseStreamReader(response)\n yield* linesToNdjsonEvents(readStreamLines(reader, abortSignal))\n}\n\nasync function* responseToSSEChunks(\n response: Response,\n abortSignal?: AbortSignal,\n): AsyncGenerator<StreamChunk> {\n for await (const { chunk } of responseToSSEEvents(response, abortSignal)) {\n yield chunk\n }\n}\n\n/**\n * A re-issuable event source. Given extra headers (a `Last-Event-ID` on a\n * reconnect) and an abort signal, it opens the transport and yields stream\n * events. {@link resumableStream} calls it once per attempt, so each call MUST\n * open a fresh underlying request (a new fetch or a new XHR).\n */\ntype StreamEventSource = (\n extraHeaders: Record<string, string>,\n abortSignal?: AbortSignal,\n) => AsyncIterable<StreamEvent>\n\n/**\n * Build a fetch-backed {@link StreamEventSource}. `parseResponse` decodes the\n * body into events (SSE or NDJSON) — the reconnect engine is identical for both.\n */\nfunction fetchEventSource(\n fetchClient: typeof globalThis.fetch,\n url: string,\n requestInit: RequestInit,\n parseResponse: (\n response: Response,\n abortSignal?: AbortSignal,\n ) => AsyncIterable<StreamEvent>,\n): StreamEventSource {\n return async function* (extraHeaders, abortSignal) {\n let response: Response\n try {\n response = await fetchClient(url, {\n ...requestInit,\n headers: {\n ...(requestInit.headers as Record<string, string> | undefined),\n ...extraHeaders,\n },\n ...(abortSignal ? { signal: abortSignal } : {}),\n })\n } catch (error) {\n // A fetch REJECTION (device offline, DNS blip, connection refused) is a\n // recoverable transport failure, not a fatal one — surface it as\n // StreamReadError so resumableStream retries from the last offset, mirroring\n // the XHR path (whose onerror wraps the same way). On a genuine abort this\n // wraps the AbortError too, but that's harmless: resumableStream checks\n // `abortSignal.aborted` first and returns, so the wrapped error's type is\n // never inspected. Without an offset (initial connect / non-durable), it\n // still surfaces as a hard failure.\n throw new StreamReadError(error)\n }\n yield* parseResponse(response, abortSignal)\n }\n}\n\n/**\n * Drive a {@link StreamEventSource} with native-style resumability. Each event's\n * adapter-owned delivery offset (its `id`) is remembered; if the connection\n * drops or ends before a terminal event, the source is re-opened with a\n * `Last-Event-ID` header so the server replays strictly after the last offset.\n * Already-seen offsets are de-duped, so an overlapping replay is safe.\n *\n * When the server does NOT tag events (no durability), no offset is ever seen,\n * so no reconnect happens — behaviour is identical to a plain single request.\n * This engine is transport-agnostic: fetch/XHR × SSE/NDJSON all share it, the\n * only difference being the {@link StreamEventSource} they pass in.\n */\nasync function* resumableStream(\n openEventSource: StreamEventSource,\n abortSignal?: AbortSignal,\n reconnectOptions?: ReconnectOptions,\n): AsyncGenerator<StreamChunk> {\n const tracker = createReconnectTracker(reconnectOptions)\n\n for (;;) {\n if (abortSignal?.aborted) return\n const extraHeaders: Record<string, string> =\n tracker.lastEventId !== undefined\n ? { 'Last-Event-ID': tracker.lastEventId }\n : {}\n\n let sawTerminal = false\n let progressed = false\n try {\n for await (const { chunk, id } of openEventSource(\n extraHeaders,\n abortSignal,\n )) {\n if (tracker.note(id) === 'duplicate') continue\n progressed = true\n if (chunk.type === 'RUN_FINISHED' || chunk.type === 'RUN_ERROR') {\n sawTerminal = true\n }\n yield chunk\n // Do NOT stop on a terminal mid-source: an agent loop emits one\n // RUN_STARTED/RUN_FINISHED pair PER turn, so a tool-calling run carries\n // several RUN_FINISHED events before the run is truly done. Returning on\n // the first one would drop every subsequent turn (the tool result and\n // the final answer). Instead, drain the event source to its natural end\n // — the server closes the response only when the run is actually\n // complete — and use `sawTerminal` below to decide done-vs-reconnect.\n }\n } catch (error) {\n if (abortSignal?.aborted) return\n // A transport drop is resumable once we hold an offset — retry from it,\n // even if THIS attempt made no new progress. A caught-up run whose parked\n // long-poll socket drops (or a proxy that drops just after replaying the\n // de-duped overlap) is transient, not fatal; the consecutive-no-progress\n // ceiling in waitBeforeReconnect already bounds a genuinely stuck flapper,\n // so a per-attempt progress requirement here would only convert\n // recoverable drops into hard failures on flaky (mobile/edge) networks.\n // Without an offset (a non-durable stream), surface the failure.\n if (\n (error instanceof StreamTruncatedError ||\n error instanceof StreamReadError) &&\n tracker.lastEventId !== undefined\n ) {\n await tracker.waitBeforeReconnect(progressed, abortSignal)\n continue\n }\n throw error\n }\n\n if (abortSignal?.aborted) return\n\n // The source ended after delivering a terminal event: the run is genuinely\n // finished (for an agentic run this is the LAST turn's terminal, since we no\n // longer stop on intermediate ones). Stop — reconnecting a durable run here\n // would re-open past the final offset and see an empty window.\n if (sawTerminal) return\n\n if (tracker.lastEventId !== undefined) {\n // A durable (id-tagged) run.\n if (progressed) {\n // Clean end WITHOUT a terminal event but we advanced — the producer is\n // still going (or the socket rolled over). Reconnect from the last\n // offset (backing off to avoid a hot loop against the origin). Progress\n // resets the no-progress ceiling.\n await tracker.waitBeforeReconnect(true, abortSignal)\n continue\n }\n // Ended without a terminal event AND made no forward progress on this\n // pass: the run cannot complete. Surface an error rather than returning\n // silently, which would leave the consumer with neither a terminal event\n // nor a failure.\n //\n // Invariant this relies on: a durable transport must never surface an\n // empty long-poll window as a CLEAN end while the producer is still\n // alive. Both shipped backends honor it — memoryStream parks until data\n // or completion, and durableStream keeps one continuous response across\n // windows — so this fires only on a genuinely complete-but-unterminated\n // log. A custom StreamDurability transport that ends a response empty\n // mid-run would trip this; keep the response open until data or terminal.\n throw new DurableStreamIncompleteError()\n }\n\n // A non-durable (untagged) stream that ended cleanly. Legitimate — the\n // upper layer synthesizes a terminal event. Stop.\n return\n }\n}\n\n/**\n * Per-send context provided by the chat client to the connection adapter.\n * The adapter combines this with serialized messages to build a full\n * AG-UI `RunAgentInput` payload.\n */\nexport interface RunAgentInputContext {\n threadId: string\n runId: string\n parentRunId?: string\n /** AG-UI interrupt resume entries returned to the server on a follow-up run. */\n resume?: Array<RunAgentResumeItem>\n /** Client-declared tools to advertise in the request payload. */\n clientTools?: Array<{\n name: string\n description: string\n parameters: unknown\n }>\n /** Arbitrary user-controlled passthrough data. */\n forwardedProps?: Record<string, unknown>\n /** Extra request headers for this run (e.g. BYOK keys). POST only. */\n headers?: Record<string, string>\n}\n\nexport interface ConnectConnectionAdapter {\n /**\n * Connect and return an async iterable of StreamChunks.\n */\n connect: (\n messages: Array<UIMessage> | Array<ModelMessage>,\n data?: Record<string, any>,\n abortSignal?: AbortSignal,\n runContext?: RunAgentInputContext,\n ) => AsyncIterable<StreamChunk>\n /**\n * Fetch server-driven hydration for a generation `threadId`: the last\n * generation's resume snapshot, plus a cursor to a run still generating if\n * one exists. The generation client calls this itself on mount when\n * `persistence: true` (no loader/prop) and repaints the snapshot — it never\n * auto-starts a run. Read-only JSON GET (`?threadId`), so it is\n * transport-agnostic. Optional and feature-detected exactly like the chat\n * `hydrate` handler.\n */\n hydrateGeneration?: (threadId: string) => Promise<GenerationHydrationResult>\n /**\n * Re-attach to a run that is still generating and replay it from the start\n * (read-only `?offset=-1&runId` against the delivery-durability log). The\n * generation client tails this on mount when hydration reports a run still in\n * flight, so a dropped connection or a full reload finishes the generation in\n * place — the same durability replay the chat client uses. Optional and\n * feature-detected; present on `fetchServerSentEvents` / `fetchHttpStream`.\n */\n joinRun?: (\n runId: string,\n abortSignal?: AbortSignal,\n ) => AsyncIterable<StreamChunk>\n /**\n * Fetch server-driven hydration for a chat `threadId`: the stored transcript\n * plus a cursor to an in-flight run and any pending interrupts. The chat\n * client calls this itself on mount when `persistence: true` (no loader/prop)\n * and repaints it — it never auto-sends. Read-only JSON GET (`?threadId`), so\n * it is transport-agnostic. Optional and feature-detected; present on\n * `fetchServerSentEvents` / `fetchHttpStream`, and on `stream()` /\n * `rpcStream()` when supplied via {@link StreamConnectionHandlers}.\n */\n hydrate?: (threadId: string) => Promise<ChatHydrationResult>\n}\n\n/**\n * Server-resolved hydration for a generation thread. `resumeSnapshot` is the\n * last generation's lightweight snapshot (validated client-side before it is\n * adopted); `activeRun` is a cursor to a run still generating for the thread\n * (or `null`).\n *\n * Field-for-field compatible with `@tanstack/ai-persistence`'s\n * `ReconstructedGeneration` (the body `reconstructGeneration` returns) — the\n * client never imports that package, so this is a structural contract, not a\n * shared type. Two deliberate widenings on this side: `schemaVersion` is\n * optional (the server always writes `1`, but a hand-written fixture need not),\n * and `status` also admits `'idle'`, which the server's mapper never emits.\n * Only a client-local snapshot reaches it, when `stop()` retires a cancelled\n * run.\n */\nexport interface GenerationHydrationResult {\n resumeSnapshot: {\n schemaVersion?: 1\n resumeState: { threadId: string; runId: string } | null\n status: 'idle' | 'running' | 'complete' | 'error'\n result?: unknown\n error?: { message: string; code?: string }\n activity?: string\n } | null\n activeRun: { runId: string } | null\n}\n\n/**\n * Server-resolved hydration for a thread. `messages` is the stored transcript;\n * `activeRun` is a cursor to a run still generating for the thread (or `null`).\n * Keyed on the STABLE thread id — the client never handles a run id, so a turn\n * that spans several runs (interrupt/tool continuations) reconnects correctly.\n */\nexport interface ChatHydrationResult {\n messages: Array<UIMessage>\n activeRun: { runId: string } | null\n /**\n * Pending human-in-the-loop interrupts for the thread and the run they paused,\n * so a reload (or another device) re-prompts the approval from the server. The\n * client restores them exactly as a persisted resume snapshot would.\n */\n interrupts: {\n runId: string\n pending: Array<ChatPendingInterrupt>\n } | null\n}\n\n/**\n * A {@link ConnectConnectionAdapter} that also supports joining an existing run\n * (a second tab, or re-attaching after a full reload) via `joinRun`, replaying\n * the ordered stream from the start off the server's delivery-durability sink.\n */\nexport interface ResumableConnectConnectionAdapter extends ConnectConnectionAdapter {\n /**\n * Join an in-flight or finished run by id, replaying from the start\n * (`?offset=-1`). Read-only — sends no messages.\n */\n joinRun: (\n runId: string,\n abortSignal?: AbortSignal,\n ) => AsyncIterable<StreamChunk>\n /**\n * Fetch server-authoritative hydration for `threadId`: the stored transcript,\n * and a cursor to an in-flight run if one exists. The client calls this itself\n * on mount (no loader/prop), then tails `activeRun` via `joinRun`. Read-only\n * JSON GET (`?threadId`), so it is transport-agnostic regardless of how the\n * delivery stream is served.\n */\n hydrate?: (threadId: string) => Promise<ChatHydrationResult>\n}\n\nexport interface SubscribeConnectionAdapter {\n /**\n * Subscribe to stream chunks.\n */\n subscribe: (abortSignal?: AbortSignal) => AsyncIterable<StreamChunk>\n /**\n * Send a request; chunks arrive through subscribe().\n */\n send: (\n messages: Array<UIMessage> | Array<ModelMessage>,\n data?: Record<string, any>,\n abortSignal?: AbortSignal,\n runContext?: RunAgentInputContext,\n ) => Promise<void>\n /**\n * Re-attach to an existing run by id, replaying its stream from the start off\n * the server's delivery-durability sink. Present only when the underlying\n * connection is resumable (a `ResumableConnectConnectionAdapter`). Used to\n * rejoin an in-flight run after a full page reload.\n */\n joinRun?: (\n runId: string,\n abortSignal?: AbortSignal,\n ) => AsyncIterable<StreamChunk>\n /**\n * Server-authoritative hydration for a thread (transcript + in-flight-run\n * cursor). Present only when the underlying connection supports it. The client\n * calls it on mount to re-hydrate without any app-side loader or prop.\n */\n hydrate?: (threadId: string) => Promise<ChatHydrationResult>\n}\n\n/**\n * Connection adapter union.\n * Provide either `connect`, or `subscribe` + `send`.\n */\nexport type ConnectionAdapter =\n | ConnectConnectionAdapter\n | SubscribeConnectionAdapter\n\n/**\n * Normalize a ConnectionAdapter to subscribe/send operations.\n *\n * If a connection provides native subscribe/send, that mode is used.\n * Otherwise, connect() is wrapped using an async queue.\n */\nexport function normalizeConnectionAdapter(\n connection: ConnectionAdapter | undefined,\n): SubscribeConnectionAdapter {\n if (!connection) {\n throw new Error('Connection adapter is required')\n }\n\n const hasConnect = 'connect' in connection\n const hasSubscribe = 'subscribe' in connection\n const hasSend = 'send' in connection\n\n if (hasConnect && (hasSubscribe || hasSend)) {\n throw new Error(\n 'Connection adapter must provide either connect or both subscribe and send, not both modes',\n )\n }\n\n if (hasSubscribe && hasSend) {\n const joinRun = (connection as SubscribeConnectionAdapter).joinRun?.bind(\n connection,\n )\n const hydrate = (connection as SubscribeConnectionAdapter).hydrate?.bind(\n connection,\n )\n return {\n subscribe: connection.subscribe.bind(connection),\n send: connection.send.bind(connection),\n ...(joinRun ? { joinRun } : {}),\n ...(hydrate ? { hydrate } : {}),\n }\n }\n\n if (!hasConnect) {\n throw new Error(\n 'Connection adapter must provide either connect or both subscribe and send',\n )\n }\n\n // Legacy connect() wrapper\n let activeBuffer: Array<StreamChunk> = []\n let activeWaiters: Array<(chunk: StreamChunk | null) => void> = []\n let activeSubscriber: typeof activeWaiters | undefined\n\n function push(chunk: StreamChunk, runId?: string): void {\n if (runId) {\n chunkRunIds.set(chunk, runId)\n }\n const waiter = activeWaiters.shift()\n if (waiter) {\n waiter(chunk)\n } else {\n activeBuffer.push(chunk)\n }\n }\n\n async function waitUntilSubscriberIdle(\n abortSignal?: AbortSignal,\n ): Promise<void> {\n // Idle means the subscriber is waiting for the next chunk, so the\n // previous chunk has left processIncomingChunk. Empty waiters with an\n // empty buffer is in-flight delivery, not idle.\n const idle = () =>\n activeSubscriber !== activeWaiters ||\n (activeBuffer.length === 0 &&\n (activeWaiters.length > 0 || abortSignal?.aborted))\n for (let i = 0; i < 16 && !abortSignal?.aborted; i++) {\n if (idle()) return\n await Promise.resolve()\n }\n while (!abortSignal?.aborted) {\n if (idle()) return\n await new Promise<void>((resolve) => setTimeout(resolve, 0))\n }\n }\n\n return {\n subscribe(abortSignal?: AbortSignal): AsyncIterable<StreamChunk> {\n // Transfer ownership to the latest subscriber so only one active\n // subscribe() call receives chunks from the shared connect-wrapper queue.\n const myBuffer: Array<StreamChunk> = activeBuffer.splice(0)\n const myWaiters: Array<(chunk: StreamChunk | null) => void> = []\n activeBuffer = myBuffer\n activeWaiters = myWaiters\n\n return (async function* () {\n activeSubscriber = myWaiters\n try {\n while (!abortSignal?.aborted) {\n let chunk: StreamChunk | null\n const buffered = myBuffer.shift()\n if (buffered !== undefined) {\n chunk = buffered\n } else {\n chunk = await new Promise<StreamChunk | null>((resolve) => {\n const onAbort = () => resolve(null)\n myWaiters.push((c) => {\n abortSignal?.removeEventListener('abort', onAbort)\n resolve(c)\n })\n abortSignal?.addEventListener('abort', onAbort, { once: true })\n })\n }\n if (chunk !== null) yield chunk\n }\n } finally {\n if (activeSubscriber === myWaiters) activeSubscriber = undefined\n }\n })()\n },\n async send(messages, data, abortSignal, runContext) {\n let hasTerminalEvent = false\n let upstreamThreadId: string | undefined\n let upstreamRunId: string | undefined\n try {\n const stream = connection.connect(\n messages,\n data,\n abortSignal,\n runContext,\n )\n for await (const chunk of stream) {\n if ('threadId' in chunk && typeof chunk.threadId === 'string') {\n upstreamThreadId = chunk.threadId\n }\n if ('runId' in chunk && typeof chunk.runId === 'string') {\n upstreamRunId = chunk.runId\n }\n if (chunk.type === 'RUN_FINISHED' || chunk.type === 'RUN_ERROR') {\n hasTerminalEvent = true\n }\n push(chunk, runContext?.runId)\n }\n\n // If the connect stream ended cleanly without a terminal event,\n // synthesize RUN_FINISHED so request-scoped consumers can complete.\n // The event payload may carry an upstream/provider runId when one was\n // observed, but stamp the caller's request runId so getChunkRunId()\n // correlates to activeRunIds / currentRunId (same as real stream chunks).\n if (!abortSignal?.aborted && !hasTerminalEvent) {\n push(\n withTanstackMetadata(\n {\n type: EventType.RUN_FINISHED,\n threadId: requireSyntheticId(\n upstreamThreadId ?? runContext?.threadId,\n 'threadId',\n ),\n runId: requireSyntheticId(\n upstreamRunId ?? runContext?.runId,\n 'runId',\n ),\n timestamp: Date.now(),\n },\n { finishReason: 'stop', model: 'connect-wrapper' },\n ) as StreamChunk,\n runContext?.runId,\n )\n }\n } catch (err) {\n if (!abortSignal?.aborted && !hasTerminalEvent) {\n // Guard synthesis: requireSyntheticId throws when no id is available,\n // and that must not replace the original `err` we are about to\n // rethrow. If we can't synthesize a terminal, the real failure still\n // surfaces below.\n try {\n const message =\n err instanceof Error ? err.message : 'Unknown error in connect()'\n const synthetic: RunErrorEvent = {\n type: EventType.RUN_ERROR,\n threadId: requireSyntheticId(\n upstreamThreadId ?? runContext?.threadId,\n 'threadId',\n ),\n runId: requireSyntheticId(\n upstreamRunId ?? runContext?.runId,\n 'runId',\n ),\n timestamp: Date.now(),\n message,\n }\n push(synthetic, runContext?.runId)\n } catch {\n // fall through to rethrow the original error\n }\n }\n throw err\n }\n await waitUntilSubscriberIdle(abortSignal)\n },\n // Expose joinRun only when the underlying connection is resumable. Require\n // a real function — `'joinRun' in connection` is true for\n // `{ joinRun: undefined }`, which would wrap a non-callable and throw on\n // rehydration rejoin.\n ...(typeof (connection as ResumableConnectConnectionAdapter).joinRun ===\n 'function'\n ? {\n joinRun: (runId: string, abortSignal?: AbortSignal) =>\n (connection as ResumableConnectConnectionAdapter).joinRun(\n runId,\n abortSignal,\n ),\n }\n : {}),\n ...(() => {\n // Capture under the typeof guard so `hydrate` narrows to the function type\n // (no non-null assertion). Present only when the connection supports it.\n const hydrate = (connection as ResumableConnectConnectionAdapter).hydrate\n return typeof hydrate === 'function'\n ? { hydrate: (threadId: string) => hydrate(threadId) }\n : {}\n })(),\n }\n}\n\n/**\n * Options for fetch-based connection adapters\n */\nexport interface FetchConnectionOptions {\n headers?: Record<string, string> | Headers\n credentials?: RequestCredentials\n signal?: AbortSignal\n body?: Record<string, any>\n fetchClient?: typeof globalThis.fetch\n /** Bounding for resumable-SSE reconnection (throttle delay + attempt ceiling). */\n reconnect?: ReconnectOptions\n}\n\n/**\n * Options for XHR-based connection adapters.\n */\nexport interface XhrConnectionOptions {\n headers?: Record<string, string> | Headers\n withCredentials?: boolean\n signal?: AbortSignal\n body?: Record<string, any>\n xhrFactory?: () => XMLHttpRequest\n /** Bounding for resumable reconnection (throttle delay + attempt ceiling). */\n reconnect?: ReconnectOptions\n}\n\ntype ResolvedConnectionOptions = Pick<\n FetchConnectionOptions,\n 'body' | 'headers'\n>\n\nfunction buildRunAgentInputBody(\n messages: Array<UIMessage> | Array<ModelMessage>,\n data: Record<string, any> | undefined,\n runContext: RunAgentInputContext | undefined,\n options: ResolvedConnectionOptions,\n): Record<string, unknown> {\n // Precedence (later spreads win): static adapter `body` is the base,\n // overridden by `runContext.forwardedProps`, overridden by per-message `data`.\n const wireMessages = uiMessagesToWire(messages)\n const forwardedProps = {\n ...options.body,\n ...(runContext?.forwardedProps ?? {}),\n ...data,\n }\n\n return {\n threadId: runContext?.threadId ?? generateRunId('thread'),\n runId: runContext?.runId ?? generateRunId('run'),\n ...(runContext?.parentRunId !== undefined && {\n parentRunId: runContext.parentRunId,\n }),\n ...(runContext?.resume !== undefined && { resume: runContext.resume }),\n state: {},\n messages: wireMessages,\n tools: runContext?.clientTools ?? [],\n context: [],\n forwardedProps,\n // Backward-compat mirror of `forwardedProps` under the legacy field name.\n data: { ...forwardedProps },\n }\n}\n\n/**\n * Create a Server-Sent Events connection adapter\n *\n * @param url - The API endpoint URL (or a function that returns the URL)\n * @param options - Fetch options (headers, credentials, body, etc.) or a function that returns options (can be async)\n * @returns A connection adapter for SSE streams\n *\n * @example\n * ```typescript\n * // Static URL\n * const connection = fetchServerSentEvents('/api/chat');\n *\n * // Dynamic URL\n * const connection = fetchServerSentEvents(() => `/api/chat?user=${userId}`);\n *\n * // With options\n * const connection = fetchServerSentEvents('/api/chat', {\n * headers: { 'Authorization': 'Bearer token' }\n * });\n *\n * // With dynamic options\n * const connection = fetchServerSentEvents('/api/chat', () => ({\n * headers: { 'Authorization': `Bearer ${getToken()}` }\n * }));\n *\n * // With additional body data\n * const connection = fetchServerSentEvents('/api/chat', async () => ({\n * body: {\n * provider: 'openai',\n * model: 'gpt-5.5',\n * }\n * }));\n * ```\n */\nexport function fetchServerSentEvents(\n url: string | (() => string),\n options:\n | FetchConnectionOptions\n | (() => FetchConnectionOptions | Promise<FetchConnectionOptions>) = {},\n): ResumableConnectConnectionAdapter {\n return {\n async *connect(messages, data, abortSignal, runContext) {\n // Resolve URL and options if they are functions\n const resolvedUrl = typeof url === 'function' ? url() : url\n const resolvedOptions =\n typeof options === 'function' ? await options() : options\n\n const requestHeaders: Record<string, string> = {\n 'Content-Type': 'application/json',\n ...mergeHeaders(resolvedOptions.headers),\n ...mergeHeaders(runContext?.headers),\n ...runIdHeader(runContext?.runId),\n }\n\n // Build AG-UI RunAgentInput payload.\n //\n // Precedence (later spreads win): static adapter `body` is the base,\n // overridden by `runContext.forwardedProps` (constructor body /\n // forwardedProps options), overridden by per-message `data` passed\n // to `connection.send`. Runtime values win over static config —\n // this matches the documented \"forwardedProps wins\" semantic.\n const requestBody = buildRunAgentInputBody(\n messages,\n data,\n runContext,\n resolvedOptions,\n )\n\n const fetchClient = resolvedOptions.fetchClient ?? fetch\n // `RequestInit.signal` is typed `AbortSignal | null` (no `undefined`\n // under `exactOptionalPropertyTypes`), so spread it conditionally\n // rather than passing `undefined` explicitly.\n const signal = abortSignal || resolvedOptions.signal\n // POST URL is byte-identical to a plain request; the run id (when set)\n // rides in the X-Run-Id header so durability can key the log by it\n // without changing the request URL existing clients rely on.\n const requestUrl = resolvedUrl\n\n // Resumable SSE: if the server tags events with `id:` offsets (delivery\n // durability), a dropped/rolled-over connection auto-reconnects with a\n // `Last-Event-ID` header and de-dupes the replayed prefix. With no tags,\n // this is a single plain fetch.\n yield* resumableStream(\n fetchEventSource(\n fetchClient,\n requestUrl,\n {\n method: 'POST',\n headers: requestHeaders,\n body: JSON.stringify(requestBody),\n credentials: resolvedOptions.credentials || 'same-origin',\n },\n // Thread the run's ids so a `[DONE]`-terminating server that doesn't\n // stamp them onto events still yields a correlated terminal (parity\n // with the XHR adapter's xhrSSEParser).\n (response, sseSignal) =>\n responseToSSEEvents(response, sseSignal, {\n ...(runContext?.threadId !== undefined\n ? { threadId: runContext.threadId }\n : {}),\n ...(runContext?.runId !== undefined\n ? { runId: runContext.runId }\n : {}),\n }),\n ),\n signal,\n resolvedOptions.reconnect,\n )\n },\n async *joinRun(runId, abortSignal) {\n // Read an in-flight or finished run from the start. `?offset=-1` tells the\n // server's delivery-durability sink to replay from the beginning; `runId`\n // identifies which run. This is a read-only GET — no messages are sent.\n const resolvedUrl = typeof url === 'function' ? url() : url\n const resolvedOptions =\n typeof options === 'function' ? await options() : options\n\n const joinUrl = withSearchParams(resolvedUrl, {\n offset: '-1',\n runId,\n })\n\n const requestHeaders: Record<string, string> = {\n ...mergeHeaders(resolvedOptions.headers),\n }\n const fetchClient = resolvedOptions.fetchClient ?? fetch\n const signal = abortSignal || resolvedOptions.signal\n\n yield* resumableStream(\n fetchEventSource(\n fetchClient,\n joinUrl,\n {\n method: 'GET',\n headers: requestHeaders,\n credentials: resolvedOptions.credentials || 'same-origin',\n },\n // A `[DONE]` during a join correlates to the joined run id.\n (response, sseSignal) =>\n responseToSSEEvents(response, sseSignal, { runId }),\n ),\n signal,\n resolvedOptions.reconnect,\n )\n },\n async hydrate(threadId) {\n const resolvedUrl = typeof url === 'function' ? url() : url\n const resolvedOptions =\n typeof options === 'function' ? await options() : options\n return fetchThreadHydration(\n resolvedOptions.fetchClient ?? fetch,\n resolvedUrl,\n mergeHeaders(resolvedOptions.headers),\n resolvedOptions.credentials || 'same-origin',\n threadId,\n )\n },\n async hydrateGeneration(threadId) {\n const resolvedUrl = typeof url === 'function' ? url() : url\n const resolvedOptions =\n typeof options === 'function' ? await options() : options\n return fetchGenerationHydration(\n resolvedOptions.fetchClient ?? fetch,\n resolvedUrl,\n mergeHeaders(resolvedOptions.headers),\n resolvedOptions.credentials || 'same-origin',\n threadId,\n )\n },\n }\n}\n\n/**\n * Create an HTTP streaming connection adapter (for raw streaming without SSE format)\n *\n * @param url - The API endpoint URL (or a function that returns the URL)\n * @param options - Fetch options (headers, credentials, body, etc.) or a function that returns options (can be async)\n * @returns A connection adapter for HTTP streams\n *\n * @example\n * ```typescript\n * // Static URL\n * const connection = fetchHttpStream('/api/chat');\n *\n * // Dynamic URL\n * const connection = fetchHttpStream(() => `/api/chat?user=${userId}`);\n *\n * // With options\n * const connection = fetchHttpStream('/api/chat', {\n * headers: { 'Authorization': 'Bearer token' }\n * });\n *\n * // With dynamic options\n * const connection = fetchHttpStream('/api/chat', () => ({\n * headers: { 'Authorization': `Bearer ${getToken()}` }\n * }));\n *\n * // With additional body data\n * const connection = fetchHttpStream('/api/chat', async () => ({\n * body: {\n * provider: 'openai',\n * model: 'gpt-5.5',\n * }\n * }));\n * ```\n */\nexport function fetchHttpStream(\n url: string | (() => string),\n options:\n | FetchConnectionOptions\n | (() => FetchConnectionOptions | Promise<FetchConnectionOptions>) = {},\n): ResumableConnectConnectionAdapter {\n return {\n async *connect(messages, data, abortSignal, runContext) {\n // Resolve URL and options if they are functions\n const resolvedUrl = typeof url === 'function' ? url() : url\n const resolvedOptions =\n typeof options === 'function' ? await options() : options\n\n const requestHeaders: Record<string, string> = {\n 'Content-Type': 'application/json',\n ...mergeHeaders(resolvedOptions.headers),\n ...mergeHeaders(runContext?.headers),\n ...runIdHeader(runContext?.runId),\n }\n\n // Build AG-UI RunAgentInput payload.\n //\n // Precedence (later spreads win): static adapter `body` is the base,\n // overridden by `runContext.forwardedProps` (constructor body /\n // forwardedProps options), overridden by per-message `data` passed\n // to `connection.send`. Runtime values win over static config —\n // this matches the documented \"forwardedProps wins\" semantic.\n const requestBody = buildRunAgentInputBody(\n messages,\n data,\n runContext,\n resolvedOptions,\n )\n\n const fetchClient = resolvedOptions.fetchClient ?? fetch\n // `RequestInit.signal` is typed `AbortSignal | null` (no `undefined`\n // under `exactOptionalPropertyTypes`), so spread it conditionally\n // rather than passing `undefined` explicitly.\n const signal = abortSignal || resolvedOptions.signal\n // POST URL is byte-identical to a plain request; the run id (when set)\n // rides in the X-Run-Id header so durability can key the log by it\n // without changing the request URL existing clients rely on.\n const requestUrl = resolvedUrl\n\n // Resumable NDJSON: if the server envelopes each line with an\n // `{ id, chunk }` offset (delivery durability), a dropped/rolled-over\n // connection auto-reconnects with a `Last-Event-ID` header and de-dupes\n // the replayed prefix. With bare lines (no durability), this is a single\n // plain fetch — identical to before.\n yield* resumableStream(\n fetchEventSource(\n fetchClient,\n requestUrl,\n {\n method: 'POST',\n headers: requestHeaders,\n body: JSON.stringify(requestBody),\n credentials: resolvedOptions.credentials || 'same-origin',\n },\n responseToNdjsonEvents,\n ),\n signal,\n resolvedOptions.reconnect,\n )\n },\n async *joinRun(runId, abortSignal) {\n // Read an in-flight or finished run from the start. `?offset=-1` tells the\n // server's delivery-durability sink to replay from the beginning; `runId`\n // identifies which run. This is a read-only GET — no messages are sent.\n const resolvedUrl = typeof url === 'function' ? url() : url\n const resolvedOptions =\n typeof options === 'function' ? await options() : options\n\n const joinUrl = withSearchParams(resolvedUrl, { offset: '-1', runId })\n const requestHeaders: Record<string, string> = {\n ...mergeHeaders(resolvedOptions.headers),\n }\n const fetchClient = resolvedOptions.fetchClient ?? fetch\n const signal = abortSignal || resolvedOptions.signal\n\n yield* resumableStream(\n fetchEventSource(\n fetchClient,\n joinUrl,\n {\n method: 'GET',\n headers: requestHeaders,\n credentials: resolvedOptions.credentials || 'same-origin',\n },\n responseToNdjsonEvents,\n ),\n signal,\n resolvedOptions.reconnect,\n )\n },\n async hydrate(threadId) {\n const resolvedUrl = typeof url === 'function' ? url() : url\n const resolvedOptions =\n typeof options === 'function' ? await options() : options\n return fetchThreadHydration(\n resolvedOptions.fetchClient ?? fetch,\n resolvedUrl,\n mergeHeaders(resolvedOptions.headers),\n resolvedOptions.credentials || 'same-origin',\n threadId,\n )\n },\n async hydrateGeneration(threadId) {\n const resolvedUrl = typeof url === 'function' ? url() : url\n const resolvedOptions =\n typeof options === 'function' ? await options() : options\n return fetchGenerationHydration(\n resolvedOptions.fetchClient ?? fetch,\n resolvedUrl,\n mergeHeaders(resolvedOptions.headers),\n resolvedOptions.credentials || 'same-origin',\n threadId,\n )\n },\n }\n}\n\ntype XhrConnectionOptionsResolver =\n | XhrConnectionOptions\n | (() => XhrConnectionOptions | Promise<XhrConnectionOptions>)\n\nfunction createDefaultXMLHttpRequest(): XMLHttpRequest {\n if (typeof globalThis.XMLHttpRequest !== 'function') {\n throw new Error('XMLHttpRequest is not available in this runtime')\n }\n\n return new globalThis.XMLHttpRequest()\n}\n\nfunction cleanupXhr(\n xhr: XMLHttpRequest,\n abortSignal: AbortSignal | undefined,\n onAbort: (() => void) | undefined,\n): void {\n xhr.onprogress = null\n xhr.onload = null\n xhr.onerror = null\n xhr.onabort = null\n xhr.onloadend = null\n\n if (abortSignal && onAbort) {\n abortSignal.removeEventListener('abort', onAbort)\n }\n}\n\nfunction readXhrLines(\n xhr: XMLHttpRequest,\n abortSignal?: AbortSignal,\n): AsyncGenerator<string> {\n let offset = 0\n let buffer = ''\n const lines: Array<string> = []\n const waiters: Array<() => void> = []\n let done = false\n let aborted = false\n let error: unknown\n let onAbort: (() => void) | undefined\n\n const wake = () => {\n const waiter = waiters.shift()\n waiter?.()\n }\n\n const enqueueDelta = () => {\n if (xhr.status !== 0 && (xhr.status < 200 || xhr.status >= 300)) {\n error = errorFromXhrStatus(xhr)\n done = true\n return\n }\n\n const responseText = xhr.responseText\n if (responseText.length <= offset) {\n return\n }\n\n buffer += responseText.slice(offset)\n offset = responseText.length\n const splitLines = buffer.split('\\n')\n buffer = splitLines.pop() ?? ''\n\n for (const line of splitLines) {\n const normalized = line.endsWith('\\r') ? line.slice(0, -1) : line\n if (normalized.trim()) {\n lines.push(normalized)\n }\n }\n }\n\n const finish = () => {\n enqueueDelta()\n // Tolerate a transient status === 0 (matches enqueueDelta): a real non-2xx\n // is an error, but status 0 here is not — treat the trailing buffer as a\n // truncation check instead of fabricating a bogus \"status: 0\" error.\n if (xhr.status !== 0 && (xhr.status < 200 || xhr.status >= 300)) {\n error = errorFromXhrStatus(xhr)\n } else if (buffer.trim() && !aborted) {\n error = new StreamTruncatedError()\n }\n done = true\n wake()\n }\n\n xhr.onprogress = () => {\n enqueueDelta()\n wake()\n }\n xhr.onload = finish\n xhr.onerror = () => {\n // Surface as StreamReadError so a durable (id-tagged) run whose socket\n // drops mid-stream is eligible for auto-reconnect, matching the fetch path.\n // A non-durable run has no offset, so resumableStream rethrows it as-is.\n error = new StreamReadError(new Error('XHR request failed'))\n done = true\n wake()\n }\n xhr.onabort = () => {\n aborted = true\n done = true\n wake()\n }\n xhr.onloadend = () => {\n if (!done) {\n finish()\n }\n }\n\n if (abortSignal) {\n onAbort = () => {\n aborted = true\n xhr.abort()\n }\n if (abortSignal.aborted) {\n onAbort()\n } else {\n abortSignal.addEventListener('abort', onAbort, { once: true })\n }\n }\n\n return (async function* () {\n try {\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n while (true) {\n const line = lines.shift()\n if (line !== undefined) {\n yield line\n continue\n }\n\n if (error) {\n throw error\n }\n\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n if (done || abortSignal?.aborted) {\n return\n }\n\n await new Promise<void>((resolve) => {\n waiters.push(resolve)\n })\n }\n } finally {\n cleanupXhr(xhr, abortSignal, onAbort)\n }\n })()\n}\n\ninterface ConfiguredXhrRequest {\n xhr: XMLHttpRequest\n body: string\n}\n\nfunction createConfiguredXhrRequest(\n url: string,\n options: XhrConnectionOptions,\n messages: Array<UIMessage> | Array<ModelMessage>,\n data: Record<string, any> | undefined,\n runContext: RunAgentInputContext | undefined,\n method: string = 'POST',\n extraHeaders: Record<string, string> = {},\n): ConfiguredXhrRequest {\n const xhr = options.xhrFactory?.() ?? createDefaultXMLHttpRequest()\n xhr.open(method, url)\n if (options.withCredentials !== undefined) {\n xhr.withCredentials = options.withCredentials\n }\n\n const requestHeaders: Record<string, string> = {\n 'Content-Type': 'application/json',\n ...mergeHeaders(options.headers),\n ...mergeHeaders(method === 'POST' ? runContext?.headers : undefined),\n // Client-chosen run id for durability (POST only; the GET join carries it\n // in the query instead).\n ...(method === 'POST' ? runIdHeader(runContext?.runId) : {}),\n // Reconnect offset (`Last-Event-ID`) wins over static headers.\n ...extraHeaders,\n }\n\n for (const [name, value] of Object.entries(requestHeaders)) {\n xhr.setRequestHeader(name, value)\n }\n\n const requestBody = buildRunAgentInputBody(\n messages,\n data,\n runContext,\n options,\n )\n\n return { xhr, body: JSON.stringify(requestBody) }\n}\n\nasync function resolveXhrConnectionOptions(\n options: XhrConnectionOptionsResolver,\n): Promise<XhrConnectionOptions> {\n return typeof options === 'function' ? await options() : options\n}\n\n/**\n * Build an XHR-backed {@link StreamEventSource}. `parseLines` decodes the raw\n * newline-delimited body into events (SSE or NDJSON); the reconnect engine is\n * shared with the fetch adapters. A fresh XHR is opened per attempt, so a\n * `Last-Event-ID` reconnect header (via `extraHeaders`) is applied at open time.\n */\nfunction xhrEventSource(\n url: string,\n options: XhrConnectionOptions,\n method: string,\n messages: Array<UIMessage> | Array<ModelMessage>,\n data: Record<string, any> | undefined,\n runContext: RunAgentInputContext | undefined,\n parseLines: (lines: AsyncIterable<string>) => AsyncIterable<StreamEvent>,\n): StreamEventSource {\n return async function* (extraHeaders, abortSignal) {\n const request = createConfiguredXhrRequest(\n url,\n options,\n messages,\n data,\n runContext,\n method,\n extraHeaders,\n )\n const lines = readXhrLines(request.xhr, abortSignal)\n if (abortSignal?.aborted) {\n await lines.next()\n return\n }\n // A read-only join is a bodyless GET; a run POSTs the RunAgentInput payload.\n request.xhr.send(method === 'GET' ? null : request.body)\n try {\n yield* parseLines(lines)\n } finally {\n // Tear the socket down on an early exit (terminal reached or reconnect\n // break) so late bytes stop downloading. When the abort signal fired,\n // `readXhrLines` already aborted — skip here to avoid a double abort().\n if (!abortSignal?.aborted) request.xhr.abort()\n }\n }\n}\n\n/** SSE line parser bound to the run's ids for a `[DONE]` fallback. */\nfunction xhrSSEParser(runContext: RunAgentInputContext | undefined) {\n const fallbackIds: { threadId?: string; runId?: string } = {\n ...(runContext?.threadId !== undefined\n ? { threadId: runContext.threadId }\n : {}),\n ...(runContext?.runId !== undefined ? { runId: runContext.runId } : {}),\n }\n return (lines: AsyncIterable<string>) => linesToSSEEvents(lines, fallbackIds)\n}\n\n/**\n * Create an XMLHttpRequest-backed Server-Sent Events connection adapter.\n *\n * Resumable: against a durable (`id:`-tagged) server response, a dropped socket\n * auto-reconnects with `Last-Event-ID` and de-dupes the replayed prefix, and\n * `joinRun` attaches to an existing run from the start. A non-durable response\n * is a single plain request, exactly as before.\n */\nexport function xhrServerSentEvents(\n url: string | (() => string),\n options: XhrConnectionOptionsResolver = {},\n): ResumableConnectConnectionAdapter {\n return {\n async *connect(messages, data, abortSignal, runContext) {\n const resolvedUrl = typeof url === 'function' ? url() : url\n const resolvedOptions = await resolveXhrConnectionOptions(options)\n const signal = abortSignal || resolvedOptions.signal\n // POST URL is byte-identical to a plain request; the run id (when set)\n // rides in the X-Run-Id header so durability can key the log by it\n // without changing the request URL existing clients rely on.\n const requestUrl = resolvedUrl\n yield* resumableStream(\n xhrEventSource(\n requestUrl,\n resolvedOptions,\n 'POST',\n messages,\n data,\n runContext,\n xhrSSEParser(runContext),\n ),\n signal,\n resolvedOptions.reconnect,\n )\n },\n async *joinRun(runId, abortSignal) {\n const resolvedUrl = typeof url === 'function' ? url() : url\n const resolvedOptions = await resolveXhrConnectionOptions(options)\n const signal = abortSignal || resolvedOptions.signal\n const joinUrl = withSearchParams(resolvedUrl, { offset: '-1', runId })\n yield* resumableStream(\n xhrEventSource(\n joinUrl,\n resolvedOptions,\n 'GET',\n [],\n undefined,\n undefined,\n // A `[DONE]` during a join correlates to the joined run id (parity\n // with fetchServerSentEvents.joinRun).\n (lines) => linesToSSEEvents(lines, { runId }),\n ),\n signal,\n resolvedOptions.reconnect,\n )\n },\n async hydrate(threadId) {\n const resolvedUrl = typeof url === 'function' ? url() : url\n const resolvedOptions = await resolveXhrConnectionOptions(options)\n // Hydration is a non-streaming JSON GET, so fetch is fine even for the\n // XHR-backed streaming adapter.\n return fetchThreadHydration(\n fetch,\n resolvedUrl,\n mergeHeaders(resolvedOptions.headers),\n resolvedOptions.withCredentials ? 'include' : 'same-origin',\n threadId,\n )\n },\n async hydrateGeneration(threadId) {\n const resolvedUrl = typeof url === 'function' ? url() : url\n const resolvedOptions = await resolveXhrConnectionOptions(options)\n // Hydration is a non-streaming JSON GET, so fetch is fine even for the\n // XHR-backed streaming adapter.\n return fetchGenerationHydration(\n fetch,\n resolvedUrl,\n mergeHeaders(resolvedOptions.headers),\n resolvedOptions.withCredentials ? 'include' : 'same-origin',\n threadId,\n )\n },\n }\n}\n\n/**\n * Create an XMLHttpRequest-backed newline-delimited JSON stream adapter.\n *\n * Resumable: against a durable (envelope-tagged) server response, a dropped\n * socket auto-reconnects with `Last-Event-ID` and de-dupes the replayed prefix,\n * and `joinRun` attaches to an existing run from the start. A non-durable\n * (bare-line) response is a single plain request, exactly as before.\n */\nexport function xhrHttpStream(\n url: string | (() => string),\n options: XhrConnectionOptionsResolver = {},\n): ResumableConnectConnectionAdapter {\n return {\n async *connect(messages, data, abortSignal, runContext) {\n const resolvedUrl = typeof url === 'function' ? url() : url\n const resolvedOptions = await resolveXhrConnectionOptions(options)\n const signal = abortSignal || resolvedOptions.signal\n // POST URL is byte-identical to a plain request; the run id (when set)\n // rides in the X-Run-Id header so durability can key the log by it\n // without changing the request URL existing clients rely on.\n const requestUrl = resolvedUrl\n yield* resumableStream(\n xhrEventSource(\n requestUrl,\n resolvedOptions,\n 'POST',\n messages,\n data,\n runContext,\n linesToNdjsonEvents,\n ),\n signal,\n resolvedOptions.reconnect,\n )\n },\n async *joinRun(runId, abortSignal) {\n const resolvedUrl = typeof url === 'function' ? url() : url\n const resolvedOptions = await resolveXhrConnectionOptions(options)\n const signal = abortSignal || resolvedOptions.signal\n const joinUrl = withSearchParams(resolvedUrl, { offset: '-1', runId })\n yield* resumableStream(\n xhrEventSource(\n joinUrl,\n resolvedOptions,\n 'GET',\n [],\n undefined,\n undefined,\n linesToNdjsonEvents,\n ),\n signal,\n resolvedOptions.reconnect,\n )\n },\n async hydrate(threadId) {\n const resolvedUrl = typeof url === 'function' ? url() : url\n const resolvedOptions = await resolveXhrConnectionOptions(options)\n // Hydration is a non-streaming JSON GET, so fetch is fine even for the\n // XHR-backed streaming adapter.\n return fetchThreadHydration(\n fetch,\n resolvedUrl,\n mergeHeaders(resolvedOptions.headers),\n resolvedOptions.withCredentials ? 'include' : 'same-origin',\n threadId,\n )\n },\n async hydrateGeneration(threadId) {\n const resolvedUrl = typeof url === 'function' ? url() : url\n const resolvedOptions = await resolveXhrConnectionOptions(options)\n // Hydration is a non-streaming JSON GET, so fetch is fine even for the\n // XHR-backed streaming adapter.\n return fetchGenerationHydration(\n fetch,\n resolvedUrl,\n mergeHeaders(resolvedOptions.headers),\n resolvedOptions.withCredentials ? 'include' : 'same-origin',\n threadId,\n )\n },\n }\n}\n\nexport interface WebSocketConnectionOptions {\n protocols?: string | Array<string>\n body?: Record<string, unknown>\n reconnect?: ReconnectOptions\n /** Override the WebSocket implementation (tests / non-browser runtimes). */\n WebSocketImpl?: typeof WebSocket\n}\n\nfunction runIdQuery(url: string, runId: string | undefined): string {\n return runId ? withSearchParams(url, { runId }) : url\n}\n\nfunction isPingFrame(parsed: unknown): boolean {\n return (\n typeof parsed === 'object' &&\n parsed !== null &&\n (parsed as { type?: unknown }).type === 'ping'\n )\n}\n\n/** A subscribe() consumer's registration: receives chunks or a fatal error. */\ninterface WebSocketChunkSink {\n push: (chunk: StreamChunk) => void\n fail: (error: unknown) => void\n}\n\n/**\n * A push→pull bridge from socket callbacks to an async iterable: chunks queue\n * until the consumer pulls, a recorded failure rejects the iterator, and\n * `end()` (or the abort signal) finishes it cleanly. Shared by `webSocket()`'s\n * `subscribe()` and `joinRun()`.\n */\nfunction createChunkPipe(\n abortSignal: AbortSignal | undefined,\n onFinally: () => void,\n): {\n push: (chunk: StreamChunk) => void\n fail: (error: unknown) => void\n end: () => void\n iterable: AsyncIterable<StreamChunk>\n} {\n const queue: Array<StreamChunk> = []\n const waiters: Array<(c: StreamChunk | null) => void> = []\n let failure: unknown\n let ended = false\n const wake = () => waiters.shift()?.(null)\n const push = (chunk: StreamChunk) => {\n const w = waiters.shift()\n if (w) w(chunk)\n else queue.push(chunk)\n }\n const fail = (error: unknown) => {\n failure = error\n wake()\n }\n const end = () => {\n ended = true\n wake()\n }\n const onAbort = () => wake()\n abortSignal?.addEventListener('abort', onAbort)\n const iterable = (async function* () {\n try {\n while (!abortSignal?.aborted) {\n // Drain buffered chunks before ever awaiting a new promise — a\n // fatal drop that lands while chunks are still queued (fail()\n // finds no pending waiter, since the consumer hasn't caught up\n // to its buffer yet) must not be lost.\n const buffered = queue.shift()\n if (buffered !== undefined) {\n yield buffered\n continue\n }\n // Buffer exhausted: surface a failure recorded while we were\n // draining, rather than awaiting a promise that will never\n // resolve (the connection is dead — no future push/fail).\n if (failure !== undefined) throw failure\n if (ended) return\n const chunk = await new Promise<StreamChunk | null>((r) =>\n waiters.push(r),\n )\n // The wait resolved because fail() woke us — surface the error\n // instead of treating the null sentinel as a clean end. TS narrows\n // `failure` to `undefined` from the check above and doesn't know\n // the `fail()` closure can reassign it while we were awaiting —\n // this check is very much still reachable.\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n if (failure !== undefined) throw failure\n if (chunk === null) return\n yield chunk\n }\n } finally {\n abortSignal?.removeEventListener('abort', onAbort)\n onFinally()\n }\n })()\n return { push, fail, end, iterable }\n}\n\n/**\n * The send()-driven run currently owning auto-reconnect for a `webSocket()`\n * connection: reconnect is scoped to the run `send()` is driving, so a drop\n * with no established run is surfaced to subscribers rather than auto-resumed.\n */\ninterface WebSocketRunSession {\n runId: string | undefined\n readonly tracker: ReconnectTracker\n sawTerminal: boolean\n /** Made forward progress (a new, non-duplicate chunk) since the last (re)connect. */\n progressed: boolean\n signal: AbortSignal | undefined\n}\n\n/**\n * Full-duplex, conversation-scoped WebSocket connection adapter. Pairs with the\n * server `toWebSocketResponse` / `toWebSocketStream`. `send()` writes a\n * RunAgentInput frame; `subscribe()` yields inbound chunks.\n *\n * Resumable: `send()` establishes a run session backed by a\n * {@link createReconnectTracker}. If the socket closes before a terminal\n * (`RUN_FINISHED`/`RUN_ERROR`) chunk is seen and the run is durable\n * (offset-tagged `{ id, chunk }` envelopes), the socket is reopened at\n * `?runId=&offset=<lastEventId>`, de-duping the replayed boundary. A drop with\n * no offset ever observed (non-durable) surfaces {@link StreamReadError}\n * instead of reconnecting — there is nothing to resume from.\n */\nexport function webSocket(\n url: string | (() => string),\n options: WebSocketConnectionOptions = {},\n): SubscribeConnectionAdapter & {\n joinRun: (\n runId: string,\n abortSignal?: AbortSignal,\n ) => AsyncIterable<StreamChunk>\n} {\n const Impl = options.WebSocketImpl ?? WebSocket\n let socket: WebSocket | undefined\n // Whether the current socket is the conversation socket ('run') or a\n // read-only replay connection opened by a reconnect ('resume'). Only the\n // conversation socket accepts run frames server-side.\n let socketMode: 'run' | 'resume' | undefined\n // Memoized per-socket open promise. `openOnce` sets `onopen`/`onerror`\n // exactly ONCE, at socket-creation time, and stores the resulting promise\n // here. Without this, `waitOpen` assigning `onopen`/`onerror` on every call\n // would clobber a still-pending prior caller's handlers: `openOnce` reuses\n // the same in-flight socket for concurrent callers (`readyState <= 1`), so a\n // second `send()` issued before the handshake completes would overwrite the\n // first call's handlers and leave its promise permanently unresolved.\n let openPromise: Promise<void> | undefined\n const listeners = new Set<WebSocketChunkSink>()\n let currentSession: WebSocketRunSession | undefined\n\n function failAll(error: unknown): void {\n for (const l of listeners) l.fail(error)\n }\n\n function openOnce(target: string, mode: 'run' | 'resume'): WebSocket {\n // Only the conversation socket is reused — it multiplexes many turns. A\n // 'resume' handshake carries ?offset and must reach the server as its own\n // connection (reusing any open socket would discard that query, so no\n // replay would ever be requested), and a run frame must never be written\n // to a read-only resume socket (the server registers no message listener\n // there, so the frame would be silently ignored).\n if (\n socket &&\n socket.readyState <= 1 &&\n mode === 'run' &&\n socketMode === 'run'\n ) {\n return socket\n }\n const prior = socket\n const ws = options.protocols\n ? new Impl(target, options.protocols)\n : new Impl(target)\n socket = ws\n socketMode = mode\n openPromise = new Promise<void>((resolve, reject) => {\n ws.onopen = () => resolve()\n ws.onerror = (e) => reject(new StreamReadError(e))\n })\n // Attach a no-op handler so a socket nobody awaits can't raise an\n // unhandled rejection if it errors. Awaiters of openPromise still see the rejection.\n openPromise.catch(() => {})\n ws.onmessage = (event: MessageEvent) => {\n // A retired socket (a newer connection took over below) must not keep\n // feeding the shared listeners.\n if (ws !== socket) return\n let parsed: unknown\n try {\n parsed = JSON.parse(String(event.data))\n } catch (error) {\n failAll(new StreamReadError(error))\n return\n }\n if (isPingFrame(parsed)) return\n const envelopeId = isNdjsonEnvelope(parsed) ? parsed.id : undefined\n const chunk = restoreInboundUsage(\n isNdjsonEnvelope(parsed) ? parsed.chunk : (parsed as StreamChunk),\n )\n\n // Thread durable chunks through the active run session's tracker (if\n // any) so a later reconnect knows the last offset and can skip a\n // replayed boundary. A socket with no active session dispatches chunks\n // as-is.\n const session = currentSession\n if (session) {\n if (session.tracker.note(envelopeId) === 'duplicate') return\n session.progressed = true\n if (session.runId === undefined) {\n session.runId = getChunkRunId(chunk)\n }\n if (chunk.type === 'RUN_FINISHED' || chunk.type === 'RUN_ERROR') {\n session.sawTerminal = true\n }\n }\n for (const l of listeners) l.push(chunk)\n }\n ws.onclose = () => {\n // Retired deliberately in favor of a newer connection — not a drop.\n if (ws !== socket) return\n const session = currentSession\n if (!session) {\n // No run session (never established, or cleared by a prior failure).\n // Surface the drop so subscribers do not stay parked on a dead socket.\n failAll(new StreamReadError(new Error('WebSocket connection closed')))\n return\n }\n if (session.signal?.aborted || session.sawTerminal) return\n const lastEventId = session.tracker.lastEventId\n if (lastEventId === undefined) {\n // Non-durable run (no offset ever observed) — nothing to resume\n // from. Surface a hard failure rather than silently reconnecting\n // forever against a server that never tags its events.\n currentSession = undefined\n failAll(new StreamReadError(new Error('WebSocket connection closed')))\n return\n }\n void reconnect(session, lastEventId)\n }\n // Retire a superseded socket (e.g. a lingering resume socket when send()\n // opens the next conversation socket) so two sockets never feed the\n // shared listeners at once. Its handlers see it is no longer current and\n // ignore the close.\n if (prior && prior.readyState <= 1) prior.close()\n return ws\n }\n\n async function reconnect(\n session: WebSocketRunSession,\n offset: string,\n ): Promise<void> {\n try {\n // Bounded by the shared tracker's consecutive-no-progress ceiling —\n // mirrors resumableStream so a flapping server can't reconnect forever.\n await session.tracker.waitBeforeReconnect(\n session.progressed,\n session.signal,\n )\n } catch (error) {\n if (currentSession === session) currentSession = undefined\n failAll(error)\n return\n }\n if (session.signal?.aborted) return\n // A send() issued during the backoff supersedes this resume: a newer run\n // (or a resubmit of this one) already owns a fresh conversation socket,\n // and its turn re-delivers from the durability log — the tracker de-dupes\n // any overlap. Opening the resume socket anyway would retire that live\n // conversation socket.\n if (currentSession !== session) return\n if (socket && socket.readyState <= 1) return\n session.progressed = false\n const base = typeof url === 'function' ? url() : url\n const target = withSearchParams(base, {\n ...(session.runId !== undefined ? { runId: session.runId } : {}),\n offset,\n })\n openOnce(target, 'resume')\n }\n\n function waitOpen(ws: WebSocket): Promise<void> {\n if (ws.readyState === 1) return Promise.resolve()\n // Concurrent callers awaiting the SAME in-flight socket share the SAME\n // memoized promise (set once in `openOnce`), so none of them clobber\n // another's onopen/onerror handler.\n return openPromise ?? Promise.resolve()\n }\n\n return {\n subscribe(abortSignal?: AbortSignal): AsyncIterable<StreamChunk> {\n const pipe = createChunkPipe(abortSignal, () => listeners.delete(sink))\n const sink: WebSocketChunkSink = { push: pipe.push, fail: pipe.fail }\n listeners.add(sink)\n return pipe.iterable\n },\n async send(messages, data, abortSignal, runContext) {\n const target = typeof url === 'function' ? url() : url\n const ws = openOnce(runIdQuery(target, runContext?.runId), 'run')\n await waitOpen(ws)\n // Establish (or continue) the run session this socket is driving, so\n // an unterminated drop can auto-resume it. A distinct runId starts a\n // fresh tracker (a new run's offsets are unrelated to the last one's);\n // the same runId reuses the tracker so a repeat send() on an\n // already-tracked run doesn't lose its de-dupe/offset state.\n if (!currentSession || currentSession.runId !== runContext?.runId) {\n currentSession = {\n runId: runContext?.runId,\n tracker: createReconnectTracker(options.reconnect),\n sawTerminal: false,\n progressed: false,\n signal: abortSignal,\n }\n } else {\n // Same-runId resubmit (e.g. a client-tool continuation): keep the\n // tracker, but this is a NEW turn — with the previous turn's\n // `sawTerminal` left set, a drop during the resubmitted turn would\n // neither reconnect nor surface an error.\n currentSession.signal = abortSignal\n currentSession.sawTerminal = false\n currentSession.progressed = false\n }\n const session = currentSession\n // stop() must reach the server: the conversation socket outlives the\n // turn, so without an abort frame the model keeps generating (and\n // billing) server-side. The frame aborts only this run's turn.\n abortSignal?.addEventListener(\n 'abort',\n () => {\n const abortRunId = session.runId\n const live = socket\n if (\n abortRunId === undefined ||\n session.sawTerminal ||\n socketMode !== 'run' ||\n live === undefined ||\n live.readyState !== 1\n ) {\n return\n }\n try {\n live.send(JSON.stringify({ type: 'abort', runId: abortRunId }))\n } catch {\n // Socket is CLOSING/CLOSED — the server aborts the turn on close.\n }\n },\n { once: true },\n )\n const body = buildRunAgentInputBody(messages, data, runContext, {\n body: options.body,\n })\n ws.send(JSON.stringify(body))\n },\n joinRun(runId, abortSignal): AsyncIterable<StreamChunk> {\n const target = withSearchParams(typeof url === 'function' ? url() : url, {\n offset: '-1',\n runId,\n })\n // A replay handshake must reach the server as its own connection:\n // reusing the conversation socket would discard the ?offset query (no\n // replay ever requested), and the conversation socket must not be\n // replaced by a read-only replay socket. So joinRun owns a dedicated\n // socket and never touches the shared socket or run session.\n const ws = options.protocols\n ? new Impl(target, options.protocols)\n : new Impl(target)\n const pipe = createChunkPipe(abortSignal, () => {\n if (ws.readyState <= 1) ws.close()\n })\n ws.onmessage = (event: MessageEvent) => {\n let parsed: unknown\n try {\n parsed = JSON.parse(String(event.data))\n } catch (error) {\n pipe.fail(new StreamReadError(error))\n return\n }\n if (isPingFrame(parsed)) return\n pipe.push(\n restoreInboundUsage(\n isNdjsonEnvelope(parsed) ? parsed.chunk : (parsed as StreamChunk),\n ),\n )\n }\n ws.onclose = (event?: CloseEvent) => {\n // 1000 = the server finished replaying the log and closed cleanly.\n // Anything else is a drop or a policy refusal (e.g. 1008 \"no resume\n // offset\") and must surface — a joinRun socket never auto-reconnects.\n if (event?.code === 1000) {\n pipe.end()\n return\n }\n const detail = event\n ? `${event.code}${event.reason ? `: ${event.reason}` : ''}`\n : 'unknown'\n pipe.fail(\n new StreamReadError(\n new Error(`WebSocket connection closed (${detail})`),\n ),\n )\n }\n return pipe.iterable\n },\n }\n}\n\n/**\n * Optional persistence handlers for the lightweight adapters (`stream()`,\n * `rpcStream()`). These are one-shot, request-scoped calls with no built-in\n * GET endpoint or second channel, so hydration and run-rejoin only exist if\n * the app supplies them — typically thin wrappers over TanStack Start server\n * functions backed by `@tanstack/ai-persistence` (`getGenerationHydration`)\n * and a delivery-durability log (`memoryStream` / `replayRunStream`).\n *\n * Each handler is spread onto the returned adapter only when defined, so\n * feature detection (`connection.hydrateGeneration` etc.) keeps working.\n */\nexport interface StreamConnectionHandlers {\n /**\n * Server-driven chat hydration for `persistence: true`: the stored\n * transcript for `threadId` plus a cursor to an in-flight run.\n */\n hydrate?: (threadId: string) => Promise<ChatHydrationResult>\n /**\n * Server-driven generation hydration for `persistence: true`: the last\n * generation's resume snapshot for `threadId` plus a cursor to a run still\n * generating. See {@link ConnectConnectionAdapter.hydrateGeneration}.\n */\n hydrateGeneration?: (threadId: string) => Promise<GenerationHydrationResult>\n /**\n * Re-attach to a run still generating and replay it from the start. See\n * {@link ConnectConnectionAdapter.joinRun}.\n */\n joinRun?: (\n runId: string,\n abortSignal?: AbortSignal,\n ) => AsyncIterable<StreamChunk>\n}\n\n/**\n * Create a direct stream connection adapter (for server functions or direct streams)\n *\n * @param streamFactory - A function that returns an async iterable of StreamChunks\n * @param handlers - Optional persistence handlers (`hydrate`,\n * `hydrateGeneration`, `joinRun`) that let server-driven persistence work\n * without an HTTP endpoint — each is usually a one-line server-function call\n * @returns A connection adapter for direct streams\n *\n * @example\n * ```typescript\n * // With TanStack Start server function\n * const connection = stream(() => serverFunction({ messages }));\n *\n * const client = new ChatClient({ connection });\n *\n * // With generation persistence over server functions\n * const connection = stream(\n * () => generateImageFn({ data: input }),\n * {\n * hydrateGeneration: (threadId) => getImageHydrationFn({ data: threadId }),\n * joinRun: (runId) => joinImageRunFn({ data: runId }),\n * },\n * );\n * ```\n */\nexport function stream(\n streamFactory: (\n messages: Array<UIMessage> | Array<ModelMessage>,\n data?: Record<string, any>,\n abortSignal?: AbortSignal,\n ) => AsyncIterable<StreamChunk>,\n handlers?: StreamConnectionHandlers,\n): ConnectConnectionAdapter {\n return {\n async *connect(messages, data, abortSignal) {\n // Pass messages as-is (UIMessages with parts preserved)\n // Server-side chat() handles conversion to ModelMessages\n yield* streamFactory(messages, data, abortSignal)\n },\n ...(handlers?.hydrate ? { hydrate: handlers.hydrate } : {}),\n ...(handlers?.hydrateGeneration\n ? { hydrateGeneration: handlers.hydrateGeneration }\n : {}),\n ...(handlers?.joinRun ? { joinRun: handlers.joinRun } : {}),\n }\n}\n\n/**\n * Wrap a `ChatFetcher` as a `ConnectConnectionAdapter` so the chat client can\n * consume it through the same `subscribe`/`send` plumbing used for SSE /\n * HTTP-stream / RPC connections. May return either a `Response` (parsed as\n * SSE) or an `AsyncIterable<StreamChunk>` (yielded directly).\n *\n * @internal\n */\nexport function fetcherToConnectionAdapter(\n fetcher: ChatFetcher,\n): ConnectConnectionAdapter {\n return {\n async *connect(messages, data, abortSignal, runContext) {\n if (!abortSignal) {\n throw new Error(\n 'fetcherToConnectionAdapter requires an AbortSignal — the chat client always supplies one.',\n )\n }\n if (!runContext) {\n throw new Error(\n 'fetcherToConnectionAdapter requires a RunAgentInputContext — the chat client always supplies one.',\n )\n }\n const uiMessages = messages as Array<UIMessage>\n const result = await fetcher(\n {\n messages: uiMessages,\n data,\n threadId: runContext.threadId,\n runId: runContext.runId,\n ...(runContext.parentRunId !== undefined\n ? { parentRunId: runContext.parentRunId }\n : {}),\n ...(runContext.resume !== undefined\n ? { resume: runContext.resume }\n : {}),\n },\n { signal: abortSignal, headers: runContext.headers },\n )\n if (result instanceof Response) {\n yield* responseToSSEChunks(result, abortSignal)\n } else {\n yield* abortableIterable(result, abortSignal)\n }\n },\n }\n}\n\n/**\n * Wrap an AsyncIterable so iteration aborts when `signal` fires. Without\n * this, a fetcher that returns a generator ignoring its signal would leave\n * the for-await loop hanging until the iterable naturally ends.\n */\nasync function* abortableIterable<T>(\n iterable: AsyncIterable<T>,\n signal: AbortSignal,\n): AsyncGenerator<T> {\n if (signal.aborted) return\n const iterator = iterable[Symbol.asyncIterator]()\n const abortPromise = new Promise<{ done: true; value: undefined }>(\n (resolve) => {\n signal.addEventListener(\n 'abort',\n () => resolve({ done: true, value: undefined }),\n { once: true },\n )\n },\n )\n try {\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n while (true) {\n const result = await Promise.race([iterator.next(), abortPromise])\n if (result.done) return\n yield result.value\n }\n } finally {\n await iterator.return?.()\n }\n}\n\n/**\n * Create an RPC stream connection adapter (for RPC-based streaming like Cap'n Web RPC)\n *\n * @param rpcCall - A function that accepts messages and returns an async iterable of StreamChunks\n * @param handlers - Optional persistence handlers (`hydrate`,\n * `hydrateGeneration`, `joinRun`) that let server-driven persistence work\n * without an HTTP endpoint — each is usually a one-line RPC call\n * @returns A connection adapter for RPC streams\n *\n * @example\n * ```typescript\n * // With Cap'n Web RPC\n * const connection = rpcStream((messages, data) =>\n * api.streamMurfResponse(messages, data)\n * );\n *\n * const client = new ChatClient({ connection });\n *\n * // With generation persistence over RPC\n * const connection = rpcStream(\n * (messages, data) => api.streamMurfResponse(messages, data),\n * {\n * hydrateGeneration: (threadId) => api.getGenerationHydration(threadId),\n * joinRun: (runId) => api.replayRun(runId),\n * },\n * );\n * ```\n */\nexport function rpcStream(\n rpcCall: (\n messages: Array<UIMessage> | Array<ModelMessage>,\n data?: Record<string, any>,\n abortSignal?: AbortSignal,\n ) => AsyncIterable<StreamChunk>,\n handlers?: StreamConnectionHandlers,\n): ConnectConnectionAdapter {\n return {\n async *connect(messages, data, abortSignal) {\n // Pass messages as-is (UIMessages with parts preserved)\n // Server-side chat() handles conversion to ModelMessages\n yield* rpcCall(messages, data, abortSignal)\n },\n ...(handlers?.hydrate ? { hydrate: handlers.hydrate } : {}),\n ...(handlers?.hydrateGeneration\n ? { hydrateGeneration: handlers.hydrateGeneration }\n : {}),\n ...(handlers?.joinRun ? { joinRun: handlers.joinRun } : {}),\n }\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/ai-client",
3
- "version": "0.31.0",
3
+ "version": "0.31.1",
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",
@@ -57,8 +57,8 @@
57
57
  "src"
58
58
  ],
59
59
  "dependencies": {
60
- "@tanstack/ai": "^0.53.0",
61
- "@tanstack/ai-event-client": "^0.11.2",
60
+ "@tanstack/ai": "^0.54.0",
61
+ "@tanstack/ai-event-client": "^0.11.3",
62
62
  "@tanstack/ai-utils": "^0.4.0"
63
63
  },
64
64
  "devDependencies": {
@@ -83,6 +83,22 @@ export function isPasskeyStorageSupported(): boolean {
83
83
  export async function deriveAesKey(
84
84
  prfOutput: BufferSource,
85
85
  ): Promise<CryptoKey> {
86
+ // Some passkey providers return a plain byte array instead of BufferSource.
87
+ if (Array.isArray(prfOutput)) {
88
+ if (
89
+ prfOutput.length !== 32 ||
90
+ !Array.from(prfOutput).every(
91
+ (byte: unknown) =>
92
+ typeof byte === 'number' &&
93
+ Number.isInteger(byte) &&
94
+ byte >= 0 &&
95
+ byte <= 255,
96
+ )
97
+ ) {
98
+ throw new Error('Invalid passkey PRF byte array')
99
+ }
100
+ prfOutput = new Uint8Array(prfOutput)
101
+ }
86
102
  const base = await crypto.subtle.importKey('raw', prfOutput, 'HKDF', false, [
87
103
  'deriveKey',
88
104
  ])
@@ -196,6 +212,25 @@ function requirePublicKeyCredential(
196
212
  return credential
197
213
  }
198
214
 
215
+ /**
216
+ * Some browsers (Dia, Safari) silently suppress `navigator.credentials.get()`
217
+ * — no prompt, never resolves — when it runs without transient user activation.
218
+ * The activation from a click expires (~5s) and is lost across enough async
219
+ * work, so an unlock buried deep in a send pipeline just hangs. Fail fast with
220
+ * a clear, catchable error so the app can re-run it from a fresh gesture.
221
+ */
222
+ function requireUserActivation(action: string): void {
223
+ // ponytail: only enforce where the API exists; absent means "can't tell", so
224
+ // fall through rather than block a ceremony that would have worked.
225
+ const activation = globalThis.navigator?.userActivation
226
+ if (activation && activation.isActive === false) {
227
+ throw new Error(
228
+ `Passkey ${action} needs a fresh user action (e.g. a click). Run it ` +
229
+ 'directly from the event handler, before awaiting other work.',
230
+ )
231
+ }
232
+ }
233
+
199
234
  async function registerPasskey(
200
235
  rpName: string,
201
236
  userName: string,
@@ -316,6 +351,7 @@ export function passkeyStorage(
316
351
  }
317
352
  const existing = await idbGet(dbName)
318
353
  if (existing) {
354
+ requireUserActivation('unlock')
319
355
  const prf = await evaluatePrf(existing.credentialId, existing.salt)
320
356
  cachedKey = await deriveAesKey(prf)
321
357
  cachedMeta = {
@@ -324,6 +360,8 @@ export function passkeyStorage(
324
360
  }
325
361
  } else {
326
362
  const reg = await registerPasskey(rpName, userName, rpId)
363
+ // Registration can consume activation. Let the browser handle its
364
+ // follow-up PRF ceremony rather than rejecting a valid new-key save.
327
365
  const prf = reg.prf ?? (await evaluatePrf(reg.credentialId, reg.salt))
328
366
  cachedKey = await deriveAesKey(prf)
329
367
  cachedMeta = { credentialId: reg.credentialId, salt: reg.salt }
@@ -1042,6 +1042,7 @@ export function normalizeConnectionAdapter(
1042
1042
  // Legacy connect() wrapper
1043
1043
  let activeBuffer: Array<StreamChunk> = []
1044
1044
  let activeWaiters: Array<(chunk: StreamChunk | null) => void> = []
1045
+ let activeSubscriber: typeof activeWaiters | undefined
1045
1046
 
1046
1047
  function push(chunk: StreamChunk, runId?: string): void {
1047
1048
  if (runId) {
@@ -1062,18 +1063,16 @@ export function normalizeConnectionAdapter(
1062
1063
  // previous chunk has left processIncomingChunk. Empty waiters with an
1063
1064
  // empty buffer is in-flight delivery, not idle.
1064
1065
  const idle = () =>
1065
- activeBuffer.length === 0 &&
1066
- (activeWaiters.length > 0 || abortSignal?.aborted)
1066
+ activeSubscriber !== activeWaiters ||
1067
+ (activeBuffer.length === 0 &&
1068
+ (activeWaiters.length > 0 || abortSignal?.aborted))
1067
1069
  for (let i = 0; i < 16 && !abortSignal?.aborted; i++) {
1068
1070
  if (idle()) return
1069
1071
  await Promise.resolve()
1070
1072
  }
1071
- let macrotaskWaits = 0
1072
1073
  while (!abortSignal?.aborted) {
1073
1074
  if (idle()) return
1074
1075
  await new Promise<void>((resolve) => setTimeout(resolve, 0))
1075
- macrotaskWaits++
1076
- if (activeWaiters.length === 0 && macrotaskWaits >= 32) return
1077
1076
  }
1078
1077
  }
1079
1078
 
@@ -1087,22 +1086,27 @@ export function normalizeConnectionAdapter(
1087
1086
  activeWaiters = myWaiters
1088
1087
 
1089
1088
  return (async function* () {
1090
- while (!abortSignal?.aborted) {
1091
- let chunk: StreamChunk | null
1092
- const buffered = myBuffer.shift()
1093
- if (buffered !== undefined) {
1094
- chunk = buffered
1095
- } else {
1096
- chunk = await new Promise<StreamChunk | null>((resolve) => {
1097
- const onAbort = () => resolve(null)
1098
- myWaiters.push((c) => {
1099
- abortSignal?.removeEventListener('abort', onAbort)
1100
- resolve(c)
1089
+ activeSubscriber = myWaiters
1090
+ try {
1091
+ while (!abortSignal?.aborted) {
1092
+ let chunk: StreamChunk | null
1093
+ const buffered = myBuffer.shift()
1094
+ if (buffered !== undefined) {
1095
+ chunk = buffered
1096
+ } else {
1097
+ chunk = await new Promise<StreamChunk | null>((resolve) => {
1098
+ const onAbort = () => resolve(null)
1099
+ myWaiters.push((c) => {
1100
+ abortSignal?.removeEventListener('abort', onAbort)
1101
+ resolve(c)
1102
+ })
1103
+ abortSignal?.addEventListener('abort', onAbort, { once: true })
1101
1104
  })
1102
- abortSignal?.addEventListener('abort', onAbort, { once: true })
1103
- })
1105
+ }
1106
+ if (chunk !== null) yield chunk
1104
1107
  }
1105
- if (chunk !== null) yield chunk
1108
+ } finally {
1109
+ if (activeSubscriber === myWaiters) activeSubscriber = undefined
1106
1110
  }
1107
1111
  })()
1108
1112
  },