00881a3e5f
CI / benchmark-publish (pull_request) Has been skipped
CI / lint (pull_request) Successful in 3m26s
CI / build (pull_request) Successful in 14s
CI / typecheck (pull_request) Successful in 4m2s
CI / quality (pull_request) Successful in 4m2s
CI / security (pull_request) Successful in 4m23s
CI / integration_tests (pull_request) Successful in 9m35s
CI / unit_tests (pull_request) Successful in 9m39s
CI / docker (pull_request) Successful in 2m15s
CI / e2e_tests (pull_request) Successful in 12m32s
CI / coverage (pull_request) Successful in 11m41s
CI / status-check (pull_request) Successful in 1s
CI / build (push) Successful in 23s
CI / lint (push) Successful in 3m17s
CI / typecheck (push) Successful in 4m11s
CI / quality (push) Successful in 4m11s
CI / security (push) Successful in 4m28s
CI / benchmark-regression (push) Has been skipped
CI / integration_tests (push) Successful in 9m13s
CI / unit_tests (push) Successful in 9m26s
CI / e2e_tests (push) Successful in 10m20s
CI / docker (push) Successful in 1m16s
CI / coverage (push) Successful in 12m2s
CI / status-check (push) Successful in 1s
CI / benchmark-publish (push) Successful in 33m17s
CI / benchmark-regression (pull_request) Successful in 57m23s
Verified and completed the Event System Domain Event Taxonomy implementation. Gap analysis: - EventType enum (38 types across 12 domains): ALREADY EXISTED, verified complete - DomainEvent Pydantic model (all 9 fields): ALREADY EXISTED, verified complete - EventBus Protocol (emit + subscribe): ALREADY EXISTED, verified complete - ReactiveEventBus (RxPY Subject, stream property): ALREADY EXISTED, verified - LoggingEventBus (structlog-based): ALREADY EXISTED, verified complete Added: - In-memory audit_log on ReactiveEventBus (list[DomainEvent] with defensive copy) - Emit ordering aligned with specification: RxPY stream push, then audit append, then handler dispatch (§Event System) - Clarified audit_log docstring: volatile in-memory log, not durable SQLite persistence (durable persistence wired separately via audit service layer) - Behave feature: features/observability/event_system_taxonomy.feature (36 scenarios) - Step definitions: features/steps/event_system_taxonomy_steps.py - Robot integration test: robot/event_system_taxonomy_integration.robot (13 test cases) - Robot helper: robot/helper_event_system_taxonomy.py (13 subcommands) - ASV benchmark: benchmarks/bench_event_bus.py (5 benchmark suites) - vulture_whitelist.py: added audit_log property Code review fixes applied: - Reordered emit() to match spec: on_next → audit_log → handlers (was: audit → on_next → handlers) - Clarified audit_log docstring to distinguish volatile in-memory log from spec SQLite table - Fixed benchmark TaxonomyAuditLogSuite: parameterized setup instead of inline construction - Added teardown to TaxonomyLoggingEmitSuite to restore logging.disable(NOTSET) - Clear _received list between benchmark iterations to reduce noise - Catch specific pydantic.ValidationError in frozen model mutation tests - Consolidated duplicate singular/plural step definitions - Tightened EventType count threshold from >=30 to >=38 Quality gates: - lint: PASSED - typecheck: PASSED (0 errors) - unit_tests: 36/36 new scenarios PASSED (pre-existing 12 flaky failures unchanged) - integration_tests: 1483/1483 PASSED - security_scan: PASSED - dead_code: PASSED ISSUES CLOSED: #587
222 lines
6.7 KiB
Markdown
222 lines
6.7 KiB
Markdown
# 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()
|
|
```
|