diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a78e3645eb..5ba06215e87 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/docs/.generated/plugin-sdk-api-baseline.sha256 b/docs/.generated/plugin-sdk-api-baseline.sha256 index 258f9e38b31..d04608d92a0 100644 --- a/docs/.generated/plugin-sdk-api-baseline.sha256 +++ b/docs/.generated/plugin-sdk-api-baseline.sha256 @@ -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 diff --git a/docs/concepts/context-engine.md b/docs/concepts/context-engine.md index 6778d561001..1e75e6e4e15 100644 --- a/docs/concepts/context-engine.md +++ b/docs/concepts/context-engine.md @@ -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 diff --git a/src/agents/pi-embedded-runner/run/attempt.context-engine-helpers.ts b/src/agents/pi-embedded-runner/run/attempt.context-engine-helpers.ts index 280dc3df336..77129f90618 100644 --- a/src/agents/pi-embedded-runner/run/attempt.context-engine-helpers.ts +++ b/src/agents/pi-embedded-runner/run/attempt.context-engine-helpers.ts @@ -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; + 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 } : {}), }); diff --git a/src/agents/pi-embedded-runner/run/attempt.spawn-workspace.context-engine.test.ts b/src/agents/pi-embedded-runner/run/attempt.spawn-workspace.context-engine.test.ts index f5198b98c85..e937e59f25e 100644 --- a/src/agents/pi-embedded-runner/run/attempt.spawn-workspace.context-engine.test.ts +++ b/src/agents/pi-embedded-runner/run/attempt.spawn-workspace.context-engine.test.ts @@ -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( diff --git a/src/agents/pi-embedded-runner/run/attempt.ts b/src/agents/pi-embedded-runner/run/attempt.ts index 0611dc8fcfe..935f8898115 100644 --- a/src/agents/pi-embedded-runner/run/attempt.ts +++ b/src/agents/pi-embedded-runner/run/attempt.ts @@ -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 } : {}), }); diff --git a/src/context-engine/context-engine.test.ts b/src/context-engine/context-engine.test.ts index 89b820e047f..01071593856 100644 --- a/src/context-engine/context-engine.test.ts +++ b/src/context-engine/context-engine.test.ts @@ -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; + citationsMode?: MemoryCitationsMode; }): Promise { return { messages: params.messages, @@ -168,6 +172,8 @@ class LegacySessionKeyStrictEngine implements ContextEngine { sessionKey?: string; messages: AgentMessage[]; tokenBudget?: number; + availableTools?: Set; + citationsMode?: MemoryCitationsMode; prompt?: string; }): Promise { this.assembleCalls.push({ ...params }); @@ -279,6 +285,8 @@ class LegacyAssembleStrictEngine implements ContextEngine { sessionKey?: string; messages: AgentMessage[]; tokenBudget?: number; + availableTools?: Set; + citationsMode?: MemoryCitationsMode; prompt?: string; }): Promise { 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(); + }); }); // ═══════════════════════════════════════════════════════════════════════════ diff --git a/src/context-engine/delegate.ts b/src/context-engine/delegate.ts index d378ee01ff8..bd4b6cd965c 100644 --- a/src/context-engine/delegate.ts +++ b/src/context-engine/delegate.ts @@ -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; + 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; +} diff --git a/src/context-engine/legacy.ts b/src/context-engine/legacy.ts index c823979c964..ea12ed2eee2 100644 --- a/src/context-engine/legacy.ts +++ b/src/context-engine/legacy.ts @@ -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; + citationsMode?: MemoryCitationsMode; model?: string; }): Promise { // Pass-through: the existing sanitize -> validate -> limit -> repair pipeline diff --git a/src/context-engine/types.ts b/src/context-engine/types.ts index 03401fdf3f2..6c013ee8952 100644 --- a/src/context-engine/types.ts +++ b/src/context-engine/types.ts @@ -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; + /** 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; diff --git a/src/plugin-sdk/compat.ts b/src/plugin-sdk/compat.ts index c351863859d..5ad6e71f26a 100644 --- a/src/plugin-sdk/compat.ts +++ b/src/plugin-sdk/compat.ts @@ -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 { diff --git a/src/plugin-sdk/core.ts b/src/plugin-sdk/core.ts index e5c8a65224c..81128579541 100644 --- a/src/plugin-sdk/core.ts +++ b/src/plugin-sdk/core.ts @@ -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, diff --git a/src/plugin-sdk/index.ts b/src/plugin-sdk/index.ts index 891f603d030..ed0e7cf0954 100644 --- a/src/plugin-sdk/index.ts +++ b/src/plugin-sdk/index.ts @@ -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";