255 lines
8.6 KiB
TypeScript
255 lines
8.6 KiB
TypeScript
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 { clampInt, clampNumber, resolveUserPath } from "../utils.js";
|
|
import { resolveAgentConfig } from "./agent-scope.js";
|
|
|
|
export type ResolvedMemorySearchConfig = {
|
|
enabled: boolean;
|
|
sources: Array<"memory" | "sessions">;
|
|
provider: "openai" | "local" | "gemini" | "auto";
|
|
remote?: {
|
|
baseUrl?: string;
|
|
apiKey?: string;
|
|
headers?: Record<string, string>;
|
|
batch?: {
|
|
enabled: boolean;
|
|
wait: boolean;
|
|
concurrency: number;
|
|
pollIntervalMs: number;
|
|
timeoutMinutes: number;
|
|
};
|
|
};
|
|
experimental: {
|
|
sessionMemory: boolean;
|
|
};
|
|
fallback: "openai" | "gemini" | "local" | "none";
|
|
model: string;
|
|
local: {
|
|
modelPath?: string;
|
|
modelCacheDir?: string;
|
|
};
|
|
store: {
|
|
driver: "sqlite";
|
|
path: string;
|
|
vector: {
|
|
enabled: boolean;
|
|
extensionPath?: string;
|
|
};
|
|
};
|
|
chunking: {
|
|
tokens: number;
|
|
overlap: number;
|
|
};
|
|
sync: {
|
|
onSessionStart: boolean;
|
|
onSearch: boolean;
|
|
watch: boolean;
|
|
watchDebounceMs: number;
|
|
intervalMinutes: number;
|
|
};
|
|
query: {
|
|
maxResults: number;
|
|
minScore: number;
|
|
hybrid: {
|
|
enabled: boolean;
|
|
vectorWeight: number;
|
|
textWeight: number;
|
|
candidateMultiplier: number;
|
|
};
|
|
};
|
|
cache: {
|
|
enabled: boolean;
|
|
maxEntries?: number;
|
|
};
|
|
};
|
|
|
|
const DEFAULT_OPENAI_MODEL = "text-embedding-3-small";
|
|
const DEFAULT_GEMINI_MODEL = "gemini-embedding-001";
|
|
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;
|
|
const DEFAULT_HYBRID_ENABLED = true;
|
|
const DEFAULT_HYBRID_VECTOR_WEIGHT = 0.7;
|
|
const DEFAULT_HYBRID_TEXT_WEIGHT = 0.3;
|
|
const DEFAULT_HYBRID_CANDIDATE_MULTIPLIER = 4;
|
|
const DEFAULT_CACHE_ENABLED = true;
|
|
const DEFAULT_SOURCES: Array<"memory" | "sessions"> = ["memory"];
|
|
|
|
function normalizeSources(
|
|
sources: Array<"memory" | "sessions"> | undefined,
|
|
sessionMemoryEnabled: boolean,
|
|
): Array<"memory" | "sessions"> {
|
|
const normalized = new Set<"memory" | "sessions">();
|
|
const input = sources?.length ? sources : DEFAULT_SOURCES;
|
|
for (const source of input) {
|
|
if (source === "memory") normalized.add("memory");
|
|
if (source === "sessions" && sessionMemoryEnabled) normalized.add("sessions");
|
|
}
|
|
if (normalized.size === 0) normalized.add("memory");
|
|
return Array.from(normalized);
|
|
}
|
|
|
|
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 sessionMemory =
|
|
overrides?.experimental?.sessionMemory ?? defaults?.experimental?.sessionMemory ?? false;
|
|
const provider = overrides?.provider ?? defaults?.provider ?? "auto";
|
|
const defaultRemote = defaults?.remote;
|
|
const overrideRemote = overrides?.remote;
|
|
const hasRemote = Boolean(defaultRemote || overrideRemote);
|
|
const includeRemote =
|
|
hasRemote || provider === "openai" || provider === "gemini" || provider === "auto";
|
|
const batch = {
|
|
enabled: overrideRemote?.batch?.enabled ?? defaultRemote?.batch?.enabled ?? true,
|
|
wait: overrideRemote?.batch?.wait ?? defaultRemote?.batch?.wait ?? true,
|
|
concurrency: Math.max(
|
|
1,
|
|
overrideRemote?.batch?.concurrency ?? defaultRemote?.batch?.concurrency ?? 2,
|
|
),
|
|
pollIntervalMs:
|
|
overrideRemote?.batch?.pollIntervalMs ?? defaultRemote?.batch?.pollIntervalMs ?? 2000,
|
|
timeoutMinutes:
|
|
overrideRemote?.batch?.timeoutMinutes ?? defaultRemote?.batch?.timeoutMinutes ?? 60,
|
|
};
|
|
const remote = includeRemote
|
|
? {
|
|
baseUrl: overrideRemote?.baseUrl ?? defaultRemote?.baseUrl,
|
|
apiKey: overrideRemote?.apiKey ?? defaultRemote?.apiKey,
|
|
headers: overrideRemote?.headers ?? defaultRemote?.headers,
|
|
batch,
|
|
}
|
|
: undefined;
|
|
const fallback = overrides?.fallback ?? defaults?.fallback ?? "none";
|
|
const modelDefault =
|
|
provider === "gemini"
|
|
? DEFAULT_GEMINI_MODEL
|
|
: provider === "openai"
|
|
? DEFAULT_OPENAI_MODEL
|
|
: undefined;
|
|
const model = overrides?.model ?? defaults?.model ?? modelDefault ?? "";
|
|
const local = {
|
|
modelPath: overrides?.local?.modelPath ?? defaults?.local?.modelPath,
|
|
modelCacheDir: overrides?.local?.modelCacheDir ?? defaults?.local?.modelCacheDir,
|
|
};
|
|
const sources = normalizeSources(overrides?.sources ?? defaults?.sources, sessionMemory);
|
|
const vector = {
|
|
enabled: overrides?.store?.vector?.enabled ?? defaults?.store?.vector?.enabled ?? true,
|
|
extensionPath:
|
|
overrides?.store?.vector?.extensionPath ?? defaults?.store?.vector?.extensionPath,
|
|
};
|
|
const store = {
|
|
driver: overrides?.store?.driver ?? defaults?.store?.driver ?? "sqlite",
|
|
path: resolveStorePath(agentId, overrides?.store?.path ?? defaults?.store?.path),
|
|
vector,
|
|
};
|
|
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 hybrid = {
|
|
enabled:
|
|
overrides?.query?.hybrid?.enabled ??
|
|
defaults?.query?.hybrid?.enabled ??
|
|
DEFAULT_HYBRID_ENABLED,
|
|
vectorWeight:
|
|
overrides?.query?.hybrid?.vectorWeight ??
|
|
defaults?.query?.hybrid?.vectorWeight ??
|
|
DEFAULT_HYBRID_VECTOR_WEIGHT,
|
|
textWeight:
|
|
overrides?.query?.hybrid?.textWeight ??
|
|
defaults?.query?.hybrid?.textWeight ??
|
|
DEFAULT_HYBRID_TEXT_WEIGHT,
|
|
candidateMultiplier:
|
|
overrides?.query?.hybrid?.candidateMultiplier ??
|
|
defaults?.query?.hybrid?.candidateMultiplier ??
|
|
DEFAULT_HYBRID_CANDIDATE_MULTIPLIER,
|
|
};
|
|
const cache = {
|
|
enabled: overrides?.cache?.enabled ?? defaults?.cache?.enabled ?? DEFAULT_CACHE_ENABLED,
|
|
maxEntries: overrides?.cache?.maxEntries ?? defaults?.cache?.maxEntries,
|
|
};
|
|
|
|
const overlap = clampNumber(chunking.overlap, 0, Math.max(0, chunking.tokens - 1));
|
|
const minScore = clampNumber(query.minScore, 0, 1);
|
|
const vectorWeight = clampNumber(hybrid.vectorWeight, 0, 1);
|
|
const textWeight = clampNumber(hybrid.textWeight, 0, 1);
|
|
const sum = vectorWeight + textWeight;
|
|
const normalizedVectorWeight = sum > 0 ? vectorWeight / sum : DEFAULT_HYBRID_VECTOR_WEIGHT;
|
|
const normalizedTextWeight = sum > 0 ? textWeight / sum : DEFAULT_HYBRID_TEXT_WEIGHT;
|
|
const candidateMultiplier = clampInt(hybrid.candidateMultiplier, 1, 20);
|
|
return {
|
|
enabled,
|
|
sources,
|
|
provider,
|
|
remote,
|
|
experimental: {
|
|
sessionMemory,
|
|
},
|
|
fallback,
|
|
model,
|
|
local,
|
|
store,
|
|
chunking: { tokens: Math.max(1, chunking.tokens), overlap },
|
|
sync,
|
|
query: {
|
|
...query,
|
|
minScore,
|
|
hybrid: {
|
|
enabled: Boolean(hybrid.enabled),
|
|
vectorWeight: normalizedVectorWeight,
|
|
textWeight: normalizedTextWeight,
|
|
candidateMultiplier,
|
|
},
|
|
},
|
|
cache: {
|
|
enabled: Boolean(cache.enabled),
|
|
maxEntries:
|
|
typeof cache.maxEntries === "number" && Number.isFinite(cache.maxEntries)
|
|
? Math.max(1, Math.floor(cache.maxEntries))
|
|
: undefined,
|
|
},
|
|
};
|
|
}
|
|
|
|
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;
|
|
}
|