1071 lines
48 KiB
Python
1071 lines
48 KiB
Python
"""SQLAlchemy table definitions for the omnigent database."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import contextlib
|
|
import hashlib
|
|
from collections.abc import Iterator
|
|
from contextvars import ContextVar
|
|
from typing import Any
|
|
|
|
from sqlalchemy import (
|
|
BigInteger,
|
|
Boolean,
|
|
CheckConstraint,
|
|
Float,
|
|
Index,
|
|
Integer,
|
|
LargeBinary,
|
|
SmallInteger,
|
|
String,
|
|
Text,
|
|
UniqueConstraint,
|
|
false,
|
|
text,
|
|
true,
|
|
)
|
|
from sqlalchemy.dialects.mysql import BINARY as MySQLBinary
|
|
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
|
|
|
|
from omnigent.db.compression import CompressedText
|
|
|
|
# 32-byte sha256 digest column. LargeBinary → BYTEA (Postgres) / BLOB (SQLite),
|
|
# but MySQL cannot index a BLOB without a key-prefix length, so use fixed-length
|
|
# BINARY(32) there — an exact fit for the digest and fully indexable.
|
|
_CKSUM32 = LargeBinary(32).with_variant(MySQLBinary(32), "mysql")
|
|
|
|
|
|
class Base(DeclarativeBase):
|
|
"""Shared declarative base for all omnigent tables."""
|
|
|
|
|
|
# Default workspace id stamped on every row and used as the leading
|
|
# member of every composite primary key. 0 is the single-workspace /
|
|
# unassigned sentinel: with no workspace bound to the request, all rows
|
|
# live in workspace 0.
|
|
DEFAULT_WORKSPACE_ID = 0
|
|
|
|
# Ambient per-request workspace id. Stores are process-wide singletons, so
|
|
# the active workspace can't ride on the store instance — it lives here.
|
|
# OSS leaves this at the default (single-workspace 0); a multi-tenant
|
|
# deployment (e.g. universe) sets it per request from the authenticated
|
|
# context (via ``workspace_scope`` in middleware). Reads and inserts
|
|
# resolve it through ``current_workspace_id()`` so the same store code
|
|
# scopes to the caller's workspace without threading the id through every
|
|
# signature — keeping this file byte-identical across deployments.
|
|
_current_workspace_id: ContextVar[int] = ContextVar(
|
|
"omnigent_workspace_id", default=DEFAULT_WORKSPACE_ID
|
|
)
|
|
|
|
|
|
def current_workspace_id() -> int:
|
|
"""Return the workspace id bound to the active request/context.
|
|
|
|
Defaults to :data:`DEFAULT_WORKSPACE_ID` (0) — the single-workspace OSS
|
|
deployment. Multi-tenant deployments set it per request so every
|
|
primary-key lookup, filter, and insert scopes to that workspace.
|
|
"""
|
|
return _current_workspace_id.get()
|
|
|
|
|
|
@contextlib.contextmanager
|
|
def workspace_scope(workspace_id: int) -> Iterator[None]:
|
|
"""Bind *workspace_id* for the duration of the ``with`` block.
|
|
|
|
Used by multi-tenant request middleware (and tests) to scope all
|
|
store access to one workspace; resets to the prior value on exit so
|
|
nested / concurrent contexts don't leak.
|
|
"""
|
|
token = _current_workspace_id.set(workspace_id)
|
|
try:
|
|
yield
|
|
finally:
|
|
_current_workspace_id.reset(token)
|
|
|
|
|
|
AGENT_KIND_TEMPLATE = "template"
|
|
AGENT_KIND_SESSION = "session"
|
|
|
|
POLICY_SCOPE_DEFAULT = "default"
|
|
POLICY_SCOPE_SESSION = "session"
|
|
|
|
|
|
class SqlAgent(Base):
|
|
"""
|
|
SQLAlchemy model for the ``agents`` table.
|
|
|
|
Each row represents a registered agent in the system.
|
|
|
|
:param id: Unique agent identifier, e.g. ``"ag_0f1a2b3c..."``.
|
|
:param created_at: Unix epoch seconds when the agent was created.
|
|
:param name: Human-readable agent name. Registered template
|
|
agents require unique names; session-scoped copies may reuse
|
|
the same name across different sessions.
|
|
:param bundle_location: Artifact store key for the current bundle.
|
|
Content-addressed (SHA-256 hex), e.g.
|
|
``"ag_abc123/a1b2c3d4e5f6..."``.
|
|
:param version: Monotonic version counter. Starts at 1, incremented
|
|
on each update via ``PUT /api/agents/{id}``.
|
|
:param kind: ``"template"`` for server-wide registered agents;
|
|
``"session"`` for per-conversation copies.
|
|
:param description: Optional free-text description of the agent's
|
|
purpose. ``None`` when not provided.
|
|
:param updated_at: Unix epoch seconds of the last update, or
|
|
``None`` if the agent has never been updated.
|
|
"""
|
|
|
|
__tablename__ = "agents"
|
|
|
|
# Tenant partition key: Databricks workspace id owning this row (0 = default). Part of the PK.
|
|
workspace_id: Mapped[int] = mapped_column(
|
|
BigInteger,
|
|
primary_key=True,
|
|
nullable=False,
|
|
server_default="0",
|
|
default=current_workspace_id,
|
|
)
|
|
id: Mapped[str] = mapped_column(String(64), primary_key=True)
|
|
created_at: Mapped[int] = mapped_column(Integer)
|
|
name: Mapped[str] = mapped_column(String(256))
|
|
bundle_location: Mapped[str] = mapped_column(String(512))
|
|
version: Mapped[int] = mapped_column(Integer, default=1)
|
|
# Enum stored as a stable int code (see omnigent.db.enum_codecs
|
|
# AGENT_KIND: template=1, session=2). The store converts to/from the
|
|
# string name at the row↔entity boundary.
|
|
kind: Mapped[int] = mapped_column(SmallInteger)
|
|
description: Mapped[str | None] = mapped_column(CompressedText, nullable=True)
|
|
updated_at: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
|
|
|
__table_args__ = (
|
|
CheckConstraint("kind IN (1, 2)", name="ck_agents_kind"),
|
|
Index("ix_agents_created_at", "workspace_id", "created_at", "id"),
|
|
# Template agents have unique names; session-scoped agents (kind=2)
|
|
# may reuse the same name. That "unique only within the template set"
|
|
# rule can't be a partial unique index (MySQL has none), so it is
|
|
# enforced in the store (SqlAlchemyAgentStore.create). This plain index
|
|
# backs the (workspace_id, name, kind) lookup that check and get_by_name
|
|
# do — kind is included so the seek skips same-named session copies
|
|
# straight to the template row.
|
|
Index("ix_agents_name", "workspace_id", "name", "kind", "id"),
|
|
)
|
|
|
|
|
|
class SqlFile(Base):
|
|
"""
|
|
SQLAlchemy model for the ``files`` table.
|
|
|
|
Each row represents an uploaded file tracked by the system.
|
|
|
|
:param id: Unique file identifier, e.g. ``"file_a1b2c3d4..."``.
|
|
:param created_at: Unix epoch seconds when the file record was
|
|
created.
|
|
:param filename: Original filename as provided by the uploader,
|
|
max 512 characters. e.g. ``"report.pdf"``.
|
|
:param bytes: Size of the file in bytes.
|
|
:param content_type: MIME type of the file, e.g.
|
|
``"application/pdf"``. ``None`` when not provided.
|
|
"""
|
|
|
|
__tablename__ = "files"
|
|
|
|
# Tenant partition key: Databricks workspace id owning this row (0 = default). Part of the PK.
|
|
workspace_id: Mapped[int] = mapped_column(
|
|
BigInteger,
|
|
primary_key=True,
|
|
nullable=False,
|
|
server_default="0",
|
|
default=current_workspace_id,
|
|
)
|
|
id: Mapped[str] = mapped_column(String(64), primary_key=True)
|
|
created_at: Mapped[int] = mapped_column(Integer)
|
|
filename: Mapped[str] = mapped_column(String(512))
|
|
bytes: Mapped[int] = mapped_column(Integer)
|
|
content_type: Mapped[str | None] = mapped_column(String(256), nullable=True)
|
|
session_id: Mapped[str | None] = mapped_column(String(64), nullable=True)
|
|
|
|
__table_args__ = (
|
|
Index("ix_files_created_at", "workspace_id", "created_at", "id"),
|
|
Index(
|
|
"ix_files_session_id_created_at",
|
|
"workspace_id",
|
|
"session_id",
|
|
"created_at",
|
|
"id",
|
|
),
|
|
)
|
|
|
|
|
|
class SqlUser(Base):
|
|
"""
|
|
SQLAlchemy model for the ``users`` table.
|
|
|
|
Each row represents a user. In header / OIDC modes, ``id`` is
|
|
the upstream identity (email or ``"local"``); the row is
|
|
upserted on first sight and ``password_hash`` stays ``NULL``.
|
|
In ``accounts`` mode, rows are created explicitly by the admin
|
|
or via invite redemption with a populated ``password_hash``.
|
|
|
|
:param id: User identifier — email in header/OIDC modes, chosen
|
|
username in accounts mode, ``"local"`` in single-user.
|
|
:param is_admin: When ``True``, the user bypasses all
|
|
permission checks. ``False`` by default.
|
|
:param password_hash: argon2id hash of the user's password.
|
|
``NULL`` for users created via header/OIDC modes (their
|
|
password is the upstream IdP's).
|
|
:param created_at: Unix epoch seconds when the row was inserted.
|
|
Populated for accounts-mode users; ``NULL`` for legacy rows
|
|
backfilled by the original permissions migration.
|
|
:param last_login_at: Unix epoch seconds of the most recent
|
|
successful ``/auth/login`` (accounts mode). ``NULL`` until
|
|
the first login.
|
|
"""
|
|
|
|
__tablename__ = "users"
|
|
|
|
# Tenant partition key: Databricks workspace id owning this row (0 = default). Part of the PK.
|
|
workspace_id: Mapped[int] = mapped_column(
|
|
BigInteger,
|
|
primary_key=True,
|
|
nullable=False,
|
|
server_default="0",
|
|
default=current_workspace_id,
|
|
)
|
|
id: Mapped[str] = mapped_column(String(128), primary_key=True)
|
|
is_admin: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default=false())
|
|
password_hash: Mapped[str | None] = mapped_column(String(256), nullable=True)
|
|
created_at: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
|
last_login_at: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
|
|
|
|
|
class SqlAccountToken(Base):
|
|
"""
|
|
SQLAlchemy model for the ``account_tokens`` table.
|
|
|
|
Backs both invite tokens (admin-issued, allow self-serve
|
|
registration) and magic-login tokens (CLI-minted, hand off a
|
|
signed-in session into the web UI). Both have the same
|
|
short-TTL single-use lifecycle, so they share one table.
|
|
|
|
:param id: Opaque random token string (43+ URL-safe base64
|
|
chars). This is the secret — the user presents it as a
|
|
query param. Stored verbatim because we need
|
|
constant-time lookup; rotation = delete + recreate.
|
|
:param kind: ``"invite"`` (anyone can redeem; creates a new
|
|
user) or ``"magic"`` (the bound ``user_id`` is signed in).
|
|
:param user_id: For ``magic``, the user the token signs in as.
|
|
For ``invite``, ``NULL`` (the username is chosen at
|
|
redemption time).
|
|
:param created_by: User id of the admin who issued an invite
|
|
(``NULL`` for magic tokens, which are self-issued).
|
|
:param created_at: Unix epoch seconds when the token was
|
|
minted. ``expires_at = created_at + ttl_seconds``.
|
|
:param expires_at: Unix epoch seconds when the token stops
|
|
being redeemable. Single-use enforcement is via
|
|
``redeemed_at``, this just bounds the window.
|
|
:param redeemed_at: Unix epoch seconds when the token was
|
|
consumed. ``NULL`` until then. After being set, the token
|
|
is dead — redeem checks this column atomically.
|
|
:param invited_is_admin: For invite tokens, whether the
|
|
resulting user should be created with admin rights. False
|
|
for magic tokens.
|
|
"""
|
|
|
|
__tablename__ = "account_tokens"
|
|
|
|
# Tenant partition key: Databricks workspace id owning this row (0 = default). Part of the PK.
|
|
workspace_id: Mapped[int] = mapped_column(
|
|
BigInteger,
|
|
primary_key=True,
|
|
nullable=False,
|
|
server_default="0",
|
|
default=current_workspace_id,
|
|
)
|
|
id: Mapped[str] = mapped_column(String(128), primary_key=True)
|
|
# Enum stored as a stable int code (see omnigent.db.enum_codecs
|
|
# ACCOUNT_TOKEN_KIND: invite=1, magic=2). The store converts to/from
|
|
# the string name at the row↔entity boundary.
|
|
kind: Mapped[int] = mapped_column(SmallInteger, nullable=False)
|
|
user_id: Mapped[str | None] = mapped_column(String(128), nullable=True)
|
|
created_by: Mapped[str | None] = mapped_column(String(128), nullable=True)
|
|
created_at: Mapped[int] = mapped_column(Integer, nullable=False)
|
|
expires_at: Mapped[int] = mapped_column(Integer, nullable=False)
|
|
redeemed_at: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
|
invited_is_admin: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default=false())
|
|
|
|
__table_args__ = (
|
|
CheckConstraint("kind IN (1, 2)", name="ck_account_tokens_kind"),
|
|
Index("ix_account_tokens_expires_at", "workspace_id", "expires_at", "id"),
|
|
)
|
|
|
|
|
|
class SqlSessionPermission(Base):
|
|
"""
|
|
SQLAlchemy model for the ``session_permissions`` table.
|
|
|
|
Junction table mapping ``(user_id, conversation_id)`` to a
|
|
numeric permission level. PK is ``(user_id, conversation_id)``
|
|
— optimized for the hot path ("list sessions I can access"
|
|
= prefix scan on ``user_id``).
|
|
|
|
The ``"__public__"`` sentinel ``user_id`` represents public
|
|
read access to a session.
|
|
|
|
:param user_id: The grantee, e.g. ``"alice@example.com"``
|
|
or ``"__public__"`` for public access.
|
|
:param conversation_id: The session being shared, e.g.
|
|
``"conv_e4f5a6b7..."``.
|
|
:param level: Numeric permission level: ``1`` = read,
|
|
``2`` = edit, ``3`` = manage. Each level subsumes the
|
|
ones below it (comparison is ``>=``).
|
|
"""
|
|
|
|
__tablename__ = "session_permissions"
|
|
|
|
# Tenant partition key: Databricks workspace id owning this row (0 = default). Part of the PK.
|
|
workspace_id: Mapped[int] = mapped_column(
|
|
BigInteger,
|
|
primary_key=True,
|
|
nullable=False,
|
|
server_default="0",
|
|
default=current_workspace_id,
|
|
)
|
|
user_id: Mapped[str] = mapped_column(
|
|
String(128),
|
|
primary_key=True,
|
|
)
|
|
conversation_id: Mapped[str] = mapped_column(
|
|
String(64),
|
|
primary_key=True,
|
|
)
|
|
level: Mapped[int] = mapped_column(Integer, nullable=False)
|
|
|
|
__table_args__ = (
|
|
CheckConstraint("level IN (1, 2, 3, 4)", name="ck_session_permissions_level"),
|
|
# Lookups by conversation (get_session_owner) filter workspace_id +
|
|
# conversation_id; user_id trails to complete the PK.
|
|
Index(
|
|
"ix_session_permissions_conversation_id",
|
|
"workspace_id",
|
|
"conversation_id",
|
|
"user_id",
|
|
),
|
|
)
|
|
|
|
|
|
class SqlConversation(Base):
|
|
"""
|
|
SQLAlchemy model for the ``conversations`` table.
|
|
|
|
Each row represents a conversation thread that contains one or
|
|
more conversation items.
|
|
|
|
:param id: Unique conversation identifier, e.g.
|
|
``"conv_e4f5a6b7..."``.
|
|
:param created_at: Unix epoch seconds when the conversation was
|
|
created.
|
|
:param updated_at: Unix epoch seconds when the conversation was
|
|
last updated (item append, title change, etc.).
|
|
:param title: Human-readable title; empty string when untitled.
|
|
:param kind: Conversation type. ``"default"`` for user-initiated,
|
|
``"sub_agent"`` for sub-agent execution conversations.
|
|
:param parent_conversation_id: For Phase 4 named sub-agents,
|
|
points at the parent conversation. ``None`` for top-level
|
|
conversations. ``ON DELETE CASCADE`` so removing a parent
|
|
cleans up the entire sub-tree.
|
|
:param root_conversation_id: Id of the root (top-level)
|
|
conversation in the spawn tree. Equal to ``id`` for
|
|
top-level conversations. Indexed so ``sys_session_get_history`` /
|
|
``sys_session_close`` can verify that a target
|
|
``conversation_id`` lives in the caller's tree in O(1) —
|
|
any agent in the tree can address any other by
|
|
``conversation_id``. ``ON DELETE CASCADE`` to keep it
|
|
consistent with ``parent_conversation_id`` when a root is
|
|
deleted.
|
|
:param agent_id: Foreign key to the agent bound to this
|
|
conversation at creation time. ``None`` for legacy
|
|
conversations created without an agent binding (these are
|
|
excluded from ``GET /v1/sessions`` results).
|
|
:param runner_id: Runner the conversation is pinned to (hard
|
|
affinity per ``designs/RUNNER.md`` §5). ``None`` until the
|
|
first dispatch claims a runner; thereafter every subsequent
|
|
dispatch routes to this runner while it is online (or fails
|
|
with ``runner_unavailable`` if it isn't). No FK because
|
|
runner records are not persisted in v1 — the registry is
|
|
purely in-memory.
|
|
:param external_session_id: Runtime-native session id this
|
|
conversation wraps, e.g. Claude Code's session uuid for
|
|
``omnigent claude`` sessions. ``None`` for regular
|
|
AP-only conversations. Populated by the wrapper bridge
|
|
from the underlying runtime and used by ``--resume`` to
|
|
recover the external session's prior transcript. Generic
|
|
across runtimes — at most one external session per
|
|
conversation. No FK because the id is generated externally
|
|
(by Claude Code, Codex, Pi, etc.) and is not tracked in
|
|
any AP-side table.
|
|
:param workspace: Absolute path on disk where the runner should
|
|
start, e.g. ``"/Users/corey/universe/src/foo"``. Required
|
|
when ``host_id`` is set (enforced by check constraint
|
|
``ck_conversations_workspace_required_for_host``); optional
|
|
for CLI-launched sessions that record their starting cwd
|
|
for display. Stored as the canonicalized realpath returned
|
|
by ``host.stat`` at session-create time; runtime symlinks
|
|
are pre-resolved so the boundary check on the agent's
|
|
``os_env.cwd`` cannot be smuggled past via a symlink.
|
|
Immutable after creation —
|
|
designs/SESSION_WORKSPACE_SELECTION.md. When a git worktree
|
|
was created for the session, this is the worktree directory
|
|
path rather than the picked source repo.
|
|
:param git_branch: Git branch checked out in the session's
|
|
worktree, e.g. ``"feature/login"``. Set only when the
|
|
session was created with a server-created git worktree;
|
|
``None`` otherwise. ``git_branch IS NOT NULL`` gates worktree
|
|
cleanup on session delete. See
|
|
designs/SESSION_GIT_WORKTREE.md.
|
|
:param archived: Whether the session is archived. Archived
|
|
sessions are hidden from the default ``GET /v1/sessions``
|
|
listing (and the sidebar); the listing returns them only when
|
|
``include_archived=True``. ``False`` for normal sessions.
|
|
Reversible via ``PATCH /v1/sessions/{id}``.
|
|
"""
|
|
|
|
__tablename__ = "conversations"
|
|
|
|
# Tenant partition key: Databricks workspace id owning this row (0 = default). Part of the PK.
|
|
workspace_id: Mapped[int] = mapped_column(
|
|
BigInteger,
|
|
primary_key=True,
|
|
nullable=False,
|
|
server_default="0",
|
|
default=current_workspace_id,
|
|
)
|
|
id: Mapped[str] = mapped_column(String(64), primary_key=True)
|
|
created_at: Mapped[int] = mapped_column(Integer)
|
|
updated_at: Mapped[int] = mapped_column(Integer)
|
|
title: Mapped[str] = mapped_column(String(768), nullable=False, server_default="")
|
|
# Enum stored as a stable int code (see omnigent.db.enum_codecs
|
|
# CONVERSATION_KIND: default=1, sub_agent=2). The store converts to/from
|
|
# the string name at the row↔entity boundary.
|
|
kind: Mapped[int] = mapped_column(SmallInteger, default=1)
|
|
parent_conversation_id: Mapped[str | None] = mapped_column(
|
|
String(64),
|
|
nullable=True,
|
|
)
|
|
root_conversation_id: Mapped[str] = mapped_column(
|
|
String(64),
|
|
nullable=False,
|
|
)
|
|
agent_id: Mapped[str | None] = mapped_column(
|
|
String(64),
|
|
nullable=True,
|
|
)
|
|
runner_id: Mapped[str | None] = mapped_column(String(64), nullable=True)
|
|
# Host that launched (or should launch) the runner for this
|
|
# session. Set when a session is created via the Web UI on a
|
|
# specific host. No FK: host records are managed outside this
|
|
# table; deletion is handled explicitly by the application.
|
|
host_id: Mapped[str | None] = mapped_column(
|
|
String(64),
|
|
nullable=True,
|
|
)
|
|
# Per-session reasoning-effort hint, e.g. "high". Nullable;
|
|
# None means use the agent default.
|
|
reasoning_effort: Mapped[str | None] = mapped_column(String(32), nullable=True)
|
|
# Per-session LLM model override, e.g. "claude-opus-4-7". Nullable;
|
|
# None means use the agent default from the spec.
|
|
model_override: Mapped[str | None] = mapped_column(String(128), nullable=True)
|
|
# Per-session cost-control switch: "on" | "off". Nullable; None
|
|
# means use the spec default (see entities.Conversation).
|
|
cost_control_mode_override: Mapped[str | None] = mapped_column(String(8), nullable=True)
|
|
# Per-session brain-harness override, e.g. "pi". Nullable; None
|
|
# means use the spec's executor.config.harness (see entities.Conversation).
|
|
harness_override: Mapped[str | None] = mapped_column(String(64), nullable=True)
|
|
# Sub-agent type name within the parent's spec tree, e.g.
|
|
# "summarizer". The runner uses this to load the sub-agent's
|
|
# AgentSpec instead of the parent's. Replaces task.agent_name
|
|
# from the removed task store. None for top-level sessions.
|
|
sub_agent_name: Mapped[str | None] = mapped_column(String(128), nullable=True)
|
|
# Monotonic allocator for the next item position in this conversation.
|
|
# append() reads and advances this instead of scanning
|
|
# MAX(SqlConversationItem.position) on every write, making position
|
|
# assignment O(1) and collision-free under the conversation lock. New rows
|
|
# start at 0 (column default); NULL marks a row created before this column
|
|
# existed, which append() backfills via a one-time scan on its next write.
|
|
next_position: Mapped[int | None] = mapped_column(Integer, nullable=True, default=0)
|
|
external_session_id: Mapped[str | None] = mapped_column(String(128), nullable=True)
|
|
# JSON-serialized mutable per-conversation key/value store
|
|
# used by policy callables to accumulate state across turns.
|
|
# NULL when no policy has written state yet; empty JSON object
|
|
# "{}" is equivalent. Stored as Text (not a native JSON column)
|
|
# for SQLite compatibility.
|
|
session_state: Mapped[str | None] = mapped_column(CompressedText, nullable=True)
|
|
# JSON-serialized cumulative LLM token usage for policy
|
|
# callables. Shape: {"input_tokens": N, "output_tokens": M,
|
|
# "total_tokens": T, "cache_read_input_tokens": C1,
|
|
# "cache_creation_input_tokens": C2, "total_cost_usd": X}.
|
|
# NULL when no LLM calls have been recorded yet.
|
|
session_usage: Mapped[str | None] = mapped_column(CompressedText, nullable=True)
|
|
# Pass-through CLI args for a native terminal wrapper (claude /
|
|
# codex), JSON-encoded list of strings, e.g.
|
|
# '["--dangerously-skip-permissions"]'. NULL for non-native
|
|
# sessions. The runner reconstructs the terminal launch command
|
|
# from these plus the harness binary; the command itself and all
|
|
# bridge / AP-URL / auth wiring are runner-owned and never stored
|
|
# here. A flat list (not a dict) is deliberate: there is no key for
|
|
# a user to smuggle internal wiring through. See
|
|
# designs/NATIVE_RUNNER_SERVER_LAUNCH.md.
|
|
terminal_launch_args: Mapped[str | None] = mapped_column(CompressedText, nullable=True)
|
|
# Absolute path on the host where the runner cd's. Required
|
|
# when host_id is set; CHECK constraint below. When a git worktree
|
|
# was created for the session, this is the worktree directory path.
|
|
workspace: Mapped[str | None] = mapped_column(String(2048), nullable=True)
|
|
# Git branch checked out in the session's worktree, e.g.
|
|
# "feature/login". Set only when the session was created with a
|
|
# server-created git worktree; None otherwise. Gates worktree
|
|
# cleanup on delete. See designs/SESSION_GIT_WORKTREE.md.
|
|
git_branch: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
|
# Whether the session is archived (hidden from the default
|
|
# /v1/sessions listing and the sidebar). False for normal
|
|
# sessions; server_default false backfills existing rows on the
|
|
# migration that adds this column. Low-cardinality, so no index —
|
|
# the listing's accessible_by subquery is the selective filter.
|
|
archived: Mapped[bool] = mapped_column(
|
|
Boolean, nullable=False, default=False, server_default=false()
|
|
)
|
|
|
|
__table_args__ = (
|
|
CheckConstraint("kind IN (1, 2)", name="ck_conversations_kind"),
|
|
CheckConstraint(
|
|
"host_id IS NULL OR workspace IS NOT NULL",
|
|
name="ck_conversations_workspace_required_for_host",
|
|
),
|
|
Index("ix_conversations_created_at", "workspace_id", "created_at", "id"),
|
|
Index("ix_conversations_updated_at", "workspace_id", "updated_at", "id"),
|
|
Index("ix_conversations_kind", "workspace_id", "kind", "id"),
|
|
# Agent lookups: find the conversation(s) that own a given agent.
|
|
Index("ix_conversations_agent_id", "workspace_id", "agent_id", "id"),
|
|
Index(
|
|
"ix_conversations_root_conversation_id",
|
|
"workspace_id",
|
|
"root_conversation_id",
|
|
"id",
|
|
),
|
|
# Reconnect/relaunch reconciliation looks up a runner's session(s)
|
|
# by runner_id (list_conversations_by_runner_id) on every runner
|
|
# reconnect; index it to avoid a full scan.
|
|
Index("ix_conversations_runner_id", "workspace_id", "runner_id", "id"),
|
|
# Unique index on (parent_conversation_id, title) prevents two
|
|
# same-named children under the same parent (G36 race protection at
|
|
# the DB layer). Top-level conversations (NULL parent) are exempt
|
|
# automatically: NULLs are distinct in a unique index, so no WHERE
|
|
# predicate is needed — keeping it a plain index MySQL can build.
|
|
Index(
|
|
"ix_conversations_parent_title_unique",
|
|
"workspace_id",
|
|
"parent_conversation_id",
|
|
"title",
|
|
unique=True,
|
|
mysql_length={"title": 512},
|
|
),
|
|
# Composite index for child-session listing
|
|
# (list_conversations(kind="sub_agent", parent_conversation_id=...)).
|
|
# Non-unique, so no scoping predicate is required; it simply indexes
|
|
# every parented row rather than only the sub-agent ones.
|
|
Index(
|
|
"idx_conversations_parent",
|
|
"workspace_id",
|
|
"parent_conversation_id",
|
|
text("created_at DESC"),
|
|
text("id DESC"),
|
|
),
|
|
)
|
|
|
|
|
|
class SqlConversationItem(Base):
|
|
"""
|
|
SQLAlchemy model for the ``conversation_items`` table.
|
|
|
|
Each row represents a single item (message, function call,
|
|
function call output, or reasoning block) within a conversation.
|
|
|
|
:param id: Unique item identifier with a type-based prefix,
|
|
e.g. ``"msg_a1b2c3..."``, ``"fc_d4e5f6..."``.
|
|
:param conversation_id: Foreign key to
|
|
:class:`SqlConversation.id`. Cascades on delete.
|
|
:param response_id: The task/response ID this item belongs to,
|
|
e.g. ``"resp_d8e9f0a1..."``.
|
|
:param created_at: Unix epoch seconds when the item was created.
|
|
:param status: Item status string. Defaults to ``"completed"``.
|
|
:param position: Zero-based ordering index within the
|
|
conversation. Used for deterministic item ordering.
|
|
:param type: Item type discriminator, one of ``"message"``,
|
|
``"function_call"``, ``"function_call_output"``,
|
|
``"reasoning"``.
|
|
:param data: JSON-serialized item payload. Structure varies by
|
|
``type``.
|
|
:param search_text: Plain-text extraction of ``data`` used for
|
|
full-text search indexing.
|
|
:param created_by: Identity of the human actor who authored the
|
|
item, or ``None`` for agent/tool/system items and single-user
|
|
mode. Mirrors :class:`SqlComment.created_by`.
|
|
"""
|
|
|
|
__tablename__ = "conversation_items"
|
|
|
|
# Tenant partition key: Databricks workspace id owning this row (0 = default). Part of the PK.
|
|
workspace_id: Mapped[int] = mapped_column(
|
|
BigInteger,
|
|
primary_key=True,
|
|
nullable=False,
|
|
server_default="0",
|
|
default=current_workspace_id,
|
|
)
|
|
# conversation_id leads id in the PK so a conversation's items stay
|
|
# contiguous for the per-conversation prefix scans that dominate reads.
|
|
conversation_id: Mapped[str] = mapped_column(
|
|
String(64),
|
|
primary_key=True,
|
|
)
|
|
id: Mapped[str] = mapped_column(String(64), primary_key=True)
|
|
response_id: Mapped[str] = mapped_column(String(64))
|
|
created_at: Mapped[int] = mapped_column(Integer)
|
|
# Enum stored as a stable int code (see omnigent.db.enum_codecs
|
|
# ITEM_STATUS: completed=1). Only "completed" is written today, but the
|
|
# CHECK admits the wider OpenAI-style status vocabulary reserved there.
|
|
status: Mapped[int] = mapped_column(SmallInteger, default=1)
|
|
position: Mapped[int] = mapped_column(Integer)
|
|
# Enum stored as a stable int code (see omnigent.db.enum_codecs
|
|
# ITEM_TYPE). The store converts to/from the string name at the
|
|
# row↔entity boundary.
|
|
type: Mapped[int] = mapped_column(SmallInteger)
|
|
data: Mapped[str] = mapped_column(Text)
|
|
search_text: Mapped[str] = mapped_column(Text)
|
|
created_by: Mapped[str | None] = mapped_column(String(128), nullable=True)
|
|
|
|
__table_args__ = (
|
|
Index(
|
|
"ix_conversation_items_conversation_id_position",
|
|
"workspace_id",
|
|
"conversation_id",
|
|
"position",
|
|
unique=True,
|
|
),
|
|
# Fork-truncation looks up by workspace_id + conversation_id +
|
|
# response_id; id trails to complete the PK.
|
|
Index(
|
|
"ix_conversation_items_response_id",
|
|
"workspace_id",
|
|
"conversation_id",
|
|
"response_id",
|
|
"id",
|
|
),
|
|
CheckConstraint(
|
|
"type IN (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11)",
|
|
name="ck_conversation_items_type",
|
|
),
|
|
CheckConstraint("status IN (1, 2, 3, 4)", name="ck_conversation_items_status"),
|
|
)
|
|
|
|
|
|
# Width of the ``conversation_labels.value`` column. Exported so the store
|
|
# (and the session-status error-label path) can clamp values to fit instead
|
|
# of letting an over-length write raise ``DataError`` on PostgreSQL.
|
|
LABEL_VALUE_MAX_LEN = 256
|
|
|
|
|
|
class SqlConversationLabel(Base):
|
|
"""
|
|
SQLAlchemy model for the ``conversation_labels`` table.
|
|
|
|
One row per (conversation, label-key) pair. Labels live in
|
|
a dedicated table rather than a JSON column on
|
|
``conversations`` so per-key UPDATEs are atomic without
|
|
read-modify-write (see POLICIES.md §6). The table is keyed
|
|
only by ``conversation_id`` + ``key``, so it is untouched
|
|
by compaction (which rewrites ``conversation_items``) —
|
|
labels set turn 3 still exist turn 20 even after the
|
|
earlier turns have been folded into a summary.
|
|
|
|
:param conversation_id: The conversation this label belongs
|
|
to. Composite PK member. Deleted with the conversation
|
|
via ``ON DELETE CASCADE``.
|
|
:param key: The label key, e.g. ``"integrity"``,
|
|
``"sensitivity"``. Composite PK member.
|
|
:param value: The label value as a string, e.g. ``"0"``,
|
|
``"confidential"``. All label values are string-typed
|
|
regardless of what the YAML author wrote — the parser
|
|
coerces scalar / list values during spec load
|
|
(POLICIES.md §14).
|
|
:param updated_at: Unix epoch seconds of the last write.
|
|
Single timestamp for each row; on UPSERT the row's
|
|
timestamp is refreshed even when the value is
|
|
unchanged (matches omnigent parity and keeps
|
|
debugging timelines accurate).
|
|
"""
|
|
|
|
__tablename__ = "conversation_labels"
|
|
|
|
# Tenant partition key: Databricks workspace id owning this row (0 = default). Part of the PK.
|
|
workspace_id: Mapped[int] = mapped_column(
|
|
BigInteger,
|
|
primary_key=True,
|
|
nullable=False,
|
|
server_default="0",
|
|
default=current_workspace_id,
|
|
)
|
|
conversation_id: Mapped[str] = mapped_column(
|
|
String(64),
|
|
primary_key=True,
|
|
)
|
|
key: Mapped[str] = mapped_column(String(128), primary_key=True)
|
|
value: Mapped[str] = mapped_column(String(LABEL_VALUE_MAX_LEN))
|
|
updated_at: Mapped[int] = mapped_column(Integer)
|
|
|
|
|
|
class SqlComment(Base):
|
|
"""SQLAlchemy model for the ``comments`` table.
|
|
|
|
Stores per-review comments associated with a conversation.
|
|
Each comment is anchored to a character range in the file expressed
|
|
as absolute document-level offsets. Comments survive server restarts
|
|
and are cleaned up when the owning conversation is deleted.
|
|
|
|
:param id: UUID primary key, e.g. ``"a1b2c3d4-..."``.
|
|
:param conversation_id: The conversation this comment belongs to.
|
|
:param path: File path relative to the workspace root,
|
|
e.g. ``"src/App.tsx"``.
|
|
:param start_index: 0-based absolute character offset (inclusive)
|
|
within the file where the anchor range begins.
|
|
:param end_index: 0-based absolute character offset (exclusive)
|
|
within the file where the anchor range ends.
|
|
:param body: The comment text.
|
|
:param status: One of ``"draft"``, ``"addressed"``.
|
|
:param created_at: Unix epoch seconds at row creation.
|
|
:param updated_at: Unix epoch **microseconds** of the last
|
|
body/status mutation; set at creation for never-edited
|
|
comments. Feeds the per-session comments fingerprint surfaced
|
|
on ``GET /v1/sessions`` so clients can detect comment changes;
|
|
microsecond precision keeps back-to-back mutations within one
|
|
second distinguishable while remaining an exact integer in
|
|
JavaScript. ``BigInteger`` because epoch-µs overflows a
|
|
32-bit column on PostgreSQL.
|
|
:param anchor_content: Plain-text snapshot of the selected range at
|
|
comment creation time. Used to re-anchor the comment (e.g. via
|
|
content search) when the file is subsequently edited.
|
|
``NULL`` for legacy comments created before anchor support.
|
|
:param created_by: Email of the user who created this comment,
|
|
e.g. ``"alice@example.com"``. ``NULL`` for legacy comments or
|
|
comments created in single-user mode.
|
|
"""
|
|
|
|
__tablename__ = "comments"
|
|
|
|
# Tenant partition key: Databricks workspace id owning this row (0 = default). Part of the PK.
|
|
workspace_id: Mapped[int] = mapped_column(
|
|
BigInteger,
|
|
primary_key=True,
|
|
nullable=False,
|
|
server_default="0",
|
|
default=current_workspace_id,
|
|
)
|
|
id: Mapped[str] = mapped_column(String(64), primary_key=True)
|
|
conversation_id: Mapped[str] = mapped_column(String(64))
|
|
path: Mapped[str] = mapped_column(String(4096))
|
|
start_index: Mapped[int] = mapped_column(Integer)
|
|
end_index: Mapped[int] = mapped_column(Integer)
|
|
body: Mapped[str] = mapped_column(CompressedText)
|
|
# Enum stored as a stable int code (see omnigent.db.enum_codecs
|
|
# COMMENT_STATUS: draft=1, addressed=2).
|
|
status: Mapped[int] = mapped_column(SmallInteger)
|
|
created_at: Mapped[int] = mapped_column(Integer)
|
|
updated_at: Mapped[int] = mapped_column(BigInteger)
|
|
anchor_content: Mapped[str | None] = mapped_column(CompressedText, nullable=True)
|
|
created_by: Mapped[str | None] = mapped_column(String(128), nullable=True)
|
|
|
|
__table_args__ = (
|
|
CheckConstraint("status IN (1, 2)", name="ck_comments_status"),
|
|
# Serves list_for_conversation: WHERE workspace_id + conversation_id
|
|
# ORDER BY created_at, id. Folds created_at in (over a bare
|
|
# conversation_id index) so the sort is index-ordered; trails id to
|
|
# complete the PK.
|
|
Index(
|
|
"ix_comments_conversation_id",
|
|
"workspace_id",
|
|
"conversation_id",
|
|
"created_at",
|
|
"id",
|
|
),
|
|
)
|
|
|
|
|
|
def policy_name_cksum(name: str) -> bytes:
|
|
"""Return the sha256 digest of a policy name.
|
|
|
|
This 32-byte digest is what the name-uniqueness indexes key on instead
|
|
of the raw ``VARCHAR(256)`` name — a fixed, compact index entry. Two
|
|
names collide iff their digests do, so uniqueness is preserved.
|
|
"""
|
|
return hashlib.sha256(name.encode("utf-8")).digest()
|
|
|
|
|
|
def _default_policy_name_cksum(context: Any) -> bytes:
|
|
"""Column default: derive ``name_cksum`` from the bound ``name`` on INSERT.
|
|
|
|
Mirrors the ``workspace_id`` default pattern so every ORM insert stamps
|
|
the checksum without the caller setting it. Column defaults do not fire
|
|
on UPDATE, so renames recompute it explicitly in the store.
|
|
"""
|
|
return policy_name_cksum(context.get_current_parameters()["name"])
|
|
|
|
|
|
class SqlPolicy(Base):
|
|
"""
|
|
SQLAlchemy model for the ``policies`` table.
|
|
|
|
Policies are either session-scoped (``session_id`` set, FK to
|
|
``conversations.id``) or server-wide defaults
|
|
(``session_id IS NULL``).
|
|
|
|
Session-scoped policies are created via
|
|
``POST /v1/sessions/{session_id}/policies``. Default policies
|
|
are created via ``POST /v1/policies``.
|
|
|
|
:param id: Opaque PK, e.g. ``"pol_a1b2c3..."``.
|
|
:param name: Human-readable name. UNIQUE per session for
|
|
session policies; globally unique for default policies
|
|
(``session_id IS NULL``). Uniqueness is enforced on
|
|
``name_cksum`` rather than this column.
|
|
:param name_cksum: sha256 digest of ``name`` (32 bytes). The
|
|
name-uniqueness indexes key on this compact digest instead
|
|
of the wide ``VARCHAR(256)`` name. Stamped on INSERT by a
|
|
column default; recomputed by the store on rename.
|
|
:param session_id: FK to ``conversations.id``. ``None`` for
|
|
server-wide default policies. ``ON DELETE CASCADE`` so
|
|
removing a session cleans up its policies.
|
|
:param created_at: Unix epoch seconds at row creation.
|
|
:param updated_at: Unix epoch seconds of the last write,
|
|
``None`` if the row has never been updated.
|
|
:param type: Handler discriminator: ``"python"``,
|
|
``"url"``.
|
|
:param handler: Dotted import path (``type="python"``)
|
|
or HTTPS URL (``type="url"``).
|
|
:param factory_params: JSON-encoded dict of kwargs passed to
|
|
the handler when it is a factory function. ``None`` when
|
|
the handler is a direct callable or for ``type="url"``.
|
|
:param enabled: Whether the engine consults this row.
|
|
Defaults to true.
|
|
:param scope: ``"default"`` for server-wide policies;
|
|
``"session"`` for session-scoped policies. Explicit
|
|
discriminator so queries filter by column value instead
|
|
of checking ``session_id IS NULL``.
|
|
:param created_by: User ID of the admin who created this
|
|
policy. ``None`` in single-user mode or for
|
|
session-scoped policies.
|
|
"""
|
|
|
|
__tablename__ = "policies"
|
|
|
|
# Tenant partition key: Databricks workspace id owning this row (0 = default). Part of the PK.
|
|
workspace_id: Mapped[int] = mapped_column(
|
|
BigInteger,
|
|
primary_key=True,
|
|
nullable=False,
|
|
server_default="0",
|
|
default=current_workspace_id,
|
|
)
|
|
id: Mapped[str] = mapped_column(String(64), primary_key=True)
|
|
name: Mapped[str] = mapped_column(String(256))
|
|
# sha256(name) — the value the name-uniqueness indexes key on instead of
|
|
# the wide name column. Stamped from `name` on INSERT via the column
|
|
# default; the store recomputes it on rename (defaults don't fire on UPDATE).
|
|
name_cksum: Mapped[bytes] = mapped_column(_CKSUM32, default=_default_policy_name_cksum)
|
|
# Nullable: NULL for server-wide default policies.
|
|
session_id: Mapped[str | None] = mapped_column(
|
|
String(64),
|
|
nullable=True,
|
|
)
|
|
created_at: Mapped[int] = mapped_column(Integer)
|
|
updated_at: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
|
# Handler discriminator stored as a stable int code (see
|
|
# omnigent.db.enum_codecs POLICY_TYPE: python=1, url=2).
|
|
type: Mapped[int] = mapped_column(SmallInteger)
|
|
# Dotted import path (type="python") or HTTPS URL
|
|
# (type="url") for the policy handler.
|
|
handler: Mapped[str] = mapped_column(Text)
|
|
# JSON-encoded dict of factory kwargs for type="python" when
|
|
# the handler is a factory function. NULL when the handler is
|
|
# a direct callable or for type="url". See the design doc's
|
|
# FunctionRef.arguments pattern.
|
|
factory_params: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
enabled: Mapped[bool] = mapped_column(Boolean, server_default=true())
|
|
# "default" for server-wide policies; "session" for per-conversation
|
|
# copies. Mirrors the agents.kind pattern so queries filter by column
|
|
# value rather than session_id IS NULL. Enum stored as a stable int
|
|
# code (see omnigent.db.enum_codecs POLICY_SCOPE: default=1, session=2).
|
|
scope: Mapped[int] = mapped_column(SmallInteger)
|
|
created_by: Mapped[str | None] = mapped_column(String(128), nullable=True)
|
|
|
|
__table_args__ = (
|
|
CheckConstraint("type IN (1, 2)", name="ck_policies_type"),
|
|
CheckConstraint("scope IN (1, 2)", name="ck_policies_scope"),
|
|
Index("ix_policies_created_at", "workspace_id", "created_at", "id"),
|
|
Index("ix_policies_session_id", "workspace_id", "session_id", "id"),
|
|
# Name uniqueness keys on name_cksum (sha256 of name) rather than the
|
|
# wide name column, for a compact 32-byte index entry.
|
|
UniqueConstraint(
|
|
"workspace_id",
|
|
"session_id",
|
|
"name_cksum",
|
|
name="uq_policies_session_id_name_cksum",
|
|
),
|
|
# Default policies must have unique names; session-scoped policies
|
|
# may reuse the same name. That "unique only within the default set"
|
|
# rule can't be a partial unique index (MySQL has none), so it is
|
|
# enforced in the store (add_default / update_default). This plain
|
|
# index just backs the name_cksum lookup those checks perform.
|
|
Index("ix_policies_name_cksum", "workspace_id", "name_cksum", "id"),
|
|
)
|
|
|
|
|
|
class SqlHost(Base):
|
|
"""
|
|
SQLAlchemy model for the ``hosts`` table.
|
|
|
|
Each row represents a machine that has connected to the server
|
|
via ``omnigent host``. The row is upserted on first connect
|
|
and updated on subsequent reconnects (name, status, timestamps).
|
|
|
|
:param host_id: Stable host identifier from the host's local
|
|
``~/.omnigent/config.yaml``, e.g. ``"host_a1b2c3d4e5f6..."``.
|
|
:param name: Human-readable name from ``config.yaml``, e.g.
|
|
``"corey-laptop"``. Displayed in the Web UI host picker. Max 64
|
|
characters.
|
|
:param owner: User ID from the Databricks auth Bearer token
|
|
presented during the host's WebSocket handshake, e.g.
|
|
``"corey.zumar@databricks.com"``.
|
|
:param status: ``"online"`` when the host has an active WebSocket
|
|
connection, ``"offline"`` when disconnected.
|
|
:param created_at: Unix epoch seconds when the host was first
|
|
registered (first ``omnigent host``).
|
|
:param updated_at: Unix epoch seconds the row was last touched — a
|
|
status change (connect/disconnect) or a tunnel heartbeat. Doubles
|
|
as the host's last-seen for the liveness freshness gate, so a
|
|
host that crashed without a graceful disconnect ages out of the
|
|
"online" set once this stops advancing.
|
|
:param token_hash: Hex SHA-256 digest of the launch token that
|
|
authenticates a SERVER-MANAGED sandbox host's tunnel connection
|
|
(``host_type="managed"`` sessions) — never the raw token.
|
|
``NULL`` for external (user-connected) hosts. Overwritten when
|
|
the sandbox is relaunched, which atomically revokes the
|
|
previous generation's token.
|
|
:param token_expires_at: Unix epoch seconds after which the launch
|
|
token no longer authenticates. Scoped to the TOKEN, not the
|
|
host — the host row is durable across sandbox generations; the
|
|
expiry is set past the provider's maximum sandbox lifetime so a
|
|
live sandbox can always reconnect while a token leaked from a
|
|
dead one cannot. ``NULL`` for external hosts.
|
|
:param sandbox_provider: Sandbox provider backing a managed host,
|
|
e.g. ``"modal"``. ``NULL`` for external hosts — non-NULL is the
|
|
"this host is server-managed" discriminator.
|
|
:param sandbox_id: Provider-assigned id of the sandbox currently
|
|
backing the host, e.g. ``"sb-a1b2c3"`` — what termination is
|
|
issued against. ``NULL`` for external hosts.
|
|
:param configured_harnesses: JSON-encoded per-harness readiness map
|
|
reported in the host's last ``host.hello`` frame, e.g.
|
|
``'{"claude-sdk": true, "codex": false}'``. ``NULL`` when the
|
|
host has never reported it (older host build) — unknown, not
|
|
"nothing configured". Surfaced via ``GET /v1/hosts`` so the web
|
|
agent picker can warn about unconfigured harnesses.
|
|
"""
|
|
|
|
__tablename__ = "hosts"
|
|
|
|
# Tenant partition key: Databricks workspace id owning this row (0 = default). Part of the PK.
|
|
workspace_id: Mapped[int] = mapped_column(
|
|
BigInteger,
|
|
primary_key=True,
|
|
nullable=False,
|
|
server_default="0",
|
|
default=current_workspace_id,
|
|
)
|
|
host_id: Mapped[str] = mapped_column(String(64), primary_key=True)
|
|
owner: Mapped[str] = mapped_column(String(256), nullable=False)
|
|
name: Mapped[str] = mapped_column(String(64), nullable=False)
|
|
# Enum stored as a stable int code (see omnigent.db.enum_codecs
|
|
# HOST_STATUS: online=1, offline=2).
|
|
status: Mapped[int] = mapped_column(SmallInteger)
|
|
created_at: Mapped[int] = mapped_column(Integer)
|
|
updated_at: Mapped[int] = mapped_column(Integer)
|
|
token_hash: Mapped[str | None] = mapped_column(String(64), nullable=True)
|
|
token_expires_at: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
|
sandbox_provider: Mapped[str | None] = mapped_column(String(32), nullable=True)
|
|
sandbox_id: Mapped[str | None] = mapped_column(String(256), nullable=True)
|
|
configured_harnesses: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
|
|
__table_args__ = (
|
|
CheckConstraint(
|
|
"status IN (1, 2)",
|
|
name="ck_hosts_status",
|
|
),
|
|
# (workspace_id, owner, name) was the old PK; keep it unique so the
|
|
# upsert-on-connect logic (look up by owner+name to detect host_id
|
|
# rotation) stays consistent.
|
|
UniqueConstraint("workspace_id", "owner", "name", name="uq_hosts_workspace_owner_name"),
|
|
# resolve_launch_token filters workspace_id + token_hash, so scoping
|
|
# the unique to the workspace keeps that lookup index-served.
|
|
UniqueConstraint("workspace_id", "token_hash", name="uq_hosts_token_hash"),
|
|
)
|
|
|
|
|
|
class SqlUserDailyCost(Base):
|
|
"""
|
|
SQLAlchemy model for the ``user_daily_cost`` table.
|
|
|
|
A running per-user, per-UTC-day rollup of LLM spend, used by
|
|
cost-aware policies (e.g. the "downgrade expensive model once a
|
|
user has spent >$X today" sample policy) to read a user's
|
|
accumulated daily cost as a single O(1) point lookup instead of
|
|
aggregating the per-session ``conversations.session_usage`` blobs
|
|
on every policy evaluation.
|
|
|
|
One row per ``(user_id, day_utc)``. Incremented (UPSERT
|
|
``cost_usd = cost_usd + delta``) at each turn boundary from the
|
|
cost write sites — but only when the session runs under at least
|
|
one policy, so the table is never touched in deployments that
|
|
have no policies configured (this keeps the shared server code
|
|
inert against a database that lacks this table).
|
|
|
|
:param user_id: The user the cost is attributed to — the session
|
|
creator (``LEVEL_OWNER`` grantee), e.g.
|
|
``"alice@example.com"``.
|
|
:param day_utc: The UTC calendar day the spend occurred, as an
|
|
ISO date string ``"YYYY-MM-DD"``, e.g. ``"2026-06-05"``.
|
|
Bucketed by the turn's wall-clock time, so a session spanning
|
|
midnight splits its cost across both days correctly.
|
|
:param cost_usd: Cumulative USD spend for this user on this day.
|
|
Starts at the first turn's delta and grows by each subsequent
|
|
turn's delta.
|
|
:param ask_approved_usd: Highest soft warning checkpoint (USD) the
|
|
user has already approved continuing past for this day — read
|
|
and written by the per-user daily cost-budget policy so an
|
|
approved checkpoint prompts at most once per day (across all of
|
|
the user's sessions), not once per session. ``0.0`` (the
|
|
server default) means no checkpoint approved yet.
|
|
:param updated_at: Unix epoch seconds of the last increment.
|
|
"""
|
|
|
|
__tablename__ = "user_daily_cost"
|
|
|
|
# Tenant partition key: Databricks workspace id owning this row (0 = default). Part of the PK.
|
|
workspace_id: Mapped[int] = mapped_column(
|
|
BigInteger,
|
|
primary_key=True,
|
|
nullable=False,
|
|
server_default="0",
|
|
default=current_workspace_id,
|
|
)
|
|
user_id: Mapped[str] = mapped_column(String(128), primary_key=True)
|
|
day_utc: Mapped[str] = mapped_column(String(10), primary_key=True)
|
|
cost_usd: Mapped[float] = mapped_column(Float, nullable=False)
|
|
ask_approved_usd: Mapped[float] = mapped_column(Float, nullable=False, server_default="0")
|
|
updated_at: Mapped[int] = mapped_column(Integer)
|