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:
@@ -558,6 +558,88 @@ describe("InProcessRuntime", () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 25));
|
||||
expect(executeSpy).not.toHaveBeenCalled();
|
||||
}, 30000);
|
||||
|
||||
it("auto-deletes task-worker agent on task completion after 5 second delay", async () => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
try {
|
||||
await runtime.start();
|
||||
|
||||
const store = getAgentStore(runtime);
|
||||
const deleteAgentSpy = vi.spyOn(store, "deleteAgent").mockResolvedValue(undefined);
|
||||
|
||||
const executorOptions = mockExecutorCtor.mock.calls.at(-1)?.[0] as {
|
||||
onStart?: (task: Task, worktreePath: string) => void;
|
||||
onComplete?: (task: Task) => void;
|
||||
};
|
||||
expect(executorOptions.onComplete).toBeTypeOf("function");
|
||||
|
||||
// Create a task-worker agent first via onStart
|
||||
executorOptions.onStart?.({ id: "FN-AUTO1" } as Task, join(testDir, "worktree-FN-AUTO1"));
|
||||
|
||||
await vi.waitFor(async () => {
|
||||
const agents = await store.listAgents();
|
||||
expect(agents.some((a: Agent) => a.name === "executor-FN-AUTO1")).toBe(true);
|
||||
});
|
||||
|
||||
// Clear previous calls and trigger onComplete
|
||||
deleteAgentSpy.mockClear();
|
||||
executorOptions.onComplete?.({ id: "FN-AUTO1" } as Task);
|
||||
|
||||
// Verify deleteAgent was not called immediately (before 5 seconds)
|
||||
expect(deleteAgentSpy).not.toHaveBeenCalled();
|
||||
|
||||
// Advance timers by 5 seconds
|
||||
await vi.advanceTimersByTimeAsync(5000);
|
||||
|
||||
// Now deleteAgent should have been called
|
||||
expect(deleteAgentSpy).toHaveBeenCalledTimes(1);
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
}, 30000);
|
||||
|
||||
it("auto-deletes task-worker agent on task error after 5 second delay", async () => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
try {
|
||||
await runtime.start();
|
||||
|
||||
const store = getAgentStore(runtime);
|
||||
const deleteAgentSpy = vi.spyOn(store, "deleteAgent").mockResolvedValue(undefined);
|
||||
|
||||
const executorOptions = mockExecutorCtor.mock.calls.at(-1)?.[0] as {
|
||||
onError?: (task: Task, error: Error) => void;
|
||||
};
|
||||
expect(executorOptions.onError).toBeTypeOf("function");
|
||||
|
||||
// Create a task-worker agent first via onStart
|
||||
const onStartOptions = mockExecutorCtor.mock.calls.at(-1)?.[0] as {
|
||||
onStart?: (task: Task, worktreePath: string) => void;
|
||||
};
|
||||
onStartOptions.onStart?.({ id: "FN-AUTO2" } as Task, join(testDir, "worktree-FN-AUTO2"));
|
||||
|
||||
await vi.waitFor(async () => {
|
||||
const agents = await store.listAgents();
|
||||
expect(agents.some((a: Agent) => a.name === "executor-FN-AUTO2")).toBe(true);
|
||||
});
|
||||
|
||||
// Clear previous calls and trigger onError
|
||||
deleteAgentSpy.mockClear();
|
||||
executorOptions.onError?.({ id: "FN-AUTO2" } as Task, new Error("Task failed"));
|
||||
|
||||
// Verify deleteAgent was not called immediately (before 5 seconds)
|
||||
expect(deleteAgentSpy).not.toHaveBeenCalled();
|
||||
|
||||
// Advance timers by 5 seconds
|
||||
await vi.advanceTimersByTimeAsync(5000);
|
||||
|
||||
// Now deleteAgent should have been called
|
||||
expect(deleteAgentSpy).toHaveBeenCalledTimes(1);
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
}, 30000);
|
||||
});
|
||||
|
||||
describe("configuration", () => {
|
||||
|
||||
@@ -304,6 +304,11 @@ export class InProcessRuntime
|
||||
if (agentId && this.agentStore) {
|
||||
void this.agentStore.updateAgentState(agentId, "terminated").catch(() => {});
|
||||
this.taskAgentMap.delete(task.id);
|
||||
// Auto-delete the task-worker agent after a short delay so the UI
|
||||
// can observe the terminal state before the agent is removed.
|
||||
void setTimeout(() => {
|
||||
this.agentStore?.deleteAgent(agentId).catch(() => {});
|
||||
}, 5000);
|
||||
}
|
||||
},
|
||||
onError: (task, error) => {
|
||||
@@ -331,6 +336,11 @@ export class InProcessRuntime
|
||||
if (agentId && this.agentStore) {
|
||||
void this.agentStore.updateAgentState(agentId, "terminated").catch(() => {});
|
||||
this.taskAgentMap.delete(task.id);
|
||||
// Auto-delete the task-worker agent after a short delay so the UI
|
||||
// can observe the terminal state before the agent is removed.
|
||||
void setTimeout(() => {
|
||||
this.agentStore?.deleteAgent(agentId).catch(() => {});
|
||||
}, 5000);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user