Learn Claude Code

Learning Path Compare

Compare what capability is introduced between two chapters, why it appears there, and what you should focus on first.

Learning Jump

Choose the step you want to compare

This page is designed to explain capability shifts before it throws you into code-level detail.

One-Click Compare

Start with these safe comparison moves instead of selecting two chapters every time

These presets cover the most useful adjacent upgrades and stage boundaries. They work both for a first pass and for resetting when chapter boundaries start to blur.

Learning Jump

The Agent LoopTool Use

This is the next natural step in the path. It is the best way to study how the system grows chapter by chapter.

Carry From A

No short chapter thesis was extracted for this chapter yet.

New In B

No short chapter thesis was extracted for this chapter yet.

Progression

This is the next natural step in the path. It is the best way to study how the system grows chapter by chapter.

After B

Add a new tool without changing the main loop.

The Agent Loop

No short chapter thesis was extracted for this chapter yet.

130 LOC1 toolsCore Loop

Tool Use

No short chapter thesis was extracted for this chapter yet.

169 LOC4 toolsCore Loop
Chapter Distance

1

New Tools in B

3

Shared Tools

1

New Surface Area

5

Jump Diagnosis

This is the safest upgrade step

A and B are adjacent, so this is the cleanest way to see the exact new branch, state container, and reason for introducing it now.

Safer Reading Move

Read the execution flow first, then the architecture view, and only then decide whether you need the source diff.

Bridge docs most worth reading before this jump

Jump Reading Support

Before jumping from The Agent Loop to Tool Use, read these bridge docs

A good comparison page should not only show what was added. It should also point you to the best bridge docs for understanding the jump.

Mainline Flow Comparison

Compare how one request evolves between the two chapters: where the new branch appears, what writes back into the loop, and what remains a side lane.

The Agent Loop

How to Read

Read the mainline first, then inspect the side branches

Read top to bottom for time order. The center usually carries the mainline, while the sides hold branches, isolated lanes, or recovery paths. The key question is not how many nodes exist, but where this chapter introduces a new split and write-back.

Focus First

Focus first on how `messages`, `tool_use`, and `tool_result` close the loop.

Easy to Confuse

Do not confuse model reasoning with system action. The loop is what turns thought into work.

Build Goal

Be able to write a minimal but real agent loop by hand.

Node Legend

Entry

Where the current turn enters the system.

Process

A stable internal processing step.

Decision

Where the system chooses a branch.

Subprocess / Lane

Often used for external execution, sidecars, or isolated lanes.

Write-back / End

Where the turn ends or writes back into the loop.

Branch / Side Lane

Permission branches, autonomy scans, background slots, and worktree lanes often expand here.

Mainline

The path the system keeps returning to during the turn.

Branch / Side Lane

Permission branches, autonomy scans, background slots, and worktree lanes often expand here.

yesnoUser InputLLM Calltool_use?Execute BashAppend ResultOutput

Dashed borders usually indicate a subprocess or external lane; arrow labels explain why a branch was taken.

Tool Use

How to Read

Read the mainline first, then inspect the side branches

Read top to bottom for time order. The center usually carries the mainline, while the sides hold branches, isolated lanes, or recovery paths. The key question is not how many nodes exist, but where this chapter introduces a new split and write-back.

Focus First

Focus on the relationship between `ToolSpec`, the dispatch map, and `tool_result`.

Easy to Confuse

A tool schema is not the handler itself. One describes the tool to the model; the other executes it.

Build Goal

Add a new tool without changing the main loop.

Node Legend

Entry

Where the current turn enters the system.

Process

A stable internal processing step.

Decision

Where the system chooses a branch.

Subprocess / Lane

Often used for external execution, sidecars, or isolated lanes.

Write-back / End

Where the turn ends or writes back into the loop.

Branch / Side Lane

Permission branches, autonomy scans, background slots, and worktree lanes often expand here.

Mainline

The path the system keeps returning to during the turn.

Branch / Side Lane

Permission branches, autonomy scans, background slots, and worktree lanes often expand here.

yesnoUser InputLLM Calltool_use?Tool Dispatchbash / read / write / editAppend ResultOutput

Dashed borders usually indicate a subprocess or external lane; arrow labels explain why a branch was taken.

Architecture

Read module boundaries and collaboration first, then drop into implementation detail only if you need it.

The Agent Loop

What This Chapter Actually Adds

LoopState + tool_result feedback

The first chapter establishes the smallest closed loop: user input enters messages[], the model decides whether to call a tool, and the result flows back into the same loop.

Mainline

The path that actually pushes the system forward.

Agent Loop

NEW

Each turn calls the model, handles the output, then decides whether to continue.

State Records

The structures the system must remember and write back.

messages[]

NEW

User, assistant, and tool result history accumulates here.

tool_result write-back

NEW

The agent becomes real when tool results return into the next reasoning step.

Key Records

These are the state containers worth holding onto when you rebuild the system yourself.

LoopStateNEW

The smallest runnable session state.

Assistant ContentNEW

The model output for the current turn.

Primary Handoff Path

1

User message enters messages[]

2

Model emits tool_use or text

3

Tool result writes back into the next turn

Tool Use

What This Chapter Actually Adds

Tool specs + dispatch map

This chapter upgrades one tool call into a stable multi-tool routing layer while keeping the main loop unchanged.

Mainline

The path that actually pushes the system forward.

Stable Main Loop

The main loop still only owns model calls and write-back.

Control Plane

Decides how execution is controlled, gated, and redirected.

ToolSpec Catalog

NEW

Describes tool capabilities to the model.

Dispatch Map

NEW

Routes a tool call to the correct handler by name.

State Records

The structures the system must remember and write back.

tool_input

NEW

Structured tool arguments emitted by the model.

Key Records

These are the state containers worth holding onto when you rebuild the system yourself.

ToolSpecNEW

Schema plus description.

Dispatch EntryNEW

Mapping from tool name to function.

Primary Handoff Path

1

The model selects a tool

2

The dispatch map resolves the handler

3

The handler returns a tool_result

Tool Comparison

Only in The Agent Loop

None

Shared

bash

Only in Tool Use

read_filewrite_fileedit_file

Source Diff (Optional)

If you care about implementation detail, read the diff next. If you only care about the mechanism, the learning cards above should be enough. LOC Delta: +39 lines

s01 (s01_agent_loop.py) -> s02 (s02_tool_use.py)
11#!/usr/bin/env python3
2-# Harness: the loop -- keep feeding real tool results back into the model.
2+# Harness: tool dispatch -- expanding what the model can reach.
33"""
4-s01_agent_loop.py - The Agent Loop
4+s02_tool_use.py - Tool dispatch + message normalization
55
6-This file teaches the smallest useful coding-agent pattern:
6+The agent loop from s01 didn't change. We added tools to the dispatch map,
7+and a normalize_messages() function that cleans up the message list before
8+each API call.
79
8- user message
9- -> model reply
10- -> if tool_use: execute tools
11- -> write tool_result back to messages
12- -> continue
13-
14-It intentionally keeps the loop small, but still makes the loop state explicit
15-so later chapters can grow from the same structure.
10+Key insight: "The loop didn't change at all. I just added tools."
1611"""
1712
1813import os
1914import subprocess
20-from dataclasses import dataclass
15+from pathlib import Path
2116
22-try:
23- import readline
24- # #143 UTF-8 backspace fix for macOS libedit
25- readline.parse_and_bind('set bind-tty-special-chars off')
26- readline.parse_and_bind('set input-meta on')
27- readline.parse_and_bind('set output-meta on')
28- readline.parse_and_bind('set convert-meta off')
29- readline.parse_and_bind('set enable-meta-keybindings on')
30-except ImportError:
31- pass
32-
3317from anthropic import Anthropic
3418from dotenv import load_dotenv
3519
3620load_dotenv(override=True)
3721
3822if os.getenv("ANTHROPIC_BASE_URL"):
3923 os.environ.pop("ANTHROPIC_AUTH_TOKEN", None)
4024
25+WORKDIR = Path.cwd()
4126client = Anthropic(base_url=os.getenv("ANTHROPIC_BASE_URL"))
4227MODEL = os.environ["MODEL_ID"]
4328
44-SYSTEM = (
45- f"You are a coding agent at {os.getcwd()}. "
46- "Use bash to inspect and change the workspace. Act first, then report clearly."
47-)
29+SYSTEM = f"You are a coding agent at {WORKDIR}. Use tools to solve tasks. Act, don't explain."
4830
49-TOOLS = [{
50- "name": "bash",
51- "description": "Run a shell command in the current workspace.",
52- "input_schema": {
53- "type": "object",
54- "properties": {"command": {"type": "string"}},
55- "required": ["command"],
56- },
57-}]
5831
32+def safe_path(p: str) -> Path:
33+ path = (WORKDIR / p).resolve()
34+ if not path.is_relative_to(WORKDIR):
35+ raise ValueError(f"Path escapes workspace: {p}")
36+ return path
5937
60-@dataclass
61-class LoopState:
62- # The minimal loop state: history, loop count, and why we continue.
63- messages: list
64- turn_count: int = 1
65- transition_reason: str | None = None
6638
67-
6839def run_bash(command: str) -> str:
6940 dangerous = ["rm -rf /", "sudo", "shutdown", "reboot", "> /dev/"]
70- if any(item in command for item in dangerous):
41+ if any(d in command for d in dangerous):
7142 return "Error: Dangerous command blocked"
7243 try:
73- result = subprocess.run(
74- command,
75- shell=True,
76- cwd=os.getcwd(),
77- capture_output=True,
78- text=True,
79- timeout=120,
80- )
44+ r = subprocess.run(command, shell=True, cwd=WORKDIR,
45+ capture_output=True, text=True, timeout=120)
46+ out = (r.stdout + r.stderr).strip()
47+ return out[:50000] if out else "(no output)"
8148 except subprocess.TimeoutExpired:
8249 return "Error: Timeout (120s)"
83- except (FileNotFoundError, OSError) as e:
50+
51+
52+def run_read(path: str, limit: int = None) -> str:
53+ try:
54+ text = safe_path(path).read_text()
55+ lines = text.splitlines()
56+ if limit and limit < len(lines):
57+ lines = lines[:limit] + [f"... ({len(lines) - limit} more lines)"]
58+ return "\n".join(lines)[:50000]
59+ except Exception as e:
8460 return f"Error: {e}"
8561
86- output = (result.stdout + result.stderr).strip()
87- return output[:50000] if output else "(no output)"
8862
63+def run_write(path: str, content: str) -> str:
64+ try:
65+ fp = safe_path(path)
66+ fp.parent.mkdir(parents=True, exist_ok=True)
67+ fp.write_text(content)
68+ return f"Wrote {len(content)} bytes to {path}"
69+ except Exception as e:
70+ return f"Error: {e}"
8971
90-def extract_text(content) -> str:
91- if not isinstance(content, list):
92- return ""
93- texts = []
94- for block in content:
95- text = getattr(block, "text", None)
96- if text:
97- texts.append(text)
98- return "\n".join(texts).strip()
9972
73+def run_edit(path: str, old_text: str, new_text: str) -> str:
74+ try:
75+ fp = safe_path(path)
76+ content = fp.read_text()
77+ if old_text not in content:
78+ return f"Error: Text not found in {path}"
79+ fp.write_text(content.replace(old_text, new_text, 1))
80+ return f"Edited {path}"
81+ except Exception as e:
82+ return f"Error: {e}"
10083
101-def execute_tool_calls(response_content) -> list[dict]:
102- results = []
103- for block in response_content:
104- if block.type != "tool_use":
105- continue
106- command = block.input["command"]
107- print(f"\033[33m$ {command}\033[0m")
108- output = run_bash(command)
109- print(output[:200])
110- results.append({
111- "type": "tool_result",
112- "tool_use_id": block.id,
113- "content": output,
114- })
115- return results
11684
85+# -- Concurrency safety classification --
86+# Read-only tools can safely run in parallel; mutating tools must be serialized.
87+CONCURRENCY_SAFE = {"read_file"}
88+CONCURRENCY_UNSAFE = {"write_file", "edit_file"}
11789
118-def run_one_turn(state: LoopState) -> bool:
119- response = client.messages.create(
120- model=MODEL,
121- system=SYSTEM,
122- messages=state.messages,
123- tools=TOOLS,
124- max_tokens=8000,
125- )
126- state.messages.append({"role": "assistant", "content": response.content})
90+# -- The dispatch map: {tool_name: handler} --
91+TOOL_HANDLERS = {
92+ "bash": lambda **kw: run_bash(kw["command"]),
93+ "read_file": lambda **kw: run_read(kw["path"], kw.get("limit")),
94+ "write_file": lambda **kw: run_write(kw["path"], kw["content"]),
95+ "edit_file": lambda **kw: run_edit(kw["path"], kw["old_text"], kw["new_text"]),
96+}
12797
128- if response.stop_reason != "tool_use":
129- state.transition_reason = None
130- return False
98+TOOLS = [
99+ {"name": "bash", "description": "Run a shell command.",
100+ "input_schema": {"type": "object", "properties": {"command": {"type": "string"}}, "required": ["command"]}},
101+ {"name": "read_file", "description": "Read file contents.",
102+ "input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "limit": {"type": "integer"}}, "required": ["path"]}},
103+ {"name": "write_file", "description": "Write content to file.",
104+ "input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "content": {"type": "string"}}, "required": ["path", "content"]}},
105+ {"name": "edit_file", "description": "Replace exact text in file.",
106+ "input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "old_text": {"type": "string"}, "new_text": {"type": "string"}}, "required": ["path", "old_text", "new_text"]}},
107+]
131108
132- results = execute_tool_calls(response.content)
133- if not results:
134- state.transition_reason = None
135- return False
136109
137- state.messages.append({"role": "user", "content": results})
138- state.turn_count += 1
139- state.transition_reason = "tool_result"
140- return True
110+def normalize_messages(messages: list) -> list:
111+ """Clean up messages before sending to the API.
141112
113+ Three jobs:
114+ 1. Strip internal metadata fields the API doesn't understand
115+ 2. Ensure every tool_use has a matching tool_result (insert placeholder if missing)
116+ 3. Merge consecutive same-role messages (API requires strict alternation)
117+ """
118+ cleaned = []
119+ for msg in messages:
120+ clean = {"role": msg["role"]}
121+ if isinstance(msg.get("content"), str):
122+ clean["content"] = msg["content"]
123+ elif isinstance(msg.get("content"), list):
124+ clean["content"] = [
125+ {k: v for k, v in block.items()
126+ if not k.startswith("_")}
127+ for block in msg["content"]
128+ if isinstance(block, dict)
129+ ]
130+ else:
131+ clean["content"] = msg.get("content", "")
132+ cleaned.append(clean)
142133
143-def agent_loop(state: LoopState) -> None:
144- while run_one_turn(state):
145- pass
134+ # Collect existing tool_result IDs
135+ existing_results = set()
136+ for msg in cleaned:
137+ if isinstance(msg.get("content"), list):
138+ for block in msg["content"]:
139+ if isinstance(block, dict) and block.get("type") == "tool_result":
140+ existing_results.add(block.get("tool_use_id"))
146141
142+ # Find orphaned tool_use blocks and insert placeholder results
143+ for msg in cleaned:
144+ if msg["role"] != "assistant" or not isinstance(msg.get("content"), list):
145+ continue
146+ for block in msg["content"]:
147+ if not isinstance(block, dict):
148+ continue
149+ if block.get("type") == "tool_use" and block.get("id") not in existing_results:
150+ cleaned.append({"role": "user", "content": [
151+ {"type": "tool_result", "tool_use_id": block["id"],
152+ "content": "(cancelled)"}
153+ ]})
147154
155+ # Merge consecutive same-role messages
156+ if not cleaned:
157+ return cleaned
158+ merged = [cleaned[0]]
159+ for msg in cleaned[1:]:
160+ if msg["role"] == merged[-1]["role"]:
161+ prev = merged[-1]
162+ prev_c = prev["content"] if isinstance(prev["content"], list) \
163+ else [{"type": "text", "text": str(prev["content"])}]
164+ curr_c = msg["content"] if isinstance(msg["content"], list) \
165+ else [{"type": "text", "text": str(msg["content"])}]
166+ prev["content"] = prev_c + curr_c
167+ else:
168+ merged.append(msg)
169+ return merged
170+
171+
172+def agent_loop(messages: list):
173+ while True:
174+ response = client.messages.create(
175+ model=MODEL, system=SYSTEM,
176+ messages=normalize_messages(messages),
177+ tools=TOOLS, max_tokens=8000,
178+ )
179+ messages.append({"role": "assistant", "content": response.content})
180+ if response.stop_reason != "tool_use":
181+ return
182+ results = []
183+ for block in response.content:
184+ if block.type == "tool_use":
185+ handler = TOOL_HANDLERS.get(block.name)
186+ output = handler(**block.input) if handler else f"Unknown tool: {block.name}"
187+ print(f"> {block.name}:")
188+ print(output[:200])
189+ results.append({"type": "tool_result", "tool_use_id": block.id, "content": output})
190+ messages.append({"role": "user", "content": results})
191+
192+
148193if __name__ == "__main__":
149194 history = []
150195 while True:
151196 try:
152- query = input("\033[36ms01 >> \033[0m")
197+ query = input("\033[36ms02 >> \033[0m")
153198 except (EOFError, KeyboardInterrupt):
154199 break
155200 if query.strip().lower() in ("q", "exit", ""):
156201 break
157-
158202 history.append({"role": "user", "content": query})
159- state = LoopState(messages=history)
160- agent_loop(state)
161-
162- final_text = extract_text(history[-1]["content"])
163- if final_text:
164- print(final_text)
203+ agent_loop(history)
204+ response_content = history[-1]["content"]
205+ if isinstance(response_content, list):
206+ for block in response_content:
207+ if hasattr(block, "text"):
208+ print(block.text)
165209 print()