@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,728 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* createAgent.test.ts - Unit tests for createAgent factory
|
|
3
|
+
*
|
|
4
|
+
* Tests the AgentEngine creation and event processing via EventBus.
|
|
5
|
+
* Uses MockEventBus to simulate Driver behavior.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, it, expect, beforeEach } from "bun:test";
|
|
9
|
+
import { createAgent } from "../createAgent";
|
|
10
|
+
import type {
|
|
11
|
+
AgentEventBus,
|
|
12
|
+
UserMessage,
|
|
13
|
+
StreamEvent,
|
|
14
|
+
AgentOutput,
|
|
15
|
+
CreateAgentOptions,
|
|
16
|
+
} from "../types";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* MockEventBus - Simulates EventBus for testing
|
|
20
|
+
*
|
|
21
|
+
* Flow:
|
|
22
|
+
* 1. agent.receive() emits user_message to bus
|
|
23
|
+
* 2. MockEventBus captures user_message and triggers Driver simulation
|
|
24
|
+
* 3. MockEventBus emits StreamEvents (simulating Driver)
|
|
25
|
+
* 4. Source subscribes to StreamEvents and forwards to AgentEngine
|
|
26
|
+
* 5. AgentEngine processes and emits AgentOutput via Presenter
|
|
27
|
+
*/
|
|
28
|
+
class MockEventBus implements AgentEventBus {
|
|
29
|
+
private handlers: Map<string, Set<(event: unknown) => void>> = new Map();
|
|
30
|
+
private anyHandlers: Set<(event: unknown) => void> = new Set();
|
|
31
|
+
|
|
32
|
+
// Events captured for assertions
|
|
33
|
+
readonly emittedEvents: unknown[] = [];
|
|
34
|
+
|
|
35
|
+
// Configure stream events to emit when user_message is received
|
|
36
|
+
private streamEventsToEmit: StreamEvent[] = [];
|
|
37
|
+
|
|
38
|
+
constructor(streamEvents: StreamEvent[] = []) {
|
|
39
|
+
this.streamEventsToEmit = streamEvents;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
setStreamEvents(events: StreamEvent[]): void {
|
|
43
|
+
this.streamEventsToEmit = events;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
emit(event: unknown): void {
|
|
47
|
+
this.emittedEvents.push(event);
|
|
48
|
+
|
|
49
|
+
const e = event as { type?: string };
|
|
50
|
+
const type = e.type;
|
|
51
|
+
|
|
52
|
+
// When user_message is received, simulate Driver behavior
|
|
53
|
+
// by emitting configured StreamEvents
|
|
54
|
+
if (type === "user_message") {
|
|
55
|
+
// Emit stream events asynchronously (simulating LLM response)
|
|
56
|
+
setTimeout(() => {
|
|
57
|
+
for (const streamEvent of this.streamEventsToEmit) {
|
|
58
|
+
this.emitInternal(streamEvent);
|
|
59
|
+
}
|
|
60
|
+
}, 0);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Notify handlers
|
|
64
|
+
if (type) {
|
|
65
|
+
const typeHandlers = this.handlers.get(type);
|
|
66
|
+
if (typeHandlers) {
|
|
67
|
+
for (const handler of typeHandlers) {
|
|
68
|
+
handler(event);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
for (const handler of this.anyHandlers) {
|
|
74
|
+
handler(event);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Internal emit without triggering user_message handling
|
|
80
|
+
* Adds source: "driver" and category: "stream" to simulate Driver behavior
|
|
81
|
+
*/
|
|
82
|
+
private emitInternal(event: unknown): void {
|
|
83
|
+
// Add source: "driver" and category: "stream" to simulate DriveableEvent
|
|
84
|
+
const eventWithSource = {
|
|
85
|
+
...(event as object),
|
|
86
|
+
source: "driver",
|
|
87
|
+
category: "stream",
|
|
88
|
+
intent: "notification",
|
|
89
|
+
};
|
|
90
|
+
this.emittedEvents.push(eventWithSource);
|
|
91
|
+
|
|
92
|
+
const e = eventWithSource as { type?: string };
|
|
93
|
+
const type = e.type;
|
|
94
|
+
|
|
95
|
+
if (type) {
|
|
96
|
+
const typeHandlers = this.handlers.get(type);
|
|
97
|
+
if (typeHandlers) {
|
|
98
|
+
for (const handler of typeHandlers) {
|
|
99
|
+
handler(eventWithSource);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
for (const handler of this.anyHandlers) {
|
|
105
|
+
handler(eventWithSource);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
on(type: string, handler: (event: unknown) => void): () => void {
|
|
110
|
+
if (!this.handlers.has(type)) {
|
|
111
|
+
this.handlers.set(type, new Set());
|
|
112
|
+
}
|
|
113
|
+
this.handlers.get(type)!.add(handler);
|
|
114
|
+
return () => this.handlers.get(type)?.delete(handler);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
onAny(handler: (event: unknown) => void): () => void {
|
|
118
|
+
this.anyHandlers.add(handler);
|
|
119
|
+
return () => this.anyHandlers.delete(handler);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Get emitted events of a specific type
|
|
124
|
+
*/
|
|
125
|
+
getEvents(type: string): unknown[] {
|
|
126
|
+
return this.emittedEvents.filter((e) => (e as { type?: string }).type === type);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Wait for stream events to be processed
|
|
131
|
+
*/
|
|
132
|
+
async waitForProcessing(): Promise<void> {
|
|
133
|
+
// Wait for setTimeout callbacks
|
|
134
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Helper to create stream events
|
|
139
|
+
function createStreamEvent(type: string, data: unknown = {}): StreamEvent {
|
|
140
|
+
return { type, data, timestamp: Date.now() } as StreamEvent;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Create a simple message flow (message_start -> text_delta -> message_stop)
|
|
144
|
+
function createSimpleMessageFlow(text: string, messageId = "msg_1"): StreamEvent[] {
|
|
145
|
+
return [
|
|
146
|
+
createStreamEvent("message_start", { messageId }),
|
|
147
|
+
createStreamEvent("text_delta", { text }),
|
|
148
|
+
createStreamEvent("message_stop", { stopReason: "end_turn" }),
|
|
149
|
+
];
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
describe("createAgent", () => {
|
|
153
|
+
let bus: MockEventBus;
|
|
154
|
+
let options: CreateAgentOptions;
|
|
155
|
+
|
|
156
|
+
beforeEach(() => {
|
|
157
|
+
bus = new MockEventBus(createSimpleMessageFlow("Hello, world!"));
|
|
158
|
+
options = { bus };
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
describe("creation", () => {
|
|
162
|
+
it("should create an agent with unique ID", () => {
|
|
163
|
+
const agent1 = createAgent(options);
|
|
164
|
+
const agent2 = createAgent(options);
|
|
165
|
+
|
|
166
|
+
expect(agent1.agentId).toBeDefined();
|
|
167
|
+
expect(agent2.agentId).toBeDefined();
|
|
168
|
+
expect(agent1.agentId).not.toBe(agent2.agentId);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("should create agent with createdAt timestamp", () => {
|
|
172
|
+
const before = Date.now();
|
|
173
|
+
const agent = createAgent(options);
|
|
174
|
+
const after = Date.now();
|
|
175
|
+
|
|
176
|
+
expect(agent.createdAt).toBeGreaterThanOrEqual(before);
|
|
177
|
+
expect(agent.createdAt).toBeLessThanOrEqual(after);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("should start in idle state", () => {
|
|
181
|
+
const agent = createAgent(options);
|
|
182
|
+
|
|
183
|
+
expect(agent.state).toBe("idle");
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("should have empty message queue", () => {
|
|
187
|
+
const agent = createAgent(options);
|
|
188
|
+
|
|
189
|
+
expect(agent.messageQueue.isEmpty).toBe(true);
|
|
190
|
+
expect(agent.messageQueue.length).toBe(0);
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
describe("receive", () => {
|
|
195
|
+
it("should emit user_message to EventBus", async () => {
|
|
196
|
+
const agent = createAgent(options);
|
|
197
|
+
|
|
198
|
+
await agent.receive("Hello");
|
|
199
|
+
|
|
200
|
+
const userMessages = bus.getEvents("user_message");
|
|
201
|
+
expect(userMessages.length).toBe(1);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it("should process string message", async () => {
|
|
205
|
+
const outputs: AgentOutput[] = [];
|
|
206
|
+
const agent = createAgent(options);
|
|
207
|
+
agent.on((e) => outputs.push(e));
|
|
208
|
+
|
|
209
|
+
await agent.receive("Hello");
|
|
210
|
+
await bus.waitForProcessing();
|
|
211
|
+
|
|
212
|
+
expect(outputs.length).toBeGreaterThan(0);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it("should process UserMessage object", async () => {
|
|
216
|
+
const outputs: AgentOutput[] = [];
|
|
217
|
+
const agent = createAgent(options);
|
|
218
|
+
agent.on((e) => outputs.push(e));
|
|
219
|
+
|
|
220
|
+
const userMessage: UserMessage = {
|
|
221
|
+
id: "custom_msg_1",
|
|
222
|
+
role: "user",
|
|
223
|
+
subtype: "user",
|
|
224
|
+
content: "Hello",
|
|
225
|
+
timestamp: Date.now(),
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
await agent.receive(userMessage);
|
|
229
|
+
await bus.waitForProcessing();
|
|
230
|
+
|
|
231
|
+
expect(outputs.length).toBeGreaterThan(0);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it("should emit stream events through presenter", async () => {
|
|
235
|
+
const events = [
|
|
236
|
+
createStreamEvent("message_start", { messageId: "msg_1" }),
|
|
237
|
+
createStreamEvent("text_delta", { text: "Hi" }),
|
|
238
|
+
createStreamEvent("message_stop", { stopReason: "end_turn" }),
|
|
239
|
+
];
|
|
240
|
+
|
|
241
|
+
const mockBus = new MockEventBus(events);
|
|
242
|
+
const outputs: AgentOutput[] = [];
|
|
243
|
+
const agent = createAgent({ bus: mockBus });
|
|
244
|
+
agent.on((e) => outputs.push(e));
|
|
245
|
+
|
|
246
|
+
await agent.receive("Hello");
|
|
247
|
+
await mockBus.waitForProcessing();
|
|
248
|
+
|
|
249
|
+
const types = outputs.map((o) => o.type);
|
|
250
|
+
expect(types).toContain("message_start");
|
|
251
|
+
expect(types).toContain("text_delta");
|
|
252
|
+
expect(types).toContain("message_stop");
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it("should emit state events from processor", async () => {
|
|
256
|
+
const outputs: AgentOutput[] = [];
|
|
257
|
+
const agent = createAgent(options);
|
|
258
|
+
agent.on((e) => outputs.push(e));
|
|
259
|
+
|
|
260
|
+
await agent.receive("Hello");
|
|
261
|
+
await bus.waitForProcessing();
|
|
262
|
+
|
|
263
|
+
const types = outputs.map((o) => o.type);
|
|
264
|
+
expect(types).toContain("conversation_start");
|
|
265
|
+
expect(types).toContain("conversation_responding");
|
|
266
|
+
expect(types).toContain("conversation_end");
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it("should emit message events from processor", async () => {
|
|
270
|
+
const outputs: AgentOutput[] = [];
|
|
271
|
+
const agent = createAgent(options);
|
|
272
|
+
agent.on((e) => outputs.push(e));
|
|
273
|
+
|
|
274
|
+
await agent.receive("Hello");
|
|
275
|
+
await bus.waitForProcessing();
|
|
276
|
+
|
|
277
|
+
const types = outputs.map((o) => o.type);
|
|
278
|
+
expect(types).toContain("assistant_message");
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
describe("state transitions", () => {
|
|
283
|
+
it("should transition through states during message processing", async () => {
|
|
284
|
+
const states: string[] = [];
|
|
285
|
+
const agent = createAgent(options);
|
|
286
|
+
|
|
287
|
+
agent.onStateChange((change) => {
|
|
288
|
+
states.push(change.current);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
await agent.receive("Hello");
|
|
292
|
+
await bus.waitForProcessing();
|
|
293
|
+
|
|
294
|
+
// Should have gone through thinking -> responding -> idle
|
|
295
|
+
expect(states).toContain("thinking");
|
|
296
|
+
expect(states).toContain("responding");
|
|
297
|
+
expect(states).toContain("idle");
|
|
298
|
+
});
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
describe("event handlers", () => {
|
|
302
|
+
describe("on(handler)", () => {
|
|
303
|
+
it("should subscribe to all events", async () => {
|
|
304
|
+
const agent = createAgent(options);
|
|
305
|
+
const events: AgentOutput[] = [];
|
|
306
|
+
|
|
307
|
+
agent.on((event) => events.push(event));
|
|
308
|
+
|
|
309
|
+
await agent.receive("Hello");
|
|
310
|
+
await bus.waitForProcessing();
|
|
311
|
+
|
|
312
|
+
expect(events.length).toBeGreaterThan(0);
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it("should return unsubscribe function", async () => {
|
|
316
|
+
const agent = createAgent(options);
|
|
317
|
+
const events: AgentOutput[] = [];
|
|
318
|
+
|
|
319
|
+
const unsubscribe = agent.on((event) => events.push(event));
|
|
320
|
+
unsubscribe();
|
|
321
|
+
|
|
322
|
+
await agent.receive("Hello");
|
|
323
|
+
await bus.waitForProcessing();
|
|
324
|
+
|
|
325
|
+
expect(events).toHaveLength(0);
|
|
326
|
+
});
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
describe("on(type, handler)", () => {
|
|
330
|
+
it("should subscribe to specific event type", async () => {
|
|
331
|
+
const agent = createAgent(options);
|
|
332
|
+
const textDeltas: AgentOutput[] = [];
|
|
333
|
+
|
|
334
|
+
agent.on("text_delta", (event) => textDeltas.push(event));
|
|
335
|
+
|
|
336
|
+
await agent.receive("Hello");
|
|
337
|
+
await bus.waitForProcessing();
|
|
338
|
+
|
|
339
|
+
expect(textDeltas.length).toBeGreaterThan(0);
|
|
340
|
+
expect(textDeltas.every((e) => e.type === "text_delta")).toBe(true);
|
|
341
|
+
});
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
describe("on(types[], handler)", () => {
|
|
345
|
+
it("should subscribe to multiple event types", async () => {
|
|
346
|
+
const agent = createAgent(options);
|
|
347
|
+
const events: AgentOutput[] = [];
|
|
348
|
+
|
|
349
|
+
agent.on(["message_start", "message_stop"], (event) => events.push(event));
|
|
350
|
+
|
|
351
|
+
await agent.receive("Hello");
|
|
352
|
+
await bus.waitForProcessing();
|
|
353
|
+
|
|
354
|
+
const types = events.map((e) => e.type);
|
|
355
|
+
expect(types).toContain("message_start");
|
|
356
|
+
expect(types).toContain("message_stop");
|
|
357
|
+
expect(types.every((t) => t === "message_start" || t === "message_stop")).toBe(true);
|
|
358
|
+
});
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
describe("on(handlers: EventHandlerMap)", () => {
|
|
362
|
+
it("should subscribe using handler map", async () => {
|
|
363
|
+
const agent = createAgent(options);
|
|
364
|
+
const starts: AgentOutput[] = [];
|
|
365
|
+
const stops: AgentOutput[] = [];
|
|
366
|
+
|
|
367
|
+
agent.on({
|
|
368
|
+
message_start: (e) => starts.push(e),
|
|
369
|
+
message_stop: (e) => stops.push(e),
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
await agent.receive("Hello");
|
|
373
|
+
await bus.waitForProcessing();
|
|
374
|
+
|
|
375
|
+
expect(starts).toHaveLength(1);
|
|
376
|
+
expect(stops).toHaveLength(1);
|
|
377
|
+
});
|
|
378
|
+
});
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
describe("react", () => {
|
|
382
|
+
it("should subscribe using camelCase handlers", async () => {
|
|
383
|
+
const agent = createAgent(options);
|
|
384
|
+
const textDeltas: AgentOutput[] = [];
|
|
385
|
+
const assistantMessages: AgentOutput[] = [];
|
|
386
|
+
|
|
387
|
+
agent.react({
|
|
388
|
+
onTextDelta: (e) => textDeltas.push(e),
|
|
389
|
+
onAssistantMessage: (e) => assistantMessages.push(e),
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
await agent.receive("Hello");
|
|
393
|
+
await bus.waitForProcessing();
|
|
394
|
+
|
|
395
|
+
expect(textDeltas.length).toBeGreaterThan(0);
|
|
396
|
+
expect(assistantMessages.length).toBeGreaterThan(0);
|
|
397
|
+
});
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
describe("onStateChange", () => {
|
|
401
|
+
it("should notify on state changes", async () => {
|
|
402
|
+
const agent = createAgent(options);
|
|
403
|
+
const changes: Array<{ prev: string; current: string }> = [];
|
|
404
|
+
|
|
405
|
+
agent.onStateChange((change) => changes.push(change));
|
|
406
|
+
|
|
407
|
+
await agent.receive("Hello");
|
|
408
|
+
await bus.waitForProcessing();
|
|
409
|
+
|
|
410
|
+
expect(changes.length).toBeGreaterThan(0);
|
|
411
|
+
expect(changes[0].prev).toBe("idle");
|
|
412
|
+
});
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
describe("onReady", () => {
|
|
416
|
+
it("should call handler immediately", () => {
|
|
417
|
+
const agent = createAgent(options);
|
|
418
|
+
let called = false;
|
|
419
|
+
|
|
420
|
+
agent.onReady(() => {
|
|
421
|
+
called = true;
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
expect(called).toBe(true);
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
it("should return unsubscribe function", () => {
|
|
428
|
+
const agent = createAgent(options);
|
|
429
|
+
let callCount = 0;
|
|
430
|
+
|
|
431
|
+
const unsubscribe = agent.onReady(() => {
|
|
432
|
+
callCount++;
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
expect(callCount).toBe(1);
|
|
436
|
+
unsubscribe();
|
|
437
|
+
// No additional calls after unsubscribe
|
|
438
|
+
});
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
describe("onDestroy", () => {
|
|
442
|
+
it("should call handler on destroy", async () => {
|
|
443
|
+
const agent = createAgent(options);
|
|
444
|
+
let called = false;
|
|
445
|
+
|
|
446
|
+
agent.onDestroy(() => {
|
|
447
|
+
called = true;
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
await agent.destroy();
|
|
451
|
+
|
|
452
|
+
expect(called).toBe(true);
|
|
453
|
+
});
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
describe("middleware", () => {
|
|
457
|
+
it("should pass message through middleware", async () => {
|
|
458
|
+
const agent = createAgent(options);
|
|
459
|
+
const middlewareMessages: UserMessage[] = [];
|
|
460
|
+
|
|
461
|
+
agent.use(async (message, next) => {
|
|
462
|
+
middlewareMessages.push(message);
|
|
463
|
+
await next(message);
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
await agent.receive("Hello");
|
|
467
|
+
|
|
468
|
+
expect(middlewareMessages).toHaveLength(1);
|
|
469
|
+
expect(middlewareMessages[0].content).toBe("Hello");
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
it("should allow middleware to modify message", async () => {
|
|
473
|
+
// Capture user_message content from EventBus
|
|
474
|
+
let receivedContent: string | undefined;
|
|
475
|
+
|
|
476
|
+
const mockBus = new MockEventBus(createSimpleMessageFlow("Response"));
|
|
477
|
+
mockBus.on("user_message", (event) => {
|
|
478
|
+
const e = event as { data?: { content?: string } };
|
|
479
|
+
receivedContent = e.data?.content;
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
const agent = createAgent({ bus: mockBus });
|
|
483
|
+
|
|
484
|
+
agent.use(async (message, next) => {
|
|
485
|
+
const modified: UserMessage = {
|
|
486
|
+
...message,
|
|
487
|
+
content: message.content + " MODIFIED",
|
|
488
|
+
};
|
|
489
|
+
await next(modified);
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
await agent.receive("Hello");
|
|
493
|
+
|
|
494
|
+
expect(receivedContent).toBe("Hello MODIFIED");
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
it("should allow middleware to block message", async () => {
|
|
498
|
+
const agent = createAgent(options);
|
|
499
|
+
|
|
500
|
+
agent.use(async (_message, _next) => {
|
|
501
|
+
// Don't call next - block the message
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
await agent.receive("Hello");
|
|
505
|
+
|
|
506
|
+
// No user_message should be emitted
|
|
507
|
+
const userMessages = bus.getEvents("user_message");
|
|
508
|
+
expect(userMessages).toHaveLength(0);
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
it("should chain multiple middlewares", async () => {
|
|
512
|
+
const agent = createAgent(options);
|
|
513
|
+
const order: number[] = [];
|
|
514
|
+
|
|
515
|
+
agent.use(async (message, next) => {
|
|
516
|
+
order.push(1);
|
|
517
|
+
await next(message);
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
agent.use(async (message, next) => {
|
|
521
|
+
order.push(2);
|
|
522
|
+
await next(message);
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
await agent.receive("Hello");
|
|
526
|
+
|
|
527
|
+
expect(order).toEqual([1, 2]);
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
it("should return unsubscribe function", async () => {
|
|
531
|
+
const agent = createAgent(options);
|
|
532
|
+
let middlewareCalled = false;
|
|
533
|
+
|
|
534
|
+
const unsubscribe = agent.use(async (message, next) => {
|
|
535
|
+
middlewareCalled = true;
|
|
536
|
+
await next(message);
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
unsubscribe();
|
|
540
|
+
|
|
541
|
+
await agent.receive("Hello");
|
|
542
|
+
|
|
543
|
+
expect(middlewareCalled).toBe(false);
|
|
544
|
+
});
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
describe("interceptor", () => {
|
|
548
|
+
it("should intercept output events", async () => {
|
|
549
|
+
const agent = createAgent(options);
|
|
550
|
+
const interceptedEvents: AgentOutput[] = [];
|
|
551
|
+
|
|
552
|
+
agent.intercept((event, next) => {
|
|
553
|
+
interceptedEvents.push(event);
|
|
554
|
+
next(event);
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
await agent.receive("Hello");
|
|
558
|
+
await bus.waitForProcessing();
|
|
559
|
+
|
|
560
|
+
expect(interceptedEvents.length).toBeGreaterThan(0);
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
it("should allow interceptor to modify events", async () => {
|
|
564
|
+
const agent = createAgent(options);
|
|
565
|
+
const outputs: AgentOutput[] = [];
|
|
566
|
+
|
|
567
|
+
agent.on((e) => outputs.push(e));
|
|
568
|
+
|
|
569
|
+
agent.intercept((event, next) => {
|
|
570
|
+
const modified = { ...event, modified: true } as AgentOutput & { modified: boolean };
|
|
571
|
+
next(modified);
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
await agent.receive("Hello");
|
|
575
|
+
await bus.waitForProcessing();
|
|
576
|
+
|
|
577
|
+
expect(outputs.every((e) => (e as unknown as { modified: boolean }).modified)).toBe(true);
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
it("should allow interceptor to filter events", async () => {
|
|
581
|
+
const agent = createAgent(options);
|
|
582
|
+
const outputs: AgentOutput[] = [];
|
|
583
|
+
|
|
584
|
+
agent.on((e) => outputs.push(e));
|
|
585
|
+
|
|
586
|
+
agent.intercept((event, next) => {
|
|
587
|
+
// Only pass through text_delta events
|
|
588
|
+
if (event.type === "text_delta") {
|
|
589
|
+
next(event);
|
|
590
|
+
}
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
await agent.receive("Hello");
|
|
594
|
+
await bus.waitForProcessing();
|
|
595
|
+
|
|
596
|
+
// Filtered to only text_delta events
|
|
597
|
+
const textDeltas = outputs.filter((e) => e.type === "text_delta");
|
|
598
|
+
expect(textDeltas.length).toBeGreaterThan(0);
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
it("should return unsubscribe function", async () => {
|
|
602
|
+
const agent = createAgent(options);
|
|
603
|
+
let interceptorCalled = false;
|
|
604
|
+
|
|
605
|
+
const unsubscribe = agent.intercept((event, next) => {
|
|
606
|
+
interceptorCalled = true;
|
|
607
|
+
next(event);
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
unsubscribe();
|
|
611
|
+
|
|
612
|
+
await agent.receive("Hello");
|
|
613
|
+
await bus.waitForProcessing();
|
|
614
|
+
|
|
615
|
+
expect(interceptorCalled).toBe(false);
|
|
616
|
+
});
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
describe("interrupt", () => {
|
|
620
|
+
it("should emit interrupt_request to EventBus when not idle", async () => {
|
|
621
|
+
// Create a bus that doesn't auto-emit events
|
|
622
|
+
const mockBus = new MockEventBus([]);
|
|
623
|
+
const agent = createAgent({ bus: mockBus });
|
|
624
|
+
|
|
625
|
+
// Manually trigger state change to non-idle by simulating stream start
|
|
626
|
+
const messageStart = createStreamEvent("message_start", { messageId: "msg_1" });
|
|
627
|
+
agent.handleStreamEvent(messageStart);
|
|
628
|
+
|
|
629
|
+
// Agent should be in non-idle state now
|
|
630
|
+
expect(agent.state).not.toBe("idle");
|
|
631
|
+
|
|
632
|
+
// Now interrupt
|
|
633
|
+
agent.interrupt();
|
|
634
|
+
|
|
635
|
+
const interruptRequests = mockBus.getEvents("interrupt_request");
|
|
636
|
+
expect(interruptRequests.length).toBe(1);
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
it("should not emit interrupt when idle", () => {
|
|
640
|
+
const mockBus = new MockEventBus([]);
|
|
641
|
+
const agent = createAgent({ bus: mockBus });
|
|
642
|
+
|
|
643
|
+
// Agent is idle
|
|
644
|
+
expect(agent.state).toBe("idle");
|
|
645
|
+
|
|
646
|
+
agent.interrupt();
|
|
647
|
+
|
|
648
|
+
const interruptRequests = mockBus.getEvents("interrupt_request");
|
|
649
|
+
expect(interruptRequests.length).toBe(0);
|
|
650
|
+
});
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
describe("destroy", () => {
|
|
654
|
+
it("should clear handlers", async () => {
|
|
655
|
+
const agent = createAgent(options);
|
|
656
|
+
const events: AgentOutput[] = [];
|
|
657
|
+
|
|
658
|
+
agent.on((e) => events.push(e));
|
|
659
|
+
|
|
660
|
+
await agent.destroy();
|
|
661
|
+
|
|
662
|
+
// After destroy, handlers should be cleared
|
|
663
|
+
// Sending another message should not add to events
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
it("should call onDestroy handlers", async () => {
|
|
667
|
+
const agent = createAgent(options);
|
|
668
|
+
const destroyCalls: number[] = [];
|
|
669
|
+
|
|
670
|
+
agent.onDestroy(() => destroyCalls.push(1));
|
|
671
|
+
agent.onDestroy(() => destroyCalls.push(2));
|
|
672
|
+
|
|
673
|
+
await agent.destroy();
|
|
674
|
+
|
|
675
|
+
expect(destroyCalls).toEqual([1, 2]);
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
it("should clear message queue", async () => {
|
|
679
|
+
const agent = createAgent(options);
|
|
680
|
+
|
|
681
|
+
expect(agent.messageQueue.isEmpty).toBe(true);
|
|
682
|
+
|
|
683
|
+
await agent.destroy();
|
|
684
|
+
|
|
685
|
+
expect(agent.messageQueue.isEmpty).toBe(true);
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
it("should reject receive after destroy", async () => {
|
|
689
|
+
const agent = createAgent(options);
|
|
690
|
+
|
|
691
|
+
await agent.destroy();
|
|
692
|
+
|
|
693
|
+
await expect(agent.receive("Hello")).rejects.toThrow("destroyed");
|
|
694
|
+
});
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
describe("handleStreamEvent", () => {
|
|
698
|
+
it("should process stream events directly", () => {
|
|
699
|
+
const outputs: AgentOutput[] = [];
|
|
700
|
+
const agent = createAgent(options);
|
|
701
|
+
agent.on((e) => outputs.push(e));
|
|
702
|
+
|
|
703
|
+
// Directly push stream events
|
|
704
|
+
agent.handleStreamEvent(createStreamEvent("message_start", { messageId: "msg_1" }));
|
|
705
|
+
agent.handleStreamEvent(createStreamEvent("text_delta", { text: "Hello" }));
|
|
706
|
+
agent.handleStreamEvent(createStreamEvent("message_stop", { stopReason: "end_turn" }));
|
|
707
|
+
|
|
708
|
+
const types = outputs.map((o) => o.type);
|
|
709
|
+
expect(types).toContain("message_start");
|
|
710
|
+
expect(types).toContain("text_delta");
|
|
711
|
+
expect(types).toContain("message_stop");
|
|
712
|
+
});
|
|
713
|
+
});
|
|
714
|
+
|
|
715
|
+
describe("error handling", () => {
|
|
716
|
+
it("should handle handler errors gracefully", async () => {
|
|
717
|
+
const agent = createAgent(options);
|
|
718
|
+
|
|
719
|
+
agent.on(() => {
|
|
720
|
+
throw new Error("Handler error");
|
|
721
|
+
});
|
|
722
|
+
|
|
723
|
+
// Should not throw
|
|
724
|
+
await agent.receive("Hello");
|
|
725
|
+
await bus.waitForProcessing();
|
|
726
|
+
});
|
|
727
|
+
});
|
|
728
|
+
});
|