Files
AgentCompany/src/main/database/company-settings.ts
T
2026-04-05 20:14:48 -04:00

331 lines
13 KiB
TypeScript

import type { DatabaseSync } from "node:sqlite";
import type {
CompanyMetrics,
AgentMetrics,
ProfileSnapshot,
RunStatus,
StandupReport,
} from "@shared/types";
import {
type ReadAll,
type Transaction,
} from "./helpers";
export function buildCompanySettingsMethods(
getDb: () => DatabaseSync,
readAll: ReadAll,
transaction: Transaction,
deps: {
listSnapshot: (options: { companyId?: string | null }) => ProfileSnapshot;
},
) {
return {
getCompanyMetrics(companyId: string, days: number = 30): CompanyMetrics {
const snapshot = deps.listSnapshot({ companyId });
const agents = snapshot.agents;
const tasks = snapshot.tasks;
const runs = snapshot.runs;
const costs = snapshot.costs;
const cutoff = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString();
const recentRuns = runs.filter((r) => r.createdAt > cutoff);
const recentTasks = tasks.filter((t) => t.status === "done" && t.updatedAt > cutoff);
const successfulRuns = recentRuns.filter((r) => r.status === "succeeded").length;
const failedRuns = recentRuns.filter(
(r) => r.status === "failed" || r.status === "timed_out",
).length;
const totalCost = costs.reduce((sum, c) => sum + (c.amountUsd ?? 0), 0);
const completedTasks = tasks.filter((t) => t.status === "done").length;
const agentMetrics: AgentMetrics[] = agents
.filter((a) => a.status !== "pending_approval" && a.status !== "terminated")
.map((agent) => {
const agentTasks = tasks.filter((t) => t.assigneeAgentId === agent.id);
const agentRuns = runs.filter((r) => r.agentId === agent.id);
const agentRecentRuns = agentRuns.filter((r) => r.createdAt > cutoff);
const agentCosts = costs.filter((c) => c.agentId === agent.id);
const agentSucceeded = agentRecentRuns.filter((r) => r.status === "succeeded").length;
const agentFailed = agentRecentRuns.filter(
(r) => r.status === "failed" || r.status === "timed_out",
).length;
// Avg run duration
const finishedRuns = agentRuns.filter(
(r): r is typeof r & { startedAt: string; finishedAt: string } =>
Boolean(r.startedAt && r.finishedAt),
);
const avgDuration =
finishedRuns.length > 0
? finishedRuns.reduce(
(sum, r) =>
sum +
(new Date(r.finishedAt).getTime() - new Date(r.startedAt).getTime()) / 1000,
0,
) / finishedRuns.length
: 0;
// Review cycles: count task status transitions by this agent
// Actual action strings: "task.status_changed" (api-server), "task.updated" (index.ts), "task.auto_assigned" (database)
const reviewCycles = snapshot.activity.filter(
(a) =>
a.companyId === companyId &&
a.actor === agent.id &&
(a.action === "task.status_changed" ||
a.action === "task.updated" ||
a.action === "task.auto_assigned"),
).length;
const lastRun = agentRuns.sort(
(a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(),
)[0];
return {
agentId: agent.id,
agentName: agent.name,
tasksCompleted: agentTasks.filter((t) => t.status === "done").length,
tasksAssigned: agentTasks.filter(
(t) => t.status !== "done" && t.status !== "cancelled",
).length,
tasksFailed: agentTasks.filter((t) => t.status === "cancelled").length,
successRate:
agentRecentRuns.length > 0
? Math.round((agentSucceeded / agentRecentRuns.length) * 100)
: 0,
avgRunDurationSec: Math.round(avgDuration),
totalCostUsd: agentCosts.reduce((sum, c) => sum + (c.amountUsd ?? 0), 0),
runsTotal: agentRecentRuns.length,
runsSucceeded: agentSucceeded,
runsFailed: agentFailed,
reviewCycles,
lastActiveAt: lastRun?.createdAt ?? null,
};
});
return {
totalAgents: agents.filter(
(a) => a.status !== "pending_approval" && a.status !== "terminated",
).length,
activeAgents: agents.filter((a) => a.status === "active" || a.status === "running").length,
totalTasks: tasks.length,
completedTasks,
blockedTasks: tasks.filter((t) => t.status === "blocked").length,
totalRuns: recentRuns.length,
successfulRuns,
failedRuns,
overallSuccessRate:
recentRuns.length > 0 ? Math.round((successfulRuns / recentRuns.length) * 100) : 0,
totalCostUsd: totalCost,
avgCostPerTask: completedTasks > 0 ? totalCost / completedTasks : 0,
throughputTasksPerDay: days > 0 ? recentTasks.length / days : 0,
agentMetrics,
};
},
getStandupReport(companyId: string): StandupReport {
const snapshot = deps.listSnapshot({ companyId });
const agents = snapshot.agents.filter(
(a) => a.status !== "terminated" && a.status !== "pending_approval",
);
const tasks = snapshot.tasks;
const runs = snapshot.runs;
const costs = snapshot.costs;
const oneDayAgo = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString();
const recentActivity = snapshot.activity.filter(
(a) => a.companyId === companyId && a.createdAt > oneDayAgo,
);
const companyHighlights: string[] = [];
const blockers: string[] = [];
const recentCompletedTasks = tasks.filter(
(t) => t.status === "done" && t.updatedAt > oneDayAgo,
);
if (recentCompletedTasks.length > 0) {
companyHighlights.push(`${recentCompletedTasks.length} task(s) completed in last 24h`);
}
const recentRuns = runs.filter((r) => r.createdAt > oneDayAgo);
const successfulRuns = recentRuns.filter((r) => r.status === "succeeded").length;
const failedRuns = recentRuns.filter(
(r) => r.status === "failed" || r.status === "timed_out",
).length;
if (recentRuns.length > 0) {
companyHighlights.push(
`${recentRuns.length} runs (${successfulRuns} succeeded, ${failedRuns} failed)`,
);
}
const blockedTasks = tasks.filter((t) => t.status === "blocked");
for (const task of blockedTasks) {
const assignee = agents.find((a) => a.id === task.assigneeAgentId);
blockers.push(`${task.title} — blocked (${assignee?.name ?? "unassigned"})`);
}
const pendingApprovals = snapshot.approvals.filter(
(a) => a.companyId === companyId && a.state === "pending",
).length;
const totalBudget = agents.reduce((sum, a) => sum + a.budgetMonthlyUsd, 0);
const totalSpent = agents.reduce((sum, a) => sum + a.spentMonthlyUsd, 0);
const budgetUtilization =
totalBudget > 0 ? Math.round((totalSpent / totalBudget) * 100) : 0;
// Active sprint progress
const activeSprint = snapshot.sprints.find(
(s) => s.companyId === companyId && s.status === "active",
);
if (activeSprint) {
const pct =
activeSprint.velocityPoints > 0
? Math.round(
(activeSprint.completedPoints / activeSprint.velocityPoints) * 100,
)
: 0;
companyHighlights.push(
`Sprint "${activeSprint.name}": ${activeSprint.completedPoints}/${activeSprint.velocityPoints} pts (${pct}%)`,
);
}
// Unread messages requiring attention
const urgentUnread = snapshot.agentMessages.filter(
(m) =>
m.companyId === companyId &&
!m.readAt &&
(m.priority === "urgent" || m.channel === "incident"),
).length;
if (urgentUnread > 0) {
blockers.push(`${urgentUnread} urgent unread message(s) need attention`);
}
const agentReports = agents.map((agent) => {
const agentTasks = tasks.filter((t) => t.assigneeAgentId === agent.id);
const agentRuns = runs.filter((r) => r.agentId === agent.id);
const agentRecentRuns = agentRuns.filter((r) => r.createdAt > oneDayAgo);
const agentCosts = costs.filter(
(c) => c.agentId === agent.id && c.createdAt > oneDayAgo,
);
const lastRun = agentRecentRuns.sort(
(a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(),
)[0];
const highlights: string[] = [];
const completedRecently = agentTasks.filter(
(t) => t.status === "done" && t.updatedAt > oneDayAgo,
);
if (completedRecently.length > 0)
highlights.push(`Completed ${completedRecently.length} task(s)`);
if (agentRecentRuns.length > 0)
highlights.push(`${agentRecentRuns.length} run(s) in last 24h`);
const reviewedTasks = recentActivity.filter(
(a) =>
a.actor === agent.id &&
(a.action === "task.status_changed" ||
a.action === "task.updated" ||
a.action === "task.auto_assigned"),
).length;
if (reviewedTasks > 0) highlights.push(`${reviewedTasks} status update(s)`);
return {
agentId: agent.id,
agentName: agent.name,
role: agent.role,
status: agent.status,
tasksInProgress: agentTasks.filter((t) => t.status === "in_progress").length,
tasksCompleted24h: completedRecently.length,
tasksBlocked: agentTasks.filter((t) => t.status === "blocked").length,
lastRunStatus: (lastRun?.status ?? null) as RunStatus | null,
lastRunAt: lastRun?.createdAt ?? null,
costLast24h: agentCosts.reduce((sum, c) => sum + (c.amountUsd ?? 0), 0),
highlights,
};
});
const summaryParts: string[] = [];
if (agents.length > 0) summaryParts.push(`${agents.length} active agents`);
summaryParts.push(`${recentCompletedTasks.length} tasks done`);
if (blockedTasks.length > 0) summaryParts.push(`${blockedTasks.length} blocked`);
if (pendingApprovals > 0) summaryParts.push(`${pendingApprovals} pending approvals`);
return {
companyId,
generatedAt: new Date().toISOString(),
summary: summaryParts.join(" · "),
agentReports,
companyHighlights,
blockers,
pendingApprovals,
budgetUtilization,
};
},
getSidebarBadges(companyId: string): {
pendingApprovals: number;
activeRuns: number;
failedRuns: number;
todoTasks: number;
budgetWarnings: number;
unreadMessages: number;
} {
const BOARD_READER_ID = "__board__";
const pendingApprovals =
(
getDb()
.prepare(
"select count(*) as c from approvals where company_id = ? and state = 'pending'",
)
.get(companyId) as { c: number }
)?.c ?? 0;
const activeRuns =
(
getDb()
.prepare(
"select count(*) as c from runs where company_id = ? and status in ('queued', 'running')",
)
.get(companyId) as { c: number }
)?.c ?? 0;
const failedRuns =
(
getDb()
.prepare(
"select count(*) as c from runs where company_id = ? and status in ('failed', 'timed_out', 'interrupted')",
)
.get(companyId) as { c: number }
)?.c ?? 0;
const todoTasks =
(
getDb()
.prepare(
"select count(*) as c from tasks where company_id = ? and status in ('todo', 'in_progress', 'in_review')",
)
.get(companyId) as { c: number }
)?.c ?? 0;
const budgetWarnings =
(
getDb()
.prepare(
"select count(*) as c from agents where company_id = ? and budget_monthly_usd > 0 and (spent_monthly_usd * 1.0 / budget_monthly_usd) >= 0.8",
)
.get(companyId) as { c: number }
)?.c ?? 0;
const unreadMessages =
(
getDb()
.prepare(
`select count(*) as c
from agent_messages messages
left join agent_message_reads reads
on reads.message_id = messages.id
and reads.agent_id = ?
where messages.company_id = ?
and reads.read_at is null
and (messages.channel = 'company' or messages.channel = 'incident' or messages.priority = 'urgent')`,
)
.get(BOARD_READER_ID, companyId) as { c: number }
)?.c ?? 0;
return { pendingApprovals, activeRuns, failedRuns, todoTasks, budgetWarnings, unreadMessages };
},
};
}