Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
"""deployments

Revision ID: 4a9b7787ccd7
Revises: d1a6cde41b3f
Create Date: 2026-03-30 19:00:35.962651

"""
from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects.postgresql import JSONB


# revision identifiers, used by Alembic.
revision: str = '4a9b7787ccd7'
down_revision: Union[str, None] = 'd1a6cde41b3f'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
# 1. Create deployments table
op.create_table('deployments',
sa.Column('id', sa.String(), nullable=False),
sa.Column('agent_id', sa.String(length=64), nullable=False),
sa.Column('docker_image', sa.String(), nullable=False),
sa.Column('registration_metadata', JSONB, nullable=True),
sa.Column('status', sa.Enum('Pending', 'Ready', 'Failed', name='deploymentstatus'), nullable=False, server_default='Pending'),
sa.Column('acp_url', sa.String(), nullable=True),
sa.Column('is_production', sa.Boolean(), nullable=False, server_default=sa.text('false')),
sa.Column('sgp_deploy_id', sa.String(), nullable=True),
sa.Column('helm_release_name', sa.String(), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
sa.Column('promoted_at', sa.DateTime(timezone=True), nullable=True),
sa.Column('expires_at', sa.DateTime(timezone=True), nullable=True),
sa.ForeignKeyConstraint(['agent_id'], ['agents.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_index('idx_deployments_agent_id', 'deployments', ['agent_id'], unique=False)
op.create_index('idx_deployments_agent_production', 'deployments', ['agent_id', 'is_production'], unique=False)
op.create_index(
'uq_deployments_one_production_per_agent',
'deployments',
['agent_id'],
unique=True,
postgresql_where=sa.text('is_production = true'),
)
op.create_index('idx_deployments_sgp_deploy_id', 'deployments', ['sgp_deploy_id'], unique=False)

# 2. Add production_deployment_id to agents table
op.add_column('agents', sa.Column('production_deployment_id', sa.String(), nullable=True))
op.create_foreign_key('fk_agents_production_deployment', 'agents', 'deployments', ['production_deployment_id'], ['id'])


def downgrade() -> None:
# Remove production_deployment_id from agents
op.drop_constraint('fk_agents_production_deployment', 'agents', type_='foreignkey')
op.drop_column('agents', 'production_deployment_id')

# Drop deployments table
op.drop_index('idx_deployments_sgp_deploy_id', table_name='deployments')
op.drop_index('uq_deployments_one_production_per_agent', table_name='deployments')
op.drop_index('idx_deployments_agent_production', table_name='deployments')
op.drop_index('idx_deployments_agent_id', table_name='deployments')
op.drop_table('deployments')

# Drop the enum type
sa.Enum(name='deploymentstatus').drop(op.get_bind(), checkfirst=True)
3 changes: 2 additions & 1 deletion agentex/database/migrations/migration_history.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
d024851e790c -> d1a6cde41b3f (head), add_langgraph_checkpoint_tables
d1a6cde41b3f -> 4a9b7787ccd7 (head), deployments table + agents.production_deployment_id + data migration from deployment_history
d024851e790c -> d1a6cde41b3f, add_langgraph_checkpoint_tables
24429f13b8bd -> d024851e790c, add_performance_indexes
a5d67f2d7356 -> 24429f13b8bd, add agent input type
329fbafa4ff9 -> a5d67f2d7356, add unhealthy status
Expand Down
60 changes: 51 additions & 9 deletions agentex/src/adapters/orm.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from sqlalchemy import (
JSON,
BigInteger,
Boolean,
Column,
DateTime,
ForeignKey,
Expand All @@ -20,6 +21,7 @@

from src.domain.entities.agent_api_keys import AgentAPIKeyType
from src.domain.entities.agents import AgentInputType, AgentStatus
from src.domain.entities.deployments import DeploymentStatus
from src.domain.entities.tasks import TaskStatus
from src.utils.ids import orm_id

Expand All @@ -44,6 +46,9 @@ class AgentORM(BaseORM):
registration_metadata = Column(JSONB, nullable=True)
registered_at = Column(DateTime(timezone=True), nullable=True)
agent_input_type = Column(SQLAlchemyEnum(AgentInputType), nullable=True)
production_deployment_id = Column(
String, ForeignKey("deployments.id"), nullable=True
)

# Many-to-Many relationship with tasks
tasks = relationship("TaskORM", secondary="task_agents", back_populates="agents")
Expand Down Expand Up @@ -217,6 +222,49 @@ class DeploymentHistoryORM(BaseORM):
)


class DeploymentORM(BaseORM):
__tablename__ = "deployments"

id = Column(String, primary_key=True, default=orm_id)
agent_id = Column(String(64), ForeignKey("agents.id"), nullable=False)

# Image (immutable after creation)
docker_image = Column(String, nullable=False)

# Git/build metadata (commit_hash, branch_name, author_name, author_email, build_timestamp)
registration_metadata = Column(JSONB, nullable=True)

# Runtime state (updated during lifecycle)
status = Column(
SQLAlchemyEnum(DeploymentStatus),
nullable=False,
default=DeploymentStatus.PENDING,
)
acp_url = Column(String, nullable=True)
is_production = Column(Boolean, nullable=False, default=False)

# Infra references
sgp_deploy_id = Column(String, nullable=True)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this from cloud deploy?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes

helm_release_name = Column(String, nullable=True)

# Timestamps
created_at = Column(DateTime(timezone=True), server_default=func.now())
promoted_at = Column(DateTime(timezone=True), nullable=True)
expires_at = Column(DateTime(timezone=True), nullable=True)

__table_args__ = (
Index("idx_deployments_agent_id", "agent_id"),
Index("idx_deployments_agent_production", "agent_id", "is_production"),
Index(
"uq_deployments_one_production_per_agent",
"agent_id",
unique=True,
postgresql_where=(is_production.is_(True)),
),
Index("idx_deployments_sgp_deploy_id", "sgp_deploy_id"),
)


# LangGraph checkpoint tables
# These mirror the schema from langgraph.checkpoint.postgres so that
# tables are created via Alembic migrations rather than at agent runtime.
Expand All @@ -236,9 +284,7 @@ class CheckpointORM(BaseORM):
type = Column(Text, nullable=True)
checkpoint = Column(JSONB, nullable=False)
metadata_ = Column("metadata", JSONB, nullable=False, server_default="{}")
__table_args__ = (
Index("checkpoints_thread_id_idx", "thread_id"),
)
__table_args__ = (Index("checkpoints_thread_id_idx", "thread_id"),)


class CheckpointBlobORM(BaseORM):
Expand All @@ -249,9 +295,7 @@ class CheckpointBlobORM(BaseORM):
version = Column(Text, nullable=False, primary_key=True)
type = Column(Text, nullable=False)
blob = Column(LargeBinary, nullable=True)
__table_args__ = (
Index("checkpoint_blobs_thread_id_idx", "thread_id"),
)
__table_args__ = (Index("checkpoint_blobs_thread_id_idx", "thread_id"),)


class CheckpointWriteORM(BaseORM):
Expand All @@ -265,6 +309,4 @@ class CheckpointWriteORM(BaseORM):
type = Column(Text, nullable=True)
blob = Column(LargeBinary, nullable=False)
task_path = Column(Text, nullable=False, server_default="")
__table_args__ = (
Index("checkpoint_writes_thread_id_idx", "thread_id"),
)
__table_args__ = (Index("checkpoint_writes_thread_id_idx", "thread_id"),)
2 changes: 2 additions & 0 deletions agentex/src/api/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
agents,
checkpoints,
deployment_history,
deployments,
events,
messages,
schedules,
Expand Down Expand Up @@ -183,6 +184,7 @@ async def handle_unexpected(request, exc):
fastapi_app.include_router(agent_task_tracker.router)
fastapi_app.include_router(agent_api_keys.router)
fastapi_app.include_router(deployment_history.router)
fastapi_app.include_router(deployments.router)
fastapi_app.include_router(schedules.router)
fastapi_app.include_router(checkpoints.router)

Expand Down
Loading
Loading