Make the hire_agent department contract explicit

The CEO's first run hired five reports into departments "general" and
"engineering"; the hiring-policy lookup was exact-match against the
seeded names (Executive, Engineering, ...), so every hire failed and
the kickoff produced nothing. The tool schema now enumerates the
canonical departments, the policy lookup matches case-insensitively
and stores the canonical name on the agent, and an unknown department
error lists the valid choices so the model can self-correct.
This commit is contained in:
ZacharyZhang-NY committed 2026-06-09 21:21:01 -04:00
1 parent 2dd4f3976a
commit d6d6656428
4 files changed
+63 -12

No files matched your search

@@ -1,5 +1,6 @@
import type { DatabaseSync } from "node:sqlite";
import { RISK_LEVEL, TASK_PRIORITY, TASK_STATUS, type RiskLevel } from "../../shared/domain";
import { ORGANIZATION_DEPARTMENTS } from "./org-governance-service";
import type { OrganizationService } from "./organization-service";
import type { ChatService } from "./chat-service";
import { writeDeliverable } from "./deliverable-writer";
@@ -38,7 +39,7 @@ export const AGENT_TOOLS: ToolDefinition[] = [
properties: {
name: { type: "string", description: "Full name of the new agent." },
role: { type: "string", description: "Their role/title, e.g. 'Growth Marketer'." },
department: { type: "string", description: "Department, e.g. 'Marketing' or 'Operations'." },
department: { type: "string", enum: [...ORGANIZATION_DEPARTMENTS], description: "The company department the hire belongs to." },
model: { type: "string", description: "Optional model id; defaults to your own model." },
},
required: ["name", "role", "department"],
@@ -5,7 +5,7 @@ import { createId } from "../../shared/ids";
import { AuditService } from "./audit-service";
import { OperationsService } from "./operations-service";
const DEFAULT_DEPARTMENTS = ["Executive", "Engineering", "Product", "Design", "Marketing", "Operations", "Support"] as const;
export const ORGANIZATION_DEPARTMENTS = ["Executive", "Engineering", "Product", "Design", "Marketing", "Operations", "Support"] as const;
const RISK_RANK: Record<RiskLevel, number> = { low: 0, medium: 1, high: 2 };
export class OrgGovernanceService {
@@ -18,7 +18,7 @@ export class OrgGovernanceService {
}
seedOrganizationGovernance(orgId: string, boardEmployeeId: string, now: string): void {
for (const department of DEFAULT_DEPARTMENTS) this.seedDepartmentPolicy(orgId, department, now);
for (const department of ORGANIZATION_DEPARTMENTS) this.seedDepartmentPolicy(orgId, department, now);
this.db.prepare(`
insert into approval_routing_rules(id, org_id, kind, minimum_risk, route_to_type, route_to_id, created_at)
values (?, ?, ?, ?, ?, ?, ?)
@@ -27,14 +27,19 @@ export class OrgGovernanceService {
}
enforceHiringPolicy(input: CreateAgentInput): HiringPolicyDecision {
const departmentName = input.department.trim();
if (!departmentName) throw new Error("Department is required.");
const requested = input.department.trim();
if (!requested) throw new Error("Department is required.");
const policy = this.db.prepare(`
select max_headcount, monthly_budget_limit_cents, require_approval_above_cents
select department_name, max_headcount, monthly_budget_limit_cents, require_approval_above_cents
from hiring_policies
where org_id = ? and department_name = ?
`).get(input.orgId, departmentName) as HiringPolicyRow | undefined;
if (!policy) throw new Error("Hiring policy is required for department.");
where org_id = ? and lower(department_name) = lower(?)
`).get(input.orgId, requested) as HiringPolicyRow | undefined;
if (!policy) {
const known = this.db.prepare("select department_name from hiring_policies where org_id = ? order by department_name asc")
.all(input.orgId) as Array<{ department_name: string }>;
throw new Error(`Unknown department "${requested}". Departments: ${known.map((row) => row.department_name).join(", ")}.`);
}
const departmentName = policy.department_name;
const usage = this.db.prepare(`
select count(*) as headcount, coalesce(sum(budget_cents), 0) as budget_cents
from agents
@@ -44,7 +49,7 @@ export class OrgGovernanceService {
if (usage.budget_cents + input.budgetCents > policy.monthly_budget_limit_cents) {
throw new Error("Department monthly budget limit has been reached.");
}
return { reviewRequired: input.budgetCents > policy.require_approval_above_cents };
return { reviewRequired: input.budgetCents > policy.require_approval_above_cents, departmentName };
}
routeApproval(input: { orgId: string; approvalId: string; kind: string; risk: RiskLevel }): void {
@@ -121,6 +126,7 @@ export class OrgGovernanceService {
}
interface HiringPolicyRow {
department_name: string;
max_headcount: number;
monthly_budget_limit_cents: number;
require_approval_above_cents: number;
@@ -128,6 +134,8 @@ interface HiringPolicyRow {
interface HiringPolicyDecision {
reviewRequired: boolean;
/** Canonical department name from the policy table (the input may differ in case). */
departmentName: string;
}
interface ApprovalRoutingRuleRow {
@@ -161,7 +161,7 @@ export class OrganizationService {
input.orgId,
input.name.trim(),
input.role.trim(),
input.department.trim(),
hiringPolicy.departmentName,
input.modelProviderId,
input.modelId,
input.budgetCents,
@@ -182,7 +182,7 @@ export class OrganizationService {
actorType: "system",
actorId: null,
action: "created",
payload: { role: input.role, department: input.department },
payload: { role: input.role, department: hiringPolicy.departmentName },
});
this.addAgentToWorkspaceSpaces(input.orgId, id);
this.identityAccess.createAgentServiceIdentity(input.orgId, id, input.name, now);
@@ -707,4 +707,46 @@ describe("domain services", () => {
expect(ownedKickoff.assignee_agent_id).toBe(ownerId);
connection.close();
});
test("hiring normalizes department case and rejects unknown departments with guidance", () => {
const connection = createConnection();
const organization = new OrganizationService(connection.db);
let orgId = "";
connection.transaction(() => {
orgId = organization.createOrganization({
organizationName: "Acme Operations",
employeeName: "Dana Lee",
mission: "Coordinate agents with auditable approvals.",
goal: "Ship a verified control plane.",
});
});
let agentId = "";
connection.transaction(() => {
agentId = organization.createAgent({
orgId,
name: "Zhang Hao",
role: "Algorithm Lead",
department: "engineering",
modelProviderId: null,
modelId: null,
budgetCents: 25000,
managerAgentId: null,
});
});
const hired = connection.db.prepare("select department from agents where id = ?").get(agentId) as { department: string };
expect(hired.department).toBe("Engineering");
expect(() => connection.transaction(() => organization.createAgent({
orgId,
name: "Chen Wei",
role: "Research Lead",
department: "general",
modelProviderId: null,
modelId: null,
budgetCents: 25000,
managerAgentId: null,
}))).toThrow(/Unknown department "general"\. Departments: .*Engineering.*Executive/);
connection.close();
});
});