feat(memory): delegate system prompt section to active memory plugin

Add registerMemoryPromptSection to the plugin API so the active memory
plugin provides its own system prompt builder callback. The built-in
memory-core plugin registers the existing Memory Recall section via
this new API.

When no memory plugin is active (or a plugin does not register a
builder), the memory section is simply omitted from the system prompt.
This lets third-party memory plugins inject their own guidance at the
same position in the prompt.
This commit is contained in:
Jari Mustonen 2026-03-08 11:30:34 +02:00 committed by Josh Lehman
parent 4c60956d8e
commit 02222b93c5
No known key found for this signature in database
GPG Key ID: D141B425AC7F876B
8 changed files with 117 additions and 18 deletions

View File

@ -54,6 +54,7 @@ function fakeApi(overrides: Partial<OpenClawPluginApi> = {}): OpenClawPluginApi
registerHttpRoute() {},
registerCommand() {},
registerContextEngine() {},
registerMemoryPromptSection() {},
on() {},
resolvePath: (p) => p,
...overrides,

View File

@ -1,11 +1,35 @@
import type { MemoryPromptSectionBuilder } from "openclaw/plugin-sdk/memory-core";
import { definePluginEntry } from "openclaw/plugin-sdk/core";
const buildPromptSection: MemoryPromptSectionBuilder = ({ availableTools, citationsMode }) => {
if (!availableTools.has("memory_search") && !availableTools.has("memory_get")) {
return [];
}
const lines = [
"## Memory Recall",
"Before answering anything about prior work, decisions, dates, people, preferences, or todos: run memory_search on MEMORY.md + memory/*.md; then use memory_get to pull only the needed lines. If low confidence after search, say you checked.",
];
if (citationsMode === "off") {
lines.push(
"Citations are disabled: do not mention file paths or line numbers in replies unless the user explicitly asks.",
);
} else {
lines.push(
"Citations: include Source: <path#line> when it helps the user verify memory snippets.",
);
}
lines.push("");
return lines;
};
export default definePluginEntry({
id: "memory-core",
name: "Memory (Core)",
description: "File-backed memory search tools and CLI",
kind: "memory",
register(api) {
api.registerMemoryPromptSection(buildPromptSection);
api.registerTool(
(ctx) => {
const memorySearchTool = api.runtime.tools.createMemorySearchTool({

View File

@ -2,6 +2,7 @@ import { createHmac, createHash } from "node:crypto";
import type { ReasoningLevel, ThinkLevel } from "../auto-reply/thinking.js";
import { SILENT_REPLY_TOKEN } from "../auto-reply/tokens.js";
import type { MemoryCitationsMode } from "../config/types.memory.js";
import { buildMemoryPromptSection } from "../memory/prompt-section.js";
import { listDeliverableMessageChannels } from "../utils/message-channel.js";
import type { ResolvedTimeFormat } from "./date-time.js";
import type { EmbeddedContextFile } from "./pi-embedded-helpers.js";
@ -43,24 +44,10 @@ function buildMemorySection(params: {
if (params.isMinimal) {
return [];
}
if (!params.availableTools.has("memory_search") && !params.availableTools.has("memory_get")) {
return [];
}
const lines = [
"## Memory Recall",
"Before answering anything about prior work, decisions, dates, people, preferences, or todos: run memory_search on MEMORY.md + memory/*.md; then use memory_get to pull only the needed lines. If low confidence after search, say you checked.",
];
if (params.citationsMode === "off") {
lines.push(
"Citations are disabled: do not mention file paths or line numbers in replies unless the user explicitly asks.",
);
} else {
lines.push(
"Citations: include Source: <path#line> when it helps the user verify memory snippets.",
);
}
lines.push("");
return lines;
return buildMemoryPromptSection({
availableTools: params.availableTools,
citationsMode: params.citationsMode,
});
}
function buildUserIdentitySection(ownerLine: string | undefined, isMinimal: boolean) {

View File

@ -0,0 +1,52 @@
import { describe, it, expect, beforeEach } from "vitest";
import {
registerMemoryPromptSection,
buildMemoryPromptSection,
_resetMemoryPromptSection,
} from "./prompt-section.js";
describe("memory prompt section registry", () => {
beforeEach(() => {
_resetMemoryPromptSection();
});
it("returns empty array when no builder is registered", () => {
const result = buildMemoryPromptSection({
availableTools: new Set(["memory_search", "memory_get"]),
});
expect(result).toEqual([]);
});
it("delegates to the registered builder", () => {
registerMemoryPromptSection(({ availableTools }) => {
if (!availableTools.has("memory_search")) return [];
return ["## Custom Memory", "Use custom memory tools.", ""];
});
const result = buildMemoryPromptSection({
availableTools: new Set(["memory_search"]),
});
expect(result).toEqual(["## Custom Memory", "Use custom memory tools.", ""]);
});
it("passes citationsMode to the builder", () => {
registerMemoryPromptSection(({ citationsMode }) => {
return [`citations: ${citationsMode ?? "default"}`];
});
expect(
buildMemoryPromptSection({
availableTools: new Set(),
citationsMode: "off",
}),
).toEqual(["citations: off"]);
});
it("last registration wins", () => {
registerMemoryPromptSection(() => ["first"]);
registerMemoryPromptSection(() => ["second"]);
const result = buildMemoryPromptSection({ availableTools: new Set() });
expect(result).toEqual(["second"]);
});
});

View File

@ -0,0 +1,29 @@
import type { MemoryCitationsMode } from "../config/types.memory.js";
/**
* Callback that the active memory plugin provides to build
* its section of the agent system prompt.
*/
export type MemoryPromptSectionBuilder = (params: {
availableTools: Set<string>;
citationsMode?: MemoryCitationsMode;
}) => string[];
// Module-level singleton — only one memory plugin can be active (exclusive slot).
let _builder: MemoryPromptSectionBuilder | undefined;
export function registerMemoryPromptSection(builder: MemoryPromptSectionBuilder): void {
_builder = builder;
}
export function buildMemoryPromptSection(params: {
availableTools: Set<string>;
citationsMode?: MemoryCitationsMode;
}): string[] {
return _builder?.(params) ?? [];
}
/** Reset state (for tests). */
export function _resetMemoryPromptSection(): void {
_builder = undefined;
}

View File

@ -2,4 +2,5 @@
// Keep this list additive and scoped to symbols used under extensions/memory-core.
export { emptyPluginConfigSchema } from "../plugins/config-schema.js";
export type { MemoryPromptSectionBuilder } from "../memory/prompt-section.js";
export type { OpenClawPluginApi } from "../plugins/types.js";

View File

@ -1341,6 +1341,10 @@ export type OpenClawPluginApi = {
id: string,
factory: import("../context-engine/registry.js").ContextEngineFactory,
) => void;
/** Register the system prompt section builder for this memory plugin (exclusive slot). */
registerMemoryPromptSection: (
builder: import("../memory/prompt-section.js").MemoryPromptSectionBuilder,
) => void;
resolvePath: (input: string) => string;
/** Register a lifecycle hook handler */
on: <K extends PluginHookName>(

View File

@ -23,6 +23,7 @@ export function createTestPluginApi(api: TestPluginApiInput): OpenClawPluginApi
onConversationBindingResolved() {},
registerCommand() {},
registerContextEngine() {},
registerMemoryPromptSection() {},
resolvePath(input: string) {
return input;
},