@tanstack/ai-client 0.23.0 → 0.23.2
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.
|
@@ -474,7 +474,7 @@ function normalizeConnectionAdapter(connection) {
|
|
|
474
474
|
model: "connect-wrapper",
|
|
475
475
|
timestamp: Date.now(),
|
|
476
476
|
finishReason: "stop"
|
|
477
|
-
});
|
|
477
|
+
}, runContext?.runId);
|
|
478
478
|
} catch (err) {
|
|
479
479
|
if (!abortSignal?.aborted && !hasTerminalEvent) try {
|
|
480
480
|
const message = err instanceof Error ? err.message : "Unknown error in connect()";
|
|
@@ -484,7 +484,7 @@ function normalizeConnectionAdapter(connection) {
|
|
|
484
484
|
runId: requireSyntheticId(upstreamRunId ?? runContext?.runId, "runId"),
|
|
485
485
|
timestamp: Date.now(),
|
|
486
486
|
message
|
|
487
|
-
});
|
|
487
|
+
}, runContext?.runId);
|
|
488
488
|
} catch {}
|
|
489
489
|
throw err;
|
|
490
490
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"connection-adapters.js","names":[],"sources":["../../src/connection-adapters.ts"],"sourcesContent":["import { EventType, uiMessagesToWire } from '@tanstack/ai/client'\nimport {\n createResponseStreamTextDecoder,\n getResponseStreamReader,\n} from './response-stream'\nimport { parseSseDataLine } from './sse-utils'\nimport type {\n ModelMessage,\n RunAgentResumeItem,\n RunErrorEvent,\n RunFinishedEvent,\n StreamChunk,\n UIMessage,\n} from '@tanstack/ai/client'\nimport type { ChatFetcher, ChatPendingInterrupt } from './types'\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 (\n requestRunId ??\n ('runId' in chunk && typeof chunk.runId === 'string'\n ? chunk.runId\n : undefined)\n )\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/** 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/**\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 const synthetic: RunFinishedEvent = {\n type: EventType.RUN_FINISHED,\n threadId: lastThreadId ?? fallbackIds?.threadId ?? '',\n runId: lastRunId ?? fallbackIds?.runId ?? '',\n model: lastModel ?? '',\n timestamp: Date.now(),\n finishReason: 'stop',\n }\n yield { chunk: synthetic }\n return\n }\n const chunk = 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 if ('model' in chunk && typeof chunk.model === 'string') {\n lastModel = chunk.model\n }\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: parsed.chunk, id: parsed.id }\n } else {\n yield { chunk: parsed as StreamChunk }\n }\n }\n}\n\nfunction assertResponseOk(response: Response): void {\n if (!response.ok) {\n throw new Error(\n `HTTP error! status: ${response.status} ${response.statusText}`,\n )\n }\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 assertResponseOk(response)\n const data = (await response.json()) as {\n messages?: Array<UIMessage>\n activeRun?: { runId?: unknown } | null\n interrupts?: { runId?: unknown; pending?: unknown } | 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) ? data.messages : [],\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 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 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 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 // Retains every delivered offset for the run's lifetime. Intentionally bounded\n // by run length (not evicted): a conforming server replays strictly after the\n // acknowledged offset, so this only needs to catch the single boundary event\n // on reconnect, but keeping the full set keeps de-dup correct even if a server\n // replays a wider overlap.\n const seen = new Set<string>()\n let lastEventId: string | undefined\n const reconnect = resolveReconnectOptions(reconnectOptions)\n let reconnectAttempts = 0\n\n // Throttle before re-issuing the request, and enforce the total ceiling so a\n // producer that keeps dropping after each event is bounded rather than\n // reconnecting forever.\n // Bound only CONSECUTIVE no-progress reconnects. A reconnect that made forward\n // progress resets the counter, so a healthy long run (even one whose socket\n // rolls after every event) never approaches the ceiling; it fires only when\n // the run is genuinely stuck — reconnecting repeatedly with nothing new.\n async function waitBeforeReconnect(madeProgress: boolean): Promise<void> {\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, abortSignal)\n }\n\n for (;;) {\n if (abortSignal?.aborted) return\n const extraHeaders: Record<string, string> =\n lastEventId !== undefined ? { 'Last-Event-ID': lastEventId } : {}\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 (id !== undefined) {\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 } else {\n if (seen.has(id)) continue\n seen.add(id)\n lastEventId = id\n }\n }\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 lastEventId !== undefined\n ) {\n await waitBeforeReconnect(progressed)\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 (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 waitBeforeReconnect(true)\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}\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: { runId: string; pending: Array<ChatPendingInterrupt> } | 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 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 // Reuse the caller's threadId/runId so client-side activeRunIds tracking matches.\n if (!abortSignal?.aborted && !hasTerminalEvent) {\n const synthetic: RunFinishedEvent = {\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 model: 'connect-wrapper',\n timestamp: Date.now(),\n finishReason: 'stop',\n }\n push(synthetic)\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)\n } catch {\n // fall through to rethrow the original error\n }\n }\n throw err\n }\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 as Array<UIMessage>)\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 ...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 ...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 = new Error(`XHR error! status: ${xhr.status} ${xhr.statusText}`)\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 = new Error(`XHR error! status: ${xhr.status} ${xhr.statusText}`)\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 // 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\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 },\n { signal: abortSignal },\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":";;;;;;;;;;;AAuBA,IAAM,8BAAc,IAAI,QAA6B;;;;;;AAOrD,SAAgB,cAAc,OAAwC;CAMpE,OADqB,YAAY,IAAI,KAEnC,MACC,WAAW,SAAS,OAAO,MAAM,UAAU,WACxC,MAAM,QACN,KAAA;AAER;;;;;;AAOA,IAAa,uBAAb,cAA0C,MAAM;CAC9C,cAAc;EACZ,MACE,iFACF;EACA,KAAK,OAAO;CACd;AACF;AAEA,IAAM,kBAAN,cAA8B,MAAM;CAClC,YAAY,OAAgB;EAC1B,MAAM,oCAAoC,EAAE,MAAM,CAAC;EACnD,KAAK,OAAO;CACd;AACF;;;;;;AAOA,IAAa,+BAAb,cAAkD,MAAM;CACtD,cAAc;EACZ,MACE,6FACF;EACA,KAAK,OAAO;CACd;AACF;;;;;;;AAQA,IAAa,4BAAb,cAA+C,MAAM;CACnD,YAAY,UAAkB;EAC5B,MACE,iDAAiD,SAAS,uBAC5D;EACA,KAAK,OAAO;CACd;AACF;AA0BA,SAAS,wBACP,SAC0B;CAC1B,MAAM,cAAc,SAAS,eAAe;CAC5C,MAAM,UAAU,SAAS,WAAW;CAIpC,IAAI,CAAC,OAAO,UAAU,WAAW,KAAK,cAAc,GAClD,MAAM,IAAI,MACR,kCAAkC,YAAY,kCAChD;CAEF,IAAI,CAAC,OAAO,SAAS,OAAO,KAAK,UAAU,GACzC,MAAM,IAAI,MACR,8BAA8B,QAAQ,wCACxC;CAEF,OAAO;EAAE;EAAa;CAAQ;AAChC;;AAGA,SAAS,eAAe,IAAY,QAAqC;CACvE,IAAI,MAAM,KAAK,QAAQ,SAAS,OAAO,QAAQ,QAAQ;CACvD,OAAO,IAAI,SAAS,YAAY;EAC9B,MAAM,gBAAgB;GACpB,aAAa,KAAK;GAClB,QAAQ;EACV;EACA,MAAM,QAAQ,iBAAiB;GAC7B,QAAQ,oBAAoB,SAAS,OAAO;GAC5C,QAAQ;EACV,GAAG,EAAE;EACL,QAAQ,iBAAiB,SAAS,SAAS,EAAE,MAAM,KAAK,CAAC;CAC3D,CAAC;AACH;AAEA,SAAS,cAAc,QAAwB;CAC7C,OAAO,GAAG,OAAO,GAAG,KAAK,IAAI,EAAE,GAAG,KAAK,OAAO,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC,MAAM,GAAG,CAAC;AACzE;;;;;;;AAQA,SAAS,mBACP,OACA,OACQ;CACR,IAAI,CAAC,OACH,MAAM,IAAI,MACR,qCAAqC,MAAM,sEAC7C;CAEF,OAAO;AACT;;;;AAKA,SAAS,aACP,eACwB;CACxB,IAAI,CAAC,eACH,OAAO,CAAC;CAEV,IAAI,yBAAyB,SAAS;EACpC,MAAM,SAAiC,CAAC;EACxC,cAAc,SAAS,OAAO,QAAQ;GACpC,OAAO,OAAO;EAChB,CAAC;EACD,OAAO;CACT;CACA,OAAO;AACT;;;;;;;;;;AAWA,IAAM,gBAAgB;AAEtB,SAAS,YAAY,OAAmD;CACtE,OAAO,UAAU,KAAA,IAAY,CAAC,IAAI,GAAG,gBAAgB,MAAM;AAC7D;AAEA,SAAS,iBAAiB,KAAa,QAAwC;CAC7E,MAAM,YAAY,IAAI,QAAQ,GAAG;CACjC,MAAM,OAAO,cAAc,KAAK,KAAK,IAAI,MAAM,SAAS;CACxD,MAAM,cAAc,cAAc,KAAK,MAAM,IAAI,MAAM,GAAG,SAAS;CACnE,MAAM,aAAa,YAAY,QAAQ,GAAG;CAC1C,MAAM,OACJ,eAAe,KAAK,cAAc,YAAY,MAAM,GAAG,UAAU;CACnE,MAAM,SAAS,IAAI,gBACjB,eAAe,KAAK,KAAK,YAAY,MAAM,aAAa,CAAC,CAC3D;CACA,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,MAAM,GAAG,OAAO,IAAI,KAAK,KAAK;CACxE,MAAM,QAAQ,OAAO,SAAS;CAC9B,OAAO,GAAG,OAAO,MAAM,WAAW,IAAI,KAAK,IAAI,UAAU;AAC3D;;;;AAKA,gBAAgB,gBACd,QACA,aACwB;CACxB,IAAI;EACF,MAAM,UAAU,gCAAgC;EAChD,IAAI,SAAS;EAEb,OAAO,CAAC,aAAa,SAAS;GAC5B,IAAI;GACJ,IAAI;IACF,SAAS,MAAM,OAAO,KAAK;GAC7B,SAAS,OAAO;IACd,IAAI,aAAa,SAAS;IAC1B,MAAM,IAAI,gBAAgB,KAAK;GACjC;GACA,MAAM,EAAE,MAAM,UAAU;GACxB,IAAI,MAAM;GAEV,UAAU,QAAQ,OAAO,OAAO,EAAE,QAAQ,KAAK,CAAC;GAChD,MAAM,QAAQ,OAAO,MAAM,IAAI;GAG/B,SAAS,MAAM,IAAI,KAAK;GAExB,KAAK,MAAM,QAAQ,OAAO;IAIxB,MAAM,aAAa,KAAK,SAAS,IAAI,IAAI,KAAK,MAAM,GAAG,EAAE,IAAI;IAC7D,IAAI,WAAW,KAAK,GAClB,MAAM;GAEV;EACF;EAMA,UAAU,QAAQ,OAAO;EAOzB,IAAI,OAAO,KAAK,KAAK,CAAC,aAAa,SACjC,MAAM,IAAI,qBAAqB;CAEnC,UAAU;EACR,OAAO,YAAY;CACrB;AACF;;;;;;;AAcA,SAAS,iBACP,OAC6C;CAC7C,OACE,OAAO,UAAU,YACjB,UAAU,QACV,WAAW,SACX,QAAQ,SACR,OAAQ,MAA0B,OAAO,YACzC,EAAE,UAAU;AAEhB;;;;;;;;;;;;;;;AAgBA,gBAAgB,iBACd,OACA,aAC6B;CAC7B,IAAI;CACJ,IAAI;CACJ,IAAI;CACJ,IAAI;CACJ,WAAW,MAAM,QAAQ,OAAO;EAC9B,IAAI,SAAS,QAAQ,KAAK,WAAW,KAAK,GAAG;GAK3C,MAAM,QAAQ,SAAS,OAAO,KAAK,KAAK,MAAM,CAAC;GAC/C,YAAY,MAAM,WAAW,GAAG,IAAI,MAAM,MAAM,CAAC,IAAI;GACrD;EACF;EAMA,IACE,KAAK,WAAW,GAAG,KACnB,KAAK,WAAW,QAAQ,KACxB,KAAK,WAAW,QAAQ,GAExB;EAEF,MAAM,OAAO,iBAAiB,IAAI;EAClC,IAAI,SAAS,UAAU;GASrB,MAAM,EAAE,OAAO;IAPb,MAAM,UAAU;IAChB,UAAU,gBAAgB,aAAa,YAAY;IACnD,OAAO,aAAa,aAAa,SAAS;IAC1C,OAAO,aAAa;IACpB,WAAW,KAAK,IAAI;IACpB,cAAc;GAED,EAAU;GACzB;EACF;EACA,MAAM,QAAQ,KAAK,MAAM,IAAI;EAC7B,IAAI,cAAc,SAAS,OAAO,MAAM,aAAa,UACnD,eAAe,MAAM;EAEvB,IAAI,WAAW,SAAS,OAAO,MAAM,UAAU,UAC7C,YAAY,MAAM;EAEpB,IAAI,WAAW,SAAS,OAAO,MAAM,UAAU,UAC7C,YAAY,MAAM;EAEpB,MAAM,KAAK;EACX,YAAY,KAAA;EACZ,MAAM;GAAE;GAAO,GAAI,OAAO,KAAA,IAAY,EAAE,GAAG,IAAI,CAAC;EAAG;CACrD;AACF;;;;;;;AAQA,gBAAgB,oBACd,OAC6B;CAC7B,WAAW,MAAM,QAAQ,OAAO;EAC9B,MAAM,SAAS,KAAK,MAAM,IAAI;EAC9B,IAAI,iBAAiB,MAAM,GACzB,MAAM;GAAE,OAAO,OAAO;GAAO,IAAI,OAAO;EAAG;OAE3C,MAAM,EAAE,OAAO,OAAsB;CAEzC;AACF;AAEA,SAAS,iBAAiB,UAA0B;CAClD,IAAI,CAAC,SAAS,IACZ,MAAM,IAAI,MACR,uBAAuB,SAAS,OAAO,GAAG,SAAS,YACrD;AAEJ;;;;;;;;AASA,eAAe,qBACb,aACA,KACA,SACA,aACA,UAC8B;CAC9B,MAAM,WAAW,MAAM,YAAY,iBAAiB,KAAK,EAAE,SAAS,CAAC,GAAG;EACtE,QAAQ;EACR,SAAS;GAAE,QAAQ;GAAoB,GAAG;EAAQ;EAClD;CACF,CAAC;CACD,iBAAiB,QAAQ;CACzB,MAAM,OAAQ,MAAM,SAAS,KAAK;CAKlC,MAAM,YACJ,KAAK,aAAa,OAAO,KAAK,UAAU,UAAU,WAC9C,EAAE,OAAO,KAAK,UAAU,MAAM,IAC9B;CACN,MAAM,aACJ,KAAK,cACL,OAAO,KAAK,WAAW,UAAU,YACjC,MAAM,QAAQ,KAAK,WAAW,OAAO,KACrC,KAAK,WAAW,QAAQ,SAAS,IAC7B;EACE,OAAO,KAAK,WAAW;EACvB,SAAS,KAAK,WAAW;CAC3B,IACA;CACN,OAAO;EACL,UAAU,MAAM,QAAQ,KAAK,QAAQ,IAAI,KAAK,WAAW,CAAC;EAC1D;EACA;CACF;AACF;;;;;;;;AASA,eAAe,yBACb,aACA,KACA,SACA,aACA,UACoC;CACpC,MAAM,WAAW,MAAM,YAAY,iBAAiB,KAAK,EAAE,SAAS,CAAC,GAAG;EACtE,QAAQ;EACR,SAAS;GAAE,QAAQ;GAAoB,GAAG;EAAQ;EAClD;CACF,CAAC;CACD,iBAAiB,QAAQ;CACzB,MAAM,MAAe,MAAM,SAAS,KAAK;CAGzC,IAAI,QAAQ,MACV,OAAO;EAAE,gBAAgB;EAAM,WAAW;CAAK;CAKjD,IAAI,OAAO,QAAQ,YAAY,MAAM,QAAQ,GAAG,GAC9C,MAAM,IAAI,MACR,oDAAoD,IAAI,aAAa,MAAM,QAAQ,GAAG,IAAI,aAAa,OAAO,IAAI,EACpH;CAEF,MAAM,OAAO;CAIb,MAAM,YACJ,KAAK,aAAa,OAAO,KAAK,UAAU,UAAU,WAC9C,EAAE,OAAO,KAAK,UAAU,MAAM,IAC9B;CACN,OAAO;EACL,gBAAgB,KAAK,kBAAkB;EACvC;CACF;AACF;;AAGA,gBAAgB,oBACd,UACA,aACA,aAC6B;CAC7B,iBAAiB,QAAQ;CAEzB,OAAO,iBAAiB,gBADT,wBAAwB,QACC,GAAQ,WAAW,GAAG,WAAW;AAC3E;;AAGA,gBAAgB,uBACd,UACA,aAC6B;CAC7B,iBAAiB,QAAQ;CAEzB,OAAO,oBAAoB,gBADZ,wBAAwB,QACI,GAAQ,WAAW,CAAC;AACjE;AAEA,gBAAgB,oBACd,UACA,aAC6B;CAC7B,WAAW,MAAM,EAAE,WAAW,oBAAoB,UAAU,WAAW,GACrE,MAAM;AAEV;;;;;AAiBA,SAAS,iBACP,aACA,KACA,aACA,eAImB;CACnB,OAAO,iBAAiB,cAAc,aAAa;EACjD,IAAI;EACJ,IAAI;GACF,WAAW,MAAM,YAAY,KAAK;IAChC,GAAG;IACH,SAAS;KACP,GAAI,YAAY;KAChB,GAAG;IACL;IACA,GAAI,cAAc,EAAE,QAAQ,YAAY,IAAI,CAAC;GAC/C,CAAC;EACH,SAAS,OAAO;GASd,MAAM,IAAI,gBAAgB,KAAK;EACjC;EACA,OAAO,cAAc,UAAU,WAAW;CAC5C;AACF;;;;;;;;;;;;;AAcA,gBAAgB,gBACd,iBACA,aACA,kBAC6B;CAM7B,MAAM,uBAAO,IAAI,IAAY;CAC7B,IAAI;CACJ,MAAM,YAAY,wBAAwB,gBAAgB;CAC1D,IAAI,oBAAoB;CASxB,eAAe,oBAAoB,cAAsC;EACvE,IAAI,cACF,oBAAoB;OACf;GACL,qBAAqB;GACrB,IAAI,oBAAoB,UAAU,aAChC,MAAM,IAAI,0BAA0B,UAAU,WAAW;EAE7D;EACA,MAAM,eAAe,UAAU,SAAS,WAAW;CACrD;CAEA,SAAS;EACP,IAAI,aAAa,SAAS;EAC1B,MAAM,eACJ,gBAAgB,KAAA,IAAY,EAAE,iBAAiB,YAAY,IAAI,CAAC;EAElE,IAAI,cAAc;EAClB,IAAI,aAAa;EACjB,IAAI;GACF,WAAW,MAAM,EAAE,OAAO,QAAQ,gBAChC,cACA,WACF,GAAG;IACD,IAAI,OAAO,KAAA,GACT,IAAI,OAAO,IAAI;KAGb,cAAc,KAAA;KACd,KAAK,MAAM;IACb,OAAO;KACL,IAAI,KAAK,IAAI,EAAE,GAAG;KAClB,KAAK,IAAI,EAAE;KACX,cAAc;IAChB;IAEF,aAAa;IACb,IAAI,MAAM,SAAS,kBAAkB,MAAM,SAAS,aAClD,cAAc;IAEhB,MAAM;GAQR;EACF,SAAS,OAAO;GACd,IAAI,aAAa,SAAS;GAS1B,KACG,iBAAiB,wBAChB,iBAAiB,oBACnB,gBAAgB,KAAA,GAChB;IACA,MAAM,oBAAoB,UAAU;IACpC;GACF;GACA,MAAM;EACR;EAEA,IAAI,aAAa,SAAS;EAM1B,IAAI,aAAa;EAEjB,IAAI,gBAAgB,KAAA,GAAW;GAE7B,IAAI,YAAY;IAKd,MAAM,oBAAoB,IAAI;IAC9B;GACF;GAaA,MAAM,IAAI,6BAA6B;EACzC;EAIA;CACF;AACF;;;;;;;AAqLA,SAAgB,2BACd,YAC4B;CAC5B,IAAI,CAAC,YACH,MAAM,IAAI,MAAM,gCAAgC;CAGlD,MAAM,aAAa,aAAa;CAChC,MAAM,eAAe,eAAe;CACpC,MAAM,UAAU,UAAU;CAE1B,IAAI,eAAe,gBAAgB,UACjC,MAAM,IAAI,MACR,2FACF;CAGF,IAAI,gBAAgB,SAAS;EAC3B,MAAM,UAAW,WAA0C,SAAS,KAClE,UACF;EACA,MAAM,UAAW,WAA0C,SAAS,KAClE,UACF;EACA,OAAO;GACL,WAAW,WAAW,UAAU,KAAK,UAAU;GAC/C,MAAM,WAAW,KAAK,KAAK,UAAU;GACrC,GAAI,UAAU,EAAE,QAAQ,IAAI,CAAC;GAC7B,GAAI,UAAU,EAAE,QAAQ,IAAI,CAAC;EAC/B;CACF;CAEA,IAAI,CAAC,YACH,MAAM,IAAI,MACR,2EACF;CAIF,IAAI,eAAmC,CAAC;CACxC,IAAI,gBAA4D,CAAC;CAEjE,SAAS,KAAK,OAAoB,OAAsB;EACtD,IAAI,OACF,YAAY,IAAI,OAAO,KAAK;EAE9B,MAAM,SAAS,cAAc,MAAM;EACnC,IAAI,QACF,OAAO,KAAK;OAEZ,aAAa,KAAK,KAAK;CAE3B;CAEA,OAAO;EACL,UAAU,aAAuD;GAG/D,MAAM,WAA+B,aAAa,OAAO,CAAC;GAC1D,MAAM,YAAwD,CAAC;GAC/D,eAAe;GACf,gBAAgB;GAEhB,QAAQ,mBAAmB;IACzB,OAAO,CAAC,aAAa,SAAS;KAC5B,IAAI;KACJ,MAAM,WAAW,SAAS,MAAM;KAChC,IAAI,aAAa,KAAA,GACf,QAAQ;UAER,QAAQ,MAAM,IAAI,SAA6B,YAAY;MACzD,MAAM,gBAAgB,QAAQ,IAAI;MAClC,UAAU,MAAM,MAAM;OACpB,aAAa,oBAAoB,SAAS,OAAO;OACjD,QAAQ,CAAC;MACX,CAAC;MACD,aAAa,iBAAiB,SAAS,SAAS,EAAE,MAAM,KAAK,CAAC;KAChE,CAAC;KAEH,IAAI,UAAU,MAAM,MAAM;IAC5B;GACF,EAAA,CAAG;EACL;EACA,MAAM,KAAK,UAAU,MAAM,aAAa,YAAY;GAClD,IAAI,mBAAmB;GACvB,IAAI;GACJ,IAAI;GACJ,IAAI;IACF,MAAM,SAAS,WAAW,QACxB,UACA,MACA,aACA,UACF;IACA,WAAW,MAAM,SAAS,QAAQ;KAChC,IAAI,cAAc,SAAS,OAAO,MAAM,aAAa,UACnD,mBAAmB,MAAM;KAE3B,IAAI,WAAW,SAAS,OAAO,MAAM,UAAU,UAC7C,gBAAgB,MAAM;KAExB,IAAI,MAAM,SAAS,kBAAkB,MAAM,SAAS,aAClD,mBAAmB;KAErB,KAAK,OAAO,YAAY,KAAK;IAC/B;IAKA,IAAI,CAAC,aAAa,WAAW,CAAC,kBAe5B,KAAK;KAbH,MAAM,UAAU;KAChB,UAAU,mBACR,oBAAoB,YAAY,UAChC,UACF;KACA,OAAO,mBACL,iBAAiB,YAAY,OAC7B,OACF;KACA,OAAO;KACP,WAAW,KAAK,IAAI;KACpB,cAAc;IAEX,CAAS;GAElB,SAAS,KAAK;IACZ,IAAI,CAAC,aAAa,WAAW,CAAC,kBAK5B,IAAI;KACF,MAAM,UACJ,eAAe,QAAQ,IAAI,UAAU;KAcvC,KAAK;MAZH,MAAM,UAAU;MAChB,UAAU,mBACR,oBAAoB,YAAY,UAChC,UACF;MACA,OAAO,mBACL,iBAAiB,YAAY,OAC7B,OACF;MACA,WAAW,KAAK,IAAI;MACpB;KAEG,CAAS;IAChB,QAAQ,CAER;IAEF,MAAM;GACR;EACF;EAKA,GAAI,OAAQ,WAAiD,YAC7D,aACI,EACE,UAAU,OAAe,gBACtB,WAAiD,QAChD,OACA,WACF,EACJ,IACA,CAAC;EACL,UAAU;GAGR,MAAM,UAAW,WAAiD;GAClE,OAAO,OAAO,YAAY,aACtB,EAAE,UAAU,aAAqB,QAAQ,QAAQ,EAAE,IACnD,CAAC;EACP,EAAA,CAAG;CACL;AACF;AAiCA,SAAS,uBACP,UACA,MACA,YACA,SACyB;CAGzB,MAAM,eAAe,iBAAiB,QAA4B;CAClE,MAAM,iBAAiB;EACrB,GAAG,QAAQ;EACX,GAAI,YAAY,kBAAkB,CAAC;EACnC,GAAG;CACL;CAEA,OAAO;EACL,UAAU,YAAY,YAAY,cAAc,QAAQ;EACxD,OAAO,YAAY,SAAS,cAAc,KAAK;EAC/C,GAAI,YAAY,gBAAgB,KAAA,KAAa,EAC3C,aAAa,WAAW,YAC1B;EACA,GAAI,YAAY,WAAW,KAAA,KAAa,EAAE,QAAQ,WAAW,OAAO;EACpE,OAAO,CAAC;EACR,UAAU;EACV,OAAO,YAAY,eAAe,CAAC;EACnC,SAAS,CAAC;EACV;EAEA,MAAM,EAAE,GAAG,eAAe;CAC5B;AACF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoCA,SAAgB,sBACd,KACA,UAEuE,CAAC,GACrC;CACnC,OAAO;EACL,OAAO,QAAQ,UAAU,MAAM,aAAa,YAAY;GAEtD,MAAM,cAAc,OAAO,QAAQ,aAAa,IAAI,IAAI;GACxD,MAAM,kBACJ,OAAO,YAAY,aAAa,MAAM,QAAQ,IAAI;GAEpD,MAAM,iBAAyC;IAC7C,gBAAgB;IAChB,GAAG,aAAa,gBAAgB,OAAO;IACvC,GAAG,YAAY,YAAY,KAAK;GAClC;GASA,MAAM,cAAc,uBAClB,UACA,MACA,YACA,eACF;GAEA,MAAM,cAAc,gBAAgB,eAAe;GAInD,MAAM,SAAS,eAAe,gBAAgB;GAU9C,OAAO,gBACL,iBACE,aACA,aACA;IACE,QAAQ;IACR,SAAS;IACT,MAAM,KAAK,UAAU,WAAW;IAChC,aAAa,gBAAgB,eAAe;GAC9C,IAIC,UAAU,cACT,oBAAoB,UAAU,WAAW;IACvC,GAAI,YAAY,aAAa,KAAA,IACzB,EAAE,UAAU,WAAW,SAAS,IAChC,CAAC;IACL,GAAI,YAAY,UAAU,KAAA,IACtB,EAAE,OAAO,WAAW,MAAM,IAC1B,CAAC;GACP,CAAC,CACL,GACA,QACA,gBAAgB,SAClB;EACF;EACA,OAAO,QAAQ,OAAO,aAAa;GAIjC,MAAM,cAAc,OAAO,QAAQ,aAAa,IAAI,IAAI;GACxD,MAAM,kBACJ,OAAO,YAAY,aAAa,MAAM,QAAQ,IAAI;GAEpD,MAAM,UAAU,iBAAiB,aAAa;IAC5C,QAAQ;IACR;GACF,CAAC;GAED,MAAM,iBAAyC,EAC7C,GAAG,aAAa,gBAAgB,OAAO,EACzC;GACA,MAAM,cAAc,gBAAgB,eAAe;GACnD,MAAM,SAAS,eAAe,gBAAgB;GAE9C,OAAO,gBACL,iBACE,aACA,SACA;IACE,QAAQ;IACR,SAAS;IACT,aAAa,gBAAgB,eAAe;GAC9C,IAEC,UAAU,cACT,oBAAoB,UAAU,WAAW,EAAE,MAAM,CAAC,CACtD,GACA,QACA,gBAAgB,SAClB;EACF;EACA,MAAM,QAAQ,UAAU;GACtB,MAAM,cAAc,OAAO,QAAQ,aAAa,IAAI,IAAI;GACxD,MAAM,kBACJ,OAAO,YAAY,aAAa,MAAM,QAAQ,IAAI;GACpD,OAAO,qBACL,gBAAgB,eAAe,OAC/B,aACA,aAAa,gBAAgB,OAAO,GACpC,gBAAgB,eAAe,eAC/B,QACF;EACF;EACA,MAAM,kBAAkB,UAAU;GAChC,MAAM,cAAc,OAAO,QAAQ,aAAa,IAAI,IAAI;GACxD,MAAM,kBACJ,OAAO,YAAY,aAAa,MAAM,QAAQ,IAAI;GACpD,OAAO,yBACL,gBAAgB,eAAe,OAC/B,aACA,aAAa,gBAAgB,OAAO,GACpC,gBAAgB,eAAe,eAC/B,QACF;EACF;CACF;AACF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoCA,SAAgB,gBACd,KACA,UAEuE,CAAC,GACrC;CACnC,OAAO;EACL,OAAO,QAAQ,UAAU,MAAM,aAAa,YAAY;GAEtD,MAAM,cAAc,OAAO,QAAQ,aAAa,IAAI,IAAI;GACxD,MAAM,kBACJ,OAAO,YAAY,aAAa,MAAM,QAAQ,IAAI;GAEpD,MAAM,iBAAyC;IAC7C,gBAAgB;IAChB,GAAG,aAAa,gBAAgB,OAAO;IACvC,GAAG,YAAY,YAAY,KAAK;GAClC;GASA,MAAM,cAAc,uBAClB,UACA,MACA,YACA,eACF;GAEA,MAAM,cAAc,gBAAgB,eAAe;GAInD,MAAM,SAAS,eAAe,gBAAgB;GAW9C,OAAO,gBACL,iBACE,aACA,aACA;IACE,QAAQ;IACR,SAAS;IACT,MAAM,KAAK,UAAU,WAAW;IAChC,aAAa,gBAAgB,eAAe;GAC9C,GACA,sBACF,GACA,QACA,gBAAgB,SAClB;EACF;EACA,OAAO,QAAQ,OAAO,aAAa;GAIjC,MAAM,cAAc,OAAO,QAAQ,aAAa,IAAI,IAAI;GACxD,MAAM,kBACJ,OAAO,YAAY,aAAa,MAAM,QAAQ,IAAI;GAEpD,MAAM,UAAU,iBAAiB,aAAa;IAAE,QAAQ;IAAM;GAAM,CAAC;GACrE,MAAM,iBAAyC,EAC7C,GAAG,aAAa,gBAAgB,OAAO,EACzC;GACA,MAAM,cAAc,gBAAgB,eAAe;GACnD,MAAM,SAAS,eAAe,gBAAgB;GAE9C,OAAO,gBACL,iBACE,aACA,SACA;IACE,QAAQ;IACR,SAAS;IACT,aAAa,gBAAgB,eAAe;GAC9C,GACA,sBACF,GACA,QACA,gBAAgB,SAClB;EACF;EACA,MAAM,QAAQ,UAAU;GACtB,MAAM,cAAc,OAAO,QAAQ,aAAa,IAAI,IAAI;GACxD,MAAM,kBACJ,OAAO,YAAY,aAAa,MAAM,QAAQ,IAAI;GACpD,OAAO,qBACL,gBAAgB,eAAe,OAC/B,aACA,aAAa,gBAAgB,OAAO,GACpC,gBAAgB,eAAe,eAC/B,QACF;EACF;EACA,MAAM,kBAAkB,UAAU;GAChC,MAAM,cAAc,OAAO,QAAQ,aAAa,IAAI,IAAI;GACxD,MAAM,kBACJ,OAAO,YAAY,aAAa,MAAM,QAAQ,IAAI;GACpD,OAAO,yBACL,gBAAgB,eAAe,OAC/B,aACA,aAAa,gBAAgB,OAAO,GACpC,gBAAgB,eAAe,eAC/B,QACF;EACF;CACF;AACF;AAMA,SAAS,8BAA8C;CACrD,IAAI,OAAO,WAAW,mBAAmB,YACvC,MAAM,IAAI,MAAM,iDAAiD;CAGnE,OAAO,IAAI,WAAW,eAAe;AACvC;AAEA,SAAS,WACP,KACA,aACA,SACM;CACN,IAAI,aAAa;CACjB,IAAI,SAAS;CACb,IAAI,UAAU;CACd,IAAI,UAAU;CACd,IAAI,YAAY;CAEhB,IAAI,eAAe,SACjB,YAAY,oBAAoB,SAAS,OAAO;AAEpD;AAEA,SAAS,aACP,KACA,aACwB;CACxB,IAAI,SAAS;CACb,IAAI,SAAS;CACb,MAAM,QAAuB,CAAC;CAC9B,MAAM,UAA6B,CAAC;CACpC,IAAI,OAAO;CACX,IAAI,UAAU;CACd,IAAI;CACJ,IAAI;CAEJ,MAAM,aAAa;EAEjB,QADuB,MACvB,CAAA,GAAS;CACX;CAEA,MAAM,qBAAqB;EACzB,IAAI,IAAI,WAAW,MAAM,IAAI,SAAS,OAAO,IAAI,UAAU,MAAM;GAC/D,wBAAQ,IAAI,MAAM,sBAAsB,IAAI,OAAO,GAAG,IAAI,YAAY;GACtE,OAAO;GACP;EACF;EAEA,MAAM,eAAe,IAAI;EACzB,IAAI,aAAa,UAAU,QACzB;EAGF,UAAU,aAAa,MAAM,MAAM;EACnC,SAAS,aAAa;EACtB,MAAM,aAAa,OAAO,MAAM,IAAI;EACpC,SAAS,WAAW,IAAI,KAAK;EAE7B,KAAK,MAAM,QAAQ,YAAY;GAC7B,MAAM,aAAa,KAAK,SAAS,IAAI,IAAI,KAAK,MAAM,GAAG,EAAE,IAAI;GAC7D,IAAI,WAAW,KAAK,GAClB,MAAM,KAAK,UAAU;EAEzB;CACF;CAEA,MAAM,eAAe;EACnB,aAAa;EAIb,IAAI,IAAI,WAAW,MAAM,IAAI,SAAS,OAAO,IAAI,UAAU,MACzD,wBAAQ,IAAI,MAAM,sBAAsB,IAAI,OAAO,GAAG,IAAI,YAAY;OACjE,IAAI,OAAO,KAAK,KAAK,CAAC,SAC3B,QAAQ,IAAI,qBAAqB;EAEnC,OAAO;EACP,KAAK;CACP;CAEA,IAAI,mBAAmB;EACrB,aAAa;EACb,KAAK;CACP;CACA,IAAI,SAAS;CACb,IAAI,gBAAgB;EAIlB,QAAQ,IAAI,gCAAgB,IAAI,MAAM,oBAAoB,CAAC;EAC3D,OAAO;EACP,KAAK;CACP;CACA,IAAI,gBAAgB;EAClB,UAAU;EACV,OAAO;EACP,KAAK;CACP;CACA,IAAI,kBAAkB;EACpB,IAAI,CAAC,MACH,OAAO;CAEX;CAEA,IAAI,aAAa;EACf,gBAAgB;GACd,UAAU;GACV,IAAI,MAAM;EACZ;EACA,IAAI,YAAY,SACd,QAAQ;OAER,YAAY,iBAAiB,SAAS,SAAS,EAAE,MAAM,KAAK,CAAC;CAEjE;CAEA,QAAQ,mBAAmB;EACzB,IAAI;GAEF,OAAO,MAAM;IACX,MAAM,OAAO,MAAM,MAAM;IACzB,IAAI,SAAS,KAAA,GAAW;KACtB,MAAM;KACN;IACF;IAEA,IAAI,OACF,MAAM;IAIR,IAAI,QAAQ,aAAa,SACvB;IAGF,MAAM,IAAI,SAAe,YAAY;KACnC,QAAQ,KAAK,OAAO;IACtB,CAAC;GACH;EACF,UAAU;GACR,WAAW,KAAK,aAAa,OAAO;EACtC;CACF,EAAA,CAAG;AACL;AAOA,SAAS,2BACP,KACA,SACA,UACA,MACA,YACA,SAAiB,QACjB,eAAuC,CAAC,GAClB;CACtB,MAAM,MAAM,QAAQ,aAAa,KAAK,4BAA4B;CAClE,IAAI,KAAK,QAAQ,GAAG;CACpB,IAAI,QAAQ,oBAAoB,KAAA,GAC9B,IAAI,kBAAkB,QAAQ;CAGhC,MAAM,iBAAyC;EAC7C,gBAAgB;EAChB,GAAG,aAAa,QAAQ,OAAO;EAG/B,GAAI,WAAW,SAAS,YAAY,YAAY,KAAK,IAAI,CAAC;EAE1D,GAAG;CACL;CAEA,KAAK,MAAM,CAAC,MAAM,UAAU,OAAO,QAAQ,cAAc,GACvD,IAAI,iBAAiB,MAAM,KAAK;CAGlC,MAAM,cAAc,uBAClB,UACA,MACA,YACA,OACF;CAEA,OAAO;EAAE;EAAK,MAAM,KAAK,UAAU,WAAW;CAAE;AAClD;AAEA,eAAe,4BACb,SAC+B;CAC/B,OAAO,OAAO,YAAY,aAAa,MAAM,QAAQ,IAAI;AAC3D;;;;;;;AAQA,SAAS,eACP,KACA,SACA,QACA,UACA,MACA,YACA,YACmB;CACnB,OAAO,iBAAiB,cAAc,aAAa;EACjD,MAAM,UAAU,2BACd,KACA,SACA,UACA,MACA,YACA,QACA,YACF;EACA,MAAM,QAAQ,aAAa,QAAQ,KAAK,WAAW;EACnD,IAAI,aAAa,SAAS;GACxB,MAAM,MAAM,KAAK;GACjB;EACF;EAEA,QAAQ,IAAI,KAAK,WAAW,QAAQ,OAAO,QAAQ,IAAI;EACvD,IAAI;GACF,OAAO,WAAW,KAAK;EACzB,UAAU;GAIR,IAAI,CAAC,aAAa,SAAS,QAAQ,IAAI,MAAM;EAC/C;CACF;AACF;;AAGA,SAAS,aAAa,YAA8C;CAClE,MAAM,cAAqD;EACzD,GAAI,YAAY,aAAa,KAAA,IACzB,EAAE,UAAU,WAAW,SAAS,IAChC,CAAC;EACL,GAAI,YAAY,UAAU,KAAA,IAAY,EAAE,OAAO,WAAW,MAAM,IAAI,CAAC;CACvE;CACA,QAAQ,UAAiC,iBAAiB,OAAO,WAAW;AAC9E;;;;;;;;;AAUA,SAAgB,oBACd,KACA,UAAwC,CAAC,GACN;CACnC,OAAO;EACL,OAAO,QAAQ,UAAU,MAAM,aAAa,YAAY;GACtD,MAAM,cAAc,OAAO,QAAQ,aAAa,IAAI,IAAI;GACxD,MAAM,kBAAkB,MAAM,4BAA4B,OAAO;GACjE,MAAM,SAAS,eAAe,gBAAgB;GAK9C,OAAO,gBACL,eACE,aACA,iBACA,QACA,UACA,MACA,YACA,aAAa,UAAU,CACzB,GACA,QACA,gBAAgB,SAClB;EACF;EACA,OAAO,QAAQ,OAAO,aAAa;GACjC,MAAM,cAAc,OAAO,QAAQ,aAAa,IAAI,IAAI;GACxD,MAAM,kBAAkB,MAAM,4BAA4B,OAAO;GACjE,MAAM,SAAS,eAAe,gBAAgB;GAE9C,OAAO,gBACL,eAFc,iBAAiB,aAAa;IAAE,QAAQ;IAAM;GAAM,CAGhE,GACA,iBACA,OACA,CAAC,GACD,KAAA,GACA,KAAA,IAGC,UAAU,iBAAiB,OAAO,EAAE,MAAM,CAAC,CAC9C,GACA,QACA,gBAAgB,SAClB;EACF;EACA,MAAM,QAAQ,UAAU;GACtB,MAAM,cAAc,OAAO,QAAQ,aAAa,IAAI,IAAI;GACxD,MAAM,kBAAkB,MAAM,4BAA4B,OAAO;GAGjE,OAAO,qBACL,OACA,aACA,aAAa,gBAAgB,OAAO,GACpC,gBAAgB,kBAAkB,YAAY,eAC9C,QACF;EACF;EACA,MAAM,kBAAkB,UAAU;GAChC,MAAM,cAAc,OAAO,QAAQ,aAAa,IAAI,IAAI;GACxD,MAAM,kBAAkB,MAAM,4BAA4B,OAAO;GAGjE,OAAO,yBACL,OACA,aACA,aAAa,gBAAgB,OAAO,GACpC,gBAAgB,kBAAkB,YAAY,eAC9C,QACF;EACF;CACF;AACF;;;;;;;;;AAUA,SAAgB,cACd,KACA,UAAwC,CAAC,GACN;CACnC,OAAO;EACL,OAAO,QAAQ,UAAU,MAAM,aAAa,YAAY;GACtD,MAAM,cAAc,OAAO,QAAQ,aAAa,IAAI,IAAI;GACxD,MAAM,kBAAkB,MAAM,4BAA4B,OAAO;GACjE,MAAM,SAAS,eAAe,gBAAgB;GAK9C,OAAO,gBACL,eACE,aACA,iBACA,QACA,UACA,MACA,YACA,mBACF,GACA,QACA,gBAAgB,SAClB;EACF;EACA,OAAO,QAAQ,OAAO,aAAa;GACjC,MAAM,cAAc,OAAO,QAAQ,aAAa,IAAI,IAAI;GACxD,MAAM,kBAAkB,MAAM,4BAA4B,OAAO;GACjE,MAAM,SAAS,eAAe,gBAAgB;GAE9C,OAAO,gBACL,eAFc,iBAAiB,aAAa;IAAE,QAAQ;IAAM;GAAM,CAGhE,GACA,iBACA,OACA,CAAC,GACD,KAAA,GACA,KAAA,GACA,mBACF,GACA,QACA,gBAAgB,SAClB;EACF;EACA,MAAM,QAAQ,UAAU;GACtB,MAAM,cAAc,OAAO,QAAQ,aAAa,IAAI,IAAI;GACxD,MAAM,kBAAkB,MAAM,4BAA4B,OAAO;GAGjE,OAAO,qBACL,OACA,aACA,aAAa,gBAAgB,OAAO,GACpC,gBAAgB,kBAAkB,YAAY,eAC9C,QACF;EACF;EACA,MAAM,kBAAkB,UAAU;GAChC,MAAM,cAAc,OAAO,QAAQ,aAAa,IAAI,IAAI;GACxD,MAAM,kBAAkB,MAAM,4BAA4B,OAAO;GAGjE,OAAO,yBACL,OACA,aACA,aAAa,gBAAgB,OAAO,GACpC,gBAAgB,kBAAkB,YAAY,eAC9C,QACF;EACF;CACF;AACF;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6DA,SAAgB,OACd,eAKA,UAC0B;CAC1B,OAAO;EACL,OAAO,QAAQ,UAAU,MAAM,aAAa;GAG1C,OAAO,cAAc,UAAU,MAAM,WAAW;EAClD;EACA,GAAI,UAAU,UAAU,EAAE,SAAS,SAAS,QAAQ,IAAI,CAAC;EACzD,GAAI,UAAU,oBACV,EAAE,mBAAmB,SAAS,kBAAkB,IAChD,CAAC;EACL,GAAI,UAAU,UAAU,EAAE,SAAS,SAAS,QAAQ,IAAI,CAAC;CAC3D;AACF;;;;;;;;;AAUA,SAAgB,2BACd,SAC0B;CAC1B,OAAO,EACL,OAAO,QAAQ,UAAU,MAAM,aAAa,YAAY;EACtD,IAAI,CAAC,aACH,MAAM,IAAI,MACR,2FACF;EAEF,IAAI,CAAC,YACH,MAAM,IAAI,MACR,mGACF;EAGF,MAAM,SAAS,MAAM,QACnB;GACY;GACV;GACA,UAAU,WAAW;GACrB,OAAO,WAAW;EACpB,GACA,EAAE,QAAQ,YAAY,CACxB;EACA,IAAI,kBAAkB,UACpB,OAAO,oBAAoB,QAAQ,WAAW;OAE9C,OAAO,kBAAkB,QAAQ,WAAW;CAEhD,EACF;AACF;;;;;;AAOA,gBAAgB,kBACd,UACA,QACmB;CACnB,IAAI,OAAO,SAAS;CACpB,MAAM,WAAW,SAAS,OAAO,cAAc,CAAC;CAChD,MAAM,eAAe,IAAI,SACtB,YAAY;EACX,OAAO,iBACL,eACM,QAAQ;GAAE,MAAM;GAAM,OAAO,KAAA;EAAU,CAAC,GAC9C,EAAE,MAAM,KAAK,CACf;CACF,CACF;CACA,IAAI;EAEF,OAAO,MAAM;GACX,MAAM,SAAS,MAAM,QAAQ,KAAK,CAAC,SAAS,KAAK,GAAG,YAAY,CAAC;GACjE,IAAI,OAAO,MAAM;GACjB,MAAM,OAAO;EACf;CACF,UAAU;EACR,MAAM,SAAS,SAAS;CAC1B;AACF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8BA,SAAgB,UACd,SAKA,UAC0B;CAC1B,OAAO;EACL,OAAO,QAAQ,UAAU,MAAM,aAAa;GAG1C,OAAO,QAAQ,UAAU,MAAM,WAAW;EAC5C;EACA,GAAI,UAAU,UAAU,EAAE,SAAS,SAAS,QAAQ,IAAI,CAAC;EACzD,GAAI,UAAU,oBACV,EAAE,mBAAmB,SAAS,kBAAkB,IAChD,CAAC;EACL,GAAI,UAAU,UAAU,EAAE,SAAS,SAAS,QAAQ,IAAI,CAAC;CAC3D;AACF"}
|
|
1
|
+
{"version":3,"file":"connection-adapters.js","names":[],"sources":["../../src/connection-adapters.ts"],"sourcesContent":["import { EventType, uiMessagesToWire } from '@tanstack/ai/client'\nimport {\n createResponseStreamTextDecoder,\n getResponseStreamReader,\n} from './response-stream'\nimport { parseSseDataLine } from './sse-utils'\nimport type {\n ModelMessage,\n RunAgentResumeItem,\n RunErrorEvent,\n RunFinishedEvent,\n StreamChunk,\n UIMessage,\n} from '@tanstack/ai/client'\nimport type { ChatFetcher, ChatPendingInterrupt } from './types'\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 (\n requestRunId ??\n ('runId' in chunk && typeof chunk.runId === 'string'\n ? chunk.runId\n : undefined)\n )\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/** 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/**\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 const synthetic: RunFinishedEvent = {\n type: EventType.RUN_FINISHED,\n threadId: lastThreadId ?? fallbackIds?.threadId ?? '',\n runId: lastRunId ?? fallbackIds?.runId ?? '',\n model: lastModel ?? '',\n timestamp: Date.now(),\n finishReason: 'stop',\n }\n yield { chunk: synthetic }\n return\n }\n const chunk = 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 if ('model' in chunk && typeof chunk.model === 'string') {\n lastModel = chunk.model\n }\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: parsed.chunk, id: parsed.id }\n } else {\n yield { chunk: parsed as StreamChunk }\n }\n }\n}\n\nfunction assertResponseOk(response: Response): void {\n if (!response.ok) {\n throw new Error(\n `HTTP error! status: ${response.status} ${response.statusText}`,\n )\n }\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 assertResponseOk(response)\n const data = (await response.json()) as {\n messages?: Array<UIMessage>\n activeRun?: { runId?: unknown } | null\n interrupts?: { runId?: unknown; pending?: unknown } | 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) ? data.messages : [],\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 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 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 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 // Retains every delivered offset for the run's lifetime. Intentionally bounded\n // by run length (not evicted): a conforming server replays strictly after the\n // acknowledged offset, so this only needs to catch the single boundary event\n // on reconnect, but keeping the full set keeps de-dup correct even if a server\n // replays a wider overlap.\n const seen = new Set<string>()\n let lastEventId: string | undefined\n const reconnect = resolveReconnectOptions(reconnectOptions)\n let reconnectAttempts = 0\n\n // Throttle before re-issuing the request, and enforce the total ceiling so a\n // producer that keeps dropping after each event is bounded rather than\n // reconnecting forever.\n // Bound only CONSECUTIVE no-progress reconnects. A reconnect that made forward\n // progress resets the counter, so a healthy long run (even one whose socket\n // rolls after every event) never approaches the ceiling; it fires only when\n // the run is genuinely stuck — reconnecting repeatedly with nothing new.\n async function waitBeforeReconnect(madeProgress: boolean): Promise<void> {\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, abortSignal)\n }\n\n for (;;) {\n if (abortSignal?.aborted) return\n const extraHeaders: Record<string, string> =\n lastEventId !== undefined ? { 'Last-Event-ID': lastEventId } : {}\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 (id !== undefined) {\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 } else {\n if (seen.has(id)) continue\n seen.add(id)\n lastEventId = id\n }\n }\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 lastEventId !== undefined\n ) {\n await waitBeforeReconnect(progressed)\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 (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 waitBeforeReconnect(true)\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}\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: { runId: string; pending: Array<ChatPendingInterrupt> } | 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 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 const synthetic: RunFinishedEvent = {\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 model: 'connect-wrapper',\n timestamp: Date.now(),\n finishReason: 'stop',\n }\n push(synthetic, runContext?.runId)\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 },\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 as Array<UIMessage>)\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 ...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 ...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 = new Error(`XHR error! status: ${xhr.status} ${xhr.statusText}`)\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 = new Error(`XHR error! status: ${xhr.status} ${xhr.statusText}`)\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 // 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\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 },\n { signal: abortSignal },\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.23.
|
|
3
|
+
"version": "0.23.2",
|
|
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",
|
|
@@ -49,9 +49,9 @@
|
|
|
49
49
|
"src"
|
|
50
50
|
],
|
|
51
51
|
"dependencies": {
|
|
52
|
-
"@tanstack/ai": "0.
|
|
53
|
-
"@tanstack/ai-event-client": "0.
|
|
54
|
-
"@tanstack/ai-utils": "0.4.0"
|
|
52
|
+
"@tanstack/ai": "^0.44.0",
|
|
53
|
+
"@tanstack/ai-event-client": "^0.8.0",
|
|
54
|
+
"@tanstack/ai-utils": "^0.4.0"
|
|
55
55
|
},
|
|
56
56
|
"devDependencies": {
|
|
57
57
|
"@standard-schema/spec": "^1.1.0",
|
|
@@ -1012,7 +1012,9 @@ export function normalizeConnectionAdapter(
|
|
|
1012
1012
|
|
|
1013
1013
|
// If the connect stream ended cleanly without a terminal event,
|
|
1014
1014
|
// synthesize RUN_FINISHED so request-scoped consumers can complete.
|
|
1015
|
-
//
|
|
1015
|
+
// The event payload may carry an upstream/provider runId when one was
|
|
1016
|
+
// observed, but stamp the caller's request runId so getChunkRunId()
|
|
1017
|
+
// correlates to activeRunIds / currentRunId (same as real stream chunks).
|
|
1016
1018
|
if (!abortSignal?.aborted && !hasTerminalEvent) {
|
|
1017
1019
|
const synthetic: RunFinishedEvent = {
|
|
1018
1020
|
type: EventType.RUN_FINISHED,
|
|
@@ -1028,7 +1030,7 @@ export function normalizeConnectionAdapter(
|
|
|
1028
1030
|
timestamp: Date.now(),
|
|
1029
1031
|
finishReason: 'stop',
|
|
1030
1032
|
}
|
|
1031
|
-
push(synthetic)
|
|
1033
|
+
push(synthetic, runContext?.runId)
|
|
1032
1034
|
}
|
|
1033
1035
|
} catch (err) {
|
|
1034
1036
|
if (!abortSignal?.aborted && !hasTerminalEvent) {
|
|
@@ -1052,7 +1054,7 @@ export function normalizeConnectionAdapter(
|
|
|
1052
1054
|
timestamp: Date.now(),
|
|
1053
1055
|
message,
|
|
1054
1056
|
}
|
|
1055
|
-
push(synthetic)
|
|
1057
|
+
push(synthetic, runContext?.runId)
|
|
1056
1058
|
} catch {
|
|
1057
1059
|
// fall through to rethrow the original error
|
|
1058
1060
|
}
|