feat(FN-3094): add cached plugin contribution accessors
- Add PluginRunner contribution cache plumbing to store and reuse plugin-provided contributions - Expose new PluginLoader accessors for reading cached contributions during runtime workflows - Add core plugin-loader tests covering contribution accessor behavior and edge cases - Add engine plugin-runner tests validating contribution cache population and retrieval Fusion-Task-Id: FN-3094
This commit is contained in:
@@ -1511,6 +1511,107 @@ describe("PluginLoader", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── new plugin contribution accessors ───────────────────────────────
|
||||||
|
|
||||||
|
describe("new contribution accessors", () => {
|
||||||
|
it("returns empty arrays when no contribution types are present", async () => {
|
||||||
|
await pluginStore.init();
|
||||||
|
loader = new PluginLoader({ pluginStore, taskStore: mockTaskStore });
|
||||||
|
expect(loader.getPluginSkills()).toEqual([]);
|
||||||
|
expect(loader.getPluginWorkflowSteps()).toEqual([]);
|
||||||
|
expect(loader.getPluginPromptContributions()).toEqual([]);
|
||||||
|
expect(loader.getPluginSetupInfo()).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("getPluginSkills returns skills with pluginId", async () => {
|
||||||
|
await pluginStore.init();
|
||||||
|
loader = new PluginLoader({ pluginStore, taskStore: mockTaskStore });
|
||||||
|
(loader as any).plugins.set("skills-plugin", {
|
||||||
|
manifest: makeManifest({ id: "skills-plugin" }),
|
||||||
|
state: "started",
|
||||||
|
hooks: {},
|
||||||
|
skills: [{ skillId: "browser", name: "Browser", description: "Web", skillFiles: ["./SKILL.md"] }],
|
||||||
|
} as FusionPlugin);
|
||||||
|
expect(loader.getPluginSkills()).toEqual([
|
||||||
|
{
|
||||||
|
pluginId: "skills-plugin",
|
||||||
|
skill: { skillId: "browser", name: "Browser", description: "Web", skillFiles: ["./SKILL.md"] },
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns workflow steps, prompt contributions, and setup info", async () => {
|
||||||
|
await pluginStore.init();
|
||||||
|
loader = new PluginLoader({ pluginStore, taskStore: mockTaskStore });
|
||||||
|
const checkSetup = vi.fn().mockResolvedValue({ status: "installed" });
|
||||||
|
(loader as any).plugins.set("contrib-plugin", {
|
||||||
|
manifest: makeManifest({ id: "contrib-plugin" }),
|
||||||
|
state: "started",
|
||||||
|
hooks: {},
|
||||||
|
workflowSteps: [{ stepId: "wf", name: "WF", description: "desc", mode: "prompt", prompt: "check" }],
|
||||||
|
promptContributions: {
|
||||||
|
enabledByDefault: true,
|
||||||
|
contributions: [{ surface: "executor-system", content: "inject" }],
|
||||||
|
},
|
||||||
|
setup: {
|
||||||
|
manifest: { binaryName: "agent-browser", description: "Binary" },
|
||||||
|
hooks: { checkSetup },
|
||||||
|
},
|
||||||
|
} as FusionPlugin);
|
||||||
|
|
||||||
|
expect(loader.getPluginWorkflowSteps()).toEqual([
|
||||||
|
{
|
||||||
|
pluginId: "contrib-plugin",
|
||||||
|
step: { stepId: "wf", name: "WF", description: "desc", mode: "prompt", prompt: "check" },
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
expect(loader.getPluginPromptContributions()).toEqual([
|
||||||
|
{
|
||||||
|
pluginId: "contrib-plugin",
|
||||||
|
contribution: { surface: "executor-system", content: "inject" },
|
||||||
|
config: {
|
||||||
|
enabledByDefault: true,
|
||||||
|
contributions: [{ surface: "executor-system", content: "inject" }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
expect(loader.getPluginSetupInfo()).toEqual([
|
||||||
|
{
|
||||||
|
pluginId: "contrib-plugin",
|
||||||
|
manifest: { binaryName: "agent-browser", description: "Binary" },
|
||||||
|
hooks: { checkSetup },
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("stopped or unloaded plugins are not included", async () => {
|
||||||
|
await pluginStore.init();
|
||||||
|
loader = new PluginLoader({ pluginStore, taskStore: mockTaskStore });
|
||||||
|
(loader as any).plugins.set("started-plugin", {
|
||||||
|
manifest: makeManifest({ id: "started-plugin" }),
|
||||||
|
state: "started",
|
||||||
|
hooks: {},
|
||||||
|
skills: [{ skillId: "a", name: "A", description: "A", skillFiles: ["./a.md"] }],
|
||||||
|
} as FusionPlugin);
|
||||||
|
(loader as any).plugins.set("stopped-plugin", {
|
||||||
|
manifest: makeManifest({ id: "stopped-plugin" }),
|
||||||
|
state: "stopped",
|
||||||
|
hooks: {},
|
||||||
|
skills: [{ skillId: "b", name: "B", description: "B", skillFiles: ["./b.md"] }],
|
||||||
|
} as FusionPlugin);
|
||||||
|
|
||||||
|
const filtered = loader.getPluginSkills().filter((entry) => {
|
||||||
|
const plugin = loader.getPlugin(entry.pluginId);
|
||||||
|
return plugin?.state === "started";
|
||||||
|
});
|
||||||
|
expect(filtered).toHaveLength(1);
|
||||||
|
expect(filtered[0].pluginId).toBe("started-plugin");
|
||||||
|
|
||||||
|
(loader as any).plugins.delete("stopped-plugin");
|
||||||
|
expect(loader.getPluginSkills().map((entry) => entry.pluginId)).toEqual(["started-plugin"]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// ── getLoadedPlugins ───────────────────────────────────────────────
|
// ── getLoadedPlugins ───────────────────────────────────────────────
|
||||||
|
|
||||||
describe("getLoadedPlugins", () => {
|
describe("getLoadedPlugins", () => {
|
||||||
|
|||||||
@@ -24,6 +24,12 @@ import type {
|
|||||||
PluginUiSlotDefinition,
|
PluginUiSlotDefinition,
|
||||||
PluginRuntimeRegistration,
|
PluginRuntimeRegistration,
|
||||||
PluginInstallation,
|
PluginInstallation,
|
||||||
|
PluginSkillContribution,
|
||||||
|
PluginWorkflowStepContribution,
|
||||||
|
PluginPromptContribution,
|
||||||
|
PluginPromptContributions,
|
||||||
|
PluginSetupManifest,
|
||||||
|
PluginSetupHooks,
|
||||||
} from "./plugin-types.js";
|
} from "./plugin-types.js";
|
||||||
import { validatePluginManifest } from "./plugin-types.js";
|
import { validatePluginManifest } from "./plugin-types.js";
|
||||||
import { createLogger } from "./logger.js";
|
import { createLogger } from "./logger.js";
|
||||||
@@ -783,6 +789,72 @@ export class PluginLoader extends EventEmitter<{
|
|||||||
return runtimes;
|
return runtimes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all skill contributions from loaded plugins.
|
||||||
|
*/
|
||||||
|
getPluginSkills(): Array<{ pluginId: string; skill: PluginSkillContribution }> {
|
||||||
|
const skills: Array<{ pluginId: string; skill: PluginSkillContribution }> = [];
|
||||||
|
for (const [pluginId, plugin] of this.plugins) {
|
||||||
|
if (plugin.skills) {
|
||||||
|
for (const skill of plugin.skills) {
|
||||||
|
skills.push({ pluginId, skill });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return skills;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all workflow step contributions from loaded plugins.
|
||||||
|
*/
|
||||||
|
getPluginWorkflowSteps(): Array<{ pluginId: string; step: PluginWorkflowStepContribution }> {
|
||||||
|
const steps: Array<{ pluginId: string; step: PluginWorkflowStepContribution }> = [];
|
||||||
|
for (const [pluginId, plugin] of this.plugins) {
|
||||||
|
if (plugin.workflowSteps) {
|
||||||
|
for (const step of plugin.workflowSteps) {
|
||||||
|
steps.push({ pluginId, step });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return steps;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all prompt contributions from loaded plugins.
|
||||||
|
*/
|
||||||
|
getPluginPromptContributions(): Array<{
|
||||||
|
pluginId: string;
|
||||||
|
contribution: PluginPromptContribution;
|
||||||
|
config: PluginPromptContributions;
|
||||||
|
}> {
|
||||||
|
const contributions: Array<{
|
||||||
|
pluginId: string;
|
||||||
|
contribution: PluginPromptContribution;
|
||||||
|
config: PluginPromptContributions;
|
||||||
|
}> = [];
|
||||||
|
for (const [pluginId, plugin] of this.plugins) {
|
||||||
|
if (plugin.promptContributions) {
|
||||||
|
for (const contribution of plugin.promptContributions.contributions) {
|
||||||
|
contributions.push({ pluginId, contribution, config: plugin.promptContributions });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return contributions;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all setup metadata and hooks from loaded plugins.
|
||||||
|
*/
|
||||||
|
getPluginSetupInfo(): Array<{ pluginId: string; manifest: PluginSetupManifest; hooks: PluginSetupHooks }> {
|
||||||
|
const setups: Array<{ pluginId: string; manifest: PluginSetupManifest; hooks: PluginSetupHooks }> = [];
|
||||||
|
for (const [pluginId, plugin] of this.plugins) {
|
||||||
|
if (plugin.setup) {
|
||||||
|
setups.push({ pluginId, manifest: plugin.setup.manifest, hooks: plugin.setup.hooks });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return setups;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all loaded plugin instances.
|
* Get all loaded plugin instances.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -34,6 +34,10 @@ describe("PluginRunner", () => {
|
|||||||
getPluginRoutes: ReturnType<typeof vi.fn>;
|
getPluginRoutes: ReturnType<typeof vi.fn>;
|
||||||
getPluginUiSlots: ReturnType<typeof vi.fn>;
|
getPluginUiSlots: ReturnType<typeof vi.fn>;
|
||||||
getPluginRuntimes: ReturnType<typeof vi.fn>;
|
getPluginRuntimes: ReturnType<typeof vi.fn>;
|
||||||
|
getPluginSkills: ReturnType<typeof vi.fn>;
|
||||||
|
getPluginWorkflowSteps: ReturnType<typeof vi.fn>;
|
||||||
|
getPluginPromptContributions: ReturnType<typeof vi.fn>;
|
||||||
|
getPluginSetupInfo: ReturnType<typeof vi.fn>;
|
||||||
getLoadedPlugins: ReturnType<typeof vi.fn>;
|
getLoadedPlugins: ReturnType<typeof vi.fn>;
|
||||||
getPlugin: ReturnType<typeof vi.fn>;
|
getPlugin: ReturnType<typeof vi.fn>;
|
||||||
loadPlugin: ReturnType<typeof vi.fn>;
|
loadPlugin: ReturnType<typeof vi.fn>;
|
||||||
@@ -87,6 +91,10 @@ describe("PluginRunner", () => {
|
|||||||
getPluginRoutes: vi.fn().mockReturnValue([]),
|
getPluginRoutes: vi.fn().mockReturnValue([]),
|
||||||
getPluginUiSlots: vi.fn().mockReturnValue([]),
|
getPluginUiSlots: vi.fn().mockReturnValue([]),
|
||||||
getPluginRuntimes: vi.fn().mockReturnValue([]),
|
getPluginRuntimes: vi.fn().mockReturnValue([]),
|
||||||
|
getPluginSkills: vi.fn().mockReturnValue([]),
|
||||||
|
getPluginWorkflowSteps: vi.fn().mockReturnValue([]),
|
||||||
|
getPluginPromptContributions: vi.fn().mockReturnValue([]),
|
||||||
|
getPluginSetupInfo: vi.fn().mockReturnValue([]),
|
||||||
getLoadedPlugins: vi.fn().mockReturnValue([]),
|
getLoadedPlugins: vi.fn().mockReturnValue([]),
|
||||||
getPlugin: vi.fn(),
|
getPlugin: vi.fn(),
|
||||||
loadPlugin: vi.fn().mockResolvedValue({}),
|
loadPlugin: vi.fn().mockResolvedValue({}),
|
||||||
@@ -738,6 +746,85 @@ describe("PluginRunner", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("new plugin contribution accessors", () => {
|
||||||
|
it("getPluginSkills returns empty array initially", async () => {
|
||||||
|
await pluginRunner.init();
|
||||||
|
expect(pluginRunner.getPluginSkills()).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("getPluginSkills returns cached skills after init", async () => {
|
||||||
|
const skills = [{ pluginId: "test-plugin", skill: { skillId: "s1", name: "Skill", description: "d", skillFiles: ["./skill.md"] } }];
|
||||||
|
mockPluginLoader.getPluginSkills.mockReturnValue(skills);
|
||||||
|
await pluginRunner.init();
|
||||||
|
const first = pluginRunner.getPluginSkills();
|
||||||
|
const second = pluginRunner.getPluginSkills();
|
||||||
|
expect(first).toEqual(skills);
|
||||||
|
expect(second).toBe(first);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns workflow steps, prompt contributions, and setup info", async () => {
|
||||||
|
const steps = [{ pluginId: "test-plugin", step: { stepId: "ws1", name: "Step", description: "d", mode: "prompt", prompt: "Run checks" } }];
|
||||||
|
const prompts = [{ pluginId: "test-plugin", contribution: { surface: "executor-system", content: "extra" }, config: { enabledByDefault: true, contributions: [] } }];
|
||||||
|
const setups = [{ pluginId: "test-plugin", manifest: { binaryName: "agent-browser", description: "Do it" }, hooks: { checkSetup: vi.fn().mockResolvedValue({ status: "installed" }) } }];
|
||||||
|
mockPluginLoader.getPluginWorkflowSteps.mockReturnValue(steps);
|
||||||
|
mockPluginLoader.getPluginPromptContributions.mockReturnValue(prompts);
|
||||||
|
mockPluginLoader.getPluginSetupInfo.mockReturnValue(setups);
|
||||||
|
await pluginRunner.init();
|
||||||
|
expect(pluginRunner.getPluginWorkflowSteps()).toEqual(steps);
|
||||||
|
expect(pluginRunner.getPluginPromptContributions()).toEqual(prompts);
|
||||||
|
expect(pluginRunner.getPluginSetupInfo()).toEqual(setups);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("getPromptContributionsForSurface filters by surface", async () => {
|
||||||
|
mockPluginLoader.getPluginPromptContributions.mockReturnValue([
|
||||||
|
{ pluginId: "test-plugin", contribution: { surface: "executor-system", content: "ok" }, config: { enabledByDefault: true, contributions: [] } },
|
||||||
|
{ pluginId: "test-plugin", contribution: { surface: "triage", content: "skip" }, config: { enabledByDefault: true, contributions: [] } },
|
||||||
|
]);
|
||||||
|
mockPluginLoader.getPlugin.mockReturnValue(createMockPlugin({ state: "started" }));
|
||||||
|
await pluginRunner.init();
|
||||||
|
const filtered = pluginRunner.getPromptContributionsForSurface("executor-system");
|
||||||
|
expect(filtered).toHaveLength(1);
|
||||||
|
expect(filtered[0].contribution.content).toBe("ok");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("getPromptContributionsForSurface returns empty when no matches", async () => {
|
||||||
|
mockPluginLoader.getPluginPromptContributions.mockReturnValue([
|
||||||
|
{ pluginId: "test-plugin", contribution: { surface: "executor-system", content: "disabled" }, config: { enabledByDefault: false, contributions: [] } },
|
||||||
|
]);
|
||||||
|
mockPluginLoader.getPlugin.mockReturnValue(createMockPlugin({ state: "started" }));
|
||||||
|
await pluginRunner.init();
|
||||||
|
expect(pluginRunner.getPromptContributionsForSurface("reviewer")).toEqual([]);
|
||||||
|
expect(pluginRunner.getPromptContributionsForSurface("executor-system")).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("invalidates new contribution caches on state change and loader events", async () => {
|
||||||
|
await pluginRunner.init();
|
||||||
|
pluginRunner.getPluginSkills();
|
||||||
|
pluginRunner.getPluginWorkflowSteps();
|
||||||
|
pluginRunner.getPluginPromptContributions();
|
||||||
|
pluginRunner.getPluginSetupInfo();
|
||||||
|
|
||||||
|
const stateChanged = mockPluginStore.on.mock.calls.find((call) => call[0] === "plugin:stateChanged")?.[1];
|
||||||
|
stateChanged?.();
|
||||||
|
pluginRunner.getPluginSkills();
|
||||||
|
pluginRunner.getPluginWorkflowSteps();
|
||||||
|
pluginRunner.getPluginPromptContributions();
|
||||||
|
pluginRunner.getPluginSetupInfo();
|
||||||
|
|
||||||
|
const loaded = mockPluginLoader.on.mock.calls.find((call) => call[0] === "plugin:loaded")?.[1];
|
||||||
|
loaded?.({ pluginId: "test-plugin" });
|
||||||
|
pluginRunner.getPluginSkills();
|
||||||
|
pluginRunner.getPluginWorkflowSteps();
|
||||||
|
pluginRunner.getPluginPromptContributions();
|
||||||
|
pluginRunner.getPluginSetupInfo();
|
||||||
|
|
||||||
|
expect(mockPluginLoader.getPluginSkills).toHaveBeenCalledTimes(3);
|
||||||
|
expect(mockPluginLoader.getPluginWorkflowSteps).toHaveBeenCalledTimes(3);
|
||||||
|
expect(mockPluginLoader.getPluginPromptContributions).toHaveBeenCalledTimes(3);
|
||||||
|
expect(mockPluginLoader.getPluginSetupInfo).toHaveBeenCalledTimes(3);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("getRuntimeById()", () => {
|
describe("getRuntimeById()", () => {
|
||||||
it("should return undefined when no runtimes exist", () => {
|
it("should return undefined when no runtimes exist", () => {
|
||||||
mockPluginLoader.getPluginRuntimes.mockReturnValue([]);
|
mockPluginLoader.getPluginRuntimes.mockReturnValue([]);
|
||||||
|
|||||||
@@ -17,6 +17,13 @@ import type {
|
|||||||
PluginUiSlotDefinition,
|
PluginUiSlotDefinition,
|
||||||
PluginRuntimeRegistration,
|
PluginRuntimeRegistration,
|
||||||
PluginContext,
|
PluginContext,
|
||||||
|
PluginSkillContribution,
|
||||||
|
PluginWorkflowStepContribution,
|
||||||
|
PluginPromptContribution,
|
||||||
|
PluginPromptContributions,
|
||||||
|
PluginPromptSurface,
|
||||||
|
PluginSetupManifest,
|
||||||
|
PluginSetupHooks,
|
||||||
} from "@fusion/core";
|
} from "@fusion/core";
|
||||||
import type { ToolDefinition } from "@mariozechner/pi-coding-agent";
|
import type { ToolDefinition } from "@mariozechner/pi-coding-agent";
|
||||||
import { Type } from "@mariozechner/pi-ai";
|
import { Type } from "@mariozechner/pi-ai";
|
||||||
@@ -69,6 +76,30 @@ interface CachedRuntimes {
|
|||||||
version: number;
|
version: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface CachedSkills {
|
||||||
|
skills: Array<{ pluginId: string; skill: PluginSkillContribution }>;
|
||||||
|
version: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CachedWorkflowSteps {
|
||||||
|
steps: Array<{ pluginId: string; step: PluginWorkflowStepContribution }>;
|
||||||
|
version: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CachedPromptContributions {
|
||||||
|
contributions: Array<{
|
||||||
|
pluginId: string;
|
||||||
|
contribution: PluginPromptContribution;
|
||||||
|
config: PluginPromptContributions;
|
||||||
|
}>;
|
||||||
|
version: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CachedSetupInfo {
|
||||||
|
setups: Array<{ pluginId: string; manifest: PluginSetupManifest; hooks: PluginSetupHooks }>;
|
||||||
|
version: number;
|
||||||
|
}
|
||||||
|
|
||||||
const DEFAULT_HOOK_TIMEOUT_MS = 5000;
|
const DEFAULT_HOOK_TIMEOUT_MS = 5000;
|
||||||
|
|
||||||
export class PluginRunner {
|
export class PluginRunner {
|
||||||
@@ -77,10 +108,18 @@ export class PluginRunner {
|
|||||||
private cachedRoutes: CachedRoutes | null = null;
|
private cachedRoutes: CachedRoutes | null = null;
|
||||||
private cachedUiSlots: CachedUiSlots | null = null;
|
private cachedUiSlots: CachedUiSlots | null = null;
|
||||||
private cachedRuntimes: CachedRuntimes | null = null;
|
private cachedRuntimes: CachedRuntimes | null = null;
|
||||||
|
private cachedSkills: CachedSkills | null = null;
|
||||||
|
private cachedWorkflowSteps: CachedWorkflowSteps | null = null;
|
||||||
|
private cachedPromptContributions: CachedPromptContributions | null = null;
|
||||||
|
private cachedSetupInfo: CachedSetupInfo | null = null;
|
||||||
private toolsCacheVersion = 0;
|
private toolsCacheVersion = 0;
|
||||||
private routesCacheVersion = 0;
|
private routesCacheVersion = 0;
|
||||||
private uiSlotsCacheVersion = 0;
|
private uiSlotsCacheVersion = 0;
|
||||||
private runtimesCacheVersion = 0;
|
private runtimesCacheVersion = 0;
|
||||||
|
private skillsCacheVersion = 0;
|
||||||
|
private workflowStepsCacheVersion = 0;
|
||||||
|
private promptContributionsCacheVersion = 0;
|
||||||
|
private setupCacheVersion = 0;
|
||||||
private hookTimeoutMs: number;
|
private hookTimeoutMs: number;
|
||||||
|
|
||||||
// Event handler references for cleanup
|
// Event handler references for cleanup
|
||||||
@@ -138,6 +177,10 @@ export class PluginRunner {
|
|||||||
this.invalidateRoutesCache();
|
this.invalidateRoutesCache();
|
||||||
this.invalidateUiSlotsCache();
|
this.invalidateUiSlotsCache();
|
||||||
this.invalidateRuntimesCache();
|
this.invalidateRuntimesCache();
|
||||||
|
this.invalidateSkillsCache();
|
||||||
|
this.invalidateWorkflowStepsCache();
|
||||||
|
this.invalidatePromptContributionsCache();
|
||||||
|
this.invalidateSetupCache();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -234,6 +277,67 @@ export class PluginRunner {
|
|||||||
return this.cachedRuntimes.runtimes;
|
return this.cachedRuntimes.runtimes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getPluginSkills(): Array<{ pluginId: string; skill: PluginSkillContribution }> {
|
||||||
|
if (!this.cachedSkills || this.cachedSkills.version !== this.skillsCacheVersion) {
|
||||||
|
this.cachedSkills = {
|
||||||
|
skills: this.options.pluginLoader.getPluginSkills(),
|
||||||
|
version: this.skillsCacheVersion,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return this.cachedSkills.skills;
|
||||||
|
}
|
||||||
|
|
||||||
|
getPluginWorkflowSteps(): Array<{ pluginId: string; step: PluginWorkflowStepContribution }> {
|
||||||
|
if (!this.cachedWorkflowSteps || this.cachedWorkflowSteps.version !== this.workflowStepsCacheVersion) {
|
||||||
|
this.cachedWorkflowSteps = {
|
||||||
|
steps: this.options.pluginLoader.getPluginWorkflowSteps(),
|
||||||
|
version: this.workflowStepsCacheVersion,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return this.cachedWorkflowSteps.steps;
|
||||||
|
}
|
||||||
|
|
||||||
|
getPluginPromptContributions(): Array<{
|
||||||
|
pluginId: string;
|
||||||
|
contribution: PluginPromptContribution;
|
||||||
|
config: PluginPromptContributions;
|
||||||
|
}> {
|
||||||
|
if (!this.cachedPromptContributions || this.cachedPromptContributions.version !== this.promptContributionsCacheVersion) {
|
||||||
|
this.cachedPromptContributions = {
|
||||||
|
contributions: this.options.pluginLoader.getPluginPromptContributions(),
|
||||||
|
version: this.promptContributionsCacheVersion,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return this.cachedPromptContributions.contributions;
|
||||||
|
}
|
||||||
|
|
||||||
|
getPluginSetupInfo(): Array<{ pluginId: string; manifest: PluginSetupManifest; hooks: PluginSetupHooks }> {
|
||||||
|
if (!this.cachedSetupInfo || this.cachedSetupInfo.version !== this.setupCacheVersion) {
|
||||||
|
this.cachedSetupInfo = {
|
||||||
|
setups: this.options.pluginLoader.getPluginSetupInfo(),
|
||||||
|
version: this.setupCacheVersion,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return this.cachedSetupInfo.setups;
|
||||||
|
}
|
||||||
|
|
||||||
|
getPromptContributionsForSurface(surface: PluginPromptSurface): Array<{
|
||||||
|
pluginId: string;
|
||||||
|
contribution: PluginPromptContribution;
|
||||||
|
config: PluginPromptContributions;
|
||||||
|
}> {
|
||||||
|
return this.getPluginPromptContributions().filter(({ pluginId, contribution, config }) => {
|
||||||
|
const plugin = this.options.pluginLoader.getPlugin(pluginId);
|
||||||
|
if (!plugin || plugin.state !== "started") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (contribution.surface !== surface) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return config.enabledByDefault !== false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a specific runtime registration by its runtimeId.
|
* Get a specific runtime registration by its runtimeId.
|
||||||
*
|
*
|
||||||
@@ -270,6 +374,10 @@ export class PluginRunner {
|
|||||||
this.invalidateRoutesCache();
|
this.invalidateRoutesCache();
|
||||||
this.invalidateUiSlotsCache();
|
this.invalidateUiSlotsCache();
|
||||||
this.invalidateRuntimesCache();
|
this.invalidateRuntimesCache();
|
||||||
|
this.invalidateSkillsCache();
|
||||||
|
this.invalidateWorkflowStepsCache();
|
||||||
|
this.invalidatePromptContributionsCache();
|
||||||
|
this.invalidateSetupCache();
|
||||||
executorLog.log(`Plugin ${pluginId} reloaded`);
|
executorLog.log(`Plugin ${pluginId} reloaded`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -284,6 +392,10 @@ export class PluginRunner {
|
|||||||
this.invalidateRoutesCache();
|
this.invalidateRoutesCache();
|
||||||
this.invalidateUiSlotsCache();
|
this.invalidateUiSlotsCache();
|
||||||
this.invalidateRuntimesCache();
|
this.invalidateRuntimesCache();
|
||||||
|
this.invalidateSkillsCache();
|
||||||
|
this.invalidateWorkflowStepsCache();
|
||||||
|
this.invalidatePromptContributionsCache();
|
||||||
|
this.invalidateSetupCache();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
executorLog.log(`Auto-loading enabled plugin: ${plugin.id}`);
|
executorLog.log(`Auto-loading enabled plugin: ${plugin.id}`);
|
||||||
@@ -303,6 +415,10 @@ export class PluginRunner {
|
|||||||
this.invalidateRoutesCache();
|
this.invalidateRoutesCache();
|
||||||
this.invalidateUiSlotsCache();
|
this.invalidateUiSlotsCache();
|
||||||
this.invalidateRuntimesCache();
|
this.invalidateRuntimesCache();
|
||||||
|
this.invalidateSkillsCache();
|
||||||
|
this.invalidateWorkflowStepsCache();
|
||||||
|
this.invalidatePromptContributionsCache();
|
||||||
|
this.invalidateSetupCache();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
executorLog.log(`Auto-stopping disabled plugin: ${plugin.id}`);
|
executorLog.log(`Auto-stopping disabled plugin: ${plugin.id}`);
|
||||||
@@ -322,6 +438,10 @@ export class PluginRunner {
|
|||||||
this.invalidateRoutesCache();
|
this.invalidateRoutesCache();
|
||||||
this.invalidateUiSlotsCache();
|
this.invalidateUiSlotsCache();
|
||||||
this.invalidateRuntimesCache();
|
this.invalidateRuntimesCache();
|
||||||
|
this.invalidateSkillsCache();
|
||||||
|
this.invalidateWorkflowStepsCache();
|
||||||
|
this.invalidatePromptContributionsCache();
|
||||||
|
this.invalidateSetupCache();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
executorLog.log(`Stopping unregistered plugin: ${plugin.id}`);
|
executorLog.log(`Stopping unregistered plugin: ${plugin.id}`);
|
||||||
@@ -340,6 +460,10 @@ export class PluginRunner {
|
|||||||
this.invalidateRoutesCache();
|
this.invalidateRoutesCache();
|
||||||
this.invalidateUiSlotsCache();
|
this.invalidateUiSlotsCache();
|
||||||
this.invalidateRuntimesCache();
|
this.invalidateRuntimesCache();
|
||||||
|
this.invalidateSkillsCache();
|
||||||
|
this.invalidateWorkflowStepsCache();
|
||||||
|
this.invalidatePromptContributionsCache();
|
||||||
|
this.invalidateSetupCache();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -350,6 +474,10 @@ export class PluginRunner {
|
|||||||
this.invalidateRoutesCache();
|
this.invalidateRoutesCache();
|
||||||
this.invalidateUiSlotsCache();
|
this.invalidateUiSlotsCache();
|
||||||
this.invalidateRuntimesCache();
|
this.invalidateRuntimesCache();
|
||||||
|
this.invalidateSkillsCache();
|
||||||
|
this.invalidateWorkflowStepsCache();
|
||||||
|
this.invalidatePromptContributionsCache();
|
||||||
|
this.invalidateSetupCache();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -360,6 +488,10 @@ export class PluginRunner {
|
|||||||
this.invalidateRoutesCache();
|
this.invalidateRoutesCache();
|
||||||
this.invalidateUiSlotsCache();
|
this.invalidateUiSlotsCache();
|
||||||
this.invalidateRuntimesCache();
|
this.invalidateRuntimesCache();
|
||||||
|
this.invalidateSkillsCache();
|
||||||
|
this.invalidateWorkflowStepsCache();
|
||||||
|
this.invalidatePromptContributionsCache();
|
||||||
|
this.invalidateSetupCache();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -370,6 +502,10 @@ export class PluginRunner {
|
|||||||
this.invalidateRoutesCache();
|
this.invalidateRoutesCache();
|
||||||
this.invalidateUiSlotsCache();
|
this.invalidateUiSlotsCache();
|
||||||
this.invalidateRuntimesCache();
|
this.invalidateRuntimesCache();
|
||||||
|
this.invalidateSkillsCache();
|
||||||
|
this.invalidateWorkflowStepsCache();
|
||||||
|
this.invalidatePromptContributionsCache();
|
||||||
|
this.invalidateSetupCache();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -380,6 +516,10 @@ export class PluginRunner {
|
|||||||
this.invalidateRoutesCache();
|
this.invalidateRoutesCache();
|
||||||
this.invalidateUiSlotsCache();
|
this.invalidateUiSlotsCache();
|
||||||
this.invalidateRuntimesCache();
|
this.invalidateRuntimesCache();
|
||||||
|
this.invalidateSkillsCache();
|
||||||
|
this.invalidateWorkflowStepsCache();
|
||||||
|
this.invalidatePromptContributionsCache();
|
||||||
|
this.invalidateSetupCache();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Tool Conversion ───────────────────────────────────────────────
|
// ── Tool Conversion ───────────────────────────────────────────────
|
||||||
@@ -585,6 +725,26 @@ export class PluginRunner {
|
|||||||
this.log.log(`Runtimes cache invalidated (version: ${this.runtimesCacheVersion})`);
|
this.log.log(`Runtimes cache invalidated (version: ${this.runtimesCacheVersion})`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private invalidateSkillsCache(): void {
|
||||||
|
this.skillsCacheVersion++;
|
||||||
|
this.log.log(`Skills cache invalidated (version: ${this.skillsCacheVersion})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
private invalidateWorkflowStepsCache(): void {
|
||||||
|
this.workflowStepsCacheVersion++;
|
||||||
|
this.log.log(`Workflow steps cache invalidated (version: ${this.workflowStepsCacheVersion})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
private invalidatePromptContributionsCache(): void {
|
||||||
|
this.promptContributionsCacheVersion++;
|
||||||
|
this.log.log(`Prompt contributions cache invalidated (version: ${this.promptContributionsCacheVersion})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
private invalidateSetupCache(): void {
|
||||||
|
this.setupCacheVersion++;
|
||||||
|
this.log.log(`Setup cache invalidated (version: ${this.setupCacheVersion})`);
|
||||||
|
}
|
||||||
|
|
||||||
// ── Store Event Subscriptions ────────────────────────────────────
|
// ── Store Event Subscriptions ────────────────────────────────────
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user