Article illustration 1

When watching an AI agent edit files, debug errors, and iteratively improve code, it’s easy to assume complex proprietary technology powers it. Thorsten Ball’s recent deep dive shatters that illusion, demonstrating how to build a functional code-editing agent in under 400 lines of Go. The revelation? The core architecture is astonishingly simple: an LLM, a loop, and well-designed tools.

The Naked Truth About AI Agents

"It’s an LLM, a loop, and enough tokens. The rest? Elbow grease." — Thorsten Ball

Ball dismantles the mystique surrounding tools like GitHub Copilot and his own product Amp, emphasizing that modern LLMs like Anthropic’s Claude inherently understand tool usage. Agents emerge when we give models:
1. Tool definitions (programmatic capabilities)
2. Execution mechanisms (code that runs those tools)
3. State management (context preservation across interactions)

Building Blocks: From Chatbot to Agent

The journey starts with a basic CLI chatbot:

// Initialize Anthropic client & conversation state
type Agent struct {
    client *anthropic.Client
    messages []anthropic.Message
}

func (a *Agent) Run() {
    // Loop: User input → LLM inference → Output
}

This 90-line skeleton handles multi-turn conversations but lacks agency. The transformation begins with tool integration. Ball’s minimal agent implements three essential tools:

1. File Reading (read_file)

func ReadFile(input ReadFileInput) (string, error) {
    content, err := os.ReadFile(input.Path)
    return string(content), err
}

2. Directory Listing (list_files)

func ListFiles() ([]string, error) {
    entries, _ := os.ReadDir(".")
    var files []string
    for _, e := range entries {
        name := e.Name()
        if e.IsDir() { name += "/" }
        files = append(files, name)
    }
    return files, nil
}

3. String-Based Editing (edit_file)

func EditFile(input EditFileInput) (string, error) {
    content := strings.ReplaceAll(originalContent, input.OldStr, input.NewStr)
    return "OK", os.WriteFile(input.Path, []byte(content), 0644)
}

The Execution Loop: Where Magic Meets Mechanics

The agent’s intelligence lies in its loop structure:

for {
    response := a.client.Messages() // Send conversation + tools

    if response.Content.Type == "tool_use" {
        toolOutput := executeTool(response) // Run actual OS commands
        a.messages = append(a.messages, toolOutput) // Feed results back
    } 

    // ... handle text responses ...
}

When Claude detects a tool could help (e.g., reading a file mentioned by the user), it returns a structured tool_use request. The runtime executes the operation and injects the results into the next LLM payload. No hidden APIs — just clean prompt engineering and I/O.

Witnessing Emergent Behavior

With tools operational, Ball demonstrates the agent:
1. Reading project files to answer questions about code
2. Combining list_files + read_file to inspect go.mod
3. Writing new code via edit_file:

User: Create fizzbuzz.js
Agent: 
  Uses edit_file to write:
    for (let i=1; i<=30; i++) { 
      let output = ... // Full FizzBuzz impl
    }

The jaw-dropper: When asked to create a script decoding a ROT13 string, the agent wrote functional JavaScript without explicit instructions for decoding logic.

Why This Changes Everything

At ~190 lines with three tools, this agent exhibits behaviors resembling commercial coding assistants. Ball stresses this isn’t a toy:
- No special prompting is needed — Claude natively leverages tools when beneficial
- Token efficiency is manageable (critical for cost/performance)
- Extension points are obvious (add linters, test runners, etc.)

The "elbow grease" comes in refining tool reliability, UI/UX, and system prompts — but the core agent pattern is accessible to any developer. As Ball concludes:

"These models are incredibly powerful now. 300 lines of code and three tools and you’re talking to an alien intelligence that edits your code. Everything’s changing."

Source: Thorsten Ball, AmpCode