mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-02 13:51:33 +00:00
442 lines
16 KiB
TypeScript
442 lines
16 KiB
TypeScript
import { createLazyRuntimeModule } from "openclaw/plugin-sdk/lazy-runtime";
|
|
// Memory Core plugin entrypoint registers its OpenClaw integration.
|
|
import {
|
|
jsonResult,
|
|
resolveMemorySearchConfig,
|
|
resolveSessionAgentIds,
|
|
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 { normalizePluginsConfig } from "openclaw/plugin-sdk/plugin-config-runtime";
|
|
import {
|
|
definePluginEntry,
|
|
type AnyAgentTool,
|
|
type OpenClawPluginApi,
|
|
type OpenClawPluginToolContext,
|
|
} from "openclaw/plugin-sdk/plugin-entry";
|
|
import type {
|
|
OpenKeyedStoreOptions,
|
|
PluginStateLeaseRunner,
|
|
} from "openclaw/plugin-sdk/plugin-state-runtime";
|
|
import type { TSchema } from "typebox";
|
|
import { configureMemoryCoreDreamingState } from "./src/dreaming-state.js";
|
|
import { registerShortTermPromotionDreaming } from "./src/dreaming.js";
|
|
import { buildMemoryFlushPlan } from "./src/flush-plan.js";
|
|
import type { MemoryCoreAcquireLocalService } from "./src/memory/embedding-local-service.js";
|
|
import type { MemoryCoreRuntimeHost } from "./src/memory/runtime-host.js";
|
|
import { buildPromptSection } from "./src/prompt-section.js";
|
|
import { registerSessionBackfillGatewayMethods } from "./src/session-backfill-gateway.js";
|
|
|
|
type MemoryToolsModule = typeof import("./src/tools.js");
|
|
type StandingIntentToolModule = typeof import("./src/standing-intents-tool.js");
|
|
|
|
type MemoryToolOptions = {
|
|
config?: OpenClawConfig;
|
|
getConfig?: () => OpenClawConfig | undefined;
|
|
agentId?: string;
|
|
agentSessionKey?: string;
|
|
sandboxed?: boolean;
|
|
oneShotCliRun?: boolean;
|
|
conversationRecall?: OpenClawPluginToolContext["conversationRecall"];
|
|
activeProjectKeys?: readonly string[];
|
|
acquireLocalService?: MemoryCoreAcquireLocalService;
|
|
withLease?: PluginStateLeaseRunner;
|
|
};
|
|
|
|
const loadMemoryToolsModule = createLazyRuntimeModule(() => import("./src/tools.js"));
|
|
const loadStandingIntentsModule = createLazyRuntimeModule(
|
|
() => import("./src/standing-intents.js"),
|
|
);
|
|
const loadStandingIntentToolModule = createLazyRuntimeModule(
|
|
() => import("./src/standing-intents-tool.js"),
|
|
);
|
|
|
|
const loadRuntimeProviderModule = createLazyRuntimeModule(
|
|
() => import("./src/runtime-provider.js"),
|
|
);
|
|
|
|
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 { sessionAgentId: agentId } = resolveSessionAgentIds({
|
|
sessionKey: options.agentSessionKey,
|
|
config: cfg,
|
|
agentId: options.agentId,
|
|
});
|
|
return Boolean(resolveMemorySearchConfig(cfg, agentId));
|
|
}
|
|
|
|
const MemorySearchSchema = {
|
|
type: "object",
|
|
properties: {
|
|
query: { type: "string" },
|
|
maxResults: { type: "integer", minimum: 1 },
|
|
minScore: { type: "number" },
|
|
corpus: { type: "string", enum: ["memory", "wiki", "all", "sessions"] },
|
|
},
|
|
required: ["query"],
|
|
additionalProperties: false,
|
|
} as const satisfies TSchema;
|
|
|
|
const MemoryGetSchema = {
|
|
type: "object",
|
|
properties: {
|
|
path: { type: "string" },
|
|
from: { type: "integer", minimum: 1 },
|
|
lines: { type: "integer", minimum: 1 },
|
|
corpus: { type: "string", enum: ["memory", "wiki", "all"] },
|
|
},
|
|
required: ["path"],
|
|
additionalProperties: false,
|
|
} as const satisfies TSchema;
|
|
|
|
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<AnyAgentTool | null> | 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 createLazyStandingIntentTool(ctx: OpenClawPluginToolContext): AnyAgentTool | null {
|
|
if (ctx.senderIsOwner !== true) {
|
|
return null;
|
|
}
|
|
const cfg = ctx.getRuntimeConfig?.() ?? ctx.runtimeConfig ?? ctx.config;
|
|
const provider = ctx.messageChannel?.trim();
|
|
const senderId = ctx.requesterSenderId?.trim();
|
|
if (!cfg) {
|
|
return null;
|
|
}
|
|
const { sessionAgentId: agentId } = resolveSessionAgentIds({
|
|
sessionKey: ctx.sessionKey,
|
|
config: cfg,
|
|
agentId: ctx.agentId,
|
|
});
|
|
let toolPromise: Promise<AnyAgentTool> | undefined;
|
|
const loadTool = async (): Promise<AnyAgentTool> => {
|
|
toolPromise ??= loadStandingIntentToolModule().then((module: StandingIntentToolModule) =>
|
|
module.createStandingIntentTool({
|
|
agentId,
|
|
...(ctx.sessionId ? { sourceSessionId: ctx.sessionId } : {}),
|
|
...(ctx.nativeChannelId ? { conversationId: ctx.nativeChannelId } : {}),
|
|
...(provider ? { provider } : {}),
|
|
...(ctx.agentAccountId ? { accountId: ctx.agentAccountId } : {}),
|
|
...(senderId ? { senderId } : {}),
|
|
}),
|
|
);
|
|
return await toolPromise;
|
|
};
|
|
return {
|
|
label: "Standing Intent",
|
|
name: "intent",
|
|
description:
|
|
"Create, list, or explicitly cancel event-conditioned standing intents. A created intent is armed; the system injects the reminder automatically when it triggers. Do not deliver it early or cancel it unless the user asks. Use cron or scheduled tasks for time-based reminders.",
|
|
parameters: {
|
|
type: "object",
|
|
properties: {
|
|
action: { type: "string", enum: ["create", "list", "cancel"] },
|
|
id: { type: "string" },
|
|
description: { type: "string" },
|
|
triggerKeywords: { type: "array", items: { type: "string" } },
|
|
scope: {
|
|
type: "string",
|
|
enum: ["conversation", "channel", "anywhere"],
|
|
default: "channel",
|
|
},
|
|
senderScope: {
|
|
type: "string",
|
|
enum: ["sender", "anyone"],
|
|
default: "sender",
|
|
},
|
|
expiresAt: { type: "string" },
|
|
maxFires: { type: "integer", minimum: 1 },
|
|
cooldownSeconds: { type: "integer", minimum: 0 },
|
|
status: {
|
|
type: "string",
|
|
enum: ["pending", "armed", "fired", "done", "cancelled", "expired"],
|
|
},
|
|
},
|
|
required: ["action"],
|
|
additionalProperties: false,
|
|
},
|
|
execute: async (toolCallId, params, signal, onUpdate) => {
|
|
const tool = await loadTool();
|
|
return await tool.execute(toolCallId, params, signal, onUpdate);
|
|
},
|
|
};
|
|
}
|
|
|
|
function resolveMemoryToolOptions(
|
|
ctx: OpenClawPluginToolContext,
|
|
host: MemoryCoreRuntimeHost,
|
|
): MemoryToolOptions {
|
|
const getConfig = () => ctx.getRuntimeConfig?.() ?? ctx.runtimeConfig ?? ctx.config;
|
|
return {
|
|
config: getConfig(),
|
|
getConfig,
|
|
agentId: ctx.agentId,
|
|
agentSessionKey: ctx.sessionKey,
|
|
sandboxed: ctx.sandboxed,
|
|
oneShotCliRun: ctx.oneShotCliRun,
|
|
conversationRecall: ctx.conversationRecall,
|
|
activeProjectKeys: ctx.activeProjectKeys,
|
|
...(host.acquireLocalService ? { acquireLocalService: host.acquireLocalService } : {}),
|
|
...(host.withLease ? { withLease: host.withLease } : {}),
|
|
};
|
|
}
|
|
|
|
function createLazyMemoryRuntime(host: MemoryCoreRuntimeHost): MemoryPluginRuntime {
|
|
return {
|
|
async getMemorySearchManager(params) {
|
|
const { createMemoryRuntime } = await loadRuntimeProviderModule();
|
|
return await createMemoryRuntime(host).getMemorySearchManager(params);
|
|
},
|
|
resolveMemoryBackendConfig(params) {
|
|
return resolveMemoryBackendConfig(params);
|
|
},
|
|
async closeAllMemorySearchManagers() {
|
|
const { memoryRuntime: runtime } = await loadRuntimeProviderModule();
|
|
await runtime.closeAllMemorySearchManagers?.();
|
|
},
|
|
async closeMemorySearchManager(params) {
|
|
const { memoryRuntime: runtime } = await loadRuntimeProviderModule();
|
|
await runtime.closeMemorySearchManager?.(params);
|
|
},
|
|
};
|
|
}
|
|
|
|
function configuredMemoryAgentIds(config: OpenClawConfig): string[] {
|
|
const configured = (config.agents?.list ?? []).map((entry) => entry.id).filter(Boolean);
|
|
if (configured.length > 0) {
|
|
return [...new Set(configured)];
|
|
}
|
|
return [resolveSessionAgentIds({ config }).sessionAgentId];
|
|
}
|
|
|
|
function registerMemoryManagerWarmup(
|
|
api: OpenClawPluginApi,
|
|
memoryRuntime: MemoryPluginRuntime,
|
|
): void {
|
|
api.on("gateway_start", (_event, ctx) => {
|
|
const config = (api.runtime.config?.current?.() ?? ctx.config ?? api.config) as OpenClawConfig;
|
|
if (normalizePluginsConfig(config.plugins).slots.memory !== "memory-core") {
|
|
return;
|
|
}
|
|
for (const agentId of configuredMemoryAgentIds(config)) {
|
|
const backend = memoryRuntime.resolveMemoryBackendConfig({ cfg: config, agentId });
|
|
void memoryRuntime
|
|
.getMemorySearchManager({ cfg: config, agentId })
|
|
.then(async ({ manager, error }) => {
|
|
if (!manager) {
|
|
if (error) {
|
|
api.logger.debug?.(`memory-core: startup index warmup unavailable: ${error}`);
|
|
}
|
|
return;
|
|
}
|
|
if (backend.backend === "builtin") {
|
|
await manager.sync?.({ reason: "startup-warmup" });
|
|
}
|
|
})
|
|
.catch((error: unknown) => {
|
|
api.logger.debug?.(
|
|
`memory-core: startup index warmup failed for ${agentId}: ${
|
|
error instanceof Error ? error.message : String(error)
|
|
}`,
|
|
);
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
export default definePluginEntry({
|
|
id: "memory-core",
|
|
name: "OpenClaw Memory",
|
|
description: "File-backed memory search tools and CLI",
|
|
kind: "memory",
|
|
register(api) {
|
|
const acquireLocalService = api.runtime.llm?.acquireLocalService;
|
|
const withLease = api.runtime.state.withLease.bind(api.runtime.state);
|
|
const host = { acquireLocalService, withLease } satisfies MemoryCoreRuntimeHost;
|
|
configureMemoryCoreDreamingState(<T>(options: OpenKeyedStoreOptions) =>
|
|
api.runtime.state.openKeyedStore<T>(options),
|
|
);
|
|
const memoryRuntime = createLazyMemoryRuntime(host);
|
|
registerShortTermPromotionDreaming(api);
|
|
registerSessionBackfillGatewayMethods(api);
|
|
registerMemoryManagerWarmup(api, memoryRuntime);
|
|
api.registerMemoryCapability({
|
|
promptBuilder: buildPromptSection,
|
|
flushPlanResolver: buildMemoryFlushPlan,
|
|
runtime: memoryRuntime,
|
|
publicArtifacts: {
|
|
async listArtifacts(params) {
|
|
const { listMemoryCorePublicArtifacts } = await import("./src/public-artifacts.js");
|
|
return await listMemoryCorePublicArtifacts(params);
|
|
},
|
|
},
|
|
});
|
|
|
|
api.registerTool((ctx) => createLazyMemorySearchTool(resolveMemoryToolOptions(ctx, host)), {
|
|
names: ["memory_search"],
|
|
});
|
|
|
|
api.registerTool((ctx) => createLazyMemoryGetTool(resolveMemoryToolOptions(ctx, host)), {
|
|
names: ["memory_get"],
|
|
});
|
|
|
|
api.registerTool((ctx) => createLazyStandingIntentTool(ctx), {
|
|
names: ["intent"],
|
|
});
|
|
|
|
api.on("before_prompt_build", async (event, ctx) => {
|
|
if (ctx.trigger !== "user") {
|
|
return undefined;
|
|
}
|
|
try {
|
|
const module = await loadStandingIntentsModule();
|
|
if (!module.isEligibleStandingIntentTurn(ctx)) {
|
|
return undefined;
|
|
}
|
|
const config = (api.runtime.config?.current?.() ?? api.config) as OpenClawConfig;
|
|
const { sessionAgentId: agentId } = resolveSessionAgentIds({
|
|
sessionKey: ctx.sessionKey,
|
|
config,
|
|
agentId: ctx.agentId,
|
|
});
|
|
const intents = module.matchStandingIntents({
|
|
agentId,
|
|
prompt: event.prompt,
|
|
...((ctx.channelId ?? ctx.chatId)
|
|
? { channel: (ctx.channelId ?? ctx.chatId) as string }
|
|
: {}),
|
|
...((ctx.channel ?? ctx.messageProvider)
|
|
? { provider: (ctx.channel ?? ctx.messageProvider) as string }
|
|
: {}),
|
|
...(ctx.accountId ? { accountId: ctx.accountId } : {}),
|
|
...(ctx.senderId ? { senderId: ctx.senderId } : {}),
|
|
});
|
|
const prependContext = module.buildStandingIntentContext(intents);
|
|
return prependContext ? { prependContext } : undefined;
|
|
} catch (error) {
|
|
api.logger.warn?.(
|
|
`memory-core: standing intent matching failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
);
|
|
return undefined;
|
|
}
|
|
});
|
|
|
|
api.on(
|
|
"before_agent_reply",
|
|
async (_event, ctx) => {
|
|
if (ctx.trigger !== "heartbeat" && ctx.trigger !== "cron") {
|
|
return undefined;
|
|
}
|
|
try {
|
|
const module = await loadStandingIntentsModule();
|
|
const config = (api.runtime.config?.current?.() ?? api.config) as OpenClawConfig;
|
|
const { sessionAgentId: agentId } = resolveSessionAgentIds({
|
|
sessionKey: ctx.sessionKey,
|
|
config,
|
|
agentId: ctx.agentId,
|
|
});
|
|
module.sweepStandingIntents({ agentId });
|
|
} catch (error) {
|
|
api.logger.warn?.(
|
|
`memory-core: standing intent maintenance failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
);
|
|
}
|
|
return undefined;
|
|
},
|
|
{ eligibleTriggers: ["heartbeat", "cron"] },
|
|
);
|
|
|
|
api.registerCommand({
|
|
name: "dreaming",
|
|
description: "Enable or disable memory dreaming.",
|
|
acceptsArgs: true,
|
|
exposeSenderIsOwner: true,
|
|
handler: async (ctx) => {
|
|
const { handleDreamingCommand } = await import("./src/dreaming-command.js");
|
|
return await handleDreamingCommand(api, ctx);
|
|
},
|
|
});
|
|
|
|
api.registerCli(
|
|
async ({ program }) => {
|
|
const { registerMemoryCli } = await import("./cli.js");
|
|
registerMemoryCli(program, host);
|
|
},
|
|
{
|
|
descriptors: [
|
|
{
|
|
name: "memory",
|
|
description: "Search, inspect, and reindex memory files",
|
|
hasSubcommands: true,
|
|
},
|
|
],
|
|
},
|
|
);
|
|
},
|
|
});
|