|
7 | 7 | import { describe, expect, it, afterAll, beforeAll } from "vitest"; |
8 | 8 | import { ServiceBroker } from "moleculer"; |
9 | 9 | import AgentMixin from "../../src/agent.mixin.ts"; |
| 10 | +import MemoryMixin from "../../src/memory.mixin.ts"; |
10 | 11 | import LLMService from "../../src/llm.service.ts"; |
11 | 12 | import FakeAdapter from "../../src/adapters/fake.ts"; |
12 | 13 |
|
13 | 14 | describe("AgentMixin E2E", () => { |
14 | | - const broker = new ServiceBroker({ logger: false }); |
| 15 | + const broker = new ServiceBroker({ logger: false, cacher: "Memory" }); |
15 | 16 |
|
16 | 17 | beforeAll(() => broker.start()); |
17 | 18 | afterAll(() => broker.stop()); |
@@ -614,4 +615,192 @@ describe("AgentMixin E2E", () => { |
614 | 615 | await broker.destroyService(agentSvc); |
615 | 616 | await broker.destroyService(llmSvc); |
616 | 617 | }); |
| 618 | + |
| 619 | + it("should persist history with MemoryMixin across multi-turn chat", async () => { |
| 620 | + const adapter = new FakeAdapter({ |
| 621 | + responses: ["Hello! How can I help?", "The weather is sunny."] |
| 622 | + }); |
| 623 | + |
| 624 | + const llmSvc = broker.createService({ |
| 625 | + name: "llm.mem1", |
| 626 | + mixins: [LLMService()], |
| 627 | + settings: { adapter } |
| 628 | + }); |
| 629 | + |
| 630 | + const agentSvc = broker.createService({ |
| 631 | + name: "mem-agent-1", |
| 632 | + mixins: [MemoryMixin(), AgentMixin()], |
| 633 | + settings: { |
| 634 | + agent: { |
| 635 | + llm: "llm.mem1", |
| 636 | + description: "Memory agent", |
| 637 | + instructions: "You are a helpful assistant." |
| 638 | + } |
| 639 | + }, |
| 640 | + actions: { |
| 641 | + getCurrent: { |
| 642 | + description: "Get weather", |
| 643 | + params: { city: { type: "string", description: "City" } }, |
| 644 | + async handler() { |
| 645 | + return { temp: 22 }; |
| 646 | + } |
| 647 | + } |
| 648 | + } |
| 649 | + }); |
| 650 | + |
| 651 | + await broker.waitForServices(["llm.mem1", "mem-agent-1"]); |
| 652 | + |
| 653 | + // First chat |
| 654 | + const result1 = await broker.call("mem-agent-1.chat", { |
| 655 | + message: "Hello", |
| 656 | + sessionId: "persist-session" |
| 657 | + }); |
| 658 | + expect(result1).toBe("Hello! How can I help?"); |
| 659 | + |
| 660 | + // Verify history was saved to cacher |
| 661 | + const saved = await broker.cacher!.get("agent:history:mem-agent-1:persist-session"); |
| 662 | + expect(saved).toBeDefined(); |
| 663 | + expect(Array.isArray(saved)).toBe(true); |
| 664 | + // Should contain: system + user("Hello") + assistant("Hello! How can I help?") |
| 665 | + const savedArr = saved as { role: string; content: string }[]; |
| 666 | + expect(savedArr.length).toBe(3); |
| 667 | + expect(savedArr[0].role).toBe("system"); |
| 668 | + expect(savedArr[1].role).toBe("user"); |
| 669 | + expect(savedArr[1].content).toBe("Hello"); |
| 670 | + expect(savedArr[2].role).toBe("assistant"); |
| 671 | + expect(savedArr[2].content).toBe("Hello! How can I help?"); |
| 672 | + |
| 673 | + // Second chat — should load previous history and append |
| 674 | + const result2 = await broker.call("mem-agent-1.chat", { |
| 675 | + message: "What is the weather?", |
| 676 | + sessionId: "persist-session" |
| 677 | + }); |
| 678 | + expect(result2).toBe("The weather is sunny."); |
| 679 | + |
| 680 | + // Verify the full history was saved after the second call |
| 681 | + const saved2 = await broker.cacher!.get("agent:history:mem-agent-1:persist-session"); |
| 682 | + const savedArr2 = saved2 as { role: string; content: string }[]; |
| 683 | + // system + user(Hello) + assistant(Hello!...) + user(Weather?) + assistant(Sunny.) |
| 684 | + expect(savedArr2.length).toBe(5); |
| 685 | + expect(savedArr2[0].role).toBe("system"); |
| 686 | + expect(savedArr2[1].content).toBe("Hello"); |
| 687 | + expect(savedArr2[2].content).toBe("Hello! How can I help?"); |
| 688 | + expect(savedArr2[3].content).toBe("What is the weather?"); |
| 689 | + expect(savedArr2[4].content).toBe("The weather is sunny."); |
| 690 | + |
| 691 | + await broker.destroyService(agentSvc); |
| 692 | + await broker.destroyService(llmSvc); |
| 693 | + }); |
| 694 | + |
| 695 | + it("should trigger compaction with MemoryMixin on long history", async () => { |
| 696 | + const adapter = new FakeAdapter({ responses: ["compacted reply"] }); |
| 697 | + |
| 698 | + const llmSvc = broker.createService({ |
| 699 | + name: "llm.mem2", |
| 700 | + mixins: [LLMService()], |
| 701 | + settings: { adapter } |
| 702 | + }); |
| 703 | + |
| 704 | + // Pre-populate a long history in the cacher |
| 705 | + const longHistory = [ |
| 706 | + { role: "system", content: "instructions" }, |
| 707 | + { role: "user", content: "u1" }, |
| 708 | + { role: "assistant", content: "a1" }, |
| 709 | + { role: "user", content: "u2" }, |
| 710 | + { role: "assistant", content: "a2" }, |
| 711 | + { role: "user", content: "u3" }, |
| 712 | + { role: "assistant", content: "a3" }, |
| 713 | + { role: "user", content: "u4" }, |
| 714 | + { role: "assistant", content: "a4" } |
| 715 | + ]; |
| 716 | + await broker.cacher!.set("agent:history:compact-mem-agent:compact-sess", longHistory, 3600); |
| 717 | + |
| 718 | + const agentSvc = broker.createService({ |
| 719 | + name: "compact-mem-agent", |
| 720 | + mixins: [MemoryMixin(), AgentMixin()], |
| 721 | + settings: { |
| 722 | + agent: { |
| 723 | + llm: "llm.mem2", |
| 724 | + description: "Compact memory agent", |
| 725 | + instructions: "instructions", |
| 726 | + maxHistoryMessages: 5 |
| 727 | + } |
| 728 | + }, |
| 729 | + actions: {} |
| 730 | + }); |
| 731 | + |
| 732 | + await broker.waitForServices(["llm.mem2", "compact-mem-agent"]); |
| 733 | + |
| 734 | + const result = await broker.call("compact-mem-agent.chat", { |
| 735 | + message: "new message", |
| 736 | + sessionId: "compact-sess" |
| 737 | + }); |
| 738 | + expect(result).toBe("compacted reply"); |
| 739 | + |
| 740 | + // Verify saved history was compacted |
| 741 | + // 9 existing + 1 new user msg = 10 > maxHistoryMessages=5 → compaction triggered |
| 742 | + // After compaction: system + last 4 from the 10, then assistant reply appended |
| 743 | + const saved = await broker.cacher!.get("agent:history:compact-mem-agent:compact-sess"); |
| 744 | + const savedArr = saved as { role: string; content: string }[]; |
| 745 | + // Compacted to 5, then +1 assistant = 6, but compaction happens before LLM call |
| 746 | + // so saved result is: compacted(5) + assistant("compacted reply") = 6 |
| 747 | + expect(savedArr.length).toBeLessThanOrEqual(6); |
| 748 | + // System message should be preserved |
| 749 | + expect(savedArr[0].role).toBe("system"); |
| 750 | + expect(savedArr[0].content).toBe("instructions"); |
| 751 | + // Last message should be the assistant reply |
| 752 | + expect(savedArr[savedArr.length - 1].role).toBe("assistant"); |
| 753 | + expect(savedArr[savedArr.length - 1].content).toBe("compacted reply"); |
| 754 | + // The new user message should be in the saved history |
| 755 | + const userMsgs = savedArr.filter(m => m.role === "user" && m.content === "new message"); |
| 756 | + expect(userMsgs.length).toBe(1); |
| 757 | + |
| 758 | + await broker.destroyService(agentSvc); |
| 759 | + await broker.destroyService(llmSvc); |
| 760 | + }); |
| 761 | + |
| 762 | + it("should not duplicate system message on subsequent chats", async () => { |
| 763 | + const adapter = new FakeAdapter({ |
| 764 | + responses: ["first", "second"] |
| 765 | + }); |
| 766 | + |
| 767 | + const llmSvc = broker.createService({ |
| 768 | + name: "llm.mem3", |
| 769 | + mixins: [LLMService()], |
| 770 | + settings: { adapter } |
| 771 | + }); |
| 772 | + |
| 773 | + const agentSvc = broker.createService({ |
| 774 | + name: "sysdup-agent", |
| 775 | + mixins: [MemoryMixin(), AgentMixin()], |
| 776 | + settings: { |
| 777 | + agent: { |
| 778 | + llm: "llm.mem3", |
| 779 | + description: "System dup test agent", |
| 780 | + instructions: "Be helpful." |
| 781 | + } |
| 782 | + }, |
| 783 | + actions: {} |
| 784 | + }); |
| 785 | + |
| 786 | + await broker.waitForServices(["llm.mem3", "sysdup-agent"]); |
| 787 | + |
| 788 | + await broker.call("sysdup-agent.chat", { |
| 789 | + message: "hi", |
| 790 | + sessionId: "sysdup-sess" |
| 791 | + }); |
| 792 | + await broker.call("sysdup-agent.chat", { |
| 793 | + message: "hello again", |
| 794 | + sessionId: "sysdup-sess" |
| 795 | + }); |
| 796 | + |
| 797 | + // Verify the saved history has exactly ONE system message |
| 798 | + const saved = await broker.cacher!.get("agent:history:sysdup-agent:sysdup-sess"); |
| 799 | + const savedArr = saved as { role: string }[]; |
| 800 | + const systemMsgs = savedArr.filter(m => m.role === "system"); |
| 801 | + expect(systemMsgs).toHaveLength(1); |
| 802 | + |
| 803 | + await broker.destroyService(agentSvc); |
| 804 | + await broker.destroyService(llmSvc); |
| 805 | + }); |
617 | 806 | }); |
0 commit comments