Skip to content

ADR-005: Event System Architecture

Metadata

  • Status: Accepted
  • Date: 2025-02-26
  • Decision Makers: Vijaykumar Singh
  • Related ADRs: ADR-001 (Agent Orchestration), ADR-002 (State Management)

Context

Victor needs an event system that:
- Provides observability into agent execution
- Enables real-time monitoring and debugging
- Supports event streaming to clients
- Allows middleware to intercept events
- Maintains performance for high-throughput scenarios

The challenge is balancing:
- Rich event data for observability
- Performance for production workloads
- Flexibility for different consumers
- Simplicity of the API

Decision

We will implement a Structured Event Model with typed events, middleware pipeline, and subscription support.

Architecture

Agent Execution
Event Emission
Middleware Pipeline (pre-processing)
Event Bus (Pub/Sub)
Subscribers (Metrics, Logging, Streaming, etc.)

Event Types

class EventType(str, Enum):
    # LLM Events
    THINKING = "thinking"           # Extended thinking
    CONTENT = "content"             # Text content

    # Tool Events
    TOOL_CALL = "tool_call"         # Tool invocation
    TOOL_RESULT = "tool_result"     # Tool response
    TOOL_ERROR = "tool_error"       # Tool failure

    # Agent Events
    STAGE_CHANGE = "stage_change"   # Conversation stage change
    ERROR = "error"                 # Agent error

    # System Events
    STREAM_START = "stream_start"   # Stream begin
    STREAM_END = "stream_end"       # Stream complete
    CHECKPOINT = "checkpoint"       # State saved

Event Structure

@dataclass
class AgentExecutionEvent:
    """Structured event from agent execution."""
    event_type: EventType
    timestamp: datetime
    correlation_id: str
    agent_id: str
    session_id: str
    content: Optional[str]
    metadata: Dict[str, Any]

Middleware Pipeline

class Middleware:
    """Base middleware for event processing."""

    async def pre_process(
        self,
        event: AgentExecutionEvent
    ) -> Optional[AgentExecutionEvent]:
        """Process event before emission."""
        pass

    async def post_process(
        self,
        event: AgentExecutionEvent
    ) -> Optional[AgentExecutionEvent]:
        """Process event after emission."""
        pass

Rationale

Why Structured Events?

Benefits:
- Type-safe event handling
- Self-documenting event schema
- Easy to serialize
- Better IDE support

Trade-offs:
- More boilerplate than unstructured events
- Need to define event types upfront

Why Middleware Pipeline?

Benefits:
- Consistent event processing
- Composable behaviors
- Easy to add new middleware
- Order of operations controlled

Trade-offs:
- Slightly more complex than direct emission
- Need to manage middleware order

Why Event Bus?

Benefits:
- Decoupled producers and consumers
- Multiple subscribers support
- Easy to add monitoring
- Natural streaming support

Trade-offs:
- More complex than direct callbacks
- Need to manage subscriptions

Consequences

Positive

  • Observable: Rich event data for monitoring
  • Extensible: Easy to add event consumers
  • Flexible: Middleware provides customization
  • Performant: Minimal overhead from event system
  • Debuggable: Events provide execution trace

Negative

  • Complexity: More moving parts
  • Learning Curve: Need to understand event types
  • Overhead: Event emission has cost

Neutral

  • API: Event emission is automatic
  • Performance: Optimized for low overhead
  • Compatibility: Non-breaking to add events

Implementation

Phase 1: Core Events (Completed)

  • ✅ Event type definitions
  • ✅ Event structure
  • ✅ Event emission from orchestrator
  • ✅ Basic event bus

Phase 2: Middleware (Completed)

  • ✅ Logging middleware
  • ✅ Metrics middleware
  • ✅ Secret masking middleware
  • ✅ Git safety middleware

Phase 3: Advanced Features (In Progress)

  • 🔄 Event filtering
  • 🔄 Event aggregation
  • 🔄 Event replay
  • 🔄 Event archiving

Code Example

Event Emission

from victor import Agent

agent = Agent.create()

# Events emitted automatically
async for event in agent.stream("Hello, World!"):
    if event.type == "content":
        print(event.content)
    elif event.type == "thinking":
        print("[Thinking...]")

Custom Middleware

from victor.framework.middleware import Middleware

class CustomMiddleware(Middleware):
    async def pre_process(self, event):
        print(f"Before: {event.event_type}")
        return event

    async def post_process(self, event):
        print(f"After: {event.event_type}")
        return event

# Add to agent
agent = Agent.create(middleware=[CustomMiddleware()])

Event Subscription

from victor.core.events import EventBus

bus = EventBus.get_instance()

async def my_subscriber(event):
    if event.event_type == "error":
        print(f"Error occurred: {event.content}")

# Subscribe
bus.subscribe(my_subscriber)

Alternatives Considered

1. Callback-based

Description: Direct callbacks instead of events

Rejected Because:
- Tightly coupled
- Hard to extend
- No natural streaming

2. Unstructured Events

Description: Simple dict-based events

Rejected Because:
- No type safety
- Poor documentation
- Hard to maintain

3. No Events

Description: No built-in event system

Rejected Because:
- No observability
- Hard to debug
- No streaming support

References

Revision History

Date Version Changes Author
2025-02-26 1.0 Initial ADR Vijaykumar Singh