|
| 1 | +--- |
| 2 | +layout: post |
| 3 | +title: "Building Your First Coding Agent — A Practical Walkthrough" |
| 4 | +date: 2026-04-30 |
| 5 | +categories: [ai, coding-agents] |
| 6 | +tags: [coding-agents, agentic-ai, agent-skills, automation, claude-code] |
| 7 | +description: "Stop reading about coding agents and build one. A practical walkthrough of a real, useful coding agent — the decisions made along the way, the things that broke, and what the finished version looks like." |
| 8 | +author: akashtalole |
| 9 | +--- |
| 10 | + |
| 11 | +Yesterday I covered the design principles for coding agents. Today I want to get concrete: let's actually build one. |
| 12 | + |
| 13 | +Not a toy demo. A useful agent that solves a real problem — one I've built and use in my own workflow. I'll walk through the decisions, the things that broke during development, and what the finished version looks like. |
| 14 | + |
| 15 | +--- |
| 16 | + |
| 17 | +## The Task: A Test Gap Finder Agent |
| 18 | + |
| 19 | +The agent I'm going to walk through does one thing: given a Python module, it finds functions that have no corresponding tests and reports them with enough context to write those tests. |
| 20 | + |
| 21 | +This is useful because test coverage metrics lie. 80% coverage tells you lines are executed, not that behaviour is tested. The gap finder finds the specific functions with no test coverage at all — the easiest wins. |
| 22 | + |
| 23 | +The agent needs to: |
| 24 | +1. Read the source module and identify all functions |
| 25 | +2. Read the test files and identify which functions are tested |
| 26 | +3. Compare the two and report the gaps |
| 27 | +4. For each gap, provide enough context to write a test |
| 28 | + |
| 29 | +Simple enough to build in a day. Real enough to use in production. |
| 30 | + |
| 31 | +--- |
| 32 | + |
| 33 | +## Step 1: Define the Tools |
| 34 | + |
| 35 | +Following the design principle from Day 20: define the minimum action space required. For this agent: |
| 36 | + |
| 37 | +```python |
| 38 | +tools = [ |
| 39 | + { |
| 40 | + "name": "read_file", |
| 41 | + "description": "Read the contents of a file at the given path. Returns the file content as a string. Use this to read source files and test files.", |
| 42 | + "input_schema": { |
| 43 | + "type": "object", |
| 44 | + "properties": { |
| 45 | + "path": { |
| 46 | + "type": "string", |
| 47 | + "description": "Absolute or relative path to the file" |
| 48 | + } |
| 49 | + }, |
| 50 | + "required": ["path"] |
| 51 | + } |
| 52 | + }, |
| 53 | + { |
| 54 | + "name": "list_files", |
| 55 | + "description": "List all files in a directory matching a glob pattern. Returns a list of file paths.", |
| 56 | + "input_schema": { |
| 57 | + "type": "object", |
| 58 | + "properties": { |
| 59 | + "directory": {"type": "string"}, |
| 60 | + "pattern": { |
| 61 | + "type": "string", |
| 62 | + "description": "Glob pattern, e.g. '**/*.py'" |
| 63 | + } |
| 64 | + }, |
| 65 | + "required": ["directory", "pattern"] |
| 66 | + } |
| 67 | + } |
| 68 | +] |
| 69 | +``` |
| 70 | + |
| 71 | +Two tools. Read-only. No write access, no execution. The blast radius of a mistake is zero — the agent can look, not touch. |
| 72 | + |
| 73 | +--- |
| 74 | + |
| 75 | +## Step 2: Implement the Tool Handlers |
| 76 | + |
| 77 | +```python |
| 78 | +import glob |
| 79 | +import os |
| 80 | + |
| 81 | +def handle_tool_call(tool_name, tool_input): |
| 82 | + if tool_name == "read_file": |
| 83 | + path = tool_input["path"] |
| 84 | + if not os.path.exists(path): |
| 85 | + return {"error": f"File not found: {path}"} |
| 86 | + with open(path, "r") as f: |
| 87 | + return {"content": f.read(), "path": path} |
| 88 | + |
| 89 | + elif tool_name == "list_files": |
| 90 | + directory = tool_input["directory"] |
| 91 | + pattern = tool_input.get("pattern", "**/*.py") |
| 92 | + full_pattern = os.path.join(directory, pattern) |
| 93 | + files = glob.glob(full_pattern, recursive=True) |
| 94 | + return {"files": files, "count": len(files)} |
| 95 | + |
| 96 | + return {"error": f"Unknown tool: {tool_name}"} |
| 97 | +``` |
| 98 | + |
| 99 | +Notice the structured error returns. When a file isn't found, the agent gets a message it can reason about — not a Python exception that crashes the loop. |
| 100 | + |
| 101 | +--- |
| 102 | + |
| 103 | +## Step 3: The Agent Loop |
| 104 | + |
| 105 | +```python |
| 106 | +import anthropic |
| 107 | + |
| 108 | +client = anthropic.Anthropic() |
| 109 | + |
| 110 | +def run_agent(source_path, test_directory): |
| 111 | + system_prompt = """You are a test coverage analyst. Your job is to identify |
| 112 | +functions in a Python source file that have no corresponding tests. |
| 113 | +
|
| 114 | +For each function with no tests, report: |
| 115 | +- The function name and signature |
| 116 | +- What it does (from the docstring or code) |
| 117 | +- What test cases would be most valuable |
| 118 | +
|
| 119 | +Be specific. Do not report functions that are clearly tested.""" |
| 120 | + |
| 121 | + messages = [ |
| 122 | + { |
| 123 | + "role": "user", |
| 124 | + "content": f"Find untested functions in {source_path}. Test files are in {test_directory}." |
| 125 | + } |
| 126 | + ] |
| 127 | + |
| 128 | + while True: |
| 129 | + response = client.messages.create( |
| 130 | + model="claude-opus-4-6", |
| 131 | + max_tokens=4096, |
| 132 | + system=system_prompt, |
| 133 | + tools=tools, |
| 134 | + messages=messages |
| 135 | + ) |
| 136 | + |
| 137 | + # Add assistant response to history |
| 138 | + messages.append({"role": "assistant", "content": response.content}) |
| 139 | + |
| 140 | + # If no tool calls, we're done |
| 141 | + if response.stop_reason == "end_turn": |
| 142 | + # Extract final text response |
| 143 | + for block in response.content: |
| 144 | + if hasattr(block, "text"): |
| 145 | + return block.text |
| 146 | + break |
| 147 | + |
| 148 | + # Process tool calls |
| 149 | + tool_results = [] |
| 150 | + for block in response.content: |
| 151 | + if block.type == "tool_use": |
| 152 | + result = handle_tool_call(block.name, block.input) |
| 153 | + tool_results.append({ |
| 154 | + "type": "tool_result", |
| 155 | + "tool_use_id": block.id, |
| 156 | + "content": str(result) |
| 157 | + }) |
| 158 | + |
| 159 | + # Add tool results and continue loop |
| 160 | + if tool_results: |
| 161 | + messages.append({"role": "user", "content": tool_results}) |
| 162 | +``` |
| 163 | + |
| 164 | +The loop is straightforward: send messages, check if done, process tool calls, add results, repeat. This is the core pattern for any tool-using agent with the Anthropic API. |
| 165 | + |
| 166 | +--- |
| 167 | + |
| 168 | +## What Broke During Development |
| 169 | + |
| 170 | +**First problem: the agent read too many files.** Without constraints, it tried to read every Python file in the project — which hit rate limits and took forever. Fix: narrow the task in the prompt. "Limit your search to the test directory provided — don't explore the whole project." |
| 171 | + |
| 172 | +**Second problem: false positives.** The agent reported functions as untested when they were tested indirectly through integration tests. Fix: add a note in the system prompt acknowledging this limitation. "Note: you can only detect direct test references, not indirect coverage through integration tests." |
| 173 | + |
| 174 | +**Third problem: vague output.** Early versions reported "function X has no tests" without enough context to act on. Fix: update the prompt to require the structured output format (function name, signature, purpose, suggested test cases). |
| 175 | + |
| 176 | +Each of these was a prompt or tool design fix, not a code fix. That's the pattern: when an agent misbehaves, look at the system prompt and tool descriptions before touching the implementation. |
| 177 | + |
| 178 | +--- |
| 179 | + |
| 180 | +## Using It |
| 181 | + |
| 182 | +```python |
| 183 | +report = run_agent( |
| 184 | + source_path="src/orders/processor.py", |
| 185 | + test_directory="tests/" |
| 186 | +) |
| 187 | +print(report) |
| 188 | +``` |
| 189 | + |
| 190 | +Output looks like: |
| 191 | + |
| 192 | +``` |
| 193 | +## Untested Functions Found |
| 194 | +
|
| 195 | +### `calculate_shipping_cost(order, destination, express=False)` |
| 196 | +**What it does:** Calculates shipping cost based on order weight and destination zone. |
| 197 | +**Suggested tests:** |
| 198 | +- Standard domestic shipping at different weight thresholds |
| 199 | +- Express vs standard rate comparison |
| 200 | +- International destination handling |
| 201 | +- Edge case: zero-weight order |
| 202 | +``` |
| 203 | + |
| 204 | +Actionable. Takes ten seconds to run. I run it on every module before closing a sprint. |
| 205 | + |
| 206 | +--- |
| 207 | + |
| 208 | +## The Broader Pattern |
| 209 | + |
| 210 | +Every coding agent follows this same structure: |
| 211 | + |
| 212 | +1. **Define the minimum tools** the task requires |
| 213 | +2. **Write structured error returns** so the agent can recover |
| 214 | +3. **Write a precise system prompt** that constrains scope and output format |
| 215 | +4. **Build the observe-reason-act loop** with message history |
| 216 | +5. **Test it on real cases, fix the misbehaviours in prompt and tool design** |
| 217 | + |
| 218 | +The implementation is the easy part. The design — especially the system prompt and the tool definitions — is where the quality of the agent lives. |
| 219 | + |
| 220 | +--- |
| 221 | + |
| 222 | +*Day 21 of the [30-Day AI Engineering series](/posts/30-day-ai-engineering-blog-plan/). Previous: [What Makes a Good Coding Agent](/posts/what-makes-a-good-coding-agent-design-principles/).* |
0 commit comments