@mastra/client-js 0.10.17-alpha.1 → 0.10.17-alpha.3

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,5 +1,5 @@
1
1
 
2
- > @mastra/client-js@0.10.17-alpha.1 build /home/runner/work/mastra/mastra/client-sdks/client-js
2
+ > @mastra/client-js@0.10.17-alpha.3 build /home/runner/work/mastra/mastra/client-sdks/client-js
3
3
  > tsup src/index.ts --format esm,cjs --dts --clean --treeshake=smallest --splitting
4
4
 
5
5
  CLI Building entry: src/index.ts
@@ -9,11 +9,11 @@
9
9
  CLI Cleaning output folder
10
10
  ESM Build start
11
11
  CJS Build start
12
- CJS dist/index.cjs 74.04 KB
13
- CJS ⚡️ Build success in 2302ms
14
- ESM dist/index.js 73.73 KB
15
- ESM ⚡️ Build success in 2304ms
12
+ ESM dist/index.js 75.21 KB
13
+ ESM ⚡️ Build success in 2334ms
14
+ CJS dist/index.cjs 75.52 KB
15
+ CJS ⚡️ Build success in 2348ms
16
16
  DTS Build start
17
- DTS ⚡️ Build success in 19056ms
18
- DTS dist/index.d.ts 46.21 KB
19
- DTS dist/index.d.cts 46.21 KB
17
+ DTS ⚡️ Build success in 17829ms
18
+ DTS dist/index.d.ts 47.19 KB
19
+ DTS dist/index.d.cts 47.19 KB
package/CHANGELOG.md CHANGED
@@ -1,5 +1,41 @@
1
1
  # @mastra/client-js
2
2
 
3
+ ## 0.10.17-alpha.3
4
+
5
+ ### Patch Changes
6
+
7
+ - 9802f42: Added types and tests to ensure client-js and hono endpoints can save memory messages where the input is either a v1 or v2 mastra message
8
+ - 1ac8f6b: deduplicate message
9
+ - @mastra/core@0.12.0-alpha.3
10
+
11
+ ## 0.10.17-alpha.2
12
+
13
+ ### Patch Changes
14
+
15
+ - aa2715b: process stream response error handling
16
+ - b8efbb9: feat: add flexible deleteMessages method to memory API
17
+ - Added `memory.deleteMessages(input)` method that accepts multiple input types:
18
+ - Single message ID as string: `deleteMessages('msg-123')`
19
+ - Array of message IDs: `deleteMessages(['msg-1', 'msg-2'])`
20
+ - Message object with id property: `deleteMessages({ id: 'msg-123' })`
21
+ - Array of message objects: `deleteMessages([{ id: 'msg-1' }, { id: 'msg-2' }])`
22
+ - Implemented in all storage adapters (LibSQL, PostgreSQL, Upstash, InMemory)
23
+ - Added REST API endpoint: `POST /api/memory/messages/delete`
24
+ - Updated client SDK: `thread.deleteMessages()` accepts all input types
25
+ - Updates thread timestamps when messages are deleted
26
+ - Added comprehensive test coverage and documentation
27
+
28
+ - Updated dependencies [27cc97a]
29
+ - Updated dependencies [41daa63]
30
+ - Updated dependencies [254a36b]
31
+ - Updated dependencies [0b89602]
32
+ - Updated dependencies [4d37822]
33
+ - Updated dependencies [ff9c125]
34
+ - Updated dependencies [b8efbb9]
35
+ - Updated dependencies [71466e7]
36
+ - Updated dependencies [0c99fbe]
37
+ - @mastra/core@0.12.0-alpha.2
38
+
3
39
  ## 0.10.17-alpha.1
4
40
 
5
41
  ### Patch Changes
package/dist/index.cjs CHANGED
@@ -741,7 +741,12 @@ var Agent = class extends BaseResource {
741
741
  this.processChatResponse({
742
742
  stream: streamForProcessing,
743
743
  update: ({ message }) => {
744
- messages.push(message);
744
+ const existingIndex = messages.findIndex((m) => m.id === message.id);
745
+ if (existingIndex !== -1) {
746
+ messages[existingIndex] = message;
747
+ } else {
748
+ messages.push(message);
749
+ }
745
750
  },
746
751
  onFinish: async ({ finishReason, message }) => {
747
752
  if (finishReason === "tool-calls") {
@@ -801,10 +806,12 @@ var Agent = class extends BaseResource {
801
806
  this.processStreamResponse(
802
807
  {
803
808
  ...processedParams,
804
- messages: [...messageArray, ...messages, lastMessage]
809
+ messages: [...messageArray, ...messages.filter((m) => m.id !== lastMessage.id), lastMessage]
805
810
  },
806
811
  writable
807
- );
812
+ ).catch((error) => {
813
+ console.error("Error processing stream response:", error);
814
+ });
808
815
  }
809
816
  }
810
817
  } else {
@@ -814,6 +821,8 @@ var Agent = class extends BaseResource {
814
821
  }
815
822
  },
816
823
  lastMessage: void 0
824
+ }).catch((error) => {
825
+ console.error("Error processing stream response:", error);
817
826
  });
818
827
  } catch (error) {
819
828
  console.error("Error processing stream response:", error);
@@ -977,6 +986,21 @@ var MemoryThread = class extends BaseResource {
977
986
  });
978
987
  return this.request(`/api/memory/threads/${this.threadId}/messages/paginated?${query.toString()}`);
979
988
  }
989
+ /**
990
+ * Deletes one or more messages from the thread
991
+ * @param messageIds - Can be a single message ID (string), array of message IDs,
992
+ * message object with id property, or array of message objects
993
+ * @returns Promise containing deletion result
994
+ */
995
+ deleteMessages(messageIds) {
996
+ const query = new URLSearchParams({
997
+ agentId: this.agentId
998
+ });
999
+ return this.request(`/api/memory/messages/delete?${query.toString()}`, {
1000
+ method: "POST",
1001
+ body: { messageIds }
1002
+ });
1003
+ }
980
1004
  };
981
1005
 
982
1006
  // src/resources/vector.ts
@@ -1725,6 +1749,21 @@ var NetworkMemoryThread = class extends BaseResource {
1725
1749
  });
1726
1750
  return this.request(`/api/memory/network/threads/${this.threadId}/messages?${query.toString()}`);
1727
1751
  }
1752
+ /**
1753
+ * Deletes one or more messages from the thread
1754
+ * @param messageIds - Can be a single message ID (string), array of message IDs,
1755
+ * message object with id property, or array of message objects
1756
+ * @returns Promise containing deletion result
1757
+ */
1758
+ deleteMessages(messageIds) {
1759
+ const query = new URLSearchParams({
1760
+ networkId: this.networkId
1761
+ });
1762
+ return this.request(`/api/memory/network/messages/delete?${query.toString()}`, {
1763
+ method: "POST",
1764
+ body: { messageIds }
1765
+ });
1766
+ }
1728
1767
  };
1729
1768
 
1730
1769
  // src/resources/vNextNetwork.ts
package/dist/index.d.cts CHANGED
@@ -164,14 +164,14 @@ interface GetVectorIndexResponse {
164
164
  count: number;
165
165
  }
166
166
  interface SaveMessageToMemoryParams {
167
- messages: MastraMessageV1[];
167
+ messages: (MastraMessageV1 | MastraMessageV2)[];
168
168
  agentId: string;
169
169
  }
170
170
  interface SaveNetworkMessageToMemoryParams {
171
- messages: MastraMessageV1[];
171
+ messages: (MastraMessageV1 | MastraMessageV2)[];
172
172
  networkId: string;
173
173
  }
174
- type SaveMessageToMemoryResponse = MastraMessageV1[];
174
+ type SaveMessageToMemoryResponse = (MastraMessageV1 | MastraMessageV2)[];
175
175
  interface CreateMemoryThreadParams {
176
176
  title?: string;
177
177
  metadata?: Record<string, any>;
@@ -601,6 +601,20 @@ declare class MemoryThread extends BaseResource {
601
601
  * @returns Promise containing paginated thread messages with pagination metadata (total, page, perPage, hasMore)
602
602
  */
603
603
  getMessagesPaginated({ selectBy, ...rest }: GetMemoryThreadMessagesPaginatedParams): Promise<GetMemoryThreadMessagesPaginatedResponse>;
604
+ /**
605
+ * Deletes one or more messages from the thread
606
+ * @param messageIds - Can be a single message ID (string), array of message IDs,
607
+ * message object with id property, or array of message objects
608
+ * @returns Promise containing deletion result
609
+ */
610
+ deleteMessages(messageIds: string | string[] | {
611
+ id: string;
612
+ } | {
613
+ id: string;
614
+ }[]): Promise<{
615
+ success: boolean;
616
+ message: string;
617
+ }>;
604
618
  }
605
619
 
606
620
  declare class Vector extends BaseResource {
@@ -998,6 +1012,20 @@ declare class NetworkMemoryThread extends BaseResource {
998
1012
  * @returns Promise containing thread messages and UI messages
999
1013
  */
1000
1014
  getMessages(params?: GetMemoryThreadMessagesParams): Promise<GetMemoryThreadMessagesResponse>;
1015
+ /**
1016
+ * Deletes one or more messages from the thread
1017
+ * @param messageIds - Can be a single message ID (string), array of message IDs,
1018
+ * message object with id property, or array of message objects
1019
+ * @returns Promise containing deletion result
1020
+ */
1021
+ deleteMessages(messageIds: string | string[] | {
1022
+ id: string;
1023
+ } | {
1024
+ id: string;
1025
+ }[]): Promise<{
1026
+ success: boolean;
1027
+ message: string;
1028
+ }>;
1001
1029
  }
1002
1030
 
1003
1031
  declare class VNextNetwork extends BaseResource {
package/dist/index.d.ts CHANGED
@@ -164,14 +164,14 @@ interface GetVectorIndexResponse {
164
164
  count: number;
165
165
  }
166
166
  interface SaveMessageToMemoryParams {
167
- messages: MastraMessageV1[];
167
+ messages: (MastraMessageV1 | MastraMessageV2)[];
168
168
  agentId: string;
169
169
  }
170
170
  interface SaveNetworkMessageToMemoryParams {
171
- messages: MastraMessageV1[];
171
+ messages: (MastraMessageV1 | MastraMessageV2)[];
172
172
  networkId: string;
173
173
  }
174
- type SaveMessageToMemoryResponse = MastraMessageV1[];
174
+ type SaveMessageToMemoryResponse = (MastraMessageV1 | MastraMessageV2)[];
175
175
  interface CreateMemoryThreadParams {
176
176
  title?: string;
177
177
  metadata?: Record<string, any>;
@@ -601,6 +601,20 @@ declare class MemoryThread extends BaseResource {
601
601
  * @returns Promise containing paginated thread messages with pagination metadata (total, page, perPage, hasMore)
602
602
  */
603
603
  getMessagesPaginated({ selectBy, ...rest }: GetMemoryThreadMessagesPaginatedParams): Promise<GetMemoryThreadMessagesPaginatedResponse>;
604
+ /**
605
+ * Deletes one or more messages from the thread
606
+ * @param messageIds - Can be a single message ID (string), array of message IDs,
607
+ * message object with id property, or array of message objects
608
+ * @returns Promise containing deletion result
609
+ */
610
+ deleteMessages(messageIds: string | string[] | {
611
+ id: string;
612
+ } | {
613
+ id: string;
614
+ }[]): Promise<{
615
+ success: boolean;
616
+ message: string;
617
+ }>;
604
618
  }
605
619
 
606
620
  declare class Vector extends BaseResource {
@@ -998,6 +1012,20 @@ declare class NetworkMemoryThread extends BaseResource {
998
1012
  * @returns Promise containing thread messages and UI messages
999
1013
  */
1000
1014
  getMessages(params?: GetMemoryThreadMessagesParams): Promise<GetMemoryThreadMessagesResponse>;
1015
+ /**
1016
+ * Deletes one or more messages from the thread
1017
+ * @param messageIds - Can be a single message ID (string), array of message IDs,
1018
+ * message object with id property, or array of message objects
1019
+ * @returns Promise containing deletion result
1020
+ */
1021
+ deleteMessages(messageIds: string | string[] | {
1022
+ id: string;
1023
+ } | {
1024
+ id: string;
1025
+ }[]): Promise<{
1026
+ success: boolean;
1027
+ message: string;
1028
+ }>;
1001
1029
  }
1002
1030
 
1003
1031
  declare class VNextNetwork extends BaseResource {
package/dist/index.js CHANGED
@@ -735,7 +735,12 @@ var Agent = class extends BaseResource {
735
735
  this.processChatResponse({
736
736
  stream: streamForProcessing,
737
737
  update: ({ message }) => {
738
- messages.push(message);
738
+ const existingIndex = messages.findIndex((m) => m.id === message.id);
739
+ if (existingIndex !== -1) {
740
+ messages[existingIndex] = message;
741
+ } else {
742
+ messages.push(message);
743
+ }
739
744
  },
740
745
  onFinish: async ({ finishReason, message }) => {
741
746
  if (finishReason === "tool-calls") {
@@ -795,10 +800,12 @@ var Agent = class extends BaseResource {
795
800
  this.processStreamResponse(
796
801
  {
797
802
  ...processedParams,
798
- messages: [...messageArray, ...messages, lastMessage]
803
+ messages: [...messageArray, ...messages.filter((m) => m.id !== lastMessage.id), lastMessage]
799
804
  },
800
805
  writable
801
- );
806
+ ).catch((error) => {
807
+ console.error("Error processing stream response:", error);
808
+ });
802
809
  }
803
810
  }
804
811
  } else {
@@ -808,6 +815,8 @@ var Agent = class extends BaseResource {
808
815
  }
809
816
  },
810
817
  lastMessage: void 0
818
+ }).catch((error) => {
819
+ console.error("Error processing stream response:", error);
811
820
  });
812
821
  } catch (error) {
813
822
  console.error("Error processing stream response:", error);
@@ -971,6 +980,21 @@ var MemoryThread = class extends BaseResource {
971
980
  });
972
981
  return this.request(`/api/memory/threads/${this.threadId}/messages/paginated?${query.toString()}`);
973
982
  }
983
+ /**
984
+ * Deletes one or more messages from the thread
985
+ * @param messageIds - Can be a single message ID (string), array of message IDs,
986
+ * message object with id property, or array of message objects
987
+ * @returns Promise containing deletion result
988
+ */
989
+ deleteMessages(messageIds) {
990
+ const query = new URLSearchParams({
991
+ agentId: this.agentId
992
+ });
993
+ return this.request(`/api/memory/messages/delete?${query.toString()}`, {
994
+ method: "POST",
995
+ body: { messageIds }
996
+ });
997
+ }
974
998
  };
975
999
 
976
1000
  // src/resources/vector.ts
@@ -1719,6 +1743,21 @@ var NetworkMemoryThread = class extends BaseResource {
1719
1743
  });
1720
1744
  return this.request(`/api/memory/network/threads/${this.threadId}/messages?${query.toString()}`);
1721
1745
  }
1746
+ /**
1747
+ * Deletes one or more messages from the thread
1748
+ * @param messageIds - Can be a single message ID (string), array of message IDs,
1749
+ * message object with id property, or array of message objects
1750
+ * @returns Promise containing deletion result
1751
+ */
1752
+ deleteMessages(messageIds) {
1753
+ const query = new URLSearchParams({
1754
+ networkId: this.networkId
1755
+ });
1756
+ return this.request(`/api/memory/network/messages/delete?${query.toString()}`, {
1757
+ method: "POST",
1758
+ body: { messageIds }
1759
+ });
1760
+ }
1722
1761
  };
1723
1762
 
1724
1763
  // src/resources/vNextNetwork.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mastra/client-js",
3
- "version": "0.10.17-alpha.1",
3
+ "version": "0.10.17-alpha.3",
4
4
  "description": "The official TypeScript library for the Mastra Client API",
5
5
  "author": "",
6
6
  "type": "module",
@@ -34,7 +34,7 @@
34
34
  "rxjs": "7.8.1",
35
35
  "zod": "^3.25.67",
36
36
  "zod-to-json-schema": "^3.24.5",
37
- "@mastra/core": "0.12.0-alpha.1"
37
+ "@mastra/core": "0.12.0-alpha.3"
38
38
  },
39
39
  "peerDependencies": {
40
40
  "zod": "^3.0.0"
package/src/index.test.ts CHANGED
@@ -310,6 +310,114 @@ describe('MastraClient Resources', () => {
310
310
  }
311
311
  });
312
312
 
313
+ it('should stream responses with tool calls', async () => {
314
+ const firstMockChunk = `0:"test "
315
+ 0:"response"
316
+ 9:{"toolCallId":"tool1","toolName":"testTool","args":{"arg1":"value1"}}
317
+ e:{"finishReason":"tool-calls","usage":{"promptTokens":1,"completionTokens":1},"isContinued":false}
318
+ d:{"finishReason":"tool-calls","usage":{"promptTokens":2,"completionTokens":2}}
319
+ `;
320
+
321
+ const secondMockChunk = `0:"final response"
322
+ e:{"finishReason":"stop","usage":{"promptTokens":2,"completionTokens":2},"isContinued":false}
323
+ d:{"finishReason":"stop","usage":{"promptTokens":2,"completionTokens":2}}
324
+ `;
325
+
326
+ const firstResponseBody = new ReadableStream({
327
+ start(controller) {
328
+ controller.enqueue(new TextEncoder().encode(firstMockChunk));
329
+ controller.close();
330
+ },
331
+ });
332
+
333
+ const secondResponseBody = new ReadableStream({
334
+ start(controller) {
335
+ controller.enqueue(new TextEncoder().encode(secondMockChunk));
336
+ controller.close();
337
+ },
338
+ });
339
+
340
+ (global.fetch as any)
341
+ .mockResolvedValueOnce(
342
+ new Response(firstResponseBody, {
343
+ status: 200,
344
+ headers: new Headers({ 'Content-Type': 'text/event-stream' }),
345
+ }),
346
+ )
347
+ .mockResolvedValueOnce(
348
+ new Response(secondResponseBody, {
349
+ status: 200,
350
+ headers: new Headers({ 'Content-Type': 'text/event-stream' }),
351
+ }),
352
+ );
353
+
354
+ const response = await agent.stream({
355
+ messages: [
356
+ {
357
+ role: 'user',
358
+ content: 'test',
359
+ },
360
+ ],
361
+ clientTools: {
362
+ testTool: {
363
+ id: 'testTool',
364
+ description: 'Test Tool',
365
+ inputSchema: {
366
+ type: 'object',
367
+ properties: {
368
+ arg1: { type: 'string' },
369
+ },
370
+ },
371
+ execute: async () => {
372
+ return 'test result';
373
+ },
374
+ },
375
+ },
376
+ });
377
+
378
+ expect(response.body).toBeInstanceOf(ReadableStream);
379
+ const reader = response?.body?.getReader();
380
+ expect(reader).toBeDefined();
381
+
382
+ let output = '';
383
+ if (reader) {
384
+ while (true) {
385
+ const { value, done } = await reader.read();
386
+ if (done) break;
387
+ output += new TextDecoder().decode(value);
388
+ }
389
+ }
390
+
391
+ expect(global.fetch).toHaveBeenCalledTimes(2);
392
+
393
+ const [secondUrl, secondConfig] = (global.fetch as any).mock.calls[1];
394
+ expect(secondUrl).toBe(`${clientOptions.baseUrl}/api/agents/test-agent/stream`);
395
+
396
+ const secondRequestBody = JSON.parse(secondConfig.body);
397
+ expect(secondRequestBody.messages).toHaveLength(2);
398
+ expect(secondRequestBody.messages[0].content).toBe('test');
399
+ expect(secondRequestBody.messages[1].content).toBe('test response');
400
+ expect(secondRequestBody.messages[1].parts).toEqual([
401
+ {
402
+ type: 'text',
403
+ text: 'test response',
404
+ },
405
+ {
406
+ type: 'tool-invocation',
407
+ toolInvocation: {
408
+ state: 'result',
409
+ step: 0,
410
+ toolCallId: 'tool1',
411
+ toolName: 'testTool',
412
+ args: {
413
+ arg1: 'value1',
414
+ },
415
+ result: 'test result',
416
+ },
417
+ },
418
+ ]);
419
+ });
420
+
313
421
  it('should get agent tool', async () => {
314
422
  const mockResponse = {
315
423
  id: 'tool1',
@@ -632,7 +632,13 @@ export class Agent extends BaseResource {
632
632
  this.processChatResponse({
633
633
  stream: streamForProcessing,
634
634
  update: ({ message }) => {
635
- messages.push(message);
635
+ const existingIndex = messages.findIndex(m => m.id === message.id);
636
+
637
+ if (existingIndex !== -1) {
638
+ messages[existingIndex] = message;
639
+ } else {
640
+ messages.push(message);
641
+ }
636
642
  },
637
643
  onFinish: async ({ finishReason, message }) => {
638
644
  if (finishReason === 'tool-calls') {
@@ -711,10 +717,12 @@ export class Agent extends BaseResource {
711
717
  this.processStreamResponse(
712
718
  {
713
719
  ...processedParams,
714
- messages: [...messageArray, ...messages, lastMessage],
720
+ messages: [...messageArray, ...messages.filter(m => m.id !== lastMessage.id), lastMessage],
715
721
  },
716
722
  writable,
717
- );
723
+ ).catch(error => {
724
+ console.error('Error processing stream response:', error);
725
+ });
718
726
  }
719
727
  }
720
728
  } else {
@@ -724,6 +732,8 @@ export class Agent extends BaseResource {
724
732
  }
725
733
  },
726
734
  lastMessage: undefined,
735
+ }).catch(error => {
736
+ console.error('Error processing stream response:', error);
727
737
  });
728
738
  } catch (error) {
729
739
  console.error('Error processing stream response:', error);
@@ -0,0 +1,285 @@
1
+ import { describe, expect, beforeEach, it, vi } from 'vitest';
2
+ import { MemoryThread } from './memory-thread';
3
+ import type { ClientOptions } from '../types';
4
+
5
+ // Mock fetch globally
6
+ global.fetch = vi.fn();
7
+
8
+ describe('MemoryThread', () => {
9
+ let thread: MemoryThread;
10
+ const clientOptions: ClientOptions = {
11
+ baseUrl: 'http://localhost:4111',
12
+ headers: {
13
+ Authorization: 'Bearer test-key',
14
+ },
15
+ };
16
+ const threadId = 'test-thread-id';
17
+ const agentId = 'test-agent-id';
18
+
19
+ beforeEach(() => {
20
+ vi.clearAllMocks();
21
+ thread = new MemoryThread(clientOptions, threadId, agentId);
22
+ });
23
+
24
+ const mockFetchResponse = (data: any) => {
25
+ (global.fetch as any).mockResolvedValueOnce({
26
+ ok: true,
27
+ status: 200,
28
+ json: async () => data,
29
+ headers: new Headers({
30
+ 'content-type': 'application/json',
31
+ }),
32
+ });
33
+ };
34
+
35
+ describe('get', () => {
36
+ it('should retrieve thread details', async () => {
37
+ const mockThread = {
38
+ id: threadId,
39
+ title: 'Test Thread',
40
+ metadata: { test: true },
41
+ createdAt: new Date().toISOString(),
42
+ updatedAt: new Date().toISOString(),
43
+ };
44
+
45
+ mockFetchResponse(mockThread);
46
+
47
+ const result = await thread.get();
48
+
49
+ expect(global.fetch).toHaveBeenCalledWith(
50
+ `http://localhost:4111/api/memory/threads/${threadId}?agentId=${agentId}`,
51
+ expect.objectContaining({
52
+ headers: expect.objectContaining({
53
+ Authorization: 'Bearer test-key',
54
+ }),
55
+ }),
56
+ );
57
+ expect(result).toEqual(mockThread);
58
+ });
59
+ });
60
+
61
+ describe('update', () => {
62
+ it('should update thread properties', async () => {
63
+ const updateParams = {
64
+ title: 'Updated Title',
65
+ metadata: { updated: true },
66
+ resourceid: 'resource-1',
67
+ };
68
+
69
+ const mockUpdatedThread = {
70
+ id: threadId,
71
+ ...updateParams,
72
+ createdAt: new Date().toISOString(),
73
+ updatedAt: new Date().toISOString(),
74
+ };
75
+
76
+ mockFetchResponse(mockUpdatedThread);
77
+
78
+ const result = await thread.update(updateParams);
79
+
80
+ expect(global.fetch).toHaveBeenCalledWith(
81
+ `http://localhost:4111/api/memory/threads/${threadId}?agentId=${agentId}`,
82
+ expect.objectContaining({
83
+ method: 'PATCH',
84
+ headers: expect.objectContaining({
85
+ Authorization: 'Bearer test-key',
86
+ }),
87
+ body: JSON.stringify(updateParams),
88
+ }),
89
+ );
90
+ expect(result).toEqual(mockUpdatedThread);
91
+ });
92
+ });
93
+
94
+ describe('delete', () => {
95
+ it('should delete the thread', async () => {
96
+ const mockResponse = { result: 'Thread deleted' };
97
+ mockFetchResponse(mockResponse);
98
+
99
+ const result = await thread.delete();
100
+
101
+ expect(global.fetch).toHaveBeenCalledWith(
102
+ `http://localhost:4111/api/memory/threads/${threadId}?agentId=${agentId}`,
103
+ expect.objectContaining({
104
+ method: 'DELETE',
105
+ headers: expect.objectContaining({
106
+ Authorization: 'Bearer test-key',
107
+ }),
108
+ }),
109
+ );
110
+ expect(result).toEqual(mockResponse);
111
+ });
112
+ });
113
+
114
+ describe('getMessages', () => {
115
+ it('should retrieve thread messages', async () => {
116
+ const mockMessages = {
117
+ messages: [
118
+ { id: 'msg-1', content: 'Hello', role: 'user' },
119
+ { id: 'msg-2', content: 'Hi there', role: 'assistant' },
120
+ ],
121
+ uiMessages: [
122
+ { id: 'msg-1', content: 'Hello', role: 'user' },
123
+ { id: 'msg-2', content: 'Hi there', role: 'assistant' },
124
+ ],
125
+ };
126
+
127
+ mockFetchResponse(mockMessages);
128
+
129
+ const result = await thread.getMessages();
130
+
131
+ expect(global.fetch).toHaveBeenCalledWith(
132
+ `http://localhost:4111/api/memory/threads/${threadId}/messages?agentId=${agentId}`,
133
+ expect.objectContaining({
134
+ headers: expect.objectContaining({
135
+ Authorization: 'Bearer test-key',
136
+ }),
137
+ }),
138
+ );
139
+ expect(result).toEqual(mockMessages);
140
+ });
141
+
142
+ it('should retrieve thread messages with limit', async () => {
143
+ const mockMessages = {
144
+ messages: [{ id: 'msg-1', content: 'Hello', role: 'user' }],
145
+ uiMessages: [{ id: 'msg-1', content: 'Hello', role: 'user' }],
146
+ };
147
+
148
+ mockFetchResponse(mockMessages);
149
+
150
+ const result = await thread.getMessages({ limit: 5 });
151
+
152
+ expect(global.fetch).toHaveBeenCalledWith(
153
+ `http://localhost:4111/api/memory/threads/${threadId}/messages?agentId=${agentId}&limit=5`,
154
+ expect.objectContaining({
155
+ headers: expect.objectContaining({
156
+ Authorization: 'Bearer test-key',
157
+ }),
158
+ }),
159
+ );
160
+ expect(result).toEqual(mockMessages);
161
+ });
162
+ });
163
+
164
+ describe('deleteMessages', () => {
165
+ it('should delete a single message by string ID', async () => {
166
+ const messageId = 'test-message-id';
167
+ const mockResponse = { success: true, message: '1 message deleted successfully' };
168
+
169
+ mockFetchResponse(mockResponse);
170
+
171
+ const result = await thread.deleteMessages(messageId);
172
+
173
+ expect(global.fetch).toHaveBeenCalledWith(
174
+ `http://localhost:4111/api/memory/messages/delete?agentId=${agentId}`,
175
+ expect.objectContaining({
176
+ method: 'POST',
177
+ headers: expect.objectContaining({
178
+ 'content-type': 'application/json',
179
+ Authorization: 'Bearer test-key',
180
+ }),
181
+ body: JSON.stringify({ messageIds: messageId }),
182
+ }),
183
+ );
184
+ expect(result).toEqual(mockResponse);
185
+ });
186
+
187
+ it('should delete multiple messages by array of string IDs', async () => {
188
+ const messageIds = ['msg-1', 'msg-2', 'msg-3'];
189
+ const mockResponse = { success: true, message: '3 messages deleted successfully' };
190
+
191
+ mockFetchResponse(mockResponse);
192
+
193
+ const result = await thread.deleteMessages(messageIds);
194
+
195
+ expect(global.fetch).toHaveBeenCalledWith(
196
+ `http://localhost:4111/api/memory/messages/delete?agentId=${agentId}`,
197
+ expect.objectContaining({
198
+ method: 'POST',
199
+ headers: expect.objectContaining({
200
+ 'content-type': 'application/json',
201
+ Authorization: 'Bearer test-key',
202
+ }),
203
+ body: JSON.stringify({ messageIds }),
204
+ }),
205
+ );
206
+ expect(result).toEqual(mockResponse);
207
+ });
208
+
209
+ it('should delete a message by object with id property', async () => {
210
+ const messageObj = { id: 'test-message-id' };
211
+ const mockResponse = { success: true, message: '1 message deleted successfully' };
212
+
213
+ mockFetchResponse(mockResponse);
214
+
215
+ const result = await thread.deleteMessages(messageObj);
216
+
217
+ expect(global.fetch).toHaveBeenCalledWith(
218
+ `http://localhost:4111/api/memory/messages/delete?agentId=${agentId}`,
219
+ expect.objectContaining({
220
+ method: 'POST',
221
+ headers: expect.objectContaining({
222
+ 'content-type': 'application/json',
223
+ Authorization: 'Bearer test-key',
224
+ }),
225
+ body: JSON.stringify({ messageIds: messageObj }),
226
+ }),
227
+ );
228
+ expect(result).toEqual(mockResponse);
229
+ });
230
+
231
+ it('should delete messages by array of objects', async () => {
232
+ const messageObjs = [{ id: 'msg-1' }, { id: 'msg-2' }];
233
+ const mockResponse = { success: true, message: '2 messages deleted successfully' };
234
+
235
+ mockFetchResponse(mockResponse);
236
+
237
+ const result = await thread.deleteMessages(messageObjs);
238
+
239
+ expect(global.fetch).toHaveBeenCalledWith(
240
+ `http://localhost:4111/api/memory/messages/delete?agentId=${agentId}`,
241
+ expect.objectContaining({
242
+ method: 'POST',
243
+ headers: expect.objectContaining({
244
+ 'content-type': 'application/json',
245
+ Authorization: 'Bearer test-key',
246
+ }),
247
+ body: JSON.stringify({ messageIds: messageObjs }),
248
+ }),
249
+ );
250
+ expect(result).toEqual(mockResponse);
251
+ });
252
+
253
+ it('should handle empty array', async () => {
254
+ const messageIds: string[] = [];
255
+
256
+ (global.fetch as any).mockResolvedValueOnce({
257
+ ok: false,
258
+ status: 400,
259
+ statusText: 'Bad Request',
260
+ json: async () => ({ error: 'messageIds array cannot be empty' }),
261
+ headers: new Headers({
262
+ 'content-type': 'application/json',
263
+ }),
264
+ });
265
+
266
+ await expect(thread.deleteMessages(messageIds)).rejects.toThrow();
267
+ });
268
+
269
+ it('should handle bulk delete errors', async () => {
270
+ const messageIds = ['msg-1', 'msg-2'];
271
+
272
+ (global.fetch as any).mockResolvedValueOnce({
273
+ ok: false,
274
+ status: 500,
275
+ statusText: 'Internal Server Error',
276
+ json: async () => ({ error: 'Database error' }),
277
+ headers: new Headers({
278
+ 'content-type': 'application/json',
279
+ }),
280
+ });
281
+
282
+ await expect(thread.deleteMessages(messageIds)).rejects.toThrow();
283
+ });
284
+ });
285
+ });
@@ -78,4 +78,22 @@ export class MemoryThread extends BaseResource {
78
78
  });
79
79
  return this.request(`/api/memory/threads/${this.threadId}/messages/paginated?${query.toString()}`);
80
80
  }
81
+
82
+ /**
83
+ * Deletes one or more messages from the thread
84
+ * @param messageIds - Can be a single message ID (string), array of message IDs,
85
+ * message object with id property, or array of message objects
86
+ * @returns Promise containing deletion result
87
+ */
88
+ deleteMessages(
89
+ messageIds: string | string[] | { id: string } | { id: string }[],
90
+ ): Promise<{ success: boolean; message: string }> {
91
+ const query = new URLSearchParams({
92
+ agentId: this.agentId,
93
+ });
94
+ return this.request(`/api/memory/messages/delete?${query.toString()}`, {
95
+ method: 'POST',
96
+ body: { messageIds },
97
+ });
98
+ }
81
99
  }
@@ -0,0 +1,269 @@
1
+ import { describe, expect, beforeEach, it, vi } from 'vitest';
2
+ import { NetworkMemoryThread } from './network-memory-thread';
3
+ import type { ClientOptions } from '../types';
4
+
5
+ // Mock fetch globally
6
+ global.fetch = vi.fn();
7
+
8
+ describe('NetworkMemoryThread', () => {
9
+ let thread: NetworkMemoryThread;
10
+ const clientOptions: ClientOptions = {
11
+ baseUrl: 'http://localhost:4111',
12
+ headers: {
13
+ Authorization: 'Bearer test-key',
14
+ },
15
+ };
16
+ const threadId = 'test-thread-id';
17
+ const networkId = 'test-network-id';
18
+
19
+ beforeEach(() => {
20
+ vi.clearAllMocks();
21
+ thread = new NetworkMemoryThread(clientOptions, threadId, networkId);
22
+ });
23
+
24
+ const mockFetchResponse = (data: any) => {
25
+ (global.fetch as any).mockResolvedValueOnce({
26
+ ok: true,
27
+ status: 200,
28
+ json: async () => data,
29
+ headers: new Headers({
30
+ 'content-type': 'application/json',
31
+ }),
32
+ });
33
+ };
34
+
35
+ describe('get', () => {
36
+ it('should retrieve thread details', async () => {
37
+ const mockThread = {
38
+ id: threadId,
39
+ title: 'Test Thread',
40
+ metadata: { test: true },
41
+ createdAt: new Date().toISOString(),
42
+ updatedAt: new Date().toISOString(),
43
+ };
44
+
45
+ mockFetchResponse(mockThread);
46
+
47
+ const result = await thread.get();
48
+
49
+ expect(global.fetch).toHaveBeenCalledWith(
50
+ `http://localhost:4111/api/memory/network/threads/${threadId}?networkId=${networkId}`,
51
+ expect.objectContaining({
52
+ headers: expect.objectContaining({
53
+ Authorization: 'Bearer test-key',
54
+ }),
55
+ }),
56
+ );
57
+ expect(result).toEqual(mockThread);
58
+ });
59
+ });
60
+
61
+ describe('update', () => {
62
+ it('should update thread properties', async () => {
63
+ const updateParams = {
64
+ title: 'Updated Title',
65
+ metadata: { updated: true },
66
+ resourceid: 'resource-1',
67
+ };
68
+
69
+ const mockUpdatedThread = {
70
+ id: threadId,
71
+ ...updateParams,
72
+ createdAt: new Date().toISOString(),
73
+ updatedAt: new Date().toISOString(),
74
+ };
75
+
76
+ mockFetchResponse(mockUpdatedThread);
77
+
78
+ const result = await thread.update(updateParams);
79
+
80
+ expect(global.fetch).toHaveBeenCalledWith(
81
+ `http://localhost:4111/api/memory/network/threads/${threadId}?networkId=${networkId}`,
82
+ expect.objectContaining({
83
+ method: 'PATCH',
84
+ headers: expect.objectContaining({
85
+ Authorization: 'Bearer test-key',
86
+ }),
87
+ body: JSON.stringify(updateParams),
88
+ }),
89
+ );
90
+ expect(result).toEqual(mockUpdatedThread);
91
+ });
92
+ });
93
+
94
+ describe('delete', () => {
95
+ it('should delete the thread', async () => {
96
+ const mockResponse = { result: 'Thread deleted' };
97
+ mockFetchResponse(mockResponse);
98
+
99
+ const result = await thread.delete();
100
+
101
+ expect(global.fetch).toHaveBeenCalledWith(
102
+ `http://localhost:4111/api/memory/network/threads/${threadId}?networkId=${networkId}`,
103
+ expect.objectContaining({
104
+ method: 'DELETE',
105
+ headers: expect.objectContaining({
106
+ Authorization: 'Bearer test-key',
107
+ }),
108
+ }),
109
+ );
110
+ expect(result).toEqual(mockResponse);
111
+ });
112
+ });
113
+
114
+ describe('getMessages', () => {
115
+ it('should retrieve thread messages', async () => {
116
+ const mockMessages = {
117
+ messages: [
118
+ { id: 'msg-1', content: 'Hello', role: 'user' },
119
+ { id: 'msg-2', content: 'Hi there', role: 'assistant' },
120
+ ],
121
+ uiMessages: [
122
+ { id: 'msg-1', content: 'Hello', role: 'user' },
123
+ { id: 'msg-2', content: 'Hi there', role: 'assistant' },
124
+ ],
125
+ };
126
+
127
+ mockFetchResponse(mockMessages);
128
+
129
+ const result = await thread.getMessages();
130
+
131
+ expect(global.fetch).toHaveBeenCalledWith(
132
+ `http://localhost:4111/api/memory/network/threads/${threadId}/messages?networkId=${networkId}`,
133
+ expect.objectContaining({
134
+ headers: expect.objectContaining({
135
+ Authorization: 'Bearer test-key',
136
+ }),
137
+ }),
138
+ );
139
+ expect(result).toEqual(mockMessages);
140
+ });
141
+
142
+ it('should retrieve thread messages with limit', async () => {
143
+ const mockMessages = {
144
+ messages: [{ id: 'msg-1', content: 'Hello', role: 'user' }],
145
+ uiMessages: [{ id: 'msg-1', content: 'Hello', role: 'user' }],
146
+ };
147
+
148
+ mockFetchResponse(mockMessages);
149
+
150
+ const result = await thread.getMessages({ limit: 5 });
151
+
152
+ expect(global.fetch).toHaveBeenCalledWith(
153
+ `http://localhost:4111/api/memory/network/threads/${threadId}/messages?networkId=${networkId}&limit=5`,
154
+ expect.objectContaining({
155
+ headers: expect.objectContaining({
156
+ Authorization: 'Bearer test-key',
157
+ }),
158
+ }),
159
+ );
160
+ expect(result).toEqual(mockMessages);
161
+ });
162
+ });
163
+
164
+ describe('deleteMessages', () => {
165
+ it('should delete a single message by string ID', async () => {
166
+ const messageId = 'test-message-id';
167
+ const mockResponse = { success: true, message: '1 message deleted successfully' };
168
+
169
+ mockFetchResponse(mockResponse);
170
+
171
+ const result = await thread.deleteMessages(messageId);
172
+
173
+ expect(global.fetch).toHaveBeenCalledWith(
174
+ `http://localhost:4111/api/memory/network/messages/delete?networkId=${networkId}`,
175
+ expect.objectContaining({
176
+ method: 'POST',
177
+ headers: expect.objectContaining({
178
+ 'content-type': 'application/json',
179
+ Authorization: 'Bearer test-key',
180
+ }),
181
+ body: JSON.stringify({ messageIds: messageId }),
182
+ }),
183
+ );
184
+ expect(result).toEqual(mockResponse);
185
+ });
186
+
187
+ it('should delete multiple messages by array of string IDs', async () => {
188
+ const messageIds = ['msg-1', 'msg-2', 'msg-3'];
189
+ const mockResponse = { success: true, message: '3 messages deleted successfully' };
190
+
191
+ mockFetchResponse(mockResponse);
192
+
193
+ const result = await thread.deleteMessages(messageIds);
194
+
195
+ expect(global.fetch).toHaveBeenCalledWith(
196
+ `http://localhost:4111/api/memory/network/messages/delete?networkId=${networkId}`,
197
+ expect.objectContaining({
198
+ method: 'POST',
199
+ headers: expect.objectContaining({
200
+ 'content-type': 'application/json',
201
+ Authorization: 'Bearer test-key',
202
+ }),
203
+ body: JSON.stringify({ messageIds }),
204
+ }),
205
+ );
206
+ expect(result).toEqual(mockResponse);
207
+ });
208
+
209
+ it('should delete a message by object with id property', async () => {
210
+ const messageObj = { id: 'test-message-id' };
211
+ const mockResponse = { success: true, message: '1 message deleted successfully' };
212
+
213
+ mockFetchResponse(mockResponse);
214
+
215
+ const result = await thread.deleteMessages(messageObj);
216
+
217
+ expect(global.fetch).toHaveBeenCalledWith(
218
+ `http://localhost:4111/api/memory/network/messages/delete?networkId=${networkId}`,
219
+ expect.objectContaining({
220
+ method: 'POST',
221
+ headers: expect.objectContaining({
222
+ 'content-type': 'application/json',
223
+ Authorization: 'Bearer test-key',
224
+ }),
225
+ body: JSON.stringify({ messageIds: messageObj }),
226
+ }),
227
+ );
228
+ expect(result).toEqual(mockResponse);
229
+ });
230
+
231
+ it('should delete messages by array of objects', async () => {
232
+ const messageObjs = [{ id: 'msg-1' }, { id: 'msg-2' }];
233
+ const mockResponse = { success: true, message: '2 messages deleted successfully' };
234
+
235
+ mockFetchResponse(mockResponse);
236
+
237
+ const result = await thread.deleteMessages(messageObjs);
238
+
239
+ expect(global.fetch).toHaveBeenCalledWith(
240
+ `http://localhost:4111/api/memory/network/messages/delete?networkId=${networkId}`,
241
+ expect.objectContaining({
242
+ method: 'POST',
243
+ headers: expect.objectContaining({
244
+ 'content-type': 'application/json',
245
+ Authorization: 'Bearer test-key',
246
+ }),
247
+ body: JSON.stringify({ messageIds: messageObjs }),
248
+ }),
249
+ );
250
+ expect(result).toEqual(mockResponse);
251
+ });
252
+
253
+ it('should handle delete messages errors', async () => {
254
+ const messageId = 'non-existent-id';
255
+
256
+ (global.fetch as any).mockResolvedValueOnce({
257
+ ok: false,
258
+ status: 404,
259
+ statusText: 'Not Found',
260
+ json: async () => ({ error: 'Message not found' }),
261
+ headers: new Headers({
262
+ 'content-type': 'application/json',
263
+ }),
264
+ });
265
+
266
+ await expect(thread.deleteMessages(messageId)).rejects.toThrow();
267
+ });
268
+ });
269
+ });
@@ -60,4 +60,22 @@ export class NetworkMemoryThread extends BaseResource {
60
60
  });
61
61
  return this.request(`/api/memory/network/threads/${this.threadId}/messages?${query.toString()}`);
62
62
  }
63
+
64
+ /**
65
+ * Deletes one or more messages from the thread
66
+ * @param messageIds - Can be a single message ID (string), array of message IDs,
67
+ * message object with id property, or array of message objects
68
+ * @returns Promise containing deletion result
69
+ */
70
+ deleteMessages(
71
+ messageIds: string | string[] | { id: string } | { id: string }[],
72
+ ): Promise<{ success: boolean; message: string }> {
73
+ const query = new URLSearchParams({
74
+ networkId: this.networkId,
75
+ });
76
+ return this.request(`/api/memory/network/messages/delete?${query.toString()}`, {
77
+ method: 'POST',
78
+ body: { messageIds },
79
+ });
80
+ }
63
81
  }
package/src/types.ts CHANGED
@@ -198,16 +198,16 @@ export interface GetVectorIndexResponse {
198
198
  }
199
199
 
200
200
  export interface SaveMessageToMemoryParams {
201
- messages: MastraMessageV1[];
201
+ messages: (MastraMessageV1 | MastraMessageV2)[];
202
202
  agentId: string;
203
203
  }
204
204
 
205
205
  export interface SaveNetworkMessageToMemoryParams {
206
- messages: MastraMessageV1[];
206
+ messages: (MastraMessageV1 | MastraMessageV2)[];
207
207
  networkId: string;
208
208
  }
209
209
 
210
- export type SaveMessageToMemoryResponse = MastraMessageV1[];
210
+ export type SaveMessageToMemoryResponse = (MastraMessageV1 | MastraMessageV2)[];
211
211
 
212
212
  export interface CreateMemoryThreadParams {
213
213
  title?: string;
@@ -0,0 +1,180 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import type { MastraMessageV1, MastraMessageV2 } from '@mastra/core';
3
+ import { MastraClient } from './client';
4
+
5
+ describe('V2 Message Format Support', () => {
6
+ let client: MastraClient;
7
+ const agentId = 'test-agent';
8
+
9
+ beforeEach(() => {
10
+ global.fetch = vi.fn();
11
+ client = new MastraClient({
12
+ baseUrl: 'http://localhost:3000',
13
+ });
14
+ });
15
+
16
+ it('should send v1 messages successfully', async () => {
17
+ const v1Messages: MastraMessageV1[] = [
18
+ {
19
+ id: 'msg-v1-1',
20
+ role: 'user',
21
+ content: 'Hello from v1!',
22
+ type: 'text',
23
+ createdAt: new Date(),
24
+ threadId: 'thread-123',
25
+ resourceId: 'resource-123',
26
+ },
27
+ ];
28
+
29
+ (global.fetch as any).mockResolvedValueOnce({
30
+ ok: true,
31
+ json: async () => v1Messages,
32
+ });
33
+
34
+ const result = await client.saveMessageToMemory({
35
+ agentId,
36
+ messages: v1Messages,
37
+ });
38
+
39
+ expect(result).toEqual(v1Messages);
40
+ expect(global.fetch).toHaveBeenCalledWith(
41
+ expect.stringContaining('/api/memory/save-messages'),
42
+ expect.objectContaining({
43
+ method: 'POST',
44
+ body: JSON.stringify({ agentId, messages: v1Messages }),
45
+ }),
46
+ );
47
+ });
48
+
49
+ it('should send v2 messages successfully', async () => {
50
+ const v2Messages: MastraMessageV2[] = [
51
+ {
52
+ id: 'msg-v2-1',
53
+ role: 'assistant',
54
+ createdAt: new Date(),
55
+ threadId: 'thread-123',
56
+ resourceId: 'resource-123',
57
+ content: {
58
+ format: 2,
59
+ parts: [{ type: 'text', text: 'Hello from v2!' }],
60
+ content: 'Hello from v2!',
61
+ },
62
+ },
63
+ ];
64
+
65
+ (global.fetch as any).mockResolvedValueOnce({
66
+ ok: true,
67
+ json: async () => v2Messages,
68
+ });
69
+
70
+ const result = await client.saveMessageToMemory({
71
+ agentId,
72
+ messages: v2Messages,
73
+ });
74
+
75
+ expect(result).toEqual(v2Messages);
76
+ expect(global.fetch).toHaveBeenCalledWith(
77
+ expect.stringContaining('/api/memory/save-messages'),
78
+ expect.objectContaining({
79
+ method: 'POST',
80
+ body: JSON.stringify({ agentId, messages: v2Messages }),
81
+ }),
82
+ );
83
+ });
84
+
85
+ it('should send mixed v1 and v2 messages successfully', async () => {
86
+ const mixedMessages: (MastraMessageV1 | MastraMessageV2)[] = [
87
+ {
88
+ id: 'msg-v1-1',
89
+ role: 'user',
90
+ content: 'Question in v1 format',
91
+ type: 'text',
92
+ createdAt: new Date(),
93
+ threadId: 'thread-123',
94
+ resourceId: 'resource-123',
95
+ },
96
+ {
97
+ id: 'msg-v2-1',
98
+ role: 'assistant',
99
+ createdAt: new Date(),
100
+ threadId: 'thread-123',
101
+ resourceId: 'resource-123',
102
+ content: {
103
+ format: 2,
104
+ parts: [
105
+ { type: 'text', text: 'Answer in v2 format' },
106
+ {
107
+ type: 'tool-invocation',
108
+ toolInvocation: {
109
+ state: 'result' as const,
110
+ toolCallId: 'call-123',
111
+ toolName: 'calculator',
112
+ args: { a: 1, b: 2 },
113
+ result: '3',
114
+ },
115
+ },
116
+ ],
117
+ toolInvocations: [
118
+ {
119
+ state: 'result' as const,
120
+ toolCallId: 'call-123',
121
+ toolName: 'calculator',
122
+ args: { a: 1, b: 2 },
123
+ result: '3',
124
+ },
125
+ ],
126
+ },
127
+ },
128
+ ];
129
+
130
+ (global.fetch as any).mockResolvedValueOnce({
131
+ ok: true,
132
+ json: async () => mixedMessages,
133
+ });
134
+
135
+ const result = await client.saveMessageToMemory({
136
+ agentId,
137
+ messages: mixedMessages,
138
+ });
139
+
140
+ expect(result).toEqual(mixedMessages);
141
+ expect(global.fetch).toHaveBeenCalledWith(
142
+ expect.stringContaining('/api/memory/save-messages'),
143
+ expect.objectContaining({
144
+ method: 'POST',
145
+ body: JSON.stringify({ agentId, messages: mixedMessages }),
146
+ }),
147
+ );
148
+ });
149
+
150
+ it('should handle v2 messages with attachments', async () => {
151
+ const v2MessageWithAttachments: MastraMessageV2 = {
152
+ id: 'msg-v2-att',
153
+ role: 'user',
154
+ createdAt: new Date(),
155
+ threadId: 'thread-123',
156
+ resourceId: 'resource-123',
157
+ content: {
158
+ format: 2,
159
+ parts: [
160
+ { type: 'text', text: 'Check out this image:' },
161
+ { type: 'file', data: 'data:image/png;base64,iVBORw0...', mimeType: 'image/png' },
162
+ ],
163
+ experimental_attachments: [{ url: 'data:image/png;base64,iVBORw0...', contentType: 'image/png' }],
164
+ },
165
+ };
166
+
167
+ (global.fetch as any).mockResolvedValueOnce({
168
+ ok: true,
169
+ json: async () => [v2MessageWithAttachments],
170
+ });
171
+
172
+ const result = await client.saveMessageToMemory({
173
+ agentId,
174
+ messages: [v2MessageWithAttachments],
175
+ });
176
+
177
+ expect(result).toHaveLength(1);
178
+ expect(result[0]).toEqual(v2MessageWithAttachments);
179
+ });
180
+ });