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 f83d3483a6
commit c8a0876f45
13 changed files with 390 additions and 10 deletions

View File

@@ -251,6 +251,7 @@ export function AgentsView({ addToast, projectId }: AgentsViewProps) {
const [editingRoleForAgent, setEditingRoleForAgent] = useState<string | null>(null);
const roleSelectRef = useRef<HTMLSelectElement>(null);
const [showSystemAgents, setShowSystemAgents] = useState(false);
const hierarchy = useAgentHierarchy(agents, projectId);
@@ -287,14 +288,14 @@ export function AgentsView({ addToast, projectId }: AgentsViewProps) {
setIsLoading(true);
try {
const filter = filterState !== "all" ? { state: filterState } : undefined;
const data = await fetchAgents(filter, projectId);
const data = await fetchAgents({ ...filter, includeSystem: showSystemAgents }, projectId);
setAgents(data);
} catch (err: any) {
addToast(`Failed to load agents: ${err.message}`, "error");
} finally {
setIsLoading(false);
}
}, [filterState, addToast, projectId]);
}, [filterState, showSystemAgents, addToast, projectId]);
useEffect(() => {
void loadAgents();
@@ -528,6 +529,16 @@ export function AgentsView({ addToast, projectId }: AgentsViewProps) {
</select>
</div>
<label className="checkbox-label agent-system-filter">
<input
type="checkbox"
checked={showSystemAgents}
onChange={(e) => setShowSystemAgents(e.target.checked)}
aria-label="Show system agents"
/>
Show system agents
</label>
<div className="agent-controls-actions">
<button
className="btn"

View File

@@ -430,7 +430,7 @@ describe("AgentsView", () => {
fireEvent.change(filterSelect, { target: { value: "active" } });
await waitFor(() => {
expect(mockFetchAgents).toHaveBeenCalledWith({ state: "active" }, undefined);
expect(mockFetchAgents).toHaveBeenCalledWith({ state: "active", includeSystem: false }, undefined);
});
});
@@ -445,13 +445,117 @@ describe("AgentsView", () => {
fireEvent.change(filterSelect, { target: { value: "idle" } });
await waitFor(() => {
expect(mockFetchAgents).toHaveBeenLastCalledWith({ state: "idle" }, undefined);
expect(mockFetchAgents).toHaveBeenLastCalledWith({ state: "idle", includeSystem: false }, undefined);
});
fireEvent.change(filterSelect, { target: { value: "all" } });
await waitFor(() => {
expect(mockFetchAgents).toHaveBeenLastCalledWith(undefined, undefined);
expect(mockFetchAgents).toHaveBeenLastCalledWith({ includeSystem: false }, undefined);
});
});
});
describe("show system agents toggle", () => {
it("renders the system agents checkbox", async () => {
render(<AgentsView addToast={mockAddToast} />);
await waitFor(() => {
expect(screen.getByLabelText("Show system agents")).toBeTruthy();
});
// Checkbox should be unchecked by default
const checkbox = screen.getByLabelText("Show system agents") as HTMLInputElement;
expect(checkbox.checked).toBe(false);
});
it("passes includeSystem: false by default to fetchAgents", async () => {
render(<AgentsView addToast={mockAddToast} />);
await waitFor(() => {
expect(screen.getByText("All States")).toBeTruthy();
});
// Default call should include includeSystem: false
await waitFor(() => {
expect(mockFetchAgents).toHaveBeenLastCalledWith({ includeSystem: false }, undefined);
});
});
it("toggles system agents visibility when checkbox is clicked", async () => {
render(<AgentsView addToast={mockAddToast} projectId={projectId} />);
await waitFor(() => {
expect(screen.getByText("All States")).toBeTruthy();
});
const checkbox = screen.getByLabelText("Show system agents");
fireEvent.click(checkbox);
await waitFor(() => {
expect(mockFetchAgents).toHaveBeenLastCalledWith({ includeSystem: true }, projectId);
});
});
it("combines system agents toggle with state filter", async () => {
render(<AgentsView addToast={mockAddToast} projectId={projectId} />);
await waitFor(() => {
expect(screen.getByText("All States")).toBeTruthy();
});
// First enable system agents toggle
const checkbox = screen.getByLabelText("Show system agents");
fireEvent.click(checkbox);
await waitFor(() => {
expect(mockFetchAgents).toHaveBeenLastCalledWith({ includeSystem: true }, projectId);
});
// Then filter by state
const filterSelect = screen.getByDisplayValue("All States");
fireEvent.change(filterSelect, { target: { value: "active" } });
await waitFor(() => {
expect(mockFetchAgents).toHaveBeenLastCalledWith({ state: "active", includeSystem: true }, projectId);
});
});
it("shows system agents in agent list when checkbox is enabled", async () => {
const systemAgents: Agent[] = [
{
id: "agent-sys-001",
name: "executor-FN-TEST",
role: "executor" as AgentCapability,
state: "terminated" as AgentState,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
metadata: { agentKind: "task-worker" },
},
];
// Mock returns only normal agents by default (excluding terminated)
mockFetchAgents.mockResolvedValue(mockAgents.slice(0, 3));
render(<AgentsView addToast={mockAddToast} projectId={projectId} />);
await waitFor(() => {
expect(screen.getByText("Test Agent 1")).toBeTruthy();
});
// Normal agents should be visible
expect(screen.queryByText("executor-FN-TEST")).toBeNull();
// Update mock to return system agents too (next call)
mockFetchAgents.mockResolvedValueOnce([...mockAgents.slice(0, 3), ...systemAgents]);
// Enable system agents toggle
const checkbox = screen.getByLabelText("Show system agents");
fireEvent.click(checkbox);
// Now the agents should be reloaded with system agents included
await waitFor(() => {
expect(mockFetchAgents).toHaveBeenCalledWith({ includeSystem: true }, projectId);
});
});
});