refactor: make memory embedding adapters generic

This commit is contained in:
Peter Steinberger
2026-03-27 02:01:07 +00:00
parent 42be3fb059
commit 7a35bca2ec
12 changed files with 233 additions and 41 deletions

View File

@@ -12,7 +12,10 @@ import {
type MemoryEmbeddingProviderCreateOptions,
type MemoryEmbeddingProviderRuntime,
} from "../engine-host-api.js";
import { canAutoSelectLocal } from "./provider-adapters.js";
import {
canAutoSelectLocal,
getBuiltinMemoryEmbeddingProviderAdapter,
} from "./provider-adapters.js";
export {
DEFAULT_GEMINI_EMBEDDING_MODEL,
@@ -92,6 +95,15 @@ function resolveProviderModel(
return adapter.defaultModel ?? "";
}
export function resolveEmbeddingProviderFallbackModel(
providerId: string,
fallbackSourceModel: string,
): string {
const adapter =
getMemoryEmbeddingProvider(providerId) ?? getBuiltinMemoryEmbeddingProviderAdapter(providerId);
return adapter?.defaultModel ?? fallbackSourceModel;
}
async function createWithAdapter(
adapter: MemoryEmbeddingProviderAdapter,
options: CreateEmbeddingProviderOptions,

View File

@@ -32,14 +32,10 @@ import {
} from "../engine-host-api.js";
import {
createEmbeddingProvider,
DEFAULT_GEMINI_EMBEDDING_MODEL,
DEFAULT_MISTRAL_EMBEDDING_MODEL,
DEFAULT_OLLAMA_EMBEDDING_MODEL,
DEFAULT_OPENAI_EMBEDDING_MODEL,
DEFAULT_VOYAGE_EMBEDDING_MODEL,
type EmbeddingProvider,
type EmbeddingProviderId,
type EmbeddingProviderRuntime,
resolveEmbeddingProviderFallbackModel,
} from "./embeddings.js";
import { buildFileEntry } from "./internal.js";
import { loadSqliteVecExtension } from "./sqlite-vec.js";
@@ -1119,18 +1115,7 @@ export abstract class MemoryManagerSyncOps {
}
const fallbackFrom = this.provider.id as EmbeddingProviderId;
const fallbackModel =
fallback === "gemini"
? DEFAULT_GEMINI_EMBEDDING_MODEL
: fallback === "openai"
? DEFAULT_OPENAI_EMBEDDING_MODEL
: fallback === "voyage"
? DEFAULT_VOYAGE_EMBEDDING_MODEL
: fallback === "mistral"
? DEFAULT_MISTRAL_EMBEDDING_MODEL
: fallback === "ollama"
? DEFAULT_OLLAMA_EMBEDDING_MODEL
: this.settings.model;
const fallbackModel = resolveEmbeddingProviderFallbackModel(fallback, this.settings.model);
const fallbackResult = await createEmbeddingProvider({
config: this.cfg,

View File

@@ -334,6 +334,16 @@ export const builtinMemoryEmbeddingProviderAdapters = [
ollamaAdapter,
] as const;
const builtinMemoryEmbeddingProviderAdapterById = new Map(
builtinMemoryEmbeddingProviderAdapters.map((adapter) => [adapter.id, adapter]),
);
export function getBuiltinMemoryEmbeddingProviderAdapter(
id: string,
): MemoryEmbeddingProviderAdapter | undefined {
return builtinMemoryEmbeddingProviderAdapterById.get(id);
}
export function registerBuiltInMemoryEmbeddingProviders(register: {
registerMemoryEmbeddingProvider: (adapter: MemoryEmbeddingProviderAdapter) => void;
}): void {

View File

@@ -420,6 +420,10 @@
"types": "./dist/plugin-sdk/memory-core.d.ts",
"default": "./dist/plugin-sdk/memory-core.js"
},
"./plugin-sdk/memory-core-engine-runtime": {
"types": "./dist/plugin-sdk/memory-core-engine-runtime.d.ts",
"default": "./dist/plugin-sdk/memory-core-engine-runtime.js"
},
"./plugin-sdk/memory-core-host": {
"types": "./dist/plugin-sdk/memory-core-host.d.ts",
"default": "./dist/plugin-sdk/memory-core-host.js"

View File

@@ -95,6 +95,7 @@
"matrix",
"mattermost",
"memory-core",
"memory-core-engine-runtime",
"memory-core-host",
"memory-core-host-engine",
"memory-core-host-runtime",

View File

@@ -0,0 +1,7 @@
// Thin engine runtime compat surface for the bundled memory-core plugin.
// Keep extension-owned engine exports isolated behind a dedicated SDK subpath.
export {
getMemorySearchManager,
MemoryIndexManager,
} from "../../extensions/memory-core/src/memory/index.js";

View File

@@ -14,10 +14,7 @@ export { SILENT_REPLY_TOKEN } from "../auto-reply/tokens.js";
export { loadConfig } from "../config/config.js";
export { resolveStateDir } from "../config/paths.js";
export { resolveSessionTranscriptsDirForAgent } from "../config/sessions/paths.js";
export {
getMemorySearchManager,
MemoryIndexManager,
} from "../../extensions/memory-core/src/memory/index.js";
export { getMemorySearchManager, MemoryIndexManager } from "./memory-core-engine-runtime.js";
export {
listMemoryFiles,
normalizeExtraMemoryPaths,

View File

@@ -0,0 +1,97 @@
import { describe, expect, it } from "vitest";
import type { OpenClawConfig } from "../../config/config.js";
import { getRegisteredMemoryEmbeddingProvider } from "../memory-embedding-providers.js";
import { createPluginRegistry, type PluginRecord } from "../registry.js";
import type { PluginRuntime } from "../runtime/types.js";
import { createPluginRecord } from "../status.test-helpers.js";
import type { OpenClawPluginApi } from "../types.js";
function registerTestPlugin(params: {
registry: ReturnType<typeof createPluginRegistry>;
config: OpenClawConfig;
record: PluginRecord;
register(api: OpenClawPluginApi): void;
}) {
params.registry.registry.plugins.push(params.record);
params.register(
params.registry.createApi(params.record, {
config: params.config,
}),
);
}
describe("memory embedding provider registration", () => {
it("only allows memory plugins to register adapters", () => {
const config = {} as OpenClawConfig;
const registry = createPluginRegistry({
logger: {
info() {},
warn() {},
error() {},
debug() {},
},
runtime: {} as PluginRuntime,
});
registerTestPlugin({
registry,
config,
record: createPluginRecord({
id: "not-memory",
name: "Not Memory",
source: "/virtual/not-memory/index.ts",
}),
register(api) {
api.registerMemoryEmbeddingProvider({
id: "forbidden",
create: async () => ({ provider: null }),
});
},
});
expect(getRegisteredMemoryEmbeddingProvider("forbidden")).toBeUndefined();
expect(registry.registry.diagnostics).toEqual(
expect.arrayContaining([
expect.objectContaining({
pluginId: "not-memory",
message: "only memory plugins can register memory embedding providers",
}),
]),
);
});
it("records the owning memory plugin id for registered adapters", () => {
const config = {} as OpenClawConfig;
const registry = createPluginRegistry({
logger: {
info() {},
warn() {},
error() {},
debug() {},
},
runtime: {} as PluginRuntime,
});
registerTestPlugin({
registry,
config,
record: createPluginRecord({
id: "memory-core",
name: "Memory Core",
kind: "memory",
source: "/virtual/memory-core/index.ts",
}),
register(api) {
api.registerMemoryEmbeddingProvider({
id: "openai",
create: async () => ({ provider: null }),
});
},
});
expect(getRegisteredMemoryEmbeddingProvider("openai")).toEqual({
adapter: expect.objectContaining({ id: "openai" }),
ownerPluginId: "memory-core",
});
});
});

View File

@@ -24,8 +24,8 @@ import { clearPluginInteractiveHandlers } from "./interactive.js";
import { loadPluginManifestRegistry } from "./manifest-registry.js";
import {
clearMemoryEmbeddingProviders,
listMemoryEmbeddingProviders,
restoreMemoryEmbeddingProviders,
listRegisteredMemoryEmbeddingProviders,
restoreRegisteredMemoryEmbeddingProviders,
} from "./memory-embedding-providers.js";
import {
clearMemoryPluginState,
@@ -104,7 +104,7 @@ export class PluginLoadFailureError extends Error {
type CachedPluginState = {
registry: PluginRegistry;
memoryEmbeddingProviders: ReturnType<typeof listMemoryEmbeddingProviders>;
memoryEmbeddingProviders: ReturnType<typeof listRegisteredMemoryEmbeddingProviders>;
memoryFlushPlanResolver: ReturnType<typeof getMemoryFlushPlanResolver>;
memoryPromptBuilder: ReturnType<typeof getMemoryPromptSectionBuilder>;
memoryRuntime: ReturnType<typeof getMemoryRuntime>;
@@ -719,7 +719,7 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi
if (cacheEnabled) {
const cached = getCachedPluginRegistry(cacheKey);
if (cached) {
restoreMemoryEmbeddingProviders(cached.memoryEmbeddingProviders);
restoreRegisteredMemoryEmbeddingProviders(cached.memoryEmbeddingProviders);
restoreMemoryPluginState({
promptBuilder: cached.memoryPromptBuilder,
flushPlanResolver: cached.memoryFlushPlanResolver,
@@ -1235,7 +1235,7 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi
hookPolicy: entry?.hooks,
registrationMode,
});
const previousMemoryEmbeddingProviders = listMemoryEmbeddingProviders();
const previousMemoryEmbeddingProviders = listRegisteredMemoryEmbeddingProviders();
const previousMemoryFlushPlanResolver = getMemoryFlushPlanResolver();
const previousMemoryPromptBuilder = getMemoryPromptSectionBuilder();
const previousMemoryRuntime = getMemoryRuntime();
@@ -1252,7 +1252,7 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi
}
// Snapshot loads should not replace process-global runtime prompt state.
if (!shouldActivate) {
restoreMemoryEmbeddingProviders(previousMemoryEmbeddingProviders);
restoreRegisteredMemoryEmbeddingProviders(previousMemoryEmbeddingProviders);
restoreMemoryPluginState({
promptBuilder: previousMemoryPromptBuilder,
flushPlanResolver: previousMemoryFlushPlanResolver,
@@ -1262,7 +1262,7 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi
registry.plugins.push(record);
seenIds.set(pluginId, candidate.origin);
} catch (err) {
restoreMemoryEmbeddingProviders(previousMemoryEmbeddingProviders);
restoreRegisteredMemoryEmbeddingProviders(previousMemoryEmbeddingProviders);
restoreMemoryPluginState({
promptBuilder: previousMemoryPromptBuilder,
flushPlanResolver: previousMemoryFlushPlanResolver,
@@ -1303,7 +1303,7 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi
if (cacheEnabled) {
setCachedPluginRegistry(cacheKey, {
registry,
memoryEmbeddingProviders: listMemoryEmbeddingProviders(),
memoryEmbeddingProviders: listRegisteredMemoryEmbeddingProviders(),
memoryFlushPlanResolver: getMemoryFlushPlanResolver(),
memoryPromptBuilder: getMemoryPromptSectionBuilder(),
memoryRuntime: getMemoryRuntime(),

View File

@@ -2,8 +2,11 @@ import { afterEach, describe, expect, it } from "vitest";
import {
clearMemoryEmbeddingProviders,
getMemoryEmbeddingProvider,
getRegisteredMemoryEmbeddingProvider,
listMemoryEmbeddingProviders,
listRegisteredMemoryEmbeddingProviders,
registerMemoryEmbeddingProvider,
restoreRegisteredMemoryEmbeddingProviders,
restoreMemoryEmbeddingProviders,
type MemoryEmbeddingProviderAdapter,
} from "./memory-embedding-providers.js";
@@ -39,6 +42,38 @@ describe("memory embedding provider registry", () => {
expect(getMemoryEmbeddingProvider("beta")).toBe(beta);
});
it("tracks owner plugin ids in registered snapshots", () => {
const alpha = createAdapter("alpha");
registerMemoryEmbeddingProvider(alpha, { ownerPluginId: "memory-core" });
expect(getRegisteredMemoryEmbeddingProvider("alpha")).toEqual({
adapter: alpha,
ownerPluginId: "memory-core",
});
expect(listRegisteredMemoryEmbeddingProviders()).toEqual([
{
adapter: alpha,
ownerPluginId: "memory-core",
},
]);
});
it("restores registered snapshots with owner metadata", () => {
const beta = createAdapter("beta");
restoreRegisteredMemoryEmbeddingProviders([
{
adapter: beta,
ownerPluginId: "memory-core",
},
]);
expect(getRegisteredMemoryEmbeddingProvider("beta")).toEqual({
adapter: beta,
ownerPluginId: "memory-core",
});
});
it("clears the registry", () => {
registerMemoryEmbeddingProvider(createAdapter("alpha"));

View File

@@ -67,24 +67,56 @@ export type MemoryEmbeddingProviderAdapter = {
shouldContinueAutoSelection?: (err: unknown) => boolean;
};
const memoryEmbeddingProviders = new Map<string, MemoryEmbeddingProviderAdapter>();
export type RegisteredMemoryEmbeddingProvider = {
adapter: MemoryEmbeddingProviderAdapter;
ownerPluginId?: string;
};
export function registerMemoryEmbeddingProvider(adapter: MemoryEmbeddingProviderAdapter): void {
memoryEmbeddingProviders.set(adapter.id, adapter);
const memoryEmbeddingProviders = new Map<string, RegisteredMemoryEmbeddingProvider>();
export function registerMemoryEmbeddingProvider(
adapter: MemoryEmbeddingProviderAdapter,
options?: { ownerPluginId?: string },
): void {
memoryEmbeddingProviders.set(adapter.id, {
adapter,
ownerPluginId: options?.ownerPluginId,
});
}
export function getMemoryEmbeddingProvider(id: string): MemoryEmbeddingProviderAdapter | undefined {
export function getRegisteredMemoryEmbeddingProvider(
id: string,
): RegisteredMemoryEmbeddingProvider | undefined {
return memoryEmbeddingProviders.get(id);
}
export function listMemoryEmbeddingProviders(): MemoryEmbeddingProviderAdapter[] {
export function getMemoryEmbeddingProvider(id: string): MemoryEmbeddingProviderAdapter | undefined {
return memoryEmbeddingProviders.get(id)?.adapter;
}
export function listRegisteredMemoryEmbeddingProviders(): RegisteredMemoryEmbeddingProvider[] {
return Array.from(memoryEmbeddingProviders.values());
}
export function listMemoryEmbeddingProviders(): MemoryEmbeddingProviderAdapter[] {
return listRegisteredMemoryEmbeddingProviders().map((entry) => entry.adapter);
}
export function restoreMemoryEmbeddingProviders(adapters: MemoryEmbeddingProviderAdapter[]): void {
memoryEmbeddingProviders.clear();
for (const adapter of adapters) {
memoryEmbeddingProviders.set(adapter.id, adapter);
registerMemoryEmbeddingProvider(adapter);
}
}
export function restoreRegisteredMemoryEmbeddingProviders(
entries: RegisteredMemoryEmbeddingProvider[],
): void {
memoryEmbeddingProviders.clear();
for (const entry of entries) {
registerMemoryEmbeddingProvider(entry.adapter, {
ownerPluginId: entry.ownerPluginId,
});
}
}

View File

@@ -15,7 +15,7 @@ import { normalizePluginHttpPath } from "./http-path.js";
import { findOverlappingPluginHttpRoute } from "./http-route-overlap.js";
import { registerPluginInteractiveHandler } from "./interactive.js";
import {
getMemoryEmbeddingProvider,
getRegisteredMemoryEmbeddingProvider,
registerMemoryEmbeddingProvider,
} from "./memory-embedding-providers.js";
import {
@@ -1106,17 +1106,29 @@ export function createPluginRegistry(registryParams: PluginRegistryParams) {
if (registrationMode !== "full") {
return;
}
const existing = getMemoryEmbeddingProvider(adapter.id);
if (existing) {
if (record.kind !== "memory") {
pushDiagnostic({
level: "error",
pluginId: record.id,
source: record.source,
message: `memory embedding provider already registered: ${adapter.id}`,
message: "only memory plugins can register memory embedding providers",
});
return;
}
registerMemoryEmbeddingProvider(adapter);
const existing = getRegisteredMemoryEmbeddingProvider(adapter.id);
if (existing) {
const ownerDetail = existing.ownerPluginId ? ` (owner: ${existing.ownerPluginId})` : "";
pushDiagnostic({
level: "error",
pluginId: record.id,
source: record.source,
message: `memory embedding provider already registered: ${adapter.id}${ownerDetail}`,
});
return;
}
registerMemoryEmbeddingProvider(adapter, {
ownerPluginId: record.id,
});
},
resolvePath: (input: string) => resolveUserPath(input),
on: (hookName, handler, opts) =>