@btst/stack 4.3.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 (120) hide show
  1. package/README.md +2 -2
  2. package/dist/packages/stack/src/plugins/ai-chat/api/compaction.cjs +289 -0
  3. package/dist/packages/stack/src/plugins/ai-chat/api/compaction.mjs +281 -0
  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 +285 -89
  9. package/dist/packages/stack/src/plugins/ai-chat/api/operations.mjs +285 -89
  10. package/dist/packages/stack/src/plugins/ai-chat/api/plugin.cjs +6 -3
  11. package/dist/packages/stack/src/plugins/ai-chat/api/plugin.mjs +6 -3
  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 +62 -17
  15. package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-interface.mjs +62 -17
  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/client/context-checkpoint.cjs +30 -0
  19. package/dist/packages/stack/src/plugins/ai-chat/client/context-checkpoint.mjs +27 -0
  20. package/dist/packages/stack/src/plugins/ai-chat/db.cjs +8 -0
  21. package/dist/packages/stack/src/plugins/ai-chat/db.mjs +8 -0
  22. package/dist/packages/stack/src/plugins/ai-chat/history.cjs +47 -0
  23. package/dist/packages/stack/src/plugins/ai-chat/history.mjs +45 -0
  24. package/dist/packages/stack/src/plugins/ai-chat/schemas.cjs +2 -0
  25. package/dist/packages/stack/src/plugins/ai-chat/schemas.mjs +2 -0
  26. package/dist/packages/ui/src/components/markdown-content.cjs +9 -9
  27. package/dist/packages/ui/src/components/markdown-content.mjs +9 -9
  28. package/dist/plugins/ai-chat/api/index.d.cts +2 -2
  29. package/dist/plugins/ai-chat/api/index.d.mts +2 -2
  30. package/dist/plugins/ai-chat/api/index.d.ts +2 -2
  31. package/dist/plugins/ai-chat/client/components/index.d.cts +1 -1
  32. package/dist/plugins/ai-chat/client/components/index.d.mts +1 -1
  33. package/dist/plugins/ai-chat/client/components/index.d.ts +1 -1
  34. package/dist/plugins/ai-chat/client/hooks/index.d.cts +2 -2
  35. package/dist/plugins/ai-chat/client/hooks/index.d.mts +2 -2
  36. package/dist/plugins/ai-chat/client/hooks/index.d.ts +2 -2
  37. package/dist/plugins/ai-chat/client/index.d.cts +3 -3
  38. package/dist/plugins/ai-chat/client/index.d.mts +3 -3
  39. package/dist/plugins/ai-chat/client/index.d.ts +3 -3
  40. package/dist/plugins/ai-chat/query-keys.d.cts +1 -1
  41. package/dist/plugins/ai-chat/query-keys.d.mts +1 -1
  42. package/dist/plugins/ai-chat/query-keys.d.ts +1 -1
  43. package/dist/plugins/blog/api/index.d.cts +2 -2
  44. package/dist/plugins/blog/api/index.d.mts +2 -2
  45. package/dist/plugins/blog/api/index.d.ts +2 -2
  46. package/dist/plugins/blog/client/hooks/index.d.cts +2 -2
  47. package/dist/plugins/blog/client/hooks/index.d.mts +2 -2
  48. package/dist/plugins/blog/client/hooks/index.d.ts +2 -2
  49. package/dist/plugins/blog/client/index.d.cts +2 -2
  50. package/dist/plugins/blog/client/index.d.mts +2 -2
  51. package/dist/plugins/blog/client/index.d.ts +2 -2
  52. package/dist/plugins/blog/query-keys.d.cts +2 -2
  53. package/dist/plugins/blog/query-keys.d.mts +2 -2
  54. package/dist/plugins/blog/query-keys.d.ts +2 -2
  55. package/dist/plugins/cms/api/index.d.cts +1 -1
  56. package/dist/plugins/cms/api/index.d.mts +1 -1
  57. package/dist/plugins/cms/api/index.d.ts +1 -1
  58. package/dist/plugins/cms/query-keys.d.cts +1 -1
  59. package/dist/plugins/cms/query-keys.d.mts +1 -1
  60. package/dist/plugins/cms/query-keys.d.ts +1 -1
  61. package/dist/plugins/form-builder/api/index.d.cts +1 -1
  62. package/dist/plugins/form-builder/api/index.d.mts +1 -1
  63. package/dist/plugins/form-builder/api/index.d.ts +1 -1
  64. package/dist/plugins/form-builder/client/hooks/index.d.cts +1 -1
  65. package/dist/plugins/form-builder/client/hooks/index.d.mts +1 -1
  66. package/dist/plugins/form-builder/client/hooks/index.d.ts +1 -1
  67. package/dist/plugins/form-builder/query-keys.d.cts +1 -1
  68. package/dist/plugins/form-builder/query-keys.d.mts +1 -1
  69. package/dist/plugins/form-builder/query-keys.d.ts +1 -1
  70. package/dist/plugins/ui-builder/client/hooks/index.d.cts +1 -1
  71. package/dist/plugins/ui-builder/client/hooks/index.d.mts +1 -1
  72. package/dist/plugins/ui-builder/client/hooks/index.d.ts +1 -1
  73. package/dist/plugins/ui-builder/client/index.d.cts +1 -1
  74. package/dist/plugins/ui-builder/client/index.d.mts +1 -1
  75. package/dist/plugins/ui-builder/client/index.d.ts +1 -1
  76. package/dist/plugins/ui-builder/query-keys.d.cts +1 -1
  77. package/dist/plugins/ui-builder/query-keys.d.mts +1 -1
  78. package/dist/plugins/ui-builder/query-keys.d.ts +1 -1
  79. package/dist/shared/{stack.D2y4gl03.d.mts → stack.94kN3pEK.d.mts} +5 -5
  80. package/dist/shared/{stack.DBH1Bjko.d.ts → stack.B2EYZyh_.d.ts} +16 -2
  81. package/dist/shared/{stack.rmWRtSjs.d.cts → stack.BTStHVTy.d.cts} +5 -5
  82. package/dist/shared/{stack.CZ1h_T4k.d.mts → stack.BggTHKlo.d.mts} +90 -90
  83. package/dist/shared/{stack.DzJgoNqJ.d.cts → stack.Cei2T2gc.d.cts} +2 -0
  84. package/dist/shared/{stack.DiYg7Jw1.d.cts → stack.CtrnoQgH.d.cts} +90 -90
  85. package/dist/shared/{stack.CHrtGKLz.d.mts → stack.DZRZAzcx.d.mts} +2 -0
  86. package/dist/shared/{stack.CnDMBj4z.d.ts → stack.DfrRYOIl.d.ts} +5 -5
  87. package/dist/shared/{stack.B7Mj74BQ.d.mts → stack.DivuAnNI.d.mts} +16 -2
  88. package/dist/shared/{stack.qMp2VzNw.d.ts → stack.byjorQi5.d.ts} +2 -0
  89. package/dist/shared/{stack.Dgt3MCtW.d.cts → stack.fF273jws.d.cts} +16 -2
  90. package/dist/shared/{stack.DQpyA3Cq.d.ts → stack.lPKwd_v0.d.ts} +90 -90
  91. package/package.json +1 -1
  92. package/src/__tests__/markdown-link-navigation.test.tsx +29 -0
  93. package/src/plugins/ai-chat/__tests__/authorization.test.ts +56 -30
  94. package/src/plugins/ai-chat/__tests__/classnames-overrides.test.tsx +25 -0
  95. package/src/plugins/ai-chat/__tests__/client-sweep.test.tsx +101 -0
  96. package/src/plugins/ai-chat/__tests__/compaction.test.ts +272 -0
  97. package/src/plugins/ai-chat/__tests__/persisted-compaction.test.ts +819 -0
  98. package/src/plugins/ai-chat/api/compaction.ts +377 -0
  99. package/src/plugins/ai-chat/api/context-checkpoint.ts +66 -0
  100. package/src/plugins/ai-chat/api/getters.ts +2 -0
  101. package/src/plugins/ai-chat/api/operations.ts +380 -93
  102. package/src/plugins/ai-chat/api/plugin.ts +15 -4
  103. package/src/plugins/ai-chat/api/serializers.ts +1 -0
  104. package/src/plugins/ai-chat/client/components/chat-interface.tsx +97 -21
  105. package/src/plugins/ai-chat/client/components/chat-message.tsx +32 -2
  106. package/src/plugins/ai-chat/client/context-checkpoint.ts +47 -0
  107. package/src/plugins/ai-chat/client/localization/index.ts +2 -0
  108. package/src/plugins/ai-chat/db.ts +8 -0
  109. package/src/plugins/ai-chat/history.ts +68 -0
  110. package/src/plugins/ai-chat/schemas.ts +2 -0
  111. package/src/plugins/ai-chat/types.ts +2 -0
  112. package/dist/shared/{stack.lsbdKRj7.d.mts → stack.B8PG21zX.d.mts} +16 -16
  113. package/dist/shared/{stack.CGmh-Lpe.d.ts → stack.BuxUQqL8.d.ts} +12 -12
  114. package/dist/shared/{stack.DtZgEX84.d.cts → stack.CKc_Bais.d.cts} +6 -6
  115. package/dist/shared/{stack.DtZgEX84.d.mts → stack.CKc_Bais.d.mts} +6 -6
  116. package/dist/shared/{stack.DtZgEX84.d.ts → stack.CKc_Bais.d.ts} +6 -6
  117. package/dist/shared/{stack.zch70Loj.d.cts → stack.CX83YNuS.d.cts} +12 -12
  118. package/dist/shared/{stack.BMlTZnZp.d.ts → stack.CyalJ0j6.d.ts} +16 -16
  119. package/dist/shared/{stack.BV0uNV4f.d.cts → stack.D6Pg-ufX.d.cts} +16 -16
  120. package/dist/shared/{stack.Doy2_wgl.d.mts → stack.Egr3RWhA.d.mts} +12 -12
@@ -1,5 +1,7 @@
1
1
  "use client";
2
2
 
3
+ import { persistedHistory } from "../../history";
4
+
3
5
  import { useChat } from "@ai-sdk/react";
4
6
  import {
5
7
  useEffect,
@@ -46,6 +48,11 @@ import {
46
48
  import { usePageAIContext } from "../context/page-ai-context";
47
49
  import { useChatAnalytics } from "../context/chat-analytics";
48
50
  import { navigateAiChatCrossOrigin } from "../navigation";
51
+ import {
52
+ captureContextCheckpoint,
53
+ contextRequest,
54
+ type ContextCheckpoint,
55
+ } from "../context-checkpoint";
49
56
 
50
57
  interface ChatInterfaceProps {
51
58
  initialMessages?: UIMessage[];
@@ -89,30 +96,28 @@ function reconcilePersistedMessageIds(
89
96
  return messages.map((message) => ({
90
97
  ...message,
91
98
  id: persistedIds.get(message) ?? message.id,
99
+ ...(persistedMessages.some(
100
+ (saved) => saved.id === message.id && saved.interrupted,
101
+ )
102
+ ? { metadata: { ...(message.metadata as object), interrupted: true } }
103
+ : {}),
92
104
  }));
93
105
  }
94
106
 
95
107
  function persistedMessagesToUiMessages(
96
108
  messages: readonly SerializedMessage[],
97
109
  ): UIMessage[] {
98
- return messages
99
- .filter((message) => message.role !== "data")
100
- .map((message) => {
101
- let parts: UIMessage["parts"];
102
- try {
103
- const parsed = JSON.parse(message.content);
104
- parts = Array.isArray(parsed)
105
- ? parsed
106
- : [{ type: "text" as const, text: message.content }];
107
- } catch {
108
- parts = [{ type: "text" as const, text: message.content }];
109
- }
110
- return {
111
- id: message.id,
112
- role: message.role as "user" | "assistant" | "system",
113
- parts,
114
- };
115
- });
110
+ return persistedHistory(messages).map((message) => {
111
+ // Browser tool callbacks do not resume when saved history is restored.
112
+ const abandoned = message.parts.some(
113
+ (part) =>
114
+ "toolCallId" in part &&
115
+ (part.state === "input-streaming" || part.state === "input-available"),
116
+ );
117
+ return abandoned
118
+ ? { ...message, metadata: { interrupted: true } }
119
+ : message;
120
+ });
116
121
  }
117
122
 
118
123
  function ChatActionCheck({
@@ -314,6 +319,9 @@ export function ChatInterface({
314
319
  Array<{ generation: number; identityPartitionKey: string }>
315
320
  >([]);
316
321
  const latestMessages = useRef<UIMessage[]>([]);
322
+ const contextCheckpoint = useRef<ContextCheckpoint | undefined>(undefined);
323
+ const submittedMessages = useRef<UIMessage[]>([]);
324
+ const [isCompacting, setIsCompacting] = useState(false);
317
325
 
318
326
  const conversationsListQueryKey = useMemo(() => {
319
327
  // In public mode, we don't need conversation queries
@@ -465,6 +473,7 @@ export function ChatInterface({
465
473
  pendingStreamRequests.current.push(request);
466
474
  activeStreamPartitionKey.current = request.identityPartitionKey;
467
475
  }
476
+ setIsCompacting(false);
468
477
  const currentPageContext = pageAIContextRef.current;
469
478
 
470
479
  // Build page context fields to include in every request
@@ -483,20 +492,26 @@ export function ChatInterface({
483
492
  if (newUserMessage) {
484
493
  messagesToSend.push(newUserMessage);
485
494
  }
495
+ submittedMessages.current = messagesToSend;
486
496
  // Clear the ref after use
487
497
  editMessagesRef.current = null;
488
498
  return {
489
499
  body: {
490
- messages: messagesToSend,
500
+ ...(isPublicMode
501
+ ? contextRequest(messagesToSend, contextCheckpoint.current)
502
+ : { messages: messagesToSend }),
491
503
  conversationId: conversationIdRef.current,
492
504
  ...pageContextBody,
493
505
  },
494
506
  };
495
507
  }
496
- // Normal case - use the messages as-is
508
+ submittedMessages.current = hookMessages;
509
+ // Retain the full UI history; compact only the transport payload.
497
510
  return {
498
511
  body: {
499
- messages: hookMessages,
512
+ ...(isPublicMode
513
+ ? contextRequest(hookMessages, contextCheckpoint.current)
514
+ : { messages: hookMessages }),
500
515
  conversationId: conversationIdRef.current,
501
516
  ...pageContextBody,
502
517
  },
@@ -550,6 +565,33 @@ export function ChatInterface({
550
565
  } = useChat({
551
566
  id: `${chatInstanceId}:${isPublicMode ? "public" : identitySessionVersion}`,
552
567
  transport,
568
+ onData: ({ type, data }) => {
569
+ if (!mounted.current || !data || typeof data !== "object") return;
570
+ if (
571
+ !isPublicMode &&
572
+ (identitySessionVersion !== identitySessionGeneration.current ||
573
+ activeStreamPartitionKey.current !==
574
+ latestIdentityPartitionKey.current)
575
+ )
576
+ return;
577
+ if (type === "data-context-status" && "compacting" in data) {
578
+ setIsCompacting(data.compacting === true);
579
+ }
580
+ if (
581
+ isPublicMode &&
582
+ type === "data-context-checkpoint" &&
583
+ "summary" in data &&
584
+ "throughMessageId" in data &&
585
+ typeof data.summary === "string" &&
586
+ typeof data.throughMessageId === "string"
587
+ ) {
588
+ contextCheckpoint.current = captureContextCheckpoint(
589
+ submittedMessages.current,
590
+ data.summary,
591
+ data.throughMessageId,
592
+ );
593
+ }
594
+ },
553
595
  // Automatically resubmit after all client-side tool results are provided
554
596
  sendAutomaticallyWhen: (options) =>
555
597
  mounted.current &&
@@ -630,6 +672,7 @@ export function ChatInterface({
630
672
  }
631
673
  },
632
674
  onError: (err) => {
675
+ setIsCompacting(false);
633
676
  if (!mounted.current) return;
634
677
  console.error("useChat onError:", err);
635
678
  if (
@@ -653,6 +696,7 @@ export function ChatInterface({
653
696
  // the provider or response stream failed.
654
697
  },
655
698
  onFinish: async (completion) => {
699
+ setIsCompacting(false);
656
700
  if (
657
701
  !mounted.current ||
658
702
  (!isPublicMode &&
@@ -684,6 +728,25 @@ export function ChatInterface({
684
728
  ).length,
685
729
  });
686
730
  }
731
+ if (
732
+ completion?.isAbort ||
733
+ completion?.isError ||
734
+ completion?.isDisconnect
735
+ ) {
736
+ setMessages((current) =>
737
+ current.map((message) =>
738
+ message.id === completion.message.id
739
+ ? {
740
+ ...message,
741
+ metadata: {
742
+ ...(message.metadata as object),
743
+ interrupted: true,
744
+ },
745
+ }
746
+ : message,
747
+ ),
748
+ );
749
+ }
687
750
  // In public mode, skip all persistence-related operations
688
751
  if (isPublicMode) {
689
752
  activeStreamPartitionKey.current = undefined;
@@ -1328,7 +1391,20 @@ export function ChatInterface({
1328
1391
  />
1329
1392
  ))
1330
1393
  )}
1394
+ {isLoading && isCompacting && (
1395
+ <div
1396
+ role="status"
1397
+ className="text-muted-foreground text-sm py-4"
1398
+ >
1399
+ {tr(
1400
+ "CHAT_COMPACTING",
1401
+ "aiChat.chat.compacting",
1402
+ "Summarizing earlier conversation…",
1403
+ )}
1404
+ </div>
1405
+ )}
1331
1406
  {isLoading &&
1407
+ !isCompacting &&
1332
1408
  messages[messages.length - 1]?.role !== "assistant" && (
1333
1409
  <div className="flex items-center gap-2 text-muted-foreground text-sm py-4">
1334
1410
  <div className="animate-pulse">
@@ -132,6 +132,12 @@ export function ChatMessage({
132
132
  isRetrying = false,
133
133
  }: ChatMessageProps) {
134
134
  const isUser = message.role === "user";
135
+ const interrupted = Boolean(
136
+ message.metadata &&
137
+ typeof message.metadata === "object" &&
138
+ "interrupted" in message.metadata &&
139
+ message.metadata.interrupted,
140
+ );
135
141
 
136
142
  const {
137
143
  localization: customLocalization,
@@ -479,10 +485,22 @@ export function ChatMessage({
479
485
  {toolParts.map((part: any) => {
480
486
  const toolName = getToolName(part);
481
487
  const toolCallId = part.toolCallId;
482
- const state = part.state as ToolCallState;
488
+ const unfinished =
489
+ interrupted &&
490
+ (part.state === "input-streaming" ||
491
+ part.state === "input-available");
492
+ const state = (
493
+ unfinished ? "output-error" : part.state
494
+ ) as ToolCallState;
483
495
  const input = part.input;
484
496
  const output = part.output;
485
- const errorText = part.errorText;
497
+ const errorText = unfinished
498
+ ? tr(
499
+ "CHAT_INTERRUPTED",
500
+ "aiChat.interrupted",
501
+ "Response interrupted",
502
+ )
503
+ : part.errorText;
486
504
  const isLoading =
487
505
  state === "input-streaming" ||
488
506
  state === "input-available";
@@ -513,6 +531,18 @@ export function ChatMessage({
513
531
  </div>
514
532
  )}
515
533
  {/* Text content */}
534
+ {interrupted && !isStreaming && (
535
+ <p
536
+ className="text-xs text-muted-foreground"
537
+ data-testid="chat-interrupted"
538
+ >
539
+ {tr(
540
+ "CHAT_INTERRUPTED",
541
+ "aiChat.interrupted",
542
+ "Response interrupted",
543
+ )}
544
+ </p>
545
+ )}
516
546
  {displayContent ? (
517
547
  <MarkdownContent
518
548
  markdown={displayContent}
@@ -0,0 +1,47 @@
1
+ import type { UIMessage } from "ai";
2
+
3
+ /** Browser-only snapshot; never replaces or mutates the visible transcript. */
4
+ export interface ContextCheckpoint {
5
+ summary: string;
6
+ throughMessageId: string;
7
+ prefix: string;
8
+ }
9
+
10
+ export function captureContextCheckpoint(
11
+ messages: UIMessage[],
12
+ summary: string,
13
+ throughMessageId: string,
14
+ ): ContextCheckpoint | undefined {
15
+ const index = messages.findIndex(
16
+ (message) => message.id === throughMessageId,
17
+ );
18
+ if (index < 0) return undefined;
19
+ return {
20
+ summary,
21
+ throughMessageId,
22
+ prefix: JSON.stringify(messages.slice(0, index + 1)),
23
+ };
24
+ }
25
+
26
+ export function contextRequest(
27
+ messages: UIMessage[],
28
+ checkpoint?: ContextCheckpoint,
29
+ ) {
30
+ if (checkpoint) {
31
+ const index = messages.findIndex(
32
+ (message) => message.id === checkpoint.throughMessageId,
33
+ );
34
+ if (
35
+ index >= 0 &&
36
+ JSON.stringify(messages.slice(0, index + 1)) === checkpoint.prefix
37
+ ) {
38
+ return {
39
+ messages: messages.slice(index + 1),
40
+ contextSummary: checkpoint.summary,
41
+ };
42
+ }
43
+ }
44
+ // Editing or retrying a summarized turn invalidates the snapshot. Resend
45
+ // the original transcript so the server builds a fresh summary if needed.
46
+ return { messages };
47
+ }
@@ -12,6 +12,8 @@ export interface AiChatLocalization {
12
12
  CHAT_SEND_BUTTON: string;
13
13
  CHAT_EMPTY_STATE: string;
14
14
  CHAT_LOADING: string;
15
+ CHAT_COMPACTING?: string;
16
+ CHAT_INTERRUPTED?: string;
15
17
  CHAT_ERROR: string;
16
18
  CHAT_RETRY: string;
17
19
 
@@ -21,6 +21,10 @@ export const aiChatSchema = createDbPlugin("aiChat", {
21
21
  type: "string",
22
22
  required: true,
23
23
  },
24
+ contextCheckpoint: {
25
+ type: "string",
26
+ required: false,
27
+ },
24
28
  createdAt: {
25
29
  type: "date",
26
30
  defaultValue: () => new Date(),
@@ -56,6 +60,10 @@ export const aiChatSchema = createDbPlugin("aiChat", {
56
60
  type: "string",
57
61
  required: true,
58
62
  },
63
+ interrupted: {
64
+ type: "boolean",
65
+ required: false,
66
+ },
59
67
  createdAt: {
60
68
  type: "date",
61
69
  defaultValue: () => new Date(),
@@ -0,0 +1,68 @@
1
+ import type { UIMessage } from "ai";
2
+ import type { Message } from "./types";
3
+
4
+ function isMessagePart(value: unknown): boolean {
5
+ if (!value || typeof value !== "object" || !("type" in value)) return false;
6
+ const part = value as Record<string, unknown>;
7
+ if (typeof part.type !== "string") return false;
8
+ switch (part.type) {
9
+ case "text":
10
+ case "reasoning":
11
+ return typeof part.text === "string";
12
+ case "file":
13
+ return typeof part.url === "string" && typeof part.mediaType === "string";
14
+ case "source-url":
15
+ return typeof part.sourceId === "string" && typeof part.url === "string";
16
+ case "source-document":
17
+ return (
18
+ typeof part.sourceId === "string" &&
19
+ typeof part.mediaType === "string" &&
20
+ typeof part.title === "string"
21
+ );
22
+ case "step-start":
23
+ return true;
24
+ default:
25
+ if (part.type.startsWith("data-")) return "data" in part;
26
+ return (
27
+ (part.type.startsWith("tool-") ||
28
+ (part.type === "dynamic-tool" &&
29
+ typeof part.toolName === "string")) &&
30
+ typeof part.toolCallId === "string" &&
31
+ [
32
+ "input-streaming",
33
+ "input-available",
34
+ "output-available",
35
+ "output-error",
36
+ ].includes(String(part.state))
37
+ );
38
+ }
39
+ }
40
+
41
+ export function persistedHistory(
42
+ messages: readonly Pick<Message, "id" | "role" | "content" | "interrupted">[],
43
+ ): UIMessage[] {
44
+ return messages
45
+ .filter((message) => message.role !== "data")
46
+ .map((message) => {
47
+ let parts: UIMessage["parts"];
48
+ try {
49
+ const parsed = JSON.parse(message.content);
50
+ // The old completion writer saved [] for empty assistant replies.
51
+ // Empty arrays in legacy user text must remain literal text.
52
+ parts =
53
+ Array.isArray(parsed) &&
54
+ (parsed.length > 0 || message.role === "assistant") &&
55
+ parsed.every(isMessagePart)
56
+ ? parsed
57
+ : [{ type: "text", text: message.content }];
58
+ } catch {
59
+ parts = [{ type: "text", text: message.content }];
60
+ }
61
+ return {
62
+ id: message.id,
63
+ role: message.role as UIMessage["role"],
64
+ parts,
65
+ ...(message.interrupted ? { metadata: { interrupted: true } } : {}),
66
+ };
67
+ });
68
+ }
@@ -36,6 +36,8 @@ export const chatRequestSchema = z.object({
36
36
  ]),
37
37
  ),
38
38
  conversationId: z.string().optional(),
39
+ /** Untrusted continuation summary for opt-in public context compaction. */
40
+ contextSummary: z.string().max(32_000).optional(),
39
41
  model: z.string().optional(),
40
42
  /**
41
43
  * Description of the current page context, injected into the AI system prompt.
@@ -15,6 +15,8 @@ export type Message = {
15
15
  conversationId: string;
16
16
  role: "system" | "user" | "assistant" | "data";
17
17
  content: string;
18
+ /** True when generation stopped before a successful completion. */
19
+ interrupted?: boolean | null;
18
20
  createdAt: Date;
19
21
  };
20
22
 
@@ -95,8 +95,8 @@ declare const cmsBackendPlugin: (config: CMSBackendConfig) => _btst_stack_plugin
95
95
  readonly id: string;
96
96
  readonly slug: string;
97
97
  readonly contentTypeId: string;
98
- readonly data: string;
99
98
  readonly authorId?: string | undefined;
99
+ readonly data: string;
100
100
  }[];
101
101
  readonly total: number;
102
102
  }>;
@@ -128,8 +128,8 @@ declare const cmsBackendPlugin: (config: CMSBackendConfig) => _btst_stack_plugin
128
128
  readonly id: string;
129
129
  readonly slug: string;
130
130
  readonly contentTypeId: string;
131
- readonly data: string;
132
131
  readonly authorId?: string | undefined;
132
+ readonly data: string;
133
133
  }>;
134
134
  readonly createContentItem: better_call.StrictEndpoint<"/content/:typeSlug", {
135
135
  params: z.ZodObject<{
@@ -165,8 +165,8 @@ declare const cmsBackendPlugin: (config: CMSBackendConfig) => _btst_stack_plugin
165
165
  readonly id: string;
166
166
  readonly slug: string;
167
167
  readonly contentTypeId: string;
168
- readonly data: string;
169
168
  readonly authorId?: string | undefined;
169
+ readonly data: string;
170
170
  }>;
171
171
  readonly updateContentItem: better_call.StrictEndpoint<"/content/:typeSlug/:id", {
172
172
  params: z.ZodObject<{
@@ -203,8 +203,8 @@ declare const cmsBackendPlugin: (config: CMSBackendConfig) => _btst_stack_plugin
203
203
  readonly id: string;
204
204
  readonly slug: string;
205
205
  readonly contentTypeId: string;
206
- readonly data: string;
207
206
  readonly authorId?: string | undefined;
207
+ readonly data: string;
208
208
  }>;
209
209
  readonly deleteContentItem: better_call.StrictEndpoint<"/content/:typeSlug/:id", {
210
210
  params: z.ZodObject<{
@@ -248,8 +248,8 @@ declare const cmsBackendPlugin: (config: CMSBackendConfig) => _btst_stack_plugin
248
248
  readonly id: string;
249
249
  readonly slug: string;
250
250
  readonly contentTypeId: string;
251
- readonly data: string;
252
251
  readonly authorId?: string | undefined;
252
+ readonly data: string;
253
253
  }[];
254
254
  };
255
255
  readonly contentType?: {
@@ -271,8 +271,8 @@ declare const cmsBackendPlugin: (config: CMSBackendConfig) => _btst_stack_plugin
271
271
  readonly id: string;
272
272
  readonly slug: string;
273
273
  readonly contentTypeId: string;
274
- readonly data: string;
275
274
  readonly authorId?: string | undefined;
275
+ readonly data: string;
276
276
  }>;
277
277
  readonly listContentByRelation: better_call.StrictEndpoint<"/content/:typeSlug/by-relation", {
278
278
  params: z.ZodObject<{
@@ -314,8 +314,8 @@ declare const cmsBackendPlugin: (config: CMSBackendConfig) => _btst_stack_plugin
314
314
  readonly id: string;
315
315
  readonly slug: string;
316
316
  readonly contentTypeId: string;
317
- readonly data: string;
318
317
  readonly authorId?: string | undefined;
318
+ readonly data: string;
319
319
  }[];
320
320
  readonly total: number;
321
321
  readonly limit: number;
@@ -383,8 +383,8 @@ declare const cmsBackendPlugin: (config: CMSBackendConfig) => _btst_stack_plugin
383
383
  readonly id: string;
384
384
  readonly slug: string;
385
385
  readonly contentTypeId: string;
386
- readonly data: string;
387
386
  readonly authorId?: string | undefined;
387
+ readonly data: string;
388
388
  }[];
389
389
  readonly total: number;
390
390
  readonly limit: number;
@@ -459,8 +459,8 @@ declare const cmsBackendPlugin: (config: CMSBackendConfig) => _btst_stack_plugin
459
459
  id: string;
460
460
  slug: string;
461
461
  contentTypeId: string;
462
- data: string;
463
462
  authorId?: string | undefined;
463
+ data: string;
464
464
  }[];
465
465
  total: number;
466
466
  }, false>;
@@ -493,8 +493,8 @@ declare const cmsBackendPlugin: (config: CMSBackendConfig) => _btst_stack_plugin
493
493
  id: string;
494
494
  slug: string;
495
495
  contentTypeId: string;
496
- data: string;
497
496
  authorId?: string | undefined;
497
+ data: string;
498
498
  }, false>;
499
499
  readonly createContentItem: _btst_stack_plugins_api.Operation<z.ZodObject<{
500
500
  typeSlug: z.ZodString;
@@ -522,8 +522,8 @@ declare const cmsBackendPlugin: (config: CMSBackendConfig) => _btst_stack_plugin
522
522
  id: string;
523
523
  slug: string;
524
524
  contentTypeId: string;
525
- data: string;
526
525
  authorId?: string | undefined;
526
+ data: string;
527
527
  }, false>;
528
528
  readonly updateContentItem: _btst_stack_plugins_api.Operation<z.ZodObject<{
529
529
  typeSlug: z.ZodString;
@@ -554,8 +554,8 @@ declare const cmsBackendPlugin: (config: CMSBackendConfig) => _btst_stack_plugin
554
554
  id: string;
555
555
  slug: string;
556
556
  contentTypeId: string;
557
- data: string;
558
557
  authorId?: string | undefined;
558
+ data: string;
559
559
  }, false>;
560
560
  readonly deleteContentItem: _btst_stack_plugins_api.Operation<z.ZodObject<{
561
561
  typeSlug: z.ZodString;
@@ -598,8 +598,8 @@ declare const cmsBackendPlugin: (config: CMSBackendConfig) => _btst_stack_plugin
598
598
  id: string;
599
599
  slug: string;
600
600
  contentTypeId: string;
601
- data: string;
602
601
  authorId?: string | undefined;
602
+ data: string;
603
603
  }[];
604
604
  };
605
605
  contentType?: {
@@ -619,8 +619,8 @@ declare const cmsBackendPlugin: (config: CMSBackendConfig) => _btst_stack_plugin
619
619
  id: string;
620
620
  slug: string;
621
621
  contentTypeId: string;
622
- data: string;
623
622
  authorId?: string | undefined;
623
+ data: string;
624
624
  }, false>;
625
625
  readonly listContentByRelation: _btst_stack_plugins_api.Operation<z.ZodObject<{
626
626
  typeSlug: z.ZodString;
@@ -657,8 +657,8 @@ declare const cmsBackendPlugin: (config: CMSBackendConfig) => _btst_stack_plugin
657
657
  id: string;
658
658
  slug: string;
659
659
  contentTypeId: string;
660
- data: string;
661
660
  authorId?: string | undefined;
661
+ data: string;
662
662
  }[];
663
663
  total: number;
664
664
  limit: number;
@@ -715,8 +715,8 @@ declare const cmsBackendPlugin: (config: CMSBackendConfig) => _btst_stack_plugin
715
715
  id: string;
716
716
  slug: string;
717
717
  contentTypeId: string;
718
- data: string;
719
718
  authorId?: string | undefined;
719
+ data: string;
720
720
  }[];
721
721
  total: number;
722
722
  limit: number;
@@ -127,11 +127,11 @@ declare const formBuilderBackendPlugin: (config?: FormBuilderBackendConfig) => _
127
127
  readonly status: Form["status"];
128
128
  readonly createdAt: string;
129
129
  readonly updatedAt: string;
130
- readonly id: string;
131
- readonly name: string;
132
130
  readonly schema: string;
131
+ readonly id: string;
133
132
  readonly description?: string | undefined;
134
133
  readonly slug: string;
134
+ readonly name: string;
135
135
  readonly successMessage?: string | undefined;
136
136
  readonly redirectUrl?: string | undefined;
137
137
  readonly createdBy?: string | undefined;
@@ -151,11 +151,11 @@ declare const formBuilderBackendPlugin: (config?: FormBuilderBackendConfig) => _
151
151
  readonly status: Form["status"];
152
152
  readonly createdAt: string;
153
153
  readonly updatedAt: string;
154
- readonly id: string;
155
- readonly name: string;
156
154
  readonly schema: string;
155
+ readonly id: string;
157
156
  readonly description?: string | undefined;
158
157
  readonly slug: string;
158
+ readonly name: string;
159
159
  readonly successMessage?: string | undefined;
160
160
  readonly redirectUrl?: string | undefined;
161
161
  readonly createdBy?: string | undefined;
@@ -171,11 +171,11 @@ declare const formBuilderBackendPlugin: (config?: FormBuilderBackendConfig) => _
171
171
  readonly status: Form["status"];
172
172
  readonly createdAt: string;
173
173
  readonly updatedAt: string;
174
- readonly id: string;
175
- readonly name: string;
176
174
  readonly schema: string;
175
+ readonly id: string;
177
176
  readonly description?: string | undefined;
178
177
  readonly slug: string;
178
+ readonly name: string;
179
179
  readonly successMessage?: string | undefined;
180
180
  readonly redirectUrl?: string | undefined;
181
181
  readonly createdBy?: string | undefined;
@@ -191,11 +191,11 @@ declare const formBuilderBackendPlugin: (config?: FormBuilderBackendConfig) => _
191
191
  readonly status: Form["status"];
192
192
  readonly createdAt: string;
193
193
  readonly updatedAt: string;
194
- readonly id: string;
195
- readonly name: string;
196
194
  readonly schema: string;
195
+ readonly id: string;
197
196
  readonly description?: string | undefined;
198
197
  readonly slug: string;
198
+ readonly name: string;
199
199
  readonly successMessage?: string | undefined;
200
200
  readonly redirectUrl?: string | undefined;
201
201
  readonly createdBy?: string | undefined;
@@ -225,11 +225,11 @@ declare const formBuilderBackendPlugin: (config?: FormBuilderBackendConfig) => _
225
225
  readonly status: Form["status"];
226
226
  readonly createdAt: string;
227
227
  readonly updatedAt: string;
228
- readonly id: string;
229
- readonly name: string;
230
228
  readonly schema: string;
229
+ readonly id: string;
231
230
  readonly description?: string | undefined;
232
231
  readonly slug: string;
232
+ readonly name: string;
233
233
  readonly successMessage?: string | undefined;
234
234
  readonly redirectUrl?: string | undefined;
235
235
  readonly createdBy?: string | undefined;
@@ -259,11 +259,11 @@ declare const formBuilderBackendPlugin: (config?: FormBuilderBackendConfig) => _
259
259
  readonly status: Form["status"];
260
260
  readonly createdAt: string;
261
261
  readonly updatedAt: string;
262
- readonly id: string;
263
- readonly name: string;
264
262
  readonly schema: string;
263
+ readonly id: string;
265
264
  readonly description?: string | undefined;
266
265
  readonly slug: string;
266
+ readonly name: string;
267
267
  readonly successMessage?: string | undefined;
268
268
  readonly redirectUrl?: string | undefined;
269
269
  readonly createdBy?: string | undefined;