feat(agent-runner): add event-first context facts and pull APIs

Add EventLog and Transcript persistence entities for storing auditable
event facts and conversation history projection. Implement event-first
AgentRunContext builder that produces Protocol v1 compliant context
payloads with required fields: event, delivery, context (ContextAccess).

Key changes:
- EventLog ORM: auditable event records with indexes
- Transcript ORM: conversation history projection with composite indexes
- AgentRunContextBuilder: Protocol v1 payload with delivery, context, bootstrap
- EventLogStore/TranscriptStore: async stores for fact sources
- Host action handlers: HISTORY_PAGE, HISTORY_SEARCH, EVENT_GET, EVENT_PAGE
- Context validation: build_context output validates via SDK AgentRunContext
- Alembic migration for event_log and transcript tables
- Alembic env.py imports all ORM models for autogenerate discovery

Legacy compatibility: max-round messages go into bootstrap.messages and
compatibility.legacy_messages, not top-level messages field.
This commit is contained in:
huanghuoguoguo
2026-05-23 16:07:46 +08:00
parent 9086f77cc5
commit 0a3bafae4b
18 changed files with 3705 additions and 60 deletions

View File

@@ -13,6 +13,26 @@ from sqlalchemy.engine import Connection
from langbot.pkg.entity.persistence.base import Base
# Import all ORM models so they are registered with Base.metadata
# This is required for autogenerate to detect model changes
from langbot.pkg.entity.persistence import (
apikey,
bot,
bstorage,
event_log,
mcp,
metadata,
model,
monitoring,
pipeline,
plugin,
rag,
transcript,
user,
vector,
webhook,
)
target_metadata = Base.metadata

View File

@@ -0,0 +1,102 @@
"""add_event_log_and_transcript_tables
Revision ID: 58846a8d7a81
Revises: 0004_migrate_runner_config
Create Date: 2026-05-23 15:41:47.030841
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers
revision = '58846a8d7a81'
down_revision = '0004_migrate_runner_config'
branch_labels = None
depends_on = None
def upgrade() -> None:
# Create event_log table
op.create_table(
'event_log',
sa.Column('id', sa.Integer(), primary_key=True, autoincrement=True),
sa.Column('event_id', sa.String(255), nullable=False, unique=True),
sa.Column('event_type', sa.String(100), nullable=False),
sa.Column('event_time', sa.DateTime(), nullable=True),
sa.Column('source', sa.String(50), nullable=False),
sa.Column('bot_id', sa.String(255), nullable=True),
sa.Column('workspace_id', sa.String(255), nullable=True),
sa.Column('conversation_id', sa.String(255), nullable=True),
sa.Column('thread_id', sa.String(255), nullable=True),
sa.Column('actor_type', sa.String(50), nullable=True),
sa.Column('actor_id', sa.String(255), nullable=True),
sa.Column('actor_name', sa.String(255), nullable=True),
sa.Column('subject_type', sa.String(50), nullable=True),
sa.Column('subject_id', sa.String(255), nullable=True),
sa.Column('input_summary', sa.Text(), nullable=True),
sa.Column('input_json', sa.Text(), nullable=True),
sa.Column('raw_ref', sa.String(255), nullable=True),
sa.Column('run_id', sa.String(255), nullable=True),
sa.Column('runner_id', sa.String(255), nullable=True),
sa.Column('created_at', sa.DateTime(), nullable=False, server_default=sa.text('(CURRENT_TIMESTAMP)')),
sa.Column('metadata_json', sa.Text(), nullable=True),
)
# Create indexes for event_log
with op.batch_alter_table('event_log', schema=None) as batch_op:
batch_op.create_index('ix_event_log_event_id', ['event_id'], unique=True)
batch_op.create_index('ix_event_log_event_type', ['event_type'], unique=False)
batch_op.create_index('ix_event_log_bot_id', ['bot_id'], unique=False)
batch_op.create_index('ix_event_log_conversation_id', ['conversation_id'], unique=False)
batch_op.create_index('ix_event_log_run_id', ['run_id'], unique=False)
# Create transcript table
op.create_table(
'transcript',
sa.Column('id', sa.Integer(), primary_key=True, autoincrement=True),
sa.Column('transcript_id', sa.String(255), nullable=False, unique=True),
sa.Column('event_id', sa.String(255), nullable=False),
sa.Column('conversation_id', sa.String(255), nullable=False),
sa.Column('thread_id', sa.String(255), nullable=True),
sa.Column('role', sa.String(50), nullable=False),
sa.Column('item_type', sa.String(50), nullable=False, server_default='message'),
sa.Column('content', sa.Text(), nullable=True),
sa.Column('content_json', sa.Text(), nullable=True),
sa.Column('artifact_refs_json', sa.Text(), nullable=True),
sa.Column('seq', sa.Integer(), nullable=False),
sa.Column('run_id', sa.String(255), nullable=True),
sa.Column('runner_id', sa.String(255), nullable=True),
sa.Column('created_at', sa.DateTime(), nullable=False, server_default=sa.text('(CURRENT_TIMESTAMP)')),
sa.Column('metadata_json', sa.Text(), nullable=True),
)
# Create indexes for transcript
with op.batch_alter_table('transcript', schema=None) as batch_op:
batch_op.create_index('ix_transcript_transcript_id', ['transcript_id'], unique=True)
batch_op.create_index('ix_transcript_event_id', ['event_id'], unique=False)
batch_op.create_index('ix_transcript_conversation_id', ['conversation_id'], unique=False)
batch_op.create_index('ix_transcript_conversation_seq', ['conversation_id', 'seq'], unique=False)
batch_op.create_index('ix_transcript_conversation_created', ['conversation_id', 'created_at'], unique=False)
batch_op.create_index('ix_transcript_run_id', ['run_id'], unique=False)
def downgrade() -> None:
# Drop transcript table
with op.batch_alter_table('transcript', schema=None) as batch_op:
batch_op.drop_index('ix_transcript_run_id')
batch_op.drop_index('ix_transcript_conversation_created')
batch_op.drop_index('ix_transcript_conversation_seq')
batch_op.drop_index('ix_transcript_conversation_id')
batch_op.drop_index('ix_transcript_event_id')
batch_op.drop_index('ix_transcript_transcript_id')
op.drop_table('transcript')
# Drop event_log table
with op.batch_alter_table('event_log', schema=None) as batch_op:
batch_op.drop_index('ix_event_log_run_id')
batch_op.drop_index('ix_event_log_conversation_id')
batch_op.drop_index('ix_event_log_bot_id')
batch_op.drop_index('ix_event_log_event_type')
batch_op.drop_index('ix_event_log_event_id')
op.drop_table('event_log')