@dbx-tools/ui-mastra 0.3.7 → 0.3.8
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 +16 -3
- package/package.json +7 -7
- package/src/react/chat-view.tsx +4 -1
- package/src/react/mastra-chat.tsx +81 -41
- package/src/react/thread-sidebar.tsx +36 -6
- package/src/react/types.ts +8 -1
- package/src/support/mastra-client.ts +95 -77
package/README.md
CHANGED
|
@@ -140,13 +140,26 @@ import {
|
|
|
140
140
|
} from "@dbx-tools/ui-mastra/react";
|
|
141
141
|
|
|
142
142
|
const client = new MastraPluginClient(clientConfig);
|
|
143
|
-
|
|
144
|
-
|
|
143
|
+
|
|
144
|
+
// Routing (thread + model) is passed per call, so concurrent runs on
|
|
145
|
+
// different threads never share state.
|
|
146
|
+
const stream = await client.streamAgent({
|
|
147
|
+
agentId: client.defaultAgent,
|
|
148
|
+
messages: [{ role: "user", content: "Hello" }],
|
|
149
|
+
runId,
|
|
150
|
+
threadId: activeThreadId,
|
|
151
|
+
model: "claude sonnet",
|
|
152
|
+
signal: controller.signal,
|
|
153
|
+
});
|
|
145
154
|
|
|
146
155
|
const models = await client.models();
|
|
147
|
-
const history = await client.history({ page: 0, perPage: 20 });
|
|
156
|
+
const history = await client.history({ threadId: activeThreadId, page: 0, perPage: 20 });
|
|
148
157
|
```
|
|
149
158
|
|
|
159
|
+
Each conversation thread runs independently: start a turn on one thread, switch
|
|
160
|
+
to another and start a second, and both stream concurrently. Cancel one via its
|
|
161
|
+
`AbortSignal` (or the driver's `onCancelThread`) without touching the others.
|
|
162
|
+
|
|
150
163
|
`MastraPluginClient` extends `@mastra/client-js` with the AppKit-Mastra custom
|
|
151
164
|
routes. It uses `credentials: "include"` so session cookies travel with streaming
|
|
152
165
|
and REST calls. The React hooks wrap common route calls for model catalogues,
|
package/package.json
CHANGED
|
@@ -26,19 +26,19 @@
|
|
|
26
26
|
"shiki": "^3.0.0",
|
|
27
27
|
"sql-formatter": "^15.6.9",
|
|
28
28
|
"streamdown": "^2.5.0",
|
|
29
|
-
"@dbx-tools/shared-core": "0.3.
|
|
30
|
-
"@dbx-tools/shared-
|
|
31
|
-
"@dbx-tools/shared-
|
|
32
|
-
"@dbx-tools/ui-appkit": "0.3.
|
|
33
|
-
"@dbx-tools/shared-
|
|
34
|
-
"@dbx-tools/ui-branding": "0.3.
|
|
29
|
+
"@dbx-tools/shared-core": "0.3.8",
|
|
30
|
+
"@dbx-tools/shared-mastra": "0.3.8",
|
|
31
|
+
"@dbx-tools/shared-model": "0.3.8",
|
|
32
|
+
"@dbx-tools/ui-appkit": "0.3.8",
|
|
33
|
+
"@dbx-tools/shared-genie": "0.3.8",
|
|
34
|
+
"@dbx-tools/ui-branding": "0.3.8"
|
|
35
35
|
},
|
|
36
36
|
"main": "index.ts",
|
|
37
37
|
"license": "UNLICENSED",
|
|
38
38
|
"publishConfig": {
|
|
39
39
|
"access": "public"
|
|
40
40
|
},
|
|
41
|
-
"version": "0.3.
|
|
41
|
+
"version": "0.3.8",
|
|
42
42
|
"types": "index.ts",
|
|
43
43
|
"type": "module",
|
|
44
44
|
"exports": {
|
package/src/react/chat-view.tsx
CHANGED
|
@@ -124,6 +124,7 @@ export const ChatView = ({
|
|
|
124
124
|
onNewThread,
|
|
125
125
|
onDeleteThread,
|
|
126
126
|
onRenameThread,
|
|
127
|
+
onCancelThread,
|
|
127
128
|
sidebarOpen: sidebarOpenProp,
|
|
128
129
|
onToggleSidebar,
|
|
129
130
|
onExportConversation,
|
|
@@ -424,6 +425,7 @@ export const ChatView = ({
|
|
|
424
425
|
: {})}
|
|
425
426
|
{...(onDeleteThread ? { onDelete: onDeleteThread } : {})}
|
|
426
427
|
{...(onRenameThread ? { onRename: onRenameThread } : {})}
|
|
428
|
+
{...(onCancelThread ? { onCancel: onCancelThread } : {})}
|
|
427
429
|
onHide={toggleSidebar}
|
|
428
430
|
className="relative z-10 w-[85vw] max-w-xs shadow-xl"
|
|
429
431
|
/>
|
|
@@ -444,6 +446,7 @@ export const ChatView = ({
|
|
|
444
446
|
{...(onNewThread ? { onNew: onNewThread } : {})}
|
|
445
447
|
{...(onDeleteThread ? { onDelete: onDeleteThread } : {})}
|
|
446
448
|
{...(onRenameThread ? { onRename: onRenameThread } : {})}
|
|
449
|
+
{...(onCancelThread ? { onCancel: onCancelThread } : {})}
|
|
447
450
|
onHide={toggleSidebar}
|
|
448
451
|
/>
|
|
449
452
|
)
|
|
@@ -630,7 +633,7 @@ export const ChatView = ({
|
|
|
630
633
|
type="button"
|
|
631
634
|
size="icon-sm"
|
|
632
635
|
variant="default"
|
|
633
|
-
onClick={onStop}
|
|
636
|
+
onClick={() => onStop()}
|
|
634
637
|
aria-label="Stop response"
|
|
635
638
|
>
|
|
636
639
|
<SquareIcon className="size-3 fill-current" />
|
|
@@ -246,18 +246,20 @@ export const useMastraChat = (
|
|
|
246
246
|
// using it as a hook dep doesn't refire the initial-history fetch on
|
|
247
247
|
// every parent render.
|
|
248
248
|
const mastraClient = useMastraClient();
|
|
249
|
-
//
|
|
250
|
-
//
|
|
251
|
-
//
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
249
|
+
// The selected model rides each turn as a per-call override (see
|
|
250
|
+
// `runStream`), never stored on the shared client - so two threads can
|
|
251
|
+
// stream under different models without clobbering each other. Mirror it
|
|
252
|
+
// into a ref so the send path reads the latest selection without adding
|
|
253
|
+
// `model` to its dependency list.
|
|
254
|
+
const modelRef = useRef(model);
|
|
255
|
+
modelRef.current = model;
|
|
255
256
|
const agentId = options.agentId ?? mastraClient.defaultAgent;
|
|
256
257
|
// Built-in conversation management. When on, the chat always drives an
|
|
257
258
|
// explicit client thread id (rather than leaning on the per-session
|
|
258
259
|
// cookie) so it can reference, persist, and switch between the
|
|
259
|
-
// conversations a user owns.
|
|
260
|
-
//
|
|
260
|
+
// conversations a user owns. Each call (stream / history / clear) carries
|
|
261
|
+
// its thread id per request, so many threads can run concurrently without
|
|
262
|
+
// sharing routing state.
|
|
261
263
|
const enableThreads = options.enableThreads !== false;
|
|
262
264
|
// Export is opt-in (default off): the host turns it on explicitly.
|
|
263
265
|
const enableExport = options.enableExport === true;
|
|
@@ -693,7 +695,7 @@ export const useMastraChat = (
|
|
|
693
695
|
async (
|
|
694
696
|
threadId: string,
|
|
695
697
|
assistantId: string,
|
|
696
|
-
open: () => Promise<MastraStreamResponse>,
|
|
698
|
+
open: (signal: AbortSignal) => Promise<MastraStreamResponse>,
|
|
697
699
|
) => {
|
|
698
700
|
const controller = new AbortController();
|
|
699
701
|
let token = 0;
|
|
@@ -712,10 +714,7 @@ export const useMastraChat = (
|
|
|
712
714
|
});
|
|
713
715
|
const runIdRef = { current: getSession(threadId).runId };
|
|
714
716
|
try {
|
|
715
|
-
const
|
|
716
|
-
threadId === DEFAULT_THREAD_SESSION_KEY ? undefined : threadId;
|
|
717
|
-
mastraClient.setThreadId(streamThreadId);
|
|
718
|
-
const stream = await open();
|
|
717
|
+
const stream = await open(controller.signal);
|
|
719
718
|
await processStream(threadId, stream, assistantId, runIdRef, controller.signal);
|
|
720
719
|
updateSession(threadId, (session) => {
|
|
721
720
|
if (session.runToken !== token) return session;
|
|
@@ -745,7 +744,7 @@ export const useMastraChat = (
|
|
|
745
744
|
}));
|
|
746
745
|
}
|
|
747
746
|
},
|
|
748
|
-
[getSession,
|
|
747
|
+
[getSession, processStream, refreshThreadsSoon, updateSession],
|
|
749
748
|
);
|
|
750
749
|
|
|
751
750
|
const runStream = useCallback(
|
|
@@ -757,35 +756,53 @@ export const useMastraChat = (
|
|
|
757
756
|
assistantId,
|
|
758
757
|
runId,
|
|
759
758
|
}));
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
759
|
+
// Capture this run's thread + model at send time and pass them per
|
|
760
|
+
// call, so a run keeps its own routing even if the user switches
|
|
761
|
+
// threads or changes the model picker while it streams.
|
|
762
|
+
const streamThreadId =
|
|
763
|
+
threadId === DEFAULT_THREAD_SESSION_KEY ? undefined : threadId;
|
|
764
|
+
const model = modelRef.current || undefined;
|
|
765
|
+
return driveStream(threadId, assistantId, (signal) => {
|
|
766
|
+
const messages = history.flatMap((m) =>
|
|
763
767
|
m.parts
|
|
764
768
|
.filter((p): p is { type: "text"; text: string } => p.type === "text")
|
|
765
769
|
.map((p) => ({ role: m.role, content: p.text })),
|
|
766
|
-
)
|
|
767
|
-
return
|
|
770
|
+
);
|
|
771
|
+
return mastraClient.streamAgent({
|
|
772
|
+
agentId,
|
|
773
|
+
messages,
|
|
768
774
|
runId,
|
|
769
|
-
|
|
775
|
+
threadId: streamThreadId,
|
|
776
|
+
model,
|
|
777
|
+
signal,
|
|
778
|
+
});
|
|
770
779
|
});
|
|
771
780
|
},
|
|
772
781
|
[driveStream, mastraClient, agentId, updateSession],
|
|
773
782
|
);
|
|
774
783
|
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
784
|
+
// Cancel a thread's in-flight run. Defaults to the active thread (the
|
|
785
|
+
// composer stop button), but takes an explicit thread id so the sidebar
|
|
786
|
+
// can cancel a background thread without switching to it. Aborting one
|
|
787
|
+
// thread's controller leaves every other run streaming - each run owns
|
|
788
|
+
// its own controller and routing.
|
|
789
|
+
const stop = useCallback(
|
|
790
|
+
(threadId?: string) => {
|
|
791
|
+
const key = threadId ?? activeKey;
|
|
792
|
+
updateSession(key, (session) => {
|
|
793
|
+
if (!isSessionRunning(session)) return session;
|
|
794
|
+
session.abortController?.abort();
|
|
795
|
+
return {
|
|
796
|
+
...session,
|
|
797
|
+
abortController: null,
|
|
798
|
+
runToken: session.runToken + 1,
|
|
799
|
+
error: null,
|
|
800
|
+
status: "ready",
|
|
801
|
+
};
|
|
802
|
+
});
|
|
803
|
+
},
|
|
804
|
+
[activeKey, updateSession],
|
|
805
|
+
);
|
|
789
806
|
|
|
790
807
|
/**
|
|
791
808
|
* Approve or deny an in-flight `requireApproval` tool call. The
|
|
@@ -830,10 +847,22 @@ export const useMastraChat = (
|
|
|
830
847
|
toolCallId,
|
|
831
848
|
runId,
|
|
832
849
|
});
|
|
833
|
-
|
|
850
|
+
const streamThreadId =
|
|
851
|
+
activeKey === DEFAULT_THREAD_SESSION_KEY ? undefined : activeKey;
|
|
852
|
+
await driveStream(activeKey, assistantId, (signal) =>
|
|
834
853
|
decision.approved
|
|
835
|
-
? mastraClient.approveToolCallStream(agentId, {
|
|
836
|
-
|
|
854
|
+
? mastraClient.approveToolCallStream(agentId, {
|
|
855
|
+
runId,
|
|
856
|
+
toolCallId,
|
|
857
|
+
threadId: streamThreadId,
|
|
858
|
+
signal,
|
|
859
|
+
})
|
|
860
|
+
: mastraClient.declineToolCallStream(agentId, {
|
|
861
|
+
runId,
|
|
862
|
+
toolCallId,
|
|
863
|
+
threadId: streamThreadId,
|
|
864
|
+
signal,
|
|
865
|
+
}),
|
|
837
866
|
);
|
|
838
867
|
},
|
|
839
868
|
[activeKey, driveStream, getSession, mastraClient, agentId, updateSession],
|
|
@@ -882,7 +911,7 @@ export const useMastraChat = (
|
|
|
882
911
|
const handleClear = useCallback(async () => {
|
|
883
912
|
const threadId = activeKey;
|
|
884
913
|
try {
|
|
885
|
-
const result = await mastraClient.clearHistory({ agentId });
|
|
914
|
+
const result = await mastraClient.clearHistory({ agentId, threadId: activeThreadId });
|
|
886
915
|
logger.info("history cleared", { cleared: result.cleared });
|
|
887
916
|
} catch (error) {
|
|
888
917
|
logger.error("history clear error", {
|
|
@@ -1039,7 +1068,6 @@ export const useMastraChat = (
|
|
|
1039
1068
|
// refetching or aborting other threads' runs.
|
|
1040
1069
|
useEffect(() => {
|
|
1041
1070
|
const threadId = activeKey;
|
|
1042
|
-
mastraClient.setThreadId(activeThreadId);
|
|
1043
1071
|
const session = getSession(threadId);
|
|
1044
1072
|
if (session.historyLoaded) {
|
|
1045
1073
|
setIsLoadingHistory(false);
|
|
@@ -1053,6 +1081,7 @@ export const useMastraChat = (
|
|
|
1053
1081
|
mastraClient
|
|
1054
1082
|
.history({
|
|
1055
1083
|
agentId,
|
|
1084
|
+
threadId: activeThreadId,
|
|
1056
1085
|
page: 0,
|
|
1057
1086
|
perPage: HISTORY_PAGE_SIZE,
|
|
1058
1087
|
signal: controller.signal,
|
|
@@ -1096,7 +1125,7 @@ export const useMastraChat = (
|
|
|
1096
1125
|
const page = session.historyPage;
|
|
1097
1126
|
updateSession(threadId, (current) => ({ ...current, historyPage: page + 1 }));
|
|
1098
1127
|
mastraClient
|
|
1099
|
-
.history({ agentId, page, perPage: HISTORY_PAGE_SIZE })
|
|
1128
|
+
.history({ agentId, threadId: activeThreadId, page, perPage: HISTORY_PAGE_SIZE })
|
|
1100
1129
|
.then((response) => {
|
|
1101
1130
|
const uiMessages = response.uiMessages as unknown as UIMessage[];
|
|
1102
1131
|
if (uiMessages.length > 0) {
|
|
@@ -1123,7 +1152,15 @@ export const useMastraChat = (
|
|
|
1123
1152
|
historyInFlightRef.current = false;
|
|
1124
1153
|
setIsLoadingMore(false);
|
|
1125
1154
|
});
|
|
1126
|
-
}, [
|
|
1155
|
+
}, [
|
|
1156
|
+
activeKey,
|
|
1157
|
+
activeThreadId,
|
|
1158
|
+
getSession,
|
|
1159
|
+
mastraClient,
|
|
1160
|
+
agentId,
|
|
1161
|
+
updateSession,
|
|
1162
|
+
writeMessages,
|
|
1163
|
+
]);
|
|
1127
1164
|
|
|
1128
1165
|
// Chat export (opt-in). Resolves `[chart:<id>]` / `[data:<id>]` embeds
|
|
1129
1166
|
// straight off the client so the export inlines the same charts /
|
|
@@ -1356,6 +1393,9 @@ export const useMastraChat = (
|
|
|
1356
1393
|
onNewThread: newThread,
|
|
1357
1394
|
onDeleteThread: deleteThread,
|
|
1358
1395
|
onRenameThread: renameThread,
|
|
1396
|
+
// Cancel a background thread's run from the sidebar without
|
|
1397
|
+
// switching to it.
|
|
1398
|
+
onCancelThread: stop,
|
|
1359
1399
|
// Persisted sidebar visibility, controlled from the driver so
|
|
1360
1400
|
// the show/hide choice survives reloads.
|
|
1361
1401
|
sidebarOpen,
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
MessageSquarePlusIcon,
|
|
14
14
|
PanelLeftIcon,
|
|
15
15
|
PencilIcon,
|
|
16
|
+
SquareIcon,
|
|
16
17
|
Trash2Icon,
|
|
17
18
|
} from "lucide-react";
|
|
18
19
|
import { useState } from "react";
|
|
@@ -42,6 +43,12 @@ export interface ThreadSidebarProps {
|
|
|
42
43
|
onDelete?: (threadId: string) => void;
|
|
43
44
|
/** Rename a thread. Per-row edit affordance (inline text field) hidden when omitted. */
|
|
44
45
|
onRename?: (threadId: string, title: string) => void;
|
|
46
|
+
/**
|
|
47
|
+
* Cancel a thread's in-flight run. When provided, a streaming row (one in
|
|
48
|
+
* {@link streamingThreadIds}) shows a stop button in place of its spinner
|
|
49
|
+
* on hover, so a background run can be cancelled without switching to it.
|
|
50
|
+
*/
|
|
51
|
+
onCancel?: (threadId: string) => void;
|
|
45
52
|
/** Collapse the sidebar. Renders the hide button in the header when provided. */
|
|
46
53
|
onHide?: () => void;
|
|
47
54
|
/** Extra classes merged onto the sidebar root. */
|
|
@@ -67,6 +74,7 @@ export const ThreadSidebar = ({
|
|
|
67
74
|
onNew,
|
|
68
75
|
onDelete,
|
|
69
76
|
onRename,
|
|
77
|
+
onCancel,
|
|
70
78
|
onHide,
|
|
71
79
|
className,
|
|
72
80
|
}: ThreadSidebarProps) => {
|
|
@@ -212,12 +220,34 @@ export const ThreadSidebar = ({
|
|
|
212
220
|
>
|
|
213
221
|
<div className="min-w-0 flex-1">
|
|
214
222
|
<div className="flex items-center gap-1.5 truncate">
|
|
215
|
-
{isStreaming &&
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
223
|
+
{isStreaming &&
|
|
224
|
+
(onCancel ? (
|
|
225
|
+
<Tooltip>
|
|
226
|
+
<TooltipTrigger asChild>
|
|
227
|
+
<Button
|
|
228
|
+
type="button"
|
|
229
|
+
variant="ghost"
|
|
230
|
+
size="icon"
|
|
231
|
+
onClick={(e) => {
|
|
232
|
+
e.stopPropagation();
|
|
233
|
+
onCancel(thread.id);
|
|
234
|
+
}}
|
|
235
|
+
aria-label="Stop generating"
|
|
236
|
+
className="size-4 shrink-0 text-primary"
|
|
237
|
+
>
|
|
238
|
+
{/* Spinner by default; swap to a stop square on hover/focus. */}
|
|
239
|
+
<Loader2Icon className="size-3 animate-spin group-hover:hidden" />
|
|
240
|
+
<SquareIcon className="hidden size-3 fill-current group-hover:block" />
|
|
241
|
+
</Button>
|
|
242
|
+
</TooltipTrigger>
|
|
243
|
+
<TooltipContent>Stop generating</TooltipContent>
|
|
244
|
+
</Tooltip>
|
|
245
|
+
) : (
|
|
246
|
+
<Loader2Icon
|
|
247
|
+
aria-label="Streaming"
|
|
248
|
+
className="size-3 shrink-0 animate-spin text-primary"
|
|
249
|
+
/>
|
|
250
|
+
))}
|
|
221
251
|
<span className="truncate">{threadTitle(thread)}</span>
|
|
222
252
|
</div>
|
|
223
253
|
{thread.updatedAt && (
|
package/src/react/types.ts
CHANGED
|
@@ -99,7 +99,7 @@ export type ChatViewProps = {
|
|
|
99
99
|
* to hide the Stop affordance (the Send button just disables while a
|
|
100
100
|
* response streams).
|
|
101
101
|
*/
|
|
102
|
-
onStop?: () => void;
|
|
102
|
+
onStop?: (threadId?: string) => void;
|
|
103
103
|
/** Extra classes merged onto the root layout container. */
|
|
104
104
|
className?: string;
|
|
105
105
|
/**
|
|
@@ -220,6 +220,13 @@ export type ChatViewProps = {
|
|
|
220
220
|
* affordance that swaps the title into an inline text field.
|
|
221
221
|
*/
|
|
222
222
|
onRenameThread?: (threadId: string, title: string) => void;
|
|
223
|
+
/**
|
|
224
|
+
* Cancel a thread's in-flight run by id, without switching to it. When
|
|
225
|
+
* provided, each running sidebar row shows a stop affordance next to its
|
|
226
|
+
* streaming indicator (see {@link streamingThreadIds}). Isolated to that
|
|
227
|
+
* thread - other threads keep streaming.
|
|
228
|
+
*/
|
|
229
|
+
onCancelThread?: (threadId: string) => void;
|
|
223
230
|
/**
|
|
224
231
|
* Controlled open/closed state for the conversation sidebar. When
|
|
225
232
|
* omitted the view manages its own (session-only) open state; pass
|
|
@@ -72,43 +72,55 @@ export class MastraPluginClient extends MastraClient {
|
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
/**
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
* (fuzzy-matched server-side) or a model class slug
|
|
80
|
-
* (`ModelClass.ChatFast` / `"chat-thinking"`); pass `undefined` to
|
|
81
|
-
* fall back to the agent's configured default.
|
|
75
|
+
* Build the per-request routing headers for a call: the thread-selection
|
|
76
|
+
* header (`THREAD_ID_HEADER`) so the server pins `RequestContext`'s thread
|
|
77
|
+
* id, and the model-override header (`MODEL_OVERRIDE_HEADER`) so the server
|
|
78
|
+
* swaps the resolved model for that request without an agent redeploy.
|
|
82
79
|
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
80
|
+
* Routing is per-CALL - never stored on the shared client - so concurrent
|
|
81
|
+
* runs on different threads / models can't clobber each other's headers.
|
|
82
|
+
* `threadId` is a conversation id (omit to fall back to the per-session
|
|
83
|
+
* cookie thread); `model` is a concrete endpoint name (fuzzy-matched
|
|
84
|
+
* server-side) or a model-class slug (`"chat-fast"` / `"chat-thinking"`).
|
|
87
85
|
*/
|
|
88
|
-
|
|
89
|
-
const headers
|
|
90
|
-
if (
|
|
91
|
-
|
|
86
|
+
#routingHeaders(routing: { threadId?: string; model?: string }): Record<string, string> {
|
|
87
|
+
const headers: Record<string, string> = {};
|
|
88
|
+
if (routing.threadId) headers[thread.THREAD_ID_HEADER] = routing.threadId;
|
|
89
|
+
if (routing.model) headers[override.MODEL_OVERRIDE_HEADER] = routing.model;
|
|
90
|
+
return headers;
|
|
92
91
|
}
|
|
93
92
|
|
|
94
93
|
/**
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
* Mutates the shared `options.headers` in place (like
|
|
103
|
-
* {@link setModelOverride}) so the client identity stays stable; the
|
|
104
|
-
* inherited `agent.stream()` and the custom routes
|
|
105
|
-
* ({@link history} / {@link clearHistory} / {@link threads}) all read
|
|
106
|
-
* these headers, so selecting a thread routes every call at once.
|
|
94
|
+
* Open an agent stream for one conversation turn, routed and cancelled
|
|
95
|
+
* PER CALL. Posts to the agent `/stream` route with this run's own thread
|
|
96
|
+
* id + model as request headers and its own `AbortSignal` on the fetch, so
|
|
97
|
+
* many threads can stream at once and aborting one leaves the others
|
|
98
|
+
* untouched. Reads the SSE directly (like the tool-approval streams) rather
|
|
99
|
+
* than through the inherited `agent.stream()`, whose shared-client headers
|
|
100
|
+
* and single client-level abort signal can't isolate concurrent runs.
|
|
107
101
|
*/
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
102
|
+
async streamAgent(params: {
|
|
103
|
+
agentId: string;
|
|
104
|
+
messages: Array<{ role: string; content: string }>;
|
|
105
|
+
runId: string;
|
|
106
|
+
threadId?: string;
|
|
107
|
+
model?: string;
|
|
108
|
+
signal?: AbortSignal;
|
|
109
|
+
}): Promise<MastraStreamResponse> {
|
|
110
|
+
const url = `${this.basePath}/agents/${encodeURIComponent(params.agentId)}/stream`;
|
|
111
|
+
const init: RequestInit = {
|
|
112
|
+
method: "POST",
|
|
113
|
+
credentials: "include",
|
|
114
|
+
headers: {
|
|
115
|
+
"Content-Type": "application/json",
|
|
116
|
+
...this.#routingHeaders(params),
|
|
117
|
+
},
|
|
118
|
+
body: JSON.stringify({ messages: params.messages, runId: params.runId }),
|
|
119
|
+
};
|
|
120
|
+
if (params.signal) init.signal = params.signal;
|
|
121
|
+
const response = await fetch(url, init);
|
|
122
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
123
|
+
return asMastraStreamResponse(response);
|
|
112
124
|
}
|
|
113
125
|
|
|
114
126
|
/**
|
|
@@ -120,7 +132,7 @@ export class MastraPluginClient extends MastraClient {
|
|
|
120
132
|
*/
|
|
121
133
|
async approveToolCallStream(
|
|
122
134
|
agentId: string,
|
|
123
|
-
params: { runId: string; toolCallId: string },
|
|
135
|
+
params: { runId: string; toolCallId: string; threadId?: string; signal?: AbortSignal },
|
|
124
136
|
): Promise<MastraStreamResponse> {
|
|
125
137
|
return this.#toolApprovalStream(agentId, "approve-tool-call", params);
|
|
126
138
|
}
|
|
@@ -131,7 +143,7 @@ export class MastraPluginClient extends MastraClient {
|
|
|
131
143
|
*/
|
|
132
144
|
async declineToolCallStream(
|
|
133
145
|
agentId: string,
|
|
134
|
-
params: { runId: string; toolCallId: string },
|
|
146
|
+
params: { runId: string; toolCallId: string; threadId?: string; signal?: AbortSignal },
|
|
135
147
|
): Promise<MastraStreamResponse> {
|
|
136
148
|
return this.#toolApprovalStream(agentId, "decline-tool-call", params);
|
|
137
149
|
}
|
|
@@ -139,24 +151,22 @@ export class MastraPluginClient extends MastraClient {
|
|
|
139
151
|
async #toolApprovalStream(
|
|
140
152
|
agentId: string,
|
|
141
153
|
route: "approve-tool-call" | "decline-tool-call",
|
|
142
|
-
params: { runId: string; toolCallId: string },
|
|
154
|
+
params: { runId: string; toolCallId: string; threadId?: string; signal?: AbortSignal },
|
|
143
155
|
): Promise<MastraStreamResponse> {
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
156
|
+
const url = `${this.basePath}/agents/${encodeURIComponent(agentId)}/${route}`;
|
|
157
|
+
const init: RequestInit = {
|
|
158
|
+
method: "POST",
|
|
159
|
+
credentials: "include",
|
|
160
|
+
headers: {
|
|
161
|
+
"Content-Type": "application/json",
|
|
162
|
+
...this.#routingHeaders({ threadId: params.threadId }),
|
|
150
163
|
},
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
headers: response.headers,
|
|
158
|
-
}),
|
|
159
|
-
);
|
|
164
|
+
body: JSON.stringify({ runId: params.runId, toolCallId: params.toolCallId }),
|
|
165
|
+
};
|
|
166
|
+
if (params.signal) init.signal = params.signal;
|
|
167
|
+
const response = await fetch(url, init);
|
|
168
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
169
|
+
return asMastraStreamResponse(response);
|
|
160
170
|
}
|
|
161
171
|
|
|
162
172
|
/**
|
|
@@ -209,11 +219,14 @@ export class MastraPluginClient extends MastraClient {
|
|
|
209
219
|
/**
|
|
210
220
|
* Fetch one page of thread history from `GET ${basePath}/history`.
|
|
211
221
|
* Messages come back oldest -> newest so the caller can prepend them
|
|
212
|
-
* to a live transcript.
|
|
222
|
+
* to a live transcript. `threadId` targets a specific conversation
|
|
223
|
+
* (sent as the `?threadId=` query so it doesn't depend on shared client
|
|
224
|
+
* state); omit it to read the per-session cookie thread.
|
|
213
225
|
*/
|
|
214
226
|
async history(
|
|
215
227
|
options: {
|
|
216
228
|
agentId?: string;
|
|
229
|
+
threadId?: string;
|
|
217
230
|
page?: number;
|
|
218
231
|
perPage?: number;
|
|
219
232
|
signal?: AbortSignal;
|
|
@@ -222,6 +235,7 @@ export class MastraPluginClient extends MastraClient {
|
|
|
222
235
|
const params = new URLSearchParams();
|
|
223
236
|
if (options.page !== undefined) params.set("page", String(options.page));
|
|
224
237
|
if (options.perPage !== undefined) params.set("perPage", String(options.perPage));
|
|
238
|
+
if (options.threadId) params.set(thread.THREAD_ID_QUERY, options.threadId);
|
|
225
239
|
const qs = params.toString();
|
|
226
240
|
const base = this.#agentScoped(routes.MASTRA_ROUTES.history, options.agentId);
|
|
227
241
|
return this.#getJson(
|
|
@@ -232,19 +246,26 @@ export class MastraPluginClient extends MastraClient {
|
|
|
232
246
|
}
|
|
233
247
|
|
|
234
248
|
/**
|
|
235
|
-
* Wipe
|
|
236
|
-
*
|
|
237
|
-
*
|
|
249
|
+
* Wipe a thread's history (`DELETE ${basePath}/history`). `threadId`
|
|
250
|
+
* targets a specific conversation via the thread-selection header (so it
|
|
251
|
+
* doesn't depend on shared client state); omit it to clear the per-session
|
|
252
|
+
* cookie thread. The session cookie that anchors the thread id is
|
|
253
|
+
* preserved - only the messages go away. Idempotent: a fresh thread reports
|
|
238
254
|
* `cleared: 0` without erroring.
|
|
239
255
|
*/
|
|
240
256
|
async clearHistory(
|
|
241
|
-
options: { agentId?: string; signal?: AbortSignal } = {},
|
|
257
|
+
options: { agentId?: string; threadId?: string; signal?: AbortSignal } = {},
|
|
242
258
|
): Promise<MastraClearHistoryResponse> {
|
|
243
259
|
return this.#mutateJson(
|
|
244
260
|
this.#agentScoped(routes.MASTRA_ROUTES.history, options.agentId),
|
|
245
261
|
"DELETE",
|
|
246
262
|
wire.MastraClearHistoryResponseSchema,
|
|
247
|
-
|
|
263
|
+
{
|
|
264
|
+
...(options.threadId
|
|
265
|
+
? { headers: { [thread.THREAD_ID_HEADER]: options.threadId } }
|
|
266
|
+
: {}),
|
|
267
|
+
...(options.signal ? { signal: options.signal } : {}),
|
|
268
|
+
},
|
|
248
269
|
);
|
|
249
270
|
}
|
|
250
271
|
|
|
@@ -282,9 +303,8 @@ export class MastraPluginClient extends MastraClient {
|
|
|
282
303
|
* clashing with the inherited `MastraClient.deleteThread`, which hits
|
|
283
304
|
* Mastra's stock thread route rather than our OBO-scoped, custom
|
|
284
305
|
* mount). The id is sent as the thread-selection header for this one
|
|
285
|
-
* call
|
|
286
|
-
*
|
|
287
|
-
* while the user stays on another. Idempotent: deleting an unknown /
|
|
306
|
+
* call, so the sidebar can remove any thread while the user stays on
|
|
307
|
+
* another (routing is per call, never shared). Idempotent: deleting an unknown /
|
|
288
308
|
* already-removed thread reports `deleted: false` without erroring.
|
|
289
309
|
*/
|
|
290
310
|
async removeThread(
|
|
@@ -306,9 +326,8 @@ export class MastraPluginClient extends MastraClient {
|
|
|
306
326
|
* Rename a single conversation thread via the plugin's own
|
|
307
327
|
* `PATCH ${basePath}/threads` route. The id travels as the
|
|
308
328
|
* thread-selection header for this one call (mirroring
|
|
309
|
-
* {@link removeThread}, so the sidebar can rename any thread
|
|
310
|
-
*
|
|
311
|
-
* `title` rides in the JSON body. The server enforces ownership and
|
|
329
|
+
* {@link removeThread}, so the sidebar can rename any thread; routing is
|
|
330
|
+
* per call, never shared) and the new `title` rides in the JSON body. The server enforces ownership and
|
|
312
331
|
* echoes back the updated thread, so the caller can reflect the new
|
|
313
332
|
* title immediately. Throws on an unknown / unowned thread (HTTP 404).
|
|
314
333
|
*/
|
|
@@ -384,13 +403,13 @@ export class MastraPluginClient extends MastraClient {
|
|
|
384
403
|
}
|
|
385
404
|
|
|
386
405
|
/**
|
|
387
|
-
* Snapshot of the client's
|
|
388
|
-
*
|
|
389
|
-
*
|
|
390
|
-
*
|
|
391
|
-
*
|
|
392
|
-
*
|
|
393
|
-
*
|
|
406
|
+
* Snapshot of the client's base headers for the custom-route fetches
|
|
407
|
+
* that don't go through `@mastra/client-js`'s own request pipeline.
|
|
408
|
+
* Returns a fresh object each call so a caller can safely add one-off
|
|
409
|
+
* headers without mutating the shared `options.headers`. Routing
|
|
410
|
+
* (thread selection, model override) is passed per call - via a
|
|
411
|
+
* `?threadId=` query or the thread-selection header on the individual
|
|
412
|
+
* request - not stored here, so concurrent calls never share routing.
|
|
394
413
|
*/
|
|
395
414
|
#defaultHeaders(): Record<string, string> {
|
|
396
415
|
return { ...((this.options.headers as Record<string, string>) ?? {}) };
|
|
@@ -416,9 +435,8 @@ export class MastraPluginClient extends MastraClient {
|
|
|
416
435
|
* `POST` / `DELETE` / `PATCH` + JSON-parse + schema-validate for the
|
|
417
436
|
* mutating routes (`clearHistory` / `removeThread` / `renameThread` /
|
|
418
437
|
* `feedback`). A JSON body, when present, sets `Content-Type`;
|
|
419
|
-
* `options.headers` add
|
|
420
|
-
* header for a targeted delete / rename) over the client's
|
|
421
|
-
* override headers.
|
|
438
|
+
* `options.headers` add per-call headers (e.g. the thread-selection
|
|
439
|
+
* header for a targeted delete / rename) over the client's base headers.
|
|
422
440
|
*/
|
|
423
441
|
async #mutateJson<T>(
|
|
424
442
|
url: string,
|
|
@@ -477,12 +495,12 @@ export const useMastraConfig = (): MastraClientConfig =>
|
|
|
477
495
|
/**
|
|
478
496
|
* The single {@link MastraPluginClient} for the plugin, built once from
|
|
479
497
|
* the published `basePath`. Use it for everything: stream a turn with
|
|
480
|
-
* `client.
|
|
481
|
-
* `client.history()`, resolve embeds with
|
|
482
|
-
* Rebuilt only when `basePath` / `defaultAgent`
|
|
483
|
-
* mount), so it's safe as a `useMemo` / `useCallback`
|
|
484
|
-
* dependency;
|
|
485
|
-
*
|
|
498
|
+
* `client.streamAgent({ agentId, messages, runId, threadId, model, signal })`,
|
|
499
|
+
* page history with `client.history()`, resolve embeds with
|
|
500
|
+
* `client.chart(id)`, etc. Rebuilt only when `basePath` / `defaultAgent`
|
|
501
|
+
* change (e.g. a custom mount), so it's safe as a `useMemo` / `useCallback`
|
|
502
|
+
* / `useEffect` dependency; per-turn thread + model routing is passed per
|
|
503
|
+
* call rather than stored on the client, so concurrent runs stay isolated.
|
|
486
504
|
*/
|
|
487
505
|
export const useMastraClient = (): MastraPluginClient => {
|
|
488
506
|
const config = useMastraConfig();
|