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

# Agent Middleware in Laravel AI SDK

> Learn how middleware runs on each model call in the Laravel 13 AI SDK, how to process responses, and how to stop based on cumulative tokens.

When an agent uses several tools, you may want to **check usage before calling the model and record each step's response**. Laravel AI SDK middleware does not wrap the whole agent just once. It runs on every step of the generation. Based on the official Laravel 13 documentation and the `v1.3.0` implementation of `laravel/ai`, this page goes a step beyond the [AI SDK basics](/en/ai-sdk) and explains execution order and stop conditions.

## The unit that middleware runs on

When an agent receives a model response, runs tools, and calls the model again, the next model call is a **separate step**. Right before each step, the middleware returned from `HasMiddleware::middleware()` runs in the order it was declared.

The `PendingStep` passed to the middleware's `handle(PendingStep $step, Closure $next)` contains `steps`, an array of completed steps, `usage`, their cumulative usage, and `number`, the current zero-based step number. On the first step, `usage` does not yet include the usage of the current model call.

```mermaid theme={null}
flowchart TD
    A["Next PendingStep<br>Cumulative usage of completed steps"] --> B["Evaluate in middleware() declaration order"]
    B --> C{"Stop condition met?"}
    C -->|Yes| D["Finish without calling the model for this step"]
    C -->|No| E["Call the model"]
    E --> F["Receive StepResponse"]
    F --> G["Run then() callback"]
    G --> H["Run tools if needed"]
    H --> I{"Another step needed?"}
    I -->|Yes| A
    I -->|No| J["Finalize the agent response"]
```

The `then()` attached to the result of `$next($step)` runs **after the model responds and before any tool calls run**. It is not a hook that is called only after tools run. Also, `usage` at this point is the total of previously completed steps, and it does not predict the token count of the in-progress request.

## Registering per-step processing

This example logs only the step number and the number of input tokens used so far. Avoid unconditionally saving prompt text or model output to logs.

```php theme={null}
<?php

namespace App\Ai\Middleware;

use Closure;
use Illuminate\Support\Facades\Log;
use Laravel\Ai\Gateway\StepResponse;
use Laravel\Ai\PendingStep;

final class LogAgentSteps
{
    public function handle(PendingStep $step, Closure $next)
    {
        Log::info('AI step starting', [
            'step' => $step->number,
            'input_tokens_used' => $step->usage->inputTokens,
        ]);

        return $next($step)->then(function (StepResponse $response) use ($step): void {
            Log::info('AI model responded', ['step' => $step->number]);
        });
    }
}
```

Implement `HasMiddleware` on the agent and return instances from `middleware()`.

```php theme={null}
<?php

namespace App\Ai\Agents;

use App\Ai\Middleware\LogAgentSteps;
use Laravel\Ai\Contracts\Agent;
use Laravel\Ai\Contracts\HasMiddleware;
use Laravel\Ai\Middleware\Stop;
use Laravel\Ai\PendingStep;
use Laravel\Ai\Promptable;

class SupportAgent implements Agent, HasMiddleware
{
    use Promptable;

    public function instructions(): string
    {
        return 'Answer support questions concisely.';
    }

    public function middleware(): array
    {
        return [
            Stop::when(fn (PendingStep $step): bool => $step->usage->inputTokens >= 50_000),
            new LogAgentSteps,
        ];
    }
}
```

Put `Stop` first in the `middleware()` list. When the limit is exceeded and **the next step does not start**, the subsequent `LogAgentSteps` does not run for that step. If you reverse the order, only the "starting" log may be recorded, without the "model responded" log. Return the result of `$next($step)` from `handle()`. The `then()` callback does not need to return the response.

## Stop prevents the next model call

`Stop::when(...)` ends processing before the step when the condition is true, and `Stop::unless(...)` does so when the condition is false. The example above skips the next call once the cumulative input tokens of **completed steps** reach 50,000 or more. It is not a hard limit that forcibly stops a single model call before it crosses the threshold.

| When it is evaluated | Result when the condition is met |
| - | - |
| Before calling the first step | The model is not called, and both the completed steps and the response text are empty |
| Before calling the next step after tools run | Finishes while keeping the steps and usage completed so far |
| Before calling the next step in streaming | The stream's finish reason becomes `cancelled` |

> [!WARNING]
>
> `Stop` does not abort an in-progress request to the provider. If you need a strict guarantee on usage limits, do not rely only on next-step checks. Also consider limits on the model or provider side.

Middleware that wants to stop on a custom condition can also return `Laravel\Ai\Gateway\StepResult::stop()`. From `handle()`, return the result of `$next($step)`, a `StepResponse`, or `StepResult::stop()`. Returning an unrelated type throws an exception.

## Verifying execution order

To avoid confusing model responses with tool execution, check at least the following cases.

* For a single-step generation, the starting log and the model response log each appear once.
* For additional steps following tools, the number increases from 0, and the `usage` of later steps includes the usage of earlier steps.
* When the stop condition is met from the start, no model call and no `LogAgentSteps` logs occur.
* When the condition is reached partway through, the earlier steps remain and the model call for the next step does not occur.

Separate the normal path from the stopping path and check how many times each step runs. If `then()` needs side effects, design them on the assumption that it is not called on paths that fail or stop before a model response is received.

## Primary sources

* [Laravel 13 official documentation: AI SDK middleware](https://github.com/laravel/docs/blob/13.x/ai-sdk.md)
* [Laravel AI SDK v1.3.0: PendingStep values](https://github.com/laravel/ai/blob/v1.3.0/src/PendingStep.php#L18-L46)
* [Laravel AI SDK v1.3.0: Generation loop](https://github.com/laravel/ai/blob/v1.3.0/src/Gateway/TextGenerationLoop.php#L133-L188) and [middleware execution order](https://github.com/laravel/ai/blob/v1.3.0/src/Gateway/TextGenerationLoop.php#L441-L468)
* [Laravel AI SDK v1.3.0: StepResult::stop() and then()](https://github.com/laravel/ai/blob/v1.3.0/src/Gateway/StepResult.php#L37-L59)
* [Laravel AI SDK v1.3.0: Stop middleware](https://github.com/laravel/ai/blob/v1.3.0/src/Middleware/Stop.php)
* [Laravel AI SDK v1.3.0: Step stopping tests](https://github.com/laravel/ai/blob/v1.3.0/tests/Feature/AgentMiddlewareTest.php#L300-L345)


## Related topics

- [Advanced Topics](/en/advanced/index.md)
- [Laravel AI SDK](/en/ai-sdk.md)
- [Laravel Agent Detector — AI agent detection package](/en/blog/agent-detector-introduction.md)
- [Laravel AI SDK integration](/en/packages/laravel-copilot-sdk/ai-sdk.md)
- [Laravel AI agents now support MCP servers](/en/blog/ai-sdk-mcp-client.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.