"""Create automation_agent_run_steps table for ReAct loop step tracking. Revision ID: 0121 Revises: 0120 """ from alembic import op import sqlalchemy as sa from sqlalchemy.dialects.postgresql import JSONB, UUID as PGUUID revision = "0121" down_revision = "0120" branch_labels = None depends_on = None def _table_exists(conn, table_name: str) -> bool: """True when the table exists (dual-path convergence, Gate B). On a fresh install the automation plugin SQL migration has not run yet when Alembic reaches this revision — skip instead of failing. The plugin-side convergence migration creates the same table. """ row = conn.execute( sa.text("SELECT to_regclass(:tname) IS NOT NULL"), {"tname": f"public.{table_name}"}, ).scalar() return bool(row) def upgrade() -> None: conn = op.get_bind() if not _table_exists(conn, "automation_agent_runs"): return op.create_table( "automation_agent_run_steps", sa.Column("id", PGUUID(as_uuid=True), primary_key=True), sa.Column("tenant_id", PGUUID(as_uuid=True), nullable=False, index=True), sa.Column( "agent_run_id", PGUUID(as_uuid=True), sa.ForeignKey("automation_agent_runs.id", ondelete="CASCADE"), nullable=False, index=True, ), sa.Column("step_number", sa.Integer, nullable=False), sa.Column("thought", sa.Text, nullable=True), sa.Column("action", sa.String(255), nullable=True), sa.Column("action_input", JSONB, nullable=True), sa.Column("observation", sa.Text, nullable=True), sa.Column("cost_usd", sa.Float, nullable=False, server_default="0.0"), sa.Column( "created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now(), ), ) op.create_index( "ix_agent_run_steps_run", "automation_agent_run_steps", ["tenant_id", "agent_run_id"], ) def downgrade() -> None: op.drop_index("ix_agent_run_steps_run", table_name="automation_agent_run_steps") op.drop_table("automation_agent_run_steps")