Lesson 49: Lab — Build Your Own Agent
Day 49 of 50 · ~20 min · Phase 6: Mastery
The Mission
You've learned the theory. Now it's time to build something real.
In this lab, you'll create a functional agent from scratch using the Claude Agent SDK. You'll define tools, implement the agentic loop, handle errors, and test your agent.
Your agent will be a code quality analyzer — it reads a codebase, identifies patterns, and generates a quality report. You'll use:
- The Agent SDK (Lesson 45)
- Custom tool design (Lesson 46)
- Error handling patterns (Lesson 46)
- Testing strategies (Lesson 46)
This is the most ambitious lab in the curriculum — a real agent you can use on your own projects.
What You'll Practice
- Defining custom tools with proper schemas and error handling
- Implementing the agentic loop with tool execution and result processing
- Managing context as the agent reasons and makes decisions
- Handling edge cases (infinite loops, API errors, missing files)
- Testing tools independently before running the agent
- Iterating on system prompts to improve agent behavior
- Debugging by logging and tracing agent decisions
Setup
Prerequisites
- Node.js with TypeScript, or Python 3.8+
- Claude API key (set
ANTHROPIC_API_KEY) - A test codebase to analyze (use an existing project or create a small one)
Initialize your project
TypeScript/Node:
mkdir code-quality-agent
cd code-quality-agent
npm init -y
npm install @anthropic-ai/sdk
npm install --save-dev typescript ts-node @types/node
npx tsc --init
Python:
mkdir code-quality-agent
cd code-quality-agent
python -m venv venv
source venv/bin/activate
pip install anthropic
Steps
Step 1: Design Your Tools (5 min)
Your agent needs tools to understand a codebase. Define these tools:
Tool 1: find_files
Search for files by pattern.
{
name: "find_files",
description: "Find files matching a pattern (e.g., *.js, *.ts, *.py)",
input_schema: {
type: "object",
properties: {
directory: {
type: "string",
description: "Directory to search in"
},
pattern: {
type: "string",
description: "File pattern (e.g., '*.js' or 'src/**/*.ts')"
}
},
required: ["directory", "pattern"]
}
}
Tool 2: read_file
Read a file's content.
{
name: "read_file",
description: "Read a file's full content",
input_schema: {
type: "object",
properties: {
file_path: {
type: "string",
description: "Path to the file"
}
},
required: ["file_path"]
}
}
Tool 3: analyze_syntax
Run a linter on code (use Node's built-in checks or Python's ast module).
{
name: "analyze_syntax",
description: "Run syntax and style checks on code",
input_schema: {
type: "object",
properties: {
code: {
type: "string",
description: "Code to analyze"
},
language: {
type: "string",
enum: ["javascript", "typescript", "python"],
description: "Programming language"
}
},
required: ["code", "language"]
}
}
Tool 4: report_finding
Log findings (this accumulates a report).
{
name: "report_finding",
description: "Report a quality finding",
input_schema: {
type: "object",
properties: {
file: { type: "string" },
issue_type: {
type: "string",
enum: ["style", "bug", "performance", "security", "documentation"]
},
description: { type: "string" },
line: { type: "number" }
},
required: ["file", "issue_type", "description"]
}
}
Step 2: Implement Tool Handlers (7 min)
Write the code that executes each tool. Include error handling.
TypeScript example:
import * as fs from "fs";
import * as path from "path";
import { glob } from "glob"; // npm install glob
const findings: any[] = [];
async function executeTool(name: string, input: any): Promise<any> {
try {
switch (name) {
case "find_files":
return await findFiles(input.directory, input.pattern);
case "read_file":
return await readFile(input.file_path);
case "analyze_syntax":
return await analyzeSyntax(input.code, input.language);
case "report_finding":
return reportFinding(input);
default:
return { error: "Unknown tool" };
}
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : String(error)
};
}
}
async function findFiles(directory: string, pattern: string): Promise<any> {
try {
const files = await glob(pattern, { cwd: directory });
return {
success: true,
files: files.slice(0, 20) // Limit to 20 files
};
} catch (error) {
return {
success: false,
error: `Failed to find files: ${error}`
};
}
}
async function readFile(filePath: string): Promise<any> {
try {
const content = fs.readFileSync(filePath, "utf-8");
const lines = content.split("\n").length;
return {
success: true,
content: content.slice(0, 5000), // Limit to 5000 chars
total_lines: lines
};
} catch (error) {
return {
success: false,
error: `Failed to read file: ${error}`
};
}
}
async function analyzeSyntax(code: string, language: string): Promise<any> {
// Simple checks (replace with real linter)
const issues = [];
if (code.includes("console.log") && language === "javascript") {
issues.push({
type: "debug-statement",
message: "Found console.log (should be removed in production)"
});
}
if (code.length > 500 && !code.includes("\n")) {
issues.push({
type: "style",
message: "Very long line (>500 chars)"
});
}
return { success: true, issues };
}
function reportFinding(input: any): any {
findings.push(input);
return {
success: true,
total_findings: findings.length
};
}
export { executeTool, findings };
Step 3: Implement the Agentic Loop (5 min)
Write the loop that sends messages, handles tool calls, and continues until done.
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic({
apiKey: process.env.ANTHROPIC_API_KEY
});
async function runAgent(userRequest: string) {
const systemPrompt = `You are a code quality analyzer. Your job is to analyze a codebase and identify quality issues.
When analyzing, you should:
1. Find all source code files in the target directory
2. Read and analyze each file
3. Check for common issues: style problems, potential bugs, security concerns, performance issues, missing documentation
4. Report findings using the report_finding tool
5. Summarize findings at the end
Be thorough but focus on actionable issues. Don't report trivial style issues.`;
const messages: Anthropic.Messages.MessageParam[] = [
{ role: "user", content: userRequest }
];
let loopCount = 0;
const maxLoops = 10; // Prevent infinite loops
while (loopCount < maxLoops) {
loopCount++;
console.log(`\n--- Loop ${loopCount} ---`);
const response = await client.messages.create({
model: "claude-3-5-sonnet-20241022",
max_tokens: 4096,
system: systemPrompt,
tools: [
// Insert your tool definitions here (from Step 1)
],
messages
});
console.log(`Stop reason: ${response.stop_reason}`);
// Check if agent is done
if (response.stop_reason === "end_turn") {
console.log("\nAgent completed.");
for (const block of response.content) {
if (block.type === "text") {
console.log("Final report:\n", block.text);
}
}
break;
}
// Handle tool calls
if (response.stop_reason === "tool_use") {
messages.push({ role: "assistant", content: response.content });
const toolResults = [];
for (const block of response.content) {
if (block.type === "tool_use") {
console.log(`Calling tool: ${block.name}`);
const result = await executeTool(block.name, block.input);
console.log(`Tool result:`, result);
toolResults.push({
type: "tool_result" as const,
tool_use_id: block.id,
content: JSON.stringify(result)
});
}
}
messages.push({ role: "user", content: toolResults });
}
}
if (loopCount >= maxLoops) {
console.log("Reached maximum loop iterations. Stopping.");
}
}
// Run the agent
runAgent("Analyze the quality of my code in ./src directory. Focus on JavaScript/TypeScript files.");
Step 4: Test and Iterate (3 min)
-
Test tools independently:
// Before running the full agent, test each tool const fileResults = await executeTool("find_files", { directory: "./src", pattern: "*.js" }); console.log("Files found:", fileResults); -
Run the agent:
npx ts-node agent.ts -
Observe and improve:
- Does the agent find files correctly?
- Does it read files?
- Does it report issues?
- Does it eventually stop (or loop infinitely)?
-
Debug loops:
- Add logging at each step (print what the agent is doing)
- If looping forever, adjust the system prompt to be more directive
- Add context windowing if the agent forgets earlier findings
-
Improve the system prompt:
- If the agent is unfocused, make the system prompt more specific
- If it's missing issues, give it examples of what to look for
- If it's reporting too many trivial issues, tell it to filter
Reflect
Answer these questions about what you've built:
-
What surprised you about building an agent? Was it the loop, error handling, or something else?
-
When did the agent struggle? Did it have trouble reading files? Finding patterns? Reporting findings?
-
What would make this agent better? More tools? Better system prompt? Different tool descriptions?
-
When would you use this agent in real work? Code review? Onboarding new developers? Continuous quality checks?
-
What did you learn about the agentic loop? How did your understanding change from Lesson 46?
Bonus Challenge
If you complete the basic agent, try these extensions:
Challenge 1: Agent teams Create two agents — one finds bugs, one finds style issues. Have them report to a shared findings list.
Challenge 2: Plugin
Package your agent as a Claude Code plugin with a skill that users can invoke: /quality-analyzer:analyze.
Challenge 3: CI/CD integration Run your agent in a GitHub Action or CI pipeline. Have it comment on pull requests with findings.
Challenge 4: Customization
Let users pass options: --focus=performance or --language=python to analyze specific aspects.
Key Takeaways
By completing this lab, you've practiced:
- Designing tools with proper schemas and error handling
- Implementing the agentic loop from scratch
- Managing tool execution and result processing
- Debugging agent behavior
- Iterating on system prompts
- Building something useful and real
This is the culmination of the entire curriculum. You're not just learning Claude Code — you're building with the foundations it's built on.