Skip to main content

Overview

Middleware in the BeeAI Framework is code that runs “in the middle” of an execution lifecycle—intercepting the flow between when a component (like an Agent, Tool, or Model) starts and when it finishes. As these components execute, they emit events at key moments, such as starting a task, calling a tool, or completing a response . Middleware hooks into these events to inject behaviors like logging, filtering, or safety checks—all without modifying the component’s core logic. This modular approach allows you to apply consistent policies across your entire system. You can use built-in tools like GlobalTrajectoryMiddleware for immediate debugging, or write custom middleware to handle complex needs like blocking unsafe content, enforcing rate limits, or managing authentication.
Note on Terminology: In this framework, Middleware refers to the classic software design pattern (pipeline interceptors) that runs between execution steps. This is distinct from the industry term “Agentic Middleware,” which typically refers to entire orchestration platforms.

Built-in Middleware

The following section showcases built-in middleware that you can start using right away.

Global Trajectory

The fastest way to understand your agent’s execution flow is by using the GlobalTrajectoryMiddleware. It captures all events, including deeply-nested ones, and prints them to the console, using indentation to visualize the call stack . Example
Python
You can customize the output by passing parameters to the constructor: Example
Python
You can listen to events emitted throughout the execution to build your custom trajectory.

Tool Call Streaming

This middleware handles streaming tool calls in a ChatModel. It observes stream updates from the Chat Model and parses tool calls on demand so that they can be consumed immediately. It works even without streaming enabled, in which case it emits the update event at the end of the execution.. Example
Python
The following parameters can be passed to the constructor:

Core Primatives

The BeeAI Framework middleware is built on an underlying system of primitives, which are described in this section. Understanding these primitives is helpful for building complex middleware.

Events

An event refers to an action initiated by a component. It carries the details of what just happened within the system. Every event has three key properties:
  • Name: A string identifier (e.g., start, success, error, or custom names like fetch_data).
  • Data payload: The content of the event, typically astructured as a Pydantic model.
  • Metadata: Information about the context where the event was fired.
You process these events using callbacks that follow this structure:
Python

Emitter

The Emitter is the core component that lets you send and watch for events. While it is typically attached to a specific class, you can also use it on its own. An emitter instance is typically the child of a root emitter to which all events are propagated. Emitters can be nested (one can be a child of another), hence they internally create a tree hierarchy. Every emitter instance has the following properties:
  • namespace in which the emitter operates (eg: agents.requirement, tool.open_meteo, …).
  • creator class which the given emitter belongs to.
  • context (dictionary which is attached to all events emitted via the given emitter).
  • trace metadata (such as current id, run_id and parent_id)
It also gives you the following methods for managing listeners:
  • on for registering a new event listener
    • The method takes matcher (event name, callback, regex), callback (sync/async function), and options (priority, etc.)
    • The method can be used as a decorator or as a standalone function
  • off for deregistering an event listener
  • pipe for propagating all captured events to another emitter
  • child for creating a child emitter
The event’s path attribute is created by concatenating namespace with an event name (eg: backend.chat.ollama.start).
Example The following example depicts a minimal application that does the following:
  1. defines a data object for the fetch_data event,
  2. creates an emitter from the root one,
  3. registers a callback listening to the fetch_data event, which modifies its content,
  4. fires the fetch_data event,
  5. logs the modified event’s data.
Python
The emitter.on can be used directly and not just as a decorator. Example: emitter.on("fetch_data", callback).
If you name your function as either handle_{event_name} or on_{event_name}, then you don’t need to provide the event name as a parameter, as it gets inferred automatically.

Run (Context)

The Run class acts as a wrapper of the target implementation with its own lifecycle (an emitter with a set of events) and context (data that gets propagated to all events). The RunContext class is a container that stores information about the current execution context. These abstractions allow you to:
  • modify input to the given target (listen to a start event and modify the content of the input property),
  • modify output from the given target (listen to a success event and modify the content of the output property),
  • stop the run early (listen to a start event and set the output property to a non-None value),
  • propagate context (dictionary) to any component of your system,
  • cancel the execution in an arbitrary place,
  • gain observability into runs via structured events (for logging, tracing, and debugging).
The Run and Run Context gets created when a run method gets called on a framework class that can be executed (eg, ChatModel, Agent, …). The run object has the following methods:
  • The on method allows registering a callback to its emitter.
  • The middleware for registering middleware (a function that takes RunContext as a first parameter or a class with a bind method that takes the RunContext as a first parameter).
  • The context allows data to be set for a given execution. That data will then be propagated as metadata in every event that gets emitted.
The target implementation (handler) becomes part of the shared context (RunContext), which internally forms a hierarchical tree structure that shares the same context. In simpler terms, when you call one runnable (e.g., ChatModel) from within another runnable (e.g., Agent), the inner call (ChatModel) is attached to the context of the outer one (Agent).
The current execution context can be retrieved anytime by calling RunContext.get().

Runnable

The Runnable[R] class unifies common objects that can be executed and observed. It is an abstract class with the following traits:
  • It has an abstract run method that executes the class and returns a Run[R] (R is bound to the RunnableOutput).
  • It has an abstract emitter getter.
  • It has a middlewares getter that lists the existing middlewares.
Invoking a Runnable Every runnable takes a list of messages as its first (positional) parameter, followed by the following optional keyword arguments (RunnableOptions):
  • signal (an instance of AbortSignal) — allows aborting the execution.
  • context (a dictionary) — used to propagate additional data.
You can also pass extra arguments that may or may not be processed by the given handler. The RunnableOutput has the following properties:
  • output: a list of messages (can be empty)
  • context: a dictionary that can store additional data
  • last_message (getter): returns the last message if it exists, or creates an empty AssistantMessage otherwise
Creating a custom Runnable
Python

Event Handling

Building robust agents requires precise control over the execution lifecycle. You need the ability to not only observe your agent’s behavior but also intercept and modify it at specific points. The following sections covers the mechanics of the BeeAI Framework event system and will enable you to manage:
  • Scopes: Deciding whether to listen globally, per instance, or for a single run.
  • *Config: Controlling listener priority, persistence, and blocking behavior.
  • Lifecycle: Undestanding the exact sequence events that occur during execution.
  • Debugging: Inspecting raw event streams to see exactly what your agent is doing.
  • Piping: Linking emitters together via piping to create unified event streams.

Scopes

Events can be observed at three different levels. 1. Global Level Every emitter provided by the out-of-the-box modules is a child of the root emitter. This means you can listen to all events directly from the root emitter.
Python
Listeners that are bound “closer” to the source are executed earlier. For those that reside at the same level, the order can be altered by setting a priority value which is part of the EmitterOptions class. A higher priority value means the listener will be executed earlier. The default priority is 0.
2. Instance Level You can also listen to events emitted by a specific instance of a class.
Python
This registers a callback to the class’s emitter so that all events in a given class will be captured. 3. Run (Invocation) Level Sometimes you may want to listen to events emitted by a single run of a class.
Python
Here, the callback is registered on the run instance (created by the run method). The run’s emitter is a child of the class emitter, allowing you to modify behavior for a single invocation without affecting others.

Config

When working with multiple callbacks, you may need to control execution order, or ensure that some run exclusively. You can do this using the optional options argument of type EmitterOptions. Example
Python
Nested events Based on the value of the matcher parameter (the one that is used to match the event), the framework decides whether to include/exclude nested events (events created from children emitters or from piping). The default value of the match_nested depends on the matcher value. Note that the value can be set directly as shown in the example above.
If two events have the same priority, they are executed in the order they were added.

Lifecycle

When a framework component is executed, it creates a run context, which wraps the target handler and allows you to modify its input and output (Learn more in the Run (Context) section). Once a Run instance is executed (i.e., awaited), its lifecycle proceeds through the following steps:
  1. The start event is emitted.
  2. The target implementation is executed.
  3. Depending on the outcome, either a success or error event is emitted.
  4. Finally, the finish event is emitted.
The appropriate events are depicted in the following table: Below is an example showing how to listen to these events:
Python
In this example, create_internal_event_matcher ensures we correctly match the event.
You can retrieve the current run context at any time within a callback using RunContext.get().

Debugging

While the Global Trajectory middleware is excellent for visualizing the structural hierarchy of a run, sometimes you need to inspect the raw stream of events as they happen. To do this quickly without setting up a full middleware class, you can register a wildcard listener (*.*) directly on your run. This captures every single event emitted during that specific execution.
Python

Piping

In some cases, one might want to propagate all events from one emitter to another (for instance when creating a child emitter).

Creating Custom Middleware

While you can register individual callbacks to handle specific events, this approach can become cluttered if you have complex logic. To make your event handling reusable and modular, the BeeAI framework allows you to group listeners into a class called Middleware.

When to use Middleware vs. Callbacks

  • Use Callbacks (.on / .match): For simple, one-off logic, such as logging a specific event or debugging a single run.
  • Use Middleware: When the logic is complex, multi-step or needs to be reused across different parts of your application.

The Middleware Protocol

A middleware component is defined by how it interacts with the RunContext. It can be structured in two ways:
  1. A Function: A simple function that accepts RunContext as its first parameter.
  2. A Class: A class that implements a bind method, which accepts RunContext as its first parameter.
The RunContex provides access to the emitter, the instance being run, and the shared memory for that specific execution.

Example: Intercepting and Overriding

A common use case for middleware is intercepting a request before the target component executes to modify the input or provide a mock response. The following example demonstrates a middleware that intercepts the start event. By setting the output property on the event data, the middleware effectively “mocks” the result, preventing the actual ChatModel from running.
Python
Key Implementation Details:
  • create_internal_event_matcher: A helper used to ensure you are matching the specific internal event (like start / success / error / finish) for the correct component instance.
  • EmitterOptions: Used here to set priority=1 and is_blocking=True, ensuring this middleware executes early and takes precedence over other callbacks.
  • data.output: Setting this property during a start event signals the framework to skip the underlying execution (e.g., the LLM call) and return this value immediately.
Ensure that your mock response matches the expected output type of the component you are intercepting. For example, if you override a ChatModel, the return type must be ChatModelOutput.

Registering Middleware

Once defined, you can attach middleware to a component using the .middleware() method just before execution.
Python
Note that middleware is applied to the Run instance (the result of calling .run()), not the standalone emitter class itself. However, in some cases, middleware can be passed via the component’s constructor if supported.

Events glossary

The following sections list all events that can be observed for built-in components. Note that your tools/agents/etc. can emit additional events.

Tools

The following events can be observed when calling Tool.run(...).

Chat Models

The following events can be observed when calling ChatModel.run(...). Check out the in-code definition

Requirement Agent

Check out the in-code definition.

ToolCalling Agent

The following events can be observed by calling ToolCallingAgent.run(...). Check out the in-code definition.

ReAct Agent

The following events can be observed by calling ReActAgent.run(...). Check out the in-code definition Check out the in-code definition.

Workflow

The following events can be observed when calling Workflow.run(...). Check out the in-code definition.

LinePrefixParser

The following events are caught internally by the LinePrefixParser.

StreamToolCallMiddleware

The following events are caught internally by the StreamToolCallMiddleware. Check out the in-code definition.

GlobalTrajectoryMiddleware

The following events are handled internally by the GlobalTrajectoryMiddleware: All events inherit from the GlobalTrajectoryMiddlewareEvent class.
Python
The first element of the origin attribute is the original event (e.g., startRunContextStartEvent, etc.) that comes from the RunContext.

RunContext

Special events that are emitted before the target’s handler gets executed. A run event contains .run. in its event’s path and has internal set to true in the event’s context object. Check out the in-code definition.
Instead of a manual matching, use create_internal_event_matcher helper function.

Examples

Python

Explore reference middleware implementations in Python.

TypeScript

Explore reference middleware implementations in TypeScript.