Event Bus Module

Typed publish-subscribe events for agents, tools, and sessions

Subscribe to agent lifecycle and tool events with a typed event bus — zero cost when nothing is listening.

python
from praisonaiagents import Agent
from praisonaiagents.bus import EventBus, EventType

bus = EventBus()

def on_tool_done(event):
    print(f"Tool finished: {event.data}")

bus.subscribe(on_tool_done, event_types=EventType.TOOL_COMPLETED)

agent = Agent(name="assistant", instructions="Be helpful")
agent.start("Say hello briefly.")

The user runs the agent; tool and lifecycle events publish to subscribers for logging, UI updates, or custom automation.

Diagram

🧠 Agent

📡 EventBus

🔧 Tool

👂 Subscriber

👂 Async subscriber

How It Works

Diagram
Event Bus ModuleAgentUserEvent Bus ModuleAgentUserRequestProcess requestResult Agent–>>User: Response

Quick Start

  1. Subscribe and publish
    python
    from praisonaiagents.bus import EventBus, EventType
    
    bus = EventBus()
    
    def on_message(event):
        print(f"Received: {event.data}")
    
    bus.subscribe(on_message, event_types=EventType.MESSAGE_CREATED)
    
    bus.publish(
        EventType.MESSAGE_CREATED,
        data={"text": "Hello, World!"},
        source="demo",
    )
  2. Global bus with an agent
    python
    from praisonaiagents import Agent
    from praisonaiagents.bus import get_default_bus, EventType
    
    bus = get_default_bus()
    
    bus.subscribe(
        lambda e: print(f"Agent started: {e.data}"),
        event_types=EventType.AGENT_STARTED,
    )
    bus.subscribe(
        lambda e: print(f"Tool used: {e.data}"),
        event_types=EventType.TOOL_COMPLETED,
    )
    
    agent = Agent(name="Assistant", instructions="Be helpful")
    agent.start("Hello!")

Event Types

Event TypeDescription
SESSION_CREATED / UPDATED / DELETED / FORKEDSession lifecycle
MESSAGE_CREATEDNew message added
TOOL_STARTED / TOOL_COMPLETEDTool execution
AGENT_STARTED / AGENT_COMPLETEDAgent execution
SUBAGENT_SPAWNED / COMPLETED / ERRORSub-agent coordination
SNAPSHOT_CREATEDFile snapshot
COMPACTION_COMPLETEDContext compaction
CUSTOMApplication-defined events

Durable Persistence

Attach a durable sink to persist every published event to SQLite across processes. In-memory get_history() already records every event; a durable sink adds cross-process persistence on top. See Durable Event Log for the full story.

python
from praisonaiagents.bus import get_default_bus, SqliteEventLog

bus = get_default_bus()
bus.attach_sink(SqliteEventLog())

EventBus.attach_sink(sink) / detach_sink(sink) -> bool / has_sinks manage sinks; a sink is any object implementing EventLogProtocol.append(event).


Common Patterns

Async subscriber:

python
import asyncio
from praisonaiagents.bus import EventBus, EventType

bus = EventBus()

async def async_handler(event):
    await asyncio.sleep(0.1)
    print(event.data)

bus.subscribe(async_handler, event_types=EventType.TOOL_COMPLETED)
# Even with no subscribers attached, this is still recorded to get_history()
# (was silently dropped on the async path before PR #5258).
await bus.publish_async(EventType.TOOL_COMPLETED, data={"tool": "bash"})

If an async subscriber raises when publish() is called inside a running event loop, the bus logs it as Error in subscriber <id>: ... (the same prefix sync-subscriber errors use), plus the traceback. Previously it surfaced only as Python’s Task exception was never retrieved warning at GC time.

Guard expensive payloads:

python
from praisonaiagents.bus import get_default_bus, EventType

bus = get_default_bus()

if bus.has_subscribers:
    bus.publish(
        EventType.CUSTOM,
        data={"snippet": expensive_summarise(text)},
        source="memory",
    )

Event history:

Every published event is recorded, whether or not subscribers are registered — no need to subscribe first. History is capped at the 1000 most recent events.

python
from praisonaiagents.bus import EventBus, EventType

bus = EventBus()                                     # no subscribers needed

bus.publish(EventType.CUSTOM, data={"index": 1})
bus.publish(EventType.CUSTOM, data={"index": 2})

recent = bus.get_history(limit=5)                    # -> two events

For cross-process persistence, attach a durable sink as well (see Durable Event Log).


Best Practices

Check has_subscribers before heavy work

Building summaries or embeddings for events nobody listens to wastes CPU — guard with bus.has_subscribers.

get_history() works without subscribers

Every published event lands in history regardless of subscribers, so get_history() is reliable for after-the-fact debugging on a bare bus. Since PR #4958 you no longer need a no-op subscriber to capture a record on the sync path; PR #5258 closed the same gap for publish_async(), so async lifecycle events fired before an observability subscriber attaches are no longer lost. For cross-process persistence, attach a durable sink (see Durable Event Log).

Filter by event_types

Pass event_types= to subscribe() so handlers only run for relevant events.

Use get_default_bus for cross-component wiring

The shared default bus lets agents, memory, and hooks emit events without passing a bus instance everywhere.

Prefer sync handlers unless you need async

Sync callbacks run inline; async handlers are awaited during publish_async only.

Async subscriber errors are logged, not raised

A raising async subscriber does not break publish() for other subscribers. Watch logs for Error in subscriber <id>:, or wrap the handler body in try/except to handle failures yourself.


Related