@agentxjs/core 1.9.10-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 +15 -15
  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-AT5P47YA.js → chunk-JTKCV7IS.js} +9 -9
  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 +16 -16
  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-AT5P47YA.js.map +0 -1
  89. package/dist/chunk-E5FPOAPO.js.map +0 -1
  90. package/dist/chunk-I7GYR3MN.js.map +0 -1
  91. package/dist/chunk-K6WXQ2RW.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
@@ -2,6 +2,9 @@
2
2
  * turnTrackerProcessor.test.ts - Unit tests for turn tracker processor
3
3
  *
4
4
  * Tests the pure Mealy transition function that tracks request-response turn pairs.
5
+ * Turn events are derived entirely from stream-layer events:
6
+ * - message_start → turn_request
7
+ * - message_stop → turn_response
5
8
  */
6
9
 
7
10
  import { describe, it, expect, beforeEach } from "bun:test";
@@ -18,23 +21,12 @@ function createEvent(type: string, data: unknown, timestamp = Date.now()): TurnT
18
21
  return { type, data, timestamp } as TurnTrackerInput;
19
22
  }
20
23
 
21
- // Helper to create user message event
22
- function createUserMessageEvent(
23
- content: string,
24
+ // Helper to create message_start event (begins a turn)
25
+ function createMessageStartEvent(
24
26
  messageId: string = `msg_${Date.now()}`,
25
27
  timestamp = Date.now()
26
28
  ): TurnTrackerInput {
27
- return createEvent(
28
- "user_message",
29
- {
30
- id: messageId,
31
- role: "user",
32
- subtype: "user",
33
- content,
34
- timestamp,
35
- },
36
- timestamp
37
- );
29
+ return createEvent("message_start", { messageId, model: "test-model" }, timestamp);
38
30
  }
39
31
 
40
32
  // Helper to create message_stop event
@@ -59,16 +51,15 @@ describe("turnTrackerProcessor", () => {
59
51
  });
60
52
  });
61
53
 
62
- describe("user_message event", () => {
54
+ describe("message_start event", () => {
63
55
  it("should create pending turn and emit turn_request", () => {
64
- const event = createUserMessageEvent("Hello", "msg_123", 1000);
56
+ const event = createMessageStartEvent("msg_123", 1000);
65
57
 
66
58
  const [newState, outputs] = turnTrackerProcessor(state, event);
67
59
 
68
60
  // Should have pending turn
69
61
  expect(newState.pendingTurn).not.toBeNull();
70
62
  expect(newState.pendingTurn?.messageId).toBe("msg_123");
71
- expect(newState.pendingTurn?.content).toBe("Hello");
72
63
  expect(newState.pendingTurn?.requestedAt).toBe(1000);
73
64
  expect(newState.pendingTurn?.turnId).toBeDefined();
74
65
 
@@ -76,35 +67,39 @@ describe("turnTrackerProcessor", () => {
76
67
  expect(outputs).toHaveLength(1);
77
68
  expect(outputs[0].type).toBe("turn_request");
78
69
  expect(outputs[0].data.messageId).toBe("msg_123");
79
- expect(outputs[0].data.content).toBe("Hello");
80
70
  expect(outputs[0].data.timestamp).toBe(1000);
81
71
  });
82
72
 
83
73
  it("should generate unique turn IDs", () => {
84
- const [state1, outputs1] = turnTrackerProcessor(
85
- state,
86
- createUserMessageEvent("First", "msg_1")
87
- );
74
+ const [state1, outputs1] = turnTrackerProcessor(state, createMessageStartEvent("msg_1"));
88
75
 
89
76
  // Complete the turn
90
77
  const [state2] = turnTrackerProcessor(state1, createMessageStopEvent("end_turn"));
91
78
 
92
79
  // Start new turn
93
- const [state3, outputs3] = turnTrackerProcessor(
94
- state2,
95
- createUserMessageEvent("Second", "msg_2")
96
- );
80
+ const [state3, outputs3] = turnTrackerProcessor(state2, createMessageStartEvent("msg_2"));
97
81
 
98
82
  expect(outputs1[0].data.turnId).not.toBe(outputs3[0].data.turnId);
99
83
  });
100
84
 
101
- it("should handle empty content", () => {
102
- const event = createUserMessageEvent("", "msg_empty");
85
+ it("should NOT start a new turn if one is already pending", () => {
86
+ // Start first turn
87
+ const [state1, outputs1] = turnTrackerProcessor(
88
+ state,
89
+ createMessageStartEvent("msg_1", 1000)
90
+ );
91
+ expect(outputs1).toHaveLength(1);
103
92
 
104
- const [newState, outputs] = turnTrackerProcessor(state, event);
93
+ // Second message_start while turn is pending (e.g. after tool_use)
94
+ const [state2, outputs2] = turnTrackerProcessor(
95
+ state1,
96
+ createMessageStartEvent("msg_2", 1500)
97
+ );
105
98
 
106
- expect(newState.pendingTurn?.content).toBe("");
107
- expect(outputs[0].data.content).toBe("");
99
+ // Should NOT emit another turn_request
100
+ expect(outputs2).toHaveLength(0);
101
+ // Pending turn should still be the original
102
+ expect(state2.pendingTurn?.messageId).toBe("msg_1");
108
103
  });
109
104
  });
110
105
 
@@ -117,7 +112,7 @@ describe("turnTrackerProcessor", () => {
117
112
  pendingTurn: {
118
113
  turnId: "turn_123",
119
114
  messageId: "msg_123",
120
- content: "Test message",
115
+ content: "",
121
116
  requestedAt: 1000,
122
117
  },
123
118
  };
@@ -196,14 +191,23 @@ describe("turnTrackerProcessor", () => {
196
191
  });
197
192
  });
198
193
 
199
- describe("assistant_message event", () => {
200
- it("should not emit output (handled in message_stop)", () => {
194
+ describe("unhandled events", () => {
195
+ it("should pass through unhandled events", () => {
196
+ const event = createEvent("text_delta", { text: "Hello" });
197
+
198
+ const [newState, outputs] = turnTrackerProcessor(state, event);
199
+
200
+ expect(newState).toEqual(state);
201
+ expect(outputs).toHaveLength(0);
202
+ });
203
+
204
+ it("should ignore assistant_message (not a stream event)", () => {
201
205
  state = {
202
206
  ...state,
203
207
  pendingTurn: {
204
208
  turnId: "turn_123",
205
209
  messageId: "msg_123",
206
- content: "Test",
210
+ content: "",
207
211
  requestedAt: 1000,
208
212
  },
209
213
  };
@@ -216,39 +220,22 @@ describe("turnTrackerProcessor", () => {
216
220
 
217
221
  const [newState, outputs] = turnTrackerProcessor(state, event);
218
222
 
219
- // State should be unchanged
220
223
  expect(newState.pendingTurn).not.toBeNull();
221
-
222
- // No output
223
- expect(outputs).toHaveLength(0);
224
- });
225
- });
226
-
227
- describe("unhandled events", () => {
228
- it("should pass through unhandled events", () => {
229
- const event = createEvent("text_delta", { text: "Hello" });
230
-
231
- const [newState, outputs] = turnTrackerProcessor(state, event);
232
-
233
- expect(newState).toEqual(state);
234
224
  expect(outputs).toHaveLength(0);
235
225
  });
236
226
  });
237
227
 
238
228
  describe("complete turn flow", () => {
239
- it("should track complete turn from request to response", () => {
229
+ it("should track complete turn from message_start to message_stop", () => {
240
230
  const allOutputs: TurnTrackerOutput[] = [];
241
231
  let currentState = createInitialTurnTrackerState();
242
232
 
243
- // User sends message at time 1000
244
- const [s1, o1] = turnTrackerProcessor(
245
- currentState,
246
- createUserMessageEvent("What is 2+2?", "msg_1", 1000)
247
- );
233
+ // message_start at time 1000
234
+ const [s1, o1] = turnTrackerProcessor(currentState, createMessageStartEvent("msg_1", 1000));
248
235
  currentState = s1;
249
236
  allOutputs.push(...o1);
250
237
 
251
- // Assistant responds, message_stop at time 2500
238
+ // message_stop at time 2500
252
239
  const [s2, o2] = turnTrackerProcessor(currentState, createMessageStopEvent("end_turn", 2500));
253
240
  currentState = s2;
254
241
  allOutputs.push(...o2);
@@ -257,7 +244,7 @@ describe("turnTrackerProcessor", () => {
257
244
 
258
245
  // turn_request
259
246
  expect(allOutputs[0].type).toBe("turn_request");
260
- expect(allOutputs[0].data.content).toBe("What is 2+2?");
247
+ expect(allOutputs[0].data.messageId).toBe("msg_1");
261
248
 
262
249
  // turn_response
263
250
  expect(allOutputs[1].type).toBe("turn_response");
@@ -271,11 +258,8 @@ describe("turnTrackerProcessor", () => {
271
258
  const allOutputs: TurnTrackerOutput[] = [];
272
259
  let currentState = createInitialTurnTrackerState();
273
260
 
274
- // User sends message
275
- const [s1, o1] = turnTrackerProcessor(
276
- currentState,
277
- createUserMessageEvent("Search for X", "msg_1", 1000)
278
- );
261
+ // message_start begins the turn
262
+ const [s1, o1] = turnTrackerProcessor(currentState, createMessageStartEvent("msg_1", 1000));
279
263
  currentState = s1;
280
264
  allOutputs.push(...o1);
281
265
 
@@ -287,7 +271,13 @@ describe("turnTrackerProcessor", () => {
287
271
  expect(currentState.pendingTurn).not.toBeNull();
288
272
  expect(allOutputs).toHaveLength(1); // Only turn_request
289
273
 
290
- // Second message_stop with end_turn - turn completes
274
+ // Second message_start after tool_use should NOT start new turn (pending exists)
275
+ const [s2b, o2b] = turnTrackerProcessor(currentState, createMessageStartEvent("msg_2", 2000));
276
+ currentState = s2b;
277
+ allOutputs.push(...o2b);
278
+ expect(allOutputs).toHaveLength(1); // Still only turn_request
279
+
280
+ // Final message_stop with end_turn - turn completes
291
281
  const [s3, o3] = turnTrackerProcessor(currentState, createMessageStopEvent("end_turn", 3000));
292
282
  currentState = s3;
293
283
  allOutputs.push(...o3);
@@ -303,10 +293,7 @@ describe("turnTrackerProcessor", () => {
303
293
  const allTurnIds: string[] = [];
304
294
 
305
295
  // First turn
306
- const [s1, o1] = turnTrackerProcessor(
307
- currentState,
308
- createUserMessageEvent("First", "msg_1", 1000)
309
- );
296
+ const [s1, o1] = turnTrackerProcessor(currentState, createMessageStartEvent("msg_1", 1000));
310
297
  currentState = s1;
311
298
  allTurnIds.push(o1[0].data.turnId);
312
299
 
@@ -314,10 +301,7 @@ describe("turnTrackerProcessor", () => {
314
301
  currentState = s2;
315
302
 
316
303
  // Second turn
317
- const [s3, o3] = turnTrackerProcessor(
318
- currentState,
319
- createUserMessageEvent("Second", "msg_2", 2000)
320
- );
304
+ const [s3, o3] = turnTrackerProcessor(currentState, createMessageStartEvent("msg_2", 2000));
321
305
  currentState = s3;
322
306
  allTurnIds.push(o3[0].data.turnId);
323
307
 
@@ -325,10 +309,7 @@ describe("turnTrackerProcessor", () => {
325
309
  currentState = s4;
326
310
 
327
311
  // Third turn
328
- const [s5, o5] = turnTrackerProcessor(
329
- currentState,
330
- createUserMessageEvent("Third", "msg_3", 3000)
331
- );
312
+ const [s5, o5] = turnTrackerProcessor(currentState, createMessageStartEvent("msg_3", 3000));
332
313
  currentState = s5;
333
314
  allTurnIds.push(o5[0].data.turnId);
334
315
 
@@ -19,7 +19,7 @@
19
19
  * AgentOutput (to AgentEngine/Presenter)
20
20
  * │
21
21
  * ├── StateEvent (conversation_start, conversation_end...)
22
- * ├── MessageEvent (assistant_message, tool_call_message...)
22
+ * ├── MessageEvent (assistant_message, tool_result_message...)
23
23
  * └── TurnEvent (turn_request, turn_response)
24
24
  * ```
25
25
  *
@@ -14,9 +14,8 @@
14
14
  * - message_stop
15
15
  *
16
16
  * Output Events (Message Layer):
17
- * - tool_call_message (Message - AI's request to call a tool)
17
+ * - assistant_message (Message - includes text and tool calls in content)
18
18
  * - tool_result_message (Message - tool execution result)
19
- * - assistant_message (Message - complete assistant response)
20
19
  */
21
20
 
22
21
  import type { Processor, ProcessorDefinition } from "../mealy";
@@ -31,12 +30,10 @@ import type {
31
30
  MessageStopEvent,
32
31
  // Output: Message events
33
32
  AssistantMessageEvent,
34
- ToolCallMessageEvent,
35
33
  ToolResultMessageEvent,
36
34
  ErrorMessageEvent,
37
35
  // Message types
38
36
  AssistantMessage,
39
- ToolCallMessage,
40
37
  ToolResultMessage,
41
38
  ErrorMessage,
42
39
  // Content parts
@@ -49,16 +46,22 @@ import type {
49
46
 
50
47
  /**
51
48
  * Pending content accumulator
49
+ *
50
+ * Tracks content blocks in the order they appear in the stream.
51
+ * Text and tool_use blocks may be interleaved.
52
52
  */
53
53
  export interface PendingContent {
54
54
  type: "text" | "tool_use";
55
- index: number;
56
55
  // For text content
57
56
  textDeltas?: string[];
58
57
  // For tool use
59
58
  toolId?: string;
60
59
  toolName?: string;
61
60
  toolInputJson?: string;
61
+ /** True when tool_use_stop has been processed and input is fully parsed */
62
+ assembled?: boolean;
63
+ /** Parsed tool input (set at tool_use_stop time) */
64
+ parsedInput?: Record<string, unknown>;
62
65
  }
63
66
 
64
67
  /**
@@ -86,10 +89,10 @@ export interface MessageAssemblerState {
86
89
  messageStartTime: number | null;
87
90
 
88
91
  /**
89
- * Pending content blocks being accumulated
90
- * Key is the content block index
92
+ * Pending content blocks in stream order.
93
+ * Preserves the interleaved order of text and tool_use blocks.
91
94
  */
92
- pendingContents: Record<number, PendingContent>;
95
+ pendingContents: PendingContent[];
93
96
 
94
97
  /**
95
98
  * Pending tool calls waiting for results
@@ -105,7 +108,7 @@ export function createInitialMessageAssemblerState(): MessageAssemblerState {
105
108
  return {
106
109
  currentMessageId: null,
107
110
  messageStartTime: null,
108
- pendingContents: {},
111
+ pendingContents: [],
109
112
  pendingToolCalls: {},
110
113
  };
111
114
  }
@@ -119,12 +122,21 @@ function generateId(): string {
119
122
  return `msg_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
120
123
  }
121
124
 
125
+ /**
126
+ * Find last index matching a predicate (Array.findLastIndex polyfill)
127
+ */
128
+ function findLastIndex<T>(arr: readonly T[], predicate: (item: T) => boolean): number {
129
+ for (let i = arr.length - 1; i >= 0; i--) {
130
+ if (predicate(arr[i])) return i;
131
+ }
132
+ return -1;
133
+ }
134
+
122
135
  /**
123
136
  * Output event types from MessageAssembler
124
137
  */
125
138
  export type MessageAssemblerOutput =
126
139
  | AssistantMessageEvent
127
- | ToolCallMessageEvent
128
140
  | ToolResultMessageEvent
129
141
  | ErrorMessageEvent;
130
142
 
@@ -188,7 +200,7 @@ function handleMessageStart(
188
200
  ...state,
189
201
  currentMessageId: data.messageId,
190
202
  messageStartTime: event.timestamp,
191
- pendingContents: {},
203
+ pendingContents: [],
192
204
  },
193
205
  [],
194
206
  ];
@@ -196,34 +208,32 @@ function handleMessageStart(
196
208
 
197
209
  /**
198
210
  * Handle text_delta event
211
+ *
212
+ * Appends to the last text block if one exists, otherwise creates a new one.
213
+ * This preserves the interleaved order: text after a tool_use gets its own block.
199
214
  */
200
215
  function handleTextDelta(
201
216
  state: Readonly<MessageAssemblerState>,
202
217
  event: StreamEvent
203
218
  ): [MessageAssemblerState, MessageAssemblerOutput[]] {
204
219
  const { data } = event as TextDeltaEvent;
205
- const index = 0; // Text content uses index 0
206
- const existingContent = state.pendingContents[index];
207
-
208
- const pendingContent: PendingContent =
209
- existingContent?.type === "text"
210
- ? {
211
- ...existingContent,
212
- textDeltas: [...(existingContent.textDeltas || []), data.text],
213
- }
214
- : {
215
- type: "text",
216
- index,
217
- textDeltas: [data.text],
218
- };
220
+ const lastContent = state.pendingContents[state.pendingContents.length - 1];
221
+
222
+ // Append to last text block if it exists
223
+ if (lastContent?.type === "text") {
224
+ const updated = [...state.pendingContents];
225
+ updated[updated.length - 1] = {
226
+ ...lastContent,
227
+ textDeltas: [...(lastContent.textDeltas || []), data.text],
228
+ };
229
+ return [{ ...state, pendingContents: updated }, []];
230
+ }
219
231
 
232
+ // Create a new text block (preserves position after any preceding tool_use)
220
233
  return [
221
234
  {
222
235
  ...state,
223
- pendingContents: {
224
- ...state.pendingContents,
225
- [index]: pendingContent,
226
- },
236
+ pendingContents: [...state.pendingContents, { type: "text", textDeltas: [data.text] }],
227
237
  },
228
238
  [],
229
239
  ];
@@ -237,23 +247,19 @@ function handleToolUseStart(
237
247
  event: StreamEvent
238
248
  ): [MessageAssemblerState, MessageAssemblerOutput[]] {
239
249
  const { data } = event as ToolUseStartEvent;
240
- const index = 1; // Tool use uses index 1
241
-
242
- const pendingContent: PendingContent = {
243
- type: "tool_use",
244
- index,
245
- toolId: data.toolCallId,
246
- toolName: data.toolName,
247
- toolInputJson: "",
248
- };
249
250
 
250
251
  return [
251
252
  {
252
253
  ...state,
253
- pendingContents: {
254
+ pendingContents: [
254
255
  ...state.pendingContents,
255
- [index]: pendingContent,
256
- },
256
+ {
257
+ type: "tool_use",
258
+ toolId: data.toolCallId,
259
+ toolName: data.toolName,
260
+ toolInputJson: "",
261
+ },
262
+ ],
257
263
  },
258
264
  [],
259
265
  ];
@@ -267,102 +273,76 @@ function handleInputJsonDelta(
267
273
  event: StreamEvent
268
274
  ): [MessageAssemblerState, MessageAssemblerOutput[]] {
269
275
  const { data } = event as InputJsonDeltaEvent;
270
- const index = 1; // Tool use uses index 1
271
- const existingContent = state.pendingContents[index];
272
276
 
273
- if (!existingContent || existingContent.type !== "tool_use") {
274
- // No pending tool_use content, ignore
277
+ // Find the last tool_use content in the array
278
+ const lastToolIndex = findLastIndex(
279
+ state.pendingContents,
280
+ (c) => c.type === "tool_use" && !c.assembled
281
+ );
282
+ if (lastToolIndex === -1) {
275
283
  return [state, []];
276
284
  }
277
285
 
278
- const pendingContent: PendingContent = {
286
+ const existingContent = state.pendingContents[lastToolIndex];
287
+ const updated = [...state.pendingContents];
288
+ updated[lastToolIndex] = {
279
289
  ...existingContent,
280
290
  toolInputJson: (existingContent.toolInputJson || "") + data.partialJson,
281
291
  };
282
292
 
283
- return [
284
- {
285
- ...state,
286
- pendingContents: {
287
- ...state.pendingContents,
288
- [index]: pendingContent,
289
- },
290
- },
291
- [],
292
- ];
293
+ return [{ ...state, pendingContents: updated }, []];
293
294
  }
294
295
 
295
296
  /**
296
297
  * Handle tool_use_stop event
297
298
  *
298
- * Emits:
299
- * - tool_call_message (Message Event) - for UI display and tool execution
299
+ * Marks the tool_use entry as assembled with parsed input.
300
+ * The entry stays in pendingContents to preserve its position.
301
+ * No event is emitted — tool calls are part of the assistant message.
300
302
  */
301
303
  function handleToolUseStop(
302
304
  state: Readonly<MessageAssemblerState>,
303
305
  _event: StreamEvent
304
306
  ): [MessageAssemblerState, MessageAssemblerOutput[]] {
305
- const index = 1;
306
- const pendingContent = state.pendingContents[index];
307
-
308
- if (!pendingContent || pendingContent.type !== "tool_use") {
307
+ // Find the last unassembled tool_use content
308
+ const lastToolIndex = findLastIndex(
309
+ state.pendingContents,
310
+ (c) => c.type === "tool_use" && !c.assembled
311
+ );
312
+ if (lastToolIndex === -1) {
309
313
  return [state, []];
310
314
  }
311
315
 
312
- // Get tool info from pendingContent (saved during tool_use_start)
316
+ const pendingContent = state.pendingContents[lastToolIndex];
313
317
  const toolId = pendingContent.toolId || "";
314
318
  const toolName = pendingContent.toolName || "";
315
319
 
316
- // Parse tool input JSON (accumulated during input_json_delta)
320
+ // Parse tool input JSON
317
321
  let toolInput: Record<string, unknown> = {};
318
322
  try {
319
323
  toolInput = pendingContent.toolInputJson ? JSON.parse(pendingContent.toolInputJson) : {};
320
324
  } catch {
321
- // Failed to parse, use empty object
322
325
  toolInput = {};
323
326
  }
324
327
 
325
- // Create ToolCallPart
326
- const toolCall: ToolCallPart = {
327
- type: "tool-call",
328
- id: toolId,
329
- name: toolName,
330
- input: toolInput,
331
- };
332
-
333
- // Create ToolCallMessage (complete Message object)
334
- // parentId links this tool call to its parent assistant message
335
- const messageId = generateId();
336
- const timestamp = Date.now();
337
- const toolCallMessage: ToolCallMessage = {
338
- id: messageId,
339
- role: "assistant",
340
- subtype: "tool-call",
341
- toolCall,
342
- timestamp,
343
- parentId: state.currentMessageId || undefined,
328
+ // Mark as assembled in-place (preserves position)
329
+ const updated = [...state.pendingContents];
330
+ updated[lastToolIndex] = {
331
+ ...pendingContent,
332
+ assembled: true,
333
+ parsedInput: toolInput,
344
334
  };
345
335
 
346
- // Emit tool_call_message event - data is complete Message object
347
- const toolCallMessageEvent: ToolCallMessageEvent = {
348
- type: "tool_call_message",
349
- timestamp,
350
- data: toolCallMessage,
351
- };
352
-
353
- // Remove from pending contents, add to pending tool calls
354
- const { [index]: _, ...remainingContents } = state.pendingContents;
355
-
356
336
  return [
357
337
  {
358
338
  ...state,
359
- pendingContents: remainingContents,
339
+ pendingContents: updated,
360
340
  pendingToolCalls: {
361
341
  ...state.pendingToolCalls,
362
342
  [toolId]: { id: toolId, name: toolName },
363
343
  },
364
344
  },
365
- [toolCallMessageEvent],
345
+ [], // No event emitted
366
346
  ];
367
347
  }
368
348
 
@@ -427,6 +407,9 @@ function handleToolResult(
427
407
 
428
408
  /**
429
409
  * Handle message_stop event
410
+ *
411
+ * Assembles the complete AssistantMessage from pendingContents in stream order.
412
+ * Text and tool call parts are interleaved as they appeared in the stream.
430
413
  */
431
414
  function handleMessageStop(
432
415
  state: Readonly<MessageAssemblerState>,
@@ -438,21 +421,31 @@ function handleMessageStop(
438
421
  return [state, []];
439
422
  }
440
423
 
441
- // Assemble all text content
442
- const textParts: string[] = [];
443
- const sortedContents = Object.values(state.pendingContents).sort((a, b) => a.index - b.index);
424
+ // Build content parts in stream order from pendingContents
425
+ const contentParts: Array<TextPart | ToolCallPart> = [];
444
426
 
445
- for (const pending of sortedContents) {
427
+ for (const pending of state.pendingContents) {
446
428
  if (pending.type === "text" && pending.textDeltas) {
447
- textParts.push(pending.textDeltas.join(""));
429
+ const text = pending.textDeltas.join("");
430
+ if (text.trim().length > 0) {
431
+ contentParts.push({ type: "text", text });
432
+ }
433
+ } else if (pending.type === "tool_use" && pending.assembled) {
434
+ contentParts.push({
435
+ type: "tool-call",
436
+ id: pending.toolId || "",
437
+ name: pending.toolName || "",
438
+ input: pending.parsedInput || {},
439
+ });
448
440
  }
449
441
  }
450
442
 
451
- const textContent = textParts.join("");
443
+ const hasToolCalls = contentParts.some((p) => p.type === "tool-call");
444
+ const hasText = contentParts.some((p) => p.type === "text");
452
445
 
453
- // Skip empty messages (but preserve pendingToolCalls if stopReason is "tool_use")
446
+ // Skip truly empty messages (no text AND no tool calls)
454
447
  const stopReason = data.stopReason;
455
- if (!textContent || textContent.trim().length === 0) {
448
+ if (!hasText && !hasToolCalls) {
456
449
  const shouldPreserveToolCalls = stopReason === "tool_use";
457
450
  return [
458
451
  {
@@ -463,15 +456,7 @@ function handleMessageStop(
463
456
  ];
464
457
  }
465
458
 
466
- // Create content parts (new structure uses ContentPart[])
467
- const contentParts: TextPart[] = [
468
- {
469
- type: "text",
470
- text: textContent,
471
- },
472
- ];
473
-
474
- // Create AssistantMessage (complete Message object)
459
+ // Create AssistantMessage with interleaved content
475
460
  const timestamp = state.messageStartTime || Date.now();
476
461
  const assistantMessage: AssistantMessage = {
477
462
  id: state.currentMessageId,
@@ -481,7 +466,7 @@ function handleMessageStop(
481
466
  timestamp,
482
467
  };
483
468
 
484
- // Emit AssistantMessageEvent - data is complete Message object
469
+ // Emit AssistantMessageEvent
485
470
  const assistantEvent: AssistantMessageEvent = {
486
471
  type: "assistant_message",
487
472
  timestamp,