@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/README.md +338 -2
- package/dist/ai-sdk.cjs +344 -0
- package/dist/ai-sdk.d.ts +38 -0
- package/dist/ai-sdk.js +342 -0
- package/dist/api.d.ts +182 -0
- package/dist/assistant-ui.cjs +122 -0
- package/dist/assistant-ui.d.ts +22 -0
- package/dist/assistant-ui.js +119 -0
- package/dist/attachments.d.ts +27 -0
- package/dist/chat-DgINfcUU.js +1182 -0
- package/dist/chat-u1tspn7Q.cjs +1211 -0
- package/dist/chat.d.ts +2 -0
- package/dist/config.d.ts +33 -0
- package/dist/follow-DXfmfPfr.js +556 -0
- package/dist/follow-Dug-Afeu.cjs +705 -0
- package/dist/follow.d.ts +33 -0
- package/dist/history.d.ts +17 -0
- package/dist/index.cjs +18 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +3 -0
- package/dist/react.cjs +64 -0
- package/dist/react.d.ts +16 -0
- package/dist/react.js +63 -0
- package/dist/stream.d.ts +38 -0
- package/dist/types.d.ts +208 -0
- package/dist/utils.d.ts +33 -0
- package/package.json +106 -4
|
@@ -0,0 +1,705 @@
|
|
|
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
|
+
Object.defineProperty(exports, "TurnRunningError", {
|
|
557
|
+
enumerable: true,
|
|
558
|
+
get: function() {
|
|
559
|
+
return TurnRunningError;
|
|
560
|
+
}
|
|
561
|
+
});
|
|
562
|
+
Object.defineProperty(exports, "WindmillApiError", {
|
|
563
|
+
enumerable: true,
|
|
564
|
+
get: function() {
|
|
565
|
+
return WindmillApiError;
|
|
566
|
+
}
|
|
567
|
+
});
|
|
568
|
+
Object.defineProperty(exports, "WindmillChatApi", {
|
|
569
|
+
enumerable: true,
|
|
570
|
+
get: function() {
|
|
571
|
+
return WindmillChatApi;
|
|
572
|
+
}
|
|
573
|
+
});
|
|
574
|
+
Object.defineProperty(exports, "_assertClassBrand", {
|
|
575
|
+
enumerable: true,
|
|
576
|
+
get: function() {
|
|
577
|
+
return _assertClassBrand;
|
|
578
|
+
}
|
|
579
|
+
});
|
|
580
|
+
Object.defineProperty(exports, "_classPrivateFieldGet2", {
|
|
581
|
+
enumerable: true,
|
|
582
|
+
get: function() {
|
|
583
|
+
return _classPrivateFieldGet2;
|
|
584
|
+
}
|
|
585
|
+
});
|
|
586
|
+
Object.defineProperty(exports, "_classPrivateFieldInitSpec", {
|
|
587
|
+
enumerable: true,
|
|
588
|
+
get: function() {
|
|
589
|
+
return _classPrivateFieldInitSpec;
|
|
590
|
+
}
|
|
591
|
+
});
|
|
592
|
+
Object.defineProperty(exports, "_classPrivateFieldSet2", {
|
|
593
|
+
enumerable: true,
|
|
594
|
+
get: function() {
|
|
595
|
+
return _classPrivateFieldSet2;
|
|
596
|
+
}
|
|
597
|
+
});
|
|
598
|
+
Object.defineProperty(exports, "_classPrivateMethodInitSpec", {
|
|
599
|
+
enumerable: true,
|
|
600
|
+
get: function() {
|
|
601
|
+
return _classPrivateMethodInitSpec;
|
|
602
|
+
}
|
|
603
|
+
});
|
|
604
|
+
Object.defineProperty(exports, "abortError", {
|
|
605
|
+
enumerable: true,
|
|
606
|
+
get: function() {
|
|
607
|
+
return abortError;
|
|
608
|
+
}
|
|
609
|
+
});
|
|
610
|
+
Object.defineProperty(exports, "conversationIdFor", {
|
|
611
|
+
enumerable: true,
|
|
612
|
+
get: function() {
|
|
613
|
+
return conversationIdFor;
|
|
614
|
+
}
|
|
615
|
+
});
|
|
616
|
+
Object.defineProperty(exports, "conversationTitle", {
|
|
617
|
+
enumerable: true,
|
|
618
|
+
get: function() {
|
|
619
|
+
return conversationTitle;
|
|
620
|
+
}
|
|
621
|
+
});
|
|
622
|
+
Object.defineProperty(exports, "createStreamEventParser", {
|
|
623
|
+
enumerable: true,
|
|
624
|
+
get: function() {
|
|
625
|
+
return createStreamEventParser;
|
|
626
|
+
}
|
|
627
|
+
});
|
|
628
|
+
Object.defineProperty(exports, "errorResultMessage", {
|
|
629
|
+
enumerable: true,
|
|
630
|
+
get: function() {
|
|
631
|
+
return errorResultMessage;
|
|
632
|
+
}
|
|
633
|
+
});
|
|
634
|
+
Object.defineProperty(exports, "extractChatAnswer", {
|
|
635
|
+
enumerable: true,
|
|
636
|
+
get: function() {
|
|
637
|
+
return extractChatAnswer;
|
|
638
|
+
}
|
|
639
|
+
});
|
|
640
|
+
Object.defineProperty(exports, "followJob", {
|
|
641
|
+
enumerable: true,
|
|
642
|
+
get: function() {
|
|
643
|
+
return followJob;
|
|
644
|
+
}
|
|
645
|
+
});
|
|
646
|
+
Object.defineProperty(exports, "isAbortError", {
|
|
647
|
+
enumerable: true,
|
|
648
|
+
get: function() {
|
|
649
|
+
return isAbortError;
|
|
650
|
+
}
|
|
651
|
+
});
|
|
652
|
+
Object.defineProperty(exports, "isErrorResult", {
|
|
653
|
+
enumerable: true,
|
|
654
|
+
get: function() {
|
|
655
|
+
return isErrorResult;
|
|
656
|
+
}
|
|
657
|
+
});
|
|
658
|
+
Object.defineProperty(exports, "now", {
|
|
659
|
+
enumerable: true,
|
|
660
|
+
get: function() {
|
|
661
|
+
return now;
|
|
662
|
+
}
|
|
663
|
+
});
|
|
664
|
+
Object.defineProperty(exports, "parseJsonOr", {
|
|
665
|
+
enumerable: true,
|
|
666
|
+
get: function() {
|
|
667
|
+
return parseJsonOr;
|
|
668
|
+
}
|
|
669
|
+
});
|
|
670
|
+
Object.defineProperty(exports, "parseStreamEvents", {
|
|
671
|
+
enumerable: true,
|
|
672
|
+
get: function() {
|
|
673
|
+
return parseStreamEvents;
|
|
674
|
+
}
|
|
675
|
+
});
|
|
676
|
+
Object.defineProperty(exports, "randomId", {
|
|
677
|
+
enumerable: true,
|
|
678
|
+
get: function() {
|
|
679
|
+
return randomId;
|
|
680
|
+
}
|
|
681
|
+
});
|
|
682
|
+
Object.defineProperty(exports, "readServerSentEvents", {
|
|
683
|
+
enumerable: true,
|
|
684
|
+
get: function() {
|
|
685
|
+
return readServerSentEvents;
|
|
686
|
+
}
|
|
687
|
+
});
|
|
688
|
+
Object.defineProperty(exports, "sleep", {
|
|
689
|
+
enumerable: true,
|
|
690
|
+
get: function() {
|
|
691
|
+
return sleep;
|
|
692
|
+
}
|
|
693
|
+
});
|
|
694
|
+
Object.defineProperty(exports, "truncateTitle", {
|
|
695
|
+
enumerable: true,
|
|
696
|
+
get: function() {
|
|
697
|
+
return truncateTitle;
|
|
698
|
+
}
|
|
699
|
+
});
|
|
700
|
+
Object.defineProperty(exports, "turnRunningError", {
|
|
701
|
+
enumerable: true,
|
|
702
|
+
get: function() {
|
|
703
|
+
return turnRunningError;
|
|
704
|
+
}
|
|
705
|
+
});
|