@mastra/client-js 0.10.22-alpha.3 → 0.10.22-alpha.5
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/.turbo/turbo-build.log +7 -7
- package/CHANGELOG.md +30 -0
- package/dist/index.cjs +502 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +502 -0
- package/dist/index.js.map +1 -1
- package/dist/resources/agent.d.ts +11 -1
- package/dist/resources/agent.d.ts.map +1 -1
- package/dist/types.d.ts +9 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/process-mastra-stream.d.ts +7 -0
- package/dist/utils/process-mastra-stream.d.ts.map +1 -0
- package/package.json +5 -5
- package/src/resources/agent.ts +647 -2
- package/src/types.ts +16 -1
- package/src/utils/process-mastra-stream.test.ts +353 -0
- package/src/utils/process-mastra-stream.ts +49 -0
package/src/types.ts
CHANGED
|
@@ -11,7 +11,14 @@ import type {
|
|
|
11
11
|
PaginationInfo,
|
|
12
12
|
MastraMessageV2,
|
|
13
13
|
} from '@mastra/core';
|
|
14
|
-
import type {
|
|
14
|
+
import type {
|
|
15
|
+
AgentExecutionOptions,
|
|
16
|
+
AgentGenerateOptions,
|
|
17
|
+
AgentStreamOptions,
|
|
18
|
+
ToolsInput,
|
|
19
|
+
UIMessageWithMetadata,
|
|
20
|
+
} from '@mastra/core/agent';
|
|
21
|
+
import type { MessageListInput } from '@mastra/core/agent/message-list';
|
|
15
22
|
import type { BaseLogMessage, LogLevel } from '@mastra/core/logger';
|
|
16
23
|
|
|
17
24
|
import type { MCPToolType, ServerInfo } from '@mastra/core/mcp';
|
|
@@ -65,6 +72,7 @@ export interface GetAgentResponse {
|
|
|
65
72
|
workflows: Record<string, GetWorkflowResponse>;
|
|
66
73
|
provider: string;
|
|
67
74
|
modelId: string;
|
|
75
|
+
modelVersion: string;
|
|
68
76
|
defaultGenerateOptions: WithoutMethods<AgentGenerateOptions>;
|
|
69
77
|
defaultStreamOptions: WithoutMethods<AgentStreamOptions>;
|
|
70
78
|
}
|
|
@@ -89,6 +97,13 @@ export type StreamParams<T extends JSONSchema7 | ZodSchema | undefined = undefin
|
|
|
89
97
|
Omit<AgentStreamOptions<T>, 'output' | 'experimental_output' | 'runtimeContext' | 'clientTools' | 'abortSignal'>
|
|
90
98
|
>;
|
|
91
99
|
|
|
100
|
+
export type StreamVNextParams<T extends JSONSchema7 | ZodSchema | undefined = undefined> = {
|
|
101
|
+
messages: MessageListInput;
|
|
102
|
+
output?: T;
|
|
103
|
+
runtimeContext?: RuntimeContext | Record<string, any>;
|
|
104
|
+
clientTools?: ToolsInput;
|
|
105
|
+
} & WithoutMethods<Omit<AgentExecutionOptions<T>, 'output' | 'runtimeContext' | 'clientTools' | 'options'>>;
|
|
106
|
+
|
|
92
107
|
export type UpdateModelParams = {
|
|
93
108
|
modelId: string;
|
|
94
109
|
provider: 'openai' | 'anthropic' | 'groq' | 'xai' | 'google';
|
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
import { describe, expect, it, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { processMastraStream } from './process-mastra-stream';
|
|
3
|
+
import type { ChunkType } from '@mastra/core/stream';
|
|
4
|
+
import { ReadableStream } from 'stream/web';
|
|
5
|
+
|
|
6
|
+
describe('processMastraStream', () => {
|
|
7
|
+
let mockOnChunk: ReturnType<typeof vi.fn>;
|
|
8
|
+
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
mockOnChunk = vi.fn().mockResolvedValue(undefined);
|
|
11
|
+
vi.clearAllMocks();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
const createMockStream = (data: string): ReadableStream<Uint8Array> => {
|
|
15
|
+
return new ReadableStream({
|
|
16
|
+
start(controller) {
|
|
17
|
+
const encoder = new TextEncoder();
|
|
18
|
+
controller.enqueue(encoder.encode(data));
|
|
19
|
+
controller.close();
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const createChunkedMockStream = (chunks: string[]): ReadableStream<Uint8Array> => {
|
|
25
|
+
let currentIndex = 0;
|
|
26
|
+
return new ReadableStream({
|
|
27
|
+
start(controller) {
|
|
28
|
+
const encoder = new TextEncoder();
|
|
29
|
+
|
|
30
|
+
const pushNext = () => {
|
|
31
|
+
if (currentIndex < chunks.length) {
|
|
32
|
+
controller.enqueue(encoder.encode(chunks[currentIndex]));
|
|
33
|
+
currentIndex++;
|
|
34
|
+
// Simulate async processing
|
|
35
|
+
setTimeout(pushNext, 10);
|
|
36
|
+
} else {
|
|
37
|
+
controller.close();
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
pushNext();
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
it('should process valid SSE messages and call onChunk', async () => {
|
|
47
|
+
const testChunk: ChunkType = {
|
|
48
|
+
type: 'test',
|
|
49
|
+
runId: 'run-123',
|
|
50
|
+
from: 'agent',
|
|
51
|
+
payload: { message: 'hello' },
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const sseData = `data: ${JSON.stringify(testChunk)}\n\n`;
|
|
55
|
+
const stream = createMockStream(sseData);
|
|
56
|
+
|
|
57
|
+
await processMastraStream({
|
|
58
|
+
stream,
|
|
59
|
+
onChunk: mockOnChunk,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
expect(mockOnChunk).toHaveBeenCalledTimes(1);
|
|
63
|
+
expect(mockOnChunk).toHaveBeenCalledWith(testChunk);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('should process multiple SSE messages in sequence', async () => {
|
|
67
|
+
const testChunk1: ChunkType = {
|
|
68
|
+
type: 'message',
|
|
69
|
+
runId: 'run-123',
|
|
70
|
+
from: 'agent',
|
|
71
|
+
payload: { text: 'first message' },
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const testChunk2: ChunkType = {
|
|
75
|
+
type: 'message',
|
|
76
|
+
runId: 'run-123',
|
|
77
|
+
from: 'agent',
|
|
78
|
+
payload: { text: 'second message' },
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const sseData = `data: ${JSON.stringify(testChunk1)}\n\ndata: ${JSON.stringify(testChunk2)}\n\n`;
|
|
82
|
+
const stream = createMockStream(sseData);
|
|
83
|
+
|
|
84
|
+
await processMastraStream({
|
|
85
|
+
stream,
|
|
86
|
+
onChunk: mockOnChunk,
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
expect(mockOnChunk).toHaveBeenCalledTimes(2);
|
|
90
|
+
expect(mockOnChunk).toHaveBeenNthCalledWith(1, testChunk1);
|
|
91
|
+
expect(mockOnChunk).toHaveBeenNthCalledWith(2, testChunk2);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('should handle [DONE] marker and terminate stream processing', async () => {
|
|
95
|
+
const testChunk: ChunkType = {
|
|
96
|
+
type: 'message',
|
|
97
|
+
runId: 'run-123',
|
|
98
|
+
from: 'agent',
|
|
99
|
+
payload: { text: 'message before done' },
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const sseData = `data: ${JSON.stringify(testChunk)}\n\ndata: [DONE]\n\n`;
|
|
103
|
+
const stream = createMockStream(sseData);
|
|
104
|
+
|
|
105
|
+
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
106
|
+
|
|
107
|
+
await processMastraStream({
|
|
108
|
+
stream,
|
|
109
|
+
onChunk: mockOnChunk,
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
expect(mockOnChunk).toHaveBeenCalledTimes(1);
|
|
113
|
+
expect(mockOnChunk).toHaveBeenCalledWith(testChunk);
|
|
114
|
+
expect(consoleSpy).toHaveBeenCalledWith('🏁 Stream finished');
|
|
115
|
+
|
|
116
|
+
consoleSpy.mockRestore();
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('should handle JSON parsing errors gracefully', async () => {
|
|
120
|
+
const invalidJson = 'data: {invalid json}\n\n';
|
|
121
|
+
const validChunk: ChunkType = {
|
|
122
|
+
type: 'message',
|
|
123
|
+
runId: 'run-123',
|
|
124
|
+
from: 'agent',
|
|
125
|
+
payload: { text: 'valid message' },
|
|
126
|
+
};
|
|
127
|
+
const validData = `data: ${JSON.stringify(validChunk)}\n\n`;
|
|
128
|
+
|
|
129
|
+
const sseData = invalidJson + validData;
|
|
130
|
+
const stream = createMockStream(sseData);
|
|
131
|
+
|
|
132
|
+
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
133
|
+
|
|
134
|
+
await processMastraStream({
|
|
135
|
+
stream,
|
|
136
|
+
onChunk: mockOnChunk,
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// Should have called onChunk only for the valid message
|
|
140
|
+
expect(mockOnChunk).toHaveBeenCalledTimes(1);
|
|
141
|
+
expect(mockOnChunk).toHaveBeenCalledWith(validChunk);
|
|
142
|
+
|
|
143
|
+
// Should have logged the JSON parsing error
|
|
144
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
145
|
+
'❌ JSON parse error:',
|
|
146
|
+
expect.any(SyntaxError),
|
|
147
|
+
'Data:',
|
|
148
|
+
'{invalid json}',
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
consoleErrorSpy.mockRestore();
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('should handle incomplete SSE messages across chunks', async () => {
|
|
155
|
+
const testChunk: ChunkType = {
|
|
156
|
+
type: 'message',
|
|
157
|
+
runId: 'run-123',
|
|
158
|
+
from: 'agent',
|
|
159
|
+
payload: { text: 'complete message' },
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
// Split the SSE message across multiple chunks
|
|
163
|
+
const chunks = [
|
|
164
|
+
'data: {"type":"message","runId":"run-123"',
|
|
165
|
+
',"from":"agent","payload":{"text":"complete message"}}\n\n',
|
|
166
|
+
];
|
|
167
|
+
|
|
168
|
+
const stream = createChunkedMockStream(chunks);
|
|
169
|
+
|
|
170
|
+
await processMastraStream({
|
|
171
|
+
stream,
|
|
172
|
+
onChunk: mockOnChunk,
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
expect(mockOnChunk).toHaveBeenCalledTimes(1);
|
|
176
|
+
expect(mockOnChunk).toHaveBeenCalledWith(testChunk);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('should handle empty stream', async () => {
|
|
180
|
+
const stream = createMockStream('');
|
|
181
|
+
|
|
182
|
+
await processMastraStream({
|
|
183
|
+
stream,
|
|
184
|
+
onChunk: mockOnChunk,
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
expect(mockOnChunk).not.toHaveBeenCalled();
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('should ignore non-data lines', async () => {
|
|
191
|
+
const testChunk: ChunkType = {
|
|
192
|
+
type: 'message',
|
|
193
|
+
runId: 'run-123',
|
|
194
|
+
from: 'agent',
|
|
195
|
+
payload: { text: 'valid message' },
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
// SSE format: each line ends with \n, and messages are separated by \n\n
|
|
199
|
+
const sseData = `event: test-event\nid: 123\n\ndata: ${JSON.stringify(testChunk)}\n\nretry: 5000\n\n`;
|
|
200
|
+
|
|
201
|
+
const stream = createMockStream(sseData);
|
|
202
|
+
|
|
203
|
+
await processMastraStream({
|
|
204
|
+
stream,
|
|
205
|
+
onChunk: mockOnChunk,
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
expect(mockOnChunk).toHaveBeenCalledTimes(1);
|
|
209
|
+
expect(mockOnChunk).toHaveBeenCalledWith(testChunk);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('should properly clean up stream reader resources', async () => {
|
|
213
|
+
const testChunk: ChunkType = {
|
|
214
|
+
type: 'message',
|
|
215
|
+
runId: 'run-123',
|
|
216
|
+
from: 'agent',
|
|
217
|
+
payload: { text: 'test message' },
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
const sseData = `data: ${JSON.stringify(testChunk)}\n\n`;
|
|
221
|
+
const stream = createMockStream(sseData);
|
|
222
|
+
|
|
223
|
+
// Spy on the reader's releaseLock method
|
|
224
|
+
const reader = stream.getReader();
|
|
225
|
+
const releaseLockSpy = vi.spyOn(reader, 'releaseLock');
|
|
226
|
+
reader.releaseLock(); // Release it so processMastraStream can get it
|
|
227
|
+
|
|
228
|
+
await processMastraStream({
|
|
229
|
+
stream,
|
|
230
|
+
onChunk: mockOnChunk,
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
// The function should have called releaseLock in the finally block
|
|
234
|
+
expect(releaseLockSpy).toHaveBeenCalled();
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('should handle onChunk errors by logging them as JSON parse errors', async () => {
|
|
238
|
+
const testChunk: ChunkType = {
|
|
239
|
+
type: 'message',
|
|
240
|
+
runId: 'run-123',
|
|
241
|
+
from: 'agent',
|
|
242
|
+
payload: { text: 'first message' },
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
const sseData = `data: ${JSON.stringify(testChunk)}\n\n`;
|
|
246
|
+
const stream = createMockStream(sseData);
|
|
247
|
+
|
|
248
|
+
// Make the call to onChunk reject
|
|
249
|
+
mockOnChunk.mockRejectedValueOnce(new Error('onChunk error'));
|
|
250
|
+
|
|
251
|
+
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
252
|
+
|
|
253
|
+
// Should not throw an error but handle it gracefully
|
|
254
|
+
await processMastraStream({
|
|
255
|
+
stream,
|
|
256
|
+
onChunk: mockOnChunk,
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
expect(mockOnChunk).toHaveBeenCalledTimes(1);
|
|
260
|
+
expect(mockOnChunk).toHaveBeenCalledWith(testChunk);
|
|
261
|
+
|
|
262
|
+
// Should log the onChunk error as a JSON parse error
|
|
263
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
264
|
+
'❌ JSON parse error:',
|
|
265
|
+
expect.any(Error),
|
|
266
|
+
'Data:',
|
|
267
|
+
JSON.stringify(testChunk),
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
consoleErrorSpy.mockRestore();
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it('should handle stream read errors', async () => {
|
|
274
|
+
const errorMessage = 'Stream read error';
|
|
275
|
+
const stream = new ReadableStream({
|
|
276
|
+
start(controller) {
|
|
277
|
+
controller.error(new Error(errorMessage));
|
|
278
|
+
},
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
await expect(
|
|
282
|
+
processMastraStream({
|
|
283
|
+
stream,
|
|
284
|
+
onChunk: mockOnChunk,
|
|
285
|
+
}),
|
|
286
|
+
).rejects.toThrow(errorMessage);
|
|
287
|
+
|
|
288
|
+
expect(mockOnChunk).not.toHaveBeenCalled();
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it('should handle mixed valid and invalid data lines', async () => {
|
|
292
|
+
const validChunk1: ChunkType = {
|
|
293
|
+
type: 'message',
|
|
294
|
+
runId: 'run-123',
|
|
295
|
+
from: 'agent',
|
|
296
|
+
payload: { text: 'first valid message' },
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
const validChunk2: ChunkType = {
|
|
300
|
+
type: 'message',
|
|
301
|
+
runId: 'run-123',
|
|
302
|
+
from: 'agent',
|
|
303
|
+
payload: { text: 'second valid message' },
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
const sseData = `data: ${JSON.stringify(validChunk1)}\n\ndata: {invalid json}\n\ndata: ${JSON.stringify(validChunk2)}\n\ndata: [DONE]\n\n`;
|
|
307
|
+
|
|
308
|
+
const stream = createMockStream(sseData);
|
|
309
|
+
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
310
|
+
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
311
|
+
|
|
312
|
+
await processMastraStream({
|
|
313
|
+
stream,
|
|
314
|
+
onChunk: mockOnChunk,
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
expect(mockOnChunk).toHaveBeenCalledTimes(2);
|
|
318
|
+
expect(mockOnChunk).toHaveBeenNthCalledWith(1, validChunk1);
|
|
319
|
+
expect(mockOnChunk).toHaveBeenNthCalledWith(2, validChunk2);
|
|
320
|
+
|
|
321
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
322
|
+
'❌ JSON parse error:',
|
|
323
|
+
expect.any(SyntaxError),
|
|
324
|
+
'Data:',
|
|
325
|
+
'{invalid json}',
|
|
326
|
+
);
|
|
327
|
+
expect(consoleSpy).toHaveBeenCalledWith('🏁 Stream finished');
|
|
328
|
+
|
|
329
|
+
consoleErrorSpy.mockRestore();
|
|
330
|
+
consoleSpy.mockRestore();
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
it('should handle data lines without "data: " prefix', async () => {
|
|
334
|
+
const testChunk: ChunkType = {
|
|
335
|
+
type: 'message',
|
|
336
|
+
runId: 'run-123',
|
|
337
|
+
from: 'agent',
|
|
338
|
+
payload: { text: 'valid message' },
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
const sseData = `some random line\n\ndata: ${JSON.stringify(testChunk)}\n\nanother line without prefix\n\n`;
|
|
342
|
+
|
|
343
|
+
const stream = createMockStream(sseData);
|
|
344
|
+
|
|
345
|
+
await processMastraStream({
|
|
346
|
+
stream,
|
|
347
|
+
onChunk: mockOnChunk,
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
expect(mockOnChunk).toHaveBeenCalledTimes(1);
|
|
351
|
+
expect(mockOnChunk).toHaveBeenCalledWith(testChunk);
|
|
352
|
+
});
|
|
353
|
+
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { ReadableStream } from 'stream/web';
|
|
2
|
+
import type { ChunkType } from '@mastra/core/stream';
|
|
3
|
+
|
|
4
|
+
export async function processMastraStream({
|
|
5
|
+
stream,
|
|
6
|
+
onChunk,
|
|
7
|
+
}: {
|
|
8
|
+
stream: ReadableStream<Uint8Array>;
|
|
9
|
+
onChunk: (chunk: ChunkType) => Promise<void>;
|
|
10
|
+
}) {
|
|
11
|
+
const reader = stream.getReader();
|
|
12
|
+
const decoder = new TextDecoder();
|
|
13
|
+
let buffer = '';
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
while (true) {
|
|
17
|
+
const { done, value } = await reader.read();
|
|
18
|
+
|
|
19
|
+
if (done) break;
|
|
20
|
+
|
|
21
|
+
// Decode the chunk and add to buffer
|
|
22
|
+
buffer += decoder.decode(value, { stream: true });
|
|
23
|
+
|
|
24
|
+
// Process complete SSE messages
|
|
25
|
+
const lines = buffer.split('\n\n');
|
|
26
|
+
buffer = lines.pop() || ''; // Keep incomplete line in buffer
|
|
27
|
+
|
|
28
|
+
for (const line of lines) {
|
|
29
|
+
if (line.startsWith('data: ')) {
|
|
30
|
+
const data = line.slice(6); // Remove 'data: '
|
|
31
|
+
|
|
32
|
+
if (data === '[DONE]') {
|
|
33
|
+
console.log('🏁 Stream finished');
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
const json = JSON.parse(data);
|
|
39
|
+
await onChunk(json);
|
|
40
|
+
} catch (error) {
|
|
41
|
+
console.error('❌ JSON parse error:', error, 'Data:', data);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
} finally {
|
|
47
|
+
reader.releaseLock();
|
|
48
|
+
}
|
|
49
|
+
}
|