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:
@@ -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 ────────────────────────────────────────────────
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user