fix(FN-2210): classify legacy verification agents as ephemeral

- Extend isEphemeralAgent to treat metadata.internal agents as internal system agents
- Add legacy fallback detection for executor agents named verification-agent with no reportsTo
- Update AgentsView list and org tree filtering to honor the Show system agents toggle
- Add regression coverage in core and dashboard tests for default filtering and includeEphemeral visibility
This commit is contained in:
Fusion
2026-04-21 02:40:50 -07:00
committed by gsxdsm
parent 16e3a5104a
commit fd83cc6fd5
4 changed files with 57 additions and 7 deletions

View File

@@ -1097,6 +1097,38 @@ describe("AgentStore", () => {
expect(activeAll).toHaveLength(1);
expect(activeAll[0].id).toBe(taskWorker.id);
});
it("filters out agents marked with metadata.internal", async () => {
const normal = await store.createAgent({ name: "Normal Agent", role: "executor" });
await store.createAgent({
name: "internal-agent",
role: "executor",
metadata: { internal: true },
});
const defaultAgents = await store.listAgents();
expect(defaultAgents).toHaveLength(1);
expect(defaultAgents[0].id).toBe(normal.id);
const includingEphemeral = await store.listAgents({ includeEphemeral: true });
expect(includingEphemeral).toHaveLength(2);
});
it("filters legacy verification-agent fallback by default", async () => {
const normal = await store.createAgent({ name: "Normal Agent", role: "executor" });
await store.createAgent({
name: "verification-agent",
role: "executor",
metadata: {},
});
const defaultAgents = await store.listAgents();
expect(defaultAgents).toHaveLength(1);
expect(defaultAgents[0].id).toBe(normal.id);
const includingEphemeral = await store.listAgents({ includeEphemeral: true });
expect(includingEphemeral).toHaveLength(2);
});
});
// ── Org Hierarchy ────────────────────────────────────────────────

View File

@@ -2034,19 +2034,21 @@ export const AGENT_VALID_TRANSITIONS: Record<AgentState, AgentState[]> = {
};
/**
* Detect if an agent is a runtime-created ephemeral agent (task-worker or spawned child).
* These agents are created by the engine for task execution and should typically be
* hidden from the default agents page listing.
* Detect if an agent is a runtime-created ephemeral/internal agent.
* These agents are created by the engine for task execution/system workflows and should
* typically be hidden from the default agents page listing.
*
* Detection heuristics (returns true if ANY match):
* - `agent.metadata?.agentKind === "task-worker"` — task-worker agents from InProcessRuntime
* - `agent.metadata?.taskWorker === true` — legacy task-worker marker
* - `agent.metadata?.managedBy === "task-executor"` — executor-managed agents
* - `agent.metadata?.type === "spawned"` — spawned child agents from TaskExecutor
* - `agent.metadata?.internal === true` — explicitly internal/system agent marker
* - Legacy fallback: executor role with name starting with "executor-" and no reportsTo
* - Legacy fallback: executor role named "verification-agent" with no reportsTo
*
* @param agent - Agent object (partial shape accepted)
* @returns true if the agent is an ephemeral/runtime-created agent
* @returns true if the agent is an ephemeral/runtime-created/internal system agent
*/
export function isEphemeralAgent(
agent: { metadata?: Record<string, unknown> | null; name?: string; role?: string; reportsTo?: string | null },
@@ -2058,6 +2060,7 @@ export function isEphemeralAgent(
if (metadata.taskWorker === true) return true;
if (metadata.managedBy === "task-executor") return true;
if (metadata.type === "spawned") return true;
if (metadata.internal === true) return true;
// Legacy fallback: executor agents with "executor-" prefix and no manager
// These are task workers that were created before metadata was standardized
@@ -2070,6 +2073,15 @@ export function isEphemeralAgent(
return true;
}
// Legacy internal system agent used by older verification flows.
if (
agent.role === "executor" &&
agent.name === "verification-agent" &&
agent.reportsTo == null
) {
return true;
}
return false;
}

View File

@@ -266,12 +266,17 @@ export function AgentsView({ addToast, projectId }: AgentsViewProps) {
// Filter agents for display. "All States" means all non-ephemeral agents,
// including disabled/terminated agents that still carry configuration.
// When "Show system agents" is enabled, include ephemeral/internal agents.
const displayAgents = useMemo(() => {
return agents.filter(a => !isEphemeralAgent(a));
}, [agents]);
return agents.filter((agent) => showSystemAgents || !isEphemeralAgent(agent));
}, [agents, showSystemAgents]);
// Filter org tree to exclude ephemeral agents in default view.
const displayOrgTree = useMemo(() => {
if (showSystemAgents) {
return orgTree;
}
// Recursively filter out ephemeral agents from the org tree.
const filterNode = (node: OrgTreeNode): OrgTreeNode | null => {
if (isEphemeralAgent(node.agent)) return null;
@@ -285,7 +290,7 @@ export function AgentsView({ addToast, projectId }: AgentsViewProps) {
return orgTree
.map(filterNode)
.filter((n): n is OrgTreeNode => n !== null);
}, [orgTree]);
}, [orgTree, showSystemAgents]);
const loadAgents = useCallback(async () => {
setIsLoading(true);

View File

@@ -628,6 +628,7 @@ describe("AgentsView", () => {
// Now the agents should be reloaded with system agents included
await waitFor(() => {
expect(mockFetchAgents).toHaveBeenCalledWith({ includeEphemeral: true }, projectId);
expect(screen.getByText("executor-FN-TEST")).toBeTruthy();
});
});
});