From 6992b3e70e9c84294ff43d96c6dec46085d6ad93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Thu, 21 May 2026 08:43:50 -0400 Subject: [PATCH] Implement original onboarding bootstrap --- AgentCompany-New/src/main/app/ipc-handlers.ts | 18 + AgentCompany-New/src/main/database/schema.ts | 82 ++++- .../src/main/services/onboarding-service.ts | 215 ++++++++++++ .../src/main/services/organization-service.ts | 41 ++- .../src/main/services/snapshot-service.ts | 21 +- AgentCompany-New/src/preload/index.ts | 1 + AgentCompany-New/src/renderer/src/app/App.tsx | 2 +- .../src/renderer/src/app/Onboarding.tsx | 323 ++++++++++++++++-- .../src/renderer/src/shared/ui.tsx | 4 +- AgentCompany-New/src/renderer/src/styles.css | 50 +++ AgentCompany-New/src/shared/commands.ts | 37 ++ AgentCompany-New/src/shared/desktop-api.ts | 3 + AgentCompany-New/src/shared/domain.ts | 69 ++++ AgentCompany-New/src/shared/ipc.ts | 1 + AgentCompany-New/tests/e2e/desktop.spec.ts | 14 +- .../tests/unit/domain-services.test.ts | 39 +++ 16 files changed, 873 insertions(+), 47 deletions(-) create mode 100644 AgentCompany-New/src/main/services/onboarding-service.ts diff --git a/AgentCompany-New/src/main/app/ipc-handlers.ts b/AgentCompany-New/src/main/app/ipc-handlers.ts index aa3b730b..7eda07ca 100644 --- a/AgentCompany-New/src/main/app/ipc-handlers.ts +++ b/AgentCompany-New/src/main/app/ipc-handlers.ts @@ -12,6 +12,7 @@ import { SkillService } from "../services/skill-service"; import { SocialService } from "../services/social-service"; import { FileService } from "../services/file-service"; import { MigrationService } from "../services/migration-service"; +import { OnboardingService } from "../services/onboarding-service"; export interface IpcDeps { connection: DatabaseConnection; @@ -29,6 +30,10 @@ export function registerIpcHandlers(deps: IpcDeps): void { const social = new SocialService(deps.connection.db); const files = new FileService(deps.connection.db, deps.artifactRoot); const migrations = new MigrationService(deps.connection.db); + const onboarding = new OnboardingService({ + db: deps.connection.db, + saveProviderCredential: (input) => providers.saveCredential(input), + }); const mutate = (operation: () => T): DesktopResult => { try { @@ -48,6 +53,19 @@ export function registerIpcHandlers(deps: IpcDeps): void { } }); + ipcMain.handle(IPC_CHANNELS.bootstrapOnboarding, (_event, input) => { + try { + let summary; + deps.connection.transaction(() => { + summary = onboarding.bootstrap(input); + }); + deps.mainWindow.webContents.send(IPC_CHANNELS.rendererEvent, { type: "snapshot-changed" }); + return ok(summary); + } catch (error) { + return fail("onboarding_failed", error instanceof Error ? error.message : String(error)); + } + }); + ipcMain.handle(IPC_CHANNELS.createOrganization, (_event, input) => mutate(() => organization.createOrganization(input))); ipcMain.handle(IPC_CHANNELS.createAgent, (_event, input) => mutate(() => organization.createAgent(input))); ipcMain.handle(IPC_CHANNELS.createTask, (_event, input) => mutate(() => organization.createTask(input))); diff --git a/AgentCompany-New/src/main/database/schema.ts b/AgentCompany-New/src/main/database/schema.ts index 876a747b..76c8bb96 100644 --- a/AgentCompany-New/src/main/database/schema.ts +++ b/AgentCompany-New/src/main/database/schema.ts @@ -1,6 +1,6 @@ import type { DatabaseSync } from "node:sqlite"; -const SCHEMA_VERSION = 2; +const SCHEMA_VERSION = 3; export function installSchema(db: DatabaseSync): void { db.exec(` @@ -375,5 +375,85 @@ export function installSchema(db: DatabaseSync): void { insert into schema_migrations(version, name, applied_at) values (2, 'collaboration_provider_policy_schema', datetime('now')); `); + currentVersion = 2; + } + + if (currentVersion < 3) { + db.exec(` + alter table organizations add column description text not null default ''; + alter table organizations add column execution_mode text not null default 'ai_ceo'; + alter table organizations add column auto_approve_hires integer not null default 1; + alter table organizations add column review_deliverables integer not null default 0; + + alter table agents add column title text not null default ''; + alter table agents add column role_level text not null default 'staff'; + alter table agents add column model_strategy text not null default 'pooled'; + alter table agents add column workspace_id text; + alter table agents add column heartbeat_enabled integer not null default 0; + alter table agents add column heartbeat_interval_sec integer not null default 0; + + alter table tasks add column project_id text; + alter table tasks add column goal_id text; + alter table tasks add column workspace_id text; + alter table tasks add column priority text not null default 'normal'; + alter table tasks add column requires_filesystem integer not null default 0; + + create table if not exists workspaces ( + id text primary key, + org_id text not null references organizations(id) on delete cascade, + name text not null, + local_path text not null, + repo_url text not null, + repo_ref text not null, + project_id text, + is_primary integer not null, + created_at text not null, + updated_at text not null + ); + + create table if not exists goals ( + id text primary key, + org_id text not null references organizations(id) on delete cascade, + title text not null, + description text not null, + status text not null, + owner_agent_id text references agents(id) on delete set null, + created_at text not null, + updated_at text not null + ); + + create table if not exists projects ( + id text primary key, + org_id text not null references organizations(id) on delete cascade, + goal_id text references goals(id) on delete set null, + name text not null, + description text not null, + lead_agent_id text references agents(id) on delete set null, + status text not null, + created_at text not null, + updated_at text not null + ); + + create table if not exists task_runs ( + id text primary key, + org_id text not null references organizations(id) on delete cascade, + task_id text not null references tasks(id) on delete cascade, + agent_id text references agents(id) on delete set null, + provider_id text references providers(id) on delete set null, + trigger text not null, + state text not null, + created_at text not null, + started_at text, + completed_at text, + error_message text + ); + + create index if not exists idx_workspaces_org_primary on workspaces(org_id, is_primary); + create index if not exists idx_goals_org_status on goals(org_id, status); + create index if not exists idx_projects_org_status on projects(org_id, status); + create index if not exists idx_task_runs_task_created on task_runs(task_id, created_at); + insert into schema_migrations(version, name, applied_at) + values (3, 'onboarding_execution_schema', datetime('now')); + `); } } diff --git a/AgentCompany-New/src/main/services/onboarding-service.ts b/AgentCompany-New/src/main/services/onboarding-service.ts new file mode 100644 index 00000000..88d66519 --- /dev/null +++ b/AgentCompany-New/src/main/services/onboarding-service.ts @@ -0,0 +1,215 @@ +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, 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.API_KEY) { + throw new Error("Onboarding requires an API key provider."); + } + 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, now: string): string { + const runId = createId("run"); + this.deps.db.prepare(` + insert into task_runs( + id, org_id, task_id, agent_id, provider_id, trigger, state, created_at, started_at, completed_at, error_message + ) + values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + `).run(runId, orgId, taskId, ceoAgentId, providerId, "bootstrap", "queued", now, null, null, null); + return runId; + } + + 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"); + } +} diff --git a/AgentCompany-New/src/main/services/organization-service.ts b/AgentCompany-New/src/main/services/organization-service.ts index 6efd9397..0be9dc3c 100644 --- a/AgentCompany-New/src/main/services/organization-service.ts +++ b/AgentCompany-New/src/main/services/organization-service.ts @@ -20,8 +20,22 @@ export class OrganizationService { const employeeId = createId("emp"); const now = new Date().toISOString(); - this.db.prepare("insert into organizations(id, name, mission, goal, created_at) values (?, ?, ?, ?, ?)") - .run(orgId, input.organizationName.trim(), input.mission.trim(), input.goal.trim(), now); + this.db.prepare(` + insert into organizations( + id, name, mission, goal, description, execution_mode, auto_approve_hires, review_deliverables, created_at + ) + values (?, ?, ?, ?, ?, ?, ?, ?, ?) + `).run( + orgId, + input.organizationName.trim(), + input.mission.trim(), + input.goal.trim(), + input.description?.trim() ?? "", + input.executionMode ?? "ai_ceo", + input.autoApproveHires === false ? 0 : 1, + input.reviewDeliverables === true ? 1 : 0, + now, + ); this.db.prepare("insert into employees(id, org_id, display_name, role, created_at) values (?, ?, ?, ?, ?)") .run(employeeId, orgId, input.employeeName.trim(), "Board Chair", now); @@ -78,8 +92,9 @@ export class OrganizationService { this.db.prepare(` insert into agents( id, org_id, name, role, department, model_provider_id, model_id, - budget_cents, manager_agent_id, lifecycle, created_at - ) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + budget_cents, manager_agent_id, title, role_level, model_strategy, + workspace_id, heartbeat_enabled, heartbeat_interval_sec, lifecycle, created_at + ) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `).run( id, input.orgId, @@ -90,6 +105,12 @@ export class OrganizationService { input.modelId, input.budgetCents, input.managerAgentId, + input.title?.trim() || input.role.trim(), + input.roleLevel ?? "staff", + input.modelStrategy ?? "pooled", + input.workspaceId ?? null, + input.heartbeatEnabled === true ? 1 : 0, + input.heartbeatIntervalSec ?? 0, "active", now, ); @@ -109,8 +130,11 @@ export class OrganizationService { const id = createId("tsk"); const now = new Date().toISOString(); this.db.prepare(` - insert into tasks(id, org_id, title, description, status, assignee_agent_id, risk, created_at, updated_at) - values (?, ?, ?, ?, ?, ?, ?, ?, ?) + 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( id, input.orgId, @@ -119,6 +143,11 @@ export class OrganizationService { input.status, input.assigneeAgentId, input.risk, + null, + null, + null, + "normal", + 0, now, now, ); diff --git a/AgentCompany-New/src/main/services/snapshot-service.ts b/AgentCompany-New/src/main/services/snapshot-service.ts index 5efbfb66..1d892b6f 100644 --- a/AgentCompany-New/src/main/services/snapshot-service.ts +++ b/AgentCompany-New/src/main/services/snapshot-service.ts @@ -28,8 +28,19 @@ export class SnapshotService { constructor(private readonly db: DatabaseSync) {} load(): AppSnapshot { - const organizations = list(this.db, "organizations", "created_at asc"); + const organizations = list(this.db, "organizations", "created_at asc") + .map((org) => ({ + ...org, + autoApproveHires: toBool(org.autoApproveHires), + reviewDeliverables: toBool(org.reviewDeliverables), + })); const activeOrgId = organizations[0]?.id ?? null; + const agents = list(this.db, "agents", "created_at asc") + .map((agent) => ({ ...agent, heartbeatEnabled: toBool(agent.heartbeatEnabled) })); + const tasks = list(this.db, "tasks", "created_at asc") + .map((task) => ({ ...task, requiresFilesystem: toBool(task.requiresFilesystem) })); + const workspaces = list(this.db, "workspaces", "created_at asc") + .map((workspace) => ({ ...workspace, isPrimary: toBool(workspace.isPrimary) })); const providers = list(this.db, "providers", "label asc") .map((provider) => ({ ...provider, enabled: toBool(provider.enabled) })); const skillBindings = list(this.db, "skill_bindings", "priority asc") @@ -39,8 +50,12 @@ export class SnapshotService { activeOrgId, organizations, employees: list(this.db, "employees", "created_at asc"), - agents: list(this.db, "agents", "created_at asc"), - tasks: list(this.db, "tasks", "created_at asc"), + agents, + tasks, + workspaces, + goals: list(this.db, "goals", "created_at asc"), + projects: list(this.db, "projects", "created_at asc"), + taskRuns: list(this.db, "task_runs", "created_at asc"), approvals: list(this.db, "approvals", "created_at desc"), chatSpaces: list(this.db, "chat_spaces", "created_at asc"), chatMessages: list(this.db, "chat_messages", "created_at asc"), diff --git a/AgentCompany-New/src/preload/index.ts b/AgentCompany-New/src/preload/index.ts index 4deb1e12..c67eadf1 100644 --- a/AgentCompany-New/src/preload/index.ts +++ b/AgentCompany-New/src/preload/index.ts @@ -8,6 +8,7 @@ function invoke(channel: string, payload?: unknown): Promise { const api: DesktopApi = { loadSnapshot: () => invoke(IPC_CHANNELS.loadSnapshot), + bootstrapOnboarding: (input) => invoke(IPC_CHANNELS.bootstrapOnboarding, input), createOrganization: (input) => invoke(IPC_CHANNELS.createOrganization, input), createAgent: (input) => invoke(IPC_CHANNELS.createAgent, input), createTask: (input) => invoke(IPC_CHANNELS.createTask, input), diff --git a/AgentCompany-New/src/renderer/src/app/App.tsx b/AgentCompany-New/src/renderer/src/app/App.tsx index 27ac006c..08dbe9ea 100644 --- a/AgentCompany-New/src/renderer/src/app/App.tsx +++ b/AgentCompany-New/src/renderer/src/app/App.tsx @@ -19,7 +19,7 @@ export function App() { return
; } if (snapshot.organizations.length === 0) { - return ; + return ; } const pageProps = { snapshot, actions }; diff --git a/AgentCompany-New/src/renderer/src/app/Onboarding.tsx b/AgentCompany-New/src/renderer/src/app/Onboarding.tsx index cde0d01b..7829bb3e 100644 --- a/AgentCompany-New/src/renderer/src/app/Onboarding.tsx +++ b/AgentCompany-New/src/renderer/src/app/Onboarding.tsx @@ -1,46 +1,307 @@ -import type { FormEvent } from "react"; -import { Button, Field } from "../shared/ui"; +import { useMemo, useState, type FormEvent } from "react"; +import { PROVIDER_AUTH_TYPE } from "../../../shared/domain"; +import { Button, Field, Icon, Pill } from "../shared/ui"; import type { SnapshotActions } from "./useSnapshot"; +import type { AppSnapshot, ProviderCatalogEntry } from "../../../shared/domain"; -function value(form: FormData, key: string): string { - return String(form.get(key) ?? "").trim(); +type Step = "welcome" | "provider" | "company" | "mode" | "mission" | "launch"; + +interface Draft { + companyName: string; + companyDescription: string; + employeeName: string; + workspacePath: string; + executionMode: "ai_ceo" | "ai_cofounder"; + providerKey: string; + providerSecret: string; + providerModel: string; + goalTitle: string; + goalDescription: string; } -export function Onboarding({ actions }: { actions: SnapshotActions }) { - const submit = (event: FormEvent) => { +const steps: Array<{ id: Step; label: string }> = [ + { id: "welcome", label: "Start" }, + { id: "provider", label: "Provider" }, + { id: "company", label: "Company" }, + { id: "mode", label: "Mode" }, + { id: "mission", label: "Goal" }, + { id: "launch", label: "Launch" }, +]; + +const initialDraft: Draft = { + companyName: "", + companyDescription: "", + employeeName: "", + workspacePath: "", + executionMode: "ai_ceo", + providerKey: "", + providerSecret: "", + providerModel: "", + goalTitle: "", + goalDescription: "", +}; + +export function Onboarding({ snapshot, actions }: { snapshot: AppSnapshot; actions: SnapshotActions }) { + const apiProviders = useMemo( + () => snapshot.providerCatalog.filter((entry) => entry.authType === PROVIDER_AUTH_TYPE.API_KEY), + [snapshot.providerCatalog], + ); + const [step, setStep] = useState("welcome"); + const [draft, setDraft] = useState(() => { + const firstProvider = apiProviders[0]; + if (!firstProvider?.defaultModel) throw new Error("Provider catalog requires a default API model."); + return { ...initialDraft, providerKey: firstProvider.key, providerModel: firstProvider.defaultModel }; + }); + const [error, setError] = useState(null); + const [submitting, setSubmitting] = useState(false); + const selectedProvider = requireProvider(apiProviders, draft.providerKey); + + const update = (patch: Partial) => setDraft((current) => ({ ...current, ...patch })); + const go = (next: Step) => { + setError(null); + setStep(next); + }; + + const launch = async (event: FormEvent) => { event.preventDefault(); - const form = new FormData(event.currentTarget); - void actions.run(() => window.agentCompany.createOrganization({ - organizationName: value(form, "organizationName"), - mission: value(form, "mission"), - goal: value(form, "goal"), - employeeName: value(form, "employeeName"), - })); + const validation = validateDraft(draft); + if (validation) { + setError(validation); + return; + } + setSubmitting(true); + setError(null); + const result = await window.agentCompany.bootstrapOnboarding({ + companyName: draft.companyName.trim(), + companyDescription: draft.companyDescription.trim(), + employeeName: draft.employeeName.trim(), + workspacePath: draft.workspacePath.trim(), + executionMode: draft.executionMode, + autoApproveHires: draft.executionMode === "ai_ceo", + reviewDeliverables: draft.executionMode === "ai_cofounder", + goalTitle: draft.goalTitle.trim(), + goalDescription: draft.goalDescription.trim(), + providerKey: draft.providerKey, + providerSecret: draft.providerSecret.trim(), + providerModel: draft.providerModel.trim(), + }); + setSubmitting(false); + if (!result.ok) { + setError(result.error.message); + return; + } + await actions.reload(); }; return ( -
-
+
+
-

AgentCompany

-

Create the company control plane.

+
+
A
+
AgentCompany
+
+

Start your AI company.

+

+ Create the company, bind the CEO runtime, and queue the first operating loop. +

+
+ {steps.map((item) => ( + {item.label} + ))} +
- - - - - - - -