ADR-0013: Event bus promotion — implementation shape¶
Status: Accepted (2026-05-25)
Status pass (2026-10-18): the decision holds; the code now lives in foundation/eventbus/. exact-match topics, no wildcard, no audit handler, singleton kept; the coding-side helpers are in the private iris_code tree
Context: Personal-assistant upgrade Phase 0 track 4. Companion: canonical doc §3.4 (parent design); ADR-Q9.
Context¶
The canonical doc §3.4 named promotion of the existing
src/iris_harness/coding/event_bus.py (a coding-agent–internal event bus, 365
LOC) to a runtime-wide package src/iris_harness/eventbus/ as a Phase 0 track.
The doc specified what to do (move generic infrastructure out, keep
coding-specific topics with their producer) but left the shape
decisions to the implementation phase.
Phase 0 track 4 executed the promotion (commit f23dcb0). Six
implementation decisions resolved during that work aren't in the parent
design and would be invisible to future readers without this record —
same gap that ADRs 0010, 0011, and 0012 closed for their respective
tracks.
Decision¶
Six implementation decisions:
-
Coding-specific convenience methods removed from
EventBus; replaced with module-level helper functions iniris_harness.coding.events. The old class had four methods (tool_call_context,stage_context,persona_context,pipeline_context) that each instantiated coding-specific context-manager classes with coding-specific payloads. The generic bus must not know about specific topics or payloads, so those methods became standalone helpers that takebusas the first argument:tool_call_context(bus, tool_name, args, ...)instead ofbus.tool_call_context(tool_name, args, ...). Eight call sites updated. -
Old
event_bus.pyis deleted (no compatibility shim). Afrom iris_harness.coding.event_bus import *shim would have eased migration, but: (a) all 5 caller files were known and updated in the same commit, (b) the shim adds an indirection layer that discourages converging on the new import paths, (c) pre-commit + tests catch any missed import. Clean delete. -
src/iris_harness/eventbus/topics.pyships empty (docstring only). The file is a deliberate scaffolding placeholder: Phase 1+ producers (email-triage, finance-statements, calendar-sync, reminders, tasks) will populate it with topic name constants and frozen-dataclass payloads as those subsystems land. Pre-populating with speculative constants before any consumer exists would be guessing. -
No wildcard
"*"subscription support added in this track. The canonical doc §3.4 mentions a wildcard-subscriber pattern for the audit handler. Adding it requires changing the handler signature (wildcard subscribers need to know which event fired) — a meaningful API extension. Deferred until a real cross-cutting subscriber (e.g., audit log) needs it. The dispatch loop inEventBus.emit/emit_syncdoes exact-match only today. -
No
audit_handler.pyadded in this track. The canonical doc §3.4 lists it as "optional: subscribes to*and appends todata/audit.db." Same reasoning as (4): defer until there's a real consumer. Adding it now would be speculation. -
Module-level singleton (
get_default_bus/reset_default_bus) is preserved as-is; not converted to DI. The canonical doc §3.4 says "Singleton lifecycle. IrisRuntime.startup() instantiates the bus. Skills/routines/heartbeats receive the bus via dependency injection, NOT via a global." The coding agent's existing code is module-singleton-based. Converting to DI would touch every coding-agent call site simultaneously — well beyond the promotion scope. The singleton stays for the coding-agent paths; Phase 1 (when skills start needing the bus injected into their contexts) is where the DI conversion belongs. Flagged as future ADR.
Alternatives considered¶
-
Keep convenience methods on
EventBus. Smaller move; less call-site churn. Rejected: generic bus would carry coding-specific knowledge forever; Phase 1+ producers would be tempted to add their ownbus.email_classified()etc. methods; god-class incrementally accreted. -
Subclass:
CodingEventBus(EventBus)adds the four methods. No call-site changes for coding-agent code. Rejected:get_default_bus()returns plainEventBus(the singleton pattern), so coding paths using it would lose the methods unless we add aget_coding_bus()(more API). Adds a class that exists only to host four methods. -
Compatibility shim at old path. Re-export from
src/iris_harness/coding/event_bus.pyfor one phase. Rejected: clutters the import graph; discourages converging on the new paths; indirection makes editor "go to definition" worse. -
Pre-populate
topics.pywith canonical-doc §3.4 topics from day one. Addsemail.new_arrived,email.classified,finance.statement_ingested,task.due_soon, etc. as constants before any producer exists. Rejected: speculative; producers should land with their topic constants in the same commit. Empty-stub file is honest about MVP scope. -
Convert singleton → DI in the same commit. Right architectural end state. Rejected for this track: touches every coding-agent call site that uses
get_default_bus(), which is well beyond the move's scope. Belongs in Phase 1 alongside the bus-injection into skill contexts. -
Add wildcard support in this commit. Cleaner future state. Rejected: requires an API change (handler signature for wildcard subscribers); no current consumer needs it; defer to the first real cross-cutting subscriber.
Consequences¶
Positive:
- Generic EventBus is now genuinely topic-agnostic; Phase 1+
producers can publish their topics + helpers in their own packages
without inflating the generic class.
- 4 directory-level boundaries make the runtime layering visible:
iris_harness.eventbus.bus (generic), iris_harness.eventbus.topics (runtime
payloads), iris_harness.coding.events (coding-pipeline payloads).
- 8 call-site updates were mechanical and bounded; pre-commit + tests
caught the one ruff/black interaction (# noqa: S603 had to
move down a line after black split a multi-line subprocess.run
call).
- 524 tests across coding + identity + knowledge + skills pass
unchanged; behavior preserved.
Negative:
- Singleton-vs-DI question is unresolved. The canonical doc names DI
as the right end state; this ADR explicitly defers it. Future
Phase 1 work will pick this up alongside skill-context injection,
and a future ADR will record that decision.
- topics.py is an empty file that exists only to document the
intent. Looks weird until Phase 1 producers populate it.
- Eight call-site changes from method-style to function-style
(bus.X(...) → X(bus, ...)) are mildly less ergonomic for
callers, even though architecturally correct.
Reversibility: moderate. Re-introducing the convenience methods
on EventBus is easy (re-add four methods that delegate to the
helpers). Re-introducing the old file path requires moving code back
and updating callers. The singleton-vs-DI deferral is a future
choice, not a lock-in.
Related ADRs¶
- ADR-Q9 / canonical doc §3.4 — parent design naming this track.
- ADR-0010 (entity extractor implementation shape), ADR-0011 (Keychain credentials shape), ADR-0012 (manifest schema extension shape) — sibling implementation-shape ADRs from Phase 0.
- A future ADR will record the singleton → DI conversion when Phase 1 skill-context injection lands.
- A future ADR may extend
EventBuswith wildcard"*"subscription support when the first cross-cutting subscriber (likely an audit handler) lands.