ubiquitous-invention/packages/database/src/schema/agent_runs.ts
Randall Stillwell f014686412 feat(runs): agent_runs table + tRPC router + settings UI
Read-side only — write paths land with claim_task / complete_task
in the next epic. Keyset pagination on started_at, three procedures
(listRecent, listForTask, summary), and a /settings/runs view that
mirrors the audit page's visual language.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-02 22:16:04 -05:00

81 lines
2.9 KiB
TypeScript

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<Record<string, unknown> | 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(),
),
}),
);