@happyvertical/smrt-chat 0.53.4 → 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.
package/dist/sveltekit.js CHANGED
@@ -1,5 +1,7 @@
1
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";
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";
3
5
  //#region src/sveltekit.ts
4
6
  var AssistantRouteError = class extends Error {
5
7
  constructor(status, code, message) {
@@ -60,6 +62,29 @@ async function resolveValue(value, context) {
60
62
  if (typeof value === "function") return value(context);
61
63
  return value;
62
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
+ }
63
88
  async function readBounded(request, max) {
64
89
  const declared = Number(request.headers.get("content-length"));
65
90
  const tooLarge = () => new AssistantRouteError(413, "payload_too_large", "The request is too large.");
@@ -209,8 +234,17 @@ function matchRoute(segments) {
209
234
  };
210
235
  return null;
211
236
  }
212
- function mountAssistantRoutes(options) {
213
- if (!options?.ai) throw new Error("mountAssistantRoutes: `ai` is required (an AI client or a factory).");
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
+ }
214
248
  const agentId = options.agentId ?? "smrt-assistant";
215
249
  const paramName = options.paramName ?? "path";
216
250
  const maxBody = options.maxBodyBytes ?? 1048576;
@@ -228,6 +262,7 @@ function mountAssistantRoutes(options) {
228
262
  } catch {}
229
263
  };
230
264
  const abandonedTurnMs = options.abandonedTurnMs ?? 9e5;
265
+ const turnStartTimeoutMs = options.turnStartTimeoutMs ?? 6e4;
231
266
  const segmentsOf = (event) => {
232
267
  const param = event.params?.[paramName];
233
268
  let path = typeof param === "string" ? param : void 0;
@@ -239,10 +274,22 @@ function mountAssistantRoutes(options) {
239
274
  if (path === void 0) return null;
240
275
  return path.split("/").filter((segment) => segment.length > 0);
241
276
  };
242
- const chatFor = async (principal) => {
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);
243
290
  const chat = await ChatService.create({
244
- tenantId: principal.tenantId,
245
- ...options.db ? { db: options.db } : {}
291
+ tenantId: context.principal.tenantId,
292
+ ...db ? { db } : {}
246
293
  });
247
294
  await chat.initialize();
248
295
  return chat;
@@ -276,10 +323,10 @@ function mountAssistantRoutes(options) {
276
323
  };
277
324
  const resolveAI = async (context, model) => {
278
325
  try {
279
- const ai = typeof options.ai === "function" ? await options.ai({
326
+ const ai = typeof aiSource === "function" ? await aiSource({
280
327
  ...context,
281
328
  ...model ? { model } : {}
282
- }) : options.ai;
329
+ }) : aiSource;
283
330
  if (!ai || typeof ai.chat !== "function") throw new Error("The AI factory returned no client.");
284
331
  return ai;
285
332
  } catch (error) {
@@ -287,6 +334,24 @@ function mountAssistantRoutes(options) {
287
334
  throw new AssistantRouteError(503, "assistant_unavailable", "The assistant is unavailable right now. Please try again later.");
288
335
  }
289
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
+ };
290
355
  const continuationStoreFor = (context, chat, session) => {
291
356
  if (options.continuations) return options.continuations({
292
357
  ...context,
@@ -301,18 +366,33 @@ function mountAssistantRoutes(options) {
301
366
  return current && current.participantProfileId === principal.profileId && current.isActive() ? current : null;
302
367
  });
303
368
  };
304
- const turnSetup = async (context, chat, session, thread, body) => {
305
- const { principal } = context;
369
+ const prepareTurn = async (context, body) => {
306
370
  const model = resolveModel(body.model);
307
371
  const allowedTools = await allowedToolsFor(context);
308
372
  const clientTools = sanitizeClientToolDeclarations(body.clientTools, options.clientToolAllowList ?? []);
309
373
  const ai = await resolveAI(context, model);
310
- const [extraTools, tools, systemPrompt] = await Promise.all([
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([
311
390
  resolveValue(options.extraTools, context),
312
- resolveValue(options.tools, context),
313
- resolveValue(options.systemPrompt, context)
391
+ resolveValue(options.systemPrompt, context),
392
+ dbFor(context)
314
393
  ]);
315
- if (options.authorInvocation) {
394
+ const tools = await manifestToolsFor(context, allowedTools, extraTools ?? [], db);
395
+ if (options.authorInvocation && mirror) {
316
396
  const current = session.getAllowedTools();
317
397
  if (current.length !== allowedTools.length || current.some((tool, index) => tool !== allowedTools[index])) await chat.updateAgentSessionConfig({
318
398
  agentSessionId: String(session.id),
@@ -322,10 +402,6 @@ function mountAssistantRoutes(options) {
322
402
  });
323
403
  }
324
404
  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
405
  return {
330
406
  abort,
331
407
  ai,
@@ -340,10 +416,10 @@ function mountAssistantRoutes(options) {
340
416
  tenantId: principal.tenantId,
341
417
  allowedTools
342
418
  },
343
- ...options.db ? { db: options.db } : {},
419
+ ...db ? { db } : {},
344
420
  ...systemPrompt ? { systemPrompt } : {},
345
421
  extraTools: [...extraTools ?? []],
346
- tools: [...tools ?? []],
422
+ tools: [...tools],
347
423
  clientTools,
348
424
  continuations,
349
425
  continuationKey: String(thread.id),
@@ -383,8 +459,103 @@ function mountAssistantRoutes(options) {
383
459
  onError: safeLog,
384
460
  onCancel: () => abort.abort()
385
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
+ };
386
557
  const listThreads = async (context) => {
387
- const chat = await chatFor(context.principal);
558
+ const chat = await chatFor(context);
388
559
  const session = await findSession(chat, context.principal);
389
560
  if (!session?.chatRoomId) return json({ items: [] });
390
561
  return json({ items: (await chat.listRoomThreads({
@@ -398,7 +569,7 @@ function mountAssistantRoutes(options) {
398
569
  const title = typeof body.title === "string" ? body.title.trim() : "";
399
570
  if (!title || title.length > MAX_TITLE_LENGTH) throw badRequest(`A title of 1 to ${MAX_TITLE_LENGTH} characters is required.`);
400
571
  const { principal } = context;
401
- const chat = await chatFor(principal);
572
+ const chat = await chatFor(context);
402
573
  const allowedTools = await allowedToolsFor(context);
403
574
  const { session } = await chat.createAgentSession({
404
575
  tenantId: principal.tenantId,
@@ -417,7 +588,7 @@ function mountAssistantRoutes(options) {
417
588
  };
418
589
  const loadMessages = async (context, threadId) => {
419
590
  const { principal } = context;
420
- const chat = await chatFor(principal);
591
+ const chat = await chatFor(context);
421
592
  const session = await findSession(chat, principal);
422
593
  await requireOwnThread(chat, principal, session, threadId);
423
594
  return json({ items: (await chat.getThreadMessages({
@@ -508,7 +679,7 @@ function mountAssistantRoutes(options) {
508
679
  if (hasAttachments && !options.attachments) throw badRequest("This assistant does not accept attachments.", "attachments_unsupported");
509
680
  resolveModel(body.model);
510
681
  const { principal } = context;
511
- const chat = await chatFor(principal);
682
+ const chat = await chatFor(context);
512
683
  const session = await findSession(chat, principal);
513
684
  const thread = await requireOwnThread(chat, principal, session, threadId);
514
685
  if (!session?.chatRoomId) throw notFound();
@@ -533,7 +704,8 @@ function mountAssistantRoutes(options) {
533
704
  ...record.url ? { url: record.url } : {}
534
705
  }));
535
706
  }
536
- const setup = await turnSetup(context, chat, session, thread, body);
707
+ const prepared = await prepareTurn(context, body);
708
+ const setup = await turnSetup(context, chat, session, thread, prepared);
537
709
  const recordFailure = settleOutcome(chat, principal, threadId, reservationId);
538
710
  let userMessage;
539
711
  try {
@@ -574,17 +746,17 @@ function mountAssistantRoutes(options) {
574
746
  });
575
747
  }
576
748
  }
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({
749
+ const transcript = history.slice(-historyLimit);
750
+ return await respondWithTurn(context, chat, session, thread, prepared, setup, {
585
751
  type: "message",
586
752
  message: userWire
587
- }, events), setup.abort);
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);
588
760
  } catch (error) {
589
761
  await recordFailure("failed");
590
762
  throw error;
@@ -610,19 +782,20 @@ function mountAssistantRoutes(options) {
610
782
  });
611
783
  resolveModel(body.model);
612
784
  const { principal } = context;
613
- const chat = await chatFor(principal);
785
+ const chat = await chatFor(context);
614
786
  const session = await findSession(chat, principal);
615
787
  const thread = await requireOwnThread(chat, principal, session, threadId);
616
788
  if (!session) throw notFound();
617
- const setup = await turnSetup(context, chat, session, thread, body);
618
- return stream(detached(null, runAssistantTurn({
619
- ...setup.turn,
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,
620
793
  resume: {
621
794
  continuationId,
622
795
  results
623
796
  },
624
- onState: recordTurnState(chat, principal, threadId)
625
- })), setup.abort);
797
+ onState: recordTurnState(turnChat, principal, threadId)
798
+ }));
626
799
  };
627
800
  const uploadAttachment = async (context) => {
628
801
  const storage = options.attachments;
@@ -658,8 +831,9 @@ function mountAssistantRoutes(options) {
658
831
  if (!adapter) throw new Error("No data-surface action adapter resolved.");
659
832
  const { principal } = context;
660
833
  const allowedTools = await allowedToolsFor(context);
834
+ const db = await dbFor(context);
661
835
  const actionContext = { principal: {
662
- ...options.db ? { db: options.db } : {},
836
+ ...db ? { db } : {},
663
837
  principal: {
664
838
  runAsUserId: principal.userId,
665
839
  tenantId: principal.tenantId,
@@ -726,7 +900,38 @@ function mountAssistantRoutes(options) {
726
900
  handle
727
901
  };
728
902
  }
729
- function detached(first, events) {
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) {
730
935
  const buffer = first ? [first] : [];
731
936
  let finished = false;
732
937
  let notify = null;
@@ -735,31 +940,34 @@ function detached(first, events) {
735
940
  notify = null;
736
941
  resume?.();
737
942
  };
738
- (async () => {
943
+ const settled = (async () => {
739
944
  try {
740
- for await (const event of events) {
945
+ await produce((event) => {
741
946
  buffer.push(event);
742
947
  wake();
743
- }
948
+ });
744
949
  } catch {} finally {
745
950
  finished = true;
746
951
  wake();
747
952
  }
748
953
  })();
749
- return (async function* () {
750
- for (;;) {
751
- if (buffer.length > 0) {
752
- yield buffer.shift();
753
- continue;
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
+ });
754
965
  }
755
- if (finished) return;
756
- await new Promise((resolve) => {
757
- notify = resolve;
758
- });
759
- }
760
- })();
966
+ })(),
967
+ settled
968
+ };
761
969
  }
762
970
  //#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 };
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 };
764
972
 
765
973
  //# sourceMappingURL=sveltekit.js.map