fix(hooks): expose sessionKey and agentId in agent_end and before_agent_start events

This fix allows memory plugins (like memos-local) to properly identify
which agent is running when capturing conversation history.

Previously, agent_end and before_agent_start hooks only received
generic event data without session context. Memory plugins could not
distinguish between different agents (e.g., 'main' vs 'qian-duoduo'),
causing all memories to be tagged with the wrong owner.

Changes:
- Add sessionKey and agentId fields to PluginHookAgentEndEvent
- Add sessionKey and agentId fields to PluginHookBeforeAgentStartEvent
- Pass sessionKey and agentId when triggering agent_end hook
- Pass sessionKey and agentId when triggering before_agent_start hook

Fixes context issue where multi-agent setups could not properly
isolate per-agent memory.
This commit is contained in:
MemOS Fix 2026-03-12 19:34:58 +00:00 committed by coderzc
parent 9fb78453e0
commit a0d031e30a
4 changed files with 181 additions and 2 deletions

View File

@ -348,7 +348,11 @@ export async function runEmbeddedPiAgent(
if (hookRunner?.hasHooks("before_agent_start")) {
try {
legacyBeforeAgentStartResult = await hookRunner.runBeforeAgentStart(
{ prompt: params.prompt },
{
prompt: params.prompt,
sessionKey: params.sessionKey,
agentId: workspaceResolution.agentId,
},
hookCtx,
);
modelResolveOverride = {

View File

@ -157,7 +157,7 @@ type PromptBuildHookRunner = {
ctx: PluginHookAgentContext,
) => Promise<PluginHookBeforePromptBuildResult | undefined>;
runBeforeAgentStart: (
event: { prompt: string; messages: unknown[] },
event: { prompt: string; messages?: unknown[]; sessionKey?: string; agentId?: string },
ctx: PluginHookAgentContext,
) => Promise<PluginHookBeforeAgentStartResult | undefined>;
};
@ -1439,6 +1439,8 @@ export async function resolvePromptBuildHookResult(params: {
{
prompt: params.prompt,
messages: params.messages,
sessionKey: params.hookCtx.sessionKey,
agentId: params.hookCtx.agentId,
},
params.hookCtx,
)
@ -3078,6 +3080,8 @@ export async function runEmbeddedAttempt(
success: !aborted && !promptError,
error: promptError ? describeUnknownError(promptError) : undefined,
durationMs: Date.now() - promptStartedAt,
sessionKey: params.sessionKey,
agentId: hookAgentId,
},
{
agentId: hookAgentId,

View File

@ -0,0 +1,163 @@
/**
* Tests for agent context (sessionKey, agentId) in hook events
*
* Validates that sessionKey and agentId are correctly passed to
* agent_end and before_agent_start hook handlers.
*/
import { beforeEach, describe, expect, it, vi } from "vitest";
import { createHookRunner } from "./hooks.js";
import { addTestHook, TEST_PLUGIN_AGENT_CTX } from "./hooks.test-helpers.js";
import { createEmptyPluginRegistry, type PluginRegistry } from "./registry.js";
import type { PluginHookRegistration } from "./types.js";
function addAgentEndHook(
registry: PluginRegistry,
pluginId: string,
handler: (event: { messages: unknown[]; success: boolean; sessionKey?: string; agentId?: string }) => void | Promise<void>,
priority?: number,
) {
addTestHook({
registry,
pluginId,
hookName: "agent_end",
handler: handler as PluginHookRegistration["handler"],
priority,
});
}
function addBeforeAgentStartHook(
registry: PluginRegistry,
pluginId: string,
handler: (event: { prompt: string; messages?: unknown[]; sessionKey?: string; agentId?: string }) => void | Promise<void>,
priority?: number,
) {
addTestHook({
registry,
pluginId,
hookName: "before_agent_start",
handler: handler as PluginHookRegistration["handler"],
priority,
});
}
describe("hook events include sessionKey and agentId", () => {
let registry: PluginRegistry;
const stubCtx = TEST_PLUGIN_AGENT_CTX;
beforeEach(() => {
registry = createEmptyPluginRegistry();
});
describe("agent_end hook", () => {
it("receives sessionKey and agentId in event", async () => {
const handler = vi.fn().mockResolvedValue(undefined);
addAgentEndHook(registry, "memory-plugin", handler);
const runner = createHookRunner(registry);
const testMessages = [{ role: "user", content: "hello" }];
await runner.runAgentEnd(
{
messages: testMessages,
success: true,
sessionKey: "agent:assistant-beta:main",
agentId: "assistant-beta",
},
stubCtx,
);
expect(handler).toHaveBeenCalledWith(
expect.objectContaining({
messages: testMessages,
success: true,
sessionKey: "agent:assistant-beta:main",
agentId: "assistant-beta",
}),
stubCtx,
);
});
it("works with default values when sessionKey and agentId are not provided", async () => {
const handler = vi.fn().mockResolvedValue(undefined);
addAgentEndHook(registry, "memory-plugin", handler);
const runner = createHookRunner(registry);
const testMessages = [{ role: "user", content: "hello" }];
// Call without sessionKey and agentId (backward compatibility)
await runner.runAgentEnd(
{
messages: testMessages,
success: true,
},
stubCtx,
);
expect(handler).toHaveBeenCalledWith(
expect.objectContaining({
messages: testMessages,
success: true,
sessionKey: undefined,
agentId: undefined,
}),
stubCtx,
);
});
});
describe("before_agent_start hook", () => {
it("receives sessionKey and agentId in event", async () => {
const handler = vi.fn().mockResolvedValue(undefined);
addBeforeAgentStartHook(registry, "memory-plugin", handler);
const runner = createHookRunner(registry);
await runner.runBeforeAgentStart(
{
prompt: "hello",
messages: [{ role: "user", content: "hello" }],
sessionKey: "agent:assistant-beta:main",
agentId: "assistant-beta",
},
stubCtx,
);
expect(handler).toHaveBeenCalledWith(
expect.objectContaining({
prompt: "hello",
sessionKey: "agent:assistant-beta:main",
agentId: "assistant-beta",
}),
stubCtx,
);
});
it("works with legacy event shape (backward compatibility)", async () => {
const handler = vi.fn().mockResolvedValue({ prependContext: "context" });
addBeforeAgentStartHook(registry, "legacy-plugin", handler);
const runner = createHookRunner(registry);
// Call with legacy event shape (without sessionKey and agentId)
await runner.runBeforeAgentStart(
{
prompt: "hello",
},
stubCtx,
);
expect(handler).toHaveBeenCalledWith(
expect.objectContaining({
prompt: "hello",
sessionKey: undefined,
agentId: undefined,
}),
stubCtx,
);
});
});
});

View File

@ -1513,6 +1513,10 @@ export type PluginHookBeforeAgentStartEvent = {
prompt: string;
/** Optional because legacy hook can run in pre-session phase. */
messages?: unknown[];
/** Session key for this agent run (e.g., "agent:qian-duoduo:main") */
sessionKey?: string;
/** Agent ID for this run (e.g., "qian-duoduo") */
agentId?: string;
};
export type PluginHookBeforeAgentStartResult = PluginHookBeforePromptBuildResult &
@ -1573,6 +1577,10 @@ export type PluginHookAgentEndEvent = {
success: boolean;
error?: string;
durationMs?: number;
/** Session key for this agent run (e.g., "agent:qian-duoduo:main") */
sessionKey?: string;
/** Agent ID for this run (e.g., "qian-duoduo") */
agentId?: string;
};
// Compaction hooks