ai, agents, deep dive,

Deep Dive: The Agent Loop Inside anthropics/claude-quickstarts

Cui Cui Follow Nov 12, 2026 · 5 mins read
Deep Dive: The Agent Loop Inside anthropics/claude-quickstarts
Share this

Companion to the hands-on tutorial: Build a tool-using agent loop with Claude Quickstarts on Cui.Guru. The tutorial shows how to use it. This post explains why it is built this way.

anthropics/claude-quickstarts (about 20.8k stars when I read it) is a collection of projects for building applications with the Claude API. Its agents/ folder is described as “a minimal educational implementation of LLM agents” and “NOT an SDK, but a reference implementation of key concepts”. The README claims the core logic is under 300 lines. The core idea it states: sophisticated behaviour emerges from “LLMs using tools in a loop”.

The workflow in motion

Animated workflow: user input, message history, model call, parallel tool execution, MCP tools, final response

Architecture

Three parts, as the README lists them: agent.py (API calls and tool execution), tools/ (native and MCP tools) and utils/ (history and MCP connections).

The core design idea

The whole agent is one while True. There is no planner and no state machine: the model decides, through tool_use blocks, whether it needs another turn. From agent.py:

tool_calls = [
    block for block in response.content if block.type == "tool_use"
]
...
if tool_calls:
    tool_results = await execute_tools(tool_calls, tool_dict)
    await self.history.add_message("user", tool_results)
else:
    return response

Everything else exists to keep that loop alive: a uniform tool contract, errors that do not break it, history truncation to manage context growth, and a way to import tools from elsewhere.

A tool is a name, a description and a schema

tools/base.py is the whole contract (abbreviated here):

@dataclass
class Tool:
    name: str
    description: str
    input_schema: dict[str, Any]

    def to_dict(self) -> dict[str, Any]:
        return {"name": self.name, "description": self.description,
                "input_schema": self.input_schema}

    async def execute(self, **kwargs) -> str:
        raise NotImplementedError(...)

The agent sends to_dict() for each tool and calls execute(**call.input) on the match. The bundled ThinkTool shows how small a tool can be: it appends nothing and returns "Thinking complete!"; its value is that the model gets a place to reason.

Failures become results

utils/tool_util.py runs tool calls in parallel by default with asyncio.gather, and converts problems into tool_result blocks the model can read:

except KeyError:
    response["content"] = f"Tool '{call.name}' not found"
    response["is_error"] = True
except Exception as e:
    response["content"] = f"Error executing tool: {str(e)}"
    response["is_error"] = True

History is bounded and cache-friendly

MessageHistory (utils/history_util.py) tracks tokens from API usage (including cache read and creation tokens). When total_tokens exceeds context_window_tokens, truncate() removes the oldest message pairs and puts a "[Earlier history has been truncated.]" notice at the front. format_for_api() marks the content blocks of the last message with cache_control: {"type": "ephemeral"} when caching is enabled, so the growing prefix can be cached between loop turns.

MCP tools are wrapped as ordinary tools

utils/connections.py builds a stdio or SSE connection from a config dict, lists the server’s tools and wraps each as an MCPTool, a Tool subclass whose execute calls the server (tools/mcp_tool.py). The loop never knows the difference. Agent.run_async adds the MCP tools for one run inside an AsyncExitStack and restores the original tool list in a finally block, so connections do not outlive the call.

Server-side tools follow another path: WebSearchServerTool and CodeExecutionServerTool only emit Anthropic’s server-tool dictionaries (types web_search_20250305 and code_execution_20250522); Anthropic runs them rather than this loop. The loop also sends an anthropic-beta: code-execution-2025-05-22 header by default, which message_params["extra_headers"] can extend.

Takeaways

  • An agent is a loop: call the model, run its tool calls, feed back results, stop when there are none.
  • Keep the tool contract tiny (name, description, JSON schema, async execute) so anything can be a tool, including MCP servers.
  • Return errors as is_error results so the model can recover.
  • Budget context explicitly and mark the tail for prompt caching.
  • The README is honest that this lacks production features; treat it as a reading exercise and port the shape to your stack.

Want a production-grade agent loop built for your product? Talk to Cui.Guru.

Join Newsletter
Get the latest news right in your inbox. We never spam!
Cui
Written by Cui Follow
Hi, I am Z, the coder for cuizhanming.com!

Click to load Disqus comments