"""Agent model — tracks OpenClaw agents linked to HarborForge users. An Agent represents an AI agent (identified by its OpenClaw ``agent_id``) that is bound to exactly one HarborForge User. The Calendar system uses Agent status to decide whether to wake an agent for scheduled slots. See: NEXT_WAVE_DEV_DIRECTION.md §1.4 (Agent table) and §6 (Agent wakeup) Implements: BE-CAL-003 """ from sqlalchemy import ( Column, Integer, String, DateTime, Enum, ForeignKey, ) from sqlalchemy.orm import relationship from sqlalchemy.sql import func from app.core.config import Base import enum # --------------------------------------------------------------------------- # Enums # --------------------------------------------------------------------------- class AgentStatus(str, enum.Enum): """Runtime status of an Agent.""" IDLE = "idle" ON_CALL = "on_call" BUSY = "busy" EXHAUSTED = "exhausted" OFFLINE = "offline" class ExhaustReason(str, enum.Enum): """Why an agent entered the Exhausted state.""" RATE_LIMIT = "rate_limit" BILLING = "billing" # --------------------------------------------------------------------------- # Agent model # --------------------------------------------------------------------------- class Agent(Base): """An OpenClaw agent bound to a HarborForge user. Fields ------ user_id : int One-to-one FK to ``users.id``. Each user has at most one agent. agent_id : str The ``$AGENT_ID`` value from OpenClaw (globally unique). claw_identifier : str The OpenClaw instance identifier (matches ``MonitoredServer.identifier`` by convention, but has no FK — they are independent concepts). status : AgentStatus Current runtime status, managed by heartbeat / calendar wakeup logic. last_heartbeat : datetime | None Timestamp of the most recent heartbeat received from this agent. exhausted_at : datetime | None When the agent entered the ``EXHAUSTED`` state. recovery_at : datetime | None Estimated time the agent will recover from ``EXHAUSTED`` → ``IDLE``. exhaust_reason : ExhaustReason | None Why the agent became exhausted (rate-limit vs billing). """ __tablename__ = "agents" id = Column(Integer, primary_key=True, index=True) user_id = Column( Integer, ForeignKey("users.id"), nullable=False, unique=True, index=True, comment="1-to-1 link to the owning HarborForge user", ) agent_id = Column( String(128), nullable=False, unique=True, index=True, comment="OpenClaw $AGENT_ID", ) claw_identifier = Column( String(128), nullable=False, comment="OpenClaw instance identifier (same value as MonitoredServer.identifier by convention)", ) # -- runtime status fields ---------------------------------------------- status = Column( Enum(AgentStatus, values_callable=lambda x: [e.value for e in x]), nullable=False, default=AgentStatus.IDLE, comment="Current agent status: idle | on_call | busy | exhausted | offline", ) last_heartbeat = Column( DateTime(timezone=True), nullable=True, comment="Timestamp of the most recent heartbeat", ) # -- exhausted state detail --------------------------------------------- exhausted_at = Column( DateTime(timezone=True), nullable=True, comment="When the agent entered EXHAUSTED state", ) recovery_at = Column( DateTime(timezone=True), nullable=True, comment="Estimated recovery time from EXHAUSTED → IDLE", ) exhaust_reason = Column( Enum(ExhaustReason, values_callable=lambda x: [e.value for e in x]), nullable=True, comment="rate_limit | billing — why the agent is exhausted", ) # -- timestamps --------------------------------------------------------- created_at = Column(DateTime(timezone=True), server_default=func.now()) # -- relationships ------------------------------------------------------ user = relationship("User", back_populates="agent", uselist=False)