Files
LangBot/src/langbot/pkg/entity/persistence/agent_runner_state.py
2026-06-03 17:41:52 +08:00

90 lines
3.6 KiB
Python

"""Agent runner state persistence entity for host-owned state."""
from __future__ import annotations
import sqlalchemy
import datetime
from .base import Base
class AgentRunnerState(Base):
"""AgentRunnerState stores host-owned state for AgentRunner protocol.
State is:
- Host-owned: Managed by LangBot, not by plugin instances
- Scope-isolated: Separated by runner_id + binding_identity + scope
- Policy-enforced: Controlled by StatePolicy (enable_state, state_scopes)
Scope key design:
- conversation: runner_id + binding_id + conversation_id [+ thread_id]
- actor: runner_id + binding_id + actor_type + actor_id
- subject: runner_id + binding_id + subject_type + subject_id
- runner: runner_id + binding_id
This table persists state across runs, replacing the in-memory
RunnerScopedStateStore._store dict.
"""
__tablename__ = 'agent_runner_state'
id = sqlalchemy.Column(sqlalchemy.Integer, primary_key=True, autoincrement=True)
"""Auto-increment ID for sequencing."""
# Identity
runner_id = sqlalchemy.Column(sqlalchemy.String(255), nullable=False, index=True)
"""Runner descriptor ID (plugin:author/name/runner)."""
binding_identity = sqlalchemy.Column(sqlalchemy.String(255), nullable=False, index=True)
"""Binding identity for isolation (binding_id or scope_type:scope_id)."""
scope = sqlalchemy.Column(sqlalchemy.String(50), nullable=False, index=True)
"""State scope: 'conversation', 'actor', 'subject', or 'runner'."""
scope_key = sqlalchemy.Column(sqlalchemy.String(512), nullable=False, index=True)
"""Full scope key for unique lookup (includes all identity parts)."""
state_key = sqlalchemy.Column(sqlalchemy.String(255), nullable=False)
"""State key within scope (should use namespace prefix like external.*)."""
value_json = sqlalchemy.Column(sqlalchemy.Text, nullable=True)
"""State value as JSON string (size-limited by host)."""
# Context fields for querying/filtering
bot_id = sqlalchemy.Column(sqlalchemy.String(255), nullable=True, index=True)
"""Bot UUID if applicable."""
workspace_id = sqlalchemy.Column(sqlalchemy.String(255), nullable=True)
"""Workspace ID for multi-tenant."""
conversation_id = sqlalchemy.Column(sqlalchemy.String(255), nullable=True, index=True)
"""Conversation ID for conversation scope."""
thread_id = sqlalchemy.Column(sqlalchemy.String(255), nullable=True)
"""Thread ID for thread-scoped conversation state."""
actor_type = sqlalchemy.Column(sqlalchemy.String(50), nullable=True)
"""Actor type for actor scope."""
actor_id = sqlalchemy.Column(sqlalchemy.String(255), nullable=True, index=True)
"""Actor ID for actor scope."""
subject_type = sqlalchemy.Column(sqlalchemy.String(50), nullable=True)
"""Subject type for subject scope."""
subject_id = sqlalchemy.Column(sqlalchemy.String(255), nullable=True)
"""Subject ID for subject scope."""
# Lifecycle
created_at = sqlalchemy.Column(sqlalchemy.DateTime, nullable=False, default=datetime.datetime.utcnow)
"""When this state entry was created."""
updated_at = sqlalchemy.Column(sqlalchemy.DateTime, nullable=False, default=datetime.datetime.utcnow, onupdate=datetime.datetime.utcnow)
"""When this state entry was last updated."""
# Unique constraint: scope_key + state_key
__table_args__ = (
sqlalchemy.UniqueConstraint('scope_key', 'state_key', name='uq_agent_runner_state_scope_key_state_key'),
sqlalchemy.Index('ix_agent_runner_state_runner_binding', 'runner_id', 'binding_identity'),
sqlalchemy.Index('ix_agent_runner_state_scope_key_lookup', 'scope_key'),
)