Building an Agent with Tool Access: A Practical Guide

Building a code-editing agent doesn't have to be complicated, as demonstrated by Thorsten Ball in his article. The agent in this example is created using an LLM (large language model), a loop, and enough tokens. The code for the agent is under 400 lines, with most of it being boilerplate. The agent has access to an anthropic.Client and can get a user message by reading from stdin on the terminal.

The article also discusses the concept of tools and how they can be used in the context of an LLM. A tool is a prompt that the model uses to reply in a certain way. The receiver of the message then "uses the tool" by executing it and replying with the result. The article provides an example of a read\_file tool that reads the contents of a given file path.

To make it easier for the model to use tools, the big model providers have built-in APIs to send tool definitions along. The agent in the example has a structure that includes tools, and these tools have a name, description, input schema, and a function that executes the tool with the input the model sends to it. The agent then sends the tool definitions along with the message to the server, where the model can use them. The article also provides a GenerateSchema function that generates a JSON schema for the tool definition, which is used by the server.

The example code provides a read\_file tool that reads the contents of a file and returns it as a string. This tool is then used in the agent's runInference function, which sends the tool definitions along with the message to the server. The model can then use the tool to read files as needed.

# How to Build an Agent - Amp

Thorsten Ball, April 15, 2025

It’s not that hard to build a fully functioning, code-editing agent.

It seems like it would be. When you look at an agent editing files, running commands, wriggling itself out of errors, retrying different strategies - it seems like there has to be a secret behind it.

There isn’t. It’s an LLM, a loop, and enough tokens. It’s what we’ve been saying on the podcast from the start. The rest, the stuff that makes Amp so addictive and impressive? Elbow grease.

But building a small and yet highly impressive agent doesn’t even require that. You can do it in less than 400 lines of code, most of which is boilerplate.

I’m going to show you how, right now. We’re going to write some code together and go from zero lines of code to “oh wow, this is… a game changer.”

I urge you to follow along. No, really. You might think you can just read this and that you don’t have to type out the code, but it’s less than 400 lines of code. I need you to feel how little code it is and I want you to see this with your own eyes in your own terminal in your own folders.

Here’s what we need:

- Go
- Anthropic API key that you set as an environment variable, ANTHROPIC_API_KEY

Pencils out!

Let’s dive right in and get ourselves a new Go project set up in four easy commands:

mkdir code-editing-agent cd code-editing-agent go mod init agent touch main.go


Now, let’s open main.go and, as a first step, put a skeleton of things we need in it:

package main

import ( "bufio" "context" "fmt" "os" "github.com/anthropics/anthropic-sdk-go" )

func main() { client := anthropic.NewClient() scanner := bufio.NewScanner(os.Stdin) getUserMessage := func() (string, bool) { if !scanner.Scan() { return "", false } return scanner.Text(), true } agent := NewAgent(&client, getUserMessage) err := agent.Run(context.TODO()) if err != nil { fmt.Printf("Error: %s\n", err.Error()) } }

func NewAgent(client *anthropic.Client, getUserMessage func() (string, bool)) *Agent { return &Agent{client: client, getUserMessage: getUserMessage,} }

type Agent struct { client *anthropic.Client getUserMessage func() (string, bool) }


Yes, this doesn’t compile yet. But what we have here is an Agent that has access to an anthropic.Client (which, by default, looks for ANTHROPIC_API_KEY) and that can get a user message by reading from stdin on the terminal.

Now let’s add the missing Run() method:

// main.go

func (a *Agent) Run(ctx context.Context) error { conversation := [0]anthropic.MessageParam{} fmt.Println("Chat with Claude (use 'ctrl-c' to quit)") for { fmt.Print("\u001b[94mYou\u001b[0m: ") userInput, ok := a.getUserMessage() if !ok { break } userMessage := anthropic.NewUserMessage(anthropic.NewTextBlock(userInput)) conversation = append(conversation, userMessage) message, err := a.runInference(ctx, conversation) if err != nil { return err } conversation = append(conversation, message.ToParam()) for _, content := range message.Content { switch content.Type { case "text": fmt.Printf("\u001b[93mClaude\u001b[0m: %s\n", content.Text) } } } return nil }


It’s not a lot, is it? This loop in Run() lets us talk to Claude. But that’s already the heartbeat of this program.

And for a heartbeat, it’s pretty straightforward: we first print a prompt, ask the user to type something, add it to the conversation, send it to Claude, add Claude’s response to the conversation, print the response, and around we go.

This is every AI chat application you’ve ever used, except it’s in the terminal.

Let’s run it:

export ANTHROPIC_API_KEY="this is the last time i will tell you to set this"

Download the dependencies

go mod tidy

Run it

go run main.go


Then you can just talk to Claude, like this:

$ go run main.go Chat with Claude (use 'ctrl-c' to quit) You : Hey! I'm Thorsten! How are you? Claude : Hi Thorsten! I'm doing well, thanks for asking. It's nice to meet you. How are you doing today? Is there something I can help you with? You : Can you come up with any horse-related nicknames that make fun of my first name? Claude : I can try to come up with some playful horse-related nicknames based on "Thorsten":

  • Thorough-bred Thorsten
  • Trotsten
  • Thorsten the Stallion
  • Neighsten
  • Horseten
  • Thorsten the Galloper
  • Saddle-sten
  • Thorsten of the Mane
  • Canter-sten
  • Thorsteed

These are meant to be lighthearted - I hope they give you a chuckle!

You