@windmill-labs/chat-sdk 0.0.0-stage → 1.830.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,1182 @@
|
|
|
1
|
+
import { C as _assertClassBrand, S as _classPrivateFieldSet2, T as _classPrivateMethodInitSpec, _ as WindmillApiError, a as errorResultMessage, c as isErrorResult, d as randomId, f as sleep, g as TurnRunningError, i as conversationTitle, l as now, n as abortError, o as extractChatAnswer, p as truncateTitle, s as isAbortError, t as followJob, v as WindmillChatApi, w as _classPrivateFieldInitSpec, x as _classPrivateFieldGet2 } from "./follow-DXfmfPfr.js";
|
|
2
|
+
//#region src/config.ts
|
|
3
|
+
/**
|
|
4
|
+
* The Windmill a raw app bundle runs in. A sandboxed app gets `window.process.env`
|
|
5
|
+
* from its wrapper once the viewer consented to the declared SDK scopes; an
|
|
6
|
+
* unsandboxed one runs on the Windmill origin with the viewer's session and only
|
|
7
|
+
* has `window.ctx`.
|
|
8
|
+
*/
|
|
9
|
+
function detectRawApp() {
|
|
10
|
+
const g = globalThis;
|
|
11
|
+
const env = g.process?.env;
|
|
12
|
+
if (env?.WM_RAW_APP === "true" && env.WM_TOKEN && env.BASE_URL && env.WM_WORKSPACE) return {
|
|
13
|
+
baseUrl: env.BASE_URL,
|
|
14
|
+
workspace: env.WM_WORKSPACE,
|
|
15
|
+
token: env.WM_TOKEN
|
|
16
|
+
};
|
|
17
|
+
const workspace = g.ctx?.workspace;
|
|
18
|
+
const origin = g.location?.origin;
|
|
19
|
+
if (typeof workspace === "string" && workspace && origin && origin !== "null") return {
|
|
20
|
+
baseUrl: origin,
|
|
21
|
+
workspace
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
function resolveConfig(options) {
|
|
25
|
+
if (!options.flowPath) throw new Error("windmill-chat: flowPath is required");
|
|
26
|
+
const explicitToken = options.token !== void 0;
|
|
27
|
+
const detected = options.baseUrl && options.workspace && explicitToken ? void 0 : detectRawApp();
|
|
28
|
+
const baseUrl = options.baseUrl ?? detected?.baseUrl;
|
|
29
|
+
const workspace = options.workspace ?? detected?.workspace;
|
|
30
|
+
if (!baseUrl || !workspace) throw new Error("windmill-chat: pass baseUrl and workspace. They are only detected inside a raw app: an unsandboxed one on the Windmill origin, or a sandboxed one whose policy declares frontend SDK scopes.");
|
|
31
|
+
const token = options.token ?? (options.baseUrl === void 0 || options.baseUrl === detected?.baseUrl ? detected?.token : void 0);
|
|
32
|
+
return {
|
|
33
|
+
flowPath: options.flowPath,
|
|
34
|
+
baseUrl,
|
|
35
|
+
workspace,
|
|
36
|
+
token,
|
|
37
|
+
history: options.history ?? (explicitToken ? "local" : "server"),
|
|
38
|
+
historyExplicit: options.history !== void 0,
|
|
39
|
+
inputs: options.inputs ?? {},
|
|
40
|
+
fetch: options.fetch,
|
|
41
|
+
storage: options.storage,
|
|
42
|
+
storageKey: options.storageKey,
|
|
43
|
+
pageSize: options.pageSize ?? 50,
|
|
44
|
+
pollDelayMs: options.pollDelayMs,
|
|
45
|
+
run: options.run,
|
|
46
|
+
onFinish: options.onFinish,
|
|
47
|
+
onError: options.onError
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
//#endregion
|
|
51
|
+
//#region src/history.ts
|
|
52
|
+
const MAX_CONVERSATIONS = 100;
|
|
53
|
+
/**
|
|
54
|
+
* Conversation history in the browser, for a credential shared by every visitor
|
|
55
|
+
* (server history would show them each other's chats). One key per flow, and per
|
|
56
|
+
* `storageKey` when the caller sets one; every operation re-reads the store so
|
|
57
|
+
* several tabs stay consistent.
|
|
58
|
+
*/
|
|
59
|
+
function createLocalHistory(storage, key) {
|
|
60
|
+
const store = storage ?? defaultStorage();
|
|
61
|
+
const read = () => {
|
|
62
|
+
try {
|
|
63
|
+
const raw = store.getItem(key);
|
|
64
|
+
if (raw) {
|
|
65
|
+
const parsed = JSON.parse(raw);
|
|
66
|
+
if (parsed && parsed.v === 1) return parsed;
|
|
67
|
+
}
|
|
68
|
+
} catch {}
|
|
69
|
+
return {
|
|
70
|
+
v: 1,
|
|
71
|
+
conversations: [],
|
|
72
|
+
messages: {}
|
|
73
|
+
};
|
|
74
|
+
};
|
|
75
|
+
const write = (snapshot) => {
|
|
76
|
+
try {
|
|
77
|
+
store.setItem(key, JSON.stringify(snapshot));
|
|
78
|
+
} catch {}
|
|
79
|
+
};
|
|
80
|
+
return {
|
|
81
|
+
listConversations: () => read().conversations,
|
|
82
|
+
getMessages: (id) => read().messages[id] ?? [],
|
|
83
|
+
upsertConversation(conversation) {
|
|
84
|
+
const s = read();
|
|
85
|
+
s.conversations = [conversation, ...s.conversations.filter((c) => c.id !== conversation.id)];
|
|
86
|
+
for (const dropped of s.conversations.splice(MAX_CONVERSATIONS)) delete s.messages[dropped.id];
|
|
87
|
+
write(s);
|
|
88
|
+
},
|
|
89
|
+
renameConversation(id, title) {
|
|
90
|
+
const s = read();
|
|
91
|
+
s.conversations = s.conversations.map((c) => c.id === id ? {
|
|
92
|
+
...c,
|
|
93
|
+
title
|
|
94
|
+
} : c);
|
|
95
|
+
write(s);
|
|
96
|
+
},
|
|
97
|
+
saveMessages(id, messages) {
|
|
98
|
+
const s = read();
|
|
99
|
+
s.messages[id] = messages.map((m) => ({
|
|
100
|
+
...m,
|
|
101
|
+
pending: false
|
|
102
|
+
}));
|
|
103
|
+
write(s);
|
|
104
|
+
},
|
|
105
|
+
deleteConversation(id) {
|
|
106
|
+
const s = read();
|
|
107
|
+
s.conversations = s.conversations.filter((c) => c.id !== id);
|
|
108
|
+
delete s.messages[id];
|
|
109
|
+
write(s);
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
function defaultStorage() {
|
|
114
|
+
try {
|
|
115
|
+
const ls = globalThis.localStorage;
|
|
116
|
+
if (ls) {
|
|
117
|
+
const probe = "__windmill_chat_probe__";
|
|
118
|
+
ls.setItem(probe, "1");
|
|
119
|
+
ls.removeItem(probe);
|
|
120
|
+
return ls;
|
|
121
|
+
}
|
|
122
|
+
} catch {}
|
|
123
|
+
const memory = /* @__PURE__ */ new Map();
|
|
124
|
+
return {
|
|
125
|
+
getItem: (k) => memory.get(k) ?? null,
|
|
126
|
+
setItem: (k, v) => void memory.set(k, v),
|
|
127
|
+
removeItem: (k) => void memory.delete(k)
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
//#endregion
|
|
131
|
+
//#region src/attachments.ts
|
|
132
|
+
/**
|
|
133
|
+
* Where a chat's uploads live in the workspace's object storage. Under `windmill_uploads/`
|
|
134
|
+
* because the default Enterprise storage permissions grant every user write and read there
|
|
135
|
+
* and deny any other top-level prefix: a key outside it is refused for non-admins, both on
|
|
136
|
+
* upload and when the agent's job reads the file back.
|
|
137
|
+
*/
|
|
138
|
+
const CHAT_UPLOADS_PREFIX = "windmill_uploads/chat";
|
|
139
|
+
/**
|
|
140
|
+
* The extension each type must be stored under. The worker reads an attachment's media type
|
|
141
|
+
* from the key's extension only (`mime_guess` in `windmill-ai/src/image_handler.rs`, falling
|
|
142
|
+
* back to `image/png`), never from the stored content type, so the extension must be true.
|
|
143
|
+
*/
|
|
144
|
+
const EXTENSION_BY_MEDIA_TYPE = {
|
|
145
|
+
"image/png": "png",
|
|
146
|
+
"image/jpeg": "jpg",
|
|
147
|
+
"application/pdf": "pdf"
|
|
148
|
+
};
|
|
149
|
+
/**
|
|
150
|
+
* The name an attachment is stored under: the picked name with the extension its media type
|
|
151
|
+
* needs, e.g. a `photo.webp` re-encoded to PNG becomes `photo.png`. Other types keep their name.
|
|
152
|
+
*/
|
|
153
|
+
function storedAttachmentName(filename, mediaType) {
|
|
154
|
+
const extension = EXTENSION_BY_MEDIA_TYPE[mediaType];
|
|
155
|
+
if (!extension) return filename;
|
|
156
|
+
return `${filename.replace(/\.[^./]+$/, "") || filename}.${extension}`;
|
|
157
|
+
}
|
|
158
|
+
/** The bytes of an attachment as a Blob carrying its media type. */
|
|
159
|
+
function attachmentBlob(attachment) {
|
|
160
|
+
const data = typeof attachment.data === "string" ? dataUrlToBlob(attachment.data, "application/octet-stream") : attachment.data;
|
|
161
|
+
return attachment.mediaType && attachment.mediaType !== data.type ? new Blob([data], { type: attachment.mediaType }) : data;
|
|
162
|
+
}
|
|
163
|
+
function dataUrlToBlob(dataUrl, fallbackType) {
|
|
164
|
+
const comma = dataUrl.indexOf(",");
|
|
165
|
+
if (!dataUrl.startsWith("data:") || comma === -1) throw new Error("windmill-chat: an attachment given as a string must be a data: URL");
|
|
166
|
+
const header = dataUrl.slice(5, comma);
|
|
167
|
+
const isBase64 = header.endsWith(";base64");
|
|
168
|
+
const mediaType = (isBase64 ? header.slice(0, -7) : header) || fallbackType;
|
|
169
|
+
const payload = dataUrl.slice(comma + 1);
|
|
170
|
+
if (!isBase64) return new Blob([decodeURIComponent(payload)], { type: mediaType });
|
|
171
|
+
const binary = atob(payload);
|
|
172
|
+
const bytes = new Uint8Array(binary.length);
|
|
173
|
+
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
|
174
|
+
return new Blob([bytes], { type: mediaType });
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Put each attachment in the workspace's object storage and hand back what the agent reads.
|
|
178
|
+
* The key's turn prefix and per-file index keep two files with the same name, in this turn or
|
|
179
|
+
* an earlier one, from overwriting each other; the name stays the last segment.
|
|
180
|
+
*/
|
|
181
|
+
async function uploadAttachments(api, attachments, turnId, signal) {
|
|
182
|
+
const prefix = `${CHAT_UPLOADS_PREFIX}/${turnId}`;
|
|
183
|
+
if (signal?.aborted) throw abortError();
|
|
184
|
+
const batch = new AbortController();
|
|
185
|
+
const abortBatch = () => batch.abort();
|
|
186
|
+
signal?.addEventListener("abort", abortBatch, { once: true });
|
|
187
|
+
try {
|
|
188
|
+
const results = await Promise.allSettled(attachments.map(async (attachment, index) => {
|
|
189
|
+
try {
|
|
190
|
+
const blob = attachmentBlob(attachment);
|
|
191
|
+
const filename = storedAttachmentName(attachment.name || `attachment-${index + 1}`, blob.type);
|
|
192
|
+
const { file_key } = await api.uploadFile(`${prefix}/${index}/${filename}`, blob, {
|
|
193
|
+
contentType: blob.type,
|
|
194
|
+
signal: batch.signal
|
|
195
|
+
});
|
|
196
|
+
return {
|
|
197
|
+
s3: file_key,
|
|
198
|
+
filename
|
|
199
|
+
};
|
|
200
|
+
} catch (e) {
|
|
201
|
+
batch.abort();
|
|
202
|
+
throw e;
|
|
203
|
+
}
|
|
204
|
+
}));
|
|
205
|
+
const reasons = results.flatMap((r) => r.status === "rejected" ? [r.reason] : []);
|
|
206
|
+
if (reasons.length === 0 && !signal?.aborted) return results.flatMap((r) => r.status === "fulfilled" ? [r.value] : []);
|
|
207
|
+
throw reasons.find((reason) => !isAbortError(reason)) ?? reasons[0] ?? abortError();
|
|
208
|
+
} finally {
|
|
209
|
+
signal?.removeEventListener("abort", abortBatch);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
//#endregion
|
|
213
|
+
//#region src/chat.ts
|
|
214
|
+
const POLL_INTERVAL_MS = 1e3;
|
|
215
|
+
/** Local history mirrors the state; a stream of deltas is coalesced into one write. */
|
|
216
|
+
const PERSIST_DEBOUNCE_MS = 250;
|
|
217
|
+
/** Messages persist from spawned tasks that can land just after the flow completes. */
|
|
218
|
+
const RECONCILE_ATTEMPTS = 3;
|
|
219
|
+
const RECONCILE_DELAY_MS = 400;
|
|
220
|
+
/** Rows per read of what a turn wrote; a fuller page is read on from its last row. */
|
|
221
|
+
const ROWS_PAGE = 100;
|
|
222
|
+
function createChat(options) {
|
|
223
|
+
return new ChatImpl(options);
|
|
224
|
+
}
|
|
225
|
+
var _config = /* @__PURE__ */ new WeakMap();
|
|
226
|
+
var _api = /* @__PURE__ */ new WeakMap();
|
|
227
|
+
var _local = /* @__PURE__ */ new WeakMap();
|
|
228
|
+
var _listeners = /* @__PURE__ */ new WeakMap();
|
|
229
|
+
var _state = /* @__PURE__ */ new WeakMap();
|
|
230
|
+
var _turn = /* @__PURE__ */ new WeakMap();
|
|
231
|
+
var _page = /* @__PURE__ */ new WeakMap();
|
|
232
|
+
var _conversationKind = /* @__PURE__ */ new WeakMap();
|
|
233
|
+
var _persistTimer = /* @__PURE__ */ new WeakMap();
|
|
234
|
+
var _selecting = /* @__PURE__ */ new WeakMap();
|
|
235
|
+
var _epoch = /* @__PURE__ */ new WeakMap();
|
|
236
|
+
var _ChatImpl_brand = /* @__PURE__ */ new WeakSet();
|
|
237
|
+
var ChatImpl = class {
|
|
238
|
+
constructor(options) {
|
|
239
|
+
_classPrivateMethodInitSpec(this, _ChatImpl_brand);
|
|
240
|
+
_classPrivateFieldInitSpec(this, _config, void 0);
|
|
241
|
+
_classPrivateFieldInitSpec(this, _api, void 0);
|
|
242
|
+
_classPrivateFieldInitSpec(this, _local, void 0);
|
|
243
|
+
_classPrivateFieldInitSpec(this, _listeners, /* @__PURE__ */ new Set());
|
|
244
|
+
_classPrivateFieldInitSpec(this, _state, void 0);
|
|
245
|
+
_classPrivateFieldInitSpec(this, _turn, void 0);
|
|
246
|
+
_classPrivateFieldInitSpec(this, _page, 1);
|
|
247
|
+
_classPrivateFieldInitSpec(this, _conversationKind, void 0);
|
|
248
|
+
_classPrivateFieldInitSpec(this, _persistTimer, void 0);
|
|
249
|
+
_classPrivateFieldInitSpec(this, _selecting, Promise.resolve());
|
|
250
|
+
_classPrivateFieldInitSpec(this, _epoch, 0);
|
|
251
|
+
this.getState = () => _classPrivateFieldGet2(_state, this);
|
|
252
|
+
this.subscribe = (listener) => {
|
|
253
|
+
_classPrivateFieldGet2(_listeners, this).add(listener);
|
|
254
|
+
listener(_classPrivateFieldGet2(_state, this));
|
|
255
|
+
return () => {
|
|
256
|
+
_classPrivateFieldGet2(_listeners, this).delete(listener);
|
|
257
|
+
};
|
|
258
|
+
};
|
|
259
|
+
this.sendMessage = async (text, options = {}) => {
|
|
260
|
+
var _this$epoch;
|
|
261
|
+
const content = text.trim();
|
|
262
|
+
if (!content) {
|
|
263
|
+
if (options.attachments?.length) throw new Error("windmill-chat: attachments need a message to go with them");
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
if (_classPrivateFieldGet2(_turn, this)) throw new Error("windmill-chat: a message is already being answered; call stop() first");
|
|
267
|
+
const attachments = options.attachments ?? [];
|
|
268
|
+
const attachmentsInput = options.attachmentsInput;
|
|
269
|
+
if (attachments.length > 0 && !attachmentsInput) throw new Error("windmill-chat: attachments need `attachmentsInput`, the flow input that takes them");
|
|
270
|
+
if (attachmentsInput && !attachmentsInput.multiple && attachments.length > 1) throw new Error(`windmill-chat: \`${attachmentsInput.name}\` holds one file; got ${attachments.length}`);
|
|
271
|
+
const isNew = _classPrivateFieldGet2(_state, this).conversationId === void 0;
|
|
272
|
+
const conversationId = _classPrivateFieldGet2(_state, this).conversationId ?? randomId();
|
|
273
|
+
const turn = {
|
|
274
|
+
controller: new AbortController(),
|
|
275
|
+
conversationId,
|
|
276
|
+
userMessageId: `pending-${randomId()}`,
|
|
277
|
+
isNew,
|
|
278
|
+
started: false,
|
|
279
|
+
withdrawn: false,
|
|
280
|
+
streamedText: false
|
|
281
|
+
};
|
|
282
|
+
_classPrivateFieldSet2(_turn, this, turn);
|
|
283
|
+
_classPrivateFieldSet2(_epoch, this, (_this$epoch = _classPrivateFieldGet2(_epoch, this), _this$epoch++, _this$epoch));
|
|
284
|
+
const timestamp = now();
|
|
285
|
+
const touched = {
|
|
286
|
+
..._classPrivateFieldGet2(_state, this).conversations.find((c) => c.id === conversationId) ?? {
|
|
287
|
+
id: conversationId,
|
|
288
|
+
title: conversationTitle(content),
|
|
289
|
+
createdAt: timestamp,
|
|
290
|
+
updatedAt: timestamp
|
|
291
|
+
},
|
|
292
|
+
updatedAt: timestamp
|
|
293
|
+
};
|
|
294
|
+
_assertClassBrand(_ChatImpl_brand, this, _set).call(this, {
|
|
295
|
+
conversationId,
|
|
296
|
+
messages: [..._classPrivateFieldGet2(_state, this).messages, {
|
|
297
|
+
id: turn.userMessageId,
|
|
298
|
+
role: "user",
|
|
299
|
+
content,
|
|
300
|
+
success: true,
|
|
301
|
+
createdAt: timestamp,
|
|
302
|
+
pending: true
|
|
303
|
+
}],
|
|
304
|
+
status: "submitted",
|
|
305
|
+
error: void 0
|
|
306
|
+
});
|
|
307
|
+
try {
|
|
308
|
+
const args = {
|
|
309
|
+
..._classPrivateFieldGet2(_config, this).inputs,
|
|
310
|
+
...options.inputs,
|
|
311
|
+
user_message: content
|
|
312
|
+
};
|
|
313
|
+
if (attachmentsInput && attachments.length > 0) {
|
|
314
|
+
const uploaded = await uploadAttachments(_classPrivateFieldGet2(_api, this), attachments, randomId(), turn.controller.signal);
|
|
315
|
+
args[attachmentsInput.name] = attachmentsInput.multiple ? uploaded : uploaded[0];
|
|
316
|
+
const carried = uploaded.map((u) => ({
|
|
317
|
+
input: attachmentsInput.name,
|
|
318
|
+
s3: u.s3,
|
|
319
|
+
filename: u.filename
|
|
320
|
+
}));
|
|
321
|
+
if (_assertClassBrand(_ChatImpl_brand, this, _turnActive).call(this, turn)) _assertClassBrand(_ChatImpl_brand, this, _set).call(this, { messages: _classPrivateFieldGet2(_state, this).messages.map((m) => m.id === turn.userMessageId ? {
|
|
322
|
+
...m,
|
|
323
|
+
attachments: carried
|
|
324
|
+
} : m) });
|
|
325
|
+
}
|
|
326
|
+
if (turn.controller.signal.aborted) throw abortError();
|
|
327
|
+
turn.started = true;
|
|
328
|
+
_assertClassBrand(_ChatImpl_brand, this, _set).call(this, { conversations: [touched, ..._classPrivateFieldGet2(_state, this).conversations.filter((c) => c.id !== conversationId)] });
|
|
329
|
+
_assertClassBrand(_ChatImpl_brand, this, _rememberConversation).call(this);
|
|
330
|
+
const context = {
|
|
331
|
+
memoryId: conversationId,
|
|
332
|
+
conversationId,
|
|
333
|
+
signal: turn.controller.signal
|
|
334
|
+
};
|
|
335
|
+
turn.jobId = _classPrivateFieldGet2(_config, this).run ? await _classPrivateFieldGet2(_config, this).run(args, context) : await _classPrivateFieldGet2(_api, this).runFlow(_classPrivateFieldGet2(_config, this).flowPath, args, context);
|
|
336
|
+
} catch (e) {
|
|
337
|
+
try {
|
|
338
|
+
if (e instanceof TurnRunningError || !turn.started) {
|
|
339
|
+
_assertClassBrand(_ChatImpl_brand, this, _withdrawTurn).call(this, turn);
|
|
340
|
+
throw e;
|
|
341
|
+
}
|
|
342
|
+
if (turn.controller.signal.aborted || isAbortError(e)) return;
|
|
343
|
+
_assertClassBrand(_ChatImpl_brand, this, _failTurn).call(this, turn, e);
|
|
344
|
+
return;
|
|
345
|
+
} finally {
|
|
346
|
+
if (_classPrivateFieldGet2(_turn, this) === turn) _classPrivateFieldSet2(_turn, this, void 0);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
await _assertClassBrand(_ChatImpl_brand, this, _followTurn).call(this, turn, isNew);
|
|
350
|
+
};
|
|
351
|
+
this.resumeTurn = async ({ jobId, userSeq }) => {
|
|
352
|
+
var _this$epoch3;
|
|
353
|
+
const conversationId = _classPrivateFieldGet2(_state, this).conversationId;
|
|
354
|
+
if (!conversationId || _classPrivateFieldGet2(_turn, this)) return;
|
|
355
|
+
if (_classPrivateFieldGet2(_state, this).history !== "server") {
|
|
356
|
+
if (_classPrivateFieldGet2(_state, this).status === "submitted") _assertClassBrand(_ChatImpl_brand, this, _set).call(this, { status: "idle" });
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
const turn = {
|
|
360
|
+
controller: new AbortController(),
|
|
361
|
+
conversationId,
|
|
362
|
+
userMessageId: "",
|
|
363
|
+
jobId,
|
|
364
|
+
isNew: false,
|
|
365
|
+
started: true,
|
|
366
|
+
withdrawn: false,
|
|
367
|
+
streamedText: false
|
|
368
|
+
};
|
|
369
|
+
_classPrivateFieldSet2(_turn, this, turn);
|
|
370
|
+
_classPrivateFieldSet2(_epoch, this, (_this$epoch3 = _classPrivateFieldGet2(_epoch, this), _this$epoch3++, _this$epoch3));
|
|
371
|
+
_assertClassBrand(_ChatImpl_brand, this, _set).call(this, {
|
|
372
|
+
status: "submitted",
|
|
373
|
+
error: void 0
|
|
374
|
+
});
|
|
375
|
+
try {
|
|
376
|
+
await _classPrivateFieldGet2(_selecting, this);
|
|
377
|
+
if (!_assertClassBrand(_ChatImpl_brand, this, _turnActive).call(this, turn)) return;
|
|
378
|
+
const newest = _assertClassBrand(_ChatImpl_brand, this, _latestUserAfter).call(this, userSeq);
|
|
379
|
+
if (newest) {
|
|
380
|
+
if (!newest.jobId) {
|
|
381
|
+
if (_classPrivateFieldGet2(_turn, this) === turn) _classPrivateFieldSet2(_turn, this, void 0);
|
|
382
|
+
_assertClassBrand(_ChatImpl_brand, this, _set).call(this, { status: "idle" });
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
turn.jobId = newest.jobId;
|
|
386
|
+
userSeq = newest.seq;
|
|
387
|
+
}
|
|
388
|
+
const held = _classPrivateFieldGet2(_state, this).messages;
|
|
389
|
+
const at = held.findIndex((m) => m.seq === userSeq);
|
|
390
|
+
let user = at === -1 ? void 0 : held[at];
|
|
391
|
+
let messages = held.filter((m, i) => {
|
|
392
|
+
if (m.seq !== void 0) return m.seq < userSeq;
|
|
393
|
+
if (m.pending || m.jobId === turn.jobId) return false;
|
|
394
|
+
return m.jobId !== void 0 || at !== -1 && i < at;
|
|
395
|
+
});
|
|
396
|
+
if (user) messages = [...messages, user];
|
|
397
|
+
if (!user) {
|
|
398
|
+
const [row] = await _classPrivateFieldGet2(_api, this).listMessages(conversationId, {
|
|
399
|
+
afterSeq: userSeq - 1,
|
|
400
|
+
perPage: 1,
|
|
401
|
+
signal: turn.controller.signal
|
|
402
|
+
});
|
|
403
|
+
if (!_assertClassBrand(_ChatImpl_brand, this, _turnActive).call(this, turn)) return;
|
|
404
|
+
if (row?.created_seq !== userSeq || row.message_type !== "user") throw new Error("windmill-chat: the message that started the running turn is gone");
|
|
405
|
+
user = fromRow(row);
|
|
406
|
+
messages = [...messages, user];
|
|
407
|
+
}
|
|
408
|
+
turn.userMessageId = user.id;
|
|
409
|
+
_assertClassBrand(_ChatImpl_brand, this, _set).call(this, { messages });
|
|
410
|
+
} catch (e) {
|
|
411
|
+
if (!(turn.controller.signal.aborted || isAbortError(e))) _assertClassBrand(_ChatImpl_brand, this, _failTurn).call(this, turn, e);
|
|
412
|
+
if (_classPrivateFieldGet2(_turn, this) === turn) _classPrivateFieldSet2(_turn, this, void 0);
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
await _assertClassBrand(_ChatImpl_brand, this, _followTurn).call(this, turn, false);
|
|
416
|
+
};
|
|
417
|
+
this.stop = async () => {
|
|
418
|
+
const turn = _classPrivateFieldGet2(_turn, this);
|
|
419
|
+
if (!turn) return;
|
|
420
|
+
_assertClassBrand(_ChatImpl_brand, this, _detachTurn).call(this);
|
|
421
|
+
if (!turn.started) {
|
|
422
|
+
_assertClassBrand(_ChatImpl_brand, this, _withdrawTurn).call(this, turn);
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
if (_classPrivateFieldGet2(_state, this).conversationId === turn.conversationId) {
|
|
426
|
+
_assertClassBrand(_ChatImpl_brand, this, _set).call(this, {
|
|
427
|
+
messages: finalized(_classPrivateFieldGet2(_state, this).messages),
|
|
428
|
+
status: "idle"
|
|
429
|
+
});
|
|
430
|
+
_assertClassBrand(_ChatImpl_brand, this, _persistLocal).call(this);
|
|
431
|
+
}
|
|
432
|
+
if (turn.jobId) await _classPrivateFieldGet2(_api, this).cancelJob(turn.jobId).catch(() => {});
|
|
433
|
+
if (_classPrivateFieldGet2(_state, this).history !== "server") return;
|
|
434
|
+
await sleep(RECONCILE_DELAY_MS).catch(() => {});
|
|
435
|
+
if (_classPrivateFieldGet2(_turn, this) || _classPrivateFieldGet2(_state, this).conversationId !== turn.conversationId) return;
|
|
436
|
+
await _assertClassBrand(_ChatImpl_brand, this, _syncFromServer).call(this, turn.conversationId).catch(() => {});
|
|
437
|
+
};
|
|
438
|
+
this.newConversation = () => {
|
|
439
|
+
_assertClassBrand(_ChatImpl_brand, this, _leaveConversation).call(this);
|
|
440
|
+
_classPrivateFieldSet2(_page, this, 1);
|
|
441
|
+
_assertClassBrand(_ChatImpl_brand, this, _set).call(this, {
|
|
442
|
+
conversationId: void 0,
|
|
443
|
+
messages: [],
|
|
444
|
+
status: "idle",
|
|
445
|
+
error: void 0,
|
|
446
|
+
loadingMessages: false,
|
|
447
|
+
hasMoreMessages: false
|
|
448
|
+
});
|
|
449
|
+
};
|
|
450
|
+
this.selectConversation = (conversationId) => {
|
|
451
|
+
if (conversationId === _classPrivateFieldGet2(_state, this).conversationId) return _classPrivateFieldGet2(_selecting, this);
|
|
452
|
+
const selecting = _assertClassBrand(_ChatImpl_brand, this, _select).call(this, conversationId);
|
|
453
|
+
_classPrivateFieldSet2(_selecting, this, selecting.catch(() => {}));
|
|
454
|
+
return selecting;
|
|
455
|
+
};
|
|
456
|
+
this.loadConversations = async (options = {}) => {
|
|
457
|
+
const page = options.page ?? 1;
|
|
458
|
+
const kindChanged = "kind" in options && options.kind !== _classPrivateFieldGet2(_conversationKind, this);
|
|
459
|
+
if ("kind" in options) _classPrivateFieldSet2(_conversationKind, this, options.kind);
|
|
460
|
+
const kind = _classPrivateFieldGet2(_conversationKind, this);
|
|
461
|
+
let conversations;
|
|
462
|
+
if (_classPrivateFieldGet2(_state, this).history === "server") try {
|
|
463
|
+
conversations = (await _classPrivateFieldGet2(_api, this).listConversations(_classPrivateFieldGet2(_config, this).flowPath, {
|
|
464
|
+
page,
|
|
465
|
+
perPage: options.perPage ?? _classPrivateFieldGet2(_config, this).pageSize,
|
|
466
|
+
kind
|
|
467
|
+
})).map(fromConversation);
|
|
468
|
+
} catch (e) {
|
|
469
|
+
if (!_assertClassBrand(_ChatImpl_brand, this, _fallBackToLocal).call(this, e)) throw e;
|
|
470
|
+
conversations = _classPrivateFieldGet2(_local, this).listConversations();
|
|
471
|
+
}
|
|
472
|
+
else conversations = _classPrivateFieldGet2(_state, this).history === "local" ? _classPrivateFieldGet2(_local, this).listConversations() : [];
|
|
473
|
+
if (kind !== _classPrivateFieldGet2(_conversationKind, this)) return conversations;
|
|
474
|
+
const known = new Set(_classPrivateFieldGet2(_state, this).conversations.map((c) => c.id));
|
|
475
|
+
_assertClassBrand(_ChatImpl_brand, this, _set).call(this, { conversations: page === 1 || kindChanged ? conversations : [..._classPrivateFieldGet2(_state, this).conversations, ...conversations.filter((c) => !known.has(c.id))] });
|
|
476
|
+
return conversations;
|
|
477
|
+
};
|
|
478
|
+
this.deleteConversation = async (conversationId) => {
|
|
479
|
+
if (_classPrivateFieldGet2(_state, this).conversationId === conversationId) {
|
|
480
|
+
_assertClassBrand(_ChatImpl_brand, this, _detachTurn).call(this);
|
|
481
|
+
clearTimeout(_classPrivateFieldGet2(_persistTimer, this));
|
|
482
|
+
_classPrivateFieldSet2(_persistTimer, this, void 0);
|
|
483
|
+
this.newConversation();
|
|
484
|
+
}
|
|
485
|
+
if (_classPrivateFieldGet2(_state, this).history === "server") await _classPrivateFieldGet2(_api, this).deleteConversation(conversationId);
|
|
486
|
+
else if (_classPrivateFieldGet2(_state, this).history === "local") _classPrivateFieldGet2(_local, this).deleteConversation(conversationId);
|
|
487
|
+
_assertClassBrand(_ChatImpl_brand, this, _set).call(this, { conversations: _classPrivateFieldGet2(_state, this).conversations.filter((c) => c.id !== conversationId) });
|
|
488
|
+
};
|
|
489
|
+
this.renameConversation = async (conversationId, title) => {
|
|
490
|
+
const trimmed = truncateTitle(title.trim());
|
|
491
|
+
if (!trimmed) return;
|
|
492
|
+
if (_classPrivateFieldGet2(_state, this).history === "server") await _classPrivateFieldGet2(_api, this).renameConversation(conversationId, trimmed);
|
|
493
|
+
else if (_classPrivateFieldGet2(_state, this).history === "local") _classPrivateFieldGet2(_local, this).renameConversation(conversationId, trimmed);
|
|
494
|
+
_assertClassBrand(_ChatImpl_brand, this, _set).call(this, { conversations: _classPrivateFieldGet2(_state, this).conversations.map((c) => c.id === conversationId ? {
|
|
495
|
+
...c,
|
|
496
|
+
title: trimmed
|
|
497
|
+
} : c) });
|
|
498
|
+
};
|
|
499
|
+
this.loadOlderMessages = async () => {
|
|
500
|
+
const conversationId = _classPrivateFieldGet2(_state, this).conversationId;
|
|
501
|
+
if (!conversationId || _classPrivateFieldGet2(_state, this).history !== "server" || !_classPrivateFieldGet2(_state, this).hasMoreMessages || _classPrivateFieldGet2(_state, this).loadingMessages) return;
|
|
502
|
+
const page = _classPrivateFieldGet2(_page, this) + 1;
|
|
503
|
+
_assertClassBrand(_ChatImpl_brand, this, _set).call(this, { loadingMessages: true });
|
|
504
|
+
try {
|
|
505
|
+
const rows = await _classPrivateFieldGet2(_api, this).listMessages(conversationId, {
|
|
506
|
+
page,
|
|
507
|
+
perPage: _classPrivateFieldGet2(_config, this).pageSize
|
|
508
|
+
});
|
|
509
|
+
if (_classPrivateFieldGet2(_state, this).conversationId !== conversationId) return;
|
|
510
|
+
const known = new Set(_classPrivateFieldGet2(_state, this).messages.map((m) => m.serverId ?? m.id));
|
|
511
|
+
const oldest = _classPrivateFieldGet2(_state, this).messages.reduce((min, m) => m.seq !== void 0 && (min === void 0 || m.seq < min) ? m.seq : min, void 0);
|
|
512
|
+
const older = rows.map(fromRow).filter((m) => !known.has(m.id) && (oldest === void 0 || m.seq < oldest));
|
|
513
|
+
_classPrivateFieldSet2(_page, this, page);
|
|
514
|
+
_assertClassBrand(_ChatImpl_brand, this, _set).call(this, {
|
|
515
|
+
messages: [...older, ..._classPrivateFieldGet2(_state, this).messages],
|
|
516
|
+
hasMoreMessages: rows.length === _classPrivateFieldGet2(_config, this).pageSize
|
|
517
|
+
});
|
|
518
|
+
} finally {
|
|
519
|
+
if (_classPrivateFieldGet2(_state, this).conversationId === conversationId) _assertClassBrand(_ChatImpl_brand, this, _set).call(this, { loadingMessages: false });
|
|
520
|
+
}
|
|
521
|
+
};
|
|
522
|
+
this.refreshMessages = async () => {
|
|
523
|
+
const conversationId = _classPrivateFieldGet2(_state, this).conversationId;
|
|
524
|
+
if (!conversationId || _classPrivateFieldGet2(_turn, this) || _classPrivateFieldGet2(_state, this).history !== "server" || _classPrivateFieldGet2(_state, this).loadingMessages) return;
|
|
525
|
+
const newestBefore = latestSeq(_classPrivateFieldGet2(_state, this).messages);
|
|
526
|
+
const failure = lastFailureShown(_classPrivateFieldGet2(_state, this).messages);
|
|
527
|
+
const epoch = _classPrivateFieldGet2(_epoch, this);
|
|
528
|
+
await _assertClassBrand(_ChatImpl_brand, this, _syncFromServer).call(this, conversationId, () => _classPrivateFieldGet2(_epoch, this) === epoch);
|
|
529
|
+
if (_classPrivateFieldGet2(_epoch, this) !== epoch) return;
|
|
530
|
+
if (!failure || _classPrivateFieldGet2(_state, this).conversationId !== conversationId) return;
|
|
531
|
+
if (!_classPrivateFieldGet2(_state, this).messages.some((m) => m.id === failure.id)) return;
|
|
532
|
+
const jobs = failure.jobId ? await _assertClassBrand(_ChatImpl_brand, this, _turnJobIds).call(this, failure.jobId) : void 0;
|
|
533
|
+
if (_classPrivateFieldGet2(_state, this).conversationId !== conversationId) return;
|
|
534
|
+
const held = _classPrivateFieldGet2(_state, this).messages;
|
|
535
|
+
if (!held.some((m) => m.id === failure.id)) return;
|
|
536
|
+
if (!held.some((m) => m.seq !== void 0 && m.seq > newestBefore && m.role === "assistant" && m.success && (jobs === void 0 || m.jobId === void 0 || jobs.has(m.jobId)))) return;
|
|
537
|
+
const settles = _classPrivateFieldGet2(_state, this).status === "error" && lastFailureShown(held)?.id === failure.id;
|
|
538
|
+
_assertClassBrand(_ChatImpl_brand, this, _set).call(this, {
|
|
539
|
+
messages: held.filter((m) => m.id !== failure.id),
|
|
540
|
+
...settles ? {
|
|
541
|
+
status: "idle",
|
|
542
|
+
error: void 0
|
|
543
|
+
} : {}
|
|
544
|
+
});
|
|
545
|
+
};
|
|
546
|
+
this.destroy = () => {
|
|
547
|
+
_assertClassBrand(_ChatImpl_brand, this, _leaveConversation).call(this);
|
|
548
|
+
};
|
|
549
|
+
_classPrivateFieldSet2(_config, this, resolveConfig(options));
|
|
550
|
+
_classPrivateFieldSet2(_api, this, new WindmillChatApi({
|
|
551
|
+
baseUrl: _classPrivateFieldGet2(_config, this).baseUrl,
|
|
552
|
+
workspace: _classPrivateFieldGet2(_config, this).workspace,
|
|
553
|
+
token: _classPrivateFieldGet2(_config, this).token,
|
|
554
|
+
fetch: _classPrivateFieldGet2(_config, this).fetch,
|
|
555
|
+
pollDelayMs: _classPrivateFieldGet2(_config, this).pollDelayMs
|
|
556
|
+
}));
|
|
557
|
+
_classPrivateFieldSet2(_local, this, createLocalHistory(_classPrivateFieldGet2(_config, this).storage, `windmill-chat:${_classPrivateFieldGet2(_config, this).baseUrl}:${_classPrivateFieldGet2(_config, this).workspace}:${_classPrivateFieldGet2(_config, this).flowPath}` + (_classPrivateFieldGet2(_config, this).storageKey ? `:${_classPrivateFieldGet2(_config, this).storageKey}` : "")));
|
|
558
|
+
_classPrivateFieldSet2(_state, this, {
|
|
559
|
+
conversationId: void 0,
|
|
560
|
+
messages: [],
|
|
561
|
+
status: "idle",
|
|
562
|
+
error: void 0,
|
|
563
|
+
conversations: [],
|
|
564
|
+
history: _classPrivateFieldGet2(_config, this).history,
|
|
565
|
+
loadingMessages: false,
|
|
566
|
+
hasMoreMessages: false
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
};
|
|
570
|
+
async function _followTurn(turn, isNew) {
|
|
571
|
+
let nextTurn;
|
|
572
|
+
try {
|
|
573
|
+
const stopPolling = _classPrivateFieldGet2(_state, this).history === "server" ? _assertClassBrand(_ChatImpl_brand, this, _startPolling).call(this, turn) : () => {};
|
|
574
|
+
let result;
|
|
575
|
+
try {
|
|
576
|
+
result = await _assertClassBrand(_ChatImpl_brand, this, _follow).call(this, turn, stopPolling);
|
|
577
|
+
} finally {
|
|
578
|
+
stopPolling();
|
|
579
|
+
}
|
|
580
|
+
nextTurn = await _assertClassBrand(_ChatImpl_brand, this, _finishTurn).call(this, turn, result, isNew);
|
|
581
|
+
} catch (e) {
|
|
582
|
+
if (!turn.started) {
|
|
583
|
+
if (_classPrivateFieldGet2(_turn, this) === turn) _classPrivateFieldSet2(_turn, this, void 0);
|
|
584
|
+
_assertClassBrand(_ChatImpl_brand, this, _withdrawTurn).call(this, turn);
|
|
585
|
+
throw e;
|
|
586
|
+
}
|
|
587
|
+
if (turn.controller.signal.aborted || isAbortError(e)) return;
|
|
588
|
+
_assertClassBrand(_ChatImpl_brand, this, _failTurn).call(this, turn, e);
|
|
589
|
+
} finally {
|
|
590
|
+
if (_classPrivateFieldGet2(_turn, this) === turn) _classPrivateFieldSet2(_turn, this, void 0);
|
|
591
|
+
}
|
|
592
|
+
if (nextTurn) await this.resumeTurn(nextTurn);
|
|
593
|
+
}
|
|
594
|
+
async function _select(conversationId) {
|
|
595
|
+
_assertClassBrand(_ChatImpl_brand, this, _leaveConversation).call(this);
|
|
596
|
+
_classPrivateFieldSet2(_page, this, 1);
|
|
597
|
+
_assertClassBrand(_ChatImpl_brand, this, _set).call(this, {
|
|
598
|
+
conversationId,
|
|
599
|
+
messages: [],
|
|
600
|
+
status: "idle",
|
|
601
|
+
error: void 0,
|
|
602
|
+
loadingMessages: true,
|
|
603
|
+
hasMoreMessages: false
|
|
604
|
+
});
|
|
605
|
+
if (_classPrivateFieldGet2(_state, this).history !== "server") {
|
|
606
|
+
_assertClassBrand(_ChatImpl_brand, this, _set).call(this, {
|
|
607
|
+
messages: _classPrivateFieldGet2(_state, this).history === "local" ? _classPrivateFieldGet2(_local, this).getMessages(conversationId) : [],
|
|
608
|
+
loadingMessages: false
|
|
609
|
+
});
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
try {
|
|
613
|
+
const rows = await _classPrivateFieldGet2(_api, this).listMessages(conversationId, { perPage: _classPrivateFieldGet2(_config, this).pageSize });
|
|
614
|
+
if (_classPrivateFieldGet2(_state, this).conversationId !== conversationId) return;
|
|
615
|
+
_assertClassBrand(_ChatImpl_brand, this, _set).call(this, {
|
|
616
|
+
messages: rows.map(fromRow),
|
|
617
|
+
loadingMessages: false,
|
|
618
|
+
hasMoreMessages: rows.length === _classPrivateFieldGet2(_config, this).pageSize
|
|
619
|
+
});
|
|
620
|
+
} catch (e) {
|
|
621
|
+
if (_classPrivateFieldGet2(_state, this).conversationId !== conversationId) return;
|
|
622
|
+
if (_assertClassBrand(_ChatImpl_brand, this, _fallBackToLocal).call(this, e)) {
|
|
623
|
+
_assertClassBrand(_ChatImpl_brand, this, _set).call(this, {
|
|
624
|
+
messages: _classPrivateFieldGet2(_local, this).getMessages(conversationId),
|
|
625
|
+
loadingMessages: false
|
|
626
|
+
});
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
_assertClassBrand(_ChatImpl_brand, this, _set).call(this, {
|
|
630
|
+
loadingMessages: false,
|
|
631
|
+
status: "error",
|
|
632
|
+
error: toError(e)
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
async function _follow(turn, onStreamStart) {
|
|
637
|
+
let started = false;
|
|
638
|
+
for await (const event of followJob(_classPrivateFieldGet2(_api, this), turn.jobId, { signal: turn.controller.signal })) {
|
|
639
|
+
if (event.type === "completed") {
|
|
640
|
+
if (event.streamLost) _assertClassBrand(_ChatImpl_brand, this, _dropCutRound).call(this, turn);
|
|
641
|
+
return event.result;
|
|
642
|
+
}
|
|
643
|
+
if (!started) {
|
|
644
|
+
started = true;
|
|
645
|
+
onStreamStart();
|
|
646
|
+
}
|
|
647
|
+
_assertClassBrand(_ChatImpl_brand, this, _applyEvents).call(this, turn, event.events);
|
|
648
|
+
}
|
|
649
|
+
throw new Error("windmill-chat: the job stream ended before the flow completed");
|
|
650
|
+
}
|
|
651
|
+
/**
|
|
652
|
+
* The stream failed while a round's text was arriving, so that text stops wherever the
|
|
653
|
+
* connection did. It goes: the persisted rows or, without them, the flow result give
|
|
654
|
+
* the whole answer instead. Rounds a tool call closed were complete and stay.
|
|
655
|
+
*/
|
|
656
|
+
function _dropCutRound(turn) {
|
|
657
|
+
if (!_assertClassBrand(_ChatImpl_brand, this, _turnActive).call(this, turn)) return;
|
|
658
|
+
const cut = turn.assistantId;
|
|
659
|
+
turn.assistantId = void 0;
|
|
660
|
+
turn.streamedText = false;
|
|
661
|
+
if (cut) _assertClassBrand(_ChatImpl_brand, this, _set).call(this, { messages: _classPrivateFieldGet2(_state, this).messages.filter((m) => m.id !== cut) });
|
|
662
|
+
}
|
|
663
|
+
function _applyEvents(turn, events) {
|
|
664
|
+
if (events.length === 0 || !_assertClassBrand(_ChatImpl_brand, this, _turnActive).call(this, turn)) return;
|
|
665
|
+
let messages = [..._classPrivateFieldGet2(_state, this).messages];
|
|
666
|
+
const upsertTool = (callId, name, patch) => {
|
|
667
|
+
const { content, success, ...toolPatch } = patch;
|
|
668
|
+
const i = messages.findIndex((m) => m.pending && m.role === "tool" && m.tool?.callId === callId);
|
|
669
|
+
if (i >= 0) {
|
|
670
|
+
const existing = messages[i];
|
|
671
|
+
messages[i] = {
|
|
672
|
+
...existing,
|
|
673
|
+
content: content ?? existing.content,
|
|
674
|
+
success: success ?? existing.success,
|
|
675
|
+
tool: {
|
|
676
|
+
...existing.tool,
|
|
677
|
+
...toolPatch
|
|
678
|
+
}
|
|
679
|
+
};
|
|
680
|
+
} else {
|
|
681
|
+
const a = turn.assistantId ? messages.findIndex((m) => m.id === turn.assistantId) : -1;
|
|
682
|
+
const reasoning = a >= 0 && messages[a].content === "" ? messages.splice(a, 1)[0].reasoning : void 0;
|
|
683
|
+
messages.push({
|
|
684
|
+
id: `pending-${randomId()}`,
|
|
685
|
+
role: "tool",
|
|
686
|
+
content: content ?? "",
|
|
687
|
+
reasoning,
|
|
688
|
+
success: success ?? true,
|
|
689
|
+
createdAt: now(),
|
|
690
|
+
pending: true,
|
|
691
|
+
tool: {
|
|
692
|
+
callId,
|
|
693
|
+
name,
|
|
694
|
+
status: "running",
|
|
695
|
+
...toolPatch
|
|
696
|
+
}
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
turn.assistantId = void 0;
|
|
700
|
+
};
|
|
701
|
+
const appendAssistant = (text, reasoning) => {
|
|
702
|
+
const i = turn.assistantId ? messages.findIndex((m) => m.id === turn.assistantId) : -1;
|
|
703
|
+
if (i >= 0) {
|
|
704
|
+
const m = messages[i];
|
|
705
|
+
messages[i] = {
|
|
706
|
+
...m,
|
|
707
|
+
content: m.content + text,
|
|
708
|
+
reasoning: reasoning ? (m.reasoning ?? "") + reasoning : m.reasoning
|
|
709
|
+
};
|
|
710
|
+
} else {
|
|
711
|
+
turn.assistantId = `pending-${randomId()}`;
|
|
712
|
+
messages.push({
|
|
713
|
+
id: turn.assistantId,
|
|
714
|
+
role: "assistant",
|
|
715
|
+
content: text,
|
|
716
|
+
reasoning: reasoning || void 0,
|
|
717
|
+
success: true,
|
|
718
|
+
createdAt: now(),
|
|
719
|
+
pending: true
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
};
|
|
723
|
+
for (const event of events) switch (event.type) {
|
|
724
|
+
case "token_delta":
|
|
725
|
+
turn.streamedText = true;
|
|
726
|
+
appendAssistant(event.content, "");
|
|
727
|
+
break;
|
|
728
|
+
case "reasoning_token_delta":
|
|
729
|
+
appendAssistant("", event.content);
|
|
730
|
+
break;
|
|
731
|
+
case "tool_call":
|
|
732
|
+
upsertTool(event.call_id, event.function_name, { status: "running" });
|
|
733
|
+
break;
|
|
734
|
+
case "tool_call_arguments":
|
|
735
|
+
upsertTool(event.call_id, event.function_name, { arguments: event.arguments });
|
|
736
|
+
break;
|
|
737
|
+
case "tool_execution":
|
|
738
|
+
upsertTool(event.call_id, event.function_name, { status: "running" });
|
|
739
|
+
break;
|
|
740
|
+
case "tool_result": upsertTool(event.call_id, event.function_name, {
|
|
741
|
+
status: event.success ? "success" : "error",
|
|
742
|
+
result: event.result,
|
|
743
|
+
success: event.success,
|
|
744
|
+
content: event.success ? `Used ${event.function_name} tool` : `Error executing ${event.function_name}`
|
|
745
|
+
});
|
|
746
|
+
}
|
|
747
|
+
_assertClassBrand(_ChatImpl_brand, this, _set).call(this, {
|
|
748
|
+
messages,
|
|
749
|
+
status: "streaming"
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
async function _finishTurn(turn, result, isNew) {
|
|
753
|
+
if (!_assertClassBrand(_ChatImpl_brand, this, _turnActive).call(this, turn)) return;
|
|
754
|
+
if (_classPrivateFieldGet2(_state, this).history === "server") {
|
|
755
|
+
turn.jobIds = await _assertClassBrand(_ChatImpl_brand, this, _turnJobIds).call(this, turn.jobId, turn.controller.signal);
|
|
756
|
+
if (!_assertClassBrand(_ChatImpl_brand, this, _turnActive).call(this, turn)) return;
|
|
757
|
+
const reconciled = await _assertClassBrand(_ChatImpl_brand, this, _reconcileTurn).call(this, turn);
|
|
758
|
+
if (!_assertClassBrand(_ChatImpl_brand, this, _turnActive).call(this, turn)) return;
|
|
759
|
+
if (reconciled) {
|
|
760
|
+
const nextTurn = _assertClassBrand(_ChatImpl_brand, this, _nextTurnAfter).call(this, turn);
|
|
761
|
+
_assertClassBrand(_ChatImpl_brand, this, _set).call(this, { status: nextTurn ? "submitted" : "idle" });
|
|
762
|
+
_classPrivateFieldGet2(_config, this).onFinish?.({
|
|
763
|
+
conversationId: turn.conversationId,
|
|
764
|
+
jobId: turn.jobId,
|
|
765
|
+
messages: _classPrivateFieldGet2(_state, this).messages
|
|
766
|
+
});
|
|
767
|
+
if (isNew) await this.loadConversations().catch(() => {});
|
|
768
|
+
if (!_assertClassBrand(_ChatImpl_brand, this, _turnActive).call(this, turn)) return;
|
|
769
|
+
return nextTurn;
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
let messages = _classPrivateFieldGet2(_state, this).messages;
|
|
773
|
+
let failed = false;
|
|
774
|
+
if (isErrorResult(result)) {
|
|
775
|
+
failed = await _classPrivateFieldGet2(_api, this).getCompletedResult(turn.jobId, turn.controller.signal).then((r) => r.success === false).catch(() => true);
|
|
776
|
+
if (!_assertClassBrand(_ChatImpl_brand, this, _turnActive).call(this, turn)) return;
|
|
777
|
+
if (failed) messages = [...messages, assistantMessage(errorResultMessage(result), false, turn.jobId)];
|
|
778
|
+
}
|
|
779
|
+
if (!failed && !turn.streamedText) {
|
|
780
|
+
const answer = extractChatAnswer(result);
|
|
781
|
+
if (answer !== void 0) messages = [...messages, assistantMessage(answer, true, turn.jobId)];
|
|
782
|
+
}
|
|
783
|
+
const nextTurn = _classPrivateFieldGet2(_state, this).history === "server" ? _assertClassBrand(_ChatImpl_brand, this, _nextTurnAfter).call(this, turn) : void 0;
|
|
784
|
+
_assertClassBrand(_ChatImpl_brand, this, _set).call(this, {
|
|
785
|
+
messages: finalized(messages),
|
|
786
|
+
status: nextTurn ? "submitted" : "idle"
|
|
787
|
+
});
|
|
788
|
+
_assertClassBrand(_ChatImpl_brand, this, _persistLocal).call(this);
|
|
789
|
+
_classPrivateFieldGet2(_config, this).onFinish?.({
|
|
790
|
+
conversationId: turn.conversationId,
|
|
791
|
+
jobId: turn.jobId,
|
|
792
|
+
messages: _classPrivateFieldGet2(_state, this).messages
|
|
793
|
+
});
|
|
794
|
+
if (!_assertClassBrand(_ChatImpl_brand, this, _turnActive).call(this, turn)) return;
|
|
795
|
+
return nextTurn;
|
|
796
|
+
}
|
|
797
|
+
/**
|
|
798
|
+
* Folds what the server persisted for the turn into the message list. The rows
|
|
799
|
+
* are written by the worker in their own transactions, each of which can trail
|
|
800
|
+
* the flow's completion, so a streamed message whose row hasn't landed stays and
|
|
801
|
+
* the list is re-read a few times before the rest is kept as streamed.
|
|
802
|
+
* Returns false when the server holds no answer for the turn: history fell back to
|
|
803
|
+
* local, the read was refused or kept failing, or no assistant row has landed. The
|
|
804
|
+
* caller then finishes the turn from the flow result, so an answer is never lost
|
|
805
|
+
* to history. Whether a row counts is read from the message list, not from what
|
|
806
|
+
* this read returned: the turn's polling may have merged the answer already.
|
|
807
|
+
*/
|
|
808
|
+
async function _reconcileTurn(turn) {
|
|
809
|
+
const answered = () => _assertClassBrand(_ChatImpl_brand, this, _answered).call(this, turn);
|
|
810
|
+
for (let attempt = 1; attempt <= RECONCILE_ATTEMPTS; attempt++) {
|
|
811
|
+
let rows;
|
|
812
|
+
try {
|
|
813
|
+
rows = await _assertClassBrand(_ChatImpl_brand, this, _rowsAfterLastSeq).call(this, turn.conversationId, turn.controller.signal);
|
|
814
|
+
} catch (e) {
|
|
815
|
+
if (isAbortError(e)) throw e;
|
|
816
|
+
if (_assertClassBrand(_ChatImpl_brand, this, _fallBackToLocal).call(this, e)) return false;
|
|
817
|
+
if (e instanceof WindmillApiError && (e.status === 401 || e.status === 403) || attempt === RECONCILE_ATTEMPTS) return answered();
|
|
818
|
+
await sleep(RECONCILE_DELAY_MS, turn.controller.signal);
|
|
819
|
+
continue;
|
|
820
|
+
}
|
|
821
|
+
if (!_assertClassBrand(_ChatImpl_brand, this, _turnActive).call(this, turn)) return true;
|
|
822
|
+
_assertClassBrand(_ChatImpl_brand, this, _mergeRows).call(this, rows);
|
|
823
|
+
if (answered() && !_classPrivateFieldGet2(_state, this).messages.some((m) => m.pending && m.content)) break;
|
|
824
|
+
if (attempt < RECONCILE_ATTEMPTS) await sleep(RECONCILE_DELAY_MS, turn.controller.signal);
|
|
825
|
+
}
|
|
826
|
+
if (!_assertClassBrand(_ChatImpl_brand, this, _turnActive).call(this, turn)) return true;
|
|
827
|
+
_assertClassBrand(_ChatImpl_brand, this, _set).call(this, { messages: finalized(_classPrivateFieldGet2(_state, this).messages) });
|
|
828
|
+
return answered();
|
|
829
|
+
}
|
|
830
|
+
/**
|
|
831
|
+
* The latest row the turn persisted after its user message is an assistant
|
|
832
|
+
* message. An agent issues each round's text row before that round's tool rows,
|
|
833
|
+
* and a tool row when the tool finishes, so an earlier round's text is followed
|
|
834
|
+
* by a tool row and only the answer closes the turn (the inserts are spawned, so
|
|
835
|
+
* a badly delayed one can invert that order at the cost of the reconcile
|
|
836
|
+
* retries). The content is not compared with the flow result: an image answer, a
|
|
837
|
+
* structured one and a forwarded agent result are all persisted in a shape the
|
|
838
|
+
* result does not reproduce. A row belongs to the turn when it was created after
|
|
839
|
+
* the user message and, when it carries a job id, the job is one of the turn's. The
|
|
840
|
+
* server refuses a turn while the previous run is queued, but an agent writes its
|
|
841
|
+
* answer row from a task the run does not wait for, so that row can still land after
|
|
842
|
+
* the next user message; its job says whose it is. A tool row without a job (an MCP
|
|
843
|
+
* call runs inside the agent step) belongs to the turn under way. Until the user
|
|
844
|
+
* message's own row has been read, its position in the list stands in for its seq.
|
|
845
|
+
*/
|
|
846
|
+
function _answered(turn) {
|
|
847
|
+
const messages = _classPrivateFieldGet2(_state, this).messages;
|
|
848
|
+
const from = messages.findIndex((m) => m.id === turn.userMessageId);
|
|
849
|
+
const userSeq = messages[from]?.seq;
|
|
850
|
+
const ownJob = (m) => turn.jobIds === void 0 || (m.jobId === void 0 ? m.role === "tool" : turn.jobIds.has(m.jobId));
|
|
851
|
+
let latest;
|
|
852
|
+
messages.forEach((m, i) => {
|
|
853
|
+
if (m.seq === void 0 || m.role === "user" || !ownJob(m)) return;
|
|
854
|
+
if (userSeq !== void 0 ? m.seq <= userSeq : i <= from) return;
|
|
855
|
+
if (latest === void 0 || m.seq > latest.seq) latest = m;
|
|
856
|
+
});
|
|
857
|
+
return latest?.role === "assistant";
|
|
858
|
+
}
|
|
859
|
+
/**
|
|
860
|
+
* The flow job plus every step job it ran, the failure and preprocessor steps
|
|
861
|
+
* included (a failure handler's answer is persisted under its own job), and the
|
|
862
|
+
* jobs an agent step's tool calls ran as (a tool row is persisted under its own
|
|
863
|
+
* job too). A failed read is retried like the rows are; unknown when it keeps
|
|
864
|
+
* failing or the credential may not read jobs. Unknown accepts every row after the
|
|
865
|
+
* question: refusing them would leave a token without job access with no turn ever
|
|
866
|
+
* answered, each one finished a second time from its result.
|
|
867
|
+
*/
|
|
868
|
+
async function _turnJobIds(jobId, signal) {
|
|
869
|
+
for (let attempt = 1;; attempt++) try {
|
|
870
|
+
const job = await _classPrivateFieldGet2(_api, this).getFlowJob(jobId, signal);
|
|
871
|
+
const ids = /* @__PURE__ */ new Set([jobId]);
|
|
872
|
+
const status = job.flow_status;
|
|
873
|
+
for (const m of [
|
|
874
|
+
...status?.modules ?? [],
|
|
875
|
+
status?.failure_module,
|
|
876
|
+
status?.preprocessor_module
|
|
877
|
+
]) {
|
|
878
|
+
if (m?.job) ids.add(m.job);
|
|
879
|
+
for (const j of m?.flow_jobs ?? []) ids.add(j);
|
|
880
|
+
for (const a of m?.agent_actions ?? []) if (a.job_id) ids.add(a.job_id);
|
|
881
|
+
}
|
|
882
|
+
return ids;
|
|
883
|
+
} catch (e) {
|
|
884
|
+
if (isAbortError(e)) throw e;
|
|
885
|
+
if (e instanceof WindmillApiError && e.status >= 400 && e.status < 500 || attempt === RECONCILE_ATTEMPTS) return void 0;
|
|
886
|
+
await sleep(RECONCILE_DELAY_MS, signal);
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
/**
|
|
890
|
+
* Take back the user message of a turn that never ran. A conversation it would have opened
|
|
891
|
+
* was never listed (see `sendMessage`), so only the message goes, and, while it is the turn
|
|
892
|
+
* on screen, the busy status. A switch away mid-upload has already written the message to
|
|
893
|
+
* local history, so it is removed there too.
|
|
894
|
+
*/
|
|
895
|
+
function _withdrawTurn(turn) {
|
|
896
|
+
if (turn.withdrawn) return;
|
|
897
|
+
turn.withdrawn = true;
|
|
898
|
+
const id = turn.conversationId;
|
|
899
|
+
const withoutTurn = (messages) => messages.filter((m) => m.id !== turn.userMessageId);
|
|
900
|
+
if (_classPrivateFieldGet2(_state, this).conversationId === id) {
|
|
901
|
+
const messages = withoutTurn(_classPrivateFieldGet2(_state, this).messages);
|
|
902
|
+
if (_classPrivateFieldGet2(_turn, this) !== void 0 && _classPrivateFieldGet2(_turn, this) !== turn) _assertClassBrand(_ChatImpl_brand, this, _set).call(this, { messages });
|
|
903
|
+
else {
|
|
904
|
+
const unopened = turn.isNew && messages.length === 0;
|
|
905
|
+
_assertClassBrand(_ChatImpl_brand, this, _set).call(this, {
|
|
906
|
+
messages,
|
|
907
|
+
status: "idle",
|
|
908
|
+
error: void 0,
|
|
909
|
+
...unopened ? { conversationId: void 0 } : {}
|
|
910
|
+
});
|
|
911
|
+
}
|
|
912
|
+
_assertClassBrand(_ChatImpl_brand, this, _persistLocal).call(this);
|
|
913
|
+
}
|
|
914
|
+
if (_classPrivateFieldGet2(_state, this).history === "local" && _classPrivateFieldGet2(_state, this).conversationId !== id) {
|
|
915
|
+
const stored = withoutTurn(_classPrivateFieldGet2(_local, this).getMessages(id));
|
|
916
|
+
if (stored.length > 0) _classPrivateFieldGet2(_local, this).saveMessages(id, stored);
|
|
917
|
+
else if (!_classPrivateFieldGet2(_state, this).conversations.some((c) => c.id === id)) _classPrivateFieldGet2(_local, this).deleteConversation(id);
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
function _failTurn(turn, e) {
|
|
921
|
+
if (!_assertClassBrand(_ChatImpl_brand, this, _turnActive).call(this, turn)) return;
|
|
922
|
+
const error = toError(e);
|
|
923
|
+
_assertClassBrand(_ChatImpl_brand, this, _set).call(this, {
|
|
924
|
+
messages: [...finalized(_classPrivateFieldGet2(_state, this).messages), assistantMessage(error.message, false, turn.jobId)],
|
|
925
|
+
status: "error",
|
|
926
|
+
error
|
|
927
|
+
});
|
|
928
|
+
_assertClassBrand(_ChatImpl_brand, this, _persistLocal).call(this);
|
|
929
|
+
_classPrivateFieldGet2(_config, this).onError?.(error, {
|
|
930
|
+
conversationId: turn.conversationId,
|
|
931
|
+
jobId: turn.jobId
|
|
932
|
+
});
|
|
933
|
+
}
|
|
934
|
+
/** Before the answer streams, earlier steps may already have persisted messages. */
|
|
935
|
+
function _startPolling(turn) {
|
|
936
|
+
let stopped = false;
|
|
937
|
+
const { signal } = turn.controller;
|
|
938
|
+
const loop = async () => {
|
|
939
|
+
while (!stopped) {
|
|
940
|
+
try {
|
|
941
|
+
await sleep(POLL_INTERVAL_MS, signal);
|
|
942
|
+
} catch {
|
|
943
|
+
return;
|
|
944
|
+
}
|
|
945
|
+
if (stopped) return;
|
|
946
|
+
try {
|
|
947
|
+
const rows = await _assertClassBrand(_ChatImpl_brand, this, _rowsAfterLastSeq).call(this, turn.conversationId, signal);
|
|
948
|
+
if (!stopped && _assertClassBrand(_ChatImpl_brand, this, _turnActive).call(this, turn)) _assertClassBrand(_ChatImpl_brand, this, _mergeRows).call(this, rows);
|
|
949
|
+
} catch {}
|
|
950
|
+
}
|
|
951
|
+
};
|
|
952
|
+
loop();
|
|
953
|
+
return () => {
|
|
954
|
+
stopped = true;
|
|
955
|
+
};
|
|
956
|
+
}
|
|
957
|
+
/** Every row created after the newest one held, however many pages that takes. */
|
|
958
|
+
async function _rowsAfterLastSeq(conversationId, signal) {
|
|
959
|
+
const rows = [];
|
|
960
|
+
let afterSeq = _assertClassBrand(_ChatImpl_brand, this, _lastSeq).call(this);
|
|
961
|
+
while (true) {
|
|
962
|
+
const page = await _classPrivateFieldGet2(_api, this).listMessages(conversationId, {
|
|
963
|
+
afterSeq,
|
|
964
|
+
perPage: ROWS_PAGE,
|
|
965
|
+
signal
|
|
966
|
+
});
|
|
967
|
+
rows.push(...page);
|
|
968
|
+
if (page.length < ROWS_PAGE) return rows;
|
|
969
|
+
afterSeq = page[page.length - 1].created_seq;
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
async function _syncFromServer(conversationId, stillCurrent) {
|
|
973
|
+
const rows = await _assertClassBrand(_ChatImpl_brand, this, _rowsAfterLastSeq).call(this, conversationId);
|
|
974
|
+
if (_classPrivateFieldGet2(_turn, this) || _classPrivateFieldGet2(_state, this).conversationId !== conversationId) return;
|
|
975
|
+
if (stillCurrent && !stillCurrent()) return;
|
|
976
|
+
_assertClassBrand(_ChatImpl_brand, this, _mergeRows).call(this, rows);
|
|
977
|
+
_assertClassBrand(_ChatImpl_brand, this, _set).call(this, { messages: finalized(_classPrivateFieldGet2(_state, this).messages) });
|
|
978
|
+
}
|
|
979
|
+
/**
|
|
980
|
+
* Folds persisted rows into the message list. A row standing for a message the
|
|
981
|
+
* client already shows (same role and text; for a tool, the same tool name, since
|
|
982
|
+
* the server words a failure differently) takes its place under the client's id
|
|
983
|
+
* and keeps what only the stream knew: reasoning, call id, arguments, result.
|
|
984
|
+
* Other rows append in server order. Nothing is dropped: a streamed message
|
|
985
|
+
* outlives a row that never lands.
|
|
986
|
+
*/
|
|
987
|
+
function _mergeRows(rows) {
|
|
988
|
+
if (rows.length === 0) return;
|
|
989
|
+
const messages = [..._classPrivateFieldGet2(_state, this).messages];
|
|
990
|
+
const known = new Set(messages.map((m) => m.serverId ?? m.id));
|
|
991
|
+
for (const row of rows.map(fromRow)) {
|
|
992
|
+
if (known.has(row.id)) continue;
|
|
993
|
+
known.add(row.id);
|
|
994
|
+
let i = messages.findIndex((m) => m.seq === void 0 && m.role === row.role && (m.content === row.content || row.tool !== void 0 && m.tool?.name === row.tool.name));
|
|
995
|
+
if (i < 0 && row.role === "assistant") {
|
|
996
|
+
let j = messages.length - 1;
|
|
997
|
+
while (j >= 0 && messages[j].role !== "user") {
|
|
998
|
+
const m = messages[j];
|
|
999
|
+
if (m.seq === void 0 && m.role === "tool" && m.tool?.arguments === row.content) i = j;
|
|
1000
|
+
j--;
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
if (i >= 0) {
|
|
1004
|
+
const m = messages[i];
|
|
1005
|
+
messages[i] = {
|
|
1006
|
+
...row,
|
|
1007
|
+
id: m.id,
|
|
1008
|
+
reasoning: m.reasoning ?? row.reasoning,
|
|
1009
|
+
tool: row.role === "tool" && m.tool ? {
|
|
1010
|
+
...m.tool,
|
|
1011
|
+
arguments: m.tool.arguments ?? row.tool?.arguments,
|
|
1012
|
+
result: m.tool.result ?? row.tool?.result,
|
|
1013
|
+
status: row.tool?.status ?? m.tool.status
|
|
1014
|
+
} : row.tool
|
|
1015
|
+
};
|
|
1016
|
+
} else {
|
|
1017
|
+
let at = messages.length;
|
|
1018
|
+
for (let j = messages.length - 1; row.role === "tool" && j >= 0; j--) {
|
|
1019
|
+
const seq = messages[j].seq;
|
|
1020
|
+
if (seq !== void 0 && seq < row.seq) {
|
|
1021
|
+
at = j + 1;
|
|
1022
|
+
break;
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
messages.splice(at, 0, row);
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
_assertClassBrand(_ChatImpl_brand, this, _set).call(this, { messages });
|
|
1029
|
+
}
|
|
1030
|
+
function _lastSeq() {
|
|
1031
|
+
let last;
|
|
1032
|
+
for (const m of _classPrivateFieldGet2(_state, this).messages) if (m.seq !== void 0 && (last === void 0 || m.seq > last)) last = m.seq;
|
|
1033
|
+
return last;
|
|
1034
|
+
}
|
|
1035
|
+
/** Writes the current messages to local history; the conversation entry itself is `#rememberConversation`'s. */
|
|
1036
|
+
function _persistLocal() {
|
|
1037
|
+
clearTimeout(_classPrivateFieldGet2(_persistTimer, this));
|
|
1038
|
+
_classPrivateFieldSet2(_persistTimer, this, void 0);
|
|
1039
|
+
const id = _classPrivateFieldGet2(_state, this).conversationId;
|
|
1040
|
+
if (_classPrivateFieldGet2(_state, this).history !== "local" || !id) return;
|
|
1041
|
+
_classPrivateFieldGet2(_local, this).saveMessages(id, _classPrivateFieldGet2(_state, this).messages);
|
|
1042
|
+
}
|
|
1043
|
+
/**
|
|
1044
|
+
* Puts the current conversation at the head of local history. Only a turn moves
|
|
1045
|
+
* a conversation there: merely viewing one must not reorder the list.
|
|
1046
|
+
*/
|
|
1047
|
+
function _rememberConversation() {
|
|
1048
|
+
const id = _classPrivateFieldGet2(_state, this).conversationId;
|
|
1049
|
+
if (_classPrivateFieldGet2(_state, this).history !== "local" || !id) return;
|
|
1050
|
+
const conversation = _classPrivateFieldGet2(_state, this).conversations.find((c) => c.id === id);
|
|
1051
|
+
if (conversation) _classPrivateFieldGet2(_local, this).upsertConversation(conversation);
|
|
1052
|
+
}
|
|
1053
|
+
/** Whether an unreadable server history should silently become local history. */
|
|
1054
|
+
function _fallBackToLocal(e) {
|
|
1055
|
+
if (_classPrivateFieldGet2(_state, this).history !== "server" || _classPrivateFieldGet2(_config, this).historyExplicit) return false;
|
|
1056
|
+
if (e instanceof WindmillApiError && (e.status === 401 || e.status === 403)) {
|
|
1057
|
+
_assertClassBrand(_ChatImpl_brand, this, _set).call(this, { history: "local" });
|
|
1058
|
+
_assertClassBrand(_ChatImpl_brand, this, _rememberConversation).call(this);
|
|
1059
|
+
return true;
|
|
1060
|
+
}
|
|
1061
|
+
return false;
|
|
1062
|
+
}
|
|
1063
|
+
function _turnActive(turn) {
|
|
1064
|
+
return _classPrivateFieldGet2(_turn, this) === turn && _classPrivateFieldGet2(_state, this).conversationId === turn.conversationId;
|
|
1065
|
+
}
|
|
1066
|
+
function _latestUserAfter(userSeq) {
|
|
1067
|
+
return _classPrivateFieldGet2(_state, this).messages.reduce((found, message) => message.role === "user" && message.seq !== void 0 && message.seq > (found?.seq ?? userSeq) ? message : found, void 0);
|
|
1068
|
+
}
|
|
1069
|
+
function _nextTurnAfter(turn) {
|
|
1070
|
+
const userSeq = _classPrivateFieldGet2(_state, this).messages.find((message) => message.id === turn.userMessageId)?.seq;
|
|
1071
|
+
if (userSeq === void 0) return void 0;
|
|
1072
|
+
const next = _assertClassBrand(_ChatImpl_brand, this, _latestUserAfter).call(this, userSeq);
|
|
1073
|
+
return next?.jobId && next.seq !== void 0 ? {
|
|
1074
|
+
jobId: next.jobId,
|
|
1075
|
+
userSeq: next.seq
|
|
1076
|
+
} : void 0;
|
|
1077
|
+
}
|
|
1078
|
+
/** Stops following the current answer; the flow itself keeps running. */
|
|
1079
|
+
function _detachTurn() {
|
|
1080
|
+
const turn = _classPrivateFieldGet2(_turn, this);
|
|
1081
|
+
if (!turn) return;
|
|
1082
|
+
_classPrivateFieldSet2(_turn, this, void 0);
|
|
1083
|
+
turn.controller.abort();
|
|
1084
|
+
}
|
|
1085
|
+
/**
|
|
1086
|
+
* Leaves the current conversation (for another one, or because the page goes
|
|
1087
|
+
* away). A turn still in flight is detached and what it showed so far is kept,
|
|
1088
|
+
* written out now rather than on the debounce that may never fire.
|
|
1089
|
+
*/
|
|
1090
|
+
function _leaveConversation() {
|
|
1091
|
+
var _this$epoch5;
|
|
1092
|
+
_classPrivateFieldSet2(_epoch, this, (_this$epoch5 = _classPrivateFieldGet2(_epoch, this), _this$epoch5++, _this$epoch5));
|
|
1093
|
+
if (_classPrivateFieldGet2(_turn, this)) {
|
|
1094
|
+
_assertClassBrand(_ChatImpl_brand, this, _detachTurn).call(this);
|
|
1095
|
+
_assertClassBrand(_ChatImpl_brand, this, _set).call(this, {
|
|
1096
|
+
messages: finalized(_classPrivateFieldGet2(_state, this).messages),
|
|
1097
|
+
status: "idle"
|
|
1098
|
+
});
|
|
1099
|
+
}
|
|
1100
|
+
if (_classPrivateFieldGet2(_persistTimer, this)) _assertClassBrand(_ChatImpl_brand, this, _persistLocal).call(this);
|
|
1101
|
+
}
|
|
1102
|
+
function _set(patch) {
|
|
1103
|
+
_classPrivateFieldSet2(_state, this, {
|
|
1104
|
+
..._classPrivateFieldGet2(_state, this),
|
|
1105
|
+
...patch
|
|
1106
|
+
});
|
|
1107
|
+
for (const listener of _classPrivateFieldGet2(_listeners, this)) listener(_classPrivateFieldGet2(_state, this));
|
|
1108
|
+
if (_classPrivateFieldGet2(_state, this).history === "local" && _classPrivateFieldGet2(_state, this).conversationId) {
|
|
1109
|
+
clearTimeout(_classPrivateFieldGet2(_persistTimer, this));
|
|
1110
|
+
_classPrivateFieldSet2(_persistTimer, this, setTimeout(() => _assertClassBrand(_ChatImpl_brand, this, _persistLocal).call(this), PERSIST_DEBOUNCE_MS));
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
/** The failure this chat is showing: the message `#failTurn` left, which is never a row. */
|
|
1114
|
+
function lastFailureShown(messages) {
|
|
1115
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
1116
|
+
const m = messages[i];
|
|
1117
|
+
if (m.seq === void 0 && m.success === false) return m;
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
/** The newest row seq a list holds; 0 when it holds none. */
|
|
1121
|
+
function latestSeq(messages) {
|
|
1122
|
+
return messages.reduce((newest, m) => m.seq !== void 0 && m.seq > newest ? m.seq : newest, 0);
|
|
1123
|
+
}
|
|
1124
|
+
function fromRow(row) {
|
|
1125
|
+
const toolName = row.message_type === "tool" ? /^Used (.+) tool$/.exec(row.content)?.[1] ?? /^Error executing (.+)$/.exec(row.content)?.[1] : void 0;
|
|
1126
|
+
const success = row.success ?? true;
|
|
1127
|
+
return {
|
|
1128
|
+
id: row.id,
|
|
1129
|
+
serverId: row.id,
|
|
1130
|
+
role: row.message_type,
|
|
1131
|
+
content: row.content,
|
|
1132
|
+
success,
|
|
1133
|
+
createdAt: row.created_at,
|
|
1134
|
+
jobId: row.job_id ?? void 0,
|
|
1135
|
+
stepName: row.step_name ?? void 0,
|
|
1136
|
+
pending: false,
|
|
1137
|
+
seq: row.created_seq,
|
|
1138
|
+
reasoning: row.reasoning ?? void 0,
|
|
1139
|
+
attachments: row.attachments ?? void 0,
|
|
1140
|
+
tool: toolName ? {
|
|
1141
|
+
name: toolName,
|
|
1142
|
+
status: success ? "success" : "error",
|
|
1143
|
+
arguments: row.tool_arguments ?? void 0,
|
|
1144
|
+
result: row.tool_result ?? void 0
|
|
1145
|
+
} : void 0
|
|
1146
|
+
};
|
|
1147
|
+
}
|
|
1148
|
+
function fromConversation(row) {
|
|
1149
|
+
return {
|
|
1150
|
+
id: row.id,
|
|
1151
|
+
title: row.title ?? void 0,
|
|
1152
|
+
createdAt: row.created_at,
|
|
1153
|
+
updatedAt: row.updated_at,
|
|
1154
|
+
isTest: row.is_test,
|
|
1155
|
+
runningTurn: row.running_turn ? {
|
|
1156
|
+
jobId: row.running_turn.job_id,
|
|
1157
|
+
userSeq: row.running_turn.user_seq
|
|
1158
|
+
} : void 0
|
|
1159
|
+
};
|
|
1160
|
+
}
|
|
1161
|
+
function assistantMessage(content, success, jobId) {
|
|
1162
|
+
return {
|
|
1163
|
+
id: `local-${randomId()}`,
|
|
1164
|
+
role: "assistant",
|
|
1165
|
+
content,
|
|
1166
|
+
success,
|
|
1167
|
+
createdAt: now(),
|
|
1168
|
+
jobId,
|
|
1169
|
+
pending: false
|
|
1170
|
+
};
|
|
1171
|
+
}
|
|
1172
|
+
function finalized(messages) {
|
|
1173
|
+
return messages.some((m) => m.pending) ? messages.map((m) => m.pending ? {
|
|
1174
|
+
...m,
|
|
1175
|
+
pending: false
|
|
1176
|
+
} : m) : messages;
|
|
1177
|
+
}
|
|
1178
|
+
function toError(e) {
|
|
1179
|
+
return e instanceof Error ? e : new Error(String(e));
|
|
1180
|
+
}
|
|
1181
|
+
//#endregion
|
|
1182
|
+
export { detectRawApp as a, uploadAttachments as i, CHAT_UPLOADS_PREFIX as n, storedAttachmentName as r, createChat as t };
|