BE-CAL-003: Add Agent model with status/heartbeat/exhausted fields
- New app/models/agent.py with Agent model, AgentStatus & ExhaustReason enums - Agent has 1-to-1 FK to User, unique agent_id (OpenClaw $AGENT_ID), claw_identifier (OpenClaw instance, convention-matches MonitoredServer.identifier) - Status fields: status (idle/on_call/busy/exhausted/offline), last_heartbeat - Exhausted tracking: exhausted_at, recovery_at, exhaust_reason (rate_limit/billing) - User model: added 'agent' back-reference (uselist=False) - Schemas: AgentResponse, AgentStatusUpdate, UserCreate now accepts agent_id+claw_identifier - UserResponse: includes agent_id when agent is bound - Users router: create_user creates Agent record when agent_id+claw_identifier provided - Auto-migration: CREATE TABLE agents in _migrate_schema() - Startup imports: agent and calendar models registered
This commit is contained in:
140
app/models/agent.py
Normal file
140
app/models/agent.py
Normal file
@@ -0,0 +1,140 @@
|
||||
"""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)
|
||||
@@ -81,6 +81,7 @@ class User(Base):
|
||||
owned_projects = relationship("Project", back_populates="owner")
|
||||
comments = relationship("Comment", back_populates="author")
|
||||
project_memberships = relationship("ProjectMember", back_populates="user")
|
||||
agent = relationship("Agent", back_populates="user", uselist=False)
|
||||
|
||||
@property
|
||||
def role_name(self):
|
||||
|
||||
Reference in New Issue
Block a user