@agentxjs/core 1.9.9-dev → 2.0.0

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 (99) hide show
  1. package/README.md +342 -0
  2. package/dist/RpcClient-BcJ_zAGu.d.ts +304 -0
  3. package/dist/agent/engine/internal/index.d.ts +20 -15
  4. package/dist/agent/engine/internal/index.js +1 -2
  5. package/dist/agent/engine/mealy/index.js +0 -1
  6. package/dist/agent/index.d.ts +4 -4
  7. package/dist/agent/index.js +6 -6
  8. package/dist/agent/types/index.d.ts +4 -4
  9. package/dist/agent/types/index.js +1 -2
  10. package/dist/bash/index.d.ts +29 -0
  11. package/dist/bash/index.js +7 -0
  12. package/dist/{bus-uF1DM2ox.d.ts → bus-C9FLWIu8.d.ts} +3 -1
  13. package/dist/{chunk-K6WXQ2RW.js → chunk-23UUBQXR.js} +1 -2
  14. package/dist/chunk-23UUBQXR.js.map +1 -0
  15. package/dist/chunk-BHOD5PKR.js +55 -0
  16. package/dist/chunk-BHOD5PKR.js.map +1 -0
  17. package/dist/{chunk-I7GYR3MN.js → chunk-DEAR6N3O.js} +77 -91
  18. package/dist/chunk-DEAR6N3O.js.map +1 -0
  19. package/dist/chunk-FI7WQFGV.js +37 -0
  20. package/dist/chunk-FI7WQFGV.js.map +1 -0
  21. package/dist/{chunk-TBU7FFZT.js → chunk-JTKCV7IS.js} +4 -4
  22. package/dist/chunk-JTKCV7IS.js.map +1 -0
  23. package/dist/{chunk-E5FPOAPO.js → chunk-LTVNPHST.js} +1 -1
  24. package/dist/chunk-LTVNPHST.js.map +1 -0
  25. package/dist/chunk-SKS7S2RY.js +1 -0
  26. package/dist/common/logger/index.js +0 -2
  27. package/dist/common/logger/index.js.map +1 -1
  28. package/dist/container/index.d.ts +3 -4
  29. package/dist/container/index.js +0 -2
  30. package/dist/container/index.js.map +1 -1
  31. package/dist/driver/index.d.ts +2 -310
  32. package/dist/event/index.d.ts +4 -4
  33. package/dist/event/index.js +1 -2
  34. package/dist/event/types/index.d.ts +4 -10
  35. package/dist/event/types/index.js +1 -2
  36. package/dist/{event-CDuTzs__.d.ts → event-DNWOBSBO.d.ts} +3 -4
  37. package/dist/image/index.d.ts +9 -5
  38. package/dist/image/index.js +5 -2
  39. package/dist/image/index.js.map +1 -1
  40. package/dist/index-CuS1i5V-.d.ts +609 -0
  41. package/dist/index.d.ts +2 -2
  42. package/dist/index.js +6 -6
  43. package/dist/{message-BMrMm1pq.d.ts → message-03TJzvIX.d.ts} +10 -33
  44. package/dist/mq/index.js +0 -2
  45. package/dist/mq/index.js.map +1 -1
  46. package/dist/network/index.d.ts +3 -291
  47. package/dist/network/index.js +3 -14
  48. package/dist/network/index.js.map +1 -1
  49. package/dist/persistence/index.d.ts +2 -155
  50. package/dist/platform/index.d.ts +76 -0
  51. package/dist/platform/index.js.map +1 -0
  52. package/dist/runtime/index.d.ts +26 -59
  53. package/dist/runtime/index.js +117 -33
  54. package/dist/runtime/index.js.map +1 -1
  55. package/dist/session/index.d.ts +4 -52
  56. package/dist/session/index.js +4 -51
  57. package/dist/session/index.js.map +1 -1
  58. package/dist/types-aE74Eo6G.d.ts +90 -0
  59. package/package.json +10 -5
  60. package/src/agent/__tests__/engine/internal/messageAssemblerProcessor.test.ts +291 -87
  61. package/src/agent/__tests__/engine/internal/turnTrackerProcessor.test.ts +56 -75
  62. package/src/agent/engine/MealyMachine.ts +1 -1
  63. package/src/agent/engine/internal/messageAssemblerProcessor.ts +99 -114
  64. package/src/agent/engine/internal/turnTrackerProcessor.ts +23 -27
  65. package/src/agent/types/event.ts +0 -4
  66. package/src/agent/types/index.ts +1 -3
  67. package/src/agent/types/message.ts +9 -43
  68. package/src/bash/index.ts +21 -0
  69. package/src/bash/tool.ts +57 -0
  70. package/src/bash/types.ts +108 -0
  71. package/src/driver/index.ts +1 -0
  72. package/src/driver/types.ts +122 -4
  73. package/src/event/__tests__/EventBus.test.ts +1 -1
  74. package/src/event/types/agent.ts +0 -11
  75. package/src/event/types/command.ts +3 -1
  76. package/src/image/Image.ts +11 -1
  77. package/src/image/types.ts +8 -2
  78. package/src/network/RpcClient.ts +21 -20
  79. package/src/network/index.ts +1 -1
  80. package/src/persistence/types.ts +5 -2
  81. package/src/platform/index.ts +21 -0
  82. package/src/platform/types.ts +84 -0
  83. package/src/runtime/AgentXRuntime.ts +184 -57
  84. package/src/runtime/__tests__/AgentXRuntime.test.ts +343 -0
  85. package/src/runtime/index.ts +7 -19
  86. package/src/runtime/types.ts +10 -62
  87. package/dist/chunk-7D4SUZUM.js +0 -38
  88. package/dist/chunk-E5FPOAPO.js.map +0 -1
  89. package/dist/chunk-I7GYR3MN.js.map +0 -1
  90. package/dist/chunk-K6WXQ2RW.js.map +0 -1
  91. package/dist/chunk-TBU7FFZT.js.map +0 -1
  92. package/dist/workspace/index.d.ts +0 -111
  93. package/dist/wrapper-Y3UTVU2E.js +0 -3635
  94. package/dist/wrapper-Y3UTVU2E.js.map +0 -1
  95. package/src/workspace/index.ts +0 -27
  96. package/src/workspace/types.ts +0 -131
  97. /package/dist/{workspace → bash}/index.js.map +0 -0
  98. /package/dist/{chunk-7D4SUZUM.js.map → chunk-SKS7S2RY.js.map} +0 -0
  99. /package/dist/{workspace → platform}/index.js +0 -0
@@ -36,7 +36,7 @@ describe("messageAssemblerProcessor", () => {
36
36
 
37
37
  expect(initialState.currentMessageId).toBeNull();
38
38
  expect(initialState.messageStartTime).toBeNull();
39
- expect(initialState.pendingContents).toEqual({});
39
+ expect(initialState.pendingContents).toEqual([]);
40
40
  expect(initialState.pendingToolCalls).toEqual({});
41
41
  });
42
42
  });
@@ -49,19 +49,19 @@ describe("messageAssemblerProcessor", () => {
49
49
 
50
50
  expect(newState.currentMessageId).toBe("msg_123");
51
51
  expect(newState.messageStartTime).toBe(1000);
52
- expect(newState.pendingContents).toEqual({});
52
+ expect(newState.pendingContents).toEqual([]);
53
53
  expect(outputs).toHaveLength(0);
54
54
  });
55
55
 
56
56
  it("should reset pendingContents on new message", () => {
57
57
  // First message with some content
58
- state.pendingContents = { 0: { type: "text", index: 0, textDeltas: ["old"] } };
58
+ state.pendingContents = [{ type: "text", textDeltas: ["old"] }];
59
59
 
60
60
  const event = createStreamEvent("message_start", { messageId: "msg_new" });
61
61
 
62
62
  const [newState, outputs] = messageAssemblerProcessor(state, event);
63
63
 
64
- expect(newState.pendingContents).toEqual({});
64
+ expect(newState.pendingContents).toEqual([]);
65
65
  expect(outputs).toHaveLength(0);
66
66
  });
67
67
  });
@@ -72,7 +72,7 @@ describe("messageAssemblerProcessor", () => {
72
72
 
73
73
  const [newState, outputs] = messageAssemblerProcessor(state, event);
74
74
 
75
- expect(newState.pendingContents[0]).toBeDefined();
75
+ expect(newState.pendingContents).toHaveLength(1);
76
76
  expect(newState.pendingContents[0].type).toBe("text");
77
77
  expect(newState.pendingContents[0].textDeltas).toContain("Hello");
78
78
  expect(outputs).toHaveLength(0);
@@ -104,6 +104,32 @@ describe("messageAssemblerProcessor", () => {
104
104
  expect(finalState.pendingContents[0].textDeltas).toEqual(["Hello", " World", "!"]);
105
105
  expect(outputs).toHaveLength(0);
106
106
  });
107
+
108
+ it("should create new text block after a tool_use block", () => {
109
+ // Setup: text then tool_use already in pendingContents
110
+ state.pendingContents = [
111
+ { type: "text", textDeltas: ["Before tool"] },
112
+ {
113
+ type: "tool_use",
114
+ toolId: "t1",
115
+ toolName: "test",
116
+ toolInputJson: "{}",
117
+ assembled: true,
118
+ parsedInput: {},
119
+ },
120
+ ];
121
+
122
+ const event = createStreamEvent("text_delta", { text: "After tool" });
123
+ const [newState] = messageAssemblerProcessor(state, event);
124
+
125
+ // Should create a NEW text block (not append to the first one)
126
+ expect(newState.pendingContents).toHaveLength(3);
127
+ expect(newState.pendingContents[0].type).toBe("text");
128
+ expect(newState.pendingContents[0].textDeltas).toEqual(["Before tool"]);
129
+ expect(newState.pendingContents[1].type).toBe("tool_use");
130
+ expect(newState.pendingContents[2].type).toBe("text");
131
+ expect(newState.pendingContents[2].textDeltas).toEqual(["After tool"]);
132
+ });
107
133
  });
108
134
 
109
135
  describe("tool_use_start event", () => {
@@ -115,43 +141,60 @@ describe("messageAssemblerProcessor", () => {
115
141
 
116
142
  const [newState, outputs] = messageAssemblerProcessor(state, event);
117
143
 
118
- expect(newState.pendingContents[1]).toBeDefined();
119
- expect(newState.pendingContents[1].type).toBe("tool_use");
120
- expect(newState.pendingContents[1].toolId).toBe("tool_123");
121
- expect(newState.pendingContents[1].toolName).toBe("calculate");
122
- expect(newState.pendingContents[1].toolInputJson).toBe("");
144
+ expect(newState.pendingContents).toHaveLength(1);
145
+ expect(newState.pendingContents[0].type).toBe("tool_use");
146
+ expect(newState.pendingContents[0].toolId).toBe("tool_123");
147
+ expect(newState.pendingContents[0].toolName).toBe("calculate");
148
+ expect(newState.pendingContents[0].toolInputJson).toBe("");
123
149
  expect(outputs).toHaveLength(0);
124
150
  });
151
+
152
+ it("should append tool_use after existing text", () => {
153
+ state.pendingContents = [{ type: "text", textDeltas: ["Some text"] }];
154
+
155
+ const event = createStreamEvent("tool_use_start", {
156
+ toolCallId: "tool_123",
157
+ toolName: "calculate",
158
+ });
159
+
160
+ const [newState] = messageAssemblerProcessor(state, event);
161
+
162
+ expect(newState.pendingContents).toHaveLength(2);
163
+ expect(newState.pendingContents[0].type).toBe("text");
164
+ expect(newState.pendingContents[1].type).toBe("tool_use");
165
+ });
125
166
  });
126
167
 
127
168
  describe("input_json_delta event", () => {
128
169
  it("should accumulate JSON input for tool use", () => {
129
170
  // Setup: start a tool use
130
- state.pendingContents[1] = {
131
- type: "tool_use",
132
- index: 1,
133
- toolId: "tool_123",
134
- toolName: "calculate",
135
- toolInputJson: "",
136
- };
171
+ state.pendingContents = [
172
+ {
173
+ type: "tool_use",
174
+ toolId: "tool_123",
175
+ toolName: "calculate",
176
+ toolInputJson: "",
177
+ },
178
+ ];
137
179
 
138
180
  const event = createStreamEvent("input_json_delta", { partialJson: '{"value":' });
139
181
 
140
182
  const [newState, outputs] = messageAssemblerProcessor(state, event);
141
183
 
142
- expect(newState.pendingContents[1].toolInputJson).toBe('{"value":');
184
+ expect(newState.pendingContents[0].toolInputJson).toBe('{"value":');
143
185
  expect(outputs).toHaveLength(0);
144
186
  });
145
187
 
146
188
  it("should append multiple JSON deltas", () => {
147
189
  // Setup
148
- state.pendingContents[1] = {
149
- type: "tool_use",
150
- index: 1,
151
- toolId: "tool_123",
152
- toolName: "calculate",
153
- toolInputJson: "",
154
- };
190
+ state.pendingContents = [
191
+ {
192
+ type: "tool_use",
193
+ toolId: "tool_123",
194
+ toolName: "calculate",
195
+ toolInputJson: "",
196
+ },
197
+ ];
155
198
 
156
199
  let currentState = state;
157
200
 
@@ -168,7 +211,7 @@ describe("messageAssemblerProcessor", () => {
168
211
  createStreamEvent("input_json_delta", { partialJson: "42}" })
169
212
  );
170
213
 
171
- expect(finalState.pendingContents[1].toolInputJson).toBe('{"value":42}');
214
+ expect(finalState.pendingContents[0].toolInputJson).toBe('{"value":42}');
172
215
  });
173
216
 
174
217
  it("should ignore input_json_delta without pending tool use", () => {
@@ -182,57 +225,55 @@ describe("messageAssemblerProcessor", () => {
182
225
  });
183
226
 
184
227
  describe("tool_use_stop event", () => {
185
- it("should emit tool_call_message event", () => {
228
+ it("should mark tool_use as assembled without emitting event", () => {
186
229
  // Setup: complete tool use sequence
187
230
  state.currentMessageId = "parent_msg";
188
- state.pendingContents[1] = {
189
- type: "tool_use",
190
- index: 1,
191
- toolId: "tool_123",
192
- toolName: "calculate",
193
- toolInputJson: '{"x":10,"y":20}',
194
- };
231
+ state.pendingContents = [
232
+ {
233
+ type: "tool_use",
234
+ toolId: "tool_123",
235
+ toolName: "calculate",
236
+ toolInputJson: '{"x":10,"y":20}',
237
+ },
238
+ ];
195
239
 
196
240
  const event = createStreamEvent("tool_use_stop", {});
197
241
 
198
242
  const [newState, outputs] = messageAssemblerProcessor(state, event);
199
243
 
200
- expect(outputs).toHaveLength(1);
201
- expect(outputs[0].type).toBe("tool_call_message");
244
+ // No event emitted — tool calls are part of the assistant message
245
+ expect(outputs).toHaveLength(0);
202
246
 
203
- const toolCallMessage = outputs[0].data;
204
- expect(toolCallMessage.role).toBe("assistant");
205
- expect(toolCallMessage.subtype).toBe("tool-call");
206
- expect(toolCallMessage.toolCall.id).toBe("tool_123");
207
- expect(toolCallMessage.toolCall.name).toBe("calculate");
208
- expect(toolCallMessage.toolCall.input).toEqual({ x: 10, y: 20 });
209
- expect(toolCallMessage.parentId).toBe("parent_msg");
247
+ // Should mark as assembled with parsed input (stays in pendingContents)
248
+ expect(newState.pendingContents).toHaveLength(1);
249
+ expect(newState.pendingContents[0].type).toBe("tool_use");
250
+ expect(newState.pendingContents[0].assembled).toBe(true);
251
+ expect(newState.pendingContents[0].parsedInput).toEqual({ x: 10, y: 20 });
210
252
 
211
253
  // Should add to pending tool calls
212
254
  expect(newState.pendingToolCalls["tool_123"]).toEqual({
213
255
  id: "tool_123",
214
256
  name: "calculate",
215
257
  });
216
-
217
- // Should remove from pending contents
218
- expect(newState.pendingContents[1]).toBeUndefined();
219
258
  });
220
259
 
221
260
  it("should handle invalid JSON input gracefully", () => {
222
- state.pendingContents[1] = {
223
- type: "tool_use",
224
- index: 1,
225
- toolId: "tool_123",
226
- toolName: "test",
227
- toolInputJson: "invalid json",
228
- };
261
+ state.pendingContents = [
262
+ {
263
+ type: "tool_use",
264
+ toolId: "tool_123",
265
+ toolName: "test",
266
+ toolInputJson: "invalid json",
267
+ },
268
+ ];
229
269
 
230
270
  const event = createStreamEvent("tool_use_stop", {});
231
271
 
232
272
  const [newState, outputs] = messageAssemblerProcessor(state, event);
233
273
 
234
- expect(outputs).toHaveLength(1);
235
- expect(outputs[0].data.toolCall.input).toEqual({});
274
+ expect(outputs).toHaveLength(0);
275
+ expect(newState.pendingContents[0].assembled).toBe(true);
276
+ expect(newState.pendingContents[0].parsedInput).toEqual({});
236
277
  });
237
278
 
238
279
  it("should handle missing pending tool use", () => {
@@ -313,11 +354,12 @@ describe("messageAssemblerProcessor", () => {
313
354
  // Setup: complete message with text
314
355
  state.currentMessageId = "msg_123";
315
356
  state.messageStartTime = 1000;
316
- state.pendingContents[0] = {
317
- type: "text",
318
- index: 0,
319
- textDeltas: ["Hello", " ", "World!"],
320
- };
357
+ state.pendingContents = [
358
+ {
359
+ type: "text",
360
+ textDeltas: ["Hello", " ", "World!"],
361
+ },
362
+ ];
321
363
 
322
364
  const event = createStreamEvent("message_stop", { stopReason: "end_turn" });
323
365
 
@@ -336,7 +378,7 @@ describe("messageAssemblerProcessor", () => {
336
378
 
337
379
  // Should reset state
338
380
  expect(newState.currentMessageId).toBeNull();
339
- expect(newState.pendingContents).toEqual({});
381
+ expect(newState.pendingContents).toEqual([]);
340
382
  });
341
383
 
342
384
  it("should skip empty messages", () => {
@@ -352,11 +394,12 @@ describe("messageAssemblerProcessor", () => {
352
394
 
353
395
  it("should skip whitespace-only messages", () => {
354
396
  state.currentMessageId = "msg_123";
355
- state.pendingContents[0] = {
356
- type: "text",
357
- index: 0,
358
- textDeltas: [" ", "\n", "\t"],
359
- };
397
+ state.pendingContents = [
398
+ {
399
+ type: "text",
400
+ textDeltas: [" ", "\n", "\t"],
401
+ },
402
+ ];
360
403
 
361
404
  const event = createStreamEvent("message_stop", { stopReason: "end_turn" });
362
405
 
@@ -368,28 +411,41 @@ describe("messageAssemblerProcessor", () => {
368
411
  it("should preserve pending tool calls when stopReason is tool_use", () => {
369
412
  state.currentMessageId = "msg_123";
370
413
  state.pendingToolCalls["tool_123"] = { id: "tool_123", name: "test" };
371
- state.pendingContents[0] = {
372
- type: "text",
373
- index: 0,
374
- textDeltas: ["Calling tool..."],
375
- };
414
+ state.pendingContents = [
415
+ {
416
+ type: "tool_use",
417
+ toolId: "tool_123",
418
+ toolName: "test",
419
+ toolInputJson: '{"q":"hello"}',
420
+ assembled: true,
421
+ parsedInput: { q: "hello" },
422
+ },
423
+ ];
376
424
 
377
425
  const event = createStreamEvent("message_stop", { stopReason: "tool_use" });
378
426
 
379
427
  const [newState, outputs] = messageAssemblerProcessor(state, event);
380
428
 
381
- expect(outputs).toHaveLength(1); // Still emits assistant message
429
+ // Emits assistant_message with tool call in content
430
+ expect(outputs).toHaveLength(1);
431
+ expect(outputs[0].type).toBe("assistant_message");
432
+ const content = outputs[0].data.content;
433
+ expect(content).toHaveLength(1);
434
+ expect(content[0].type).toBe("tool-call");
435
+ expect(content[0].id).toBe("tool_123");
436
+
382
437
  expect(newState.pendingToolCalls["tool_123"]).toBeDefined();
383
438
  });
384
439
 
385
440
  it("should clear pending tool calls for non-tool_use stop reason", () => {
386
441
  state.currentMessageId = "msg_123";
387
442
  state.pendingToolCalls["tool_123"] = { id: "tool_123", name: "test" };
388
- state.pendingContents[0] = {
389
- type: "text",
390
- index: 0,
391
- textDeltas: ["Done"],
392
- };
443
+ state.pendingContents = [
444
+ {
445
+ type: "text",
446
+ textDeltas: ["Done"],
447
+ },
448
+ ];
393
449
 
394
450
  const event = createStreamEvent("message_stop", { stopReason: "end_turn" });
395
451
 
@@ -399,11 +455,12 @@ describe("messageAssemblerProcessor", () => {
399
455
  });
400
456
 
401
457
  it("should handle missing currentMessageId", () => {
402
- state.pendingContents[0] = {
403
- type: "text",
404
- index: 0,
405
- textDeltas: ["Some text"],
406
- };
458
+ state.pendingContents = [
459
+ {
460
+ type: "text",
461
+ textDeltas: ["Some text"],
462
+ },
463
+ ];
407
464
 
408
465
  const event = createStreamEvent("message_stop", { stopReason: "end_turn" });
409
466
 
@@ -433,7 +490,7 @@ describe("messageAssemblerProcessor", () => {
433
490
 
434
491
  // Should reset state
435
492
  expect(newState.currentMessageId).toBeNull();
436
- expect(newState.pendingContents).toEqual({});
493
+ expect(newState.pendingContents).toEqual([]);
437
494
  });
438
495
 
439
496
  it("should handle missing errorCode", () => {
@@ -529,15 +586,16 @@ describe("messageAssemblerProcessor", () => {
529
586
  currentState = s3;
530
587
  allOutputs.push(...o3);
531
588
 
532
- // tool_use_stop
589
+ // tool_use_stop — no event emitted, marks as assembled
533
590
  const [s4, o4] = messageAssemblerProcessor(
534
591
  currentState,
535
592
  createStreamEvent("tool_use_stop", {})
536
593
  );
537
594
  currentState = s4;
538
595
  allOutputs.push(...o4);
596
+ expect(o4).toHaveLength(0);
539
597
 
540
- // message_stop with tool_use
598
+ // message_stop with tool_use — emits assistant_message with tool call in content
541
599
  const [s5, o5] = messageAssemblerProcessor(
542
600
  currentState,
543
601
  createStreamEvent("message_stop", { stopReason: "tool_use" })
@@ -557,11 +615,157 @@ describe("messageAssemblerProcessor", () => {
557
615
  currentState = s6;
558
616
  allOutputs.push(...o6);
559
617
 
618
+ // assistant_message (with tool call) + tool_result_message
560
619
  expect(allOutputs).toHaveLength(2);
561
- expect(allOutputs[0].type).toBe("tool_call_message");
562
- expect(allOutputs[0].data.toolCall.name).toBe("search");
620
+ expect(allOutputs[0].type).toBe("assistant_message");
621
+ // Verify tool call is inside assistant message content
622
+ const content = allOutputs[0].data.content;
623
+ expect(content).toHaveLength(1);
624
+ expect(content[0].type).toBe("tool-call");
625
+ expect(content[0].name).toBe("search");
626
+ expect(content[0].input).toEqual({ query: "test" });
627
+
563
628
  expect(allOutputs[1].type).toBe("tool_result_message");
564
629
  expect(allOutputs[1].data.toolResult.output.value).toBe("Found it!");
565
630
  });
631
+
632
+ it("should preserve interleaved text and tool call order", () => {
633
+ let currentState = createInitialMessageAssemblerState();
634
+ const allOutputs: MessageAssemblerOutput[] = [];
635
+
636
+ // message_start
637
+ const [s1, o1] = messageAssemblerProcessor(
638
+ currentState,
639
+ createStreamEvent("message_start", { messageId: "msg_1" }, 1000)
640
+ );
641
+ currentState = s1;
642
+ allOutputs.push(...o1);
643
+
644
+ // Text before tool
645
+ const [s2] = messageAssemblerProcessor(
646
+ currentState,
647
+ createStreamEvent("text_delta", { text: "Let me search for that." })
648
+ );
649
+ currentState = s2;
650
+
651
+ // tool_use_start
652
+ const [s3] = messageAssemblerProcessor(
653
+ currentState,
654
+ createStreamEvent("tool_use_start", { toolCallId: "tool_1", toolName: "search" })
655
+ );
656
+ currentState = s3;
657
+
658
+ // input_json_delta
659
+ const [s4] = messageAssemblerProcessor(
660
+ currentState,
661
+ createStreamEvent("input_json_delta", { partialJson: '{"q":"test"}' })
662
+ );
663
+ currentState = s4;
664
+
665
+ // tool_use_stop
666
+ const [s5] = messageAssemblerProcessor(currentState, createStreamEvent("tool_use_stop", {}));
667
+ currentState = s5;
668
+
669
+ // Text after tool
670
+ const [s6] = messageAssemblerProcessor(
671
+ currentState,
672
+ createStreamEvent("text_delta", { text: "Here are the results." })
673
+ );
674
+ currentState = s6;
675
+
676
+ // message_stop
677
+ const [, o7] = messageAssemblerProcessor(
678
+ currentState,
679
+ createStreamEvent("message_stop", { stopReason: "end_turn" })
680
+ );
681
+ allOutputs.push(...o7);
682
+
683
+ expect(allOutputs).toHaveLength(1);
684
+ expect(allOutputs[0].type).toBe("assistant_message");
685
+
686
+ const content = allOutputs[0].data.content;
687
+ // Order must be: text, tool-call, text (preserving stream order)
688
+ expect(content).toHaveLength(3);
689
+ expect(content[0].type).toBe("text");
690
+ expect(content[0].text).toBe("Let me search for that.");
691
+ expect(content[1].type).toBe("tool-call");
692
+ expect(content[1].name).toBe("search");
693
+ expect(content[1].input).toEqual({ q: "test" });
694
+ expect(content[2].type).toBe("text");
695
+ expect(content[2].text).toBe("Here are the results.");
696
+ });
697
+
698
+ it("should handle text + tool + text + tool interleaving", () => {
699
+ let currentState = createInitialMessageAssemblerState();
700
+
701
+ // message_start
702
+ const [s1] = messageAssemblerProcessor(
703
+ currentState,
704
+ createStreamEvent("message_start", { messageId: "msg_1" }, 1000)
705
+ );
706
+ currentState = s1;
707
+
708
+ // Text 1
709
+ const [s2] = messageAssemblerProcessor(
710
+ currentState,
711
+ createStreamEvent("text_delta", { text: "First " })
712
+ );
713
+ currentState = s2;
714
+
715
+ // Tool 1
716
+ const [s3] = messageAssemblerProcessor(
717
+ currentState,
718
+ createStreamEvent("tool_use_start", { toolCallId: "t1", toolName: "toolA" })
719
+ );
720
+ currentState = s3;
721
+ const [s4] = messageAssemblerProcessor(
722
+ currentState,
723
+ createStreamEvent("input_json_delta", { partialJson: '{"a":1}' })
724
+ );
725
+ currentState = s4;
726
+ const [s5] = messageAssemblerProcessor(currentState, createStreamEvent("tool_use_stop", {}));
727
+ currentState = s5;
728
+
729
+ // Text 2
730
+ const [s6] = messageAssemblerProcessor(
731
+ currentState,
732
+ createStreamEvent("text_delta", { text: "Second " })
733
+ );
734
+ currentState = s6;
735
+
736
+ // Tool 2
737
+ const [s7] = messageAssemblerProcessor(
738
+ currentState,
739
+ createStreamEvent("tool_use_start", { toolCallId: "t2", toolName: "toolB" })
740
+ );
741
+ currentState = s7;
742
+ const [s8] = messageAssemblerProcessor(
743
+ currentState,
744
+ createStreamEvent("input_json_delta", { partialJson: '{"b":2}' })
745
+ );
746
+ currentState = s8;
747
+ const [s9] = messageAssemblerProcessor(currentState, createStreamEvent("tool_use_stop", {}));
748
+ currentState = s9;
749
+
750
+ // message_stop
751
+ const [, outputs] = messageAssemblerProcessor(
752
+ currentState,
753
+ createStreamEvent("message_stop", { stopReason: "tool_use" })
754
+ );
755
+
756
+ expect(outputs).toHaveLength(1);
757
+ const content = outputs[0].data.content;
758
+
759
+ // Must preserve: text, tool, text, tool
760
+ expect(content).toHaveLength(4);
761
+ expect(content[0]).toEqual({ type: "text", text: "First " });
762
+ expect(content[1].type).toBe("tool-call");
763
+ expect(content[1].name).toBe("toolA");
764
+ expect(content[1].input).toEqual({ a: 1 });
765
+ expect(content[2]).toEqual({ type: "text", text: "Second " });
766
+ expect(content[3].type).toBe("tool-call");
767
+ expect(content[3].name).toBe("toolB");
768
+ expect(content[3].input).toEqual({ b: 2 });
769
+ });
566
770
  });
567
771
  });