import { pgTable, uuid, varchar, text, integer, jsonb, timestamp, index, } from "drizzle-orm/pg-core"; import { workspaces } from "./workspaces"; import { users } from "./users"; import { markdownBacklogItems } from "./markdown_backlog"; /** * One row per agent session against a backlog item. The MCP tools * (`claim_task`, `complete_task`, …) will be the write path in the next * epic; this table is read-side + storage only at this stage. Rows can * be inserted manually for development. * * Conventions captured here so future maintainers don't drift: * * - `outcome` is constrained at the application layer to one of * {succeeded, failed, cancelled, stalled}. The column is nullable * because in-flight runs (no `finished_at`) carry NULL outcomes — * surfaced in the UI as the synthetic "open" bucket. * * - `actorUserId` is null for anonymous / dev sessions where no user * is attached. The settings UI shows "—" for null actors. * * - Token totals follow the Symphony model: a single absolute * `tokens_total` recorded at close-out, not incremental deltas. * `tokens_input` / `tokens_output` are optional breakdowns. * * - `metadata` is JSONB but should stay small (roughly < 1 KB). * Don't dump full transcripts here — that's what an external * object store is for. * * - Indexes are tuned for the two read paths the UI exercises: * `(workspace_id, started_at DESC)` for the recent-runs settings * view and `(backlog_item_id, started_at DESC)` for per-task * history on the task detail panel (deferred to a follow-up task * to avoid stepping on the parent's concurrent work). */ export const agentRuns = pgTable( "agent_runs", { id: uuid("id").primaryKey().defaultRandom(), workspaceId: uuid("workspace_id") .notNull() .references(() => workspaces.id, { onDelete: "cascade" }), backlogItemId: uuid("backlog_item_id") .notNull() .references(() => markdownBacklogItems.id, { onDelete: "cascade" }), actorUserId: uuid("actor_user_id").references(() => users.id, { onDelete: "set null", }), startedAt: timestamp("started_at", { withTimezone: true }) .defaultNow() .notNull(), finishedAt: timestamp("finished_at", { withTimezone: true }), outcome: varchar("outcome", { length: 20 }), error: text("error"), tokensInput: integer("tokens_input"), tokensOutput: integer("tokens_output"), tokensTotal: integer("tokens_total"), notes: text("notes"), metadata: jsonb("metadata").$type | null>(), }, (table) => ({ workspaceStartedIdx: index("agent_runs_workspace_id_started_at_idx").on( table.workspaceId, table.startedAt.desc(), ), backlogStartedIdx: index("agent_runs_backlog_item_id_started_at_idx").on( table.backlogItemId, table.startedAt.desc(), ), }), );