Plugins: extract slot arbitration

This commit is contained in:
Gustavo Madeira Santana 2026-03-15 19:06:28 +00:00
parent cefa80a409
commit 871086537b
4 changed files with 165 additions and 112 deletions

View File

@ -1,6 +1,6 @@
import type { OpenClawConfig } from "../config/config.js";
import type { ContextEngine } from "../context-engine/types.js";
import { defaultSlotIdForKey } from "../plugins/slots.js";
import { getExtensionHostDefaultSlotId } from "./slot-arbitration.js";
export type ExtensionHostContextEngineFactory = () => ContextEngine | Promise<ContextEngine>;
@ -46,7 +46,7 @@ export async function resolveExtensionHostContextEngine(
const engineId =
typeof slotValue === "string" && slotValue.trim()
? slotValue.trim()
: defaultSlotIdForKey("contextEngine");
: getExtensionHostDefaultSlotId("contextEngine");
const factory = getExtensionHostContextEngineRuntimeState().engines.get(engineId);
if (!factory) {

View File

@ -0,0 +1,44 @@
import { describe, expect, it } from "vitest";
import type { OpenClawConfig } from "../config/config.js";
import { applyExtensionHostExclusiveSlotSelection } from "./slot-arbitration.js";
describe("extension host slot arbitration", () => {
const createMemoryConfig = (plugins?: OpenClawConfig["plugins"]): OpenClawConfig => ({
plugins: {
...plugins,
entries: {
...plugins?.entries,
memory: {
enabled: true,
...plugins?.entries?.memory,
},
},
},
});
it("selects the slot and disables competing plugins of the same kind", () => {
const config = createMemoryConfig({
slots: { memory: "memory-core" },
entries: { "memory-core": { enabled: true } },
});
const result = applyExtensionHostExclusiveSlotSelection({
config,
selectedId: "memory",
selectedKind: "memory",
registry: {
plugins: [
{ id: "memory-core", kind: "memory" },
{ id: "memory", kind: "memory" },
],
},
});
expect(result.changed).toBe(true);
expect(result.config.plugins?.slots?.memory).toBe("memory");
expect(result.config.plugins?.entries?.["memory-core"]?.enabled).toBe(false);
expect(result.warnings).toContain(
'Exclusive slot "memory" switched from "memory-core" to "memory".',
);
expect(result.warnings).toContain('Disabled other "memory" slot plugins: memory-core.');
});
});

View File

@ -0,0 +1,112 @@
import type { OpenClawConfig } from "../config/config.js";
import type { PluginSlotsConfig } from "../config/types.plugins.js";
import type { PluginKind } from "../plugins/types.js";
export type ExtensionHostPluginSlotKey = keyof PluginSlotsConfig;
type SlotPluginRecord = {
id: string;
kind?: PluginKind;
};
const SLOT_BY_KIND: Record<PluginKind, ExtensionHostPluginSlotKey> = {
memory: "memory",
"context-engine": "contextEngine",
};
const DEFAULT_SLOT_BY_KEY: Record<ExtensionHostPluginSlotKey, string> = {
memory: "memory-core",
contextEngine: "legacy",
};
export function extensionHostSlotKeyForPluginKind(
kind?: PluginKind,
): ExtensionHostPluginSlotKey | null {
if (!kind) {
return null;
}
return SLOT_BY_KIND[kind] ?? null;
}
export function getExtensionHostDefaultSlotId(slotKey: ExtensionHostPluginSlotKey): string {
return DEFAULT_SLOT_BY_KEY[slotKey];
}
export type ExtensionHostSlotSelectionResult = {
config: OpenClawConfig;
warnings: string[];
changed: boolean;
};
export function applyExtensionHostExclusiveSlotSelection(params: {
config: OpenClawConfig;
selectedId: string;
selectedKind?: PluginKind;
registry?: { plugins: SlotPluginRecord[] };
}): ExtensionHostSlotSelectionResult {
const slotKey = extensionHostSlotKeyForPluginKind(params.selectedKind);
if (!slotKey) {
return { config: params.config, warnings: [], changed: false };
}
const warnings: string[] = [];
const pluginsConfig = params.config.plugins ?? {};
const prevSlot = pluginsConfig.slots?.[slotKey];
const slots = {
...pluginsConfig.slots,
[slotKey]: params.selectedId,
};
const inferredPrevSlot = prevSlot ?? getExtensionHostDefaultSlotId(slotKey);
if (inferredPrevSlot && inferredPrevSlot !== params.selectedId) {
warnings.push(
`Exclusive slot "${slotKey}" switched from "${inferredPrevSlot}" to "${params.selectedId}".`,
);
}
const entries = { ...pluginsConfig.entries };
const disabledIds: string[] = [];
if (params.registry) {
for (const plugin of params.registry.plugins) {
if (plugin.id === params.selectedId) {
continue;
}
if (plugin.kind !== params.selectedKind) {
continue;
}
const entry = entries[plugin.id];
if (!entry || entry.enabled !== false) {
entries[plugin.id] = {
...entry,
enabled: false,
};
disabledIds.push(plugin.id);
}
}
}
if (disabledIds.length > 0) {
warnings.push(
`Disabled other "${slotKey}" slot plugins: ${disabledIds.toSorted().join(", ")}.`,
);
}
const changed = prevSlot !== params.selectedId || disabledIds.length > 0;
if (!changed) {
return { config: params.config, warnings: [], changed: false };
}
return {
config: {
...params.config,
plugins: {
...pluginsConfig,
slots,
entries,
},
},
warnings,
changed: true,
};
}

View File

@ -1,110 +1,7 @@
import type { OpenClawConfig } from "../config/config.js";
import type { PluginSlotsConfig } from "../config/types.plugins.js";
import type { PluginKind } from "./types.js";
export type PluginSlotKey = keyof PluginSlotsConfig;
type SlotPluginRecord = {
id: string;
kind?: PluginKind;
};
const SLOT_BY_KIND: Record<PluginKind, PluginSlotKey> = {
memory: "memory",
"context-engine": "contextEngine",
};
const DEFAULT_SLOT_BY_KEY: Record<PluginSlotKey, string> = {
memory: "memory-core",
contextEngine: "legacy",
};
export function slotKeyForPluginKind(kind?: PluginKind): PluginSlotKey | null {
if (!kind) {
return null;
}
return SLOT_BY_KIND[kind] ?? null;
}
export function defaultSlotIdForKey(slotKey: PluginSlotKey): string {
return DEFAULT_SLOT_BY_KEY[slotKey];
}
export type SlotSelectionResult = {
config: OpenClawConfig;
warnings: string[];
changed: boolean;
};
export function applyExclusiveSlotSelection(params: {
config: OpenClawConfig;
selectedId: string;
selectedKind?: PluginKind;
registry?: { plugins: SlotPluginRecord[] };
}): SlotSelectionResult {
const slotKey = slotKeyForPluginKind(params.selectedKind);
if (!slotKey) {
return { config: params.config, warnings: [], changed: false };
}
const warnings: string[] = [];
const pluginsConfig = params.config.plugins ?? {};
const prevSlot = pluginsConfig.slots?.[slotKey];
const slots = {
...pluginsConfig.slots,
[slotKey]: params.selectedId,
};
const inferredPrevSlot = prevSlot ?? defaultSlotIdForKey(slotKey);
if (inferredPrevSlot && inferredPrevSlot !== params.selectedId) {
warnings.push(
`Exclusive slot "${slotKey}" switched from "${inferredPrevSlot}" to "${params.selectedId}".`,
);
}
const entries = { ...pluginsConfig.entries };
const disabledIds: string[] = [];
if (params.registry) {
for (const plugin of params.registry.plugins) {
if (plugin.id === params.selectedId) {
continue;
}
if (plugin.kind !== params.selectedKind) {
continue;
}
const entry = entries[plugin.id];
if (!entry || entry.enabled !== false) {
entries[plugin.id] = {
...entry,
enabled: false,
};
disabledIds.push(plugin.id);
}
}
}
if (disabledIds.length > 0) {
warnings.push(
`Disabled other "${slotKey}" slot plugins: ${disabledIds.toSorted().join(", ")}.`,
);
}
const changed = prevSlot !== params.selectedId || disabledIds.length > 0;
if (!changed) {
return { config: params.config, warnings: [], changed: false };
}
return {
config: {
...params.config,
plugins: {
...pluginsConfig,
slots,
entries,
},
},
warnings,
changed: true,
};
}
export {
applyExtensionHostExclusiveSlotSelection as applyExclusiveSlotSelection,
getExtensionHostDefaultSlotId as defaultSlotIdForKey,
extensionHostSlotKeyForPluginKind as slotKeyForPluginKind,
type ExtensionHostPluginSlotKey as PluginSlotKey,
type ExtensionHostSlotSelectionResult as SlotSelectionResult,
} from "../extension-host/slot-arbitration.js";