refactor: move memory engine behind plugin adapters

This commit is contained in:
Peter Steinberger
2026-03-27 00:40:45 +00:00
parent aed6283faa
commit dbf78de7c6
142 changed files with 1610 additions and 966 deletions

View File

@@ -1,13 +1,64 @@
import { describe, expect, it } from "vitest";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import type { OpenClawConfig } from "../config/config.js";
import {
clearMemoryEmbeddingProviders,
registerMemoryEmbeddingProvider,
} from "../plugins/memory-embedding-providers.js";
import { resolveMemorySearchConfig } from "./memory-search.js";
const asConfig = (cfg: OpenClawConfig): OpenClawConfig => cfg;
describe("memory search config", () => {
function configWithDefaultProvider(
provider: "openai" | "local" | "gemini" | "mistral" | "ollama",
): OpenClawConfig {
beforeEach(() => {
clearMemoryEmbeddingProviders();
registerMemoryEmbeddingProvider({
id: "openai",
defaultModel: "text-embedding-3-small",
transport: "remote",
create: async () => ({ provider: null }),
});
registerMemoryEmbeddingProvider({
id: "local",
defaultModel: "local-default",
transport: "local",
create: async () => ({ provider: null }),
});
registerMemoryEmbeddingProvider({
id: "gemini",
defaultModel: "gemini-embedding-001",
transport: "remote",
supportsMultimodalEmbeddings: ({ model }) =>
model
.trim()
.replace(/^models\//, "")
.replace(/^(gemini|google)\//, "") === "gemini-embedding-2-preview",
create: async () => ({ provider: null }),
});
registerMemoryEmbeddingProvider({
id: "voyage",
defaultModel: "voyage-4-large",
transport: "remote",
create: async () => ({ provider: null }),
});
registerMemoryEmbeddingProvider({
id: "mistral",
defaultModel: "mistral-embed",
transport: "remote",
create: async () => ({ provider: null }),
});
registerMemoryEmbeddingProvider({
id: "ollama",
defaultModel: "nomic-embed-text",
transport: "remote",
create: async () => ({ provider: null }),
});
});
afterEach(() => {
clearMemoryEmbeddingProviders();
});
function configWithDefaultProvider(provider: string): OpenClawConfig {
return asConfig({
agents: {
defaults: {
@@ -258,7 +309,7 @@ describe("memory search config", () => {
},
});
expect(() => resolveMemorySearchConfig(cfg, "main")).toThrow(
/memorySearch\.multimodal requires memorySearch\.provider = "gemini"/,
/memorySearch\.multimodal requires a provider adapter that supports multimodal embeddings/,
);
});

View File

@@ -3,12 +3,12 @@ import path from "node:path";
import type { OpenClawConfig, MemorySearchConfig } from "../config/config.js";
import { resolveStateDir } from "../config/paths.js";
import type { SecretInput } from "../config/types.secrets.js";
import { getMemoryEmbeddingProvider } from "../plugins/memory-embedding-providers.js";
import {
isMemoryMultimodalEnabled,
normalizeMemoryMultimodalSettings,
supportsMemoryMultimodalEmbeddings,
type MemoryMultimodalSettings,
} from "../memory/multimodal.js";
} from "../plugins/memory-host/multimodal.js";
import { clampInt, clampNumber, resolveUserPath } from "../utils.js";
import { resolveAgentConfig } from "./agent-scope.js";
@@ -17,7 +17,7 @@ export type ResolvedMemorySearchConfig = {
sources: Array<"memory" | "sessions">;
extraPaths: string[];
multimodal: MemoryMultimodalSettings;
provider: "openai" | "local" | "gemini" | "voyage" | "mistral" | "ollama" | "auto";
provider: string;
remote?: {
baseUrl?: string;
apiKey?: SecretInput;
@@ -33,7 +33,7 @@ export type ResolvedMemorySearchConfig = {
experimental: {
sessionMemory: boolean;
};
fallback: "openai" | "gemini" | "local" | "voyage" | "mistral" | "ollama" | "none";
fallback: string;
model: string;
outputDimensionality?: number;
local: {
@@ -88,11 +88,6 @@ export type ResolvedMemorySearchConfig = {
};
};
const DEFAULT_OPENAI_MODEL = "text-embedding-3-small";
const DEFAULT_GEMINI_MODEL = "gemini-embedding-001";
const DEFAULT_VOYAGE_MODEL = "voyage-4-large";
const DEFAULT_MISTRAL_MODEL = "mistral-embed";
const DEFAULT_OLLAMA_MODEL = "nomic-embed-text";
const DEFAULT_CHUNK_TOKENS = 400;
const DEFAULT_CHUNK_OVERLAP = 80;
const DEFAULT_WATCH_DEBOUNCE_MS = 1500;
@@ -150,8 +145,12 @@ function mergeConfig(
const sessionMemory =
overrides?.experimental?.sessionMemory ?? defaults?.experimental?.sessionMemory ?? false;
const provider = overrides?.provider ?? defaults?.provider ?? "auto";
const primaryAdapter = provider === "auto" ? undefined : getMemoryEmbeddingProvider(provider);
const defaultRemote = defaults?.remote;
const overrideRemote = overrides?.remote;
const fallback = overrides?.fallback ?? defaults?.fallback ?? "none";
const fallbackAdapter =
fallback && fallback !== "none" ? getMemoryEmbeddingProvider(fallback) : undefined;
const hasRemoteConfig = Boolean(
overrideRemote?.baseUrl ||
overrideRemote?.apiKey ||
@@ -162,12 +161,9 @@ function mergeConfig(
);
const includeRemote =
hasRemoteConfig ||
provider === "openai" ||
provider === "gemini" ||
provider === "voyage" ||
provider === "mistral" ||
provider === "ollama" ||
provider === "auto";
provider === "auto" ||
primaryAdapter?.transport !== "local" ||
fallbackAdapter?.transport === "remote";
const batch = {
enabled: overrideRemote?.batch?.enabled ?? defaultRemote?.batch?.enabled ?? false,
wait: overrideRemote?.batch?.wait ?? defaultRemote?.batch?.wait ?? true,
@@ -188,19 +184,7 @@ function mergeConfig(
batch,
}
: undefined;
const fallback = overrides?.fallback ?? defaults?.fallback ?? "none";
const modelDefault =
provider === "gemini"
? DEFAULT_GEMINI_MODEL
: provider === "openai"
? DEFAULT_OPENAI_MODEL
: provider === "voyage"
? DEFAULT_VOYAGE_MODEL
: provider === "mistral"
? DEFAULT_MISTRAL_MODEL
: provider === "ollama"
? DEFAULT_OLLAMA_MODEL
: undefined;
const modelDefault = provider === "auto" ? undefined : primaryAdapter?.defaultModel;
const model = overrides?.model ?? defaults?.model ?? modelDefault ?? "";
const outputDimensionality = overrides?.outputDimensionality ?? defaults?.outputDimensionality;
const local = {
@@ -386,15 +370,16 @@ export function resolveMemorySearchConfig(
return null;
}
const multimodalActive = isMemoryMultimodalEnabled(resolved.multimodal);
const multimodalProvider =
resolved.provider === "auto" ? undefined : getMemoryEmbeddingProvider(resolved.provider);
if (
multimodalActive &&
!supportsMemoryMultimodalEmbeddings({
provider: resolved.provider,
!multimodalProvider?.supportsMultimodalEmbeddings?.({
model: resolved.model,
})
) {
throw new Error(
'agents.*.memorySearch.multimodal requires memorySearch.provider = "gemini" and model = "gemini-embedding-2-preview".',
"agents.*.memorySearch.multimodal requires a provider adapter that supports multimodal embeddings for the configured model.",
);
}
if (multimodalActive && resolved.fallback !== "none") {

View File

@@ -5,7 +5,10 @@ import type { ExtensionAPI, FileOperations } from "@mariozechner/pi-coding-agent
import { extractSections } from "../../auto-reply/reply/post-compaction-context.js";
import { openBoundaryFile } from "../../infra/boundary-file-read.js";
import { createSubsystemLogger } from "../../logging/subsystem.js";
import { extractKeywords, isQueryStopWordToken } from "../../memory/query-expansion.js";
import {
extractKeywords,
isQueryStopWordToken,
} from "../../plugins/memory-host/query-expansion.js";
import {
hasMeaningfulConversationContent,
isRealConversationMessage,