@tanstack/ai-client 0.26.0 → 0.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/esm/byok/client.d.ts +52 -0
- package/dist/esm/byok/client.js +300 -0
- package/dist/esm/byok/client.js.map +1 -0
- package/dist/esm/byok/passkey.d.ts +43 -0
- package/dist/esm/byok/passkey.js +251 -0
- package/dist/esm/byok/passkey.js.map +1 -0
- package/dist/esm/byok/resolve.d.ts +8 -0
- package/dist/esm/byok/resolve.js +20 -0
- package/dist/esm/byok/resolve.js.map +1 -0
- package/dist/esm/byok/storage.d.ts +15 -0
- package/dist/esm/byok/storage.js +16 -0
- package/dist/esm/byok/storage.js.map +1 -0
- package/dist/esm/byok.d.ts +8 -0
- package/dist/esm/byok.js +5 -0
- package/dist/esm/chat-client.d.ts +19 -3
- package/dist/esm/chat-client.js +82 -42
- package/dist/esm/chat-client.js.map +1 -1
- package/dist/esm/connection-adapters.d.ts +2 -0
- package/dist/esm/connection-adapters.js +35 -10
- package/dist/esm/connection-adapters.js.map +1 -1
- package/dist/esm/generation-client.d.ts +3 -1
- package/dist/esm/generation-client.js +23 -4
- package/dist/esm/generation-client.js.map +1 -1
- package/dist/esm/generation-types.d.ts +14 -0
- package/dist/esm/generation-types.js.map +1 -1
- package/dist/esm/interrupt-manager.d.ts +1 -0
- package/dist/esm/interrupt-manager.js +15 -6
- package/dist/esm/interrupt-manager.js.map +1 -1
- package/dist/esm/message-date-normalizer.d.ts +3 -0
- package/dist/esm/message-date-normalizer.js +35 -0
- package/dist/esm/message-date-normalizer.js.map +1 -0
- package/dist/esm/sse-parser.js +8 -1
- package/dist/esm/sse-parser.js.map +1 -1
- package/dist/esm/storage-adapters.js +13 -1
- package/dist/esm/storage-adapters.js.map +1 -1
- package/dist/esm/types.d.ts +34 -3
- package/dist/esm/types.js.map +1 -1
- package/dist/esm/video-generation-client.d.ts +3 -1
- package/dist/esm/video-generation-client.js +25 -6
- package/dist/esm/video-generation-client.js.map +1 -1
- package/package.json +7 -3
- package/src/byok/client.ts +360 -0
- package/src/byok/passkey.ts +399 -0
- package/src/byok/resolve.ts +30 -0
- package/src/byok/storage.ts +29 -0
- package/src/byok.ts +20 -0
- package/src/chat-client.ts +165 -72
- package/src/connection-adapters.ts +46 -14
- package/src/generation-client.ts +48 -4
- package/src/generation-types.ts +16 -0
- package/src/interrupt-manager.ts +20 -6
- package/src/message-date-normalizer.ts +29 -0
- package/src/sse-parser.ts +10 -0
- package/src/storage-adapters.ts +13 -2
- package/src/types.ts +36 -3
- package/src/video-generation-client.ts +45 -4
|
@@ -0,0 +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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"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { ProviderId } from '@tanstack/ai/byok';
|
|
2
|
+
import { ByokClient } from './client.js';
|
|
3
|
+
export declare function resolveByokProviderId(byokProvider: (() => string | undefined) | undefined, ...candidates: Array<unknown>): ProviderId | undefined;
|
|
4
|
+
/**
|
|
5
|
+
* Prepare and stamp headers for one resolved slug. Throws instead of
|
|
6
|
+
* attaching every stored key when the slug is missing.
|
|
7
|
+
*/
|
|
8
|
+
export declare function prepareResolvedByokHeaders(byok: ByokClient, provider: ProviderId | undefined): Promise<Record<string, string>>;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { ByokUnresolvedProviderError, isProviderId } from "@tanstack/ai/byok";
|
|
2
|
+
//#region src/byok/resolve.ts
|
|
3
|
+
function resolveByokProviderId(byokProvider, ...candidates) {
|
|
4
|
+
const fromFn = byokProvider?.();
|
|
5
|
+
if (isProviderId(fromFn)) return fromFn;
|
|
6
|
+
for (const candidate of candidates) if (isProviderId(candidate)) return candidate;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Prepare and stamp headers for one resolved slug. Throws instead of
|
|
10
|
+
* attaching every stored key when the slug is missing.
|
|
11
|
+
*/
|
|
12
|
+
async function prepareResolvedByokHeaders(byok, provider) {
|
|
13
|
+
if (!provider) throw new ByokUnresolvedProviderError();
|
|
14
|
+
await byok.prepare(provider);
|
|
15
|
+
return byok.headers(provider);
|
|
16
|
+
}
|
|
17
|
+
//#endregion
|
|
18
|
+
export { prepareResolvedByokHeaders, resolveByokProviderId };
|
|
19
|
+
|
|
20
|
+
//# sourceMappingURL=resolve.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolve.js","names":[],"sources":["../../../src/byok/resolve.ts"],"sourcesContent":["import { ByokUnresolvedProviderError, isProviderId } from '@tanstack/ai/byok'\nimport type { ProviderId } from '@tanstack/ai/byok'\nimport type { ByokClient } from './client'\n\nexport function resolveByokProviderId(\n byokProvider: (() => string | undefined) | undefined,\n ...candidates: Array<unknown>\n): ProviderId | undefined {\n const fromFn = byokProvider?.()\n if (isProviderId(fromFn)) return fromFn\n for (const candidate of candidates) {\n if (isProviderId(candidate)) return candidate\n }\n return undefined\n}\n\n/**\n * Prepare and stamp headers for one resolved slug. Throws instead of\n * attaching every stored key when the slug is missing.\n */\nexport async function prepareResolvedByokHeaders(\n byok: ByokClient,\n provider: ProviderId | undefined,\n): Promise<Record<string, string>> {\n if (!provider) {\n throw new ByokUnresolvedProviderError()\n }\n await byok.prepare(provider)\n return byok.headers(provider)\n}\n"],"mappings":";;AAIA,SAAgB,sBACd,cACA,GAAG,YACqB;CACxB,MAAM,SAAS,eAAe;CAC9B,IAAI,aAAa,MAAM,GAAG,OAAO;CACjC,KAAK,MAAM,aAAa,YACtB,IAAI,aAAa,SAAS,GAAG,OAAO;AAGxC;;;;;AAMA,eAAsB,2BACpB,MACA,UACiC;CACjC,IAAI,CAAC,UACH,MAAM,IAAI,4BAA4B;CAExC,MAAM,KAAK,QAAQ,QAAQ;CAC3B,OAAO,KAAK,QAAQ,QAAQ;AAC9B"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { ProviderId } from '@tanstack/ai/byok';
|
|
2
|
+
export type Keyring = Partial<Record<ProviderId, string>>;
|
|
3
|
+
export type KeyPreview = Partial<Record<ProviderId, string>>;
|
|
4
|
+
export interface KeyringStorage {
|
|
5
|
+
readonly id: string;
|
|
6
|
+
readonly label: string;
|
|
7
|
+
readonly persistent: boolean;
|
|
8
|
+
readonly unlockable?: boolean;
|
|
9
|
+
readonly warning?: string;
|
|
10
|
+
peek?: () => KeyPreview | Promise<KeyPreview>;
|
|
11
|
+
load: () => Keyring | Promise<Keyring>;
|
|
12
|
+
save: (keys: Keyring) => void | Promise<void>;
|
|
13
|
+
clear: () => void | Promise<void>;
|
|
14
|
+
}
|
|
15
|
+
export declare function memoryStorage(): KeyringStorage;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
//#region src/byok/storage.ts
|
|
2
|
+
function memoryStorage() {
|
|
3
|
+
return {
|
|
4
|
+
id: "memory",
|
|
5
|
+
label: "Session only (not saved)",
|
|
6
|
+
persistent: false,
|
|
7
|
+
warning: "Keys stay in memory for this tab only. They are not saved across reloads.",
|
|
8
|
+
load: () => ({}),
|
|
9
|
+
save: () => {},
|
|
10
|
+
clear: () => {}
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
//#endregion
|
|
14
|
+
export { memoryStorage };
|
|
15
|
+
|
|
16
|
+
//# sourceMappingURL=storage.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"storage.js","names":[],"sources":["../../../src/byok/storage.ts"],"sourcesContent":["import type { ProviderId } from '@tanstack/ai/byok'\n\nexport type Keyring = Partial<Record<ProviderId, string>>\nexport type KeyPreview = Partial<Record<ProviderId, string>>\n\nexport interface KeyringStorage {\n readonly id: string\n readonly label: string\n readonly persistent: boolean\n readonly unlockable?: boolean\n readonly warning?: string\n peek?: () => KeyPreview | Promise<KeyPreview>\n load: () => Keyring | Promise<Keyring>\n save: (keys: Keyring) => void | Promise<void>\n clear: () => void | Promise<void>\n}\n\nexport function memoryStorage(): KeyringStorage {\n return {\n id: 'memory',\n label: 'Session only (not saved)',\n persistent: false,\n warning:\n 'Keys stay in memory for this tab only. They are not saved across reloads.',\n load: () => ({}),\n save: () => {},\n clear: () => {},\n }\n}\n"],"mappings":";AAiBA,SAAgB,gBAAgC;CAC9C,OAAO;EACL,IAAI;EACJ,OAAO;EACP,YAAY;EACZ,SACE;EACF,aAAa,CAAC;EACd,YAAY,CAAC;EACb,aAAa,CAAC;CAChB;AACF"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { defineByok, ByokClient, EMPTY_BYOK_SNAPSHOT } from './byok/client.js';
|
|
2
|
+
export type { ByokPrompt, ByokSnapshot, DefineByokOptions, KeyStatus, } from './byok/client.js';
|
|
3
|
+
export { defineByokProvider } from '@tanstack/ai/byok';
|
|
4
|
+
export type { ByokProvider, ByokProviderInit } from '@tanstack/ai/byok';
|
|
5
|
+
export { memoryStorage } from './byok/storage.js';
|
|
6
|
+
export type { KeyPreview, Keyring, KeyringStorage } from './byok/storage.js';
|
|
7
|
+
export { defaultByokStorage, decryptKeyring, deriveAesKey, encryptKeyring, isPasskeyStorageSupported, passkeyStorage, } from './byok/passkey.js';
|
|
8
|
+
export type { PasskeyStorageOptions } from './byok/passkey.js';
|
package/dist/esm/byok.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { memoryStorage } from "./byok/storage.js";
|
|
2
|
+
import { ByokClient, EMPTY_BYOK_SNAPSHOT, defineByok } from "./byok/client.js";
|
|
3
|
+
import { decryptKeyring, defaultByokStorage, deriveAesKey, encryptKeyring, isPasskeyStorageSupported, passkeyStorage } from "./byok/passkey.js";
|
|
4
|
+
import { defineByokProvider } from "@tanstack/ai/byok";
|
|
5
|
+
export { ByokClient, EMPTY_BYOK_SNAPSHOT, decryptKeyring, defaultByokStorage, defineByok, defineByokProvider, deriveAesKey, encryptKeyring, isPasskeyStorageSupported, memoryStorage, passkeyStorage };
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { AnyClientTool, InterruptDefinition, ModelMessage, RunAgentResumeItem, StreamChunk } from '@tanstack/ai/client';
|
|
2
|
+
import { ByokClient } from './byok.js';
|
|
2
3
|
import { ConnectionAdapter } from './connection-adapters.js';
|
|
3
4
|
import { BoundInterrupts, ChatClientOptions, ChatClientState, ChatFetcher, ChatInterruptState, ResolvableChatInterrupt, ChatResumeState, ConnectionStatus, MultimodalContent, QueueOption, QueueStrategy, QueuedMessage, SendMessageOptions, UIMessage, WhenBusy } from './types.js';
|
|
4
5
|
type ChatClientUpdateOptionsWithoutContext<TTools extends ReadonlyArray<AnyClientTool>, TInterrupts extends ReadonlyArray<InterruptDefinition<any, any, any, any>> = readonly []> = {
|
|
@@ -7,6 +8,8 @@ type ChatClientUpdateOptionsWithoutContext<TTools extends ReadonlyArray<AnyClien
|
|
|
7
8
|
/** @deprecated Use `forwardedProps` instead. */
|
|
8
9
|
body?: Record<string, any>;
|
|
9
10
|
forwardedProps?: Record<string, any>;
|
|
11
|
+
byok?: ByokClient;
|
|
12
|
+
byokProvider?: () => string | undefined;
|
|
10
13
|
tools?: TTools;
|
|
11
14
|
interrupts?: TInterrupts;
|
|
12
15
|
queue?: QueueOption;
|
|
@@ -67,6 +70,8 @@ export declare class ChatClient<TTools extends ReadonlyArray<AnyClientTool> = an
|
|
|
67
70
|
private activeResumeRunId;
|
|
68
71
|
private bodyOption;
|
|
69
72
|
private forwardedPropsOption;
|
|
73
|
+
private byok;
|
|
74
|
+
private byokProvider;
|
|
70
75
|
private context;
|
|
71
76
|
private pendingMessageBody;
|
|
72
77
|
private queueConfig;
|
|
@@ -120,6 +125,8 @@ export declare class ChatClient<TTools extends ReadonlyArray<AnyClientTool> = an
|
|
|
120
125
|
private processingResolve;
|
|
121
126
|
private errorReportedGeneration;
|
|
122
127
|
private streamGeneration;
|
|
128
|
+
private continuationGeneration;
|
|
129
|
+
private streamContinuationGeneration;
|
|
123
130
|
private continuationSkipped;
|
|
124
131
|
private draining;
|
|
125
132
|
private sessionGenerating;
|
|
@@ -234,6 +241,7 @@ export declare class ChatClient<TTools extends ReadonlyArray<AnyClientTool> = an
|
|
|
234
241
|
retryInterrupts(): void;
|
|
235
242
|
/** Unsafe low-level resume escape hatch. Prefer bound interrupt methods. */
|
|
236
243
|
resumeInterruptsUnsafe(resume: Array<RunAgentResumeItem>, state?: ChatResumeState): Promise<boolean>;
|
|
244
|
+
private resumeInterruptsUnsafeForGeneration;
|
|
237
245
|
/** @deprecated Use bound interrupt methods or resumeInterruptsUnsafe(). */
|
|
238
246
|
resumeInterrupts(resume: Array<RunAgentResumeItem>, state?: ChatResumeState): Promise<boolean>;
|
|
239
247
|
private submitInterruptBatch;
|
|
@@ -320,8 +328,12 @@ export declare class ChatClient<TTools extends ReadonlyArray<AnyClientTool> = an
|
|
|
320
328
|
* - A MultimodalContent object with content array and optional custom ID
|
|
321
329
|
* @param body - Optional body parameters to merge with the client's base body for this request.
|
|
322
330
|
* Uses shallow merge with per-message body taking priority.
|
|
323
|
-
* @param sendOptions - Per-call overrides
|
|
324
|
-
*
|
|
331
|
+
* @param sendOptions - Per-call overrides. `{ whenBusy }` overrides the
|
|
332
|
+
* queue policy for this one send. `{ body }`
|
|
333
|
+
* shallow-merges with `body` and with the chat-level
|
|
334
|
+
* `body` / `forwardedProps`. `sendOptions.body` wins
|
|
335
|
+
* on key collisions. Framework hooks forward this
|
|
336
|
+
* object as their second argument.
|
|
325
337
|
*
|
|
326
338
|
* @example
|
|
327
339
|
* ```ts
|
|
@@ -331,9 +343,13 @@ export declare class ChatClient<TTools extends ReadonlyArray<AnyClientTool> = an
|
|
|
331
343
|
* // Text message with custom body params
|
|
332
344
|
* await client.sendMessage('Hello!', { temperature: 0.7 })
|
|
333
345
|
*
|
|
334
|
-
* // Per-call whenBusy override
|
|
346
|
+
* // Per-call whenBusy override
|
|
335
347
|
* await client.sendMessage('Urgent', undefined, { whenBusy: 'interrupt' })
|
|
336
348
|
*
|
|
349
|
+
* // Per-call body via options. Same effect as the positional arg.
|
|
350
|
+
* // This is the shape the framework hooks (`useChat`, `injectChat`) forward.
|
|
351
|
+
* await client.sendMessage('Hello!', undefined, { body: { temperature: 0.7 } })
|
|
352
|
+
*
|
|
337
353
|
* // Multimodal message with image
|
|
338
354
|
* await client.sendMessage({
|
|
339
355
|
* content: [
|
package/dist/esm/chat-client.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
|
+
import { prepareResolvedByokHeaders, resolveByokProviderId } from "./byok/resolve.js";
|
|
1
2
|
import { createNoOpChatDevtoolsBridge } from "./devtools-noop.js";
|
|
3
|
+
import { normalizeMessagesDates } from "./message-date-normalizer.js";
|
|
2
4
|
import { fetcherToConnectionAdapter, getChunkRunId as getChunkRunId$1, normalizeConnectionAdapter } from "./connection-adapters.js";
|
|
3
5
|
import { ChatPersistor } from "./client-persistor.js";
|
|
4
6
|
import { ClearedStreamTracker } from "./cleared-stream-tracker.js";
|
|
5
7
|
import { InterruptManager } from "./interrupt-manager.js";
|
|
6
8
|
import { StreamProcessor, convertSchemaToJsonSchema, generateMessageId, isStandardSchema, mergeMetadata, normalizeToUIMessage, parseWithStandardSchema, restoreInboundChunk, tanstackMetadata } from "@tanstack/ai/client";
|
|
9
|
+
import { ByokBlockedError, ByokMissingError, ByokUnresolvedProviderError } from "@tanstack/ai/byok";
|
|
7
10
|
//#region src/chat-client.ts
|
|
8
11
|
function assertUniqueInterruptDefinitions(interrupts) {
|
|
9
12
|
const ids = /* @__PURE__ */ new Set();
|
|
@@ -148,6 +151,8 @@ var ChatClient = class {
|
|
|
148
151
|
activeResumeRunId = null;
|
|
149
152
|
bodyOption = {};
|
|
150
153
|
forwardedPropsOption = {};
|
|
154
|
+
byok;
|
|
155
|
+
byokProvider;
|
|
151
156
|
context = void 0;
|
|
152
157
|
pendingMessageBody = void 0;
|
|
153
158
|
queueConfig;
|
|
@@ -201,6 +206,8 @@ var ChatClient = class {
|
|
|
201
206
|
processingResolve = null;
|
|
202
207
|
errorReportedGeneration = null;
|
|
203
208
|
streamGeneration = 0;
|
|
209
|
+
continuationGeneration = 0;
|
|
210
|
+
streamContinuationGeneration = 0;
|
|
204
211
|
continuationSkipped = false;
|
|
205
212
|
draining = false;
|
|
206
213
|
sessionGenerating = false;
|
|
@@ -228,6 +235,8 @@ var ChatClient = class {
|
|
|
228
235
|
}
|
|
229
236
|
this.bodyOption = options.body || {};
|
|
230
237
|
this.forwardedPropsOption = options.forwardedProps || {};
|
|
238
|
+
this.byok = options.byok;
|
|
239
|
+
this.byokProvider = options.byokProvider;
|
|
231
240
|
this.context = options.context;
|
|
232
241
|
this.queueConfig = normalizeQueueOption(options.queue);
|
|
233
242
|
this.connection = normalizeConnectionAdapter(resolveTransport(options));
|
|
@@ -327,6 +336,7 @@ var ChatClient = class {
|
|
|
327
336
|
const clientTool = (this.activeClientTools ?? this.clientToolsRef.current).get(args.toolName);
|
|
328
337
|
const executeFunc = clientTool?.execute;
|
|
329
338
|
if (executeFunc) {
|
|
339
|
+
const continuationGeneration = this.continuationGeneration;
|
|
330
340
|
const runEventContext = this.devtoolsBridge.getCurrentRunEventContext();
|
|
331
341
|
const executionPromise = (async () => {
|
|
332
342
|
try {
|
|
@@ -341,7 +351,7 @@ var ChatClient = class {
|
|
|
341
351
|
tool: args.toolName,
|
|
342
352
|
output,
|
|
343
353
|
state: "output-available"
|
|
344
|
-
}, clientTool, runEventContext);
|
|
354
|
+
}, clientTool, continuationGeneration, runEventContext);
|
|
345
355
|
} catch (error) {
|
|
346
356
|
await this.addToolResultForClientTool({
|
|
347
357
|
toolCallId: args.toolCallId,
|
|
@@ -349,7 +359,7 @@ var ChatClient = class {
|
|
|
349
359
|
output: null,
|
|
350
360
|
state: "output-error",
|
|
351
361
|
errorText: error.message
|
|
352
|
-
}, clientTool, runEventContext);
|
|
362
|
+
}, clientTool, continuationGeneration, runEventContext);
|
|
353
363
|
} finally {
|
|
354
364
|
this.pendingToolExecutions.delete(args.toolCallId);
|
|
355
365
|
}
|
|
@@ -485,7 +495,7 @@ var ChatClient = class {
|
|
|
485
495
|
}
|
|
486
496
|
if (this.disposed || !this.tailing) return;
|
|
487
497
|
if (this.isLoading || this.abortController) return;
|
|
488
|
-
if (result.messages.length > 0) this.processor.setMessages(result.messages);
|
|
498
|
+
if (result.messages.length > 0) this.processor.setMessages(normalizeMessagesDates(result.messages));
|
|
489
499
|
if (result.interrupts && result.interrupts.pending.length > 0) this.applyResumeSnapshot({
|
|
490
500
|
resumeState: {
|
|
491
501
|
threadId: this.threadId,
|
|
@@ -643,10 +653,14 @@ var ChatClient = class {
|
|
|
643
653
|
resumeInterruptsUnsafe(resume, state) {
|
|
644
654
|
const target = state ?? this.lastResume;
|
|
645
655
|
if (!target) return Promise.resolve(false);
|
|
656
|
+
return this.resumeInterruptsUnsafeForGeneration(resume, target, this.continuationGeneration);
|
|
657
|
+
}
|
|
658
|
+
resumeInterruptsUnsafeForGeneration(resume, target, continuationGeneration) {
|
|
659
|
+
if (continuationGeneration !== this.continuationGeneration) return Promise.resolve(false);
|
|
646
660
|
if (this.isLoading) return new Promise((resolve, reject) => {
|
|
647
661
|
this.queuePostStreamAction(async () => {
|
|
648
662
|
try {
|
|
649
|
-
resolve(await this.
|
|
663
|
+
resolve(await this.resumeInterruptsUnsafeForGeneration(resume, target, continuationGeneration));
|
|
650
664
|
} catch (error) {
|
|
651
665
|
reject(error);
|
|
652
666
|
}
|
|
@@ -662,6 +676,7 @@ var ChatClient = class {
|
|
|
662
676
|
return this.resumeInterruptsUnsafe(resume, state);
|
|
663
677
|
}
|
|
664
678
|
async submitInterruptBatch(submission) {
|
|
679
|
+
const continuationGeneration = this.continuationGeneration;
|
|
665
680
|
this.activeInterruptSubmission = submission;
|
|
666
681
|
this.interruptSubmissionFailure = void 0;
|
|
667
682
|
for (const resolution of submission.resolutions) {
|
|
@@ -670,12 +685,13 @@ var ChatClient = class {
|
|
|
670
685
|
const approvalId = resolution.interruptId;
|
|
671
686
|
this.processor.addToolApprovalResponse(approvalId, approved);
|
|
672
687
|
}
|
|
673
|
-
const resumed = await this.
|
|
688
|
+
const resumed = await this.resumeInterruptsUnsafeForGeneration([...submission.resolutions], {
|
|
674
689
|
threadId: submission.threadId,
|
|
675
690
|
runId: submission.interruptedRunId
|
|
676
|
-
}).finally(() => {
|
|
677
|
-
this.activeInterruptSubmission = void 0;
|
|
691
|
+
}, continuationGeneration).finally(() => {
|
|
692
|
+
if (this.activeInterruptSubmission === submission) this.activeInterruptSubmission = void 0;
|
|
678
693
|
});
|
|
694
|
+
if (continuationGeneration !== this.continuationGeneration) return;
|
|
679
695
|
const failure = this.takeInterruptSubmissionFailure();
|
|
680
696
|
if (failure !== void 0) throw { errors: failure.errors };
|
|
681
697
|
if (!resumed) throw new Error("Interrupt continuation could not be started.");
|
|
@@ -900,6 +916,7 @@ var ChatClient = class {
|
|
|
900
916
|
threadId: this.threadId,
|
|
901
917
|
runId
|
|
902
918
|
};
|
|
919
|
+
this.streamContinuationGeneration = this.continuationGeneration;
|
|
903
920
|
this.setIsLoading(true);
|
|
904
921
|
this.setStatus("streaming");
|
|
905
922
|
(async () => {
|
|
@@ -1027,8 +1044,12 @@ var ChatClient = class {
|
|
|
1027
1044
|
* - A MultimodalContent object with content array and optional custom ID
|
|
1028
1045
|
* @param body - Optional body parameters to merge with the client's base body for this request.
|
|
1029
1046
|
* Uses shallow merge with per-message body taking priority.
|
|
1030
|
-
* @param sendOptions - Per-call overrides
|
|
1031
|
-
*
|
|
1047
|
+
* @param sendOptions - Per-call overrides. `{ whenBusy }` overrides the
|
|
1048
|
+
* queue policy for this one send. `{ body }`
|
|
1049
|
+
* shallow-merges with `body` and with the chat-level
|
|
1050
|
+
* `body` / `forwardedProps`. `sendOptions.body` wins
|
|
1051
|
+
* on key collisions. Framework hooks forward this
|
|
1052
|
+
* object as their second argument.
|
|
1032
1053
|
*
|
|
1033
1054
|
* @example
|
|
1034
1055
|
* ```ts
|
|
@@ -1038,9 +1059,13 @@ var ChatClient = class {
|
|
|
1038
1059
|
* // Text message with custom body params
|
|
1039
1060
|
* await client.sendMessage('Hello!', { temperature: 0.7 })
|
|
1040
1061
|
*
|
|
1041
|
-
* // Per-call whenBusy override
|
|
1062
|
+
* // Per-call whenBusy override
|
|
1042
1063
|
* await client.sendMessage('Urgent', undefined, { whenBusy: 'interrupt' })
|
|
1043
1064
|
*
|
|
1065
|
+
* // Per-call body via options. Same effect as the positional arg.
|
|
1066
|
+
* // This is the shape the framework hooks (`useChat`, `injectChat`) forward.
|
|
1067
|
+
* await client.sendMessage('Hello!', undefined, { body: { temperature: 0.7 } })
|
|
1068
|
+
*
|
|
1044
1069
|
* // Multimodal message with image
|
|
1045
1070
|
* await client.sendMessage({
|
|
1046
1071
|
* content: [
|
|
@@ -1066,11 +1091,15 @@ var ChatClient = class {
|
|
|
1066
1091
|
this.mountDevtools();
|
|
1067
1092
|
if (typeof content === "string" && !content.trim()) return;
|
|
1068
1093
|
if (this.hasBlockingInterrupts()) throw new Error("ChatClient: cannot send normal input while pending interrupts exist. Use resumeInterrupts() instead.");
|
|
1094
|
+
const resolvedBody = {
|
|
1095
|
+
...body,
|
|
1096
|
+
...sendOptions?.body
|
|
1097
|
+
};
|
|
1069
1098
|
if (this.isSendBusy()) {
|
|
1070
1099
|
const { action, id } = this.decideWhenBusy(content, sendOptions);
|
|
1071
1100
|
if (action === "drop") return;
|
|
1072
1101
|
if (action === "queue") {
|
|
1073
|
-
this.enqueueMessage(content,
|
|
1102
|
+
this.enqueueMessage(content, resolvedBody, id);
|
|
1074
1103
|
return;
|
|
1075
1104
|
}
|
|
1076
1105
|
this.stopMessageQueueDrain = true;
|
|
@@ -1079,7 +1108,7 @@ var ChatClient = class {
|
|
|
1079
1108
|
this.resetSessionGenerating();
|
|
1080
1109
|
} else this.sendInFlight = true;
|
|
1081
1110
|
try {
|
|
1082
|
-
await this.deliverMessage(content,
|
|
1111
|
+
await this.deliverMessage(content, resolvedBody);
|
|
1083
1112
|
} finally {
|
|
1084
1113
|
this.sendInFlight = false;
|
|
1085
1114
|
}
|
|
@@ -1206,6 +1235,7 @@ var ChatClient = class {
|
|
|
1206
1235
|
async streamResponse() {
|
|
1207
1236
|
if (this.isLoading) return false;
|
|
1208
1237
|
const generation = ++this.streamGeneration;
|
|
1238
|
+
this.streamContinuationGeneration = this.continuationGeneration;
|
|
1209
1239
|
const resumeThreadId = this.pendingResumeThreadId;
|
|
1210
1240
|
const resumeParentRunId = this.pendingResumeParentRunId;
|
|
1211
1241
|
const resumeItems = this.pendingResumeItems;
|
|
@@ -1247,6 +1277,11 @@ var ChatClient = class {
|
|
|
1247
1277
|
this.processor.prepareAssistantMessage();
|
|
1248
1278
|
this.ensureSubscription();
|
|
1249
1279
|
const processingComplete = this.waitForProcessing();
|
|
1280
|
+
let byokHeaders;
|
|
1281
|
+
if (this.byok) {
|
|
1282
|
+
const provider = resolveByokProviderId(this.byokProvider, mergedBody.provider);
|
|
1283
|
+
byokHeaders = await prepareResolvedByokHeaders(this.byok, provider);
|
|
1284
|
+
}
|
|
1250
1285
|
const runContext = {
|
|
1251
1286
|
threadId: resumeThreadId ?? this.threadId,
|
|
1252
1287
|
runId,
|
|
@@ -1257,7 +1292,8 @@ var ChatClient = class {
|
|
|
1257
1292
|
parameters: t.inputSchema ? convertSchemaToJsonSchema(t.inputSchema) : { type: "object" }
|
|
1258
1293
|
})),
|
|
1259
1294
|
forwardedProps: { ...mergedBody },
|
|
1260
|
-
...resumeItems ? { resume: resumeItems } : {}
|
|
1295
|
+
...resumeItems ? { resume: resumeItems } : {},
|
|
1296
|
+
...byokHeaders ? { headers: byokHeaders } : {}
|
|
1261
1297
|
};
|
|
1262
1298
|
this.devtoolsBridge.beginRun(runContext.runId, runContext.threadId);
|
|
1263
1299
|
activeDevtoolsRunId = runContext.runId;
|
|
@@ -1279,22 +1315,24 @@ var ChatClient = class {
|
|
|
1279
1315
|
this.processor.finalizeStream();
|
|
1280
1316
|
streamCompletedSuccessfully = true;
|
|
1281
1317
|
} catch (err) {
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
}
|
|
1288
|
-
return false;
|
|
1318
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
1319
|
+
if (error.name === "AbortError") {
|
|
1320
|
+
if (activeDevtoolsRunId) {
|
|
1321
|
+
this.devtoolsBridge.emitRunLifecycle("run:cancelled", activeDevtoolsRunId, "cancelled");
|
|
1322
|
+
runTerminalEventEmitted = true;
|
|
1289
1323
|
}
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1324
|
+
return false;
|
|
1325
|
+
}
|
|
1326
|
+
if (error instanceof ByokMissingError) this.byok?.request(error.provider, "missing");
|
|
1327
|
+
if (error instanceof ByokBlockedError && error.reason === "locked") this.byok?.request(error.provider, "locked");
|
|
1328
|
+
if (generation === this.streamGeneration) {
|
|
1329
|
+
this.reportStreamError(error);
|
|
1330
|
+
if (activeDevtoolsRunId) {
|
|
1331
|
+
this.devtoolsBridge.emitRunLifecycle("run:errored", activeDevtoolsRunId, "errored", { error: error.message });
|
|
1332
|
+
runTerminalEventEmitted = true;
|
|
1296
1333
|
}
|
|
1297
1334
|
}
|
|
1335
|
+
if (generation === this.streamGeneration && (error instanceof ByokMissingError || error instanceof ByokBlockedError || error instanceof ByokUnresolvedProviderError)) throw error;
|
|
1298
1336
|
} finally {
|
|
1299
1337
|
if (generation === this.streamGeneration) {
|
|
1300
1338
|
this.currentStreamId = null;
|
|
@@ -1312,18 +1350,8 @@ var ChatClient = class {
|
|
|
1312
1350
|
}
|
|
1313
1351
|
await this.drainPostStreamActions();
|
|
1314
1352
|
if (streamCompletedSuccessfully) {
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
if (lastPart?.type === "tool-result" && finishReason !== "stop" && this.shouldAutoSend()) try {
|
|
1318
|
-
await this.checkForContinuation();
|
|
1319
|
-
} catch (error) {
|
|
1320
|
-
console.error("Failed to continue flow after tool result:", error);
|
|
1321
|
-
await this.drainQueue();
|
|
1322
|
-
}
|
|
1323
|
-
else {
|
|
1324
|
-
if (this.status !== "ready") this.setStatus("ready");
|
|
1325
|
-
if (!this.messageQueueDraining) await this.drainQueue();
|
|
1326
|
-
}
|
|
1353
|
+
if (this.status !== "ready") this.setStatus("ready");
|
|
1354
|
+
if (!this.messageQueueDraining) await this.drainQueue();
|
|
1327
1355
|
} else this.flushQueue();
|
|
1328
1356
|
}
|
|
1329
1357
|
}
|
|
@@ -1374,9 +1402,13 @@ var ChatClient = class {
|
|
|
1374
1402
|
* Stop the current stream
|
|
1375
1403
|
*/
|
|
1376
1404
|
stop() {
|
|
1405
|
+
this.continuationGeneration++;
|
|
1377
1406
|
const hadLocalStream = this.abortController !== null;
|
|
1378
1407
|
this.cancelInFlightStream({ setReadyStatus: true });
|
|
1379
1408
|
this.discardPendingSends();
|
|
1409
|
+
this.lastResume = null;
|
|
1410
|
+
this.activeInterruptSubmission = void 0;
|
|
1411
|
+
this.interruptManager.reset();
|
|
1380
1412
|
if (hadLocalStream) this.resetSessionGenerating();
|
|
1381
1413
|
this.events.stopped();
|
|
1382
1414
|
}
|
|
@@ -1411,9 +1443,9 @@ var ChatClient = class {
|
|
|
1411
1443
|
*/
|
|
1412
1444
|
async addToolResult(result) {
|
|
1413
1445
|
const clientTool = this.clientToolsRef.current.get(result.tool);
|
|
1414
|
-
await this.addToolResultForClientTool(result, clientTool);
|
|
1446
|
+
await this.addToolResultForClientTool(result, clientTool, this.streamContinuationGeneration);
|
|
1415
1447
|
}
|
|
1416
|
-
async addToolResultForClientTool(result, clientTool, context) {
|
|
1448
|
+
async addToolResultForClientTool(result, clientTool, continuationGeneration, context) {
|
|
1417
1449
|
if (clientTool && result.state !== "output-error") try {
|
|
1418
1450
|
result = {
|
|
1419
1451
|
...result,
|
|
@@ -1428,11 +1460,12 @@ var ChatClient = class {
|
|
|
1428
1460
|
};
|
|
1429
1461
|
}
|
|
1430
1462
|
this.events.toolResultAdded(result.toolCallId, result.tool, result.output, result.state || "output-available", context);
|
|
1463
|
+
if (continuationGeneration !== this.continuationGeneration) return;
|
|
1431
1464
|
this.processor.addToolResult(result.toolCallId, result.output, result.state === "output-error" ? result.errorText || "Tool execution failed" : void 0);
|
|
1432
1465
|
this.devtoolsBridge.emitSnapshot();
|
|
1433
1466
|
if (this.interruptManager.resolveClientToolOutput(result.toolCallId, result.state === "output-error" ? { error: result.errorText || "Tool execution failed" } : result.output)) return;
|
|
1434
1467
|
if (this.isLoading) {
|
|
1435
|
-
this.queuePostStreamAction(() => this.checkForContinuation());
|
|
1468
|
+
this.queuePostStreamAction(() => continuationGeneration === this.continuationGeneration ? this.checkForContinuation() : Promise.resolve());
|
|
1436
1469
|
return;
|
|
1437
1470
|
}
|
|
1438
1471
|
await this.checkForContinuation();
|
|
@@ -1470,7 +1503,11 @@ var ChatClient = class {
|
|
|
1470
1503
|
* Queue an action to be executed after the current stream ends
|
|
1471
1504
|
*/
|
|
1472
1505
|
queuePostStreamAction(action) {
|
|
1473
|
-
this.
|
|
1506
|
+
const continuationGeneration = this.continuationGeneration;
|
|
1507
|
+
this.postStreamActions.push(async () => {
|
|
1508
|
+
if (continuationGeneration !== this.continuationGeneration) return;
|
|
1509
|
+
await action();
|
|
1510
|
+
});
|
|
1474
1511
|
}
|
|
1475
1512
|
/**
|
|
1476
1513
|
* Drain and execute all queued post-stream actions
|
|
@@ -1489,6 +1526,7 @@ var ChatClient = class {
|
|
|
1489
1526
|
* Check if we should continue the flow and do so if needed
|
|
1490
1527
|
*/
|
|
1491
1528
|
async checkForContinuation() {
|
|
1529
|
+
if (this.streamContinuationGeneration !== this.continuationGeneration) return;
|
|
1492
1530
|
if (this.hasPendingInterrupts()) return;
|
|
1493
1531
|
if (this.continuationPending || this.isLoading) {
|
|
1494
1532
|
this.continuationSkipped = true;
|
|
@@ -1680,6 +1718,8 @@ var ChatClient = class {
|
|
|
1680
1718
|
}
|
|
1681
1719
|
if (options.body !== void 0) this.bodyOption = options.body;
|
|
1682
1720
|
if (options.forwardedProps !== void 0) this.forwardedPropsOption = options.forwardedProps;
|
|
1721
|
+
if (options.byok !== void 0) this.byok = options.byok;
|
|
1722
|
+
if (options.byokProvider !== void 0) this.byokProvider = options.byokProvider;
|
|
1683
1723
|
if ("context" in options) this.context = options.context;
|
|
1684
1724
|
if (options.tools !== void 0) {
|
|
1685
1725
|
this.interruptManager.updateTools(options.tools);
|