@agentscope-ai/agentscope 0.0.2

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.
Files changed (136) hide show
  1. package/dist/agent/index.d.mts +234 -0
  2. package/dist/agent/index.d.ts +234 -0
  3. package/dist/agent/index.js +1412 -0
  4. package/dist/agent/index.js.map +1 -0
  5. package/dist/agent/index.mjs +1375 -0
  6. package/dist/agent/index.mjs.map +1 -0
  7. package/dist/base-BOx3UzOl.d.mts +41 -0
  8. package/dist/base-BoIps2RL.d.ts +41 -0
  9. package/dist/base-C7jwyH4Z.d.mts +52 -0
  10. package/dist/base-Cwi4bjze.d.ts +127 -0
  11. package/dist/base-DYlBMCy_.d.mts +127 -0
  12. package/dist/base-NX-knWOv.d.ts +52 -0
  13. package/dist/block-VsnHrllL.d.mts +48 -0
  14. package/dist/block-VsnHrllL.d.ts +48 -0
  15. package/dist/event/index.d.mts +181 -0
  16. package/dist/event/index.d.ts +181 -0
  17. package/dist/event/index.js +58 -0
  18. package/dist/event/index.js.map +1 -0
  19. package/dist/event/index.mjs +33 -0
  20. package/dist/event/index.mjs.map +1 -0
  21. package/dist/formatter/index.d.mts +187 -0
  22. package/dist/formatter/index.d.ts +187 -0
  23. package/dist/formatter/index.js +647 -0
  24. package/dist/formatter/index.js.map +1 -0
  25. package/dist/formatter/index.mjs +616 -0
  26. package/dist/formatter/index.mjs.map +1 -0
  27. package/dist/index-BTJDlKvQ.d.mts +195 -0
  28. package/dist/index-BcatlwXQ.d.ts +195 -0
  29. package/dist/index-CAxQAkiP.d.mts +21 -0
  30. package/dist/index-CAxQAkiP.d.ts +21 -0
  31. package/dist/mcp/index.d.mts +9 -0
  32. package/dist/mcp/index.d.ts +9 -0
  33. package/dist/mcp/index.js +432 -0
  34. package/dist/mcp/index.js.map +1 -0
  35. package/dist/mcp/index.mjs +408 -0
  36. package/dist/mcp/index.mjs.map +1 -0
  37. package/dist/message/index.d.mts +10 -0
  38. package/dist/message/index.d.ts +10 -0
  39. package/dist/message/index.js +67 -0
  40. package/dist/message/index.js.map +1 -0
  41. package/dist/message/index.mjs +37 -0
  42. package/dist/message/index.mjs.map +1 -0
  43. package/dist/message-CkN21KaY.d.mts +99 -0
  44. package/dist/message-CzLeTlua.d.ts +99 -0
  45. package/dist/model/index.d.mts +377 -0
  46. package/dist/model/index.d.ts +377 -0
  47. package/dist/model/index.js +1880 -0
  48. package/dist/model/index.js.map +1 -0
  49. package/dist/model/index.mjs +1849 -0
  50. package/dist/model/index.mjs.map +1 -0
  51. package/dist/storage/index.d.mts +68 -0
  52. package/dist/storage/index.d.ts +68 -0
  53. package/dist/storage/index.js +250 -0
  54. package/dist/storage/index.js.map +1 -0
  55. package/dist/storage/index.mjs +212 -0
  56. package/dist/storage/index.mjs.map +1 -0
  57. package/dist/tool/index.d.mts +311 -0
  58. package/dist/tool/index.d.ts +311 -0
  59. package/dist/tool/index.js +1494 -0
  60. package/dist/tool/index.js.map +1 -0
  61. package/dist/tool/index.mjs +1447 -0
  62. package/dist/tool/index.mjs.map +1 -0
  63. package/dist/toolkit-CEpulFi0.d.ts +99 -0
  64. package/dist/toolkit-CGEZSZPa.d.mts +99 -0
  65. package/jest.config.js +11 -0
  66. package/package.json +92 -0
  67. package/src/_utils/common.ts +104 -0
  68. package/src/_utils/index.ts +1 -0
  69. package/src/agent/agent-base.ts +0 -0
  70. package/src/agent/agent.test.ts +1028 -0
  71. package/src/agent/agent.ts +1032 -0
  72. package/src/agent/index.ts +2 -0
  73. package/src/agent/interfaces.ts +23 -0
  74. package/src/agent/test-compression.ts +72 -0
  75. package/src/event/index.ts +250 -0
  76. package/src/formatter/base.ts +133 -0
  77. package/src/formatter/dashscope-chat-formatter.test.ts +372 -0
  78. package/src/formatter/dashscope-chat-formatter.ts +163 -0
  79. package/src/formatter/deepseek-chat-formatter.ts +130 -0
  80. package/src/formatter/index.ts +5 -0
  81. package/src/formatter/ollama-chat-formatter.ts +67 -0
  82. package/src/formatter/openai-chat-formatter.test.ts +263 -0
  83. package/src/formatter/openai-chat-formatter.ts +301 -0
  84. package/src/formatter/openai.md +767 -0
  85. package/src/mcp/base.ts +114 -0
  86. package/src/mcp/http.test.ts +303 -0
  87. package/src/mcp/http.ts +224 -0
  88. package/src/mcp/index.ts +2 -0
  89. package/src/mcp/stdio.test.ts +91 -0
  90. package/src/mcp/stdio.ts +119 -0
  91. package/src/message/block.ts +60 -0
  92. package/src/message/enums.ts +4 -0
  93. package/src/message/index.ts +12 -0
  94. package/src/message/message.test.ts +80 -0
  95. package/src/message/message.ts +131 -0
  96. package/src/model/base.ts +226 -0
  97. package/src/model/dashscope-model.test.ts +335 -0
  98. package/src/model/dashscope-model.ts +441 -0
  99. package/src/model/deepseek-model.test.ts +279 -0
  100. package/src/model/deepseek-model.ts +401 -0
  101. package/src/model/index.ts +7 -0
  102. package/src/model/ollama-model.test.ts +307 -0
  103. package/src/model/ollama-model.ts +356 -0
  104. package/src/model/openai-model.ts +327 -0
  105. package/src/model/response.ts +22 -0
  106. package/src/model/usage.ts +12 -0
  107. package/src/storage/base.ts +52 -0
  108. package/src/storage/file-system.test.ts +587 -0
  109. package/src/storage/file-system.ts +269 -0
  110. package/src/storage/index.ts +2 -0
  111. package/src/tool/base.ts +23 -0
  112. package/src/tool/bash.test.ts +174 -0
  113. package/src/tool/bash.ts +152 -0
  114. package/src/tool/edit.test.ts +83 -0
  115. package/src/tool/edit.ts +95 -0
  116. package/src/tool/glob.test.ts +63 -0
  117. package/src/tool/glob.ts +166 -0
  118. package/src/tool/grep.test.ts +74 -0
  119. package/src/tool/grep.ts +256 -0
  120. package/src/tool/index.ts +10 -0
  121. package/src/tool/read.test.ts +77 -0
  122. package/src/tool/read.ts +117 -0
  123. package/src/tool/response.ts +82 -0
  124. package/src/tool/task.test.ts +299 -0
  125. package/src/tool/task.ts +399 -0
  126. package/src/tool/toolkit.test.ts +636 -0
  127. package/src/tool/toolkit.ts +601 -0
  128. package/src/tool/write.test.ts +52 -0
  129. package/src/tool/write.ts +57 -0
  130. package/src/type/index.ts +52 -0
  131. package/tsconfig.build.json +4 -0
  132. package/tsconfig.cjs.json +11 -0
  133. package/tsconfig.esm.json +10 -0
  134. package/tsconfig.json +14 -0
  135. package/tsup.config.ts +20 -0
  136. package/typedoc.json +52 -0
@@ -0,0 +1,587 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+
4
+ import { LocalFileStorage } from './file-system';
5
+ import { createMsg } from '../message';
6
+
7
+ describe('LocalFileStorage', () => {
8
+ const testDir = path.join(__dirname, '.test-storage');
9
+ let storage: LocalFileStorage;
10
+
11
+ beforeEach(() => {
12
+ // Clean up test directory before each test
13
+ if (fs.existsSync(testDir)) {
14
+ fs.rmSync(testDir, { recursive: true, force: true });
15
+ }
16
+ storage = new LocalFileStorage({ pathSegments: [testDir, 'test-user', 'test-session'] });
17
+ });
18
+
19
+ afterEach(() => {
20
+ // Clean up test directory after each test
21
+ if (fs.existsSync(testDir)) {
22
+ fs.rmSync(testDir, { recursive: true, force: true });
23
+ }
24
+ });
25
+
26
+ describe('Basic save and load', () => {
27
+ it('should save and load agent state correctly', async () => {
28
+ const context = [
29
+ createMsg({
30
+ name: 'user',
31
+ content: [{ type: 'text', text: 'Hello', id: 'text-1' }],
32
+ role: 'user',
33
+ }),
34
+ createMsg({
35
+ name: 'assistant',
36
+ content: [{ type: 'text', text: 'Hi there!', id: 'text-2' }],
37
+ role: 'assistant',
38
+ }),
39
+ ];
40
+
41
+ const metadata = {
42
+ replyId: 'reply-123',
43
+ curIter: 1,
44
+ curSummary: '',
45
+ };
46
+
47
+ // Save state
48
+ await storage.saveAgentState({
49
+ agentId: 'agent-1',
50
+ context,
51
+ metadata,
52
+ });
53
+
54
+ // Load state
55
+ const loaded = await storage.loadAgentState({ agentId: 'agent-1' });
56
+
57
+ expect(loaded.context).toHaveLength(2);
58
+ expect(loaded.context[0].content[0]).toEqual({
59
+ type: 'text',
60
+ text: 'Hello',
61
+ id: 'text-1',
62
+ });
63
+ expect(loaded.context[1].content[0]).toEqual({
64
+ type: 'text',
65
+ text: 'Hi there!',
66
+ id: 'text-2',
67
+ });
68
+ expect(loaded.metadata.replyId).toBe('reply-123');
69
+ expect(loaded.metadata.curIter).toBe(1);
70
+ });
71
+
72
+ it('should return empty state when loading non-existent agent', async () => {
73
+ const loaded = await storage.loadAgentState({ agentId: 'non-existent' });
74
+
75
+ expect(loaded.context).toEqual([]);
76
+ expect(loaded.metadata).toEqual({});
77
+ });
78
+
79
+ it('should handle incremental context updates', async () => {
80
+ const msg1 = createMsg({
81
+ name: 'user',
82
+ content: [{ type: 'text', text: 'Message 1', id: 'text-3' }],
83
+ role: 'user',
84
+ });
85
+ const msg2 = createMsg({
86
+ name: 'assistant',
87
+ content: [{ type: 'text', text: 'Response 1', id: 'text-4' }],
88
+ role: 'assistant',
89
+ });
90
+ const msg3 = createMsg({
91
+ name: 'user',
92
+ content: [{ type: 'text', text: 'Message 2', id: 'text-5' }],
93
+ role: 'user',
94
+ });
95
+
96
+ // First save
97
+ await storage.saveAgentState({
98
+ agentId: 'agent-1',
99
+ context: [msg1, msg2],
100
+ metadata: { replyId: 'reply-1' },
101
+ });
102
+
103
+ // Second save with additional message
104
+ await storage.saveAgentState({
105
+ agentId: 'agent-1',
106
+ context: [msg1, msg2, msg3],
107
+ metadata: { replyId: 'reply-2' },
108
+ });
109
+
110
+ // Load and verify all messages are present
111
+ const loaded = await storage.loadAgentState({ agentId: 'agent-1' });
112
+ expect(loaded.context).toHaveLength(3);
113
+ expect(loaded.context[2].content[0]).toEqual({
114
+ type: 'text',
115
+ text: 'Message 2',
116
+ id: 'text-5',
117
+ });
118
+ });
119
+ });
120
+
121
+ describe('Compression support', () => {
122
+ it('should store full history but load only from compression boundary', async () => {
123
+ // Create 10 messages
124
+ const allMessages = Array.from({ length: 10 }, (_, i) =>
125
+ createMsg({
126
+ name: i % 2 === 0 ? 'user' : 'assistant',
127
+ content: [{ type: 'text', text: `Message ${i + 1}`, id: `text-${i + 6}` }],
128
+ role: i % 2 === 0 ? 'user' : 'assistant',
129
+ })
130
+ );
131
+
132
+ // First save: all 10 messages
133
+ await storage.saveAgentState({
134
+ agentId: 'agent-1',
135
+ context: allMessages,
136
+ metadata: {
137
+ replyId: 'reply-1',
138
+ curIter: 5,
139
+ curSummary: '',
140
+ },
141
+ });
142
+
143
+ // Verify all messages are saved to file
144
+ const contextFile = path.join(
145
+ testDir,
146
+ 'test-user',
147
+ 'test-session',
148
+ 'agent-1',
149
+ 'context.jsonl'
150
+ );
151
+ const fileContent = fs.readFileSync(contextFile, 'utf-8');
152
+ const savedMessages = fileContent.trim().split('\n');
153
+ expect(savedMessages).toHaveLength(10);
154
+
155
+ // Simulate compression: agent only keeps last 3 messages
156
+ const compressedContext = allMessages.slice(7); // Messages 8, 9, 10
157
+
158
+ // Second save: only 3 messages (after compression)
159
+ await storage.saveAgentState({
160
+ agentId: 'agent-1',
161
+ context: compressedContext,
162
+ metadata: {
163
+ replyId: 'reply-2',
164
+ curIter: 6,
165
+ curSummary: '<system-info>Summary of messages 1-7</system-info>',
166
+ },
167
+ });
168
+
169
+ // Verify file still contains all 10 messages (full history preserved)
170
+ const updatedFileContent = fs.readFileSync(contextFile, 'utf-8');
171
+ const allSavedMessages = updatedFileContent.trim().split('\n');
172
+ expect(allSavedMessages).toHaveLength(10);
173
+
174
+ // Load state: should only get messages from compression boundary (last 3)
175
+ const loaded = await storage.loadAgentState({ agentId: 'agent-1' });
176
+ expect(loaded.context).toHaveLength(3);
177
+ expect(loaded.context[0].content[0]).toEqual({
178
+ type: 'text',
179
+ text: 'Message 8',
180
+ id: 'text-13',
181
+ });
182
+ expect(loaded.context[1].content[0]).toEqual({
183
+ type: 'text',
184
+ text: 'Message 9',
185
+ id: 'text-14',
186
+ });
187
+ expect(loaded.context[2].content[0]).toEqual({
188
+ type: 'text',
189
+ text: 'Message 10',
190
+ id: 'text-15',
191
+ });
192
+ expect(loaded.metadata.curSummary).toBe(
193
+ '<system-info>Summary of messages 1-7</system-info>'
194
+ );
195
+ });
196
+
197
+ it('should handle multiple compression cycles', async () => {
198
+ // First cycle: 5 messages
199
+ const batch1 = Array.from({ length: 5 }, (_, i) =>
200
+ createMsg({
201
+ name: 'user',
202
+ content: [
203
+ { type: 'text', text: `Batch 1 Message ${i + 1}`, id: `text-${i + 16}` },
204
+ ],
205
+ role: 'user',
206
+ })
207
+ );
208
+
209
+ await storage.saveAgentState({
210
+ agentId: 'agent-1',
211
+ context: batch1,
212
+ metadata: { replyId: 'reply-1' },
213
+ });
214
+
215
+ // First compression: keep last 2
216
+ await storage.saveAgentState({
217
+ agentId: 'agent-1',
218
+ context: batch1.slice(3),
219
+ metadata: { replyId: 'reply-2', curSummary: 'Summary 1' },
220
+ });
221
+
222
+ // Add more messages
223
+ const batch2 = [
224
+ ...batch1.slice(3),
225
+ ...Array.from({ length: 5 }, (_, i) =>
226
+ createMsg({
227
+ name: 'user',
228
+ content: [
229
+ {
230
+ type: 'text',
231
+ text: `Batch 2 Message ${i + 1}`,
232
+ id: `text-${i + 21}`,
233
+ },
234
+ ],
235
+ role: 'user',
236
+ })
237
+ ),
238
+ ];
239
+
240
+ await storage.saveAgentState({
241
+ agentId: 'agent-1',
242
+ context: batch2,
243
+ metadata: { replyId: 'reply-3' },
244
+ });
245
+
246
+ // Second compression: keep last 3
247
+ await storage.saveAgentState({
248
+ agentId: 'agent-1',
249
+ context: batch2.slice(-3),
250
+ metadata: { replyId: 'reply-4', curSummary: 'Summary 2' },
251
+ });
252
+
253
+ // Load and verify only last 3 messages are returned
254
+ const loaded = await storage.loadAgentState({ agentId: 'agent-1' });
255
+ expect(loaded.context).toHaveLength(3);
256
+ expect(loaded.context[0].content[0]).toEqual({
257
+ type: 'text',
258
+ text: 'Batch 2 Message 3',
259
+ id: 'text-23',
260
+ });
261
+ expect(loaded.metadata.curSummary).toBe('Summary 2');
262
+ });
263
+
264
+ it('should not expose internal storage fields to agent layer', async () => {
265
+ const context = [
266
+ createMsg({
267
+ name: 'user',
268
+ content: [{ type: 'text', text: 'Test', id: 'text-26' }],
269
+ role: 'user',
270
+ }),
271
+ ];
272
+
273
+ await storage.saveAgentState({
274
+ agentId: 'agent-1',
275
+ context,
276
+ metadata: { replyId: 'reply-1' },
277
+ });
278
+
279
+ // Check that internal field is saved in file
280
+ const stateFile = path.join(
281
+ testDir,
282
+ 'test-user',
283
+ 'test-session',
284
+ 'agent-1',
285
+ 'state.json'
286
+ );
287
+ const fileContent = JSON.parse(fs.readFileSync(stateFile, 'utf-8'));
288
+ expect(fileContent).toHaveProperty('_storage_compressionBoundaryMsgId');
289
+
290
+ // Check that internal field is filtered out when loading
291
+ const loaded = await storage.loadAgentState({ agentId: 'agent-1' });
292
+ expect(loaded.metadata).not.toHaveProperty('_storage_compressionBoundaryMsgId');
293
+ expect(loaded.metadata.replyId).toBe('reply-1');
294
+ });
295
+ });
296
+
297
+ describe('Multiple agents', () => {
298
+ it('should handle multiple agents independently', async () => {
299
+ const agent1Context = [
300
+ createMsg({
301
+ name: 'user',
302
+ content: [{ type: 'text', text: 'Agent 1 message', id: 'text-27' }],
303
+ role: 'user',
304
+ }),
305
+ ];
306
+ const agent2Context = [
307
+ createMsg({
308
+ name: 'user',
309
+ content: [{ type: 'text', text: 'Agent 2 message', id: 'text-28' }],
310
+ role: 'user',
311
+ }),
312
+ ];
313
+
314
+ await storage.saveAgentState({
315
+ agentId: 'agent-1',
316
+ context: agent1Context,
317
+ metadata: { replyId: 'reply-1' },
318
+ });
319
+
320
+ await storage.saveAgentState({
321
+ agentId: 'agent-2',
322
+ context: agent2Context,
323
+ metadata: { replyId: 'reply-2' },
324
+ });
325
+
326
+ const loaded1 = await storage.loadAgentState({ agentId: 'agent-1' });
327
+ const loaded2 = await storage.loadAgentState({ agentId: 'agent-2' });
328
+
329
+ expect(loaded1.context[0].content[0]).toEqual({
330
+ type: 'text',
331
+ text: 'Agent 1 message',
332
+ id: 'text-27',
333
+ });
334
+ expect(loaded2.context[0].content[0]).toEqual({
335
+ type: 'text',
336
+ text: 'Agent 2 message',
337
+ id: 'text-28',
338
+ });
339
+ expect(loaded1.metadata.replyId).toBe('reply-1');
340
+ expect(loaded2.metadata.replyId).toBe('reply-2');
341
+ });
342
+ });
343
+
344
+ describe('Context offloading', () => {
345
+ it('should return undefined when offloadDir is not configured', async () => {
346
+ const msgs = [
347
+ createMsg({
348
+ name: 'user',
349
+ content: [{ type: 'text', text: 'Test message', id: 'text-29' }],
350
+ role: 'user',
351
+ }),
352
+ ];
353
+
354
+ const result = await storage.offloadContext({ agentId: 'agent-1', msgs });
355
+ expect(result).toBeUndefined();
356
+ });
357
+
358
+ it('should offload text messages to a date-based file', async () => {
359
+ const offloadDir = path.join(testDir, 'offload');
360
+ const storageWithOffload = new LocalFileStorage({
361
+ pathSegments: [testDir, 'test-user', 'test-session'],
362
+ offloadPathSegments: [offloadDir, 'test-user', 'test-session'],
363
+ });
364
+
365
+ const msgs = [
366
+ createMsg({
367
+ name: 'user',
368
+ content: [{ type: 'text', text: 'Hello world', id: 'text-30' }],
369
+ role: 'user',
370
+ }),
371
+ createMsg({
372
+ name: 'assistant',
373
+ content: [{ type: 'text', text: 'Hi there!', id: 'text-31' }],
374
+ role: 'assistant',
375
+ }),
376
+ ];
377
+
378
+ const offloadPath = await storageWithOffload.offloadContext({
379
+ agentId: 'agent-1',
380
+ msgs,
381
+ });
382
+
383
+ expect(offloadPath).toBeDefined();
384
+ expect(fs.existsSync(offloadPath!)).toBe(true);
385
+
386
+ const content = fs.readFileSync(offloadPath!, 'utf-8');
387
+ expect(content).toContain('user: Hello world');
388
+ expect(content).toContain('assistant: Hi there!');
389
+ });
390
+
391
+ it('should append to existing offload file on same day', async () => {
392
+ const offloadDir = path.join(testDir, 'offload');
393
+ const storageWithOffload = new LocalFileStorage({
394
+ pathSegments: [testDir, 'test-user', 'test-session'],
395
+ offloadPathSegments: [offloadDir, 'test-user', 'test-session'],
396
+ });
397
+
398
+ const msgs1 = [
399
+ createMsg({
400
+ name: 'user',
401
+ content: [{ type: 'text', text: 'First batch', id: 'text-32' }],
402
+ role: 'user',
403
+ }),
404
+ ];
405
+
406
+ const msgs2 = [
407
+ createMsg({
408
+ name: 'user',
409
+ content: [{ type: 'text', text: 'Second batch', id: 'text-33' }],
410
+ role: 'user',
411
+ }),
412
+ ];
413
+
414
+ const path1 = await storageWithOffload.offloadContext({
415
+ agentId: 'agent-1',
416
+ msgs: msgs1,
417
+ });
418
+ const path2 = await storageWithOffload.offloadContext({
419
+ agentId: 'agent-1',
420
+ msgs: msgs2,
421
+ });
422
+
423
+ expect(path1).toBe(path2);
424
+
425
+ const content = fs.readFileSync(path1!, 'utf-8');
426
+ expect(content).toContain('First batch');
427
+ expect(content).toContain('Second batch');
428
+ });
429
+
430
+ it('should handle data blocks with URL source', async () => {
431
+ const offloadDir = path.join(testDir, 'offload');
432
+ const storageWithOffload = new LocalFileStorage({
433
+ pathSegments: [testDir, 'test-user', 'test-session'],
434
+ offloadPathSegments: [offloadDir, 'test-user', 'test-session'],
435
+ });
436
+
437
+ const msgs = [
438
+ createMsg({
439
+ name: 'user',
440
+ content: [
441
+ {
442
+ type: 'data',
443
+ id: 'data-1',
444
+ source: {
445
+ type: 'url',
446
+ url: 'https://example.com/image.png',
447
+ mediaType: 'image/png',
448
+ },
449
+ },
450
+ ],
451
+ role: 'user',
452
+ }),
453
+ ];
454
+
455
+ const offloadPath = await storageWithOffload.offloadContext({
456
+ agentId: 'agent-1',
457
+ msgs,
458
+ });
459
+
460
+ const content = fs.readFileSync(offloadPath!, 'utf-8');
461
+ expect(content).toContain(
462
+ '<data src={https://example.com/image.png} type={image/png} />'
463
+ );
464
+ });
465
+
466
+ it('should handle data blocks with base64 source', async () => {
467
+ const offloadDir = path.join(testDir, 'offload');
468
+ const storageWithOffload = new LocalFileStorage({
469
+ pathSegments: [testDir, 'test-user', 'test-session'],
470
+ offloadPathSegments: [offloadDir, 'test-user', 'test-session'],
471
+ });
472
+
473
+ // Create a simple base64 encoded text
474
+ const base64Data = Buffer.from('Hello, World!').toString('base64');
475
+
476
+ const msgs = [
477
+ createMsg({
478
+ name: 'user',
479
+ content: [
480
+ {
481
+ type: 'data',
482
+ id: 'data-2',
483
+ source: {
484
+ type: 'base64',
485
+ data: base64Data,
486
+ mediaType: 'text/plain',
487
+ },
488
+ },
489
+ ],
490
+ role: 'user',
491
+ }),
492
+ ];
493
+
494
+ const offloadPath = await storageWithOffload.offloadContext({
495
+ agentId: 'agent-1',
496
+ msgs,
497
+ });
498
+
499
+ const content = fs.readFileSync(offloadPath!, 'utf-8');
500
+ expect(content).toMatch(/user: <data src=\{.*\.txt\} type=\{text\/plain\} \/>/);
501
+
502
+ // Verify the data file was created
503
+ const dataDir = path.join(offloadDir, 'test-user', 'test-session', 'agent-1', 'data');
504
+ expect(fs.existsSync(dataDir)).toBe(true);
505
+
506
+ const dataFiles = fs.readdirSync(dataDir);
507
+ expect(dataFiles.length).toBeGreaterThan(0);
508
+ expect(dataFiles[0]).toMatch(/text-\d+\.txt/);
509
+
510
+ // Verify the content of the data file
511
+ const dataFilePath = path.join(dataDir, dataFiles[0]);
512
+ const dataContent = fs.readFileSync(dataFilePath, 'utf-8');
513
+ expect(dataContent).toBe('Hello, World!');
514
+ });
515
+
516
+ it('should handle tool_call blocks', async () => {
517
+ const offloadDir = path.join(testDir, 'offload');
518
+ const storageWithOffload = new LocalFileStorage({
519
+ pathSegments: [testDir, 'test-user', 'test-session'],
520
+ offloadPathSegments: [offloadDir, 'test-user', 'test-session'],
521
+ });
522
+
523
+ const msgs = [
524
+ createMsg({
525
+ name: 'assistant',
526
+ content: [
527
+ {
528
+ type: 'tool_call',
529
+ id: 'call-123',
530
+ name: 'search',
531
+ input: JSON.stringify({ query: 'test' }),
532
+ },
533
+ ],
534
+ role: 'assistant',
535
+ }),
536
+ ];
537
+
538
+ const offloadPath = await storageWithOffload.offloadContext({
539
+ agentId: 'agent-1',
540
+ msgs,
541
+ });
542
+
543
+ const content = fs.readFileSync(offloadPath!, 'utf-8');
544
+ expect(content).toContain('assistant: Calling tool search ...');
545
+ });
546
+
547
+ it('should handle mixed content types in a single message', async () => {
548
+ const offloadDir = path.join(testDir, 'offload');
549
+ const storageWithOffload = new LocalFileStorage({
550
+ pathSegments: [testDir, 'test-user', 'test-session'],
551
+ offloadPathSegments: [offloadDir, 'test-user', 'test-session'],
552
+ });
553
+
554
+ const msgs = [
555
+ createMsg({
556
+ name: 'user',
557
+ content: [
558
+ { type: 'text', text: 'Here is an image:', id: 'text-34' },
559
+ {
560
+ type: 'data',
561
+ id: 'data-3',
562
+ source: {
563
+ type: 'url',
564
+ url: 'https://example.com/image.png',
565
+ mediaType: 'image/png',
566
+ },
567
+ },
568
+ { type: 'text', text: 'What do you see?', id: 'text-35' },
569
+ ],
570
+ role: 'user',
571
+ }),
572
+ ];
573
+
574
+ const offloadPath = await storageWithOffload.offloadContext({
575
+ agentId: 'agent-1',
576
+ msgs,
577
+ });
578
+
579
+ const content = fs.readFileSync(offloadPath!, 'utf-8');
580
+ expect(content).toContain('user: Here is an image:');
581
+ expect(content).toContain(
582
+ '<data src={https://example.com/image.png} type={image/png} />'
583
+ );
584
+ expect(content).toContain('user: What do you see?');
585
+ });
586
+ });
587
+ });