refactor(integrations): shared agent plugin runtimes and native adapters (#7203)
This commit is contained in:
@@ -25,15 +25,28 @@ interface RegisteredTool {
|
||||
execute(args: unknown, exec: unknown): Promise<unknown>;
|
||||
}
|
||||
|
||||
type HarnessListener = (...args: any[]) => unknown;
|
||||
|
||||
function applyAndCollect(config: Config): Map<string, RegisteredTool> {
|
||||
const tools = new Map<string, RegisteredTool>();
|
||||
const ctx = {
|
||||
tools: { register: (t: RegisteredTool) => tools.set(t.name, t) },
|
||||
on: vi.fn(),
|
||||
};
|
||||
apply(ctx as never, config);
|
||||
return tools;
|
||||
}
|
||||
|
||||
function applyAndCollectListeners(config: Config): Map<string, HarnessListener> {
|
||||
const listeners = new Map<string, HarnessListener>();
|
||||
const ctx = {
|
||||
tools: { register: vi.fn() },
|
||||
on: (event: string, listener: HarnessListener) => listeners.set(event, listener),
|
||||
};
|
||||
apply(ctx as never, config);
|
||||
return listeners;
|
||||
}
|
||||
|
||||
let savedKey: string | undefined;
|
||||
let savedTelemetry: string | undefined;
|
||||
|
||||
@@ -68,6 +81,97 @@ describe("apply() config validation", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("Harness lifecycle", () => {
|
||||
it("automatically recalls memory into the prompt for the latest human message", async () => {
|
||||
mockSearch.mockResolvedValue({
|
||||
results: [{ id: "m1", memory: "Likes tea" }],
|
||||
});
|
||||
const listeners = applyAndCollectListeners({ apiKey: "k", userId: "u" });
|
||||
const assemble = listeners.get("system-prompt/assemble")!;
|
||||
const base = { sections: [], contexts: [], tools: [], variables: {} };
|
||||
|
||||
const result = await assemble(
|
||||
base,
|
||||
{
|
||||
agent: {
|
||||
session: {
|
||||
deriveMessages: () => [
|
||||
{
|
||||
role: "user",
|
||||
content: [{ type: "text", text: "What do I drink?" }],
|
||||
source: { kind: "user" },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
async () => base,
|
||||
);
|
||||
|
||||
expect(mockSearch).toHaveBeenCalledWith("What do I drink?", {
|
||||
filters: { user_id: "u" },
|
||||
topK: 5,
|
||||
});
|
||||
expect(result).toMatchObject({
|
||||
contexts: [{ name: "mem0:recall", text: expect.stringContaining("Likes tea") }],
|
||||
});
|
||||
});
|
||||
|
||||
it("automatically captures a completed human and assistant turn", async () => {
|
||||
mockAdd.mockResolvedValue({ eventId: "evt-1", status: "PENDING" });
|
||||
const listeners = applyAndCollectListeners({ apiKey: "k", userId: "u" });
|
||||
const onSessionEvent = listeners.get("session/event")!;
|
||||
const session = {};
|
||||
|
||||
onSessionEvent(session, { type: "turn/start", data: { turn: 1 } });
|
||||
onSessionEvent(session, {
|
||||
type: "user/message",
|
||||
data: {
|
||||
role: "user",
|
||||
content: [{ type: "text", text: "Remember I like tea" }],
|
||||
source: { kind: "user" },
|
||||
},
|
||||
});
|
||||
onSessionEvent(session, {
|
||||
type: "assistant/message",
|
||||
data: {
|
||||
turn: 1,
|
||||
message: {
|
||||
role: "assistant",
|
||||
content: [{ type: "text", text: "I will remember that." }],
|
||||
source: { kind: "model" },
|
||||
},
|
||||
},
|
||||
});
|
||||
onSessionEvent(session, {
|
||||
type: "turn/end",
|
||||
data: { turn: 1, reason: { kind: "completed" } },
|
||||
});
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(mockAdd).toHaveBeenCalledWith(
|
||||
[
|
||||
{ role: "user", content: "Remember I like tea" },
|
||||
{ role: "assistant", content: "I will remember that." },
|
||||
],
|
||||
{ userId: "u", source: "DEEPSEEK_HARNESS" },
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("can disable automatic recall and capture without removing the memory tools", () => {
|
||||
const listeners = applyAndCollectListeners({
|
||||
apiKey: "k",
|
||||
userId: "u",
|
||||
autoRecall: false,
|
||||
autoCapture: false,
|
||||
});
|
||||
|
||||
expect(listeners.has("system-prompt/assemble")).toBe(false);
|
||||
expect(listeners.has("session/event")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("search_memory tool", () => {
|
||||
it("returns a formatted list scoped to the configured user", async () => {
|
||||
mockSearch.mockResolvedValue({
|
||||
@@ -87,7 +191,7 @@ describe("search_memory tool", () => {
|
||||
|
||||
it("honors a per-call userId override and limit", async () => {
|
||||
mockSearch.mockResolvedValue({ results: [] });
|
||||
const tools = applyAndCollect({ apiKey: "k", userId: "u" });
|
||||
const tools = applyAndCollect({ apiKey: "k", userId: "u", allowUserOverride: true });
|
||||
|
||||
await tools.get("search_memory")!.execute({ query: "x", userId: "alice", limit: 3 }, {});
|
||||
|
||||
@@ -136,6 +240,18 @@ describe("add_memory tool", () => {
|
||||
expect(out).toContain("[mem0:m1]");
|
||||
});
|
||||
|
||||
it("redacts credentials before storing explicit memory", async () => {
|
||||
mockAdd.mockResolvedValue([]);
|
||||
const tools = applyAndCollect({ apiKey: "k", userId: "u" });
|
||||
|
||||
await tools.get("add_memory")!.execute({ text: "api_key=do-not-store-this" }, {});
|
||||
|
||||
expect(mockAdd).toHaveBeenCalledWith(
|
||||
[{ role: "user", content: "api_key=[REDACTED]" }],
|
||||
{ userId: "u", source: "DEEPSEEK_HARNESS" },
|
||||
);
|
||||
});
|
||||
|
||||
it("returns a graceful failure line on error", async () => {
|
||||
mockAdd.mockRejectedValue(new Error("boom"));
|
||||
const tools = applyAndCollect({ apiKey: "k", userId: "u" });
|
||||
@@ -146,3 +262,13 @@ describe("add_memory tool", () => {
|
||||
expect(out).toContain("boom");
|
||||
});
|
||||
});
|
||||
|
||||
describe("tool user ownership", () => {
|
||||
it("rejects cross-user reads and writes before contacting Mem0", async () => {
|
||||
const tools = applyAndCollect({ apiKey: "k", userId: "u" });
|
||||
await expect(tools.get("search_memory")!.execute({ query: "x", userId: "other" }, {})).rejects.toThrow(/allowUserOverride/);
|
||||
await expect(tools.get("add_memory")!.execute({ text: "x", userId: "other" }, {})).rejects.toThrow(/allowUserOverride/);
|
||||
expect(mockSearch).not.toHaveBeenCalled();
|
||||
expect(mockAdd).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -31,7 +31,10 @@ interface RegisteredTool {
|
||||
|
||||
function applyAndCollect(config: Config): Map<string, RegisteredTool> {
|
||||
const tools = new Map<string, RegisteredTool>();
|
||||
apply({ tools: { register: (t: RegisteredTool) => tools.set(t.name, t) } } as never, config);
|
||||
apply({
|
||||
tools: { register: (t: RegisteredTool) => tools.set(t.name, t) },
|
||||
on: vi.fn(),
|
||||
} as never, config);
|
||||
return tools;
|
||||
}
|
||||
|
||||
@@ -190,7 +193,7 @@ describe("tool instrumentation", () => {
|
||||
|
||||
it("records a write with its size but not its text", async () => {
|
||||
mockAdd.mockResolvedValue([{ id: "m1", memory: "Fact" }]);
|
||||
const tools = applyAndCollect({ apiKey: "k", userId: "u" });
|
||||
const tools = applyAndCollect({ apiKey: "k", userId: "u", allowUserOverride: true });
|
||||
|
||||
await tools.get("add_memory")!.execute({ text: "secret fact", userId: "alice" }, {});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user