A few weeks ago Thorsten Ball (ampcode.com) wrote an article “How to Build an Agent”. The article went viral, and I though it would be great to convert it to Laravel and add my own spice to it. So thanks for the inspiration Thorsten, now let’s dive in.
But we have Cursor, Claude and Windsurf already
Over the past few months, everyone's been obsessed with coding agents—constantly jumping between different ones, burning through API credits like crazy, and running multiple agents at once for better productivity.
Instead of joining the "which agent is best" debate, let's actually figure out how these things work. At first, they seem like pure magic. You hit enter and boom, code just starts appearing in your terminal, writing itself line by line without breaking a sweat.
But once you peek under the hood and understand what's really going on, it's way less mysterious than you'd think. We can actually build our own agent with just a handful of code.
What is an Agent?
It took me some time to understand what an Agent means. The word itself is somewhat frightening, as if it suggests that we're dealing with something secret and incomprehensible to us. But as I delved into it a bit deeper, I had to realize that an Agent is just a for loop that can run until it reaches the goal we gave it, continuously running itself over and over again. (Just bear it with tokens / credits…)
Okay, but how does it code?
This is where so-called tool calling comes into play. Again, another word that needs demystifying. The easiest way I can put it is that a tool is like a function in our codebase. When we call an LLM, we can predetermine which functions the LLM is allowed to call and with what parameters. When our agent 'thinks' it needs to make such a call, it will actually do so.
At this point the typically non-deterministic LLM follows a deterministic path: it runs a function that we have explicitly defined. The function’s output is then returned to the model. By invoking such tools, the LLM can pull in external information or carry out actions that would otherwise be beyond its reach.
Enough from talking, let’s build
Here are the prerequisites for our project.
- A running Laravel 12.x project
- An Anthropic / OpenAI API token
We will use PrismPHP for the AI calls, this package makes it easy to work together with multiple LLM providers like Anthropic, OpenAI etc... Let’s install it.
composer require prism-php/prism
Publish the config file.
php artisan vendor:publish --tag=prism-config
We will use Anthropic for testing the agent, add your API key to the .env file. (You can use OpenAI or any other provider as well)
ANTHROPIC_API_KEY=xxx
Creating the agent
For simplicity we will use a command for that. Let’s create it with artisan.
php artisan make:command Agent
Now edit the Agent.php and add the basic logic to talk with Claude.
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Prism\Prism\Enums\Provider;
use Prism\Prism\Prism;
use Prism\Prism\ValueObjects\Messages\UserMessage;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Str;
class Agent extends Command
{
protected $signature = 'app:agent';
protected $description = 'Run the coding agent';
protected array $messages = [];
public function handle()
{
while (true) {
$input = $this->ask('Prompt');
$this->messages[] = new UserMessage($input);
$response = Prism::text()
->using(Provider::Anthropic, 'claude-3-7-sonnet-latest')
->withMessages($this->messages)
->asText();
$this->messages = [
...$this->messages,
...$response->messages,
];
$this->output->text($response->text);
}
}
}
As you can see we created an infinite loop, then we ask for a prompt from the user in every iteration. After that we call Anthropic (using Prism) and we also push the previous messages for some context.
With just a few lines of code, now we are able to talk with AI. Let’s try it out by running the agent.
php artisan app:agent
Prompt:
> Hello, I'm Aprod
Agent: Hello Aprod! It's nice to meet you. How are you doing today? Is there something I can help you with or would you like to chat about a particular topic?
Prompt:
> Tell me a joke
Agent: Here's a joke for you:
Why don't scientists trust atoms?
Because they make up everything!
It works! Now we are ready to empower it with tools!
Adding the tools
In order to have a working coding agent we will need 3 tools
- Read file
- List files
- Edit file
With these 3 tools our agent will be able to look through our codebase, view files and create or modify files.
To implement a tool we will use the Tool facade of Prism. Every tool needs a name, a description, the input parameters and a callable function that should be run when the Agent calls the tool.
First add the Read File tool as a private function.
private function readFileTool(): \Prism\Prism\Tool
{
return Tool::as('read_file')
->for('Read the contents of a given relative file path. Use this when you want to see what\'s inside a file. Do not use this with directory names.')
->withStringParameter('path', 'The relative path of a file in the working directory.')
->using(function (string $path) {
$this->output->text('Read file: ' . $path);
if (!File::exists($path)) {
return 'File does not exist.';
}
return File::get($path);
});
}
That’s it. Pretty easy, isn’t it?
Now add the List Files tool.
private function listFilesTool(): \Prism\Prism\Tool
{
return Tool::as('list_files')
->for('List files and directories at a given path. If no path is provided, lists files in the current directory.')
->withStringParameter('path', 'Optional relative path to list files from. Defaults to current directory if not provided.')
->using(function (string $path) {
$this->output->text('List files: ' . $path);
if (!File::isDirectory($path)) {
return 'Directory does not exist.';
}
$files = File::files($path);
$output = [
...File::directories($path),
];
foreach ($files as $file) {
$output[] = $file->getPathname();
}
return json_encode($output);
});
}
And finally add the Edit File tool
private function editFileTool(): \Prism\Prism\Tool
{
return Tool::as('edit_file')
->for('Make edits to a text file. Replaces \'oldStr\' with \'newStr\' in the given file. \'oldStr\' and \'newStr\' MUST be different from each other. If the file specified with path doesn\'t exist, it will be created.')
->withStringParameter('path', 'The path to the file')
->withStringParameter('oldStr', 'Text to search for - must match exactly and must only have one match exactly, it can be an empty string.')
->withStringParameter('newStr', 'Text to replace oldStr with')
->using(function (string $path, string $oldStr, string $newStr) {
$this->output->text('Make edits here: ' . $path);
if (!File::exists($path) && empty($oldStr)) {
File::put($path, $newStr);
return 'File ' . $path . ' was created.';
}
$content = File::get($path);
$newContent = str_replace($oldStr, $newStr, $content);
File::put($path, $newContent);
return 'File ' . $path . ' was modified.';
});
}
Now we need to add these tools to the Prism call
...
$response = Prism::text()
->using(Provider::Anthropic, 'claude-3-7-sonnet-latest')
->withMessages($this->messages)
->withTools([
$this->readFileTool(),
$this->listFilesTool(),
$this->editFileTool(),
])
->withMaxSteps(50)
->asText();
...
We used the ->withTools
method, and also added the ->withMaxSteps(50)
modifier, every tool call count as a step, to allow the LLM to run the tools we needed to raise the limit.
The Magic
Now we just need to try out our newly created Agent.
php artisan app:agent
Prompt:
> Create a new controller called AgentTestController and add a test method that returns a joke"
List files: .
List files: ./app
List files: ./app/Http
Make edits here: ././app/Http/Controllers/AgentTestController.php
Agent: I've successfully created a new controller called `AgentTestController.php` in the `app/Http/Controllers` directory.
The controller includes a `test()` method that returns a joke in JSON format.
To access this endpoint, you would typically need to add a route to it in your routes file. If you'd like me to add a route for this controller as well, please let me know.
It worked! Now we could ask it to add the route into the api.php
but I will leave it to you!
Conclusion
What seemed like magic at first turned out to be surprisingly straightforward. With just 100 lines of code and three simple tools, we've built our own coding agent that can navigate, read, and modify files in our codebase. The simplicity is almost shocking, no complex frameworks, no mysterious algorithms, just basic PHP and API calls.
The real insight here is that agents aren't mystical black boxes. They're just sophisticated loops that combine LLM reasoning with practical tools. By understanding this fundamental concept, we've demystified something that felt impossibly complex just an hour ago. Yes, we're still using API credits like everyone else, but now we understand exactly what's happening with each call and why.
Of course, this is more of a learning exercise than a production-ready solution. Our simple agent lacks the sophisticated error handling, context management, and safety features that make commercial agents reliable for real work. But that's exactly the point, we've stripped away the complexity to understand the core concepts.
The beauty of this exercise is how it changes your perspective. The next time you use Cursor or Claude, you'll think "ah, it's probably calling a read_file tool here" or "now it's using edit_file to make that change." You'll understand the conversation between the LLM and the tools, rather than seeing it as pure magic.
Building this agent was almost embarrassingly easy, if you can write a Laravel command and make an API call, you can build an agent. The hardest part was probably setting up the API key. This simplicity is the real takeaway: understanding how these tools work removes the mystique and makes you a more informed user of the commercial alternatives.
Now go ahead and experiment: add that route to api.php, create new tools, or integrate this agent into your workflow.
The magic is in your hands now.
Top comments (0)