twdnhfr/laravel-deepagents

A deep-agent harness for the Laravel AI SDK: an owned agent loop with planning, sub-agents, persistent memory, human-in-the-loop approval and automatic context management.

Maintainers

Package info

github.com/twdnhfr/laravel-deepagents

pkg:composer/twdnhfr/laravel-deepagents

Transparency log

Fund package maintenance!

twdnhfr

Statistics

Installs: 38

Dependents: 0

Suggesters: 0

Stars: 7

Open Issues: 0

v0.7.1 2026-07-16 14:14 UTC

This package is auto-updated.

Last update: 2026-07-16 14:24:52 UTC


README

Latest Version on Packagist Tests License

A deep-agent harness for the Laravel AI SDK — an owned, resumable agent loop with planning, sub-agents, human-in-the-loop approval, multi-turn conversations, memory and automatic context management, built on top of laravel/ai.

Warning

Early work in progress. The runtime and core harness are built and tested — owned agent loop, human-in-the-loop, multi-turn, planning, sub-agents, memory and context management (summarization + tool-output offloading) over pluggable backends. Skills are planned; filesystem/shell tools and token streaming are out of scope for now (see TODO.md and docs/adr/). APIs may still change before 1.0.

Note

Not affiliated with or endorsed by LangChain. An independent reimplementation for Laravel, inspired by the deepagents project.

What is Laravel Deep Agents?

The Laravel AI SDK (laravel/ai) is an excellent engine: one unified, expressive API over many providers (OpenAI, Anthropic, Gemini, and more), with tool calling, structured output, streaming, embeddings and more. Point it at a model, hand it some tools, call prompt() — done.

But an engine is not an agent harness. The moment you ask an agent to do real, long-horizon work — research across many steps, read and write files, delegate subtasks to focused sub-agents, pause for your approval before doing something destructive, and later pick up exactly where it left off — you need a layer of opinionated machinery on top: planning, a virtual filesystem, sub-agents, automatic context management, human-in-the-loop, skills and memory.

In the Python/LangChain world that layer is deepagents"the batteries-included agent harness", itself an attempt to distill what makes Claude Code general-purpose. There was no equivalent for Laravel.

Laravel Deep Agents is that layer. It builds on top of laravel/ai and brings the deepagents feature set to PHP:

The SDK stays the engine. This package adds the harness.

Why a custom loop?

laravel/ai runs its model↔tool loop inside the provider — great for a one-shot prompt(), but it gives you no place to step in between the model choosing a tool and that tool running. That in-between is exactly where approval gates, permission checks and context compaction live.

So this package owns the loop: it drives one model turn at a time and decides for itself when to run a tool, when to pause for a human, and when it's done. The entire state of a run is a plain, serializable value object — so a run can pause, be stored in your database or a queued job, and resume in a completely different request.

The how-and-why is recorded as Architecture Decision Records in docs/adr/.

Requirements

  • PHP 8.3+
  • Laravel 13
  • laravel/ai ^0.9 (configured with at least one provider/API key)

Installation

composer require twdnhfr/laravel-deepagents

The package auto-registers. Configure your provider and API key through the Laravel AI SDK as usual (e.g. ANTHROPIC_API_KEY in .env and the provider entry in config/ai.php).

Quickstart

Define a tool (the standard laravel/ai Tool contract), then build and run an agent:

use Illuminate\Contracts\JsonSchema\JsonSchema;
use Laravel\Ai\Contracts\Tool;
use Laravel\Ai\Tools\Request;

class GetWeather implements Tool
{
    public function name(): string
    {
        return 'get_weather';
    }

    public function description(): string
    {
        return 'Get the current weather for a city.';
    }

    public function handle(Request $request): string
    {
        return "It's 21°C and sunny in {$request['city']}.";
    }

    public function schema(JsonSchema $schema): array
    {
        return ['city' => $schema->string()->description('The city name.')->required()];
    }
}
use Twdnhfr\LaravelDeepagents\DeepAgent;

$state = DeepAgent::make()
    ->provider('anthropic')              // resolved from config/ai.php
    ->model('claude-sonnet-4-5')         // optional — defaults to the provider's default model
    ->instructions('You are a helpful weather assistant.')
    ->tool(new GetWeather)
    ->run('What should I wear in Berlin today?');

echo $state->finalText;

By default the agent runs autonomously: it calls tools and loops until it has a final answer.

Note

One tool instance per agent. Built-in tools receive run-scoped state (the RunState, the storage backend) by injection right before execution — an instance shared between two agents (e.g. a parent and a sub-agent) would leak state between their runs. Construct tools fresh per agent, as in the example.

Planning with todos

Give the agent the built-in write_todos tool so it can keep a visible plan. The list lives on the run state:

$state = DeepAgent::make()
    ->provider('anthropic')
    ->withTodos()
    ->instructions('Plan your work with the todo list before acting.')
    ->run('Research and outline a short article on vector databases');

foreach ($state->todos as $todo) {
    echo "[{$todo['status']}] {$todo['content']}\n";
}

Human-in-the-loop: approve before tools run

Opt into approval mode and the agent suspends before any tool call instead of executing it. The suspended run is fully serializable, so you can store it, show the pending action to a human, and resume later — in another request or a queued job:

use Twdnhfr\LaravelDeepagents\DeepAgent;
use Twdnhfr\LaravelDeepagents\Runtime\RunState;

$agent = DeepAgent::make()
    ->provider('anthropic')
    ->tool(new DeleteStaleRecords)   // something you don't want running unsupervised
    ->requireApproval();

$state = $agent->run('Clean up records older than a year');

if ($state->isSuspended()) {
    // Show the human exactly what the model wants to do…
    foreach ($state->pendingToolCalls as $call) {
        info("Agent wants to call {$call['name']}", $call['arguments']);
    }

    // …and persist the run while you wait for their decision.
    $stored = $state->toJson();   // -> a DB column, cache entry, queued job, etc.
}

// Later, once approved (possibly in a different request):
$final = $agent->resume(RunState::fromJson($stored));

echo $final->finalText;

The human does not have to wave everything through. Record a per-call decision on the state before resuming — approve as-is (the default), execute with corrected arguments, or reject with a reason the model gets back as the tool's result so it can adjust its plan:

$state = RunState::fromJson($stored);

$state->approve('tc_1');                                  // optional — approved is the default
$state->edit('tc_2', ['path' => 'drafts/safe.md']);       // run with corrected arguments
$state->reject('tc_3', 'Never email customers directly.'); // skip; the reason becomes the result

$final = $agent->resume($state);

Decisions are plain data on the RunState, so they survive toJson()/fromJson() — collect them in a controller, persist, and resume in a queued job. A rejected call is never executed; the model sees The user rejected this tool call: … and reacts. With edit(), the model's original arguments stay visible on the assistant message in the history, so the change is auditable.

Multi-turn conversations

run() starts a fresh run; continue() carries an existing run forward with the user's next message, so the agent keeps full prior context. The run state is serializable, so you can persist it between turns (session, DB, …):

$state = $agent->run('What is the weather in Berlin?');
// ...store $state->toJson()...

// next turn, same conversation:
$state = $agent->continue(RunState::fromJson($stored), 'And in Tokyo?');
echo $state->finalText; // resolves "And in Tokyo?" using the prior turn

A suspended run (pending tool approval) cannot be continue()d — it throws; resume() it first. Each continue() resets the maxTurns budget, which is otherwise tracked on the run state across suspend/resume.

Sub-agents: delegate to an isolated context

Register a sub-agent and the parent gets a task tool to delegate self-contained work to it. The sub-agent runs as its own DeepAgent to completion and hands back only its final text:

$researcher = DeepAgent::make()
    ->provider('anthropic')
    ->instructions('You research a topic and return a concise summary.');

$state = DeepAgent::make()
    ->provider('anthropic')
    ->subAgent('researcher', 'Researches a topic in depth.', $researcher)
    ->run('Delegate research on vector databases, then summarize the findings.');

Important

"Isolated" means the context, not the workspace. A sub-agent gets a fresh conversation — it never sees the parent's message history or todos, and only its final text returns to the parent. But it shares the parent's storage backend (artifacts/memory) by default, mirroring deepagents' shared virtual filesystem. So:

  • Artifacts a sub-agent writes are readable by the parent and its siblings — they all hit the same store. Two agents writing the same path overwrite each other; there is no per-sub-agent sandbox.
  • The exception is offloaded tool output (offloadLargeToolResults()): it is written under the run-scoped path runs/{runId}/tool/{callId}, so runs sharing a persistent backend never collide — and a host can clean up after a run via backend->list("runs/{$state->id}/").
  • To give a sub-agent its own private store, set ->backend(...) on it explicitly — an explicit backend is always kept, never replaced by the parent's.

How it works

DeepAgent (fluent builder)              ← the public API
   │ configures & starts
   ▼
Runtime\Loop  ── drives one turn ──►  laravel/ai
   │  maxSteps: 0                       TextProvider::textGenerationLoop()->generate($messages)
   │  (autonomous | approval pause)      └─ any provider (OpenAI, Anthropic, Gemini, …)
   ▼
Runtime\RunState  ── json_encode ──►  DB / queue / HTTP body ── json_decode ──►  resume()
  • DeepAgent — the fluent front door (make(), provider(), model(), instructions(), tool()/tools(), withTodos(), subAgent(), memory(), summarize(), requireApproval(), maxTurns(), run(), resume(), continue()).
  • Runtime\Loop — drives the agent one model turn at a time using maxSteps: 0, the seam that returns the model's tool-call intention without executing it (verified across Anthropic, OpenAI and Gemini — see ADR-0002).
  • Runtime\RunState — the serializable state of a run (history, pending tool calls, todos, status). The single source of truth that survives suspend → persist → resume.
  • Contracts\Backend + Backends\StateBackend — the pluggable file-storage seam for the upcoming filesystem tools.

Status & roadmap

Area State
Agent loop (autonomous + per-tool approval), serializable RunState, HITL resume ✅ built & tested
DeepAgent fluent builder + multi-turn (continue())
write_todos planning tool
Sub-agents (task)
Context management (summarization + tool-output offloading to artifacts)
Memory (AGENTS.md) + BASE prompt assembly
Safe tool execution + dangling-tool-call repair
Pluggable backends (state, filesystem, database, cache) + config-driven default
Filesystem & shell tools 🧊 deferred (see adoption)
Skills, harness profiles, MCP ⏳ planned
Token streaming through the loop ❌ by design — see ADR-0004

The full plan lives in TODO.md.

Demo

See every feature run offline (no API keys), via a scripted provider:

php examples/demo.php

It walks through autonomous tool use, planning, human-in-the-loop approval, context summarization and sub-agents. See examples/.

Example app: deepagents-chat

twdnhfr/deepagents-chat is a small Laravel chat app built on this package — a live, end-to-end example. It shows the owned loop in a real request cycle: a tool-call trace, the human-in-the-loop approval flow, multi-turn conversations, a DatabaseBackend with tool-output offloading, and Markdown-rendered replies.

Testing

composer test

Credits & inspiration

License

The MIT License (MIT). See LICENSE.md.