feat(FN-4199): complete Step 2 — extend agents stats capacity counts

Fusion-Task-Id: FN-4199
Fusion-Task-Lineage: fd530f3d-ef2e-4684-ab05-5aaab6856f15
This commit is contained in:
Fusion
2026-05-13 04:00:13 -07:00
committed by gsxdsm
parent d31c17b41a
commit 031ef2b777
2 changed files with 55 additions and 1 deletions

View File

@@ -4250,6 +4250,47 @@ describe("Agent stale task-link sanitization", () => {
expect(res.body.assignedTaskCount).toBe(0);
});
it("GET /api/agents/stats returns idle non-ephemeral and todo counts", async () => {
const store = createMockStore({
getFusionDir: vi.fn().mockReturnValue(fusionDir),
listTasks: vi.fn().mockResolvedValue([
{ id: "FN-1", column: "todo" },
{ id: "FN-2", column: "todo" },
{ id: "FN-3", column: "triage" },
{ id: "FN-4", column: "in-progress" },
{ id: "FN-5", column: "in-review" },
{ id: "FN-6", column: "done" },
]),
} as any);
const app = express();
app.use(express.json());
app.use("/api", createApiRoutes(store));
const { AgentStore } = await import("@fusion/core");
const agentStore = new AgentStore({ rootDir: fusionDir });
await agentStore.init();
const idleNonEphemeral = await agentStore.createAgent({ name: "Idle Exec", role: "executor" });
await agentStore.updateAgentState(idleNonEphemeral.id, "idle");
const idleEphemeral = await agentStore.createAgent({
name: "executor-ephemeral",
role: "executor",
metadata: { type: "spawned" },
});
await agentStore.updateAgentState(idleEphemeral.id, "idle");
const active = await agentStore.createAgent({ name: "Active Exec", role: "executor" });
await agentStore.updateAgentState(active.id, "active");
const res = await GET(app, "/api/agents/stats");
expect(res.status).toBe(200);
expect(res.body.idleNonEphemeralCount).toBe(1);
expect(res.body.todoTaskCount).toBe(2);
});
it("GET /api/agents handles task lookup failure gracefully", async () => {
const taskId = "FN-LOOKUP-FAIL";
const store = createMockStore({

View File

@@ -6,6 +6,7 @@ import {
ApprovalRequestStore,
getDefaultHeartbeatProcedurePath,
isAgentPermissionPolicyPresetId,
isEphemeralAgent,
normalizeAgentPermissionPolicyFromPreset,
} from "@fusion/core";
import { ApiError, badRequest, notFound } from "../api-error.js";
@@ -268,6 +269,7 @@ export function registerAgentCoreRoutes(ctx: ApiRoutesContext, deps: AgentCoreRo
* Return aggregate stats across all agents.
* Must be registered before /agents/:id to avoid "stats" matching :id.
* Note: assignedTaskCount excludes agents whose linked task is in a terminal state.
* Includes idleNonEphemeralCount and todoTaskCount to back capacity-risk signaling.
*/
router.get("/agents/stats", async (req, res) => {
try {
@@ -278,6 +280,7 @@ export function registerAgentCoreRoutes(ctx: ApiRoutesContext, deps: AgentCoreRo
const agents = await agentStore.listAgents();
const activeCount = agents.filter((a) => a.state === "active" || a.state === "running").length;
const idleNonEphemeralCount = agents.filter((a) => a.state === "idle" && !isEphemeralAgent(a)).length;
// Count only agents with non-terminal linked tasks
const sanitizedAgents = await sanitizeAgentTaskLinks(agents, scopedStore);
@@ -293,7 +296,17 @@ export function registerAgentCoreRoutes(ctx: ApiRoutesContext, deps: AgentCoreRo
const total = completedRuns + failedRuns;
const successRate = total > 0 ? completedRuns / total : 0;
res.json({ activeCount, assignedTaskCount, completedRuns, failedRuns, successRate });
const tasks = await scopedStore.listTasks({ slim: true, includeArchived: false });
const todoTaskCount = tasks.filter((task) => task.column === "todo").length;
res.json({
activeCount,
assignedTaskCount,
completedRuns,
failedRuns,
successRate,
idleNonEphemeralCount,
todoTaskCount,
});
} catch (err: unknown) {
if (err instanceof ApiError) {
throw err;