@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.
- package/README.md +342 -0
- package/dist/RpcClient-BcJ_zAGu.d.ts +304 -0
- package/dist/agent/engine/internal/index.d.ts +20 -15
- package/dist/agent/engine/internal/index.js +1 -2
- package/dist/agent/engine/mealy/index.js +0 -1
- package/dist/agent/index.d.ts +4 -4
- package/dist/agent/index.js +15 -15
- package/dist/agent/types/index.d.ts +4 -4
- package/dist/agent/types/index.js +1 -2
- package/dist/bash/index.d.ts +29 -0
- package/dist/bash/index.js +7 -0
- package/dist/{bus-uF1DM2ox.d.ts → bus-C9FLWIu8.d.ts} +3 -1
- package/dist/{chunk-K6WXQ2RW.js → chunk-23UUBQXR.js} +1 -2
- package/dist/chunk-23UUBQXR.js.map +1 -0
- package/dist/chunk-BHOD5PKR.js +55 -0
- package/dist/chunk-BHOD5PKR.js.map +1 -0
- package/dist/{chunk-I7GYR3MN.js → chunk-DEAR6N3O.js} +77 -91
- package/dist/chunk-DEAR6N3O.js.map +1 -0
- package/dist/chunk-FI7WQFGV.js +37 -0
- package/dist/chunk-FI7WQFGV.js.map +1 -0
- package/dist/{chunk-AT5P47YA.js → chunk-JTKCV7IS.js} +9 -9
- package/dist/chunk-JTKCV7IS.js.map +1 -0
- package/dist/{chunk-E5FPOAPO.js → chunk-LTVNPHST.js} +1 -1
- package/dist/chunk-LTVNPHST.js.map +1 -0
- package/dist/chunk-SKS7S2RY.js +1 -0
- package/dist/common/logger/index.js +0 -2
- package/dist/common/logger/index.js.map +1 -1
- package/dist/container/index.d.ts +3 -4
- package/dist/container/index.js +0 -2
- package/dist/container/index.js.map +1 -1
- package/dist/driver/index.d.ts +2 -310
- package/dist/event/index.d.ts +4 -4
- package/dist/event/index.js +1 -2
- package/dist/event/types/index.d.ts +4 -10
- package/dist/event/types/index.js +1 -2
- package/dist/{event-CDuTzs__.d.ts → event-DNWOBSBO.d.ts} +3 -4
- package/dist/image/index.d.ts +9 -5
- package/dist/image/index.js +5 -2
- package/dist/image/index.js.map +1 -1
- package/dist/index-CuS1i5V-.d.ts +609 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +16 -16
- package/dist/{message-BMrMm1pq.d.ts → message-03TJzvIX.d.ts} +10 -33
- package/dist/mq/index.js +0 -2
- package/dist/mq/index.js.map +1 -1
- package/dist/network/index.d.ts +3 -291
- package/dist/network/index.js +3 -14
- package/dist/network/index.js.map +1 -1
- package/dist/persistence/index.d.ts +2 -155
- package/dist/platform/index.d.ts +76 -0
- package/dist/platform/index.js.map +1 -0
- package/dist/runtime/index.d.ts +26 -59
- package/dist/runtime/index.js +117 -33
- package/dist/runtime/index.js.map +1 -1
- package/dist/session/index.d.ts +4 -52
- package/dist/session/index.js +4 -51
- package/dist/session/index.js.map +1 -1
- package/dist/types-aE74Eo6G.d.ts +90 -0
- package/package.json +10 -5
- package/src/agent/__tests__/engine/internal/messageAssemblerProcessor.test.ts +291 -87
- package/src/agent/__tests__/engine/internal/turnTrackerProcessor.test.ts +56 -75
- package/src/agent/engine/MealyMachine.ts +1 -1
- package/src/agent/engine/internal/messageAssemblerProcessor.ts +99 -114
- package/src/agent/engine/internal/turnTrackerProcessor.ts +23 -27
- package/src/agent/types/event.ts +0 -4
- package/src/agent/types/index.ts +1 -3
- package/src/agent/types/message.ts +9 -43
- package/src/bash/index.ts +21 -0
- package/src/bash/tool.ts +57 -0
- package/src/bash/types.ts +108 -0
- package/src/driver/index.ts +1 -0
- package/src/driver/types.ts +122 -4
- package/src/event/__tests__/EventBus.test.ts +1 -1
- package/src/event/types/agent.ts +0 -11
- package/src/event/types/command.ts +3 -1
- package/src/image/Image.ts +11 -1
- package/src/image/types.ts +8 -2
- package/src/network/RpcClient.ts +21 -20
- package/src/network/index.ts +1 -1
- package/src/persistence/types.ts +5 -2
- package/src/platform/index.ts +21 -0
- package/src/platform/types.ts +84 -0
- package/src/runtime/AgentXRuntime.ts +184 -57
- package/src/runtime/__tests__/AgentXRuntime.test.ts +343 -0
- package/src/runtime/index.ts +7 -19
- package/src/runtime/types.ts +10 -62
- package/dist/chunk-7D4SUZUM.js +0 -38
- package/dist/chunk-AT5P47YA.js.map +0 -1
- package/dist/chunk-E5FPOAPO.js.map +0 -1
- package/dist/chunk-I7GYR3MN.js.map +0 -1
- package/dist/chunk-K6WXQ2RW.js.map +0 -1
- package/dist/workspace/index.d.ts +0 -111
- package/dist/wrapper-Y3UTVU2E.js +0 -3635
- package/dist/wrapper-Y3UTVU2E.js.map +0 -1
- package/src/workspace/index.ts +0 -27
- package/src/workspace/types.ts +0 -131
- /package/dist/{workspace → bash}/index.js.map +0 -0
- /package/dist/{chunk-7D4SUZUM.js.map → chunk-SKS7S2RY.js.map} +0 -0
- /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
|
|
22
|
-
function
|
|
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("
|
|
54
|
+
describe("message_start event", () => {
|
|
63
55
|
it("should create pending turn and emit turn_request", () => {
|
|
64
|
-
const event =
|
|
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
|
|
102
|
-
|
|
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
|
-
|
|
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
|
-
|
|
107
|
-
expect(
|
|
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: "
|
|
115
|
+
content: "",
|
|
121
116
|
requestedAt: 1000,
|
|
122
117
|
},
|
|
123
118
|
};
|
|
@@ -196,14 +191,23 @@ describe("turnTrackerProcessor", () => {
|
|
|
196
191
|
});
|
|
197
192
|
});
|
|
198
193
|
|
|
199
|
-
describe("
|
|
200
|
-
it("should
|
|
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: "
|
|
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
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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.
|
|
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
|
-
//
|
|
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
|
|
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,
|
|
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
|
-
* -
|
|
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
|
|
90
|
-
*
|
|
92
|
+
* Pending content blocks in stream order.
|
|
93
|
+
* Preserves the interleaved order of text and tool_use blocks.
|
|
91
94
|
*/
|
|
92
|
-
pendingContents:
|
|
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
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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
|
-
|
|
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
|
-
|
|
274
|
-
|
|
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
|
|
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
|
-
*
|
|
299
|
-
*
|
|
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
|
-
|
|
306
|
-
const
|
|
307
|
-
|
|
308
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
//
|
|
326
|
-
const
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
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:
|
|
339
|
+
pendingContents: updated,
|
|
360
340
|
pendingToolCalls: {
|
|
361
341
|
...state.pendingToolCalls,
|
|
362
342
|
[toolId]: { id: toolId, name: toolName },
|
|
363
343
|
},
|
|
364
344
|
},
|
|
365
|
-
[
|
|
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
|
-
//
|
|
442
|
-
const
|
|
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
|
|
427
|
+
for (const pending of state.pendingContents) {
|
|
446
428
|
if (pending.type === "text" && pending.textDeltas) {
|
|
447
|
-
|
|
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
|
|
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 (
|
|
446
|
+
// Skip truly empty messages (no text AND no tool calls)
|
|
454
447
|
const stopReason = data.stopReason;
|
|
455
|
-
if (!
|
|
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
|
|
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
|
|
469
|
+
// Emit AssistantMessageEvent
|
|
485
470
|
const assistantEvent: AssistantMessageEvent = {
|
|
486
471
|
type: "assistant_message",
|
|
487
472
|
timestamp,
|