Files
AgentHub/server/routes/agents.js
T
nntrivi2001 03f1673fce feat(dashboard): lazy routes, Needs-you strip, URL state, exact numbers
- Workspace, SettingsHub, SessionDetail and the Workflow sub-tab are lazy
  (main bundle 2,166 kB -> ~920 kB), wrapped in RouteErrorBoundary: a stale
  chunk after a self-update reloads once (vite:preloadError too), any other
  error shows a Reload panel instead of a blank page.
- Live refresh: lib/throttle.ts + hooks/useLiveRefresh.ts replace trailing
  debounces that froze under a steady event stream; refresh skips hidden
  tabs and refetches after a WebSocket reconnect. Monitor loads only while
  shown, with one batched GET /api/agents?session_ids= instead of N+1; a
  failing endpoint is an inline error, not a blank Dashboard.
- NeedsYouStrip on Monitor and above Analytics. Tabs, filters, page, sort
  and search live in the URL (Dashboard, Sessions, Activity, SessionDetail).
- Sessions: tool / workspace / date-range filters, CSV/JSON export
  (formula-neutralised), focusable row links, Open in workspace (the dead
  /run links are gone), stale responses dropped, error + Retry.
- Fixes: "NaNs" durations, 30/7-day charts skipping idle days, weekday
  labels west of UTC (now averages), cache hit rate denominator, billable
  tokens vs cache reads, exact tool-call counts, "To" date = end of day,
  invalid ?from= no longer throws, recent list no longer shrinks to 15,
  stable message keys, transcript switch races.
- Light-theme-unreadable bg-black code panels and dark-only DAG fills use
  tokens; status configs and chart colours use theme tokens; Health tab
  and other literals translated.
- Server (additive): GET /api/agents?session_ids=, GET /api/sessions
  awaiting=1&from=&to=, stats tool_call_count/distinct_tool_count, event
  rows carry session/agent labels.
2026-10-05 13:22:04 +07:00

132 lines
4.3 KiB
JavaScript

/**
* @file Express router for managing agents, providing endpoints to list, retrieve, create, and update agents. It interacts with the database using prepared statements and broadcasts changes to connected WebSocket clients for real-time updates.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
const { Router } = require("express");
const { stmts, db } = require("../db");
const { broadcast } = require("../websocket");
const { attachAgentCosts } = require("./pricing");
const { parseSources, sessionIdInSourcesClause } = require("../lib/source-filter");
const router = Router();
router.get("/", (req, res) => {
const rawLimit = parseInt(req.query.limit);
const limit = rawLimit > 0 ? rawLimit : 10000;
const offset = parseInt(req.query.offset) || 0;
const status = req.query.status;
const session_id = req.query.session_id;
const sources = parseSources(req);
// `session_ids=a,b,c` = every agent of those sessions in ONE call (the
// Dashboard's active trees), capped so the IN list stays small.
const sessionIds =
typeof req.query.session_ids === "string"
? [...new Set(req.query.session_ids.split(",").map((s) => s.trim()))]
.filter(Boolean)
.slice(0, 200)
: null;
let rows;
if (sessionIds) {
rows = sessionIds.length
? db
.prepare(
`SELECT * FROM agents WHERE session_id IN (${sessionIds.map(() => "?").join(",")})
ORDER BY started_at DESC LIMIT ?`
)
.all(...sessionIds, limit)
: [];
} else if (session_id) {
// A session belongs to exactly one source, so no extra source filter needed.
rows = stmts.listAgentsBySession.all(session_id);
} else if (sources) {
// Data-scope active: build a dynamic query restricting agents to sessions
// from the chosen machines. `agents` carries only session_id → subquery.
const scope = sessionIdInSourcesClause(sources, "session_id");
const clauses = [scope.clause];
const params = [...scope.params];
if (status) {
clauses.push("status = ?");
params.push(status);
}
rows = db
.prepare(
`SELECT * FROM agents WHERE ${clauses.join(" AND ")} ORDER BY started_at DESC LIMIT ? OFFSET ?`
)
.all(...params, limit, offset);
} else if (status) {
rows = stmts.listAgentsByStatus.all(status, limit, offset);
} else {
rows = stmts.listAgents.all(limit, offset);
}
// Attach each agent's OWN cost (from its metadata token buckets) so subagent
// cards can show their real cost instead of the session total.
res.json({ agents: attachAgentCosts(rows), limit, offset });
});
router.get("/:id", (req, res) => {
const agent = stmts.getAgent.get(req.params.id);
if (!agent) {
return res.status(404).json({ error: { code: "NOT_FOUND", message: "Agent not found" } });
}
res.json({ agent });
});
router.post("/", (req, res) => {
const { id, session_id, name, type, subagent_type, status, task, parent_agent_id, metadata } =
req.body;
if (!id || !session_id || !name) {
return res.status(400).json({
error: { code: "INVALID_INPUT", message: "id, session_id, and name are required" },
});
}
const existing = stmts.getAgent.get(id);
if (existing) {
return res.json({ agent: existing, created: false });
}
stmts.insertAgent.run(
id,
session_id,
name,
type || "main",
subagent_type || null,
status || "waiting",
task || null,
parent_agent_id || null,
metadata ? JSON.stringify(metadata) : null
);
const agent = stmts.getAgent.get(id);
broadcast("agent_created", agent);
res.status(201).json({ agent, created: true });
});
router.patch("/:id", (req, res) => {
const { name, status, task, current_tool, ended_at, metadata } = req.body;
const existing = stmts.getAgent.get(req.params.id);
if (!existing) {
return res.status(404).json({ error: { code: "NOT_FOUND", message: "Agent not found" } });
}
stmts.updateAgent.run(
name || null,
status || null,
task || null,
current_tool !== undefined ? current_tool : existing.current_tool,
ended_at || null,
metadata ? JSON.stringify(metadata) : null,
req.params.id
);
const agent = stmts.getAgent.get(req.params.id);
broadcast("agent_updated", agent);
res.json({ agent });
});
module.exports = router;