@tanstack/ai-client 0.22.1 → 0.23.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -1
- package/dist/esm/audio-recorder.js +190 -213
- package/dist/esm/audio-recorder.js.map +1 -1
- package/dist/esm/chat-client.d.ts +172 -3
- package/dist/esm/chat-client.js +1656 -1386
- package/dist/esm/chat-client.js.map +1 -1
- package/dist/esm/cleared-stream-tracker.d.ts +23 -0
- package/dist/esm/cleared-stream-tracker.js +97 -0
- package/dist/esm/cleared-stream-tracker.js.map +1 -0
- package/dist/esm/client-persistor.d.ts +25 -12
- package/dist/esm/client-persistor.js +260 -235
- package/dist/esm/client-persistor.js.map +1 -1
- package/dist/esm/connection-adapters.d.ts +231 -10
- package/dist/esm/connection-adapters.js +989 -574
- package/dist/esm/connection-adapters.js.map +1 -1
- package/dist/esm/devtools-noop.d.ts +1 -0
- package/dist/esm/devtools-noop.js +79 -139
- package/dist/esm/devtools-noop.js.map +1 -1
- package/dist/esm/devtools.d.ts +31 -1
- package/dist/esm/devtools.js +977 -1127
- package/dist/esm/devtools.js.map +1 -1
- package/dist/esm/events.js +224 -226
- package/dist/esm/events.js.map +1 -1
- package/dist/esm/generation-client.d.ts +145 -2
- package/dist/esm/generation-client.js +659 -321
- package/dist/esm/generation-client.js.map +1 -1
- package/dist/esm/generation-reconstruct.d.ts +21 -0
- package/dist/esm/generation-reconstruct.js +85 -0
- package/dist/esm/generation-reconstruct.js.map +1 -0
- package/dist/esm/generation-types.d.ts +289 -3
- package/dist/esm/generation-types.js +356 -13
- package/dist/esm/generation-types.js.map +1 -1
- package/dist/esm/index.d.ts +9 -4
- package/dist/esm/index.js +7 -39
- package/dist/esm/interrupt-manager.d.ts +77 -0
- package/dist/esm/interrupt-manager.js +787 -0
- package/dist/esm/interrupt-manager.js.map +1 -0
- package/dist/esm/mcp-app-bridge.js +56 -64
- package/dist/esm/mcp-app-bridge.js.map +1 -1
- package/dist/esm/realtime-client.js +366 -440
- package/dist/esm/realtime-client.js.map +1 -1
- package/dist/esm/response-stream.js +19 -26
- package/dist/esm/response-stream.js.map +1 -1
- package/dist/esm/sse-parser.js +44 -47
- package/dist/esm/sse-parser.js.map +1 -1
- package/dist/esm/sse-utils.js +8 -9
- package/dist/esm/sse-utils.js.map +1 -1
- package/dist/esm/storage-adapters.d.ts +62 -0
- package/dist/esm/storage-adapters.js +174 -0
- package/dist/esm/storage-adapters.js.map +1 -0
- package/dist/esm/types.d.ts +212 -10
- package/dist/esm/types.js +38 -7
- package/dist/esm/types.js.map +1 -1
- package/dist/esm/video-generation-client.d.ts +113 -2
- package/dist/esm/video-generation-client.js +665 -379
- package/dist/esm/video-generation-client.js.map +1 -1
- package/package.json +7 -7
- package/src/chat-client.ts +1079 -61
- package/src/cleared-stream-tracker.ts +151 -0
- package/src/client-persistor.ts +102 -33
- package/src/connection-adapters.ts +1185 -142
- package/src/devtools-noop.ts +4 -3
- package/src/devtools.ts +121 -3
- package/src/generation-client.ts +563 -13
- package/src/generation-reconstruct.ts +121 -0
- package/src/generation-types.ts +727 -3
- package/src/index.ts +56 -1
- package/src/interrupt-manager.ts +1440 -0
- package/src/storage-adapters.ts +242 -0
- package/src/types.ts +301 -9
- package/src/video-generation-client.ts +479 -13
- package/dist/esm/index.js.map +0 -1
|
@@ -1,611 +1,1026 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { getResponseStreamReader, createResponseStreamTextDecoder } from "./response-stream.js";
|
|
1
|
+
import { createResponseStreamTextDecoder, getResponseStreamReader } from "./response-stream.js";
|
|
3
2
|
import { parseSseDataLine } from "./sse-utils.js";
|
|
4
|
-
|
|
3
|
+
import { EventType, uiMessagesToWire } from "@tanstack/ai/client";
|
|
4
|
+
//#region src/connection-adapters.ts
|
|
5
|
+
/**
|
|
6
|
+
* Associates connect-wrapped chunks with the run they were produced under.
|
|
7
|
+
* Content events (TEXT_MESSAGE_CONTENT, TOOL_CALL_*, …) carry no `runId` of
|
|
8
|
+
* their own, so the connect wrapper stamps the caller's run id here. Lets
|
|
9
|
+
* run-scoped consumers (e.g. clear-during-stream suppression) attribute those
|
|
10
|
+
* otherwise-runless chunks to their originating request.
|
|
11
|
+
*/
|
|
12
|
+
var chunkRunIds = /* @__PURE__ */ new WeakMap();
|
|
13
|
+
/**
|
|
14
|
+
* Resolve a chunk's run id, preferring the value on the chunk itself
|
|
15
|
+
* (RUN_STARTED / RUN_FINISHED / RUN_ERROR carry one) and falling back to the
|
|
16
|
+
* run the connect wrapper stamped it with.
|
|
17
|
+
*/
|
|
5
18
|
function getChunkRunId(chunk) {
|
|
6
|
-
|
|
19
|
+
return chunkRunIds.get(chunk) ?? ("runId" in chunk && typeof chunk.runId === "string" ? chunk.runId : void 0);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Thrown when an SSE/HTTP stream ends with a non-empty unterminated buffer.
|
|
23
|
+
* Indicates the connection was cut mid-line (server crash, dropped TCP, proxy
|
|
24
|
+
* timeout) so the partial content cannot be safely parsed.
|
|
25
|
+
*/
|
|
26
|
+
var StreamTruncatedError = class extends Error {
|
|
27
|
+
constructor() {
|
|
28
|
+
super("Stream ended with unterminated trailing data — connection was likely cut short.");
|
|
29
|
+
this.name = "StreamTruncatedError";
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
var StreamReadError = class extends Error {
|
|
33
|
+
constructor(cause) {
|
|
34
|
+
super("Stream response body read failed", { cause });
|
|
35
|
+
this.name = "StreamReadError";
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* Thrown when a durable (id-tagged) run's stream ends with no terminal event
|
|
40
|
+
* and a reconnect makes no forward progress — the run cannot complete, so the
|
|
41
|
+
* consumer must not be left silently hanging on a stream that just stops.
|
|
42
|
+
*/
|
|
43
|
+
var DurableStreamIncompleteError = class extends Error {
|
|
44
|
+
constructor() {
|
|
45
|
+
super("Durable run ended without a terminal event and could not resume — the run did not complete.");
|
|
46
|
+
this.name = "DurableStreamIncompleteError";
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* Thrown when a durable run exceeds its reconnect ceiling. Bounds the
|
|
51
|
+
* otherwise-unbounded reconnect loop so a flapping producer (or a proxy that
|
|
52
|
+
* rolls the socket after every event) surfaces a failure instead of
|
|
53
|
+
* reconnecting without end.
|
|
54
|
+
*/
|
|
55
|
+
var StreamReconnectLimitError = class extends Error {
|
|
56
|
+
constructor(attempts) {
|
|
57
|
+
super(`Durable run exceeded its reconnect ceiling of ${attempts} attempts — giving up.`);
|
|
58
|
+
this.name = "StreamReconnectLimitError";
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
function resolveReconnectOptions(options) {
|
|
62
|
+
const maxAttempts = options?.maxAttempts ?? 5;
|
|
63
|
+
const delayMs = options?.delayMs ?? 250;
|
|
64
|
+
if (!Number.isInteger(maxAttempts) || maxAttempts < 0) throw new Error(`Invalid reconnect.maxAttempts: ${maxAttempts}. Must be a non-negative integer.`);
|
|
65
|
+
if (!Number.isFinite(delayMs) || delayMs < 0) throw new Error(`Invalid reconnect.delayMs: ${delayMs}. Must be a non-negative finite number.`);
|
|
66
|
+
return {
|
|
67
|
+
maxAttempts,
|
|
68
|
+
delayMs
|
|
69
|
+
};
|
|
7
70
|
}
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
71
|
+
/** Resolve after `ms`, or immediately once `signal` aborts. Never rejects. */
|
|
72
|
+
function abortableDelay(ms, signal) {
|
|
73
|
+
if (ms <= 0 || signal?.aborted) return Promise.resolve();
|
|
74
|
+
return new Promise((resolve) => {
|
|
75
|
+
const onAbort = () => {
|
|
76
|
+
clearTimeout(timer);
|
|
77
|
+
resolve();
|
|
78
|
+
};
|
|
79
|
+
const timer = setTimeout(() => {
|
|
80
|
+
signal?.removeEventListener("abort", onAbort);
|
|
81
|
+
resolve();
|
|
82
|
+
}, ms);
|
|
83
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
84
|
+
});
|
|
15
85
|
}
|
|
16
86
|
function generateRunId(prefix) {
|
|
17
|
-
|
|
87
|
+
return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
18
88
|
}
|
|
89
|
+
/**
|
|
90
|
+
* Asserts an id is present when synthesizing a terminal event. The chat
|
|
91
|
+
* client always supplies `runContext.threadId` / `runContext.runId`, so an
|
|
92
|
+
* absent id at this layer indicates the adapter was wired up by a caller
|
|
93
|
+
* that bypassed that contract — surface it rather than fabricating one.
|
|
94
|
+
*/
|
|
19
95
|
function requireSyntheticId(value, field) {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
`Cannot synthesize terminal event: ${field} not supplied via runContext and not observed in the upstream stream.`
|
|
23
|
-
);
|
|
24
|
-
}
|
|
25
|
-
return value;
|
|
96
|
+
if (!value) throw new Error(`Cannot synthesize terminal event: ${field} not supplied via runContext and not observed in the upstream stream.`);
|
|
97
|
+
return value;
|
|
26
98
|
}
|
|
99
|
+
/**
|
|
100
|
+
* Merge custom headers into request headers
|
|
101
|
+
*/
|
|
27
102
|
function mergeHeaders(customHeaders) {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
}
|
|
38
|
-
return customHeaders;
|
|
103
|
+
if (!customHeaders) return {};
|
|
104
|
+
if (customHeaders instanceof Headers) {
|
|
105
|
+
const result = {};
|
|
106
|
+
customHeaders.forEach((value, key) => {
|
|
107
|
+
result[key] = value;
|
|
108
|
+
});
|
|
109
|
+
return result;
|
|
110
|
+
}
|
|
111
|
+
return customHeaders;
|
|
39
112
|
}
|
|
113
|
+
/**
|
|
114
|
+
* Request header carrying the client-chosen run id to a delivery-durability
|
|
115
|
+
* sink. The durable log is then keyed by the SAME id the client already holds,
|
|
116
|
+
* so a later join/resume can address the run without first reading back a
|
|
117
|
+
* server-generated id. Sent as a header — NOT a query param — so the POST URL
|
|
118
|
+
* stays byte-identical to a plain, non-durable request; a server that isn't
|
|
119
|
+
* durable simply ignores the header. (The GET join path keeps `?runId` in the
|
|
120
|
+
* query, since a GET has no body/handler contract to disturb.)
|
|
121
|
+
*/
|
|
122
|
+
var RUN_ID_HEADER = "X-Run-Id";
|
|
123
|
+
function runIdHeader(runId) {
|
|
124
|
+
return runId === void 0 ? {} : { [RUN_ID_HEADER]: runId };
|
|
125
|
+
}
|
|
126
|
+
function withSearchParams(url, values) {
|
|
127
|
+
const hashIndex = url.indexOf("#");
|
|
128
|
+
const hash = hashIndex === -1 ? "" : url.slice(hashIndex);
|
|
129
|
+
const withoutHash = hashIndex === -1 ? url : url.slice(0, hashIndex);
|
|
130
|
+
const queryIndex = withoutHash.indexOf("?");
|
|
131
|
+
const base = queryIndex === -1 ? withoutHash : withoutHash.slice(0, queryIndex);
|
|
132
|
+
const search = new URLSearchParams(queryIndex === -1 ? "" : withoutHash.slice(queryIndex + 1));
|
|
133
|
+
for (const [key, value] of Object.entries(values)) search.set(key, value);
|
|
134
|
+
const query = search.toString();
|
|
135
|
+
return `${base}${query.length === 0 ? "" : `?${query}`}${hash}`;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Read lines from a stream (newline-delimited)
|
|
139
|
+
*/
|
|
40
140
|
async function* readStreamLines(reader, abortSignal) {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
141
|
+
try {
|
|
142
|
+
const decoder = createResponseStreamTextDecoder();
|
|
143
|
+
let buffer = "";
|
|
144
|
+
while (!abortSignal?.aborted) {
|
|
145
|
+
let result;
|
|
146
|
+
try {
|
|
147
|
+
result = await reader.read();
|
|
148
|
+
} catch (error) {
|
|
149
|
+
if (abortSignal?.aborted) return;
|
|
150
|
+
throw new StreamReadError(error);
|
|
151
|
+
}
|
|
152
|
+
const { done, value } = result;
|
|
153
|
+
if (done) break;
|
|
154
|
+
buffer += decoder.decode(value, { stream: true });
|
|
155
|
+
const lines = buffer.split("\n");
|
|
156
|
+
buffer = lines.pop() || "";
|
|
157
|
+
for (const line of lines) {
|
|
158
|
+
const normalized = line.endsWith("\r") ? line.slice(0, -1) : line;
|
|
159
|
+
if (normalized.trim()) yield normalized;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
buffer += decoder.decode();
|
|
163
|
+
if (buffer.trim() && !abortSignal?.aborted) throw new StreamTruncatedError();
|
|
164
|
+
} finally {
|
|
165
|
+
reader.releaseLock();
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Type guard for a durable NDJSON envelope `{ id, chunk }`. NDJSON has no
|
|
170
|
+
* native event-id field, so durability rides the offset inside the payload.
|
|
171
|
+
* A bare `StreamChunk` always has a top-level `type`, and the envelope never
|
|
172
|
+
* does, so the two forms are unambiguous — a non-durable line stays bare.
|
|
173
|
+
*/
|
|
174
|
+
function isNdjsonEnvelope(value) {
|
|
175
|
+
return typeof value === "object" && value !== null && "chunk" in value && "id" in value && typeof value.id === "string" && !("type" in value);
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Parse SSE-format lines into stream events, pairing each chunk with the `id:`
|
|
179
|
+
* offset of the event it arrived on. Shared by the fetch- and XHR-backed SSE
|
|
180
|
+
* adapters so both track delivery offsets identically.
|
|
181
|
+
*
|
|
182
|
+
* Accepts either `data: {...}` lines or bare JSON lines. Skips comments
|
|
183
|
+
* starting with `:` (proxies and CDNs inject these as keepalives) and the
|
|
184
|
+
* `event:` / `retry:` SSE control fields. A `[DONE]` sentinel is treated as a
|
|
185
|
+
* terminal event: a synthesized RUN_FINISHED is yielded using the most recent
|
|
186
|
+
* upstream `threadId` / `runId` (falling back to `fallbackIds`), so the
|
|
187
|
+
* consumer sees a clean terminal event with real correlation ids.
|
|
188
|
+
*
|
|
189
|
+
* A JSON parse failure throws — the consumer surfaces it as an error.
|
|
190
|
+
*/
|
|
191
|
+
async function* linesToSSEEvents(lines, fallbackIds) {
|
|
192
|
+
let lastThreadId;
|
|
193
|
+
let lastRunId;
|
|
194
|
+
let lastModel;
|
|
195
|
+
let pendingId;
|
|
196
|
+
for await (const line of lines) {
|
|
197
|
+
if (line === "id" || line.startsWith("id:")) {
|
|
198
|
+
const rawId = line === "id" ? "" : line.slice(3);
|
|
199
|
+
pendingId = rawId.startsWith(" ") ? rawId.slice(1) : rawId;
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
if (line.startsWith(":") || line.startsWith("event:") || line.startsWith("retry:")) continue;
|
|
203
|
+
const data = parseSseDataLine(line);
|
|
204
|
+
if (data === "[DONE]") {
|
|
205
|
+
yield { chunk: {
|
|
206
|
+
type: EventType.RUN_FINISHED,
|
|
207
|
+
threadId: lastThreadId ?? fallbackIds?.threadId ?? "",
|
|
208
|
+
runId: lastRunId ?? fallbackIds?.runId ?? "",
|
|
209
|
+
model: lastModel ?? "",
|
|
210
|
+
timestamp: Date.now(),
|
|
211
|
+
finishReason: "stop"
|
|
212
|
+
} };
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
const chunk = JSON.parse(data);
|
|
216
|
+
if ("threadId" in chunk && typeof chunk.threadId === "string") lastThreadId = chunk.threadId;
|
|
217
|
+
if ("runId" in chunk && typeof chunk.runId === "string") lastRunId = chunk.runId;
|
|
218
|
+
if ("model" in chunk && typeof chunk.model === "string") lastModel = chunk.model;
|
|
219
|
+
const id = pendingId;
|
|
220
|
+
pendingId = void 0;
|
|
221
|
+
yield {
|
|
222
|
+
chunk,
|
|
223
|
+
...id !== void 0 ? { id } : {}
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Parse NDJSON-format lines into stream events. Durable streams emit each line
|
|
229
|
+
* as an `{ id, chunk }` envelope carrying the delivery offset; non-durable
|
|
230
|
+
* streams emit bare chunks. Both are auto-detected (see {@link isNdjsonEnvelope}),
|
|
231
|
+
* so an untagged stream behaves exactly as a plain single fetch used to.
|
|
232
|
+
*/
|
|
233
|
+
async function* linesToNdjsonEvents(lines) {
|
|
234
|
+
for await (const line of lines) {
|
|
235
|
+
const parsed = JSON.parse(line);
|
|
236
|
+
if (isNdjsonEnvelope(parsed)) yield {
|
|
237
|
+
chunk: parsed.chunk,
|
|
238
|
+
id: parsed.id
|
|
239
|
+
};
|
|
240
|
+
else yield { chunk: parsed };
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
function assertResponseOk(response) {
|
|
244
|
+
if (!response.ok) throw new Error(`HTTP error! status: ${response.status} ${response.statusText}`);
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* GET the hydration endpoint for a thread and parse its JSON `{ messages,
|
|
248
|
+
* activeRun }` body. This is the transport-agnostic reconnect probe: keyed on
|
|
249
|
+
* the STABLE thread id, it returns the stored transcript and — if a run is still
|
|
250
|
+
* generating — a cursor the caller tails via `joinRun`. Shared by every fetch/
|
|
251
|
+
* XHR adapter so the client never has to know which transport is in use.
|
|
252
|
+
*/
|
|
253
|
+
async function fetchThreadHydration(fetchClient, url, headers, credentials, threadId) {
|
|
254
|
+
const response = await fetchClient(withSearchParams(url, { threadId }), {
|
|
255
|
+
method: "GET",
|
|
256
|
+
headers: {
|
|
257
|
+
Accept: "application/json",
|
|
258
|
+
...headers
|
|
259
|
+
},
|
|
260
|
+
credentials
|
|
261
|
+
});
|
|
262
|
+
assertResponseOk(response);
|
|
263
|
+
const data = await response.json();
|
|
264
|
+
const activeRun = data.activeRun && typeof data.activeRun.runId === "string" ? { runId: data.activeRun.runId } : null;
|
|
265
|
+
const interrupts = data.interrupts && typeof data.interrupts.runId === "string" && Array.isArray(data.interrupts.pending) && data.interrupts.pending.length > 0 ? {
|
|
266
|
+
runId: data.interrupts.runId,
|
|
267
|
+
pending: data.interrupts.pending
|
|
268
|
+
} : null;
|
|
269
|
+
return {
|
|
270
|
+
messages: Array.isArray(data.messages) ? data.messages : [],
|
|
271
|
+
activeRun,
|
|
272
|
+
interrupts
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* GET the hydration endpoint for a generation thread and parse its JSON
|
|
277
|
+
* `{ resumeSnapshot, activeRun }` body. Mirrors {@link fetchThreadHydration} for
|
|
278
|
+
* the generation clients: keyed on the stable thread id, it returns the last
|
|
279
|
+
* generation's resume snapshot (re-validated client-side before adoption) and —
|
|
280
|
+
* if a run is still generating — a cursor. Shared by every fetch/XHR adapter.
|
|
281
|
+
*/
|
|
282
|
+
async function fetchGenerationHydration(fetchClient, url, headers, credentials, threadId) {
|
|
283
|
+
const response = await fetchClient(withSearchParams(url, { threadId }), {
|
|
284
|
+
method: "GET",
|
|
285
|
+
headers: {
|
|
286
|
+
Accept: "application/json",
|
|
287
|
+
...headers
|
|
288
|
+
},
|
|
289
|
+
credentials
|
|
290
|
+
});
|
|
291
|
+
assertResponseOk(response);
|
|
292
|
+
const raw = await response.json();
|
|
293
|
+
if (raw === null) return {
|
|
294
|
+
resumeSnapshot: null,
|
|
295
|
+
activeRun: null
|
|
296
|
+
};
|
|
297
|
+
if (typeof raw !== "object" || Array.isArray(raw)) throw new Error(`Generation hydration expected a JSON object from ${url}, received ${Array.isArray(raw) ? "an array" : typeof raw}.`);
|
|
298
|
+
const data = raw;
|
|
299
|
+
const activeRun = data.activeRun && typeof data.activeRun.runId === "string" ? { runId: data.activeRun.runId } : null;
|
|
300
|
+
return {
|
|
301
|
+
resumeSnapshot: data.resumeSnapshot ?? null,
|
|
302
|
+
activeRun
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
/** Yield SSE stream events (chunk + offset) from a fetch Response body. */
|
|
306
|
+
async function* responseToSSEEvents(response, abortSignal, fallbackIds) {
|
|
307
|
+
assertResponseOk(response);
|
|
308
|
+
yield* linesToSSEEvents(readStreamLines(getResponseStreamReader(response), abortSignal), fallbackIds);
|
|
309
|
+
}
|
|
310
|
+
/** Yield NDJSON stream events (chunk + offset) from a fetch Response body. */
|
|
311
|
+
async function* responseToNdjsonEvents(response, abortSignal) {
|
|
312
|
+
assertResponseOk(response);
|
|
313
|
+
yield* linesToNdjsonEvents(readStreamLines(getResponseStreamReader(response), abortSignal));
|
|
62
314
|
}
|
|
63
315
|
async function* responseToSSEChunks(response, abortSignal) {
|
|
64
|
-
|
|
65
|
-
throw new Error(
|
|
66
|
-
`HTTP error! status: ${response.status} ${response.statusText}`
|
|
67
|
-
);
|
|
68
|
-
}
|
|
69
|
-
const reader = getResponseStreamReader(response);
|
|
70
|
-
let lastThreadId;
|
|
71
|
-
let lastRunId;
|
|
72
|
-
let lastModel;
|
|
73
|
-
for await (const line of readStreamLines(reader, abortSignal)) {
|
|
74
|
-
if (line.startsWith(":") || line.startsWith("event:") || line.startsWith("id:") || line.startsWith("retry:")) {
|
|
75
|
-
continue;
|
|
76
|
-
}
|
|
77
|
-
const data = parseSseDataLine(line);
|
|
78
|
-
if (data === "[DONE]") {
|
|
79
|
-
const synthetic = {
|
|
80
|
-
type: EventType.RUN_FINISHED,
|
|
81
|
-
threadId: lastThreadId ?? "",
|
|
82
|
-
runId: lastRunId ?? "",
|
|
83
|
-
model: lastModel ?? "",
|
|
84
|
-
timestamp: Date.now(),
|
|
85
|
-
finishReason: "stop"
|
|
86
|
-
};
|
|
87
|
-
yield synthetic;
|
|
88
|
-
return;
|
|
89
|
-
}
|
|
90
|
-
const chunk = JSON.parse(data);
|
|
91
|
-
if ("threadId" in chunk && typeof chunk.threadId === "string") {
|
|
92
|
-
lastThreadId = chunk.threadId;
|
|
93
|
-
}
|
|
94
|
-
if ("runId" in chunk && typeof chunk.runId === "string") {
|
|
95
|
-
lastRunId = chunk.runId;
|
|
96
|
-
}
|
|
97
|
-
if ("model" in chunk && typeof chunk.model === "string") {
|
|
98
|
-
lastModel = chunk.model;
|
|
99
|
-
}
|
|
100
|
-
yield chunk;
|
|
101
|
-
}
|
|
316
|
+
for await (const { chunk } of responseToSSEEvents(response, abortSignal)) yield chunk;
|
|
102
317
|
}
|
|
318
|
+
/**
|
|
319
|
+
* Build a fetch-backed {@link StreamEventSource}. `parseResponse` decodes the
|
|
320
|
+
* body into events (SSE or NDJSON) — the reconnect engine is identical for both.
|
|
321
|
+
*/
|
|
322
|
+
function fetchEventSource(fetchClient, url, requestInit, parseResponse) {
|
|
323
|
+
return async function* (extraHeaders, abortSignal) {
|
|
324
|
+
let response;
|
|
325
|
+
try {
|
|
326
|
+
response = await fetchClient(url, {
|
|
327
|
+
...requestInit,
|
|
328
|
+
headers: {
|
|
329
|
+
...requestInit.headers,
|
|
330
|
+
...extraHeaders
|
|
331
|
+
},
|
|
332
|
+
...abortSignal ? { signal: abortSignal } : {}
|
|
333
|
+
});
|
|
334
|
+
} catch (error) {
|
|
335
|
+
throw new StreamReadError(error);
|
|
336
|
+
}
|
|
337
|
+
yield* parseResponse(response, abortSignal);
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Drive a {@link StreamEventSource} with native-style resumability. Each event's
|
|
342
|
+
* adapter-owned delivery offset (its `id`) is remembered; if the connection
|
|
343
|
+
* drops or ends before a terminal event, the source is re-opened with a
|
|
344
|
+
* `Last-Event-ID` header so the server replays strictly after the last offset.
|
|
345
|
+
* Already-seen offsets are de-duped, so an overlapping replay is safe.
|
|
346
|
+
*
|
|
347
|
+
* When the server does NOT tag events (no durability), no offset is ever seen,
|
|
348
|
+
* so no reconnect happens — behaviour is identical to a plain single request.
|
|
349
|
+
* This engine is transport-agnostic: fetch/XHR × SSE/NDJSON all share it, the
|
|
350
|
+
* only difference being the {@link StreamEventSource} they pass in.
|
|
351
|
+
*/
|
|
352
|
+
async function* resumableStream(openEventSource, abortSignal, reconnectOptions) {
|
|
353
|
+
const seen = /* @__PURE__ */ new Set();
|
|
354
|
+
let lastEventId;
|
|
355
|
+
const reconnect = resolveReconnectOptions(reconnectOptions);
|
|
356
|
+
let reconnectAttempts = 0;
|
|
357
|
+
async function waitBeforeReconnect(madeProgress) {
|
|
358
|
+
if (madeProgress) reconnectAttempts = 0;
|
|
359
|
+
else {
|
|
360
|
+
reconnectAttempts += 1;
|
|
361
|
+
if (reconnectAttempts > reconnect.maxAttempts) throw new StreamReconnectLimitError(reconnect.maxAttempts);
|
|
362
|
+
}
|
|
363
|
+
await abortableDelay(reconnect.delayMs, abortSignal);
|
|
364
|
+
}
|
|
365
|
+
for (;;) {
|
|
366
|
+
if (abortSignal?.aborted) return;
|
|
367
|
+
const extraHeaders = lastEventId !== void 0 ? { "Last-Event-ID": lastEventId } : {};
|
|
368
|
+
let sawTerminal = false;
|
|
369
|
+
let progressed = false;
|
|
370
|
+
try {
|
|
371
|
+
for await (const { chunk, id } of openEventSource(extraHeaders, abortSignal)) {
|
|
372
|
+
if (id !== void 0) if (id === "") {
|
|
373
|
+
lastEventId = void 0;
|
|
374
|
+
seen.clear();
|
|
375
|
+
} else {
|
|
376
|
+
if (seen.has(id)) continue;
|
|
377
|
+
seen.add(id);
|
|
378
|
+
lastEventId = id;
|
|
379
|
+
}
|
|
380
|
+
progressed = true;
|
|
381
|
+
if (chunk.type === "RUN_FINISHED" || chunk.type === "RUN_ERROR") sawTerminal = true;
|
|
382
|
+
yield chunk;
|
|
383
|
+
}
|
|
384
|
+
} catch (error) {
|
|
385
|
+
if (abortSignal?.aborted) return;
|
|
386
|
+
if ((error instanceof StreamTruncatedError || error instanceof StreamReadError) && lastEventId !== void 0) {
|
|
387
|
+
await waitBeforeReconnect(progressed);
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
throw error;
|
|
391
|
+
}
|
|
392
|
+
if (abortSignal?.aborted) return;
|
|
393
|
+
if (sawTerminal) return;
|
|
394
|
+
if (lastEventId !== void 0) {
|
|
395
|
+
if (progressed) {
|
|
396
|
+
await waitBeforeReconnect(true);
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
throw new DurableStreamIncompleteError();
|
|
400
|
+
}
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* Normalize a ConnectionAdapter to subscribe/send operations.
|
|
406
|
+
*
|
|
407
|
+
* If a connection provides native subscribe/send, that mode is used.
|
|
408
|
+
* Otherwise, connect() is wrapped using an async queue.
|
|
409
|
+
*/
|
|
103
410
|
function normalizeConnectionAdapter(connection) {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
threadId: requireSyntheticId(
|
|
192
|
-
upstreamThreadId ?? runContext?.threadId,
|
|
193
|
-
"threadId"
|
|
194
|
-
),
|
|
195
|
-
runId: requireSyntheticId(
|
|
196
|
-
upstreamRunId ?? runContext?.runId,
|
|
197
|
-
"runId"
|
|
198
|
-
),
|
|
199
|
-
model: "connect-wrapper",
|
|
200
|
-
timestamp: Date.now(),
|
|
201
|
-
finishReason: "stop"
|
|
202
|
-
};
|
|
203
|
-
push(synthetic);
|
|
204
|
-
}
|
|
205
|
-
} catch (err) {
|
|
206
|
-
if (!abortSignal?.aborted && !hasTerminalEvent) {
|
|
207
|
-
const message = err instanceof Error ? err.message : "Unknown error in connect()";
|
|
208
|
-
const synthetic = {
|
|
209
|
-
type: EventType.RUN_ERROR,
|
|
210
|
-
threadId: requireSyntheticId(
|
|
211
|
-
upstreamThreadId ?? runContext?.threadId,
|
|
212
|
-
"threadId"
|
|
213
|
-
),
|
|
214
|
-
runId: requireSyntheticId(
|
|
215
|
-
upstreamRunId ?? runContext?.runId,
|
|
216
|
-
"runId"
|
|
217
|
-
),
|
|
218
|
-
timestamp: Date.now(),
|
|
219
|
-
message
|
|
220
|
-
};
|
|
221
|
-
push(synthetic);
|
|
222
|
-
}
|
|
223
|
-
throw err;
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
};
|
|
411
|
+
if (!connection) throw new Error("Connection adapter is required");
|
|
412
|
+
const hasConnect = "connect" in connection;
|
|
413
|
+
const hasSubscribe = "subscribe" in connection;
|
|
414
|
+
const hasSend = "send" in connection;
|
|
415
|
+
if (hasConnect && (hasSubscribe || hasSend)) throw new Error("Connection adapter must provide either connect or both subscribe and send, not both modes");
|
|
416
|
+
if (hasSubscribe && hasSend) {
|
|
417
|
+
const joinRun = connection.joinRun?.bind(connection);
|
|
418
|
+
const hydrate = connection.hydrate?.bind(connection);
|
|
419
|
+
return {
|
|
420
|
+
subscribe: connection.subscribe.bind(connection),
|
|
421
|
+
send: connection.send.bind(connection),
|
|
422
|
+
...joinRun ? { joinRun } : {},
|
|
423
|
+
...hydrate ? { hydrate } : {}
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
if (!hasConnect) throw new Error("Connection adapter must provide either connect or both subscribe and send");
|
|
427
|
+
let activeBuffer = [];
|
|
428
|
+
let activeWaiters = [];
|
|
429
|
+
function push(chunk, runId) {
|
|
430
|
+
if (runId) chunkRunIds.set(chunk, runId);
|
|
431
|
+
const waiter = activeWaiters.shift();
|
|
432
|
+
if (waiter) waiter(chunk);
|
|
433
|
+
else activeBuffer.push(chunk);
|
|
434
|
+
}
|
|
435
|
+
return {
|
|
436
|
+
subscribe(abortSignal) {
|
|
437
|
+
const myBuffer = activeBuffer.splice(0);
|
|
438
|
+
const myWaiters = [];
|
|
439
|
+
activeBuffer = myBuffer;
|
|
440
|
+
activeWaiters = myWaiters;
|
|
441
|
+
return (async function* () {
|
|
442
|
+
while (!abortSignal?.aborted) {
|
|
443
|
+
let chunk;
|
|
444
|
+
const buffered = myBuffer.shift();
|
|
445
|
+
if (buffered !== void 0) chunk = buffered;
|
|
446
|
+
else chunk = await new Promise((resolve) => {
|
|
447
|
+
const onAbort = () => resolve(null);
|
|
448
|
+
myWaiters.push((c) => {
|
|
449
|
+
abortSignal?.removeEventListener("abort", onAbort);
|
|
450
|
+
resolve(c);
|
|
451
|
+
});
|
|
452
|
+
abortSignal?.addEventListener("abort", onAbort, { once: true });
|
|
453
|
+
});
|
|
454
|
+
if (chunk !== null) yield chunk;
|
|
455
|
+
}
|
|
456
|
+
})();
|
|
457
|
+
},
|
|
458
|
+
async send(messages, data, abortSignal, runContext) {
|
|
459
|
+
let hasTerminalEvent = false;
|
|
460
|
+
let upstreamThreadId;
|
|
461
|
+
let upstreamRunId;
|
|
462
|
+
try {
|
|
463
|
+
const stream = connection.connect(messages, data, abortSignal, runContext);
|
|
464
|
+
for await (const chunk of stream) {
|
|
465
|
+
if ("threadId" in chunk && typeof chunk.threadId === "string") upstreamThreadId = chunk.threadId;
|
|
466
|
+
if ("runId" in chunk && typeof chunk.runId === "string") upstreamRunId = chunk.runId;
|
|
467
|
+
if (chunk.type === "RUN_FINISHED" || chunk.type === "RUN_ERROR") hasTerminalEvent = true;
|
|
468
|
+
push(chunk, runContext?.runId);
|
|
469
|
+
}
|
|
470
|
+
if (!abortSignal?.aborted && !hasTerminalEvent) push({
|
|
471
|
+
type: EventType.RUN_FINISHED,
|
|
472
|
+
threadId: requireSyntheticId(upstreamThreadId ?? runContext?.threadId, "threadId"),
|
|
473
|
+
runId: requireSyntheticId(upstreamRunId ?? runContext?.runId, "runId"),
|
|
474
|
+
model: "connect-wrapper",
|
|
475
|
+
timestamp: Date.now(),
|
|
476
|
+
finishReason: "stop"
|
|
477
|
+
});
|
|
478
|
+
} catch (err) {
|
|
479
|
+
if (!abortSignal?.aborted && !hasTerminalEvent) try {
|
|
480
|
+
const message = err instanceof Error ? err.message : "Unknown error in connect()";
|
|
481
|
+
push({
|
|
482
|
+
type: EventType.RUN_ERROR,
|
|
483
|
+
threadId: requireSyntheticId(upstreamThreadId ?? runContext?.threadId, "threadId"),
|
|
484
|
+
runId: requireSyntheticId(upstreamRunId ?? runContext?.runId, "runId"),
|
|
485
|
+
timestamp: Date.now(),
|
|
486
|
+
message
|
|
487
|
+
});
|
|
488
|
+
} catch {}
|
|
489
|
+
throw err;
|
|
490
|
+
}
|
|
491
|
+
},
|
|
492
|
+
...typeof connection.joinRun === "function" ? { joinRun: (runId, abortSignal) => connection.joinRun(runId, abortSignal) } : {},
|
|
493
|
+
...(() => {
|
|
494
|
+
const hydrate = connection.hydrate;
|
|
495
|
+
return typeof hydrate === "function" ? { hydrate: (threadId) => hydrate(threadId) } : {};
|
|
496
|
+
})()
|
|
497
|
+
};
|
|
227
498
|
}
|
|
228
499
|
function buildRunAgentInputBody(messages, data, runContext, options) {
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
data: { ...forwardedProps }
|
|
248
|
-
};
|
|
500
|
+
const wireMessages = uiMessagesToWire(messages);
|
|
501
|
+
const forwardedProps = {
|
|
502
|
+
...options.body,
|
|
503
|
+
...runContext?.forwardedProps ?? {},
|
|
504
|
+
...data
|
|
505
|
+
};
|
|
506
|
+
return {
|
|
507
|
+
threadId: runContext?.threadId ?? generateRunId("thread"),
|
|
508
|
+
runId: runContext?.runId ?? generateRunId("run"),
|
|
509
|
+
...runContext?.parentRunId !== void 0 && { parentRunId: runContext.parentRunId },
|
|
510
|
+
...runContext?.resume !== void 0 && { resume: runContext.resume },
|
|
511
|
+
state: {},
|
|
512
|
+
messages: wireMessages,
|
|
513
|
+
tools: runContext?.clientTools ?? [],
|
|
514
|
+
context: [],
|
|
515
|
+
forwardedProps,
|
|
516
|
+
data: { ...forwardedProps }
|
|
517
|
+
};
|
|
249
518
|
}
|
|
519
|
+
/**
|
|
520
|
+
* Create a Server-Sent Events connection adapter
|
|
521
|
+
*
|
|
522
|
+
* @param url - The API endpoint URL (or a function that returns the URL)
|
|
523
|
+
* @param options - Fetch options (headers, credentials, body, etc.) or a function that returns options (can be async)
|
|
524
|
+
* @returns A connection adapter for SSE streams
|
|
525
|
+
*
|
|
526
|
+
* @example
|
|
527
|
+
* ```typescript
|
|
528
|
+
* // Static URL
|
|
529
|
+
* const connection = fetchServerSentEvents('/api/chat');
|
|
530
|
+
*
|
|
531
|
+
* // Dynamic URL
|
|
532
|
+
* const connection = fetchServerSentEvents(() => `/api/chat?user=${userId}`);
|
|
533
|
+
*
|
|
534
|
+
* // With options
|
|
535
|
+
* const connection = fetchServerSentEvents('/api/chat', {
|
|
536
|
+
* headers: { 'Authorization': 'Bearer token' }
|
|
537
|
+
* });
|
|
538
|
+
*
|
|
539
|
+
* // With dynamic options
|
|
540
|
+
* const connection = fetchServerSentEvents('/api/chat', () => ({
|
|
541
|
+
* headers: { 'Authorization': `Bearer ${getToken()}` }
|
|
542
|
+
* }));
|
|
543
|
+
*
|
|
544
|
+
* // With additional body data
|
|
545
|
+
* const connection = fetchServerSentEvents('/api/chat', async () => ({
|
|
546
|
+
* body: {
|
|
547
|
+
* provider: 'openai',
|
|
548
|
+
* model: 'gpt-5.5',
|
|
549
|
+
* }
|
|
550
|
+
* }));
|
|
551
|
+
* ```
|
|
552
|
+
*/
|
|
250
553
|
function fetchServerSentEvents(url, options = {}) {
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
554
|
+
return {
|
|
555
|
+
async *connect(messages, data, abortSignal, runContext) {
|
|
556
|
+
const resolvedUrl = typeof url === "function" ? url() : url;
|
|
557
|
+
const resolvedOptions = typeof options === "function" ? await options() : options;
|
|
558
|
+
const requestHeaders = {
|
|
559
|
+
"Content-Type": "application/json",
|
|
560
|
+
...mergeHeaders(resolvedOptions.headers),
|
|
561
|
+
...runIdHeader(runContext?.runId)
|
|
562
|
+
};
|
|
563
|
+
const requestBody = buildRunAgentInputBody(messages, data, runContext, resolvedOptions);
|
|
564
|
+
const fetchClient = resolvedOptions.fetchClient ?? fetch;
|
|
565
|
+
const signal = abortSignal || resolvedOptions.signal;
|
|
566
|
+
yield* resumableStream(fetchEventSource(fetchClient, resolvedUrl, {
|
|
567
|
+
method: "POST",
|
|
568
|
+
headers: requestHeaders,
|
|
569
|
+
body: JSON.stringify(requestBody),
|
|
570
|
+
credentials: resolvedOptions.credentials || "same-origin"
|
|
571
|
+
}, (response, sseSignal) => responseToSSEEvents(response, sseSignal, {
|
|
572
|
+
...runContext?.threadId !== void 0 ? { threadId: runContext.threadId } : {},
|
|
573
|
+
...runContext?.runId !== void 0 ? { runId: runContext.runId } : {}
|
|
574
|
+
})), signal, resolvedOptions.reconnect);
|
|
575
|
+
},
|
|
576
|
+
async *joinRun(runId, abortSignal) {
|
|
577
|
+
const resolvedUrl = typeof url === "function" ? url() : url;
|
|
578
|
+
const resolvedOptions = typeof options === "function" ? await options() : options;
|
|
579
|
+
const joinUrl = withSearchParams(resolvedUrl, {
|
|
580
|
+
offset: "-1",
|
|
581
|
+
runId
|
|
582
|
+
});
|
|
583
|
+
const requestHeaders = { ...mergeHeaders(resolvedOptions.headers) };
|
|
584
|
+
const fetchClient = resolvedOptions.fetchClient ?? fetch;
|
|
585
|
+
const signal = abortSignal || resolvedOptions.signal;
|
|
586
|
+
yield* resumableStream(fetchEventSource(fetchClient, joinUrl, {
|
|
587
|
+
method: "GET",
|
|
588
|
+
headers: requestHeaders,
|
|
589
|
+
credentials: resolvedOptions.credentials || "same-origin"
|
|
590
|
+
}, (response, sseSignal) => responseToSSEEvents(response, sseSignal, { runId })), signal, resolvedOptions.reconnect);
|
|
591
|
+
},
|
|
592
|
+
async hydrate(threadId) {
|
|
593
|
+
const resolvedUrl = typeof url === "function" ? url() : url;
|
|
594
|
+
const resolvedOptions = typeof options === "function" ? await options() : options;
|
|
595
|
+
return fetchThreadHydration(resolvedOptions.fetchClient ?? fetch, resolvedUrl, mergeHeaders(resolvedOptions.headers), resolvedOptions.credentials || "same-origin", threadId);
|
|
596
|
+
},
|
|
597
|
+
async hydrateGeneration(threadId) {
|
|
598
|
+
const resolvedUrl = typeof url === "function" ? url() : url;
|
|
599
|
+
const resolvedOptions = typeof options === "function" ? await options() : options;
|
|
600
|
+
return fetchGenerationHydration(resolvedOptions.fetchClient ?? fetch, resolvedUrl, mergeHeaders(resolvedOptions.headers), resolvedOptions.credentials || "same-origin", threadId);
|
|
601
|
+
}
|
|
602
|
+
};
|
|
277
603
|
}
|
|
604
|
+
/**
|
|
605
|
+
* Create an HTTP streaming connection adapter (for raw streaming without SSE format)
|
|
606
|
+
*
|
|
607
|
+
* @param url - The API endpoint URL (or a function that returns the URL)
|
|
608
|
+
* @param options - Fetch options (headers, credentials, body, etc.) or a function that returns options (can be async)
|
|
609
|
+
* @returns A connection adapter for HTTP streams
|
|
610
|
+
*
|
|
611
|
+
* @example
|
|
612
|
+
* ```typescript
|
|
613
|
+
* // Static URL
|
|
614
|
+
* const connection = fetchHttpStream('/api/chat');
|
|
615
|
+
*
|
|
616
|
+
* // Dynamic URL
|
|
617
|
+
* const connection = fetchHttpStream(() => `/api/chat?user=${userId}`);
|
|
618
|
+
*
|
|
619
|
+
* // With options
|
|
620
|
+
* const connection = fetchHttpStream('/api/chat', {
|
|
621
|
+
* headers: { 'Authorization': 'Bearer token' }
|
|
622
|
+
* });
|
|
623
|
+
*
|
|
624
|
+
* // With dynamic options
|
|
625
|
+
* const connection = fetchHttpStream('/api/chat', () => ({
|
|
626
|
+
* headers: { 'Authorization': `Bearer ${getToken()}` }
|
|
627
|
+
* }));
|
|
628
|
+
*
|
|
629
|
+
* // With additional body data
|
|
630
|
+
* const connection = fetchHttpStream('/api/chat', async () => ({
|
|
631
|
+
* body: {
|
|
632
|
+
* provider: 'openai',
|
|
633
|
+
* model: 'gpt-5.5',
|
|
634
|
+
* }
|
|
635
|
+
* }));
|
|
636
|
+
* ```
|
|
637
|
+
*/
|
|
278
638
|
function fetchHttpStream(url, options = {}) {
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
639
|
+
return {
|
|
640
|
+
async *connect(messages, data, abortSignal, runContext) {
|
|
641
|
+
const resolvedUrl = typeof url === "function" ? url() : url;
|
|
642
|
+
const resolvedOptions = typeof options === "function" ? await options() : options;
|
|
643
|
+
const requestHeaders = {
|
|
644
|
+
"Content-Type": "application/json",
|
|
645
|
+
...mergeHeaders(resolvedOptions.headers),
|
|
646
|
+
...runIdHeader(runContext?.runId)
|
|
647
|
+
};
|
|
648
|
+
const requestBody = buildRunAgentInputBody(messages, data, runContext, resolvedOptions);
|
|
649
|
+
const fetchClient = resolvedOptions.fetchClient ?? fetch;
|
|
650
|
+
const signal = abortSignal || resolvedOptions.signal;
|
|
651
|
+
yield* resumableStream(fetchEventSource(fetchClient, resolvedUrl, {
|
|
652
|
+
method: "POST",
|
|
653
|
+
headers: requestHeaders,
|
|
654
|
+
body: JSON.stringify(requestBody),
|
|
655
|
+
credentials: resolvedOptions.credentials || "same-origin"
|
|
656
|
+
}, responseToNdjsonEvents), signal, resolvedOptions.reconnect);
|
|
657
|
+
},
|
|
658
|
+
async *joinRun(runId, abortSignal) {
|
|
659
|
+
const resolvedUrl = typeof url === "function" ? url() : url;
|
|
660
|
+
const resolvedOptions = typeof options === "function" ? await options() : options;
|
|
661
|
+
const joinUrl = withSearchParams(resolvedUrl, {
|
|
662
|
+
offset: "-1",
|
|
663
|
+
runId
|
|
664
|
+
});
|
|
665
|
+
const requestHeaders = { ...mergeHeaders(resolvedOptions.headers) };
|
|
666
|
+
const fetchClient = resolvedOptions.fetchClient ?? fetch;
|
|
667
|
+
const signal = abortSignal || resolvedOptions.signal;
|
|
668
|
+
yield* resumableStream(fetchEventSource(fetchClient, joinUrl, {
|
|
669
|
+
method: "GET",
|
|
670
|
+
headers: requestHeaders,
|
|
671
|
+
credentials: resolvedOptions.credentials || "same-origin"
|
|
672
|
+
}, responseToNdjsonEvents), signal, resolvedOptions.reconnect);
|
|
673
|
+
},
|
|
674
|
+
async hydrate(threadId) {
|
|
675
|
+
const resolvedUrl = typeof url === "function" ? url() : url;
|
|
676
|
+
const resolvedOptions = typeof options === "function" ? await options() : options;
|
|
677
|
+
return fetchThreadHydration(resolvedOptions.fetchClient ?? fetch, resolvedUrl, mergeHeaders(resolvedOptions.headers), resolvedOptions.credentials || "same-origin", threadId);
|
|
678
|
+
},
|
|
679
|
+
async hydrateGeneration(threadId) {
|
|
680
|
+
const resolvedUrl = typeof url === "function" ? url() : url;
|
|
681
|
+
const resolvedOptions = typeof options === "function" ? await options() : options;
|
|
682
|
+
return fetchGenerationHydration(resolvedOptions.fetchClient ?? fetch, resolvedUrl, mergeHeaders(resolvedOptions.headers), resolvedOptions.credentials || "same-origin", threadId);
|
|
683
|
+
}
|
|
684
|
+
};
|
|
313
685
|
}
|
|
314
686
|
function createDefaultXMLHttpRequest() {
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
}
|
|
318
|
-
return new globalThis.XMLHttpRequest();
|
|
687
|
+
if (typeof globalThis.XMLHttpRequest !== "function") throw new Error("XMLHttpRequest is not available in this runtime");
|
|
688
|
+
return new globalThis.XMLHttpRequest();
|
|
319
689
|
}
|
|
320
690
|
function cleanupXhr(xhr, abortSignal, onAbort) {
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
abortSignal.removeEventListener("abort", onAbort);
|
|
328
|
-
}
|
|
691
|
+
xhr.onprogress = null;
|
|
692
|
+
xhr.onload = null;
|
|
693
|
+
xhr.onerror = null;
|
|
694
|
+
xhr.onabort = null;
|
|
695
|
+
xhr.onloadend = null;
|
|
696
|
+
if (abortSignal && onAbort) abortSignal.removeEventListener("abort", onAbort);
|
|
329
697
|
}
|
|
330
698
|
function readXhrLines(xhr, abortSignal) {
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
}
|
|
428
|
-
function createConfiguredXhrRequest(url, options, messages, data, runContext) {
|
|
429
|
-
const xhr = options.xhrFactory?.() ?? createDefaultXMLHttpRequest();
|
|
430
|
-
xhr.open("POST", url);
|
|
431
|
-
if (options.withCredentials !== void 0) {
|
|
432
|
-
xhr.withCredentials = options.withCredentials;
|
|
433
|
-
}
|
|
434
|
-
const requestHeaders = {
|
|
435
|
-
"Content-Type": "application/json",
|
|
436
|
-
...mergeHeaders(options.headers)
|
|
437
|
-
};
|
|
438
|
-
for (const [name, value] of Object.entries(requestHeaders)) {
|
|
439
|
-
xhr.setRequestHeader(name, value);
|
|
440
|
-
}
|
|
441
|
-
const requestBody = buildRunAgentInputBody(
|
|
442
|
-
messages,
|
|
443
|
-
data,
|
|
444
|
-
runContext,
|
|
445
|
-
options
|
|
446
|
-
);
|
|
447
|
-
return { xhr, body: JSON.stringify(requestBody) };
|
|
699
|
+
let offset = 0;
|
|
700
|
+
let buffer = "";
|
|
701
|
+
const lines = [];
|
|
702
|
+
const waiters = [];
|
|
703
|
+
let done = false;
|
|
704
|
+
let aborted = false;
|
|
705
|
+
let error;
|
|
706
|
+
let onAbort;
|
|
707
|
+
const wake = () => {
|
|
708
|
+
waiters.shift()?.();
|
|
709
|
+
};
|
|
710
|
+
const enqueueDelta = () => {
|
|
711
|
+
if (xhr.status !== 0 && (xhr.status < 200 || xhr.status >= 300)) {
|
|
712
|
+
error = /* @__PURE__ */ new Error(`XHR error! status: ${xhr.status} ${xhr.statusText}`);
|
|
713
|
+
done = true;
|
|
714
|
+
return;
|
|
715
|
+
}
|
|
716
|
+
const responseText = xhr.responseText;
|
|
717
|
+
if (responseText.length <= offset) return;
|
|
718
|
+
buffer += responseText.slice(offset);
|
|
719
|
+
offset = responseText.length;
|
|
720
|
+
const splitLines = buffer.split("\n");
|
|
721
|
+
buffer = splitLines.pop() ?? "";
|
|
722
|
+
for (const line of splitLines) {
|
|
723
|
+
const normalized = line.endsWith("\r") ? line.slice(0, -1) : line;
|
|
724
|
+
if (normalized.trim()) lines.push(normalized);
|
|
725
|
+
}
|
|
726
|
+
};
|
|
727
|
+
const finish = () => {
|
|
728
|
+
enqueueDelta();
|
|
729
|
+
if (xhr.status !== 0 && (xhr.status < 200 || xhr.status >= 300)) error = /* @__PURE__ */ new Error(`XHR error! status: ${xhr.status} ${xhr.statusText}`);
|
|
730
|
+
else if (buffer.trim() && !aborted) error = new StreamTruncatedError();
|
|
731
|
+
done = true;
|
|
732
|
+
wake();
|
|
733
|
+
};
|
|
734
|
+
xhr.onprogress = () => {
|
|
735
|
+
enqueueDelta();
|
|
736
|
+
wake();
|
|
737
|
+
};
|
|
738
|
+
xhr.onload = finish;
|
|
739
|
+
xhr.onerror = () => {
|
|
740
|
+
error = new StreamReadError(/* @__PURE__ */ new Error("XHR request failed"));
|
|
741
|
+
done = true;
|
|
742
|
+
wake();
|
|
743
|
+
};
|
|
744
|
+
xhr.onabort = () => {
|
|
745
|
+
aborted = true;
|
|
746
|
+
done = true;
|
|
747
|
+
wake();
|
|
748
|
+
};
|
|
749
|
+
xhr.onloadend = () => {
|
|
750
|
+
if (!done) finish();
|
|
751
|
+
};
|
|
752
|
+
if (abortSignal) {
|
|
753
|
+
onAbort = () => {
|
|
754
|
+
aborted = true;
|
|
755
|
+
xhr.abort();
|
|
756
|
+
};
|
|
757
|
+
if (abortSignal.aborted) onAbort();
|
|
758
|
+
else abortSignal.addEventListener("abort", onAbort, { once: true });
|
|
759
|
+
}
|
|
760
|
+
return (async function* () {
|
|
761
|
+
try {
|
|
762
|
+
while (true) {
|
|
763
|
+
const line = lines.shift();
|
|
764
|
+
if (line !== void 0) {
|
|
765
|
+
yield line;
|
|
766
|
+
continue;
|
|
767
|
+
}
|
|
768
|
+
if (error) throw error;
|
|
769
|
+
if (done || abortSignal?.aborted) return;
|
|
770
|
+
await new Promise((resolve) => {
|
|
771
|
+
waiters.push(resolve);
|
|
772
|
+
});
|
|
773
|
+
}
|
|
774
|
+
} finally {
|
|
775
|
+
cleanupXhr(xhr, abortSignal, onAbort);
|
|
776
|
+
}
|
|
777
|
+
})();
|
|
778
|
+
}
|
|
779
|
+
function createConfiguredXhrRequest(url, options, messages, data, runContext, method = "POST", extraHeaders = {}) {
|
|
780
|
+
const xhr = options.xhrFactory?.() ?? createDefaultXMLHttpRequest();
|
|
781
|
+
xhr.open(method, url);
|
|
782
|
+
if (options.withCredentials !== void 0) xhr.withCredentials = options.withCredentials;
|
|
783
|
+
const requestHeaders = {
|
|
784
|
+
"Content-Type": "application/json",
|
|
785
|
+
...mergeHeaders(options.headers),
|
|
786
|
+
...method === "POST" ? runIdHeader(runContext?.runId) : {},
|
|
787
|
+
...extraHeaders
|
|
788
|
+
};
|
|
789
|
+
for (const [name, value] of Object.entries(requestHeaders)) xhr.setRequestHeader(name, value);
|
|
790
|
+
const requestBody = buildRunAgentInputBody(messages, data, runContext, options);
|
|
791
|
+
return {
|
|
792
|
+
xhr,
|
|
793
|
+
body: JSON.stringify(requestBody)
|
|
794
|
+
};
|
|
448
795
|
}
|
|
449
796
|
async function resolveXhrConnectionOptions(options) {
|
|
450
|
-
|
|
797
|
+
return typeof options === "function" ? await options() : options;
|
|
451
798
|
}
|
|
799
|
+
/**
|
|
800
|
+
* Build an XHR-backed {@link StreamEventSource}. `parseLines` decodes the raw
|
|
801
|
+
* newline-delimited body into events (SSE or NDJSON); the reconnect engine is
|
|
802
|
+
* shared with the fetch adapters. A fresh XHR is opened per attempt, so a
|
|
803
|
+
* `Last-Event-ID` reconnect header (via `extraHeaders`) is applied at open time.
|
|
804
|
+
*/
|
|
805
|
+
function xhrEventSource(url, options, method, messages, data, runContext, parseLines) {
|
|
806
|
+
return async function* (extraHeaders, abortSignal) {
|
|
807
|
+
const request = createConfiguredXhrRequest(url, options, messages, data, runContext, method, extraHeaders);
|
|
808
|
+
const lines = readXhrLines(request.xhr, abortSignal);
|
|
809
|
+
if (abortSignal?.aborted) {
|
|
810
|
+
await lines.next();
|
|
811
|
+
return;
|
|
812
|
+
}
|
|
813
|
+
request.xhr.send(method === "GET" ? null : request.body);
|
|
814
|
+
try {
|
|
815
|
+
yield* parseLines(lines);
|
|
816
|
+
} finally {
|
|
817
|
+
if (!abortSignal?.aborted) request.xhr.abort();
|
|
818
|
+
}
|
|
819
|
+
};
|
|
820
|
+
}
|
|
821
|
+
/** SSE line parser bound to the run's ids for a `[DONE]` fallback. */
|
|
822
|
+
function xhrSSEParser(runContext) {
|
|
823
|
+
const fallbackIds = {
|
|
824
|
+
...runContext?.threadId !== void 0 ? { threadId: runContext.threadId } : {},
|
|
825
|
+
...runContext?.runId !== void 0 ? { runId: runContext.runId } : {}
|
|
826
|
+
};
|
|
827
|
+
return (lines) => linesToSSEEvents(lines, fallbackIds);
|
|
828
|
+
}
|
|
829
|
+
/**
|
|
830
|
+
* Create an XMLHttpRequest-backed Server-Sent Events connection adapter.
|
|
831
|
+
*
|
|
832
|
+
* Resumable: against a durable (`id:`-tagged) server response, a dropped socket
|
|
833
|
+
* auto-reconnects with `Last-Event-ID` and de-dupes the replayed prefix, and
|
|
834
|
+
* `joinRun` attaches to an existing run from the start. A non-durable response
|
|
835
|
+
* is a single plain request, exactly as before.
|
|
836
|
+
*/
|
|
452
837
|
function xhrServerSentEvents(url, options = {}) {
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
const synthetic = {
|
|
481
|
-
type: EventType.RUN_FINISHED,
|
|
482
|
-
threadId: lastThreadId ?? runContext?.threadId ?? "",
|
|
483
|
-
runId: lastRunId ?? runContext?.runId ?? "",
|
|
484
|
-
model: lastModel ?? "",
|
|
485
|
-
timestamp: Date.now(),
|
|
486
|
-
finishReason: "stop"
|
|
487
|
-
};
|
|
488
|
-
request.xhr.abort();
|
|
489
|
-
yield synthetic;
|
|
490
|
-
return;
|
|
491
|
-
}
|
|
492
|
-
const chunk = JSON.parse(chunkData);
|
|
493
|
-
if ("threadId" in chunk && typeof chunk.threadId === "string") {
|
|
494
|
-
lastThreadId = chunk.threadId;
|
|
495
|
-
}
|
|
496
|
-
if ("runId" in chunk && typeof chunk.runId === "string") {
|
|
497
|
-
lastRunId = chunk.runId;
|
|
498
|
-
}
|
|
499
|
-
if ("model" in chunk && typeof chunk.model === "string") {
|
|
500
|
-
lastModel = chunk.model;
|
|
501
|
-
}
|
|
502
|
-
yield chunk;
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
};
|
|
838
|
+
return {
|
|
839
|
+
async *connect(messages, data, abortSignal, runContext) {
|
|
840
|
+
const resolvedUrl = typeof url === "function" ? url() : url;
|
|
841
|
+
const resolvedOptions = await resolveXhrConnectionOptions(options);
|
|
842
|
+
const signal = abortSignal || resolvedOptions.signal;
|
|
843
|
+
yield* resumableStream(xhrEventSource(resolvedUrl, resolvedOptions, "POST", messages, data, runContext, xhrSSEParser(runContext)), signal, resolvedOptions.reconnect);
|
|
844
|
+
},
|
|
845
|
+
async *joinRun(runId, abortSignal) {
|
|
846
|
+
const resolvedUrl = typeof url === "function" ? url() : url;
|
|
847
|
+
const resolvedOptions = await resolveXhrConnectionOptions(options);
|
|
848
|
+
const signal = abortSignal || resolvedOptions.signal;
|
|
849
|
+
yield* resumableStream(xhrEventSource(withSearchParams(resolvedUrl, {
|
|
850
|
+
offset: "-1",
|
|
851
|
+
runId
|
|
852
|
+
}), resolvedOptions, "GET", [], void 0, void 0, (lines) => linesToSSEEvents(lines, { runId })), signal, resolvedOptions.reconnect);
|
|
853
|
+
},
|
|
854
|
+
async hydrate(threadId) {
|
|
855
|
+
const resolvedUrl = typeof url === "function" ? url() : url;
|
|
856
|
+
const resolvedOptions = await resolveXhrConnectionOptions(options);
|
|
857
|
+
return fetchThreadHydration(fetch, resolvedUrl, mergeHeaders(resolvedOptions.headers), resolvedOptions.withCredentials ? "include" : "same-origin", threadId);
|
|
858
|
+
},
|
|
859
|
+
async hydrateGeneration(threadId) {
|
|
860
|
+
const resolvedUrl = typeof url === "function" ? url() : url;
|
|
861
|
+
const resolvedOptions = await resolveXhrConnectionOptions(options);
|
|
862
|
+
return fetchGenerationHydration(fetch, resolvedUrl, mergeHeaders(resolvedOptions.headers), resolvedOptions.withCredentials ? "include" : "same-origin", threadId);
|
|
863
|
+
}
|
|
864
|
+
};
|
|
506
865
|
}
|
|
866
|
+
/**
|
|
867
|
+
* Create an XMLHttpRequest-backed newline-delimited JSON stream adapter.
|
|
868
|
+
*
|
|
869
|
+
* Resumable: against a durable (envelope-tagged) server response, a dropped
|
|
870
|
+
* socket auto-reconnects with `Last-Event-ID` and de-dupes the replayed prefix,
|
|
871
|
+
* and `joinRun` attaches to an existing run from the start. A non-durable
|
|
872
|
+
* (bare-line) response is a single plain request, exactly as before.
|
|
873
|
+
*/
|
|
507
874
|
function xhrHttpStream(url, options = {}) {
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
875
|
+
return {
|
|
876
|
+
async *connect(messages, data, abortSignal, runContext) {
|
|
877
|
+
const resolvedUrl = typeof url === "function" ? url() : url;
|
|
878
|
+
const resolvedOptions = await resolveXhrConnectionOptions(options);
|
|
879
|
+
const signal = abortSignal || resolvedOptions.signal;
|
|
880
|
+
yield* resumableStream(xhrEventSource(resolvedUrl, resolvedOptions, "POST", messages, data, runContext, linesToNdjsonEvents), signal, resolvedOptions.reconnect);
|
|
881
|
+
},
|
|
882
|
+
async *joinRun(runId, abortSignal) {
|
|
883
|
+
const resolvedUrl = typeof url === "function" ? url() : url;
|
|
884
|
+
const resolvedOptions = await resolveXhrConnectionOptions(options);
|
|
885
|
+
const signal = abortSignal || resolvedOptions.signal;
|
|
886
|
+
yield* resumableStream(xhrEventSource(withSearchParams(resolvedUrl, {
|
|
887
|
+
offset: "-1",
|
|
888
|
+
runId
|
|
889
|
+
}), resolvedOptions, "GET", [], void 0, void 0, linesToNdjsonEvents), signal, resolvedOptions.reconnect);
|
|
890
|
+
},
|
|
891
|
+
async hydrate(threadId) {
|
|
892
|
+
const resolvedUrl = typeof url === "function" ? url() : url;
|
|
893
|
+
const resolvedOptions = await resolveXhrConnectionOptions(options);
|
|
894
|
+
return fetchThreadHydration(fetch, resolvedUrl, mergeHeaders(resolvedOptions.headers), resolvedOptions.withCredentials ? "include" : "same-origin", threadId);
|
|
895
|
+
},
|
|
896
|
+
async hydrateGeneration(threadId) {
|
|
897
|
+
const resolvedUrl = typeof url === "function" ? url() : url;
|
|
898
|
+
const resolvedOptions = await resolveXhrConnectionOptions(options);
|
|
899
|
+
return fetchGenerationHydration(fetch, resolvedUrl, mergeHeaders(resolvedOptions.headers), resolvedOptions.withCredentials ? "include" : "same-origin", threadId);
|
|
900
|
+
}
|
|
901
|
+
};
|
|
902
|
+
}
|
|
903
|
+
/**
|
|
904
|
+
* Create a direct stream connection adapter (for server functions or direct streams)
|
|
905
|
+
*
|
|
906
|
+
* @param streamFactory - A function that returns an async iterable of StreamChunks
|
|
907
|
+
* @param handlers - Optional persistence handlers (`hydrate`,
|
|
908
|
+
* `hydrateGeneration`, `joinRun`) that let server-driven persistence work
|
|
909
|
+
* without an HTTP endpoint — each is usually a one-line server-function call
|
|
910
|
+
* @returns A connection adapter for direct streams
|
|
911
|
+
*
|
|
912
|
+
* @example
|
|
913
|
+
* ```typescript
|
|
914
|
+
* // With TanStack Start server function
|
|
915
|
+
* const connection = stream(() => serverFunction({ messages }));
|
|
916
|
+
*
|
|
917
|
+
* const client = new ChatClient({ connection });
|
|
918
|
+
*
|
|
919
|
+
* // With generation persistence over server functions
|
|
920
|
+
* const connection = stream(
|
|
921
|
+
* () => generateImageFn({ data: input }),
|
|
922
|
+
* {
|
|
923
|
+
* hydrateGeneration: (threadId) => getImageHydrationFn({ data: threadId }),
|
|
924
|
+
* joinRun: (runId) => joinImageRunFn({ data: runId }),
|
|
925
|
+
* },
|
|
926
|
+
* );
|
|
927
|
+
* ```
|
|
928
|
+
*/
|
|
929
|
+
function stream(streamFactory, handlers) {
|
|
930
|
+
return {
|
|
931
|
+
async *connect(messages, data, abortSignal) {
|
|
932
|
+
yield* streamFactory(messages, data, abortSignal);
|
|
933
|
+
},
|
|
934
|
+
...handlers?.hydrate ? { hydrate: handlers.hydrate } : {},
|
|
935
|
+
...handlers?.hydrateGeneration ? { hydrateGeneration: handlers.hydrateGeneration } : {},
|
|
936
|
+
...handlers?.joinRun ? { joinRun: handlers.joinRun } : {}
|
|
937
|
+
};
|
|
538
938
|
}
|
|
939
|
+
/**
|
|
940
|
+
* Wrap a `ChatFetcher` as a `ConnectConnectionAdapter` so the chat client can
|
|
941
|
+
* consume it through the same `subscribe`/`send` plumbing used for SSE /
|
|
942
|
+
* HTTP-stream / RPC connections. May return either a `Response` (parsed as
|
|
943
|
+
* SSE) or an `AsyncIterable<StreamChunk>` (yielded directly).
|
|
944
|
+
*
|
|
945
|
+
* @internal
|
|
946
|
+
*/
|
|
539
947
|
function fetcherToConnectionAdapter(fetcher) {
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
const uiMessages = messages;
|
|
553
|
-
const result = await fetcher(
|
|
554
|
-
{
|
|
555
|
-
messages: uiMessages,
|
|
556
|
-
data,
|
|
557
|
-
threadId: runContext.threadId,
|
|
558
|
-
runId: runContext.runId
|
|
559
|
-
},
|
|
560
|
-
{ signal: abortSignal }
|
|
561
|
-
);
|
|
562
|
-
if (result instanceof Response) {
|
|
563
|
-
yield* responseToSSEChunks(result, abortSignal);
|
|
564
|
-
} else {
|
|
565
|
-
yield* abortableIterable(result, abortSignal);
|
|
566
|
-
}
|
|
567
|
-
}
|
|
568
|
-
};
|
|
948
|
+
return { async *connect(messages, data, abortSignal, runContext) {
|
|
949
|
+
if (!abortSignal) throw new Error("fetcherToConnectionAdapter requires an AbortSignal — the chat client always supplies one.");
|
|
950
|
+
if (!runContext) throw new Error("fetcherToConnectionAdapter requires a RunAgentInputContext — the chat client always supplies one.");
|
|
951
|
+
const result = await fetcher({
|
|
952
|
+
messages,
|
|
953
|
+
data,
|
|
954
|
+
threadId: runContext.threadId,
|
|
955
|
+
runId: runContext.runId
|
|
956
|
+
}, { signal: abortSignal });
|
|
957
|
+
if (result instanceof Response) yield* responseToSSEChunks(result, abortSignal);
|
|
958
|
+
else yield* abortableIterable(result, abortSignal);
|
|
959
|
+
} };
|
|
569
960
|
}
|
|
961
|
+
/**
|
|
962
|
+
* Wrap an AsyncIterable so iteration aborts when `signal` fires. Without
|
|
963
|
+
* this, a fetcher that returns a generator ignoring its signal would leave
|
|
964
|
+
* the for-await loop hanging until the iterable naturally ends.
|
|
965
|
+
*/
|
|
570
966
|
async function* abortableIterable(iterable, signal) {
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
967
|
+
if (signal.aborted) return;
|
|
968
|
+
const iterator = iterable[Symbol.asyncIterator]();
|
|
969
|
+
const abortPromise = new Promise((resolve) => {
|
|
970
|
+
signal.addEventListener("abort", () => resolve({
|
|
971
|
+
done: true,
|
|
972
|
+
value: void 0
|
|
973
|
+
}), { once: true });
|
|
974
|
+
});
|
|
975
|
+
try {
|
|
976
|
+
while (true) {
|
|
977
|
+
const result = await Promise.race([iterator.next(), abortPromise]);
|
|
978
|
+
if (result.done) return;
|
|
979
|
+
yield result.value;
|
|
980
|
+
}
|
|
981
|
+
} finally {
|
|
982
|
+
await iterator.return?.();
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
/**
|
|
986
|
+
* Create an RPC stream connection adapter (for RPC-based streaming like Cap'n Web RPC)
|
|
987
|
+
*
|
|
988
|
+
* @param rpcCall - A function that accepts messages and returns an async iterable of StreamChunks
|
|
989
|
+
* @param handlers - Optional persistence handlers (`hydrate`,
|
|
990
|
+
* `hydrateGeneration`, `joinRun`) that let server-driven persistence work
|
|
991
|
+
* without an HTTP endpoint — each is usually a one-line RPC call
|
|
992
|
+
* @returns A connection adapter for RPC streams
|
|
993
|
+
*
|
|
994
|
+
* @example
|
|
995
|
+
* ```typescript
|
|
996
|
+
* // With Cap'n Web RPC
|
|
997
|
+
* const connection = rpcStream((messages, data) =>
|
|
998
|
+
* api.streamMurfResponse(messages, data)
|
|
999
|
+
* );
|
|
1000
|
+
*
|
|
1001
|
+
* const client = new ChatClient({ connection });
|
|
1002
|
+
*
|
|
1003
|
+
* // With generation persistence over RPC
|
|
1004
|
+
* const connection = rpcStream(
|
|
1005
|
+
* (messages, data) => api.streamMurfResponse(messages, data),
|
|
1006
|
+
* {
|
|
1007
|
+
* hydrateGeneration: (threadId) => api.getGenerationHydration(threadId),
|
|
1008
|
+
* joinRun: (runId) => api.replayRun(runId),
|
|
1009
|
+
* },
|
|
1010
|
+
* );
|
|
1011
|
+
* ```
|
|
1012
|
+
*/
|
|
1013
|
+
function rpcStream(rpcCall, handlers) {
|
|
1014
|
+
return {
|
|
1015
|
+
async *connect(messages, data, abortSignal) {
|
|
1016
|
+
yield* rpcCall(messages, data, abortSignal);
|
|
1017
|
+
},
|
|
1018
|
+
...handlers?.hydrate ? { hydrate: handlers.hydrate } : {},
|
|
1019
|
+
...handlers?.hydrateGeneration ? { hydrateGeneration: handlers.hydrateGeneration } : {},
|
|
1020
|
+
...handlers?.joinRun ? { joinRun: handlers.joinRun } : {}
|
|
1021
|
+
};
|
|
1022
|
+
}
|
|
1023
|
+
//#endregion
|
|
1024
|
+
export { DurableStreamIncompleteError, StreamReconnectLimitError, StreamTruncatedError, fetchHttpStream, fetchServerSentEvents, fetcherToConnectionAdapter, getChunkRunId, normalizeConnectionAdapter, rpcStream, stream, xhrHttpStream, xhrServerSentEvents };
|
|
1025
|
+
|
|
1026
|
+
//# sourceMappingURL=connection-adapters.js.map
|