# Event Bus

The **EventBus** infrastructure provides a general-purpose domain event system
for CleverAgents.  Domain services emit typed events at significant state changes
(plan creation, decision recording, phase transitions, etc.), enabling audit
logging, reactive subscribers, and observability without coupling services to
one another.

---

## Overview

The system consists of four cooperating components:

| Component | Role |
|-----------|------|
| `EventType` | `StrEnum` of all domain event identifiers |
| `DomainEvent` | Frozen Pydantic model carrying event data |
| `EventBus` | `Protocol` that all bus implementations satisfy |
| `ReactiveEventBus` | RxPY-backed in-process bus (default) |
| `LoggingEventBus` | Structured-log-only bus (audit / test) |

---

## EventType

`cleveragents.infrastructure.events.EventType` is a `StrEnum` whose members
cover all domains:

```python
from cleveragents.infrastructure.events import EventType

print(EventType.PLAN_CREATED)        # "plan.created"
print(EventType.DECISION_CREATED)    # "decision.created"
print(EventType.ACTOR_COMPLETED)     # "actor.completed"
```

**Categories:**

| Category | Examples |
|----------|---------|
| Plan lifecycle | `plan.created`, `plan.phase_changed`, `plan.applied` |
| Decision | `decision.created`, `decision.superseded` |
| Actor | `actor.invoked`, `actor.completed`, `actor.errored` |
| Tool | `tool.invoked`, `tool.completed`, `tool.errored` |
| Resource | `resource.accessed`, `resource.modified` |
| Sandbox | `sandbox.created`, `checkpoint.restored` |
| Validation | `validation.started`, `validation.passed` |
| Session | `session.created`, `session.message_sent` |
| Budget | `budget.warning`, `budget.exceeded` |

---

## DomainEvent

All events are instances of the frozen Pydantic model
`cleveragents.infrastructure.events.DomainEvent`:

```python
from cleveragents.infrastructure.events import DomainEvent, EventType

event = DomainEvent(
    event_type=EventType.PLAN_CREATED,
    plan_id="01ARZ3NDEKTSV4RRFFQ69G5FAV",
    actor_name="strategists/planner",
    details={"phase": "strategize"},
)
```

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `event_type` | `EventType` | required | The type of event |
| `timestamp` | `datetime` | `now(UTC)` | UTC time of emission |
| `correlation_id` | `str` | auto ULID | Links related events in a request chain |
| `plan_id` | `str \| None` | `None` | Associated plan ULID |
| `root_plan_id` | `str \| None` | `None` | Root plan in a hierarchy |
| `session_id` | `str \| None` | `None` | Associated session ULID |
| `actor_name` | `str \| None` | `None` | Actor that emitted the event |
| `project_name` | `str \| None` | `None` | Project context |
| `details` | `dict` | `{}` | Event-type-specific payload |

`DomainEvent` is **immutable** (`frozen=True`).  Attempting to set an attribute
after construction raises a `ValidationError`.

---

## EventBus Protocol

`cleveragents.infrastructure.events.EventBus` is a `@runtime_checkable`
structural protocol.  Any object with `emit()` and `subscribe()` methods
satisfies it:

```python
from cleveragents.infrastructure.events import EventBus, ReactiveEventBus

bus = ReactiveEventBus(max_audit_log_size=5000)  # optional retention cap
assert isinstance(bus, EventBus)  # True
```

**Interface:**

```python
class EventBus(Protocol):
    def emit(self, event: DomainEvent) -> None: ...
    def subscribe(self, event_type: EventType,
                  handler: Callable[[DomainEvent], None]) -> None: ...
```

---

## ReactiveEventBus

`cleveragents.infrastructure.events.ReactiveEventBus` is the default
in-process implementation.  It uses an RxPY `Subject` as its hot observable
backbone.

```python
from cleveragents.infrastructure.events import (
    DomainEvent, EventType, ReactiveEventBus,
)

bus = ReactiveEventBus()

# Subscribe a plain callback
bus.subscribe(EventType.PLAN_CREATED, lambda e: print("Plan created:", e.plan_id))

# Use the read-only RxPY stream for advanced operators
bus.stream.pipe(
    rx.operators.filter(lambda e: e.plan_id is not None),
).subscribe(lambda e: print("Plan event:", e))

# Emit an event
bus.emit(DomainEvent(event_type=EventType.PLAN_CREATED, plan_id="01ABC..."))
```

- `emit(event)` pushes to the RxPY Subject **and** calls all type-specific
  handlers synchronously.
- `subscribe(event_type, handler)` registers a callback for one event type.
- `stream` exposes a read-only `rx.Observable` view for RxPY operators.
- `clear_audit_log()` clears retained in-memory events when needed.

---

## LoggingEventBus

`cleveragents.infrastructure.events.LoggingEventBus` logs every event to
`structlog` at `INFO` level.  It is useful for audit trails, CI pipelines, and
environments where RxPY overhead is undesirable.

```python
from cleveragents.infrastructure.events import LoggingEventBus, EventType, DomainEvent

bus = LoggingEventBus()
bus.emit(DomainEvent(event_type=EventType.DECISION_CREATED, plan_id="01ABC..."))
# → structured log: {"event": "domain_event", "event_type": "decision.created", ...}
```

---

## Dependency Injection

The DI container (`cleveragents.application.container.Container`) registers
`ReactiveEventBus` as a **Singleton** provider named `event_bus`.  It is
automatically wired into `DecisionService` and `PlanLifecycleService`:

```python
from cleveragents.application.container import Container

container = Container()
bus = container.event_bus()   # ReactiveEventBus singleton
```

To substitute a different implementation (e.g. in tests):

```python
from cleveragents.application.container import Container
from cleveragents.infrastructure.events import LoggingEventBus
from dependency_injector import providers

container = Container()
container.event_bus.override(providers.Singleton(LoggingEventBus))
```

---

## Service Event Emission

### DecisionService

`cleveragents.application.services.decision_service.DecisionService.record_decision`
emits **`DECISION_CREATED`** after a decision is persisted.

### PlanLifecycleService

| Method | Event emitted |
|--------|---------------|
| `use_action(...)` | `PLAN_CREATED` |
| `execute_plan(...)` | `PLAN_PHASE_CHANGED` |

Both services accept an optional `event_bus` constructor parameter; when
`None`, event emission is silently skipped (backward-compatible).

---

## Example: Subscribing to Plan Events

```python
from cleveragents.application.container import Container
from cleveragents.infrastructure.events import EventType, DomainEvent

container = Container()
bus = container.event_bus()

def on_plan_created(event: DomainEvent) -> None:
    print(f"New plan: {event.plan_id} | details: {event.details}")

bus.subscribe(EventType.PLAN_CREATED, on_plan_created)

# All subsequent plan.create operations will call on_plan_created
svc = container.plan_lifecycle_service()
```
