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 8063303cfa
commit 8db23bf950
18 changed files with 3705 additions and 60 deletions

View File

@@ -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 as artifacts; 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_compat)."""
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, artifact)."""
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 in ArtifactStore."""
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."""

View File

@@ -0,0 +1,72 @@
"""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 artifact 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."""
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)."""
# Artifact references
artifact_refs_json = sqlalchemy.Column(sqlalchemy.Text, nullable=True)
"""Artifact references as JSON string (list of ArtifactRef)."""
# Sequence for cursor-based pagination
seq = sqlalchemy.Column(sqlalchemy.Integer, nullable=False, index=True)
"""Sequence number within conversation (auto-increment per conversation)."""
# 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'),
)