@agentxjs/core 1.9.1-dev
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/package.json +31 -0
- package/src/agent/AgentStateMachine.ts +151 -0
- package/src/agent/README.md +296 -0
- package/src/agent/__tests__/AgentStateMachine.test.ts +346 -0
- package/src/agent/__tests__/createAgent.test.ts +728 -0
- package/src/agent/__tests__/engine/internal/messageAssemblerProcessor.test.ts +567 -0
- package/src/agent/__tests__/engine/internal/stateEventProcessor.test.ts +315 -0
- package/src/agent/__tests__/engine/internal/turnTrackerProcessor.test.ts +340 -0
- package/src/agent/__tests__/engine/mealy/Mealy.test.ts +370 -0
- package/src/agent/__tests__/engine/mealy/Store.test.ts +123 -0
- package/src/agent/__tests__/engine/mealy/combinators.test.ts +322 -0
- package/src/agent/createAgent.ts +467 -0
- package/src/agent/engine/AgentProcessor.ts +106 -0
- package/src/agent/engine/MealyMachine.ts +184 -0
- package/src/agent/engine/internal/index.ts +35 -0
- package/src/agent/engine/internal/messageAssemblerProcessor.ts +550 -0
- package/src/agent/engine/internal/stateEventProcessor.ts +313 -0
- package/src/agent/engine/internal/turnTrackerProcessor.ts +239 -0
- package/src/agent/engine/mealy/Mealy.ts +308 -0
- package/src/agent/engine/mealy/Processor.ts +70 -0
- package/src/agent/engine/mealy/Sink.ts +56 -0
- package/src/agent/engine/mealy/Source.ts +51 -0
- package/src/agent/engine/mealy/Store.ts +98 -0
- package/src/agent/engine/mealy/combinators.ts +176 -0
- package/src/agent/engine/mealy/index.ts +45 -0
- package/src/agent/index.ts +106 -0
- package/src/agent/types/engine.ts +395 -0
- package/src/agent/types/event.ts +478 -0
- package/src/agent/types/index.ts +197 -0
- package/src/agent/types/message.ts +387 -0
- package/src/common/index.ts +8 -0
- package/src/common/logger/ConsoleLogger.ts +137 -0
- package/src/common/logger/LoggerFactoryImpl.ts +123 -0
- package/src/common/logger/index.ts +26 -0
- package/src/common/logger/types.ts +98 -0
- package/src/container/Container.ts +185 -0
- package/src/container/index.ts +44 -0
- package/src/container/types.ts +71 -0
- package/src/driver/index.ts +42 -0
- package/src/driver/types.ts +363 -0
- package/src/event/EventBus.ts +260 -0
- package/src/event/README.md +237 -0
- package/src/event/__tests__/EventBus.test.ts +251 -0
- package/src/event/index.ts +46 -0
- package/src/event/types/agent.ts +512 -0
- package/src/event/types/base.ts +241 -0
- package/src/event/types/bus.ts +429 -0
- package/src/event/types/command.ts +749 -0
- package/src/event/types/container.ts +471 -0
- package/src/event/types/driver.ts +452 -0
- package/src/event/types/index.ts +26 -0
- package/src/event/types/session.ts +314 -0
- package/src/image/Image.ts +203 -0
- package/src/image/index.ts +36 -0
- package/src/image/types.ts +77 -0
- package/src/index.ts +20 -0
- package/src/mq/OffsetGenerator.ts +48 -0
- package/src/mq/README.md +166 -0
- package/src/mq/__tests__/OffsetGenerator.test.ts +121 -0
- package/src/mq/index.ts +18 -0
- package/src/mq/types.ts +172 -0
- package/src/network/RpcClient.ts +455 -0
- package/src/network/index.ts +76 -0
- package/src/network/jsonrpc.ts +336 -0
- package/src/network/protocol.ts +90 -0
- package/src/network/types.ts +284 -0
- package/src/persistence/index.ts +27 -0
- package/src/persistence/types.ts +226 -0
- package/src/runtime/AgentXRuntime.ts +501 -0
- package/src/runtime/index.ts +56 -0
- package/src/runtime/types.ts +236 -0
- package/src/session/Session.ts +71 -0
- package/src/session/index.ts +25 -0
- package/src/session/types.ts +77 -0
- package/src/workspace/index.ts +27 -0
- package/src/workspace/types.ts +131 -0
- package/tsconfig.json +10 -0
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* stateEventProcessor.test.ts - Unit tests for state event processor
|
|
3
|
+
*
|
|
4
|
+
* Tests the stateless event transformer: Stream Events -> State Events
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, it, expect, beforeEach } from "bun:test";
|
|
8
|
+
import {
|
|
9
|
+
stateEventProcessor,
|
|
10
|
+
createInitialStateEventProcessorContext,
|
|
11
|
+
type StateEventProcessorContext,
|
|
12
|
+
type StateEventProcessorInput,
|
|
13
|
+
type StateEventProcessorOutput,
|
|
14
|
+
} from "../../../engine/internal/stateEventProcessor";
|
|
15
|
+
|
|
16
|
+
// Helper to create test events
|
|
17
|
+
function createStreamEvent(
|
|
18
|
+
type: string,
|
|
19
|
+
data: unknown = {},
|
|
20
|
+
timestamp = Date.now()
|
|
21
|
+
): StateEventProcessorInput {
|
|
22
|
+
return { type, data, timestamp } as StateEventProcessorInput;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
describe("stateEventProcessor", () => {
|
|
26
|
+
let context: StateEventProcessorContext;
|
|
27
|
+
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
context = createInitialStateEventProcessorContext();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe("initial context", () => {
|
|
33
|
+
it("should create empty initial context", () => {
|
|
34
|
+
const initialContext = createInitialStateEventProcessorContext();
|
|
35
|
+
expect(initialContext).toEqual({});
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe("message_start event", () => {
|
|
40
|
+
it("should emit conversation_start event", () => {
|
|
41
|
+
const event = createStreamEvent("message_start", { messageId: "msg_123" });
|
|
42
|
+
|
|
43
|
+
const [newContext, outputs] = stateEventProcessor(context, event);
|
|
44
|
+
|
|
45
|
+
expect(outputs).toHaveLength(1);
|
|
46
|
+
expect(outputs[0].type).toBe("conversation_start");
|
|
47
|
+
expect(outputs[0].data.messageId).toBe("msg_123");
|
|
48
|
+
expect(outputs[0].timestamp).toBeDefined();
|
|
49
|
+
|
|
50
|
+
// Context should remain unchanged
|
|
51
|
+
expect(newContext).toEqual(context);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe("message_delta event", () => {
|
|
56
|
+
it("should not emit any event (no-op)", () => {
|
|
57
|
+
const event = createStreamEvent("message_delta", { stopReason: "end_turn" });
|
|
58
|
+
|
|
59
|
+
const [newContext, outputs] = stateEventProcessor(context, event);
|
|
60
|
+
|
|
61
|
+
expect(outputs).toHaveLength(0);
|
|
62
|
+
expect(newContext).toEqual(context);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe("message_stop event", () => {
|
|
67
|
+
it("should emit conversation_end for end_turn stop reason", () => {
|
|
68
|
+
const event = createStreamEvent("message_stop", { stopReason: "end_turn" });
|
|
69
|
+
|
|
70
|
+
const [newContext, outputs] = stateEventProcessor(context, event);
|
|
71
|
+
|
|
72
|
+
expect(outputs).toHaveLength(1);
|
|
73
|
+
expect(outputs[0].type).toBe("conversation_end");
|
|
74
|
+
expect(outputs[0].data.reason).toBe("completed");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("should emit conversation_end for max_tokens stop reason", () => {
|
|
78
|
+
const event = createStreamEvent("message_stop", { stopReason: "max_tokens" });
|
|
79
|
+
|
|
80
|
+
const [, outputs] = stateEventProcessor(context, event);
|
|
81
|
+
|
|
82
|
+
expect(outputs).toHaveLength(1);
|
|
83
|
+
expect(outputs[0].type).toBe("conversation_end");
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("should emit conversation_end for stop_sequence stop reason", () => {
|
|
87
|
+
const event = createStreamEvent("message_stop", { stopReason: "stop_sequence" });
|
|
88
|
+
|
|
89
|
+
const [, outputs] = stateEventProcessor(context, event);
|
|
90
|
+
|
|
91
|
+
expect(outputs).toHaveLength(1);
|
|
92
|
+
expect(outputs[0].type).toBe("conversation_end");
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("should NOT emit conversation_end for tool_use stop reason", () => {
|
|
96
|
+
const event = createStreamEvent("message_stop", { stopReason: "tool_use" });
|
|
97
|
+
|
|
98
|
+
const [newContext, outputs] = stateEventProcessor(context, event);
|
|
99
|
+
|
|
100
|
+
expect(outputs).toHaveLength(0);
|
|
101
|
+
expect(newContext).toEqual(context);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe("text_delta event", () => {
|
|
106
|
+
it("should emit conversation_responding event", () => {
|
|
107
|
+
const event = createStreamEvent("text_delta", { text: "Hello" });
|
|
108
|
+
|
|
109
|
+
const [newContext, outputs] = stateEventProcessor(context, event);
|
|
110
|
+
|
|
111
|
+
expect(outputs).toHaveLength(1);
|
|
112
|
+
expect(outputs[0].type).toBe("conversation_responding");
|
|
113
|
+
expect(outputs[0].data).toEqual({});
|
|
114
|
+
expect(outputs[0].timestamp).toBeDefined();
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
describe("tool_use_start event", () => {
|
|
119
|
+
it("should emit tool_planned and tool_executing events", () => {
|
|
120
|
+
const event = createStreamEvent("tool_use_start", {
|
|
121
|
+
toolCallId: "tool_123",
|
|
122
|
+
toolName: "search",
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const [newContext, outputs] = stateEventProcessor(context, event);
|
|
126
|
+
|
|
127
|
+
expect(outputs).toHaveLength(2);
|
|
128
|
+
|
|
129
|
+
// First output: tool_planned
|
|
130
|
+
expect(outputs[0].type).toBe("tool_planned");
|
|
131
|
+
expect(outputs[0].data.toolId).toBe("tool_123");
|
|
132
|
+
expect(outputs[0].data.toolName).toBe("search");
|
|
133
|
+
|
|
134
|
+
// Second output: tool_executing
|
|
135
|
+
expect(outputs[1].type).toBe("tool_executing");
|
|
136
|
+
expect(outputs[1].data.toolId).toBe("tool_123");
|
|
137
|
+
expect(outputs[1].data.toolName).toBe("search");
|
|
138
|
+
expect(outputs[1].data.input).toEqual({});
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
describe("tool_use_stop event", () => {
|
|
143
|
+
it("should not emit any event (pass through)", () => {
|
|
144
|
+
const event = createStreamEvent("tool_use_stop", {});
|
|
145
|
+
|
|
146
|
+
const [newContext, outputs] = stateEventProcessor(context, event);
|
|
147
|
+
|
|
148
|
+
expect(outputs).toHaveLength(0);
|
|
149
|
+
expect(newContext).toEqual(context);
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
describe("error_received event", () => {
|
|
154
|
+
it("should emit error_occurred event with error details", () => {
|
|
155
|
+
const event = createStreamEvent("error_received", {
|
|
156
|
+
message: "API rate limit exceeded",
|
|
157
|
+
errorCode: "rate_limit",
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
const [newContext, outputs] = stateEventProcessor(context, event);
|
|
161
|
+
|
|
162
|
+
expect(outputs).toHaveLength(1);
|
|
163
|
+
expect(outputs[0].type).toBe("error_occurred");
|
|
164
|
+
expect(outputs[0].data.code).toBe("rate_limit");
|
|
165
|
+
expect(outputs[0].data.message).toBe("API rate limit exceeded");
|
|
166
|
+
expect(outputs[0].data.recoverable).toBe(true);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("should use unknown_error code when errorCode is missing", () => {
|
|
170
|
+
const event = createStreamEvent("error_received", {
|
|
171
|
+
message: "Something went wrong",
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
const [, outputs] = stateEventProcessor(context, event);
|
|
175
|
+
|
|
176
|
+
expect(outputs[0].data.code).toBe("unknown_error");
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
describe("unhandled events", () => {
|
|
181
|
+
it("should pass through unhandled events without output", () => {
|
|
182
|
+
const event = createStreamEvent("input_json_delta", { partialJson: "{}" });
|
|
183
|
+
|
|
184
|
+
const [newContext, outputs] = stateEventProcessor(context, event);
|
|
185
|
+
|
|
186
|
+
expect(outputs).toHaveLength(0);
|
|
187
|
+
expect(newContext).toEqual(context);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("should handle completely unknown event types", () => {
|
|
191
|
+
const event = createStreamEvent("completely_unknown_event", { anything: true });
|
|
192
|
+
|
|
193
|
+
const [newContext, outputs] = stateEventProcessor(context, event);
|
|
194
|
+
|
|
195
|
+
expect(outputs).toHaveLength(0);
|
|
196
|
+
expect(newContext).toEqual(context);
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
describe("complete conversation flow", () => {
|
|
201
|
+
it("should produce correct state events for simple message", () => {
|
|
202
|
+
const allOutputs: StateEventProcessorOutput[] = [];
|
|
203
|
+
|
|
204
|
+
// message_start
|
|
205
|
+
const [, o1] = stateEventProcessor(
|
|
206
|
+
context,
|
|
207
|
+
createStreamEvent("message_start", { messageId: "msg_1" })
|
|
208
|
+
);
|
|
209
|
+
allOutputs.push(...o1);
|
|
210
|
+
|
|
211
|
+
// text_delta
|
|
212
|
+
const [, o2] = stateEventProcessor(
|
|
213
|
+
context,
|
|
214
|
+
createStreamEvent("text_delta", { text: "Hello" })
|
|
215
|
+
);
|
|
216
|
+
allOutputs.push(...o2);
|
|
217
|
+
|
|
218
|
+
// message_stop
|
|
219
|
+
const [, o3] = stateEventProcessor(
|
|
220
|
+
context,
|
|
221
|
+
createStreamEvent("message_stop", { stopReason: "end_turn" })
|
|
222
|
+
);
|
|
223
|
+
allOutputs.push(...o3);
|
|
224
|
+
|
|
225
|
+
expect(allOutputs).toHaveLength(3);
|
|
226
|
+
expect(allOutputs.map((o) => o.type)).toEqual([
|
|
227
|
+
"conversation_start",
|
|
228
|
+
"conversation_responding",
|
|
229
|
+
"conversation_end",
|
|
230
|
+
]);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it("should produce correct state events for tool call flow", () => {
|
|
234
|
+
const allOutputs: StateEventProcessorOutput[] = [];
|
|
235
|
+
|
|
236
|
+
// message_start
|
|
237
|
+
const [, o1] = stateEventProcessor(
|
|
238
|
+
context,
|
|
239
|
+
createStreamEvent("message_start", { messageId: "msg_1" })
|
|
240
|
+
);
|
|
241
|
+
allOutputs.push(...o1);
|
|
242
|
+
|
|
243
|
+
// tool_use_start
|
|
244
|
+
const [, o2] = stateEventProcessor(
|
|
245
|
+
context,
|
|
246
|
+
createStreamEvent("tool_use_start", { toolCallId: "tool_1", toolName: "search" })
|
|
247
|
+
);
|
|
248
|
+
allOutputs.push(...o2);
|
|
249
|
+
|
|
250
|
+
// tool_use_stop
|
|
251
|
+
const [, o3] = stateEventProcessor(context, createStreamEvent("tool_use_stop", {}));
|
|
252
|
+
allOutputs.push(...o3);
|
|
253
|
+
|
|
254
|
+
// message_stop with tool_use (should NOT emit conversation_end)
|
|
255
|
+
const [, o4] = stateEventProcessor(
|
|
256
|
+
context,
|
|
257
|
+
createStreamEvent("message_stop", { stopReason: "tool_use" })
|
|
258
|
+
);
|
|
259
|
+
allOutputs.push(...o4);
|
|
260
|
+
|
|
261
|
+
// New message after tool result
|
|
262
|
+
const [, o5] = stateEventProcessor(
|
|
263
|
+
context,
|
|
264
|
+
createStreamEvent("message_start", { messageId: "msg_2" })
|
|
265
|
+
);
|
|
266
|
+
allOutputs.push(...o5);
|
|
267
|
+
|
|
268
|
+
// text_delta with final response
|
|
269
|
+
const [, o6] = stateEventProcessor(
|
|
270
|
+
context,
|
|
271
|
+
createStreamEvent("text_delta", { text: "Result" })
|
|
272
|
+
);
|
|
273
|
+
allOutputs.push(...o6);
|
|
274
|
+
|
|
275
|
+
// Final message_stop
|
|
276
|
+
const [, o7] = stateEventProcessor(
|
|
277
|
+
context,
|
|
278
|
+
createStreamEvent("message_stop", { stopReason: "end_turn" })
|
|
279
|
+
);
|
|
280
|
+
allOutputs.push(...o7);
|
|
281
|
+
|
|
282
|
+
expect(allOutputs.map((o) => o.type)).toEqual([
|
|
283
|
+
"conversation_start", // msg_1 start
|
|
284
|
+
"tool_planned", // tool started
|
|
285
|
+
"tool_executing", // tool executing
|
|
286
|
+
// No output for tool_use_stop
|
|
287
|
+
// No conversation_end for tool_use
|
|
288
|
+
"conversation_start", // msg_2 start
|
|
289
|
+
"conversation_responding", // text delta
|
|
290
|
+
"conversation_end", // final end
|
|
291
|
+
]);
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
describe("statelessness", () => {
|
|
296
|
+
it("should not modify context across multiple calls", () => {
|
|
297
|
+
const originalContext = createInitialStateEventProcessorContext();
|
|
298
|
+
|
|
299
|
+
// Process multiple events
|
|
300
|
+
stateEventProcessor(originalContext, createStreamEvent("message_start", { messageId: "1" }));
|
|
301
|
+
stateEventProcessor(originalContext, createStreamEvent("text_delta", { text: "Hello" }));
|
|
302
|
+
stateEventProcessor(
|
|
303
|
+
originalContext,
|
|
304
|
+
createStreamEvent("tool_use_start", { toolCallId: "t1", toolName: "test" })
|
|
305
|
+
);
|
|
306
|
+
stateEventProcessor(
|
|
307
|
+
originalContext,
|
|
308
|
+
createStreamEvent("error_received", { message: "error" })
|
|
309
|
+
);
|
|
310
|
+
|
|
311
|
+
// Original context should remain unchanged
|
|
312
|
+
expect(originalContext).toEqual({});
|
|
313
|
+
});
|
|
314
|
+
});
|
|
315
|
+
});
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* turnTrackerProcessor.test.ts - Unit tests for turn tracker processor
|
|
3
|
+
*
|
|
4
|
+
* Tests the pure Mealy transition function that tracks request-response turn pairs.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, it, expect, beforeEach } from "bun:test";
|
|
8
|
+
import {
|
|
9
|
+
turnTrackerProcessor,
|
|
10
|
+
createInitialTurnTrackerState,
|
|
11
|
+
type TurnTrackerState,
|
|
12
|
+
type TurnTrackerInput,
|
|
13
|
+
type TurnTrackerOutput,
|
|
14
|
+
} from "../../../engine/internal/turnTrackerProcessor";
|
|
15
|
+
|
|
16
|
+
// Helper to create test events
|
|
17
|
+
function createEvent(type: string, data: unknown, timestamp = Date.now()): TurnTrackerInput {
|
|
18
|
+
return { type, data, timestamp } as TurnTrackerInput;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Helper to create user message event
|
|
22
|
+
function createUserMessageEvent(
|
|
23
|
+
content: string,
|
|
24
|
+
messageId: string = `msg_${Date.now()}`,
|
|
25
|
+
timestamp = Date.now()
|
|
26
|
+
): 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
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Helper to create message_stop event
|
|
41
|
+
function createMessageStopEvent(stopReason: string, timestamp = Date.now()): TurnTrackerInput {
|
|
42
|
+
return createEvent("message_stop", { stopReason }, timestamp);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
describe("turnTrackerProcessor", () => {
|
|
46
|
+
let state: TurnTrackerState;
|
|
47
|
+
|
|
48
|
+
beforeEach(() => {
|
|
49
|
+
state = createInitialTurnTrackerState();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe("initial state", () => {
|
|
53
|
+
it("should create correct initial state", () => {
|
|
54
|
+
const initialState = createInitialTurnTrackerState();
|
|
55
|
+
|
|
56
|
+
expect(initialState.pendingTurn).toBeNull();
|
|
57
|
+
expect(initialState.costPerInputToken).toBe(0.000003);
|
|
58
|
+
expect(initialState.costPerOutputToken).toBe(0.000015);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe("user_message event", () => {
|
|
63
|
+
it("should create pending turn and emit turn_request", () => {
|
|
64
|
+
const event = createUserMessageEvent("Hello", "msg_123", 1000);
|
|
65
|
+
|
|
66
|
+
const [newState, outputs] = turnTrackerProcessor(state, event);
|
|
67
|
+
|
|
68
|
+
// Should have pending turn
|
|
69
|
+
expect(newState.pendingTurn).not.toBeNull();
|
|
70
|
+
expect(newState.pendingTurn?.messageId).toBe("msg_123");
|
|
71
|
+
expect(newState.pendingTurn?.content).toBe("Hello");
|
|
72
|
+
expect(newState.pendingTurn?.requestedAt).toBe(1000);
|
|
73
|
+
expect(newState.pendingTurn?.turnId).toBeDefined();
|
|
74
|
+
|
|
75
|
+
// Should emit turn_request
|
|
76
|
+
expect(outputs).toHaveLength(1);
|
|
77
|
+
expect(outputs[0].type).toBe("turn_request");
|
|
78
|
+
expect(outputs[0].data.messageId).toBe("msg_123");
|
|
79
|
+
expect(outputs[0].data.content).toBe("Hello");
|
|
80
|
+
expect(outputs[0].data.timestamp).toBe(1000);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("should generate unique turn IDs", () => {
|
|
84
|
+
const [state1, outputs1] = turnTrackerProcessor(
|
|
85
|
+
state,
|
|
86
|
+
createUserMessageEvent("First", "msg_1")
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
// Complete the turn
|
|
90
|
+
const [state2] = turnTrackerProcessor(state1, createMessageStopEvent("end_turn"));
|
|
91
|
+
|
|
92
|
+
// Start new turn
|
|
93
|
+
const [state3, outputs3] = turnTrackerProcessor(
|
|
94
|
+
state2,
|
|
95
|
+
createUserMessageEvent("Second", "msg_2")
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
expect(outputs1[0].data.turnId).not.toBe(outputs3[0].data.turnId);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("should handle empty content", () => {
|
|
102
|
+
const event = createUserMessageEvent("", "msg_empty");
|
|
103
|
+
|
|
104
|
+
const [newState, outputs] = turnTrackerProcessor(state, event);
|
|
105
|
+
|
|
106
|
+
expect(newState.pendingTurn?.content).toBe("");
|
|
107
|
+
expect(outputs[0].data.content).toBe("");
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
describe("message_stop event", () => {
|
|
112
|
+
describe("with pending turn", () => {
|
|
113
|
+
beforeEach(() => {
|
|
114
|
+
// Setup pending turn
|
|
115
|
+
state = {
|
|
116
|
+
...state,
|
|
117
|
+
pendingTurn: {
|
|
118
|
+
turnId: "turn_123",
|
|
119
|
+
messageId: "msg_123",
|
|
120
|
+
content: "Test message",
|
|
121
|
+
requestedAt: 1000,
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("should complete turn and emit turn_response for end_turn", () => {
|
|
127
|
+
const event = createMessageStopEvent("end_turn", 2000);
|
|
128
|
+
|
|
129
|
+
const [newState, outputs] = turnTrackerProcessor(state, event);
|
|
130
|
+
|
|
131
|
+
expect(newState.pendingTurn).toBeNull();
|
|
132
|
+
|
|
133
|
+
expect(outputs).toHaveLength(1);
|
|
134
|
+
expect(outputs[0].type).toBe("turn_response");
|
|
135
|
+
expect(outputs[0].data.turnId).toBe("turn_123");
|
|
136
|
+
expect(outputs[0].data.messageId).toBe("msg_123");
|
|
137
|
+
expect(outputs[0].data.duration).toBe(1000); // 2000 - 1000
|
|
138
|
+
expect(outputs[0].data.timestamp).toBe(2000);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("should complete turn for max_tokens", () => {
|
|
142
|
+
const event = createMessageStopEvent("max_tokens", 3000);
|
|
143
|
+
|
|
144
|
+
const [newState, outputs] = turnTrackerProcessor(state, event);
|
|
145
|
+
|
|
146
|
+
expect(newState.pendingTurn).toBeNull();
|
|
147
|
+
expect(outputs).toHaveLength(1);
|
|
148
|
+
expect(outputs[0].type).toBe("turn_response");
|
|
149
|
+
expect(outputs[0].data.duration).toBe(2000); // 3000 - 1000
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("should complete turn for stop_sequence", () => {
|
|
153
|
+
const event = createMessageStopEvent("stop_sequence", 1500);
|
|
154
|
+
|
|
155
|
+
const [newState, outputs] = turnTrackerProcessor(state, event);
|
|
156
|
+
|
|
157
|
+
expect(newState.pendingTurn).toBeNull();
|
|
158
|
+
expect(outputs).toHaveLength(1);
|
|
159
|
+
expect(outputs[0].type).toBe("turn_response");
|
|
160
|
+
expect(outputs[0].data.duration).toBe(500); // 1500 - 1000
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("should NOT complete turn for tool_use", () => {
|
|
164
|
+
const event = createMessageStopEvent("tool_use", 2000);
|
|
165
|
+
|
|
166
|
+
const [newState, outputs] = turnTrackerProcessor(state, event);
|
|
167
|
+
|
|
168
|
+
// Should keep pending turn
|
|
169
|
+
expect(newState.pendingTurn).not.toBeNull();
|
|
170
|
+
expect(newState.pendingTurn?.turnId).toBe("turn_123");
|
|
171
|
+
|
|
172
|
+
// Should not emit output
|
|
173
|
+
expect(outputs).toHaveLength(0);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("should include usage information", () => {
|
|
177
|
+
const event = createMessageStopEvent("end_turn", 2000);
|
|
178
|
+
|
|
179
|
+
const [, outputs] = turnTrackerProcessor(state, event);
|
|
180
|
+
|
|
181
|
+
expect(outputs[0].data.usage).toBeDefined();
|
|
182
|
+
expect(outputs[0].data.usage.inputTokens).toBe(0);
|
|
183
|
+
expect(outputs[0].data.usage.outputTokens).toBe(0);
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
describe("without pending turn", () => {
|
|
188
|
+
it("should not emit output when no pending turn", () => {
|
|
189
|
+
const event = createMessageStopEvent("end_turn", 2000);
|
|
190
|
+
|
|
191
|
+
const [newState, outputs] = turnTrackerProcessor(state, event);
|
|
192
|
+
|
|
193
|
+
expect(newState).toEqual(state);
|
|
194
|
+
expect(outputs).toHaveLength(0);
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
describe("assistant_message event", () => {
|
|
200
|
+
it("should not emit output (handled in message_stop)", () => {
|
|
201
|
+
state = {
|
|
202
|
+
...state,
|
|
203
|
+
pendingTurn: {
|
|
204
|
+
turnId: "turn_123",
|
|
205
|
+
messageId: "msg_123",
|
|
206
|
+
content: "Test",
|
|
207
|
+
requestedAt: 1000,
|
|
208
|
+
},
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
const event = createEvent("assistant_message", {
|
|
212
|
+
id: "msg_assistant",
|
|
213
|
+
role: "assistant",
|
|
214
|
+
content: [{ type: "text", text: "Response" }],
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
const [newState, outputs] = turnTrackerProcessor(state, event);
|
|
218
|
+
|
|
219
|
+
// State should be unchanged
|
|
220
|
+
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
|
+
expect(outputs).toHaveLength(0);
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
describe("complete turn flow", () => {
|
|
239
|
+
it("should track complete turn from request to response", () => {
|
|
240
|
+
const allOutputs: TurnTrackerOutput[] = [];
|
|
241
|
+
let currentState = createInitialTurnTrackerState();
|
|
242
|
+
|
|
243
|
+
// User sends message at time 1000
|
|
244
|
+
const [s1, o1] = turnTrackerProcessor(
|
|
245
|
+
currentState,
|
|
246
|
+
createUserMessageEvent("What is 2+2?", "msg_1", 1000)
|
|
247
|
+
);
|
|
248
|
+
currentState = s1;
|
|
249
|
+
allOutputs.push(...o1);
|
|
250
|
+
|
|
251
|
+
// Assistant responds, message_stop at time 2500
|
|
252
|
+
const [s2, o2] = turnTrackerProcessor(currentState, createMessageStopEvent("end_turn", 2500));
|
|
253
|
+
currentState = s2;
|
|
254
|
+
allOutputs.push(...o2);
|
|
255
|
+
|
|
256
|
+
expect(allOutputs).toHaveLength(2);
|
|
257
|
+
|
|
258
|
+
// turn_request
|
|
259
|
+
expect(allOutputs[0].type).toBe("turn_request");
|
|
260
|
+
expect(allOutputs[0].data.content).toBe("What is 2+2?");
|
|
261
|
+
|
|
262
|
+
// turn_response
|
|
263
|
+
expect(allOutputs[1].type).toBe("turn_response");
|
|
264
|
+
expect(allOutputs[1].data.duration).toBe(1500);
|
|
265
|
+
|
|
266
|
+
// State should be clean
|
|
267
|
+
expect(currentState.pendingTurn).toBeNull();
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it("should track turn with tool use (turn continues)", () => {
|
|
271
|
+
const allOutputs: TurnTrackerOutput[] = [];
|
|
272
|
+
let currentState = createInitialTurnTrackerState();
|
|
273
|
+
|
|
274
|
+
// User sends message
|
|
275
|
+
const [s1, o1] = turnTrackerProcessor(
|
|
276
|
+
currentState,
|
|
277
|
+
createUserMessageEvent("Search for X", "msg_1", 1000)
|
|
278
|
+
);
|
|
279
|
+
currentState = s1;
|
|
280
|
+
allOutputs.push(...o1);
|
|
281
|
+
|
|
282
|
+
// First message_stop with tool_use - turn should NOT complete
|
|
283
|
+
const [s2, o2] = turnTrackerProcessor(currentState, createMessageStopEvent("tool_use", 1500));
|
|
284
|
+
currentState = s2;
|
|
285
|
+
allOutputs.push(...o2);
|
|
286
|
+
|
|
287
|
+
expect(currentState.pendingTurn).not.toBeNull();
|
|
288
|
+
expect(allOutputs).toHaveLength(1); // Only turn_request
|
|
289
|
+
|
|
290
|
+
// Second message_stop with end_turn - turn completes
|
|
291
|
+
const [s3, o3] = turnTrackerProcessor(currentState, createMessageStopEvent("end_turn", 3000));
|
|
292
|
+
currentState = s3;
|
|
293
|
+
allOutputs.push(...o3);
|
|
294
|
+
|
|
295
|
+
expect(allOutputs).toHaveLength(2); // turn_request + turn_response
|
|
296
|
+
expect(allOutputs[1].type).toBe("turn_response");
|
|
297
|
+
expect(allOutputs[1].data.duration).toBe(2000); // 3000 - 1000
|
|
298
|
+
expect(currentState.pendingTurn).toBeNull();
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it("should handle multiple consecutive turns", () => {
|
|
302
|
+
let currentState = createInitialTurnTrackerState();
|
|
303
|
+
const allTurnIds: string[] = [];
|
|
304
|
+
|
|
305
|
+
// First turn
|
|
306
|
+
const [s1, o1] = turnTrackerProcessor(
|
|
307
|
+
currentState,
|
|
308
|
+
createUserMessageEvent("First", "msg_1", 1000)
|
|
309
|
+
);
|
|
310
|
+
currentState = s1;
|
|
311
|
+
allTurnIds.push(o1[0].data.turnId);
|
|
312
|
+
|
|
313
|
+
const [s2] = turnTrackerProcessor(currentState, createMessageStopEvent("end_turn", 1500));
|
|
314
|
+
currentState = s2;
|
|
315
|
+
|
|
316
|
+
// Second turn
|
|
317
|
+
const [s3, o3] = turnTrackerProcessor(
|
|
318
|
+
currentState,
|
|
319
|
+
createUserMessageEvent("Second", "msg_2", 2000)
|
|
320
|
+
);
|
|
321
|
+
currentState = s3;
|
|
322
|
+
allTurnIds.push(o3[0].data.turnId);
|
|
323
|
+
|
|
324
|
+
const [s4] = turnTrackerProcessor(currentState, createMessageStopEvent("end_turn", 2500));
|
|
325
|
+
currentState = s4;
|
|
326
|
+
|
|
327
|
+
// Third turn
|
|
328
|
+
const [s5, o5] = turnTrackerProcessor(
|
|
329
|
+
currentState,
|
|
330
|
+
createUserMessageEvent("Third", "msg_3", 3000)
|
|
331
|
+
);
|
|
332
|
+
currentState = s5;
|
|
333
|
+
allTurnIds.push(o5[0].data.turnId);
|
|
334
|
+
|
|
335
|
+
// All turn IDs should be unique
|
|
336
|
+
const uniqueIds = new Set(allTurnIds);
|
|
337
|
+
expect(uniqueIds.size).toBe(3);
|
|
338
|
+
});
|
|
339
|
+
});
|
|
340
|
+
});
|