@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,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
+ });