Files
AgentCompany/AgentCompany-New/src/main/services/onboarding-service.ts
T

253 lines
8.9 KiB
TypeScript

import type { DatabaseSync } from "node:sqlite";
import type {
BootstrapOnboardingInput,
BootstrapOnboardingSummary,
SaveProviderCredentialInput,
} from "../../shared/commands";
import { PROVIDER_AUTH_TYPE, RISK_LEVEL, TASK_STATUS } from "../../shared/domain";
import { PROVIDER_CATALOG } from "../../shared/provider-catalog";
import { createId } from "../../shared/ids";
import { AuditService } from "./audit-service";
import { OrganizationService } from "./organization-service";
interface OnboardingServiceDeps {
db: DatabaseSync;
saveProviderCredential(input: SaveProviderCredentialInput): void;
}
export class OnboardingService {
private readonly audit: AuditService;
private readonly organization: OrganizationService;
constructor(private readonly deps: OnboardingServiceDeps) {
this.audit = new AuditService(deps.db);
this.organization = new OrganizationService(deps.db);
}
bootstrap(input: BootstrapOnboardingInput): BootstrapOnboardingSummary {
const provider = this.resolveProvider(input.providerKey);
const orgId = this.organization.createOrganization({
organizationName: input.companyName,
description: input.companyDescription,
mission: input.companyDescription,
goal: input.goalTitle,
employeeName: input.employeeName,
executionMode: input.executionMode,
autoApproveHires: input.autoApproveHires,
reviewDeliverables: input.reviewDeliverables,
});
const employeeId = this.requirePrimaryEmployee(orgId);
const providerId = this.requireProvider(orgId, provider.key);
this.deps.saveProviderCredential({
orgId,
providerId,
secret: input.providerSecret,
model: input.providerModel,
});
const now = new Date().toISOString();
const workspaceId = this.createWorkspace(orgId, input.workspacePath.trim(), now);
const ceoAgentId = this.organization.createAgent({
orgId,
name: "CEO",
role: "CEO",
title: "Chief Executive Officer",
department: "Executive",
modelProviderId: providerId,
modelId: input.providerModel.trim(),
budgetCents: 25000,
managerAgentId: null,
roleLevel: "c_suite",
modelStrategy: "explicit",
workspaceId,
heartbeatEnabled: true,
heartbeatIntervalSec: 120,
});
const goalId = this.createGoal(orgId, ceoAgentId, input, now);
const projectId = this.createProject(orgId, goalId, ceoAgentId, input, now);
if (workspaceId) this.bindWorkspaceToProject(workspaceId, projectId, now);
const taskId = this.createKickoffTask(orgId, goalId, projectId, workspaceId, ceoAgentId, input, now);
const runId = this.createTaskRun(orgId, taskId, ceoAgentId, providerId, workspaceId, now);
this.audit.record({
orgId,
entityType: "organization",
entityId: orgId,
actorType: "employee",
actorId: employeeId,
action: "bootstrapped",
payload: { providerKey: provider.key, goalId, projectId, taskId, runId },
});
return { orgId, employeeId, ceoAgentId, workspaceId, goalId, projectId, taskId, runId, providerId };
}
private resolveProvider(providerKey: string) {
const provider = PROVIDER_CATALOG.find((entry) => entry.key === providerKey);
if (!provider) throw new Error(`Provider ${providerKey} is unavailable.`);
if (provider.authType === PROVIDER_AUTH_TYPE.CLOUD && !provider.customEndpoint) {
throw new Error("Cloud providers need extended configuration — connect them from Settings after the company is created.");
}
return provider;
}
private requirePrimaryEmployee(orgId: string): string {
const row = this.deps.db.prepare("select id from employees where org_id = ? order by created_at asc limit 1")
.get(orgId) as { id: string } | undefined;
if (!row) throw new Error("Primary employee was not created.");
return row.id;
}
private requireProvider(orgId: string, providerKey: string): string {
const row = this.deps.db.prepare("select id from providers where org_id = ? and provider_key = ?")
.get(orgId, providerKey) as { id: string } | undefined;
if (!row) throw new Error(`Provider ${providerKey} was not created.`);
return row.id;
}
private createWorkspace(orgId: string, workspacePath: string, now: string): string | null {
if (!workspacePath) return null;
const workspaceId = createId("wsp");
this.deps.db.prepare(`
insert into workspaces(
id, org_id, name, local_path, repo_url, repo_ref, project_id, is_primary, created_at, updated_at
)
values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(workspaceId, orgId, "Default Workspace", workspacePath, "", "", null, 1, now, now);
return workspaceId;
}
private createGoal(orgId: string, ceoAgentId: string, input: BootstrapOnboardingInput, now: string): string {
const goalTitle = input.goalTitle.trim();
if (!goalTitle) throw new Error("Goal title is required.");
const goalId = createId("gol");
this.deps.db.prepare(`
insert into goals(id, org_id, title, description, status, owner_agent_id, created_at, updated_at)
values (?, ?, ?, ?, ?, ?, ?, ?)
`).run(goalId, orgId, goalTitle, input.goalDescription.trim(), "active", ceoAgentId, now, now);
return goalId;
}
private createProject(
orgId: string,
goalId: string,
ceoAgentId: string,
input: BootstrapOnboardingInput,
now: string,
): string {
const projectId = createId("prj");
this.deps.db.prepare(`
insert into projects(id, org_id, goal_id, name, description, lead_agent_id, status, created_at, updated_at)
values (?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(
projectId,
orgId,
goalId,
`Execution plan: ${input.goalTitle.trim()}`,
input.companyDescription.trim(),
ceoAgentId,
"in_progress",
now,
now,
);
return projectId;
}
private bindWorkspaceToProject(workspaceId: string, projectId: string, now: string): void {
this.deps.db.prepare("update workspaces set project_id = ?, updated_at = ? where id = ?")
.run(projectId, now, workspaceId);
}
private createKickoffTask(
orgId: string,
goalId: string,
projectId: string,
workspaceId: string | null,
ceoAgentId: string,
input: BootstrapOnboardingInput,
now: string,
): string {
const taskId = createId("tsk");
this.deps.db.prepare(`
insert into tasks(
id, org_id, title, description, status, assignee_agent_id, risk,
project_id, goal_id, workspace_id, priority, requires_filesystem, created_at, updated_at
)
values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(
taskId,
orgId,
`Operationalize goal: ${input.goalTitle.trim()}`,
this.createKickoffTaskDescription(input),
TASK_STATUS.BACKLOG,
ceoAgentId,
RISK_LEVEL.MEDIUM,
projectId,
goalId,
workspaceId,
"high",
workspaceId ? 1 : 0,
now,
now,
);
return taskId;
}
private createTaskRun(orgId: string, taskId: string, ceoAgentId: string, providerId: string, workspaceId: string | null, now: string): string {
const runId = createId("run");
const executionContextId = createId("ctx");
const serviceIdentityId = this.requireServiceIdentity(orgId, ceoAgentId);
const workspaceMountJson = JSON.stringify(workspaceId ? [{
workspaceId,
mode: "read_write",
}] : []);
const secretScopeJson = JSON.stringify({
serviceIdentityId,
providerIds: [providerId],
scope: "bootstrap",
credentialAccess: "provider_runtime_only",
});
this.deps.db.prepare(`
insert into task_runs(
id, org_id, task_id, agent_id, provider_id, provider_request_id,
execution_context_id, service_identity_id, workspace_mount_json, secret_scope_json,
trigger, state, created_at, started_at, completed_at, error_message
)
values (?, ?, ?, ?, ?, null, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(
runId,
orgId,
taskId,
ceoAgentId,
providerId,
executionContextId,
serviceIdentityId,
workspaceMountJson,
secretScopeJson,
"bootstrap",
"queued",
now,
null,
null,
null,
);
return runId;
}
private requireServiceIdentity(orgId: string, agentId: string): string {
const row = this.deps.db.prepare("select id from service_identities where org_id = ? and agent_id = ?")
.get(orgId, agentId) as { id: string } | undefined;
if (!row) throw new Error("Agent service identity was not found.");
return row.id;
}
private createKickoffTaskDescription(input: BootstrapOnboardingInput): string {
return [
`Company: ${input.companyName.trim()}`,
`Goal: ${input.goalTitle.trim()}`,
input.goalDescription.trim(),
"Read the bound workspace, create the first operating plan, delegate work, and route decisions through approvals and messages.",
].filter((line) => line.length > 0).join("\n\n");
}
}