Introduction

Recently, I discovered an invaluable resource called Learn Claude Code. Despite its name suggesting a guide to using Claude Code, it actually teaches you how to build a nano Claude Code-like agent from the ground up, adding one mechanism at a time through twelve progressive stages.

Let me share the link again: https://learn.shareai.run/zh/

The website opens with a refreshingly direct statement: All AI programming agents share the same fundamental loop—call the model, execute tools, return results. Production-grade systems layer strategies, permissions, and lifecycle management on top of this core foundation.

Here's the essential loop in its simplest form:

while True:
    response = client.messages.create(messages=messages, tools=tools)
    if response.stop_reason != "tool_use":
        break
    for tool_call in response.content:
        result = execute_tool(tool_call.name, tool_call.input)
        messages.append(result)

Let me translate this into plain language:

  1. Call the Model: Send instructions to the LLM
  2. Execute Tools: Perform actions like reading/writing files or running commands
  3. Return Results: Inform the model about what was accomplished
  4. Continue Iterating: Repeat until the task completes

That's it. Really. Everything else consists of optimizations and patches built around this fundamental cycle.

Considering that many of our readers come from Java backgrounds, I've decided to implement this learning journey using Java. Let's explore together how to build an agent system step by step.

Progressive Learning Path

The website organizes these 12 stages (s01-s12) into five core competency advancements, demonstrating how a mature agent system gradually takes shape:

Stage S01: The Agent Loop

The minimum viable agent kernel requires just two components: a while loop + one tool.

Put simply, this represents the Agent's most essential "brain-hands" cycle—the fundamental pattern that powers all autonomous AI systems.

Java Implementation

Here's a complete Java implementation of the core agent loop:

public class AgentLoop {

    // Simulated Anthropic API client
    private static final String API_KEY = System.getenv("ANTHROPIC_API_KEY");
    private static final String MODEL_ID = System.getenv("MODEL_ID");
    private static final HttpClient client = HttpClient.newHttpClient();

    // Core agent loop
    public static void agentLoop(List<Map<String, Object>> messages) {
        while (true) {
            // 1. Call LLM
            System.out.println(">>> Thinking...");
            Map<String, Object> response = callLLM(messages);

            // 2. Add assistant response to history
            messages.add(response);

            // 3. Check stop reason
            // Note: This simplifies logic; actual implementation needs JSON parsing
            String stopReason = (String) response.get("stop_reason");

            if (!"tool_use".equals(stopReason)) {
                return; // Task complete, exit loop
            }

            // 4. Execute tools
            List<Map<String, Object>> toolResults = new ArrayList<>();
            List<Map<String, Object>> content = 
                (List<Map<String, Object>>) response.get("content");

            for (Map<String, Object> block : content) {
                if ("tool_use".equals(block.get("type"))) {
                    Map<String, Object> input = 
                        (Map<String, Object>) block.get("input");
                    String command = (String) input.get("command");
                    String toolId = (String) block.get("id");

                    System.out.println("\033[33m$ " + command + "\033[0m"); 
                    // Yellow output for commands

                    // Execute Bash command
                    String output = runBash(command);
                    System.out.println(
                        output.length() > 200 ? 
                        output.substring(0, 200) + "..." : output);

                    // Construct tool result
                    Map<String, Object> result = new HashMap<>();
                    result.put("type", "tool_result");
                    result.put("tool_use_id", toolId);
                    result.put("content", output);
                    toolResults.add(result);
                }
            }

            // 5. Add tool results as user input back to history
            Map<String, Object> userTurn = new HashMap<>();
            userTurn.put("role", "user");
            userTurn.put("content", toolResults);
            messages.add(userTurn);
        }
    }

    // Simulated LLM call (replace with actual SDK call in production)
    private static Map<String, Object> callLLM(
        List<Map<String, Object>> messages) {
        // Placeholder - actual implementation sends HTTP request 
        // to Anthropic API
        // Return structure must match API response format
        return new HashMap<>();
    }

    // Execute Shell commands with safety checks
    private static String runBash(String command) {
        // Security check
        if (command.contains("rm -rf /") || command.contains("sudo")) {
            return "Error: Dangerous command blocked";
        }

        try {
            ProcessBuilder pb = new ProcessBuilder("bash", "-c", command);
            pb.redirectErrorStream(true);
            Process p = pb.start();

            // Read output
            BufferedReader reader = new BufferedReader(
                new InputStreamReader(p.getInputStream()));
            StringBuilder output = new StringBuilder();
            String line;
            while ((line = reader.readLine()) != null) {
                output.append(line).append("\n");
            }

            // Wait for completion (with timeout)
            if (!p.waitFor(120, TimeUnit.SECONDS)) {
                p.destroyForcibly();
                return "Error: Timeout (120s)";
            }

            String result = output.toString().trim();
            return result.isEmpty() ? 
                "(no output)" : 
                result.substring(0, Math.min(result.length(), 50000));

        } catch (IOException | InterruptedException e) {
            return "Error: " + e.getMessage();
        }
    }

    public static void main(String[] args) {
        List<Map<String, Object>> history = new ArrayList<>();
        Scanner scanner = new Scanner(System.in);

        System.out.println("Agent started (enter 'q' to quit)");

        while (true) {
            System.out.print("\033[36ms01 >> \033[0m");
            String query = scanner.nextLine();

            if (query.trim().equalsIgnoreCase("q") || query.isEmpty()) {
                break;
            }

            Map<String, Object> userMsg = new HashMap<>();
            userMsg.put("role", "user");
            userMsg.put("content", query);
            history.add(userMsg);

            agentLoop(history);

            // Print final response
            System.out.println("Agent execution complete.");
        }
    }
}

This code contains the soul of every AI Agent. Let's break down its essential components.

Core Pattern: The ReAct Loop

The while loop in this code represents the Agent's beating heart. Its logic follows a clear pattern:

1. Think

Ask the LLM "What should be done?" The model analyzes the current context and determines the next appropriate action.

2. Act

If the LLM indicates it wants to call a tool (such as writing code or running a command), the code executes that tool. This is where abstract decisions become concrete actions.

3. Observe

Feed the tool's execution results (output, errors, status) back to the LLM. This closes the perception-action loop, enabling the model to understand consequences.

4. Cycle

Based on the results, the LLM decides whether to continue working or declare the task complete. This iterative process continues until objectives are achieved.

while (true) {
    // 1. Call LLM
    Map<String, Object> response = callLLM(messages);
    messages.add(response);

    // 2. Check if complete
    String stopReason = (String) response.get("stop_reason");
    if (!"tool_use".equals(stopReason)) {
        return; // Task finished
    }

    // 3. Execute tools
    List<Map<String, Object>> toolResults = executeTools(response);

    // 4. Return results to LLM
    messages.add(createUserTurn(toolResults));
}

State Management

The messages list serves as far more than a conversation log—it represents the Agent's short-term memory.

Critical Principles:

  • Every Iteration Matters: Each loop cycle must append new dialogue—whether human instructions or tool execution results—to the messages list.
  • Context Preservation: Without feeding tool results back into the conversation, the LLM remains unaware whether its previous commands succeeded or failed, making subsequent decisions impossible.
  • Accumulating Knowledge: As the conversation progresses, the messages list grows, providing richer context for the model's decisions. This mirrors how humans build understanding through accumulated experience.

Implementation Note: In production systems, you'll need to manage context window limits through summarization or selective retention strategies.

Standardized Tool Definitions

Within the code's TOOLS variable (not shown in the main loop but essential), we define what tools look like—their names, parameters, and expected behaviors.

// Iterate through tool call blocks in response
for (Map<String, Object> block : content) {
    if ("tool_use".equals(block.get("type"))) {
        // Extract command
        Map<String, Object> input = 
            (Map<String, Object>) block.get("input");
        String command = (String) input.get("command");
        String toolId = (String) block.get("id");

        // Execute Bash
        String output = runBash(command);

        // Build tool result
        Map<String, Object> result = new HashMap<>();
        result.put("type", "tool_result");
        result.put("tool_use_id", toolId);
        result.put("content", output);
        toolResults.add(result);
    }
}

Key Insight: The LLM doesn't actually "run" code—it outputs JSON conforming to a specific format (for example, {"name": "bash", "arguments": {"command": "ls"}}). Your code bears the responsibility of parsing this JSON and actually executing Runtime.exec() or equivalent operations.

This separation of concerns proves crucial: the model decides what to do; your implementation determines how to do it safely and effectively.

Security Fences

The runBash function serves as far more than a command executor—it acts as a firewall between the LLM and your system.

private static String runBash(String command) {
    // Security check
    if (command.contains("rm -rf /") || command.contains("sudo")) {
        return "Error: Dangerous command blocked";
    }

    // Command execution with timeout
    if (!p.waitFor(120, TimeUnit.SECONDS)) {
        p.destroyForcibly();
        return "Error: Timeout (120s)";
    }

    // Limit output length
    return result.substring(0, Math.min(result.length(), 50000));
}

Critical Security Considerations

⚠️ Never grant LLMs unrestricted shell access. While the checks shown here are simple (blacklist-based), production environments should implement:

  1. Sandboxed Execution: Run commands within Docker containers or similar isolation environments
  2. Allowlist Approach: Define permitted commands rather than blocking dangerous ones
  3. Resource Limits: Enforce CPU, memory, and time constraints
  4. Filesystem Restrictions: Limit accessible directories through chroot or namespace isolation
  5. Audit Logging: Record all executed commands for security review

The Principle: Trust but verify. Even with safety measures, monitor agent behavior continuously and implement kill switches for emergency intervention.

Understanding the Agent Loop Deeply

Why This Pattern Works

The ReAct (Reasoning + Acting) loop succeeds because it mirrors human problem-solving:

  1. We think about what needs to be done
  2. We act based on our reasoning
  3. We observe the results of our actions
  4. We adjust our approach based on feedback

This iterative cycle enables complex task completion through simple, repeated steps.

Common Pitfalls and Solutions

Pitfall 1: Infinite Loops

Agents can get stuck in repetitive patterns, executing the same tool calls indefinitely.

Solution: Implement iteration limits and detect repetitive patterns. Track tool call history and intervene when cycles exceed reasonable bounds.

Pitfall 2: Context Overflow

As conversations grow, message lists can exceed model context windows.

Solution: Implement context management strategies:

  • Summarize older exchanges
  • Retain only essential information
  • Use external memory systems for long-term storage

Pitfall 3: Tool Execution Failures

Tools may fail due to permissions, missing dependencies, or invalid inputs.

Solution: Implement robust error handling:

  • Catch and report errors clearly to the LLM
  • Provide actionable error messages
  • Allow the model to retry with corrected parameters

Beyond the Basics: Stage S01 and Beyond

This Stage S01 implementation represents the foundation. Subsequent stages build upon this core by adding:

  • S02-S03: Multiple tool support and parameter validation
  • S04-S06: Error handling, retry logic, and timeout management
  • S07-S09: Parallel tool execution and result aggregation
  • S10-S12: Advanced features like planning, reflection, and self-correction

Each stage introduces new capabilities while maintaining the fundamental loop structure.

Practical Implementation Tips

1. Start Simple

Begin with a single tool and basic loop. Get it working reliably before adding complexity. The foundation matters more than features.

2. Log Everything

Implement comprehensive logging for:

  • LLM requests and responses
  • Tool calls and results
  • Loop iterations and timing

This proves invaluable for debugging and optimization.

3. Test Incrementally

Test each component independently:

  • Verify LLM communication works
  • Confirm tool execution functions correctly
  • Validate loop termination conditions

4. Plan for Production

Even in early stages, consider:

  • Security implications
  • Error handling strategies
  • Performance characteristics
  • Monitoring requirements

Conclusion

The agent loop represents one of the most elegant patterns in modern AI development. Its simplicity masks profound capability—through repeated cycles of thinking, acting, and observing, agents can accomplish remarkably complex tasks.

This Stage S01 implementation provides the essential foundation. As you progress through subsequent stages, you'll add sophistication while maintaining this core structure. Remember: every production AI agent, regardless of complexity, ultimately relies on this fundamental loop.

The journey from zero to a functioning agent begins with understanding and implementing this pattern. Once mastered, you'll have the foundation for building increasingly sophisticated autonomous systems.

Key Takeaways:

  • The agent loop (think → act → observe → repeat) powers all AI agents
  • State management through the messages list enables contextual decision-making
  • Tool definitions create the bridge between model intentions and system actions
  • Security boundaries protect your system while enabling agent capabilities
  • Start simple, test thoroughly, and build complexity gradually

Welcome to the world of AI agent development. The loop awaits—let's build something remarkable together.


This tutorial is based on the Learn Claude Code educational resource. For complete stage-by-stage guidance, visit https://learn.shareai.run/zh/