@dbx-tools/ui-mastra 0.6.218 → 0.6.220

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.
@@ -6,31 +6,37 @@ import {
6
6
  thread,
7
7
  wire,
8
8
  type Chart,
9
- type MastraClearHistoryResponse,
10
9
  type MastraClientConfig,
11
- type MastraDeleteThreadResponse,
12
10
  type MastraFeedbackRequest,
13
11
  type MastraFeedbackResponse,
14
- type MastraHistoryResponse,
15
- type MastraThread,
16
- type MastraThreadsResponse,
17
- type MastraUpdateThreadResponse,
18
12
  type StatementData,
19
13
  } from "@dbx-tools/shared-mastra";
20
14
  import type { ServingEndpointSummary } from "@dbx-tools/shared-model";
21
15
  import { usePluginClientConfig } from "@dbx-tools/ui-appkit/react";
22
- import { MastraClient } from "@mastra/client-js";
16
+ import {
17
+ MastraClient,
18
+ type ListAgentSuspendedRunsResponse,
19
+ type ListMemoryThreadMessagesResponse,
20
+ type ListMemoryThreadsResponse,
21
+ } from "@mastra/client-js";
22
+ import { DefaultChatTransport, type UIMessage, type UIMessageChunk } from "ai";
23
23
  import { useCallback, useEffect, useMemo, useState } from "react";
24
- import { asMastraStreamResponse, type MastraStreamResponse } from "./mastra-stream.ts";
24
+ import type { MastraRequestContextSnapshot } from "./request-context.ts";
25
+
26
+ type NativeAgentClient = ReturnType<MastraClient["getAgent"]>;
27
+ export interface MastraStreamResponse {
28
+ headers: Headers;
29
+ stream: ReadableStream<UIMessageChunk>;
30
+ }
31
+ export type MastraMemoryThread = ListMemoryThreadsResponse["threads"][number];
25
32
 
26
33
  /**
27
34
  * `@mastra/client-js` `MastraClient` extended with the Mastra plugin's
28
35
  * custom routes. One client drives everything the chat UI needs:
29
36
  *
30
- * - Conversation streaming via the inherited
31
- * `getAgent(id).stream()` (the standard Mastra agent route).
32
- * - Thread history (`history` / `clearHistory`), the conversation
33
- * list (`threads` / `removeThread` / `renameThread`), the model
37
+ * - Conversation streaming through Mastra's official AI SDK `chatRoute`
38
+ * and the browser-native AI SDK `DefaultChatTransport`.
39
+ * - Native Mastra memory history and thread management, plus the model
34
40
  * catalogue (`models`),
35
41
  * Genie starter prompts (`suggestions`), MLflow feedback logging
36
42
  * (`feedback`), and inline embed resolution (`chart` / `statement`)
@@ -98,83 +104,146 @@ export class MastraPluginClient extends MastraClient {
98
104
  return headers;
99
105
  }
100
106
 
107
+ async #uiStream(options: {
108
+ path: string;
109
+ chatId: string;
110
+ messages: UIMessage[];
111
+ body?: object;
112
+ routing: { threadId?: string; model?: string };
113
+ signal?: AbortSignal;
114
+ }): Promise<MastraStreamResponse> {
115
+ let headers = new Headers();
116
+ const origin = typeof window !== "undefined" ? window.location.origin : "http://localhost";
117
+ const transport = new DefaultChatTransport<UIMessage>({
118
+ api: new URL(options.path, origin).toString(),
119
+ credentials: "include",
120
+ headers: this.#routingHeaders(options.routing),
121
+ fetch: async (input, init) => {
122
+ const response = await fetch(input, init);
123
+ headers = new Headers(response.headers);
124
+ return response;
125
+ },
126
+ });
127
+ const stream = await transport.sendMessages({
128
+ trigger: "submit-message",
129
+ chatId: options.chatId,
130
+ messageId: undefined,
131
+ messages: options.messages,
132
+ abortSignal: options.signal,
133
+ ...(options.body ? { body: options.body } : {}),
134
+ });
135
+ return { headers, stream };
136
+ }
137
+
138
+ #requestClient(
139
+ routing: { threadId?: string; model?: string },
140
+ signal?: AbortSignal,
141
+ ): MastraClient {
142
+ return new MastraClient({
143
+ baseUrl: typeof window !== "undefined" ? window.location.origin : "http://localhost",
144
+ apiPrefix: this.basePath,
145
+ credentials: "include",
146
+ headers: this.#routingHeaders(routing),
147
+ ...(signal ? { abortSignal: signal } : {}),
148
+ });
149
+ }
150
+
151
+ #requestAgent(
152
+ agentId: string,
153
+ routing: { threadId?: string; model?: string },
154
+ signal?: AbortSignal,
155
+ ): NativeAgentClient {
156
+ return this.#requestClient(routing, signal).getAgent(agentId);
157
+ }
158
+
101
159
  /**
102
- * Open an agent stream for one conversation turn, routed and cancelled
103
- * PER CALL. Posts to the agent `/stream` route with this run's own thread
104
- * id + model as request headers and its own `AbortSignal` on the fetch, so
105
- * many threads can stream at once and aborting one leaves the others
106
- * untouched. Reads the SSE directly (like the tool-approval streams) rather
107
- * than through the inherited `agent.stream()`, whose shared-client headers
108
- * and single client-level abort signal can't isolate concurrent runs.
160
+ * Open a native AI SDK UI stream for one Mastra conversation turn. Routing
161
+ * headers and cancellation remain isolated to this call.
109
162
  */
110
163
  async streamAgent(params: {
111
164
  agentId: string;
112
- messages: Array<{ role: string; content: string }>;
165
+ messages: UIMessage[];
113
166
  runId: string;
114
167
  threadId?: string;
115
168
  model?: string;
169
+ requestContext?: MastraRequestContextSnapshot;
116
170
  signal?: AbortSignal;
117
171
  }): Promise<MastraStreamResponse> {
118
- const url = `${this.basePath}/agents/${encodeURIComponent(params.agentId)}/stream`;
119
- const init: RequestInit = {
120
- method: "POST",
121
- credentials: "include",
122
- headers: {
123
- "Content-Type": "application/json",
124
- ...this.#routingHeaders(params),
172
+ return this.#uiStream({
173
+ path: `${this.basePath}${routes.MASTRA_ROUTES.chat}/${encodeURIComponent(params.agentId)}`,
174
+ chatId: params.threadId ?? params.runId,
175
+ messages: params.messages,
176
+ body: {
177
+ runId: params.runId,
178
+ ...(params.requestContext ? { requestContext: params.requestContext } : {}),
125
179
  },
126
- body: JSON.stringify({ messages: params.messages, runId: params.runId }),
127
- };
128
- if (params.signal) init.signal = params.signal;
129
- const response = await fetch(url, init);
130
- if (!response.ok) throw new Error(`HTTP ${response.status}`);
131
- return asMastraStreamResponse(response);
180
+ routing: params,
181
+ signal: params.signal,
182
+ });
132
183
  }
133
184
 
134
185
  /**
135
- * Resume a suspended `requireApproval` tool via `approve-tool-call`.
136
- * Reads SSE directly instead of `agent.approveToolCall()` so the
137
- * stock client's internal `processChatResponse_vNext` tee does not
138
- * throw when the resume stream emits `tool-result` without a new
139
- * `tool-call`.
186
+ * Resume a suspended `requireApproval` tool through the same official chat
187
+ * route using Mastra's native `resumeData` contract.
140
188
  */
141
189
  async approveToolCallStream(
142
190
  agentId: string,
143
- params: { runId: string; toolCallId: string; threadId?: string; signal?: AbortSignal },
191
+ params: {
192
+ runId: string;
193
+ toolCallId: string;
194
+ threadId?: string;
195
+ requestContext?: MastraRequestContextSnapshot;
196
+ signal?: AbortSignal;
197
+ },
144
198
  ): Promise<MastraStreamResponse> {
145
- return this.#toolApprovalStream(agentId, "approve-tool-call", params);
199
+ return this.#toolApprovalStream(agentId, true, params);
146
200
  }
147
201
 
148
202
  /**
149
- * Deny a suspended `requireApproval` tool via `decline-tool-call`.
150
- * Same direct SSE reader as {@link approveToolCallStream}.
203
+ * Deny a suspended tool through the same native resume contract.
151
204
  */
152
205
  async declineToolCallStream(
153
206
  agentId: string,
154
- params: { runId: string; toolCallId: string; threadId?: string; signal?: AbortSignal },
207
+ params: {
208
+ runId: string;
209
+ toolCallId: string;
210
+ threadId?: string;
211
+ reason?: string;
212
+ requestContext?: MastraRequestContextSnapshot;
213
+ signal?: AbortSignal;
214
+ },
155
215
  ): Promise<MastraStreamResponse> {
156
- return this.#toolApprovalStream(agentId, "decline-tool-call", params);
216
+ return this.#toolApprovalStream(agentId, false, params);
157
217
  }
158
218
 
159
219
  async #toolApprovalStream(
160
220
  agentId: string,
161
- route: "approve-tool-call" | "decline-tool-call",
162
- params: { runId: string; toolCallId: string; threadId?: string; signal?: AbortSignal },
221
+ approved: boolean,
222
+ params: {
223
+ runId: string;
224
+ toolCallId: string;
225
+ threadId?: string;
226
+ reason?: string;
227
+ requestContext?: MastraRequestContextSnapshot;
228
+ signal?: AbortSignal;
229
+ },
163
230
  ): Promise<MastraStreamResponse> {
164
- const url = `${this.basePath}/agents/${encodeURIComponent(agentId)}/${route}`;
165
- const init: RequestInit = {
166
- method: "POST",
167
- credentials: "include",
168
- headers: {
169
- "Content-Type": "application/json",
170
- ...this.#routingHeaders({ threadId: params.threadId }),
231
+ return this.#uiStream({
232
+ path: `${this.basePath}${routes.MASTRA_ROUTES.chat}/${encodeURIComponent(agentId)}`,
233
+ chatId: params.threadId ?? params.runId,
234
+ messages: [],
235
+ body: {
236
+ resumeData: {
237
+ approved,
238
+ ...(!approved && params.reason ? { reason: params.reason } : {}),
239
+ },
240
+ runId: params.runId,
241
+ toolCallId: params.toolCallId,
242
+ ...(params.requestContext ? { requestContext: params.requestContext } : {}),
171
243
  },
172
- body: JSON.stringify({ runId: params.runId, toolCallId: params.toolCallId }),
173
- };
174
- if (params.signal) init.signal = params.signal;
175
- const response = await fetch(url, init);
176
- if (!response.ok) throw new Error(`HTTP ${response.status}`);
177
- return asMastraStreamResponse(response);
244
+ routing: { threadId: params.threadId },
245
+ signal: params.signal,
246
+ });
178
247
  }
179
248
 
180
249
  /**
@@ -224,65 +293,41 @@ export class MastraPluginClient extends MastraClient {
224
293
  return payload.questions;
225
294
  }
226
295
 
227
- /**
228
- * Fetch one page of thread history from `GET ${basePath}/route/history`.
229
- * Messages come back oldest -> newest so the caller can prepend them
230
- * to a live transcript. `threadId` targets a specific conversation
231
- * (sent as the `?threadId=` query so it doesn't depend on shared client
232
- * state); omit it to read the per-session cookie thread.
233
- */
234
- async history(
235
- options: {
236
- agentId?: string;
237
- threadId?: string;
238
- page?: number;
239
- perPage?: number;
240
- signal?: AbortSignal;
241
- } = {},
242
- ): Promise<MastraHistoryResponse> {
243
- const params = new URLSearchParams();
244
- if (options.page !== undefined) params.set("page", String(options.page));
245
- if (options.perPage !== undefined) params.set("perPage", String(options.perPage));
246
- if (options.threadId) params.set(thread.THREAD_ID_QUERY, options.threadId);
247
- const qs = params.toString();
248
- const base = this.#agentScoped(routes.MASTRA_ROUTES.history, options.agentId);
249
- return this.#getJson(
250
- qs ? `${base}?${qs}` : base,
251
- wire.MastraHistoryResponseSchema,
252
- options.signal,
253
- );
296
+ /** Fetch one page from Mastra's native resource-scoped memory route. */
297
+ async history(options: {
298
+ agentId?: string;
299
+ threadId: string;
300
+ page?: number;
301
+ perPage?: number;
302
+ signal?: AbortSignal;
303
+ }): Promise<ListMemoryThreadMessagesResponse> {
304
+ return this.#requestClient({ threadId: options.threadId }, options.signal)
305
+ .getMemoryThread({
306
+ threadId: options.threadId,
307
+ agentId: options.agentId ?? this.defaultAgent,
308
+ })
309
+ .listMessages({
310
+ page: options.page ?? 0,
311
+ perPage: options.perPage ?? 20,
312
+ orderBy: { field: "createdAt", direction: "DESC" },
313
+ });
254
314
  }
255
315
 
256
- /**
257
- * Wipe a thread's history (`DELETE ${basePath}/route/history`). `threadId`
258
- * targets a specific conversation via the thread-selection header (so it
259
- * doesn't depend on shared client state); omit it to clear the per-session
260
- * cookie thread. The session cookie that anchors the thread id is
261
- * preserved - only the messages go away. Idempotent: a fresh thread reports
262
- * `cleared: 0` without erroring.
263
- */
264
- async clearHistory(
265
- options: { agentId?: string; threadId?: string; signal?: AbortSignal } = {},
266
- ): Promise<MastraClearHistoryResponse> {
267
- return this.#mutateJson(
268
- this.#agentScoped(routes.MASTRA_ROUTES.history, options.agentId),
269
- "DELETE",
270
- wire.MastraClearHistoryResponseSchema,
271
- {
272
- ...(options.threadId ? { headers: { [thread.THREAD_ID_HEADER]: options.threadId } } : {}),
273
- ...(options.signal ? { signal: options.signal } : {}),
274
- },
275
- );
316
+ /** Delete one native memory thread so the same id can start cleanly. */
317
+ async clearHistory(options: {
318
+ agentId?: string;
319
+ threadId: string;
320
+ signal?: AbortSignal;
321
+ }): Promise<void> {
322
+ await this.#requestClient({ threadId: options.threadId }, options.signal)
323
+ .getMemoryThread({
324
+ threadId: options.threadId,
325
+ agentId: options.agentId ?? this.defaultAgent,
326
+ })
327
+ .delete();
276
328
  }
277
329
 
278
- /**
279
- * Fetch one page of the caller's conversation threads from
280
- * `GET ${basePath}/threads`, newest first. Used to render the
281
- * conversation list / sidebar so the user can switch between the
282
- * threads they own for this resource. Scoped server-side to the
283
- * caller's resource id, so it only ever returns the user's own
284
- * conversations.
285
- */
330
+ /** List the caller's conversations through Mastra's native memory API. */
286
331
  async threads(
287
332
  options: {
288
333
  agentId?: string;
@@ -290,68 +335,56 @@ export class MastraPluginClient extends MastraClient {
290
335
  perPage?: number;
291
336
  signal?: AbortSignal;
292
337
  } = {},
293
- ): Promise<MastraThreadsResponse> {
294
- const params = new URLSearchParams();
295
- if (options.page !== undefined) params.set("page", String(options.page));
296
- if (options.perPage !== undefined) params.set("perPage", String(options.perPage));
297
- const qs = params.toString();
298
- const base = this.#agentScoped(routes.MASTRA_ROUTES.threads, options.agentId);
299
- return this.#getJson(
300
- qs ? `${base}?${qs}` : base,
301
- wire.MastraThreadsResponseSchema,
302
- options.signal,
303
- );
338
+ ): Promise<ListMemoryThreadsResponse> {
339
+ return this.#requestClient({}, options.signal).listMemoryThreads({
340
+ agentId: options.agentId ?? this.defaultAgent,
341
+ page: options.page ?? 0,
342
+ perPage: options.perPage ?? 30,
343
+ orderBy: { field: "updatedAt", direction: "DESC" },
344
+ });
304
345
  }
305
346
 
306
- /**
307
- * Delete a single conversation thread by id via the plugin's own
308
- * `DELETE ${basePath}/threads` route (named `removeThread` to avoid
309
- * clashing with the inherited `MastraClient.deleteThread`, which hits
310
- * Mastra's stock thread route rather than our OBO-scoped, custom
311
- * mount). The id is sent as the thread-selection header for this one
312
- * call, so the sidebar can remove any thread while the user stays on
313
- * another (routing is per call, never shared). Idempotent: deleting an unknown /
314
- * already-removed thread reports `deleted: false` without erroring.
315
- */
347
+ /** Delete one native memory thread owned by the active resource. */
316
348
  async removeThread(
317
349
  threadId: string,
318
350
  options: { agentId?: string; signal?: AbortSignal } = {},
319
- ): Promise<MastraDeleteThreadResponse> {
320
- return this.#mutateJson(
321
- this.#agentScoped(routes.MASTRA_ROUTES.threads, options.agentId),
322
- "DELETE",
323
- wire.MastraDeleteThreadResponseSchema,
324
- {
325
- headers: { [thread.THREAD_ID_HEADER]: threadId },
326
- ...(options.signal ? { signal: options.signal } : {}),
327
- },
328
- );
351
+ ): Promise<void> {
352
+ await this.#requestClient({ threadId }, options.signal)
353
+ .getMemoryThread({ threadId, agentId: options.agentId ?? this.defaultAgent })
354
+ .delete();
329
355
  }
330
356
 
331
- /**
332
- * Rename a single conversation thread via the plugin's own
333
- * `PATCH ${basePath}/threads` route. The id travels as the
334
- * thread-selection header for this one call (mirroring
335
- * {@link removeThread}, so the sidebar can rename any thread; routing is
336
- * per call, never shared) and the new `title` rides in the JSON body. The server enforces ownership and
337
- * echoes back the updated thread, so the caller can reflect the new
338
- * title immediately. Throws on an unknown / unowned thread (HTTP 404).
339
- */
357
+ /** Rename one native memory thread owned by the active resource. */
340
358
  async renameThread(
341
359
  threadId: string,
342
360
  title: string,
343
361
  options: { agentId?: string; signal?: AbortSignal } = {},
344
- ): Promise<MastraUpdateThreadResponse> {
345
- return this.#mutateJson(
346
- this.#agentScoped(routes.MASTRA_ROUTES.threads, options.agentId),
347
- "PATCH",
348
- wire.MastraUpdateThreadResponseSchema,
349
- {
350
- body: { title },
351
- headers: { [thread.THREAD_ID_HEADER]: threadId },
352
- ...(options.signal ? { signal: options.signal } : {}),
353
- },
354
- );
362
+ ): Promise<void> {
363
+ await this.#requestClient({ threadId }, options.signal)
364
+ .getMemoryThread({ threadId, agentId: options.agentId ?? this.defaultAgent })
365
+ .update({ title });
366
+ }
367
+
368
+ /** Delete selected persisted messages before regenerating a turn. */
369
+ async deleteMessages(
370
+ threadId: string,
371
+ messageIds: string[],
372
+ options: { agentId?: string; signal?: AbortSignal } = {},
373
+ ): Promise<void> {
374
+ await this.#requestClient({ threadId }, options.signal)
375
+ .getMemoryThread({ threadId, agentId: options.agentId ?? this.defaultAgent })
376
+ .deleteMessages(messageIds);
377
+ }
378
+
379
+ /** Discover approval-gated runs persisted for a conversation. */
380
+ async suspendedRuns(
381
+ agentId: string,
382
+ threadId: string,
383
+ signal?: AbortSignal,
384
+ ): Promise<ListAgentSuspendedRunsResponse> {
385
+ return this.#requestAgent(agentId, { threadId }, signal).listSuspendedRuns({
386
+ threadId,
387
+ });
355
388
  }
356
389
 
357
390
  /**
@@ -439,10 +472,9 @@ export class MastraPluginClient extends MastraClient {
439
472
 
440
473
  /**
441
474
  * `POST` / `DELETE` / `PATCH` + JSON-parse + schema-validate for the
442
- * mutating routes (`clearHistory` / `removeThread` / `renameThread` /
443
- * `feedback`). A JSON body, when present, sets `Content-Type`;
444
- * `options.headers` add per-call headers (e.g. the thread-selection
445
- * header for a targeted delete / rename) over the client's base headers.
475
+ * plugin's custom mutating routes. A JSON body, when present, sets
476
+ * `Content-Type`; `options.headers` add per-call headers over the client's
477
+ * base headers.
446
478
  */
447
479
  async #mutateJson<T>(
448
480
  url: string,
@@ -684,13 +716,13 @@ export const useMastraThreads = (
684
716
  agentId?: string,
685
717
  enabled = true,
686
718
  ): {
687
- threads: MastraThread[];
719
+ threads: MastraMemoryThread[];
688
720
  loading: boolean;
689
721
  error: Error | null;
690
722
  refresh: () => void;
691
723
  } => {
692
724
  const client = useMastraClient();
693
- const [threads, setThreads] = useState<MastraThread[]>([]);
725
+ const [threads, setThreads] = useState<MastraMemoryThread[]>([]);
694
726
  const [loading, setLoading] = useState(enabled);
695
727
  const [error, setError] = useState<Error | null>(null);
696
728
  // Bumped by `refresh()` to force a re-fetch without changing any of
@@ -0,0 +1,30 @@
1
+ import { object, type SerializableValue } from "@dbx-tools/shared-core";
2
+ import type { RequestContext } from "@mastra/client-js";
3
+
4
+ /** Typed plain values or Mastra's native typed request-context container. */
5
+ export type MastraRequestContextInput<TValues extends Record<string, unknown>> =
6
+ TValues | RequestContext<TValues>;
7
+
8
+ /** Static context or a per-turn resolver read when the user submits. */
9
+ export type MastraRequestContextSource<TValues extends Record<string, unknown>> =
10
+ MastraRequestContextInput<TValues> | (() => MastraRequestContextInput<TValues> | undefined);
11
+
12
+ /** JSON-safe immutable snapshot retained for one run and its continuation. */
13
+ export type MastraRequestContextSnapshot = Record<string, SerializableValue>;
14
+
15
+ /**
16
+ * Materialize and validate one run's application context. Infrastructure keys
17
+ * remain server-owned; this only snapshots caller-provided JSON values.
18
+ */
19
+ export function snapshotRequestContext<TValues extends Record<string, unknown>>(
20
+ source: MastraRequestContextSource<TValues> | undefined,
21
+ ): MastraRequestContextSnapshot | undefined {
22
+ const input = typeof source === "function" ? source() : source;
23
+ 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;
30
+ }
@@ -1,32 +1,42 @@
1
1
  import type { UIMessage } from "ai";
2
- import type {
3
- ChatStatus,
4
- MessageFeedback,
5
- PendingApproval,
6
- QueuedSteer,
7
- ToolEvent,
8
- } from "../react/types.ts";
2
+ import type { MastraRequestContextSnapshot } from "./request-context.ts";
3
+ import type { ChatStatus, MessageFeedback, PendingApproval, QueuedSteer } from "../react/types.ts";
9
4
 
10
5
  export type { QueuedSteer } from "../react/types.ts";
11
6
 
7
+ /** A queued steer plus the application context captured when it was submitted. */
8
+ export type SessionQueuedSteer = QueuedSteer & {
9
+ requestContext?: MastraRequestContextSnapshot;
10
+ };
11
+
12
+ /** UI target and immutable application context captured for one Mastra run. */
13
+ export type SessionRun = {
14
+ assistantId: string;
15
+ requestContext?: MastraRequestContextSnapshot;
16
+ };
17
+
12
18
  /** Session-scoped transcript + stream state for one conversation thread. */
13
19
  export type ThreadSession = {
14
20
  messages: UIMessage[];
15
21
  status: ChatStatus;
16
22
  error: Error | null;
17
- toolEventsByMessage: Record<string, ToolEvent[]>;
18
23
  pendingApprovalsByMessage: Record<string, PendingApproval[]>;
19
24
  feedbackByMessage: Record<string, MessageFeedback>;
20
25
  abortController: AbortController | null;
21
26
  runToken: number;
22
27
  assistantId: string | null;
23
28
  runId: string | null;
29
+ /** Live runs retained by id so older approval cards resume their own context. */
30
+ runs: Record<string, SessionRun>;
24
31
  historyLoaded: boolean;
25
32
  hasMoreHistory: boolean;
26
33
  historyPage: number;
27
- lastUserText: string | null;
34
+ /** Context snapshot for the active run and approval continuation. */
35
+ runRequestContext?: MastraRequestContextSnapshot;
36
+ /** Context snapshot reused when regenerating the last turn. */
37
+ lastRequestContext?: MastraRequestContextSnapshot;
28
38
  /** Steers submitted mid-turn, waiting to run (oldest first). */
29
- queuedSteers: QueuedSteer[];
39
+ queuedSteers: SessionQueuedSteer[];
30
40
  };
31
41
 
32
42
  /** Map key for the classic single-thread chat (no explicit thread id). */
@@ -37,17 +47,16 @@ export function createThreadSession(): ThreadSession {
37
47
  messages: [],
38
48
  status: "ready",
39
49
  error: null,
40
- toolEventsByMessage: {},
41
50
  pendingApprovalsByMessage: {},
42
51
  feedbackByMessage: {},
43
52
  abortController: null,
44
53
  runToken: 0,
45
54
  assistantId: null,
46
55
  runId: null,
56
+ runs: {},
47
57
  historyLoaded: false,
48
58
  hasMoreHistory: false,
49
59
  historyPage: 0,
50
- lastUserText: null,
51
60
  queuedSteers: [],
52
61
  };
53
62
  }
@@ -57,12 +66,12 @@ export function isSessionRunning(session: ThreadSession): boolean {
57
66
  }
58
67
 
59
68
  /** Append a steer to the queue (oldest first). Returns a new array. */
60
- export function enqueueSteer(queue: QueuedSteer[], steer: QueuedSteer): QueuedSteer[] {
69
+ export function enqueueSteer<T extends QueuedSteer>(queue: T[], steer: T): T[] {
61
70
  return [...queue, steer];
62
71
  }
63
72
 
64
73
  /** Remove the steer with `id` from the queue. Returns a new array. */
65
- export function removeSteer(queue: QueuedSteer[], id: string): QueuedSteer[] {
74
+ export function removeSteer<T extends QueuedSteer>(queue: T[], id: string): T[] {
66
75
  return queue.filter((steer) => steer.id !== id);
67
76
  }
68
77
 
@@ -72,10 +81,10 @@ export function removeSteer(queue: QueuedSteer[], id: string): QueuedSteer[] {
72
81
  * order can't duplicate or resurrect an item; any current steer missing from
73
82
  * `orderedIds` is appended in its existing relative order as a safety net.
74
83
  */
75
- export function reorderSteers(queue: QueuedSteer[], orderedIds: string[]): QueuedSteer[] {
84
+ export function reorderSteers<T extends QueuedSteer>(queue: T[], orderedIds: string[]): T[] {
76
85
  const byId = new Map(queue.map((steer) => [steer.id, steer]));
77
86
  const seen = new Set<string>();
78
- const next: QueuedSteer[] = [];
87
+ const next: T[] = [];
79
88
  for (const id of orderedIds) {
80
89
  const steer = byId.get(id);
81
90
  if (steer && !seen.has(id)) {
@@ -89,28 +98,6 @@ export function reorderSteers(queue: QueuedSteer[], orderedIds: string[]): Queue
89
98
  return next;
90
99
  }
91
100
 
92
- /**
93
- * Settle any tool-progress pills still marked `running` to `done`. A cancelled
94
- * or interrupted turn stops delivering the `tool-result` / `tool-error` chunks
95
- * that would otherwise close them, so without this a Genie / chart pill would
96
- * spin forever after the user hits stop. Returns the same map when nothing was
97
- * running so callers can skip a needless state update.
98
- */
99
- export function terminateRunningToolEvents(
100
- toolEventsByMessage: Record<string, ToolEvent[]>,
101
- ): Record<string, ToolEvent[]> {
102
- let changed = false;
103
- const next: Record<string, ToolEvent[]> = {};
104
- for (const [messageId, events] of Object.entries(toolEventsByMessage)) {
105
- next[messageId] = events.map((event) => {
106
- if (event.status !== "running") return event;
107
- changed = true;
108
- return { ...event, status: "done" as const };
109
- });
110
- }
111
- return changed ? next : toolEventsByMessage;
112
- }
113
-
114
101
  export function sessionKey(activeThreadId: string | undefined): string {
115
102
  return activeThreadId ?? DEFAULT_THREAD_SESSION_KEY;
116
103
  }