import { BigIntType, OptionalProps } from '@mikro-orm/core' import { Entity, Index, PrimaryKey, Property, Unique } from '@mikro-orm/decorators/legacy' import type { AgentType, ProcessMilestone, ProcessMilestoneReached, ProcessRunTriggeredBy, ProcessTrigger, } from './validators' export type AgentRunStatus = 'running' | 'ok' | 'error' | 'cancelled' /** Distinguishes production traffic from eval replays across runs and proposals. */ export type AgentRunSource = 'runtime' | 'eval' /** Span kinds; OTel GenAI semantic conventions are the naming target for span attributes. */ export type AgentSpanKind = 'llm' | 'tool' | 'system' export type AgentSpanStatus = 'ok' | 'error' export type AgentToolCallStatus = 'ok' | 'error' @Entity({ tableName: 'agent_runs' }) @Index({ name: 'agent_runs_tenant_org_idx', properties: ['tenantId', 'organizationId'] }) @Index({ name: 'agent_runs_agent_idx', properties: ['organizationId', 'agentId'] }) @Unique({ name: 'agent_runs_runtime_external_uq', properties: ['runtime', 'externalRunId'] }) @Index({ name: 'agent_runs_agent_def_idx', properties: ['agentId', 'createdAt'] }) @Index({ name: 'agent_runs_org_status_created_idx', properties: ['organizationId', 'status', 'createdAt'] }) @Index({ name: 'agent_runs_eval_failed_idx', expression: `create index "agent_runs_eval_failed_idx" on "agent_runs" ("organization_id", "created_at") where "eval_passed" = false`, }) // One run per agent invocation, stated as an identity rather than guessed from // creation time. Partial so Playground/eval runs — which correlate to nothing — // are unconstrained. @Index({ name: 'agent_runs_invocation_uq', expression: `create unique index "agent_runs_invocation_uq" on "agent_runs" ("workflow_instance_id", "step_id", "invocation_id") where "workflow_instance_id" is not null and "step_id" is not null and "invocation_id" is not null`, }) export class AgentRun { [OptionalProps]?: | 'source' | 'status' | 'output' | 'resultKind' | 'agentType' | 'errorMessage' | 'parentRunId' | 'workflowInstanceId' | 'stepId' | 'invocationId' | 'proposalId' | 'agentVersion' | 'model' | 'runtime' | 'externalRunId' | 'confidence' | 'inputTokens' | 'outputTokens' | 'costMinor' | 'currency' | 'latencyMs' | 'evalScore' | 'evalPassed' | 'goldenCaseId' | 'goldenPassed' | 'contextRouting' | 'outputArtifactKey' | 'humanConfirmedAt' | 'flaggedAt' | 'flaggedBy' | 'rerunOfRunId' | 'completedAt' | 'createdAt' | 'updatedAt' | 'deletedAt' @PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' }) id!: string @Property({ name: 'tenant_id', type: 'uuid' }) tenantId!: string @Property({ name: 'organization_id', type: 'uuid' }) organizationId!: string @Property({ name: 'agent_id', type: 'varchar', length: 100 }) agentId!: string /** * Where this run came from. `eval` marks a replay: a real run with real spans * and real cost, but one that must NOT skew the agent's production metrics. * Defaults to `runtime`, so every pre-existing row keeps its meaning. */ @Property({ name: 'source', type: 'varchar', length: 20, default: 'runtime' }) source: AgentRunSource = 'runtime' /** * Parent run that delegated to this one as a sub-agent (Phase 4 nested trace). * Nullable + additive: top-level runs leave it null. Populated for the * in-process `delegate_agent` path; OpenCode-NATIVE `task` delegation runs * sub-agents inside OpenCode (not via our runner), so per-sub-agent rows are a * documented follow-up for that path. */ @Property({ name: 'parent_run_id', type: 'uuid', nullable: true }) parentRunId?: string | null // ── Execution correlation ────────────────────────────────────────────────── /** * WHICH agent invocation this run is, stated explicitly. * * `(workflowInstanceId, stepId, invocationId)` is the identity of one agent * invocation and the unique key below enforces it. Before this triple existed, * a caller found the run it had just caused with "the newest run for this agent * created since T" — a temporal lookup that cannot tell two concurrent runs of * the same agent apart. Correlation is always an explicit identifier. * * All three are nullable together: a Playground or eval run belongs to no * workflow and correlates to nothing. */ @Property({ name: 'workflow_instance_id', type: 'uuid', nullable: true }) workflowInstanceId?: string | null @Property({ name: 'step_id', type: 'varchar', length: 100, nullable: true }) stepId?: string | null /** The step's attempt id — distinguishes a retry from the run it retried. */ @Property({ name: 'invocation_id', type: 'varchar', length: 100, nullable: true }) invocationId?: string | null /** FK id → agent_proposals (orchestration). */ @Property({ name: 'proposal_id', type: 'uuid', nullable: true }) proposalId?: string | null @Property({ name: 'agent_version', type: 'varchar', length: 50, nullable: true }) agentVersion?: string | null @Property({ name: 'model', type: 'varchar', length: 100, nullable: true }) model?: string | null /** Runtime that produced the run; part of the ingestion idempotency key. */ @Property({ name: 'runtime', type: 'varchar', length: 50, nullable: true }) runtime?: string | null /** Runtime-native run id; part of the ingestion idempotency key. */ @Property({ name: 'external_run_id', type: 'varchar', length: 200, nullable: true }) externalRunId?: string | null @Property({ name: 'confidence', type: 'float', nullable: true }) confidence?: number | null @Property({ name: 'input_tokens', type: 'integer', nullable: true }) inputTokens?: number | null @Property({ name: 'output_tokens', type: 'integer', nullable: true }) outputTokens?: number | null /** Backed by a `bigint` column; hydrate as `number` (safe under Number.MAX_SAFE_INTEGER for minor-unit costs) instead of MikroORM's default JS `bigint`. */ @Property({ name: 'cost_minor', type: new BigIntType('number'), nullable: true }) costMinor?: number | null @Property({ name: 'currency', type: 'varchar', length: 3, nullable: true }) currency?: string | null @Property({ name: 'latency_ms', type: 'integer', nullable: true }) latencyMs?: number | null @Property({ name: 'eval_score', type: 'float', nullable: true }) evalScore?: number | null @Property({ name: 'eval_passed', type: 'boolean', nullable: true }) evalPassed?: boolean | null /** FK id → agent_eval_cases; the approved golden case whose input this run matched (online golden-match plane); null when no golden matched. */ @Property({ name: 'golden_case_id', type: 'uuid', nullable: true }) goldenCaseId?: string | null /** Verdict of the matched golden case's gate assertions for this run; null when no golden matched or no gate applied. */ @Property({ name: 'golden_passed', type: 'boolean', nullable: true }) goldenPassed?: boolean | null /** TDCR routed-vs-pruned context summary (context overlay). */ @Property({ name: 'context_routing', type: 'jsonb', nullable: true }) contextRouting?: unknown | null /** storage-s3 key for the offloaded, encrypted full output payload. */ @Property({ name: 'output_artifact_key', type: 'varchar', length: 500, nullable: true }) outputArtifactKey?: string | null @Property({ name: 'human_confirmed_at', type: Date, nullable: true }) humanConfirmedAt?: Date | null /** Operator triage flag (trace inspector); null = unflagged. */ @Property({ name: 'flagged_at', type: Date, nullable: true }) flaggedAt?: Date | null /** FK id → auth.users; the operator who flagged the run. */ @Property({ name: 'flagged_by', type: 'uuid', nullable: true }) flaggedBy?: string | null /** FK id → agent_runs; the source run this run is a re-run of (trace inspector "Re-run"). Distinct from `parentRunId` (sub-agent delegation). */ @Property({ name: 'rerun_of_run_id', type: 'uuid', nullable: true }) rerunOfRunId?: string | null @Property({ name: 'status', type: 'varchar', length: 20, default: 'running' }) status: AgentRunStatus = 'running' /** * Forensic completion timestamp: stamped once at the terminal transition * (`runs.complete`/`runs.fail`; trace ingest sets it null-only from span end * times) and never mutated afterwards — unlike `updatedAt`, which later * writes (e.g. flagging) legitimately bump. */ @Property({ name: 'completed_at', type: Date, nullable: true }) completedAt?: Date | null @Property({ name: 'input', type: 'jsonb' }) input!: unknown @Property({ name: 'output', type: 'jsonb', nullable: true }) output?: unknown | null @Property({ name: 'result_kind', type: 'varchar', length: 20, nullable: true }) resultKind?: 'research' | 'proposal' | 'artifact' | null /** * The agent's DECLARED type at the time of the run — the authoring fact, stamped so a * run record answers "what was this agent for" without re-reading a registry that may * have changed since. NULLABLE by design: runs that predate the declaration, and agents * that never made one, have none. `resultKind` above stays the runtime fact. */ @Property({ name: 'agent_type', type: 'varchar', length: 20, nullable: true }) agentType?: AgentType | null @Property({ name: 'error_message', type: 'text', nullable: true }) errorMessage?: string | null @Property({ name: 'created_at', type: Date, onCreate: () => new Date() }) createdAt: Date = new Date() @Property({ name: 'updated_at', type: Date, onCreate: () => new Date(), onUpdate: () => new Date() }) updatedAt: Date = new Date() @Property({ name: 'deleted_at', type: Date, nullable: true }) deletedAt?: Date | null } /** * One step in an agent run's execution trace (an LLM call, a tool invocation, or * a system step). Append-only telemetry: omits `updated_at`/`deleted_at`. High * volume — partitioned by `created_at` and tiered to archive in a later phase. * Out-of-order ingestion is tolerated: `sequence` + `parentSpanId` rebuild the * tree regardless of arrival order. Other modules referenced by FK id only. */ @Entity({ tableName: 'agent_spans' }) @Index({ name: 'agent_spans_tenant_org_idx', properties: ['tenantId', 'organizationId'] }) @Index({ name: 'agent_spans_run_idx', properties: ['agentRunId', 'sequence'] }) @Unique({ name: 'agent_spans_run_external_uq', properties: ['agentRunId', 'externalSpanId'] }) export class AgentSpan { [OptionalProps]?: 'parentSpanId' | 'endedAt' | 'durationMs' | 'status' | 'attributes' | 'createdAt' @PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' }) id!: string @Property({ name: 'tenant_id', type: 'uuid' }) tenantId!: string @Property({ name: 'organization_id', type: 'uuid' }) organizationId!: string /** FK id → agent_runs. */ @Property({ name: 'agent_run_id', type: 'uuid' }) agentRunId!: string /** * Adapter-native span id. The dedupe + parent-link key: re-ingesting the same * trace is a no-op (unique per run), and children resolve their `parentSpanId` * by matching the parent's `externalSpanId` regardless of arrival order. */ @Property({ name: 'external_span_id', type: 'varchar', length: 200 }) externalSpanId!: string /** FK id → agent_spans (parent step); null for root spans. */ @Property({ name: 'parent_span_id', type: 'uuid', nullable: true }) parentSpanId?: string | null @Property({ name: 'sequence', type: 'integer' }) sequence!: number @Property({ name: 'name', type: 'varchar', length: 200 }) name!: string @Property({ name: 'kind', type: 'varchar', length: 20 }) kind!: AgentSpanKind @Property({ name: 'started_at', type: Date }) startedAt!: Date @Property({ name: 'ended_at', type: Date, nullable: true }) endedAt?: Date | null @Property({ name: 'duration_ms', type: 'integer', nullable: true }) durationMs?: number | null @Property({ name: 'status', type: 'varchar', length: 20, default: 'ok' }) status: AgentSpanStatus = 'ok' /** OTel GenAI naming target. */ @Property({ name: 'attributes', type: 'jsonb', nullable: true }) attributes?: unknown | null @Property({ name: 'created_at', type: Date, onCreate: () => new Date() }) createdAt: Date = new Date() } /** * A single tool invocation within a span. Append-only telemetry: omits * `updated_at`/`deleted_at`. Request/response summaries are redacted and stored * inline; full payloads are offloaded to storage-s3 by key, encrypted at rest. * Other modules referenced by FK id only. */ @Entity({ tableName: 'agent_tool_calls' }) @Index({ name: 'agent_tool_calls_tenant_org_idx', properties: ['tenantId', 'organizationId'] }) @Index({ name: 'agent_tool_calls_span_idx', properties: ['spanId'] }) @Index({ name: 'agent_tool_calls_run_idx', properties: ['agentRunId'] }) export class AgentToolCall { [OptionalProps]?: | 'requestSummary' | 'responseSummary' | 'requestArtifactKey' | 'responseArtifactKey' | 'status' | 'latencyMs' | 'errorMessage' | 'createdAt' @PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' }) id!: string @Property({ name: 'tenant_id', type: 'uuid' }) tenantId!: string @Property({ name: 'organization_id', type: 'uuid' }) organizationId!: string /** FK id → agent_spans. */ @Property({ name: 'span_id', type: 'uuid' }) spanId!: string /** FK id → agent_runs (denormalized for direct run-scoped queries). */ @Property({ name: 'agent_run_id', type: 'uuid' }) agentRunId!: string @Property({ name: 'tool_name', type: 'varchar', length: 200 }) toolName!: string @Property({ name: 'request_summary', type: 'jsonb', nullable: true }) requestSummary?: unknown | null @Property({ name: 'response_summary', type: 'jsonb', nullable: true }) responseSummary?: unknown | null @Property({ name: 'request_artifact_key', type: 'varchar', length: 500, nullable: true }) requestArtifactKey?: string | null @Property({ name: 'response_artifact_key', type: 'varchar', length: 500, nullable: true }) responseArtifactKey?: string | null @Property({ name: 'status', type: 'varchar', length: 20, default: 'ok' }) status: AgentToolCallStatus = 'ok' @Property({ name: 'latency_ms', type: 'integer', nullable: true }) latencyMs?: number | null @Property({ name: 'error_message', type: 'text', nullable: true }) errorMessage?: string | null @Property({ name: 'created_at', type: Date, onCreate: () => new Date() }) createdAt: Date = new Date() } export type CorrectionAction = 'edit' | 'reject' | 'override' | 'answer' /** * A human correction of an agent proposal — the flywheel's entry point. Append-only * (omits `updated_at`/`deleted_at`): the legal/oversight record must never mutate. * `reason` is mandatory and enforced by Zod + the command. Other modules referenced * by FK id only. */ @Entity({ tableName: 'agent_corrections' }) @Index({ name: 'agent_corrections_tenant_org_idx', properties: ['tenantId', 'organizationId'] }) @Index({ name: 'agent_corrections_run_idx', properties: ['agentRunId'] }) @Index({ name: 'agent_corrections_proposal_idx', properties: ['proposalId'] }) export class AgentCorrection { [OptionalProps]?: 'workflowInstanceId' | 'stepId' | 'agentRunId' | 'correctedValue' | 'evalCaseId' | 'createdAt' @PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' }) id!: string @Property({ name: 'tenant_id', type: 'uuid' }) tenantId!: string @Property({ name: 'organization_id', type: 'uuid' }) organizationId!: string /** FK id → workflows instance (no cross-module ORM relation). */ @Property({ name: 'workflow_instance_id', type: 'uuid', nullable: true }) workflowInstanceId?: string | null @Property({ name: 'step_id', type: 'varchar', length: 100, nullable: true }) stepId?: string | null /** FK id → agent_runs. */ @Property({ name: 'agent_run_id', type: 'uuid', nullable: true }) agentRunId?: string | null /** FK id → agent_proposals. */ @Property({ name: 'proposal_id', type: 'uuid' }) proposalId!: string /** FK id → auth user who recorded the correction. */ @Property({ name: 'corrected_by_user_id', type: 'uuid' }) correctedByUserId!: string @Property({ name: 'action', type: 'varchar', length: 20 }) action!: CorrectionAction /** The agent's original proposal payload. */ @Property({ name: 'proposed_value', type: 'jsonb' }) proposedValue!: unknown /** Human-supplied corrected payload; null on a plain reject. */ @Property({ name: 'corrected_value', type: 'jsonb', nullable: true }) correctedValue?: unknown | null /** Mandatory, non-empty — enforced by Zod + command. */ @Property({ name: 'reason', type: 'text' }) reason!: string /** FK id → agent_eval_cases (the auto-drafted case). */ @Property({ name: 'eval_case_id', type: 'uuid', nullable: true }) evalCaseId?: string | null @Property({ name: 'created_at', type: Date, onCreate: () => new Date() }) createdAt: Date = new Date() } export type AgentEvalCaseSourceType = 'correction' | 'golden_run' export type AgentEvalCaseStatus = 'draft' | 'approved' | 'archived' /** * A regression eval case promoted from a correction or a golden run. Editable * (carries `updated_at` → optimistic lock); an engineer approves a draft before * it is exported to the lifecycle gate. Other modules referenced by FK id only. */ @Entity({ tableName: 'agent_eval_cases' }) @Index({ name: 'agent_eval_cases_tenant_org_idx', properties: ['tenantId', 'organizationId'] }) @Index({ name: 'agent_eval_cases_agent_status_idx', properties: ['organizationId', 'agentDefinitionId', 'status'] }) @Index({ name: 'agent_eval_cases_agent_input_key_idx', properties: ['organizationId', 'agentDefinitionId', 'inputKey'] }) export class AgentEvalCase { [OptionalProps]?: | 'name' | 'processType' | 'inputKey' | 'expected' | 'assertions' | 'status' | 'approvedByUserId' | 'createdAt' | 'updatedAt' | 'deletedAt' @PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' }) id!: string @Property({ name: 'tenant_id', type: 'uuid' }) tenantId!: string @Property({ name: 'organization_id', type: 'uuid' }) organizationId!: string @Property({ name: 'source_type', type: 'varchar', length: 20 }) sourceType!: AgentEvalCaseSourceType /** FK id → agent_corrections or agent_runs (per sourceType). */ @Property({ name: 'source_id', type: 'uuid' }) sourceId!: string @Property({ name: 'agent_definition_id', type: 'varchar', length: 100 }) agentDefinitionId!: string /** Human label for the case, shown as the first list column. Plaintext (not PII). */ @Property({ name: 'name', type: 'varchar', length: 200, nullable: true }) name?: string | null @Property({ name: 'process_type', type: 'varchar', length: 100, nullable: true }) processType?: string | null @Property({ name: 'input', type: 'jsonb' }) input!: unknown /** * SHA-256 (hex) of the canonicalized plaintext `input` — the match key used to * recognize a live/playground run whose input reproduces this golden case * (`canonicalInputKey`). Plaintext by design (must be SQL-queryable); NOT in * the encryption map. Nullable for legacy rows. */ @Property({ name: 'input_key', type: 'varchar', length: 64, nullable: true }) inputKey?: string | null /** Expected output (the corrected value); null when sourced from a plain reject. */ @Property({ name: 'expected', type: 'jsonb', nullable: true }) expected?: unknown | null /** Assertion keys to apply when this case runs. */ @Property({ name: 'assertions', type: 'jsonb', nullable: true }) assertions?: unknown | null @Property({ name: 'status', type: 'varchar', length: 20, default: 'draft' }) status: AgentEvalCaseStatus = 'draft' @Property({ name: 'approved_by_user_id', type: 'uuid', nullable: true }) approvedByUserId?: string | null @Property({ name: 'created_at', type: Date, onCreate: () => new Date() }) createdAt: Date = new Date() @Property({ name: 'updated_at', type: Date, onCreate: () => new Date(), onUpdate: () => new Date() }) updatedAt: Date = new Date() @Property({ name: 'deleted_at', type: Date, nullable: true }) deletedAt?: Date | null } export type AgentEvalAssertionType = 'deterministic' | 'llm_judge' export type AgentEvalSeverity = 'gate' | 'warn' /** * A configured assertion applied to agent runs during evaluation. Editable * (carries `updated_at` → optimistic lock). `key` selects the shared pure-function * scorer; `config` parameterizes it. Only `deterministic` assertions may carry * `severity: 'gate'` (the gate tier must be reproducible); `llm_judge` is always `warn`. */ @Entity({ tableName: 'agent_eval_assertions' }) @Index({ name: 'agent_eval_assertions_tenant_org_idx', properties: ['tenantId', 'organizationId'] }) @Index({ name: 'agent_eval_assertions_applies_idx', properties: ['organizationId', 'appliesTo', 'enabled'] }) @Index({ name: 'agent_eval_assertions_scorer_idx', properties: ['organizationId', 'scorerKey'] }) @Unique({ name: 'agent_eval_assertions_key_uq', properties: ['organizationId', 'appliesTo', 'key'] }) export class AgentEvalAssertion { [OptionalProps]?: | 'description' | 'config' | 'version' | 'enabled' | 'createdAt' | 'updatedAt' | 'deletedAt' @PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' }) id!: string @Property({ name: 'tenant_id', type: 'uuid' }) tenantId!: string @Property({ name: 'organization_id', type: 'uuid' }) organizationId!: string /** * Instance slug — unique per (organization, appliesTo). NOT the scorer identity: * `agent_eval_assertions_key_uq` allows one row per key, so overloading it with * the scorer identity capped the catalog at one assertion per scorer per agent * (e.g. two `contains` checks were unrepresentable). See `scorerKey`. */ @Property({ name: 'key', type: 'varchar', length: 100 }) key!: string /** * Which registry scorer runs — see `lib/eval/registry`. Backfilled from * `COALESCE(config->>'scorer', key)`, which reproduces the pre-column resolution * rule exactly, so existing rows keep their behaviour. */ @Property({ name: 'scorer_key', type: 'varchar', length: 100 }) scorerKey!: string @Property({ name: 'title', type: 'varchar', length: 200 }) title!: string @Property({ name: 'description', type: 'text', nullable: true }) description?: string | null /** Target agent definition id, or `'*'` to apply to every agent. */ @Property({ name: 'applies_to', type: 'varchar', length: 100 }) appliesTo!: string @Property({ name: 'type', type: 'varchar', length: 20 }) type!: AgentEvalAssertionType @Property({ name: 'severity', type: 'varchar', length: 20 }) severity!: AgentEvalSeverity @Property({ name: 'config', type: 'jsonb', nullable: true }) config?: unknown | null @Property({ name: 'version', type: 'integer', default: 1 }) version: number = 1 @Property({ name: 'enabled', type: 'boolean', default: true }) enabled: boolean = true @Property({ name: 'created_at', type: Date, onCreate: () => new Date() }) createdAt: Date = new Date() @Property({ name: 'updated_at', type: Date, onCreate: () => new Date(), onUpdate: () => new Date() }) updatedAt: Date = new Date() @Property({ name: 'deleted_at', type: Date, nullable: true }) deletedAt?: Date | null } /** * The verdict of one assertion against one run. Append-only (omits * `updated_at`/`deleted_at`) — eval results are legal records retained ≥6 years. * A failing `gate` result marks the run `evalPassed = false`; `warn` never blocks. */ @Entity({ tableName: 'agent_eval_results' }) @Index({ name: 'agent_eval_results_tenant_org_idx', properties: ['tenantId', 'organizationId'] }) @Index({ name: 'agent_eval_results_run_idx', properties: ['agentRunId'] }) @Index({ name: 'agent_eval_results_assertion_idx', properties: ['assertionId'] }) @Index({ name: 'agent_eval_results_case_run_idx', properties: ['evalCaseRunId'] }) @Index({ name: 'agent_eval_results_matched_case_idx', properties: ['matchedEvalCaseId'] }) export class AgentEvalResult { [OptionalProps]?: 'evalCaseRunId' | 'matchedEvalCaseId' | 'passed' | 'score' | 'evidence' | 'evaluatedAt' | 'createdAt' @PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' }) id!: string @Property({ name: 'tenant_id', type: 'uuid' }) tenantId!: string @Property({ name: 'organization_id', type: 'uuid' }) organizationId!: string /** FK id → agent_runs. */ @Property({ name: 'agent_run_id', type: 'uuid' }) agentRunId!: string /** FK id → agent_eval_assertions. */ @Property({ name: 'assertion_id', type: 'uuid' }) assertionId!: string @Property({ name: 'assertion_key', type: 'varchar', length: 100 }) assertionKey!: string /** * FK id → agent_eval_case_runs. Null for a result produced by the ONLINE plane * (inline at trace ingest, where there is no eval case); set for a result from * the eval plane. Nullable keeps every pre-existing row valid. */ @Property({ name: 'eval_case_run_id', type: 'uuid', nullable: true }) evalCaseRunId?: string | null /** * FK id → agent_eval_cases. Set (with `evalCaseRunId` null) when this verdict * comes from the ONLINE golden-match plane: a live/playground run whose input * matched an approved golden case, scored against that case's `expected`. Null * for plain online results and for eval-plane (`evalCaseRunId`) results. */ @Property({ name: 'matched_eval_case_id', type: 'uuid', nullable: true }) matchedEvalCaseId?: string | null /** * `null` means SKIPPED and is the single source of truth for it — there is no * separate flag. Skipped results are excluded from score AND pass aggregation * alike: never counted as 0, never counted as failing. * * Invariant: `score === null` ⟺ `passed === null`. */ @Property({ name: 'passed', type: 'boolean', nullable: true }) passed?: boolean | null @Property({ name: 'score', type: 'float', nullable: true }) score?: number | null @Property({ name: 'severity', type: 'varchar', length: 20 }) severity!: AgentEvalSeverity @Property({ name: 'evidence', type: 'jsonb', nullable: true }) evidence?: unknown | null @Property({ name: 'evaluated_at', type: Date, onCreate: () => new Date() }) evaluatedAt: Date = new Date() @Property({ name: 'created_at', type: Date, onCreate: () => new Date() }) createdAt: Date = new Date() } /** * A precomputed per-agent KPI window (F2 metric rollups). Append-only (omits * `updated_at`/`deleted_at`): each row is an immutable snapshot of an agent's * metrics over a fixed `[windowStart, windowEnd)`. The rollup worker recomputes * and re-stamps a row idempotently per `(organizationId, agentId, windowStart)` * so the metrics endpoint reads a stable window with a live fallback instead of * a capped live scan. `metrics` jsonb shape is validated by the Zod schema in * data/validators.ts. Other modules referenced by FK id only. */ @Entity({ tableName: 'agent_metric_rollups' }) @Index({ name: 'agent_metric_rollups_tenant_org_idx', properties: ['tenantId', 'organizationId'] }) @Index({ name: 'agent_metric_rollups_lookup_idx', properties: ['organizationId', 'agentId', 'windowStart'] }) export class AgentMetricRollup { [OptionalProps]?: 'computedAt' | 'createdAt' @PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' }) id!: string @Property({ name: 'tenant_id', type: 'uuid' }) tenantId!: string @Property({ name: 'organization_id', type: 'uuid' }) organizationId!: string /** Agent definition id — mirrors AgentRun.agentId (NOT agentDefinitionId). */ @Property({ name: 'agent_id', type: 'varchar', length: 100 }) agentId!: string @Property({ name: 'window_start', type: Date }) windowStart!: Date @Property({ name: 'window_end', type: Date }) windowEnd!: Date @Property({ name: 'computed_at', type: Date, onCreate: () => new Date() }) computedAt: Date = new Date() /** override/eval-pass/approve-unchanged/latency/cost/count KPIs (validated by Zod). */ @Property({ name: 'metrics', type: 'jsonb' }) metrics!: unknown @Property({ name: 'created_at', type: Date, onCreate: () => new Date() }) createdAt: Date = new Date() } export type AgentRunSessionStatus = 'pending' | 'completed' /** * Cross-process correlation for an OpenCode file-agent run. The runner (app / * worker process) and the `submit_outcome` / `load_skill` / `run_skill_script` * MCP tools (separate `mcp:serve-http` process) do NOT share memory, so the * active-agent + captured-outcome handoff cannot live in an in-process Map. This * row, keyed by the per-run session token, is the shared store both processes * reach: the runner `open`s it before sending; `submit_outcome` resolves the * active agent from it and writes the validated `outcome`; the runner polls for * the completed outcome and `dispose`s the row when the run ends. */ @Entity({ tableName: 'agent_run_sessions' }) @Index({ name: 'agent_run_sessions_token_idx', properties: ['sessionToken'] }) export class AgentRunSession { [OptionalProps]?: 'runId' | 'outcome' | 'status' | 'createdAt' | 'updatedAt' @PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' }) id!: string /** The per-run session token the runner minted = the correlation key (unique). */ @Property({ name: 'session_token', type: 'varchar', length: 100, unique: true }) sessionToken!: string @Property({ name: 'agent_id', type: 'varchar', length: 100 }) agentId!: string @Property({ name: 'run_id', type: 'uuid', nullable: true }) runId?: string | null @Property({ name: 'tenant_id', type: 'uuid' }) tenantId!: string @Property({ name: 'organization_id', type: 'uuid' }) organizationId!: string /** The validated outcome captured by `submit_outcome`, once it arrives. */ @Property({ name: 'outcome', type: 'jsonb', nullable: true }) outcome?: unknown | null @Property({ name: 'status', type: 'varchar', length: 20, default: 'pending' }) status: AgentRunSessionStatus = 'pending' @Property({ name: 'created_at', type: Date, onCreate: () => new Date() }) createdAt: Date = new Date() @Property({ name: 'updated_at', type: Date, onCreate: () => new Date(), onUpdate: () => new Date() }) updatedAt: Date = new Date() } export type GuardrailPhase = 'input' | 'output' export type GuardrailKind = | 'prompt_injection' | 'pii' | 'grounding' | 'schema' | 'moderation' | 'tool_scope' export type GuardrailResult = 'pass' | 'warn' | 'block' /** * Append-only audit of every runtime guardrail check (omits `updated_at`/ * `deleted_at`). One row per check per phase; `guardResults` on the AgentProposal * carries the same verdict for fast read. `evidence` holds REDACTED data only * (pointers/offsets into the encrypted artifact store) — never raw PII. Shape is * enforced by the Zod schema in data/validators.ts. Other modules referenced by * FK id only (agentRunId, proposalId). */ @Entity({ tableName: 'agent_guardrail_checks' }) @Index({ name: 'agent_guardrail_checks_tenant_org_idx', properties: ['tenantId', 'organizationId'] }) @Index({ name: 'agent_guardrail_checks_run_idx', properties: ['agentRunId', 'createdAt'] }) @Index({ name: 'agent_guardrail_checks_proposal_idx', properties: ['proposalId'] }) export class AgentGuardrailCheck { [OptionalProps]?: 'result' | 'evidence' | 'proposalId' | 'createdAt' @PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' }) id!: string @Property({ name: 'tenant_id', type: 'uuid' }) tenantId!: string @Property({ name: 'organization_id', type: 'uuid' }) organizationId!: string /** FK id → agent_runs; NOT an ORM relation. */ @Property({ name: 'agent_run_id', type: 'uuid' }) agentRunId!: string /** FK id → agent_proposals (null for pre-call input checks). */ @Property({ name: 'proposal_id', type: 'uuid', nullable: true }) proposalId?: string | null /** Which versioned set produced this verdict. */ @Property({ name: 'guardrail_set_version', type: 'varchar', length: 64 }) guardrailSetVersion!: string @Property({ name: 'capability', type: 'varchar', length: 100 }) capability!: string @Property({ name: 'phase', type: 'varchar', length: 10 }) phase!: GuardrailPhase @Property({ name: 'kind', type: 'varchar', length: 30 }) kind!: GuardrailKind @Property({ name: 'result', type: 'varchar', length: 10, default: 'pass' }) result: GuardrailResult = 'pass' /** * Redacted evidence ONLY — never raw PII; pointers/offsets into the encrypted * artifact store (trace spec) rather than plaintext spans. Shape enforced by a * Zod schema in data/validators.ts. */ @Property({ name: 'evidence', type: 'jsonb', nullable: true }) evidence?: unknown | null @Property({ name: 'created_at', type: Date, onCreate: () => new Date() }) createdAt: Date = new Date() } /** * A versioned guardrail SET for one capability (Wave 3, Phase 4 — grounding). * Append-only by version (omits `updated_at`/`deleted_at`): each row pins a * capability's policy body under a CONTENT-HASH `version`. The grounding sync * (setup.ts `seedDefaults`) upserts one row per `(organizationId, capability, * version)` — re-syncing an unchanged body is a no-op (idempotent), and editing * the body produces a new content-hash → a new append-only version. The * `guardrailSetVersion` recorded on every grounding `AgentGuardrailCheck` is this * `version`, so a verdict is replayable against the exact policy that produced it. * Other modules referenced by FK id only. */ @Entity({ tableName: 'agent_guardrail_sets' }) @Index({ name: 'agent_guardrail_sets_tenant_org_idx', properties: ['tenantId', 'organizationId'] }) @Index({ name: 'agent_guardrail_sets_capability_idx', properties: ['organizationId', 'capability'] }) @Unique({ name: 'agent_guardrail_sets_version_uq', properties: ['organizationId', 'capability', 'version'] }) export class AgentGuardrailSet { [OptionalProps]?: 'createdAt' @PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' }) id!: string @Property({ name: 'tenant_id', type: 'uuid' }) tenantId!: string @Property({ name: 'organization_id', type: 'uuid' }) organizationId!: string @Property({ name: 'capability', type: 'varchar', length: 100 }) capability!: string /** Content-hash of the canonical set body — the durable version key. */ @Property({ name: 'version', type: 'varchar', length: 64 }) version!: string /** The grounding policy body. Shape enforced by Zod in data/validators.ts. */ @Property({ name: 'body', type: 'jsonb' }) body!: unknown @Property({ name: 'created_at', type: Date, onCreate: () => new Date() }) createdAt: Date = new Date() } /** * Which authentication path an agent principal uses (agent identity spec). * `internal` = in-process `INVOKE_AGENT` step, NO network auth and NO interactive * credential (Phase 1; the only mode provisioned today). `oauth_client` = * net-new OAuth client-credentials `/token` server (Phase 3). `authmd` = * `auth.md` / ID-JAG self-registration (Phase 4). The non-`internal` modes are * declared here as forward-compatible seams; only `internal` is provisioned now. */ export type AgentCredentialMode = 'internal' | 'oauth_client' | 'authmd' /** * Links an AI agent to its provisioned non-interactive `auth.User` (`kind='agent'`) * and a scoped, least-privilege `auth.Role`, so every agent action is attributed * to a concrete user id through the same Command/CRUD/ACL/audit pipeline as a * human (agent identity & on-behalf-of spec, Wave 4 Phase 1). Editable (revoke / * disable) → carries `updated_at` for optimistic locking. Other modules * (`auth.User`, `auth.Role`, the agent definition) are referenced by FK id only — * NOT as ORM relations — per the cross-module decoupling rule. */ // One LIVE principal per (organization_id, agent_definition_id) is enforced by a // partial unique index (`agent_principals_org_agent_uq`) over live rows // (`WHERE deleted_at IS NULL`), created by raw SQL in Migration20260625050000. A // `@Unique` decorator can't express a partial index (it would block re-provisioning // after a soft-delete), so it is declared via `@Index({ expression })` — the // repo's partial-index convention (cf. `agent_runs_eval_failed_idx`) — which // keeps `db:generate` snapshot-aware of it instead of emitting a drop each run. @Entity({ tableName: 'agent_principals' }) @Index({ name: 'agent_principals_tenant_org_idx', properties: ['tenantId', 'organizationId'] }) @Index({ name: 'agent_principals_user_idx', properties: ['userId'] }) @Index({ name: 'agent_principals_org_agent_uq', expression: `create unique index "agent_principals_org_agent_uq" on "agent_principals" ("organization_id", "agent_definition_id") where "deleted_at" is null`, }) export class AgentPrincipal { [OptionalProps]?: 'credentialMode' | 'enabled' | 'createdAt' | 'updatedAt' | 'deletedAt' @PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' }) id!: string @Property({ name: 'tenant_id', type: 'uuid' }) tenantId!: string @Property({ name: 'organization_id', type: 'uuid' }) organizationId!: string /** FK id → auth.User (kind='agent'); NOT an ORM relation. */ @Property({ name: 'user_id', type: 'uuid' }) userId!: string /** FK id → the agent definition (the `defineAgent`/file-agent id). */ @Property({ name: 'agent_definition_id', type: 'varchar', length: 100 }) agentDefinitionId!: string /** FK id → auth.Role (scoped, least privilege); NOT an ORM relation. */ @Property({ name: 'role_id', type: 'uuid' }) roleId!: string /** Selects the auth path explicitly; only `internal` is provisioned in Phase 1. */ @Property({ name: 'credential_mode', type: 'varchar', length: 20, default: 'internal' }) credentialMode: AgentCredentialMode = 'internal' @Property({ name: 'enabled', type: 'boolean', default: true }) enabled: boolean = true @Property({ name: 'created_at', type: Date, onCreate: () => new Date() }) createdAt: Date = new Date() @Property({ name: 'updated_at', type: Date, onCreate: () => new Date(), onUpdate: () => new Date() }) updatedAt: Date = new Date() @Property({ name: 'deleted_at', type: Date, nullable: true }) deletedAt?: Date | null } /** * An external agent's delegation grant (agent identity & on-behalf-of spec, Wave 4 * Phase 3). Links an external `AgentPrincipal` (`credentialMode='oauth_client'`) * to the human delegator + the scopes it may mint OAuth client-credentials tokens * for, and is the per-request REVOCATION spine: the `/token` server refuses to * mint while every minted token re-checks `revokedAt`/`expiresAt` on the NEXT * write, so revoking stops further agent action immediately rather than at token * expiry. The `issuer`/`subject`/`audience` columns are forward-compatible seams * for the later `auth.md`/ID-JAG path (Phase 4) — null for the OAuth-now path, so * the same record bridges both with no schema change. Editable (revoke) → carries * `updated_at` for optimistic locking. Dual tenancy (tenant_id + organization_id); * reads filter by organization_id. Other modules (`auth.User`, the agent * principal) are referenced by FK id only — NOT as ORM relations. */ @Entity({ tableName: 'agent_delegation_grants' }) @Index({ name: 'agent_delegation_grants_tenant_org_idx', properties: ['tenantId', 'organizationId'] }) @Index({ name: 'agent_delegation_grants_principal_idx', properties: ['organizationId', 'agentPrincipalId'] }) export class AgentDelegationGrant { [OptionalProps]?: 'delegatorUserId' | 'expiresAt' | 'revokedAt' | 'revokedByUserId' | 'issuer' | 'subject' | 'audience' | 'createdAt' | 'updatedAt' | 'deletedAt' @PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' }) id!: string @Property({ name: 'tenant_id', type: 'uuid' }) tenantId!: string @Property({ name: 'organization_id', type: 'uuid' }) organizationId!: string /** FK id → agent_principals (the external principal); NOT an ORM relation. */ @Property({ name: 'agent_principal_id', type: 'uuid' }) agentPrincipalId!: string /** FK id → the agent principal's `auth.User` (actor on every attributed write). */ @Property({ name: 'agent_user_id', type: 'uuid' }) agentUserId!: string /** FK id → auth.User — the human delegating authority; null for system grants. */ @Property({ name: 'delegator_user_id', type: 'uuid', nullable: true }) delegatorUserId?: string | null /** `:` scopes the minted token may carry. */ @Property({ name: 'scopes', type: 'jsonb' }) scopes!: string[] /** Optional hard expiry; tokens never outlive this even before revocation. */ @Property({ name: 'expires_at', type: Date, nullable: true }) expiresAt?: Date | null /** When set, every token bound to this grant is denied on its next request. */ @Property({ name: 'revoked_at', type: Date, nullable: true }) revokedAt?: Date | null /** FK id → auth.User who revoked the grant. */ @Property({ name: 'revoked_by_user_id', type: 'uuid', nullable: true }) revokedByUserId?: string | null /** Forward-compatible ID-JAG seam (Phase 4); null for the OAuth-now path. */ @Property({ name: 'issuer', type: 'varchar', length: 500, nullable: true }) issuer?: string | null @Property({ name: 'subject', type: 'varchar', length: 500, nullable: true }) subject?: string | null @Property({ name: 'audience', type: 'varchar', length: 500, nullable: true }) audience?: string | null @Property({ name: 'created_at', type: Date, onCreate: () => new Date() }) createdAt: Date = new Date() @Property({ name: 'updated_at', type: Date, onCreate: () => new Date(), onUpdate: () => new Date() }) updatedAt: Date = new Date() @Property({ name: 'deleted_at', type: Date, nullable: true }) deletedAt?: Date | null } export type AgentProposalDisposition = | 'pending' | 'auto_approved' | 'approved' | 'edited' | 'rejected' | 'none_proposed' /** * Why an auto-approval that cleared its threshold was still routed to a human — * the gate the policy layer answered `review` on (`lib/disposition/autoApprovalPolicy.ts`). * Recorded ONLY when the proposal cleared what it was measured against and was * held anyway; a proposal that simply did not clear needs no explanation. */ export type AgentProposalAutoDispositionBlock = | 'near_tie' | 'risk' | 'guardrail' | 'trace_incomplete' | 'policy' export type AgentProposalSource = 'runtime' | 'eval' @Entity({ tableName: 'agent_proposals' }) @Index({ name: 'agent_proposals_tenant_org_idx', properties: ['tenantId', 'organizationId'] }) @Index({ name: 'agent_proposals_run_idx', properties: ['organizationId', 'runId'] }) @Index({ name: 'agent_proposals_org_disposition_created_idx', properties: ['organizationId', 'disposition', 'createdAt'] }) export class AgentProposal { [OptionalProps]?: 'source' | 'disposition' | 'dispositionBy' | 'dispositionReason' | 'workflowInstanceId' | 'stepId' | 'userTaskId' | 'confidence' | 'guardResults' | 'createdAt' | 'updatedAt' | 'deletedAt' | 'selectedOptionId' | 'autoDispositionBlock' @PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' }) id!: string @Property({ name: 'tenant_id', type: 'uuid' }) tenantId!: string @Property({ name: 'organization_id', type: 'uuid' }) organizationId!: string @Property({ name: 'agent_id', type: 'varchar', length: 100 }) agentId!: string @Property({ name: 'run_id', type: 'uuid' }) runId!: string /** FK id → workflows instance (no cross-module ORM relation). */ @Property({ name: 'workflow_instance_id', type: 'uuid', nullable: true }) workflowInstanceId?: string | null @Property({ name: 'step_id', type: 'varchar', length: 100, nullable: true }) stepId?: string | null /** * The workflows `UserTask` raised for this proposal's human review, when one * was raised (spec §7.5 / A7). * * An FK-by-id to another module's row, never an ORM relation. It exists so a * disposition can CLOSE the review task it created: without it the task * outlives the decision and sits in the operator's inbox as work that has * already been done. Null on every auto-approved proposal (no task is ever * raised) and on every row written before this column existed. */ @Property({ name: 'user_task_id', type: 'uuid', nullable: true }) userTaskId?: string | null @Property({ name: 'payload', type: 'jsonb' }) payload!: unknown @Property({ name: 'confidence', type: 'float', nullable: true }) confidence?: number | null /** * The guardrail verdict's `checks` array attached at proposal creation (output * phase). Append-only audit lives in `agent_guardrail_checks`; this is the fast * read carried on the proposal. Validated by the Zod schema in data/validators.ts. */ @Property({ name: 'guard_results', type: 'jsonb', nullable: true }) guardResults?: unknown | null /** * Where this proposal came from. `eval` marks a proposal produced by an eval * replay: it is a real record of what the agent proposed, but it must never * reach the operator caseload and must never be disposed. Defaults to `runtime` * so every pre-existing row keeps its current meaning. */ @Property({ name: 'source', type: 'varchar', length: 20, default: 'runtime' }) source: AgentProposalSource = 'runtime' @Property({ name: 'disposition', type: 'varchar', length: 20, default: 'pending' }) disposition: AgentProposalDisposition = 'pending' @Property({ name: 'disposition_by', type: 'varchar', length: 100, nullable: true }) dispositionBy?: string | null @Property({ name: 'disposition_reason', type: 'text', nullable: true }) dispositionReason?: string | null /** * Which of `payload.options` the disposition selected. Set on approve/edit (and on * the rule-driven auto-approve, which selects the leader); null on reject, on a * pending proposal, and on every row written before the envelope existed. */ @Property({ name: 'selected_option_id', type: 'varchar', length: 100, nullable: true }) selectedOptionId?: string | null /** * Why an auto-approval that cleared its threshold was still held for a human. * Distinct from `dispositionReason`, which carries the OPERATOR's words. */ @Property({ name: 'auto_disposition_block', type: 'varchar', length: 20, nullable: true }) autoDispositionBlock?: AgentProposalAutoDispositionBlock | null @Property({ name: 'created_at', type: Date, onCreate: () => new Date() }) createdAt: Date = new Date() @Property({ name: 'updated_at', type: Date, onCreate: () => new Date(), onUpdate: () => new Date() }) updatedAt: Date = new Date() @Property({ name: 'deleted_at', type: Date, nullable: true }) deletedAt?: Date | null } /** * The single durable artifact of a Task-Driven Context Routing (TDCR) assembly — * the evidence record of *what an agent saw* for one `INVOKE_AGENT` run. Append-only * (omits `updated_at`/`deleted_at`) per conventions §3.2: it is immutable evidence * read by the trace inspector (routed vs. pruned + token usage), the guardrails * grounding check (cited snippets), and compliance lineage (fact → evidence). * * `routedSources`/`prunedSources`/`sources`/`redactionApplied` jsonb shapes are * enforced by Zod in data/validators.ts (`contextBundleRoutedSourcesSchema` etc.). * Other modules referenced by FK id only (agentRunId, workflowInstanceId). */ @Entity({ tableName: 'agent_context_bundles' }) @Index({ name: 'agent_context_bundles_tenant_org_idx', properties: ['tenantId', 'organizationId'] }) @Index({ name: 'agent_context_bundles_run_idx', properties: ['agentRunId'] }) export class AgentContextBundle { [OptionalProps]?: 'workflowInstanceId' | 'stepId' | 'prunedSources' | 'redactionApplied' | 'payloadRef' | 'createdAt' @PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' }) id!: string @Property({ name: 'tenant_id', type: 'uuid' }) tenantId!: string @Property({ name: 'organization_id', type: 'uuid' }) organizationId!: string /** FK id → agent_runs; NOT an ORM relation. */ @Property({ name: 'agent_run_id', type: 'uuid' }) agentRunId!: string /** FK id → workflows process instance (null for standalone runs). */ /** FK id → workflows instance (no cross-module ORM relation). */ @Property({ name: 'workflow_instance_id', type: 'uuid', nullable: true }) workflowInstanceId?: string | null @Property({ name: 'step_id', type: 'varchar', length: 100, nullable: true }) stepId?: string | null @Property({ name: 'capability', type: 'varchar', length: 100 }) capability!: string /** * Selected & packed sources: `{ kind, ref, locator?, tokens, score? }[]`. The * mandatory floor is always present here. Shape enforced by Zod. */ @Property({ name: 'routed_sources', type: 'jsonb' }) routedSources!: unknown /** * Excluded candidates with a reason (over budget / out of scope): * `{ kind, ref, reason }[]`. Records the optional-fill variance for audit. */ @Property({ name: 'pruned_sources', type: 'jsonb', nullable: true }) prunedSources?: unknown | null /** Provenance: `{ factId, sourceKind, sourceRef, locator? }[]` (→ lineage). */ @Property({ name: 'sources', type: 'jsonb' }) sources!: unknown @Property({ name: 'token_budget', type: 'integer' }) tokenBudget!: number @Property({ name: 'tokens_used', type: 'integer' }) tokensUsed!: number /** `{ field, rule }[]` redacted before the agent saw it (P4 populates richer rules). */ @Property({ name: 'redaction_applied', type: 'jsonb', nullable: true }) redactionApplied?: unknown | null /** storage-s3 ref to the packed context payload (P4 offloads the full payload). */ @Property({ name: 'payload_ref', type: 'varchar', length: 500, nullable: true }) payloadRef?: string | null @Property({ name: 'created_at', type: Date, onCreate: () => new Date() }) createdAt: Date = new Date() } /** * The AUTHORED business definition of a process (business-process ↔ workflow * unification spec, 2026-09-06): what the process is FOR. It POINTS AT a * `workflows` definition and never restates its execution semantics — sequence, * retry, timers, waits, signals, branches and failure handling all belong to the * workflow, which is the single lifecycle owner of every execution. * * There is deliberately no agent target. A single-agent process materializes a * real `START → INVOKE_AGENT → END` workflow definition instead * (`lib/processes/materializeAgentWorkflow.ts`), so one engine runs everything * and the user can grow the process by editing it in the Studio. * * Execution identity is likewise NOT here: the bound `WorkflowDefinition` owns * `grantedFeatures` and the least-privilege `auth` principal core provisions from * it. `ProcessInstance.triggeredBy` records the INVOKER, which is provenance and * never an ACL identity — permission to start a process is not the permission set * the process runs with. * * User-editable → carries `updated_at` for optimistic locking (default ON). */ @Entity({ tableName: 'process_definitions' }) @Index({ name: 'process_definitions_tenant_org_idx', properties: ['tenantId', 'organizationId'] }) @Index({ name: 'process_definitions_workflow_idx', properties: ['organizationId', 'workflowId'] }) // The event dispatcher's lookup is a containment probe over this jsonb // (`triggers @> '[{"kind":"event","eventPattern":"claims.claim.reported"}]'`). // `jsonb_path_ops` is the smaller, faster opclass and supports exactly the `@>` // operator that probe uses. Declared via `@Index({ expression })` so // `db:generate` stays aware of it. @Index({ name: 'process_definitions_triggers_gin', expression: `create index "process_definitions_triggers_gin" on "process_definitions" using gin ("triggers" jsonb_path_ops)`, }) export class ProcessDefinition { [OptionalProps]?: | 'description' | 'inputDefaults' | 'inputSchema' | 'outcomeSchema' | 'triggers' | 'milestones' | 'uiMetadata' | 'enabled' | 'createdBy' | 'createdAt' | 'updatedAt' | 'deletedAt' @PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' }) id!: string @Property({ name: 'tenant_id', type: 'uuid' }) tenantId!: string @Property({ name: 'organization_id', type: 'uuid' }) organizationId!: string @Property({ name: 'name', type: 'varchar', length: 255 }) name!: string @Property({ name: 'description', type: 'text', nullable: true }) description?: string | null /** * `WorkflowDefinition.workflowId` — FK id only, never an ORM relation. REQUIRED: * a business process with no workflow has no execution engine, which is the * exact split this model removes. */ @Property({ name: 'workflow_id', type: 'varchar', length: 150 }) workflowId!: string /** Default input merged under the run-time input; encrypted (encryption.ts). */ @Property({ name: 'input_defaults', type: 'jsonb', nullable: true }) inputDefaults?: unknown | null /** Optional JSON-Schema (OUTCOME-compatible subset) validating start input. */ @Property({ name: 'input_schema', type: 'jsonb', nullable: true }) inputSchema?: unknown | null /** * Optional JSON-Schema describing the BUSINESS outcome a completed execution * produces. Documentation and validation of `ProcessInstance.outcome_*`, not a * completion requirement: a research or monitoring process produces nothing and * completes perfectly validly. */ @Property({ name: 'outcome_schema', type: 'jsonb', nullable: true }) outcomeSchema?: unknown | null /** * The declared entry points (`ProcessTrigger[]`, `.max(20)`): `schedule`, * `event` and `manual` — which makes hand-starting a declared capability rather * than an undocumented one. A definition with no `manual` trigger 403s on the * start route. Every kind converges on ONE command that starts a workflow. */ @Property({ name: 'triggers', type: 'jsonb', nullable: true, default: '[]' }) triggers?: ProcessTrigger[] | null /** * The declared milestone VOCABULARY (`ProcessMilestone[]`, `.max(50)`): * `{ key, label, order }`. A milestone is a business EVENT the workflow emits * (a step declares `milestone: ''` in its advanced config), NOT an alias * for a step — so a stage can be reached after a parallel join, after a retry, * or after ten steps, and the business reader never learns there were branches. * A declared key no step emits is a WARNING, never an error: a definition * mid-edit must stay saveable. */ @Property({ name: 'milestones', type: 'jsonb', nullable: true, default: '[]' }) milestones?: ProcessMilestone[] | null /** Optional presentation hints (icon, accent, column preferences). Display only. */ @Property({ name: 'ui_metadata', type: 'jsonb', nullable: true }) uiMetadata?: unknown | null @Property({ name: 'enabled', type: 'boolean', default: true }) enabled: boolean = true /** FK id → auth.users; the admin who created the definition. */ @Property({ name: 'created_by', type: 'uuid', nullable: true }) createdBy?: string | null @Property({ name: 'created_at', type: Date, onCreate: () => new Date() }) createdAt: Date = new Date() @Property({ name: 'updated_at', type: Date, onCreate: () => new Date(), onUpdate: () => new Date() }) updatedAt: Date = new Date() @Property({ name: 'deleted_at', type: Date, nullable: true }) deletedAt?: Date | null } /** Derived display status of a `ProcessInstance` (first match wins — see `deriveProcessStatus`). */ export type ProcessInstanceStatus = | 'running' | 'waiting_on_you' | 'question_open' | 'docs_requested' | 'fraud_hold' | 'auto_completing' | 'auto_completed' | 'completed' | 'failed' | 'cancelled' /** * The business-facing READ MODEL of one execution — ONE row per * `WorkflowInstance`. It is a PROJECTION and decides nothing: the workflow * instance is the single lifecycle owner, and every field here is recomputed * from that instance, this module's own run/proposal rows and the milestone * events, by an idempotent recompute-from-source service * (`lib/processes/processProjection.ts`, rebuildable via the * `rebuild-processes` CLI). * * `status` is DERIVED. There is deliberately no second status column in the * system: the predecessor model kept one here and another on the deleted * `agent_process_runs` ledger, and the two could disagree on redelivery, on a * crash between the writes, or while an instance parked at a USER_TASK. * * The row is created BEFORE the instance exists (the start command writes it, * then the worker starts the workflow and stamps `workflow_instance_id`), which * is what lets the business-execution idempotency key live here: the partial * unique index below is the lock that makes one key produce exactly one * `WorkflowInstance`, no matter how many callers race. * * Filter-driving subject facets (`subject_type`/`subject_value_minor`/ * `subject_fraud`) are deliberately PLAINTEXT typed columns (SQL-filterable); * only the free-text `subject_title` and the `input`/`failure_reason` pair are * encrypted (encryption.ts). Other modules are referenced by FK id only. */ @Entity({ tableName: 'process_instances' }) @Index({ name: 'process_instances_tenant_org_idx', properties: ['tenantId', 'organizationId'] }) @Index({ name: 'process_instances_status_idx', properties: ['organizationId', 'status', 'lastActivityAt'] }) @Index({ name: 'process_instances_value_idx', properties: ['organizationId', 'subjectValueMinor'] }) @Index({ name: 'process_instances_definition_idx', properties: ['processDefinitionId', 'createdAt'] }) @Index({ name: 'process_instances_source_idx', properties: ['sourceEntityType', 'sourceEntityId'] }) // One LIVE projection per (tenant, org, workflow instance) — the 1:1 that makes // this a projection rather than a second execution record. @Index({ name: 'process_instances_workflow_instance_uq', expression: `create unique index "process_instances_workflow_instance_uq" on "process_instances" ("tenant_id", "organization_id", "workflow_instance_id") where "deleted_at" is null and "workflow_instance_id" is not null`, }) // The BUSINESS-EXECUTION idempotency lock: one (definition, key) can never // produce two executions, and therefore never two workflow instances. @Index({ name: 'process_instances_idempotency_uq', expression: `create unique index "process_instances_idempotency_uq" on "process_instances" ("organization_id", "process_definition_id", "idempotency_key") where "idempotency_key" is not null`, }) export class ProcessInstance { [OptionalProps]?: 'processDefinitionId' | 'workflowInstanceId' | 'workflowId' | 'workflowVersion' | 'triggeredBy' | 'idempotencyKey' | 'input' | 'sourceEntityType' | 'sourceEntityId' | 'subjectType' | 'subjectId' | 'subjectLabel' | 'subjectTitle' | 'subjectFacets' | 'subjectValueMinor' | 'subjectFraud' | 'status' | 'currentStage' | 'milestonesReached' | 'agentIds' | 'costMinor' | 'currency' | 'runCount' | 'pendingProposalCount' | 'outcomeType' | 'outcomeId' | 'outcomeLabel' | 'failureReason' | 'assigneeUserId' | 'teamId' | 'waitingSince' | 'lastActivityAt' | 'completedAt' | 'createdAt' | 'updatedAt' | 'deletedAt' @PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' }) id!: string @Property({ name: 'tenant_id', type: 'uuid' }) tenantId!: string @Property({ name: 'organization_id', type: 'uuid' }) organizationId!: string /** * FK id → `workflows` instance; NOT an ORM relation. Nullable only for the * window between the start command's insert and the worker stamping the * started instance — never null for an execution that is actually running. */ @Property({ name: 'workflow_instance_id', type: 'uuid', nullable: true }) workflowInstanceId?: string | null /** * FK id → process_definitions. NULLABLE BY DESIGN: an instance started * directly from the workflow Studio still projects here, it simply has no * business definition above it. */ @Property({ name: 'process_definition_id', type: 'uuid', nullable: true }) processDefinitionId?: string | null /** Denormalized workflow identity, so history survives a definition edit. */ @Property({ name: 'workflow_id', type: 'varchar', length: 200, nullable: true }) workflowId?: string | null @Property({ name: 'workflow_version', type: 'varchar', length: 50, nullable: true }) workflowVersion?: string | null /** * WHICH declared trigger started it: `{ kind: 'schedule' }`, `{ kind: 'event', * ref: }`, `{ kind: 'manual', ref: }`. The INVOKER * identity — provenance only, NEVER an ACL identity. The execution identity is * the bound workflow definition's own least-privilege principal. */ @Property({ name: 'triggered_by', type: 'jsonb', nullable: true }) triggeredBy?: ProcessRunTriggeredBy | null /** Business-execution idempotency key; unique per (org, definition) — see the index above. */ @Property({ name: 'idempotency_key', type: 'varchar', length: 200, nullable: true }) idempotencyKey?: string | null /** The resolved start input (defaults merged under the caller's); encrypted (encryption.ts). */ @Property({ name: 'input', type: 'jsonb', nullable: true }) input?: unknown | null /** Correlates to the triggering business record for cross-module launches. */ @Property({ name: 'source_entity_type', type: 'varchar', length: 100, nullable: true }) sourceEntityType?: string | null @Property({ name: 'source_entity_id', type: 'uuid', nullable: true }) sourceEntityId?: string | null // ── Subject (the business record this process is about) ──────────────────── /** e.g. 'Motor' — TYPE column + filter. Plaintext: must be SQL-queryable. */ @Property({ name: 'subject_type', type: 'varchar', length: 100, nullable: true }) subjectType?: string | null /** Business record id (opaque; FK by value only). */ @Property({ name: 'subject_id', type: 'varchar', length: 200, nullable: true }) subjectId?: string | null /** e.g. 'CASE-2026-04417' — low-sensitivity ref kept plaintext for `q` search. */ @Property({ name: 'subject_label', type: 'varchar', length: 200, nullable: true }) subjectLabel?: string | null /** * Free-text, person-readable subject — encrypted (encryption.ts → * `agent_orchestrator:process_instance`). Never SQL-filtered. */ @Property({ name: 'subject_title', type: 'varchar', length: 300, nullable: true }) subjectTitle?: string | null /** Claim value in minor units — High-value filter / value sort. Plaintext by design. Backed by a `bigint` column; hydrate as `number`, not MikroORM's default JS `bigint`. */ @Property({ name: 'subject_value_minor', type: new BigIntType('number'), nullable: true }) subjectValueMinor?: number | null /** Fraud signal — Fraud-flagged filter. Plaintext by design. */ @Property({ name: 'subject_fraud', type: 'boolean', nullable: true }) subjectFraud?: boolean | null /** Non-filterable display extras only (never queried in SQL). Zod-validated. */ @Property({ name: 'subject_facets', type: 'jsonb', nullable: true }) subjectFacets?: unknown | null // ── Derived display + aggregates ──────────────────────────────────────────── @Property({ name: 'status', type: 'varchar', length: 30, default: 'running' }) status: ProcessInstanceStatus = 'running' @Property({ name: 'current_stage', type: 'varchar', length: 100, nullable: true }) currentStage?: string | null /** * The business milestones this execution has reached, in emission order * (`ProcessMilestoneReached[]`). Appended from the workflow's * `milestone_reached` events, idempotent per key — the business narrative, * carrying no step ids at all. */ @Property({ name: 'milestones_reached', type: 'jsonb', nullable: true, default: '[]' }) milestonesReached?: ProcessMilestoneReached[] | null /** Distinct agent ids that have run under this execution (AGENTS column). */ @Property({ name: 'agent_ids', type: 'jsonb', nullable: true }) agentIds?: string[] | null /** Backed by a `bigint` column; hydrate as `number`, not MikroORM's default JS `bigint`. */ @Property({ name: 'cost_minor', type: new BigIntType('number'), nullable: true }) costMinor?: number | null @Property({ name: 'currency', type: 'varchar', length: 3, nullable: true }) currency?: string | null @Property({ name: 'run_count', type: 'integer', default: 0 }) runCount: number = 0 @Property({ name: 'pending_proposal_count', type: 'integer', default: 0 }) pendingProposalCount: number = 0 // ── Outcome (what the BUSINESS execution produced) ────────────────────────── /** * Written when the workflow instance terminates, from the outcome the * terminating source DECLARED under its context `outcome` key. Nothing derives * one. * * Nullable BY DECISION, not by omission: a research or monitoring process * produces nothing and stays a valid completion, so an absent outcome is * NEVER a missing write. * * FK-id + snapshot per `packages/core/AGENTS.md` § Cross-Module Coupling — * `outcome_label` keeps the reference readable when the module that owns the * record is absent, and this is NEVER a cross-module ORM relation. Read * through `lib/tasks/outcome.ts`, never by touching the columns by hand. * Plaintext, like `subject_label`: a record reference, not free text. */ @Property({ name: 'outcome_type', type: 'varchar', length: 150, nullable: true }) outcomeType?: string | null @Property({ name: 'outcome_id', type: 'varchar', length: 200, nullable: true }) outcomeId?: string | null @Property({ name: 'outcome_label', type: 'varchar', length: 200, nullable: true }) outcomeLabel?: string | null /** May echo malformed input on validation failure; encrypted (encryption.ts). */ @Property({ name: 'failure_reason', type: 'text', nullable: true }) failureReason?: string | null // ── Routing / SLA (mirrored from workflows for fast filtering) ────────────── @Property({ name: 'assignee_user_id', type: 'uuid', nullable: true }) assigneeUserId?: string | null @Property({ name: 'team_id', type: 'uuid', nullable: true }) teamId?: string | null /** When the execution entered a human-waiting state (Stuck >24h filter). */ @Property({ name: 'waiting_since', type: Date, nullable: true }) waitingSince?: Date | null /** When the execution was entered (AGE column). */ @Property({ name: 'opened_at', type: Date }) openedAt!: Date @Property({ name: 'completed_at', type: Date, nullable: true }) completedAt?: Date | null @Property({ name: 'last_activity_at', type: Date, onCreate: () => new Date() }) lastActivityAt: Date = new Date() @Property({ name: 'created_at', type: Date, onCreate: () => new Date() }) createdAt: Date = new Date() @Property({ name: 'updated_at', type: Date, onCreate: () => new Date(), onUpdate: () => new Date() }) updatedAt: Date = new Date() @Property({ name: 'deleted_at', type: Date, nullable: true }) deletedAt?: Date | null } /** * Per-(tenant, organization) presentation settings for an agent DEFINITION. * Agent definitions themselves are code/file-authored (see `defineAgent`), so * they are global and carry no per-tenant state — this table holds the tenant's * editable overrides for how an agent is presented in the cockpit. Today it * carries a single `icon` (a lucide icon name from `data/agentIcons.ts`) that * replaces the auto-generated initials avatar across the agent presentation * surfaces (agents list, overview "Agent trust" card, agent detail). Seeded * with sensible defaults in `setup.ts` → `seedDefaults`, idempotently. * * Editable → carries `updated_at` for optimistic locking. Keyed by the agent * definition id (a string like `deals.health_check`), referenced by id only — * NOT an ORM relation — per the cross-module decoupling rule. */ @Entity({ tableName: 'agent_settings' }) @Index({ name: 'agent_settings_tenant_org_idx', properties: ['tenantId', 'organizationId'] }) @Unique({ name: 'agent_settings_org_agent_uq', properties: ['tenantId', 'organizationId', 'agentId'] }) export class AgentSetting { [OptionalProps]?: 'icon' | 'tags' | 'createdAt' | 'updatedAt' @PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' }) id!: string @Property({ name: 'tenant_id', type: 'uuid' }) tenantId!: string @Property({ name: 'organization_id', type: 'uuid' }) organizationId!: string /** Agent DEFINITION id (e.g. `deals.health_check`). Not an FK — decoupled. */ @Property({ name: 'agent_id', type: 'varchar', length: 100 }) agentId!: string /** Lucide icon name from `AGENT_ICON_NAMES` (data/agentIcons.ts). Null = fall back to type glyph / initials. */ @Property({ name: 'icon', type: 'varchar', length: 64, nullable: true }) icon?: string | null /** * Free-form operator labels, normalized and deduped on write. Because the row * is keyed by agent id and not by an FK, tags survive an agent that is not in * the live registry (module uninstalled, agent renamed or turned off) and come * back with it. */ @Property({ name: 'tags', type: 'jsonb', nullable: true }) tags?: string[] | null @Property({ name: 'created_at', type: Date, onCreate: () => new Date() }) createdAt: Date = new Date() @Property({ name: 'updated_at', type: Date, onCreate: () => new Date(), onUpdate: () => new Date() }) updatedAt: Date = new Date() } // ── File plane: agent-produced artifacts (attachments-in / artifacts-out) ───── /** Where a captured artifact came from. `tool_output` is reserved for a future tool-file channel. */ export type AgentRunArtifactSource = 'agent_output' | 'tool_output' /** * One file an OpenCode file-agent produced in a run (scanned from the per-run * sandbox `out/` dir, hashed, and uploaded encrypted to `storage-s3`). Append-only * (immutable after capture, so no `updated_at`); keeps `deleted_at` for DSAR/erasure. * The file BYTES live in `storage-s3` (referenced by `storageKey`, encrypted at rest); * this row is inert metadata until an `attachments.attach_artifact` proposal is * approved and the effector materializes a durable `Attachment` (`promotedAttachmentId`). * Referenced by FK ids only (`runId`, `promotedAttachmentId`) — never an ORM relation. */ @Entity({ tableName: 'agent_run_artifacts' }) @Index({ name: 'agent_run_artifacts_tenant_org_idx', properties: ['tenantId', 'organizationId'] }) @Index({ name: 'agent_run_artifacts_run_idx', properties: ['organizationId', 'runId'] }) @Unique({ name: 'agent_run_artifacts_run_sha_uq', properties: ['runId', 'sha256', 'fileName'] }) export class AgentRunArtifact { [OptionalProps]?: 'source' | 'caption' | 'promotedAttachmentId' | 'createdAt' | 'deletedAt' @PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' }) id!: string @Property({ name: 'tenant_id', type: 'uuid' }) tenantId!: string @Property({ name: 'organization_id', type: 'uuid' }) organizationId!: string /** FK id → agent_runs; NOT an ORM relation. */ @Property({ name: 'run_id', type: 'uuid' }) runId!: string /** Sanitized basename produced by the agent (no path segments). Non-sensitive metadata. */ @Property({ name: 'file_name', type: 'varchar', length: 255 }) fileName!: string @Property({ name: 'mime_type', type: 'varchar', length: 150 }) mimeType!: string @Property({ name: 'file_size', type: 'integer' }) fileSize!: number @Property({ name: 'sha256', type: 'varchar', length: 64 }) sha256!: string /** storage-s3 object key; bytes encrypted at rest. */ @Property({ name: 'storage_key', type: 'varchar', length: 500 }) storageKey!: string /** Agent-supplied description; encrypted (encryption.ts → `agent_orchestrator:agent_run_artifact`). */ @Property({ name: 'caption', type: 'text', nullable: true }) caption?: string | null @Property({ name: 'source', type: 'varchar', length: 20, default: 'agent_output' }) source: AgentRunArtifactSource = 'agent_output' /** Set when an `attachments.attach_artifact` proposal is approved and the effector runs. */ @Property({ name: 'promoted_attachment_id', type: 'uuid', nullable: true }) promotedAttachmentId?: string | null @Property({ name: 'created_at', type: Date, onCreate: () => new Date() }) createdAt: Date = new Date() @Property({ name: 'deleted_at', type: Date, nullable: true }) deletedAt?: Date | null } // ── Eval plane: suite runs and case runs ──────────────────────────────────── export type EvalSuiteTrigger = 'manual' | 'ci' | 'scheduled' export type EvalSuiteStatus = 'queued' | 'running' | 'completed' | 'failed' | 'cancelled' export type EvalSuiteOutcome = 'passed' | 'failed' | 'advisory' /** * One execution of an evaluation over a set of cases. Append-only (no * `updated_at`/`deleted_at`) and retained >=6 years: gate-run summaries are legal * records, not CI-log ephemera. * * `releaseId` is nullable so ONE entity serves both planes — an ad-hoc workbench * run has no release and pins no dataset snapshot, while a CI gate run has both. * Other modules referenced by FK id only. */ @Entity({ tableName: 'agent_eval_suite_runs' }) @Index({ name: 'agent_eval_suite_runs_tenant_org_idx', properties: ['tenantId', 'organizationId'] }) @Index({ name: 'agent_eval_suite_runs_agent_idx', properties: ['organizationId', 'agentDefinitionId', 'createdAt'] }) @Index({ name: 'agent_eval_suite_runs_release_idx', properties: ['releaseId', 'createdAt'] }) export class AgentEvalSuiteRun { [OptionalProps]?: | 'releaseId' | 'outcome' | 'evalSetVersion' | 'passScore' | 'scoreVariance' | 'safetyRegressions' | 'baselineSuiteRunId' | 'summary' | 'triggeredBy' | 'startedAt' | 'finishedAt' | 'errorCount' | 'createdAt' @PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' }) id!: string @Property({ name: 'tenant_id', type: 'uuid' }) tenantId!: string @Property({ name: 'organization_id', type: 'uuid' }) organizationId!: string /** The subject under test — the only field both planes always have. */ @Property({ name: 'agent_definition_id', type: 'varchar', length: 100 }) agentDefinitionId!: string /** FK id -> agent_releases (lifecycle spec). Null for ad-hoc workbench runs. */ @Property({ name: 'release_id', type: 'uuid', nullable: true }) releaseId?: string | null @Property({ name: 'trigger', type: 'varchar', length: 20 }) trigger!: EvalSuiteTrigger @Property({ name: 'status', type: 'varchar', length: 20, default: 'queued' }) status: EvalSuiteStatus = 'queued' @Property({ name: 'outcome', type: 'varchar', length: 12, nullable: true }) outcome?: EvalSuiteOutcome | null /** Records the gate policy in force, so a stored result is self-describing. */ @Property({ name: 'judge_may_gate', type: 'boolean' }) judgeMayGate!: boolean @Property({ name: 'repeat_count', type: 'integer', default: 1 }) repeatCount: number = 1 @Property({ name: 'case_count', type: 'integer' }) caseCount!: number /** Errored != failed: excluded from passScore and reported separately. */ @Property({ name: 'error_count', type: 'integer', default: 0 }) errorCount: number = 0 /** Pinned dataset snapshot; null for ad-hoc runs. */ @Property({ name: 'eval_set_version', type: 'varchar', length: 100, nullable: true }) evalSetVersion?: string | null /** Mean over non-errored, non-skipped case runs. Null when nothing was measurable. */ @Property({ name: 'pass_score', type: 'float', nullable: true }) passScore?: number | null /** Null when repeatCount = 1 — variance is only meaningful across trials. */ @Property({ name: 'score_variance', type: 'float', nullable: true }) scoreVariance?: number | null /** Assertion keys that regressed vs the baseline; non-empty => the caller blocks. */ @Property({ name: 'safety_regressions', type: 'jsonb', nullable: true }) safetyRegressions?: unknown | null @Property({ name: 'baseline_suite_run_id', type: 'uuid', nullable: true }) baselineSuiteRunId?: string | null /** Per-assertion pass/fail plus judge counts. Encrypted: may quote agent output. */ @Property({ name: 'summary', type: 'jsonb', nullable: true }) summary?: unknown | null /** `'ci'` or the invoking userId. */ @Property({ name: 'triggered_by', type: 'varchar', length: 100, nullable: true }) triggeredBy?: string | null @Property({ name: 'started_at', type: Date, nullable: true }) startedAt?: Date | null @Property({ name: 'finished_at', type: Date, nullable: true }) finishedAt?: Date | null @Property({ name: 'created_at', type: Date, onCreate: () => new Date() }) createdAt: Date = new Date() } export type EvalCaseRunStatus = 'pending' | 'running' | 'passed' | 'failed' | 'error' | 'skipped' /** * One case, executed once, inside a suite run. Append-only. * * `agentRunId` is the load-bearing link: every case run points at a REAL AgentRun * with real spans, tool calls, tokens and cost, so the existing trace inspector * works on eval runs with no new UI. It is null only when the run never started * (admission refused, agent unresolved), and such a case run produces no * AgentEvalResult rows — which is why AgentEvalResult.agent_run_id stays NOT NULL. */ @Entity({ tableName: 'agent_eval_case_runs' }) @Index({ name: 'agent_eval_case_runs_tenant_org_idx', properties: ['tenantId', 'organizationId'] }) @Index({ name: 'agent_eval_case_runs_suite_idx', properties: ['suiteRunId', 'createdAt'] }) @Index({ name: 'agent_eval_case_runs_case_idx', properties: ['evalCaseId', 'createdAt'] }) export class AgentEvalCaseRun { [OptionalProps]?: | 'agentRunId' | 'trialIndex' | 'status' | 'score' | 'passed' | 'latencyMs' | 'costMinor' | 'errorMessage' | 'createdAt' @PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' }) id!: string @Property({ name: 'tenant_id', type: 'uuid' }) tenantId!: string @Property({ name: 'organization_id', type: 'uuid' }) organizationId!: string /** FK id -> agent_eval_suite_runs. */ @Property({ name: 'suite_run_id', type: 'uuid' }) suiteRunId!: string /** FK id -> agent_eval_cases. */ @Property({ name: 'eval_case_id', type: 'uuid' }) evalCaseId!: string /** FK id -> agent_runs. Null when the run never started. */ @Property({ name: 'agent_run_id', type: 'uuid', nullable: true }) agentRunId?: string | null /** 0-based trial index when the suite runs each case more than once. */ @Property({ name: 'trial_index', type: 'integer', default: 0 }) trialIndex: number = 0 @Property({ name: 'status', type: 'varchar', length: 20, default: 'pending' }) status: EvalCaseRunStatus = 'pending' @Property({ name: 'score', type: 'float', nullable: true }) score?: number | null /** Null means SKIPPED — excluded from both score and pass aggregation. */ @Property({ name: 'passed', type: 'boolean', nullable: true }) passed?: boolean | null @Property({ name: 'latency_ms', type: 'integer', nullable: true }) latencyMs?: number | null @Property({ name: 'cost_minor', type: 'integer', nullable: true }) costMinor?: number | null /** Encrypted: a failure message can quote model output. */ @Property({ name: 'error_message', type: 'text', nullable: true }) errorMessage?: string | null @Property({ name: 'created_at', type: Date, onCreate: () => new Date() }) createdAt: Date = new Date() }