feat(FN-1776): expose AgentStore for SSE event forwarding with ephemeral agent filtering

- Expose getAgentStore from runtime and engine packages
- Add includeSystem filter to getOrgTree API endpoint
- Wire AgentStore into SSE endpoint for event forwarding
- Forward agent events through the SSE pipeline
- Exclude ephemeral agents from org tree API and UI
- Filter ephemeral agents in useAgents hook
- Update AgentsView test to expect includeSystem filter
- Document agent SSE event forwarding architecture
This commit is contained in:
Fusion
2026-04-16 01:44:54 -07:00
committed by gsxdsm
parent 47be23932c
commit 21759d34e7
12 changed files with 85 additions and 15 deletions

View File

@@ -10597,7 +10597,8 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
const agentStore = new AgentStore({ rootDir: scopedStore.getFusionDir() });
await agentStore.init();
const tree = await agentStore.getOrgTree();
const includeSystem = req.query.includeSystem === "true";
const tree = await agentStore.getOrgTree({ includeSystem });
res.json(tree);
} catch (err: unknown) {
if (err instanceof ApiError) {

View File

@@ -359,7 +359,11 @@ export function createServer(store: TaskStore, options?: ServerOptions): ReturnT
const engineManager = options?.engineManager;
if (!projectId) {
createSSE(store, store.getMissionStore(), aiSessionStore, store.getPluginStore())(req, res);
// Create AgentStore for default project SSE
const { AgentStore: AgentStoreClass } = await import("@fusion/core");
const defaultAgentStore = new AgentStoreClass({ rootDir: store.getFusionDir() });
await defaultAgentStore.init();
createSSE(store, store.getMissionStore(), aiSessionStore, store.getPluginStore(), undefined, defaultAgentStore)(req, res);
return;
}
@@ -368,15 +372,24 @@ export function createServer(store: TaskStore, options?: ServerOptions): ReturnT
// attach to the same EventEmitter instance that the engine writes to,
// rather than a separate store created by getOrCreateProjectStore.
let scopedStore: TaskStore;
let agentStore;
if (engineManager) {
const engine = engineManager.getEngine(projectId);
scopedStore = engine?.getTaskStore() ?? await getOrCreateProjectStore(projectId);
// Use the engine's AgentStore if available
agentStore = engine?.getAgentStore();
} else {
scopedStore = await getOrCreateProjectStore(projectId);
}
// Fallback: create AgentStore if engine doesn't have one
if (!agentStore) {
const { AgentStore: AgentStoreClass } = await import("@fusion/core");
agentStore = new AgentStoreClass({ rootDir: scopedStore.getFusionDir() });
await agentStore.init();
}
createSSE(scopedStore, scopedStore.getMissionStore(), aiSessionStore, scopedStore.getPluginStore(), {
projectId,
})(req, res);
}, agentStore)(req, res);
} catch (err: unknown) {
sendErrorResponse(res, 500, err instanceof Error ? err.message : "Failed to open project event stream");
}

View File

@@ -1,5 +1,5 @@
import type { Request, Response } from "express";
import type { TaskStore, MissionStore, PluginStore, PluginInstallation, PluginState } from "@fusion/core";
import type { TaskStore, MissionStore, PluginStore, PluginInstallation, PluginState, AgentStore } from "@fusion/core";
import type { AiSessionStore } from "./ai-session-store.js";
let activeConnections = 0;
@@ -172,6 +172,7 @@ export function createSSE(
aiSessionStore?: AiSessionStore,
pluginStore?: PluginStore,
options?: CreateSSEOptions,
agentStore?: AgentStore,
) {
const { projectId } = options ?? {};
@@ -324,6 +325,23 @@ export function createSSE(
send(`event: plugin:lifecycle\ndata: ${JSON.stringify(payload)}\n\n`);
};
// --- Agent lifecycle event handlers ---
const onAgentCreated = (agent: any) => {
send(`event: agent:created\ndata: ${JSON.stringify(agent)}\n\n`);
};
const onAgentUpdated = (agent: any) => {
send(`event: agent:updated\ndata: ${JSON.stringify(agent)}\n\n`);
};
const onAgentDeleted = (agentId: string) => {
send(`event: agent:deleted\ndata: ${JSON.stringify({ id: agentId })}\n\n`);
};
const onAgentStateChanged = (agentId: string, fromState: string, toState: string) => {
send(`event: agent:stateChanged\ndata: ${JSON.stringify({ id: agentId, from: fromState, to: toState })}\n\n`);
};
// --- Cleanup (all handlers are defined above, safe to reference) ---
let cleaned = false;
@@ -372,6 +390,12 @@ export function createSSE(
pluginStore.off("plugin:disabled", onPluginDisabled);
pluginStore.off("plugin:stateChanged", onPluginStateChanged);
}
if (agentStore) {
agentStore.off("agent:created", onAgentCreated);
agentStore.off("agent:updated", onAgentUpdated);
agentStore.off("agent:deleted", onAgentDeleted);
agentStore.off("agent:stateChanged", onAgentStateChanged);
}
};
// --- Subscribe ---
@@ -420,6 +444,13 @@ export function createSSE(
pluginStore.on("plugin:stateChanged", onPluginStateChanged);
}
if (agentStore) {
agentStore.on("agent:created", onAgentCreated);
agentStore.on("agent:updated", onAgentUpdated);
agentStore.on("agent:deleted", onAgentDeleted);
agentStore.on("agent:stateChanged", onAgentStateChanged);
}
// Heartbeat every 30s to keep connection alive.
// Sent as a named event so the client's EventSource can detect it
// (SSE comments starting with ":" are silently consumed and never