dify/api/models/agent.py
盐粒 Yanli f6683c2c6b
feat(agent): make home snapshots optional and use backend defaults (#39702)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-07-29 02:01:12 +00:00

591 lines
26 KiB
Python

import json
from datetime import datetime
from enum import StrEnum
from typing import Any
import sqlalchemy as sa
from sqlalchemy import DateTime, Index, String, UniqueConstraint, func
from sqlalchemy.orm import Mapped, mapped_column
from libs.datetime_utils import naive_utc_now
from libs.uuid_utils import uuidv7
from .agent_config_entities import AgentSoulConfig, WorkflowNodeJobConfig
from .base import Base, DefaultFieldsMixin
from .types import EnumText, JSONModelColumn, LongText, StringUUID
class AgentKind(StrEnum):
"""Agent implementation family.
This leaves room for future non-Dify agent implementations while keeping
the current roster/workflow APIs scoped to Dify Agent.
"""
# Native Agent backed by the Dify Agent runtime/protocol.
DIFY_AGENT = "dify_agent"
class AgentScope(StrEnum):
"""Visibility and lifecycle scope of an Agent record."""
# Workspace-visible Agent that can be reused from Agent Roster.
ROSTER = "roster"
# Temporary workflow-local Agent created inside one draft workflow node.
WORKFLOW_ONLY = "workflow_only"
class AgentSource(StrEnum):
"""Origin that created or imported the Agent."""
# Created directly as a reusable Agent Roster asset.
ROSTER = "roster"
# Created from an Agent App composer.
AGENT_APP = "agent_app"
# Created from a Workflow Agent Composer flow.
WORKFLOW = "workflow"
# Imported from an external artifact or future CLI/export flow.
IMPORTED = "imported"
# Created by system bootstrap or managed templates.
SYSTEM = "system"
# Source records provenance. Product capability is determined by scope and
# backing ownership, so imported resources participate in both supported Agent
# surfaces instead of being filtered out by their origin.
APP_BACKED_AGENT_SOURCES = (AgentSource.AGENT_APP, AgentSource.IMPORTED)
WORKFLOW_ONLY_AGENT_SOURCES = (AgentSource.WORKFLOW, AgentSource.IMPORTED)
class AgentIconType(StrEnum):
"""Supported icon storage formats for Agent roster entries."""
# ``icon`` stores an uploaded image reference.
IMAGE = "image"
# ``icon`` stores an emoji character.
EMOJI = "emoji"
# ``icon`` stores an external image URL.
LINK = "link"
class AgentStatus(StrEnum):
"""Soft lifecycle state for Agent records."""
# Available for roster lookup, composer use, and workflow binding.
ACTIVE = "active"
# Hidden from active roster queries while preserving historical bindings.
ARCHIVED = "archived"
class AgentConfigRevisionOperation(StrEnum):
"""Audit operation recorded for Agent Soul version/revision changes."""
# Initial version creation for a new Agent.
CREATE_VERSION = "create_version"
# Saves over the user-facing current version by creating a replacement snapshot.
SAVE_CURRENT_VERSION = "save_current_version"
# Creates a new semantic version for the same Agent.
SAVE_NEW_VERSION = "save_new_version"
# Saves composer content into a brand-new roster Agent.
SAVE_NEW_AGENT = "save_new_agent"
# Promotes a workflow-only Agent into the reusable Agent Roster.
SAVE_TO_ROSTER = "save_to_roster"
# Switches the Agent's current published config back to an existing version.
RESTORE_VERSION = "restore_version"
# Publishes the editable Agent Soul draft as a new immutable version.
PUBLISH_DRAFT = "publish_draft"
# Seeds a new Agent from a portable DSL package.
IMPORT_PACKAGE = "import_package"
class AgentConfigDraftType(StrEnum):
"""Editable Agent Soul draft workspace type."""
# Shared Agent Console draft edited by users before publishing.
DRAFT = "draft"
# Per-editor build draft mutated during debug/build mode.
DEBUG_BUILD = "debug_build"
class WorkflowAgentBindingType(StrEnum):
"""How a workflow node is bound to an Agent."""
# Node uses a reusable Agent from the workspace roster.
ROSTER_AGENT = "roster_agent"
# Node owns a workflow-only Agent that is not visible in the roster.
INLINE_AGENT = "inline_agent"
class AgentWorkingResourceStatus(StrEnum):
"""Product lifecycle state for a persistent working-environment resource."""
ACTIVE = "active"
RETIRED = "retired"
class AgentWorkspaceOwnerType(StrEnum):
"""Product scope that owns a Workspace."""
WORKFLOW_RUN = "workflow_run"
CONVERSATION = "conversation"
BUILD_DRAFT = "build_draft"
class AgentConfigVersionKind(StrEnum):
SNAPSHOT = "snapshot"
DRAFT = "draft"
BUILD_DRAFT = "build_draft"
class Agent(DefaultFieldsMixin, Base):
"""Agent Soul and source lineage; ``AgentWorkspaceBinding.id`` identifies each materialized participant."""
__tablename__ = "agents"
__table_args__ = (
sa.PrimaryKeyConstraint("id", name="agent_pkey"),
UniqueConstraint("tenant_id", "roster_unique_name"),
Index("agent_tenant_updated_at_idx", "tenant_id", "updated_at"),
Index("agent_tenant_scope_idx", "tenant_id", "scope"),
Index("agent_tenant_workflow_id_idx", "tenant_id", "workflow_id"),
Index("agent_tenant_app_id_idx", "tenant_id", "app_id"),
Index("agent_tenant_backing_app_id_idx", "tenant_id", "backing_app_id"),
Index("agent_active_config_snapshot_id_idx", "active_config_snapshot_id"),
Index(
"agent_tenant_invitable_idx",
"tenant_id",
"scope",
"status",
"active_config_has_model",
"updated_at",
),
)
tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
name: Mapped[str] = mapped_column(String(255), nullable=False)
description: Mapped[str] = mapped_column(LongText, nullable=False, default="")
role: Mapped[str] = mapped_column(String(255), nullable=False, default="")
icon_type: Mapped[AgentIconType | None] = mapped_column(EnumText(AgentIconType, length=32), nullable=True)
icon: Mapped[str | None] = mapped_column(
String(255),
nullable=True,
comment="Icon payload interpreted by icon_type: emoji character, image file id, or external URL.",
)
icon_background: Mapped[str | None] = mapped_column(String(255), nullable=True)
agent_kind: Mapped[AgentKind] = mapped_column(
EnumText(AgentKind, length=32), nullable=False, default=AgentKind.DIFY_AGENT
)
scope: Mapped[AgentScope] = mapped_column(EnumText(AgentScope, length=32), nullable=False)
source: Mapped[AgentSource] = mapped_column(EnumText(AgentSource, length=32), nullable=False)
app_id: Mapped[str | None] = mapped_column(StringUUID, nullable=True)
backing_app_id: Mapped[str | None] = mapped_column(
StringUUID,
nullable=True,
comment=(
"Runtime Agent App used for chat/log/monitoring. For workflow-only agents, "
"app_id remains the parent workflow app id and this points to the hidden backing app."
),
)
workflow_id: Mapped[str | None] = mapped_column(StringUUID, nullable=True)
workflow_node_id: Mapped[str | None] = mapped_column(String(255), nullable=True)
active_config_snapshot_id: Mapped[str | None] = mapped_column(StringUUID, nullable=True)
active_config_has_model: Mapped[bool] = mapped_column(
sa.Boolean, nullable=False, default=False, server_default=sa.text("false")
)
active_config_is_published: Mapped[bool] = mapped_column(
sa.Boolean,
nullable=False,
default=False,
server_default=sa.text("false"),
comment=(
"Whether the normal shared Agent draft has been published into the active config snapshot. "
"User-scoped debug drafts do not affect this flag."
),
)
status: Mapped[AgentStatus] = mapped_column(
EnumText(AgentStatus, length=32), nullable=False, default=AgentStatus.ACTIVE
)
roster_unique_name: Mapped[str | None] = mapped_column(
String(255),
sa.Computed("CASE WHEN scope = 'roster' AND status = 'active' THEN name ELSE NULL END"),
nullable=True,
)
created_by: Mapped[str | None] = mapped_column(StringUUID, nullable=True)
updated_by: Mapped[str | None] = mapped_column(StringUUID, nullable=True)
archived_by: Mapped[str | None] = mapped_column(StringUUID, nullable=True)
archived_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
class AgentHomeSnapshot(Base):
"""Append-only mapping from one Agent-owned Home identity to its backend ref.
Product tables reference ``id``. ``snapshot_ref`` remains an opaque
deployment-specific handle and is only consumed at Dify Agent boundaries.
Snapshot bytes and ``snapshot_ref`` are immutable. Lifecycle metadata can
transition ACTIVE -> RETIRED; successful physical collection deletes row.
"""
__tablename__ = "agent_home_snapshots"
__table_args__ = (
sa.PrimaryKeyConstraint("id", name="agent_home_snapshot_pkey"),
Index("agent_home_snapshot_tenant_agent_idx", "tenant_id", "agent_id"),
Index("agent_home_snapshot_status_retired_idx", "status", "retired_at"),
)
id: Mapped[str] = mapped_column(StringUUID, default=lambda: str(uuidv7()))
tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
agent_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
snapshot_ref: Mapped[str] = mapped_column(String(255), nullable=False)
status: Mapped[AgentWorkingResourceStatus] = mapped_column(
EnumText(AgentWorkingResourceStatus, length=32),
nullable=False,
default=AgentWorkingResourceStatus.ACTIVE,
server_default=AgentWorkingResourceStatus.ACTIVE.value,
)
retired_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, server_default=func.current_timestamp())
class AgentDebugConversation(DefaultFieldsMixin, Base):
"""Current console Conversation pointer for one account and draft surface.
This row owns no Binding or runtime. A Preview Conversation holds its
CONVERSATION Binding pointer, while a DEBUG_BUILD AgentConfigDraft holds its
BUILD_DRAFT Binding pointer.
"""
__tablename__ = "agent_debug_conversations"
__table_args__ = (
sa.PrimaryKeyConstraint("id", name="agent_debug_conversation_pkey"),
UniqueConstraint(
"tenant_id",
"agent_id",
"account_id",
"draft_type",
name="agent_debug_conversation_agent_account_draft_type_unique",
),
Index("agent_debug_conversation_conversation_idx", "conversation_id"),
Index("agent_debug_conversation_account_idx", "tenant_id", "account_id"),
)
tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
agent_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
app_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
account_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
draft_type: Mapped[AgentConfigDraftType] = mapped_column(
EnumText(AgentConfigDraftType, length=32),
nullable=False,
default=AgentConfigDraftType.DEBUG_BUILD,
server_default=sa.text("'debug_build'"),
)
conversation_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
class AgentConfigDraft(DefaultFieldsMixin, Base):
"""Editable Agent Soul draft separated from immutable published snapshots.
A DEBUG_BUILD draft owns its materialized participant through
``agent_workspace_binding_id``. Normal drafts leave that pointer unset.
"""
__tablename__ = "agent_config_drafts"
__table_args__ = (
sa.PrimaryKeyConstraint("id", name="agent_config_draft_pkey"),
UniqueConstraint(
"tenant_id",
"agent_id",
"draft_type",
"draft_owner_key",
name="agent_config_draft_agent_type_account_unique",
),
Index("agent_config_draft_tenant_agent_idx", "tenant_id", "agent_id"),
Index("agent_config_draft_base_snapshot_idx", "tenant_id", "base_snapshot_id"),
)
tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
agent_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
draft_type: Mapped[AgentConfigDraftType] = mapped_column(EnumText(AgentConfigDraftType, length=32), nullable=False)
account_id: Mapped[str | None] = mapped_column(StringUUID, nullable=True)
draft_owner_key: Mapped[str] = mapped_column(String(255), nullable=False, default="")
base_snapshot_id: Mapped[str | None] = mapped_column(StringUUID, nullable=True)
home_snapshot_id: Mapped[str | None] = mapped_column(StringUUID, nullable=True)
agent_workspace_binding_id: Mapped[str | None] = mapped_column(StringUUID, nullable=True)
config_snapshot: Mapped[Any] = mapped_column(JSONModelColumn(AgentSoulConfig), nullable=False)
created_by: Mapped[str | None] = mapped_column(StringUUID, nullable=True)
updated_by: Mapped[str | None] = mapped_column(StringUUID, nullable=True)
@property
def config_snapshot_dict(self) -> dict[str, Any]:
if not self.config_snapshot:
return {}
if hasattr(self.config_snapshot, "model_dump"):
return self.config_snapshot.model_dump(mode="json")
if isinstance(self.config_snapshot, str):
return json.loads(self.config_snapshot)
return dict(self.config_snapshot)
class AgentConfigSnapshot(DefaultFieldsMixin, Base):
"""Immutable Agent Soul snapshot.
``config_snapshot`` stores ``AgentSoulConfig`` as JSON-backed ``LongText``.
It may contain credential or secret references, but must never contain
plaintext secrets.
"""
__tablename__ = "agent_config_snapshots"
__table_args__ = (
sa.PrimaryKeyConstraint("id", name="agent_config_snapshot_pkey"),
UniqueConstraint("agent_id", "version", name="agent_config_snapshot_agent_version_unique"),
Index("agent_config_snapshot_tenant_agent_created_at_idx", "tenant_id", "agent_id", "created_at"),
Index("agent_config_snapshot_tenant_created_at_idx", "tenant_id", "created_at"),
)
tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
agent_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
version: Mapped[int] = mapped_column(sa.Integer, nullable=False)
config_snapshot: Mapped[Any] = mapped_column(JSONModelColumn(AgentSoulConfig), nullable=False)
home_snapshot_id: Mapped[str | None] = mapped_column(StringUUID, nullable=True)
summary: Mapped[str | None] = mapped_column(LongText, nullable=True)
version_note: Mapped[str | None] = mapped_column(LongText, nullable=True)
created_by: Mapped[str | None] = mapped_column(StringUUID, nullable=True)
@property
def config_snapshot_dict(self) -> dict[str, Any]:
if not self.config_snapshot:
return {}
if hasattr(self.config_snapshot, "model_dump"):
return self.config_snapshot.model_dump(mode="json")
if isinstance(self.config_snapshot, str):
return json.loads(self.config_snapshot)
return dict(self.config_snapshot)
class AgentConfigRevision(Base):
"""Audit edge for every Agent Soul save operation.
Revisions link immutable Agent Soul snapshots instead of duplicating the
serialized configuration JSON.
"""
__tablename__ = "agent_config_revisions"
__table_args__ = (
sa.PrimaryKeyConstraint("id", name="agent_config_revision_pkey"),
UniqueConstraint(
"agent_id",
"revision",
name="agent_config_revision_agent_revision_unique",
),
Index("agent_config_revision_tenant_agent_created_at_idx", "tenant_id", "agent_id", "created_at"),
Index(
"agent_config_revision_tenant_current_snapshot_created_at_idx",
"tenant_id",
"current_snapshot_id",
"created_at",
),
)
id: Mapped[str] = mapped_column(StringUUID, primary_key=True, default=lambda: str(uuidv7()))
tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
agent_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
previous_snapshot_id: Mapped[str | None] = mapped_column(StringUUID, nullable=True)
current_snapshot_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
revision: Mapped[int] = mapped_column(sa.Integer, nullable=False)
operation: Mapped[AgentConfigRevisionOperation] = mapped_column(
EnumText(AgentConfigRevisionOperation, length=64), nullable=False
)
summary: Mapped[str | None] = mapped_column(LongText, nullable=True)
version_note: Mapped[str | None] = mapped_column(LongText, nullable=True)
created_by: Mapped[str | None] = mapped_column(StringUUID, nullable=True)
created_at: Mapped[datetime] = mapped_column(
DateTime,
nullable=False,
default=naive_utc_now,
server_default=func.current_timestamp(),
)
class WorkflowAgentNodeBinding(DefaultFieldsMixin, Base):
"""Binding between one workflow node and one Agent config snapshot.
``node_job_config`` stores Workflow Node Job JSON only. Agent Soul belongs
to ``AgentConfigSnapshot.config_snapshot`` and must not be duplicated here.
"""
__tablename__ = "workflow_agent_node_bindings"
__table_args__ = (
sa.PrimaryKeyConstraint("id", name="workflow_agent_node_binding_pkey"),
UniqueConstraint(
"tenant_id",
"workflow_id",
"workflow_version",
"node_id",
name="workflow_agent_node_binding_node_version_unique",
),
Index("workflow_agent_node_binding_agent_idx", "tenant_id", "agent_id"),
Index("workflow_agent_node_binding_current_snapshot_idx", "tenant_id", "current_snapshot_id"),
Index("workflow_agent_node_binding_app_idx", "tenant_id", "app_id"),
Index(
"workflow_agent_node_binding_workflow_version_idx",
"tenant_id",
"workflow_id",
"workflow_version",
),
)
tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
app_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
workflow_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
# Tracks which workflow version (draft or a published version string) this
# binding belongs to. Mirrors ``Workflow.version`` and lets us keep separate
# rows for the draft workflow and each published copy under the same
# workflow_id, restoring the stage 1 §5.3 unique key.
workflow_version: Mapped[str] = mapped_column(String(255), nullable=False)
node_id: Mapped[str] = mapped_column(String(255), nullable=False)
binding_type: Mapped[WorkflowAgentBindingType] = mapped_column(
EnumText(WorkflowAgentBindingType, length=32), nullable=False
)
agent_id: Mapped[str | None] = mapped_column(StringUUID, nullable=True)
current_snapshot_id: Mapped[str | None] = mapped_column(StringUUID, nullable=True)
node_job_config: Mapped[Any] = mapped_column(JSONModelColumn(WorkflowNodeJobConfig), nullable=False)
created_by: Mapped[str | None] = mapped_column(StringUUID, nullable=True)
updated_by: Mapped[str | None] = mapped_column(StringUUID, nullable=True)
@property
def node_job_config_dict(self) -> dict[str, Any]:
if not self.node_job_config:
return {}
if hasattr(self.node_job_config, "model_dump"):
return self.node_job_config.model_dump(mode="json")
if isinstance(self.node_job_config, str):
return json.loads(self.node_job_config)
return dict(self.node_job_config)
class AgentWorkspace(DefaultFieldsMixin, Base):
"""Mutable Workspace owned by one product scope, independent of Agents."""
__tablename__ = "agent_workspaces"
__table_args__ = (
sa.PrimaryKeyConstraint("id", name="agent_workspace_pkey"),
Index(
"agent_workspace_owner_active_unique",
"tenant_id",
"owner_type",
"owner_id",
"owner_scope_key",
"active_guard",
unique=True,
),
Index("agent_workspace_tenant_status_idx", "tenant_id", "status"),
Index("agent_workspace_tenant_app_status_idx", "tenant_id", "app_id", "status"),
Index("agent_workspace_status_retired_idx", "status", "retired_at"),
)
tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
app_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
owner_type: Mapped[AgentWorkspaceOwnerType] = mapped_column(
EnumText(AgentWorkspaceOwnerType, length=32), nullable=False
)
owner_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
owner_scope_key: Mapped[str] = mapped_column(String(255), nullable=False)
backend_workspace_ref: Mapped[str] = mapped_column(String(255), nullable=False)
status: Mapped[AgentWorkingResourceStatus] = mapped_column(
EnumText(AgentWorkingResourceStatus, length=32),
nullable=False,
default=AgentWorkingResourceStatus.ACTIVE,
server_default=AgentWorkingResourceStatus.ACTIVE.value,
)
active_guard: Mapped[int | None] = mapped_column(sa.SmallInteger, nullable=True, default=1, server_default="1")
retired_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
class AgentWorkspaceBinding(DefaultFieldsMixin, Base):
"""One materialized Agent participant and session attached to a Workspace.
All resource IDs are logical associations rather than database foreign
keys, so RETIRED rows can outlive their Workspace or base Home Snapshot.
``agent_id`` identifies the source Agent Soul; this row's ``id`` identifies
the participant and its private Materialized Home.
"""
__tablename__ = "agent_workspace_bindings"
__table_args__ = (
sa.PrimaryKeyConstraint("id", name="agent_workspace_binding_pkey"),
Index("agent_workspace_binding_workspace_status_idx", "tenant_id", "workspace_id", "status"),
Index("agent_workspace_binding_agent_status_idx", "tenant_id", "agent_id", "status"),
Index("agent_workspace_binding_status_retired_idx", "status", "retired_at"),
)
tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
app_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
workspace_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
agent_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
base_home_snapshot_id: Mapped[str | None] = mapped_column(StringUUID, nullable=True)
agent_config_version_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
agent_config_version_kind: Mapped[AgentConfigVersionKind] = mapped_column(
EnumText(AgentConfigVersionKind, length=32), nullable=False
)
backend_binding_ref: Mapped[str] = mapped_column(String(255), nullable=False)
session_snapshot: Mapped[str | None] = mapped_column(LongText, nullable=True)
status: Mapped[AgentWorkingResourceStatus] = mapped_column(
EnumText(AgentWorkingResourceStatus, length=32),
nullable=False,
default=AgentWorkingResourceStatus.ACTIVE,
server_default=AgentWorkingResourceStatus.ACTIVE.value,
)
retired_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
pending_form_id: Mapped[str | None] = mapped_column(StringUUID, nullable=True)
pending_tool_call_id: Mapped[str | None] = mapped_column(String(255), nullable=True)
class AgentDriveFileKind(StrEnum):
"""Kind of existing file record an agent-drive KV entry points at."""
UPLOAD_FILE = "upload_file"
TOOL_FILE = "tool_file"
class AgentDriveFile(DefaultFieldsMixin, Base):
"""Per-agent path-like KV index into existing file records (agent 网盘 / agent drive).
A row maps a path-like ``key`` to a *pointer* (``file_kind`` + ``file_id``) at an
existing ``UploadFile`` / ``ToolFile`` — it never stores file bytes. Scope/ownership
is ``tenant_id -> agent-<agent_id>`` (the drive ref; no standalone ``drive_id`` this
phase). ``key`` is opaque/path-like and carries no directory, permission, or
parent-child semantics on the API side; it maps 1:1 to a sandbox-relative path when
synced. ``value_owned_by_drive`` gates physical cleanup: only drive-owned values
(created by the agent runtime or Skill standardization, not shared with other
business records) have their storage object + record deleted when the KV entry is
overwritten or removed; otherwise only the KV row is dropped. Skills are represented
by the canonical ``<path>/SKILL.md`` row with ``is_skill=True`` and a serialized
``skill_metadata`` string. Lifecycle never relies on ``UploadFile.used/used_by``
(not a reliable refcount).
"""
__tablename__ = "agent_drive_files"
__table_args__ = (
sa.PrimaryKeyConstraint("id", name="agent_drive_file_pkey"),
UniqueConstraint("tenant_id", "agent_id", "key", name="agent_drive_file_scope_key_unique"),
Index("agent_drive_files_tenant_agent_is_skill_key_idx", "tenant_id", "agent_id", "is_skill", "key"),
)
tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
# drive ref = agent-<agent_id>; this phase has no standalone drive_id.
agent_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
# path-like opaque key; not a filesystem (no dir/permission/parent semantics).
# Bounded at 512 so the (tenant_id, agent_id, key) unique index stays within
# MySQL's 3072-byte index limit (CHAR(36)*2 + VARCHAR(512) utf8mb4 = 2336).
key: Mapped[str] = mapped_column(String(512), nullable=False)
file_kind: Mapped[AgentDriveFileKind] = mapped_column(EnumText(AgentDriveFileKind, length=32), nullable=False)
# points at UploadFile.id / ToolFile.id (the value), never the bytes.
file_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
value_owned_by_drive: Mapped[bool] = mapped_column(
sa.Boolean, nullable=False, default=False, server_default=sa.text("false")
)
is_skill: Mapped[bool] = mapped_column(sa.Boolean, nullable=False, default=False, server_default=sa.text("false"))
skill_metadata: Mapped[str | None] = mapped_column(LongText, nullable=True)
size: Mapped[int | None] = mapped_column(sa.BigInteger, nullable=True)
hash: Mapped[str | None] = mapped_column(String(255), nullable=True)
mime_type: Mapped[str | None] = mapped_column(String(255), nullable=True)
created_by: Mapped[str | None] = mapped_column(StringUUID, nullable=True)