Dashboard/Phase 6: Mastery
🔬 Lab20 min25 XP

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)

  1. 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);
    
  2. Run the agent:

    npx ts-node agent.ts
    
  3. Observe and improve:

    • Does the agent find files correctly?
    • Does it read files?
    • Does it report issues?
    • Does it eventually stop (or loop infinitely)?
  4. 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
  5. 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:

  1. What surprised you about building an agent? Was it the loop, error handling, or something else?

  2. When did the agent struggle? Did it have trouble reading files? Finding patterns? Reporting findings?

  3. What would make this agent better? More tools? Better system prompt? Different tool descriptions?

  4. When would you use this agent in real work? Code review? Onboarding new developers? Continuous quality checks?

  5. 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.


← Back to Curriculum · Lesson 50 →