@windmill-labs/chat-sdk 0.0.0-stage → 1.831.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/chat.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ import type { Chat, ChatOptions } from './types';
2
+ export declare function createChat(options: ChatOptions): Chat;
@@ -0,0 +1,33 @@
1
+ import type { ChatOptions, FetchLike, HistoryMode, StorageLike, TokenSource } from './types';
2
+ export interface ResolvedConfig {
3
+ flowPath: string;
4
+ baseUrl: string;
5
+ workspace: string;
6
+ token: TokenSource | undefined;
7
+ history: HistoryMode;
8
+ /** The caller chose `history`; a failing server history is then an error, not a fallback. */
9
+ historyExplicit: boolean;
10
+ inputs: Record<string, unknown>;
11
+ fetch: FetchLike | undefined;
12
+ storage: StorageLike | undefined;
13
+ storageKey: string | undefined;
14
+ pageSize: number;
15
+ pollDelayMs: number | undefined;
16
+ run: ChatOptions['run'];
17
+ onFinish: ChatOptions['onFinish'];
18
+ onError: ChatOptions['onError'];
19
+ }
20
+ export interface RawAppContext {
21
+ baseUrl: string;
22
+ workspace: string;
23
+ /** The viewer's SDK token in a sandboxed raw app; the session cookie otherwise. */
24
+ token?: string;
25
+ }
26
+ /**
27
+ * The Windmill a raw app bundle runs in. A sandboxed app gets `window.process.env`
28
+ * from its wrapper once the viewer consented to the declared SDK scopes; an
29
+ * unsandboxed one runs on the Windmill origin with the viewer's session and only
30
+ * has `window.ctx`.
31
+ */
32
+ export declare function detectRawApp(): RawAppContext | undefined;
33
+ export declare function resolveConfig(options: ChatOptions): ResolvedConfig;
@@ -0,0 +1,556 @@
1
+ //#region \0@oxc-project+runtime@0.149.0/helpers/esm/checkPrivateRedeclaration.js
2
+ function _checkPrivateRedeclaration(e, t) {
3
+ if (t.has(e)) throw new TypeError("Cannot initialize the same private elements twice on an object");
4
+ }
5
+ //#endregion
6
+ //#region \0@oxc-project+runtime@0.149.0/helpers/esm/classPrivateMethodInitSpec.js
7
+ function _classPrivateMethodInitSpec(e, a) {
8
+ _checkPrivateRedeclaration(e, a), a.add(e);
9
+ }
10
+ //#endregion
11
+ //#region \0@oxc-project+runtime@0.149.0/helpers/esm/classPrivateFieldInitSpec.js
12
+ function _classPrivateFieldInitSpec(e, t, a) {
13
+ _checkPrivateRedeclaration(e, t), t.set(e, a);
14
+ }
15
+ //#endregion
16
+ //#region \0@oxc-project+runtime@0.149.0/helpers/esm/assertClassBrand.js
17
+ function _assertClassBrand(e, t, n) {
18
+ if ("function" == typeof e ? e === t : e.has(t)) return arguments.length < 3 ? t : n;
19
+ throw new TypeError("Private element is not present on this object");
20
+ }
21
+ //#endregion
22
+ //#region \0@oxc-project+runtime@0.149.0/helpers/esm/classPrivateFieldSet2.js
23
+ function _classPrivateFieldSet2(s, a, r) {
24
+ return s.set(_assertClassBrand(s, a), r), r;
25
+ }
26
+ //#endregion
27
+ //#region \0@oxc-project+runtime@0.149.0/helpers/esm/classPrivateFieldGet2.js
28
+ function _classPrivateFieldGet2(s, a) {
29
+ return s.get(_assertClassBrand(s, a));
30
+ }
31
+ //#endregion
32
+ //#region src/api.ts
33
+ var WindmillApiError = class extends Error {
34
+ constructor(message, status, body) {
35
+ super(message);
36
+ this.status = status;
37
+ this.body = body;
38
+ this.name = "WindmillApiError";
39
+ }
40
+ };
41
+ /**
42
+ * A message was sent to a conversation whose turn is still running. The server refuses
43
+ * it (409) so two runs never write one agent memory; `turn` is the run to follow instead.
44
+ */
45
+ var TurnRunningError = class extends Error {
46
+ constructor(message, turn) {
47
+ super(message);
48
+ this.turn = turn;
49
+ this.name = "TurnRunningError";
50
+ }
51
+ };
52
+ /**
53
+ * The running turn named by a run's 409 body, or undefined for any other body. Exported
54
+ * for a custom `run` that calls Windmill through its own client: it rethrows the refusal
55
+ * as a `TurnRunningError` so the chat can follow the running turn.
56
+ */
57
+ function turnRunningError(body) {
58
+ try {
59
+ const parsed = JSON.parse(body);
60
+ const turn = parsed?.running_turn;
61
+ if (!turn || typeof turn.job_id !== "string" || typeof turn.user_seq !== "number") return void 0;
62
+ return new TurnRunningError(typeof parsed.error === "string" ? parsed.error : "this conversation is still answering a message", {
63
+ jobId: turn.job_id,
64
+ userSeq: turn.user_seq
65
+ });
66
+ } catch {
67
+ return;
68
+ }
69
+ }
70
+ var _baseUrl = /* @__PURE__ */ new WeakMap();
71
+ var _workspace = /* @__PURE__ */ new WeakMap();
72
+ var _token = /* @__PURE__ */ new WeakMap();
73
+ var _fetch = /* @__PURE__ */ new WeakMap();
74
+ var _pollDelayMs = /* @__PURE__ */ new WeakMap();
75
+ var _WindmillChatApi_brand = /* @__PURE__ */ new WeakSet();
76
+ /** Thin client over the Windmill endpoints a chat-mode flow uses. */
77
+ var WindmillChatApi = class {
78
+ constructor(options) {
79
+ _classPrivateMethodInitSpec(this, _WindmillChatApi_brand);
80
+ _classPrivateFieldInitSpec(this, _baseUrl, void 0);
81
+ _classPrivateFieldInitSpec(this, _workspace, void 0);
82
+ _classPrivateFieldInitSpec(this, _token, void 0);
83
+ _classPrivateFieldInitSpec(this, _fetch, void 0);
84
+ _classPrivateFieldInitSpec(this, _pollDelayMs, void 0);
85
+ _classPrivateFieldSet2(_baseUrl, this, normalizeBaseUrl(options.baseUrl));
86
+ _classPrivateFieldSet2(_workspace, this, options.workspace);
87
+ _classPrivateFieldSet2(_token, this, options.token);
88
+ _classPrivateFieldSet2(_fetch, this, options.fetch ?? ((input, init) => globalThis.fetch(input, init)));
89
+ _classPrivateFieldSet2(_pollDelayMs, this, options.pollDelayMs);
90
+ }
91
+ /**
92
+ * Starts a turn: runs the flow with `memory_id` set to the conversation id. Returns the
93
+ * job id. Throws `TurnRunningError` when the conversation is still answering.
94
+ */
95
+ async runFlow(flowPath, args, options) {
96
+ let res;
97
+ try {
98
+ res = await _assertClassBrand(_WindmillChatApi_brand, this, _request).call(this, `jobs/run/f/${encodePath(flowPath)}`, {
99
+ method: "POST",
100
+ query: {
101
+ memory_id: options.memoryId,
102
+ skip_preprocessor: "true"
103
+ },
104
+ body: args,
105
+ signal: options.signal
106
+ });
107
+ } catch (e) {
108
+ if (e instanceof WindmillApiError && e.status === 409) throw turnRunningError(e.body ?? "") ?? e;
109
+ throw e;
110
+ }
111
+ return (await res.text()).trim();
112
+ }
113
+ /**
114
+ * One server-sent-events connection to a job's updates. The server closes it after
115
+ * `TIMEOUT_SSE_STREAM` (a `timeout` event); resume by calling again with the last
116
+ * `stream_offset`, never by re-running the flow.
117
+ */
118
+ async *streamJob(jobId, options = {}) {
119
+ const query = {
120
+ fast: "true",
121
+ only_result: "true"
122
+ };
123
+ if (_classPrivateFieldGet2(_pollDelayMs, this) !== void 0) query.poll_delay_ms = String(_classPrivateFieldGet2(_pollDelayMs, this));
124
+ if (options.streamOffset !== void 0) query.stream_offset = String(options.streamOffset);
125
+ const res = await _assertClassBrand(_WindmillChatApi_brand, this, _request).call(this, `jobs_u/getupdate_sse/${encodeURIComponent(jobId)}`, {
126
+ query,
127
+ accept: "text/event-stream",
128
+ signal: options.signal
129
+ });
130
+ if (!res.body) throw new WindmillApiError("The job update stream has no body", 0);
131
+ for await (const data of readServerSentEvents(res.body)) try {
132
+ yield JSON.parse(data);
133
+ } catch {}
134
+ }
135
+ async getCompletedResult(jobId, signal) {
136
+ return await (await _assertClassBrand(_WindmillChatApi_brand, this, _request).call(this, `jobs_u/completed/get_result_maybe/${encodeURIComponent(jobId)}`, { signal })).json();
137
+ }
138
+ /** A flow job with its status: the step job ids are what persisted messages carry as `job_id`. */
139
+ async getFlowJob(jobId, signal) {
140
+ return await (await _assertClassBrand(_WindmillChatApi_brand, this, _request).call(this, `jobs_u/get/${encodeURIComponent(jobId)}`, {
141
+ query: { no_logs: "true" },
142
+ signal
143
+ })).json();
144
+ }
145
+ /**
146
+ * Where a message's attachment downloads from. The endpoint authenticates like every other
147
+ * request: a consumer holding a token must fetch it with that token, not put the URL in an
148
+ * `img src`, which would send only the Windmill session cookie.
149
+ */
150
+ attachmentUrl(attachment) {
151
+ const query = new URLSearchParams({ file_key: attachment.s3 });
152
+ if (attachment.storage) query.set("storage", attachment.storage);
153
+ return `${_classPrivateFieldGet2(_baseUrl, this)}/api/w/${encodeURIComponent(_classPrivateFieldGet2(_workspace, this))}/job_helpers/download_s3_file?${query}`;
154
+ }
155
+ async cancelJob(jobId, reason = "Stopped from the chat") {
156
+ await _assertClassBrand(_WindmillChatApi_brand, this, _request).call(this, `jobs_u/queue/cancel/${encodeURIComponent(jobId)}`, {
157
+ method: "POST",
158
+ body: { reason }
159
+ });
160
+ }
161
+ async listConversations(flowPath, options = {}) {
162
+ const extra = { flow_path: flowPath };
163
+ if (options.kind !== void 0) extra.kind = options.kind;
164
+ return await (await _assertClassBrand(_WindmillChatApi_brand, this, _request).call(this, "flow_conversations/list", {
165
+ query: pagination(options, extra),
166
+ signal: options.signal
167
+ })).json();
168
+ }
169
+ /** Sets a conversation's title. Its place in the list is kept: only a turn moves one. */
170
+ async renameConversation(conversationId, title) {
171
+ await _assertClassBrand(_WindmillChatApi_brand, this, _request).call(this, `flow_conversations/update/${encodeURIComponent(conversationId)}`, {
172
+ method: "POST",
173
+ body: { title }
174
+ });
175
+ }
176
+ /**
177
+ * Without `afterSeq`: one page counted from the newest message, returned oldest first.
178
+ * With `afterSeq`: the messages created after that cursor, oldest first.
179
+ */
180
+ async listMessages(conversationId, options = {}) {
181
+ const extra = {};
182
+ if (options.afterSeq !== void 0) extra.after_seq = String(options.afterSeq);
183
+ return await (await _assertClassBrand(_WindmillChatApi_brand, this, _request).call(this, `flow_conversations/${encodeURIComponent(conversationId)}/messages`, {
184
+ query: pagination(options, extra),
185
+ signal: options.signal
186
+ })).json();
187
+ }
188
+ /**
189
+ * Puts bytes in the workspace's object storage under `fileKey` and returns the key they were
190
+ * stored under (the server may rewrite it). Needs the workspace to have object storage set.
191
+ */
192
+ async uploadFile(fileKey, body, options = {}) {
193
+ const query = { file_key: fileKey };
194
+ if (options.contentType) query.content_type = options.contentType;
195
+ return await (await _assertClassBrand(_WindmillChatApi_brand, this, _request).call(this, "job_helpers/upload_s3_file", {
196
+ method: "POST",
197
+ query,
198
+ raw: body,
199
+ contentType: options.contentType || "application/octet-stream",
200
+ signal: options.signal
201
+ })).json();
202
+ }
203
+ async deleteConversation(conversationId) {
204
+ await _assertClassBrand(_WindmillChatApi_brand, this, _request).call(this, `flow_conversations/delete/${encodeURIComponent(conversationId)}`, { method: "DELETE" });
205
+ }
206
+ };
207
+ async function _request(path, init = {}) {
208
+ const url = new URL(`${_classPrivateFieldGet2(_baseUrl, this)}/api/w/${encodeURIComponent(_classPrivateFieldGet2(_workspace, this))}/${path}`);
209
+ for (const [k, v] of Object.entries(init.query ?? {})) url.searchParams.set(k, v);
210
+ const headers = {};
211
+ if (init.accept) headers["Accept"] = init.accept;
212
+ if (init.body !== void 0) headers["Content-Type"] = "application/json";
213
+ else if (init.raw !== void 0) headers["Content-Type"] = init.contentType ?? "application/octet-stream";
214
+ const token = typeof _classPrivateFieldGet2(_token, this) === "function" ? await _classPrivateFieldGet2(_token, this).call(this) : _classPrivateFieldGet2(_token, this);
215
+ if (token) headers["Authorization"] = `Bearer ${token}`;
216
+ const res = await _classPrivateFieldGet2(_fetch, this).call(this, url.toString(), {
217
+ method: init.method ?? "GET",
218
+ headers,
219
+ body: init.body === void 0 ? init.raw : JSON.stringify(init.body),
220
+ credentials: token ? "omit" : "same-origin",
221
+ signal: init.signal
222
+ });
223
+ if (!res.ok) {
224
+ const text = await res.text().catch(() => "");
225
+ throw new WindmillApiError(`${init.method ?? "GET"} ${path} failed (${res.status})${text ? `: ${text}` : ""}`, res.status, text);
226
+ }
227
+ return res;
228
+ }
229
+ function normalizeBaseUrl(baseUrl) {
230
+ return baseUrl.replace(/\/+$/, "").replace(/\/api$/, "");
231
+ }
232
+ function encodePath(path) {
233
+ return path.split("/").map(encodeURIComponent).join("/");
234
+ }
235
+ function pagination(options, extra) {
236
+ const query = { ...extra };
237
+ if (options.page !== void 0) query.page = String(options.page);
238
+ if (options.perPage !== void 0) query.per_page = String(options.perPage);
239
+ return query;
240
+ }
241
+ /** Yields the `data` payload of each event in a `text/event-stream` body. */
242
+ async function* readServerSentEvents(body) {
243
+ const reader = body.getReader();
244
+ const decoder = new TextDecoder();
245
+ let buffer = "";
246
+ let carry = "";
247
+ try {
248
+ while (true) {
249
+ const { value, done } = await reader.read();
250
+ if (done) break;
251
+ let text = carry + decoder.decode(value, { stream: true });
252
+ carry = "";
253
+ if (text.endsWith("\r")) {
254
+ carry = "\r";
255
+ text = text.slice(0, -1);
256
+ }
257
+ buffer += text.replace(/\r\n?/g, "\n");
258
+ let end;
259
+ while ((end = buffer.indexOf("\n\n")) !== -1) {
260
+ const data = eventData(buffer.slice(0, end));
261
+ buffer = buffer.slice(end + 2);
262
+ if (data !== void 0) yield data;
263
+ }
264
+ }
265
+ if (carry) buffer += "\n";
266
+ const data = eventData(buffer);
267
+ if (data !== void 0) yield data;
268
+ } finally {
269
+ reader.cancel().catch(() => {});
270
+ }
271
+ }
272
+ function eventData(block) {
273
+ const lines = block.split("\n").filter((line) => line.startsWith("data:")).map((line) => line.slice(line.startsWith("data: ") ? 6 : 5));
274
+ return lines.length > 0 ? lines.join("\n") : void 0;
275
+ }
276
+ //#endregion
277
+ //#region src/stream.ts
278
+ const KNOWN_TYPES = /* @__PURE__ */ new Set([
279
+ "token_delta",
280
+ "reasoning_token_delta",
281
+ "tool_call",
282
+ "tool_call_arguments",
283
+ "tool_execution",
284
+ "tool_result"
285
+ ]);
286
+ /**
287
+ * Incremental parser for the `new_result_stream` chunks of a job update. A chunk is
288
+ * not guaranteed to end on a line boundary, so an incomplete last line waits for the
289
+ * next `push` (or `flush` once the job completes).
290
+ */
291
+ function createStreamEventParser() {
292
+ let pending = "";
293
+ return {
294
+ push(chunk) {
295
+ pending += chunk;
296
+ const lastNewline = pending.lastIndexOf("\n");
297
+ if (lastNewline === -1) return [];
298
+ const complete = pending.slice(0, lastNewline);
299
+ pending = pending.slice(lastNewline + 1);
300
+ return parseStreamEvents(complete);
301
+ },
302
+ flush() {
303
+ const rest = pending;
304
+ pending = "";
305
+ return parseStreamEvents(rest);
306
+ }
307
+ };
308
+ }
309
+ /** Parses complete NDJSON lines; lines that aren't agent events are skipped. */
310
+ function parseStreamEvents(ndjson) {
311
+ const events = [];
312
+ for (const line of ndjson.split("\n")) {
313
+ const trimmed = line.trim();
314
+ if (!trimmed) continue;
315
+ try {
316
+ const parsed = JSON.parse(trimmed);
317
+ if (parsed && typeof parsed === "object" && KNOWN_TYPES.has(parsed.type)) events.push(parsed);
318
+ } catch {}
319
+ }
320
+ return events;
321
+ }
322
+ //#endregion
323
+ //#region src/utils.ts
324
+ function randomId() {
325
+ const c = globalThis.crypto;
326
+ if (c?.randomUUID) return c.randomUUID();
327
+ const bytes = /* @__PURE__ */ new Uint8Array(16);
328
+ c.getRandomValues(bytes);
329
+ return formatUuid(bytes, 4);
330
+ }
331
+ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
332
+ function isUuid(value) {
333
+ return UUID_RE.test(value);
334
+ }
335
+ /**
336
+ * The Windmill conversation id for an arbitrary chat id. A UUID is used as is;
337
+ * anything else (an AI SDK chat id, for instance) maps to the same UUID every time,
338
+ * so a page can reopen its conversation without storing a second id. Hashed in plain
339
+ * JS: `crypto.subtle` only exists in secure contexts, and the mapping must not depend
340
+ * on the origin's scheme.
341
+ */
342
+ function conversationIdFor(chatId) {
343
+ if (isUuid(chatId)) return chatId.toLowerCase();
344
+ const bytes = new TextEncoder().encode(`windmill-chat:${chatId}`);
345
+ const out = /* @__PURE__ */ new Uint8Array(16);
346
+ for (const [i, seed] of [14695981039346656037n, 9521211207457086692n].entries()) {
347
+ let h = fnv1a64(bytes, seed);
348
+ for (let b = 7; b >= 0; b--) {
349
+ out[i * 8 + b] = Number(h & 255n);
350
+ h >>= 8n;
351
+ }
352
+ }
353
+ return formatUuid(out, 5);
354
+ }
355
+ function fnv1a64(bytes, seed) {
356
+ let h = seed;
357
+ for (const byte of bytes) {
358
+ h ^= BigInt(byte);
359
+ h = h * 1099511628211n & 18446744073709551615n;
360
+ }
361
+ return h;
362
+ }
363
+ function formatUuid(bytes, version) {
364
+ bytes[6] = bytes[6] & 15 | version << 4;
365
+ bytes[8] = bytes[8] & 63 | 128;
366
+ const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
367
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
368
+ }
369
+ function parseJsonOr(text) {
370
+ if (text === void 0) return void 0;
371
+ try {
372
+ return JSON.parse(text);
373
+ } catch {
374
+ return text;
375
+ }
376
+ }
377
+ function now() {
378
+ return (/* @__PURE__ */ new Date()).toISOString();
379
+ }
380
+ /** Same rule as the server: the first message, cut to 25 characters. */
381
+ function conversationTitle(firstMessage) {
382
+ const chars = Array.from(firstMessage);
383
+ return chars.length > 25 ? `${chars.slice(0, 25).join("")}...` : firstMessage;
384
+ }
385
+ /** The server's bound on a typed title: 252 characters plus an ellipsis fits its 255-char column. */
386
+ function truncateTitle(title) {
387
+ const chars = Array.from(title);
388
+ return chars.length > 252 ? `${chars.slice(0, 252).join("")}...` : title;
389
+ }
390
+ function sleep(ms, signal) {
391
+ return new Promise((resolve, reject) => {
392
+ if (signal?.aborted) return reject(abortError());
393
+ const onAbort = () => {
394
+ clearTimeout(timer);
395
+ reject(abortError());
396
+ };
397
+ const timer = setTimeout(() => {
398
+ signal?.removeEventListener("abort", onAbort);
399
+ resolve();
400
+ }, ms);
401
+ signal?.addEventListener("abort", onAbort, { once: true });
402
+ });
403
+ }
404
+ function abortError() {
405
+ return new DOMException("The operation was aborted", "AbortError");
406
+ }
407
+ function isAbortError(e) {
408
+ return e instanceof Error && e.name === "AbortError";
409
+ }
410
+ /**
411
+ * The text a chat shows for a flow result, following what Windmill persists as the
412
+ * assistant message when the last step is not an AI agent: `windmill_chat_answer`
413
+ * when the result carries one (null means no message), an agent result's `output`,
414
+ * a string as is, anything else as JSON.
415
+ */
416
+ function extractChatAnswer(result) {
417
+ if (result === null || result === void 0) return void 0;
418
+ if (typeof result === "string") return result;
419
+ if (typeof result === "object" && !Array.isArray(result)) {
420
+ const obj = result;
421
+ if ("windmill_chat_answer" in obj) return formatAnswer(obj.windmill_chat_answer);
422
+ if ("output" in obj && Array.isArray(obj.messages)) return formatAnswer(obj.output);
423
+ }
424
+ return JSON.stringify(result, null, 2);
425
+ }
426
+ function formatAnswer(value) {
427
+ if (value === null || value === void 0) return void 0;
428
+ return typeof value === "string" ? value : JSON.stringify(value, null, 2);
429
+ }
430
+ /** A completed job whose result is Windmill's error envelope. */
431
+ function isErrorResult(result) {
432
+ return typeof result === "object" && result !== null && "error" in result && typeof result.error === "object" && result.error !== null;
433
+ }
434
+ function errorResultMessage(result) {
435
+ const { message, name } = result.error;
436
+ if (typeof message === "string" && message) return typeof name === "string" && name && name !== "Error" ? `${name}: ${message}` : message;
437
+ return JSON.stringify(result.error, null, 2);
438
+ }
439
+ //#endregion
440
+ //#region src/follow.ts
441
+ const RECONNECT_DELAY_MS = 300;
442
+ const MAX_RECONNECT_DELAY_MS = 5e3;
443
+ /** Consecutive failed connections before the job is polled instead. */
444
+ const MAX_CONNECTION_FAILURES = 3;
445
+ const RESULT_POLL_MS = 2e3;
446
+ /**
447
+ * Follows a job to completion across the server's stream timeouts: every
448
+ * connection resumes from the last `stream_offset`, so no delta is repeated and
449
+ * the flow is never re-run. `onOffset` reports each offset, and its loss, so a
450
+ * caller can resume later from another connection (see the AI SDK transport).
451
+ *
452
+ * The offset indexes the stream of one sub-job (`flow_stream_job_id`, the flow's
453
+ * streaming step). A retried step gets a new one, so when the id changes the
454
+ * offset is dropped and the connection reopened from that sub-job's start.
455
+ *
456
+ * A connection that fails (a proxy restarting, the network dropping) is retried with
457
+ * backoff. The run is still going, so after a few failures in a row the job's result is
458
+ * polled instead: the rest of the answer is not streamed, but the turn still ends.
459
+ */
460
+ async function* followJob(api, jobId, options = {}) {
461
+ let parser = createStreamEventParser();
462
+ let offset = options.streamOffset;
463
+ let streamJobId;
464
+ let failures = 0;
465
+ while (failures < MAX_CONNECTION_FAILURES) {
466
+ let reopen = false;
467
+ try {
468
+ for await (const update of api.streamJob(jobId, {
469
+ streamOffset: offset,
470
+ signal: options.signal
471
+ })) {
472
+ if (update.type === "ping") continue;
473
+ if (update.type === "timeout") {
474
+ reopen = true;
475
+ break;
476
+ }
477
+ if (update.type === "error") throw new Error(update.error);
478
+ if (update.type === "notfound") throw new Error(`Job ${jobId} not found`);
479
+ if (update.flow_stream_job_id && update.flow_stream_job_id !== streamJobId) {
480
+ const switched = streamJobId !== void 0 && offset !== void 0;
481
+ streamJobId = update.flow_stream_job_id;
482
+ if (switched) {
483
+ failures = 0;
484
+ offset = void 0;
485
+ options.onOffset?.(void 0);
486
+ parser = createStreamEventParser();
487
+ reopen = true;
488
+ break;
489
+ }
490
+ }
491
+ if (update.stream_offset !== void 0) {
492
+ if (update.stream_offset !== offset) failures = 0;
493
+ offset = update.stream_offset;
494
+ options.onOffset?.(offset);
495
+ }
496
+ if (update.new_result_stream) {
497
+ failures = 0;
498
+ yield {
499
+ type: "stream",
500
+ events: parser.push(update.new_result_stream)
501
+ };
502
+ }
503
+ if (update.completed) {
504
+ const rest = parser.flush();
505
+ if (rest.length > 0) yield {
506
+ type: "stream",
507
+ events: rest
508
+ };
509
+ yield {
510
+ type: "completed",
511
+ result: update.only_result
512
+ };
513
+ return;
514
+ }
515
+ }
516
+ } catch (e) {
517
+ if (options.signal?.aborted || isAbortError(e) || !isConnectionFailure(e)) throw e;
518
+ failures++;
519
+ if (failures >= MAX_CONNECTION_FAILURES) break;
520
+ await sleep(Math.min(RECONNECT_DELAY_MS * 2 ** failures, MAX_RECONNECT_DELAY_MS), options.signal);
521
+ continue;
522
+ }
523
+ if (options.signal?.aborted) throw abortError();
524
+ if (!reopen) {
525
+ failures++;
526
+ if (failures >= MAX_CONNECTION_FAILURES) break;
527
+ await sleep(RECONNECT_DELAY_MS, options.signal);
528
+ }
529
+ }
530
+ while (true) {
531
+ await sleep(RESULT_POLL_MS, options.signal);
532
+ try {
533
+ const { completed, result } = await api.getCompletedResult(jobId, options.signal);
534
+ if (completed) {
535
+ yield {
536
+ type: "completed",
537
+ result,
538
+ streamLost: true
539
+ };
540
+ return;
541
+ }
542
+ } catch (e) {
543
+ if (options.signal?.aborted || isAbortError(e) || !isConnectionFailure(e)) throw e;
544
+ }
545
+ }
546
+ }
547
+ /**
548
+ * A failure that says nothing about the job: the request never reached Windmill, or a
549
+ * gateway in front of it answered. A 4xx from Windmill itself (not found, refused) does.
550
+ */
551
+ function isConnectionFailure(e) {
552
+ if (e instanceof WindmillApiError) return e.status >= 500 || e.status === 0;
553
+ return e instanceof TypeError;
554
+ }
555
+ //#endregion
556
+ export { _assertClassBrand as C, _classPrivateFieldSet2 as S, _classPrivateMethodInitSpec as T, WindmillApiError as _, errorResultMessage as a, turnRunningError as b, isErrorResult as c, randomId as d, sleep as f, TurnRunningError as g, parseStreamEvents as h, conversationTitle as i, now as l, createStreamEventParser as m, abortError as n, extractChatAnswer as o, truncateTitle as p, conversationIdFor as r, isAbortError as s, followJob as t, parseJsonOr as u, WindmillChatApi as v, _classPrivateFieldInitSpec as w, _classPrivateFieldGet2 as x, readServerSentEvents as y };