feat(FN-942): add resetAgent method and allow terminated→idle transition

- Add terminated→idle as a valid state transition in AGENT_VALID_TRANSITIONS
- Add resetAgent() method that clears lastError, taskId, and ends heartbeat runs
- Clear lastError automatically when transitioning away from terminated state
- Remove inline heartbeat lifecycle from updateAgentState (moved to resetAgent)
- Add comprehensive tests for state transitions, error clearing, and resetAgent behavior
This commit is contained in:
gsxdsm
2026-04-04 17:56:07 -07:00
parent 63fbeb8d37
commit d916628442
3 changed files with 110 additions and 17 deletions

View File

@@ -255,24 +255,14 @@ export class AgentStore extends EventEmitter {
...agent,
state: newState,
updatedAt: new Date().toISOString(),
// Clear lastError when transitioning away from terminated
...(currentState === "terminated" && newState !== "terminated" && { lastError: undefined }),
};
await this.writeAgent(updated);
this.emit("agent:stateChanged", agentId, currentState, newState);
this.emit("agent:updated", updated, currentState);
// Handle heartbeat run lifecycle
if (newState === "active" && !agent.lastHeartbeatAt) {
// Starting first activity - start a heartbeat run
await this.startHeartbeatRun(agentId);
} else if (newState === "terminated") {
// End the active run if any
const activeRun = await this.getActiveHeartbeatRun(agentId);
if (activeRun) {
await this.endHeartbeatRun(activeRun.id, "terminated");
}
}
return updated;
});
}
@@ -303,6 +293,37 @@ export class AgentStore extends EventEmitter {
});
}
/**
* Reset an agent from any state back to "idle".
* Clears lastError, taskId, and ends any active heartbeat run.
* Uses updateAgentState internally for proper validation and event emission.
* @param agentId - The agent ID
* @returns The reset agent
* @throws Error if agent not found or transition is invalid
*/
async resetAgent(agentId: string): Promise<Agent> {
// End any active heartbeat run before transitioning
const activeRun = await this.getActiveHeartbeatRun(agentId);
if (activeRun) {
await this.endHeartbeatRun(activeRun.id, "terminated");
}
// Transition state via updateAgentState (validates transition, emits events)
const agent = await this.updateAgentState(agentId, "idle");
// Clear taskId and lastError on top of the state transition
const reset: Agent = {
...agent,
taskId: undefined,
lastError: undefined,
updatedAt: new Date().toISOString(),
};
await this.writeAgent(reset);
return reset;
}
/**
* List all agents, optionally filtered by state.
* @param filter - Optional filter criteria