- 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
141 lines
4.2 KiB
Python
141 lines
4.2 KiB
Python
"""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)
|