Skip to content

Commit e6da968

Browse files
committed
Add missing src/index.ts, MemoryMixin integration tests, smoke tests
1 parent a8a6ea6 commit e6da968

3 files changed

Lines changed: 394 additions & 1 deletion

File tree

src/index.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/*
2+
* @moleculer/agents
3+
* Copyright (c) 2026 MoleculerJS (https://github.com/moleculerjs/agents)
4+
* MIT Licensed
5+
*/
6+
7+
export { default as AgentMixin } from "./agent.mixin.ts";
8+
export { default as MemoryMixin } from "./memory.mixin.ts";
9+
export { default as LLMService } from "./llm.service.ts";
10+
export { default as Adapters } from "./adapters/index.ts";
11+
export type { LLMResponse, ToolCall, AgentSettings, ToolSchema } from "./types.ts";

test/integration/agent-flow.test.ts

Lines changed: 190 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,12 @@
77
import { describe, expect, it, afterAll, beforeAll } from "vitest";
88
import { ServiceBroker } from "moleculer";
99
import AgentMixin from "../../src/agent.mixin.ts";
10+
import MemoryMixin from "../../src/memory.mixin.ts";
1011
import LLMService from "../../src/llm.service.ts";
1112
import FakeAdapter from "../../src/adapters/fake.ts";
1213

1314
describe("AgentMixin E2E", () => {
14-
const broker = new ServiceBroker({ logger: false });
15+
const broker = new ServiceBroker({ logger: false, cacher: "Memory" });
1516

1617
beforeAll(() => broker.start());
1718
afterAll(() => broker.stop());
@@ -614,4 +615,192 @@ describe("AgentMixin E2E", () => {
614615
await broker.destroyService(agentSvc);
615616
await broker.destroyService(llmSvc);
616617
});
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+
});
617806
});

0 commit comments

Comments
 (0)