@ykurilov/agent-chat-server 0.0.0-stage → 0.56.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,732 @@
1
+ // src/chat-handler.ts
2
+ import {
3
+ ApprovalNotPendingError,
4
+ AttachmentRejectedError,
5
+ BrowserCallNotPendingError,
6
+ ChatBusyError,
7
+ ChatNotFoundError,
8
+ createChatEvents,
9
+ accessRefusedResponse,
10
+ allowedUser,
11
+ createChatService,
12
+ hiddenStateKeyOf,
13
+ createRunningAnswerRegistry,
14
+ DuplicateMessageError,
15
+ InvalidChatTitleError,
16
+ InvalidMessageError,
17
+ InvalidModelChoiceError,
18
+ isScreenPlaceInput,
19
+ isTabId,
20
+ isVoiceSound,
21
+ NothingToRetryError,
22
+ createRequestOriginCheck,
23
+ originRefusalHint,
24
+ isSafeMethod,
25
+ SpendingLimitReachedError,
26
+ TooManyTurnsError,
27
+ TurnRefusedError,
28
+ ActionForbiddenError,
29
+ ChatClosedError,
30
+ ChatCreationRefusedError,
31
+ turnOriginOf,
32
+ InvalidWidgetAnswerError,
33
+ WidgetAnswerCheckAbortedError,
34
+ WidgetAnswerCheckTimeoutError,
35
+ WidgetAnswerRefusedError,
36
+ WidgetNotPendingError
37
+ } from "@ykurilov/agent-chat-core";
38
+
39
+ // src/heartbeat.ts
40
+ import { JsonToSseTransformStream, UI_MESSAGE_STREAM_HEADERS } from "ai";
41
+ var HEARTBEAT_HEADER = "x-agent-chat-heartbeat-ms";
42
+ var MIN_HEARTBEAT_INTERVAL_MS = 1e3;
43
+ var DEFAULT_HEARTBEAT_INTERVAL_MS = 1e4;
44
+ var PING = new TextEncoder().encode(": ping\n\n");
45
+ function createHeartbeatResponse(options) {
46
+ const { stream, heartbeatIntervalMs } = options;
47
+ assertHeartbeatInterval(heartbeatIntervalMs);
48
+ const events = stream.pipeThrough(new JsonToSseTransformStream()).getReader();
49
+ const encoder = new TextEncoder();
50
+ let timer;
51
+ let cancelled = false;
52
+ const stopTimer = () => {
53
+ if (timer !== void 0) clearInterval(timer);
54
+ timer = void 0;
55
+ };
56
+ const body = new ReadableStream({
57
+ start(controller) {
58
+ controller.enqueue(PING);
59
+ timer = setInterval(() => controller.enqueue(PING), heartbeatIntervalMs);
60
+ },
61
+ async pull(controller) {
62
+ try {
63
+ const { done, value } = await events.read();
64
+ if (cancelled) return;
65
+ if (done) {
66
+ stopTimer();
67
+ controller.close();
68
+ return;
69
+ }
70
+ controller.enqueue(encoder.encode(value));
71
+ } catch (error) {
72
+ stopTimer();
73
+ throw error;
74
+ }
75
+ },
76
+ cancel(reason) {
77
+ cancelled = true;
78
+ stopTimer();
79
+ return events.cancel(reason);
80
+ }
81
+ });
82
+ return new Response(body, {
83
+ headers: { ...UI_MESSAGE_STREAM_HEADERS, [HEARTBEAT_HEADER]: String(heartbeatIntervalMs) }
84
+ });
85
+ }
86
+ function assertHeartbeatInterval(heartbeatIntervalMs) {
87
+ if (!Number.isFinite(heartbeatIntervalMs) || heartbeatIntervalMs < MIN_HEARTBEAT_INTERVAL_MS) {
88
+ throw new RangeError(
89
+ `heartbeatIntervalMs \u0434\u043E\u043B\u0436\u0435\u043D \u0431\u044B\u0442\u044C \u043D\u0435 \u043C\u0435\u043D\u044C\u0448\u0435 ${MIN_HEARTBEAT_INTERVAL_MS} \u043C\u0441, \u043F\u043E\u043B\u0443\u0447\u0435\u043D\u043E ${heartbeatIntervalMs}`
90
+ );
91
+ }
92
+ }
93
+
94
+ // src/request-body.ts
95
+ var MAX_JSON_BODY_BYTES = 1024 * 1024;
96
+ var RequestBodyTooLargeError = class extends Error {
97
+ constructor(maxBytes) {
98
+ super(`\u0422\u0435\u043B\u043E \u0437\u0430\u043F\u0440\u043E\u0441\u0430 \u0431\u043E\u043B\u044C\u0448\u0435 ${maxBytes} \u0431\u0430\u0439\u0442`);
99
+ this.name = "RequestBodyTooLargeError";
100
+ }
101
+ };
102
+ async function readJsonBody(request, maxBytes = MAX_JSON_BODY_BYTES) {
103
+ if (Number(request.headers.get("content-length")) > maxBytes) throw new RequestBodyTooLargeError(maxBytes);
104
+ if (!request.body) return null;
105
+ const reader = request.body.getReader();
106
+ const chunks = [];
107
+ let size = 0;
108
+ try {
109
+ for (let read = await reader.read(); !read.done; read = await reader.read()) {
110
+ size += read.value.byteLength;
111
+ if (size > maxBytes) {
112
+ void reader.cancel().catch(() => void 0);
113
+ throw new RequestBodyTooLargeError(maxBytes);
114
+ }
115
+ chunks.push(read.value);
116
+ }
117
+ } catch (error) {
118
+ if (error instanceof RequestBodyTooLargeError) throw error;
119
+ return null;
120
+ }
121
+ const bytes = new Uint8Array(size);
122
+ let offset = 0;
123
+ for (const chunk of chunks) {
124
+ bytes.set(chunk, offset);
125
+ offset += chunk.byteLength;
126
+ }
127
+ try {
128
+ return JSON.parse(new TextDecoder().decode(bytes));
129
+ } catch {
130
+ return null;
131
+ }
132
+ }
133
+
134
+ // src/memory-routes.ts
135
+ var statusOfKind = {
136
+ invalid: 400,
137
+ not_found: 404,
138
+ stale_version: 409,
139
+ duplicate: 409,
140
+ limit_reached: 409
141
+ };
142
+ var errorResponse = (kind) => Response.json({ error: { kind } }, { status: statusOfKind[kind] });
143
+ function publicNote({ id, categories, content, version, createdAt, updatedAt }) {
144
+ return { id, categories, content, version, createdAt, updatedAt };
145
+ }
146
+ function publicMerge({ runId, finishedAt, revertible, groups }) {
147
+ return {
148
+ runId,
149
+ finishedAt,
150
+ revertible,
151
+ groups: groups.map(({ result, originals }) => ({ result: publicNote(result), originals: originals.map(publicNote) }))
152
+ };
153
+ }
154
+ var isVersion = (value) => typeof value === "number" && Number.isInteger(value) && value >= 1;
155
+ async function readBody(request) {
156
+ const body = await readJsonBody(request);
157
+ return typeof body === "object" && body !== null && !Array.isArray(body) ? body : null;
158
+ }
159
+ var noteFrom = (body) => ({ categories: body?.categories, content: body?.content });
160
+ async function handleMemoryRequest(input) {
161
+ const { memory, request, userId, segments } = input;
162
+ const method = request.method.toUpperCase();
163
+ const [id, ...rest] = segments;
164
+ if (id === "merges") return handleMergesRequest({ memory, method, userId, rest });
165
+ if (rest.length > 0) return errorResponse("not_found");
166
+ if (id === void 0) {
167
+ if (method === "GET") return Response.json(await listForScreen(memory, userId));
168
+ if (method === "POST") {
169
+ const created = await memory.create(userId, noteFrom(await readBody(request)));
170
+ return created.ok ? Response.json({ note: publicNote(created.value) }, { status: 201 }) : errorResponse(created.error.kind);
171
+ }
172
+ if (method === "DELETE") return Response.json({ deleted: await memory.clear(userId) });
173
+ return errorResponse("not_found");
174
+ }
175
+ if (method === "PATCH") {
176
+ const body = await readBody(request);
177
+ if (!isVersion(body?.version)) return errorResponse("invalid");
178
+ const updated = await memory.update(userId, id, body.version, noteFrom(body));
179
+ return updated.ok ? Response.json({ note: publicNote(updated.value) }) : errorResponse(updated.error.kind);
180
+ }
181
+ if (method === "DELETE") {
182
+ const version = Number(new URL(request.url).searchParams.get("version") || Number.NaN);
183
+ if (!isVersion(version)) return errorResponse("invalid");
184
+ const removed = await memory.remove(userId, id, version);
185
+ return removed.ok ? new Response(null, { status: 204 }) : errorResponse(removed.error.kind);
186
+ }
187
+ return errorResponse("not_found");
188
+ }
189
+ async function handleMergesRequest(input) {
190
+ const { memory, method, userId, rest } = input;
191
+ const { listMerges, revertMerge } = memory;
192
+ if (typeof listMerges !== "function" || typeof revertMerge !== "function") return errorResponse("not_found");
193
+ if (rest.length === 0 && method === "GET") {
194
+ const merges = await listMerges.call(memory, userId);
195
+ return Response.json({ merges: merges.map(publicMerge) });
196
+ }
197
+ const [runId, action, ...extra] = rest;
198
+ if (runId !== void 0 && action === "revert" && extra.length === 0 && method === "POST") {
199
+ const result = await revertMerge.call(memory, userId, runId);
200
+ if (result === "ok") return Response.json({ reverted: runId });
201
+ return result === "not_found" ? errorResponse("not_found") : Response.json({ error: { kind: result } }, { status: 409 });
202
+ }
203
+ return errorResponse("not_found");
204
+ }
205
+ async function listForScreen(memory, userId) {
206
+ const { notes, counts } = await memory.list(userId);
207
+ return { notes: notes.map(publicNote), counts: { total: counts.total, limit: counts.limit, shown: counts.shown } };
208
+ }
209
+
210
+ // src/subject-chat-route.ts
211
+ import { normalizeTurnRefusal, SUBJECT_KEY_MAX_LENGTH } from "@ykurilov/agent-chat-core";
212
+ function turnRefusalResponse(refusal) {
213
+ const { code, message } = normalizeTurnRefusal(refusal);
214
+ return Response.json({ error: { kind: "turn_refused", code, message } }, { status: 403 });
215
+ }
216
+ async function openSubjectChatRoute(input) {
217
+ const key = (await readJsonBody(input.request))?.key;
218
+ if (typeof key !== "string" || key.length === 0 || key.length > SUBJECT_KEY_MAX_LENGTH) return input.invalid();
219
+ const settings = await input.resolve({ user: { id: input.userId }, key, request: input.request });
220
+ if (typeof settings !== "object" || settings === null || Array.isArray(settings)) {
221
+ throw new TypeError(`subjectChats \u0434\u043E\u043B\u0436\u0435\u043D \u0432\u0435\u0440\u043D\u0443\u0442\u044C \u043E\u0431\u044A\u0435\u043A\u0442 \u043D\u0430\u0441\u0442\u0440\u043E\u0435\u043A \u0438\u043B\u0438 \u043E\u0442\u043A\u0430\u0437 { code, message }, \u0430 \u0432\u0435\u0440\u043D\u0443\u043B ${settings === null ? "null" : Array.isArray(settings) ? "\u043C\u0430\u0441\u0441\u0438\u0432" : typeof settings}.`);
222
+ }
223
+ if ("code" in settings) return turnRefusalResponse(settings);
224
+ const { chat, created } = await input.service.openSubjectChat({ ownerId: input.userId, key, ...settings });
225
+ return Response.json({ id: chat.id, created });
226
+ }
227
+
228
+ // src/new-chat-data.ts
229
+ async function resolveNewChatData(resolve, input) {
230
+ const data = await resolve(input);
231
+ if (data === void 0) return void 0;
232
+ if (typeof data !== "object" || data === null || Array.isArray(data)) {
233
+ throw new TypeError(`newChatData \u0434\u043E\u043B\u0436\u0435\u043D \u0432\u0435\u0440\u043D\u0443\u0442\u044C \u043E\u0431\u044A\u0435\u043A\u0442 \u0434\u0430\u043D\u043D\u044B\u0445 \u0447\u0430\u0442\u0430 \u0438\u043B\u0438 undefined, \u0430 \u0432\u0435\u0440\u043D\u0443\u043B ${data === null ? "null" : Array.isArray(data) ? "\u043C\u0430\u0441\u0441\u0438\u0432" : typeof data}.`);
234
+ }
235
+ return data;
236
+ }
237
+
238
+ // src/watch-stream.ts
239
+ import { publicAppState } from "@ykurilov/agent-chat-core";
240
+ var WATCH_RECONCILE_EVERY_MS = 5e3;
241
+ var LIVE_TASK_STATUSES = /* @__PURE__ */ new Set(["queued", "running", "awaiting_approval"]);
242
+ function createWatchStream(options) {
243
+ const { ownerId, chatId, listTasks } = options;
244
+ let closed = false;
245
+ let timer;
246
+ let unsubscribe;
247
+ let lastTasks;
248
+ const announced = /* @__PURE__ */ new Set();
249
+ const close = () => {
250
+ if (closed) return;
251
+ closed = true;
252
+ clearTimeout(timer);
253
+ unsubscribe?.();
254
+ options.signal?.removeEventListener("abort", onClientGone);
255
+ options.onClose();
256
+ };
257
+ let onClientGone = () => close();
258
+ return new ReadableStream({
259
+ start(controller) {
260
+ onClientGone = () => {
261
+ const wasOpen = !closed;
262
+ close();
263
+ if (wasOpen) controller.close();
264
+ };
265
+ const send = (event) => {
266
+ if (!closed) controller.enqueue(event);
267
+ };
268
+ const checkTasks = async () => {
269
+ const tasks = await listTasks();
270
+ const snapshot = JSON.stringify(tasks);
271
+ if (lastTasks !== void 0 && snapshot !== lastTasks) send({ type: "tasks-changed", chatId });
272
+ lastTasks = snapshot;
273
+ return tasks.some((task) => LIVE_TASK_STATUSES.has(task.status));
274
+ };
275
+ const tick = async () => {
276
+ timer = void 0;
277
+ try {
278
+ const live = await checkTasks();
279
+ const turn = await options.detachedTurn();
280
+ if (turn && !announced.has(turn.messageId)) {
281
+ announced.add(turn.messageId);
282
+ send({ type: "turn-started", chatId, ...turn });
283
+ }
284
+ if (live) scheduleReconcile();
285
+ } catch (error) {
286
+ options.logError(`\u041D\u0430\u0431\u043B\u044E\u0434\u0435\u043D\u0438\u0435 \u0437\u0430 \u0447\u0430\u0442\u043E\u043C ${chatId}: \u0441\u0432\u0435\u0440\u043A\u0430 \u0441 \u0431\u0430\u0437\u043E\u0439 \u043D\u0435 \u0443\u0434\u0430\u043B\u0430\u0441\u044C: ${error instanceof Error ? error.message : String(error)}`);
287
+ scheduleReconcile();
288
+ }
289
+ };
290
+ const scheduleReconcile = () => {
291
+ if (closed || timer !== void 0 || !listTasks) return;
292
+ timer = setTimeout(() => void tick(), WATCH_RECONCILE_EVERY_MS);
293
+ };
294
+ unsubscribe = options.subscribeEvents((event) => {
295
+ if (event.ownerId !== ownerId || event.chatId !== chatId) return;
296
+ if (event.type === "chat-data-changed") return;
297
+ const { ownerId: _, ...forTab } = event;
298
+ if (event.type === "turn-started") announced.add(event.messageId);
299
+ send(forTab.type === "state-changed" && options.hiddenStateKey ? { ...forTab, state: publicAppState(forTab.state, options.hiddenStateKey) } : forTab);
300
+ if (event.type === "tasks-changed") scheduleReconcile();
301
+ });
302
+ if (options.signal?.aborted) {
303
+ onClientGone();
304
+ return;
305
+ }
306
+ options.signal?.addEventListener("abort", onClientGone, { once: true });
307
+ if (listTasks) {
308
+ void checkTasks().then((live) => {
309
+ if (live) scheduleReconcile();
310
+ }).catch((error) => {
311
+ options.logError(`\u041D\u0430\u0431\u043B\u044E\u0434\u0435\u043D\u0438\u0435 \u0437\u0430 \u0447\u0430\u0442\u043E\u043C ${chatId}: \u0437\u0430\u0434\u0430\u0447\u0438 \u043D\u0435 \u043F\u0440\u043E\u0447\u0438\u0442\u0430\u043D\u044B: ${error instanceof Error ? error.message : String(error)}`);
312
+ scheduleReconcile();
313
+ });
314
+ }
315
+ },
316
+ cancel() {
317
+ close();
318
+ }
319
+ });
320
+ }
321
+
322
+ // src/chat-handler.ts
323
+ var logValue = (value) => /[\p{Cc}\u2028\u2029]/u.test(value) ? JSON.stringify(value) : value;
324
+ function createChatHandler(options) {
325
+ return createChatServer(options).handler;
326
+ }
327
+ function createChatServer(options) {
328
+ const basePath = options.basePath.replace(/\/+$/, "");
329
+ const heartbeatIntervalMs = options.heartbeatIntervalMs ?? DEFAULT_HEARTBEAT_INTERVAL_MS;
330
+ assertHeartbeatInterval(heartbeatIntervalMs);
331
+ const streamResponse = (stream) => createHeartbeatResponse({ stream, heartbeatIntervalMs });
332
+ const logError = options.logError ?? ((message) => console.error(message));
333
+ const allowsOrigin = createRequestOriginCheck({
334
+ allowedOrigins: options.allowedOrigins,
335
+ onRefused: (refusal) => logError(`\u0427\u0430\u0442: ${originRefusalHint(refusal)}`)
336
+ });
337
+ if (options.newChatData && !options.storage.updateChatData) {
338
+ throw new Error("newChatData \u0442\u0440\u0435\u0431\u0443\u0435\u0442 \u0445\u0440\u0430\u043D\u0438\u043B\u0438\u0449\u0430 \u0441 updateChatData: \u0445\u0440\u0430\u043D\u0438\u043B\u0438\u0449\u0435 \u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u044F \u0435\u0433\u043E \u043D\u0435 \u0443\u043C\u0435\u0435\u0442.");
339
+ }
340
+ if (options.subjectChats && !options.storage.findChatBySubject) {
341
+ throw new Error("subjectChats \u0442\u0440\u0435\u0431\u0443\u0435\u0442 \u0445\u0440\u0430\u043D\u0438\u043B\u0438\u0449\u0430 \u0441 findChatBySubject: \u0445\u0440\u0430\u043D\u0438\u043B\u0438\u0449\u0435 \u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u044F \u0435\u0433\u043E \u043D\u0435 \u0443\u043C\u0435\u0435\u0442.");
342
+ }
343
+ const service = createChatService({
344
+ storage: options.storage,
345
+ catalog: options.catalog,
346
+ registry: createRunningAnswerRegistry(),
347
+ ...options.defaultChoice && { defaultChoice: options.defaultChoice },
348
+ agent: options.agent,
349
+ ...options.compaction !== void 0 && { compaction: options.compaction },
350
+ ...options.attachments && { attachments: options.attachments },
351
+ ...options.limits && { limits: options.limits },
352
+ ...options.maxConcurrentTurnsPerUser !== void 0 && { maxConcurrentTurnsPerUser: options.maxConcurrentTurnsPerUser },
353
+ ...options.beforeTurn && { beforeTurn: options.beforeTurn },
354
+ ...options.turnNotStarted && { turnNotStarted: options.turnNotStarted },
355
+ ...options.chatCapabilities && { chatCapabilities: options.chatCapabilities },
356
+ ...options.beforeCreateChat && { beforeCreateChat: options.beforeCreateChat },
357
+ ...options.memory && { memory: options.memory },
358
+ ...options.env && { env: options.env },
359
+ logError,
360
+ ...options.turnObserver && { turnObserver: options.turnObserver },
361
+ ...options.chatInstructions && { chatInstructions: options.chatInstructions },
362
+ ...options.staffOfChat && { staffOfChat: options.staffOfChat },
363
+ ...options.lockedToolOutputs && { lockedToolOutputs: options.lockedToolOutputs },
364
+ chatEvents: createChatEvents((error) => logError(`\u0421\u043E\u0431\u044B\u0442\u0438\u044F \u0447\u0430\u0442\u0430: \u0441\u0431\u043E\u0439 \u0441\u043B\u0443\u0448\u0430\u0442\u0435\u043B\u044F: ${describe(error)}`)),
365
+ ...options.background && { background: options.background },
366
+ ...options.onTurnFinished && { onTurnFinished: options.onTurnFinished },
367
+ ...options.afterTurn && { afterTurn: options.afterTurn },
368
+ ...options.afterTurnTimeoutMs !== void 0 && { afterTurnTimeoutMs: options.afterTurnTimeoutMs },
369
+ ...options.afterTurnAttempts !== void 0 && { afterTurnAttempts: options.afterTurnAttempts },
370
+ ...options.onStateChanged && { onStateChanged: options.onStateChanged },
371
+ ...options.onStateChangedTimeoutMs !== void 0 && { onStateChangedTimeoutMs: options.onStateChangedTimeoutMs },
372
+ ...options.onStateChangedAttempts !== void 0 && { onStateChangedAttempts: options.onStateChangedAttempts },
373
+ ...options.onModelChosen && { onModelChosen: options.onModelChosen },
374
+ ...options.modelMenu && { modelMenu: options.modelMenu },
375
+ ...options.privateStateKeys && { privateStateKeys: options.privateStateKeys },
376
+ ...options.replyChecks && { replyChecks: options.replyChecks },
377
+ ...options.now && { now: options.now }
378
+ });
379
+ const hiddenStateKey = hiddenStateKeyOf(options.privateStateKeys);
380
+ const background = options.background;
381
+ async function answerTaskApproval(ownerId, chatId, taskId, approvalId, decision) {
382
+ await service.assertTurnAllowed(ownerId, chatId, "approval");
383
+ return background.answerApproval(ownerId, chatId, taskId, approvalId, decision);
384
+ }
385
+ const watchers = /* @__PURE__ */ new Map();
386
+ const watcherKey = (ownerId, chatId) => JSON.stringify([ownerId, chatId]);
387
+ const hasWatchers = (ownerId, chatId) => (watchers.get(watcherKey(ownerId, chatId)) ?? 0) > 0;
388
+ function watchResponse(request, userId, chatId) {
389
+ const key = watcherKey(userId, chatId);
390
+ watchers.set(key, (watchers.get(key) ?? 0) + 1);
391
+ let counted = true;
392
+ const uncount = () => {
393
+ if (!counted) return;
394
+ counted = false;
395
+ const left = (watchers.get(key) ?? 1) - 1;
396
+ if (left > 0) watchers.set(key, left);
397
+ else watchers.delete(key);
398
+ };
399
+ try {
400
+ const stream = createWatchStream({
401
+ ownerId: userId,
402
+ chatId,
403
+ signal: request.signal,
404
+ subscribeEvents: (listener) => service.subscribeEvents(listener),
405
+ hiddenStateKey,
406
+ ...background && { listTasks: () => background.listForChat(userId, chatId) },
407
+ async detachedTurn() {
408
+ const history = await service.getHistory(userId, chatId);
409
+ const answer = history.messages.at(-1);
410
+ return history.detachedAnswer && answer ? { messageId: answer.id, ...turnOriginOf(history.messages) } : null;
411
+ },
412
+ onClose: uncount,
413
+ logError
414
+ });
415
+ return streamResponse(stream);
416
+ } catch (error) {
417
+ uncount();
418
+ throw error;
419
+ }
420
+ }
421
+ async function route(request, userId, segments) {
422
+ const [chatId, action, ...rest] = segments;
423
+ const method = request.method.toUpperCase();
424
+ if (options.attachments) {
425
+ if (chatId === "attachments" && action === "settings" && rest.length === 0 && method === "GET") {
426
+ return options.attachments.settings();
427
+ }
428
+ if (chatId !== void 0 && action === "attachments") {
429
+ await service.assertOwnChat(userId, chatId);
430
+ if (method !== "GET" && method !== "HEAD") await service.assertCapability(userId, chatId, "attachments");
431
+ return options.attachments.handle({ request, userId, chatId, segments: rest });
432
+ }
433
+ }
434
+ if (options.memory && chatId === "memory") {
435
+ return handleMemoryRequest({ memory: options.memory, request, userId, segments: segments.slice(1) });
436
+ }
437
+ if (chatId !== void 0 && action === "approvals" && rest.length === 1 && method === "POST") {
438
+ const body = await readJsonBody(request);
439
+ const decision = body?.decision;
440
+ if (decision !== "allow" && decision !== "deny" && decision !== "always") return errorResponse2(400, "invalid");
441
+ const page = parsePage(body);
442
+ if (page === "invalid") return errorResponse2(400, "invalid");
443
+ return Response.json(await service.answerApproval(userId, chatId, rest[0], decision, page));
444
+ }
445
+ if (chatId !== void 0 && action === "widgets" && rest.length === 1 && method === "POST") {
446
+ const toolCallId = rest[0];
447
+ const body = await readWidgetAnswerBody(request);
448
+ if (!body) {
449
+ logError(`[\u0432\u0438\u0434\u0436\u0435\u0442] \u043E\u0442\u043A\u0430\u0437 invalid_answer \u0447\u0430\u0442=${logValue(chatId)} \u0432\u044B\u0437\u043E\u0432=${logValue(toolCallId)}`);
450
+ return errorResponse2(400, "invalid_answer");
451
+ }
452
+ const page = parsePage(body);
453
+ if (page === "invalid") return errorResponse2(400, "invalid");
454
+ return Response.json(await service.answerWidget({ chatId, ownerId: userId, toolCallId, answer: body.answer, ...page && { page } }));
455
+ }
456
+ if (chatId !== void 0 && action === "browser-tools" && rest.length === 1 && method === "POST") {
457
+ const answer = parseBrowserCallAnswer(await readJsonBody(request, MAX_BROWSER_CALL_BODY_BYTES));
458
+ if (!answer) return errorResponse2(400, "invalid");
459
+ await service.answerBrowserCall(userId, chatId, rest[0], answer);
460
+ return new Response(null, { status: 204 });
461
+ }
462
+ if (chatId !== void 0 && action === "watch" && rest.length === 0 && method === "GET") {
463
+ await service.assertOwnChat(userId, chatId);
464
+ return watchResponse(request, userId, chatId);
465
+ }
466
+ if (background && chatId !== void 0 && action === "tasks") {
467
+ const [taskId, taskAction, ...extra] = rest;
468
+ if (taskId === void 0 && method === "GET") {
469
+ await service.assertOwnChat(userId, chatId);
470
+ return Response.json(await background.listForChat(userId, chatId));
471
+ }
472
+ if (taskId !== void 0 && taskAction === "cancel" && extra.length === 0 && method === "POST") {
473
+ await service.assertOwnChat(userId, chatId);
474
+ const status = await background.cancel(userId, chatId, taskId);
475
+ return status === "not_found" ? errorResponse2(404, "not_found") : Response.json({ status });
476
+ }
477
+ if (taskId !== void 0 && taskAction === "approval" && extra.length === 0 && method === "POST") {
478
+ await service.assertOwnChat(userId, chatId);
479
+ const body = await readJsonBody(request);
480
+ const decision = body?.decision;
481
+ const approvalId = body?.approvalId;
482
+ if (decision !== "allow" && decision !== "deny" || typeof approvalId !== "string" || approvalId === "") return errorResponse2(400, "invalid");
483
+ const status = await answerTaskApproval(userId, chatId, taskId, approvalId, decision);
484
+ if (status === "not_found") return errorResponse2(409, "approval_not_pending");
485
+ if (status === "stale") return errorResponse2(409, "approval_stale");
486
+ return Response.json({ status });
487
+ }
488
+ }
489
+ if (rest.length > 0) return errorResponse2(404, "not_found");
490
+ if (options.subjectChats && chatId === "subject" && action === void 0 && method === "POST") {
491
+ return openSubjectChatRoute({ request, userId, service, resolve: options.subjectChats, invalid: () => errorResponse2(400, "invalid") });
492
+ }
493
+ if (chatId === void 0) {
494
+ if (method === "GET" && options.chatList) return Response.json({ chats: await service.listChats(userId, chatListOptions(options.chatList)) });
495
+ if (method !== "POST") return errorResponse2(404, "not_found");
496
+ const data = options.newChatData ? await resolveNewChatData(options.newChatData, { user: { id: userId }, request }) : void 0;
497
+ const chat = await service.createChat(userId, data && { data });
498
+ return Response.json({ id: chat.id }, { status: 201 });
499
+ }
500
+ if (chatId === "models" && action === void 0 && method === "GET") {
501
+ return Response.json(await service.getModelMenu(userId));
502
+ }
503
+ if (action === void 0 && method === "GET") {
504
+ return Response.json({ ...await service.getHistory(userId, chatId), backgroundTasks: background !== void 0 });
505
+ }
506
+ if (action === void 0 && method === "PATCH" && options.chatList) {
507
+ const title = (await readJsonBody(request))?.title;
508
+ if (typeof title !== "string") return errorResponse2(400, "invalid");
509
+ return Response.json({ id: chatId, title: await service.renameChat(userId, chatId, title) });
510
+ }
511
+ if (action === void 0 && method === "DELETE" && options.chatList) {
512
+ await service.deleteChat(userId, chatId);
513
+ return new Response(null, { status: 204 });
514
+ }
515
+ if (action === "compactions" && method === "GET") {
516
+ return Response.json(await service.getCompactions(userId, chatId));
517
+ }
518
+ if (action === "spending" && method === "GET" && options.limits) {
519
+ return Response.json(await service.getSpendingStatus(userId, chatId));
520
+ }
521
+ if (action === "branches" && method === "GET") {
522
+ return Response.json(await service.getBranchForks(userId, chatId));
523
+ }
524
+ if (action === "branch" && method === "POST") {
525
+ const messageId = parseMessageId(await readJsonBody(request));
526
+ if (!messageId) return errorResponse2(400, "invalid");
527
+ return Response.json(await service.selectBranch(userId, chatId, messageId));
528
+ }
529
+ if (action === "regenerate" && method === "POST") {
530
+ const body = await readJsonBody(request);
531
+ const messageId = parseMessageId(body);
532
+ if (!messageId) return errorResponse2(400, "invalid");
533
+ const page = parsePage(body);
534
+ if (page === "invalid") return errorResponse2(400, "invalid");
535
+ return streamResponse(await service.regenerate(userId, chatId, messageId, page));
536
+ }
537
+ if (action === "edit" && method === "POST") {
538
+ const body = await readJsonBody(request);
539
+ const { editedMessageId, message } = body ?? {};
540
+ if (typeof editedMessageId !== "string" || !message || typeof message !== "object") return errorResponse2(400, "invalid");
541
+ if (body?.voiceMode !== void 0 && typeof body.voiceMode !== "boolean") return errorResponse2(400, "invalid");
542
+ const voiceSounds = parseVoiceSounds(body?.voiceSounds);
543
+ if (voiceSounds === "invalid") return errorResponse2(400, "invalid");
544
+ const page = parsePage(body);
545
+ if (page === "invalid") return errorResponse2(400, "invalid");
546
+ return streamResponse(
547
+ await service.editMessage(userId, chatId, editedMessageId, message, { voiceMode: body?.voiceMode === true, voiceSounds, ...page && { page } })
548
+ );
549
+ }
550
+ if (action === "model" && method === "PUT") {
551
+ const choice = parseModelChoice(await readJsonBody(request));
552
+ if (!choice) return errorResponse2(400, "invalid");
553
+ return Response.json(await service.setModel(userId, chatId, choice));
554
+ }
555
+ if (action === "messages" && method === "POST") {
556
+ const body = await readJsonBody(request);
557
+ const message = body?.message;
558
+ if (!message || typeof message !== "object") return errorResponse2(400, "invalid");
559
+ if (body.voiceMode !== void 0 && typeof body.voiceMode !== "boolean") return errorResponse2(400, "invalid");
560
+ const voiceSounds = parseVoiceSounds(body.voiceSounds);
561
+ if (voiceSounds === "invalid") return errorResponse2(400, "invalid");
562
+ const page = parsePage(body);
563
+ if (page === "invalid") return errorResponse2(400, "invalid");
564
+ const stream = await service.sendMessage(userId, chatId, message, { voiceMode: body.voiceMode === true, voiceSounds, ...page && { page } });
565
+ return streamResponse(stream);
566
+ }
567
+ if (action === "steer" && method === "POST") {
568
+ const body = await readJsonBody(request);
569
+ const message = body?.message;
570
+ if (!message || typeof message !== "object") return errorResponse2(400, "invalid");
571
+ const page = parsePage(body);
572
+ if (page === "invalid") return errorResponse2(400, "invalid");
573
+ return Response.json(await service.steer(userId, chatId, message, page && { page }), { status: 202 });
574
+ }
575
+ if (action === "retry" && method === "POST") {
576
+ const page = parsePage(await readJsonBody(request));
577
+ if (page === "invalid") return errorResponse2(400, "invalid");
578
+ const stream = await service.retry(userId, chatId, page);
579
+ return streamResponse(stream);
580
+ }
581
+ if (action === "stream" && method === "GET") {
582
+ const stream = await service.subscribe(userId, chatId);
583
+ if (!stream) return new Response(null, { status: 204 });
584
+ return streamResponse(stream);
585
+ }
586
+ if (action === "stop" && method === "POST") {
587
+ const stopped = await service.stop(userId, chatId);
588
+ return new Response(null, { status: stopped ? 202 : 204 });
589
+ }
590
+ return errorResponse2(404, "not_found");
591
+ }
592
+ const handler = {
593
+ basePath,
594
+ recoverInterruptedAnswers: (recoverOptions) => service.recoverInterruptedAnswers(recoverOptions),
595
+ flush: (flushOptions) => service.flush(flushOptions),
596
+ hasWatchers,
597
+ async fetch(request) {
598
+ const segments = pathSegments(new URL(request.url).pathname, basePath);
599
+ if (!segments) return errorResponse2(404, "not_found");
600
+ if (!isSafeMethod(request.method) && !allowsOrigin(request)) return errorResponse2(403, "forbidden_origin");
601
+ try {
602
+ const resolved = await options.resolveUser(request);
603
+ const refused = accessRefusedResponse(resolved);
604
+ if (refused) return refused;
605
+ const user = allowedUser(resolved);
606
+ if (!user) return errorResponse2(401, "invalid");
607
+ return await route(request, user.id, segments);
608
+ } catch (error) {
609
+ const response = errorToResponse(error);
610
+ if (response.status >= 500) logError(`\u0427\u0430\u0442: \u0441\u0431\u043E\u0439 \u043D\u0430 ${request.method} ${new URL(request.url).pathname}: ${describe(error)}`);
611
+ return response;
612
+ }
613
+ }
614
+ };
615
+ const serverService = Object.assign(service, {
616
+ hasWatchers,
617
+ ...background && { tasks: { list: background.listForChat.bind(background), cancel: background.cancel.bind(background), answerApproval: answerTaskApproval } }
618
+ });
619
+ return { handler, service: serverService };
620
+ }
621
+ function pathSegments(pathname, basePath) {
622
+ if (pathname !== basePath && !pathname.startsWith(`${basePath}/`)) return null;
623
+ try {
624
+ return pathname.slice(basePath.length).split("/").filter((segment) => segment.length > 0).map((segment) => decodeURIComponent(segment));
625
+ } catch {
626
+ return null;
627
+ }
628
+ }
629
+ function parseVoiceSounds(value) {
630
+ if (value === void 0) return void 0;
631
+ if (!Array.isArray(value) || !value.every(isVoiceSound)) return "invalid";
632
+ return value;
633
+ }
634
+ function parsePage(body) {
635
+ const { place, tabId, timeZone } = typeof body === "object" && body !== null ? body : {};
636
+ const zone = typeof timeZone === "string" ? timeZone : void 0;
637
+ if (place === void 0 && tabId === void 0 && zone === void 0) return void 0;
638
+ if (place !== void 0 && !isScreenPlaceInput(place)) return "invalid";
639
+ if (tabId !== void 0 && !isTabId(tabId)) return "invalid";
640
+ return { ...place !== void 0 && { place }, ...tabId !== void 0 && { tabId }, ...zone !== void 0 && { timeZone: zone } };
641
+ }
642
+ function parseBrowserCallAnswer(body) {
643
+ if (typeof body !== "object" || body === null || Array.isArray(body)) return null;
644
+ const { tabId } = body;
645
+ if (!isTabId(tabId)) return null;
646
+ const hasOutput = Object.hasOwn(body, "output");
647
+ const hasError = Object.hasOwn(body, "error");
648
+ if (hasOutput === hasError) return null;
649
+ if (hasOutput) return { tabId, output: body.output };
650
+ const { error } = body;
651
+ return typeof error === "string" ? { tabId, error } : null;
652
+ }
653
+ function parseMessageId(body) {
654
+ const { messageId } = body ?? {};
655
+ return typeof messageId === "string" && messageId.length > 0 ? messageId : null;
656
+ }
657
+ function parseModelChoice(body) {
658
+ const { modelId, providerId, reasoning } = body ?? {};
659
+ if (typeof modelId !== "string" || typeof providerId !== "string") return null;
660
+ if (reasoning !== void 0 && typeof reasoning !== "string") return null;
661
+ return { modelId, providerId, ...reasoning !== void 0 && { reasoning } };
662
+ }
663
+ var MAX_BROWSER_CALL_BODY_BYTES = 64 * 1024;
664
+ var MAX_WIDGET_BODY_BYTES = 64 * 1024;
665
+ async function readWidgetAnswerBody(request) {
666
+ try {
667
+ const body = await readJsonBody(request, MAX_WIDGET_BODY_BYTES);
668
+ return typeof body === "object" && body !== null && !Array.isArray(body) && Object.hasOwn(body, "answer") ? body : null;
669
+ } catch (error) {
670
+ if (error instanceof RequestBodyTooLargeError) return null;
671
+ throw error;
672
+ }
673
+ }
674
+ function chatListOptions(chatList) {
675
+ return typeof chatList === "object" && chatList.subjectChats ? { subjects: "include" } : void 0;
676
+ }
677
+ function errorResponse2(status, kind) {
678
+ return Response.json({ error: { kind } }, { status });
679
+ }
680
+ function describe(error) {
681
+ return error instanceof Error ? `${error.name}: ${error.message}` : String(error);
682
+ }
683
+ function errorToResponse(error) {
684
+ if (error instanceof RequestBodyTooLargeError) return errorResponse2(413, "too_large");
685
+ if (error instanceof ChatNotFoundError) return errorResponse2(404, "not_found");
686
+ if (error instanceof InvalidChatTitleError) return errorResponse2(400, "invalid");
687
+ if (error instanceof InvalidMessageError) return errorResponse2(400, "invalid");
688
+ if (error instanceof AttachmentRejectedError) return Response.json({ error: { kind: error.kind } }, { status: 400 });
689
+ if (error instanceof InvalidModelChoiceError) return errorResponse2(400, "invalid");
690
+ if (error instanceof SpendingLimitReachedError) {
691
+ const { scope, period, resetsAt, timeZone, reason, windowId, hours, unit } = error;
692
+ const details = { ...timeZone && { timeZone }, ...reason && { reason }, ...windowId && { windowId }, ...hours !== void 0 && { hours }, ...unit && { unit } };
693
+ return Response.json({ error: { kind: "spending_limit_reached", scope, period, resetsAt, ...details } }, { status: 429 });
694
+ }
695
+ if (error instanceof TurnRefusedError) return turnRefusalResponse(error.refusal);
696
+ if (error instanceof ActionForbiddenError) {
697
+ return Response.json({ error: { kind: "action_forbidden", action: error.action, code: error.action, message: error.message } }, { status: 403 });
698
+ }
699
+ if (error instanceof ChatClosedError) return Response.json({ error: { kind: "chat_closed", code: error.code, message: error.message } }, { status: 409 });
700
+ if (error instanceof ChatCreationRefusedError) {
701
+ return Response.json({ error: { kind: "chat_creation_refused", code: error.code, message: error.message } }, { status: 403 });
702
+ }
703
+ if (error instanceof TooManyTurnsError) return Response.json({ error: { kind: "too_many_turns", limit: error.limit } }, { status: 429 });
704
+ if (error instanceof ChatBusyError) return errorResponse2(409, "busy");
705
+ if (error instanceof DuplicateMessageError) return errorResponse2(409, "invalid");
706
+ if (error instanceof NothingToRetryError) return errorResponse2(409, "invalid");
707
+ if (error instanceof ApprovalNotPendingError) return errorResponse2(409, "approval_not_pending");
708
+ if (error instanceof BrowserCallNotPendingError) return errorResponse2(409, "not_pending");
709
+ if (error instanceof WidgetNotPendingError) return errorResponse2(409, "widget_not_pending");
710
+ if (error instanceof InvalidWidgetAnswerError) return errorResponse2(400, "invalid_answer");
711
+ if (error instanceof WidgetAnswerRefusedError) {
712
+ return Response.json({ error: { kind: "widget_answer_refused", code: "widget_answer_refused", message: error.message } }, { status: 409 });
713
+ }
714
+ if (error instanceof WidgetAnswerCheckAbortedError) {
715
+ return Response.json({ error: { kind: "widget_answer_cancelled", code: "widget_answer_cancelled", message: error.message } }, { status: 409 });
716
+ }
717
+ if (error instanceof WidgetAnswerCheckTimeoutError) {
718
+ return Response.json({ error: { kind: "widget_check_timeout", code: "widget_check_timeout", message: error.message } }, { status: 504 });
719
+ }
720
+ return errorResponse2(500, "other");
721
+ }
722
+
723
+ // src/index.ts
724
+ import { accessRefusedResponse as accessRefusedResponse2, allowedUser as allowedUser2 } from "@ykurilov/agent-chat-core";
725
+ export {
726
+ accessRefusedResponse2 as accessRefusedResponse,
727
+ allowedUser2 as allowedUser,
728
+ createChatHandler,
729
+ createChatServer,
730
+ turnRefusalResponse
731
+ };
732
+ //# sourceMappingURL=index.js.map