diff --git a/AgentCompany-New/src/main/services/agent-tools.ts b/AgentCompany-New/src/main/services/agent-tools.ts index 5365559d..1708dd77 100644 --- a/AgentCompany-New/src/main/services/agent-tools.ts +++ b/AgentCompany-New/src/main/services/agent-tools.ts @@ -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"], diff --git a/AgentCompany-New/src/main/services/org-governance-service.ts b/AgentCompany-New/src/main/services/org-governance-service.ts index 84198b1a..73793aab 100644 --- a/AgentCompany-New/src/main/services/org-governance-service.ts +++ b/AgentCompany-New/src/main/services/org-governance-service.ts @@ -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 = { 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 { diff --git a/AgentCompany-New/src/main/services/organization-service.ts b/AgentCompany-New/src/main/services/organization-service.ts index 1c442a0d..dd9891b6 100644 --- a/AgentCompany-New/src/main/services/organization-service.ts +++ b/AgentCompany-New/src/main/services/organization-service.ts @@ -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); diff --git a/AgentCompany-New/tests/unit/domain-services.test.ts b/AgentCompany-New/tests/unit/domain-services.test.ts index 36d2e61d..f4481d49 100644 --- a/AgentCompany-New/tests/unit/domain-services.test.ts +++ b/AgentCompany-New/tests/unit/domain-services.test.ts @@ -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(); + }); });