@btst/stack 4.4.0 → 4.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/README.md +2 -2
  2. package/dist/packages/stack/src/plugins/ai-chat/api/compaction.cjs +64 -24
  3. package/dist/packages/stack/src/plugins/ai-chat/api/compaction.mjs +65 -25
  4. package/dist/packages/stack/src/plugins/ai-chat/api/context-checkpoint.cjs +54 -0
  5. package/dist/packages/stack/src/plugins/ai-chat/api/context-checkpoint.mjs +51 -0
  6. package/dist/packages/stack/src/plugins/ai-chat/api/getters.cjs +2 -0
  7. package/dist/packages/stack/src/plugins/ai-chat/api/getters.mjs +2 -0
  8. package/dist/packages/stack/src/plugins/ai-chat/api/operations.cjs +269 -93
  9. package/dist/packages/stack/src/plugins/ai-chat/api/operations.mjs +270 -94
  10. package/dist/packages/stack/src/plugins/ai-chat/api/plugin.cjs +3 -5
  11. package/dist/packages/stack/src/plugins/ai-chat/api/plugin.mjs +3 -5
  12. package/dist/packages/stack/src/plugins/ai-chat/api/serializers.cjs +1 -0
  13. package/dist/packages/stack/src/plugins/ai-chat/api/serializers.mjs +1 -0
  14. package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-interface.cjs +26 -16
  15. package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-interface.mjs +26 -16
  16. package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-message.cjs +22 -2
  17. package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-message.mjs +22 -2
  18. package/dist/packages/stack/src/plugins/ai-chat/db.cjs +8 -0
  19. package/dist/packages/stack/src/plugins/ai-chat/db.mjs +8 -0
  20. package/dist/packages/stack/src/plugins/ai-chat/history.cjs +47 -0
  21. package/dist/packages/stack/src/plugins/ai-chat/history.mjs +45 -0
  22. package/dist/plugins/ai-chat/api/index.d.cts +2 -2
  23. package/dist/plugins/ai-chat/api/index.d.mts +2 -2
  24. package/dist/plugins/ai-chat/api/index.d.ts +2 -2
  25. package/dist/plugins/ai-chat/client/components/index.d.cts +1 -1
  26. package/dist/plugins/ai-chat/client/components/index.d.mts +1 -1
  27. package/dist/plugins/ai-chat/client/components/index.d.ts +1 -1
  28. package/dist/plugins/ai-chat/client/hooks/index.d.cts +2 -2
  29. package/dist/plugins/ai-chat/client/hooks/index.d.mts +2 -2
  30. package/dist/plugins/ai-chat/client/hooks/index.d.ts +2 -2
  31. package/dist/plugins/ai-chat/client/index.d.cts +3 -3
  32. package/dist/plugins/ai-chat/client/index.d.mts +3 -3
  33. package/dist/plugins/ai-chat/client/index.d.ts +3 -3
  34. package/dist/plugins/ai-chat/query-keys.d.cts +1 -1
  35. package/dist/plugins/ai-chat/query-keys.d.mts +1 -1
  36. package/dist/plugins/ai-chat/query-keys.d.ts +1 -1
  37. package/dist/plugins/blog/api/index.d.cts +2 -2
  38. package/dist/plugins/blog/api/index.d.mts +2 -2
  39. package/dist/plugins/blog/api/index.d.ts +2 -2
  40. package/dist/plugins/blog/client/hooks/index.d.cts +2 -2
  41. package/dist/plugins/blog/client/hooks/index.d.mts +2 -2
  42. package/dist/plugins/blog/client/hooks/index.d.ts +2 -2
  43. package/dist/plugins/blog/client/index.d.cts +2 -2
  44. package/dist/plugins/blog/client/index.d.mts +2 -2
  45. package/dist/plugins/blog/client/index.d.ts +2 -2
  46. package/dist/plugins/blog/query-keys.d.cts +2 -2
  47. package/dist/plugins/blog/query-keys.d.mts +2 -2
  48. package/dist/plugins/blog/query-keys.d.ts +2 -2
  49. package/dist/plugins/form-builder/api/index.d.cts +1 -1
  50. package/dist/plugins/form-builder/api/index.d.mts +1 -1
  51. package/dist/plugins/form-builder/api/index.d.ts +1 -1
  52. package/dist/plugins/form-builder/client/hooks/index.d.cts +1 -1
  53. package/dist/plugins/form-builder/client/hooks/index.d.mts +1 -1
  54. package/dist/plugins/form-builder/client/hooks/index.d.ts +1 -1
  55. package/dist/plugins/form-builder/query-keys.d.cts +1 -1
  56. package/dist/plugins/form-builder/query-keys.d.mts +1 -1
  57. package/dist/plugins/form-builder/query-keys.d.ts +1 -1
  58. package/dist/shared/{stack.CjCSDfWy.d.mts → stack.94kN3pEK.d.mts} +5 -5
  59. package/dist/shared/{stack.C4zspX0T.d.ts → stack.B2EYZyh_.d.ts} +5 -2
  60. package/dist/shared/{stack.BGzgFx3R.d.cts → stack.BTStHVTy.d.cts} +5 -5
  61. package/dist/shared/{stack.DhZtP2JZ.d.mts → stack.BggTHKlo.d.mts} +37 -37
  62. package/dist/shared/{stack.DDQlE2Pm.d.cts → stack.Cei2T2gc.d.cts} +1 -0
  63. package/dist/shared/{stack.DNw6Hv5o.d.cts → stack.CtrnoQgH.d.cts} +37 -37
  64. package/dist/shared/{stack.tAUeRAtd.d.mts → stack.DZRZAzcx.d.mts} +1 -0
  65. package/dist/shared/{stack.Dpg_JjIo.d.ts → stack.DfrRYOIl.d.ts} +5 -5
  66. package/dist/shared/{stack.Ps3MDhTC.d.mts → stack.DivuAnNI.d.mts} +5 -2
  67. package/dist/shared/{stack.DlehgNZZ.d.ts → stack.byjorQi5.d.ts} +1 -0
  68. package/dist/shared/{stack.ymWsP4HZ.d.cts → stack.fF273jws.d.cts} +5 -2
  69. package/dist/shared/{stack.Clil75lW.d.ts → stack.lPKwd_v0.d.ts} +37 -37
  70. package/package.json +1 -1
  71. package/src/plugins/ai-chat/__tests__/authorization.test.ts +56 -30
  72. package/src/plugins/ai-chat/__tests__/classnames-overrides.test.tsx +25 -0
  73. package/src/plugins/ai-chat/__tests__/client-sweep.test.tsx +101 -0
  74. package/src/plugins/ai-chat/__tests__/persisted-compaction.test.ts +819 -0
  75. package/src/plugins/ai-chat/api/compaction.ts +107 -40
  76. package/src/plugins/ai-chat/api/context-checkpoint.ts +66 -0
  77. package/src/plugins/ai-chat/api/getters.ts +2 -0
  78. package/src/plugins/ai-chat/api/operations.ts +361 -99
  79. package/src/plugins/ai-chat/api/plugin.ts +5 -8
  80. package/src/plugins/ai-chat/api/serializers.ts +1 -0
  81. package/src/plugins/ai-chat/client/components/chat-interface.tsx +43 -22
  82. package/src/plugins/ai-chat/client/components/chat-message.tsx +32 -2
  83. package/src/plugins/ai-chat/client/localization/index.ts +1 -0
  84. package/src/plugins/ai-chat/db.ts +8 -0
  85. package/src/plugins/ai-chat/history.ts +68 -0
  86. package/src/plugins/ai-chat/types.ts +2 -0
  87. package/dist/shared/{stack.CGmh-Lpe.d.ts → stack.BuxUQqL8.d.ts} +12 -12
  88. package/dist/shared/{stack.Csi-4Mgz.d.cts → stack.CKc_Bais.d.cts} +7 -7
  89. package/dist/shared/{stack.Csi-4Mgz.d.mts → stack.CKc_Bais.d.mts} +7 -7
  90. package/dist/shared/{stack.Csi-4Mgz.d.ts → stack.CKc_Bais.d.ts} +7 -7
  91. package/dist/shared/{stack.zch70Loj.d.cts → stack.CX83YNuS.d.cts} +12 -12
  92. package/dist/shared/{stack.Doy2_wgl.d.mts → stack.Egr3RWhA.d.mts} +12 -12
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  convertToModelMessages,
3
+ consumeStream,
3
4
  createUIMessageStream,
4
5
  createUIMessageStreamResponse,
5
6
  generateText,
@@ -11,7 +12,7 @@ import {
11
12
  type UIMessage,
12
13
  } from "ai";
13
14
 
14
- /** Opt-in context compaction for public, text-only conversations. */
15
+ /** Opt-in context compaction for text-only conversations. */
15
16
  export interface AiChatCompactionConfig {
16
17
  /** Actual context window. Configure the model's output limit at or below 20% of this. */
17
18
  contextWindowTokens: number;
@@ -210,7 +211,10 @@ export function createStepCompactor({
210
211
  };
211
212
  }
212
213
 
213
- export function compactingChatResponse({
214
+ export type ChatCompletion = { message: UIMessage; interrupted: boolean };
215
+
216
+ /** Stream structured responses, optionally compacting their working context. */
217
+ export function chatResponse({
214
218
  model,
215
219
  messages,
216
220
  summary,
@@ -219,17 +223,30 @@ export function compactingChatResponse({
219
223
  config,
220
224
  abortSignal,
221
225
  onError,
226
+ originalMessages = messages,
227
+ onCheckpoint,
228
+ onFinish,
229
+ generateMessageId,
222
230
  }: {
223
231
  model: LanguageModel;
224
232
  messages: UIMessage[];
225
233
  summary?: string;
226
234
  system?: string;
227
235
  tools?: Record<string, Tool>;
228
- config: AiChatCompactionConfig;
236
+ config?: AiChatCompactionConfig;
229
237
  abortSignal?: AbortSignal;
230
238
  onError: (error: unknown) => Promise<void>;
239
+ originalMessages?: UIMessage[];
240
+ onCheckpoint?: (checkpoint: {
241
+ summary: string;
242
+ throughMessageId: string;
243
+ }) => Promise<void>;
244
+ onFinish?: (completion: ChatCompletion) => Promise<void>;
245
+ generateMessageId?: () => string;
231
246
  }) {
232
- const budget = Math.floor(config.contextWindowTokens * 0.8);
247
+ const budget = config
248
+ ? Math.floor(config.contextWindowTokens * 0.8)
249
+ : Infinity;
233
250
  const overhead = [
234
251
  system,
235
252
  Object.entries(tools ?? {}).map(([name, value]) => [
@@ -238,9 +255,34 @@ export function compactingChatResponse({
238
255
  value.inputSchema,
239
256
  ]),
240
257
  ];
258
+ let failed = false;
259
+ let generationStarted = false;
260
+ const reportError = async (error: unknown) => {
261
+ if (failed) return;
262
+ failed = true;
263
+ await onError(error);
264
+ };
241
265
  const stream = createUIMessageStream({
242
- originalMessages: messages,
243
- onError: streamErrorMessage,
266
+ originalMessages,
267
+ generateId: generateMessageId,
268
+ onError: (error) => {
269
+ void reportError(error);
270
+ return streamErrorMessage(error);
271
+ },
272
+ onFinish: onFinish
273
+ ? async ({ responseMessage, isAborted, finishReason }) => {
274
+ if (!generationStarted) return;
275
+ await onFinish({
276
+ message: responseMessage,
277
+ interrupted:
278
+ failed ||
279
+ isAborted ||
280
+ Boolean(abortSignal?.aborted) ||
281
+ !finishReason ||
282
+ finishReason === "error",
283
+ });
284
+ }
285
+ : undefined,
244
286
  execute: async ({ writer }) => {
245
287
  const onCompacting = (active: boolean) =>
246
288
  writer.write({
@@ -250,50 +292,65 @@ export function compactingChatResponse({
250
292
  });
251
293
  const summarize = createSummarizer(
252
294
  model,
253
- config.contextWindowTokens,
295
+ config?.contextWindowTokens ?? 4096,
254
296
  abortSignal,
255
297
  );
256
298
  try {
257
- const compacted = await compactConversation({
258
- messages,
259
- summary,
260
- budget,
261
- overhead,
262
- summarize: async (history) => {
263
- onCompacting(true);
264
- try {
265
- return await summarize(history);
266
- } finally {
267
- onCompacting(false);
268
- }
269
- },
270
- });
271
- if (compacted.throughMessageId)
272
- writer.write({
273
- type: "data-context-checkpoint",
274
- data: {
275
- summary: compacted.summary,
276
- throughMessageId: compacted.throughMessageId,
277
- },
278
- transient: true,
279
- });
299
+ const compacted = config
300
+ ? await compactConversation({
301
+ messages,
302
+ summary,
303
+ budget,
304
+ overhead,
305
+ summarize: async (history) => {
306
+ onCompacting(true);
307
+ try {
308
+ return await summarize(history);
309
+ } finally {
310
+ onCompacting(false);
311
+ }
312
+ },
313
+ })
314
+ : { messages, summary, throughMessageId: undefined };
315
+ if (compacted.throughMessageId && compacted.summary) {
316
+ const checkpoint = {
317
+ summary: compacted.summary,
318
+ throughMessageId: compacted.throughMessageId,
319
+ };
320
+ if (onCheckpoint) await onCheckpoint(checkpoint);
321
+ else
322
+ writer.write({
323
+ type: "data-context-checkpoint",
324
+ data: {
325
+ summary: compacted.summary,
326
+ throughMessageId: compacted.throughMessageId,
327
+ },
328
+ transient: true,
329
+ });
330
+ }
331
+ abortSignal?.throwIfAborted();
332
+ generationStarted = true;
280
333
  const result = streamText({
281
334
  model,
282
335
  messages: [
283
336
  ...(system ? [{ role: "system" as const, content: system }] : []),
284
337
  ...(compacted.summary ? [summaryMessage(compacted.summary)] : []),
285
- ...(await convertToModelMessages(compacted.messages)),
338
+ ...(await convertToModelMessages(compacted.messages, {
339
+ ignoreIncompleteToolCalls: true,
340
+ })),
286
341
  ],
287
342
  tools,
288
343
  ...(tools ? { stopWhen: stepCountIs(5) } : {}),
289
- prepareStep: createStepCompactor({
290
- budget,
291
- overhead,
292
- summarize,
293
- onCompacting,
294
- }),
344
+ prepareStep: config
345
+ ? createStepCompactor({
346
+ budget,
347
+ overhead,
348
+ summarize,
349
+ onCompacting,
350
+ })
351
+ : undefined,
295
352
  abortSignal,
296
- onError: ({ error }) => onError(error),
353
+ onError: ({ error }) => reportError(error),
297
354
  });
298
355
  writer.merge(
299
356
  result.toUIMessageStream({
@@ -301,10 +358,20 @@ export function compactingChatResponse({
301
358
  }),
302
359
  );
303
360
  } catch (error) {
304
- await onError(error);
361
+ await reportError(error);
305
362
  throw error;
306
363
  }
307
364
  },
308
365
  });
309
- return createUIMessageStreamResponse({ stream });
366
+ return createUIMessageStreamResponse({
367
+ stream,
368
+ // Keep draining after a browser disconnect so abort/error completion can
369
+ // save the structured partial response. The request signal stops the model.
370
+ ...(onFinish
371
+ ? {
372
+ consumeSseStream: ({ stream }: { stream: ReadableStream<string> }) =>
373
+ consumeStream({ stream }),
374
+ }
375
+ : {}),
376
+ });
310
377
  }
@@ -0,0 +1,66 @@
1
+ import { createHash } from "node:crypto";
2
+ import { z } from "zod";
3
+ import type { Message } from "../types";
4
+
5
+ const checkpointSchema = z.object({
6
+ version: z.literal(1),
7
+ summary: z.string().min(1).max(32_000),
8
+ throughMessageId: z.string(),
9
+ prefixHash: z.string(),
10
+ });
11
+
12
+ function prefixHash(messages: readonly Message[]) {
13
+ return createHash("sha256")
14
+ .update(
15
+ JSON.stringify(
16
+ messages.map(({ id, role, content, interrupted }) => ({
17
+ id,
18
+ role,
19
+ content,
20
+ interrupted: Boolean(interrupted),
21
+ })),
22
+ ),
23
+ )
24
+ .digest("hex");
25
+ }
26
+
27
+ /** Checkpoints cover committed database messages, never browser-supplied history. */
28
+ export function createContextCheckpoint(
29
+ messages: readonly Message[],
30
+ summary: string,
31
+ throughMessageId: string,
32
+ ): string {
33
+ const boundary = messages.findIndex(
34
+ (message) => message.id === throughMessageId,
35
+ );
36
+ if (boundary < 0) throw new Error("Checkpoint boundary is not persisted.");
37
+ return JSON.stringify(
38
+ checkpointSchema.parse({
39
+ version: 1,
40
+ summary,
41
+ throughMessageId,
42
+ prefixHash: prefixHash(messages.slice(0, boundary + 1)),
43
+ }),
44
+ );
45
+ }
46
+
47
+ export function readContextCheckpoint(
48
+ value: string | null | undefined,
49
+ messages: readonly Message[],
50
+ ) {
51
+ if (!value) return undefined;
52
+ try {
53
+ const checkpoint = checkpointSchema.parse(JSON.parse(value));
54
+ const boundary = messages.findIndex(
55
+ (message) => message.id === checkpoint.throughMessageId,
56
+ );
57
+ if (
58
+ boundary < 0 ||
59
+ prefixHash(messages.slice(0, boundary + 1)) !== checkpoint.prefixHash
60
+ )
61
+ return undefined;
62
+ return { ...checkpoint, boundary };
63
+ } catch {
64
+ return undefined;
65
+ }
66
+ }
@@ -28,6 +28,7 @@ export async function getAllConversations(
28
28
 
29
29
  return adapter.findMany<Conversation>({
30
30
  model: "conversation",
31
+ select: ["id", "userId", "title", "createdAt", "updatedAt"],
31
32
  where: whereConditions.length > 0 ? whereConditions : undefined,
32
33
  sortBy: { field: "updatedAt", direction: "desc" },
33
34
  });
@@ -47,6 +48,7 @@ export async function getConversationById(
47
48
  ): Promise<(Conversation & { messages: Message[] }) | null> {
48
49
  const conversations = await adapter.findMany<ConversationWithMessages>({
49
50
  model: "conversation",
51
+ select: ["id", "userId", "title", "createdAt", "updatedAt"],
50
52
  where: [{ field: "id", value: id, operator: "eq" as const }],
51
53
  limit: 1,
52
54
  join: {