How to Implement an Event-Driven Asynchronous Architecture for AI Agents
Wrap every agent interaction in asynchronous event objects, consume them via async for streams, and orchestrate multiple agents using callbacks and event buses to build scalable, non-blocking AI systems.
The ai-agent-book repository demonstrates a production-ready approach to building AI agents that react to events asynchronously rather than blocking on synchronous API calls. By standardizing all interactions—LLM calls, tool invocations, and internal state changes—as typed event messages and processing them through Python’s asyncio framework, you can create responsive agent systems that handle concurrent tasks while maintaining observability.
The Event Model: Standardizing Agent Communication
Central to this architecture is the Message class defined in aworld/core/event/base.py. Every interaction in the system gets wrapped in this uniform event object, which carries not just the payload but also tracing headers, topic identifiers, and unique event IDs.
According to the source code in chapter8/gaia-experience/AWorld/core/event/base.py, events are structured to support:
- Payload agnosticism: The same
Messagetype handles LLM responses, tool results, and user inputs - Built-in tracing: Each message optionally contains OpenTelemetry context for distributed tracing
- Topic routing: Events can be filtered and routed using the
topicattribute (e.g.,Constants.PLAN)
This standardization allows the system to treat every step of an agent’s execution—whether calling an LLM or querying a database—as an event that can be logged, streamed, or intercepted.
Async Execution with Streaming Events
The execution layer leverages Python’s asyncio to prevent blocking during I/O-heavy operations. The Runners class provides the primary entry point for executing tasks asynchronously.
In experience_agent.py, the pattern looks like this:
from aworld.core.runner import Runners
async def execute_task(self, task):
# Returns a coroutine that yields events as they occur
runner = Runners.streamed_run_task(task)
async for event in runner.stream_events():
# event is a Message instance
yield event
Key implementation details from the repository:
Runners.streamed_run_task()initiates execution without blockingstream_events()yields partial results as they become available, not just at completion- Each yielded event contains a
kindattribute (e.g.,"tool_call","llm_response") for discrimination
This approach keeps the event loop unblocked during long-running LLM inference or HTTP tool calls, allowing the same process to manage multiple concurrent agents.
Implementing Event-Driven Agents with Callbacks
Individual agents consume events through optional callbacks rather than polling. The PhoneAgent implementation in chapter9/phone-agent/agent.py demonstrates this pattern.
from chapter9.phone_agent.agent import PhoneAgent
def on_event(kind: str, payload) -> None:
# Filter by event type
if kind == "llm_response":
update_chat_ui(payload)
elif kind == "tool_call":
log_tool_execution(payload)
agent = PhoneAgent(on_event=on_event)
# Run asynchronously
import asyncio
asyncio.run(agent.run("What's the weather in Tokyo?"))
The on_event callback receives every event emitted during execution, enabling:
- Real-time UI updates: Display partial LLM tokens as they stream in
- Conditional logic: Branch execution based on event
kindortopic - Side effects: Log to databases, trigger alerts, or update external systems without blocking the main execution flow
Coordinating Multi-Agent Swarms via Event Bus
For systems requiring multiple cooperating agents, the repository provides a Swarm class that operates in event_driven=True mode. This forwards events from each member agent to a shared event bus, maintaining a coherent execution trace across the entire swarm.
from aworld.core.agent import Swarm
# Initialize agents
search_agent = SearchAgent()
summarizer_agent = SummarizerAgent()
# Create event-driven swarm
swarm = Swarm(search_agent, summarizer_agent, event_driven=True)
async def orchestrate(query: str):
async for event in swarm.run(query):
# Events from both agents appear here
print(f"[{event.topic}] {event.payload}")
import asyncio
asyncio.run(orchestrate("Explain quantum computing"))
The event_driven parameter (demonstrated in test_state_manager.py) ensures that:
- Events from concurrent agents are merged into a single stream
- Each event preserves its origin agent’s context
- The swarm can be consumed via
async forjust like a single agent
Streaming Events to Web Clients with SSE
To expose these events to front-end clients, the repository implements Server-Sent Events (SSE) through FastAPI. The gaia_agent_server.py file shows how to bridge the internal event stream to HTTP.
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
import json
app = FastAPI()
@app.get("/agent/events")
async def event_stream():
async def generator():
task = {"messages": [{"role": "user", "content": "Analyze this data"}]}
runner = Runners.streamed_run_task(task)
async for event in runner.stream_events():
# Serialize Message to JSON and format as SSE
yield f"data: {json.dumps(event.to_dict())}\n\n"
return StreamingResponse(
generator(),
media_type="text/event-stream"
)
This pattern enables browsers to receive real-time updates as the agent thinks, searches, and responds, creating a conversational UX where users see tool calls and partial responses immediately.
Observability Through Event Instrumentation
The architecture includes OpenTelemetry integration to trace events across async boundaries. In instrumentation/eventbus.py, the system extracts trace context from incoming events and reattaches it to outgoing calls.
When a Message traverses the event bus:
- Headers containing trace IDs are extracted from the event
- Context is restored in the current execution scope
- Downstream LLM or tool calls carry the same trace context
- Spans are automatically created for event processing time
This gives you end-to-end visibility into an agent’s execution path across multiple async hops and service boundaries.
Summary
- Standardize on event objects: Use the
Messageclass fromaworld/core/event/base.pyto wrap all agent interactions, ensuring consistent handling of LLM calls, tool results, and state changes. - Leverage asyncio for concurrency: Execute tasks via
Runners.streamed_run_task()and consume results withasync forto maintain non-blocking I/O during external API calls. - Implement callback-driven agents: Pass an
on_eventcallback to agents likePhoneAgentto react to specific event types without coupling execution logic to business logic. - Coordinate with Swarms: Enable
event_driven=Truewhen instantiatingSwarmobjects to merge multiple agent event streams into a single observable sequence. - Expose via SSE: Bridge internal event streams to web clients using FastAPI’s
StreamingResponsewithtext/event-streammedia type for real-time UI updates. - Instrument for tracing: Utilize the OpenTelemetry integration in
instrumentation/eventbus.pyto maintain trace context across async event boundaries.
Frequently Asked Questions
How does the event-driven architecture improve AI agent performance?
The architecture decouples execution from waiting. By treating LLM calls and tool invocations as async events rather than blocking function calls, the system can handle multiple agent instances concurrently within a single event loop. This eliminates idle CPU time during network I/O and enables horizontal scaling patterns where one process manages dozens of simultaneous agent tasks.
What is the difference between on_event callbacks and async for streaming?
Callbacks provide targeted interception while streaming offers full consumption. The on_event callback (implemented in PhoneAgent) allows you to inject side effects—like logging or UI updates—at specific event types without managing the full stream. The async for pattern (used with Runners.streamed_run_task()) gives you complete control over the event sequence, useful when you need to implement custom filtering, buffering, or branching logic in the orchestration layer.
Can I use this pattern with existing synchronous LLM libraries?
Yes, by wrapping blocking calls in asyncio.to_thread() or loop.run_in_executor(). While the repository uses native async clients, you can adapt synchronous SDKs by executing their blocking methods in a thread pool and wrapping the results in Message events. The experience_agent.py implementation shows where to inject these wrappers—specifically in the task execution layer before events enter the stream_events() generator.
How do I maintain trace context when events cross agent boundaries?
Use the built-in OpenTelemetry instrumentation. The instrumentation/eventbus.py module automatically extracts trace headers from incoming Message objects and reattaches them to the current context using OpenTelemetry’s context propagation API. This ensures that when Agent A emits an event consumed by Agent B, both agents’ spans appear as children of the same parent trace, giving you a complete view of multi-agent workflows.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →