mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-06-26 23:44:19 +00:00
feat(agent-runner): add plugin runner host integration
This commit is contained in:
@@ -0,0 +1,200 @@
|
||||
"""Agent run ledger persistence entities."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
|
||||
import sqlalchemy
|
||||
|
||||
from .base import Base
|
||||
|
||||
|
||||
class AgentRun(Base):
|
||||
"""AgentRun stores Host-owned execution lifecycle facts."""
|
||||
|
||||
__tablename__ = 'agent_run'
|
||||
|
||||
id = sqlalchemy.Column(sqlalchemy.Integer, primary_key=True, autoincrement=True)
|
||||
"""Auto-increment ID for pagination."""
|
||||
|
||||
run_id = sqlalchemy.Column(sqlalchemy.String(255), nullable=False, unique=True, index=True)
|
||||
"""Unique AgentRunner run identifier."""
|
||||
|
||||
event_id = sqlalchemy.Column(sqlalchemy.String(255), nullable=True, index=True)
|
||||
"""Input event that triggered this run."""
|
||||
|
||||
agent_id = sqlalchemy.Column(sqlalchemy.String(255), nullable=True)
|
||||
"""Future Host-owned agent identifier."""
|
||||
|
||||
binding_id = sqlalchemy.Column(sqlalchemy.String(255), nullable=True, index=True)
|
||||
"""Binding that selected this runner."""
|
||||
|
||||
runner_id = sqlalchemy.Column(sqlalchemy.String(255), nullable=False, index=True)
|
||||
"""Runner descriptor ID."""
|
||||
|
||||
conversation_id = sqlalchemy.Column(sqlalchemy.String(255), nullable=True, index=True)
|
||||
"""Conversation this run belongs to."""
|
||||
|
||||
thread_id = sqlalchemy.Column(sqlalchemy.String(255), nullable=True)
|
||||
"""Thread this run belongs to."""
|
||||
|
||||
workspace_id = sqlalchemy.Column(sqlalchemy.String(255), nullable=True)
|
||||
"""Workspace this run belongs to."""
|
||||
|
||||
bot_id = sqlalchemy.Column(sqlalchemy.String(255), nullable=True, index=True)
|
||||
"""Bot UUID this run belongs to."""
|
||||
|
||||
status = sqlalchemy.Column(sqlalchemy.String(50), nullable=False, index=True)
|
||||
"""Run lifecycle status."""
|
||||
|
||||
status_reason = sqlalchemy.Column(sqlalchemy.Text, nullable=True)
|
||||
"""Human-readable terminal or current status reason."""
|
||||
|
||||
queue_name = sqlalchemy.Column(sqlalchemy.String(255), nullable=True, index=True)
|
||||
"""Host queue name this run is waiting in."""
|
||||
|
||||
priority = sqlalchemy.Column(sqlalchemy.Integer, nullable=False, default=0)
|
||||
"""Higher values are claimed before lower values within a queue."""
|
||||
|
||||
requested_runtime_id = sqlalchemy.Column(sqlalchemy.String(255), nullable=True, index=True)
|
||||
"""Specific runtime requested by the producer, if any."""
|
||||
|
||||
claimed_by_runtime_id = sqlalchemy.Column(sqlalchemy.String(255), nullable=True, index=True)
|
||||
"""Runtime that currently owns the claim lease."""
|
||||
|
||||
claim_token = sqlalchemy.Column(sqlalchemy.String(255), nullable=True, index=True)
|
||||
"""Opaque token required to renew or release the current claim."""
|
||||
|
||||
claim_lease_expires_at = sqlalchemy.Column(sqlalchemy.DateTime, nullable=True, index=True)
|
||||
"""When the current claim lease expires."""
|
||||
|
||||
dispatch_attempts = sqlalchemy.Column(sqlalchemy.Integer, nullable=False, default=0)
|
||||
"""Number of times this run has been claimed for dispatch."""
|
||||
|
||||
last_claimed_at = sqlalchemy.Column(sqlalchemy.DateTime, nullable=True)
|
||||
"""When this run was last claimed."""
|
||||
|
||||
created_at = sqlalchemy.Column(sqlalchemy.DateTime, nullable=False, default=datetime.datetime.utcnow)
|
||||
"""When the run record was created."""
|
||||
|
||||
started_at = sqlalchemy.Column(sqlalchemy.DateTime, nullable=True)
|
||||
"""When execution started."""
|
||||
|
||||
finished_at = sqlalchemy.Column(sqlalchemy.DateTime, nullable=True)
|
||||
"""When execution reached a terminal status."""
|
||||
|
||||
updated_at = sqlalchemy.Column(
|
||||
sqlalchemy.DateTime, nullable=False, default=datetime.datetime.utcnow, onupdate=datetime.datetime.utcnow
|
||||
)
|
||||
"""When the run record was last updated."""
|
||||
|
||||
deadline_at = sqlalchemy.Column(sqlalchemy.DateTime, nullable=True)
|
||||
"""Execution deadline if one was assigned."""
|
||||
|
||||
cancel_requested_at = sqlalchemy.Column(sqlalchemy.DateTime, nullable=True)
|
||||
"""When cancellation was requested."""
|
||||
|
||||
usage_json = sqlalchemy.Column(sqlalchemy.Text, nullable=True)
|
||||
"""Final or latest aggregate token usage JSON."""
|
||||
|
||||
cost_json = sqlalchemy.Column(sqlalchemy.Text, nullable=True)
|
||||
"""Host-calculated cost JSON, if available."""
|
||||
|
||||
authorization_json = sqlalchemy.Column(sqlalchemy.Text, nullable=True)
|
||||
"""Run-scoped authorization snapshot JSON."""
|
||||
|
||||
metadata_json = sqlalchemy.Column(sqlalchemy.Text, nullable=True)
|
||||
"""Additional metadata JSON."""
|
||||
|
||||
__table_args__ = (
|
||||
sqlalchemy.Index(
|
||||
'ix_agent_run_scope_status', 'bot_id', 'workspace_id', 'conversation_id', 'thread_id', 'status'
|
||||
),
|
||||
sqlalchemy.Index('ix_agent_run_runner_status', 'runner_id', 'status'),
|
||||
sqlalchemy.Index('ix_agent_run_queue_claim', 'queue_name', 'status', 'priority', 'id'),
|
||||
)
|
||||
|
||||
|
||||
class AgentRuntime(Base):
|
||||
"""AgentRuntime stores Host-owned runtime heartbeat registry facts."""
|
||||
|
||||
__tablename__ = 'agent_runtime'
|
||||
|
||||
id = sqlalchemy.Column(sqlalchemy.Integer, primary_key=True, autoincrement=True)
|
||||
"""Auto-increment ID."""
|
||||
|
||||
runtime_id = sqlalchemy.Column(sqlalchemy.String(255), nullable=False, unique=True, index=True)
|
||||
"""Unique runtime or daemon identifier."""
|
||||
|
||||
status = sqlalchemy.Column(sqlalchemy.String(50), nullable=False, index=True)
|
||||
"""Runtime lifecycle status."""
|
||||
|
||||
display_name = sqlalchemy.Column(sqlalchemy.String(255), nullable=True)
|
||||
"""Human-readable runtime display name."""
|
||||
|
||||
endpoint = sqlalchemy.Column(sqlalchemy.String(1024), nullable=True)
|
||||
"""Runtime endpoint, if it exposes one."""
|
||||
|
||||
version = sqlalchemy.Column(sqlalchemy.String(255), nullable=True)
|
||||
"""Runtime version string."""
|
||||
|
||||
capabilities_json = sqlalchemy.Column(sqlalchemy.Text, nullable=True)
|
||||
"""Runtime capabilities JSON."""
|
||||
|
||||
labels_json = sqlalchemy.Column(sqlalchemy.Text, nullable=True)
|
||||
"""Runtime labels JSON."""
|
||||
|
||||
metadata_json = sqlalchemy.Column(sqlalchemy.Text, nullable=True)
|
||||
"""Additional metadata JSON."""
|
||||
|
||||
last_heartbeat_at = sqlalchemy.Column(sqlalchemy.DateTime, nullable=True, index=True)
|
||||
"""When the runtime last sent a heartbeat."""
|
||||
|
||||
heartbeat_deadline_at = sqlalchemy.Column(sqlalchemy.DateTime, nullable=True, index=True)
|
||||
"""When the runtime should be considered stale."""
|
||||
|
||||
created_at = sqlalchemy.Column(sqlalchemy.DateTime, nullable=False, default=datetime.datetime.utcnow)
|
||||
"""When the runtime record was created."""
|
||||
|
||||
updated_at = sqlalchemy.Column(
|
||||
sqlalchemy.DateTime, nullable=False, default=datetime.datetime.utcnow, onupdate=datetime.datetime.utcnow
|
||||
)
|
||||
"""When the runtime record was last updated."""
|
||||
|
||||
|
||||
class AgentRunEvent(Base):
|
||||
"""AgentRunEvent stores one result event emitted by a run."""
|
||||
|
||||
__tablename__ = 'agent_run_event'
|
||||
|
||||
id = sqlalchemy.Column(sqlalchemy.Integer, primary_key=True, autoincrement=True)
|
||||
"""Auto-increment ID."""
|
||||
|
||||
run_id = sqlalchemy.Column(sqlalchemy.String(255), nullable=False, index=True)
|
||||
"""Run that produced this event."""
|
||||
|
||||
sequence = sqlalchemy.Column(sqlalchemy.Integer, nullable=False)
|
||||
"""Monotonic sequence inside the run."""
|
||||
|
||||
type = sqlalchemy.Column(sqlalchemy.String(100), nullable=False, index=True)
|
||||
"""Result event type."""
|
||||
|
||||
data_json = sqlalchemy.Column(sqlalchemy.Text, nullable=True)
|
||||
"""Result event payload JSON."""
|
||||
|
||||
usage_json = sqlalchemy.Column(sqlalchemy.Text, nullable=True)
|
||||
"""Token usage JSON for this event, if provided."""
|
||||
|
||||
created_at = sqlalchemy.Column(sqlalchemy.DateTime, nullable=False, default=datetime.datetime.utcnow)
|
||||
"""When this event was persisted."""
|
||||
|
||||
source = sqlalchemy.Column(sqlalchemy.String(50), nullable=True)
|
||||
"""Source that appended the event."""
|
||||
|
||||
metadata_json = sqlalchemy.Column(sqlalchemy.Text, nullable=True)
|
||||
"""Additional metadata JSON."""
|
||||
|
||||
__table_args__ = (
|
||||
sqlalchemy.UniqueConstraint('run_id', 'sequence', name='uq_agent_run_event_run_sequence'),
|
||||
sqlalchemy.Index('ix_agent_run_event_run_sequence', 'run_id', 'sequence'),
|
||||
)
|
||||
@@ -0,0 +1,88 @@
|
||||
"""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 is the production store for AgentRunner state.
|
||||
"""
|
||||
|
||||
__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)
|
||||
"""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'),
|
||||
)
|
||||
@@ -0,0 +1,85 @@
|
||||
"""EventLog persistence entity for storing auditable event facts."""
|
||||
from __future__ import annotations
|
||||
|
||||
import sqlalchemy
|
||||
import datetime
|
||||
|
||||
from .base import Base
|
||||
|
||||
|
||||
class EventLog(Base):
|
||||
"""EventLog stores auditable event records for AgentRunner.
|
||||
|
||||
This is the fact source for events - messages, tool calls, system events, etc.
|
||||
Large payloads are stored separately; this table stores references and
|
||||
summaries.
|
||||
"""
|
||||
|
||||
__tablename__ = 'event_log'
|
||||
|
||||
id = sqlalchemy.Column(sqlalchemy.Integer, primary_key=True, autoincrement=True)
|
||||
"""Auto-increment ID for sequencing."""
|
||||
|
||||
event_id = sqlalchemy.Column(sqlalchemy.String(255), nullable=False, unique=True, index=True)
|
||||
"""Unique event identifier."""
|
||||
|
||||
event_type = sqlalchemy.Column(sqlalchemy.String(100), nullable=False, index=True)
|
||||
"""Event type (message.received, tool.call.started, etc.)."""
|
||||
|
||||
event_time = sqlalchemy.Column(sqlalchemy.DateTime, nullable=True)
|
||||
"""When the event occurred."""
|
||||
|
||||
source = sqlalchemy.Column(sqlalchemy.String(50), nullable=False)
|
||||
"""Event source (platform, webui, api, scheduler, system, pipeline_adapter)."""
|
||||
|
||||
bot_id = sqlalchemy.Column(sqlalchemy.String(255), nullable=True, index=True)
|
||||
"""Bot UUID that handled this event."""
|
||||
|
||||
workspace_id = sqlalchemy.Column(sqlalchemy.String(255), nullable=True)
|
||||
"""Workspace ID for multi-tenant deployments."""
|
||||
|
||||
conversation_id = sqlalchemy.Column(sqlalchemy.String(255), nullable=True, index=True)
|
||||
"""Conversation ID this event belongs to."""
|
||||
|
||||
thread_id = sqlalchemy.Column(sqlalchemy.String(255), nullable=True)
|
||||
"""Thread ID if platform supports threads."""
|
||||
|
||||
# Actor information
|
||||
actor_type = sqlalchemy.Column(sqlalchemy.String(50), nullable=True)
|
||||
"""Actor type (user, system, runner)."""
|
||||
|
||||
actor_id = sqlalchemy.Column(sqlalchemy.String(255), nullable=True)
|
||||
"""Actor identifier."""
|
||||
|
||||
actor_name = sqlalchemy.Column(sqlalchemy.String(255), nullable=True)
|
||||
"""Actor display name."""
|
||||
|
||||
# Subject information
|
||||
subject_type = sqlalchemy.Column(sqlalchemy.String(50), nullable=True)
|
||||
"""Subject type (message, tool_call, attachment, etc.)."""
|
||||
|
||||
subject_id = sqlalchemy.Column(sqlalchemy.String(255), nullable=True)
|
||||
"""Subject identifier."""
|
||||
|
||||
# Input information
|
||||
input_summary = sqlalchemy.Column(sqlalchemy.Text, nullable=True)
|
||||
"""Brief summary of input (truncated text, max 1000 chars)."""
|
||||
|
||||
input_json = sqlalchemy.Column(sqlalchemy.Text, nullable=True)
|
||||
"""Full input JSON if reasonably sized (AgentInput as JSON string)."""
|
||||
|
||||
# Raw event reference
|
||||
raw_ref = sqlalchemy.Column(sqlalchemy.String(255), nullable=True)
|
||||
"""Reference to raw event payload stored outside the inline event row."""
|
||||
|
||||
run_id = sqlalchemy.Column(sqlalchemy.String(255), nullable=True, index=True)
|
||||
"""Run ID that processed this event."""
|
||||
|
||||
runner_id = sqlalchemy.Column(sqlalchemy.String(255), nullable=True)
|
||||
"""Runner ID that processed this event."""
|
||||
|
||||
created_at = sqlalchemy.Column(sqlalchemy.DateTime, nullable=False, default=datetime.datetime.utcnow)
|
||||
"""When this record was created."""
|
||||
|
||||
metadata_json = sqlalchemy.Column(sqlalchemy.Text, nullable=True)
|
||||
"""Additional metadata as JSON string."""
|
||||
@@ -0,0 +1,79 @@
|
||||
"""Transcript persistence entity for conversation history projection."""
|
||||
from __future__ import annotations
|
||||
|
||||
import sqlalchemy
|
||||
import datetime
|
||||
|
||||
from .base import Base
|
||||
|
||||
|
||||
class Transcript(Base):
|
||||
"""Transcript stores conversation-oriented message projection for history API.
|
||||
|
||||
This is a projection of EventLog, optimized for agent history retrieval.
|
||||
It includes message content and attachment refs, but not raw platform payloads.
|
||||
"""
|
||||
|
||||
__tablename__ = 'transcript'
|
||||
|
||||
id = sqlalchemy.Column(sqlalchemy.Integer, primary_key=True, autoincrement=True)
|
||||
"""Auto-increment ID for sequencing."""
|
||||
|
||||
transcript_id = sqlalchemy.Column(sqlalchemy.String(255), nullable=False, unique=True, index=True)
|
||||
"""Unique transcript item identifier."""
|
||||
|
||||
event_id = sqlalchemy.Column(sqlalchemy.String(255), nullable=False, index=True)
|
||||
"""Reference to the source event in EventLog."""
|
||||
|
||||
bot_id = sqlalchemy.Column(sqlalchemy.String(255), nullable=True, index=True)
|
||||
"""Bot UUID this item belongs to."""
|
||||
|
||||
workspace_id = sqlalchemy.Column(sqlalchemy.String(255), nullable=True)
|
||||
"""Workspace this item belongs to."""
|
||||
|
||||
conversation_id = sqlalchemy.Column(sqlalchemy.String(255), nullable=False, index=True)
|
||||
"""Conversation this item belongs to."""
|
||||
|
||||
thread_id = sqlalchemy.Column(sqlalchemy.String(255), nullable=True)
|
||||
"""Thread ID if platform supports threads."""
|
||||
|
||||
role = sqlalchemy.Column(sqlalchemy.String(50), nullable=False)
|
||||
"""Message role: 'user', 'assistant', 'system', or 'tool'."""
|
||||
|
||||
item_type = sqlalchemy.Column(sqlalchemy.String(50), nullable=False, default='message')
|
||||
"""Item type: 'message', 'tool_call', 'tool_result', 'system'."""
|
||||
|
||||
# Content
|
||||
content = sqlalchemy.Column(sqlalchemy.Text, nullable=True)
|
||||
"""Text content summary (may be truncated for large messages, max 4000 chars)."""
|
||||
|
||||
content_json = sqlalchemy.Column(sqlalchemy.Text, nullable=True)
|
||||
"""Full structured content as JSON string (Message model dump)."""
|
||||
|
||||
# Attachment references
|
||||
attachment_refs_json = sqlalchemy.Column(sqlalchemy.Text, nullable=True)
|
||||
"""Attachment references as JSON string."""
|
||||
|
||||
# Sequence for cursor-based pagination
|
||||
seq = sqlalchemy.Column(sqlalchemy.Integer, nullable=False)
|
||||
"""Monotonic cursor sequence for pagination."""
|
||||
|
||||
# Context
|
||||
run_id = sqlalchemy.Column(sqlalchemy.String(255), nullable=True, index=True)
|
||||
"""Run ID that generated this item (for assistant messages)."""
|
||||
|
||||
runner_id = sqlalchemy.Column(sqlalchemy.String(255), nullable=True)
|
||||
"""Runner ID that generated this item."""
|
||||
|
||||
created_at = sqlalchemy.Column(sqlalchemy.DateTime, nullable=False, default=datetime.datetime.utcnow)
|
||||
"""When this item was created."""
|
||||
|
||||
metadata_json = sqlalchemy.Column(sqlalchemy.Text, nullable=True)
|
||||
"""Additional metadata as JSON string (sender_id, platform, etc.)."""
|
||||
|
||||
# Indexes
|
||||
__table_args__ = (
|
||||
sqlalchemy.Index('ix_transcript_conversation_seq', 'conversation_id', 'seq'),
|
||||
sqlalchemy.Index('ix_transcript_conversation_created', 'conversation_id', 'created_at'),
|
||||
sqlalchemy.Index('ix_transcript_scope_seq', 'bot_id', 'workspace_id', 'conversation_id', 'thread_id', 'seq'),
|
||||
)
|
||||
Reference in New Issue
Block a user