feat(FN-1401): integrate PluginRunner into runtime lifecycle

- Integrate PluginRunner into InProcessRuntime for plugin lifecycle management
- Wire plugin:created, plugin:updated, plugin:deleted events through runtime event bus
- Add plugin tools to StepSessionExecutor so plugins can provide tools during step execution
- Fix PluginRunner to pass PluginContext to task lifecycle hooks (onTaskCreated, onTaskUpdated, etc.)
- Add PluginRunner tests covering init/shutdown, timeout isolation, and sync event behavior
- Add InProcessRuntime tests verifying plugin integration
- Update memory documentation with plugin runner lifecycle wiring
This commit is contained in:
gsxdsm
2026-04-10 02:29:47 -07:00
parent 3c085eed64
commit 0523089615
8 changed files with 601 additions and 11 deletions

View File

@@ -38,9 +38,27 @@ vi.mock("@fusion/core", async () => {
emit: vi.fn(),
});
self.on = vi.fn().mockReturnValue(self);
self.off = vi.fn();
self.emit = vi.fn().mockReturnValue(true);
return self;
}),
PluginStore: vi.fn().mockImplementation(function() {
const self = {} as Record<string, unknown>;
self.init = vi.fn().mockResolvedValue(undefined);
self.getPlugin = vi.fn().mockResolvedValue({});
self.on = vi.fn();
self.off = vi.fn();
return self;
}),
PluginLoader: vi.fn().mockImplementation(function() {
const self = {} as Record<string, unknown>;
self.loadAllPlugins = vi.fn().mockResolvedValue({ loaded: 0, errors: 0 });
self.stopAllPlugins = vi.fn().mockResolvedValue(undefined);
self.getLoadedPlugins = vi.fn().mockReturnValue([]);
self.on = vi.fn();
self.off = vi.fn();
return self;
}),
};
});
@@ -79,6 +97,18 @@ vi.mock("../self-healing.js", async () => {
};
});
// Mock the plugin runner
vi.mock("../plugin-runner.js", async () => {
return {
PluginRunner: vi.fn().mockImplementation(() => ({
init: vi.fn().mockResolvedValue(undefined),
shutdown: vi.fn().mockResolvedValue(undefined),
getPluginTools: vi.fn().mockReturnValue([]),
getPluginRoutes: vi.fn().mockReturnValue([]),
})),
};
});
// Mock the executor
vi.mock("../executor.js", async () => {
return {

View File

@@ -6,6 +6,8 @@ import type {
AgentStore,
HeartbeatInvocationSource,
AgentHeartbeatRun,
PluginStore,
PluginLoader,
} from "@fusion/core";
import { Scheduler } from "../scheduler.js";
import { TaskExecutor, type TaskExecutorOptions } from "../executor.js";
@@ -23,6 +25,7 @@ import { runtimeLog } from "../logger.js";
import { StuckTaskDetector } from "../stuck-task-detector.js";
import type { UsageLimitPauser } from "../usage-limit-detector.js";
import { SelfHealingManager } from "../self-healing.js";
import { PluginRunner } from "../plugin-runner.js";
import { MissionAutopilot } from "../mission-autopilot.js";
/**
@@ -76,6 +79,9 @@ export class InProcessRuntime
/** Maps task IDs to agent IDs for lifecycle tracking */
private taskAgentMap = new Map<string, string>();
private lastActivityAt: string = new Date().toISOString();
private pluginRunner?: PluginRunner;
private pluginStore?: PluginStore;
private pluginLoader?: PluginLoader;
/**
* @param config - Runtime configuration
@@ -111,12 +117,30 @@ export class InProcessRuntime
try {
// 1. Initialize TaskStore
const { TaskStore } = await import("@fusion/core");
const { TaskStore, PluginStore: PluginStoreClass, PluginLoader: PluginLoaderClass } = await import("@fusion/core");
this.taskStore = new TaskStore(this.config.workingDirectory);
await this.taskStore.init();
runtimeLog.log(`TaskStore initialized for project ${this.config.projectId}`);
// 2. Initialize WorktreePool
// 2. Initialize Plugin system (PluginStore + PluginLoader + PluginRunner)
this.pluginStore = new PluginStoreClass(this.taskStore.getFusionDir());
await this.pluginStore.init();
this.pluginLoader = new PluginLoaderClass({
pluginStore: this.pluginStore,
taskStore: this.taskStore,
});
this.pluginRunner = new PluginRunner({
pluginLoader: this.pluginLoader,
pluginStore: this.pluginStore,
taskStore: this.taskStore,
rootDir: this.config.workingDirectory,
});
await this.pluginRunner.init();
runtimeLog.log(`PluginRunner initialized`);
// 3. Initialize WorktreePool
this.worktreePool = new WorktreePool();
// Rehydrate pool from disk state (idle worktrees)
@@ -132,11 +156,11 @@ export class InProcessRuntime
);
}
// 3. Initialize global semaphore from CentralCore
// 4. Initialize global semaphore from CentralCore
const globalLimit = await this.getGlobalConcurrencyLimit();
this.globalSemaphore = new AgentSemaphore(() => globalLimit);
// 4. Initialize Scheduler
// 5. Initialize Scheduler
const missionStore = this.taskStore.getMissionStore();
const missionAutopilot = missionStore
? new MissionAutopilot(this.taskStore, missionStore)
@@ -180,6 +204,7 @@ export class InProcessRuntime
pool: this.worktreePool,
usageLimitPauser: this.usageLimitPauser,
stuckTaskDetector: this.stuckTaskDetector,
pluginRunner: this.pluginRunner,
missionStore,
onSliceComplete: (slice) => {
void this.scheduler.onSliceComplete(slice);
@@ -368,8 +393,9 @@ export class InProcessRuntime
* 1. Set status to "stopping"
* 2. Stop scheduler (no new tasks)
* 3. Wait for executor to finish active tasks (with timeout)
* 4. Drain and cleanup worktree pool
* 5. Set status to "stopped"
* 4. Shutdown plugin runner
* 5. Drain and cleanup worktree pool
* 6. Set status to "stopped"
*
* @throws Error if shutdown timeout is exceeded
*/
@@ -435,7 +461,13 @@ export class InProcessRuntime
);
}
// 3. Drain and cleanup worktree pool
// 6. Shutdown plugin runner
if (this.pluginRunner) {
await this.pluginRunner.shutdown();
runtimeLog.log("PluginRunner shutdown complete");
}
// 7. Drain and cleanup worktree pool
if (this.worktreePool) {
const worktrees = this.worktreePool.drain();
if (worktrees.length > 0) {