@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,346 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AgentStateMachine.test.ts - Unit tests for AgentStateMachine
|
|
3
|
+
*
|
|
4
|
+
* Tests the state machine that manages agent state transitions
|
|
5
|
+
* driven by StateEvents from the MealyMachine.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, it, expect, beforeEach } from "bun:test";
|
|
9
|
+
import { AgentStateMachine } from "../AgentStateMachine";
|
|
10
|
+
import type { AgentState, StateChange, AgentOutput } from "../types";
|
|
11
|
+
|
|
12
|
+
// Helper to create test events
|
|
13
|
+
function createStateEvent(type: string, data: unknown = {}): AgentOutput {
|
|
14
|
+
return { type, data, timestamp: Date.now() } as AgentOutput;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
describe("AgentStateMachine", () => {
|
|
18
|
+
let stateMachine: AgentStateMachine;
|
|
19
|
+
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
stateMachine = new AgentStateMachine();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe("initial state", () => {
|
|
25
|
+
it("should start in idle state", () => {
|
|
26
|
+
expect(stateMachine.state).toBe("idle");
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe("state transitions", () => {
|
|
31
|
+
describe("conversation lifecycle", () => {
|
|
32
|
+
it("should transition to thinking on conversation_start", () => {
|
|
33
|
+
stateMachine.process(createStateEvent("conversation_start", { messageId: "msg_1" }));
|
|
34
|
+
|
|
35
|
+
expect(stateMachine.state).toBe("thinking");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("should transition to thinking on conversation_thinking", () => {
|
|
39
|
+
stateMachine.process(createStateEvent("conversation_start", { messageId: "msg_1" }));
|
|
40
|
+
stateMachine.process(createStateEvent("conversation_thinking"));
|
|
41
|
+
|
|
42
|
+
expect(stateMachine.state).toBe("thinking");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("should transition to responding on conversation_responding", () => {
|
|
46
|
+
stateMachine.process(createStateEvent("conversation_start", { messageId: "msg_1" }));
|
|
47
|
+
stateMachine.process(createStateEvent("conversation_responding"));
|
|
48
|
+
|
|
49
|
+
expect(stateMachine.state).toBe("responding");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("should transition to idle on conversation_end", () => {
|
|
53
|
+
stateMachine.process(createStateEvent("conversation_start", { messageId: "msg_1" }));
|
|
54
|
+
stateMachine.process(createStateEvent("conversation_responding"));
|
|
55
|
+
stateMachine.process(createStateEvent("conversation_end", { reason: "completed" }));
|
|
56
|
+
|
|
57
|
+
expect(stateMachine.state).toBe("idle");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("should transition to idle on conversation_interrupted", () => {
|
|
61
|
+
stateMachine.process(createStateEvent("conversation_start", { messageId: "msg_1" }));
|
|
62
|
+
stateMachine.process(createStateEvent("conversation_responding"));
|
|
63
|
+
stateMachine.process(createStateEvent("conversation_interrupted"));
|
|
64
|
+
|
|
65
|
+
expect(stateMachine.state).toBe("idle");
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe("tool lifecycle", () => {
|
|
70
|
+
it("should transition to planning_tool on tool_planned", () => {
|
|
71
|
+
stateMachine.process(createStateEvent("conversation_start", { messageId: "msg_1" }));
|
|
72
|
+
stateMachine.process(
|
|
73
|
+
createStateEvent("tool_planned", { toolId: "t1", toolName: "search" })
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
expect(stateMachine.state).toBe("planning_tool");
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("should transition to awaiting_tool_result on tool_executing", () => {
|
|
80
|
+
stateMachine.process(createStateEvent("conversation_start", { messageId: "msg_1" }));
|
|
81
|
+
stateMachine.process(
|
|
82
|
+
createStateEvent("tool_planned", { toolId: "t1", toolName: "search" })
|
|
83
|
+
);
|
|
84
|
+
stateMachine.process(
|
|
85
|
+
createStateEvent("tool_executing", { toolId: "t1", toolName: "search", input: {} })
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
expect(stateMachine.state).toBe("awaiting_tool_result");
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("should transition to responding on tool_completed", () => {
|
|
92
|
+
stateMachine.process(createStateEvent("conversation_start", { messageId: "msg_1" }));
|
|
93
|
+
stateMachine.process(
|
|
94
|
+
createStateEvent("tool_executing", { toolId: "t1", toolName: "search", input: {} })
|
|
95
|
+
);
|
|
96
|
+
stateMachine.process(createStateEvent("tool_completed", { toolId: "t1", result: "done" }));
|
|
97
|
+
|
|
98
|
+
expect(stateMachine.state).toBe("responding");
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("should transition to responding on tool_failed", () => {
|
|
102
|
+
stateMachine.process(createStateEvent("conversation_start", { messageId: "msg_1" }));
|
|
103
|
+
stateMachine.process(
|
|
104
|
+
createStateEvent("tool_executing", { toolId: "t1", toolName: "search", input: {} })
|
|
105
|
+
);
|
|
106
|
+
stateMachine.process(createStateEvent("tool_failed", { toolId: "t1", error: "error" }));
|
|
107
|
+
|
|
108
|
+
expect(stateMachine.state).toBe("responding");
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe("error handling", () => {
|
|
113
|
+
it("should transition to error on error_occurred", () => {
|
|
114
|
+
stateMachine.process(createStateEvent("conversation_start", { messageId: "msg_1" }));
|
|
115
|
+
stateMachine.process(
|
|
116
|
+
createStateEvent("error_occurred", {
|
|
117
|
+
code: "api_error",
|
|
118
|
+
message: "API failed",
|
|
119
|
+
recoverable: true,
|
|
120
|
+
})
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
expect(stateMachine.state).toBe("error");
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
describe("non-state events", () => {
|
|
129
|
+
it("should ignore stream events", () => {
|
|
130
|
+
stateMachine.process(createStateEvent("text_delta", { text: "Hello" }));
|
|
131
|
+
expect(stateMachine.state).toBe("idle");
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("should ignore message events", () => {
|
|
135
|
+
stateMachine.process(createStateEvent("assistant_message", { content: "Hi" }));
|
|
136
|
+
expect(stateMachine.state).toBe("idle");
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("should ignore turn events", () => {
|
|
140
|
+
stateMachine.process(createStateEvent("turn_request", { turnId: "t1" }));
|
|
141
|
+
expect(stateMachine.state).toBe("idle");
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("should ignore unknown events", () => {
|
|
145
|
+
stateMachine.process(createStateEvent("completely_unknown_event", {}));
|
|
146
|
+
expect(stateMachine.state).toBe("idle");
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
describe("no redundant transitions", () => {
|
|
151
|
+
it("should not trigger handler when state does not change", () => {
|
|
152
|
+
const changes: StateChange[] = [];
|
|
153
|
+
stateMachine.onStateChange((change) => changes.push(change));
|
|
154
|
+
|
|
155
|
+
// Go to thinking
|
|
156
|
+
stateMachine.process(createStateEvent("conversation_start", { messageId: "msg_1" }));
|
|
157
|
+
|
|
158
|
+
// Process thinking again - no change
|
|
159
|
+
stateMachine.process(createStateEvent("conversation_thinking"));
|
|
160
|
+
|
|
161
|
+
// Should only have one change (idle -> thinking)
|
|
162
|
+
expect(changes).toHaveLength(1);
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
describe("onStateChange", () => {
|
|
167
|
+
it("should notify handler on state change", () => {
|
|
168
|
+
const changes: StateChange[] = [];
|
|
169
|
+
stateMachine.onStateChange((change) => changes.push(change));
|
|
170
|
+
|
|
171
|
+
stateMachine.process(createStateEvent("conversation_start", { messageId: "msg_1" }));
|
|
172
|
+
|
|
173
|
+
expect(changes).toHaveLength(1);
|
|
174
|
+
expect(changes[0]).toEqual({ prev: "idle", current: "thinking" });
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it("should notify multiple handlers", () => {
|
|
178
|
+
const changes1: StateChange[] = [];
|
|
179
|
+
const changes2: StateChange[] = [];
|
|
180
|
+
|
|
181
|
+
stateMachine.onStateChange((change) => changes1.push(change));
|
|
182
|
+
stateMachine.onStateChange((change) => changes2.push(change));
|
|
183
|
+
|
|
184
|
+
stateMachine.process(createStateEvent("conversation_start", { messageId: "msg_1" }));
|
|
185
|
+
|
|
186
|
+
expect(changes1).toHaveLength(1);
|
|
187
|
+
expect(changes2).toHaveLength(1);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("should return unsubscribe function", () => {
|
|
191
|
+
const changes: StateChange[] = [];
|
|
192
|
+
const unsubscribe = stateMachine.onStateChange((change) => changes.push(change));
|
|
193
|
+
|
|
194
|
+
stateMachine.process(createStateEvent("conversation_start", { messageId: "msg_1" }));
|
|
195
|
+
expect(changes).toHaveLength(1);
|
|
196
|
+
|
|
197
|
+
unsubscribe();
|
|
198
|
+
|
|
199
|
+
stateMachine.process(createStateEvent("conversation_responding"));
|
|
200
|
+
expect(changes).toHaveLength(1); // No new change recorded
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it("should handle handler errors gracefully", () => {
|
|
204
|
+
const goodChanges: StateChange[] = [];
|
|
205
|
+
|
|
206
|
+
stateMachine.onStateChange(() => {
|
|
207
|
+
throw new Error("Handler error");
|
|
208
|
+
});
|
|
209
|
+
stateMachine.onStateChange((change) => goodChanges.push(change));
|
|
210
|
+
|
|
211
|
+
// Should not throw
|
|
212
|
+
expect(() => {
|
|
213
|
+
stateMachine.process(createStateEvent("conversation_start", { messageId: "msg_1" }));
|
|
214
|
+
}).not.toThrow();
|
|
215
|
+
|
|
216
|
+
// Good handler should still receive the change
|
|
217
|
+
expect(goodChanges).toHaveLength(1);
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
describe("reset", () => {
|
|
222
|
+
it("should reset state to idle", () => {
|
|
223
|
+
stateMachine.process(createStateEvent("conversation_start", { messageId: "msg_1" }));
|
|
224
|
+
stateMachine.process(createStateEvent("conversation_responding"));
|
|
225
|
+
expect(stateMachine.state).toBe("responding");
|
|
226
|
+
|
|
227
|
+
stateMachine.reset();
|
|
228
|
+
|
|
229
|
+
expect(stateMachine.state).toBe("idle");
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it("should notify handlers of reset", () => {
|
|
233
|
+
const changes: StateChange[] = [];
|
|
234
|
+
stateMachine.onStateChange((change) => changes.push(change));
|
|
235
|
+
|
|
236
|
+
stateMachine.process(createStateEvent("conversation_start", { messageId: "msg_1" }));
|
|
237
|
+
expect(changes).toHaveLength(1);
|
|
238
|
+
|
|
239
|
+
stateMachine.reset();
|
|
240
|
+
|
|
241
|
+
expect(changes).toHaveLength(2);
|
|
242
|
+
expect(changes[1]).toEqual({ prev: "thinking", current: "idle" });
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it("should not notify if already idle", () => {
|
|
246
|
+
const changes: StateChange[] = [];
|
|
247
|
+
stateMachine.onStateChange((change) => changes.push(change));
|
|
248
|
+
|
|
249
|
+
stateMachine.reset();
|
|
250
|
+
|
|
251
|
+
expect(changes).toHaveLength(0);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it("should clear all handlers", () => {
|
|
255
|
+
const changes: StateChange[] = [];
|
|
256
|
+
stateMachine.onStateChange((change) => changes.push(change));
|
|
257
|
+
|
|
258
|
+
stateMachine.process(createStateEvent("conversation_start", { messageId: "msg_1" }));
|
|
259
|
+
stateMachine.reset();
|
|
260
|
+
|
|
261
|
+
// Process new event after reset
|
|
262
|
+
stateMachine.process(createStateEvent("conversation_start", { messageId: "msg_2" }));
|
|
263
|
+
|
|
264
|
+
// Handler was cleared, so no new change recorded
|
|
265
|
+
// We had 2 changes: idle->thinking and thinking->idle (from reset)
|
|
266
|
+
expect(changes).toHaveLength(2);
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
describe("complete state flow", () => {
|
|
271
|
+
it("should handle complete conversation flow", () => {
|
|
272
|
+
const states: AgentState[] = [stateMachine.state];
|
|
273
|
+
stateMachine.onStateChange((change) => states.push(change.current));
|
|
274
|
+
|
|
275
|
+
// User sends message -> conversation starts
|
|
276
|
+
stateMachine.process(createStateEvent("conversation_start", { messageId: "msg_1" }));
|
|
277
|
+
|
|
278
|
+
// AI starts responding
|
|
279
|
+
stateMachine.process(createStateEvent("conversation_responding"));
|
|
280
|
+
|
|
281
|
+
// Conversation ends
|
|
282
|
+
stateMachine.process(createStateEvent("conversation_end", { reason: "completed" }));
|
|
283
|
+
|
|
284
|
+
expect(states).toEqual(["idle", "thinking", "responding", "idle"]);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it("should handle conversation with tool use flow", () => {
|
|
288
|
+
const states: AgentState[] = [stateMachine.state];
|
|
289
|
+
stateMachine.onStateChange((change) => states.push(change.current));
|
|
290
|
+
|
|
291
|
+
// Start
|
|
292
|
+
stateMachine.process(createStateEvent("conversation_start", { messageId: "msg_1" }));
|
|
293
|
+
|
|
294
|
+
// AI plans tool
|
|
295
|
+
stateMachine.process(createStateEvent("tool_planned", { toolId: "t1", toolName: "search" }));
|
|
296
|
+
|
|
297
|
+
// Tool executing
|
|
298
|
+
stateMachine.process(
|
|
299
|
+
createStateEvent("tool_executing", { toolId: "t1", toolName: "search", input: {} })
|
|
300
|
+
);
|
|
301
|
+
|
|
302
|
+
// Tool completes
|
|
303
|
+
stateMachine.process(createStateEvent("tool_completed", { toolId: "t1", result: "done" }));
|
|
304
|
+
|
|
305
|
+
// AI continues responding
|
|
306
|
+
stateMachine.process(createStateEvent("conversation_responding"));
|
|
307
|
+
|
|
308
|
+
// End
|
|
309
|
+
stateMachine.process(createStateEvent("conversation_end", { reason: "completed" }));
|
|
310
|
+
|
|
311
|
+
// Since we only record state changes (not repeated states),
|
|
312
|
+
// conversation_responding after tool_completed (responding->responding) is not recorded
|
|
313
|
+
expect(states).toEqual([
|
|
314
|
+
"idle",
|
|
315
|
+
"thinking",
|
|
316
|
+
"planning_tool",
|
|
317
|
+
"awaiting_tool_result",
|
|
318
|
+
"responding",
|
|
319
|
+
"idle",
|
|
320
|
+
]);
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
it("should handle error recovery flow", () => {
|
|
324
|
+
const states: AgentState[] = [stateMachine.state];
|
|
325
|
+
stateMachine.onStateChange((change) => states.push(change.current));
|
|
326
|
+
|
|
327
|
+
// Start conversation
|
|
328
|
+
stateMachine.process(createStateEvent("conversation_start", { messageId: "msg_1" }));
|
|
329
|
+
|
|
330
|
+
// Error occurs
|
|
331
|
+
stateMachine.process(
|
|
332
|
+
createStateEvent("error_occurred", {
|
|
333
|
+
code: "api_error",
|
|
334
|
+
message: "API failed",
|
|
335
|
+
recoverable: true,
|
|
336
|
+
})
|
|
337
|
+
);
|
|
338
|
+
|
|
339
|
+
expect(states).toEqual(["idle", "thinking", "error"]);
|
|
340
|
+
|
|
341
|
+
// Reset via reset() to simulate recovery
|
|
342
|
+
stateMachine.reset();
|
|
343
|
+
expect(stateMachine.state).toBe("idle");
|
|
344
|
+
});
|
|
345
|
+
});
|
|
346
|
+
});
|