Files
AgentCompany/src/main/database/agents-hierarchy.ts
T
ZacharyZhang-NY 28296b3d5f refactor(api-server): move agents SQL into db.agents/db.activity/db.approvals methods
Replace seven inline queryAll calls in routes-agents.ts with typed
domain methods. db.agents gains listAgentsForApi, listAgentsForStandup,
listExistingAgentsForHireCheck. db.tasks gains listTasksForStandup.
db.approvals gains listApprovalsForStandup and
listPendingHireApprovalPayloads. db.activity gains listRecentTaskComments
(safe on empty taskIds). All HTTP response projections stay identical.
2026-04-16 22:37:10 -04:00

367 lines
14 KiB
TypeScript

import type { DatabaseSync } from "node:sqlite";
import type { AgentRecord } from "@shared/types";
import {
type ReadAll,
type Transaction,
nowIso,
parseJson,
toBoolean,
} from "./helpers";
/**
* Agent row shape returned by listAgentsForApi. Mirrors the historical
* inline SQL projection in routes-agents.ts so HTTP response bodies
* stay byte-stable. heartbeatEnabled is exposed as a boolean.
*/
export type AgentApiRow = {
id: string;
companyId: string;
name: string;
role: string;
title: string | null;
department: string | null;
status: string;
reportsTo: string | null;
connectorId: string;
workspaceId: string | null;
model: string | null;
capabilities: string | null;
budgetMonthlyUsd: number | null;
spentMonthlyUsd: number | null;
metadataJson: string;
heartbeatEnabled: boolean;
heartbeatIntervalSec: number | null;
lastHeartbeatAt: string | null;
createdAt: string;
updatedAt: string;
};
/**
* Slim agent projection used by the me/standup aggregation.
*/
export type AgentStandupRow = {
id: string;
companyId: string;
name: string;
role: string;
status: string;
reportsTo: string | null;
department: string | null;
};
export function buildAgentHierarchyMethods(
getDb: () => DatabaseSync,
readAll: ReadAll,
transaction: Transaction,
deps: {
getAgent: (id: string) => AgentRecord;
},
) {
return {
getChainOfCommand(agentId: string): AgentRecord[] {
const rows = readAll<AgentRecord & { depth: number }>(
`WITH RECURSIVE chain AS (
SELECT id, company_id as companyId, name, role, title, department, status,
reports_to as reportsTo, connector_id as connectorId, workspace_id as workspaceId,
model, capabilities, budget_monthly_usd as budgetMonthlyUsd,
spent_monthly_usd as spentMonthlyUsd, metadata_json as metadataJson,
heartbeat_enabled as heartbeatEnabled, heartbeat_interval_sec as heartbeatIntervalSec,
last_heartbeat_at as lastHeartbeatAt, created_at as createdAt, updated_at as updatedAt,
0 as depth
FROM agents WHERE id = ?
UNION ALL
SELECT a.id, a.company_id, a.name, a.role, a.title, a.department, a.status,
a.reports_to, a.connector_id, a.workspace_id,
a.model, a.capabilities, a.budget_monthly_usd,
a.spent_monthly_usd, a.metadata_json,
a.heartbeat_enabled, a.heartbeat_interval_sec,
a.last_heartbeat_at, a.created_at, a.updated_at,
c.depth + 1
FROM agents a JOIN chain c ON a.id = c.reportsTo
WHERE c.depth < 20
)
SELECT * FROM chain ORDER BY depth`,
agentId,
);
// Skip self (depth 0); return ancestors from direct manager upward.
return rows.slice(1).map((record) => ({
...record,
heartbeatEnabled: toBoolean(record.heartbeatEnabled),
})) as AgentRecord[];
},
getDirectReports(agentId: string, companyId: string): AgentRecord[] {
return readAll<AgentRecord>(
"select id, company_id as companyId, name, role, title, department, status, reports_to as reportsTo, connector_id as connectorId, workspace_id as workspaceId, model, capabilities, budget_monthly_usd as budgetMonthlyUsd, spent_monthly_usd as spentMonthlyUsd, metadata_json as metadataJson, heartbeat_enabled as heartbeatEnabled, heartbeat_interval_sec as heartbeatIntervalSec, last_heartbeat_at as lastHeartbeatAt, created_at as createdAt, updated_at as updatedAt from agents where company_id = ? and reports_to = ?",
companyId,
agentId,
).map((record) => ({
...record,
heartbeatEnabled: toBoolean(record.heartbeatEnabled),
})) as AgentRecord[];
},
setHeartbeat(agentId: string, enabled: boolean, intervalSec: number): void {
getDb()
.prepare(
"update agents set heartbeat_enabled = ?, heartbeat_interval_sec = ?, updated_at = ? where id = ?",
)
.run(enabled ? 1 : 0, intervalSec, nowIso(), agentId);
},
markHeartbeatAt(agentId: string): void {
getDb()
.prepare("update agents set last_heartbeat_at = ? where id = ?")
.run(nowIso(), agentId);
},
/**
* Returns all agents for a company with the full API projection used
* by GET /api/companies/:companyId/agents. Mirrors the historical
* inline SQL and maps heartbeat_enabled into a boolean.
*/
listAgentsForApi(companyId: string): AgentApiRow[] {
return readAll<AgentApiRow & { heartbeatEnabled: number | boolean }>(
"select id, company_id as companyId, name, role, title, department, status, reports_to as reportsTo, connector_id as connectorId, workspace_id as workspaceId, model, capabilities, budget_monthly_usd as budgetMonthlyUsd, spent_monthly_usd as spentMonthlyUsd, metadata_json as metadataJson, heartbeat_enabled as heartbeatEnabled, heartbeat_interval_sec as heartbeatIntervalSec, last_heartbeat_at as lastHeartbeatAt, created_at as createdAt, updated_at as updatedAt from agents where company_id = ? order by created_at asc",
companyId,
).map((a) => ({ ...a, heartbeatEnabled: Boolean(a.heartbeatEnabled) })) as AgentApiRow[];
},
/**
* Returns the slim standup projection used by
* GET /api/agents/me/standup — only the fields that feed the
* aggregation (id, name, role, status, reportsTo, department).
*/
listAgentsForStandup(companyId: string): AgentStandupRow[] {
return readAll<AgentStandupRow>(
"select id, company_id as companyId, name, role, status, reports_to as reportsTo, department from agents where company_id = ? order by created_at asc",
companyId,
);
},
/**
* Returns the duplicate-detection projection used by
* POST /api/companies/:id/agents to reject hires with a name that
* already belongs to a non-terminated agent.
*/
listExistingAgentsForHireCheck(
companyId: string,
): Array<{ id: string; name: string; role: string }> {
return readAll<{ id: string; name: string; role: string }>(
"select id, name, role from agents where company_id = ? and status != 'terminated'",
companyId,
);
},
listHeartbeatEligibleAgents(companyId: string): AgentRecord[] {
return readAll<AgentRecord>(
"select id, company_id as companyId, name, role, title, department, status, reports_to as reportsTo, connector_id as connectorId, workspace_id as workspaceId, model, capabilities, budget_monthly_usd as budgetMonthlyUsd, spent_monthly_usd as spentMonthlyUsd, metadata_json as metadataJson, heartbeat_enabled as heartbeatEnabled, heartbeat_interval_sec as heartbeatIntervalSec, last_heartbeat_at as lastHeartbeatAt, created_at as createdAt, updated_at as updatedAt from agents where company_id = ? and heartbeat_enabled = 1 and status not in ('terminated', 'paused', 'error', 'pending_approval')",
companyId,
).map((record) => ({
...record,
heartbeatEnabled: toBoolean(record.heartbeatEnabled),
})) as AgentRecord[];
},
getCeoForCompany(
companyId: string,
): { id: string; name: string; lastHeartbeatAt: string | null } | null {
const row = getDb()
.prepare(
"select id, name, last_heartbeat_at as lastHeartbeatAt from agents where company_id = ? and reports_to is null and status != 'terminated' limit 1",
)
.get(companyId) as
| { id: string; name: string; lastHeartbeatAt: string | null }
| undefined;
return row ?? null;
},
listActiveAgentsForCompany(
companyId: string,
): Array<{
id: string;
name: string;
department: string | null;
lastHeartbeatAt: string | null;
}> {
return readAll<{
id: string;
name: string;
department: string | null;
lastHeartbeatAt: string | null;
}>(
"select id, name, department, last_heartbeat_at as lastHeartbeatAt from agents where company_id = ? and status not in ('terminated', 'paused', 'pending_approval')",
companyId,
);
},
getIdleAgentsForDepartment(
companyId: string,
department: string,
): Array<{ id: string; name: string }> {
return readAll(
"select id, name from agents where company_id = ? and department = ? and status in ('idle', 'active') order by spent_monthly_usd asc",
companyId,
department,
);
},
getManagerForAgent(
agentId: string,
): { id: string; name: string; companyId: string } | null {
const agent = getDb()
.prepare("select reports_to, company_id from agents where id = ?")
.get(agentId) as { reports_to: string | null; company_id: string } | undefined;
if (!agent || !agent.reports_to) return null;
const manager = getDb()
.prepare("select id, name from agents where id = ?")
.get(agent.reports_to) as { id: string; name: string } | undefined;
return manager ? { ...manager, companyId: agent.company_id } : null;
},
resetMonthlyBudgets(companyId: string): void {
getDb()
.prepare(
"update agents set spent_monthly_usd = 0, updated_at = ? where company_id = ? and spent_monthly_usd > 0",
)
.run(nowIso(), companyId);
},
getAgentPermissions(agentId: string): Record<string, boolean> {
const row = getDb()
.prepare("SELECT permissions_json FROM agents WHERE id = ?")
.get(agentId) as { permissions_json: string } | undefined;
return parseJson<Record<string, boolean>>(row?.permissions_json, {});
},
setAgentPermission(
agentId: string,
companyId: string,
permissionKey: string,
enabled: boolean,
): void {
const row = getDb()
.prepare("SELECT permissions_json FROM agents WHERE id = ?")
.get(agentId) as { permissions_json: string } | undefined;
const current = parseJson<Record<string, boolean>>(row?.permissions_json, {});
if (enabled) {
current[permissionKey] = true;
} else {
delete current[permissionKey];
}
const ts = nowIso();
getDb()
.prepare(
"UPDATE agents SET permissions_json = ?, updated_at = ? WHERE id = ? AND company_id = ?",
)
.run(JSON.stringify(current), ts, agentId, companyId);
},
hasAgentPermission(agentId: string, permissionKey: string): boolean {
const row = getDb()
.prepare("SELECT permissions_json FROM agents WHERE id = ?")
.get(agentId) as { permissions_json: string } | undefined;
const perms = parseJson<Record<string, boolean>>(row?.permissions_json, {});
return perms[permissionKey] === true;
},
getRuntimeState(agentId: string): Record<string, unknown> {
const row = getDb()
.prepare("SELECT runtime_state_json FROM agents WHERE id = ?")
.get(agentId) as { runtime_state_json: string | null } | undefined;
return parseJson<Record<string, unknown>>(row?.runtime_state_json, {});
},
setRuntimeState(
agentId: string,
companyId: string,
state: Record<string, unknown>,
): void {
const ts = nowIso();
getDb()
.prepare(
"UPDATE agents SET runtime_state_json = ?, updated_at = ? WHERE id = ? AND company_id = ?",
)
.run(JSON.stringify(state), ts, agentId, companyId);
},
getHeartbeatPolicy(agentId: string): {
enabled: boolean;
intervalSec: number;
cooldownSec: number;
maxConcurrentRuns: number;
wakeOnDemand: boolean;
} {
const row = getDb()
.prepare(
"SELECT heartbeat_enabled, heartbeat_interval_sec, heartbeat_policy_json FROM agents WHERE id = ?",
)
.get(agentId) as
| {
heartbeat_enabled: number;
heartbeat_interval_sec: number;
heartbeat_policy_json: string | null;
}
| undefined;
if (!row) throw new Error(`Agent ${agentId} not found.`);
const policy = parseJson<Record<string, unknown>>(row.heartbeat_policy_json, {});
return {
enabled: toBoolean(row.heartbeat_enabled),
intervalSec: row.heartbeat_interval_sec ?? 120,
cooldownSec: typeof policy.cooldownSec === "number" ? policy.cooldownSec : 10,
maxConcurrentRuns:
typeof policy.maxConcurrentRuns === "number" ? policy.maxConcurrentRuns : 1,
wakeOnDemand: policy.wakeOnDemand !== false,
};
},
setHeartbeatPolicy(
agentId: string,
companyId: string,
policy: {
enabled?: boolean;
intervalSec?: number;
cooldownSec?: number;
maxConcurrentRuns?: number;
wakeOnDemand?: boolean;
},
): void {
const row = getDb()
.prepare(
"SELECT heartbeat_enabled, heartbeat_interval_sec, heartbeat_policy_json FROM agents WHERE id = ?",
)
.get(agentId) as
| {
heartbeat_enabled: number;
heartbeat_interval_sec: number;
heartbeat_policy_json: string | null;
}
| undefined;
if (!row) throw new Error(`Agent ${agentId} not found.`);
const current = parseJson<Record<string, unknown>>(row.heartbeat_policy_json, {});
const merged = {
enabled: policy.enabled ?? toBoolean(row.heartbeat_enabled),
intervalSec: policy.intervalSec ?? row.heartbeat_interval_sec ?? 120,
cooldownSec:
policy.cooldownSec ??
(typeof current.cooldownSec === "number" ? current.cooldownSec : 10),
maxConcurrentRuns:
policy.maxConcurrentRuns ??
(typeof current.maxConcurrentRuns === "number" ? current.maxConcurrentRuns : 1),
wakeOnDemand: policy.wakeOnDemand ?? (current.wakeOnDemand !== false),
};
const policyJson = JSON.stringify({
cooldownSec: merged.cooldownSec,
maxConcurrentRuns: merged.maxConcurrentRuns,
wakeOnDemand: merged.wakeOnDemand,
});
const ts = nowIso();
getDb()
.prepare(
"UPDATE agents SET heartbeat_enabled = ?, heartbeat_interval_sec = ?, heartbeat_policy_json = ?, updated_at = ? WHERE id = ? AND company_id = ?",
)
.run(merged.enabled ? 1 : 0, merged.intervalSec, policyJson, ts, agentId, companyId);
},
};
}