@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.
Files changed (77) hide show
  1. package/package.json +31 -0
  2. package/src/agent/AgentStateMachine.ts +151 -0
  3. package/src/agent/README.md +296 -0
  4. package/src/agent/__tests__/AgentStateMachine.test.ts +346 -0
  5. package/src/agent/__tests__/createAgent.test.ts +728 -0
  6. package/src/agent/__tests__/engine/internal/messageAssemblerProcessor.test.ts +567 -0
  7. package/src/agent/__tests__/engine/internal/stateEventProcessor.test.ts +315 -0
  8. package/src/agent/__tests__/engine/internal/turnTrackerProcessor.test.ts +340 -0
  9. package/src/agent/__tests__/engine/mealy/Mealy.test.ts +370 -0
  10. package/src/agent/__tests__/engine/mealy/Store.test.ts +123 -0
  11. package/src/agent/__tests__/engine/mealy/combinators.test.ts +322 -0
  12. package/src/agent/createAgent.ts +467 -0
  13. package/src/agent/engine/AgentProcessor.ts +106 -0
  14. package/src/agent/engine/MealyMachine.ts +184 -0
  15. package/src/agent/engine/internal/index.ts +35 -0
  16. package/src/agent/engine/internal/messageAssemblerProcessor.ts +550 -0
  17. package/src/agent/engine/internal/stateEventProcessor.ts +313 -0
  18. package/src/agent/engine/internal/turnTrackerProcessor.ts +239 -0
  19. package/src/agent/engine/mealy/Mealy.ts +308 -0
  20. package/src/agent/engine/mealy/Processor.ts +70 -0
  21. package/src/agent/engine/mealy/Sink.ts +56 -0
  22. package/src/agent/engine/mealy/Source.ts +51 -0
  23. package/src/agent/engine/mealy/Store.ts +98 -0
  24. package/src/agent/engine/mealy/combinators.ts +176 -0
  25. package/src/agent/engine/mealy/index.ts +45 -0
  26. package/src/agent/index.ts +106 -0
  27. package/src/agent/types/engine.ts +395 -0
  28. package/src/agent/types/event.ts +478 -0
  29. package/src/agent/types/index.ts +197 -0
  30. package/src/agent/types/message.ts +387 -0
  31. package/src/common/index.ts +8 -0
  32. package/src/common/logger/ConsoleLogger.ts +137 -0
  33. package/src/common/logger/LoggerFactoryImpl.ts +123 -0
  34. package/src/common/logger/index.ts +26 -0
  35. package/src/common/logger/types.ts +98 -0
  36. package/src/container/Container.ts +185 -0
  37. package/src/container/index.ts +44 -0
  38. package/src/container/types.ts +71 -0
  39. package/src/driver/index.ts +42 -0
  40. package/src/driver/types.ts +363 -0
  41. package/src/event/EventBus.ts +260 -0
  42. package/src/event/README.md +237 -0
  43. package/src/event/__tests__/EventBus.test.ts +251 -0
  44. package/src/event/index.ts +46 -0
  45. package/src/event/types/agent.ts +512 -0
  46. package/src/event/types/base.ts +241 -0
  47. package/src/event/types/bus.ts +429 -0
  48. package/src/event/types/command.ts +749 -0
  49. package/src/event/types/container.ts +471 -0
  50. package/src/event/types/driver.ts +452 -0
  51. package/src/event/types/index.ts +26 -0
  52. package/src/event/types/session.ts +314 -0
  53. package/src/image/Image.ts +203 -0
  54. package/src/image/index.ts +36 -0
  55. package/src/image/types.ts +77 -0
  56. package/src/index.ts +20 -0
  57. package/src/mq/OffsetGenerator.ts +48 -0
  58. package/src/mq/README.md +166 -0
  59. package/src/mq/__tests__/OffsetGenerator.test.ts +121 -0
  60. package/src/mq/index.ts +18 -0
  61. package/src/mq/types.ts +172 -0
  62. package/src/network/RpcClient.ts +455 -0
  63. package/src/network/index.ts +76 -0
  64. package/src/network/jsonrpc.ts +336 -0
  65. package/src/network/protocol.ts +90 -0
  66. package/src/network/types.ts +284 -0
  67. package/src/persistence/index.ts +27 -0
  68. package/src/persistence/types.ts +226 -0
  69. package/src/runtime/AgentXRuntime.ts +501 -0
  70. package/src/runtime/index.ts +56 -0
  71. package/src/runtime/types.ts +236 -0
  72. package/src/session/Session.ts +71 -0
  73. package/src/session/index.ts +25 -0
  74. package/src/session/types.ts +77 -0
  75. package/src/workspace/index.ts +27 -0
  76. package/src/workspace/types.ts +131 -0
  77. 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
+ });