Search by

thomsontochi / laravel-agent-loops

thomsontochi

A Laravel agent harness with swappable loops. Pick how your AI agent thinks, switch with one line, compare which works best.

Package info

github.com/thomsontochi/laravel-agent-loops

pkg:composer/thomsontochi/laravel-agent-loops

Statistics

Installs: 2

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v0.1.0 2026-10-03 18:46 UTC

This package is auto-updated.

Last update: 2026-10-03 18:53:51 UTC


README

tests Latest Version PHP 8.4+ Laravel 13 License: MIT

Give your agent a mindset.

Pick your AI agent's thinking strategy. Switch it with one line. Benchmark which one works best for your task.

A Laravel agent harness with swappable loops, built on the official Laravel AI SDK.

Comparing three loops on the same task with real AI

Why

An AI agent works in a cycle: think, use a tool, check, repeat. That cycle is the loop, and it decides a lot: how many AI calls a task takes, how long it runs, and how good the answer is.

Most agent packages pick the loop for you, and you never see it. This package makes the loop a choice. Run your agent step by step, make it plan first, or make it review its own work. Then compare them on your real tasks, with real numbers, instead of guessing.

It sits on top of the official Laravel AI SDK. Your agents, tools and providers stay exactly as they are. Only the thinking style changes.

What real numbers look like

The screenshot above is a real run with Gemini, on the task "Write a launch tweet for an open-source Laravel package that lets AI agents switch thinking styles":

Loop Steps Tokens in Tokens out Time
react 1 28 301 2.3s
plan-execute 7 2,559 787 23.7s
reflect-retry 2 426 337 6.4s

Two honest findings from that run:

  • Plan-execute cost about 10x more and took 10x longer, but it was the only loop that wrote one tweet. The others returned a menu of options. A single tweet isn't really a multi-step task, so plan-execute spent most of its tokens rewriting the same draft.
  • The reviewer approved the wrong answer. It called a list of options "excellent" for a task that asked for one tweet. After making the reviewer stricter, reflect-retry rejected the list, retried, and returned a single tweet: 4 steps, 1,202 tokens in, 514 out, 7.5s.

That's the point of this package: the trade-offs are real, and they depend on your task. Measure them.

Requirements

  • PHP 8.4+
  • Laravel 13
  • laravel/ai with at least one provider configured

Installation

composer require thomsontochi/laravel-agent-loops
php artisan agent-loops:install

The install command publishes config/agent-loops.php (it never overwrites an existing one unless you pass --force) and shows you where to start.

The install command

Quick start

Take any Laravel AI agent:

use Laravel\Ai\Contracts\Agent;
use Laravel\Ai\Promptable;

class SupportAgent implements Agent
{
    use Promptable;

    public function instructions(): string
    {
        return 'You answer customer questions about orders.';
    }
}

And run it through a loop:

use Developia\AgentLoops\Facades\AgentLoops;

$result = AgentLoops::run(new SupportAgent, 'Where is order 1234?');

$result->output;        // the final answer
$result->loop;          // "react"
$result->inputTokens;   // tokens sent
$result->outputTokens;  // tokens received
$result->durationMs;    // how long it took
$result->steps;         // what happened, in order

Pick a loop, three ways

The most specific choice wins.

1. At the call site (highest priority):

AgentLoops::using('plan-execute')->run($agent, $task);

2. On the agent class, for agents that have a best style:

use Developia\AgentLoops\Attributes\UseLoop;

#[UseLoop('reflect-retry')]
class CopywriterAgent implements Agent
{
    use Promptable;

    // ...
}

AgentLoops::run(new CopywriterAgent, $task); // runs reflect-retry

3. App-wide in .env (lowest priority):

AGENT_LOOPS_DEFAULT=react

The loops

Loop How it thinks AI calls Best for
react Think, use a tool, look, repeat. Laravel AI's native tool loop. 1 prompt Quick questions and simple tool use
plan-execute Writes a plan first, runs each step with the results so far, then answers. 1 + steps + 1 Multi-part tasks where order matters
reflect-retry Answers, has a reviewer check the work, retries with the feedback. 2 to 6 by default Writing, code and summaries where quality matters

react

Runs your agent once and records what happened. Laravel AI already runs the think, act, look cycle inside prompt(), so this loop is the honest baseline everything else is compared against. Steps: one tool_call per tool used, then the answer.

plan-execute

  1. Plan: a small planning agent turns the task into steps, using structured output, so the plan is always valid JSON.
  2. Execute: your agent runs each step and sees the results of the earlier ones.
  3. Answer: your agent writes the final answer from all the step results.

Plans longer than max_steps are trimmed. Steps: plan, then one step per step, then answer.

If planning fails (an empty or broken plan), it degrades gracefully and reports loudly. It falls back to react so you still get an answer, logs a warning, fires a PlanningFailed event, and flags the result:

if ($result->fellBack()) {
    // planning failed, the answer came from react
}

Prefer a hard stop? Set AGENT_LOOPS_ON_PLANNING_FAILURE=throw to get a PlanningFailedException instead.

Provider outages (overloaded, rate limited, unreachable) are not treated as planning failures. They pass straight through, because falling back would only hit the same provider again.

reflect-retry

  1. Attempt: your agent does the task.
  2. Review: a strict reviewer approves it, or rejects it with specific feedback.
  3. Retry: if rejected, your agent tries again with the feedback, up to max_retries times.

If it's still not approved, you get the last attempt, a warning in the log, and a flag:

if (! $result->approved()) {
    // the reviewer never approved this answer
}

Steps: attempt, review_rejected or review_approved, and not_approved if it ran out of retries.

Which loop should I use?

Start with react. It's the cheapest and fastest, and it's enough for most questions and simple tool use. Then move up only when you can see why:

If your task... Try Because
Is a quick question or a single tool call react 1 prompt, lowest cost and latency
Has several parts that depend on each other (research, then summarise, then format) plan-execute Each step sees the earlier results
Must come out in an exact form (one tweet, valid code, a set word count) reflect-retry A reviewer rejects answers that miss the brief
You're not sure agent-loops:compare Measure it on your real task instead of guessing

A real example from the run above: for a single tweet, plan-execute cost about 10x more than react, because a tweet isn't really a multi-step task. reflect-retry was the one that caught the "list of options instead of one tweet" problem.

Compare loops on your own tasks

From the terminal:

php artisan agent-loops:compare "Summarise this support ticket" --loops=react,reflect-retry
Option What it does
--loops= Comma-separated loop names. Default: all three
--agent= Your agent class, e.g. "App\Ai\Agents\SupportAgent". Default: a built-in assistant
--judge Also scores each answer 1 to 10 with a judge agent. One extra AI call. AI judges can be biased, so treat it as a second opinion
--json Prints the report as JSON, for scripts, CI and saving results

Or from code:

$report = AgentLoops::compare($agent, $task, ['react', 'plan-execute'], judge: true);

$report->results;       // loop name => result
$report->cheapest();    // fewest total tokens
$report->fastest();     // shortest run
$report->best();        // highest judge score, or null when not judged
$report->failures;      // loop name => error message
$report->judgeFailure;  // error message if the judge itself failed, else null

It keeps going when a loop fails. If the provider is overloaded halfway through, the loops that already ran keep their results, the failed one shows as failed with the reason, and the command exits with code 1 so scripts notice. A typo in a loop name fails before any AI call is made, so it never costs you anything.

The JSON output looks like this:

{
  "task": "...",
  "results": {
    "react": {
      "loop": "react",
      "output": "...",
      "steps": [{ "type": "answer", "content": "..." }],
      "inputTokens": 28,
      "outputTokens": 301,
      "durationMs": 2300.4
    }
  },
  "failures": { "plan-execute": "AI provider [gemini] is overloaded." },
  "judgeFailure": null,
  "scores": {},
  "summary": { "cheapest": "react", "fastest": "react", "best": null }
}

Use it in CI

Because --json prints only JSON and the command exits with 1 when any loop fails, it works as a benchmark step:

php artisan agent-loops:compare "Summarise this ticket" --loops=react,reflect-retry --json > results.json

# e.g. pull the cheapest loop and each loop's total tokens
jq -r '.summary.cheapest' results.json
jq -r '.results | to_entries[] | "\(.key): \(.value.inputTokens + .value.outputTokens) tokens"' results.json

Configuration

php artisan vendor:publish --tag=agent-loops-config
Setting Env Default What it does
default AGENT_LOOPS_DEFAULT react Loop used when nothing more specific is chosen
loops the 3 built-in loops Loop name => class. Add your own here
plan_execute.max_steps AGENT_LOOPS_MAX_STEPS 5 Longest plan allowed. Longer plans are trimmed
plan_execute.on_planning_failure AGENT_LOOPS_ON_PLANNING_FAILURE fallback fallback to react, or throw
reflect_retry.max_retries AGENT_LOOPS_MAX_RETRIES 2 Redos after a rejected review. 2 means up to 3 attempts

The planner, reviewer and judge agents use your app's default AI provider from config/ai.php.

Write your own loop

This is a harness, not a fixed set of loops. Implement the Loop contract:

use Developia\AgentLoops\Contracts\Loop;
use Developia\AgentLoops\LoopResult;
use Laravel\Ai\Contracts\Agent;

final class TwiceLoop implements Loop
{
    public function name(): string
    {
        return 'twice';
    }

    public function run(Agent $agent, string $task): LoopResult
    {
        $start = hrtime(true);

        $draft = $agent->prompt($task);
        $final = $agent->prompt("Improve this answer:\n\n{$draft->text}");

        return new LoopResult(
            loop: $this->name(),
            output: $final->text,
            steps: [
                ['type' => 'draft', 'content' => $draft->text],
                ['type' => 'answer', 'content' => $final->text],
            ],
            inputTokens: $draft->usage->inputTokens + $final->usage->inputTokens,
            outputTokens: $draft->usage->outputTokens + $final->usage->outputTokens,
            durationMs: (hrtime(true) - $start) / 1_000_000,
        );
    }
}

Register it in config/agent-loops.php:

'loops' => [
    'react' => ReActLoop::class,
    'plan-execute' => PlanExecuteLoop::class,
    'reflect-retry' => ReflectRetryLoop::class,
    'twice' => App\Loops\TwiceLoop::class,
],

Now it works everywhere the built-in loops do: AgentLoops::using('twice'), #[UseLoop('twice')], AGENT_LOOPS_DEFAULT=twice, and agent-loops:compare --loops=react,twice. Loops are resolved through Laravel's container, so constructor injection works.

Events

Event When Properties
Developia\AgentLoops\Events\PlanningFailed plan-execute couldn't get a plan and fell back agent, task, reason
use Developia\AgentLoops\Events\PlanningFailed;
use Illuminate\Support\Facades\Event;

Event::listen(PlanningFailed::class, function (PlanningFailed $event) {
    // alert your team: Slack, Sentry, email...
});

Testing your agents

Everything works with Laravel AI's fakes, so your tests never make real AI calls:

use App\Ai\Agents\SupportAgent;
use Developia\AgentLoops\Facades\AgentLoops;
use Developia\AgentLoops\Planning\Planner;
use Developia\AgentLoops\Reflection\Reviewer;

it('answers with react', function () {
    SupportAgent::fake(['Your order ships tomorrow.']);

    $result = AgentLoops::using('react')->run(new SupportAgent, 'Where is my order?');

    expect($result->output)->toBe('Your order ships tomorrow.');
});

it('plans first with plan-execute', function () {
    Planner::fake([['steps' => ['Look up the order', 'Write the reply']]]);
    SupportAgent::fake(['Order found', 'Draft reply', 'Your order ships tomorrow.']);

    $result = AgentLoops::using('plan-execute')->run(new SupportAgent, 'Where is my order?');

    expect($result->output)->toBe('Your order ships tomorrow.');
});

it('retries until the reviewer approves', function () {
    SupportAgent::fake(['A long rambling reply', 'Your order ships tomorrow.']);
    Reviewer::fake([
        ['approved' => false, 'feedback' => 'Too long.'],
        ['approved' => true, 'feedback' => 'Good.'],
    ]);

    $result = AgentLoops::using('reflect-retry')->run(new SupportAgent, 'Where is my order?');

    expect($result->approved())->toBeTrue();
});

The comparison judge can be faked the same way with Developia\AgentLoops\Comparison\Judge::fake().

Contributing

Issues, ideas and new loops are welcome. See CONTRIBUTING.md for how to add a loop. To work on the package:

git clone https://github.com/thomsontochi/laravel-agent-loops.git
cd laravel-agent-loops
composer install
composer test     # Pint style check + Pest
composer format   # fix code style

Credits

Built in public by Austin Opia (Developia), on top of the Laravel AI SDK.

LinkedIn · X · Newsletter

License

MIT. See LICENSE.