@happyvertical/smrt-chat 0.53.2 → 0.53.4

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.
@@ -0,0 +1,765 @@
1
+ import { i as clientRequestMessageId, n as ChatService, t as ChatClientRequestConflictError } from "./chunks/ChatService-Dl5tsK_F.js";
2
+ import { D as runAssistantTurn, E as createSessionContinuationStore, j as MAX_CLIENT_TOOL_RESULT_CHARS, w as createAssistantTurnResponse, z as sanitizeClientToolDeclarations } from "./chunks/chat-stream-DigP1_dw.js";
3
+ //#region src/sveltekit.ts
4
+ var AssistantRouteError = class extends Error {
5
+ constructor(status, code, message) {
6
+ super(message);
7
+ this.status = status;
8
+ this.code = code;
9
+ this.name = "AssistantRouteError";
10
+ }
11
+ status;
12
+ code;
13
+ };
14
+ var DEFAULT_ASSISTANT_AGENT_ID = "smrt-assistant";
15
+ var ASSISTANT_DOCK_SESSION_KEY = "assistant-dock";
16
+ var DEFAULT_ASSISTANT_MAX_BODY_BYTES = 1024 * 1024;
17
+ var DEFAULT_ASSISTANT_MAX_ATTACHMENT_BYTES = 10 * 1024 * 1024;
18
+ var MAX_TITLE_LENGTH = 200;
19
+ var MAX_ATTACHMENTS_PER_MESSAGE = 10;
20
+ var MAX_ID_LENGTH = 128;
21
+ var CLIENT_REQUEST_ID = /^[A-Za-z0-9_.:-]{1,128}$/;
22
+ var DEFAULT_ASSISTANT_ABANDONED_TURN_MS = 900 * 1e3;
23
+ var GENERIC_ERROR = "The assistant request failed.";
24
+ function resolveAssistantPrincipalFromLocals(locals) {
25
+ const record = locals ?? {};
26
+ const user = record.user;
27
+ const userId = nonEmpty(user?.id);
28
+ if (!user || !userId) return null;
29
+ const profileId = nonEmpty(user.profileId);
30
+ if (!profileId) throw new AssistantRouteError(403, "profile_required", "Your account has no profile, so it cannot use the assistant.");
31
+ const tenantId = nonEmpty(record.tenantId);
32
+ if (!tenantId) throw new AssistantRouteError(403, "tenant_required", "Select an organization to use the assistant.");
33
+ return {
34
+ userId,
35
+ profileId,
36
+ tenantId
37
+ };
38
+ }
39
+ function nonEmpty(value) {
40
+ return typeof value === "string" && value.trim().length > 0 ? value : null;
41
+ }
42
+ function json(body, status = 200, headers) {
43
+ const out = new Headers(headers);
44
+ out.set("content-type", "application/json; charset=utf-8");
45
+ out.set("cache-control", "no-store");
46
+ return new Response(JSON.stringify(body), {
47
+ status,
48
+ headers: out
49
+ });
50
+ }
51
+ function errorResponse(error) {
52
+ return json({
53
+ error: error.message,
54
+ code: error.code
55
+ }, error.status);
56
+ }
57
+ var notFound = () => new AssistantRouteError(404, "not_found", "Conversation not found.");
58
+ var badRequest = (message, code = "invalid_request") => new AssistantRouteError(400, code, message);
59
+ async function resolveValue(value, context) {
60
+ if (typeof value === "function") return value(context);
61
+ return value;
62
+ }
63
+ async function readBounded(request, max) {
64
+ const declared = Number(request.headers.get("content-length"));
65
+ const tooLarge = () => new AssistantRouteError(413, "payload_too_large", "The request is too large.");
66
+ if (Number.isFinite(declared) && declared > max) throw tooLarge();
67
+ if (!request.body) return /* @__PURE__ */ new Uint8Array();
68
+ const reader = request.body.getReader();
69
+ const chunks = [];
70
+ let total = 0;
71
+ for (;;) {
72
+ const { value, done } = await reader.read();
73
+ if (done) break;
74
+ total += value.byteLength;
75
+ if (total > max) {
76
+ await reader.cancel().catch(() => void 0);
77
+ throw tooLarge();
78
+ }
79
+ chunks.push(value);
80
+ }
81
+ const out = new Uint8Array(total);
82
+ let offset = 0;
83
+ for (const chunk of chunks) {
84
+ out.set(chunk, offset);
85
+ offset += chunk.byteLength;
86
+ }
87
+ return out;
88
+ }
89
+ async function readJsonObject(request, max) {
90
+ const type = request.headers.get("content-type") ?? "";
91
+ if (!/^application\/json\b/i.test(type)) throw new AssistantRouteError(415, "unsupported_media_type", "Send JSON.");
92
+ const bytes = await readBounded(request, max);
93
+ let parsed;
94
+ try {
95
+ parsed = JSON.parse(new TextDecoder().decode(bytes));
96
+ } catch {
97
+ throw badRequest("The request body is not valid JSON.");
98
+ }
99
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw badRequest("The request body must be a JSON object.");
100
+ return parsed;
101
+ }
102
+ function requireId(value, label) {
103
+ if (typeof value !== "string" || value.length === 0 || value.length > MAX_ID_LENGTH) throw badRequest(`${label} is missing or invalid.`);
104
+ return value;
105
+ }
106
+ function originAllowed(request, url, trusted) {
107
+ const allowed = /* @__PURE__ */ new Set([url.origin, ...trusted]);
108
+ const origin = request.headers.get("origin");
109
+ if (origin) return allowed.has(origin);
110
+ const site = request.headers.get("sec-fetch-site")?.toLowerCase();
111
+ if (site) return site === "same-origin";
112
+ const referer = request.headers.get("referer");
113
+ if (referer) try {
114
+ return allowed.has(new URL(referer).origin);
115
+ } catch {
116
+ return false;
117
+ }
118
+ return false;
119
+ }
120
+ function toIso(value) {
121
+ if (value == null || value === "") return null;
122
+ const date = value instanceof Date ? value : new Date(String(value));
123
+ return Number.isNaN(date.getTime()) ? null : date.toISOString();
124
+ }
125
+ function parseObject(value) {
126
+ if (value && typeof value === "object" && !Array.isArray(value)) return value;
127
+ if (typeof value !== "string" || value.length === 0) return null;
128
+ try {
129
+ const parsed = JSON.parse(value);
130
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
131
+ } catch {
132
+ return null;
133
+ }
134
+ }
135
+ function parseArray(value) {
136
+ if (Array.isArray(value)) return value;
137
+ if (typeof value !== "string" || value.length === 0) return [];
138
+ try {
139
+ const parsed = JSON.parse(value);
140
+ return Array.isArray(parsed) ? parsed : [];
141
+ } catch {
142
+ return [];
143
+ }
144
+ }
145
+ function toAssistantThreadWire(thread) {
146
+ return {
147
+ id: String(thread.id),
148
+ title: thread.title ?? "",
149
+ isResolved: Boolean(thread.isResolved),
150
+ messageCount: Number(thread.messageCount) || 0,
151
+ lastMessageAt: toIso(thread.lastMessageAt)
152
+ };
153
+ }
154
+ function toAssistantMessageWire(message) {
155
+ const metadata = parseObject(message.metadata);
156
+ const toolCallData = parseObject(message.toolCallData);
157
+ const attachments = parseArray(message.attachments).map((entry) => {
158
+ const a = entry;
159
+ const id = nonEmpty(a?.id);
160
+ const name = nonEmpty(a?.filename) ?? nonEmpty(a?.name);
161
+ if (!id || !name) return null;
162
+ return {
163
+ id,
164
+ name,
165
+ contentType: nonEmpty(a?.contentType) ?? "application/octet-stream",
166
+ size: Number(a?.size) || 0,
167
+ ...nonEmpty(a?.url) ? { url: a?.url } : {}
168
+ };
169
+ }).filter((a) => a !== null);
170
+ const clientRequestId = nonEmpty(metadata?.clientRequestId);
171
+ return {
172
+ id: String(message.id),
173
+ threadId: message.threadId ?? "",
174
+ content: message.content ?? "",
175
+ role: message.role,
176
+ createdAt: toIso(message.created_at) ?? (/* @__PURE__ */ new Date(0)).toISOString(),
177
+ ...attachments.length > 0 ? { attachments } : {},
178
+ ...toolCallData ? { toolCallData } : {},
179
+ ...clientRequestId ? { clientRequestId } : {}
180
+ };
181
+ }
182
+ function isAuthorizationFailure(error) {
183
+ const message = error instanceof Error ? error.message : "";
184
+ return /authorization denied|not found|not active/i.test(message);
185
+ }
186
+ var ROUTE_METHODS = {
187
+ threads: ["GET", "POST"],
188
+ "thread-messages": ["GET", "POST"],
189
+ "thread-resume": ["POST"],
190
+ attachments: ["POST"],
191
+ actions: ["POST"]
192
+ };
193
+ function matchRoute(segments) {
194
+ const [a, b, c, ...rest] = segments;
195
+ if (rest.length > 0) return null;
196
+ if (a === "threads" && b === void 0) return { route: "threads" };
197
+ if (a === "threads" && b && c === "messages") return {
198
+ route: "thread-messages",
199
+ threadId: b
200
+ };
201
+ if (a === "threads" && b && c === "resume") return {
202
+ route: "thread-resume",
203
+ threadId: b
204
+ };
205
+ if (a === "attachments" && b === void 0) return { route: "attachments" };
206
+ if (a === "actions" && (b === "preview" || b === "apply") && !c) return {
207
+ route: "actions",
208
+ phase: b
209
+ };
210
+ return null;
211
+ }
212
+ function mountAssistantRoutes(options) {
213
+ if (!options?.ai) throw new Error("mountAssistantRoutes: `ai` is required (an AI client or a factory).");
214
+ const agentId = options.agentId ?? "smrt-assistant";
215
+ const paramName = options.paramName ?? "path";
216
+ const maxBody = options.maxBodyBytes ?? 1048576;
217
+ const maxContent = options.maxContentLength ?? 12e3;
218
+ const historyLimit = Math.max(0, options.historyLimit ?? 20);
219
+ const checkOrigin = options.checkOrigin ?? true;
220
+ const trustedOrigins = options.trustedOrigins ?? [];
221
+ const resolvePrincipal = options.resolvePrincipal ?? ((event) => resolveAssistantPrincipalFromLocals(event.locals));
222
+ const logError = options.onError ?? ((error) => {
223
+ console.error("[smrt-chat] assistant route failed:", error);
224
+ });
225
+ const safeLog = (error) => {
226
+ try {
227
+ logError(error);
228
+ } catch {}
229
+ };
230
+ const abandonedTurnMs = options.abandonedTurnMs ?? 9e5;
231
+ const segmentsOf = (event) => {
232
+ const param = event.params?.[paramName];
233
+ let path = typeof param === "string" ? param : void 0;
234
+ if (path === void 0 && options.basePath) {
235
+ const base = options.basePath.replace(/\/+$/, "");
236
+ const pathname = event.url.pathname;
237
+ if (pathname === base || pathname.startsWith(`${base}/`)) path = pathname.slice(base.length);
238
+ }
239
+ if (path === void 0) return null;
240
+ return path.split("/").filter((segment) => segment.length > 0);
241
+ };
242
+ const chatFor = async (principal) => {
243
+ const chat = await ChatService.create({
244
+ tenantId: principal.tenantId,
245
+ ...options.db ? { db: options.db } : {}
246
+ });
247
+ await chat.initialize();
248
+ return chat;
249
+ };
250
+ const findSession = async (chat, principal) => {
251
+ return (await chat.findActiveAgentSessions({
252
+ tenantId: principal.tenantId,
253
+ agentId,
254
+ participantProfileId: principal.profileId
255
+ })).find((session) => session.getSessionKey() === "assistant-dock" && session.participantProfileId === principal.profileId && session.tenantId === principal.tenantId && Boolean(session.chatRoomId)) ?? null;
256
+ };
257
+ const requireOwnThread = async (chat, principal, session, threadId) => {
258
+ if (!session?.chatRoomId) throw notFound();
259
+ const thread = await chat.getThread({
260
+ threadId,
261
+ tenantId: principal.tenantId
262
+ });
263
+ if (!thread || thread.roomId !== session.chatRoomId || thread.tenantId !== principal.tenantId) throw notFound();
264
+ return thread;
265
+ };
266
+ const allowedToolsFor = async (context) => {
267
+ const list = await resolveValue(options.allowedTools, context);
268
+ return Array.isArray(list) ? list.filter((tool) => typeof tool === "string" && tool.length > 0) : [];
269
+ };
270
+ const resolveModel = (requested) => {
271
+ if (requested === void 0 || requested === null || requested === "") return options.defaultModel;
272
+ if (typeof requested !== "string" || requested.length > MAX_ID_LENGTH) throw badRequest("The model is invalid.", "model_not_allowed");
273
+ if (!options.models || options.models.length === 0) return options.defaultModel;
274
+ if (!options.models.some((model) => model.id === requested)) throw badRequest("That model is not available.", "model_not_allowed");
275
+ return requested;
276
+ };
277
+ const resolveAI = async (context, model) => {
278
+ try {
279
+ const ai = typeof options.ai === "function" ? await options.ai({
280
+ ...context,
281
+ ...model ? { model } : {}
282
+ }) : options.ai;
283
+ if (!ai || typeof ai.chat !== "function") throw new Error("The AI factory returned no client.");
284
+ return ai;
285
+ } catch (error) {
286
+ safeLog(error);
287
+ throw new AssistantRouteError(503, "assistant_unavailable", "The assistant is unavailable right now. Please try again later.");
288
+ }
289
+ };
290
+ const continuationStoreFor = (context, chat, session) => {
291
+ if (options.continuations) return options.continuations({
292
+ ...context,
293
+ session
294
+ });
295
+ const { principal } = context;
296
+ return createSessionContinuationStore(async () => {
297
+ const current = await chat.getAgentSession({
298
+ agentSessionId: String(session.id),
299
+ tenantId: principal.tenantId
300
+ });
301
+ return current && current.participantProfileId === principal.profileId && current.isActive() ? current : null;
302
+ });
303
+ };
304
+ const turnSetup = async (context, chat, session, thread, body) => {
305
+ const { principal } = context;
306
+ const model = resolveModel(body.model);
307
+ const allowedTools = await allowedToolsFor(context);
308
+ const clientTools = sanitizeClientToolDeclarations(body.clientTools, options.clientToolAllowList ?? []);
309
+ const ai = await resolveAI(context, model);
310
+ const [extraTools, tools, systemPrompt] = await Promise.all([
311
+ resolveValue(options.extraTools, context),
312
+ resolveValue(options.tools, context),
313
+ resolveValue(options.systemPrompt, context)
314
+ ]);
315
+ if (options.authorInvocation) {
316
+ const current = session.getAllowedTools();
317
+ if (current.length !== allowedTools.length || current.some((tool, index) => tool !== allowedTools[index])) await chat.updateAgentSessionConfig({
318
+ agentSessionId: String(session.id),
319
+ actorProfileId: principal.profileId,
320
+ tenantId: principal.tenantId,
321
+ allowedTools
322
+ });
323
+ }
324
+ const continuations = continuationStoreFor(context, chat, session);
325
+ const abort = new AbortController();
326
+ const requestSignal = context.event.request.signal;
327
+ if (requestSignal.aborted) abort.abort();
328
+ else requestSignal.addEventListener("abort", () => abort.abort(), { once: true });
329
+ return {
330
+ abort,
331
+ ai,
332
+ model,
333
+ clientTools,
334
+ continuations,
335
+ continuationKey: String(thread.id),
336
+ turn: {
337
+ ai,
338
+ principal: {
339
+ runAsUserId: principal.userId,
340
+ tenantId: principal.tenantId,
341
+ allowedTools
342
+ },
343
+ ...options.db ? { db: options.db } : {},
344
+ ...systemPrompt ? { systemPrompt } : {},
345
+ extraTools: [...extraTools ?? []],
346
+ tools: [...tools ?? []],
347
+ clientTools,
348
+ continuations,
349
+ continuationKey: String(thread.id),
350
+ ...model ? { model } : {},
351
+ ...options.maxSteps !== void 0 ? { maxSteps: options.maxSteps } : {},
352
+ ...options.maxTurnTokens !== void 0 ? { maxTurnTokens: options.maxTurnTokens } : {},
353
+ ...options.maxTurnMs !== void 0 ? { maxTurnMs: options.maxTurnMs } : {},
354
+ ...options.maxTokens !== void 0 ? { maxTokens: options.maxTokens } : {},
355
+ ...options.temperature !== void 0 ? { temperature: options.temperature } : {},
356
+ ...principal.permissions ? { permissions: principal.permissions } : {},
357
+ ...options.audit ? { audit: options.audit } : {},
358
+ ...options.postgresRls !== void 0 ? { postgresRls: options.postgresRls } : {},
359
+ onBehalfOfUserId: principal.userId,
360
+ signal: abort.signal,
361
+ ...options.onUsage ? { onUsage: (usage) => {
362
+ try {
363
+ options.onUsage?.(usage, context);
364
+ } catch (error) {
365
+ safeLog(error);
366
+ }
367
+ } } : {},
368
+ ...options.describeTool ? { describeTool: options.describeTool } : {},
369
+ onError: safeLog,
370
+ serializeMessage: (message) => toAssistantMessageWire(message),
371
+ author: {
372
+ chatService: chat,
373
+ agentSessionId: String(session.id),
374
+ tenantId: principal.tenantId,
375
+ threadId: String(thread.id),
376
+ ...options.authorInvocation ? { authorInvocation: options.authorInvocation } : {}
377
+ }
378
+ }
379
+ };
380
+ };
381
+ const stream = (events, abort) => createAssistantTurnResponse(events, {
382
+ ...options.heartbeatMs !== void 0 ? { heartbeatMs: options.heartbeatMs } : {},
383
+ onError: safeLog,
384
+ onCancel: () => abort.abort()
385
+ });
386
+ const listThreads = async (context) => {
387
+ const chat = await chatFor(context.principal);
388
+ const session = await findSession(chat, context.principal);
389
+ if (!session?.chatRoomId) return json({ items: [] });
390
+ return json({ items: (await chat.listRoomThreads({
391
+ roomId: session.chatRoomId,
392
+ actorProfileId: context.principal.profileId,
393
+ tenantId: context.principal.tenantId
394
+ })).map(toAssistantThreadWire) });
395
+ };
396
+ const createThread = async (context) => {
397
+ const body = await readJsonObject(context.event.request, maxBody);
398
+ const title = typeof body.title === "string" ? body.title.trim() : "";
399
+ if (!title || title.length > MAX_TITLE_LENGTH) throw badRequest(`A title of 1 to ${MAX_TITLE_LENGTH} characters is required.`);
400
+ const { principal } = context;
401
+ const chat = await chatFor(principal);
402
+ const allowedTools = await allowedToolsFor(context);
403
+ const { session } = await chat.createAgentSession({
404
+ tenantId: principal.tenantId,
405
+ agentId,
406
+ actorProfileId: principal.profileId,
407
+ allowedTools,
408
+ sessionKey: ASSISTANT_DOCK_SESSION_KEY
409
+ });
410
+ if (!session.chatRoomId) throw new Error("Assistant session has no room");
411
+ return json({ thread: toAssistantThreadWire(await chat.startThread({
412
+ tenantId: principal.tenantId,
413
+ roomId: session.chatRoomId,
414
+ actorProfileId: principal.profileId,
415
+ title
416
+ })) }, 201);
417
+ };
418
+ const loadMessages = async (context, threadId) => {
419
+ const { principal } = context;
420
+ const chat = await chatFor(principal);
421
+ const session = await findSession(chat, principal);
422
+ await requireOwnThread(chat, principal, session, threadId);
423
+ return json({ items: (await chat.getThreadMessages({
424
+ threadId,
425
+ actorProfileId: principal.profileId,
426
+ tenantId: principal.tenantId,
427
+ limit: 200
428
+ })).map(toAssistantMessageWire) });
429
+ };
430
+ const reservationAnswer = async (context, chat, session, threadId, messageId) => {
431
+ const { principal } = context;
432
+ const stored = await chat.getThreadMessageReplies({
433
+ threadId,
434
+ messageId,
435
+ actorProfileId: principal.profileId,
436
+ tenantId: principal.tenantId
437
+ });
438
+ const anchor = stored?.message;
439
+ if (!stored || anchor?.role !== "user" || anchor.senderProfileId !== principal.profileId) return null;
440
+ const userMessage = toAssistantMessageWire(anchor);
441
+ const after = stored.replies.map(toAssistantMessageWire);
442
+ const assistantMessage = [...after].reverse().find((message) => message.role === "assistant");
443
+ const answer = (outcome) => {
444
+ return json({
445
+ duplicate: true,
446
+ inProgress: outcome === "in_progress",
447
+ outcome,
448
+ userMessage,
449
+ ...assistantMessage ? { assistantMessage } : {},
450
+ messages: after
451
+ }, 200);
452
+ };
453
+ const turnFailed = () => new AssistantRouteError(409, "turn_failed", "The assistant could not answer this message. Send it again.");
454
+ const metadata = parseObject(anchor.metadata) ?? {};
455
+ const recorded = metadata.turnOutcome;
456
+ if (assistantMessage) return answer("completed");
457
+ if (recorded === "failed") throw turnFailed();
458
+ if (recorded === "cancelled") return answer("cancelled");
459
+ if (recorded === "completed") return answer("completed");
460
+ if (recorded === "suspended") {
461
+ const continuationId = nonEmpty(metadata.turnContinuationId);
462
+ if (!continuationId) throw turnFailed();
463
+ const store = continuationStoreFor(context, chat, session);
464
+ if (!(store.has ? await store.has(threadId, continuationId) : Date.now() - new Date(String(metadata.turnSettledAt)).getTime() <= 9e5)) throw turnFailed();
465
+ return answer("in_progress");
466
+ }
467
+ const since = recorded === "running" ? metadata.turnSettledAt : anchor.created_at;
468
+ const age = Date.now() - new Date(String(since)).getTime();
469
+ if (Number.isFinite(age) && age > abandonedTurnMs) throw turnFailed();
470
+ return answer("in_progress");
471
+ };
472
+ const settleOutcome = (chat, principal, threadId, messageId) => async (outcome, settings = {}) => {
473
+ try {
474
+ await chat.recordClientRequestOutcome({
475
+ tenantId: principal.tenantId,
476
+ threadId,
477
+ messageId,
478
+ actorProfileId: principal.profileId,
479
+ outcome,
480
+ resumedFrom: null
481
+ });
482
+ } catch (error) {
483
+ if (!settings.quiet) safeLog(error);
484
+ }
485
+ };
486
+ const recordTurnState = (chat, principal, threadId) => async (state) => {
487
+ if (!state.originMessageId) return true;
488
+ return chat.recordClientRequestOutcome({
489
+ tenantId: principal.tenantId,
490
+ threadId,
491
+ messageId: state.originMessageId,
492
+ actorProfileId: principal.profileId,
493
+ outcome: state.state,
494
+ resumedFrom: state.resumedFrom,
495
+ continuationId: state.continuationId ?? null
496
+ });
497
+ };
498
+ const sendMessage = async (context, threadId) => {
499
+ const body = await readJsonObject(context.event.request, maxBody);
500
+ const content = typeof body.content === "string" ? body.content.trim() : "";
501
+ if (!content) throw badRequest("The message is empty.", "empty_message");
502
+ if (content.length > maxContent) throw new AssistantRouteError(413, "message_too_long", `Messages are limited to ${maxContent} characters.`);
503
+ const clientRequestId = body.clientRequestId;
504
+ if (typeof clientRequestId !== "string" || !CLIENT_REQUEST_ID.test(clientRequestId)) throw badRequest("clientRequestId is missing or invalid.");
505
+ const references = body.attachments;
506
+ if (references !== void 0 && references !== null && (!Array.isArray(references) || references.length > MAX_ATTACHMENTS_PER_MESSAGE)) throw badRequest("attachments must be a short list.");
507
+ const hasAttachments = Array.isArray(references) && references.length > 0;
508
+ if (hasAttachments && !options.attachments) throw badRequest("This assistant does not accept attachments.", "attachments_unsupported");
509
+ resolveModel(body.model);
510
+ const { principal } = context;
511
+ const chat = await chatFor(principal);
512
+ const session = await findSession(chat, principal);
513
+ const thread = await requireOwnThread(chat, principal, session, threadId);
514
+ if (!session?.chatRoomId) throw notFound();
515
+ const reservationId = clientRequestMessageId({
516
+ tenantId: principal.tenantId,
517
+ roomId: session.chatRoomId,
518
+ threadId,
519
+ actorProfileId: principal.profileId,
520
+ clientRequestId
521
+ });
522
+ const known = await reservationAnswer(context, chat, session, threadId, reservationId);
523
+ if (known) return known;
524
+ let attachments = null;
525
+ if (hasAttachments && options.attachments) {
526
+ const verified = await options.attachments.verify(references, context);
527
+ if (!verified || verified.length !== references.length) throw badRequest("An attachment could not be found.", "attachment_not_found");
528
+ attachments = verified.map((record) => ({
529
+ id: record.id,
530
+ filename: record.name,
531
+ contentType: record.contentType,
532
+ size: record.size,
533
+ ...record.url ? { url: record.url } : {}
534
+ }));
535
+ }
536
+ const setup = await turnSetup(context, chat, session, thread, body);
537
+ const recordFailure = settleOutcome(chat, principal, threadId, reservationId);
538
+ let userMessage;
539
+ try {
540
+ userMessage = await chat.sendMessage({
541
+ tenantId: principal.tenantId,
542
+ roomId: session.chatRoomId,
543
+ threadId,
544
+ actorProfileId: principal.profileId,
545
+ agentSessionId: String(session.id),
546
+ content,
547
+ clientRequestId,
548
+ attachments
549
+ });
550
+ } catch (error) {
551
+ if (error instanceof ChatClientRequestConflictError) {
552
+ const answer = await reservationAnswer(context, chat, session, threadId, reservationId);
553
+ if (answer) return answer;
554
+ } else await recordFailure("failed", { quiet: true });
555
+ throw error;
556
+ }
557
+ const userWire = toAssistantMessageWire(userMessage);
558
+ try {
559
+ const history = [];
560
+ if (historyLimit > 0) {
561
+ const earlier = await chat.getThreadMessages({
562
+ threadId,
563
+ actorProfileId: principal.profileId,
564
+ tenantId: principal.tenantId,
565
+ limit: historyLimit + 1
566
+ });
567
+ for (const message of earlier) {
568
+ if (String(message.id) === userWire.id) continue;
569
+ if (message.role !== "user" && message.role !== "assistant") continue;
570
+ if (!message.content) continue;
571
+ history.push({
572
+ role: message.role,
573
+ content: message.content
574
+ });
575
+ }
576
+ }
577
+ const events = runAssistantTurn({
578
+ ...setup.turn,
579
+ history: history.slice(-historyLimit),
580
+ userMessage: content,
581
+ originMessageId: userWire.id,
582
+ onState: recordTurnState(chat, principal, threadId)
583
+ });
584
+ return stream(detached({
585
+ type: "message",
586
+ message: userWire
587
+ }, events), setup.abort);
588
+ } catch (error) {
589
+ await recordFailure("failed");
590
+ throw error;
591
+ }
592
+ };
593
+ const resumeTurn = async (context, threadId) => {
594
+ const body = await readJsonObject(context.event.request, maxBody);
595
+ const continuationId = requireId(body.continuationId, "continuationId");
596
+ if (!Array.isArray(body.results) || body.results.length > 64) throw badRequest("results must be a list of tool results.");
597
+ const results = body.results.map((raw) => {
598
+ const entry = raw;
599
+ if (!entry || typeof entry !== "object") throw badRequest("Each result must be an object.");
600
+ const id = requireId(entry.id, "result id");
601
+ if (typeof entry.ok !== "boolean") throw badRequest("Each result needs an ok flag.");
602
+ const result = typeof entry.result === "string" ? entry.result.slice(0, MAX_CLIENT_TOOL_RESULT_CHARS) : void 0;
603
+ const error = typeof entry.error === "string" ? entry.error.slice(0, 500) : void 0;
604
+ return {
605
+ id,
606
+ ok: entry.ok,
607
+ ...result !== void 0 ? { result } : {},
608
+ ...error !== void 0 ? { error } : {}
609
+ };
610
+ });
611
+ resolveModel(body.model);
612
+ const { principal } = context;
613
+ const chat = await chatFor(principal);
614
+ const session = await findSession(chat, principal);
615
+ const thread = await requireOwnThread(chat, principal, session, threadId);
616
+ if (!session) throw notFound();
617
+ const setup = await turnSetup(context, chat, session, thread, body);
618
+ return stream(detached(null, runAssistantTurn({
619
+ ...setup.turn,
620
+ resume: {
621
+ continuationId,
622
+ results
623
+ },
624
+ onState: recordTurnState(chat, principal, threadId)
625
+ })), setup.abort);
626
+ };
627
+ const uploadAttachment = async (context) => {
628
+ const storage = options.attachments;
629
+ if (!storage) throw new AssistantRouteError(404, "attachments_unsupported", "This assistant does not accept attachments.");
630
+ const request = context.event.request;
631
+ const type = request.headers.get("content-type") ?? "";
632
+ if (!/^multipart\/form-data\b/i.test(type)) throw new AssistantRouteError(415, "unsupported_media_type", "Upload a file as multipart/form-data.");
633
+ const maxBytes = storage.maxBytes ?? 10485760;
634
+ const bytes = await readBounded(request, maxBytes + 64 * 1024);
635
+ let form;
636
+ try {
637
+ form = await new Response(bytes, { headers: { "content-type": type } }).formData();
638
+ } catch {
639
+ throw badRequest("The upload could not be read.");
640
+ }
641
+ const file = form.get("file");
642
+ if (!file || typeof file === "string") throw badRequest("Attach the upload as the `file` field.");
643
+ if (file.size > maxBytes) throw new AssistantRouteError(413, "payload_too_large", "The file is too large.");
644
+ const record = await storage.upload(file, context);
645
+ return json({ attachment: {
646
+ id: record.id,
647
+ name: record.name,
648
+ contentType: record.contentType,
649
+ size: record.size,
650
+ ...record.url ? { url: record.url } : {}
651
+ } }, 201);
652
+ };
653
+ const runAction = async (context, phase) => {
654
+ if (!options.actions) throw new AssistantRouteError(404, "actions_unsupported", "This assistant has no actions.");
655
+ const body = await readJsonObject(context.event.request, maxBody);
656
+ if (body.phase !== phase) throw badRequest(`Expected a ${phase} request.`);
657
+ const adapter = await resolveValue(options.actions.adapter, context);
658
+ if (!adapter) throw new Error("No data-surface action adapter resolved.");
659
+ const { principal } = context;
660
+ const allowedTools = await allowedToolsFor(context);
661
+ const actionContext = { principal: {
662
+ ...options.db ? { db: options.db } : {},
663
+ principal: {
664
+ runAsUserId: principal.userId,
665
+ tenantId: principal.tenantId,
666
+ allowedTools
667
+ },
668
+ onBehalfOfUserId: principal.userId,
669
+ ...principal.permissions ? { permissions: principal.permissions } : {},
670
+ ...options.audit ? { audit: options.audit } : {},
671
+ ...options.postgresRls !== void 0 ? { postgresRls: options.postgresRls } : {}
672
+ } };
673
+ const request = body;
674
+ try {
675
+ return json({ result: phase === "preview" ? await adapter.preview(request, actionContext) : await adapter.apply(request, actionContext) });
676
+ } catch (error) {
677
+ safeLog(error);
678
+ return json({
679
+ error: phase === "apply" ? "The change may or may not have been applied. Check again." : GENERIC_ERROR,
680
+ code: phase === "apply" ? "outcome_unknown" : "internal_error"
681
+ }, 500);
682
+ }
683
+ };
684
+ const handle = async (event) => {
685
+ try {
686
+ const segments = segmentsOf(event);
687
+ const match = segments ? matchRoute(segments) : null;
688
+ if (!match) throw new AssistantRouteError(404, "not_found", "Not found.");
689
+ const method = event.request.method.toUpperCase();
690
+ const allowed = ROUTE_METHODS[match.route];
691
+ if (!allowed.includes(method)) return json({
692
+ error: "Method not allowed.",
693
+ code: "method_not_allowed"
694
+ }, 405, { allow: allowed.join(", ") });
695
+ if (method !== "GET" && checkOrigin && !originAllowed(event.request, event.url, trustedOrigins)) throw new AssistantRouteError(403, "invalid_origin", "Invalid request origin.");
696
+ const principal = await resolvePrincipal(event);
697
+ if (!principal) throw new AssistantRouteError(401, "unauthenticated", "Sign in to use the assistant.");
698
+ if (!nonEmpty(principal.userId) || !nonEmpty(principal.profileId) || !nonEmpty(principal.tenantId)) throw new AssistantRouteError(403, "principal_incomplete", "Your session cannot use the assistant.");
699
+ const context = {
700
+ event,
701
+ principal
702
+ };
703
+ switch (match.route) {
704
+ case "threads": return method === "GET" ? await listThreads(context) : await createThread(context);
705
+ case "thread-messages": {
706
+ const threadId = requireId(match.threadId, "thread id");
707
+ return method === "GET" ? await loadMessages(context, threadId) : await sendMessage(context, threadId);
708
+ }
709
+ case "thread-resume": return await resumeTurn(context, requireId(match.threadId, "thread id"));
710
+ case "attachments": return await uploadAttachment(context);
711
+ case "actions": return await runAction(context, match.phase);
712
+ }
713
+ } catch (error) {
714
+ if (error instanceof AssistantRouteError) return errorResponse(error);
715
+ if (isAuthorizationFailure(error)) return errorResponse(notFound());
716
+ safeLog(error);
717
+ return json({
718
+ error: GENERIC_ERROR,
719
+ code: "internal_error"
720
+ }, 500);
721
+ }
722
+ };
723
+ return {
724
+ GET: handle,
725
+ POST: handle,
726
+ handle
727
+ };
728
+ }
729
+ function detached(first, events) {
730
+ const buffer = first ? [first] : [];
731
+ let finished = false;
732
+ let notify = null;
733
+ const wake = () => {
734
+ const resume = notify;
735
+ notify = null;
736
+ resume?.();
737
+ };
738
+ (async () => {
739
+ try {
740
+ for await (const event of events) {
741
+ buffer.push(event);
742
+ wake();
743
+ }
744
+ } catch {} finally {
745
+ finished = true;
746
+ wake();
747
+ }
748
+ })();
749
+ return (async function* () {
750
+ for (;;) {
751
+ if (buffer.length > 0) {
752
+ yield buffer.shift();
753
+ continue;
754
+ }
755
+ if (finished) return;
756
+ await new Promise((resolve) => {
757
+ notify = resolve;
758
+ });
759
+ }
760
+ })();
761
+ }
762
+ //#endregion
763
+ export { ASSISTANT_DOCK_SESSION_KEY, AssistantRouteError, DEFAULT_ASSISTANT_ABANDONED_TURN_MS, DEFAULT_ASSISTANT_AGENT_ID, DEFAULT_ASSISTANT_MAX_ATTACHMENT_BYTES, DEFAULT_ASSISTANT_MAX_BODY_BYTES, mountAssistantRoutes, resolveAssistantPrincipalFromLocals, toAssistantMessageWire, toAssistantThreadWire };
764
+
765
+ //# sourceMappingURL=sveltekit.js.map