@dbx-tools/ui-mastra 0.9.131 → 0.9.136

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.
@@ -1,23 +1,58 @@
1
1
  import { errorUtils, log } from "@dbx-tools/shared-core";
2
+ import { genieModel } from "@dbx-tools/shared-genie";
3
+ import type { FeedbackMode } from "@dbx-tools/shared-mastra/feedback";
2
4
  import type { UIMessage } from "ai";
3
5
  import { useCallback, useRef } from "react";
4
6
  import type { ThreadSessionUpdater } from "./chat-sessions.ts";
5
7
  import type { FeedbackSubmission, MessageFeedback } from "./types.ts";
6
8
  import type { MastraPluginClient } from "../support/mastra-client.ts";
9
+ import { toolEventsFromParts } from "../support/tool-events.ts";
7
10
 
8
11
  const logger = log.logger("ui-mastra/chat");
9
12
 
10
13
  interface UseChatFeedbackOptions {
11
14
  activeKey: string;
12
15
  feedbackByMessage: Record<string, MessageFeedback>;
16
+ feedbackMode: FeedbackMode;
13
17
  mastraClient: MastraPluginClient;
14
18
  updateSession: ThreadSessionUpdater;
15
19
  }
16
20
 
17
- /** Submit trace-scoped MLflow feedback with optimistic thumbs state. */
21
+ /** Genie message target recovered from one completed ask_genie tool result. */
22
+ export interface GenieFeedbackTarget {
23
+ spaceId: string;
24
+ conversationId: string;
25
+ messageId: string;
26
+ }
27
+
28
+ /** Read the latest Genie target from an assistant message's tool results. */
29
+ export function genieFeedbackTarget(message: UIMessage): GenieFeedbackTarget | undefined {
30
+ for (const event of [...toolEventsFromParts(message.parts)].reverse()) {
31
+ if (!event.toolName.startsWith("ask_genie")) continue;
32
+ const output =
33
+ typeof event.output === "object" && event.output !== null && "message" in event.output
34
+ ? event.output.message
35
+ : undefined;
36
+ const parsed = genieModel.GenieMessageSchema.safeParse(output);
37
+ if (!parsed.success) continue;
38
+ const genieMessage = parsed.data;
39
+ const messageId = genieMessage.message_id ?? genieMessage.id;
40
+ if (genieMessage.space_id && genieMessage.conversation_id && messageId) {
41
+ return {
42
+ spaceId: genieMessage.space_id,
43
+ conversationId: genieMessage.conversation_id,
44
+ messageId,
45
+ };
46
+ }
47
+ }
48
+ return undefined;
49
+ }
50
+
51
+ /** Submit feedback to the single destination selected by the server. */
18
52
  export function useChatFeedback({
19
53
  activeKey,
20
54
  feedbackByMessage,
55
+ feedbackMode,
21
56
  mastraClient,
22
57
  updateSession,
23
58
  }: UseChatFeedbackOptions) {
@@ -27,34 +62,55 @@ export function useChatFeedback({
27
62
  return useCallback(
28
63
  async (message: UIMessage, submission: FeedbackSubmission) => {
29
64
  const traceId = feedbackByMessageRef.current[message.id]?.traceId;
30
- if (!traceId) return;
65
+ const target = feedbackMode === "genie" ? genieFeedbackTarget(message) : undefined;
66
+ if (feedbackMode === "none") return;
67
+
31
68
  if (submission.value) {
32
69
  updateSession(activeKey, (session) => ({
33
70
  ...session,
34
71
  feedbackByMessage: {
35
72
  ...session.feedbackByMessage,
36
- [message.id]: { traceId, value: submission.value },
73
+ [message.id]: {
74
+ ...(traceId ? { traceId } : {}),
75
+ value: submission.value,
76
+ },
37
77
  },
38
78
  }));
39
79
  }
40
80
  try {
41
- const result = await mastraClient.feedback({
42
- traceId,
43
- ...(submission.value !== undefined ? { value: submission.value === "up" } : {}),
44
- ...(submission.comment ? { comment: submission.comment } : {}),
45
- });
46
- if (!result.ok) {
47
- logger.warn("feedback not recorded (trace may still be exporting)", {
81
+ let result;
82
+ if (feedbackMode === "genie") {
83
+ if (!target || !submission.value || !submission.request) return;
84
+ result = await mastraClient.feedback({
85
+ mode: "genie",
86
+ ...target,
87
+ rating: submission.value === "up" ? "POSITIVE" : "NEGATIVE",
88
+ request: submission.request,
89
+ ...(submission.comment ? { comment: submission.comment } : {}),
90
+ });
91
+ } else {
92
+ if (!traceId) return;
93
+ result = await mastraClient.feedback({
94
+ mode: "mlflow",
48
95
  traceId,
96
+ ...(submission.value !== undefined ? { value: submission.value === "up" } : {}),
97
+ ...(submission.comment ? { comment: submission.comment } : {}),
98
+ });
99
+ }
100
+ if (!result.ok) {
101
+ logger.warn("feedback not recorded", {
102
+ feedbackMode,
103
+ ...(traceId ? { traceId } : {}),
49
104
  });
50
105
  }
51
106
  } catch (error) {
52
107
  logger.error("feedback error", {
53
- traceId,
108
+ feedbackMode,
109
+ ...(traceId ? { traceId } : {}),
54
110
  error: errorUtils.toError(error),
55
111
  });
56
112
  }
57
113
  },
58
- [activeKey, mastraClient, updateSession],
114
+ [activeKey, feedbackMode, mastraClient, updateSession],
59
115
  );
60
116
  }
@@ -15,7 +15,9 @@ import { isToolUIPart } from "ai";
15
15
  import { ArrowDownIcon, MessageSquareIcon, RefreshCwIcon, TriangleAlertIcon } from "lucide-react";
16
16
  import React, { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
17
17
  import { AssistantBubble, UserBubble } from "./bubbles.tsx";
18
+ import { genieFeedbackTarget } from "./chat-feedback.ts";
18
19
  import type { ChatViewProps } from "./types.ts";
20
+ import { messageText } from "../support/export.ts";
19
21
 
20
22
  const BOTTOM_THRESHOLD_PX = 24;
21
23
  const TOP_LOAD_MORE_THRESHOLD_PX = 120;
@@ -156,6 +158,7 @@ type ChatTranscriptProps = {
156
158
  pendingApprovalsByMessage: NonNullable<ChatViewProps["pendingApprovalsByMessage"]>;
157
159
  onExportMessage: ChatViewProps["onExportMessage"];
158
160
  feedbackByMessage: NonNullable<ChatViewProps["feedbackByMessage"]>;
161
+ feedbackMode: NonNullable<ChatViewProps["feedbackMode"]>;
159
162
  onFeedback: ChatViewProps["onFeedback"];
160
163
  mlflowExperimentUrl: ChatViewProps["mlflowExperimentUrl"];
161
164
  };
@@ -176,6 +179,7 @@ export const ChatTranscript = ({
176
179
  pendingApprovalsByMessage,
177
180
  onExportMessage,
178
181
  feedbackByMessage,
182
+ feedbackMode,
179
183
  onFeedback,
180
184
  mlflowExperimentUrl,
181
185
  }: ChatTranscriptProps) => {
@@ -229,6 +233,17 @@ export const ChatTranscript = ({
229
233
  const isLast = index === messages.length - 1;
230
234
  if (message.role === "assistant") {
231
235
  const messageFeedback = feedbackByMessage[message.id];
236
+ const genieTarget =
237
+ feedbackMode === "genie" ? genieFeedbackTarget(message) : undefined;
238
+ const canSubmitFeedback =
239
+ feedbackMode === "mlflow"
240
+ ? Boolean(messageFeedback?.traceId)
241
+ : Boolean(genieTarget);
242
+ const requestMessage = [...messages.slice(0, index)]
243
+ .reverse()
244
+ .find((candidate) => candidate.role === "user");
245
+ const feedbackRequest = requestMessage ? messageText(requestMessage) : "";
246
+ const bubbleFeedbackMode = feedbackMode === "genie" ? "genie" : "mlflow";
232
247
  return (
233
248
  <AssistantBubble
234
249
  key={message.id}
@@ -243,10 +258,12 @@ export const ChatTranscript = ({
243
258
  {...(onExportMessage
244
259
  ? { onExport: (format) => onExportMessage(message, format) }
245
260
  : {})}
246
- {...(onFeedback && messageFeedback
261
+ {...(onFeedback && canSubmitFeedback
247
262
  ? {
248
263
  onFeedback: (submission) => onFeedback(message, submission),
249
- ...(messageFeedback.value
264
+ feedbackMode: bubbleFeedbackMode,
265
+ ...(feedbackRequest ? { feedbackRequest } : {}),
266
+ ...(messageFeedback?.value
250
267
  ? { feedbackValue: messageFeedback.value }
251
268
  : {}),
252
269
  }
@@ -64,6 +64,7 @@ export const ChatView = ({
64
64
  onExportConversation,
65
65
  onExportMessage,
66
66
  feedbackByMessage = {},
67
+ feedbackMode = "none",
67
68
  onFeedback,
68
69
  mlflowExperimentUrl,
69
70
  }: ChatViewProps) => {
@@ -113,6 +114,7 @@ export const ChatView = ({
113
114
  pendingApprovalsByMessage={pendingApprovalsByMessage}
114
115
  onExportMessage={onExportMessage}
115
116
  feedbackByMessage={feedbackByMessage}
117
+ feedbackMode={feedbackMode}
116
118
  onFeedback={onFeedback}
117
119
  mlflowExperimentUrl={mlflowExperimentUrl}
118
120
  />
@@ -25,15 +25,22 @@ import type { FeedbackSubmission, FeedbackValue } from "./types.ts";
25
25
 
26
26
  /** Render per-message rating and comment controls for an MLflow-backed trace. */
27
27
  export const FeedbackControls = ({
28
+ mode = "mlflow",
29
+ request,
28
30
  value,
29
31
  onSubmit,
30
32
  }: {
33
+ /** Server-selected feedback destination. */
34
+ mode?: "mlflow" | "genie";
35
+ /** Original user request shown in Genie feedback. */
36
+ request?: string;
31
37
  /** Last thumbs the user chose, so the active button stays highlighted. */
32
38
  value?: FeedbackValue;
33
39
  onSubmit: (submission: FeedbackSubmission) => void | Promise<void>;
34
40
  }) => {
35
41
  const [open, setOpen] = useState(false);
36
42
  const [comment, setComment] = useState("");
43
+ const [rating, setRating] = useState<FeedbackValue | undefined>(value);
37
44
  const [sending, setSending] = useState(false);
38
45
 
39
46
  const submitComment = async () => {
@@ -49,6 +56,103 @@ export const FeedbackControls = ({
49
56
  }
50
57
  };
51
58
 
59
+ const submitGenieFeedback = async () => {
60
+ if (!rating || !request?.trim() || sending) return;
61
+ setSending(true);
62
+ try {
63
+ await onSubmit({
64
+ value: rating,
65
+ request: request.trim(),
66
+ ...(comment.trim() ? { comment: comment.trim() } : {}),
67
+ });
68
+ setOpen(false);
69
+ } finally {
70
+ setSending(false);
71
+ }
72
+ };
73
+
74
+ if (mode === "genie") {
75
+ return (
76
+ <Dialog open={open} onOpenChange={setOpen}>
77
+ <Tooltip>
78
+ <TooltipTrigger asChild>
79
+ <DialogTrigger asChild>
80
+ <Button
81
+ type="button"
82
+ size="icon"
83
+ variant="ghost"
84
+ className="size-7"
85
+ aria-label="Give feedback"
86
+ >
87
+ <MessageSquareTextIcon className="size-3" />
88
+ </Button>
89
+ </DialogTrigger>
90
+ </TooltipTrigger>
91
+ <TooltipContent>Give feedback</TooltipContent>
92
+ </Tooltip>
93
+ <DialogContent className="sm:max-w-md">
94
+ <DialogHeader>
95
+ <DialogTitle>Feedback</DialogTitle>
96
+ <DialogDescription>Send feedback directly to this Genie response.</DialogDescription>
97
+ </DialogHeader>
98
+ <div className="space-y-4">
99
+ <fieldset className="space-y-2">
100
+ <legend className="text-sm font-medium">Rating</legend>
101
+ <div className="flex gap-2">
102
+ <Button
103
+ type="button"
104
+ variant={rating === "up" ? "default" : "outline"}
105
+ aria-pressed={rating === "up"}
106
+ onClick={() => setRating("up")}
107
+ >
108
+ <ThumbsUpIcon className="size-4" />
109
+ Good
110
+ </Button>
111
+ <Button
112
+ type="button"
113
+ variant={rating === "down" ? "destructive" : "outline"}
114
+ aria-pressed={rating === "down"}
115
+ onClick={() => setRating("down")}
116
+ >
117
+ <ThumbsDownIcon className="size-4" />
118
+ Bad
119
+ </Button>
120
+ </div>
121
+ </fieldset>
122
+ <div className="space-y-1.5">
123
+ <p className="text-sm font-medium">Request</p>
124
+ <p className="rounded-md border bg-muted/40 px-3 py-2 text-sm text-foreground">
125
+ {request}
126
+ </p>
127
+ </div>
128
+ <label className="block space-y-1.5 text-sm font-medium">
129
+ <span>Comment</span>
130
+ <Textarea
131
+ value={comment}
132
+ maxLength={5_000}
133
+ onChange={(event) => setComment(event.target.value)}
134
+ placeholder="Optional details"
135
+ rows={4}
136
+ />
137
+ </label>
138
+ </div>
139
+ <DialogFooter>
140
+ <Button type="button" variant="ghost" onClick={() => setOpen(false)}>
141
+ Cancel
142
+ </Button>
143
+ <Button
144
+ type="button"
145
+ disabled={!rating || !request?.trim() || sending}
146
+ onClick={() => void submitGenieFeedback()}
147
+ >
148
+ {sending ? "Sending..." : "Send feedback"}
149
+ </Button>
150
+ </DialogFooter>
151
+ </DialogContent>
152
+ </Dialog>
153
+ );
154
+ }
155
+
52
156
  return (
53
157
  <>
54
158
  <Tooltip>
@@ -3,6 +3,7 @@ import { isStaleMastraResumeError } from "@dbx-tools/shared-mastra/resume";
3
3
  import type { ReasoningEffort } from "@dbx-tools/shared-model";
4
4
  import { getAuthStatus } from "@dbx-tools/ui/auth/react";
5
5
  import { useBrand } from "@dbx-tools/ui/branding/react";
6
+ import type { RequestContext } from "@mastra/client-js";
6
7
  import type { FileUIPart, UIMessage } from "ai";
7
8
  import { type ReactNode, useCallback, useEffect, useMemo, useRef, useState } from "react";
8
9
  import { toChronologicalUiMessages } from "./_history-messages.ts";
@@ -42,7 +43,6 @@ import {
42
43
  } from "../support/model-selection.ts";
43
44
  import {
44
45
  snapshotRequestContext,
45
- type MastraRequestContextSnapshot,
46
46
  type MastraRequestContextSource,
47
47
  } from "../support/request-context.ts";
48
48
  import { threadStorageKey } from "../support/thread-persistence.ts";
@@ -238,14 +238,9 @@ export interface UseMastraChatOptions<
238
238
  */
239
239
  enableClear?: boolean;
240
240
  /**
241
- * Surface per-message feedback controls (thumbs up/down + a comment
242
- * modal) that log to MLflow as trace assessments.
243
- *
244
- * Defaults to auto: controls render when the server reports MLflow
245
- * feedback is available. Pass `false` to hide them or `true` to request
246
- * them explicitly. Regardless of this flag, controls only render when the
247
- * server reports MLflow logging is enabled (`clientConfig.feedbackEnabled`)
248
- * and the turn produced a trace id.
241
+ * Surface per-message feedback controls for the destination selected by the
242
+ * server. Genie Agent Mode posts Rating and Comment to Genie; traced agent
243
+ * mode posts thumbs/comment assessments to MLflow.
249
244
  */
250
245
  enableFeedback?: boolean;
251
246
  /**
@@ -329,8 +324,9 @@ export const useMastraChat = <TValues extends Record<string, unknown> = Record<s
329
324
  // Feedback defaults to automatic server capability detection. An explicit
330
325
  // false hides it; true keeps it requested but cannot bypass the server gate.
331
326
  const enableFeedback = options.enableFeedback !== false;
332
- const feedbackAvailable = enableFeedback && mastraClient.feedbackEnabled;
333
- const { url: mlflowExperimentUrl } = useMastraMlflowExperiment(feedbackAvailable);
327
+ const feedbackMode = enableFeedback ? mastraClient.feedbackMode : "none";
328
+ const feedbackAvailable = feedbackMode !== "none";
329
+ const { url: mlflowExperimentUrl } = useMastraMlflowExperiment(feedbackMode === "mlflow");
334
330
  const [resourceScope, setResourceScope] = useState<string | null>(null);
335
331
  useEffect(() => {
336
332
  let cancelled = false;
@@ -577,6 +573,7 @@ export const useMastraChat = <TValues extends Record<string, unknown> = Record<s
577
573
  const submitFeedback = useChatFeedback({
578
574
  activeKey,
579
575
  feedbackByMessage: activeSession.feedbackByMessage,
576
+ feedbackMode,
580
577
  mastraClient,
581
578
  updateSession,
582
579
  });
@@ -758,7 +755,7 @@ export const useMastraChat = <TValues extends Record<string, unknown> = Record<s
758
755
  (
759
756
  threadId: string,
760
757
  messages: UIMessage | UIMessage[],
761
- requestContext?: MastraRequestContextSnapshot,
758
+ requestContext?: RequestContext,
762
759
  ) => {
763
760
  const turnMessages = Array.isArray(messages) ? messages : [messages];
764
761
  const userMessage = lastUserMessage(turnMessages);
@@ -1493,12 +1490,11 @@ export const useMastraChat = <TValues extends Record<string, unknown> = Record<s
1493
1490
  onExportMessage: exportMessage,
1494
1491
  }
1495
1492
  : {}),
1496
- // Feedback: only expose the state + handler (which light up the
1497
- // per-bubble thumbs / comment controls in ChatView) when feedback
1498
- // is enabled AND the server can log to MLflow. `feedbackByMessage`
1499
- // still gates per-message on a captured trace id.
1493
+ // Feedback: expose one server-selected destination. MLflow messages still
1494
+ // require trace state; Genie messages derive their target from ask_genie.
1500
1495
  ...(feedbackAvailable
1501
1496
  ? {
1497
+ feedbackMode,
1502
1498
  feedbackByMessage: activeSession.feedbackByMessage,
1503
1499
  onFeedback: submitFeedback,
1504
1500
  ...(mlflowExperimentUrl ? { mlflowExperimentUrl } : {}),
@@ -152,7 +152,9 @@ export const ThreadSidebar = ({
152
152
  New chat
153
153
  </Button>
154
154
  )}
155
- {headerActions}
155
+ {headerActions ? (
156
+ <div className={cn("shrink-0", !onNew && "ml-auto")}>{headerActions}</div>
157
+ ) : null}
156
158
  {onHide && (
157
159
  <Tooltip>
158
160
  <TooltipTrigger asChild>
@@ -162,7 +164,7 @@ export const ThreadSidebar = ({
162
164
  size="icon"
163
165
  onClick={onHide}
164
166
  aria-label="Hide conversations"
165
- className={cn("size-8 shrink-0", !onNew && "ml-auto")}
167
+ className={cn("size-8 shrink-0", !onNew && !headerActions && "ml-auto")}
166
168
  >
167
169
  <HideIcon className="size-4" />
168
170
  </Button>
@@ -70,6 +70,7 @@ export type FeedbackValue = "up" | "down";
70
70
  */
71
71
  export type FeedbackSubmission = {
72
72
  value?: FeedbackValue;
73
+ request?: string;
73
74
  comment?: string;
74
75
  };
75
76
 
@@ -81,7 +82,7 @@ export type FeedbackSubmission = {
81
82
  * active thumb stays highlighted.
82
83
  */
83
84
  export type MessageFeedback = {
84
- traceId: string;
85
+ traceId?: string;
85
86
  value?: FeedbackValue;
86
87
  };
87
88
 
@@ -364,6 +365,8 @@ export type ChatViewProps = {
364
365
  * response header when MLflow logging is enabled.
365
366
  */
366
367
  feedbackByMessage?: Record<string, MessageFeedback>;
368
+ /** Resolved feedback destination selected by the server. */
369
+ feedbackMode?: "none" | "mlflow" | "genie";
367
370
  /**
368
371
  * Submit thumbs / comment feedback for an assistant message. When
369
372
  * provided (and the message has a {@link feedbackByMessage} entry),
@@ -217,7 +217,7 @@ function splitSegments(text: string): Segment[] {
217
217
  }
218
218
 
219
219
  /** Concatenate a message's `text` parts (matches the on-screen bubbles). */
220
- function messageText(message: UIMessage): string {
220
+ export function messageText(message: UIMessage): string {
221
221
  return message.parts
222
222
  .filter((p): p is { type: "text"; text: string } => p.type === "text")
223
223
  .map((p) => p.text)
@@ -3,17 +3,19 @@ import type {
3
3
  ListAgentSuspendedRunsResponse,
4
4
  ListMemoryThreadMessagesResponse,
5
5
  ListMemoryThreadsResponse,
6
+ RequestContext,
6
7
  } from "@mastra/client-js";
7
8
  import { validateUIMessages, type UIMessage, type UIMessageChunk } from "ai";
8
9
  import { MastraPluginClient, type MastraStreamResponse } from "./mastra-client.ts";
9
10
 
10
11
  /**
11
12
  * {@link MastraPluginClient} that talks to one Genie space directly through
12
- * the Mastra plugin's `/genie/:alias` routes instead of an agent and Mastra
13
- * Memory. Genie owns all state: its conversations are the threads (the
14
- * conversation id is the thread id), its messages are the history, and each
15
- * turn streams the same AI SDK chunks as an agent's `ask_genie` call, so the
16
- * Mastra chat renders it unchanged.
13
+ * the Mastra plugin's `/genie/:alias` routes instead of an outer agent. Genie
14
+ * owns conversation state while the server may add Mastra request context and
15
+ * resource-scoped memory. Its conversations are the threads (the conversation
16
+ * id is the thread id), its messages are the history, and each turn streams
17
+ * the same AI SDK chunks as an agent's `ask_genie` call, so the Mastra chat
18
+ * renders it unchanged.
17
19
  *
18
20
  * A brand-new thread has no Genie conversation until its first turn; the
19
21
  * stream announces the conversation id, which {@link MastraStreamResponse.threadId}
@@ -23,7 +25,7 @@ export class GenieChatClient extends MastraPluginClient {
23
25
  /** Genie space alias every route is scoped to. */
24
26
  readonly alias: string;
25
27
  override readonly backgroundTurns = false;
26
- override readonly feedbackEnabled = false;
28
+ override readonly feedbackMode = "none";
27
29
  override readonly threadRename = false;
28
30
  override readonly attachments = false;
29
31
  /** Thread ids known to be Genie conversations (listed, loaded, or announced). */
@@ -54,6 +56,7 @@ export class GenieChatClient extends MastraPluginClient {
54
56
  messages: UIMessage[];
55
57
  runId: string;
56
58
  threadId?: string;
59
+ requestContext?: RequestContext;
57
60
  signal?: AbortSignal;
58
61
  }): Promise<MastraStreamResponse> {
59
62
  const userMessage = [...params.messages].reverse().find((message) => message.role === "user");
@@ -67,7 +70,11 @@ export class GenieChatClient extends MastraPluginClient {
67
70
  path: this.#url(routes.MASTRA_ROUTES.chat),
68
71
  chatId: params.threadId ?? params.runId,
69
72
  messages: userMessage ? [userMessage] : [],
70
- body: { question, ...(conversationId ? { conversationId } : {}) },
73
+ body: {
74
+ question,
75
+ ...(conversationId ? { conversationId } : {}),
76
+ ...(params.requestContext ? { requestContext: params.requestContext } : {}),
77
+ },
71
78
  routing: {},
72
79
  signal: params.signal,
73
80
  });
@@ -26,7 +26,7 @@ export { GenieChatClient } from "./genie-client.ts";
26
26
  export { MastraChatRequestError, MastraPluginClient, useMastraConfig, useMastraClient, useMastraMlflowExperiment, useMastraModels, useMastraDefaultModel, useMastraSuggestions, useMastraThreads, useChartFetch, useStatementFetch, useVisualizationFetch } from "./mastra-client.ts";
27
27
  export type { MastraStreamResponse, MastraMemoryThread, ByIdFetchState } from "./mastra-client.ts";
28
28
  export { defaultReasoningEffort, modelStorageKey, readStoredModel, storeSelectedModel } from "./model-selection.ts";
29
- export type { MastraRequestContextInput, MastraRequestContextSource, MastraRequestContextSnapshot } from "./request-context.ts";
29
+ export type { MastraRequestContextInput, MastraRequestContextSource } from "./request-context.ts";
30
30
  export { DEFAULT_THREAD_SESSION_KEY } from "./thread-sessions.ts";
31
31
  export type { SessionQueuedSteer, SessionRun, ThreadSession } from "./thread-sessions.ts";
32
32
  export { THREAD_TAB_SEED_MAX } from "./thread-tabs.ts";
@@ -20,10 +20,10 @@ import {
20
20
  type ListAgentSuspendedRunsResponse,
21
21
  type ListMemoryThreadMessagesResponse,
22
22
  type ListMemoryThreadsResponse,
23
+ type RequestContext,
23
24
  } from "@mastra/client-js";
24
25
  import { DefaultChatTransport, type UIMessage, type UIMessageChunk } from "ai";
25
26
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
26
- import type { MastraRequestContextSnapshot } from "./request-context.ts";
27
27
 
28
28
  type NativeAgentClient = ReturnType<MastraClient["getAgent"]>;
29
29
 
@@ -79,13 +79,8 @@ export class MastraPluginClient extends MastraClient {
79
79
  readonly defaultAgent: string;
80
80
  /** Registered agent ids, surfaced for pickers. */
81
81
  readonly agents: readonly string[];
82
- /**
83
- * Whether the server can log feedback to MLflow. When `false`, the
84
- * chat UI hides thumbs / comment controls and {@link feedback} would
85
- * be rejected server-side, so callers gate on this before offering
86
- * feedback affordances.
87
- */
88
- readonly feedbackEnabled: boolean;
82
+ /** Resolved feedback destination published by the server. */
83
+ readonly feedbackMode: feedback.FeedbackMode;
89
84
  /**
90
85
  * Whether the agents run their Databricks calls as the app service principal
91
86
  * rather than on-behalf-of the signed-in user. When `true` the chat works for
@@ -111,7 +106,7 @@ export class MastraPluginClient extends MastraClient {
111
106
  this.basePath = config.basePath;
112
107
  this.defaultAgent = config.defaultAgent;
113
108
  this.agents = config.agents;
114
- this.feedbackEnabled = config.feedbackEnabled ?? false;
109
+ this.feedbackMode = config.feedbackMode ?? "none";
115
110
  this.chatAlwaysAvailable = config.chatAlwaysAvailable ?? false;
116
111
  this.backgroundTurns = config.backgroundTurns ?? false;
117
112
  }
@@ -227,7 +222,7 @@ export class MastraPluginClient extends MastraClient {
227
222
  threadId?: string;
228
223
  model?: string;
229
224
  reasoningEffort?: ReasoningEffort;
230
- requestContext?: MastraRequestContextSnapshot;
225
+ requestContext?: RequestContext;
231
226
  signal?: AbortSignal;
232
227
  }): Promise<MastraStreamResponse> {
233
228
  return this.uiStream({
@@ -295,7 +290,7 @@ export class MastraPluginClient extends MastraClient {
295
290
  runId: string;
296
291
  toolCallId: string;
297
292
  threadId?: string;
298
- requestContext?: MastraRequestContextSnapshot;
293
+ requestContext?: RequestContext;
299
294
  signal?: AbortSignal;
300
295
  },
301
296
  ): Promise<MastraStreamResponse> {
@@ -312,7 +307,7 @@ export class MastraPluginClient extends MastraClient {
312
307
  toolCallId: string;
313
308
  threadId?: string;
314
309
  reason?: string;
315
- requestContext?: MastraRequestContextSnapshot;
310
+ requestContext?: RequestContext;
316
311
  signal?: AbortSignal;
317
312
  },
318
313
  ): Promise<MastraStreamResponse> {
@@ -327,7 +322,7 @@ export class MastraPluginClient extends MastraClient {
327
322
  toolCallId: string;
328
323
  threadId?: string;
329
324
  reason?: string;
330
- requestContext?: MastraRequestContextSnapshot;
325
+ requestContext?: RequestContext;
331
326
  signal?: AbortSignal;
332
327
  },
333
328
  ): Promise<MastraStreamResponse> {
@@ -1,5 +1,4 @@
1
- import { object, type SerializableValue } from "@dbx-tools/shared-core";
2
- import type { RequestContext } from "@mastra/client-js";
1
+ import { RequestContext } from "@mastra/client-js";
3
2
 
4
3
  /** Typed plain values or Mastra's native typed request-context container. */
5
4
  export type MastraRequestContextInput<TValues extends Record<string, unknown>> =
@@ -9,22 +8,18 @@ export type MastraRequestContextInput<TValues extends Record<string, unknown>> =
9
8
  export type MastraRequestContextSource<TValues extends Record<string, unknown>> =
10
9
  MastraRequestContextInput<TValues> | (() => MastraRequestContextInput<TValues> | undefined);
11
10
 
12
- /** JSON-safe immutable snapshot retained for one run and its continuation. */
13
- export type MastraRequestContextSnapshot = Record<string, SerializableValue>;
14
-
15
11
  /**
16
- * Materialize and validate one run's application context. Infrastructure keys
17
- * remain server-owned; this only snapshots caller-provided JSON values.
12
+ * Materialize one run's application context through Mastra's native container
13
+ * and serialization policy.
18
14
  */
19
15
  export function snapshotRequestContext<TValues extends Record<string, unknown>>(
20
16
  source: MastraRequestContextSource<TValues> | undefined,
21
- ): MastraRequestContextSnapshot | undefined {
17
+ ): RequestContext | undefined {
22
18
  const input = typeof source === "function" ? source() : source;
23
19
  if (input === undefined) return undefined;
24
- const candidate =
25
- "toJSON" in input && typeof input.toJSON === "function" ? input.toJSON() : input;
26
- if (!object.isRecord(candidate) || !object.isSerializableValue(candidate)) {
27
- throw new TypeError("Mastra request context must be a JSON-serializable record");
28
- }
29
- return structuredClone(candidate) as MastraRequestContextSnapshot;
20
+ const context =
21
+ input instanceof RequestContext
22
+ ? input
23
+ : new RequestContext<unknown>(Object.entries(input));
24
+ return new RequestContext<unknown>(Object.entries(structuredClone(context.toJSON())));
30
25
  }