@agentscope-ai/agentscope 0.0.3 → 0.0.4

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 (65) hide show
  1. package/dist/agent/index.d.mts +7 -7
  2. package/dist/agent/index.d.ts +7 -7
  3. package/dist/agent/index.js +14 -7
  4. package/dist/agent/index.js.map +1 -1
  5. package/dist/agent/index.mjs +14 -7
  6. package/dist/agent/index.mjs.map +1 -1
  7. package/dist/{base-Bo8TzBQq.d.mts → base-1YVBgB4n.d.mts} +2 -2
  8. package/dist/{base-D9uCcDjZ.d.mts → base-B_MQMHWr.d.mts} +3 -3
  9. package/dist/{base-TYjCCv7T.d.ts → base-BherSLRs.d.ts} +3 -3
  10. package/dist/{base-Co-MzdN5.d.ts → base-CY4DMBH1.d.ts} +1 -1
  11. package/dist/{base-t7G4uaR_.d.ts → base-ChWjyzPL.d.ts} +2 -2
  12. package/dist/{base-Dh5vEBQD.d.mts → base-ClilytRZ.d.mts} +1 -1
  13. package/dist/{block-7fd6byyN.d.mts → block-B72uPF1H.d.mts} +5 -3
  14. package/dist/{block-7fd6byyN.d.ts → block-B72uPF1H.d.ts} +5 -3
  15. package/dist/event/index.d.mts +1 -1
  16. package/dist/event/index.d.ts +1 -1
  17. package/dist/formatter/index.d.mts +4 -3
  18. package/dist/formatter/index.d.ts +4 -3
  19. package/dist/formatter/index.js.map +1 -1
  20. package/dist/formatter/index.mjs.map +1 -1
  21. package/dist/{index-DaopL-Vp.d.ts → index-BNfyKbQN.d.ts} +1 -1
  22. package/dist/{index-BVNbIN62.d.mts → index-UQCwdfet.d.mts} +1 -1
  23. package/dist/mcp/index.d.mts +2 -2
  24. package/dist/mcp/index.d.ts +2 -2
  25. package/dist/message/index.d.mts +3 -2
  26. package/dist/message/index.d.ts +3 -2
  27. package/dist/message/index.js +165 -0
  28. package/dist/message/index.js.map +1 -1
  29. package/dist/message/index.mjs +164 -0
  30. package/dist/message/index.mjs.map +1 -1
  31. package/dist/{message-DZN7LetB.d.ts → message-CPZd0NIc.d.ts} +13 -2
  32. package/dist/{message-CYnHiEVt.d.mts → message-DgpfAaHK.d.mts} +13 -2
  33. package/dist/model/index.d.mts +6 -5
  34. package/dist/model/index.d.ts +6 -5
  35. package/dist/model/index.js +22 -11
  36. package/dist/model/index.js.map +1 -1
  37. package/dist/model/index.mjs +22 -11
  38. package/dist/model/index.mjs.map +1 -1
  39. package/dist/storage/index.d.mts +4 -3
  40. package/dist/storage/index.d.ts +4 -3
  41. package/dist/tool/index.d.mts +4 -4
  42. package/dist/tool/index.d.ts +4 -4
  43. package/dist/{toolkit-BuMTkbGg.d.ts → toolkit-DeOlul5Y.d.ts} +2 -2
  44. package/dist/{toolkit-CH9qKAy9.d.mts → toolkit-jwe7NmVJ.d.mts} +2 -2
  45. package/package.json +1 -1
  46. package/src/agent/agent.test.ts +41 -8
  47. package/src/agent/agent.ts +15 -9
  48. package/src/formatter/dashscope-chat-formatter.test.ts +3 -0
  49. package/src/formatter/openai-chat-formatter.test.ts +9 -1
  50. package/src/mcp/http.test.ts +2 -0
  51. package/src/mcp/stdio.test.ts +1 -0
  52. package/src/message/append-event.test.ts +783 -0
  53. package/src/message/block.ts +6 -2
  54. package/src/message/index.ts +3 -0
  55. package/src/message/message.test.ts +2 -0
  56. package/src/message/message.ts +209 -0
  57. package/src/model/dashscope-model.test.ts +4 -0
  58. package/src/model/dashscope-model.ts +3 -0
  59. package/src/model/deepseek-model.test.ts +2 -0
  60. package/src/model/deepseek-model.ts +3 -0
  61. package/src/model/ollama-model.test.ts +1 -0
  62. package/src/model/ollama-model.ts +2 -0
  63. package/src/model/openai-model.ts +3 -0
  64. package/src/storage/file-system.test.ts +1 -0
  65. package/src/tool/toolkit.test.ts +12 -0
@@ -0,0 +1,783 @@
1
+ import { Msg, createMsg, appendEvent } from './message';
2
+ import { EventType, AgentEvent } from '../event';
3
+ import {
4
+ ContentBlock,
5
+ DataBlock,
6
+ TextBlock,
7
+ ThinkingBlock,
8
+ ToolCallBlock,
9
+ ToolResultBlock,
10
+ } from './block';
11
+
12
+ // Fixed IDs used throughout
13
+ const REPLY_ID = 'reply_001';
14
+ const SESSION_ID = 'session_001';
15
+
16
+ const B_TEXT = 'b_text_001';
17
+ const B_THINK = 'b_think_001';
18
+ const B_DATA = 'b_data_001';
19
+
20
+ const TC_ALLOW = 'tc_allow_001';
21
+ const TC_DENY = 'tc_deny_001';
22
+ const TC_EXT = 'tc_ext_001';
23
+ const TC_IMG = 'tc_img_001';
24
+
25
+ const RES_DATA_B = 'res_data_001';
26
+ const RES_URL_B = 'res_url_001';
27
+
28
+ const FIXED_END_TS = '2026-01-01T12:00:00';
29
+
30
+ // Block-dict helpers
31
+ /**
32
+ * Creates a text block dict for testing.
33
+ * @param blockId
34
+ * @param text
35
+ * @returns A text block object.
36
+ */
37
+ function tb(blockId: string, text: string): TextBlock {
38
+ return { type: 'text', id: blockId, text };
39
+ }
40
+
41
+ /**
42
+ * Creates a thinking block dict for testing.
43
+ * @param blockId
44
+ * @param thinking
45
+ * @returns A thinking block object.
46
+ */
47
+ function thb(blockId: string, thinking: string): ThinkingBlock {
48
+ return { type: 'thinking', id: blockId, thinking };
49
+ }
50
+
51
+ /**
52
+ * Creates a base64 data block dict for testing.
53
+ * @param blockId
54
+ * @param data
55
+ * @param mediaType
56
+ * @returns A base64 data block object.
57
+ */
58
+ function dbB64(blockId: string, data: string, mediaType: string): DataBlock {
59
+ return {
60
+ type: 'data',
61
+ id: blockId,
62
+ source: { type: 'base64', data, media_type: mediaType },
63
+ };
64
+ }
65
+
66
+ /**
67
+ * Creates a URL data block dict for testing.
68
+ * @param blockId
69
+ * @param url
70
+ * @param mediaType
71
+ * @returns A URL data block object.
72
+ */
73
+ function dbUrl(blockId: string, url: string, mediaType: string): DataBlock {
74
+ return {
75
+ type: 'data',
76
+ id: blockId,
77
+ source: { type: 'url', url, media_type: mediaType },
78
+ };
79
+ }
80
+
81
+ /**
82
+ * Creates a tool call block dict for testing.
83
+ * @param tcId
84
+ * @param name
85
+ * @param inp
86
+ * @param state
87
+ * @returns A tool call block object.
88
+ */
89
+ function tcb(
90
+ tcId: string,
91
+ name: string,
92
+ inp: string,
93
+ state: ToolCallBlock['state']
94
+ ): ToolCallBlock {
95
+ return { type: 'tool_call', id: tcId, name, input: inp, state };
96
+ }
97
+
98
+ /**
99
+ * Creates a tool result block dict for testing.
100
+ * @param tcId
101
+ * @param name
102
+ * @param output
103
+ * @param state
104
+ * @returns A tool result block object.
105
+ */
106
+ function trb(
107
+ tcId: string,
108
+ name: string,
109
+ output: ToolResultBlock['output'],
110
+ state: ToolResultBlock['state']
111
+ ): ToolResultBlock {
112
+ return { type: 'tool_result', id: tcId, name, output, state };
113
+ }
114
+
115
+ describe('appendEvent', () => {
116
+ let msg: Msg;
117
+ let createdAt: string;
118
+
119
+ beforeEach(() => {
120
+ msg = createMsg({
121
+ id: REPLY_ID,
122
+ name: 'TestAgent',
123
+ role: 'assistant',
124
+ content: [],
125
+ });
126
+ createdAt = msg.created_at;
127
+ });
128
+
129
+ /**
130
+ * Creates a base message object for comparison in tests.
131
+ * @param content
132
+ * @param finishedAt
133
+ * @returns A plain message object for comparison.
134
+ */
135
+ function base(content: ContentBlock[], finishedAt: string | null = null) {
136
+ return {
137
+ id: REPLY_ID,
138
+ name: 'TestAgent',
139
+ role: 'assistant',
140
+ metadata: {},
141
+ created_at: createdAt,
142
+ finished_at: finishedAt,
143
+ content,
144
+ };
145
+ }
146
+
147
+ /**
148
+ * Extracts a plain object representation of a Msg for comparison.
149
+ * @param m
150
+ * @returns A plain object with message fields.
151
+ */
152
+ function msgDump(m: Msg) {
153
+ return {
154
+ id: m.id,
155
+ name: m.name,
156
+ role: m.role,
157
+ metadata: m.metadata,
158
+ created_at: m.created_at,
159
+ finished_at: m.finished_at ?? null,
160
+ content: m.content,
161
+ };
162
+ }
163
+
164
+ test('full streaming event sequence', () => {
165
+ const events: AgentEvent[] = [];
166
+ const groundTruths: ReturnType<typeof base>[] = [];
167
+
168
+ // Stage 1: Text block streaming
169
+ events.push({
170
+ id: '1',
171
+ created_at: '',
172
+ type: EventType.TEXT_BLOCK_START,
173
+ reply_id: REPLY_ID,
174
+ block_id: B_TEXT,
175
+ });
176
+ groundTruths.push(base([tb(B_TEXT, '')]));
177
+
178
+ events.push({
179
+ id: '2',
180
+ created_at: '',
181
+ type: EventType.TEXT_BLOCK_DELTA,
182
+ reply_id: REPLY_ID,
183
+ block_id: B_TEXT,
184
+ delta: 'Hello',
185
+ });
186
+ groundTruths.push(base([tb(B_TEXT, 'Hello')]));
187
+
188
+ events.push({
189
+ id: '3',
190
+ created_at: '',
191
+ type: EventType.TEXT_BLOCK_DELTA,
192
+ reply_id: REPLY_ID,
193
+ block_id: B_TEXT,
194
+ delta: ' World',
195
+ });
196
+ groundTruths.push(base([tb(B_TEXT, 'Hello World')]));
197
+
198
+ events.push({
199
+ id: '4',
200
+ created_at: '',
201
+ type: EventType.TEXT_BLOCK_END,
202
+ reply_id: REPLY_ID,
203
+ block_id: B_TEXT,
204
+ });
205
+ groundTruths.push(base([tb(B_TEXT, 'Hello World')]));
206
+
207
+ // Stage 2: Thinking block streaming
208
+ events.push({
209
+ id: '5',
210
+ created_at: '',
211
+ type: EventType.THINKING_BLOCK_START,
212
+ reply_id: REPLY_ID,
213
+ block_id: B_THINK,
214
+ });
215
+ groundTruths.push(base([tb(B_TEXT, 'Hello World'), thb(B_THINK, '')]));
216
+
217
+ events.push({
218
+ id: '6',
219
+ created_at: '',
220
+ type: EventType.THINKING_BLOCK_DELTA,
221
+ reply_id: REPLY_ID,
222
+ block_id: B_THINK,
223
+ delta: 'Let me',
224
+ });
225
+ groundTruths.push(base([tb(B_TEXT, 'Hello World'), thb(B_THINK, 'Let me')]));
226
+
227
+ events.push({
228
+ id: '7',
229
+ created_at: '',
230
+ type: EventType.THINKING_BLOCK_DELTA,
231
+ reply_id: REPLY_ID,
232
+ block_id: B_THINK,
233
+ delta: ' think',
234
+ });
235
+ groundTruths.push(base([tb(B_TEXT, 'Hello World'), thb(B_THINK, 'Let me think')]));
236
+
237
+ events.push({
238
+ id: '8',
239
+ created_at: '',
240
+ type: EventType.THINKING_BLOCK_END,
241
+ reply_id: REPLY_ID,
242
+ block_id: B_THINK,
243
+ });
244
+ groundTruths.push(base([tb(B_TEXT, 'Hello World'), thb(B_THINK, 'Let me think')]));
245
+
246
+ // Stage 3: Data block streaming (base64)
247
+ events.push({
248
+ id: '9',
249
+ created_at: '',
250
+ type: EventType.DATA_BLOCK_START,
251
+ reply_id: REPLY_ID,
252
+ block_id: B_DATA,
253
+ media_type: 'image/png',
254
+ });
255
+ groundTruths.push(
256
+ base([
257
+ tb(B_TEXT, 'Hello World'),
258
+ thb(B_THINK, 'Let me think'),
259
+ dbB64(B_DATA, '', 'image/png'),
260
+ ])
261
+ );
262
+
263
+ events.push({
264
+ id: '10',
265
+ created_at: '',
266
+ type: EventType.DATA_BLOCK_DELTA,
267
+ reply_id: REPLY_ID,
268
+ block_id: B_DATA,
269
+ data: 'abc',
270
+ media_type: 'image/png',
271
+ });
272
+ groundTruths.push(
273
+ base([
274
+ tb(B_TEXT, 'Hello World'),
275
+ thb(B_THINK, 'Let me think'),
276
+ dbB64(B_DATA, 'abc', 'image/png'),
277
+ ])
278
+ );
279
+
280
+ events.push({
281
+ id: '11',
282
+ created_at: '',
283
+ type: EventType.DATA_BLOCK_DELTA,
284
+ reply_id: REPLY_ID,
285
+ block_id: B_DATA,
286
+ data: 'def',
287
+ media_type: 'image/png',
288
+ });
289
+ groundTruths.push(
290
+ base([
291
+ tb(B_TEXT, 'Hello World'),
292
+ thb(B_THINK, 'Let me think'),
293
+ dbB64(B_DATA, 'abcdef', 'image/png'),
294
+ ])
295
+ );
296
+
297
+ events.push({
298
+ id: '12',
299
+ created_at: '',
300
+ type: EventType.DATA_BLOCK_END,
301
+ reply_id: REPLY_ID,
302
+ block_id: B_DATA,
303
+ });
304
+ groundTruths.push(
305
+ base([
306
+ tb(B_TEXT, 'Hello World'),
307
+ thb(B_THINK, 'Let me think'),
308
+ dbB64(B_DATA, 'abcdef', 'image/png'),
309
+ ])
310
+ );
311
+
312
+ // Stage 4: ToolCall → confirm (allowed) + text result (success)
313
+ const s4Prefix = [
314
+ tb(B_TEXT, 'Hello World'),
315
+ thb(B_THINK, 'Let me think'),
316
+ dbB64(B_DATA, 'abcdef', 'image/png'),
317
+ ];
318
+
319
+ events.push({
320
+ id: '13',
321
+ created_at: '',
322
+ type: EventType.TOOL_CALL_START,
323
+ reply_id: REPLY_ID,
324
+ tool_call_id: TC_ALLOW,
325
+ tool_call_name: 'search',
326
+ });
327
+ groundTruths.push(base([...s4Prefix, tcb(TC_ALLOW, 'search', '', 'pending')]));
328
+
329
+ events.push({
330
+ id: '14',
331
+ created_at: '',
332
+ type: EventType.TOOL_CALL_DELTA,
333
+ reply_id: REPLY_ID,
334
+ tool_call_id: TC_ALLOW,
335
+ delta: '{"q"',
336
+ });
337
+ groundTruths.push(base([...s4Prefix, tcb(TC_ALLOW, 'search', '{"q"', 'pending')]));
338
+
339
+ events.push({
340
+ id: '15',
341
+ created_at: '',
342
+ type: EventType.TOOL_CALL_DELTA,
343
+ reply_id: REPLY_ID,
344
+ tool_call_id: TC_ALLOW,
345
+ delta: ': "hi"}',
346
+ });
347
+ groundTruths.push(base([...s4Prefix, tcb(TC_ALLOW, 'search', '{"q": "hi"}', 'pending')]));
348
+
349
+ events.push({
350
+ id: '16',
351
+ created_at: '',
352
+ type: EventType.TOOL_CALL_END,
353
+ reply_id: REPLY_ID,
354
+ tool_call_id: TC_ALLOW,
355
+ });
356
+ groundTruths.push(base([...s4Prefix, tcb(TC_ALLOW, 'search', '{"q": "hi"}', 'pending')]));
357
+
358
+ // RequireUserConfirmEvent → state: pending → asking
359
+ const tcAllowBlock: ToolCallBlock = {
360
+ type: 'tool_call',
361
+ id: TC_ALLOW,
362
+ name: 'search',
363
+ input: '{"q": "hi"}',
364
+ state: 'pending',
365
+ };
366
+ events.push({
367
+ id: '17',
368
+ created_at: '',
369
+ type: EventType.REQUIRE_USER_CONFIRM,
370
+ reply_id: REPLY_ID,
371
+ tool_calls: [tcAllowBlock],
372
+ });
373
+ groundTruths.push(base([...s4Prefix, tcb(TC_ALLOW, 'search', '{"q": "hi"}', 'asking')]));
374
+
375
+ // UserConfirmResultEvent (confirmed=true) → state: asking → allowed
376
+ events.push({
377
+ id: '18',
378
+ created_at: '',
379
+ type: EventType.USER_CONFIRM_RESULT,
380
+ reply_id: REPLY_ID,
381
+ confirm_results: [{ confirmed: true, tool_call: tcAllowBlock }],
382
+ });
383
+ groundTruths.push(base([...s4Prefix, tcb(TC_ALLOW, 'search', '{"q": "hi"}', 'allowed')]));
384
+
385
+ // ToolResult for TC_ALLOW - text output
386
+ events.push({
387
+ id: '19',
388
+ created_at: '',
389
+ type: EventType.TOOL_RESULT_START,
390
+ reply_id: REPLY_ID,
391
+ tool_call_id: TC_ALLOW,
392
+ tool_call_name: 'search',
393
+ });
394
+ const s4bPrefix = [...s4Prefix, tcb(TC_ALLOW, 'search', '{"q": "hi"}', 'allowed')];
395
+ groundTruths.push(base([...s4bPrefix, trb(TC_ALLOW, 'search', [], 'running')]));
396
+
397
+ // ToolResult text deltas
398
+ events.push({
399
+ id: '20',
400
+ created_at: '',
401
+ type: EventType.TOOL_RESULT_TEXT_DELTA,
402
+ reply_id: REPLY_ID,
403
+ tool_call_id: TC_ALLOW,
404
+ block_id: 'auto_1',
405
+ delta: 'Found:',
406
+ });
407
+ groundTruths.push(
408
+ base([
409
+ ...s4bPrefix,
410
+ trb(
411
+ TC_ALLOW,
412
+ 'search',
413
+ [{ type: 'text', id: expect.any(String), text: 'Found:' }],
414
+ 'running'
415
+ ),
416
+ ])
417
+ );
418
+
419
+ events.push({
420
+ id: '21',
421
+ created_at: '',
422
+ type: EventType.TOOL_RESULT_TEXT_DELTA,
423
+ reply_id: REPLY_ID,
424
+ tool_call_id: TC_ALLOW,
425
+ block_id: 'auto_1',
426
+ delta: ' 3 items',
427
+ });
428
+ groundTruths.push(
429
+ base([
430
+ ...s4bPrefix,
431
+ trb(
432
+ TC_ALLOW,
433
+ 'search',
434
+ [{ type: 'text', id: expect.any(String), text: 'Found: 3 items' }],
435
+ 'running'
436
+ ),
437
+ ])
438
+ );
439
+
440
+ events.push({
441
+ id: '22',
442
+ created_at: '',
443
+ type: EventType.TOOL_RESULT_END,
444
+ reply_id: REPLY_ID,
445
+ tool_call_id: TC_ALLOW,
446
+ state: 'success',
447
+ });
448
+ groundTruths.push(
449
+ base([
450
+ ...s4bPrefix,
451
+ trb(
452
+ TC_ALLOW,
453
+ 'search',
454
+ [{ type: 'text', id: expect.any(String), text: 'Found: 3 items' }],
455
+ 'success'
456
+ ),
457
+ ])
458
+ );
459
+
460
+ // Stage 5: ToolCall (TC_DENY) → confirm → denied (finished)
461
+ const s5Prefix = [
462
+ ...s4bPrefix,
463
+ trb(
464
+ TC_ALLOW,
465
+ 'search',
466
+ [{ type: 'text', id: expect.any(String), text: 'Found: 3 items' }],
467
+ 'success'
468
+ ),
469
+ ];
470
+
471
+ events.push({
472
+ id: '23',
473
+ created_at: '',
474
+ type: EventType.TOOL_CALL_START,
475
+ reply_id: REPLY_ID,
476
+ tool_call_id: TC_DENY,
477
+ tool_call_name: 'delete',
478
+ });
479
+ groundTruths.push(base([...s5Prefix, tcb(TC_DENY, 'delete', '', 'pending')]));
480
+
481
+ events.push({
482
+ id: '24',
483
+ created_at: '',
484
+ type: EventType.TOOL_CALL_END,
485
+ reply_id: REPLY_ID,
486
+ tool_call_id: TC_DENY,
487
+ });
488
+ groundTruths.push(base([...s5Prefix, tcb(TC_DENY, 'delete', '', 'pending')]));
489
+
490
+ const tcDenyBlock: ToolCallBlock = {
491
+ type: 'tool_call',
492
+ id: TC_DENY,
493
+ name: 'delete',
494
+ input: '',
495
+ state: 'pending',
496
+ };
497
+ events.push({
498
+ id: '25',
499
+ created_at: '',
500
+ type: EventType.REQUIRE_USER_CONFIRM,
501
+ reply_id: REPLY_ID,
502
+ tool_calls: [tcDenyBlock],
503
+ });
504
+ groundTruths.push(base([...s5Prefix, tcb(TC_DENY, 'delete', '', 'asking')]));
505
+
506
+ events.push({
507
+ id: '26',
508
+ created_at: '',
509
+ type: EventType.USER_CONFIRM_RESULT,
510
+ reply_id: REPLY_ID,
511
+ confirm_results: [{ confirmed: false, tool_call: tcDenyBlock }],
512
+ });
513
+ groundTruths.push(base([...s5Prefix, tcb(TC_DENY, 'delete', '', 'finished')]));
514
+
515
+ // Stage 6: ToolCall (TC_EXT) → external execution
516
+ const s6Prefix = [...s5Prefix, tcb(TC_DENY, 'delete', '', 'finished')];
517
+
518
+ events.push({
519
+ id: '27',
520
+ created_at: '',
521
+ type: EventType.TOOL_CALL_START,
522
+ reply_id: REPLY_ID,
523
+ tool_call_id: TC_EXT,
524
+ tool_call_name: 'run_code',
525
+ });
526
+ groundTruths.push(base([...s6Prefix, tcb(TC_EXT, 'run_code', '', 'pending')]));
527
+
528
+ events.push({
529
+ id: '28',
530
+ created_at: '',
531
+ type: EventType.TOOL_CALL_END,
532
+ reply_id: REPLY_ID,
533
+ tool_call_id: TC_EXT,
534
+ });
535
+ groundTruths.push(base([...s6Prefix, tcb(TC_EXT, 'run_code', '', 'pending')]));
536
+
537
+ const tcExtBlock: ToolCallBlock = {
538
+ type: 'tool_call',
539
+ id: TC_EXT,
540
+ name: 'run_code',
541
+ input: '',
542
+ state: 'pending',
543
+ };
544
+ events.push({
545
+ id: '29',
546
+ created_at: '',
547
+ type: EventType.REQUIRE_EXTERNAL_EXECUTION,
548
+ reply_id: REPLY_ID,
549
+ tool_calls: [tcExtBlock],
550
+ });
551
+ groundTruths.push(base([...s6Prefix, tcb(TC_EXT, 'run_code', '', 'submitted')]));
552
+
553
+ // ExternalExecutionResultEvent
554
+ const extResultBlock: ToolResultBlock = {
555
+ type: 'tool_result',
556
+ id: TC_EXT,
557
+ name: 'run_code',
558
+ output: 'output: hello',
559
+ state: 'success',
560
+ };
561
+ events.push({
562
+ id: '30',
563
+ created_at: '',
564
+ type: EventType.EXTERNAL_EXECUTION_RESULT,
565
+ reply_id: REPLY_ID,
566
+ execution_results: [extResultBlock],
567
+ });
568
+ const s6bPrefix = [...s6Prefix, tcb(TC_EXT, 'run_code', '', 'submitted')];
569
+ groundTruths.push(
570
+ base([...s6bPrefix, trb(TC_EXT, 'run_code', 'output: hello', 'success')])
571
+ );
572
+
573
+ // Stage 7: ToolResult with data output (base64 + URL)
574
+ const s7Prefix = [...s6bPrefix, trb(TC_EXT, 'run_code', 'output: hello', 'success')];
575
+
576
+ events.push({
577
+ id: '31',
578
+ created_at: '',
579
+ type: EventType.TOOL_CALL_START,
580
+ reply_id: REPLY_ID,
581
+ tool_call_id: TC_IMG,
582
+ tool_call_name: 'screenshot',
583
+ });
584
+ groundTruths.push(base([...s7Prefix, tcb(TC_IMG, 'screenshot', '', 'pending')]));
585
+
586
+ events.push({
587
+ id: '32',
588
+ created_at: '',
589
+ type: EventType.TOOL_CALL_END,
590
+ reply_id: REPLY_ID,
591
+ tool_call_id: TC_IMG,
592
+ });
593
+ groundTruths.push(base([...s7Prefix, tcb(TC_IMG, 'screenshot', '', 'pending')]));
594
+
595
+ events.push({
596
+ id: '33',
597
+ created_at: '',
598
+ type: EventType.TOOL_RESULT_START,
599
+ reply_id: REPLY_ID,
600
+ tool_call_id: TC_IMG,
601
+ tool_call_name: 'screenshot',
602
+ });
603
+ const s7bPrefix = [...s7Prefix, tcb(TC_IMG, 'screenshot', '', 'pending')];
604
+ groundTruths.push(base([...s7bPrefix, trb(TC_IMG, 'screenshot', [], 'running')]));
605
+
606
+ events.push({
607
+ id: '34',
608
+ created_at: '',
609
+ type: EventType.TOOL_RESULT_DATA_DELTA,
610
+ reply_id: REPLY_ID,
611
+ tool_call_id: TC_IMG,
612
+ block_id: RES_DATA_B,
613
+ media_type: 'image/png',
614
+ data: 'iVBOR==',
615
+ });
616
+ groundTruths.push(
617
+ base([
618
+ ...s7bPrefix,
619
+ trb(TC_IMG, 'screenshot', [dbB64(RES_DATA_B, 'iVBOR==', 'image/png')], 'running'),
620
+ ])
621
+ );
622
+
623
+ events.push({
624
+ id: '35',
625
+ created_at: '',
626
+ type: EventType.TOOL_RESULT_DATA_DELTA,
627
+ reply_id: REPLY_ID,
628
+ tool_call_id: TC_IMG,
629
+ block_id: RES_URL_B,
630
+ media_type: 'image/jpeg',
631
+ url: 'https://example.com/img.jpg',
632
+ });
633
+ groundTruths.push(
634
+ base([
635
+ ...s7bPrefix,
636
+ trb(
637
+ TC_IMG,
638
+ 'screenshot',
639
+ [
640
+ dbB64(RES_DATA_B, 'iVBOR==', 'image/png'),
641
+ dbUrl(RES_URL_B, 'https://example.com/img.jpg', 'image/jpeg'),
642
+ ],
643
+ 'running'
644
+ ),
645
+ ])
646
+ );
647
+
648
+ events.push({
649
+ id: '36',
650
+ created_at: '',
651
+ type: EventType.TOOL_RESULT_END,
652
+ reply_id: REPLY_ID,
653
+ tool_call_id: TC_IMG,
654
+ state: 'error',
655
+ });
656
+ groundTruths.push(
657
+ base([
658
+ ...s7bPrefix,
659
+ trb(
660
+ TC_IMG,
661
+ 'screenshot',
662
+ [
663
+ dbB64(RES_DATA_B, 'iVBOR==', 'image/png'),
664
+ dbUrl(RES_URL_B, 'https://example.com/img.jpg', 'image/jpeg'),
665
+ ],
666
+ 'error'
667
+ ),
668
+ ])
669
+ );
670
+
671
+ // Stage 8: ReplyEnd
672
+ events.push({
673
+ id: '37',
674
+ created_at: FIXED_END_TS,
675
+ type: EventType.REPLY_END,
676
+ reply_id: REPLY_ID,
677
+ session_id: SESSION_ID,
678
+ });
679
+ const finalContent = [
680
+ ...s7bPrefix,
681
+ trb(
682
+ TC_IMG,
683
+ 'screenshot',
684
+ [
685
+ dbB64(RES_DATA_B, 'iVBOR==', 'image/png'),
686
+ dbUrl(RES_URL_B, 'https://example.com/img.jpg', 'image/jpeg'),
687
+ ],
688
+ 'error'
689
+ ),
690
+ ];
691
+ groundTruths.push(base(finalContent, FIXED_END_TS));
692
+
693
+ // Apply all events and check ground truths
694
+ expect(events.length).toBe(groundTruths.length);
695
+ for (let i = 0; i < events.length; i++) {
696
+ appendEvent(msg, events[i]);
697
+ expect(msgDump(msg)).toEqual(groundTruths[i]);
698
+ }
699
+ });
700
+
701
+ test('wrong reply_id is skipped', () => {
702
+ const original = msgDump(msg);
703
+ const wrongEvent: AgentEvent = {
704
+ id: 'x',
705
+ created_at: '',
706
+ type: EventType.TEXT_BLOCK_START,
707
+ reply_id: 'totally_wrong_id',
708
+ block_id: 'should_not_appear',
709
+ };
710
+ appendEvent(msg, wrongEvent);
711
+ expect(msgDump(msg)).toEqual(original);
712
+ });
713
+
714
+ test('missing block does not crash', () => {
715
+ const original = msgDump(msg);
716
+ const ghostEvents: AgentEvent[] = [
717
+ {
718
+ id: 'g1',
719
+ created_at: '',
720
+ type: EventType.TEXT_BLOCK_DELTA,
721
+ reply_id: REPLY_ID,
722
+ block_id: 'ghost',
723
+ delta: 'x',
724
+ },
725
+ {
726
+ id: 'g2',
727
+ created_at: '',
728
+ type: EventType.THINKING_BLOCK_DELTA,
729
+ reply_id: REPLY_ID,
730
+ block_id: 'ghost',
731
+ delta: 'x',
732
+ },
733
+ {
734
+ id: 'g3',
735
+ created_at: '',
736
+ type: EventType.DATA_BLOCK_DELTA,
737
+ reply_id: REPLY_ID,
738
+ block_id: 'ghost',
739
+ data: 'x',
740
+ media_type: 'image/png',
741
+ },
742
+ {
743
+ id: 'g4',
744
+ created_at: '',
745
+ type: EventType.TOOL_CALL_DELTA,
746
+ reply_id: REPLY_ID,
747
+ tool_call_id: 'ghost',
748
+ delta: 'x',
749
+ },
750
+ {
751
+ id: 'g5',
752
+ created_at: '',
753
+ type: EventType.TOOL_RESULT_TEXT_DELTA,
754
+ reply_id: REPLY_ID,
755
+ tool_call_id: 'ghost',
756
+ block_id: 'b',
757
+ delta: 'x',
758
+ },
759
+ {
760
+ id: 'g6',
761
+ created_at: '',
762
+ type: EventType.TOOL_RESULT_DATA_DELTA,
763
+ reply_id: REPLY_ID,
764
+ tool_call_id: 'ghost',
765
+ block_id: 'b',
766
+ media_type: 'image/png',
767
+ data: 'x',
768
+ },
769
+ {
770
+ id: 'g7',
771
+ created_at: '',
772
+ type: EventType.TOOL_RESULT_END,
773
+ reply_id: REPLY_ID,
774
+ tool_call_id: 'ghost',
775
+ state: 'success',
776
+ },
777
+ ];
778
+ for (const ev of ghostEvents) {
779
+ appendEvent(msg, ev);
780
+ }
781
+ expect(msgDump(msg)).toEqual(original);
782
+ });
783
+ });