Expand data model with companies, approvals, costs, and heartbeats
Add new DB schemas: companies, agent_api_keys, approvals, cost_events, heartbeat_runs, issue_comments. Add corresponding shared types and validators. Update existing schemas (agents, goals, issues, projects) with new fields for company association, budgets, and richer metadata. Generate initial Drizzle migration. Update seed data. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,12 +1,22 @@
|
||||
import { pgTable, uuid, text, timestamp, jsonb } from "drizzle-orm/pg-core";
|
||||
import { pgTable, uuid, text, timestamp, jsonb, index } from "drizzle-orm/pg-core";
|
||||
import { companies } from "./companies.js";
|
||||
import { agents } from "./agents.js";
|
||||
|
||||
export const activityLog = pgTable("activity_log", {
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
action: text("action").notNull(),
|
||||
entityType: text("entity_type").notNull(),
|
||||
entityId: uuid("entity_id").notNull(),
|
||||
agentId: uuid("agent_id").references(() => agents.id),
|
||||
details: jsonb("details"),
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
});
|
||||
export const activityLog = pgTable(
|
||||
"activity_log",
|
||||
{
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
companyId: uuid("company_id").notNull().references(() => companies.id),
|
||||
actorType: text("actor_type").notNull().default("system"),
|
||||
actorId: text("actor_id").notNull(),
|
||||
action: text("action").notNull(),
|
||||
entityType: text("entity_type").notNull(),
|
||||
entityId: text("entity_id").notNull(),
|
||||
agentId: uuid("agent_id").references(() => agents.id),
|
||||
details: jsonb("details").$type<Record<string, unknown>>(),
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
},
|
||||
(table) => ({
|
||||
companyCreatedIdx: index("activity_log_company_created_idx").on(table.companyId, table.createdAt),
|
||||
}),
|
||||
);
|
||||
|
||||
21
packages/db/src/schema/agent_api_keys.ts
Normal file
21
packages/db/src/schema/agent_api_keys.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { pgTable, uuid, text, timestamp, index } from "drizzle-orm/pg-core";
|
||||
import { agents } from "./agents.js";
|
||||
import { companies } from "./companies.js";
|
||||
|
||||
export const agentApiKeys = pgTable(
|
||||
"agent_api_keys",
|
||||
{
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
agentId: uuid("agent_id").notNull().references(() => agents.id),
|
||||
companyId: uuid("company_id").notNull().references(() => companies.id),
|
||||
name: text("name").notNull(),
|
||||
keyHash: text("key_hash").notNull(),
|
||||
lastUsedAt: timestamp("last_used_at", { withTimezone: true }),
|
||||
revokedAt: timestamp("revoked_at", { withTimezone: true }),
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
},
|
||||
(table) => ({
|
||||
keyHashIdx: index("agent_api_keys_key_hash_idx").on(table.keyHash),
|
||||
companyAgentIdx: index("agent_api_keys_company_agent_idx").on(table.companyId, table.agentId),
|
||||
}),
|
||||
);
|
||||
@@ -1,15 +1,38 @@
|
||||
import { type AnyPgColumn, pgTable, uuid, text, integer, timestamp, jsonb } from "drizzle-orm/pg-core";
|
||||
import {
|
||||
type AnyPgColumn,
|
||||
pgTable,
|
||||
uuid,
|
||||
text,
|
||||
integer,
|
||||
timestamp,
|
||||
jsonb,
|
||||
index,
|
||||
} from "drizzle-orm/pg-core";
|
||||
import { companies } from "./companies.js";
|
||||
|
||||
export const agents = pgTable("agents", {
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
name: text("name").notNull(),
|
||||
role: text("role").notNull().default("general"),
|
||||
status: text("status").notNull().default("idle"),
|
||||
budgetCents: integer("budget_cents").notNull().default(0),
|
||||
spentCents: integer("spent_cents").notNull().default(0),
|
||||
lastHeartbeat: timestamp("last_heartbeat", { withTimezone: true }),
|
||||
reportsTo: uuid("reports_to").references((): AnyPgColumn => agents.id),
|
||||
metadata: jsonb("metadata"),
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
});
|
||||
export const agents = pgTable(
|
||||
"agents",
|
||||
{
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
companyId: uuid("company_id").notNull().references(() => companies.id),
|
||||
name: text("name").notNull(),
|
||||
role: text("role").notNull().default("general"),
|
||||
title: text("title"),
|
||||
status: text("status").notNull().default("idle"),
|
||||
reportsTo: uuid("reports_to").references((): AnyPgColumn => agents.id),
|
||||
capabilities: text("capabilities"),
|
||||
adapterType: text("adapter_type").notNull().default("process"),
|
||||
adapterConfig: jsonb("adapter_config").$type<Record<string, unknown>>().notNull().default({}),
|
||||
contextMode: text("context_mode").notNull().default("thin"),
|
||||
budgetMonthlyCents: integer("budget_monthly_cents").notNull().default(0),
|
||||
spentMonthlyCents: integer("spent_monthly_cents").notNull().default(0),
|
||||
lastHeartbeatAt: timestamp("last_heartbeat_at", { withTimezone: true }),
|
||||
metadata: jsonb("metadata").$type<Record<string, unknown>>(),
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
},
|
||||
(table) => ({
|
||||
companyStatusIdx: index("agents_company_status_idx").on(table.companyId, table.status),
|
||||
companyReportsToIdx: index("agents_company_reports_to_idx").on(table.companyId, table.reportsTo),
|
||||
}),
|
||||
);
|
||||
|
||||
28
packages/db/src/schema/approvals.ts
Normal file
28
packages/db/src/schema/approvals.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { pgTable, uuid, text, timestamp, jsonb, index } from "drizzle-orm/pg-core";
|
||||
import { companies } from "./companies.js";
|
||||
import { agents } from "./agents.js";
|
||||
|
||||
export const approvals = pgTable(
|
||||
"approvals",
|
||||
{
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
companyId: uuid("company_id").notNull().references(() => companies.id),
|
||||
type: text("type").notNull(),
|
||||
requestedByAgentId: uuid("requested_by_agent_id").references(() => agents.id),
|
||||
requestedByUserId: text("requested_by_user_id"),
|
||||
status: text("status").notNull().default("pending"),
|
||||
payload: jsonb("payload").$type<Record<string, unknown>>().notNull(),
|
||||
decisionNote: text("decision_note"),
|
||||
decidedByUserId: text("decided_by_user_id"),
|
||||
decidedAt: timestamp("decided_at", { withTimezone: true }),
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
},
|
||||
(table) => ({
|
||||
companyStatusTypeIdx: index("approvals_company_status_type_idx").on(
|
||||
table.companyId,
|
||||
table.status,
|
||||
table.type,
|
||||
),
|
||||
}),
|
||||
);
|
||||
12
packages/db/src/schema/companies.ts
Normal file
12
packages/db/src/schema/companies.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { pgTable, uuid, text, integer, timestamp } from "drizzle-orm/pg-core";
|
||||
|
||||
export const companies = pgTable("companies", {
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
name: text("name").notNull(),
|
||||
description: text("description"),
|
||||
status: text("status").notNull().default("active"),
|
||||
budgetMonthlyCents: integer("budget_monthly_cents").notNull().default(0),
|
||||
spentMonthlyCents: integer("spent_monthly_cents").notNull().default(0),
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
});
|
||||
34
packages/db/src/schema/cost_events.ts
Normal file
34
packages/db/src/schema/cost_events.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { pgTable, uuid, text, timestamp, integer, index } from "drizzle-orm/pg-core";
|
||||
import { companies } from "./companies.js";
|
||||
import { agents } from "./agents.js";
|
||||
import { issues } from "./issues.js";
|
||||
import { projects } from "./projects.js";
|
||||
import { goals } from "./goals.js";
|
||||
|
||||
export const costEvents = pgTable(
|
||||
"cost_events",
|
||||
{
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
companyId: uuid("company_id").notNull().references(() => companies.id),
|
||||
agentId: uuid("agent_id").notNull().references(() => agents.id),
|
||||
issueId: uuid("issue_id").references(() => issues.id),
|
||||
projectId: uuid("project_id").references(() => projects.id),
|
||||
goalId: uuid("goal_id").references(() => goals.id),
|
||||
billingCode: text("billing_code"),
|
||||
provider: text("provider").notNull(),
|
||||
model: text("model").notNull(),
|
||||
inputTokens: integer("input_tokens").notNull().default(0),
|
||||
outputTokens: integer("output_tokens").notNull().default(0),
|
||||
costCents: integer("cost_cents").notNull(),
|
||||
occurredAt: timestamp("occurred_at", { withTimezone: true }).notNull(),
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
},
|
||||
(table) => ({
|
||||
companyOccurredIdx: index("cost_events_company_occurred_idx").on(table.companyId, table.occurredAt),
|
||||
companyAgentOccurredIdx: index("cost_events_company_agent_occurred_idx").on(
|
||||
table.companyId,
|
||||
table.agentId,
|
||||
table.occurredAt,
|
||||
),
|
||||
}),
|
||||
);
|
||||
@@ -1,13 +1,29 @@
|
||||
import { type AnyPgColumn, pgTable, uuid, text, timestamp } from "drizzle-orm/pg-core";
|
||||
import {
|
||||
type AnyPgColumn,
|
||||
pgTable,
|
||||
uuid,
|
||||
text,
|
||||
timestamp,
|
||||
index,
|
||||
} from "drizzle-orm/pg-core";
|
||||
import { agents } from "./agents.js";
|
||||
import { companies } from "./companies.js";
|
||||
|
||||
export const goals = pgTable("goals", {
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
title: text("title").notNull(),
|
||||
description: text("description"),
|
||||
level: text("level").notNull().default("task"),
|
||||
parentId: uuid("parent_id").references((): AnyPgColumn => goals.id),
|
||||
ownerId: uuid("owner_id").references(() => agents.id),
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
});
|
||||
export const goals = pgTable(
|
||||
"goals",
|
||||
{
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
companyId: uuid("company_id").notNull().references(() => companies.id),
|
||||
title: text("title").notNull(),
|
||||
description: text("description"),
|
||||
level: text("level").notNull().default("task"),
|
||||
status: text("status").notNull().default("planned"),
|
||||
parentId: uuid("parent_id").references((): AnyPgColumn => goals.id),
|
||||
ownerAgentId: uuid("owner_agent_id").references(() => agents.id),
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
},
|
||||
(table) => ({
|
||||
companyIdx: index("goals_company_idx").on(table.companyId),
|
||||
}),
|
||||
);
|
||||
|
||||
28
packages/db/src/schema/heartbeat_runs.ts
Normal file
28
packages/db/src/schema/heartbeat_runs.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { pgTable, uuid, text, timestamp, jsonb, index } from "drizzle-orm/pg-core";
|
||||
import { companies } from "./companies.js";
|
||||
import { agents } from "./agents.js";
|
||||
|
||||
export const heartbeatRuns = pgTable(
|
||||
"heartbeat_runs",
|
||||
{
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
companyId: uuid("company_id").notNull().references(() => companies.id),
|
||||
agentId: uuid("agent_id").notNull().references(() => agents.id),
|
||||
invocationSource: text("invocation_source").notNull().default("manual"),
|
||||
status: text("status").notNull().default("queued"),
|
||||
startedAt: timestamp("started_at", { withTimezone: true }),
|
||||
finishedAt: timestamp("finished_at", { withTimezone: true }),
|
||||
error: text("error"),
|
||||
externalRunId: text("external_run_id"),
|
||||
contextSnapshot: jsonb("context_snapshot").$type<Record<string, unknown>>(),
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
},
|
||||
(table) => ({
|
||||
companyAgentStartedIdx: index("heartbeat_runs_company_agent_started_idx").on(
|
||||
table.companyId,
|
||||
table.agentId,
|
||||
table.startedAt,
|
||||
),
|
||||
}),
|
||||
);
|
||||
@@ -1,5 +1,11 @@
|
||||
export { companies } from "./companies.js";
|
||||
export { agents } from "./agents.js";
|
||||
export { agentApiKeys } from "./agent_api_keys.js";
|
||||
export { projects } from "./projects.js";
|
||||
export { goals } from "./goals.js";
|
||||
export { issues } from "./issues.js";
|
||||
export { issueComments } from "./issue_comments.js";
|
||||
export { heartbeatRuns } from "./heartbeat_runs.js";
|
||||
export { costEvents } from "./cost_events.js";
|
||||
export { approvals } from "./approvals.js";
|
||||
export { activityLog } from "./activity_log.js";
|
||||
|
||||
22
packages/db/src/schema/issue_comments.ts
Normal file
22
packages/db/src/schema/issue_comments.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { pgTable, uuid, text, timestamp, index } from "drizzle-orm/pg-core";
|
||||
import { companies } from "./companies.js";
|
||||
import { issues } from "./issues.js";
|
||||
import { agents } from "./agents.js";
|
||||
|
||||
export const issueComments = pgTable(
|
||||
"issue_comments",
|
||||
{
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
companyId: uuid("company_id").notNull().references(() => companies.id),
|
||||
issueId: uuid("issue_id").notNull().references(() => issues.id),
|
||||
authorAgentId: uuid("author_agent_id").references(() => agents.id),
|
||||
authorUserId: text("author_user_id"),
|
||||
body: text("body").notNull(),
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
},
|
||||
(table) => ({
|
||||
issueIdx: index("issue_comments_issue_idx").on(table.issueId),
|
||||
companyIdx: index("issue_comments_company_idx").on(table.companyId),
|
||||
}),
|
||||
);
|
||||
@@ -1,17 +1,48 @@
|
||||
import { pgTable, uuid, text, timestamp } from "drizzle-orm/pg-core";
|
||||
import {
|
||||
type AnyPgColumn,
|
||||
pgTable,
|
||||
uuid,
|
||||
text,
|
||||
timestamp,
|
||||
integer,
|
||||
index,
|
||||
} from "drizzle-orm/pg-core";
|
||||
import { agents } from "./agents.js";
|
||||
import { projects } from "./projects.js";
|
||||
import { goals } from "./goals.js";
|
||||
import { companies } from "./companies.js";
|
||||
|
||||
export const issues = pgTable("issues", {
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
title: text("title").notNull(),
|
||||
description: text("description"),
|
||||
status: text("status").notNull().default("backlog"),
|
||||
priority: text("priority").notNull().default("medium"),
|
||||
projectId: uuid("project_id").references(() => projects.id),
|
||||
assigneeId: uuid("assignee_id").references(() => agents.id),
|
||||
goalId: uuid("goal_id").references(() => goals.id),
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
});
|
||||
export const issues = pgTable(
|
||||
"issues",
|
||||
{
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
companyId: uuid("company_id").notNull().references(() => companies.id),
|
||||
projectId: uuid("project_id").references(() => projects.id),
|
||||
goalId: uuid("goal_id").references(() => goals.id),
|
||||
parentId: uuid("parent_id").references((): AnyPgColumn => issues.id),
|
||||
title: text("title").notNull(),
|
||||
description: text("description"),
|
||||
status: text("status").notNull().default("backlog"),
|
||||
priority: text("priority").notNull().default("medium"),
|
||||
assigneeAgentId: uuid("assignee_agent_id").references(() => agents.id),
|
||||
createdByAgentId: uuid("created_by_agent_id").references(() => agents.id),
|
||||
createdByUserId: text("created_by_user_id"),
|
||||
requestDepth: integer("request_depth").notNull().default(0),
|
||||
billingCode: text("billing_code"),
|
||||
startedAt: timestamp("started_at", { withTimezone: true }),
|
||||
completedAt: timestamp("completed_at", { withTimezone: true }),
|
||||
cancelledAt: timestamp("cancelled_at", { withTimezone: true }),
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
},
|
||||
(table) => ({
|
||||
companyStatusIdx: index("issues_company_status_idx").on(table.companyId, table.status),
|
||||
assigneeStatusIdx: index("issues_company_assignee_status_idx").on(
|
||||
table.companyId,
|
||||
table.assigneeAgentId,
|
||||
table.status,
|
||||
),
|
||||
parentIdx: index("issues_company_parent_idx").on(table.companyId, table.parentId),
|
||||
projectIdx: index("issues_company_project_idx").on(table.companyId, table.projectId),
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -1,9 +1,23 @@
|
||||
import { pgTable, uuid, text, timestamp } from "drizzle-orm/pg-core";
|
||||
import { pgTable, uuid, text, timestamp, date, index } from "drizzle-orm/pg-core";
|
||||
import { companies } from "./companies.js";
|
||||
import { goals } from "./goals.js";
|
||||
import { agents } from "./agents.js";
|
||||
|
||||
export const projects = pgTable("projects", {
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
name: text("name").notNull(),
|
||||
description: text("description"),
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
});
|
||||
export const projects = pgTable(
|
||||
"projects",
|
||||
{
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
companyId: uuid("company_id").notNull().references(() => companies.id),
|
||||
goalId: uuid("goal_id").references(() => goals.id),
|
||||
name: text("name").notNull(),
|
||||
description: text("description"),
|
||||
status: text("status").notNull().default("backlog"),
|
||||
leadAgentId: uuid("lead_agent_id").references(() => agents.id),
|
||||
targetDate: date("target_date"),
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
},
|
||||
(table) => ({
|
||||
companyIdx: index("projects_company_idx").on(table.companyId),
|
||||
}),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user