paperclip/packages/db/src/schema/decision_queues.ts

257 lines
11 KiB
TypeScript

import { sql } from "drizzle-orm";
import {
boolean,
check,
date,
foreignKey,
index,
integer,
jsonb,
pgTable,
text,
timestamp,
unique,
uniqueIndex,
uuid,
} from "drizzle-orm/pg-core";
import type { DecisionQueueSeedRule } from "@paperclipai/shared";
import { agentApiKeys } from "./agent_api_keys.js";
import { agents } from "./agents.js";
import { companies } from "./companies.js";
import { heartbeatRuns } from "./heartbeat_runs.js";
export const decisionQueues = pgTable(
"decision_queues",
{
id: uuid("id").primaryKey().defaultRandom(),
companyId: uuid("company_id").notNull().references(() => companies.id, { onDelete: "cascade" }),
key: text("key").notNull(),
title: text("title").notNull(),
description: text("description"),
createdByType: text("created_by_type").notNull(),
createdByAgentId: uuid("created_by_agent_id").references(() => agents.id),
createdByUserId: text("created_by_user_id"),
createdByRunId: uuid("created_by_run_id").references(() => heartbeatRuns.id),
createdByAgentApiKeyId: uuid("created_by_agent_api_key_id").references(() => agentApiKeys.id),
retentionDays: integer("retention_days"),
seedRules: jsonb("seed_rules").$type<DecisionQueueSeedRule[]>().notNull().default([]),
seedRulesEnabled: boolean("seed_rules_enabled").notNull().default(false),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
},
(table) => ({
companyKeyUq: uniqueIndex("decision_queues_company_key_uq").on(table.companyId, table.key),
companyUpdatedIdx: index("decision_queues_company_updated_idx").on(table.companyId, table.updatedAt),
idCompanyUq: unique("decision_queues_id_company_uq").on(table.id, table.companyId),
creatorCheck: check(
"decision_queues_creator_check",
sql`(
(${table.createdByType} = 'agent' AND ${table.createdByAgentId} IS NOT NULL AND ${table.createdByUserId} IS NULL)
OR (${table.createdByType} = 'user' AND ${table.createdByAgentId} IS NULL AND ${table.createdByUserId} IS NOT NULL)
OR (${table.createdByType} = 'system' AND ${table.createdByAgentId} IS NULL AND ${table.createdByUserId} IS NULL)
)`,
),
retentionDaysCheck: check(
"decision_queues_retention_days_check",
sql`${table.retentionDays} IS NULL OR (${table.retentionDays} >= 1 AND ${table.retentionDays} <= 3650)`,
),
}),
);
export const decisionQueueItems = pgTable(
"decision_queue_items",
{
id: uuid("id").primaryKey().defaultRandom(),
companyId: uuid("company_id").notNull().references(() => companies.id, { onDelete: "cascade" }),
queueId: uuid("queue_id").notNull(),
sourceKind: text("source_kind").notNull(),
sourceId: text("source_id").notNull(),
addedByType: text("added_by_type").notNull(),
addedByAgentId: uuid("added_by_agent_id").references(() => agents.id),
addedByUserId: text("added_by_user_id"),
addedByRunId: uuid("added_by_run_id").references(() => heartbeatRuns.id),
addedByAgentApiKeyId: uuid("added_by_agent_api_key_id").references(() => agentApiKeys.id),
responsibleUserId: text("responsible_user_id"),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
},
(table) => ({
queueSourceUq: uniqueIndex("decision_queue_items_queue_source_uq").on(
table.queueId,
table.sourceKind,
table.sourceId,
),
companySourceIdx: index("decision_queue_items_company_source_idx").on(
table.companyId,
table.sourceKind,
table.sourceId,
),
queueCompanyFk: foreignKey({
columns: [table.queueId, table.companyId],
foreignColumns: [decisionQueues.id, decisionQueues.companyId],
name: "decision_queue_items_queue_company_fk",
}).onDelete("cascade"),
actorCheck: check(
"decision_queue_items_actor_check",
sql`(
(${table.addedByType} = 'agent' AND ${table.addedByAgentId} IS NOT NULL AND ${table.addedByUserId} IS NULL)
OR (${table.addedByType} = 'user' AND ${table.addedByAgentId} IS NULL AND ${table.addedByUserId} IS NOT NULL)
OR (${table.addedByType} = 'system' AND ${table.addedByAgentId} IS NULL AND ${table.addedByUserId} IS NULL)
)`,
),
}),
);
export const decisionTriage = pgTable(
"decision_triage",
{
id: uuid("id").primaryKey().defaultRandom(),
companyId: uuid("company_id").notNull().references(() => companies.id, { onDelete: "cascade" }),
sourceKind: text("source_kind").notNull(),
sourceId: text("source_id").notNull(),
decideBy: text("decide_by"),
decideByDate: date("decide_by_date"),
snoozedUntil: timestamp("snoozed_until", { withTimezone: true }),
setByType: text("set_by_type").notNull(),
setByAgentId: uuid("set_by_agent_id").references(() => agents.id),
setByUserId: text("set_by_user_id"),
setByRunId: uuid("set_by_run_id").references(() => heartbeatRuns.id),
setByAgentApiKeyId: uuid("set_by_agent_api_key_id").references(() => agentApiKeys.id),
responsibleUserId: text("responsible_user_id"),
version: integer("version").notNull().default(1),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
},
(table) => ({
companySourceUq: uniqueIndex("decision_triage_company_source_uq").on(
table.companyId,
table.sourceKind,
table.sourceId,
),
companyDecideByIdx: index("decision_triage_company_decide_by_idx").on(table.companyId, table.decideBy),
actorCheck: check(
"decision_triage_actor_check",
sql`(
(${table.setByType} = 'agent' AND ${table.setByAgentId} IS NOT NULL AND ${table.setByUserId} IS NULL)
OR (${table.setByType} = 'user' AND ${table.setByAgentId} IS NULL AND ${table.setByUserId} IS NOT NULL)
)`,
),
decideByCheck: check(
"decision_triage_decide_by_check",
sql`(
(${table.decideBy} IS NULL AND ${table.decideByDate} IS NULL)
OR (${table.decideBy} IN ('today', 'this_week', 'whenever') AND ${table.decideByDate} IS NULL)
OR (${table.decideBy} = 'date' AND ${table.decideByDate} IS NOT NULL)
)`,
),
}),
);
export const decisionTriageEvents = pgTable(
"decision_triage_events",
{
id: uuid("id").primaryKey().defaultRandom(),
companyId: uuid("company_id").notNull().references(() => companies.id, { onDelete: "cascade" }),
queueId: uuid("queue_id").references(() => decisionQueues.id),
sourceKind: text("source_kind"),
sourceId: text("source_id"),
action: text("action").notNull(),
actorType: text("actor_type").notNull(),
actorAgentId: uuid("actor_agent_id").references(() => agents.id),
actorUserId: text("actor_user_id"),
actorRunId: uuid("actor_run_id").references(() => heartbeatRuns.id),
agentApiKeyId: uuid("agent_api_key_id").references(() => agentApiKeys.id),
responsibleUserId: text("responsible_user_id"),
details: jsonb("details").$type<Record<string, unknown>>().notNull().default({}),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
},
(table) => ({
companySourceCreatedIdx: index("decision_triage_events_company_source_created_idx").on(
table.companyId,
table.sourceKind,
table.sourceId,
table.createdAt,
),
queueCreatedIdx: index("decision_triage_events_queue_created_idx").on(table.queueId, table.createdAt),
actorCheck: check(
"decision_triage_events_actor_check",
sql`(
(${table.actorType} = 'agent' AND ${table.actorAgentId} IS NOT NULL AND ${table.actorUserId} IS NULL)
OR (${table.actorType} = 'user' AND ${table.actorAgentId} IS NULL AND ${table.actorUserId} IS NOT NULL)
OR (${table.actorType} = 'system' AND ${table.actorAgentId} IS NULL AND ${table.actorUserId} IS NULL)
)`,
),
}),
);
export const decisionRetention = pgTable(
"decision_retention",
{
id: uuid("id").primaryKey().defaultRandom(),
companyId: uuid("company_id").notNull().references(() => companies.id, { onDelete: "cascade" }),
sourceKind: text("source_kind").notNull(),
sourceId: text("source_id").notNull(),
sourceActivityAt: timestamp("source_activity_at", { withTimezone: true }).notNull(),
keep: boolean("keep").notNull().default(false),
archivedAt: timestamp("archived_at", { withTimezone: true }),
archivedReason: text("archived_reason"),
archivedByType: text("archived_by_type"),
archivedByAgentId: uuid("archived_by_agent_id").references(() => agents.id),
archivedByUserId: text("archived_by_user_id"),
archivedByRunId: uuid("archived_by_run_id").references(() => heartbeatRuns.id),
version: integer("version").notNull().default(1),
archiveVersion: integer("archive_version").notNull().default(0),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
},
(table) => ({
companySourceUq: uniqueIndex("decision_retention_company_source_uq").on(
table.companyId,
table.sourceKind,
table.sourceId,
),
companyArchivedIdx: index("decision_retention_company_archived_idx").on(table.companyId, table.archivedAt),
archiveActorCheck: check(
"decision_retention_archive_actor_check",
sql`(
(${table.archivedAt} IS NULL AND ${table.archivedByType} IS NULL AND ${table.archivedByAgentId} IS NULL AND ${table.archivedByUserId} IS NULL)
OR (${table.archivedAt} IS NOT NULL AND ${table.archivedByType} = 'system' AND ${table.archivedByAgentId} IS NULL AND ${table.archivedByUserId} IS NULL)
OR (${table.archivedAt} IS NOT NULL AND ${table.archivedByType} = 'agent' AND ${table.archivedByAgentId} IS NOT NULL AND ${table.archivedByUserId} IS NULL)
OR (${table.archivedAt} IS NOT NULL AND ${table.archivedByType} = 'user' AND ${table.archivedByAgentId} IS NULL AND ${table.archivedByUserId} IS NOT NULL)
)`,
),
}),
);
export const decisionArchiveNotificationOutbox = pgTable(
"decision_archive_notification_outbox",
{
id: uuid("id").primaryKey().defaultRandom(),
companyId: uuid("company_id").notNull().references(() => companies.id, { onDelete: "cascade" }),
sourceKind: text("source_kind").notNull(),
sourceId: text("source_id").notNull(),
archiveVersion: integer("archive_version").notNull(),
originAgentId: uuid("origin_agent_id").notNull().references(() => agents.id),
originIssueId: uuid("origin_issue_id").notNull(),
status: text("status").notNull().default("pending"),
attemptCount: integer("attempt_count").notNull().default(0),
lastAttemptAt: timestamp("last_attempt_at", { withTimezone: true }),
deliveredAt: timestamp("delivered_at", { withTimezone: true }),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
},
(table) => ({
notificationUq: uniqueIndex("decision_archive_notification_outbox_uq").on(
table.companyId,
table.sourceKind,
table.sourceId,
table.archiveVersion,
table.originAgentId,
),
pendingIdx: index("decision_archive_notification_outbox_pending_idx").on(table.status, table.createdAt),
statusCheck: check(
"decision_archive_notification_outbox_status_check",
sql`${table.status} IN ('pending', 'delivering', 'delivered')`,
),
}),
);