How the Full Coding Agent Loop Works in chapter5/coding-agent/

The Coding Agent implements a streaming fetch-process-execute-feedback cycle that repeatedly queries an LLM with dynamic system hints, executes requested tools via a lightweight registry, and appends results to the conversation history until the task completes or reaches the maximum iteration limit.

The chapter5/coding-agent/ directory in the bojieli/ai-agent-book repository contains a self-contained implementation of an autonomous coding agent. This module demonstrates how to orchestrate a full Coding Agent loop that leverages streaming LLM responses and a pure-Python tool registry to perform software engineering tasks iteratively without external dependencies beyond the LLM client libraries.

Architecture of the Coding Agent Loop

The core iteration cycle lives in CodingAgent.run within agent.py. The implementation follows a seven-phase architecture that maintains state across turns through the SystemState class while delegating tool execution to the ToolRegistry.

Phase 1: Initialization and Context Setup

The agent initializes its state in CodingAgent.__init__ (lines 24-45). This method instantiates SystemState to track the working directory and tool statistics, loads the declarative tool schemas from tools.json via _load_tools, and loads the system prompt template via _load_system_prompt. The initialization also configures the appropriate LLM client—either Anthropic or OpenAI/OpenRouter—based on the provider parameter.


# agent.py lines 24-45

def __init__(self, api_key, model, provider="anthropic"):
    self.system_state = SystemState()
    self.tool_registry = ToolRegistry()
    self._load_tools("tools.json")
    self._load_system_prompt("system_prompt.md")
    # Provider selection logic at lines 47-61

Phase 2: Message Preparation and System Hint Injection

When agent.run(user_message) is invoked, the loop timestamps the input and appends it to the conversation history (lines 33-38). Before each LLM call, the agent generates a dynamic system hint by calling self.system_state.get_system_hint() (lines 48-53). This hint includes the current working directory, OS information, Python version, tool-call statistics, and TODO list, providing the LLM with fresh environmental context every iteration.


# agent.py lines 33-38, 48-53

self.messages.append({
    "role": "user",
    "content": f"[{timestamp}] {user_message}"
})

# Inside the loop:

system_hint = self.system_state.get_system_hint()
self.messages.append({"role": "user", "content": system_hint})

Phase 3: Provider-Specific Streaming Implementation

The loop selects the appropriate streaming handler based on the provider type. For Anthropic models, _run_anthropic_iteration processes content_block_delta events (lines 95-104), while OpenAI models use _run_openai_iteration to handle tool_calls in streaming deltas (lines 31-38 and 40-55). Both methods yield text_delta events for streaming text output and collect tool invocations for subsequent execution.

For Anthropic (lines 9-16):

  • Emits text_delta when type == "content_block_delta"
  • Captures tool_use blocks when type == "content_block_start"

For OpenAI (lines 31-55):

  • Aggregates tool_calls arrays from response deltas
  • Handles function name and argument accumulation across chunks

Phase 4: Tool Discovery and Execution

After collecting all tool calls from the streaming response, the loop enters the execution phase (lines 68-84). For each tool_use block discovered, the agent retrieves the implementation via self.tool_registry.get_tool(name) and invokes tool.execute(**arguments). The tool registry maps names to concrete implementations such as Read, Write, Grep, and Bash defined in the codebase.


# agent.py lines 68-84

for tool_call in tool_calls:
    tool = self.tool_registry.get_tool(tool_call["name"])
    result = tool.execute(**tool_call["arguments"])
    # Results prepared for message history

Phase 5: Result Aggregation and History Management

Tool execution results are wrapped as tool_result messages and appended to self.messages (Anthropic: lines 92-98, OpenAI: lines 13-15). This closes the feedback loop, providing the LLM with concrete execution outcomes in the next iteration. The SystemState updates internal counters and the TODO list during this phase through subsequent calls to get_system_hint().

Phase 6: Termination Checking

The loop evaluates termination conditions at lines 53-57. If the LLM response contains no tool calls, the agent yields a done event and exits. Otherwise, the cycle repeats up to max_iterations, preventing infinite loops from malformed or overly aggressive tool usage.

Key Implementation Files

The Coding Agent loop spans several tightly-coupled components:

  • agent.py: Contains CodingAgent.run, provider abstraction, and streaming handlers (lines 55-63 for provider selection, lines 95-122 for Anthropic streaming).
  • system_state.py: Implements SystemState.get_system_hint() (lines 26-52) which generates the contextual metadata injected each iteration.
  • tool_registry.py: Provides ToolRegistry.get_tool() for runtime tool resolution.
  • tools.json: Declarative JSON schema defining available tools, their descriptions, and input parameters for the LLM.
  • main.py: Thin CLI wrapper that instantiates CodingAgent and streams events to stdout.

Practical Usage Examples

Running the Interactive CLI

The default entry point provides an interactive session:

python main.py

This script loads environment variables, creates a CodingAgent instance, and enters a read-eval-print loop that processes natural language requests through the full iteration cycle.

One-Shot Programmatic Execution

Integrate the Coding Agent into existing Python workflows by consuming the event iterator directly:

from chapter5.coding-agent.agent import CodingAgent
import os

agent = CodingAgent(
    api_key=os.getenv("ANTHROPIC_API_KEY"),
    model="claude-sonnet-5",
    provider="anthropic"
)

for event in agent.run("List all Python files in the current directory"):
    if event["type"] == "text_delta":
        print(event["delta"], end="", flush=True)
    elif event["type"] == "tool_call":
        print(f"\n[Calling: {event['tool']}]")
    elif event["type"] == "done":
        print("\n\n✅ Completed")

Jupyter Notebook Integration

For exploratory development, iterate over the agent events to display incremental progress:

agent = CodingAgent(api_key="key", provider="openai", model="gpt-4")

for ev in agent.run("Search for TODO comments"):
    if ev["type"] == "text_delta":
        display(ev["delta"])  # IPython.display

    elif ev["type"] == "tool_execution_complete":
        print(f"Result: {ev['result']}")

Summary

  • The Coding Agent loop is implemented as a Python generator in agent.py that yields streaming events (text_delta, tool_call, done).
  • Dynamic context is injected via SystemState.get_system_hint() before every LLM call, preventing stale environmental assumptions.
  • Provider abstraction supports both Anthropic and OpenAI streaming APIs with unified tool execution semantics.
  • Tool execution occurs through ToolRegistry, which maps LLM-requested function names to pure-Python implementations loaded from tools.json schemas.
  • Termination safety is enforced through iteration counting and detection of null tool-call responses.

Frequently Asked Questions

How does the Coding Agent prevent infinite tool loops?

The loop enforces a max_iterations parameter checked at lines 53-57 in agent.py. Additionally, SystemState tracks tool-call statistics across iterations, injecting this data into the system hint to inform the LLM of its execution history. The agent only continues if the LLM explicitly requests additional tools; otherwise, it yields a done event and terminates.

What information does the system hint contain?

According to system_state.py (lines 26-52), the system hint generated by get_system_hint() includes the current working directory, operating system details, Python version, accumulated TODO list, and counters for previous tool invocations. This contextual refresh prevents the agent from operating on stale assumptions about the filesystem or execution environment.

How does the agent handle different LLM providers?

The CodingAgent.__init__ method (lines 47-61) instantiates provider-specific clients. The run method then dispatches to _run_anthropic_iteration or _run_openai_iteration based on the provider type. Anthropic responses parse content_block_delta events (lines 95-104), while OpenAI responses handle tool_calls arrays in streaming deltas (lines 31-55), normalizing both into the internal tool execution format.

Can the Coding Agent execute tools without user intervention?

Yes. Once initialized with an API key, the agent.run() method operates autonomously through the full loop: receiving the user request, streaming LLM responses, executing requested tools, and feeding results back. The CLI wrapper in main.py demonstrates unsupervised operation for single-turn tasks, while the event iterator allows programmatic monitoring of each iteration's progress.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →