feat(FN-2255): add plugin runtime discovery contracts
- Extend core/plugin-sdk types with runtime manifest metadata, runtime factory, and runtime registration exports - Add runtime validation in plugin manifest parsing, including runtimeId slug and semver checks - Add PluginLoader.getPluginRuntimes() and PluginRunner runtime cache/invalidation plumbing across plugin lifecycle events - Expand plugin loader/runner test coverage for runtime discovery and cache behavior, and document runtime registration in PLUGIN_AUTHORING.md
This commit is contained in:
@@ -15,6 +15,7 @@ import type {
|
||||
PluginToolDefinition,
|
||||
PluginRouteDefinition,
|
||||
PluginUiSlotDefinition,
|
||||
PluginRuntimeRegistration,
|
||||
PluginContext,
|
||||
} from "@fusion/core";
|
||||
import type { ToolDefinition } from "@mariozechner/pi-coding-agent";
|
||||
@@ -60,6 +61,14 @@ interface CachedUiSlots {
|
||||
version: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cached runtimes - rebuilt when plugin state changes
|
||||
*/
|
||||
interface CachedRuntimes {
|
||||
runtimes: Array<{ pluginId: string; runtime: PluginRuntimeRegistration }>;
|
||||
version: number;
|
||||
}
|
||||
|
||||
const DEFAULT_HOOK_TIMEOUT_MS = 5000;
|
||||
|
||||
export class PluginRunner {
|
||||
@@ -67,9 +76,11 @@ export class PluginRunner {
|
||||
private cachedTools: CachedTools | null = null;
|
||||
private cachedRoutes: CachedRoutes | null = null;
|
||||
private cachedUiSlots: CachedUiSlots | null = null;
|
||||
private cachedRuntimes: CachedRuntimes | null = null;
|
||||
private toolsCacheVersion = 0;
|
||||
private routesCacheVersion = 0;
|
||||
private uiSlotsCacheVersion = 0;
|
||||
private runtimesCacheVersion = 0;
|
||||
private hookTimeoutMs: number;
|
||||
|
||||
// Event handler references for cleanup
|
||||
@@ -126,6 +137,7 @@ export class PluginRunner {
|
||||
this.invalidateToolsCache();
|
||||
this.invalidateRoutesCache();
|
||||
this.invalidateUiSlotsCache();
|
||||
this.invalidateRuntimesCache();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -208,6 +220,20 @@ export class PluginRunner {
|
||||
return this.cachedUiSlots.slots;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all runtime registrations from loaded plugins.
|
||||
* Runtimes are cached and only rebuilt when plugin state changes.
|
||||
*/
|
||||
getPluginRuntimes(): Array<{ pluginId: string; runtime: PluginRuntimeRegistration }> {
|
||||
if (!this.cachedRuntimes || this.cachedRuntimes.version !== this.runtimesCacheVersion) {
|
||||
this.cachedRuntimes = {
|
||||
runtimes: this.options.pluginLoader.getPluginRuntimes(),
|
||||
version: this.runtimesCacheVersion,
|
||||
};
|
||||
}
|
||||
return this.cachedRuntimes.runtimes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the underlying plugin loader.
|
||||
*/
|
||||
@@ -224,7 +250,7 @@ export class PluginRunner {
|
||||
|
||||
/**
|
||||
* Reload a plugin: stop the old instance, re-import, and start the new one.
|
||||
* This invalidates the tools and routes caches.
|
||||
* This invalidates the tools, routes, uiSlots, and runtimes caches.
|
||||
*/
|
||||
async reloadPlugin(pluginId: string): Promise<void> {
|
||||
executorLog.log(`Reloading plugin: ${pluginId}`);
|
||||
@@ -232,6 +258,7 @@ export class PluginRunner {
|
||||
this.invalidateToolsCache();
|
||||
this.invalidateRoutesCache();
|
||||
this.invalidateUiSlotsCache();
|
||||
this.invalidateRuntimesCache();
|
||||
executorLog.log(`Plugin ${pluginId} reloaded`);
|
||||
}
|
||||
|
||||
@@ -245,6 +272,7 @@ export class PluginRunner {
|
||||
this.invalidateToolsCache();
|
||||
this.invalidateRoutesCache();
|
||||
this.invalidateUiSlotsCache();
|
||||
this.invalidateRuntimesCache();
|
||||
|
||||
try {
|
||||
executorLog.log(`Auto-loading enabled plugin: ${plugin.id}`);
|
||||
@@ -263,6 +291,7 @@ export class PluginRunner {
|
||||
this.invalidateToolsCache();
|
||||
this.invalidateRoutesCache();
|
||||
this.invalidateUiSlotsCache();
|
||||
this.invalidateRuntimesCache();
|
||||
|
||||
try {
|
||||
executorLog.log(`Auto-stopping disabled plugin: ${plugin.id}`);
|
||||
@@ -281,6 +310,7 @@ export class PluginRunner {
|
||||
this.invalidateToolsCache();
|
||||
this.invalidateRoutesCache();
|
||||
this.invalidateUiSlotsCache();
|
||||
this.invalidateRuntimesCache();
|
||||
|
||||
try {
|
||||
executorLog.log(`Stopping unregistered plugin: ${plugin.id}`);
|
||||
@@ -298,6 +328,7 @@ export class PluginRunner {
|
||||
this.invalidateToolsCache();
|
||||
this.invalidateRoutesCache();
|
||||
this.invalidateUiSlotsCache();
|
||||
this.invalidateRuntimesCache();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -307,6 +338,7 @@ export class PluginRunner {
|
||||
this.invalidateToolsCache();
|
||||
this.invalidateRoutesCache();
|
||||
this.invalidateUiSlotsCache();
|
||||
this.invalidateRuntimesCache();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -316,6 +348,7 @@ export class PluginRunner {
|
||||
this.invalidateToolsCache();
|
||||
this.invalidateRoutesCache();
|
||||
this.invalidateUiSlotsCache();
|
||||
this.invalidateRuntimesCache();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -325,6 +358,7 @@ export class PluginRunner {
|
||||
this.invalidateToolsCache();
|
||||
this.invalidateRoutesCache();
|
||||
this.invalidateUiSlotsCache();
|
||||
this.invalidateRuntimesCache();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -334,6 +368,7 @@ export class PluginRunner {
|
||||
this.invalidateToolsCache();
|
||||
this.invalidateRoutesCache();
|
||||
this.invalidateUiSlotsCache();
|
||||
this.invalidateRuntimesCache();
|
||||
}
|
||||
|
||||
// ── Tool Conversion ───────────────────────────────────────────────
|
||||
@@ -502,6 +537,14 @@ export class PluginRunner {
|
||||
this.log.log(`UI slots cache invalidated (version: ${this.uiSlotsCacheVersion})`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate the runtimes cache, forcing rebuild on next access.
|
||||
*/
|
||||
private invalidateRuntimesCache(): void {
|
||||
this.runtimesCacheVersion++;
|
||||
this.log.log(`Runtimes cache invalidated (version: ${this.runtimesCacheVersion})`);
|
||||
}
|
||||
|
||||
// ── Store Event Subscriptions ────────────────────────────────────
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user