> ## Documentation Index
> Fetch the complete documentation index at: https://docs.ooneex.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Middleware

> Observe and transform every stage of an agent run with lifecycle hooks

Middleware lets you hook into an agent run without touching the model, the tools, or the agent class. A middleware can observe what happens, transform the configuration and streamed chunks, or react to the run's terminal outcome. You describe one as a class implementing `IMiddleware` and register it with `decorator.middleware()`.

## Defining middleware

The only required member is `getName`; every hook is optional. Implement just the hooks you need:

```typescript theme={null}
import { decorator } from "@ooneex/ai";
import type { IMiddleware } from "@ooneex/ai";

@decorator.middleware()
class AuditMiddleware implements IMiddleware<{ userId: string }> {
  public getName = (): string => "audit";

  public onStart = (ctx) =>
    ctx.defer(audit.write({ userId: ctx.context.userId, requestId: ctx.requestId }));

  public onFinish = (ctx, info) =>
    ctx.defer(audit.write({ event: "finish", tokens: info.usage }));
}
```

The type parameter on `IMiddleware<TContext>` types the [runtime context](/ai/agents/running#metadata-vs-context) read via `ctx.context`.

Add it to an agent's `getMiddlewares`, or pass it per request:

```typescript theme={null}
public getMiddlewares = (): AiMiddlewareClassType[] => [AuditMiddleware];

// or, for a single call:
await chat.run<string>({ prompt: "Hi", middlewares: [AuditMiddleware] });
```

## Lifecycle hooks

Hooks fire across the run's lifecycle. All are optional.

| Hook                       | When it fires                                                                   |
| -------------------------- | ------------------------------------------------------------------------------- |
| `setup`                    | First, before any `onConfig` — provision capabilities for later middleware.     |
| `onConfig`                 | At init and once per agent iteration — observe or transform the run config.     |
| `onBeforeModel`            | Immediately before each model call — adjust per-iteration knobs.                |
| `onStructuredOutputConfig` | Before the final structured-output call — transform its config and JSON Schema. |
| `onStart`                  | Once, when the run starts.                                                      |
| `onIteration`              | At the start of each agent loop iteration.                                      |
| `onChunk`                  | For every streamed chunk — pass through, replace, expand, or drop it.           |
| `onToolPhaseComplete`      | After all tool calls in an iteration are processed.                             |
| `onUsage`                  | Once per iteration that reports token usage.                                    |
| `onFinish`                 | Terminal — the run completed normally.                                          |
| `onAbort`                  | Terminal — the run was aborted.                                                 |
| `onError`                  | Terminal — an unhandled error occurred.                                         |

The three terminal hooks (`onFinish`, `onAbort`, `onError`) are mutually exclusive — exactly one fires per run.

## Composition

Middleware composes in the order it appears in the array — the agent's own middleware first, then any passed per request. The hooks combine differently depending on their role:

* **Piped** — `onConfig`, `onStructuredOutputConfig`, and `onChunk` each receive the previous middleware's output, so transformations chain.
* **First-win** — `onBeforeToolCall` stops at the first middleware that returns a decision.
* **Sequential** — the remaining hooks simply run one after another.

## Transforming the request

Config hooks return a partial config that is shallow-merged into the run. Use `onConfig` to adjust the whole run, or `onBeforeModel` for per-iteration tweaks like raising the temperature on a retry:

```typescript theme={null}
@decorator.middleware()
class RetryHeatMiddleware implements IMiddleware {
  public getName = (): string => "retry-heat";

  public onBeforeModel = (ctx, config) => {
    if (ctx.iteration > 0) {
      return { temperature: Math.min((config.temperature ?? 0.7) + 0.1, 2) };
    }
  };
}
```

Return `void` (or nothing) to pass the config through unchanged.

## Filtering chunks

`onChunk` sees every streamed chunk and decides its fate. Return a chunk to replace it, an array to expand it, `null` to drop it, or `void` to pass it through:

```typescript theme={null}
@decorator.middleware()
class RedactMiddleware implements IMiddleware {
  public getName = (): string => "redact";

  public onChunk = (_ctx, chunk) => {
    if (chunk.type === "TEXT_MESSAGE_CONTENT") {
      return { ...chunk, delta: chunk.delta.replace(/\d{16}/g, "[redacted]") };
    }
  };
}
```

Dropped chunks are not seen by later middleware.

<Note>
  Use `ctx.defer(promise)` inside a hook to run side effects (logging, audit writes) without blocking the run — they are awaited as part of the run's completion.
</Note>
