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.

393 LOC4 toolsCore Loop

Tool Use

No short chapter thesis was extracted for this chapter yet.

393 LOC4 toolsCore Loop
Chapter Distance

1

New Tools in B

0

Shared Tools

4

New Surface Area

0

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

bashread_filewrite_fileedit_file

Only in Tool Use

None

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: +0 lines

s01 (s01_agent_loop.ts) -> s02 (s02_tool_use.ts)
11// @ts-nocheck
22// AUTO-GENERATED by scripts/generate-self-contained-agents.mjs
3-// Source chapter: agents_self_contained/s01_agent_loop.ts
3+// Source chapter: agents_self_contained/s02_tool_use.ts
44// Tree-shaken bundle: chapter wiring + only used runtime code.
55// agents_self_contained/_runtime.ts
66import Anthropic from "@anthropic-ai/sdk";
77import dotenv from "dotenv";
88import { execSync, spawn, spawnSync } from "node:child_process";
99import fs from "node:fs";
1010import path from "node:path";
1111import process from "node:process";
1212import readline from "node:readline/promises";
1313import { stdin as input, stdout as output } from "node:process";
1414dotenv.config({ override: true });
1515var WORKDIR = process.cwd();
1616var DEFAULT_MODEL = "claude-3-5-sonnet-latest";
1717var anthropicClient = null;
1818function getModelId() {
1919 return process.env.MODEL_ID || DEFAULT_MODEL;
2020}
2121function getAnthropicClient() {
2222 if (anthropicClient) {
2323 return anthropicClient;
2424 }
2525 anthropicClient = new Anthropic({
2626 apiKey: process.env.ANTHROPIC_API_KEY || process.env.ANTHROPIC_AUTH_TOKEN || "missing-api-key",
2727 baseURL: process.env.ANTHROPIC_BASE_URL || void 0
2828 });
2929 return anthropicClient;
3030}
3131function createLoopContext() {
3232 return { workdir: WORKDIR, messages: [], meta: {} };
3333}
3434function safePath(relativePath) {
3535 const resolved = path.resolve(WORKDIR, relativePath);
3636 const relative = path.relative(WORKDIR, resolved);
3737 if (relative.startsWith("..") || path.isAbsolute(relative)) {
3838 throw new Error(`Path escapes workspace: ${relativePath}`);
3939 }
4040 return resolved;
4141}
4242function runBash(command, cwd = WORKDIR, timeoutMs = 12e4) {
4343 const dangerous = ["rm -rf /", "sudo ", "shutdown", "reboot", "> /dev/"];
4444 if (dangerous.some((item) => command.includes(item))) {
4545 return "Error: Dangerous command blocked";
4646 }
4747 try {
4848 const output2 = execSync(command, {
4949 cwd,
5050 encoding: "utf8",
5151 timeout: timeoutMs,
5252 maxBuffer: 10 * 1024 * 1024,
5353 shell: "/bin/zsh"
5454 });
5555 return output2.trim() || "(no output)";
5656 } catch (error) {
5757 const stdout = typeof error === "object" && error !== null && "stdout" in error ? String(error.stdout || "") : "";
5858 const stderr = typeof error === "object" && error !== null && "stderr" in error ? String(error.stderr || "") : "";
5959 const message = `${stdout}${stderr}`.trim();
6060 if (message) {
6161 return message.slice(0, 5e4);
6262 }
6363 if (error instanceof Error) {
6464 return `Error: ${error.message}`;
6565 }
6666 return "Error: Unknown shell failure";
6767 }
6868}
6969function readWorkspaceFile(filePath, limit) {
7070 try {
7171 const text = fs.readFileSync(safePath(filePath), "utf8");
7272 const lines = text.split(/\r?\n/u);
7373 if (typeof limit === "number" && limit > 0 && lines.length > limit) {
7474 return `${lines.slice(0, limit).join("\n")}
7575... (${lines.length - limit} more lines)`;
7676 }
7777 return text.slice(0, 5e4);
7878 } catch (error) {
7979 return `Error: ${error instanceof Error ? error.message : String(error)}`;
8080 }
8181}
8282function writeWorkspaceFile(filePath, content) {
8383 try {
8484 const resolved = safePath(filePath);
8585 fs.mkdirSync(path.dirname(resolved), { recursive: true });
8686 fs.writeFileSync(resolved, content, "utf8");
8787 return `Wrote ${Buffer.byteLength(content, "utf8")} bytes to ${filePath}`;
8888 } catch (error) {
8989 return `Error: ${error instanceof Error ? error.message : String(error)}`;
9090 }
9191}
9292function editWorkspaceFile(filePath, oldText, newText) {
9393 try {
9494 const resolved = safePath(filePath);
9595 const content = fs.readFileSync(resolved, "utf8");
9696 const index = content.indexOf(oldText);
9797 if (index === -1) {
9898 return `Error: Text not found in ${filePath}`;
9999 }
100100 const updated = `${content.slice(0, index)}${newText}${content.slice(index + oldText.length)}`;
101101 fs.writeFileSync(resolved, updated, "utf8");
102102 return `Edited ${filePath}`;
103103 } catch (error) {
104104 return `Error: ${error instanceof Error ? error.message : String(error)}`;
105105 }
106106}
107107function extractText(content) {
108108 if (typeof content === "string") {
109109 return content;
110110 }
111111 return content.map((block) => {
112112 if (typeof block === "object" && block !== null && "text" in block && typeof block.text === "string") {
113113 return block.text;
114114 }
115115 if (typeof block === "object" && block !== null && "content" in block && typeof block.content === "string") {
116116 return block.content;
117117 }
118118 return "";
119119 }).filter(Boolean).join("\n").trim();
120120}
121121function normalizeContent(content) {
122122 if (typeof content === "string") {
123123 return content;
124124 }
125125 return content.filter((block) => typeof block === "object" && block !== null).map((block) => {
126126 const cleaned = Object.fromEntries(
127127 Object.entries(block).filter(([key]) => !key.startsWith("_"))
128128 );
129129 return cleaned;
130130 });
131131}
132132function normalizeMessages(messages) {
133133 const cleaned = messages.map((message) => ({
134134 role: message.role,
135135 content: normalizeContent(message.content)
136136 }));
137137 const existingToolResults = /* @__PURE__ */ new Set();
138138 for (const message of cleaned) {
139139 if (!Array.isArray(message.content)) {
140140 continue;
141141 }
142142 for (const block of message.content) {
143143 if (typeof block === "object" && block !== null && block.type === "tool_result" && typeof block.tool_use_id === "string") {
144144 existingToolResults.add(block.tool_use_id);
145145 }
146146 }
147147 }
148148 for (const message of cleaned) {
149149 if (message.role !== "assistant" || !Array.isArray(message.content)) {
150150 continue;
151151 }
152152 for (const block of message.content) {
153153 if (typeof block === "object" && block !== null && block.type === "tool_use" && typeof block.id === "string" && !existingToolResults.has(block.id)) {
154154 cleaned.push({
155155 role: "user",
156156 content: [{ type: "tool_result", tool_use_id: block.id, content: "(cancelled)" }]
157157 });
158158 }
159159 }
160160 }
161161 const merged = [];
162162 for (const message of cleaned) {
163163 const previous = merged.at(-1);
164164 if (!previous || previous.role !== message.role) {
165165 merged.push(message);
166166 continue;
167167 }
168168 const previousBlocks = typeof previous.content === "string" ? [{ type: "text", text: previous.content }] : previous.content;
169169 const nextBlocks = typeof message.content === "string" ? [{ type: "text", text: message.content }] : message.content;
170170 previous.content = [...previousBlocks, ...nextBlocks];
171171 }
172172 return merged;
173173}
174174async function callModel(systemPrompt, messages, tools2) {
175175 const client = getAnthropicClient();
176176 return client.messages.create({
177177 model: getModelId(),
178178 system: systemPrompt,
179179 messages,
180180 tools: tools2,
181181 max_tokens: 8e3
182182 });
183183}
184184async function runAgentLoop(ctx2, config) {
185185 const maxTurns = config.maxTurns ?? 30;
186186 const toolMap = new Map(config.tools.map((tool) => [tool.spec.name, tool]));
187187 for (let turn = 0; turn < maxTurns; turn += 1) {
188188 if (config.beforeModel) {
189189 await config.beforeModel(ctx2);
190190 }
191191 const systemPrompt = typeof config.systemPrompt === "function" ? config.systemPrompt() : config.systemPrompt;
192192 const apiMessages = config.normalizeMessages ? normalizeMessages(ctx2.messages) : ctx2.messages;
193193 let response;
194194 let modelAttempt = 0;
195195 for (; ; ) {
196196 try {
197197 const retryMessages = config.normalizeMessages ? normalizeMessages(ctx2.messages) : ctx2.messages;
198198 response = await callModel(systemPrompt, retryMessages, config.tools.map((tool) => tool.spec));
199199 break;
200200 } catch (error) {
201201 modelAttempt += 1;
202202 const shouldRetry = config.onModelError ? await config.onModelError(error, ctx2, modelAttempt) : false;
203203 if (!shouldRetry) {
204204 throw error;
205205 }
206206 }
207207 }
208208 ctx2.messages.push({ role: "assistant", content: response.content });
209209 if (response.stop_reason !== "tool_use") {
210210 return;
211211 }
212212 const toolResults = [];
213213 for (const block of response.content) {
214214 if (block.type !== "tool_use") {
215215 continue;
216216 }
217217 const registered = toolMap.get(block.name);
218218 const toolUseId = String(block.id || cryptoRandomId());
219219 const input2 = typeof block.input === "object" && block.input !== null ? block.input : {};
220220 let outputText;
221221 if (!registered) {
222222 outputText = `Unknown tool: ${block.name}`;
223223 } else {
224224 outputText = await registered.execute(input2, ctx2, toolUseId);
225225 }
226226 console.log(`> ${block.name}`);
227227 console.log(outputText.slice(0, 200));
228228 toolResults.push({ type: "tool_result", tool_use_id: toolUseId, content: outputText });
229229 }
230230 ctx2.messages.push({ role: "user", content: toolResults });
231231 if (config.afterToolResults) {
232232 await config.afterToolResults(ctx2);
233233 }
234234 }
235235}
236236async function runRepl(options) {
237237 const rl = readline.createInterface({ input, output });
238238 try {
239239 if (options.onSessionStart) {
240240 await options.onSessionStart(options.ctx);
241241 }
242242 for (; ; ) {
243243 const query = await rl.question(`\x1B[36m${options.label} >> \x1B[0m`);
244244 if (!query.trim() || ["q", "exit"].includes(query.trim().toLowerCase())) {
245245 break;
246246 }
247247 options.ctx.messages.push({ role: "user", content: query });
248248 await options.run(options.ctx);
249249 const lastAssistant = [...options.ctx.messages].reverse().find((message) => message.role === "assistant");
250250 const finalText = lastAssistant ? extractText(lastAssistant.content) : "";
251251 if (finalText) {
252252 console.log(finalText);
253253 }
254254 console.log("");
255255 }
256256 } finally {
257257 rl.close();
258258 }
259259}
260260function cryptoRandomId() {
261261 return Math.random().toString(36).slice(2, 10);
262262}
263263function withGuards(toolName, input2, action, options) {
264264 const hookContext = { tool_name: toolName, tool_input: input2 };
265265 if (options?.permissions) {
266266 const decision = options.permissions.check(toolName, input2);
267267 if (decision.behavior === "deny") {
268268 return `Permission denied: ${decision.reason}`;
269269 }
270270 if (decision.behavior === "ask" && !options.permissions.askUser(toolName, input2)) {
271271 return `Permission denied by user for ${toolName}`;
272272 }
273273 }
274274 if (options?.hooks) {
275275 const pre = options.hooks.runHooks("PreToolUse", hookContext);
276276 if (pre.blocked) {
277277 return `Blocked by hook: ${pre.blockReason || "unknown"}`;
278278 }
279279 }
280280 let outputText = action();
281281 if (toolName === "read_file" && typeof input2.path === "string" && options?.onReadPath) {
282282 options.onReadPath(input2.path);
283283 }
284284 if (options?.compact) {
285285 outputText = options.compact.persistLargeOutput(options.toolUseId || cryptoRandomId(), outputText);
286286 }
287287 if (options?.hooks) {
288288 const post = options.hooks.runHooks("PostToolUse", { ...hookContext, tool_output: outputText });
289289 if (post.messages.length > 0) {
290290 outputText = `${outputText}
291291
292292${post.messages.join("\n")}`;
293293 }
294294 }
295295 return outputText.slice(0, 5e4);
296296}
297297function createBaseTools(options) {
298298 return [
299299 {
300300 spec: {
301301 name: "bash",
302302 description: "Run a shell command in the current workspace.",
303303 input_schema: {
304304 type: "object",
305305 properties: { command: { type: "string" } },
306306 required: ["command"]
307307 }
308308 },
309309 execute: (input2, _ctx, toolUseId) => withGuards(
310310 "bash",
311311 input2,
312312 () => runBash(String(input2.command || "")),
313313 { ...options, toolUseId }
314314 )
315315 },
316316 {
317317 spec: {
318318 name: "read_file",
319319 description: "Read file contents from the workspace.",
320320 input_schema: {
321321 type: "object",
322322 properties: {
323323 path: { type: "string" },
324324 limit: { type: "integer" }
325325 },
326326 required: ["path"]
327327 }
328328 },
329329 execute: (input2, _ctx, toolUseId) => withGuards(
330330 "read_file",
331331 input2,
332332 () => readWorkspaceFile(String(input2.path || ""), typeof input2.limit === "number" ? input2.limit : void 0),
333333 {
334334 ...options,
335335 toolUseId,
336336 onReadPath: (filePath) => options?.compact?.trackRecentFile(filePath)
337337 }
338338 )
339339 },
340340 {
341341 spec: {
342342 name: "write_file",
343343 description: "Write a file in the workspace.",
344344 input_schema: {
345345 type: "object",
346346 properties: {
347347 path: { type: "string" },
348348 content: { type: "string" }
349349 },
350350 required: ["path", "content"]
351351 }
352352 },
353353 execute: (input2, _ctx, toolUseId) => withGuards(
354354 "write_file",
355355 input2,
356356 () => writeWorkspaceFile(String(input2.path || ""), String(input2.content || "")),
357357 { ...options, toolUseId }
358358 )
359359 },
360360 {
361361 spec: {
362362 name: "edit_file",
363363 description: "Replace exact text in a file once.",
364364 input_schema: {
365365 type: "object",
366366 properties: {
367367 path: { type: "string" },
368368 old_text: { type: "string" },
369369 new_text: { type: "string" }
370370 },
371371 required: ["path", "old_text", "new_text"]
372372 }
373373 },
374374 execute: (input2, _ctx, toolUseId) => withGuards(
375375 "edit_file",
376376 input2,
377377 () => editWorkspaceFile(
378378 String(input2.path || ""),
379379 String(input2.old_text || ""),
380380 String(input2.new_text || "")
381381 ),
382382 { ...options, toolUseId }
383383 )
384384 }
385385 ];
386386}
387387
388-// agents_self_contained/s01_agent_loop.ts
388+// agents_self_contained/s02_tool_use.ts
389389var ctx = createLoopContext();
390-var tools = [createBaseTools()[0]];
390+var tools = createBaseTools();
391391await runRepl({
392- label: "s01",
392+ label: "s02",
393393 ctx,
394394 run: async (loopCtx) => {
395395 await runAgentLoop(loopCtx, {
396- systemPrompt: `You are a coding agent at ${WORKDIR}. Use bash to inspect and change the workspace. Act first, then report clearly.`,
396+ systemPrompt: `You are a coding agent at ${WORKDIR}. Use tools to solve tasks. Act, don't explain.`,
397397 tools,
398- maxTurns: 20
398+ normalizeMessages: true
399399 });
400400 }
401401});