@happyvertical/smrt-chat 0.53.3 → 0.53.5

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