feat(agent-runner): add plugin runner host integration

This commit is contained in:
huanghuoguoguo
2026-06-20 10:18:52 +08:00
parent d22fa82d7c
commit cede35b31b
129 changed files with 26980 additions and 6209 deletions
@@ -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'),
)