diff --git a/extensions/memory-core/index.ts b/extensions/memory-core/index.ts index ba827361fee..b12cef8345d 100644 --- a/extensions/memory-core/index.ts +++ b/extensions/memory-core/index.ts @@ -1,13 +1,168 @@ -import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry"; -import { registerMemoryCli } from "./src/cli.js"; -import { registerDreamingCommand } from "./src/dreaming-command.js"; +import { + jsonResult, + resolveMemorySearchConfig, + resolveSessionAgentId, + type MemoryPluginRuntime, + type OpenClawConfig, +} from "openclaw/plugin-sdk/memory-core-host-runtime-core"; +import { resolveMemoryBackendConfig } from "openclaw/plugin-sdk/memory-core-host-runtime-files"; +import { + definePluginEntry, + type AnyAgentTool, + type OpenClawPluginToolContext, +} from "openclaw/plugin-sdk/plugin-entry"; +import { Type } from "typebox"; import { registerShortTermPromotionDreaming } from "./src/dreaming.js"; import { buildMemoryFlushPlan } from "./src/flush-plan.js"; import { registerBuiltInMemoryEmbeddingProviders } from "./src/memory/provider-adapters.js"; import { buildPromptSection } from "./src/prompt-section.js"; -import { listMemoryCorePublicArtifacts } from "./src/public-artifacts.js"; -import { memoryRuntime } from "./src/runtime-provider.js"; -import { createMemoryGetTool, createMemorySearchTool } from "./src/tools.js"; + +type MemoryToolsModule = typeof import("./src/tools.js"); +type RuntimeProviderModule = typeof import("./src/runtime-provider.js"); + +type MemoryToolOptions = { + config?: OpenClawConfig; + getConfig?: () => OpenClawConfig | undefined; + agentSessionKey?: string; + sandboxed?: boolean; +}; + +let memoryToolsModulePromise: Promise | undefined; +let runtimeProviderModulePromise: Promise | undefined; + +function loadMemoryToolsModule(): Promise { + memoryToolsModulePromise ??= import("./src/tools.js"); + return memoryToolsModulePromise; +} + +function loadRuntimeProviderModule(): Promise { + runtimeProviderModulePromise ??= import("./src/runtime-provider.js"); + return runtimeProviderModulePromise; +} + +function getToolConfig(options: MemoryToolOptions): OpenClawConfig | undefined { + return options.getConfig?.() ?? options.config; +} + +function hasMemoryToolContext(options: MemoryToolOptions): boolean { + const cfg = getToolConfig(options); + if (!cfg) { + return false; + } + const agentId = resolveSessionAgentId({ + sessionKey: options.agentSessionKey, + config: cfg, + }); + return Boolean(resolveMemorySearchConfig(cfg, agentId)); +} + +const MemorySearchSchema = Type.Object({ + query: Type.String(), + maxResults: Type.Optional(Type.Number()), + minScore: Type.Optional(Type.Number()), + corpus: Type.Optional( + Type.Union([ + Type.Literal("memory"), + Type.Literal("wiki"), + Type.Literal("all"), + Type.Literal("sessions"), + ]), + ), +}); + +const MemoryGetSchema = Type.Object({ + path: Type.String(), + from: Type.Optional(Type.Number()), + lines: Type.Optional(Type.Number()), + corpus: Type.Optional( + Type.Union([Type.Literal("memory"), Type.Literal("wiki"), Type.Literal("all")]), + ), +}); + +function createLazyMemoryTool(params: { + options: MemoryToolOptions; + label: string; + name: "memory_search" | "memory_get"; + description: string; + parameters: typeof MemorySearchSchema | typeof MemoryGetSchema; + load: (module: MemoryToolsModule, options: MemoryToolOptions) => AnyAgentTool | null; +}): AnyAgentTool | null { + if (!hasMemoryToolContext(params.options)) { + return null; + } + + let toolPromise: Promise | undefined; + const loadTool = async () => { + toolPromise ??= loadMemoryToolsModule().then((module) => params.load(module, params.options)); + return await toolPromise; + }; + + return { + label: params.label, + name: params.name, + description: params.description, + parameters: params.parameters, + execute: async (toolCallId, toolParams, signal, onUpdate) => { + const tool = await loadTool(); + if (!tool) { + return jsonResult({ + disabled: true, + unavailable: true, + error: "memory search unavailable", + }); + } + return await tool.execute(toolCallId, toolParams, signal, onUpdate); + }, + }; +} + +function createLazyMemorySearchTool(options: MemoryToolOptions): AnyAgentTool | null { + return createLazyMemoryTool({ + options, + label: "Memory Search", + name: "memory_search", + description: + "Mandatory recall step: semantically search MEMORY.md + memory/*.md (and optional session transcripts) before answering questions about prior work, decisions, dates, people, preferences, or todos. Optional `corpus=wiki` or `corpus=all` also searches registered compiled-wiki supplements. `corpus=memory` restricts hits to indexed memory files (excludes session transcript chunks from ranking). `corpus=sessions` restricts hits to indexed session transcripts (same visibility rules as session history tools). If response has disabled=true, memory retrieval is unavailable and should be surfaced to the user.", + parameters: MemorySearchSchema, + load: (module, loadOptions) => module.createMemorySearchTool(loadOptions), + }); +} + +function createLazyMemoryGetTool(options: MemoryToolOptions): AnyAgentTool | null { + return createLazyMemoryTool({ + options, + label: "Memory Get", + name: "memory_get", + description: + "Safe exact excerpt read from MEMORY.md or memory/*.md. Defaults to a bounded excerpt when lines are omitted, includes truncation/continuation info when more content exists, and `corpus=wiki` reads from registered compiled-wiki supplements.", + parameters: MemoryGetSchema, + load: (module, loadOptions) => module.createMemoryGetTool(loadOptions), + }); +} + +function resolveMemoryToolOptions(ctx: OpenClawPluginToolContext): MemoryToolOptions { + const getConfig = () => ctx.getRuntimeConfig?.() ?? ctx.runtimeConfig ?? ctx.config; + return { + config: getConfig(), + getConfig, + agentSessionKey: ctx.sessionKey, + sandboxed: ctx.sandboxed, + }; +} + +const memoryRuntime: MemoryPluginRuntime = { + async getMemorySearchManager(params) { + const { memoryRuntime: runtime } = await loadRuntimeProviderModule(); + return await runtime.getMemorySearchManager(params); + }, + resolveMemoryBackendConfig(params) { + return resolveMemoryBackendConfig(params); + }, + async closeAllMemorySearchManagers() { + const { memoryRuntime: runtime } = await loadRuntimeProviderModule(); + await runtime.closeAllMemorySearchManagers?.(); + }, +}; export default definePluginEntry({ id: "memory-core", name: "Memory (Core)", @@ -16,43 +171,39 @@ export default definePluginEntry({ register(api) { registerBuiltInMemoryEmbeddingProviders(api); registerShortTermPromotionDreaming(api); - registerDreamingCommand(api); api.registerMemoryCapability({ promptBuilder: buildPromptSection, flushPlanResolver: buildMemoryFlushPlan, runtime: memoryRuntime, publicArtifacts: { - listArtifacts: listMemoryCorePublicArtifacts, + async listArtifacts(params) { + const { listMemoryCorePublicArtifacts } = await import("./src/public-artifacts.js"); + return await listMemoryCorePublicArtifacts(params); + }, }, }); - api.registerTool( - (ctx) => { - const getConfig = () => ctx.getRuntimeConfig?.() ?? ctx.runtimeConfig ?? ctx.config; - return createMemorySearchTool({ - config: getConfig(), - getConfig, - agentSessionKey: ctx.sessionKey, - sandboxed: ctx.sandboxed, - }); - }, - { names: ["memory_search"] }, - ); + api.registerTool((ctx) => createLazyMemorySearchTool(resolveMemoryToolOptions(ctx)), { + names: ["memory_search"], + }); - api.registerTool( - (ctx) => { - const getConfig = () => ctx.getRuntimeConfig?.() ?? ctx.runtimeConfig ?? ctx.config; - return createMemoryGetTool({ - config: getConfig(), - getConfig, - agentSessionKey: ctx.sessionKey, - }); + api.registerTool((ctx) => createLazyMemoryGetTool(resolveMemoryToolOptions(ctx)), { + names: ["memory_get"], + }); + + api.registerCommand({ + name: "dreaming", + description: "Enable or disable memory dreaming.", + acceptsArgs: true, + handler: async (ctx) => { + const { handleDreamingCommand } = await import("./src/dreaming-command.js"); + return await handleDreamingCommand(api, ctx); }, - { names: ["memory_get"] }, - ); + }); api.registerCli( - ({ program }) => { + async ({ program }) => { + const { registerMemoryCli } = await import("./src/cli.js"); registerMemoryCli(program); }, { diff --git a/extensions/memory-core/src/dreaming-command.ts b/extensions/memory-core/src/dreaming-command.ts index 86663c832db..3c7eb2dd906 100644 --- a/extensions/memory-core/src/dreaming-command.ts +++ b/extensions/memory-core/src/dreaming-command.ts @@ -1,6 +1,6 @@ import type { OpenClawConfig } from "openclaw/plugin-sdk/config-types"; import { resolveMemoryDreamingConfig } from "openclaw/plugin-sdk/memory-core-host-status"; -import type { OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-entry"; +import type { OpenClawPluginApi, PluginCommandContext } from "openclaw/plugin-sdk/plugin-entry"; import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime"; import { asRecord } from "./dreaming-shared.js"; import { resolveShortTermPromotionDreamingConfig } from "./dreaming.js"; @@ -80,52 +80,47 @@ function requiresAdminToMutateDreaming(gatewayClientScopes?: readonly string[]): return Array.isArray(gatewayClientScopes) && !gatewayClientScopes.includes("operator.admin"); } +export async function handleDreamingCommand(api: OpenClawPluginApi, ctx: PluginCommandContext) { + const args = ctx.args?.trim() ?? ""; + const [firstToken = ""] = args + .split(/\s+/) + .filter(Boolean) + .map((token) => normalizeLowercaseStringOrEmpty(token)); + const currentConfig = api.runtime.config.current() as OpenClawConfig; + + if (!firstToken || firstToken === "help" || firstToken === "options" || firstToken === "phases") { + return { text: formatUsage(formatStatus(currentConfig)) }; + } + + if (firstToken === "status") { + return { text: formatStatus(currentConfig) }; + } + + if (firstToken === "on" || firstToken === "off") { + if (requiresAdminToMutateDreaming(ctx.gatewayClientScopes)) { + return { text: "⚠️ /dreaming on|off requires operator.admin for gateway clients." }; + } + const enabled = firstToken === "on"; + const nextConfig = updateDreamingEnabledInConfig(currentConfig, enabled); + await api.runtime.config.replaceConfigFile({ + nextConfig, + afterWrite: { mode: "auto" }, + }); + return { + text: [`Dreaming ${enabled ? "enabled" : "disabled"}.`, "", formatStatus(nextConfig)].join( + "\n", + ), + }; + } + + return { text: formatUsage(formatStatus(currentConfig)) }; +} + export function registerDreamingCommand(api: OpenClawPluginApi): void { api.registerCommand({ name: "dreaming", description: "Enable or disable memory dreaming.", acceptsArgs: true, - handler: async (ctx) => { - const args = ctx.args?.trim() ?? ""; - const [firstToken = ""] = args - .split(/\s+/) - .filter(Boolean) - .map((token) => normalizeLowercaseStringOrEmpty(token)); - const currentConfig = api.runtime.config.current() as OpenClawConfig; - - if ( - !firstToken || - firstToken === "help" || - firstToken === "options" || - firstToken === "phases" - ) { - return { text: formatUsage(formatStatus(currentConfig)) }; - } - - if (firstToken === "status") { - return { text: formatStatus(currentConfig) }; - } - - if (firstToken === "on" || firstToken === "off") { - if (requiresAdminToMutateDreaming(ctx.gatewayClientScopes)) { - return { text: "⚠️ /dreaming on|off requires operator.admin for gateway clients." }; - } - const enabled = firstToken === "on"; - const nextConfig = updateDreamingEnabledInConfig(currentConfig, enabled); - await api.runtime.config.replaceConfigFile({ - nextConfig, - afterWrite: { mode: "auto" }, - }); - return { - text: [ - `Dreaming ${enabled ? "enabled" : "disabled"}.`, - "", - formatStatus(nextConfig), - ].join("\n"), - }; - } - - return { text: formatUsage(formatStatus(currentConfig)) }; - }, + handler: async (ctx) => await handleDreamingCommand(api, ctx), }); }