refactor(agents): remove terminated AgentState; collapse to paused/error
Drops "terminated" from AGENT_STATES. The agent lifecycle now runs through idle | active | running | paused | error. paused (carrying a pauseReason) absorbs every former terminated use case — manual stop, heartbeat run termination, spawned-child cleanup. Run status (agentRuns.status) is unchanged: "terminated" stays a valid run-status value. AGENT_VALID_TRANSITIONS allows direct any→idle transitions so resetAgent no longer needs the intermediate hop. Stack-wide: - core/agent-store: lastError clearing + resetAgent simplified. - engine/agent-heartbeat, executor, in-process-runtime: terminated state writes → paused; halt-state listener fires on paused/error. - dashboard: AgentsView/AgentListModal/AgentDetailView lose the Terminated badge/option/state-block; agent pickers no longer filter terminated; agentHealth drops the Terminated branch; routes/state cast widened to the new AgentState union. Tests across core and engine updated to assert paused for AgentState and left "terminated" intact for run-status assertions. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1500,11 +1500,11 @@ describe("AgentStore", () => {
|
||||
expect(updated.state).toBe("paused");
|
||||
});
|
||||
|
||||
it("active → terminated transition succeeds", async () => {
|
||||
it("active → paused transition succeeds", async () => {
|
||||
const agent = await createReadyAgent(store, "ActiveToTerminated");
|
||||
await store.updateAgentState(agent.id, "active");
|
||||
const updated = await store.updateAgentState(agent.id, "terminated");
|
||||
expect(updated.state).toBe("terminated");
|
||||
const updated = await store.updateAgentState(agent.id, "paused");
|
||||
expect(updated.state).toBe("paused");
|
||||
});
|
||||
|
||||
it("paused → active transition succeeds", async () => {
|
||||
@@ -1515,14 +1515,6 @@ describe("AgentStore", () => {
|
||||
expect(updated.state).toBe("active");
|
||||
});
|
||||
|
||||
it("paused → terminated transition succeeds", async () => {
|
||||
const agent = await createReadyAgent(store, "PausedToTerminated");
|
||||
await store.updateAgentState(agent.id, "active");
|
||||
await store.updateAgentState(agent.id, "paused");
|
||||
const updated = await store.updateAgentState(agent.id, "terminated");
|
||||
expect(updated.state).toBe("terminated");
|
||||
});
|
||||
|
||||
it("same-state transition returns agent unchanged (no-op)", async () => {
|
||||
const agent = await store.createAgent({ name: "SameState", role: "executor" });
|
||||
const unchanged = await store.updateAgentState(agent.id, "idle");
|
||||
@@ -1537,55 +1529,29 @@ describe("AgentStore", () => {
|
||||
).rejects.toThrow("Invalid state transition: idle -> paused");
|
||||
});
|
||||
|
||||
it("idle → terminated throws", async () => {
|
||||
const agent = await store.createAgent({ name: "BadTerminate", role: "executor" });
|
||||
await expect(
|
||||
store.updateAgentState(agent.id, "terminated")
|
||||
).rejects.toThrow("Invalid state transition: idle -> terminated");
|
||||
});
|
||||
|
||||
it("transition from terminated to paused still throws", async () => {
|
||||
const agent = await createReadyAgent(store, "Terminated");
|
||||
await store.updateAgentState(agent.id, "active");
|
||||
await store.updateAgentState(agent.id, "terminated");
|
||||
|
||||
await expect(
|
||||
store.updateAgentState(agent.id, "paused")
|
||||
).rejects.toThrow("Invalid state transition: terminated -> paused");
|
||||
});
|
||||
|
||||
it("terminated → active transition succeeds", async () => {
|
||||
it("paused → active transition succeeds", async () => {
|
||||
const agent = await createReadyAgent(store, "RestartActive");
|
||||
await store.updateAgentState(agent.id, "active");
|
||||
await store.updateAgentState(agent.id, "terminated");
|
||||
await store.updateAgentState(agent.id, "paused");
|
||||
|
||||
const updated = await store.updateAgentState(agent.id, "active");
|
||||
expect(updated.state).toBe("active");
|
||||
});
|
||||
|
||||
it("terminated → running transition succeeds", async () => {
|
||||
const agent = await createReadyAgent(store, "RestartRunning");
|
||||
await store.updateAgentState(agent.id, "active");
|
||||
await store.updateAgentState(agent.id, "terminated");
|
||||
|
||||
const updated = await store.updateAgentState(agent.id, "running");
|
||||
expect(updated.state).toBe("running");
|
||||
});
|
||||
|
||||
it("terminated → idle transition succeeds", async () => {
|
||||
it("paused → idle transition succeeds", async () => {
|
||||
const agent = await createReadyAgent(store, "RestartIdle");
|
||||
await store.updateAgentState(agent.id, "active");
|
||||
await store.updateAgentState(agent.id, "terminated");
|
||||
await store.updateAgentState(agent.id, "paused");
|
||||
|
||||
const updated = await store.updateAgentState(agent.id, "idle");
|
||||
expect(updated.state).toBe("idle");
|
||||
});
|
||||
|
||||
it("transitioning from terminated clears lastError", async () => {
|
||||
it("transitioning into active clears lastError", async () => {
|
||||
const agent = await createReadyAgent(store, "ClearError");
|
||||
await store.updateAgentState(agent.id, "active");
|
||||
await store.updateAgent(agent.id, { lastError: "something broke" });
|
||||
await store.updateAgentState(agent.id, "terminated");
|
||||
await store.updateAgentState(agent.id, "paused");
|
||||
|
||||
const restarted = await store.updateAgentState(agent.id, "active");
|
||||
expect(restarted.state).toBe("active");
|
||||
@@ -1894,7 +1860,7 @@ describe("AgentStore", () => {
|
||||
pauseReason: "manual",
|
||||
lastError: "something broke",
|
||||
});
|
||||
await s.updateAgentState(agent.id, "terminated");
|
||||
await s.updateAgentState(agent.id, "paused");
|
||||
return agent;
|
||||
}
|
||||
|
||||
|
||||
@@ -1173,7 +1173,9 @@ export class AgentStore extends EventEmitter {
|
||||
state: newState,
|
||||
updatedAt: new Date().toISOString(),
|
||||
// Clear lastError when transitioning away from terminated
|
||||
...(currentState === "terminated" && newState !== "terminated" && { lastError: undefined }),
|
||||
// Clear lastError when an agent re-enters an actionable state so
|
||||
// a resumed agent does not carry stale "Error" badges.
|
||||
...((newState === "active" || newState === "running") && { lastError: undefined }),
|
||||
};
|
||||
|
||||
await this.writeAgent(updated);
|
||||
@@ -1432,11 +1434,9 @@ export class AgentStore extends EventEmitter {
|
||||
await this.endHeartbeatRun(activeRun.id, "terminated");
|
||||
}
|
||||
|
||||
// Normalize to terminated first when idle is not directly reachable.
|
||||
if (agent.state !== "idle" && agent.state !== "terminated") {
|
||||
agent = await this.updateAgentState(agentId, "terminated");
|
||||
}
|
||||
|
||||
// Any non-idle state can transition directly to idle in the new
|
||||
// lifecycle (see AGENT_VALID_TRANSITIONS in types.ts), so no
|
||||
// intermediate hop is required.
|
||||
if (agent.state !== "idle") {
|
||||
agent = await this.updateAgentState(agentId, "idle");
|
||||
}
|
||||
|
||||
@@ -3201,17 +3201,16 @@ export interface PlanningSession {
|
||||
// ── Agent Types ────────────────────────────────────────────────────────────
|
||||
|
||||
/** Agent lifecycle states */
|
||||
export const AGENT_STATES = ["idle", "active", "running", "paused", "error", "terminated"] as const;
|
||||
export const AGENT_STATES = ["idle", "active", "running", "paused", "error"] as const;
|
||||
export type AgentState = (typeof AGENT_STATES)[number];
|
||||
|
||||
/** Valid state transitions for agents */
|
||||
export const AGENT_VALID_TRANSITIONS: Record<AgentState, AgentState[]> = {
|
||||
idle: ["active"],
|
||||
active: ["running", "paused", "terminated"],
|
||||
running: ["active", "paused", "error", "terminated"],
|
||||
paused: ["active", "terminated"],
|
||||
error: ["active", "terminated"],
|
||||
terminated: ["idle", "active", "running"], // Can be restarted or reset
|
||||
active: ["idle", "running", "paused", "error"],
|
||||
running: ["idle", "active", "paused", "error"],
|
||||
paused: ["idle", "active"],
|
||||
error: ["idle", "active"],
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user