feat(plugins): allow stackable memory plugins
Adds support for multiple memory plugins to run simultaneously. Previously, memory plugins were mutually exclusive - only one could be active at a time via the plugins.slots.memory config. Now, plugins.slots.memory can accept: - Single string: "memory-lancedb" (original behavior) - Array: ["memory-core", "memory-lancedb"] (stackable mode) - "none": disable all memory plugins Use case: Run memory-core for workspace file search alongside memory-lancedb for auto-recall/capture conversation memory. Changes: - types.plugins.ts: Updated PluginSlotsConfig type - zod-schema.ts: Updated schema to accept string | string[] - config-state.ts: Updated normalization and slot decision logic - validation.ts: Updated validation to check array entries - schema.ts: Updated help text
This commit is contained in:
parent
2044b3ca8d
commit
344f5abcb5
@ -551,7 +551,7 @@ const FIELD_HELP: Record<string, string> = {
|
|||||||
"plugins.load.paths": "Additional plugin files or directories to load.",
|
"plugins.load.paths": "Additional plugin files or directories to load.",
|
||||||
"plugins.slots": "Select which plugins own exclusive slots (memory, etc.).",
|
"plugins.slots": "Select which plugins own exclusive slots (memory, etc.).",
|
||||||
"plugins.slots.memory":
|
"plugins.slots.memory":
|
||||||
'Select the active memory plugin by id, or "none" to disable memory plugins.',
|
'Select the active memory plugin(s): a single id, an array of ids for stackable plugins, or "none" to disable.',
|
||||||
"plugins.entries": "Per-plugin settings keyed by plugin id (enable/disable + config payloads).",
|
"plugins.entries": "Per-plugin settings keyed by plugin id (enable/disable + config payloads).",
|
||||||
"plugins.entries.*.enabled": "Overrides plugin enable/disable for this entry (restart required).",
|
"plugins.entries.*.enabled": "Overrides plugin enable/disable for this entry (restart required).",
|
||||||
"plugins.entries.*.config": "Plugin-defined config payload (schema is provided by the plugin).",
|
"plugins.entries.*.config": "Plugin-defined config payload (schema is provided by the plugin).",
|
||||||
|
|||||||
@ -4,8 +4,13 @@ export type PluginEntryConfig = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type PluginSlotsConfig = {
|
export type PluginSlotsConfig = {
|
||||||
/** Select which plugin owns the memory slot ("none" disables memory plugins). */
|
/**
|
||||||
memory?: string;
|
* Select which plugin(s) own the memory slot.
|
||||||
|
* - Single string: "memory-lancedb" - one plugin active
|
||||||
|
* - Array: ["memory-core", "memory-lancedb"] - multiple plugins active (stackable)
|
||||||
|
* - "none": disables all memory plugins
|
||||||
|
*/
|
||||||
|
memory?: string | string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PluginsLoadConfig = {
|
export type PluginsLoadConfig = {
|
||||||
|
|||||||
@ -199,11 +199,21 @@ export function validateConfigObjectWithPlugins(raw: unknown):
|
|||||||
}
|
}
|
||||||
|
|
||||||
const memorySlot = normalizedPlugins.slots.memory;
|
const memorySlot = normalizedPlugins.slots.memory;
|
||||||
|
// Validate memory slot - can be string or array of strings
|
||||||
if (typeof memorySlot === "string" && memorySlot.trim() && !knownIds.has(memorySlot)) {
|
if (typeof memorySlot === "string" && memorySlot.trim() && !knownIds.has(memorySlot)) {
|
||||||
issues.push({
|
issues.push({
|
||||||
path: "plugins.slots.memory",
|
path: "plugins.slots.memory",
|
||||||
message: `plugin not found: ${memorySlot}`,
|
message: `plugin not found: ${memorySlot}`,
|
||||||
});
|
});
|
||||||
|
} else if (Array.isArray(memorySlot)) {
|
||||||
|
for (const slotId of memorySlot) {
|
||||||
|
if (typeof slotId === "string" && slotId.trim() && !knownIds.has(slotId)) {
|
||||||
|
issues.push({
|
||||||
|
path: "plugins.slots.memory",
|
||||||
|
message: `plugin not found: ${slotId}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const allowedChannels = new Set<string>(["defaults", ...CHANNEL_IDS]);
|
const allowedChannels = new Set<string>(["defaults", ...CHANNEL_IDS]);
|
||||||
|
|||||||
@ -493,7 +493,7 @@ export const ClawdbotSchema = z
|
|||||||
.optional(),
|
.optional(),
|
||||||
slots: z
|
slots: z
|
||||||
.object({
|
.object({
|
||||||
memory: z.string().optional(),
|
memory: z.union([z.string(), z.array(z.string())]).optional(),
|
||||||
})
|
})
|
||||||
.strict()
|
.strict()
|
||||||
.optional(),
|
.optional(),
|
||||||
|
|||||||
@ -8,7 +8,7 @@ export type NormalizedPluginsConfig = {
|
|||||||
deny: string[];
|
deny: string[];
|
||||||
loadPaths: string[];
|
loadPaths: string[];
|
||||||
slots: {
|
slots: {
|
||||||
memory?: string | null;
|
memory?: string | string[] | null;
|
||||||
};
|
};
|
||||||
entries: Record<string, { enabled?: boolean; config?: unknown }>;
|
entries: Record<string, { enabled?: boolean; config?: unknown }>;
|
||||||
};
|
};
|
||||||
@ -20,7 +20,15 @@ const normalizeList = (value: unknown): string[] => {
|
|||||||
return value.map((entry) => (typeof entry === "string" ? entry.trim() : "")).filter(Boolean);
|
return value.map((entry) => (typeof entry === "string" ? entry.trim() : "")).filter(Boolean);
|
||||||
};
|
};
|
||||||
|
|
||||||
const normalizeSlotValue = (value: unknown): string | null | undefined => {
|
const normalizeSlotValue = (value: unknown): string | string[] | null | undefined => {
|
||||||
|
// Handle array of plugin ids (stackable mode)
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
const normalized = value
|
||||||
|
.map((entry) => (typeof entry === "string" ? entry.trim() : ""))
|
||||||
|
.filter(Boolean);
|
||||||
|
return normalized.length > 0 ? normalized : undefined;
|
||||||
|
}
|
||||||
|
// Handle single string
|
||||||
if (typeof value !== "string") return undefined;
|
if (typeof value !== "string") return undefined;
|
||||||
const trimmed = value.trim();
|
const trimmed = value.trim();
|
||||||
if (!trimmed) return undefined;
|
if (!trimmed) return undefined;
|
||||||
@ -78,7 +86,9 @@ export function resolveEnableState(
|
|||||||
if (config.allow.length > 0 && !config.allow.includes(id)) {
|
if (config.allow.length > 0 && !config.allow.includes(id)) {
|
||||||
return { enabled: false, reason: "not in allowlist" };
|
return { enabled: false, reason: "not in allowlist" };
|
||||||
}
|
}
|
||||||
if (config.slots.memory === id) {
|
// Check if plugin is selected in memory slot (handles both string and array)
|
||||||
|
const memorySlot = config.slots.memory;
|
||||||
|
if (memorySlot === id || (Array.isArray(memorySlot) && memorySlot.includes(id))) {
|
||||||
return { enabled: true };
|
return { enabled: true };
|
||||||
}
|
}
|
||||||
const entry = config.entries[id];
|
const entry = config.entries[id];
|
||||||
@ -100,13 +110,24 @@ export function resolveEnableState(
|
|||||||
export function resolveMemorySlotDecision(params: {
|
export function resolveMemorySlotDecision(params: {
|
||||||
id: string;
|
id: string;
|
||||||
kind?: string;
|
kind?: string;
|
||||||
slot: string | null | undefined;
|
slot: string | string[] | null | undefined;
|
||||||
selectedId: string | null;
|
selectedId: string | null;
|
||||||
}): { enabled: boolean; reason?: string; selected?: boolean } {
|
}): { enabled: boolean; reason?: string; selected?: boolean } {
|
||||||
if (params.kind !== "memory") return { enabled: true };
|
if (params.kind !== "memory") return { enabled: true };
|
||||||
if (params.slot === null) {
|
if (params.slot === null) {
|
||||||
return { enabled: false, reason: "memory slot disabled" };
|
return { enabled: false, reason: "memory slot disabled" };
|
||||||
}
|
}
|
||||||
|
// Handle array of memory plugins (stackable mode)
|
||||||
|
if (Array.isArray(params.slot)) {
|
||||||
|
if (params.slot.includes(params.id)) {
|
||||||
|
return { enabled: true, selected: true };
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
enabled: false,
|
||||||
|
reason: `memory slot set to [${params.slot.join(", ")}]`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// Handle single string (exclusive mode - original behavior)
|
||||||
if (typeof params.slot === "string") {
|
if (typeof params.slot === "string") {
|
||||||
if (params.slot === params.id) {
|
if (params.slot === params.id) {
|
||||||
return { enabled: true, selected: true };
|
return { enabled: true, selected: true };
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user