feat(context-engine): add memory prompt helper

This commit is contained in:
Vincent Koc
2026-04-07 08:28:39 +01:00
parent 6a559f0293
commit 2988203a5e
13 changed files with 116 additions and 9 deletions

View File

@@ -23,6 +23,7 @@ Docs: https://docs.openclaw.ai
- Memory/wiki: add claim-health linting, contradiction clustering, staleness-aware dashboards, and freshness-weighted wiki search so `memory-wiki` can act more like a maintained belief layer than a passive markdown dump. Thanks @vincentkoc.
- Memory/wiki: use compiled digest artifacts as the first-pass wiki index for search/get flows, and resolve claim ids back to owning pages so agents can retrieve knowledge by belief identity instead of only by file path. Thanks @vincentkoc.
- Memory/wiki: add an opt-in `context.includeCompiledDigestPrompt` flag so memory prompt supplements can append a compact compiled wiki snapshot for legacy prompt assembly and context engines that explicitly consume memory prompt sections. Thanks @vincentkoc.
- Plugin SDK/context engines: pass `availableTools` and `citationsMode` into `assemble()`, and expose `buildMemorySystemPromptAddition(...)` so non-legacy context engines can adopt the active memory prompt path without reimplementing it. Thanks @vincentkoc.
### Fixes

View File

@@ -1,2 +1,2 @@
8592ce4554f44cd5325f44f86d37b3057548853cc9bc543b0611e5d9000f262e plugin-sdk-api-baseline.json
35393eceb6733d966430369c116511bb15a4b83eec93ebfd3b9a3e8f9ee29cec plugin-sdk-api-baseline.jsonl
5bee25156a7699938f2a25419ca44e35113cf6fbd6f533052af68712fc992629 plugin-sdk-api-baseline.json
f80212552c63be134a2ae456ce2cd4f81507c1569b60493b1d5f965dc2969041 plugin-sdk-api-baseline.jsonl

View File

@@ -115,6 +115,8 @@ engine is used automatically.
A plugin can register a context engine using the plugin API:
```ts
import { buildMemorySystemPromptAddition } from "openclaw/plugin-sdk/core";
export default function register(api) {
api.registerContextEngine("my-engine", () => ({
info: {
@@ -128,12 +130,15 @@ export default function register(api) {
return { ingested: true };
},
async assemble({ sessionId, messages, tokenBudget }) {
async assemble({ sessionId, messages, tokenBudget, availableTools, citationsMode }) {
// Return messages that fit the budget
return {
messages: buildContext(messages, tokenBudget),
estimatedTokens: countTokens(messages),
systemPromptAddition: "Use lcm_grep to search history...",
systemPromptAddition: buildMemorySystemPromptAddition({
availableTools: availableTools ?? new Set(),
citationsMode,
}),
};
},
@@ -249,7 +254,10 @@ OpenClaw resolves when it needs a context engine.
Memory plugins provide search/retrieval; context engines control what the
model sees. They can work together — a context engine might use memory
plugin data during assembly. Plugin engines that want the active memory
plugin's legacy prompt guidance can pull it explicitly from
prompt path should prefer `buildMemorySystemPromptAddition(...)` from
`openclaw/plugin-sdk/core`, which converts the active memory prompt sections
into a ready-to-prepend `systemPromptAddition`. If an engine needs lower-level
control, it can still pull raw lines from
`openclaw/plugin-sdk/memory-host-core` via
`buildActiveMemoryPromptSection(...)`.
- **Session pruning** (trimming old tool results in-memory) still runs

View File

@@ -1,4 +1,5 @@
import type { AgentMessage } from "@mariozechner/pi-agent-core";
import type { MemoryCitationsMode } from "../../../config/types.memory.js";
import type { ContextEngine, ContextEngineRuntimeContext } from "../../../context-engine/types.js";
export type AttemptContextEngine = ContextEngine;
@@ -56,6 +57,8 @@ export async function assembleAttemptContextEngine(params: {
sessionKey?: string;
messages: AgentMessage[];
tokenBudget?: number;
availableTools?: Set<string>;
citationsMode?: MemoryCitationsMode;
modelId: string;
prompt?: string;
}) {
@@ -67,6 +70,8 @@ export async function assembleAttemptContextEngine(params: {
sessionKey: params.sessionKey,
messages: params.messages,
tokenBudget: params.tokenBudget,
...(params.availableTools ? { availableTools: params.availableTools } : {}),
...(params.citationsMode ? { citationsMode: params.citationsMode } : {}),
model: params.modelId,
...(params.prompt !== undefined ? { prompt: params.prompt } : {}),
});

View File

@@ -145,6 +145,24 @@ describe("runEmbeddedAttempt context engine sessionKey forwarding", () => {
);
});
it("forwards availableTools and citationsMode to assemble", async () => {
const { bootstrap, assemble } = createContextEngineBootstrapAndAssemble();
const contextEngine = createTestContextEngine({ bootstrap, assemble });
await runBootstrap(sessionKey, contextEngine);
await runAssemble(sessionKey, contextEngine, {
availableTools: new Set(["memory_search", "wiki_search"]),
citationsMode: "on",
});
expect(assemble).toHaveBeenCalledWith(
expect.objectContaining({
availableTools: new Set(["memory_search", "wiki_search"]),
citationsMode: "on",
}),
);
});
it("forwards sessionKey to ingestBatch when afterTurn is absent", async () => {
const { bootstrap, assemble } = createContextEngineBootstrapAndAssemble();
const ingestBatch = vi.fn(

View File

@@ -1271,6 +1271,8 @@ export async function runEmbeddedAttempt(
sessionKey: params.sessionKey,
messages: activeSession.messages,
tokenBudget: params.contextTokenBudget,
availableTools: new Set(effectiveTools.map((tool) => tool.name)),
citationsMode: params.config?.memory?.citations,
modelId: params.modelId,
...(params.prompt !== undefined ? { prompt: params.prompt } : {}),
});

View File

@@ -1,11 +1,13 @@
import type { AgentMessage } from "@mariozechner/pi-agent-core";
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { MemoryCitationsMode } from "../config/types.memory.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { clearMemoryPluginState, registerMemoryPromptSection } from "../plugins/memory-state.js";
// ---------------------------------------------------------------------------
// We dynamically import the registry so we can get a fresh module per test
// group when needed. For most groups we use the shared singleton directly.
// ---------------------------------------------------------------------------
import { delegateCompactionToRuntime } from "./delegate.js";
import { buildMemorySystemPromptAddition, delegateCompactionToRuntime } from "./delegate.js";
import { LegacyContextEngine, registerLegacyContextEngine } from "./legacy.js";
import {
registerContextEngine,
@@ -100,6 +102,8 @@ class MockContextEngine implements ContextEngine {
sessionKey?: string;
messages: AgentMessage[];
tokenBudget?: number;
availableTools?: Set<string>;
citationsMode?: MemoryCitationsMode;
}): Promise<AssembleResult> {
return {
messages: params.messages,
@@ -168,6 +172,8 @@ class LegacySessionKeyStrictEngine implements ContextEngine {
sessionKey?: string;
messages: AgentMessage[];
tokenBudget?: number;
availableTools?: Set<string>;
citationsMode?: MemoryCitationsMode;
prompt?: string;
}): Promise<AssembleResult> {
this.assembleCalls.push({ ...params });
@@ -279,6 +285,8 @@ class LegacyAssembleStrictEngine implements ContextEngine {
sessionKey?: string;
messages: AgentMessage[];
tokenBudget?: number;
availableTools?: Set<string>;
citationsMode?: MemoryCitationsMode;
prompt?: string;
}): Promise<AssembleResult> {
this.assembleCalls.push({ ...params });
@@ -318,6 +326,7 @@ describe("Engine contract tests", () => {
beforeEach(() => {
vi.restoreAllMocks();
compactEmbeddedPiSessionDirectMock.mockReset();
clearMemoryPluginState();
});
it("a mock engine implementing ContextEngine can be registered and resolved", async () => {
@@ -386,6 +395,29 @@ describe("Engine contract tests", () => {
},
});
});
it("builds a normalized memory system prompt addition from the active memory prompt path", () => {
registerMemoryPromptSection(({ citationsMode }) => [
"## Memory Recall",
`citations=${citationsMode ?? "auto"}`,
"",
]);
expect(
buildMemorySystemPromptAddition({
availableTools: new Set(["memory_search"]),
citationsMode: "off",
}),
).toBe("## Memory Recall\ncitations=off");
});
it("returns undefined when the active memory prompt path contributes nothing", () => {
expect(
buildMemorySystemPromptAddition({
availableTools: new Set(["memory_search"]),
}),
).toBeUndefined();
});
});
// ═══════════════════════════════════════════════════════════════════════════

View File

@@ -1,3 +1,6 @@
import { normalizeStructuredPromptSection } from "../agents/prompt-cache-stability.js";
import type { MemoryCitationsMode } from "../config/types.memory.js";
import { buildMemoryPromptSection } from "../plugins/memory-state.js";
import type { ContextEngine, CompactResult, ContextEngineRuntimeContext } from "./types.js";
/**
@@ -61,3 +64,24 @@ export async function delegateCompactionToRuntime(
: undefined,
};
}
/**
* Build a context-engine-ready systemPromptAddition from the active memory
* plugin prompt path. This lets non-legacy engines explicitly opt into the
* same memory/wiki guidance that the legacy engine gets via system prompt
* assembly, without reimplementing memory prompt formatting.
*/
export function buildMemorySystemPromptAddition(params: {
availableTools: Set<string>;
citationsMode?: MemoryCitationsMode;
}): string | undefined {
const lines = buildMemoryPromptSection({
availableTools: params.availableTools,
citationsMode: params.citationsMode,
});
if (lines.length === 0) {
return undefined;
}
const normalized = normalizeStructuredPromptSection(lines.join("\n"));
return normalized || undefined;
}

View File

@@ -1,4 +1,5 @@
import type { AgentMessage } from "@mariozechner/pi-agent-core";
import type { MemoryCitationsMode } from "../config/types.memory.js";
import { delegateCompactionToRuntime } from "./delegate.js";
import { registerContextEngineForOwner } from "./registry.js";
import type {
@@ -40,6 +41,8 @@ export class LegacyContextEngine implements ContextEngine {
sessionKey?: string;
messages: AgentMessage[];
tokenBudget?: number;
availableTools?: Set<string>;
citationsMode?: MemoryCitationsMode;
model?: string;
}): Promise<AssembleResult> {
// Pass-through: the existing sanitize -> validate -> limit -> repair pipeline

View File

@@ -1,4 +1,5 @@
import type { AgentMessage } from "@mariozechner/pi-agent-core";
import type { MemoryCitationsMode } from "../config/types.memory.js";
// Result types
@@ -180,6 +181,10 @@ export interface ContextEngine {
sessionKey?: string;
messages: AgentMessage[];
tokenBudget?: number;
/** Tool names available for this run so engines can align prompt guidance with runtime tool access. */
availableTools?: Set<string>;
/** Active memory citation mode when engines want to mirror memory prompt guidance. */
citationsMode?: MemoryCitationsMode;
/** Current model identifier (e.g. "claude-opus-4", "gpt-4o", "qwen2.5-7b").
* Allows context engine plugins to adapt formatting per model. */
model?: string;

View File

@@ -24,7 +24,10 @@ export type {
MemoryPluginPublicArtifactsProvider,
} from "../plugins/memory-state.js";
export { resolveControlCommandGate } from "../channels/command-gating.js";
export { delegateCompactionToRuntime } from "../context-engine/delegate.js";
export {
buildMemorySystemPromptAddition,
delegateCompactionToRuntime,
} from "../context-engine/delegate.js";
export type { DiagnosticEventPayload } from "../infra/diagnostic-events.js";
export { onDiagnosticEvent } from "../infra/diagnostic-events.js";
export {

View File

@@ -151,7 +151,10 @@ export { buildPluginConfigSchema, emptyPluginConfigSchema } from "../plugins/con
export { KeyedAsyncQueue, enqueueKeyedTask } from "./keyed-async-queue.js";
export { createDedupeCache, resolveGlobalDedupeCache } from "../infra/dedupe.js";
export { generateSecureToken, generateSecureUuid } from "../infra/secure-random.js";
export { delegateCompactionToRuntime } from "../context-engine/delegate.js";
export {
buildMemorySystemPromptAddition,
delegateCompactionToRuntime,
} from "../context-engine/delegate.js";
export { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../routing/session-key.js";
export {
buildChannelConfigSchema,

View File

@@ -112,5 +112,8 @@ export type {
export { emptyPluginConfigSchema } from "../plugins/config-schema.js";
export { registerContextEngine } from "../context-engine/registry.js";
export { delegateCompactionToRuntime } from "../context-engine/delegate.js";
export {
buildMemorySystemPromptAddition,
delegateCompactionToRuntime,
} from "../context-engine/delegate.js";
export { onDiagnosticEvent } from "../infra/diagnostic-events.js";