@dbx-tools/ui-mastra 0.6.218 → 0.6.219
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +114 -21
- package/index.ts +10 -9
- package/package.json +8 -8
- package/src/react/bubbles.tsx +4 -1
- package/src/react/chat-approvals.ts +9 -4
- package/src/react/chat-composer.tsx +80 -73
- package/src/react/chat-feedback.ts +2 -2
- package/src/react/chat-history.ts +79 -10
- package/src/react/chat-stream.ts +29 -59
- package/src/react/chat-thread-layout.tsx +17 -1
- package/src/react/chat-view.tsx +10 -0
- package/src/react/export-menu.tsx +8 -1
- package/src/react/index.ts +18 -2
- package/src/react/mastra-assistant.tsx +509 -0
- package/src/react/mastra-chat.tsx +120 -72
- package/src/react/thread-sidebar.tsx +33 -43
- package/src/react/thread-tabs.tsx +141 -117
- package/src/react/tool-pill.tsx +24 -1
- package/src/react/types.ts +32 -22
- package/src/support/mastra-client.ts +203 -171
- package/src/support/request-context.ts +30 -0
- package/src/support/thread-sessions.ts +16 -38
- package/src/support/tool-events.ts +41 -0
- package/src/react/chat-stream-reducer.ts +0 -243
- package/src/support/mastra-stream.ts +0 -59
|
@@ -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 {
|
|
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 {
|
|
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
|
|
31
|
-
*
|
|
32
|
-
* -
|
|
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
|
|
103
|
-
*
|
|
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:
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
...
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
|
136
|
-
*
|
|
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: {
|
|
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,
|
|
199
|
+
return this.#toolApprovalStream(agentId, true, params);
|
|
146
200
|
}
|
|
147
201
|
|
|
148
202
|
/**
|
|
149
|
-
* Deny a suspended
|
|
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: {
|
|
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,
|
|
216
|
+
return this.#toolApprovalStream(agentId, false, params);
|
|
157
217
|
}
|
|
158
218
|
|
|
159
219
|
async #toolApprovalStream(
|
|
160
220
|
agentId: string,
|
|
161
|
-
|
|
162
|
-
params: {
|
|
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
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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<
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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<
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
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<
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
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
|
|
443
|
-
* `
|
|
444
|
-
*
|
|
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:
|
|
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<
|
|
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,20 +1,19 @@
|
|
|
1
1
|
import type { UIMessage } from "ai";
|
|
2
|
-
import type {
|
|
3
|
-
|
|
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
12
|
/** Session-scoped transcript + stream state for one conversation thread. */
|
|
13
13
|
export type ThreadSession = {
|
|
14
14
|
messages: UIMessage[];
|
|
15
15
|
status: ChatStatus;
|
|
16
16
|
error: Error | null;
|
|
17
|
-
toolEventsByMessage: Record<string, ToolEvent[]>;
|
|
18
17
|
pendingApprovalsByMessage: Record<string, PendingApproval[]>;
|
|
19
18
|
feedbackByMessage: Record<string, MessageFeedback>;
|
|
20
19
|
abortController: AbortController | null;
|
|
@@ -24,9 +23,12 @@ export type ThreadSession = {
|
|
|
24
23
|
historyLoaded: boolean;
|
|
25
24
|
hasMoreHistory: boolean;
|
|
26
25
|
historyPage: number;
|
|
27
|
-
|
|
26
|
+
/** Context snapshot for the active run and approval continuation. */
|
|
27
|
+
runRequestContext?: MastraRequestContextSnapshot;
|
|
28
|
+
/** Context snapshot reused when regenerating the last turn. */
|
|
29
|
+
lastRequestContext?: MastraRequestContextSnapshot;
|
|
28
30
|
/** Steers submitted mid-turn, waiting to run (oldest first). */
|
|
29
|
-
queuedSteers:
|
|
31
|
+
queuedSteers: SessionQueuedSteer[];
|
|
30
32
|
};
|
|
31
33
|
|
|
32
34
|
/** Map key for the classic single-thread chat (no explicit thread id). */
|
|
@@ -37,7 +39,6 @@ export function createThreadSession(): ThreadSession {
|
|
|
37
39
|
messages: [],
|
|
38
40
|
status: "ready",
|
|
39
41
|
error: null,
|
|
40
|
-
toolEventsByMessage: {},
|
|
41
42
|
pendingApprovalsByMessage: {},
|
|
42
43
|
feedbackByMessage: {},
|
|
43
44
|
abortController: null,
|
|
@@ -47,7 +48,6 @@ export function createThreadSession(): ThreadSession {
|
|
|
47
48
|
historyLoaded: false,
|
|
48
49
|
hasMoreHistory: false,
|
|
49
50
|
historyPage: 0,
|
|
50
|
-
lastUserText: null,
|
|
51
51
|
queuedSteers: [],
|
|
52
52
|
};
|
|
53
53
|
}
|
|
@@ -57,12 +57,12 @@ export function isSessionRunning(session: ThreadSession): boolean {
|
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
/** Append a steer to the queue (oldest first). Returns a new array. */
|
|
60
|
-
export function enqueueSteer(queue:
|
|
60
|
+
export function enqueueSteer<T extends QueuedSteer>(queue: T[], steer: T): T[] {
|
|
61
61
|
return [...queue, steer];
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
/** Remove the steer with `id` from the queue. Returns a new array. */
|
|
65
|
-
export function removeSteer(queue:
|
|
65
|
+
export function removeSteer<T extends QueuedSteer>(queue: T[], id: string): T[] {
|
|
66
66
|
return queue.filter((steer) => steer.id !== id);
|
|
67
67
|
}
|
|
68
68
|
|
|
@@ -72,10 +72,10 @@ export function removeSteer(queue: QueuedSteer[], id: string): QueuedSteer[] {
|
|
|
72
72
|
* order can't duplicate or resurrect an item; any current steer missing from
|
|
73
73
|
* `orderedIds` is appended in its existing relative order as a safety net.
|
|
74
74
|
*/
|
|
75
|
-
export function reorderSteers(queue:
|
|
75
|
+
export function reorderSteers<T extends QueuedSteer>(queue: T[], orderedIds: string[]): T[] {
|
|
76
76
|
const byId = new Map(queue.map((steer) => [steer.id, steer]));
|
|
77
77
|
const seen = new Set<string>();
|
|
78
|
-
const next:
|
|
78
|
+
const next: T[] = [];
|
|
79
79
|
for (const id of orderedIds) {
|
|
80
80
|
const steer = byId.get(id);
|
|
81
81
|
if (steer && !seen.has(id)) {
|
|
@@ -89,28 +89,6 @@ export function reorderSteers(queue: QueuedSteer[], orderedIds: string[]): Queue
|
|
|
89
89
|
return next;
|
|
90
90
|
}
|
|
91
91
|
|
|
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
92
|
export function sessionKey(activeThreadId: string | undefined): string {
|
|
115
93
|
return activeThreadId ?? DEFAULT_THREAD_SESSION_KEY;
|
|
116
94
|
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { GENIE_PROGRESS_PART_TYPE, GenieProgressPartDataSchema } from "@dbx-tools/shared-mastra";
|
|
2
|
+
import { getToolOrDynamicToolName, isToolOrDynamicToolUIPart, type UIMessage } from "ai";
|
|
3
|
+
import type { ToolEvent } from "../react/types.ts";
|
|
4
|
+
|
|
5
|
+
/** Project native AI SDK tool parts from persisted messages onto pill state. */
|
|
6
|
+
export function toolEventsFromParts(parts: UIMessage["parts"]): ToolEvent[] {
|
|
7
|
+
const events = parts.filter(isToolOrDynamicToolUIPart).map<ToolEvent>((part) => {
|
|
8
|
+
const error = part.state === "output-error";
|
|
9
|
+
const done = part.state === "output-available";
|
|
10
|
+
return {
|
|
11
|
+
id: part.toolCallId,
|
|
12
|
+
toolName: getToolOrDynamicToolName(part),
|
|
13
|
+
status: error ? "error" : done ? "done" : "running",
|
|
14
|
+
input: part.input,
|
|
15
|
+
...(done ? { output: part.output } : error ? { output: { error: part.errorText } } : {}),
|
|
16
|
+
};
|
|
17
|
+
});
|
|
18
|
+
const byId = new Map(events.map((event) => [event.id, event]));
|
|
19
|
+
for (const part of parts) {
|
|
20
|
+
if (part.type !== GENIE_PROGRESS_PART_TYPE) continue;
|
|
21
|
+
const progress = GenieProgressPartDataSchema.safeParse(part.data);
|
|
22
|
+
if (!progress.success) continue;
|
|
23
|
+
const event = byId.get(progress.data.toolCallId);
|
|
24
|
+
if (!event) continue;
|
|
25
|
+
event.progress = [...(event.progress ?? []), progress.data.event];
|
|
26
|
+
}
|
|
27
|
+
return events;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Merge persisted native parts with richer live progress by tool-call id. */
|
|
31
|
+
export function mergeToolEvents(
|
|
32
|
+
persisted: ToolEvent[],
|
|
33
|
+
live: ToolEvent[] | undefined,
|
|
34
|
+
): ToolEvent[] {
|
|
35
|
+
const merged = new Map(persisted.map((event) => [event.id, event]));
|
|
36
|
+
for (const event of live ?? []) {
|
|
37
|
+
const prior = merged.get(event.id);
|
|
38
|
+
merged.set(event.id, prior ? { ...prior, ...event } : event);
|
|
39
|
+
}
|
|
40
|
+
return [...merged.values()];
|
|
41
|
+
}
|