feat: add memory vector search

This commit is contained in:
Peter Steinberger
2026-01-12 11:22:56 +00:00
parent 8049f33435
commit bf11a42c37
22 changed files with 2923 additions and 94 deletions

134
src/agents/memory-search.ts Normal file
View File

@@ -0,0 +1,134 @@
import os from "node:os";
import path from "node:path";
import type { ClawdbotConfig, MemorySearchConfig } from "../config/config.js";
import { resolveStateDir } from "../config/paths.js";
import { resolveUserPath } from "../utils.js";
import { resolveAgentConfig } from "./agent-scope.js";
export type ResolvedMemorySearchConfig = {
enabled: boolean;
provider: "openai" | "local";
fallback: "openai" | "none";
model: string;
local: {
modelPath?: string;
modelCacheDir?: string;
};
store: {
driver: "sqlite";
path: string;
};
chunking: {
tokens: number;
overlap: number;
};
sync: {
onSessionStart: boolean;
onSearch: boolean;
watch: boolean;
watchDebounceMs: number;
intervalMinutes: number;
};
query: {
maxResults: number;
minScore: number;
};
};
const DEFAULT_MODEL = "text-embedding-3-small";
const DEFAULT_CHUNK_TOKENS = 400;
const DEFAULT_CHUNK_OVERLAP = 80;
const DEFAULT_WATCH_DEBOUNCE_MS = 1500;
const DEFAULT_MAX_RESULTS = 6;
const DEFAULT_MIN_SCORE = 0.35;
function resolveStorePath(agentId: string, raw?: string): string {
const stateDir = resolveStateDir(process.env, os.homedir);
const fallback = path.join(stateDir, "memory", `${agentId}.sqlite`);
if (!raw) return fallback;
const withToken = raw.includes("{agentId}")
? raw.replaceAll("{agentId}", agentId)
: raw;
return resolveUserPath(withToken);
}
function mergeConfig(
defaults: MemorySearchConfig | undefined,
overrides: MemorySearchConfig | undefined,
agentId: string,
): ResolvedMemorySearchConfig {
const enabled = overrides?.enabled ?? defaults?.enabled ?? true;
const provider = overrides?.provider ?? defaults?.provider ?? "openai";
const fallback = overrides?.fallback ?? defaults?.fallback ?? "openai";
const model = overrides?.model ?? defaults?.model ?? DEFAULT_MODEL;
const local = {
modelPath: overrides?.local?.modelPath ?? defaults?.local?.modelPath,
modelCacheDir:
overrides?.local?.modelCacheDir ?? defaults?.local?.modelCacheDir,
};
const store = {
driver: overrides?.store?.driver ?? defaults?.store?.driver ?? "sqlite",
path: resolveStorePath(
agentId,
overrides?.store?.path ?? defaults?.store?.path,
),
};
const chunking = {
tokens:
overrides?.chunking?.tokens ??
defaults?.chunking?.tokens ??
DEFAULT_CHUNK_TOKENS,
overlap:
overrides?.chunking?.overlap ??
defaults?.chunking?.overlap ??
DEFAULT_CHUNK_OVERLAP,
};
const sync = {
onSessionStart:
overrides?.sync?.onSessionStart ?? defaults?.sync?.onSessionStart ?? true,
onSearch: overrides?.sync?.onSearch ?? defaults?.sync?.onSearch ?? true,
watch: overrides?.sync?.watch ?? defaults?.sync?.watch ?? true,
watchDebounceMs:
overrides?.sync?.watchDebounceMs ??
defaults?.sync?.watchDebounceMs ??
DEFAULT_WATCH_DEBOUNCE_MS,
intervalMinutes:
overrides?.sync?.intervalMinutes ?? defaults?.sync?.intervalMinutes ?? 0,
};
const query = {
maxResults:
overrides?.query?.maxResults ??
defaults?.query?.maxResults ??
DEFAULT_MAX_RESULTS,
minScore:
overrides?.query?.minScore ??
defaults?.query?.minScore ??
DEFAULT_MIN_SCORE,
};
const overlap = Math.max(0, Math.min(chunking.overlap, chunking.tokens - 1));
const minScore = Math.max(0, Math.min(1, query.minScore));
return {
enabled,
provider,
fallback,
model,
local,
store,
chunking: { tokens: Math.max(1, chunking.tokens), overlap },
sync,
query: { ...query, minScore },
};
}
export function resolveMemorySearchConfig(
cfg: ClawdbotConfig,
agentId: string,
): ResolvedMemorySearchConfig | null {
const defaults = cfg.agents?.defaults?.memorySearch;
const overrides = resolveAgentConfig(cfg, agentId)?.memorySearch;
const resolved = mergeConfig(defaults, overrides, agentId);
if (!resolved.enabled) return null;
return resolved;
}