By Sagar Shankaran, Founder of CallSphere
Implement the Observer pattern with an event bus for AI agent systems — enabling decoupled, publish-subscribe communication between agents for flexible coordination.
Key takeaways
In tightly coupled agent systems, Agent A calls Agent B directly, which calls Agent C. This creates rigid dependencies — changing one agent requires updating all agents that reference it. The Observer pattern eliminates this coupling by introducing an event bus. Agents publish events when something happens and subscribe to events they care about. No agent needs to know about any other agent's existence.
This decoupling makes the system easier to extend (add new agents without modifying existing ones), test (test each agent in isolation), and debug (trace events through the bus).
from dataclasses import dataclass, field
from datetime import datetime
from typing import Callable, Any
from collections import defaultdict
import asyncio
import uuid
@dataclass
class Event:
type: str
payload: Any
source: str
event_id: str = field(default_factory=lambda: str(uuid.uuid4()))
timestamp: datetime = field(default_factory=datetime.now)
EventHandler = Callable[[Event], Any]
class EventBus:
def __init__(self):
self._subscribers: dict[str, list[tuple[str, EventHandler]]] = (
defaultdict(list)
)
self._event_log: list[Event] = []
def subscribe(self, event_type: str, handler: EventHandler,
subscriber_name: str = "anonymous"):
self._subscribers[event_type].append(
(subscriber_name, handler)
)
print(f"{subscriber_name} subscribed to '{event_type}'")
def unsubscribe(self, event_type: str, subscriber_name: str):
self._subscribers[event_type] = [
(name, h) for name, h in self._subscribers[event_type]
if name != subscriber_name
]
def publish(self, event: Event):
self._event_log.append(event)
handlers = self._subscribers.get(event.type, [])
print(f"Event '{event.type}' from {event.source} -> "
f"{len(handlers)} subscribers")
for name, handler in handlers:
try:
handler(event)
except Exception as e:
print(f"Handler {name} failed for "
f"{event.type}: {e}")
def get_event_history(
self, event_type: str | None = None
) -> list[Event]:
if event_type:
return [e for e in self._event_log
if e.type == event_type]
return self._event_log.copy()
Each agent subscribes to events it cares about and publishes events when it completes work:
Hear it before you finish reading
Talk to a live CallSphere AI voice agent in your browser — 60 seconds, no signup.
flowchart LR
INPUT(["User intent"])
PARSE["Parse plus<br/>classify"]
PLAN["Plan and tool<br/>selection"]
AGENT["Agent loop<br/>LLM plus tools"]
GUARD{"Guardrails<br/>and policy"}
EXEC["Execute and<br/>verify result"]
OBS[("Trace and metrics")]
OUT(["Outcome plus<br/>next action"])
INPUT --> PARSE --> PLAN --> AGENT --> GUARD
GUARD -->|Pass| EXEC --> OUT
GUARD -->|Fail| AGENT
AGENT --> OBS
style AGENT fill:#4f46e5,stroke:#4338ca,color:#fff
style GUARD fill:#f59e0b,stroke:#d97706,color:#1f2937
style OBS fill:#ede9fe,stroke:#7c3aed,color:#1e1b4b
style OUT fill:#059669,stroke:#047857,color:#fff
import openai
client = openai.OpenAI()
class AnalysisAgent:
def __init__(self, bus: EventBus):
self.bus = bus
bus.subscribe("document.uploaded", self.on_document,
"AnalysisAgent")
def on_document(self, event: Event):
text = event.payload["text"]
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system",
"content": "Extract key topics as a JSON list."},
{"role": "user", "content": text},
],
response_format={"type": "json_object"},
)
import json
topics = json.loads(response.choices[0].message.content)
self.bus.publish(Event(
type="analysis.completed",
payload={"topics": topics,
"document_id": event.payload["document_id"]},
source="AnalysisAgent",
))
class NotificationAgent:
def __init__(self, bus: EventBus):
self.bus = bus
bus.subscribe("analysis.completed", self.on_analysis,
"NotificationAgent")
bus.subscribe("error.occurred", self.on_error,
"NotificationAgent")
def on_analysis(self, event: Event):
doc_id = event.payload["document_id"]
print(f"Notifying user: analysis complete for {doc_id}")
def on_error(self, event: Event):
print(f"ALERT: Error from {event.source}: "
f"{event.payload['message']}")
class LoggingAgent:
def __init__(self, bus: EventBus):
self.bus = bus
# Subscribe to all event types we want to log
for event_type in ["document.uploaded",
"analysis.completed",
"error.occurred"]:
bus.subscribe(event_type, self.log_event,
"LoggingAgent")
def log_event(self, event: Event):
print(f"[LOG] {event.timestamp} | {event.type} | "
f"{event.source} | {event.event_id}")
bus = EventBus()
# Initialize agents — they self-register with the bus
analysis = AnalysisAgent(bus)
notifications = NotificationAgent(bus)
logging_agent = LoggingAgent(bus)
# Trigger the chain by publishing a document upload event
bus.publish(Event(
type="document.uploaded",
payload={
"document_id": "doc-123",
"text": "AI agents are transforming enterprise software..."
},
source="UploadService",
))
# This triggers: AnalysisAgent -> publishes analysis.completed
# -> NotificationAgent receives it
# -> LoggingAgent logs everything
Adding a new agent — say, a StorageAgent that archives analysis results — requires zero changes to existing agents. You simply create the new agent, subscribe it to analysis.completed, and it starts receiving events. This extensibility is what makes the Observer pattern valuable at scale.
Implement event deduplication using the event_id field and a seen-events set. You can also add rate limiting to the event bus by tracking events per second per type and dropping or queuing events that exceed the threshold. Additionally, avoid circular event chains where Event A triggers Event B which triggers Event A.
Still reading? Stop comparing — try CallSphere live.
CallSphere ships complete AI voice agents per industry — 14 tools for healthcare, 10 agents for real estate, 4 specialists for salons. See how it actually handles a call before you book a demo.
For a single-process agent system, an in-memory bus is simpler and faster. For distributed systems with agents running in separate containers or machines, use Redis Pub/Sub or RabbitMQ. The interface stays the same — only the transport layer changes.
Add a sequence number to events from the same source and buffer events in subscribers that require ordering. Process buffered events only when all preceding sequence numbers have arrived. For most agent workloads, however, event ordering is not critical.
#AgentDesignPatterns #ObserverPattern #EventDriven #Python #AgenticAI #LearnAI #AIEngineering

Written by
Sagar Shankaran· Founder, CallSphere
LinkedInSagar Shankaran is the founder of CallSphere, where he builds production AI voice and chat agents deployed across healthcare, hospitality, real estate, and home services. He writes about agentic AI, LLM engineering, and shipping voice agents that handle real calls in production.
See how AI voice agents work for your industry. Live demo available -- no signup required.
The 2026 desktop AI agent landscape — ServiceNow Project Arc, Anthropic Claude offerings, OpenAI agents, and Google Mariner. A buyer's map.
Step-by-step build of a working agent with the OpenAI Agents SDK — Agent class, tools, handoffs, tracing — plus an eval pipeline that catches regressions before merge.
An agentic-AI perspective on Anthropic Skills system, covering orchestration patterns, tool use, and how agent tooling fits production agent stacks.
Enterprise CIO Guide perspective on Comet's general-availability launch put an agentic browser in front of millions of consumers, and it works better than the demos suggested.
Enterprise CIO Guide perspective on Harvey AI's enterprise rollout numbers show legal agents have moved past the pilot stage at AmLaw 100 firms.
Enterprise CIO Guide perspective on Hippocratic AI's deployment numbers show healthcare voice agents are moving from pilot to production across major US health systems.
© 2026 CallSphere Inc. All rights reserved.
Made within San Francisco