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.
No short chapter thesis was extracted for this chapter yet.
No short chapter thesis was extracted for this chapter yet.
This is the next natural step in the path. It is the best way to study how the system grows chapter by chapter.
Add a new tool without changing the main loop.
The Agent Loop
No short chapter thesis was extracted for this chapter yet.
Tool Use
No short chapter thesis was extracted for this chapter yet.
1
0
4
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
Where the current turn enters the system.
A stable internal processing step.
Where the system chooses a branch.
Often used for external execution, sidecars, or isolated lanes.
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.
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
Where the current turn enters the system.
A stable internal processing step.
Where the system chooses a branch.
Often used for external execution, sidecars, or isolated lanes.
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.
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.
The path that actually pushes the system forward.
Agent Loop
NEWEach turn calls the model, handles the output, then decides whether to continue.
The structures the system must remember and write back.
messages[]
NEWUser, assistant, and tool result history accumulates here.
tool_result write-back
NEWThe 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.
The smallest runnable session state.
The model output for the current turn.
Primary Handoff Path
User message enters messages[]
Model emits tool_use or text
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.
The path that actually pushes the system forward.
Stable Main Loop
The main loop still only owns model calls and write-back.
Decides how execution is controlled, gated, and redirected.
ToolSpec Catalog
NEWDescribes tool capabilities to the model.
Dispatch Map
NEWRoutes a tool call to the correct handler by name.
The structures the system must remember and write back.
tool_input
NEWStructured tool arguments emitted by the model.
Key Records
These are the state containers worth holding onto when you rebuild the system yourself.
Schema plus description.
Mapping from tool name to function.
Primary Handoff Path
The model selects a tool
The dispatch map resolves the handler
The handler returns a tool_result
Tool Comparison
Only in The Agent Loop
None
Shared
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
| 1 | 1 | // @ts-nocheck | |
| 2 | 2 | // 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 | |
| 4 | 4 | // Tree-shaken bundle: chapter wiring + only used runtime code. | |
| 5 | 5 | // agents_self_contained/_runtime.ts | |
| 6 | 6 | import Anthropic from "@anthropic-ai/sdk"; | |
| 7 | 7 | import dotenv from "dotenv"; | |
| 8 | 8 | import { execSync, spawn, spawnSync } from "node:child_process"; | |
| 9 | 9 | import fs from "node:fs"; | |
| 10 | 10 | import path from "node:path"; | |
| 11 | 11 | import process from "node:process"; | |
| 12 | 12 | import readline from "node:readline/promises"; | |
| 13 | 13 | import { stdin as input, stdout as output } from "node:process"; | |
| 14 | 14 | dotenv.config({ override: true }); | |
| 15 | 15 | var WORKDIR = process.cwd(); | |
| 16 | 16 | var DEFAULT_MODEL = "claude-3-5-sonnet-latest"; | |
| 17 | 17 | var anthropicClient = null; | |
| 18 | 18 | function getModelId() { | |
| 19 | 19 | return process.env.MODEL_ID || DEFAULT_MODEL; | |
| 20 | 20 | } | |
| 21 | 21 | function getAnthropicClient() { | |
| 22 | 22 | if (anthropicClient) { | |
| 23 | 23 | return anthropicClient; | |
| 24 | 24 | } | |
| 25 | 25 | anthropicClient = new Anthropic({ | |
| 26 | 26 | apiKey: process.env.ANTHROPIC_API_KEY || process.env.ANTHROPIC_AUTH_TOKEN || "missing-api-key", | |
| 27 | 27 | baseURL: process.env.ANTHROPIC_BASE_URL || void 0 | |
| 28 | 28 | }); | |
| 29 | 29 | return anthropicClient; | |
| 30 | 30 | } | |
| 31 | 31 | function createLoopContext() { | |
| 32 | 32 | return { workdir: WORKDIR, messages: [], meta: {} }; | |
| 33 | 33 | } | |
| 34 | 34 | function safePath(relativePath) { | |
| 35 | 35 | const resolved = path.resolve(WORKDIR, relativePath); | |
| 36 | 36 | const relative = path.relative(WORKDIR, resolved); | |
| 37 | 37 | if (relative.startsWith("..") || path.isAbsolute(relative)) { | |
| 38 | 38 | throw new Error(`Path escapes workspace: ${relativePath}`); | |
| 39 | 39 | } | |
| 40 | 40 | return resolved; | |
| 41 | 41 | } | |
| 42 | 42 | function runBash(command, cwd = WORKDIR, timeoutMs = 12e4) { | |
| 43 | 43 | const dangerous = ["rm -rf /", "sudo ", "shutdown", "reboot", "> /dev/"]; | |
| 44 | 44 | if (dangerous.some((item) => command.includes(item))) { | |
| 45 | 45 | return "Error: Dangerous command blocked"; | |
| 46 | 46 | } | |
| 47 | 47 | try { | |
| 48 | 48 | const output2 = execSync(command, { | |
| 49 | 49 | cwd, | |
| 50 | 50 | encoding: "utf8", | |
| 51 | 51 | timeout: timeoutMs, | |
| 52 | 52 | maxBuffer: 10 * 1024 * 1024, | |
| 53 | 53 | shell: "/bin/zsh" | |
| 54 | 54 | }); | |
| 55 | 55 | return output2.trim() || "(no output)"; | |
| 56 | 56 | } catch (error) { | |
| 57 | 57 | const stdout = typeof error === "object" && error !== null && "stdout" in error ? String(error.stdout || "") : ""; | |
| 58 | 58 | const stderr = typeof error === "object" && error !== null && "stderr" in error ? String(error.stderr || "") : ""; | |
| 59 | 59 | const message = `${stdout}${stderr}`.trim(); | |
| 60 | 60 | if (message) { | |
| 61 | 61 | return message.slice(0, 5e4); | |
| 62 | 62 | } | |
| 63 | 63 | if (error instanceof Error) { | |
| 64 | 64 | return `Error: ${error.message}`; | |
| 65 | 65 | } | |
| 66 | 66 | return "Error: Unknown shell failure"; | |
| 67 | 67 | } | |
| 68 | 68 | } | |
| 69 | 69 | function readWorkspaceFile(filePath, limit) { | |
| 70 | 70 | try { | |
| 71 | 71 | const text = fs.readFileSync(safePath(filePath), "utf8"); | |
| 72 | 72 | const lines = text.split(/\r?\n/u); | |
| 73 | 73 | if (typeof limit === "number" && limit > 0 && lines.length > limit) { | |
| 74 | 74 | return `${lines.slice(0, limit).join("\n")} | |
| 75 | 75 | ... (${lines.length - limit} more lines)`; | |
| 76 | 76 | } | |
| 77 | 77 | return text.slice(0, 5e4); | |
| 78 | 78 | } catch (error) { | |
| 79 | 79 | return `Error: ${error instanceof Error ? error.message : String(error)}`; | |
| 80 | 80 | } | |
| 81 | 81 | } | |
| 82 | 82 | function writeWorkspaceFile(filePath, content) { | |
| 83 | 83 | try { | |
| 84 | 84 | const resolved = safePath(filePath); | |
| 85 | 85 | fs.mkdirSync(path.dirname(resolved), { recursive: true }); | |
| 86 | 86 | fs.writeFileSync(resolved, content, "utf8"); | |
| 87 | 87 | return `Wrote ${Buffer.byteLength(content, "utf8")} bytes to ${filePath}`; | |
| 88 | 88 | } catch (error) { | |
| 89 | 89 | return `Error: ${error instanceof Error ? error.message : String(error)}`; | |
| 90 | 90 | } | |
| 91 | 91 | } | |
| 92 | 92 | function editWorkspaceFile(filePath, oldText, newText) { | |
| 93 | 93 | try { | |
| 94 | 94 | const resolved = safePath(filePath); | |
| 95 | 95 | const content = fs.readFileSync(resolved, "utf8"); | |
| 96 | 96 | const index = content.indexOf(oldText); | |
| 97 | 97 | if (index === -1) { | |
| 98 | 98 | return `Error: Text not found in ${filePath}`; | |
| 99 | 99 | } | |
| 100 | 100 | const updated = `${content.slice(0, index)}${newText}${content.slice(index + oldText.length)}`; | |
| 101 | 101 | fs.writeFileSync(resolved, updated, "utf8"); | |
| 102 | 102 | return `Edited ${filePath}`; | |
| 103 | 103 | } catch (error) { | |
| 104 | 104 | return `Error: ${error instanceof Error ? error.message : String(error)}`; | |
| 105 | 105 | } | |
| 106 | 106 | } | |
| 107 | 107 | function extractText(content) { | |
| 108 | 108 | if (typeof content === "string") { | |
| 109 | 109 | return content; | |
| 110 | 110 | } | |
| 111 | 111 | return content.map((block) => { | |
| 112 | 112 | if (typeof block === "object" && block !== null && "text" in block && typeof block.text === "string") { | |
| 113 | 113 | return block.text; | |
| 114 | 114 | } | |
| 115 | 115 | if (typeof block === "object" && block !== null && "content" in block && typeof block.content === "string") { | |
| 116 | 116 | return block.content; | |
| 117 | 117 | } | |
| 118 | 118 | return ""; | |
| 119 | 119 | }).filter(Boolean).join("\n").trim(); | |
| 120 | 120 | } | |
| 121 | 121 | function normalizeContent(content) { | |
| 122 | 122 | if (typeof content === "string") { | |
| 123 | 123 | return content; | |
| 124 | 124 | } | |
| 125 | 125 | return content.filter((block) => typeof block === "object" && block !== null).map((block) => { | |
| 126 | 126 | const cleaned = Object.fromEntries( | |
| 127 | 127 | Object.entries(block).filter(([key]) => !key.startsWith("_")) | |
| 128 | 128 | ); | |
| 129 | 129 | return cleaned; | |
| 130 | 130 | }); | |
| 131 | 131 | } | |
| 132 | 132 | function normalizeMessages(messages) { | |
| 133 | 133 | const cleaned = messages.map((message) => ({ | |
| 134 | 134 | role: message.role, | |
| 135 | 135 | content: normalizeContent(message.content) | |
| 136 | 136 | })); | |
| 137 | 137 | const existingToolResults = /* @__PURE__ */ new Set(); | |
| 138 | 138 | for (const message of cleaned) { | |
| 139 | 139 | if (!Array.isArray(message.content)) { | |
| 140 | 140 | continue; | |
| 141 | 141 | } | |
| 142 | 142 | for (const block of message.content) { | |
| 143 | 143 | if (typeof block === "object" && block !== null && block.type === "tool_result" && typeof block.tool_use_id === "string") { | |
| 144 | 144 | existingToolResults.add(block.tool_use_id); | |
| 145 | 145 | } | |
| 146 | 146 | } | |
| 147 | 147 | } | |
| 148 | 148 | for (const message of cleaned) { | |
| 149 | 149 | if (message.role !== "assistant" || !Array.isArray(message.content)) { | |
| 150 | 150 | continue; | |
| 151 | 151 | } | |
| 152 | 152 | for (const block of message.content) { | |
| 153 | 153 | if (typeof block === "object" && block !== null && block.type === "tool_use" && typeof block.id === "string" && !existingToolResults.has(block.id)) { | |
| 154 | 154 | cleaned.push({ | |
| 155 | 155 | role: "user", | |
| 156 | 156 | content: [{ type: "tool_result", tool_use_id: block.id, content: "(cancelled)" }] | |
| 157 | 157 | }); | |
| 158 | 158 | } | |
| 159 | 159 | } | |
| 160 | 160 | } | |
| 161 | 161 | const merged = []; | |
| 162 | 162 | for (const message of cleaned) { | |
| 163 | 163 | const previous = merged.at(-1); | |
| 164 | 164 | if (!previous || previous.role !== message.role) { | |
| 165 | 165 | merged.push(message); | |
| 166 | 166 | continue; | |
| 167 | 167 | } | |
| 168 | 168 | const previousBlocks = typeof previous.content === "string" ? [{ type: "text", text: previous.content }] : previous.content; | |
| 169 | 169 | const nextBlocks = typeof message.content === "string" ? [{ type: "text", text: message.content }] : message.content; | |
| 170 | 170 | previous.content = [...previousBlocks, ...nextBlocks]; | |
| 171 | 171 | } | |
| 172 | 172 | return merged; | |
| 173 | 173 | } | |
| 174 | 174 | async function callModel(systemPrompt, messages, tools2) { | |
| 175 | 175 | const client = getAnthropicClient(); | |
| 176 | 176 | return client.messages.create({ | |
| 177 | 177 | model: getModelId(), | |
| 178 | 178 | system: systemPrompt, | |
| 179 | 179 | messages, | |
| 180 | 180 | tools: tools2, | |
| 181 | 181 | max_tokens: 8e3 | |
| 182 | 182 | }); | |
| 183 | 183 | } | |
| 184 | 184 | async function runAgentLoop(ctx2, config) { | |
| 185 | 185 | const maxTurns = config.maxTurns ?? 30; | |
| 186 | 186 | const toolMap = new Map(config.tools.map((tool) => [tool.spec.name, tool])); | |
| 187 | 187 | for (let turn = 0; turn < maxTurns; turn += 1) { | |
| 188 | 188 | if (config.beforeModel) { | |
| 189 | 189 | await config.beforeModel(ctx2); | |
| 190 | 190 | } | |
| 191 | 191 | const systemPrompt = typeof config.systemPrompt === "function" ? config.systemPrompt() : config.systemPrompt; | |
| 192 | 192 | const apiMessages = config.normalizeMessages ? normalizeMessages(ctx2.messages) : ctx2.messages; | |
| 193 | 193 | let response; | |
| 194 | 194 | let modelAttempt = 0; | |
| 195 | 195 | for (; ; ) { | |
| 196 | 196 | try { | |
| 197 | 197 | const retryMessages = config.normalizeMessages ? normalizeMessages(ctx2.messages) : ctx2.messages; | |
| 198 | 198 | response = await callModel(systemPrompt, retryMessages, config.tools.map((tool) => tool.spec)); | |
| 199 | 199 | break; | |
| 200 | 200 | } catch (error) { | |
| 201 | 201 | modelAttempt += 1; | |
| 202 | 202 | const shouldRetry = config.onModelError ? await config.onModelError(error, ctx2, modelAttempt) : false; | |
| 203 | 203 | if (!shouldRetry) { | |
| 204 | 204 | throw error; | |
| 205 | 205 | } | |
| 206 | 206 | } | |
| 207 | 207 | } | |
| 208 | 208 | ctx2.messages.push({ role: "assistant", content: response.content }); | |
| 209 | 209 | if (response.stop_reason !== "tool_use") { | |
| 210 | 210 | return; | |
| 211 | 211 | } | |
| 212 | 212 | const toolResults = []; | |
| 213 | 213 | for (const block of response.content) { | |
| 214 | 214 | if (block.type !== "tool_use") { | |
| 215 | 215 | continue; | |
| 216 | 216 | } | |
| 217 | 217 | const registered = toolMap.get(block.name); | |
| 218 | 218 | const toolUseId = String(block.id || cryptoRandomId()); | |
| 219 | 219 | const input2 = typeof block.input === "object" && block.input !== null ? block.input : {}; | |
| 220 | 220 | let outputText; | |
| 221 | 221 | if (!registered) { | |
| 222 | 222 | outputText = `Unknown tool: ${block.name}`; | |
| 223 | 223 | } else { | |
| 224 | 224 | outputText = await registered.execute(input2, ctx2, toolUseId); | |
| 225 | 225 | } | |
| 226 | 226 | console.log(`> ${block.name}`); | |
| 227 | 227 | console.log(outputText.slice(0, 200)); | |
| 228 | 228 | toolResults.push({ type: "tool_result", tool_use_id: toolUseId, content: outputText }); | |
| 229 | 229 | } | |
| 230 | 230 | ctx2.messages.push({ role: "user", content: toolResults }); | |
| 231 | 231 | if (config.afterToolResults) { | |
| 232 | 232 | await config.afterToolResults(ctx2); | |
| 233 | 233 | } | |
| 234 | 234 | } | |
| 235 | 235 | } | |
| 236 | 236 | async function runRepl(options) { | |
| 237 | 237 | const rl = readline.createInterface({ input, output }); | |
| 238 | 238 | try { | |
| 239 | 239 | if (options.onSessionStart) { | |
| 240 | 240 | await options.onSessionStart(options.ctx); | |
| 241 | 241 | } | |
| 242 | 242 | for (; ; ) { | |
| 243 | 243 | const query = await rl.question(`\x1B[36m${options.label} >> \x1B[0m`); | |
| 244 | 244 | if (!query.trim() || ["q", "exit"].includes(query.trim().toLowerCase())) { | |
| 245 | 245 | break; | |
| 246 | 246 | } | |
| 247 | 247 | options.ctx.messages.push({ role: "user", content: query }); | |
| 248 | 248 | await options.run(options.ctx); | |
| 249 | 249 | const lastAssistant = [...options.ctx.messages].reverse().find((message) => message.role === "assistant"); | |
| 250 | 250 | const finalText = lastAssistant ? extractText(lastAssistant.content) : ""; | |
| 251 | 251 | if (finalText) { | |
| 252 | 252 | console.log(finalText); | |
| 253 | 253 | } | |
| 254 | 254 | console.log(""); | |
| 255 | 255 | } | |
| 256 | 256 | } finally { | |
| 257 | 257 | rl.close(); | |
| 258 | 258 | } | |
| 259 | 259 | } | |
| 260 | 260 | function cryptoRandomId() { | |
| 261 | 261 | return Math.random().toString(36).slice(2, 10); | |
| 262 | 262 | } | |
| 263 | 263 | function withGuards(toolName, input2, action, options) { | |
| 264 | 264 | const hookContext = { tool_name: toolName, tool_input: input2 }; | |
| 265 | 265 | if (options?.permissions) { | |
| 266 | 266 | const decision = options.permissions.check(toolName, input2); | |
| 267 | 267 | if (decision.behavior === "deny") { | |
| 268 | 268 | return `Permission denied: ${decision.reason}`; | |
| 269 | 269 | } | |
| 270 | 270 | if (decision.behavior === "ask" && !options.permissions.askUser(toolName, input2)) { | |
| 271 | 271 | return `Permission denied by user for ${toolName}`; | |
| 272 | 272 | } | |
| 273 | 273 | } | |
| 274 | 274 | if (options?.hooks) { | |
| 275 | 275 | const pre = options.hooks.runHooks("PreToolUse", hookContext); | |
| 276 | 276 | if (pre.blocked) { | |
| 277 | 277 | return `Blocked by hook: ${pre.blockReason || "unknown"}`; | |
| 278 | 278 | } | |
| 279 | 279 | } | |
| 280 | 280 | let outputText = action(); | |
| 281 | 281 | if (toolName === "read_file" && typeof input2.path === "string" && options?.onReadPath) { | |
| 282 | 282 | options.onReadPath(input2.path); | |
| 283 | 283 | } | |
| 284 | 284 | if (options?.compact) { | |
| 285 | 285 | outputText = options.compact.persistLargeOutput(options.toolUseId || cryptoRandomId(), outputText); | |
| 286 | 286 | } | |
| 287 | 287 | if (options?.hooks) { | |
| 288 | 288 | const post = options.hooks.runHooks("PostToolUse", { ...hookContext, tool_output: outputText }); | |
| 289 | 289 | if (post.messages.length > 0) { | |
| 290 | 290 | outputText = `${outputText} | |
| 291 | 291 | ||
| 292 | 292 | ${post.messages.join("\n")}`; | |
| 293 | 293 | } | |
| 294 | 294 | } | |
| 295 | 295 | return outputText.slice(0, 5e4); | |
| 296 | 296 | } | |
| 297 | 297 | function createBaseTools(options) { | |
| 298 | 298 | return [ | |
| 299 | 299 | { | |
| 300 | 300 | spec: { | |
| 301 | 301 | name: "bash", | |
| 302 | 302 | description: "Run a shell command in the current workspace.", | |
| 303 | 303 | input_schema: { | |
| 304 | 304 | type: "object", | |
| 305 | 305 | properties: { command: { type: "string" } }, | |
| 306 | 306 | required: ["command"] | |
| 307 | 307 | } | |
| 308 | 308 | }, | |
| 309 | 309 | execute: (input2, _ctx, toolUseId) => withGuards( | |
| 310 | 310 | "bash", | |
| 311 | 311 | input2, | |
| 312 | 312 | () => runBash(String(input2.command || "")), | |
| 313 | 313 | { ...options, toolUseId } | |
| 314 | 314 | ) | |
| 315 | 315 | }, | |
| 316 | 316 | { | |
| 317 | 317 | spec: { | |
| 318 | 318 | name: "read_file", | |
| 319 | 319 | description: "Read file contents from the workspace.", | |
| 320 | 320 | input_schema: { | |
| 321 | 321 | type: "object", | |
| 322 | 322 | properties: { | |
| 323 | 323 | path: { type: "string" }, | |
| 324 | 324 | limit: { type: "integer" } | |
| 325 | 325 | }, | |
| 326 | 326 | required: ["path"] | |
| 327 | 327 | } | |
| 328 | 328 | }, | |
| 329 | 329 | execute: (input2, _ctx, toolUseId) => withGuards( | |
| 330 | 330 | "read_file", | |
| 331 | 331 | input2, | |
| 332 | 332 | () => readWorkspaceFile(String(input2.path || ""), typeof input2.limit === "number" ? input2.limit : void 0), | |
| 333 | 333 | { | |
| 334 | 334 | ...options, | |
| 335 | 335 | toolUseId, | |
| 336 | 336 | onReadPath: (filePath) => options?.compact?.trackRecentFile(filePath) | |
| 337 | 337 | } | |
| 338 | 338 | ) | |
| 339 | 339 | }, | |
| 340 | 340 | { | |
| 341 | 341 | spec: { | |
| 342 | 342 | name: "write_file", | |
| 343 | 343 | description: "Write a file in the workspace.", | |
| 344 | 344 | input_schema: { | |
| 345 | 345 | type: "object", | |
| 346 | 346 | properties: { | |
| 347 | 347 | path: { type: "string" }, | |
| 348 | 348 | content: { type: "string" } | |
| 349 | 349 | }, | |
| 350 | 350 | required: ["path", "content"] | |
| 351 | 351 | } | |
| 352 | 352 | }, | |
| 353 | 353 | execute: (input2, _ctx, toolUseId) => withGuards( | |
| 354 | 354 | "write_file", | |
| 355 | 355 | input2, | |
| 356 | 356 | () => writeWorkspaceFile(String(input2.path || ""), String(input2.content || "")), | |
| 357 | 357 | { ...options, toolUseId } | |
| 358 | 358 | ) | |
| 359 | 359 | }, | |
| 360 | 360 | { | |
| 361 | 361 | spec: { | |
| 362 | 362 | name: "edit_file", | |
| 363 | 363 | description: "Replace exact text in a file once.", | |
| 364 | 364 | input_schema: { | |
| 365 | 365 | type: "object", | |
| 366 | 366 | properties: { | |
| 367 | 367 | path: { type: "string" }, | |
| 368 | 368 | old_text: { type: "string" }, | |
| 369 | 369 | new_text: { type: "string" } | |
| 370 | 370 | }, | |
| 371 | 371 | required: ["path", "old_text", "new_text"] | |
| 372 | 372 | } | |
| 373 | 373 | }, | |
| 374 | 374 | execute: (input2, _ctx, toolUseId) => withGuards( | |
| 375 | 375 | "edit_file", | |
| 376 | 376 | input2, | |
| 377 | 377 | () => editWorkspaceFile( | |
| 378 | 378 | String(input2.path || ""), | |
| 379 | 379 | String(input2.old_text || ""), | |
| 380 | 380 | String(input2.new_text || "") | |
| 381 | 381 | ), | |
| 382 | 382 | { ...options, toolUseId } | |
| 383 | 383 | ) | |
| 384 | 384 | } | |
| 385 | 385 | ]; | |
| 386 | 386 | } | |
| 387 | 387 | ||
| 388 | - | // agents_self_contained/s01_agent_loop.ts | |
| 388 | + | // agents_self_contained/s02_tool_use.ts | |
| 389 | 389 | var ctx = createLoopContext(); | |
| 390 | - | var tools = [createBaseTools()[0]]; | |
| 390 | + | var tools = createBaseTools(); | |
| 391 | 391 | await runRepl({ | |
| 392 | - | label: "s01", | |
| 392 | + | label: "s02", | |
| 393 | 393 | ctx, | |
| 394 | 394 | run: async (loopCtx) => { | |
| 395 | 395 | 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.`, | |
| 397 | 397 | tools, | |
| 398 | - | maxTurns: 20 | |
| 398 | + | normalizeMessages: true | |
| 399 | 399 | }); | |
| 400 | 400 | } | |
| 401 | 401 | }); |