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 Message type 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 topic attribute (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 blocking
  • stream_events() yields partial results as they become available, not just at completion
  • Each yielded event contains a kind attribute (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 kind or topic
  • 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 for just 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:

  1. Headers containing trace IDs are extracted from the event
  2. Context is restored in the current execution scope
  3. Downstream LLM or tool calls carry the same trace context
  4. 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 Message class from aworld/core/event/base.py to 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 with async for to maintain non-blocking I/O during external API calls.
  • Implement callback-driven agents: Pass an on_event callback to agents like PhoneAgent to react to specific event types without coupling execution logic to business logic.
  • Coordinate with Swarms: Enable event_driven=True when instantiating Swarm objects to merge multiple agent event streams into a single observable sequence.
  • Expose via SSE: Bridge internal event streams to web clients using FastAPI’s StreamingResponse with text/event-stream media type for real-time UI updates.
  • Instrument for tracing: Utilize the OpenTelemetry integration in instrumentation/eventbus.py to 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →