ai agents crash course laravel 13 Binaryboxtuts

AI Agents Crash Course for Developers (2026): Build Your First Agent with Laravel 13

Avatar photoPosted by

Introduction:

Hi there! In this blog, I will be showing you how to build your first AI agent from scratch, step by step. No magic frameworks you don’t understand, and no 50-page theory. We will go over the few concepts you actually need, then write a small working agent that can search a database and create records on its own.

If you are new to the topic, I recommend reading What Is an AI Agent? A Developer’s Guide to Agentic AI first, and AI Agent vs AI Assistant vs Chatbot if you are still unsure where the line is. This post is the hands-on follow-up: less “what”, more “how”.

We will use the Laravel AI SDK, the official first-party AI package that came with Laravel 13. It gives us agents, tools, memory, and testing helpers with a clean Laravel-style API, and it works with OpenAI, Anthropic, Gemini, Ollama, and more. If you know a bit of PHP, you can follow along.

AI Agent Concepts in 5 Minutes

Before we write code, here are the five words you will keep seeing. Every agent framework, in any language, is built from these same parts:

  • LLM (the brain): the large language model, such as GPT, Claude, Gemini, or a local Llama model. It reads text and decides what to say or do next.
  • Tools / function calling (the hands): normal functions in your code that the model is allowed to call, like “search projects” or “create project”. The model never runs code itself. It asks your app to run a tool with some arguments, and your app sends back the result.
  • The loop (the heartbeat): ask the model, run any tool it requests, feed the result back, and repeat until the model gives a final answer or hits a step limit. That loop is what turns a chatbot into an agent.
  • Memory: short-term memory is the conversation history you send with each request. Long-term memory is stored in a database (or a vector store for RAG) and pulled in when needed.
  • MCP (Model Context Protocol): an open standard for sharing tools between apps. An MCP server exposes tools once, and any MCP-aware agent (Cursor, Claude, or your Laravel app) can use them.

In this tutorial, our agent will have one LLM, two tools, the loop (handled by the SDK), and short-term memory. That is a real agent, and it’s the best way to learn.

What We Are Building

We will build a Project Assistant that you can chat with in the terminal. It manages the projects table from my Laravel 13 CRUD App tutorial, so you can reuse that project. You can ask it things like “Which projects mention the blog?” or “Add a project for the new mobile app”, and it will decide by itself which tool to call.

Prerequisite:

  • PHP >= 8.3 and Composer
  • A Laravel 13 project with a projects table (follow the Laravel 13 CRUD tutorial, or create the model and migration from Step 3 of it)
  • An API key from an AI provider (OpenAI, Anthropic, Gemini, etc.), or Ollama installed if you want to run a free local model

Step 1: Install the Laravel AI SDK

Inside your Laravel 13 project, install the package with Composer:

composer require laravel/ai

Then publish the config file and migrations, and run the migrations. This creates the config/ai.php file and the tables the SDK uses to store conversations:

php artisan vendor:publish --provider="Laravel\Ai\AiServiceProvider"
php artisan migrate

Step 2: Add Your AI Provider Key

Open your .env file and add the key for your provider. The SDK uses OpenAI by default:

.env

OPENAI_API_KEY=your-api-key-here

Using another provider? Add its key instead (for example ANTHROPIC_API_KEY or GEMINI_API_KEY) and change the default value in config/ai.php. For a free local setup, set the default to ollama and pull a model that supports tool calling. Keep in mind that small local models are less reliable at choosing tools than the big hosted ones.

Step 3: Create the First Tool (Search Projects)

A tool is just a PHP class with three things: a description (so the model knows when to use it), a schema (the arguments it accepts), and a handle method (the code that actually runs). Generate one with Artisan:

php artisan make:tool SearchProjects

This creates the file in app/Ai/Tools/. Replace its contents with:

app/Ai/Tools/SearchProjects.php

<?php

namespace App\Ai\Tools;

use App\Models\Project;
use Illuminate\Contracts\JsonSchema\JsonSchema;
use Laravel\Ai\Contracts\Tool;
use Laravel\Ai\Tools\Request;
use Stringable;

class SearchProjects implements Tool
{
    /**
     * Get the description of the tool's purpose.
     */
    public function description(): Stringable|string
    {
        return 'Search the projects table by keyword. Leave the query empty to list the latest projects.';
    }

    /**
     * Execute the tool.
     */
    public function handle(Request $request): Stringable|string
    {
        $query = trim((string) $request->string('query'));

        $projects = Project::query()
            ->when($query !== '', function ($builder) use ($query) {
                $builder->where('name', 'like', "%{$query}%")
                    ->orWhere('description', 'like', "%{$query}%");
            })
            ->latest()
            ->limit(10)
            ->get(['id', 'name', 'description', 'created_at']);

        if ($projects->isEmpty()) {
            return 'No projects found.';
        }

        return $projects->toJson();
    }

    /**
     * Get the tool's schema definition.
     */
    public function schema(JsonSchema $schema): array
    {
        return [
            'query' => $schema->string()->description('Keyword to search for. Use an empty string to list all.'),
        ];
    }
}

Notice that the tool returns plain text (JSON here). Whatever you return is exactly what the model “sees” as the result, so keep it short and useful. I also limit the results to 10 so we don’t flood the model with data.

Step 4: Create the Second Tool (Create Project)

Now let’s give the agent a tool that changes something:

php artisan make:tool CreateProject

app/Ai/Tools/CreateProject.php

<?php

namespace App\Ai\Tools;

use App\Models\Project;
use Illuminate\Contracts\JsonSchema\JsonSchema;
use Laravel\Ai\Contracts\Tool;
use Laravel\Ai\Tools\Request;
use Stringable;

class CreateProject implements Tool
{
    /**
     * Get the description of the tool's purpose.
     */
    public function description(): Stringable|string
    {
        return 'Create a new project with a name and a short description.';
    }

    /**
     * Execute the tool.
     */
    public function handle(Request $request): Stringable|string
    {
        $validated = $request->validate([
            'name' => 'required|string|max:255',
            'description' => 'required|string',
        ]);

        $project = Project::create($validated);

        return "Project #{$project->id} \"{$project->name}\" was created.";
    }

    /**
     * Get the tool's schema definition.
     */
    public function schema(JsonSchema $schema): array
    {
        return [
            'name' => $schema->string()->description('Short project name.')->required(),
            'description' => $schema->string()->description('One or two sentences about the project.')->required(),
        ];
    }
}

Here I validate the arguments with $request->validate(). If the model sends bad data, the SDK sends the validation errors back to the model as the tool result, so it can fix the arguments and try again. Never trust tool arguments blindly. They come from a model, not from you.

Step 5: Create the Agent

The agent class ties everything together: the instructions (system prompt), the tools, and a few settings. Generate it:

php artisan make:agent ProjectAssistant

Open app/Ai/Agents/ProjectAssistant.php and replace it with:

app/Ai/Agents/ProjectAssistant.php

<?php

namespace App\Ai\Agents;

use App\Ai\Tools\CreateProject;
use App\Ai\Tools\SearchProjects;
use Laravel\Ai\Attributes\MaxSteps;
use Laravel\Ai\Attributes\Temperature;
use Laravel\Ai\Contracts\Agent;
use Laravel\Ai\Contracts\HasTools;
use Laravel\Ai\Contracts\Tool;
use Laravel\Ai\Promptable;
use Stringable;

#[MaxSteps(5)]
#[Temperature(0.2)]
class ProjectAssistant implements Agent, HasTools
{
    use Promptable;

    /**
     * Get the instructions that the agent should follow.
     */
    public function instructions(): Stringable|string
    {
        return <<<'PROMPT'
        You are a friendly project manager assistant for a small team.
        - Always use the SearchProjects tool before answering questions about existing projects.
        - Use the CreateProject tool only when the user clearly asks you to add a project.
        - Never invent projects. If a tool returns nothing, say so.
        - Keep answers short and use bullet points for lists.
        PROMPT;
    }

    /**
     * Get the tools available to the agent.
     *
     * @return Tool[]
     */
    public function tools(): iterable
    {
        return [
            new SearchProjects,
            new CreateProject,
        ];
    }
}

A few things to notice:

  • #[MaxSteps(5)] is our stop condition. The agent can call tools at most 5 times per prompt. Always set a limit so a confused model can’t loop forever and burn your credits.
  • #[Temperature(0.2)] makes answers more consistent, which is what you want for an agent that touches data.
  • The instructions tell the model when to use each tool and forbid making up data. Clear rules here matter more than clever wording.
  • I removed the Conversational interface that the generator adds, because we will pass the chat history ourselves in the next step.

Step 6: Create a Chat Command (with Memory)

Let’s talk to our agent from the terminal. Create an Artisan command:

php artisan make:command AgentChat

app/Console/Commands/AgentChat.php

<?php

namespace App\Console\Commands;

use App\Ai\Agents\ProjectAssistant;
use Illuminate\Console\Attributes\Description;
use Illuminate\Console\Attributes\Signature;
use Illuminate\Console\Command;
use Laravel\Ai\Messages\Message;

#[Signature('agent:chat')]
#[Description('Chat with the Project Assistant agent')]
class AgentChat extends Command
{
    /**
     * Execute the console command.
     */
    public function handle()
    {
        $this->info('Project Assistant is ready. Type "exit" to quit.');

        $history = [];

        while (true) {
            $input = trim((string) $this->ask('You'));

            if ($input === '' || $input === 'exit') {
                break;
            }

            $response = ProjectAssistant::make()
                ->withMessages($history)
                ->prompt($input);

            foreach ($response->toolCalls as $call) {
                $this->comment("  [tool] {$call->name} ".json_encode($call->arguments));
            }

            $this->line('Agent: '.$response->text);

            // Short-term memory: remember this turn for the next prompt.
            $history[] = new Message('user', $input);
            $history[] = new Message('assistant', $response->text);
        }

        return self::SUCCESS;
    }
}

The $history array is the agent’s short-term memory. After each turn, we save what you said and what the agent answered, then pass it back with withMessages() on the next prompt. Without it, every question would start from zero. I also print every tool call, so you can watch the agent loop in action.

Step 7: Run Your Agent

Start the chat:

php artisan agent:chat

Then try prompts like these:

You: Which projects mention the blog?
You: Add a project called Mobile App for building our iOS and Android app.
You: Now list all projects.

When you ask about existing projects, you will see a [tool] SearchProjects line before the answer. When you ask it to add one, you will see [tool] CreateProject, and the new row will show up on your /projects CRUD page. The model chose the tool, filled in the arguments, read the result, and wrote the reply. That is the agent loop.

Step 8: Test the Agent Without Calling the AI

You don’t want your test suite paying for API calls. The SDK lets you fake an agent so it returns fixed responses:

tests/Feature/ProjectAssistantTest.php

<?php

namespace Tests\Feature;

use App\Ai\Agents\ProjectAssistant;
use Tests\TestCase;

class ProjectAssistantTest extends TestCase
{
    public function test_the_agent_answers_without_calling_a_real_ai_provider(): void
    {
        ProjectAssistant::fake([
            'You have 1 project: Blog Revamp.',
        ]);

        $response = ProjectAssistant::make()->prompt('What projects do we have?');

        $this->assertSame('You have 1 project: Blog Revamp.', $response->text);

        ProjectAssistant::assertPrompted('What projects do we have?');
    }
}
php artisan test --filter=ProjectAssistant

Where to Go Next

  • Persistent memory: use the SDK’s RemembersConversations trait with ->forUser($user) to save conversations in the database instead of an array.
  • MCP tools: install Laravel MCP and spread an MCP server’s tools into your agent’s tools() array, for example ...Client::web('https://mcp.example.com')->tools().
  • RAG: use the built-in SimilaritySearch tool with embeddings so the agent can answer from your own documents.
  • Human approval: for dangerous tools (like delete), require a human to approve the call before it runs.

Conclusion:

And that’s it! We covered the core AI agent concepts (LLM, tools, the loop, memory, and MCP), then built a working Project Assistant agent with the Laravel 13 AI SDK. It has two tools, a step limit, short-term memory, a chat command, and a test that never calls a real provider. Every bigger agent you will see out there is built on this same foundation.

I hope this crash course helped you build your first agent. If you get stuck, leave a comment below and tell me what you’d like to see next in this series. Happy coding!

Leave a Reply

Your email address will not be published. Required fields are marked *