diff --git a/.changeset/fn-6018-agent-error-stop-delete.md b/.changeset/fn-6018-agent-error-stop-delete.md new file mode 100644 index 0000000000..f6011bcbd9 --- /dev/null +++ b/.changeset/fn-6018-agent-error-stop-delete.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +Allow failed agents to be stopped and deleted consistently across the dashboard and CLI guidance. + +Agents in the error state can now transition to paused, the dashboard exposes delete actions for failed agents in list/detail views, and regression coverage protects the updated behavior. diff --git a/packages/cli/src/__tests__/extension.test.ts b/packages/cli/src/__tests__/extension.test.ts index c9f5be7f9f..6b842e7237 100644 --- a/packages/cli/src/__tests__/extension.test.ts +++ b/packages/cli/src/__tests__/extension.test.ts @@ -153,6 +153,22 @@ describe("fn pi extension tool copy guardrails", () => { expect(guidelines).toMatch(/soft.?delete/i); expect(guidelines).not.toMatch(/permanent|cannot be recovered|cannot be undone|deleted immediately|irrecoverable/i); }); + + it("describes fn_agent_stop as allowing error-state agents to be paused (FN-6018)", () => { + const api = createMockAPI(); + kbExtension(api); + + const tool = api.tools.get("fn_agent_stop") as + | { description?: string; promptGuidelines?: string[] } + | undefined; + + expect(tool).toBeDefined(); + + const guidelines = (tool?.promptGuidelines ?? []).join(" "); + expect(guidelines).toMatch(/running, active, or in error/i); + expect(guidelines).toMatch(/idle.*already-paused/i); + expect(guidelines).not.toMatch(/idle, 'error', or already-paused/i); + }); }); // Audited in FN-3189: this exhaustive suite is expensive (~62s) and stale diff --git a/packages/cli/src/extension.ts b/packages/cli/src/extension.ts index 9e9ab2874d..4c87ac9696 100644 --- a/packages/cli/src/extension.ts +++ b/packages/cli/src/extension.ts @@ -3592,9 +3592,9 @@ export default function kbExtension(pi: ExtensionAPI) { "Transitions the agent from running/active to paused state.", promptSnippet: "Stop (pause) a running Fusion agent", promptGuidelines: [ - "Use to pause an agent that is currently running or active", + "Use to pause an agent that is currently running, active, or in error", "Stopped agents can be resumed with fn_agent_start", - "Agents in 'idle', 'error', or already-paused state cannot be stopped", + "Agents in 'idle' or already-paused state cannot be stopped", ], parameters: Type.Object({ id: Type.String({ description: "Agent ID to stop (e.g., agent-abc123)" }), diff --git a/packages/core/src/__tests__/agent-store.test.ts b/packages/core/src/__tests__/agent-store.test.ts index c08a22cfaa..518b94f69c 100644 --- a/packages/core/src/__tests__/agent-store.test.ts +++ b/packages/core/src/__tests__/agent-store.test.ts @@ -1669,6 +1669,22 @@ describe("AgentStore", () => { expect(updated.state).toBe("active"); }); + it("error → paused transition succeeds", async () => { + const agent = await createReadyAgent(store, "ErrorToPaused"); + await store.updateAgentState(agent.id, "active"); + await store.updateAgentState(agent.id, "error"); + const updated = await store.updateAgentState(agent.id, "paused"); + expect(updated.state).toBe("paused"); + }); + + it("error → idle transition succeeds", async () => { + const agent = await createReadyAgent(store, "ErrorToIdle"); + await store.updateAgentState(agent.id, "active"); + await store.updateAgentState(agent.id, "error"); + const updated = await store.updateAgentState(agent.id, "idle"); + expect(updated.state).toBe("idle"); + }); + it("rejects active → terminated transition", async () => { const agent = await createReadyAgent(store, "ActiveToTerminated"); await store.updateAgentState(agent.id, "active"); diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 53473ae3f4..b2814f3c26 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -5501,7 +5501,7 @@ export const AGENT_VALID_TRANSITIONS: Record = { active: ["idle", "running", "paused", "error"], running: ["idle", "active", "paused", "error"], paused: ["idle", "active"], - error: ["idle", "active"], + error: ["idle", "active", "paused"], }; /** diff --git a/packages/dashboard/app/components/AgentDetailView.tsx b/packages/dashboard/app/components/AgentDetailView.tsx index c5b580419d..a2cb6c84f4 100644 --- a/packages/dashboard/app/components/AgentDetailView.tsx +++ b/packages/dashboard/app/components/AgentDetailView.tsx @@ -797,6 +797,10 @@ export function AgentDetailView({ agentId, projectId, onClose, addToast, onChild {t("agents.stop", "Stop")} + )} diff --git a/packages/dashboard/app/components/AgentsView.tsx b/packages/dashboard/app/components/AgentsView.tsx index 235b9d8855..5e0fcbe330 100644 --- a/packages/dashboard/app/components/AgentsView.tsx +++ b/packages/dashboard/app/components/AgentsView.tsx @@ -1937,7 +1937,7 @@ export function AgentsView({ addToast, projectId, onOpenTaskLogs, agentOnboardin > {t("agents.details", "Details")} - {(agent.state === "idle" || agent.state === "paused") && ( + {(agent.state === "idle" || agent.state === "paused" || agent.state === "error") && (