Address agent lifecycle review feedback

This commit is contained in:
Dotta committed 2026-06-06 17:13:57 +00:00
1 parent 5ad85b4686
commit 802dc17784
5 files changed
+84 -17

No files matched your search

@@ -64,6 +64,19 @@ describe("agent work eligibility", () => {
});
});
it("reports unknown lifecycle statuses explicitly", () => {
const target = agent({ status: "sabbatical" });
const manager = agent({ id: "manager-1", name: "CTO", status: "active", reportsTo: null });
expect(getAgentWorkEligibility({ agent: target, agents: [target, manager] })).toMatchObject({
assignable: false,
invokable: false,
assignabilityReason: "unknown_status",
invokabilityReason: "unknown_status",
orgChainHealth: { status: "healthy" },
});
});
it("blocks active descendants of terminated ancestors and reports repair details", () => {
const target = agent({ id: "qa-2", name: "QA 2", status: "active", reportsTo: "cto-2" });
const terminatedManager = agent({
+8 -3
View File
@@ -5,7 +5,8 @@ export type AgentEligibilityLifecycleReason =
| "terminated"
| "pending_approval"
| "paused"
| "invalid_org_chain";
| "invalid_org_chain"
| "unknown_status";
export interface AgentEligibilityAgent {
id: string;
@@ -202,7 +203,9 @@ export function getAgentWorkEligibility(input: {
const assignabilityReason: AgentEligibilityLifecycleReason = !isAgentStatusAssignableToWork(input.agent.status)
? input.agent.status === "terminated"
? "terminated"
: "pending_approval"
: input.agent.status === "pending_approval"
? "pending_approval"
: "unknown_status"
: orgChainHealth.status === "invalid_org_chain"
? "invalid_org_chain"
: "eligible";
@@ -211,7 +214,9 @@ export function getAgentWorkEligibility(input: {
? "terminated"
: input.agent.status === "pending_approval"
? "pending_approval"
: "paused"
: input.agent.status === "paused"
? "paused"
: "unknown_status"
: orgChainHealth.status === "invalid_org_chain"
? "invalid_org_chain"
: "eligible";
@@ -15,6 +15,7 @@ type AssignabilityAgent = AgentEligibilityAgent;
type AgentAssignmentConflictReason =
| "pending_approval"
| "assignee_terminated"
| "assignee_unknown_status"
| "ancestor_terminated"
| "ancestor_missing"
| "ancestor_cross_company"
@@ -32,6 +33,11 @@ function assignmentMessage(kind: AgentAssignmentKind, reason: AgentAssignmentCon
? "Cannot assign routines to terminated agents"
: "Cannot assign work to terminated agents";
}
if (reason === "assignee_unknown_status") {
return kind === "routine"
? "Cannot assign routines to agents with an unsupported lifecycle status"
: "Cannot assign work to agents with an unsupported lifecycle status";
}
return kind === "routine"
? "Cannot assign routines to agents with an invalid org chain"
: "Cannot assign work to agents with an invalid org chain";
@@ -137,6 +143,14 @@ export async function assertAssignableAgent(
chain,
}));
}
if (eligibility.assignabilityReason === "unknown_status") {
throw conflict(assignmentMessage(kind, "assignee_unknown_status"), conflictDetails({
companyId,
assigneeAgentId: agentId,
reason: "assignee_unknown_status",
chain,
}));
}
const reason = assignmentReasonFromHealth(eligibility.orgChainHealth);
const firstInvalidAncestor = eligibility.orgChainHealth.firstInvalidAncestor;
+4 -1
View File
@@ -15,6 +15,7 @@ export type AgentInvokabilityBlockReason =
| "paused"
| "terminated"
| "pending_approval"
| "unknown_status"
| "manager_missing"
| "manager_company_mismatch"
| "manager_terminated"
@@ -84,7 +85,9 @@ export function evaluateAgentInvokability(
if (eligibility.invokable) return { invokable: true };
const directStatusReason = statusBlockReason(agent.status);
const directStatusReason = eligibility.invokabilityReason === "unknown_status"
? "unknown_status"
: statusBlockReason(agent.status);
if (directStatusReason) {
return blocked(
directStatusReason,
+45 -13
View File
@@ -159,6 +159,7 @@ import {
evaluateAgentInvokability,
evaluateAgentInvokabilityFromDb,
shouldCancelRunsForNonInvokableAgent,
type AgentOrgRow,
} from "./agent-invokability.js";
import {
redactQuarantinedBodyForHigherTrust,
@@ -3070,6 +3071,42 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {})
return evaluateAgentInvokabilityFromDb(db, agent);
}
function toAgentOrgRow(agent: Pick<typeof agents.$inferSelect, "id" | "companyId" | "name" | "reportsTo" | "status">): AgentOrgRow {
return {
id: agent.id,
companyId: agent.companyId,
name: agent.name,
reportsTo: agent.reportsTo,
status: agent.status,
};
}
async function listCompanyAgentOrgRows(companyId: string): Promise<AgentOrgRow[]> {
return db
.select({
id: agents.id,
companyId: agents.companyId,
name: agents.name,
reportsTo: agents.reportsTo,
status: agents.status,
})
.from(agents)
.where(eq(agents.companyId, companyId));
}
function groupAgentOrgRowsByCompany(agentRows: AgentOrgRow[]) {
const byCompany = new Map<string, AgentOrgRow[]>();
for (const agent of agentRows) {
const companyAgents = byCompany.get(agent.companyId);
if (companyAgents) {
companyAgents.push(agent);
} else {
byCompany.set(agent.companyId, [agent]);
}
}
return byCompany;
}
async function getRun(runId: string, opts?: { unsafeFullResultJson?: boolean }) {
const safeForLegacyEncoding = !opts?.unsafeFullResultJson && await hasUnsafeTextProjectionDatabase();
return db
@@ -6584,14 +6621,16 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {})
return Number(count ?? 0);
}
async function claimQueuedRun(run: typeof heartbeatRuns.$inferSelect) {
async function claimQueuedRun(run: typeof heartbeatRuns.$inferSelect, companyAgents?: AgentOrgRow[]) {
if (run.status !== "queued") return run;
const agent = await getAgent(run.agentId);
if (!agent) {
await cancelRunInternal(run.id, "Cancelled because the agent no longer exists");
return null;
}
const invokability = await getAgentInvokability(agent);
const invokability = companyAgents
? evaluateAgentInvokability(toAgentOrgRow(agent), companyAgents)
: await getAgentInvokability(agent);
if (!invokability.invokable) {
await cancelRunInternal(run.id, `Cancelled because the agent is not invokable: ${invokability.reason}`);
return null;
@@ -7542,6 +7581,7 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {})
: sql`false`,
);
const issueById = new Map(issueRows.map((row) => [row.id, row]));
const companyAgents = await listCompanyAgentOrgRows(agent.companyId);
const prioritizedRuns = [...queuedRuns].sort((left, right) => {
const leftIssueId = readNonEmptyString(parseObject(left.contextSnapshot).issueId);
const rightIssueId = readNonEmptyString(parseObject(right.contextSnapshot).issueId);
@@ -7563,7 +7603,7 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {})
const claimedRuns: Array<typeof heartbeatRuns.$inferSelect> = [];
for (const queuedRun of prioritizedRuns) {
if (claimedRuns.length >= availableSlots) break;
const claimed = await claimQueuedRun(queuedRun);
const claimed = await claimQueuedRun(queuedRun, companyAgents);
if (claimed) claimedRuns.push(claimed);
}
if (claimedRuns.length === 0) return [];
@@ -11016,21 +11056,13 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {})
.from(agents)
.innerJoin(companies, eq(companies.id, agents.companyId))
.where(eq(companies.status, "active"));
const agentsByCompany = groupAgentOrgRowsByCompany(allAgents.map(toAgentOrgRow));
let checked = 0;
let enqueued = 0;
let skipped = 0;
for (const agent of allAgents) {
const companyAgents = allAgents
.filter((candidate) => candidate.companyId === agent.companyId)
.map((candidate) => ({
id: candidate.id,
companyId: candidate.companyId,
name: candidate.name,
reportsTo: candidate.reportsTo,
status: candidate.status,
}));
const invokability = evaluateAgentInvokability(agent, companyAgents);
const invokability = evaluateAgentInvokability(toAgentOrgRow(agent), agentsByCompany.get(agent.companyId) ?? []);
if (!invokability.invokable) continue;
const policy = parseHeartbeatPolicy(agent);
if (!policy.enabled || policy.intervalSec <= 0) continue;