feat(FN-1737): auto-delete child/task-worker agents and hide system agents by default

- Auto-delete spawned child agents when their parent task terminates (reportsTo cleanup)
- Auto-delete task-worker agents when their owned task completes
- Add includeSystem filter to AgentStore.list() and REST API
- Hide system agents by default on the agents page (show only user-facing agents)
- Wire includeSystem toggle through the API layer and AgentsView component
- Add changeset for @gsxdsm/fusion patch release
- Add comprehensive tests for agent cleanup and includeSystem filtering
This commit is contained in:
Fusion
2026-04-15 20:36:42 -07:00
committed by gsxdsm
parent 9bd437e166
commit f2c5ac7b82
13 changed files with 390 additions and 10 deletions

View File

@@ -998,6 +998,75 @@ describe("AgentStore", () => {
expect(agents).toHaveLength(1);
expect(agents[0].name).toBe("Valid");
});
it("filters out system agents when includeSystem is false", async () => {
// Create a normal agent
const normal = await store.createAgent({ name: "Normal Agent", role: "executor" });
// Create a task-worker agent
const taskWorker = await store.createAgent({
name: "executor-FN-TEST",
role: "executor",
metadata: { agentKind: "task-worker" },
});
// Create a spawned child agent
const spawned = await store.createAgent({
name: "spawned-agent",
role: "executor",
metadata: { type: "spawned" },
});
// Create an agent with taskWorker metadata
const withTaskWorker = await store.createAgent({
name: "task-worker-agent",
role: "executor",
metadata: { taskWorker: true },
});
// Create an agent with managedBy metadata
const managedBy = await store.createAgent({
name: "managed-agent",
role: "executor",
metadata: { managedBy: "task-executor" },
});
// Without includeSystem filter, all agents are returned
const allAgents = await store.listAgents();
expect(allAgents).toHaveLength(5);
// With includeSystem: false, system agents are filtered out
const nonSystemAgents = await store.listAgents({ includeSystem: false });
expect(nonSystemAgents).toHaveLength(1);
expect(nonSystemAgents[0].id).toBe(normal.id);
// With includeSystem: true, all agents are returned
const systemAgents = await store.listAgents({ includeSystem: true });
expect(systemAgents).toHaveLength(5);
});
it("includeSystem filter works with state filter", async () => {
// Create a normal agent
const normal = await store.createAgent({ name: "Normal Agent", role: "executor" });
// Create a task-worker agent
const taskWorker = await store.createAgent({
name: "executor-FN-TEST",
role: "executor",
metadata: { agentKind: "task-worker" },
});
await store.recordHeartbeat(taskWorker.id, "ok");
await store.updateAgentState(taskWorker.id, "active");
// Without includeSystem, but with state=active - only returns active non-system agents
const activeNonSystem = await store.listAgents({ state: "active", includeSystem: false });
expect(activeNonSystem).toHaveLength(0);
// With includeSystem: true, returns all active agents
const activeAll = await store.listAgents({ state: "active", includeSystem: true });
expect(activeAll).toHaveLength(1);
expect(activeAll[0].id).toBe(taskWorker.id);
});
});
// ── Org Hierarchy ────────────────────────────────────────────────

View File

@@ -985,12 +985,27 @@ export class AgentStore extends EventEmitter {
return agent;
}
/**
* Check if an agent is a system-generated ephemeral agent (task-worker or spawned child).
* These agents are created at runtime by the engine and should typically be hidden
* from the default agents page view.
*/
private isSystemAgent(agent: Agent): boolean {
const metadata = agent.metadata ?? {};
return (
metadata.agentKind === "task-worker" ||
metadata.type === "spawned" ||
metadata.taskWorker === true ||
metadata.managedBy === "task-executor"
);
}
/**
* List all agents, optionally filtered by state.
* @param filter - Optional filter criteria
* @returns Array of agents
*/
async listAgents(filter?: { state?: AgentState; role?: AgentCapability }): Promise<Agent[]> {
async listAgents(filter?: { state?: AgentState; role?: AgentCapability; includeSystem?: boolean }): Promise<Agent[]> {
const files = await readdir(this.agentsDir).catch(() => [] as string[]);
const agentFiles = files.filter((f) => f.endsWith(".json") && !f.includes("-heartbeats") && !f.includes("-sessions") && !f.includes("-runs") && !f.includes("-revisions"));
@@ -1004,6 +1019,9 @@ export class AgentStore extends EventEmitter {
if (filter?.state && agent.state !== filter.state) continue;
if (filter?.role && agent.role !== filter.role) continue;
// When includeSystem is explicitly false, filter out system agents
if (filter?.includeSystem === false && this.isSystemAgent(agent)) continue;
agents.push(agent);
} catch {
// Skip corrupted files