@mastra/client-js 0.0.0-mcp-changeset-20250707162621 → 0.0.0-message-list-update-20250715150321

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  parsePartialJson,
3
3
  processDataStream,
4
+ processTextStream,
4
5
  type JSONValue,
5
6
  type ReasoningUIPart,
6
7
  type TextUIPart,
@@ -14,6 +15,7 @@ import type { JSONSchema7 } from 'json-schema';
14
15
  import { ZodSchema } from 'zod';
15
16
  import { zodToJsonSchema } from '../utils/zod-to-json-schema';
16
17
  import { processClientTools } from '../utils/process-client-tools';
18
+ import { v4 as uuid } from '@lukeed/uuid';
17
19
 
18
20
  import type {
19
21
  GenerateParams,
@@ -144,12 +146,18 @@ export class Agent extends BaseResource {
144
146
  });
145
147
 
146
148
  if (response.finishReason === 'tool-calls') {
147
- for (const toolCall of (
149
+ const toolCalls = (
148
150
  response as unknown as {
149
151
  toolCalls: { toolName: string; args: any; toolCallId: string }[];
150
152
  messages: CoreMessage[];
151
153
  }
152
- ).toolCalls) {
154
+ ).toolCalls;
155
+
156
+ if (!toolCalls || !Array.isArray(toolCalls)) {
157
+ return response;
158
+ }
159
+
160
+ for (const toolCall of toolCalls) {
153
161
  const clientTool = params.clientTools?.[toolCall.toolName] as Tool;
154
162
 
155
163
  if (clientTool && clientTool.execute) {
@@ -198,6 +206,7 @@ export class Agent extends BaseResource {
198
206
  onFinish,
199
207
  getCurrentDate = () => new Date(),
200
208
  lastMessage,
209
+ streamProtocol,
201
210
  }: {
202
211
  stream: ReadableStream<Uint8Array>;
203
212
  update: (options: { message: UIMessage; data: JSONValue[] | undefined; replaceLastMessage: boolean }) => void;
@@ -206,6 +215,7 @@ export class Agent extends BaseResource {
206
215
  generateId?: () => string;
207
216
  getCurrentDate?: () => Date;
208
217
  lastMessage: UIMessage | undefined;
218
+ streamProtocol: 'text' | 'data';
209
219
  }) {
210
220
  const replaceLastMessage = lastMessage?.role === 'assistant';
211
221
  let step = replaceLastMessage
@@ -219,7 +229,7 @@ export class Agent extends BaseResource {
219
229
  const message: UIMessage = replaceLastMessage
220
230
  ? structuredClone(lastMessage)
221
231
  : {
222
- id: crypto.randomUUID(),
232
+ id: uuid(),
223
233
  createdAt: getCurrentDate(),
224
234
  role: 'assistant',
225
235
  content: '',
@@ -279,7 +289,7 @@ export class Agent extends BaseResource {
279
289
  // changes. This is why we need to add a revision id to ensure that the message
280
290
  // is updated with SWR (without it, the changes get stuck in SWR and are not
281
291
  // forwarded to rendering):
282
- revisionId: crypto.randomUUID(),
292
+ revisionId: uuid(),
283
293
  } as UIMessage;
284
294
 
285
295
  update({
@@ -289,297 +299,265 @@ export class Agent extends BaseResource {
289
299
  });
290
300
  }
291
301
 
292
- await processDataStream({
293
- stream,
294
- onTextPart(value) {
295
- if (currentTextPart == null) {
296
- currentTextPart = {
297
- type: 'text',
298
- text: value,
299
- };
300
- message.parts.push(currentTextPart);
301
- } else {
302
- currentTextPart.text += value;
303
- }
302
+ if (streamProtocol === 'text') {
303
+ await processTextStream({
304
+ stream,
305
+ onTextPart(value) {
306
+ message.content += value;
307
+ execUpdate();
308
+ },
309
+ });
304
310
 
305
- message.content += value;
306
- execUpdate();
307
- },
308
- onReasoningPart(value) {
309
- if (currentReasoningTextDetail == null) {
310
- currentReasoningTextDetail = { type: 'text', text: value };
311
- if (currentReasoningPart != null) {
312
- currentReasoningPart.details.push(currentReasoningTextDetail);
311
+ onFinish?.({ message, finishReason, usage });
312
+ } else {
313
+ await processDataStream({
314
+ stream,
315
+ onTextPart(value) {
316
+ if (currentTextPart == null) {
317
+ currentTextPart = {
318
+ type: 'text',
319
+ text: value,
320
+ };
321
+ message.parts.push(currentTextPart);
322
+ } else {
323
+ currentTextPart.text += value;
313
324
  }
314
- } else {
315
- currentReasoningTextDetail.text += value;
316
- }
317
-
318
- if (currentReasoningPart == null) {
319
- currentReasoningPart = {
320
- type: 'reasoning',
321
- reasoning: value,
322
- details: [currentReasoningTextDetail],
323
- };
324
- message.parts.push(currentReasoningPart);
325
- } else {
326
- currentReasoningPart.reasoning += value;
327
- }
328
-
329
- message.reasoning = (message.reasoning ?? '') + value;
330
-
331
- execUpdate();
332
- },
333
- onReasoningSignaturePart(value) {
334
- if (currentReasoningTextDetail != null) {
335
- currentReasoningTextDetail.signature = value.signature;
336
- }
337
- },
338
- onRedactedReasoningPart(value) {
339
- if (currentReasoningPart == null) {
340
- currentReasoningPart = {
341
- type: 'reasoning',
342
- reasoning: '',
343
- details: [],
344
- };
345
- message.parts.push(currentReasoningPart);
346
- }
347
-
348
- currentReasoningPart.details.push({
349
- type: 'redacted',
350
- data: value.data,
351
- });
352
-
353
- currentReasoningTextDetail = undefined;
354
325
 
355
- execUpdate();
356
- },
357
- onFilePart(value) {
358
- message.parts.push({
359
- type: 'file',
360
- mimeType: value.mimeType,
361
- data: value.data,
362
- });
326
+ message.content += value;
327
+ execUpdate();
328
+ },
329
+ onReasoningPart(value) {
330
+ if (currentReasoningTextDetail == null) {
331
+ currentReasoningTextDetail = { type: 'text', text: value };
332
+ if (currentReasoningPart != null) {
333
+ currentReasoningPart.details.push(currentReasoningTextDetail);
334
+ }
335
+ } else {
336
+ currentReasoningTextDetail.text += value;
337
+ }
363
338
 
364
- execUpdate();
365
- },
366
- onSourcePart(value) {
367
- message.parts.push({
368
- type: 'source',
369
- source: value,
370
- });
339
+ if (currentReasoningPart == null) {
340
+ currentReasoningPart = {
341
+ type: 'reasoning',
342
+ reasoning: value,
343
+ details: [currentReasoningTextDetail],
344
+ };
345
+ message.parts.push(currentReasoningPart);
346
+ } else {
347
+ currentReasoningPart.reasoning += value;
348
+ }
371
349
 
372
- execUpdate();
373
- },
374
- onToolCallStreamingStartPart(value) {
375
- if (message.toolInvocations == null) {
376
- message.toolInvocations = [];
377
- }
350
+ message.reasoning = (message.reasoning ?? '') + value;
378
351
 
379
- // add the partial tool call to the map
380
- partialToolCalls[value.toolCallId] = {
381
- text: '',
382
- step,
383
- toolName: value.toolName,
384
- index: message.toolInvocations.length,
385
- };
352
+ execUpdate();
353
+ },
354
+ onReasoningSignaturePart(value) {
355
+ if (currentReasoningTextDetail != null) {
356
+ currentReasoningTextDetail.signature = value.signature;
357
+ }
358
+ },
359
+ onRedactedReasoningPart(value) {
360
+ if (currentReasoningPart == null) {
361
+ currentReasoningPart = {
362
+ type: 'reasoning',
363
+ reasoning: '',
364
+ details: [],
365
+ };
366
+ message.parts.push(currentReasoningPart);
367
+ }
386
368
 
387
- const invocation = {
388
- state: 'partial-call',
389
- step,
390
- toolCallId: value.toolCallId,
391
- toolName: value.toolName,
392
- args: undefined,
393
- } as const;
369
+ currentReasoningPart.details.push({
370
+ type: 'redacted',
371
+ data: value.data,
372
+ });
394
373
 
395
- message.toolInvocations.push(invocation);
374
+ currentReasoningTextDetail = undefined;
396
375
 
397
- updateToolInvocationPart(value.toolCallId, invocation);
376
+ execUpdate();
377
+ },
378
+ onFilePart(value) {
379
+ message.parts.push({
380
+ type: 'file',
381
+ mimeType: value.mimeType,
382
+ data: value.data,
383
+ });
398
384
 
399
- execUpdate();
400
- },
401
- onToolCallDeltaPart(value) {
402
- const partialToolCall = partialToolCalls[value.toolCallId];
385
+ execUpdate();
386
+ },
387
+ onSourcePart(value) {
388
+ message.parts.push({
389
+ type: 'source',
390
+ source: value,
391
+ });
403
392
 
404
- partialToolCall!.text += value.argsTextDelta;
393
+ execUpdate();
394
+ },
395
+ onToolCallStreamingStartPart(value) {
396
+ if (message.toolInvocations == null) {
397
+ message.toolInvocations = [];
398
+ }
405
399
 
406
- const { value: partialArgs } = parsePartialJson(partialToolCall!.text);
400
+ // add the partial tool call to the map
401
+ partialToolCalls[value.toolCallId] = {
402
+ text: '',
403
+ step,
404
+ toolName: value.toolName,
405
+ index: message.toolInvocations.length,
406
+ };
407
407
 
408
- const invocation = {
409
- state: 'partial-call',
410
- step: partialToolCall!.step,
411
- toolCallId: value.toolCallId,
412
- toolName: partialToolCall!.toolName,
413
- args: partialArgs,
414
- } as const;
408
+ const invocation = {
409
+ state: 'partial-call',
410
+ step,
411
+ toolCallId: value.toolCallId,
412
+ toolName: value.toolName,
413
+ args: undefined,
414
+ } as const;
415
415
 
416
- message.toolInvocations![partialToolCall!.index] = invocation;
416
+ message.toolInvocations.push(invocation);
417
417
 
418
- updateToolInvocationPart(value.toolCallId, invocation);
418
+ updateToolInvocationPart(value.toolCallId, invocation);
419
419
 
420
- execUpdate();
421
- },
422
- async onToolCallPart(value) {
423
- const invocation = {
424
- state: 'call',
425
- step,
426
- ...value,
427
- } as const;
428
-
429
- if (partialToolCalls[value.toolCallId] != null) {
430
- // change the partial tool call to a full tool call
431
- message.toolInvocations![partialToolCalls[value.toolCallId]!.index] = invocation;
432
- } else {
433
- if (message.toolInvocations == null) {
434
- message.toolInvocations = [];
435
- }
420
+ execUpdate();
421
+ },
422
+ onToolCallDeltaPart(value) {
423
+ const partialToolCall = partialToolCalls[value.toolCallId];
436
424
 
437
- message.toolInvocations.push(invocation);
438
- }
425
+ partialToolCall!.text += value.argsTextDelta;
439
426
 
440
- updateToolInvocationPart(value.toolCallId, invocation);
427
+ const { value: partialArgs } = parsePartialJson(partialToolCall!.text);
441
428
 
442
- execUpdate();
429
+ const invocation = {
430
+ state: 'partial-call',
431
+ step: partialToolCall!.step,
432
+ toolCallId: value.toolCallId,
433
+ toolName: partialToolCall!.toolName,
434
+ args: partialArgs,
435
+ } as const;
443
436
 
444
- // invoke the onToolCall callback if it exists. This is blocking.
445
- // In the future we should make this non-blocking, which
446
- // requires additional state management for error handling etc.
447
- if (onToolCall) {
448
- const result = await onToolCall({ toolCall: value });
449
- if (result != null) {
450
- const invocation = {
451
- state: 'result',
452
- step,
453
- ...value,
454
- result,
455
- } as const;
437
+ message.toolInvocations![partialToolCall!.index] = invocation;
456
438
 
457
- // store the result in the tool invocation
458
- message.toolInvocations![message.toolInvocations!.length - 1] = invocation;
439
+ updateToolInvocationPart(value.toolCallId, invocation);
459
440
 
460
- updateToolInvocationPart(value.toolCallId, invocation);
441
+ execUpdate();
442
+ },
443
+ async onToolCallPart(value) {
444
+ const invocation = {
445
+ state: 'call',
446
+ step,
447
+ ...value,
448
+ } as const;
449
+
450
+ if (partialToolCalls[value.toolCallId] != null) {
451
+ // change the partial tool call to a full tool call
452
+ message.toolInvocations![partialToolCalls[value.toolCallId]!.index] = invocation;
453
+ } else {
454
+ if (message.toolInvocations == null) {
455
+ message.toolInvocations = [];
456
+ }
461
457
 
462
- execUpdate();
458
+ message.toolInvocations.push(invocation);
463
459
  }
464
- }
465
- },
466
- onToolResultPart(value) {
467
- const toolInvocations = message.toolInvocations;
468
460
 
469
- if (toolInvocations == null) {
470
- throw new Error('tool_result must be preceded by a tool_call');
471
- }
461
+ updateToolInvocationPart(value.toolCallId, invocation);
472
462
 
473
- // find if there is any tool invocation with the same toolCallId
474
- // and replace it with the result
475
- const toolInvocationIndex = toolInvocations.findIndex(invocation => invocation.toolCallId === value.toolCallId);
463
+ execUpdate();
476
464
 
477
- if (toolInvocationIndex === -1) {
478
- throw new Error('tool_result must be preceded by a tool_call with the same toolCallId');
479
- }
465
+ // invoke the onToolCall callback if it exists. This is blocking.
466
+ // In the future we should make this non-blocking, which
467
+ // requires additional state management for error handling etc.
468
+ if (onToolCall) {
469
+ const result = await onToolCall({ toolCall: value });
470
+ if (result != null) {
471
+ const invocation = {
472
+ state: 'result',
473
+ step,
474
+ ...value,
475
+ result,
476
+ } as const;
480
477
 
481
- const invocation = {
482
- ...toolInvocations[toolInvocationIndex],
483
- state: 'result' as const,
484
- ...value,
485
- } as const;
478
+ // store the result in the tool invocation
479
+ message.toolInvocations![message.toolInvocations!.length - 1] = invocation;
486
480
 
487
- toolInvocations[toolInvocationIndex] = invocation as ToolInvocation;
481
+ updateToolInvocationPart(value.toolCallId, invocation);
488
482
 
489
- updateToolInvocationPart(value.toolCallId, invocation as ToolInvocation);
483
+ execUpdate();
484
+ }
485
+ }
486
+ },
487
+ onToolResultPart(value) {
488
+ const toolInvocations = message.toolInvocations;
490
489
 
491
- execUpdate();
492
- },
493
- onDataPart(value) {
494
- data.push(...value);
495
- execUpdate();
496
- },
497
- onMessageAnnotationsPart(value) {
498
- if (messageAnnotations == null) {
499
- messageAnnotations = [...value];
500
- } else {
501
- messageAnnotations.push(...value);
502
- }
490
+ if (toolInvocations == null) {
491
+ throw new Error('tool_result must be preceded by a tool_call');
492
+ }
503
493
 
504
- execUpdate();
505
- },
506
- onFinishStepPart(value) {
507
- step += 1;
494
+ // find if there is any tool invocation with the same toolCallId
495
+ // and replace it with the result
496
+ const toolInvocationIndex = toolInvocations.findIndex(
497
+ invocation => invocation.toolCallId === value.toolCallId,
498
+ );
508
499
 
509
- // reset the current text and reasoning parts
510
- currentTextPart = value.isContinued ? currentTextPart : undefined;
511
- currentReasoningPart = undefined;
512
- currentReasoningTextDetail = undefined;
513
- },
514
- onStartStepPart(value) {
515
- // keep message id stable when we are updating an existing message:
516
- if (!replaceLastMessage) {
517
- message.id = value.messageId;
518
- }
500
+ if (toolInvocationIndex === -1) {
501
+ throw new Error('tool_result must be preceded by a tool_call with the same toolCallId');
502
+ }
519
503
 
520
- // add a step boundary part to the message
521
- message.parts.push({ type: 'step-start' });
522
- execUpdate();
523
- },
524
- onFinishMessagePart(value) {
525
- finishReason = value.finishReason;
526
- if (value.usage != null) {
527
- // usage = calculateLanguageModelUsage(value.usage);
528
- usage = value.usage;
529
- }
530
- },
531
- onErrorPart(error) {
532
- throw new Error(error);
533
- },
534
- });
504
+ const invocation = {
505
+ ...toolInvocations[toolInvocationIndex],
506
+ state: 'result' as const,
507
+ ...value,
508
+ } as const;
535
509
 
536
- onFinish?.({ message, finishReason, usage });
537
- }
510
+ toolInvocations[toolInvocationIndex] = invocation as ToolInvocation;
538
511
 
539
- /**
540
- * Streams a response from the agent
541
- * @param params - Stream parameters including prompt
542
- * @returns Promise containing the enhanced Response object with processDataStream method
543
- */
544
- async stream<T extends JSONSchema7 | ZodSchema | undefined = undefined>(
545
- params: StreamParams<T>,
546
- ): Promise<
547
- Response & {
548
- processDataStream: (options?: Omit<Parameters<typeof processDataStream>[0], 'stream'>) => Promise<void>;
549
- }
550
- > {
551
- const processedParams = {
552
- ...params,
553
- output: params.output ? zodToJsonSchema(params.output) : undefined,
554
- experimental_output: params.experimental_output ? zodToJsonSchema(params.experimental_output) : undefined,
555
- runtimeContext: parseClientRuntimeContext(params.runtimeContext),
556
- clientTools: processClientTools(params.clientTools),
557
- };
512
+ updateToolInvocationPart(value.toolCallId, invocation as ToolInvocation);
558
513
 
559
- // Create a readable stream that will handle the response processing
560
- const { readable, writable } = new TransformStream<Uint8Array, Uint8Array>();
514
+ execUpdate();
515
+ },
516
+ onDataPart(value) {
517
+ data.push(...value);
518
+ execUpdate();
519
+ },
520
+ onMessageAnnotationsPart(value) {
521
+ if (messageAnnotations == null) {
522
+ messageAnnotations = [...value];
523
+ } else {
524
+ messageAnnotations.push(...value);
525
+ }
561
526
 
562
- // Start processing the response in the background
563
- const response = await this.processStreamResponse(processedParams, writable);
527
+ execUpdate();
528
+ },
529
+ onFinishStepPart(value) {
530
+ step += 1;
564
531
 
565
- // Create a new response with the readable stream
566
- const streamResponse = new Response(readable, {
567
- status: response.status,
568
- statusText: response.statusText,
569
- headers: response.headers,
570
- }) as Response & {
571
- processDataStream: (options?: Omit<Parameters<typeof processDataStream>[0], 'stream'>) => Promise<void>;
572
- };
532
+ // reset the current text and reasoning parts
533
+ currentTextPart = value.isContinued ? currentTextPart : undefined;
534
+ currentReasoningPart = undefined;
535
+ currentReasoningTextDetail = undefined;
536
+ },
537
+ onStartStepPart(value) {
538
+ // keep message id stable when we are updating an existing message:
539
+ if (!replaceLastMessage) {
540
+ message.id = value.messageId;
541
+ }
573
542
 
574
- // Add the processDataStream method to the response
575
- streamResponse.processDataStream = async (options = {}) => {
576
- await processDataStream({
577
- stream: streamResponse.body as ReadableStream<Uint8Array>,
578
- ...options,
543
+ // add a step boundary part to the message
544
+ message.parts.push({ type: 'step-start' });
545
+ execUpdate();
546
+ },
547
+ onFinishMessagePart(value) {
548
+ finishReason = value.finishReason;
549
+ if (value.usage != null) {
550
+ // usage = calculateLanguageModelUsage(value.usage);
551
+ usage = value.usage;
552
+ }
553
+ },
554
+ onErrorPart(error) {
555
+ throw new Error(error);
556
+ },
579
557
  });
580
- };
581
558
 
582
- return streamResponse;
559
+ onFinish?.({ message, finishReason, usage });
560
+ }
583
561
  }
584
562
 
585
563
  /**
@@ -599,6 +577,7 @@ export class Agent extends BaseResource {
599
577
  }
600
578
 
601
579
  try {
580
+ const streamProtocol = processedParams.output ? 'text' : 'data';
602
581
  let toolCalls: ToolInvocation[] = [];
603
582
  let finishReasonToolCalls = false;
604
583
  let messages: UIMessage[] = [];
@@ -707,11 +686,14 @@ export class Agent extends BaseResource {
707
686
  }
708
687
  } else {
709
688
  setTimeout(() => {
710
- writable.close();
689
+ if (!writable.locked) {
690
+ writable.close();
691
+ }
711
692
  }, 0);
712
693
  }
713
694
  },
714
695
  lastMessage: undefined,
696
+ streamProtocol,
715
697
  });
716
698
  } catch (error) {
717
699
  console.error('Error processing stream response:', error);
@@ -719,6 +701,61 @@ export class Agent extends BaseResource {
719
701
  return response;
720
702
  }
721
703
 
704
+ /**
705
+ * Streams a response from the agent
706
+ * @param params - Stream parameters including prompt
707
+ * @returns Promise containing the enhanced Response object with processDataStream and processTextStream methods
708
+ */
709
+ async stream<T extends JSONSchema7 | ZodSchema | undefined = undefined>(
710
+ params: StreamParams<T>,
711
+ ): Promise<
712
+ Response & {
713
+ processDataStream: (options?: Omit<Parameters<typeof processDataStream>[0], 'stream'>) => Promise<void>;
714
+ processTextStream: (options?: Omit<Parameters<typeof processTextStream>[0], 'stream'>) => Promise<void>;
715
+ }
716
+ > {
717
+ const processedParams = {
718
+ ...params,
719
+ output: params.output ? zodToJsonSchema(params.output) : undefined,
720
+ experimental_output: params.experimental_output ? zodToJsonSchema(params.experimental_output) : undefined,
721
+ runtimeContext: parseClientRuntimeContext(params.runtimeContext),
722
+ clientTools: processClientTools(params.clientTools),
723
+ };
724
+
725
+ // Create a readable stream that will handle the response processing
726
+ const { readable, writable } = new TransformStream<Uint8Array, Uint8Array>();
727
+ // Start processing the response in the background
728
+ const response = await this.processStreamResponse(processedParams, writable);
729
+
730
+ // Create a new response with the readable stream
731
+ const streamResponse = new Response(readable, {
732
+ status: response.status,
733
+ statusText: response.statusText,
734
+ headers: response.headers,
735
+ }) as Response & {
736
+ processDataStream: (options?: Omit<Parameters<typeof processDataStream>[0], 'stream'>) => Promise<void>;
737
+ processTextStream: (options?: Omit<Parameters<typeof processTextStream>[0], 'stream'>) => Promise<void>;
738
+ };
739
+
740
+ // Add the processDataStream method to the response
741
+ streamResponse.processDataStream = async (options = {}) => {
742
+ await processDataStream({
743
+ stream: streamResponse.body as ReadableStream<Uint8Array>,
744
+ ...options,
745
+ });
746
+ };
747
+
748
+ //Add the processTextStream method to the response
749
+ streamResponse.processTextStream = async options => {
750
+ await processTextStream({
751
+ stream: streamResponse.body as ReadableStream<Uint8Array>,
752
+ onTextPart: options?.onTextPart ?? (() => {}),
753
+ });
754
+ };
755
+
756
+ return streamResponse;
757
+ }
758
+
722
759
  /**
723
760
  * Gets details about a specific tool available to the agent
724
761
  * @param toolId - ID of the tool to retrieve
@@ -30,6 +30,7 @@ export class BaseResource {
30
30
  // TODO: Bring this back once we figure out what we/users need to do to make this work with cross-origin requests
31
31
  // 'x-mastra-client-type': 'js',
32
32
  },
33
+ signal: this.options.abortSignal,
33
34
  body:
34
35
  options.body instanceof FormData ? options.body : options.body ? JSON.stringify(options.body) : undefined,
35
36
  });