From 245975132e51fe99112d564fe771d974a8ada1cc Mon Sep 17 00:00:00 2001 From: octo-patch <266937838+octo-patch@users.noreply.github.com> Date: Thu, 9 Jul 2026 06:04:57 +0000 Subject: [PATCH 1/4] Add MiniMax image generation tool --- s02_tool_use/code.py | 41 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/s02_tool_use/code.py b/s02_tool_use/code.py index e6575119a..aeb0e9fd2 100644 --- a/s02_tool_use/code.py +++ b/s02_tool_use/code.py @@ -13,7 +13,7 @@ 循环本身(agent_loop)与 s01 完全一致。 """ -import os, subprocess +import os, subprocess, json, urllib.request from pathlib import Path try: @@ -114,6 +114,41 @@ def run_glob(pattern: str) -> str: return f"Error: {e}" +def run_generate_image(prompt: str, aspect_ratio: str = "1:1") -> str: + """Generate an image with MiniMax and return the image URL.""" + api_key = os.getenv("MINIMAX_API_KEY") + if not api_key: + return "Error: set MINIMAX_API_KEY to use image generation" + + payload = { + "model": "image-01", + "prompt": prompt, + "aspect_ratio": aspect_ratio, + "n": 1, + "response_format": "url", + } + req = urllib.request.Request( + "https://api.minimax.io/v1/image_generation", + data=json.dumps(payload).encode("utf-8"), + headers={ + "Authorization": f"Bearer {api_key}", + "Content-Type": "application/json", + }, + method="POST", + ) + try: + with urllib.request.urlopen(req, timeout=120) as resp: + data = json.loads(resp.read().decode("utf-8")) + status = data.get("base_resp", {}).get("status_code") + if status not in (None, 0): + msg = data.get("base_resp", {}).get("status_msg", "unknown error") + return f"Error: MiniMax image generation failed ({status}): {msg}" + urls = data.get("data", {}).get("image_urls") or [] + return urls[0] if urls else f"Error: no image URL returned: {data}" + except Exception as e: + return f"Error: {e}" + + # ═══════════════════════════════════════════════════════════ # NEW in s02: 工具定义(s01 只有一个 bash,现在扩展到 5 个) # ═══════════════════════════════════════════════════════════ @@ -129,6 +164,8 @@ def run_glob(pattern: str) -> str: "input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "old_text": {"type": "string"}, "new_text": {"type": "string"}}, "required": ["path", "old_text", "new_text"]}}, {"name": "glob", "description": "Find files matching a glob pattern.", "input_schema": {"type": "object", "properties": {"pattern": {"type": "string"}}, "required": ["pattern"]}}, + {"name": "generate_image", "description": "Generate an image from a text prompt using MiniMax.", + "input_schema": {"type": "object", "properties": {"prompt": {"type": "string"}, "aspect_ratio": {"type": "string", "enum": ["1:1", "16:9", "4:3", "3:2", "2:3", "3:4", "9:16", "21:9"]}}, "required": ["prompt"]}}, ] # ═══════════════════════════════════════════════════════════ @@ -137,7 +174,7 @@ def run_glob(pattern: str) -> str: TOOL_HANDLERS = { "bash": run_bash, "read_file": run_read, "write_file": run_write, - "edit_file": run_edit, "glob": run_glob, + "edit_file": run_edit, "glob": run_glob, "generate_image": run_generate_image, } From ade9b2e4fbe19150140136a014025a098d993084 Mon Sep 17 00:00:00 2001 From: octo-patch <266937838+octo-patch@users.noreply.github.com> Date: Sun, 12 Jul 2026 16:15:22 +0800 Subject: [PATCH 2/4] Complete image generation tool support --- .env.example | 6 + s02_tool_use/README.en.md | 21 ++- s02_tool_use/README.ja.md | 21 ++- s02_tool_use/README.md | 21 ++- s02_tool_use/code.py | 13 +- s02_tool_use/images/tool-dispatch.en.svg | 15 +- s02_tool_use/images/tool-dispatch.ja.svg | 15 +- s02_tool_use/images/tool-dispatch.svg | 15 +- tests/test_s02_generate_image.py | 129 ++++++++++++++++++ .../s02_tool_use/tool-dispatch.en.svg | 15 +- .../s02_tool_use/tool-dispatch.ja.svg | 15 +- .../s02_tool_use/tool-dispatch.svg | 15 +- web/src/data/generated/docs.json | 8 +- web/src/data/generated/versions.json | 27 ++-- 14 files changed, 275 insertions(+), 61 deletions(-) create mode 100644 tests/test_s02_generate_image.py diff --git a/.env.example b/.env.example index 1c16f1f05..c8cb6269f 100644 --- a/.env.example +++ b/.env.example @@ -30,6 +30,9 @@ MODEL_ID=claude-sonnet-4-6 # MODEL_ID=MiniMax-M3 # MODEL_ID=MiniMax-M2.7 # MODEL_ID=MiniMax-M2.7-highspeed +# Image generation tool (optional; s02) +# MINIMAX_API_KEY=sk-xxx +# MINIMAX_API_HOST=https://api.minimax.io # GLM (Zhipu) https://z.ai # ANTHROPIC_BASE_URL=https://api.z.ai/api/anthropic @@ -53,6 +56,9 @@ MODEL_ID=claude-sonnet-4-6 # MODEL_ID=MiniMax-M3 # MODEL_ID=MiniMax-M2.7 # MODEL_ID=MiniMax-M2.7-highspeed +# Image generation tool (optional; s02) +# MINIMAX_API_KEY=sk-xxx +# MINIMAX_API_HOST=https://api.minimaxi.com # GLM (Zhipu) https://open.bigmodel.cn # ANTHROPIC_BASE_URL=https://open.bigmodel.cn/api/anthropic diff --git a/s02_tool_use/README.en.md b/s02_tool_use/README.en.md index b939810a6..81706a018 100644 --- a/s02_tool_use/README.en.md +++ b/s02_tool_use/README.en.md @@ -30,7 +30,7 @@ Adding a tool to the Agent requires just two things: --- -## From 1 Tool to 5 Tools +## From 1 Tool to 6 Tools s01 had only bash: @@ -40,7 +40,7 @@ TOOLS = [{"name": "bash", ...}] def run_bash(command): ... ``` -s02 expands to 5 tools, each independently defined: +s02 expands to 6 tools, each independently defined: ```python TOOLS = [ @@ -49,6 +49,7 @@ TOOLS = [ {"name": "write_file", "description": "Write content to file.", ...}, {"name": "edit_file", "description": "Replace text in file once.", ...}, {"name": "glob", "description": "Find files by pattern.", ...}, + {"name": "generate_image", "description": "Generate an image from a text prompt.", ...}, ] ``` @@ -75,6 +76,9 @@ def run_edit(path, old_text, new_text): def run_glob(pattern): import glob as g return "\n".join(g.glob(pattern, root_dir=WORKDIR)) + +def run_generate_image(prompt, aspect_ratio="1:1"): + ... ``` --- @@ -88,6 +92,7 @@ TOOL_HANDLERS = { "write_file": run_write, "edit_file": run_edit, "glob": run_glob, + "generate_image": run_generate_image, } # Only one line changed in the loop — from hardcoded run_bash to dispatch lookup: @@ -125,7 +130,7 @@ The teaching version executes them one by one in the original `response.content` | Component | Before (s01) | After (s02) | |-----------|-------------|-------------| -| Tool count | 1 (bash) | 5 (+read, write, edit, glob) | +| Tool count | 1 (bash) | 6 (+read, write, edit, glob, generate_image) | | Tool execution | Hardcoded `run_bash()` | TOOL_HANDLERS dispatch lookup | | Path safety | None | safe_path validation (file tools only) | | Loop | `while True` + `stop_reason` | Identical to s01 | @@ -139,12 +144,20 @@ cd learn-claude-code python s02_tool_use/code.py ``` +The optional `generate_image` tool also needs these values in `.env`. Use `https://api.minimaxi.com` as the host for China mainland. + +```sh +MINIMAX_API_KEY=sk-xxx +MINIMAX_API_HOST=https://api.minimax.io +``` + Try these prompts: 1. `Read the file README.md and tell me what this project is about` 2. `Create a file called test.py that prints "hello", then read it back` 3. `Find all Python files in this directory` 4. `Read both README.md and requirements.txt, then create a summary file` +5. `Generate a 16:9 image of a coding workspace at sunrise` What to watch for: When does the model call just one tool, and when does it call multiple at once? Are multiple tool calls executed in the correct order? @@ -152,7 +165,7 @@ What to watch for: When does the model call just one tool, and when does it call ## What's Next -The Agent now has 5 specialized tools. File tools are protected by `safe_path`, but bash is unrestricted — `rm -rf /` still runs. +The Agent now has 6 specialized tools. File tools are protected by `safe_path`, but bash is unrestricted — `rm -rf /` still runs. → s03 Permission: Add a gate before tool execution — is this operation safe? Does it need user approval? diff --git a/s02_tool_use/README.ja.md b/s02_tool_use/README.ja.md index 23ff30aaa..8d437ab8d 100644 --- a/s02_tool_use/README.ja.md +++ b/s02_tool_use/README.ja.md @@ -30,7 +30,7 @@ Agent にツールを追加するには、たった二つ: --- -## 1 つのツールから 5 つのツールへ +## 1 つのツールから 6 つのツールへ s01 には bash だけだった: @@ -40,7 +40,7 @@ TOOLS = [{"name": "bash", ...}] def run_bash(command): ... ``` -s02 では 5 つに増え、各ツールは独立して定義される: +s02 では 6 つに増え、各ツールは独立して定義される: ```python TOOLS = [ @@ -49,6 +49,7 @@ TOOLS = [ {"name": "write_file", "description": "Write content to file.", ...}, {"name": "edit_file", "description": "Replace text in file once.", ...}, {"name": "glob", "description": "Find files by pattern.", ...}, + {"name": "generate_image", "description": "Generate an image from a text prompt.", ...}, ] ``` @@ -75,6 +76,9 @@ def run_edit(path, old_text, new_text): def run_glob(pattern): import glob as g return "\n".join(g.glob(pattern, root_dir=WORKDIR)) + +def run_generate_image(prompt, aspect_ratio="1:1"): + ... ``` --- @@ -88,6 +92,7 @@ TOOL_HANDLERS = { "write_file": run_write, "edit_file": run_edit, "glob": run_glob, + "generate_image": run_generate_image, } # ループ内で変更されたのは一行だけ — ハードコードの run_bash から検索ディスパッチへ: @@ -125,7 +130,7 @@ for block in response.content: | コンポーネント | 変更前 (s01) | 変更後 (s02) | |--------------|-------------|-------------| -| ツール数 | 1 (bash) | 5 (+read, write, edit, glob) | +| ツール数 | 1 (bash) | 6 (+read, write, edit, glob, generate_image) | | ツール実行 | ハードコード `run_bash()` | TOOL_HANDLERS 検索ディスパッチ | | パス安全性 | なし | safe_path 検証(file tools のみ) | | ループ | `while True` + `stop_reason` | s01 と完全に同一 | @@ -139,12 +144,20 @@ cd learn-claude-code python s02_tool_use/code.py ``` +オプションの `generate_image` ツールには、`.env` に以下の値も必要。中国本土では host を `https://api.minimaxi.com` に変更する。 + +```sh +MINIMAX_API_KEY=sk-xxx +MINIMAX_API_HOST=https://api.minimax.io +``` + 以下のプロンプトを試してみよう: 1. `Read the file README.md and tell me what this project is about` 2. `Create a file called test.py that prints "hello", then read it back` 3. `Find all Python files in this directory` 4. `Read both README.md and requirements.txt, then create a summary file` +5. `Generate a 16:9 image of a coding workspace at sunrise` 観察のポイント:モデルがツールを一つだけ呼び出すときと、複数同時に呼び出すときの違い。複数のツール呼び出しは正しい順序で実行されているか? @@ -152,7 +165,7 @@ python s02_tool_use/code.py ## 次へ -Agent は 5 つの専用ツールを持つようになった。file tools は `safe_path` で保護されるが、bash は制限なし — `rm -rf /` はまだ実行できる。 +Agent は 6 つの専用ツールを持つようになった。file tools は `safe_path` で保護されるが、bash は制限なし — `rm -rf /` はまだ実行できる。 → s03 Permission:ツール実行前にゲートを追加 — この操作は安全か? ユーザーの承認が必要か? diff --git a/s02_tool_use/README.md b/s02_tool_use/README.md index 179df58cd..5b80f70b6 100644 --- a/s02_tool_use/README.md +++ b/s02_tool_use/README.md @@ -30,7 +30,7 @@ s01 的循环完全保留(LLM 调用、stop_reason 判断、消息追加)。 --- -## 从 1 个工具到 5 个工具 +## 从 1 个工具到 6 个工具 s01 只有一个 bash: @@ -40,7 +40,7 @@ TOOLS = [{"name": "bash", ...}] def run_bash(command): ... ``` -s02 加到 5 个,每个工具都是独立定义: +s02 加到 6 个,每个工具都是独立定义: ```python TOOLS = [ @@ -49,6 +49,7 @@ TOOLS = [ {"name": "write_file", "description": "Write content to file.", ...}, {"name": "edit_file", "description": "Replace text in file once.", ...}, {"name": "glob", "description": "Find files by pattern.", ...}, + {"name": "generate_image", "description": "Generate an image from a text prompt.", ...}, ] ``` @@ -75,6 +76,9 @@ def run_edit(path, old_text, new_text): def run_glob(pattern): import glob as g return "\n".join(g.glob(pattern, root_dir=WORKDIR)) + +def run_generate_image(prompt, aspect_ratio="1:1"): + ... ``` --- @@ -88,6 +92,7 @@ TOOL_HANDLERS = { "write_file": run_write, "edit_file": run_edit, "glob": run_glob, + "generate_image": run_generate_image, } # 循环里只改了一行——从硬编码 run_bash 变成查表: @@ -125,7 +130,7 @@ for block in response.content: | 组件 | 之前 (s01) | 之后 (s02) | |------|-----------|-----------| -| 工具数量 | 1 (bash) | 5 (+read, write, edit, glob) | +| 工具数量 | 1 (bash) | 6 (+read, write, edit, glob, generate_image) | | 工具执行 | 硬编码 `run_bash()` | TOOL_HANDLERS 查表分发 | | 路径安全 | 无 | safe_path 校验(仅 file tools) | | 循环 | `while True` + `stop_reason` | 与 s01 完全一致 | @@ -139,12 +144,20 @@ cd learn-claude-code python s02_tool_use/code.py ``` +可选的 `generate_image` 工具还需要在 `.env` 中配置以下值。中国大陆区域请将 host 改为 `https://api.minimaxi.com`。 + +```sh +MINIMAX_API_KEY=sk-xxx +MINIMAX_API_HOST=https://api.minimax.io +``` + 试试这些 prompt: 1. `Read the file README.md and tell me what this project is about` 2. `Create a file called test.py that prints "hello", then read it back` 3. `Find all Python files in this directory` 4. `Read both README.md and requirements.txt, then create a summary file` +5. `Generate a 16:9 image of a coding workspace at sunrise` 观察重点:模型什么时候只调一个工具,什么时候一次调多个?多个工具调用的顺序和结果是否正确? @@ -152,7 +165,7 @@ python s02_tool_use/code.py ## 接下来 -现在 Agent 有 5 个专用工具。file tools 受 `safe_path` 保护,但 bash 不受限制,`rm -rf /` 还是能跑。 +现在 Agent 有 6 个专用工具。file tools 受 `safe_path` 保护,但 bash 不受限制,`rm -rf /` 还是能跑。 s03 Permission → 在工具执行之前加一道门:这个操作安全吗?需要用户批准吗? diff --git a/s02_tool_use/code.py b/s02_tool_use/code.py index aeb0e9fd2..d68192112 100644 --- a/s02_tool_use/code.py +++ b/s02_tool_use/code.py @@ -1,12 +1,12 @@ #!/usr/bin/env python3 """ -s02: Tool Use — 在 s01 基础上新增 4 个工具 + 分发映射。 +s02: Tool Use — 在 s01 基础上新增 5 个工具 + 分发映射。 运行: python s02_tool_use/code.py 需要: pip install anthropic python-dotenv + .env 中配置 ANTHROPIC_API_KEY 本文件 = s01 的全部代码 + 以下新增: - + run_read / run_write / run_edit / run_glob 四个工具实现 + + run_read / run_write / run_edit / run_glob / run_generate_image 五个工具实现 + TOOL_HANDLERS 分发映射(替代 s01 中硬编码的 run_bash 调用) + safe_path 路径安全校验 @@ -60,7 +60,7 @@ def run_bash(command: str) -> str: # ═══════════════════════════════════════════════════════════ -# NEW in s02: 4 个新工具 +# NEW in s02: 5 个新工具 # ═══════════════════════════════════════════════════════════ def safe_path(p: str) -> Path: @@ -119,6 +119,7 @@ def run_generate_image(prompt: str, aspect_ratio: str = "1:1") -> str: api_key = os.getenv("MINIMAX_API_KEY") if not api_key: return "Error: set MINIMAX_API_KEY to use image generation" + api_host = (os.getenv("MINIMAX_API_HOST") or "https://api.minimax.io").rstrip("/") payload = { "model": "image-01", @@ -128,7 +129,7 @@ def run_generate_image(prompt: str, aspect_ratio: str = "1:1") -> str: "response_format": "url", } req = urllib.request.Request( - "https://api.minimax.io/v1/image_generation", + f"{api_host}/v1/image_generation", data=json.dumps(payload).encode("utf-8"), headers={ "Authorization": f"Bearer {api_key}", @@ -150,7 +151,7 @@ def run_generate_image(prompt: str, aspect_ratio: str = "1:1") -> str: # ═══════════════════════════════════════════════════════════ -# NEW in s02: 工具定义(s01 只有一个 bash,现在扩展到 5 个) +# NEW in s02: 工具定义(s01 只有一个 bash,现在扩展到 6 个) # ═══════════════════════════════════════════════════════════ TOOLS = [ @@ -208,7 +209,7 @@ def agent_loop(messages: list): if __name__ == "__main__": - print("s02: Tool Use — 在 s01 基础上加了 4 个工具") + print("s02: Tool Use — 在 s01 基础上加了 5 个工具") print("输入问题,回车发送。输入 q 退出。\n") history = [] diff --git a/s02_tool_use/images/tool-dispatch.en.svg b/s02_tool_use/images/tool-dispatch.en.svg index 6fd2e6667..31e7e94b5 100644 --- a/s02_tool_use/images/tool-dispatch.en.svg +++ b/s02_tool_use/images/tool-dispatch.en.svg @@ -90,9 +90,14 @@ → run_edit() - - glob - → run_glob() + + glob + → run_glob() + + + + generate_image + → run_generate_image() @@ -101,8 +106,8 @@ - s01 Preserved (loop, LLM, decision — completely unchanged) + s01 Preserved (loop unchanged) - s02 New (5 tools + dispatch mapping) + s02 New (6 tools + dispatch mapping) Only 1 line changed in the loop: run_bash() → TOOL_HANDLERS[block.name]() diff --git a/s02_tool_use/images/tool-dispatch.ja.svg b/s02_tool_use/images/tool-dispatch.ja.svg index 8971d06ef..feaed8961 100644 --- a/s02_tool_use/images/tool-dispatch.ja.svg +++ b/s02_tool_use/images/tool-dispatch.ja.svg @@ -90,9 +90,14 @@ → run_edit() - - glob - → run_glob() + + glob + → run_glob() + + + + generate_image + → run_generate_image() @@ -101,8 +106,8 @@ - s01 保持(ループ、LLM、判定 — 完全に不変) + s01 保持(ループ不変) - s02 新規(5 つのツール + ディスパッチマッピング) + s02 新規(6 つのツール + ディスパッチマッピング) ループ内で変更されたのは 1 行だけ:run_bash() → TOOL_HANDLERS[block.name]() diff --git a/s02_tool_use/images/tool-dispatch.svg b/s02_tool_use/images/tool-dispatch.svg index a6b16ce2a..fde6ed70e 100644 --- a/s02_tool_use/images/tool-dispatch.svg +++ b/s02_tool_use/images/tool-dispatch.svg @@ -90,9 +90,14 @@ → run_edit() - - glob - → run_glob() + + glob + → run_glob() + + + + generate_image + → run_generate_image() @@ -101,8 +106,8 @@ - s01 保留(循环、LLM、判断——完全不变) + s01 保留(循环不变) - s02 新增(5 个工具 + 分发映射) + s02 新增(6 个工具 + 分发映射) 循环里只改了 1 行:run_bash() → TOOL_HANDLERS[block.name]() diff --git a/tests/test_s02_generate_image.py b/tests/test_s02_generate_image.py new file mode 100644 index 000000000..9ffcd1f08 --- /dev/null +++ b/tests/test_s02_generate_image.py @@ -0,0 +1,129 @@ +import importlib.util +import json +import os +import sys +import types +import unittest +from pathlib import Path +from unittest.mock import patch + + +REPO_ROOT = Path(__file__).resolve().parents[1] +MODULE_PATH = REPO_ROOT / "s02_tool_use" / "code.py" + + +def load_course_module(): + fake_anthropic = types.ModuleType("anthropic") + + class FakeAnthropic: + def __init__(self, *args, **kwargs): + self.messages = types.SimpleNamespace(create=None) + + fake_dotenv = types.ModuleType("dotenv") + setattr(fake_anthropic, "Anthropic", FakeAnthropic) + setattr(fake_dotenv, "load_dotenv", lambda override=True: None) + + previous_modules = { + "anthropic": sys.modules.get("anthropic"), + "dotenv": sys.modules.get("dotenv"), + } + previous_model_id = os.environ.get("MODEL_ID") + spec = importlib.util.spec_from_file_location("s02_generate_image_test", MODULE_PATH) + if spec is None or spec.loader is None: + raise RuntimeError(f"Unable to load {MODULE_PATH}") + module = importlib.util.module_from_spec(spec) + + sys.modules["anthropic"] = fake_anthropic + sys.modules["dotenv"] = fake_dotenv + try: + os.environ["MODEL_ID"] = "test-model" + spec.loader.exec_module(module) + return module + finally: + if previous_model_id is None: + os.environ.pop("MODEL_ID", None) + else: + os.environ["MODEL_ID"] = previous_model_id + for name, previous in previous_modules.items(): + if previous is None: + sys.modules.pop(name, None) + else: + sys.modules[name] = previous + + +class FakeResponse: + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + return False + + def read(self): + return json.dumps({ + "data": {"image_urls": ["https://example.com/generated.png"]}, + "base_resp": {"status_code": 0, "status_msg": "success"}, + }).encode("utf-8") + + +class GenerateImageTests(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.module = load_course_module() + + def test_uses_the_selected_regional_api_host(self): + previous_key = os.environ.get("MINIMAX_API_KEY") + previous_host = os.environ.get("MINIMAX_API_HOST") + try: + os.environ["MINIMAX_API_KEY"] = "test-key" + cases = [ + (None, "https://api.minimax.io/v1/image_generation"), + ("https://api.minimaxi.com/", "https://api.minimaxi.com/v1/image_generation"), + ] + for host, expected_url in cases: + with self.subTest(host=host): + if host is None: + os.environ.pop("MINIMAX_API_HOST", None) + else: + os.environ["MINIMAX_API_HOST"] = host + captured = {} + + def fake_urlopen(request, timeout): + captured["request"] = request + captured["timeout"] = timeout + return FakeResponse() + + with patch.object( + self.module.urllib.request, + "urlopen", + side_effect=fake_urlopen, + ): + result = self.module.run_generate_image("sunrise", "16:9") + + request = captured["request"] + self.assertEqual(result, "https://example.com/generated.png") + self.assertEqual(request.full_url, expected_url) + self.assertEqual(captured["timeout"], 120) + self.assertEqual( + json.loads(request.data.decode("utf-8")), + { + "model": "image-01", + "prompt": "sunrise", + "aspect_ratio": "16:9", + "n": 1, + "response_format": "url", + }, + ) + self.assertEqual(request.get_header("Authorization"), "Bearer test-key") + finally: + if previous_key is None: + os.environ.pop("MINIMAX_API_KEY", None) + else: + os.environ["MINIMAX_API_KEY"] = previous_key + if previous_host is None: + os.environ.pop("MINIMAX_API_HOST", None) + else: + os.environ["MINIMAX_API_HOST"] = previous_host + + +if __name__ == "__main__": + unittest.main() diff --git a/web/public/course-assets/s02_tool_use/tool-dispatch.en.svg b/web/public/course-assets/s02_tool_use/tool-dispatch.en.svg index 6fd2e6667..31e7e94b5 100644 --- a/web/public/course-assets/s02_tool_use/tool-dispatch.en.svg +++ b/web/public/course-assets/s02_tool_use/tool-dispatch.en.svg @@ -90,9 +90,14 @@ → run_edit() - - glob - → run_glob() + + glob + → run_glob() + + + + generate_image + → run_generate_image() @@ -101,8 +106,8 @@ - s01 Preserved (loop, LLM, decision — completely unchanged) + s01 Preserved (loop unchanged) - s02 New (5 tools + dispatch mapping) + s02 New (6 tools + dispatch mapping) Only 1 line changed in the loop: run_bash() → TOOL_HANDLERS[block.name]() diff --git a/web/public/course-assets/s02_tool_use/tool-dispatch.ja.svg b/web/public/course-assets/s02_tool_use/tool-dispatch.ja.svg index 8971d06ef..feaed8961 100644 --- a/web/public/course-assets/s02_tool_use/tool-dispatch.ja.svg +++ b/web/public/course-assets/s02_tool_use/tool-dispatch.ja.svg @@ -90,9 +90,14 @@ → run_edit() - - glob - → run_glob() + + glob + → run_glob() + + + + generate_image + → run_generate_image() @@ -101,8 +106,8 @@ - s01 保持(ループ、LLM、判定 — 完全に不変) + s01 保持(ループ不変) - s02 新規(5 つのツール + ディスパッチマッピング) + s02 新規(6 つのツール + ディスパッチマッピング) ループ内で変更されたのは 1 行だけ:run_bash() → TOOL_HANDLERS[block.name]() diff --git a/web/public/course-assets/s02_tool_use/tool-dispatch.svg b/web/public/course-assets/s02_tool_use/tool-dispatch.svg index a6b16ce2a..fde6ed70e 100644 --- a/web/public/course-assets/s02_tool_use/tool-dispatch.svg +++ b/web/public/course-assets/s02_tool_use/tool-dispatch.svg @@ -90,9 +90,14 @@ → run_edit() - - glob - → run_glob() + + glob + → run_glob() + + + + generate_image + → run_generate_image() @@ -101,8 +106,8 @@ - s01 保留(循环、LLM、判断——完全不变) + s01 保留(循环不变) - s02 新增(5 个工具 + 分发映射) + s02 新增(6 个工具 + 分发映射) 循环里只改了 1 行:run_bash() → TOOL_HANDLERS[block.name]() diff --git a/web/src/data/generated/docs.json b/web/src/data/generated/docs.json index 4e50b7380..d52eb2233 100644 --- a/web/src/data/generated/docs.json +++ b/web/src/data/generated/docs.json @@ -21,19 +21,19 @@ "version": "s02", "locale": "en", "title": "s02: Tool Use — Add a Tool, Add Just One Line", - "content": "# s02: Tool Use — Add a Tool, Add Just One Line\n\ns01 → `s02` → [s03](/en/s03) → s04 → ... → s20\n> *\"Add a tool, add just one handler\"* — The loop stays the same. Register the new tool in the dispatch map and you're done.\n>\n> **Harness Layer**: Tool Dispatch — Expanding the model's reach.\n\n---\n\n## Only One Tool: Bash\n\nThe s01 Agent has only one tool: bash. To read a file, `cat`; to write, `echo \"...\" > file.py`; to edit, `sed`.\n\nThe model thinks \"read this file\" but has to spell out `cat path/to/file`. An extra layer of translation that wastes tokens and invites errors.\n\n---\n\n## Overview: Tool Dispatch\n\n![Tool Dispatch](/course-assets/s02_tool_use/tool-dispatch.en.svg)\n\nThe s01 loop is fully preserved (LLM call, stop_reason check, message append — not a single word changed). The only change is in that one line of tool execution: `run_bash()` is replaced with `TOOL_HANDLERS[block.name]()` dispatch lookup.\n\nAdding a tool to the Agent requires just two things:\n\n1. **Define the tool**: Add one entry to the `TOOLS` array\n2. **Register the handler**: Add one mapping in the `TOOL_HANDLERS` dict\n\n---\n\n## From 1 Tool to 5 Tools\n\ns01 had only bash:\n\n```python\nTOOLS = [{\"name\": \"bash\", ...}]\n\ndef run_bash(command): ...\n```\n\ns02 expands to 5 tools, each independently defined:\n\n```python\nTOOLS = [\n {\"name\": \"bash\", \"description\": \"Run a shell command.\", ...},\n {\"name\": \"read_file\", \"description\": \"Read file contents.\", ...},\n {\"name\": \"write_file\", \"description\": \"Write content to file.\", ...},\n {\"name\": \"edit_file\", \"description\": \"Replace text in file once.\", ...},\n {\"name\": \"glob\", \"description\": \"Find files by pattern.\", ...},\n]\n```\n\nEach tool has its own implementation function:\n\n```python\ndef run_read(path, limit=None):\n lines = safe_path(path).read_text().splitlines()\n if limit:\n lines = lines[:limit]\n return \"\\n\".join(lines)\n\ndef run_write(path, content):\n safe_path(path).write_text(content)\n return f\"Wrote {len(content)} bytes to {path}\"\n\ndef run_edit(path, old_text, new_text):\n text = safe_path(path).read_text()\n if old_text not in text:\n return \"Error: text not found\"\n safe_path(path).write_text(text.replace(old_text, new_text, 1))\n return f\"Edited {path}\"\n\ndef run_glob(pattern):\n import glob as g\n return \"\\n\".join(g.glob(pattern, root_dir=WORKDIR))\n```\n\n---\n\n## Tool Dispatch\n\n```python\nTOOL_HANDLERS = {\n \"bash\": run_bash,\n \"read_file\": run_read,\n \"write_file\": run_write,\n \"edit_file\": run_edit,\n \"glob\": run_glob,\n}\n\n# Only one line changed in the loop — from hardcoded run_bash to dispatch lookup:\nfor block in response.content:\n if block.type == \"tool_use\":\n handler = TOOL_HANDLERS[block.name] # lookup\n output = handler(**block.input) # call\n results.append(...)\n```\n\nAdding a tool = one entry in `TOOLS` array + one line in `TOOL_HANDLERS` dict. The loop stays the same.\n\n---\n\n## Multiple Tool Calls\n\nThe model often returns multiple tool_use calls at once — \"read a.py and b.py, then list all .py files\".\n\nThe teaching version executes them one by one in the original `response.content` order. CC's approach is more complex: it slices the original order into consecutive batches, where concurrency-safe tools within a batch run in parallel, and batches are strictly sequential (see appendix).\n\n---\n\n## Quick Reference\n\n| Concept | One-Liner |\n|---------|-----------|\n| TOOL_HANDLERS | Tool name → handler function dict. Add a tool = add one mapping line |\n| Tool Definition | JSON schema telling the model \"what I can do\" |\n| Multiple tool calls | Model may return multiple tool_use at once; teaching version executes them in original order |\n| Loop Unchanged | s01's `while True` loop — not a single line changed |\n\n---\n\n## Changes from s01\n\n| Component | Before (s01) | After (s02) |\n|-----------|-------------|-------------|\n| Tool count | 1 (bash) | 5 (+read, write, edit, glob) |\n| Tool execution | Hardcoded `run_bash()` | TOOL_HANDLERS dispatch lookup |\n| Path safety | None | safe_path validation (file tools only) |\n| Loop | `while True` + `stop_reason` | Identical to s01 |\n\n---\n\n## Try It\n\n```sh\ncd learn-claude-code\npython s02_tool_use/code.py\n```\n\nTry these prompts:\n\n1. `Read the file README.md and tell me what this project is about`\n2. `Create a file called test.py that prints \"hello\", then read it back`\n3. `Find all Python files in this directory`\n4. `Read both README.md and requirements.txt, then create a summary file`\n\nWhat to watch for: When does the model call just one tool, and when does it call multiple at once? Are multiple tool calls executed in the correct order?\n\n---\n\n## What's Next\n\nThe Agent now has 5 specialized tools. File tools are protected by `safe_path`, but bash is unrestricted — `rm -rf /` still runs.\n\n→ s03 Permission: Add a gate before tool execution — is this operation safe? Does it need user approval?\n\n
\nDive into CC Source Code\n\n> The following is based on a review of CC source code `Tool.ts`, `tools.ts`, `toolOrchestration.ts`, `toolExecution.ts`, and `StreamingToolExecutor.ts`.\n\n### 1. Tool Definition Approach\n\n**Teaching version**: `TOOLS` array + `TOOL_HANDLERS` dict. Definition and implementation are separate.\n**CC**: Each tool is an independent object created by `buildTool()`, containing schema, validation, permissions, and execution. `getAllBaseTools()` aggregates all tools.\n\nThe teaching version's separation is clearer for teaching — readers immediately see \"add a tool = two definitions\".\n\n### 2. Concurrency Safety: isConcurrencySafe()\n\n![Tool Concurrency](/course-assets/s02_tool_use/concurrency-comparison.en.svg)\n\nThe teaching version executes tools one by one in original order, without concurrency. CC uses `isConcurrencySafe(input)` to determine concurrency — note this isn't simply \"read-only vs write\", but judges by specific input:\n\n| | isReadOnly | isConcurrencySafe |\n|---|---|---|\n| FileRead | true | true |\n| Glob | true | true |\n| Bash `ls` | true | **true** ← key difference |\n| Bash `rm` | false | false |\n| TaskCreate | false | **true** ← modifies state but can be concurrent (introduced in s12) |\n\nCC's Bash tool's `isConcurrencySafe` equals `isReadOnly` — read-only commands can be concurrent, write commands cannot. TaskCreate modifies task files, but each writes a different file, so it can be concurrent.\n\n### 3. Partition Algorithm\n\nCC's `partitionToolCalls()` (`toolOrchestration.ts:91-115`) doesn't split into two groups — it batches tool calls **by consecutive blocks**:\n\n```\n[read A, read B, glob *.py, bash \"rm x\", read C]\n → batch1(concurrent): [read A, read B, glob *.py]\n → batch2(serial): [bash \"rm x\"]\n → batch3(concurrent): [read C]\n```\n\nConsecutive concurrency-safe calls are grouped into the same batch for truly concurrent execution (`toolOrchestration.ts:152-176`, with a concurrency limit). When a non-concurrency-safe call is encountered, a new batch starts for serial execution. Batches are strictly sequential.\n\n### 4. Validation Pipeline\n\nEach tool call in CC goes through a strict 5-step validation (`toolExecution.ts`):\n\n1. **Zod schema validation** (`614-680`, teaching version uses JSON Schema): parameter type/structure check\n2. **Tool-level validateInput()** (`682-733`): parameter value validation (e.g., is the path within the working directory)\n3. **PreToolUse hooks** (`800-862`, covered in s04): hooks can return messages, modify input, or block execution\n4. **Permission check** (`921-931`, core topic of s03): canUseTool + checkPermissions → allow/deny/ask\n5. **Execute tool.call()** (`1207-1222`)\n\nThe teaching version omits Zod (uses JSON Schema), omits validateInput (uses safety functions), but preserves the permission check and hook concepts.\n\n### 5. Streaming Tool Execution\n\nCC's `StreamingToolExecutor` (`StreamingToolExecutor.ts`) starts tools while the model is still generating — no waiting for the model to finish. `read_file` might complete while the model is still outputting \"Let me analyze\". The teaching version doesn't implement this, consistent with s01's goal — conceptual clarity, not peak performance.\n\n### 6. Tool Result Persistence\n\nEach tool has a `maxResultSizeChars` field. Results exceeding this threshold are persisted to disk, and the model sees a preview + file path. FileRead is special — set to `Infinity`, preventing file read output from being persisted again. Specifically, if FileRead's result exceeds the threshold and gets persisted, the model's next read of that persisted file would trigger another persistence → infinite loop (read file → persist → re-read → re-persist → ...).\n\n
\n\n\n" + "content": "# s02: Tool Use — Add a Tool, Add Just One Line\n\ns01 → `s02` → [s03](/en/s03) → s04 → ... → s20\n> *\"Add a tool, add just one handler\"* — The loop stays the same. Register the new tool in the dispatch map and you're done.\n>\n> **Harness Layer**: Tool Dispatch — Expanding the model's reach.\n\n---\n\n## Only One Tool: Bash\n\nThe s01 Agent has only one tool: bash. To read a file, `cat`; to write, `echo \"...\" > file.py`; to edit, `sed`.\n\nThe model thinks \"read this file\" but has to spell out `cat path/to/file`. An extra layer of translation that wastes tokens and invites errors.\n\n---\n\n## Overview: Tool Dispatch\n\n![Tool Dispatch](/course-assets/s02_tool_use/tool-dispatch.en.svg)\n\nThe s01 loop is fully preserved (LLM call, stop_reason check, message append — not a single word changed). The only change is in that one line of tool execution: `run_bash()` is replaced with `TOOL_HANDLERS[block.name]()` dispatch lookup.\n\nAdding a tool to the Agent requires just two things:\n\n1. **Define the tool**: Add one entry to the `TOOLS` array\n2. **Register the handler**: Add one mapping in the `TOOL_HANDLERS` dict\n\n---\n\n## From 1 Tool to 6 Tools\n\ns01 had only bash:\n\n```python\nTOOLS = [{\"name\": \"bash\", ...}]\n\ndef run_bash(command): ...\n```\n\ns02 expands to 6 tools, each independently defined:\n\n```python\nTOOLS = [\n {\"name\": \"bash\", \"description\": \"Run a shell command.\", ...},\n {\"name\": \"read_file\", \"description\": \"Read file contents.\", ...},\n {\"name\": \"write_file\", \"description\": \"Write content to file.\", ...},\n {\"name\": \"edit_file\", \"description\": \"Replace text in file once.\", ...},\n {\"name\": \"glob\", \"description\": \"Find files by pattern.\", ...},\n {\"name\": \"generate_image\", \"description\": \"Generate an image from a text prompt.\", ...},\n]\n```\n\nEach tool has its own implementation function:\n\n```python\ndef run_read(path, limit=None):\n lines = safe_path(path).read_text().splitlines()\n if limit:\n lines = lines[:limit]\n return \"\\n\".join(lines)\n\ndef run_write(path, content):\n safe_path(path).write_text(content)\n return f\"Wrote {len(content)} bytes to {path}\"\n\ndef run_edit(path, old_text, new_text):\n text = safe_path(path).read_text()\n if old_text not in text:\n return \"Error: text not found\"\n safe_path(path).write_text(text.replace(old_text, new_text, 1))\n return f\"Edited {path}\"\n\ndef run_glob(pattern):\n import glob as g\n return \"\\n\".join(g.glob(pattern, root_dir=WORKDIR))\n\ndef run_generate_image(prompt, aspect_ratio=\"1:1\"):\n ...\n```\n\n---\n\n## Tool Dispatch\n\n```python\nTOOL_HANDLERS = {\n \"bash\": run_bash,\n \"read_file\": run_read,\n \"write_file\": run_write,\n \"edit_file\": run_edit,\n \"glob\": run_glob,\n \"generate_image\": run_generate_image,\n}\n\n# Only one line changed in the loop — from hardcoded run_bash to dispatch lookup:\nfor block in response.content:\n if block.type == \"tool_use\":\n handler = TOOL_HANDLERS[block.name] # lookup\n output = handler(**block.input) # call\n results.append(...)\n```\n\nAdding a tool = one entry in `TOOLS` array + one line in `TOOL_HANDLERS` dict. The loop stays the same.\n\n---\n\n## Multiple Tool Calls\n\nThe model often returns multiple tool_use calls at once — \"read a.py and b.py, then list all .py files\".\n\nThe teaching version executes them one by one in the original `response.content` order. CC's approach is more complex: it slices the original order into consecutive batches, where concurrency-safe tools within a batch run in parallel, and batches are strictly sequential (see appendix).\n\n---\n\n## Quick Reference\n\n| Concept | One-Liner |\n|---------|-----------|\n| TOOL_HANDLERS | Tool name → handler function dict. Add a tool = add one mapping line |\n| Tool Definition | JSON schema telling the model \"what I can do\" |\n| Multiple tool calls | Model may return multiple tool_use at once; teaching version executes them in original order |\n| Loop Unchanged | s01's `while True` loop — not a single line changed |\n\n---\n\n## Changes from s01\n\n| Component | Before (s01) | After (s02) |\n|-----------|-------------|-------------|\n| Tool count | 1 (bash) | 6 (+read, write, edit, glob, generate_image) |\n| Tool execution | Hardcoded `run_bash()` | TOOL_HANDLERS dispatch lookup |\n| Path safety | None | safe_path validation (file tools only) |\n| Loop | `while True` + `stop_reason` | Identical to s01 |\n\n---\n\n## Try It\n\n```sh\ncd learn-claude-code\npython s02_tool_use/code.py\n```\n\nThe optional `generate_image` tool also needs these values in `.env`. Use `https://api.minimaxi.com` as the host for China mainland.\n\n```sh\nMINIMAX_API_KEY=sk-xxx\nMINIMAX_API_HOST=https://api.minimax.io\n```\n\nTry these prompts:\n\n1. `Read the file README.md and tell me what this project is about`\n2. `Create a file called test.py that prints \"hello\", then read it back`\n3. `Find all Python files in this directory`\n4. `Read both README.md and requirements.txt, then create a summary file`\n5. `Generate a 16:9 image of a coding workspace at sunrise`\n\nWhat to watch for: When does the model call just one tool, and when does it call multiple at once? Are multiple tool calls executed in the correct order?\n\n---\n\n## What's Next\n\nThe Agent now has 6 specialized tools. File tools are protected by `safe_path`, but bash is unrestricted — `rm -rf /` still runs.\n\n→ s03 Permission: Add a gate before tool execution — is this operation safe? Does it need user approval?\n\n
\nDive into CC Source Code\n\n> The following is based on a review of CC source code `Tool.ts`, `tools.ts`, `toolOrchestration.ts`, `toolExecution.ts`, and `StreamingToolExecutor.ts`.\n\n### 1. Tool Definition Approach\n\n**Teaching version**: `TOOLS` array + `TOOL_HANDLERS` dict. Definition and implementation are separate.\n**CC**: Each tool is an independent object created by `buildTool()`, containing schema, validation, permissions, and execution. `getAllBaseTools()` aggregates all tools.\n\nThe teaching version's separation is clearer for teaching — readers immediately see \"add a tool = two definitions\".\n\n### 2. Concurrency Safety: isConcurrencySafe()\n\n![Tool Concurrency](/course-assets/s02_tool_use/concurrency-comparison.en.svg)\n\nThe teaching version executes tools one by one in original order, without concurrency. CC uses `isConcurrencySafe(input)` to determine concurrency — note this isn't simply \"read-only vs write\", but judges by specific input:\n\n| | isReadOnly | isConcurrencySafe |\n|---|---|---|\n| FileRead | true | true |\n| Glob | true | true |\n| Bash `ls` | true | **true** ← key difference |\n| Bash `rm` | false | false |\n| TaskCreate | false | **true** ← modifies state but can be concurrent (introduced in s12) |\n\nCC's Bash tool's `isConcurrencySafe` equals `isReadOnly` — read-only commands can be concurrent, write commands cannot. TaskCreate modifies task files, but each writes a different file, so it can be concurrent.\n\n### 3. Partition Algorithm\n\nCC's `partitionToolCalls()` (`toolOrchestration.ts:91-115`) doesn't split into two groups — it batches tool calls **by consecutive blocks**:\n\n```\n[read A, read B, glob *.py, bash \"rm x\", read C]\n → batch1(concurrent): [read A, read B, glob *.py]\n → batch2(serial): [bash \"rm x\"]\n → batch3(concurrent): [read C]\n```\n\nConsecutive concurrency-safe calls are grouped into the same batch for truly concurrent execution (`toolOrchestration.ts:152-176`, with a concurrency limit). When a non-concurrency-safe call is encountered, a new batch starts for serial execution. Batches are strictly sequential.\n\n### 4. Validation Pipeline\n\nEach tool call in CC goes through a strict 5-step validation (`toolExecution.ts`):\n\n1. **Zod schema validation** (`614-680`, teaching version uses JSON Schema): parameter type/structure check\n2. **Tool-level validateInput()** (`682-733`): parameter value validation (e.g., is the path within the working directory)\n3. **PreToolUse hooks** (`800-862`, covered in s04): hooks can return messages, modify input, or block execution\n4. **Permission check** (`921-931`, core topic of s03): canUseTool + checkPermissions → allow/deny/ask\n5. **Execute tool.call()** (`1207-1222`)\n\nThe teaching version omits Zod (uses JSON Schema), omits validateInput (uses safety functions), but preserves the permission check and hook concepts.\n\n### 5. Streaming Tool Execution\n\nCC's `StreamingToolExecutor` (`StreamingToolExecutor.ts`) starts tools while the model is still generating — no waiting for the model to finish. `read_file` might complete while the model is still outputting \"Let me analyze\". The teaching version doesn't implement this, consistent with s01's goal — conceptual clarity, not peak performance.\n\n### 6. Tool Result Persistence\n\nEach tool has a `maxResultSizeChars` field. Results exceeding this threshold are persisted to disk, and the model sees a preview + file path. FileRead is special — set to `Infinity`, preventing file read output from being persisted again. Specifically, if FileRead's result exceeds the threshold and gets persisted, the model's next read of that persisted file would trigger another persistence → infinite loop (read file → persist → re-read → re-persist → ...).\n\n
\n\n\n" }, { "version": "s02", "locale": "zh", "title": "s02: Tool Use — 多加一个工具,只加一行", - "content": "# s02: Tool Use — 多加一个工具,只加一行\n\ns01 → `s02` → [s03](/zh/s03) → s04 → ... → s20\n> *\"加一个工具, 只加一个 handler\"* — 循环不用动, 新工具注册进 dispatch map 就行。\n>\n> **Harness 层**: 工具分发 — 扩展模型能触达的边界。\n\n---\n\n## 只有 bash 一个工具\n\ns01 的 Agent 只有一个 bash 工具。读文件要 `cat`,写文件要 `echo \"...\" > file.py`,改文件要 `sed`。\n\n模型想的是\"读这个文件\",却要拼出 `cat path/to/file`。多了一层翻译,浪费 token,还容易拼错。\n\n---\n\n## 全局视角:工具分发\n\n![Tool Dispatch](/course-assets/s02_tool_use/tool-dispatch.svg)\n\ns01 的循环完全保留(LLM 调用、stop_reason 判断、消息追加)。唯一的变动在工具执行那 1 行:`run_bash()` 替换为 `TOOL_HANDLERS[block.name]()` 查表分发。\n\n给 Agent 加一个工具只需要做两件事:\n\n1. **定义工具**:在 `TOOLS` 数组里加一条描述\n2. **注册处理函数**:在 `TOOL_HANDLERS` 字典里加一个映射\n\n---\n\n## 从 1 个工具到 5 个工具\n\ns01 只有一个 bash:\n\n```python\nTOOLS = [{\"name\": \"bash\", ...}]\n\ndef run_bash(command): ...\n```\n\ns02 加到 5 个,每个工具都是独立定义:\n\n```python\nTOOLS = [\n {\"name\": \"bash\", \"description\": \"Run a shell command.\", ...},\n {\"name\": \"read_file\", \"description\": \"Read file contents.\", ...},\n {\"name\": \"write_file\", \"description\": \"Write content to file.\", ...},\n {\"name\": \"edit_file\", \"description\": \"Replace text in file once.\", ...},\n {\"name\": \"glob\", \"description\": \"Find files by pattern.\", ...},\n]\n```\n\n每个工具有自己的实现函数:\n\n```python\ndef run_read(path, limit=None):\n lines = safe_path(path).read_text().splitlines()\n if limit:\n lines = lines[:limit]\n return \"\\n\".join(lines)\n\ndef run_write(path, content):\n safe_path(path).write_text(content)\n return f\"Wrote {len(content)} bytes to {path}\"\n\ndef run_edit(path, old_text, new_text):\n text = safe_path(path).read_text()\n if old_text not in text:\n return \"Error: text not found\"\n safe_path(path).write_text(text.replace(old_text, new_text, 1))\n return f\"Edited {path}\"\n\ndef run_glob(pattern):\n import glob as g\n return \"\\n\".join(g.glob(pattern, root_dir=WORKDIR))\n```\n\n---\n\n## 工具分发\n\n```python\nTOOL_HANDLERS = {\n \"bash\": run_bash,\n \"read_file\": run_read,\n \"write_file\": run_write,\n \"edit_file\": run_edit,\n \"glob\": run_glob,\n}\n\n# 循环里只改了一行——从硬编码 run_bash 变成查表:\nfor block in response.content:\n if block.type == \"tool_use\":\n handler = TOOL_HANDLERS[block.name] # 查表\n output = handler(**block.input) # 调用\n results.append(...)\n```\n\n加一个工具 = 在 `TOOLS` 数组加一条 + 在 `TOOL_HANDLERS` 字典加一行。循环不变。\n\n---\n\n## 多个工具调用\n\n模型经常一次返回多个 tool_use:\"读一下 a.py 和 b.py,然后列出所有 .py 文件\"。\n\n教学版按 `response.content` 原始顺序逐个执行。CC 的做法更复杂:按原始顺序切成连续 batch,batch 内并发安全的工具并行执行,batch 间严格顺序(见附录)。\n\n---\n\n## 速查\n\n| 概念 | 一句话 |\n|------|--------|\n| TOOL_HANDLERS | 工具名 → 处理函数的字典。加工具 = 加一行映射 |\n| 工具定义 | 告诉模型\"我能做什么\"的 JSON schema |\n| 多工具调用 | 模型可一次返回多个 tool_use,教学版按原始顺序逐个执行 |\n| 循环不变 | s01 的 `while True` 循环一行都没改 |\n\n---\n\n## 相对 s01 的变更\n\n| 组件 | 之前 (s01) | 之后 (s02) |\n|------|-----------|-----------|\n| 工具数量 | 1 (bash) | 5 (+read, write, edit, glob) |\n| 工具执行 | 硬编码 `run_bash()` | TOOL_HANDLERS 查表分发 |\n| 路径安全 | 无 | safe_path 校验(仅 file tools) |\n| 循环 | `while True` + `stop_reason` | 与 s01 完全一致 |\n\n---\n\n## 试一下\n\n```sh\ncd learn-claude-code\npython s02_tool_use/code.py\n```\n\n试试这些 prompt:\n\n1. `Read the file README.md and tell me what this project is about`\n2. `Create a file called test.py that prints \"hello\", then read it back`\n3. `Find all Python files in this directory`\n4. `Read both README.md and requirements.txt, then create a summary file`\n\n观察重点:模型什么时候只调一个工具,什么时候一次调多个?多个工具调用的顺序和结果是否正确?\n\n---\n\n## 接下来\n\n现在 Agent 有 5 个专用工具。file tools 受 `safe_path` 保护,但 bash 不受限制,`rm -rf /` 还是能跑。\n\ns03 Permission → 在工具执行之前加一道门:这个操作安全吗?需要用户批准吗?\n\n
\n深入 CC 源码\n\n> 以下基于 CC 源码 `Tool.ts`、`tools.ts`、`toolOrchestration.ts`、`toolExecution.ts`、`StreamingToolExecutor.ts` 的核查。\n\n### 一、工具定义方式\n\n**教学版**:`TOOLS` 数组 + `TOOL_HANDLERS` 字典。定义和实现分开。\n**CC**:每个工具是 `buildTool()` 创建的独立对象,包含 schema、验证、权限、执行。`getAllBaseTools()` 汇总所有工具。\n\n教学版的分离方式对教学更清晰——读者一眼看到\"加一个工具 = 两条定义\"。\n\n### 二、并发安全判断:isConcurrencySafe()\n\n![Tool Concurrency](/course-assets/s02_tool_use/concurrency-comparison.svg)\n\n教学版按原始顺序逐个执行,不做并发。CC 用 `isConcurrencySafe(input)` 判断能否并发——注意这不是简单的\"只读 vs 写\",而是按具体输入判断:\n\n| | isReadOnly | isConcurrencySafe |\n|---|---|---|\n| FileRead | true | true |\n| Glob | true | true |\n| Bash `ls` | true | **true** ← 关键差异 |\n| Bash `rm` | false | false |\n| TaskCreate | false | **true** ← 改状态但可并发(TaskCreate 在 s12 介绍) |\n\nCC 的 Bash tool 的 `isConcurrencySafe` 等于 `isReadOnly`——只读命令可并发,写命令不可。TaskCreate 虽然改了任务文件,但每次都写不同的文件,所以可以并发。\n\n### 三、分区算法\n\nCC 的 `partitionToolCalls()`(`toolOrchestration.ts:91-115`)不是分两组,而是把工具调用**按连续块分批**:\n\n```\n[read A, read B, glob *.py, bash \"rm x\", read C]\n → batch1(并发): [read A, read B, glob *.py]\n → batch2(串行): [bash \"rm x\"]\n → batch3(并发): [read C]\n```\n\n并发安全的连续块编入同一个 batch,batch 内真正并发执行(`toolOrchestration.ts:152-176`,有并发上限)。遇到非并发安全的就开新 batch 串行执行。batch 之间严格顺序。\n\n### 四、验证管线\n\nCC 的每个工具调用经过严格的 5 步验证(`toolExecution.ts`):\n\n1. **Zod schema 验证**(`614-680`,教学版用 JSON Schema 替代):参数类型/结构检查\n2. **工具级 validateInput()**(`682-733`):参数值验证(如路径是否在工作区内)\n3. **PreToolUse hooks**(`800-862`,s04 详细介绍):钩子可以返回消息、修改输入、阻止执行\n4. **权限检查**(`921-931`,s03 的核心内容):canUseTool + checkPermissions → allow/deny/ask\n5. **执行 tool.call()**(`1207-1222`)\n\n教学版省略了 Zod(用 JSON Schema)、省略了 validateInput(用安全函数)、保留了权限检查和钩子概念。\n\n### 五、流式工具执行\n\nCC 的 `StreamingToolExecutor`(`StreamingToolExecutor.ts`)让工具在模型还在生成时就启动——不等模型说完。`read_file` 可能在模型还在输出\"我来分析\"的时候就跑完了。教学版不实现这个,目标和 s01 一致——概念清晰,不追求性能极致。\n\n### 六、工具结果持久化\n\n每个工具有一个 `maxResultSizeChars` 字段。结果超过这个值就落盘,模型看到的是预览 + 文件路径。FileRead 特殊——设为 `Infinity`,防止读文件的输出又被当成文件落盘。具体来说,如果 FileRead 的结果超过阈值被落盘,模型下次读那个落盘文件时又会触发落盘 → 无限循环(读文件 → 落盘 → 再读 → 再落盘 → ...)。\n\n
\n\n\n" + "content": "# s02: Tool Use — 多加一个工具,只加一行\n\ns01 → `s02` → [s03](/zh/s03) → s04 → ... → s20\n> *\"加一个工具, 只加一个 handler\"* — 循环不用动, 新工具注册进 dispatch map 就行。\n>\n> **Harness 层**: 工具分发 — 扩展模型能触达的边界。\n\n---\n\n## 只有 bash 一个工具\n\ns01 的 Agent 只有一个 bash 工具。读文件要 `cat`,写文件要 `echo \"...\" > file.py`,改文件要 `sed`。\n\n模型想的是\"读这个文件\",却要拼出 `cat path/to/file`。多了一层翻译,浪费 token,还容易拼错。\n\n---\n\n## 全局视角:工具分发\n\n![Tool Dispatch](/course-assets/s02_tool_use/tool-dispatch.svg)\n\ns01 的循环完全保留(LLM 调用、stop_reason 判断、消息追加)。唯一的变动在工具执行那 1 行:`run_bash()` 替换为 `TOOL_HANDLERS[block.name]()` 查表分发。\n\n给 Agent 加一个工具只需要做两件事:\n\n1. **定义工具**:在 `TOOLS` 数组里加一条描述\n2. **注册处理函数**:在 `TOOL_HANDLERS` 字典里加一个映射\n\n---\n\n## 从 1 个工具到 6 个工具\n\ns01 只有一个 bash:\n\n```python\nTOOLS = [{\"name\": \"bash\", ...}]\n\ndef run_bash(command): ...\n```\n\ns02 加到 6 个,每个工具都是独立定义:\n\n```python\nTOOLS = [\n {\"name\": \"bash\", \"description\": \"Run a shell command.\", ...},\n {\"name\": \"read_file\", \"description\": \"Read file contents.\", ...},\n {\"name\": \"write_file\", \"description\": \"Write content to file.\", ...},\n {\"name\": \"edit_file\", \"description\": \"Replace text in file once.\", ...},\n {\"name\": \"glob\", \"description\": \"Find files by pattern.\", ...},\n {\"name\": \"generate_image\", \"description\": \"Generate an image from a text prompt.\", ...},\n]\n```\n\n每个工具有自己的实现函数:\n\n```python\ndef run_read(path, limit=None):\n lines = safe_path(path).read_text().splitlines()\n if limit:\n lines = lines[:limit]\n return \"\\n\".join(lines)\n\ndef run_write(path, content):\n safe_path(path).write_text(content)\n return f\"Wrote {len(content)} bytes to {path}\"\n\ndef run_edit(path, old_text, new_text):\n text = safe_path(path).read_text()\n if old_text not in text:\n return \"Error: text not found\"\n safe_path(path).write_text(text.replace(old_text, new_text, 1))\n return f\"Edited {path}\"\n\ndef run_glob(pattern):\n import glob as g\n return \"\\n\".join(g.glob(pattern, root_dir=WORKDIR))\n\ndef run_generate_image(prompt, aspect_ratio=\"1:1\"):\n ...\n```\n\n---\n\n## 工具分发\n\n```python\nTOOL_HANDLERS = {\n \"bash\": run_bash,\n \"read_file\": run_read,\n \"write_file\": run_write,\n \"edit_file\": run_edit,\n \"glob\": run_glob,\n \"generate_image\": run_generate_image,\n}\n\n# 循环里只改了一行——从硬编码 run_bash 变成查表:\nfor block in response.content:\n if block.type == \"tool_use\":\n handler = TOOL_HANDLERS[block.name] # 查表\n output = handler(**block.input) # 调用\n results.append(...)\n```\n\n加一个工具 = 在 `TOOLS` 数组加一条 + 在 `TOOL_HANDLERS` 字典加一行。循环不变。\n\n---\n\n## 多个工具调用\n\n模型经常一次返回多个 tool_use:\"读一下 a.py 和 b.py,然后列出所有 .py 文件\"。\n\n教学版按 `response.content` 原始顺序逐个执行。CC 的做法更复杂:按原始顺序切成连续 batch,batch 内并发安全的工具并行执行,batch 间严格顺序(见附录)。\n\n---\n\n## 速查\n\n| 概念 | 一句话 |\n|------|--------|\n| TOOL_HANDLERS | 工具名 → 处理函数的字典。加工具 = 加一行映射 |\n| 工具定义 | 告诉模型\"我能做什么\"的 JSON schema |\n| 多工具调用 | 模型可一次返回多个 tool_use,教学版按原始顺序逐个执行 |\n| 循环不变 | s01 的 `while True` 循环一行都没改 |\n\n---\n\n## 相对 s01 的变更\n\n| 组件 | 之前 (s01) | 之后 (s02) |\n|------|-----------|-----------|\n| 工具数量 | 1 (bash) | 6 (+read, write, edit, glob, generate_image) |\n| 工具执行 | 硬编码 `run_bash()` | TOOL_HANDLERS 查表分发 |\n| 路径安全 | 无 | safe_path 校验(仅 file tools) |\n| 循环 | `while True` + `stop_reason` | 与 s01 完全一致 |\n\n---\n\n## 试一下\n\n```sh\ncd learn-claude-code\npython s02_tool_use/code.py\n```\n\n可选的 `generate_image` 工具还需要在 `.env` 中配置以下值。中国大陆区域请将 host 改为 `https://api.minimaxi.com`。\n\n```sh\nMINIMAX_API_KEY=sk-xxx\nMINIMAX_API_HOST=https://api.minimax.io\n```\n\n试试这些 prompt:\n\n1. `Read the file README.md and tell me what this project is about`\n2. `Create a file called test.py that prints \"hello\", then read it back`\n3. `Find all Python files in this directory`\n4. `Read both README.md and requirements.txt, then create a summary file`\n5. `Generate a 16:9 image of a coding workspace at sunrise`\n\n观察重点:模型什么时候只调一个工具,什么时候一次调多个?多个工具调用的顺序和结果是否正确?\n\n---\n\n## 接下来\n\n现在 Agent 有 6 个专用工具。file tools 受 `safe_path` 保护,但 bash 不受限制,`rm -rf /` 还是能跑。\n\ns03 Permission → 在工具执行之前加一道门:这个操作安全吗?需要用户批准吗?\n\n
\n深入 CC 源码\n\n> 以下基于 CC 源码 `Tool.ts`、`tools.ts`、`toolOrchestration.ts`、`toolExecution.ts`、`StreamingToolExecutor.ts` 的核查。\n\n### 一、工具定义方式\n\n**教学版**:`TOOLS` 数组 + `TOOL_HANDLERS` 字典。定义和实现分开。\n**CC**:每个工具是 `buildTool()` 创建的独立对象,包含 schema、验证、权限、执行。`getAllBaseTools()` 汇总所有工具。\n\n教学版的分离方式对教学更清晰——读者一眼看到\"加一个工具 = 两条定义\"。\n\n### 二、并发安全判断:isConcurrencySafe()\n\n![Tool Concurrency](/course-assets/s02_tool_use/concurrency-comparison.svg)\n\n教学版按原始顺序逐个执行,不做并发。CC 用 `isConcurrencySafe(input)` 判断能否并发——注意这不是简单的\"只读 vs 写\",而是按具体输入判断:\n\n| | isReadOnly | isConcurrencySafe |\n|---|---|---|\n| FileRead | true | true |\n| Glob | true | true |\n| Bash `ls` | true | **true** ← 关键差异 |\n| Bash `rm` | false | false |\n| TaskCreate | false | **true** ← 改状态但可并发(TaskCreate 在 s12 介绍) |\n\nCC 的 Bash tool 的 `isConcurrencySafe` 等于 `isReadOnly`——只读命令可并发,写命令不可。TaskCreate 虽然改了任务文件,但每次都写不同的文件,所以可以并发。\n\n### 三、分区算法\n\nCC 的 `partitionToolCalls()`(`toolOrchestration.ts:91-115`)不是分两组,而是把工具调用**按连续块分批**:\n\n```\n[read A, read B, glob *.py, bash \"rm x\", read C]\n → batch1(并发): [read A, read B, glob *.py]\n → batch2(串行): [bash \"rm x\"]\n → batch3(并发): [read C]\n```\n\n并发安全的连续块编入同一个 batch,batch 内真正并发执行(`toolOrchestration.ts:152-176`,有并发上限)。遇到非并发安全的就开新 batch 串行执行。batch 之间严格顺序。\n\n### 四、验证管线\n\nCC 的每个工具调用经过严格的 5 步验证(`toolExecution.ts`):\n\n1. **Zod schema 验证**(`614-680`,教学版用 JSON Schema 替代):参数类型/结构检查\n2. **工具级 validateInput()**(`682-733`):参数值验证(如路径是否在工作区内)\n3. **PreToolUse hooks**(`800-862`,s04 详细介绍):钩子可以返回消息、修改输入、阻止执行\n4. **权限检查**(`921-931`,s03 的核心内容):canUseTool + checkPermissions → allow/deny/ask\n5. **执行 tool.call()**(`1207-1222`)\n\n教学版省略了 Zod(用 JSON Schema)、省略了 validateInput(用安全函数)、保留了权限检查和钩子概念。\n\n### 五、流式工具执行\n\nCC 的 `StreamingToolExecutor`(`StreamingToolExecutor.ts`)让工具在模型还在生成时就启动——不等模型说完。`read_file` 可能在模型还在输出\"我来分析\"的时候就跑完了。教学版不实现这个,目标和 s01 一致——概念清晰,不追求性能极致。\n\n### 六、工具结果持久化\n\n每个工具有一个 `maxResultSizeChars` 字段。结果超过这个值就落盘,模型看到的是预览 + 文件路径。FileRead 特殊——设为 `Infinity`,防止读文件的输出又被当成文件落盘。具体来说,如果 FileRead 的结果超过阈值被落盘,模型下次读那个落盘文件时又会触发落盘 → 无限循环(读文件 → 落盘 → 再读 → 再落盘 → ...)。\n\n
\n\n\n" }, { "version": "s02", "locale": "ja", "title": "s02: Tool Use — ツール一つ追加、一行追加だけ", - "content": "# s02: Tool Use — ツール一つ追加、一行追加だけ\n\ns01 → `s02` → [s03](/ja/s03) → s04 → ... → s20\n> *\"ツールを一つ追加、ハンドラを一つ追加\"* — ループはそのまま。新しいツールをディスパッチマップに登録するだけ。\n>\n> **Harness レイヤー**: ツールディスパッチ — モデルが触れる範囲を拡張。\n\n---\n\n## ツールは bash 一つだけ\n\ns01 の Agent には bash 一つのツールしかない。ファイルを読むには `cat`、書くには `echo \"...\" > file.py`、編集するには `sed`。\n\nモデルは「このファイルを読みたい」と考えながら、`cat path/to/file` と組み立てなければならない。翻訳の層が一つ増え、トークンを無駄にし、エラーも起きやすい。\n\n---\n\n## 概要:ツールディスパッチ\n\n![Tool Dispatch](/course-assets/s02_tool_use/tool-dispatch.ja.svg)\n\ns01 のループは完全に保持される(LLM 呼び出し、stop_reason 判定、メッセージ追加 — 一文字も変更なし)。唯一の変更点はツール実行の 1 行:`run_bash()` が `TOOL_HANDLERS[block.name]()` の検索ディスパッチに置き換わる。\n\nAgent にツールを追加するには、たった二つ:\n\n1. **ツールを定義**:`TOOLS` 配列に一条を追加\n2. **ハンドラを登録**:`TOOL_HANDLERS` 辞書に一つのマッピングを追加\n\n---\n\n## 1 つのツールから 5 つのツールへ\n\ns01 には bash だけだった:\n\n```python\nTOOLS = [{\"name\": \"bash\", ...}]\n\ndef run_bash(command): ...\n```\n\ns02 では 5 つに増え、各ツールは独立して定義される:\n\n```python\nTOOLS = [\n {\"name\": \"bash\", \"description\": \"Run a shell command.\", ...},\n {\"name\": \"read_file\", \"description\": \"Read file contents.\", ...},\n {\"name\": \"write_file\", \"description\": \"Write content to file.\", ...},\n {\"name\": \"edit_file\", \"description\": \"Replace text in file once.\", ...},\n {\"name\": \"glob\", \"description\": \"Find files by pattern.\", ...},\n]\n```\n\n各ツールには専用の実装関数がある:\n\n```python\ndef run_read(path, limit=None):\n lines = safe_path(path).read_text().splitlines()\n if limit:\n lines = lines[:limit]\n return \"\\n\".join(lines)\n\ndef run_write(path, content):\n safe_path(path).write_text(content)\n return f\"Wrote {len(content)} bytes to {path}\"\n\ndef run_edit(path, old_text, new_text):\n text = safe_path(path).read_text()\n if old_text not in text:\n return \"Error: text not found\"\n safe_path(path).write_text(text.replace(old_text, new_text, 1))\n return f\"Edited {path}\"\n\ndef run_glob(pattern):\n import glob as g\n return \"\\n\".join(g.glob(pattern, root_dir=WORKDIR))\n```\n\n---\n\n## ツールディスパッチ\n\n```python\nTOOL_HANDLERS = {\n \"bash\": run_bash,\n \"read_file\": run_read,\n \"write_file\": run_write,\n \"edit_file\": run_edit,\n \"glob\": run_glob,\n}\n\n# ループ内で変更されたのは一行だけ — ハードコードの run_bash から検索ディスパッチへ:\nfor block in response.content:\n if block.type == \"tool_use\":\n handler = TOOL_HANDLERS[block.name] # 検索\n output = handler(**block.input) # 呼び出し\n results.append(...)\n```\n\nツールの追加 = `TOOLS` 配列に一条 + `TOOL_HANDLERS` 辞書に一行。ループは変わらない。\n\n---\n\n## 複数のツール呼び出し\n\nモデルはよく一度に複数の tool_use を返す — 「a.py と b.py を読んで、全 .py ファイルを列挙して」。\n\n教育版は `response.content` の元の順序で一つずつ実行する。CC のやり方はより複雑:元の順序を保ったまま連続バッチに分割し、バッチ内の並列安全なツールを並行実行し、バッチ間は厳密に順次(付録を参照)。\n\n---\n\n## 速查\n\n| 概念 | 一言で |\n|------|--------|\n| TOOL_HANDLERS | ツール名 → ハンドラ関数の辞書。ツール追加 = マッピング一行追加 |\n| ツール定義 | モデルに「何ができるか」を伝える JSON schema |\n| 複数ツール呼び出し | モデルは一度に複数の tool_use を返す可能性がある。教育版は元の順序で一つずつ実行 |\n| ループ不変 | s01 の `while True` ループ — 一行も変更なし |\n\n---\n\n## s01 からの変更\n\n| コンポーネント | 変更前 (s01) | 変更後 (s02) |\n|--------------|-------------|-------------|\n| ツール数 | 1 (bash) | 5 (+read, write, edit, glob) |\n| ツール実行 | ハードコード `run_bash()` | TOOL_HANDLERS 検索ディスパッチ |\n| パス安全性 | なし | safe_path 検証(file tools のみ) |\n| ループ | `while True` + `stop_reason` | s01 と完全に同一 |\n\n---\n\n## 試してみよう\n\n```sh\ncd learn-claude-code\npython s02_tool_use/code.py\n```\n\n以下のプロンプトを試してみよう:\n\n1. `Read the file README.md and tell me what this project is about`\n2. `Create a file called test.py that prints \"hello\", then read it back`\n3. `Find all Python files in this directory`\n4. `Read both README.md and requirements.txt, then create a summary file`\n\n観察のポイント:モデルがツールを一つだけ呼び出すときと、複数同時に呼び出すときの違い。複数のツール呼び出しは正しい順序で実行されているか?\n\n---\n\n## 次へ\n\nAgent は 5 つの専用ツールを持つようになった。file tools は `safe_path` で保護されるが、bash は制限なし — `rm -rf /` はまだ実行できる。\n\n→ s03 Permission:ツール実行前にゲートを追加 — この操作は安全か? ユーザーの承認が必要か?\n\n
\nCC ソースコードを深掘り\n\n> 以下は CC ソースコード `Tool.ts`、`tools.ts`、`toolOrchestration.ts`、`toolExecution.ts`、`StreamingToolExecutor.ts` の検証に基づく。\n\n### 一、ツール定義方式\n\n**教育版**:`TOOLS` 配列 + `TOOL_HANDLERS` 辞書。定義と実装が分離。\n**CC**:各ツールは `buildTool()` で作成された独立オブジェクトで、schema、バリデーション、権限、実行を含む。`getAllBaseTools()` が全ツールを集約。\n\n教育版の分離方式は教学に適している — 読者は「ツール追加 = 二つの定義」と一目で分かる。\n\n### 二、並列安全性:isConcurrencySafe()\n\n![Tool Concurrency](/course-assets/s02_tool_use/concurrency-comparison.ja.svg)\n\n教育版は元の順序で一つずつ実行し、並列処理は行わない。CC は `isConcurrencySafe(input)` で並列可否を判断する — これは単なる「読み取り専用 vs 書き込み」ではなく、具体的な入力で判断する:\n\n| | isReadOnly | isConcurrencySafe |\n|---|---|---|\n| FileRead | true | true |\n| Glob | true | true |\n| Bash `ls` | true | **true** ← 重要な違い |\n| Bash `rm` | false | false |\n| TaskCreate | false | **true** ← 状態変更するが並列可能(s12 で紹介) |\n\nCC の Bash ツールの `isConcurrencySafe` は `isReadOnly` と同じ — 読み取り専用コマンドは並列可能、書き込みコマンドは不可。TaskCreate はタスクファイルを変更するが、毎回異なるファイルに書き込むため並列可能。\n\n### 三、パーティションアルゴリズム\n\nCC の `partitionToolCalls()`(`toolOrchestration.ts:91-115`)は二つのグループに分けるのではなく、ツール呼び出しを**連続ブロックごとにバッチ化**する:\n\n```\n[read A, read B, glob *.py, bash \"rm x\", read C]\n → batch1(並列): [read A, read B, glob *.py]\n → batch2(直列): [bash \"rm x\"]\n → batch3(並列): [read C]\n```\n\n連続する並列安全な呼び出しを同じバッチにまとめ、真の並列実行を行う(`toolOrchestration.ts:152-176`、並列数上限あり)。非並列安全な呼び出しに遭遇すると新しいバッチを開始して直列実行。バッチ間は厳密に順次。\n\n### 四、バリデーションパイプライン\n\nCC の各ツール呼び出しは厳格な 5 段階のバリデーションを経る(`toolExecution.ts`):\n\n1. **Zod schema バリデーション**(`614-680`、教育版は JSON Schema で代替):パラメータの型/構造チェック\n2. **ツールレベル validateInput()**(`682-733`):パラメータ値の検証(例:パスが作業ディレクトリ内か)\n3. **PreToolUse フック**(`800-862`、s04 で詳解):フックはメッセージの返却、入力の変更、実行のブロックが可能\n4. **権限チェック**(`921-931`、s03 の核心):canUseTool + checkPermissions → allow/deny/ask\n5. **tool.call() の実行**(`1207-1222`)\n\n教育版は Zod を省略(JSON Schema を使用)、validateInput を省略(安全関数を使用)、権限チェックとフック概念は保持。\n\n### 五、ストリーミングツール実行\n\nCC の `StreamingToolExecutor`(`StreamingToolExecutor.ts`)はモデルがまだ生成中にツールを起動する — モデルの完了を待たない。`read_file` はモデルが「分析します」と出力中に完了するかもしれない。教育版はこれを実装しない。s01 と同じ目標 — 概念の明確さ、極限のパフォーマンスではない。\n\n### 六、ツール結果の永続化\n\n各ツールには `maxResultSizeChars` フィールドがある。この閾値を超える結果はディスクに保存され、モデルにはプレビュー + ファイルパスが表示される。FileRead は特殊 — `Infinity` に設定され、ファイル読み出し結果の再永続化を防ぐ。具体的には、FileRead の結果が閾値を超えて永続化されると、モデルがその永続化ファイルを次に読むときにまた永続化がトリガーされ → 無限ループ(ファイル読む → 永続化 → 再読み → 再永続化 → ...)になる。\n\n
\n\n\n" + "content": "# s02: Tool Use — ツール一つ追加、一行追加だけ\n\ns01 → `s02` → [s03](/ja/s03) → s04 → ... → s20\n> *\"ツールを一つ追加、ハンドラを一つ追加\"* — ループはそのまま。新しいツールをディスパッチマップに登録するだけ。\n>\n> **Harness レイヤー**: ツールディスパッチ — モデルが触れる範囲を拡張。\n\n---\n\n## ツールは bash 一つだけ\n\ns01 の Agent には bash 一つのツールしかない。ファイルを読むには `cat`、書くには `echo \"...\" > file.py`、編集するには `sed`。\n\nモデルは「このファイルを読みたい」と考えながら、`cat path/to/file` と組み立てなければならない。翻訳の層が一つ増え、トークンを無駄にし、エラーも起きやすい。\n\n---\n\n## 概要:ツールディスパッチ\n\n![Tool Dispatch](/course-assets/s02_tool_use/tool-dispatch.ja.svg)\n\ns01 のループは完全に保持される(LLM 呼び出し、stop_reason 判定、メッセージ追加 — 一文字も変更なし)。唯一の変更点はツール実行の 1 行:`run_bash()` が `TOOL_HANDLERS[block.name]()` の検索ディスパッチに置き換わる。\n\nAgent にツールを追加するには、たった二つ:\n\n1. **ツールを定義**:`TOOLS` 配列に一条を追加\n2. **ハンドラを登録**:`TOOL_HANDLERS` 辞書に一つのマッピングを追加\n\n---\n\n## 1 つのツールから 6 つのツールへ\n\ns01 には bash だけだった:\n\n```python\nTOOLS = [{\"name\": \"bash\", ...}]\n\ndef run_bash(command): ...\n```\n\ns02 では 6 つに増え、各ツールは独立して定義される:\n\n```python\nTOOLS = [\n {\"name\": \"bash\", \"description\": \"Run a shell command.\", ...},\n {\"name\": \"read_file\", \"description\": \"Read file contents.\", ...},\n {\"name\": \"write_file\", \"description\": \"Write content to file.\", ...},\n {\"name\": \"edit_file\", \"description\": \"Replace text in file once.\", ...},\n {\"name\": \"glob\", \"description\": \"Find files by pattern.\", ...},\n {\"name\": \"generate_image\", \"description\": \"Generate an image from a text prompt.\", ...},\n]\n```\n\n各ツールには専用の実装関数がある:\n\n```python\ndef run_read(path, limit=None):\n lines = safe_path(path).read_text().splitlines()\n if limit:\n lines = lines[:limit]\n return \"\\n\".join(lines)\n\ndef run_write(path, content):\n safe_path(path).write_text(content)\n return f\"Wrote {len(content)} bytes to {path}\"\n\ndef run_edit(path, old_text, new_text):\n text = safe_path(path).read_text()\n if old_text not in text:\n return \"Error: text not found\"\n safe_path(path).write_text(text.replace(old_text, new_text, 1))\n return f\"Edited {path}\"\n\ndef run_glob(pattern):\n import glob as g\n return \"\\n\".join(g.glob(pattern, root_dir=WORKDIR))\n\ndef run_generate_image(prompt, aspect_ratio=\"1:1\"):\n ...\n```\n\n---\n\n## ツールディスパッチ\n\n```python\nTOOL_HANDLERS = {\n \"bash\": run_bash,\n \"read_file\": run_read,\n \"write_file\": run_write,\n \"edit_file\": run_edit,\n \"glob\": run_glob,\n \"generate_image\": run_generate_image,\n}\n\n# ループ内で変更されたのは一行だけ — ハードコードの run_bash から検索ディスパッチへ:\nfor block in response.content:\n if block.type == \"tool_use\":\n handler = TOOL_HANDLERS[block.name] # 検索\n output = handler(**block.input) # 呼び出し\n results.append(...)\n```\n\nツールの追加 = `TOOLS` 配列に一条 + `TOOL_HANDLERS` 辞書に一行。ループは変わらない。\n\n---\n\n## 複数のツール呼び出し\n\nモデルはよく一度に複数の tool_use を返す — 「a.py と b.py を読んで、全 .py ファイルを列挙して」。\n\n教育版は `response.content` の元の順序で一つずつ実行する。CC のやり方はより複雑:元の順序を保ったまま連続バッチに分割し、バッチ内の並列安全なツールを並行実行し、バッチ間は厳密に順次(付録を参照)。\n\n---\n\n## 速查\n\n| 概念 | 一言で |\n|------|--------|\n| TOOL_HANDLERS | ツール名 → ハンドラ関数の辞書。ツール追加 = マッピング一行追加 |\n| ツール定義 | モデルに「何ができるか」を伝える JSON schema |\n| 複数ツール呼び出し | モデルは一度に複数の tool_use を返す可能性がある。教育版は元の順序で一つずつ実行 |\n| ループ不変 | s01 の `while True` ループ — 一行も変更なし |\n\n---\n\n## s01 からの変更\n\n| コンポーネント | 変更前 (s01) | 変更後 (s02) |\n|--------------|-------------|-------------|\n| ツール数 | 1 (bash) | 6 (+read, write, edit, glob, generate_image) |\n| ツール実行 | ハードコード `run_bash()` | TOOL_HANDLERS 検索ディスパッチ |\n| パス安全性 | なし | safe_path 検証(file tools のみ) |\n| ループ | `while True` + `stop_reason` | s01 と完全に同一 |\n\n---\n\n## 試してみよう\n\n```sh\ncd learn-claude-code\npython s02_tool_use/code.py\n```\n\nオプションの `generate_image` ツールには、`.env` に以下の値も必要。中国本土では host を `https://api.minimaxi.com` に変更する。\n\n```sh\nMINIMAX_API_KEY=sk-xxx\nMINIMAX_API_HOST=https://api.minimax.io\n```\n\n以下のプロンプトを試してみよう:\n\n1. `Read the file README.md and tell me what this project is about`\n2. `Create a file called test.py that prints \"hello\", then read it back`\n3. `Find all Python files in this directory`\n4. `Read both README.md and requirements.txt, then create a summary file`\n5. `Generate a 16:9 image of a coding workspace at sunrise`\n\n観察のポイント:モデルがツールを一つだけ呼び出すときと、複数同時に呼び出すときの違い。複数のツール呼び出しは正しい順序で実行されているか?\n\n---\n\n## 次へ\n\nAgent は 6 つの専用ツールを持つようになった。file tools は `safe_path` で保護されるが、bash は制限なし — `rm -rf /` はまだ実行できる。\n\n→ s03 Permission:ツール実行前にゲートを追加 — この操作は安全か? ユーザーの承認が必要か?\n\n
\nCC ソースコードを深掘り\n\n> 以下は CC ソースコード `Tool.ts`、`tools.ts`、`toolOrchestration.ts`、`toolExecution.ts`、`StreamingToolExecutor.ts` の検証に基づく。\n\n### 一、ツール定義方式\n\n**教育版**:`TOOLS` 配列 + `TOOL_HANDLERS` 辞書。定義と実装が分離。\n**CC**:各ツールは `buildTool()` で作成された独立オブジェクトで、schema、バリデーション、権限、実行を含む。`getAllBaseTools()` が全ツールを集約。\n\n教育版の分離方式は教学に適している — 読者は「ツール追加 = 二つの定義」と一目で分かる。\n\n### 二、並列安全性:isConcurrencySafe()\n\n![Tool Concurrency](/course-assets/s02_tool_use/concurrency-comparison.ja.svg)\n\n教育版は元の順序で一つずつ実行し、並列処理は行わない。CC は `isConcurrencySafe(input)` で並列可否を判断する — これは単なる「読み取り専用 vs 書き込み」ではなく、具体的な入力で判断する:\n\n| | isReadOnly | isConcurrencySafe |\n|---|---|---|\n| FileRead | true | true |\n| Glob | true | true |\n| Bash `ls` | true | **true** ← 重要な違い |\n| Bash `rm` | false | false |\n| TaskCreate | false | **true** ← 状態変更するが並列可能(s12 で紹介) |\n\nCC の Bash ツールの `isConcurrencySafe` は `isReadOnly` と同じ — 読み取り専用コマンドは並列可能、書き込みコマンドは不可。TaskCreate はタスクファイルを変更するが、毎回異なるファイルに書き込むため並列可能。\n\n### 三、パーティションアルゴリズム\n\nCC の `partitionToolCalls()`(`toolOrchestration.ts:91-115`)は二つのグループに分けるのではなく、ツール呼び出しを**連続ブロックごとにバッチ化**する:\n\n```\n[read A, read B, glob *.py, bash \"rm x\", read C]\n → batch1(並列): [read A, read B, glob *.py]\n → batch2(直列): [bash \"rm x\"]\n → batch3(並列): [read C]\n```\n\n連続する並列安全な呼び出しを同じバッチにまとめ、真の並列実行を行う(`toolOrchestration.ts:152-176`、並列数上限あり)。非並列安全な呼び出しに遭遇すると新しいバッチを開始して直列実行。バッチ間は厳密に順次。\n\n### 四、バリデーションパイプライン\n\nCC の各ツール呼び出しは厳格な 5 段階のバリデーションを経る(`toolExecution.ts`):\n\n1. **Zod schema バリデーション**(`614-680`、教育版は JSON Schema で代替):パラメータの型/構造チェック\n2. **ツールレベル validateInput()**(`682-733`):パラメータ値の検証(例:パスが作業ディレクトリ内か)\n3. **PreToolUse フック**(`800-862`、s04 で詳解):フックはメッセージの返却、入力の変更、実行のブロックが可能\n4. **権限チェック**(`921-931`、s03 の核心):canUseTool + checkPermissions → allow/deny/ask\n5. **tool.call() の実行**(`1207-1222`)\n\n教育版は Zod を省略(JSON Schema を使用)、validateInput を省略(安全関数を使用)、権限チェックとフック概念は保持。\n\n### 五、ストリーミングツール実行\n\nCC の `StreamingToolExecutor`(`StreamingToolExecutor.ts`)はモデルがまだ生成中にツールを起動する — モデルの完了を待たない。`read_file` はモデルが「分析します」と出力中に完了するかもしれない。教育版はこれを実装しない。s01 と同じ目標 — 概念の明確さ、極限のパフォーマンスではない。\n\n### 六、ツール結果の永続化\n\n各ツールには `maxResultSizeChars` フィールドがある。この閾値を超える結果はディスクに保存され、モデルにはプレビュー + ファイルパスが表示される。FileRead は特殊 — `Infinity` に設定され、ファイル読み出し結果の再永続化を防ぐ。具体的には、FileRead の結果が閾値を超えて永続化されると、モデルがその永続化ファイルを次に読むときにまた永続化がトリガーされ → 無限ループ(ファイル読む → 永続化 → 再読み → 再永続化 → ...)になる。\n\n
\n\n\n" }, { "version": "s03", @@ -359,4 +359,4 @@ "title": "s20: Comprehensive Agent — すべての仕組みを 1 つのループへ", "content": "# s20: Comprehensive Agent — すべての仕組みを 1 つのループへ\n\ns01 → ... → s18 → s19 → `s20`\n\n> *\"仕組みは多い、ループは 1 つ\"* — tools、permissions、memory、tasks、teams、plugins はすべて同じ `while True` に接続される。\n>\n> **Harness レイヤー**: 総合 — 前 19 章の仕組みを 1 つの実行可能なシステムへ戻す。\n\n---\n\n## 問題\n\n前 19 章では、各章が 1 つの仕組みだけを追加した。学習にはその形が適している。しかし実際の agent は、1 つの仕組みだけで動くわけではない。\n\n長時間動く coding agent には、同時に次のものが必要になる:\n\n- tool dispatch と permission boundary\n- hook extension point\n- todo plan と task graph\n- skill、memory、runtime system prompt assembly\n- compaction と error recovery\n- background task と cron scheduling\n- team、protocol、autonomous claiming\n- worktree isolation\n- MCP external tool integration\n\n難しいのは機能を積み上げることではない。それぞれの仕組みが loop のどこに接続されるかを見抜くことだ。S20 は終点章であり、すべての component を 1 つの harness に戻す。\n\n---\n\n## 解決策\n\n![System Architecture](/course-assets/s20_comprehensive/system-architecture.ja.svg)\n\nS20 は新しい単独 mechanism を発明しない。前章までの teaching component を 1 つの完全な harness に統合する:\n\n```text\nuser input\n → UserPromptSubmit hooks\n → cron/background notification injection\n → context compact\n → memory + skills + MCP state で system prompt を組み立てる\n → LLM\n → has tool_use block?\n no → Stop hooks → return\n yes → PreToolUse hooks + permission\n → TOOL_HANDLERS / MCP handlers / background dispatch\n → PostToolUse hooks\n → tool_result / task_notification を messages へ戻す\n → next round\n```\n\nloop 自体は同じ構造のままだ。model を呼び、response に `tool_use` block があるかを見て、tool を実行し、結果を `messages` に戻す。CC source でも `stop_reason == \"tool_use\"` を直接信頼せず、実際に tool_use block が出たかを continuation signal として扱う。変わったのは、loop の周囲の harness が完成形になったことだけ。\n\n---\n\n## 各 Component の位置\n\n| 位置 | Component | 役割 |\n|------|-----------|------|\n| user input 周辺 | `UserPromptSubmit` hooks | user input の記録、注入、監査 |\n| LLM 前 | cron queue | scheduled prompt を `messages` へ注入 |\n| LLM 前 | background notifications | 完了した background work を `` として注入 |\n| LLM 前 | compaction pipeline | 大きな出力を予算化し、履歴を切り、古い tool_result を圧縮し、必要なら要約 |\n| LLM 前 | memory / skills / MCP state | current capabilities と long-term context を system prompt に組み込む |\n| LLM call | error recovery | 429/529 retry、`max_tokens` escalation、prompt-too-long compact |\n| tool 実行前 | `PreToolUse` hooks + permission | 危険な command、範囲外 write、destructive MCP tool を止める |\n| tool dispatch | `assemble_tool_pool` | built-in tools と dynamic MCP tools を組み立てる |\n| tool 実行中 | background dispatch | 遅い bash work を daemon thread に逃がし、placeholder result を返す |\n| tool 実行後 | `PostToolUse` hooks | large-output warning、log、後処理 |\n| loop へ戻る | tool_result | 1 つの `tool_use` に 1 つの `tool_result`、そして次の model round |\n| tool_use がない round / stop 時 | `Stop` hooks | 統計、cleanup、audit |\n\n---\n\n## code.py に含まれるもの\n\n### Tools と Dispatch\n\nbuilt-in tool pool には 27 個の tool がある:\n\n```text\nbash, read_file, write_file, edit_file, glob\ntodo_write, task, load_skill, compact\ncreate_task, list_tasks, get_task, claim_task, complete_task\nschedule_cron, list_crons, cancel_cron\nspawn_teammate, send_message, check_inbox\nrequest_shutdown, request_plan, review_plan\ncreate_worktree, remove_worktree, keep_worktree\nconnect_mcp\n```\n\n`assemble_tool_pool()` は毎 round で次を組み立てる:\n\n```text\nBUILTIN_TOOLS + connected MCP tools\nBUILTIN_HANDLERS + mcp__server__tool handlers\n```\n\n`connect_mcp(\"docs\")` のあと、次の round では `mcp__docs__search` のような tool が出現する。\n\n### Permission と Hooks\n\npermission は tool 実行行に直接埋め込まない。`PreToolUse` hook として扱う:\n\n```python\nblocked = trigger_hooks(\"PreToolUse\", block)\nif blocked:\n results.append(tool_result(block.id, blocked))\n continue\n```\n\nこれにより permission、logging、audit が同じ hook point に接続できる。実行後には `PostToolUse` hook が走る。\n\n### Plan と Task\n\nS20 には 2 層の plan がある:\n\n- `todo_write`: current session 用の軽量 plan。メモリに保持。\n- task graph: cross-session、dependency-aware、claimable な task file。`.tasks/task_*.json` に保存。\n\n前者は単独 agent の drift を防ぐ。後者は team coordination の土台になる。\n\n### Subagent と Team\n\nS20 には 2 種類の delegation がある:\n\n- `task`: one-shot subagent。独立した `messages[]` を使い、中間 context を捨て、final summary だけ返す。\n- `spawn_teammate`: persistent teammate thread。`MessageBus` で通信し、idle 中に task board を polling して自律的に claim できる。\n\none-shot subagent は context isolation を解決する。persistent teammate は長期並列協作を解決する。\n\n### Memory、Skills、Prompt\n\n`assemble_system_prompt(context)` は毎 round 次を組み立てる:\n\n- identity と tool guidance\n- workspace\n- skills catalog\n- `.memory/MEMORY.md`\n- connected MCP servers\n\nskills は system prompt には catalog だけ置く。全文は `load_skill(name)` で必要な時に読む。\n\n### Compaction と Recovery\n\nLLM call の前に compaction pipeline を走らせる:\n\n```text\ntool_result_budget → snip_compact → micro_compact → compact_history\n```\n\nmodel call は recovery で包む:\n\n- 429: exponential backoff retry\n- 529: exponential backoff、連続失敗時は fallback model へ切替可能\n- `max_tokens`: max tokens を上げ、その後 continuation を要求\n- prompt too long: reactive compact 後に retry\n\n### Background と Cron\n\n遅い bash work は main loop を止めない:\n\n```text\nshould_run_background → start_background_task → placeholder tool_result\nbackground done → task_notification → next round injects messages\n```\n\ncron scheduler は daemon thread として動き、1 秒ごとに確認する。CLI は `cron_queue` を監視し、発火した job を `[Scheduled] ...` として注入して Agent を 1 turn 自動実行する。\n\n### Worktree と MCP\n\nworktree isolation は directory を担当する:\n\n- `create_worktree(name, task_id)` が isolated branch と directory を作る\n- task の `worktree` field が task と directory を紐付ける\n- teammate が worktree 付き task を claim すると、bash/read/write はその directory で実行される\n\nMCP は external capability を担当する:\n\n- `connect_mcp(name)` が mock server に接続する\n- `assemble_tool_pool()` が MCP tools を tool pool に組み立てる\n- tool name は `mcp__server__tool` 形式に統一する\n\n---\n\n## s19 からの変化\n\n| Component | s19 | s20 |\n|-----------|-----|-----|\n| tool pool | built-in + MCP | built-in + MCP、s01-s18 の tool を補完 |\n| permission | teaching body では省略 | `PreToolUse` hook で実行 |\n| hooks | 省略 | UserPromptSubmit / PreToolUse / PostToolUse / Stop |\n| todo | 省略 | `todo_write` + reminder |\n| skill | 省略 | system prompt の catalog + `load_skill` |\n| compact | 省略 | LLM 前 compaction + `compact` tool + reactive compact |\n| error recovery | simple try/except | retry / max_tokens / prompt too long |\n| background | 省略 | slow-operation thread + task notification |\n| cron | 省略 | daemon scheduler + durable jobs |\n| multi-agent | 維持 | 維持。teammate は isolated directory 上の basic tools を使う |\n| worktree | 維持 | 維持 |\n| MCP | 新規 | final tool pool の一部として維持 |\n\n---\n\n## 試す\n\n```sh\ncd learn-claude-code\npython s20_comprehensive/code.py\n```\n\n試す prompt:\n\n1. `Create a todo list for inspecting this repo, then list Python files`\n2. `Connect to the docs MCP server and search for agent loop`\n3. `Create two tasks, create worktrees for them, then spawn alice and bob. Ask them to submit plans before claiming tasks.`\n4. `remind me of the meeting in 3 minutes.`\n5. `Run npm install in the background and continue reading README.md`\n\n見るポイント:\n\n- tool call の前に hooks/permission を通るか\n- `connect_mcp` 後の次 round で MCP tool が出るか\n- 遅い operation が background placeholder を返すか\n- cron が時刻到達時に自動で reminder を返すか\n- teammate が plan を提出し、approval 前に停止するか\n- plan approval 後、teammate が task を claim できるか\n- worktree binding 後、teammate が対応 directory に切り替わるか\n\n---\n\n## 終わりは始まり\n\ns01 から s20 まで、コードの能力は増えていく。しかし中心は変わらない:\n\n```python\nwhile True:\n response = LLM(messages, tools)\n if not has_tool_use(response.content):\n return\n results = execute_tools(response.content)\n messages.append(tool_results)\n```\n\nClaude Code の複雑さは「別の agent brain」ではない。成熟した harness の複雑さだ。model は判断と action selection を担当する。harness は environment、tools、permissions、memory、teams、external capabilities を整理する。\n\nこれが本コースの終点だ:仕組みは多い、ループは 1 つ。\n" } -] \ No newline at end of file +] diff --git a/web/src/data/generated/versions.json b/web/src/data/generated/versions.json index 676a20aa5..4bb60d665 100644 --- a/web/src/data/generated/versions.json +++ b/web/src/data/generated/versions.json @@ -41,19 +41,21 @@ "filename": "s02_tool_use/code.py", "title": "Tool Use", "subtitle": "Add a Tool, Add Just One Line", - "loc": 135, + "loc": 170, "tools": [ "bash", "read_file", "write_file", "edit_file", - "glob" + "glob", + "generate_image" ], "newTools": [ "read_file", "write_file", "edit_file", - "glob" + "glob", + "generate_image" ], "coreAddition": "Tool dispatch map", "keyInsight": "The loop stays stable while capabilities register into a dispatch table.", @@ -89,14 +91,19 @@ "signature": "def run_glob(pattern: str)", "startLine": 105 }, + { + "name": "run_generate_image", + "signature": "def run_generate_image(prompt: str, aspect_ratio: str = \"1:1\")", + "startLine": 117 + }, { "name": "agent_loop", "signature": "def agent_loop(messages: list)", - "startLine": 150 + "startLine": 188 } ], "layer": "tools", - "source": "#!/usr/bin/env python3\n\"\"\"\ns02: Tool Use — 在 s01 基础上新增 4 个工具 + 分发映射。\n\n运行: python s02_tool_use/code.py\n需要: pip install anthropic python-dotenv + .env 中配置 ANTHROPIC_API_KEY\n\n本文件 = s01 的全部代码 + 以下新增:\n + run_read / run_write / run_edit / run_glob 四个工具实现\n + TOOL_HANDLERS 分发映射(替代 s01 中硬编码的 run_bash 调用)\n + safe_path 路径安全校验\n\n循环本身(agent_loop)与 s01 完全一致。\n\"\"\"\n\nimport os, subprocess\nfrom pathlib import Path\n\ntry:\n import readline\n readline.parse_and_bind('set bind-tty-special-chars off')\n readline.parse_and_bind('set input-meta on')\n readline.parse_and_bind('set output-meta on')\n readline.parse_and_bind('set convert-meta off')\nexcept ImportError:\n pass\n\nfrom anthropic import Anthropic\nfrom dotenv import load_dotenv\n\nload_dotenv(override=True)\nif os.getenv(\"ANTHROPIC_BASE_URL\"):\n os.environ.pop(\"ANTHROPIC_AUTH_TOKEN\", None)\n\nWORKDIR = Path.cwd()\nclient = Anthropic(base_url=os.getenv(\"ANTHROPIC_BASE_URL\"))\nMODEL = os.environ[\"MODEL_ID\"]\n\nSYSTEM = f\"You are a coding agent at {WORKDIR}. Use tools to solve tasks. Act, don't explain.\"\n\n\n# ═══════════════════════════════════════════════════════════\n# FROM s01 (unchanged)\n# ═══════════════════════════════════════════════════════════\n\ndef run_bash(command: str) -> str:\n dangerous = [\"rm -rf /\", \"sudo\", \"shutdown\", \"reboot\", \"> /dev/\"]\n if any(d in command for d in dangerous):\n return \"Error: Dangerous command blocked\"\n try:\n r = subprocess.run(command, shell=True, cwd=WORKDIR,\n capture_output=True, text=True,\n encoding=\"utf-8\", errors=\"replace\", timeout=120)\n out = (r.stdout + r.stderr).strip()\n return out[:50000] if out else \"(no output)\"\n except subprocess.TimeoutExpired:\n return \"Error: Timeout (120s)\"\n except (FileNotFoundError, OSError) as e:\n return f\"Error: {e}\"\n\n\n# ═══════════════════════════════════════════════════════════\n# NEW in s02: 4 个新工具\n# ═══════════════════════════════════════════════════════════\n\ndef safe_path(p: str) -> Path:\n path = (WORKDIR / p).resolve()\n if not path.is_relative_to(WORKDIR):\n raise ValueError(f\"Path escapes workspace: {p}\")\n return path\n\n\ndef run_read(path: str, limit: int | None = None) -> str:\n try:\n lines = safe_path(path).read_text().splitlines()\n if limit and limit < len(lines):\n lines = lines[:limit] + [f\"... ({len(lines) - limit} more lines)\"]\n return \"\\n\".join(lines)\n except Exception as e:\n return f\"Error: {e}\"\n\n\ndef run_write(path: str, content: str) -> str:\n try:\n file_path = safe_path(path)\n file_path.parent.mkdir(parents=True, exist_ok=True)\n file_path.write_text(content)\n return f\"Wrote {len(content)} bytes to {path}\"\n except Exception as e:\n return f\"Error: {e}\"\n\n\ndef run_edit(path: str, old_text: str, new_text: str) -> str:\n try:\n file_path = safe_path(path)\n text = file_path.read_text()\n if old_text not in text:\n return f\"Error: text not found in {path}\"\n file_path.write_text(text.replace(old_text, new_text, 1))\n return f\"Edited {path}\"\n except Exception as e:\n return f\"Error: {e}\"\n\n\ndef run_glob(pattern: str) -> str:\n import glob as g\n try:\n results = []\n for match in g.glob(pattern, root_dir=WORKDIR):\n if (WORKDIR / match).resolve().is_relative_to(WORKDIR):\n results.append(match)\n return \"\\n\".join(results) if results else \"(no matches)\"\n except Exception as e:\n return f\"Error: {e}\"\n\n\n# ═══════════════════════════════════════════════════════════\n# NEW in s02: 工具定义(s01 只有一个 bash,现在扩展到 5 个)\n# ═══════════════════════════════════════════════════════════\n\nTOOLS = [\n {\"name\": \"bash\", \"description\": \"Run a shell command.\",\n \"input_schema\": {\"type\": \"object\", \"properties\": {\"command\": {\"type\": \"string\"}}, \"required\": [\"command\"]}},\n {\"name\": \"read_file\", \"description\": \"Read file contents.\",\n \"input_schema\": {\"type\": \"object\", \"properties\": {\"path\": {\"type\": \"string\"}, \"limit\": {\"type\": \"integer\"}}, \"required\": [\"path\"]}},\n {\"name\": \"write_file\", \"description\": \"Write content to a file.\",\n \"input_schema\": {\"type\": \"object\", \"properties\": {\"path\": {\"type\": \"string\"}, \"content\": {\"type\": \"string\"}}, \"required\": [\"path\", \"content\"]}},\n {\"name\": \"edit_file\", \"description\": \"Replace exact text in a file once.\",\n \"input_schema\": {\"type\": \"object\", \"properties\": {\"path\": {\"type\": \"string\"}, \"old_text\": {\"type\": \"string\"}, \"new_text\": {\"type\": \"string\"}}, \"required\": [\"path\", \"old_text\", \"new_text\"]}},\n {\"name\": \"glob\", \"description\": \"Find files matching a glob pattern.\",\n \"input_schema\": {\"type\": \"object\", \"properties\": {\"pattern\": {\"type\": \"string\"}}, \"required\": [\"pattern\"]}},\n]\n\n# ═══════════════════════════════════════════════════════════\n# NEW in s02: 工具分发映射(s01 是硬编码 run_bash,现在改为查表)\n# ═══════════════════════════════════════════════════════════\n\nTOOL_HANDLERS = {\n \"bash\": run_bash, \"read_file\": run_read, \"write_file\": run_write,\n \"edit_file\": run_edit, \"glob\": run_glob,\n}\n\n\n# ═══════════════════════════════════════════════════════════\n# agent_loop — 与 s01 结构完全一致,只改了工具执行那部分\n# s01: output = run_bash(block.input[\"command\"])\n# s02: output = TOOL_HANDLERS[block.name](**block.input)\n# ═══════════════════════════════════════════════════════════\n\ndef agent_loop(messages: list):\n while True:\n response = client.messages.create(\n model=MODEL, system=SYSTEM, messages=messages,\n tools=TOOLS, max_tokens=8000,\n )\n messages.append({\"role\": \"assistant\", \"content\": response.content})\n\n if response.stop_reason != \"tool_use\":\n return\n\n results = []\n for block in response.content:\n if block.type == \"tool_use\":\n print(f\"\\033[33m> {block.name}\\033[0m\")\n handler = TOOL_HANDLERS.get(block.name)\n output = handler(**block.input) if handler else f\"Unknown: {block.name}\"\n print(str(output)[:200])\n results.append({\"type\": \"tool_result\", \"tool_use_id\": block.id, \"content\": output})\n\n messages.append({\"role\": \"user\", \"content\": results})\n\n\nif __name__ == \"__main__\":\n print(\"s02: Tool Use — 在 s01 基础上加了 4 个工具\")\n print(\"输入问题,回车发送。输入 q 退出。\\n\")\n\n history = []\n while True:\n try:\n query = input(\"\\033[36ms02 >> \\033[0m\")\n except (EOFError, KeyboardInterrupt):\n break\n if query.strip().lower() in (\"q\", \"exit\", \"\"):\n break\n history.append({\"role\": \"user\", \"content\": query})\n agent_loop(history)\n for block in history[-1][\"content\"]:\n if getattr(block, \"type\", None) == \"text\":\n print(block.text)\n print()\n", + "source": "#!/usr/bin/env python3\n\"\"\"\ns02: Tool Use — 在 s01 基础上新增 5 个工具 + 分发映射。\n\n运行: python s02_tool_use/code.py\n需要: pip install anthropic python-dotenv + .env 中配置 ANTHROPIC_API_KEY\n\n本文件 = s01 的全部代码 + 以下新增:\n + run_read / run_write / run_edit / run_glob / run_generate_image 五个工具实现\n + TOOL_HANDLERS 分发映射(替代 s01 中硬编码的 run_bash 调用)\n + safe_path 路径安全校验\n\n循环本身(agent_loop)与 s01 完全一致。\n\"\"\"\n\nimport os, subprocess, json, urllib.request\nfrom pathlib import Path\n\ntry:\n import readline\n readline.parse_and_bind('set bind-tty-special-chars off')\n readline.parse_and_bind('set input-meta on')\n readline.parse_and_bind('set output-meta on')\n readline.parse_and_bind('set convert-meta off')\nexcept ImportError:\n pass\n\nfrom anthropic import Anthropic\nfrom dotenv import load_dotenv\n\nload_dotenv(override=True)\nif os.getenv(\"ANTHROPIC_BASE_URL\"):\n os.environ.pop(\"ANTHROPIC_AUTH_TOKEN\", None)\n\nWORKDIR = Path.cwd()\nclient = Anthropic(base_url=os.getenv(\"ANTHROPIC_BASE_URL\"))\nMODEL = os.environ[\"MODEL_ID\"]\n\nSYSTEM = f\"You are a coding agent at {WORKDIR}. Use tools to solve tasks. Act, don't explain.\"\n\n\n# ═══════════════════════════════════════════════════════════\n# FROM s01 (unchanged)\n# ═══════════════════════════════════════════════════════════\n\ndef run_bash(command: str) -> str:\n dangerous = [\"rm -rf /\", \"sudo\", \"shutdown\", \"reboot\", \"> /dev/\"]\n if any(d in command for d in dangerous):\n return \"Error: Dangerous command blocked\"\n try:\n r = subprocess.run(command, shell=True, cwd=WORKDIR,\n capture_output=True, text=True,\n encoding=\"utf-8\", errors=\"replace\", timeout=120)\n out = (r.stdout + r.stderr).strip()\n return out[:50000] if out else \"(no output)\"\n except subprocess.TimeoutExpired:\n return \"Error: Timeout (120s)\"\n except (FileNotFoundError, OSError) as e:\n return f\"Error: {e}\"\n\n\n# ═══════════════════════════════════════════════════════════\n# NEW in s02: 5 个新工具\n# ═══════════════════════════════════════════════════════════\n\ndef safe_path(p: str) -> Path:\n path = (WORKDIR / p).resolve()\n if not path.is_relative_to(WORKDIR):\n raise ValueError(f\"Path escapes workspace: {p}\")\n return path\n\n\ndef run_read(path: str, limit: int | None = None) -> str:\n try:\n lines = safe_path(path).read_text().splitlines()\n if limit and limit < len(lines):\n lines = lines[:limit] + [f\"... ({len(lines) - limit} more lines)\"]\n return \"\\n\".join(lines)\n except Exception as e:\n return f\"Error: {e}\"\n\n\ndef run_write(path: str, content: str) -> str:\n try:\n file_path = safe_path(path)\n file_path.parent.mkdir(parents=True, exist_ok=True)\n file_path.write_text(content)\n return f\"Wrote {len(content)} bytes to {path}\"\n except Exception as e:\n return f\"Error: {e}\"\n\n\ndef run_edit(path: str, old_text: str, new_text: str) -> str:\n try:\n file_path = safe_path(path)\n text = file_path.read_text()\n if old_text not in text:\n return f\"Error: text not found in {path}\"\n file_path.write_text(text.replace(old_text, new_text, 1))\n return f\"Edited {path}\"\n except Exception as e:\n return f\"Error: {e}\"\n\n\ndef run_glob(pattern: str) -> str:\n import glob as g\n try:\n results = []\n for match in g.glob(pattern, root_dir=WORKDIR):\n if (WORKDIR / match).resolve().is_relative_to(WORKDIR):\n results.append(match)\n return \"\\n\".join(results) if results else \"(no matches)\"\n except Exception as e:\n return f\"Error: {e}\"\n\n\ndef run_generate_image(prompt: str, aspect_ratio: str = \"1:1\") -> str:\n \"\"\"Generate an image with MiniMax and return the image URL.\"\"\"\n api_key = os.getenv(\"MINIMAX_API_KEY\")\n if not api_key:\n return \"Error: set MINIMAX_API_KEY to use image generation\"\n api_host = (os.getenv(\"MINIMAX_API_HOST\") or \"https://api.minimax.io\").rstrip(\"/\")\n\n payload = {\n \"model\": \"image-01\",\n \"prompt\": prompt,\n \"aspect_ratio\": aspect_ratio,\n \"n\": 1,\n \"response_format\": \"url\",\n }\n req = urllib.request.Request(\n f\"{api_host}/v1/image_generation\",\n data=json.dumps(payload).encode(\"utf-8\"),\n headers={\n \"Authorization\": f\"Bearer {api_key}\",\n \"Content-Type\": \"application/json\",\n },\n method=\"POST\",\n )\n try:\n with urllib.request.urlopen(req, timeout=120) as resp:\n data = json.loads(resp.read().decode(\"utf-8\"))\n status = data.get(\"base_resp\", {}).get(\"status_code\")\n if status not in (None, 0):\n msg = data.get(\"base_resp\", {}).get(\"status_msg\", \"unknown error\")\n return f\"Error: MiniMax image generation failed ({status}): {msg}\"\n urls = data.get(\"data\", {}).get(\"image_urls\") or []\n return urls[0] if urls else f\"Error: no image URL returned: {data}\"\n except Exception as e:\n return f\"Error: {e}\"\n\n\n# ═══════════════════════════════════════════════════════════\n# NEW in s02: 工具定义(s01 只有一个 bash,现在扩展到 6 个)\n# ═══════════════════════════════════════════════════════════\n\nTOOLS = [\n {\"name\": \"bash\", \"description\": \"Run a shell command.\",\n \"input_schema\": {\"type\": \"object\", \"properties\": {\"command\": {\"type\": \"string\"}}, \"required\": [\"command\"]}},\n {\"name\": \"read_file\", \"description\": \"Read file contents.\",\n \"input_schema\": {\"type\": \"object\", \"properties\": {\"path\": {\"type\": \"string\"}, \"limit\": {\"type\": \"integer\"}}, \"required\": [\"path\"]}},\n {\"name\": \"write_file\", \"description\": \"Write content to a file.\",\n \"input_schema\": {\"type\": \"object\", \"properties\": {\"path\": {\"type\": \"string\"}, \"content\": {\"type\": \"string\"}}, \"required\": [\"path\", \"content\"]}},\n {\"name\": \"edit_file\", \"description\": \"Replace exact text in a file once.\",\n \"input_schema\": {\"type\": \"object\", \"properties\": {\"path\": {\"type\": \"string\"}, \"old_text\": {\"type\": \"string\"}, \"new_text\": {\"type\": \"string\"}}, \"required\": [\"path\", \"old_text\", \"new_text\"]}},\n {\"name\": \"glob\", \"description\": \"Find files matching a glob pattern.\",\n \"input_schema\": {\"type\": \"object\", \"properties\": {\"pattern\": {\"type\": \"string\"}}, \"required\": [\"pattern\"]}},\n {\"name\": \"generate_image\", \"description\": \"Generate an image from a text prompt using MiniMax.\",\n \"input_schema\": {\"type\": \"object\", \"properties\": {\"prompt\": {\"type\": \"string\"}, \"aspect_ratio\": {\"type\": \"string\", \"enum\": [\"1:1\", \"16:9\", \"4:3\", \"3:2\", \"2:3\", \"3:4\", \"9:16\", \"21:9\"]}}, \"required\": [\"prompt\"]}},\n]\n\n# ═══════════════════════════════════════════════════════════\n# NEW in s02: 工具分发映射(s01 是硬编码 run_bash,现在改为查表)\n# ═══════════════════════════════════════════════════════════\n\nTOOL_HANDLERS = {\n \"bash\": run_bash, \"read_file\": run_read, \"write_file\": run_write,\n \"edit_file\": run_edit, \"glob\": run_glob, \"generate_image\": run_generate_image,\n}\n\n\n# ═══════════════════════════════════════════════════════════\n# agent_loop — 与 s01 结构完全一致,只改了工具执行那部分\n# s01: output = run_bash(block.input[\"command\"])\n# s02: output = TOOL_HANDLERS[block.name](**block.input)\n# ═══════════════════════════════════════════════════════════\n\ndef agent_loop(messages: list):\n while True:\n response = client.messages.create(\n model=MODEL, system=SYSTEM, messages=messages,\n tools=TOOLS, max_tokens=8000,\n )\n messages.append({\"role\": \"assistant\", \"content\": response.content})\n\n if response.stop_reason != \"tool_use\":\n return\n\n results = []\n for block in response.content:\n if block.type == \"tool_use\":\n print(f\"\\033[33m> {block.name}\\033[0m\")\n handler = TOOL_HANDLERS.get(block.name)\n output = handler(**block.input) if handler else f\"Unknown: {block.name}\"\n print(str(output)[:200])\n results.append({\"type\": \"tool_result\", \"tool_use_id\": block.id, \"content\": output})\n\n messages.append({\"role\": \"user\", \"content\": results})\n\n\nif __name__ == \"__main__\":\n print(\"s02: Tool Use — 在 s01 基础上加了 5 个工具\")\n print(\"输入问题,回车发送。输入 q 退出。\\n\")\n\n history = []\n while True:\n try:\n query = input(\"\\033[36ms02 >> \\033[0m\")\n except (EOFError, KeyboardInterrupt):\n break\n if query.strip().lower() in (\"q\", \"exit\", \"\"):\n break\n history.append({\"role\": \"user\", \"content\": query})\n agent_loop(history)\n for block in history[-1][\"content\"]:\n if getattr(block, \"type\", None) == \"text\":\n print(block.text)\n print()\n", "images": [ { "src": "/course-assets/s02_tool_use/concurrency-comparison.svg", @@ -3633,15 +3640,17 @@ "run_read", "run_write", "run_edit", - "run_glob" + "run_glob", + "run_generate_image" ], "newTools": [ "read_file", "write_file", "edit_file", - "glob" + "glob", + "generate_image" ], - "locDelta": 33 + "locDelta": 68 }, { "from": "s02", @@ -3654,7 +3663,7 @@ "check_permission" ], "newTools": [], - "locDelta": 45 + "locDelta": 10 }, { "from": "s03", From 4156fe64339cdc87b5f703212d857c09cd4bb80a Mon Sep 17 00:00:00 2001 From: octo-patch <266937838+octo-patch@users.noreply.github.com> Date: Mon, 13 Jul 2026 14:41:59 +0800 Subject: [PATCH 3/4] Align image tool with API contract --- s02_tool_use/README.ja.md | 10 +++++----- s02_tool_use/README.md | 10 +++++----- s02_tool_use/code.py | 12 ++++++------ s02_tool_use/images/tool-dispatch.ja.svg | 4 ++-- s02_tool_use/images/tool-dispatch.svg | 4 ++-- tests/test_s02_generate_image.py | 12 ++++++++++++ .../course-assets/s02_tool_use/tool-dispatch.ja.svg | 4 ++-- .../course-assets/s02_tool_use/tool-dispatch.svg | 4 ++-- web/src/data/generated/docs.json | 4 ++-- web/src/data/generated/versions.json | 2 +- 10 files changed, 39 insertions(+), 27 deletions(-) diff --git a/s02_tool_use/README.ja.md b/s02_tool_use/README.ja.md index 8d437ab8d..440c43ecb 100644 --- a/s02_tool_use/README.ja.md +++ b/s02_tool_use/README.ja.md @@ -30,7 +30,7 @@ Agent にツールを追加するには、たった二つ: --- -## 1 つのツールから 6 つのツールへ +## From 1 Tool to 6 Tools s01 には bash だけだった: @@ -40,7 +40,7 @@ TOOLS = [{"name": "bash", ...}] def run_bash(command): ... ``` -s02 では 6 つに増え、各ツールは独立して定義される: +s02 expands to 6 tools, each independently defined: ```python TOOLS = [ @@ -130,7 +130,7 @@ for block in response.content: | コンポーネント | 変更前 (s01) | 変更後 (s02) | |--------------|-------------|-------------| -| ツール数 | 1 (bash) | 6 (+read, write, edit, glob, generate_image) | +| Tool count | 1 (bash) | 6 (+read, write, edit, glob, generate_image) | | ツール実行 | ハードコード `run_bash()` | TOOL_HANDLERS 検索ディスパッチ | | パス安全性 | なし | safe_path 検証(file tools のみ) | | ループ | `while True` + `stop_reason` | s01 と完全に同一 | @@ -144,7 +144,7 @@ cd learn-claude-code python s02_tool_use/code.py ``` -オプションの `generate_image` ツールには、`.env` に以下の値も必要。中国本土では host を `https://api.minimaxi.com` に変更する。 +The optional `generate_image` tool also needs these values in `.env`. Use `https://api.minimaxi.com` as the host for China mainland. ```sh MINIMAX_API_KEY=sk-xxx @@ -165,7 +165,7 @@ MINIMAX_API_HOST=https://api.minimax.io ## 次へ -Agent は 6 つの専用ツールを持つようになった。file tools は `safe_path` で保護されるが、bash は制限なし — `rm -rf /` はまだ実行できる。 +The Agent now has 6 specialized tools. File tools are protected by `safe_path`, but bash remains unrestricted: `rm -rf /` still runs. → s03 Permission:ツール実行前にゲートを追加 — この操作は安全か? ユーザーの承認が必要か? diff --git a/s02_tool_use/README.md b/s02_tool_use/README.md index 5b80f70b6..fd28005ca 100644 --- a/s02_tool_use/README.md +++ b/s02_tool_use/README.md @@ -30,7 +30,7 @@ s01 的循环完全保留(LLM 调用、stop_reason 判断、消息追加)。 --- -## 从 1 个工具到 6 个工具 +## From 1 Tool to 6 Tools s01 只有一个 bash: @@ -40,7 +40,7 @@ TOOLS = [{"name": "bash", ...}] def run_bash(command): ... ``` -s02 加到 6 个,每个工具都是独立定义: +s02 expands to 6 tools, each independently defined: ```python TOOLS = [ @@ -130,7 +130,7 @@ for block in response.content: | 组件 | 之前 (s01) | 之后 (s02) | |------|-----------|-----------| -| 工具数量 | 1 (bash) | 6 (+read, write, edit, glob, generate_image) | +| Tool count | 1 (bash) | 6 (+read, write, edit, glob, generate_image) | | 工具执行 | 硬编码 `run_bash()` | TOOL_HANDLERS 查表分发 | | 路径安全 | 无 | safe_path 校验(仅 file tools) | | 循环 | `while True` + `stop_reason` | 与 s01 完全一致 | @@ -144,7 +144,7 @@ cd learn-claude-code python s02_tool_use/code.py ``` -可选的 `generate_image` 工具还需要在 `.env` 中配置以下值。中国大陆区域请将 host 改为 `https://api.minimaxi.com`。 +The optional `generate_image` tool also needs these values in `.env`. Use `https://api.minimaxi.com` as the host for China mainland. ```sh MINIMAX_API_KEY=sk-xxx @@ -165,7 +165,7 @@ MINIMAX_API_HOST=https://api.minimax.io ## 接下来 -现在 Agent 有 6 个专用工具。file tools 受 `safe_path` 保护,但 bash 不受限制,`rm -rf /` 还是能跑。 +The Agent now has 6 specialized tools. File tools are protected by `safe_path`, but bash remains unrestricted: `rm -rf /` still runs. s03 Permission → 在工具执行之前加一道门:这个操作安全吗?需要用户批准吗? diff --git a/s02_tool_use/code.py b/s02_tool_use/code.py index d68192112..f2c69483e 100644 --- a/s02_tool_use/code.py +++ b/s02_tool_use/code.py @@ -1,12 +1,12 @@ #!/usr/bin/env python3 """ -s02: Tool Use — 在 s01 基础上新增 5 个工具 + 分发映射。 +s02: Tool Use - adds five tools and a dispatch map on top of s01. 运行: python s02_tool_use/code.py 需要: pip install anthropic python-dotenv + .env 中配置 ANTHROPIC_API_KEY 本文件 = s01 的全部代码 + 以下新增: - + run_read / run_write / run_edit / run_glob / run_generate_image 五个工具实现 + + Five tool implementations: run_read / run_write / run_edit / run_glob / run_generate_image + TOOL_HANDLERS 分发映射(替代 s01 中硬编码的 run_bash 调用) + safe_path 路径安全校验 @@ -60,7 +60,7 @@ def run_bash(command: str) -> str: # ═══════════════════════════════════════════════════════════ -# NEW in s02: 5 个新工具 +# NEW in s02: 5 tools # ═══════════════════════════════════════════════════════════ def safe_path(p: str) -> Path: @@ -151,7 +151,7 @@ def run_generate_image(prompt: str, aspect_ratio: str = "1:1") -> str: # ═══════════════════════════════════════════════════════════ -# NEW in s02: 工具定义(s01 只有一个 bash,现在扩展到 6 个) +# NEW in s02: tool definitions (s01 has only bash; s02 has 6 tools) # ═══════════════════════════════════════════════════════════ TOOLS = [ @@ -166,7 +166,7 @@ def run_generate_image(prompt: str, aspect_ratio: str = "1:1") -> str: {"name": "glob", "description": "Find files matching a glob pattern.", "input_schema": {"type": "object", "properties": {"pattern": {"type": "string"}}, "required": ["pattern"]}}, {"name": "generate_image", "description": "Generate an image from a text prompt using MiniMax.", - "input_schema": {"type": "object", "properties": {"prompt": {"type": "string"}, "aspect_ratio": {"type": "string", "enum": ["1:1", "16:9", "4:3", "3:2", "2:3", "3:4", "9:16", "21:9"]}}, "required": ["prompt"]}}, + "input_schema": {"type": "object", "properties": {"prompt": {"type": "string", "maxLength": 1500}, "aspect_ratio": {"type": "string", "default": "1:1", "enum": ["1:1", "16:9", "4:3", "3:2", "2:3", "3:4", "9:16", "21:9"]}}, "required": ["prompt"]}}, ] # ═══════════════════════════════════════════════════════════ @@ -209,7 +209,7 @@ def agent_loop(messages: list): if __name__ == "__main__": - print("s02: Tool Use — 在 s01 基础上加了 5 个工具") + print("s02: Tool Use - adds 5 tools on top of s01") print("输入问题,回车发送。输入 q 退出。\n") history = [] diff --git a/s02_tool_use/images/tool-dispatch.ja.svg b/s02_tool_use/images/tool-dispatch.ja.svg index feaed8961..f5aaaecaa 100644 --- a/s02_tool_use/images/tool-dispatch.ja.svg +++ b/s02_tool_use/images/tool-dispatch.ja.svg @@ -106,8 +106,8 @@ - s01 保持(ループ不変) + s01 Preserved (loop unchanged) - s02 新規(6 つのツール + ディスパッチマッピング) + s02 New (6 tools + dispatch mapping) ループ内で変更されたのは 1 行だけ:run_bash() → TOOL_HANDLERS[block.name]() diff --git a/s02_tool_use/images/tool-dispatch.svg b/s02_tool_use/images/tool-dispatch.svg index fde6ed70e..a3e783cae 100644 --- a/s02_tool_use/images/tool-dispatch.svg +++ b/s02_tool_use/images/tool-dispatch.svg @@ -106,8 +106,8 @@ - s01 保留(循环不变) + s01 Preserved (loop unchanged) - s02 新增(6 个工具 + 分发映射) + s02 New (6 tools + dispatch mapping) 循环里只改了 1 行:run_bash() → TOOL_HANDLERS[block.name]() diff --git a/tests/test_s02_generate_image.py b/tests/test_s02_generate_image.py index 9ffcd1f08..cc5cf4b31 100644 --- a/tests/test_s02_generate_image.py +++ b/tests/test_s02_generate_image.py @@ -70,6 +70,18 @@ class GenerateImageTests(unittest.TestCase): def setUpClass(cls): cls.module = load_course_module() + def test_tool_schema_matches_the_image_api_contract(self): + tool = next(tool for tool in self.module.TOOLS if tool["name"] == "generate_image") + schema = tool["input_schema"] + + self.assertEqual(schema["required"], ["prompt"]) + self.assertEqual(schema["properties"]["prompt"]["maxLength"], 1500) + self.assertEqual(schema["properties"]["aspect_ratio"]["default"], "1:1") + self.assertEqual( + schema["properties"]["aspect_ratio"]["enum"], + ["1:1", "16:9", "4:3", "3:2", "2:3", "3:4", "9:16", "21:9"], + ) + def test_uses_the_selected_regional_api_host(self): previous_key = os.environ.get("MINIMAX_API_KEY") previous_host = os.environ.get("MINIMAX_API_HOST") diff --git a/web/public/course-assets/s02_tool_use/tool-dispatch.ja.svg b/web/public/course-assets/s02_tool_use/tool-dispatch.ja.svg index feaed8961..f5aaaecaa 100644 --- a/web/public/course-assets/s02_tool_use/tool-dispatch.ja.svg +++ b/web/public/course-assets/s02_tool_use/tool-dispatch.ja.svg @@ -106,8 +106,8 @@ - s01 保持(ループ不変) + s01 Preserved (loop unchanged) - s02 新規(6 つのツール + ディスパッチマッピング) + s02 New (6 tools + dispatch mapping) ループ内で変更されたのは 1 行だけ:run_bash() → TOOL_HANDLERS[block.name]() diff --git a/web/public/course-assets/s02_tool_use/tool-dispatch.svg b/web/public/course-assets/s02_tool_use/tool-dispatch.svg index fde6ed70e..a3e783cae 100644 --- a/web/public/course-assets/s02_tool_use/tool-dispatch.svg +++ b/web/public/course-assets/s02_tool_use/tool-dispatch.svg @@ -106,8 +106,8 @@ - s01 保留(循环不变) + s01 Preserved (loop unchanged) - s02 新增(6 个工具 + 分发映射) + s02 New (6 tools + dispatch mapping) 循环里只改了 1 行:run_bash() → TOOL_HANDLERS[block.name]() diff --git a/web/src/data/generated/docs.json b/web/src/data/generated/docs.json index d52eb2233..2ea9e312d 100644 --- a/web/src/data/generated/docs.json +++ b/web/src/data/generated/docs.json @@ -27,13 +27,13 @@ "version": "s02", "locale": "zh", "title": "s02: Tool Use — 多加一个工具,只加一行", - "content": "# s02: Tool Use — 多加一个工具,只加一行\n\ns01 → `s02` → [s03](/zh/s03) → s04 → ... → s20\n> *\"加一个工具, 只加一个 handler\"* — 循环不用动, 新工具注册进 dispatch map 就行。\n>\n> **Harness 层**: 工具分发 — 扩展模型能触达的边界。\n\n---\n\n## 只有 bash 一个工具\n\ns01 的 Agent 只有一个 bash 工具。读文件要 `cat`,写文件要 `echo \"...\" > file.py`,改文件要 `sed`。\n\n模型想的是\"读这个文件\",却要拼出 `cat path/to/file`。多了一层翻译,浪费 token,还容易拼错。\n\n---\n\n## 全局视角:工具分发\n\n![Tool Dispatch](/course-assets/s02_tool_use/tool-dispatch.svg)\n\ns01 的循环完全保留(LLM 调用、stop_reason 判断、消息追加)。唯一的变动在工具执行那 1 行:`run_bash()` 替换为 `TOOL_HANDLERS[block.name]()` 查表分发。\n\n给 Agent 加一个工具只需要做两件事:\n\n1. **定义工具**:在 `TOOLS` 数组里加一条描述\n2. **注册处理函数**:在 `TOOL_HANDLERS` 字典里加一个映射\n\n---\n\n## 从 1 个工具到 6 个工具\n\ns01 只有一个 bash:\n\n```python\nTOOLS = [{\"name\": \"bash\", ...}]\n\ndef run_bash(command): ...\n```\n\ns02 加到 6 个,每个工具都是独立定义:\n\n```python\nTOOLS = [\n {\"name\": \"bash\", \"description\": \"Run a shell command.\", ...},\n {\"name\": \"read_file\", \"description\": \"Read file contents.\", ...},\n {\"name\": \"write_file\", \"description\": \"Write content to file.\", ...},\n {\"name\": \"edit_file\", \"description\": \"Replace text in file once.\", ...},\n {\"name\": \"glob\", \"description\": \"Find files by pattern.\", ...},\n {\"name\": \"generate_image\", \"description\": \"Generate an image from a text prompt.\", ...},\n]\n```\n\n每个工具有自己的实现函数:\n\n```python\ndef run_read(path, limit=None):\n lines = safe_path(path).read_text().splitlines()\n if limit:\n lines = lines[:limit]\n return \"\\n\".join(lines)\n\ndef run_write(path, content):\n safe_path(path).write_text(content)\n return f\"Wrote {len(content)} bytes to {path}\"\n\ndef run_edit(path, old_text, new_text):\n text = safe_path(path).read_text()\n if old_text not in text:\n return \"Error: text not found\"\n safe_path(path).write_text(text.replace(old_text, new_text, 1))\n return f\"Edited {path}\"\n\ndef run_glob(pattern):\n import glob as g\n return \"\\n\".join(g.glob(pattern, root_dir=WORKDIR))\n\ndef run_generate_image(prompt, aspect_ratio=\"1:1\"):\n ...\n```\n\n---\n\n## 工具分发\n\n```python\nTOOL_HANDLERS = {\n \"bash\": run_bash,\n \"read_file\": run_read,\n \"write_file\": run_write,\n \"edit_file\": run_edit,\n \"glob\": run_glob,\n \"generate_image\": run_generate_image,\n}\n\n# 循环里只改了一行——从硬编码 run_bash 变成查表:\nfor block in response.content:\n if block.type == \"tool_use\":\n handler = TOOL_HANDLERS[block.name] # 查表\n output = handler(**block.input) # 调用\n results.append(...)\n```\n\n加一个工具 = 在 `TOOLS` 数组加一条 + 在 `TOOL_HANDLERS` 字典加一行。循环不变。\n\n---\n\n## 多个工具调用\n\n模型经常一次返回多个 tool_use:\"读一下 a.py 和 b.py,然后列出所有 .py 文件\"。\n\n教学版按 `response.content` 原始顺序逐个执行。CC 的做法更复杂:按原始顺序切成连续 batch,batch 内并发安全的工具并行执行,batch 间严格顺序(见附录)。\n\n---\n\n## 速查\n\n| 概念 | 一句话 |\n|------|--------|\n| TOOL_HANDLERS | 工具名 → 处理函数的字典。加工具 = 加一行映射 |\n| 工具定义 | 告诉模型\"我能做什么\"的 JSON schema |\n| 多工具调用 | 模型可一次返回多个 tool_use,教学版按原始顺序逐个执行 |\n| 循环不变 | s01 的 `while True` 循环一行都没改 |\n\n---\n\n## 相对 s01 的变更\n\n| 组件 | 之前 (s01) | 之后 (s02) |\n|------|-----------|-----------|\n| 工具数量 | 1 (bash) | 6 (+read, write, edit, glob, generate_image) |\n| 工具执行 | 硬编码 `run_bash()` | TOOL_HANDLERS 查表分发 |\n| 路径安全 | 无 | safe_path 校验(仅 file tools) |\n| 循环 | `while True` + `stop_reason` | 与 s01 完全一致 |\n\n---\n\n## 试一下\n\n```sh\ncd learn-claude-code\npython s02_tool_use/code.py\n```\n\n可选的 `generate_image` 工具还需要在 `.env` 中配置以下值。中国大陆区域请将 host 改为 `https://api.minimaxi.com`。\n\n```sh\nMINIMAX_API_KEY=sk-xxx\nMINIMAX_API_HOST=https://api.minimax.io\n```\n\n试试这些 prompt:\n\n1. `Read the file README.md and tell me what this project is about`\n2. `Create a file called test.py that prints \"hello\", then read it back`\n3. `Find all Python files in this directory`\n4. `Read both README.md and requirements.txt, then create a summary file`\n5. `Generate a 16:9 image of a coding workspace at sunrise`\n\n观察重点:模型什么时候只调一个工具,什么时候一次调多个?多个工具调用的顺序和结果是否正确?\n\n---\n\n## 接下来\n\n现在 Agent 有 6 个专用工具。file tools 受 `safe_path` 保护,但 bash 不受限制,`rm -rf /` 还是能跑。\n\ns03 Permission → 在工具执行之前加一道门:这个操作安全吗?需要用户批准吗?\n\n
\n深入 CC 源码\n\n> 以下基于 CC 源码 `Tool.ts`、`tools.ts`、`toolOrchestration.ts`、`toolExecution.ts`、`StreamingToolExecutor.ts` 的核查。\n\n### 一、工具定义方式\n\n**教学版**:`TOOLS` 数组 + `TOOL_HANDLERS` 字典。定义和实现分开。\n**CC**:每个工具是 `buildTool()` 创建的独立对象,包含 schema、验证、权限、执行。`getAllBaseTools()` 汇总所有工具。\n\n教学版的分离方式对教学更清晰——读者一眼看到\"加一个工具 = 两条定义\"。\n\n### 二、并发安全判断:isConcurrencySafe()\n\n![Tool Concurrency](/course-assets/s02_tool_use/concurrency-comparison.svg)\n\n教学版按原始顺序逐个执行,不做并发。CC 用 `isConcurrencySafe(input)` 判断能否并发——注意这不是简单的\"只读 vs 写\",而是按具体输入判断:\n\n| | isReadOnly | isConcurrencySafe |\n|---|---|---|\n| FileRead | true | true |\n| Glob | true | true |\n| Bash `ls` | true | **true** ← 关键差异 |\n| Bash `rm` | false | false |\n| TaskCreate | false | **true** ← 改状态但可并发(TaskCreate 在 s12 介绍) |\n\nCC 的 Bash tool 的 `isConcurrencySafe` 等于 `isReadOnly`——只读命令可并发,写命令不可。TaskCreate 虽然改了任务文件,但每次都写不同的文件,所以可以并发。\n\n### 三、分区算法\n\nCC 的 `partitionToolCalls()`(`toolOrchestration.ts:91-115`)不是分两组,而是把工具调用**按连续块分批**:\n\n```\n[read A, read B, glob *.py, bash \"rm x\", read C]\n → batch1(并发): [read A, read B, glob *.py]\n → batch2(串行): [bash \"rm x\"]\n → batch3(并发): [read C]\n```\n\n并发安全的连续块编入同一个 batch,batch 内真正并发执行(`toolOrchestration.ts:152-176`,有并发上限)。遇到非并发安全的就开新 batch 串行执行。batch 之间严格顺序。\n\n### 四、验证管线\n\nCC 的每个工具调用经过严格的 5 步验证(`toolExecution.ts`):\n\n1. **Zod schema 验证**(`614-680`,教学版用 JSON Schema 替代):参数类型/结构检查\n2. **工具级 validateInput()**(`682-733`):参数值验证(如路径是否在工作区内)\n3. **PreToolUse hooks**(`800-862`,s04 详细介绍):钩子可以返回消息、修改输入、阻止执行\n4. **权限检查**(`921-931`,s03 的核心内容):canUseTool + checkPermissions → allow/deny/ask\n5. **执行 tool.call()**(`1207-1222`)\n\n教学版省略了 Zod(用 JSON Schema)、省略了 validateInput(用安全函数)、保留了权限检查和钩子概念。\n\n### 五、流式工具执行\n\nCC 的 `StreamingToolExecutor`(`StreamingToolExecutor.ts`)让工具在模型还在生成时就启动——不等模型说完。`read_file` 可能在模型还在输出\"我来分析\"的时候就跑完了。教学版不实现这个,目标和 s01 一致——概念清晰,不追求性能极致。\n\n### 六、工具结果持久化\n\n每个工具有一个 `maxResultSizeChars` 字段。结果超过这个值就落盘,模型看到的是预览 + 文件路径。FileRead 特殊——设为 `Infinity`,防止读文件的输出又被当成文件落盘。具体来说,如果 FileRead 的结果超过阈值被落盘,模型下次读那个落盘文件时又会触发落盘 → 无限循环(读文件 → 落盘 → 再读 → 再落盘 → ...)。\n\n
\n\n\n" + "content": "# s02: Tool Use — 多加一个工具,只加一行\n\ns01 → `s02` → [s03](/zh/s03) → s04 → ... → s20\n> *\"加一个工具, 只加一个 handler\"* — 循环不用动, 新工具注册进 dispatch map 就行。\n>\n> **Harness 层**: 工具分发 — 扩展模型能触达的边界。\n\n---\n\n## 只有 bash 一个工具\n\ns01 的 Agent 只有一个 bash 工具。读文件要 `cat`,写文件要 `echo \"...\" > file.py`,改文件要 `sed`。\n\n模型想的是\"读这个文件\",却要拼出 `cat path/to/file`。多了一层翻译,浪费 token,还容易拼错。\n\n---\n\n## 全局视角:工具分发\n\n![Tool Dispatch](/course-assets/s02_tool_use/tool-dispatch.svg)\n\ns01 的循环完全保留(LLM 调用、stop_reason 判断、消息追加)。唯一的变动在工具执行那 1 行:`run_bash()` 替换为 `TOOL_HANDLERS[block.name]()` 查表分发。\n\n给 Agent 加一个工具只需要做两件事:\n\n1. **定义工具**:在 `TOOLS` 数组里加一条描述\n2. **注册处理函数**:在 `TOOL_HANDLERS` 字典里加一个映射\n\n---\n\n## From 1 Tool to 6 Tools\n\ns01 只有一个 bash:\n\n```python\nTOOLS = [{\"name\": \"bash\", ...}]\n\ndef run_bash(command): ...\n```\n\ns02 expands to 6 tools, each independently defined:\n\n```python\nTOOLS = [\n {\"name\": \"bash\", \"description\": \"Run a shell command.\", ...},\n {\"name\": \"read_file\", \"description\": \"Read file contents.\", ...},\n {\"name\": \"write_file\", \"description\": \"Write content to file.\", ...},\n {\"name\": \"edit_file\", \"description\": \"Replace text in file once.\", ...},\n {\"name\": \"glob\", \"description\": \"Find files by pattern.\", ...},\n {\"name\": \"generate_image\", \"description\": \"Generate an image from a text prompt.\", ...},\n]\n```\n\n每个工具有自己的实现函数:\n\n```python\ndef run_read(path, limit=None):\n lines = safe_path(path).read_text().splitlines()\n if limit:\n lines = lines[:limit]\n return \"\\n\".join(lines)\n\ndef run_write(path, content):\n safe_path(path).write_text(content)\n return f\"Wrote {len(content)} bytes to {path}\"\n\ndef run_edit(path, old_text, new_text):\n text = safe_path(path).read_text()\n if old_text not in text:\n return \"Error: text not found\"\n safe_path(path).write_text(text.replace(old_text, new_text, 1))\n return f\"Edited {path}\"\n\ndef run_glob(pattern):\n import glob as g\n return \"\\n\".join(g.glob(pattern, root_dir=WORKDIR))\n\ndef run_generate_image(prompt, aspect_ratio=\"1:1\"):\n ...\n```\n\n---\n\n## 工具分发\n\n```python\nTOOL_HANDLERS = {\n \"bash\": run_bash,\n \"read_file\": run_read,\n \"write_file\": run_write,\n \"edit_file\": run_edit,\n \"glob\": run_glob,\n \"generate_image\": run_generate_image,\n}\n\n# 循环里只改了一行——从硬编码 run_bash 变成查表:\nfor block in response.content:\n if block.type == \"tool_use\":\n handler = TOOL_HANDLERS[block.name] # 查表\n output = handler(**block.input) # 调用\n results.append(...)\n```\n\n加一个工具 = 在 `TOOLS` 数组加一条 + 在 `TOOL_HANDLERS` 字典加一行。循环不变。\n\n---\n\n## 多个工具调用\n\n模型经常一次返回多个 tool_use:\"读一下 a.py 和 b.py,然后列出所有 .py 文件\"。\n\n教学版按 `response.content` 原始顺序逐个执行。CC 的做法更复杂:按原始顺序切成连续 batch,batch 内并发安全的工具并行执行,batch 间严格顺序(见附录)。\n\n---\n\n## 速查\n\n| 概念 | 一句话 |\n|------|--------|\n| TOOL_HANDLERS | 工具名 → 处理函数的字典。加工具 = 加一行映射 |\n| 工具定义 | 告诉模型\"我能做什么\"的 JSON schema |\n| 多工具调用 | 模型可一次返回多个 tool_use,教学版按原始顺序逐个执行 |\n| 循环不变 | s01 的 `while True` 循环一行都没改 |\n\n---\n\n## 相对 s01 的变更\n\n| 组件 | 之前 (s01) | 之后 (s02) |\n|------|-----------|-----------|\n| Tool count | 1 (bash) | 6 (+read, write, edit, glob, generate_image) |\n| 工具执行 | 硬编码 `run_bash()` | TOOL_HANDLERS 查表分发 |\n| 路径安全 | 无 | safe_path 校验(仅 file tools) |\n| 循环 | `while True` + `stop_reason` | 与 s01 完全一致 |\n\n---\n\n## 试一下\n\n```sh\ncd learn-claude-code\npython s02_tool_use/code.py\n```\n\nThe optional `generate_image` tool also needs these values in `.env`. Use `https://api.minimaxi.com` as the host for China mainland.\n\n```sh\nMINIMAX_API_KEY=sk-xxx\nMINIMAX_API_HOST=https://api.minimax.io\n```\n\n试试这些 prompt:\n\n1. `Read the file README.md and tell me what this project is about`\n2. `Create a file called test.py that prints \"hello\", then read it back`\n3. `Find all Python files in this directory`\n4. `Read both README.md and requirements.txt, then create a summary file`\n5. `Generate a 16:9 image of a coding workspace at sunrise`\n\n观察重点:模型什么时候只调一个工具,什么时候一次调多个?多个工具调用的顺序和结果是否正确?\n\n---\n\n## 接下来\n\nThe Agent now has 6 specialized tools. File tools are protected by `safe_path`, but bash remains unrestricted: `rm -rf /` still runs.\n\ns03 Permission → 在工具执行之前加一道门:这个操作安全吗?需要用户批准吗?\n\n
\n深入 CC 源码\n\n> 以下基于 CC 源码 `Tool.ts`、`tools.ts`、`toolOrchestration.ts`、`toolExecution.ts`、`StreamingToolExecutor.ts` 的核查。\n\n### 一、工具定义方式\n\n**教学版**:`TOOLS` 数组 + `TOOL_HANDLERS` 字典。定义和实现分开。\n**CC**:每个工具是 `buildTool()` 创建的独立对象,包含 schema、验证、权限、执行。`getAllBaseTools()` 汇总所有工具。\n\n教学版的分离方式对教学更清晰——读者一眼看到\"加一个工具 = 两条定义\"。\n\n### 二、并发安全判断:isConcurrencySafe()\n\n![Tool Concurrency](/course-assets/s02_tool_use/concurrency-comparison.svg)\n\n教学版按原始顺序逐个执行,不做并发。CC 用 `isConcurrencySafe(input)` 判断能否并发——注意这不是简单的\"只读 vs 写\",而是按具体输入判断:\n\n| | isReadOnly | isConcurrencySafe |\n|---|---|---|\n| FileRead | true | true |\n| Glob | true | true |\n| Bash `ls` | true | **true** ← 关键差异 |\n| Bash `rm` | false | false |\n| TaskCreate | false | **true** ← 改状态但可并发(TaskCreate 在 s12 介绍) |\n\nCC 的 Bash tool 的 `isConcurrencySafe` 等于 `isReadOnly`——只读命令可并发,写命令不可。TaskCreate 虽然改了任务文件,但每次都写不同的文件,所以可以并发。\n\n### 三、分区算法\n\nCC 的 `partitionToolCalls()`(`toolOrchestration.ts:91-115`)不是分两组,而是把工具调用**按连续块分批**:\n\n```\n[read A, read B, glob *.py, bash \"rm x\", read C]\n → batch1(并发): [read A, read B, glob *.py]\n → batch2(串行): [bash \"rm x\"]\n → batch3(并发): [read C]\n```\n\n并发安全的连续块编入同一个 batch,batch 内真正并发执行(`toolOrchestration.ts:152-176`,有并发上限)。遇到非并发安全的就开新 batch 串行执行。batch 之间严格顺序。\n\n### 四、验证管线\n\nCC 的每个工具调用经过严格的 5 步验证(`toolExecution.ts`):\n\n1. **Zod schema 验证**(`614-680`,教学版用 JSON Schema 替代):参数类型/结构检查\n2. **工具级 validateInput()**(`682-733`):参数值验证(如路径是否在工作区内)\n3. **PreToolUse hooks**(`800-862`,s04 详细介绍):钩子可以返回消息、修改输入、阻止执行\n4. **权限检查**(`921-931`,s03 的核心内容):canUseTool + checkPermissions → allow/deny/ask\n5. **执行 tool.call()**(`1207-1222`)\n\n教学版省略了 Zod(用 JSON Schema)、省略了 validateInput(用安全函数)、保留了权限检查和钩子概念。\n\n### 五、流式工具执行\n\nCC 的 `StreamingToolExecutor`(`StreamingToolExecutor.ts`)让工具在模型还在生成时就启动——不等模型说完。`read_file` 可能在模型还在输出\"我来分析\"的时候就跑完了。教学版不实现这个,目标和 s01 一致——概念清晰,不追求性能极致。\n\n### 六、工具结果持久化\n\n每个工具有一个 `maxResultSizeChars` 字段。结果超过这个值就落盘,模型看到的是预览 + 文件路径。FileRead 特殊——设为 `Infinity`,防止读文件的输出又被当成文件落盘。具体来说,如果 FileRead 的结果超过阈值被落盘,模型下次读那个落盘文件时又会触发落盘 → 无限循环(读文件 → 落盘 → 再读 → 再落盘 → ...)。\n\n
\n\n\n" }, { "version": "s02", "locale": "ja", "title": "s02: Tool Use — ツール一つ追加、一行追加だけ", - "content": "# s02: Tool Use — ツール一つ追加、一行追加だけ\n\ns01 → `s02` → [s03](/ja/s03) → s04 → ... → s20\n> *\"ツールを一つ追加、ハンドラを一つ追加\"* — ループはそのまま。新しいツールをディスパッチマップに登録するだけ。\n>\n> **Harness レイヤー**: ツールディスパッチ — モデルが触れる範囲を拡張。\n\n---\n\n## ツールは bash 一つだけ\n\ns01 の Agent には bash 一つのツールしかない。ファイルを読むには `cat`、書くには `echo \"...\" > file.py`、編集するには `sed`。\n\nモデルは「このファイルを読みたい」と考えながら、`cat path/to/file` と組み立てなければならない。翻訳の層が一つ増え、トークンを無駄にし、エラーも起きやすい。\n\n---\n\n## 概要:ツールディスパッチ\n\n![Tool Dispatch](/course-assets/s02_tool_use/tool-dispatch.ja.svg)\n\ns01 のループは完全に保持される(LLM 呼び出し、stop_reason 判定、メッセージ追加 — 一文字も変更なし)。唯一の変更点はツール実行の 1 行:`run_bash()` が `TOOL_HANDLERS[block.name]()` の検索ディスパッチに置き換わる。\n\nAgent にツールを追加するには、たった二つ:\n\n1. **ツールを定義**:`TOOLS` 配列に一条を追加\n2. **ハンドラを登録**:`TOOL_HANDLERS` 辞書に一つのマッピングを追加\n\n---\n\n## 1 つのツールから 6 つのツールへ\n\ns01 には bash だけだった:\n\n```python\nTOOLS = [{\"name\": \"bash\", ...}]\n\ndef run_bash(command): ...\n```\n\ns02 では 6 つに増え、各ツールは独立して定義される:\n\n```python\nTOOLS = [\n {\"name\": \"bash\", \"description\": \"Run a shell command.\", ...},\n {\"name\": \"read_file\", \"description\": \"Read file contents.\", ...},\n {\"name\": \"write_file\", \"description\": \"Write content to file.\", ...},\n {\"name\": \"edit_file\", \"description\": \"Replace text in file once.\", ...},\n {\"name\": \"glob\", \"description\": \"Find files by pattern.\", ...},\n {\"name\": \"generate_image\", \"description\": \"Generate an image from a text prompt.\", ...},\n]\n```\n\n各ツールには専用の実装関数がある:\n\n```python\ndef run_read(path, limit=None):\n lines = safe_path(path).read_text().splitlines()\n if limit:\n lines = lines[:limit]\n return \"\\n\".join(lines)\n\ndef run_write(path, content):\n safe_path(path).write_text(content)\n return f\"Wrote {len(content)} bytes to {path}\"\n\ndef run_edit(path, old_text, new_text):\n text = safe_path(path).read_text()\n if old_text not in text:\n return \"Error: text not found\"\n safe_path(path).write_text(text.replace(old_text, new_text, 1))\n return f\"Edited {path}\"\n\ndef run_glob(pattern):\n import glob as g\n return \"\\n\".join(g.glob(pattern, root_dir=WORKDIR))\n\ndef run_generate_image(prompt, aspect_ratio=\"1:1\"):\n ...\n```\n\n---\n\n## ツールディスパッチ\n\n```python\nTOOL_HANDLERS = {\n \"bash\": run_bash,\n \"read_file\": run_read,\n \"write_file\": run_write,\n \"edit_file\": run_edit,\n \"glob\": run_glob,\n \"generate_image\": run_generate_image,\n}\n\n# ループ内で変更されたのは一行だけ — ハードコードの run_bash から検索ディスパッチへ:\nfor block in response.content:\n if block.type == \"tool_use\":\n handler = TOOL_HANDLERS[block.name] # 検索\n output = handler(**block.input) # 呼び出し\n results.append(...)\n```\n\nツールの追加 = `TOOLS` 配列に一条 + `TOOL_HANDLERS` 辞書に一行。ループは変わらない。\n\n---\n\n## 複数のツール呼び出し\n\nモデルはよく一度に複数の tool_use を返す — 「a.py と b.py を読んで、全 .py ファイルを列挙して」。\n\n教育版は `response.content` の元の順序で一つずつ実行する。CC のやり方はより複雑:元の順序を保ったまま連続バッチに分割し、バッチ内の並列安全なツールを並行実行し、バッチ間は厳密に順次(付録を参照)。\n\n---\n\n## 速查\n\n| 概念 | 一言で |\n|------|--------|\n| TOOL_HANDLERS | ツール名 → ハンドラ関数の辞書。ツール追加 = マッピング一行追加 |\n| ツール定義 | モデルに「何ができるか」を伝える JSON schema |\n| 複数ツール呼び出し | モデルは一度に複数の tool_use を返す可能性がある。教育版は元の順序で一つずつ実行 |\n| ループ不変 | s01 の `while True` ループ — 一行も変更なし |\n\n---\n\n## s01 からの変更\n\n| コンポーネント | 変更前 (s01) | 変更後 (s02) |\n|--------------|-------------|-------------|\n| ツール数 | 1 (bash) | 6 (+read, write, edit, glob, generate_image) |\n| ツール実行 | ハードコード `run_bash()` | TOOL_HANDLERS 検索ディスパッチ |\n| パス安全性 | なし | safe_path 検証(file tools のみ) |\n| ループ | `while True` + `stop_reason` | s01 と完全に同一 |\n\n---\n\n## 試してみよう\n\n```sh\ncd learn-claude-code\npython s02_tool_use/code.py\n```\n\nオプションの `generate_image` ツールには、`.env` に以下の値も必要。中国本土では host を `https://api.minimaxi.com` に変更する。\n\n```sh\nMINIMAX_API_KEY=sk-xxx\nMINIMAX_API_HOST=https://api.minimax.io\n```\n\n以下のプロンプトを試してみよう:\n\n1. `Read the file README.md and tell me what this project is about`\n2. `Create a file called test.py that prints \"hello\", then read it back`\n3. `Find all Python files in this directory`\n4. `Read both README.md and requirements.txt, then create a summary file`\n5. `Generate a 16:9 image of a coding workspace at sunrise`\n\n観察のポイント:モデルがツールを一つだけ呼び出すときと、複数同時に呼び出すときの違い。複数のツール呼び出しは正しい順序で実行されているか?\n\n---\n\n## 次へ\n\nAgent は 6 つの専用ツールを持つようになった。file tools は `safe_path` で保護されるが、bash は制限なし — `rm -rf /` はまだ実行できる。\n\n→ s03 Permission:ツール実行前にゲートを追加 — この操作は安全か? ユーザーの承認が必要か?\n\n
\nCC ソースコードを深掘り\n\n> 以下は CC ソースコード `Tool.ts`、`tools.ts`、`toolOrchestration.ts`、`toolExecution.ts`、`StreamingToolExecutor.ts` の検証に基づく。\n\n### 一、ツール定義方式\n\n**教育版**:`TOOLS` 配列 + `TOOL_HANDLERS` 辞書。定義と実装が分離。\n**CC**:各ツールは `buildTool()` で作成された独立オブジェクトで、schema、バリデーション、権限、実行を含む。`getAllBaseTools()` が全ツールを集約。\n\n教育版の分離方式は教学に適している — 読者は「ツール追加 = 二つの定義」と一目で分かる。\n\n### 二、並列安全性:isConcurrencySafe()\n\n![Tool Concurrency](/course-assets/s02_tool_use/concurrency-comparison.ja.svg)\n\n教育版は元の順序で一つずつ実行し、並列処理は行わない。CC は `isConcurrencySafe(input)` で並列可否を判断する — これは単なる「読み取り専用 vs 書き込み」ではなく、具体的な入力で判断する:\n\n| | isReadOnly | isConcurrencySafe |\n|---|---|---|\n| FileRead | true | true |\n| Glob | true | true |\n| Bash `ls` | true | **true** ← 重要な違い |\n| Bash `rm` | false | false |\n| TaskCreate | false | **true** ← 状態変更するが並列可能(s12 で紹介) |\n\nCC の Bash ツールの `isConcurrencySafe` は `isReadOnly` と同じ — 読み取り専用コマンドは並列可能、書き込みコマンドは不可。TaskCreate はタスクファイルを変更するが、毎回異なるファイルに書き込むため並列可能。\n\n### 三、パーティションアルゴリズム\n\nCC の `partitionToolCalls()`(`toolOrchestration.ts:91-115`)は二つのグループに分けるのではなく、ツール呼び出しを**連続ブロックごとにバッチ化**する:\n\n```\n[read A, read B, glob *.py, bash \"rm x\", read C]\n → batch1(並列): [read A, read B, glob *.py]\n → batch2(直列): [bash \"rm x\"]\n → batch3(並列): [read C]\n```\n\n連続する並列安全な呼び出しを同じバッチにまとめ、真の並列実行を行う(`toolOrchestration.ts:152-176`、並列数上限あり)。非並列安全な呼び出しに遭遇すると新しいバッチを開始して直列実行。バッチ間は厳密に順次。\n\n### 四、バリデーションパイプライン\n\nCC の各ツール呼び出しは厳格な 5 段階のバリデーションを経る(`toolExecution.ts`):\n\n1. **Zod schema バリデーション**(`614-680`、教育版は JSON Schema で代替):パラメータの型/構造チェック\n2. **ツールレベル validateInput()**(`682-733`):パラメータ値の検証(例:パスが作業ディレクトリ内か)\n3. **PreToolUse フック**(`800-862`、s04 で詳解):フックはメッセージの返却、入力の変更、実行のブロックが可能\n4. **権限チェック**(`921-931`、s03 の核心):canUseTool + checkPermissions → allow/deny/ask\n5. **tool.call() の実行**(`1207-1222`)\n\n教育版は Zod を省略(JSON Schema を使用)、validateInput を省略(安全関数を使用)、権限チェックとフック概念は保持。\n\n### 五、ストリーミングツール実行\n\nCC の `StreamingToolExecutor`(`StreamingToolExecutor.ts`)はモデルがまだ生成中にツールを起動する — モデルの完了を待たない。`read_file` はモデルが「分析します」と出力中に完了するかもしれない。教育版はこれを実装しない。s01 と同じ目標 — 概念の明確さ、極限のパフォーマンスではない。\n\n### 六、ツール結果の永続化\n\n各ツールには `maxResultSizeChars` フィールドがある。この閾値を超える結果はディスクに保存され、モデルにはプレビュー + ファイルパスが表示される。FileRead は特殊 — `Infinity` に設定され、ファイル読み出し結果の再永続化を防ぐ。具体的には、FileRead の結果が閾値を超えて永続化されると、モデルがその永続化ファイルを次に読むときにまた永続化がトリガーされ → 無限ループ(ファイル読む → 永続化 → 再読み → 再永続化 → ...)になる。\n\n
\n\n\n" + "content": "# s02: Tool Use — ツール一つ追加、一行追加だけ\n\ns01 → `s02` → [s03](/ja/s03) → s04 → ... → s20\n> *\"ツールを一つ追加、ハンドラを一つ追加\"* — ループはそのまま。新しいツールをディスパッチマップに登録するだけ。\n>\n> **Harness レイヤー**: ツールディスパッチ — モデルが触れる範囲を拡張。\n\n---\n\n## ツールは bash 一つだけ\n\ns01 の Agent には bash 一つのツールしかない。ファイルを読むには `cat`、書くには `echo \"...\" > file.py`、編集するには `sed`。\n\nモデルは「このファイルを読みたい」と考えながら、`cat path/to/file` と組み立てなければならない。翻訳の層が一つ増え、トークンを無駄にし、エラーも起きやすい。\n\n---\n\n## 概要:ツールディスパッチ\n\n![Tool Dispatch](/course-assets/s02_tool_use/tool-dispatch.ja.svg)\n\ns01 のループは完全に保持される(LLM 呼び出し、stop_reason 判定、メッセージ追加 — 一文字も変更なし)。唯一の変更点はツール実行の 1 行:`run_bash()` が `TOOL_HANDLERS[block.name]()` の検索ディスパッチに置き換わる。\n\nAgent にツールを追加するには、たった二つ:\n\n1. **ツールを定義**:`TOOLS` 配列に一条を追加\n2. **ハンドラを登録**:`TOOL_HANDLERS` 辞書に一つのマッピングを追加\n\n---\n\n## From 1 Tool to 6 Tools\n\ns01 には bash だけだった:\n\n```python\nTOOLS = [{\"name\": \"bash\", ...}]\n\ndef run_bash(command): ...\n```\n\ns02 expands to 6 tools, each independently defined:\n\n```python\nTOOLS = [\n {\"name\": \"bash\", \"description\": \"Run a shell command.\", ...},\n {\"name\": \"read_file\", \"description\": \"Read file contents.\", ...},\n {\"name\": \"write_file\", \"description\": \"Write content to file.\", ...},\n {\"name\": \"edit_file\", \"description\": \"Replace text in file once.\", ...},\n {\"name\": \"glob\", \"description\": \"Find files by pattern.\", ...},\n {\"name\": \"generate_image\", \"description\": \"Generate an image from a text prompt.\", ...},\n]\n```\n\n各ツールには専用の実装関数がある:\n\n```python\ndef run_read(path, limit=None):\n lines = safe_path(path).read_text().splitlines()\n if limit:\n lines = lines[:limit]\n return \"\\n\".join(lines)\n\ndef run_write(path, content):\n safe_path(path).write_text(content)\n return f\"Wrote {len(content)} bytes to {path}\"\n\ndef run_edit(path, old_text, new_text):\n text = safe_path(path).read_text()\n if old_text not in text:\n return \"Error: text not found\"\n safe_path(path).write_text(text.replace(old_text, new_text, 1))\n return f\"Edited {path}\"\n\ndef run_glob(pattern):\n import glob as g\n return \"\\n\".join(g.glob(pattern, root_dir=WORKDIR))\n\ndef run_generate_image(prompt, aspect_ratio=\"1:1\"):\n ...\n```\n\n---\n\n## ツールディスパッチ\n\n```python\nTOOL_HANDLERS = {\n \"bash\": run_bash,\n \"read_file\": run_read,\n \"write_file\": run_write,\n \"edit_file\": run_edit,\n \"glob\": run_glob,\n \"generate_image\": run_generate_image,\n}\n\n# ループ内で変更されたのは一行だけ — ハードコードの run_bash から検索ディスパッチへ:\nfor block in response.content:\n if block.type == \"tool_use\":\n handler = TOOL_HANDLERS[block.name] # 検索\n output = handler(**block.input) # 呼び出し\n results.append(...)\n```\n\nツールの追加 = `TOOLS` 配列に一条 + `TOOL_HANDLERS` 辞書に一行。ループは変わらない。\n\n---\n\n## 複数のツール呼び出し\n\nモデルはよく一度に複数の tool_use を返す — 「a.py と b.py を読んで、全 .py ファイルを列挙して」。\n\n教育版は `response.content` の元の順序で一つずつ実行する。CC のやり方はより複雑:元の順序を保ったまま連続バッチに分割し、バッチ内の並列安全なツールを並行実行し、バッチ間は厳密に順次(付録を参照)。\n\n---\n\n## 速查\n\n| 概念 | 一言で |\n|------|--------|\n| TOOL_HANDLERS | ツール名 → ハンドラ関数の辞書。ツール追加 = マッピング一行追加 |\n| ツール定義 | モデルに「何ができるか」を伝える JSON schema |\n| 複数ツール呼び出し | モデルは一度に複数の tool_use を返す可能性がある。教育版は元の順序で一つずつ実行 |\n| ループ不変 | s01 の `while True` ループ — 一行も変更なし |\n\n---\n\n## s01 からの変更\n\n| コンポーネント | 変更前 (s01) | 変更後 (s02) |\n|--------------|-------------|-------------|\n| Tool count | 1 (bash) | 6 (+read, write, edit, glob, generate_image) |\n| ツール実行 | ハードコード `run_bash()` | TOOL_HANDLERS 検索ディスパッチ |\n| パス安全性 | なし | safe_path 検証(file tools のみ) |\n| ループ | `while True` + `stop_reason` | s01 と完全に同一 |\n\n---\n\n## 試してみよう\n\n```sh\ncd learn-claude-code\npython s02_tool_use/code.py\n```\n\nThe optional `generate_image` tool also needs these values in `.env`. Use `https://api.minimaxi.com` as the host for China mainland.\n\n```sh\nMINIMAX_API_KEY=sk-xxx\nMINIMAX_API_HOST=https://api.minimax.io\n```\n\n以下のプロンプトを試してみよう:\n\n1. `Read the file README.md and tell me what this project is about`\n2. `Create a file called test.py that prints \"hello\", then read it back`\n3. `Find all Python files in this directory`\n4. `Read both README.md and requirements.txt, then create a summary file`\n5. `Generate a 16:9 image of a coding workspace at sunrise`\n\n観察のポイント:モデルがツールを一つだけ呼び出すときと、複数同時に呼び出すときの違い。複数のツール呼び出しは正しい順序で実行されているか?\n\n---\n\n## 次へ\n\nThe Agent now has 6 specialized tools. File tools are protected by `safe_path`, but bash remains unrestricted: `rm -rf /` still runs.\n\n→ s03 Permission:ツール実行前にゲートを追加 — この操作は安全か? ユーザーの承認が必要か?\n\n
\nCC ソースコードを深掘り\n\n> 以下は CC ソースコード `Tool.ts`、`tools.ts`、`toolOrchestration.ts`、`toolExecution.ts`、`StreamingToolExecutor.ts` の検証に基づく。\n\n### 一、ツール定義方式\n\n**教育版**:`TOOLS` 配列 + `TOOL_HANDLERS` 辞書。定義と実装が分離。\n**CC**:各ツールは `buildTool()` で作成された独立オブジェクトで、schema、バリデーション、権限、実行を含む。`getAllBaseTools()` が全ツールを集約。\n\n教育版の分離方式は教学に適している — 読者は「ツール追加 = 二つの定義」と一目で分かる。\n\n### 二、並列安全性:isConcurrencySafe()\n\n![Tool Concurrency](/course-assets/s02_tool_use/concurrency-comparison.ja.svg)\n\n教育版は元の順序で一つずつ実行し、並列処理は行わない。CC は `isConcurrencySafe(input)` で並列可否を判断する — これは単なる「読み取り専用 vs 書き込み」ではなく、具体的な入力で判断する:\n\n| | isReadOnly | isConcurrencySafe |\n|---|---|---|\n| FileRead | true | true |\n| Glob | true | true |\n| Bash `ls` | true | **true** ← 重要な違い |\n| Bash `rm` | false | false |\n| TaskCreate | false | **true** ← 状態変更するが並列可能(s12 で紹介) |\n\nCC の Bash ツールの `isConcurrencySafe` は `isReadOnly` と同じ — 読み取り専用コマンドは並列可能、書き込みコマンドは不可。TaskCreate はタスクファイルを変更するが、毎回異なるファイルに書き込むため並列可能。\n\n### 三、パーティションアルゴリズム\n\nCC の `partitionToolCalls()`(`toolOrchestration.ts:91-115`)は二つのグループに分けるのではなく、ツール呼び出しを**連続ブロックごとにバッチ化**する:\n\n```\n[read A, read B, glob *.py, bash \"rm x\", read C]\n → batch1(並列): [read A, read B, glob *.py]\n → batch2(直列): [bash \"rm x\"]\n → batch3(並列): [read C]\n```\n\n連続する並列安全な呼び出しを同じバッチにまとめ、真の並列実行を行う(`toolOrchestration.ts:152-176`、並列数上限あり)。非並列安全な呼び出しに遭遇すると新しいバッチを開始して直列実行。バッチ間は厳密に順次。\n\n### 四、バリデーションパイプライン\n\nCC の各ツール呼び出しは厳格な 5 段階のバリデーションを経る(`toolExecution.ts`):\n\n1. **Zod schema バリデーション**(`614-680`、教育版は JSON Schema で代替):パラメータの型/構造チェック\n2. **ツールレベル validateInput()**(`682-733`):パラメータ値の検証(例:パスが作業ディレクトリ内か)\n3. **PreToolUse フック**(`800-862`、s04 で詳解):フックはメッセージの返却、入力の変更、実行のブロックが可能\n4. **権限チェック**(`921-931`、s03 の核心):canUseTool + checkPermissions → allow/deny/ask\n5. **tool.call() の実行**(`1207-1222`)\n\n教育版は Zod を省略(JSON Schema を使用)、validateInput を省略(安全関数を使用)、権限チェックとフック概念は保持。\n\n### 五、ストリーミングツール実行\n\nCC の `StreamingToolExecutor`(`StreamingToolExecutor.ts`)はモデルがまだ生成中にツールを起動する — モデルの完了を待たない。`read_file` はモデルが「分析します」と出力中に完了するかもしれない。教育版はこれを実装しない。s01 と同じ目標 — 概念の明確さ、極限のパフォーマンスではない。\n\n### 六、ツール結果の永続化\n\n各ツールには `maxResultSizeChars` フィールドがある。この閾値を超える結果はディスクに保存され、モデルにはプレビュー + ファイルパスが表示される。FileRead は特殊 — `Infinity` に設定され、ファイル読み出し結果の再永続化を防ぐ。具体的には、FileRead の結果が閾値を超えて永続化されると、モデルがその永続化ファイルを次に読むときにまた永続化がトリガーされ → 無限ループ(ファイル読む → 永続化 → 再読み → 再永続化 → ...)になる。\n\n
\n\n\n" }, { "version": "s03", diff --git a/web/src/data/generated/versions.json b/web/src/data/generated/versions.json index 4bb60d665..207a320c9 100644 --- a/web/src/data/generated/versions.json +++ b/web/src/data/generated/versions.json @@ -103,7 +103,7 @@ } ], "layer": "tools", - "source": "#!/usr/bin/env python3\n\"\"\"\ns02: Tool Use — 在 s01 基础上新增 5 个工具 + 分发映射。\n\n运行: python s02_tool_use/code.py\n需要: pip install anthropic python-dotenv + .env 中配置 ANTHROPIC_API_KEY\n\n本文件 = s01 的全部代码 + 以下新增:\n + run_read / run_write / run_edit / run_glob / run_generate_image 五个工具实现\n + TOOL_HANDLERS 分发映射(替代 s01 中硬编码的 run_bash 调用)\n + safe_path 路径安全校验\n\n循环本身(agent_loop)与 s01 完全一致。\n\"\"\"\n\nimport os, subprocess, json, urllib.request\nfrom pathlib import Path\n\ntry:\n import readline\n readline.parse_and_bind('set bind-tty-special-chars off')\n readline.parse_and_bind('set input-meta on')\n readline.parse_and_bind('set output-meta on')\n readline.parse_and_bind('set convert-meta off')\nexcept ImportError:\n pass\n\nfrom anthropic import Anthropic\nfrom dotenv import load_dotenv\n\nload_dotenv(override=True)\nif os.getenv(\"ANTHROPIC_BASE_URL\"):\n os.environ.pop(\"ANTHROPIC_AUTH_TOKEN\", None)\n\nWORKDIR = Path.cwd()\nclient = Anthropic(base_url=os.getenv(\"ANTHROPIC_BASE_URL\"))\nMODEL = os.environ[\"MODEL_ID\"]\n\nSYSTEM = f\"You are a coding agent at {WORKDIR}. Use tools to solve tasks. Act, don't explain.\"\n\n\n# ═══════════════════════════════════════════════════════════\n# FROM s01 (unchanged)\n# ═══════════════════════════════════════════════════════════\n\ndef run_bash(command: str) -> str:\n dangerous = [\"rm -rf /\", \"sudo\", \"shutdown\", \"reboot\", \"> /dev/\"]\n if any(d in command for d in dangerous):\n return \"Error: Dangerous command blocked\"\n try:\n r = subprocess.run(command, shell=True, cwd=WORKDIR,\n capture_output=True, text=True,\n encoding=\"utf-8\", errors=\"replace\", timeout=120)\n out = (r.stdout + r.stderr).strip()\n return out[:50000] if out else \"(no output)\"\n except subprocess.TimeoutExpired:\n return \"Error: Timeout (120s)\"\n except (FileNotFoundError, OSError) as e:\n return f\"Error: {e}\"\n\n\n# ═══════════════════════════════════════════════════════════\n# NEW in s02: 5 个新工具\n# ═══════════════════════════════════════════════════════════\n\ndef safe_path(p: str) -> Path:\n path = (WORKDIR / p).resolve()\n if not path.is_relative_to(WORKDIR):\n raise ValueError(f\"Path escapes workspace: {p}\")\n return path\n\n\ndef run_read(path: str, limit: int | None = None) -> str:\n try:\n lines = safe_path(path).read_text().splitlines()\n if limit and limit < len(lines):\n lines = lines[:limit] + [f\"... ({len(lines) - limit} more lines)\"]\n return \"\\n\".join(lines)\n except Exception as e:\n return f\"Error: {e}\"\n\n\ndef run_write(path: str, content: str) -> str:\n try:\n file_path = safe_path(path)\n file_path.parent.mkdir(parents=True, exist_ok=True)\n file_path.write_text(content)\n return f\"Wrote {len(content)} bytes to {path}\"\n except Exception as e:\n return f\"Error: {e}\"\n\n\ndef run_edit(path: str, old_text: str, new_text: str) -> str:\n try:\n file_path = safe_path(path)\n text = file_path.read_text()\n if old_text not in text:\n return f\"Error: text not found in {path}\"\n file_path.write_text(text.replace(old_text, new_text, 1))\n return f\"Edited {path}\"\n except Exception as e:\n return f\"Error: {e}\"\n\n\ndef run_glob(pattern: str) -> str:\n import glob as g\n try:\n results = []\n for match in g.glob(pattern, root_dir=WORKDIR):\n if (WORKDIR / match).resolve().is_relative_to(WORKDIR):\n results.append(match)\n return \"\\n\".join(results) if results else \"(no matches)\"\n except Exception as e:\n return f\"Error: {e}\"\n\n\ndef run_generate_image(prompt: str, aspect_ratio: str = \"1:1\") -> str:\n \"\"\"Generate an image with MiniMax and return the image URL.\"\"\"\n api_key = os.getenv(\"MINIMAX_API_KEY\")\n if not api_key:\n return \"Error: set MINIMAX_API_KEY to use image generation\"\n api_host = (os.getenv(\"MINIMAX_API_HOST\") or \"https://api.minimax.io\").rstrip(\"/\")\n\n payload = {\n \"model\": \"image-01\",\n \"prompt\": prompt,\n \"aspect_ratio\": aspect_ratio,\n \"n\": 1,\n \"response_format\": \"url\",\n }\n req = urllib.request.Request(\n f\"{api_host}/v1/image_generation\",\n data=json.dumps(payload).encode(\"utf-8\"),\n headers={\n \"Authorization\": f\"Bearer {api_key}\",\n \"Content-Type\": \"application/json\",\n },\n method=\"POST\",\n )\n try:\n with urllib.request.urlopen(req, timeout=120) as resp:\n data = json.loads(resp.read().decode(\"utf-8\"))\n status = data.get(\"base_resp\", {}).get(\"status_code\")\n if status not in (None, 0):\n msg = data.get(\"base_resp\", {}).get(\"status_msg\", \"unknown error\")\n return f\"Error: MiniMax image generation failed ({status}): {msg}\"\n urls = data.get(\"data\", {}).get(\"image_urls\") or []\n return urls[0] if urls else f\"Error: no image URL returned: {data}\"\n except Exception as e:\n return f\"Error: {e}\"\n\n\n# ═══════════════════════════════════════════════════════════\n# NEW in s02: 工具定义(s01 只有一个 bash,现在扩展到 6 个)\n# ═══════════════════════════════════════════════════════════\n\nTOOLS = [\n {\"name\": \"bash\", \"description\": \"Run a shell command.\",\n \"input_schema\": {\"type\": \"object\", \"properties\": {\"command\": {\"type\": \"string\"}}, \"required\": [\"command\"]}},\n {\"name\": \"read_file\", \"description\": \"Read file contents.\",\n \"input_schema\": {\"type\": \"object\", \"properties\": {\"path\": {\"type\": \"string\"}, \"limit\": {\"type\": \"integer\"}}, \"required\": [\"path\"]}},\n {\"name\": \"write_file\", \"description\": \"Write content to a file.\",\n \"input_schema\": {\"type\": \"object\", \"properties\": {\"path\": {\"type\": \"string\"}, \"content\": {\"type\": \"string\"}}, \"required\": [\"path\", \"content\"]}},\n {\"name\": \"edit_file\", \"description\": \"Replace exact text in a file once.\",\n \"input_schema\": {\"type\": \"object\", \"properties\": {\"path\": {\"type\": \"string\"}, \"old_text\": {\"type\": \"string\"}, \"new_text\": {\"type\": \"string\"}}, \"required\": [\"path\", \"old_text\", \"new_text\"]}},\n {\"name\": \"glob\", \"description\": \"Find files matching a glob pattern.\",\n \"input_schema\": {\"type\": \"object\", \"properties\": {\"pattern\": {\"type\": \"string\"}}, \"required\": [\"pattern\"]}},\n {\"name\": \"generate_image\", \"description\": \"Generate an image from a text prompt using MiniMax.\",\n \"input_schema\": {\"type\": \"object\", \"properties\": {\"prompt\": {\"type\": \"string\"}, \"aspect_ratio\": {\"type\": \"string\", \"enum\": [\"1:1\", \"16:9\", \"4:3\", \"3:2\", \"2:3\", \"3:4\", \"9:16\", \"21:9\"]}}, \"required\": [\"prompt\"]}},\n]\n\n# ═══════════════════════════════════════════════════════════\n# NEW in s02: 工具分发映射(s01 是硬编码 run_bash,现在改为查表)\n# ═══════════════════════════════════════════════════════════\n\nTOOL_HANDLERS = {\n \"bash\": run_bash, \"read_file\": run_read, \"write_file\": run_write,\n \"edit_file\": run_edit, \"glob\": run_glob, \"generate_image\": run_generate_image,\n}\n\n\n# ═══════════════════════════════════════════════════════════\n# agent_loop — 与 s01 结构完全一致,只改了工具执行那部分\n# s01: output = run_bash(block.input[\"command\"])\n# s02: output = TOOL_HANDLERS[block.name](**block.input)\n# ═══════════════════════════════════════════════════════════\n\ndef agent_loop(messages: list):\n while True:\n response = client.messages.create(\n model=MODEL, system=SYSTEM, messages=messages,\n tools=TOOLS, max_tokens=8000,\n )\n messages.append({\"role\": \"assistant\", \"content\": response.content})\n\n if response.stop_reason != \"tool_use\":\n return\n\n results = []\n for block in response.content:\n if block.type == \"tool_use\":\n print(f\"\\033[33m> {block.name}\\033[0m\")\n handler = TOOL_HANDLERS.get(block.name)\n output = handler(**block.input) if handler else f\"Unknown: {block.name}\"\n print(str(output)[:200])\n results.append({\"type\": \"tool_result\", \"tool_use_id\": block.id, \"content\": output})\n\n messages.append({\"role\": \"user\", \"content\": results})\n\n\nif __name__ == \"__main__\":\n print(\"s02: Tool Use — 在 s01 基础上加了 5 个工具\")\n print(\"输入问题,回车发送。输入 q 退出。\\n\")\n\n history = []\n while True:\n try:\n query = input(\"\\033[36ms02 >> \\033[0m\")\n except (EOFError, KeyboardInterrupt):\n break\n if query.strip().lower() in (\"q\", \"exit\", \"\"):\n break\n history.append({\"role\": \"user\", \"content\": query})\n agent_loop(history)\n for block in history[-1][\"content\"]:\n if getattr(block, \"type\", None) == \"text\":\n print(block.text)\n print()\n", + "source": "#!/usr/bin/env python3\n\"\"\"\ns02: Tool Use - adds five tools and a dispatch map on top of s01.\n\n运行: python s02_tool_use/code.py\n需要: pip install anthropic python-dotenv + .env 中配置 ANTHROPIC_API_KEY\n\n本文件 = s01 的全部代码 + 以下新增:\n + Five tool implementations: run_read / run_write / run_edit / run_glob / run_generate_image\n + TOOL_HANDLERS 分发映射(替代 s01 中硬编码的 run_bash 调用)\n + safe_path 路径安全校验\n\n循环本身(agent_loop)与 s01 完全一致。\n\"\"\"\n\nimport os, subprocess, json, urllib.request\nfrom pathlib import Path\n\ntry:\n import readline\n readline.parse_and_bind('set bind-tty-special-chars off')\n readline.parse_and_bind('set input-meta on')\n readline.parse_and_bind('set output-meta on')\n readline.parse_and_bind('set convert-meta off')\nexcept ImportError:\n pass\n\nfrom anthropic import Anthropic\nfrom dotenv import load_dotenv\n\nload_dotenv(override=True)\nif os.getenv(\"ANTHROPIC_BASE_URL\"):\n os.environ.pop(\"ANTHROPIC_AUTH_TOKEN\", None)\n\nWORKDIR = Path.cwd()\nclient = Anthropic(base_url=os.getenv(\"ANTHROPIC_BASE_URL\"))\nMODEL = os.environ[\"MODEL_ID\"]\n\nSYSTEM = f\"You are a coding agent at {WORKDIR}. Use tools to solve tasks. Act, don't explain.\"\n\n\n# ═══════════════════════════════════════════════════════════\n# FROM s01 (unchanged)\n# ═══════════════════════════════════════════════════════════\n\ndef run_bash(command: str) -> str:\n dangerous = [\"rm -rf /\", \"sudo\", \"shutdown\", \"reboot\", \"> /dev/\"]\n if any(d in command for d in dangerous):\n return \"Error: Dangerous command blocked\"\n try:\n r = subprocess.run(command, shell=True, cwd=WORKDIR,\n capture_output=True, text=True,\n encoding=\"utf-8\", errors=\"replace\", timeout=120)\n out = (r.stdout + r.stderr).strip()\n return out[:50000] if out else \"(no output)\"\n except subprocess.TimeoutExpired:\n return \"Error: Timeout (120s)\"\n except (FileNotFoundError, OSError) as e:\n return f\"Error: {e}\"\n\n\n# ═══════════════════════════════════════════════════════════\n# NEW in s02: 5 tools\n# ═══════════════════════════════════════════════════════════\n\ndef safe_path(p: str) -> Path:\n path = (WORKDIR / p).resolve()\n if not path.is_relative_to(WORKDIR):\n raise ValueError(f\"Path escapes workspace: {p}\")\n return path\n\n\ndef run_read(path: str, limit: int | None = None) -> str:\n try:\n lines = safe_path(path).read_text().splitlines()\n if limit and limit < len(lines):\n lines = lines[:limit] + [f\"... ({len(lines) - limit} more lines)\"]\n return \"\\n\".join(lines)\n except Exception as e:\n return f\"Error: {e}\"\n\n\ndef run_write(path: str, content: str) -> str:\n try:\n file_path = safe_path(path)\n file_path.parent.mkdir(parents=True, exist_ok=True)\n file_path.write_text(content)\n return f\"Wrote {len(content)} bytes to {path}\"\n except Exception as e:\n return f\"Error: {e}\"\n\n\ndef run_edit(path: str, old_text: str, new_text: str) -> str:\n try:\n file_path = safe_path(path)\n text = file_path.read_text()\n if old_text not in text:\n return f\"Error: text not found in {path}\"\n file_path.write_text(text.replace(old_text, new_text, 1))\n return f\"Edited {path}\"\n except Exception as e:\n return f\"Error: {e}\"\n\n\ndef run_glob(pattern: str) -> str:\n import glob as g\n try:\n results = []\n for match in g.glob(pattern, root_dir=WORKDIR):\n if (WORKDIR / match).resolve().is_relative_to(WORKDIR):\n results.append(match)\n return \"\\n\".join(results) if results else \"(no matches)\"\n except Exception as e:\n return f\"Error: {e}\"\n\n\ndef run_generate_image(prompt: str, aspect_ratio: str = \"1:1\") -> str:\n \"\"\"Generate an image with MiniMax and return the image URL.\"\"\"\n api_key = os.getenv(\"MINIMAX_API_KEY\")\n if not api_key:\n return \"Error: set MINIMAX_API_KEY to use image generation\"\n api_host = (os.getenv(\"MINIMAX_API_HOST\") or \"https://api.minimax.io\").rstrip(\"/\")\n\n payload = {\n \"model\": \"image-01\",\n \"prompt\": prompt,\n \"aspect_ratio\": aspect_ratio,\n \"n\": 1,\n \"response_format\": \"url\",\n }\n req = urllib.request.Request(\n f\"{api_host}/v1/image_generation\",\n data=json.dumps(payload).encode(\"utf-8\"),\n headers={\n \"Authorization\": f\"Bearer {api_key}\",\n \"Content-Type\": \"application/json\",\n },\n method=\"POST\",\n )\n try:\n with urllib.request.urlopen(req, timeout=120) as resp:\n data = json.loads(resp.read().decode(\"utf-8\"))\n status = data.get(\"base_resp\", {}).get(\"status_code\")\n if status not in (None, 0):\n msg = data.get(\"base_resp\", {}).get(\"status_msg\", \"unknown error\")\n return f\"Error: MiniMax image generation failed ({status}): {msg}\"\n urls = data.get(\"data\", {}).get(\"image_urls\") or []\n return urls[0] if urls else f\"Error: no image URL returned: {data}\"\n except Exception as e:\n return f\"Error: {e}\"\n\n\n# ═══════════════════════════════════════════════════════════\n# NEW in s02: tool definitions (s01 has only bash; s02 has 6 tools)\n# ═══════════════════════════════════════════════════════════\n\nTOOLS = [\n {\"name\": \"bash\", \"description\": \"Run a shell command.\",\n \"input_schema\": {\"type\": \"object\", \"properties\": {\"command\": {\"type\": \"string\"}}, \"required\": [\"command\"]}},\n {\"name\": \"read_file\", \"description\": \"Read file contents.\",\n \"input_schema\": {\"type\": \"object\", \"properties\": {\"path\": {\"type\": \"string\"}, \"limit\": {\"type\": \"integer\"}}, \"required\": [\"path\"]}},\n {\"name\": \"write_file\", \"description\": \"Write content to a file.\",\n \"input_schema\": {\"type\": \"object\", \"properties\": {\"path\": {\"type\": \"string\"}, \"content\": {\"type\": \"string\"}}, \"required\": [\"path\", \"content\"]}},\n {\"name\": \"edit_file\", \"description\": \"Replace exact text in a file once.\",\n \"input_schema\": {\"type\": \"object\", \"properties\": {\"path\": {\"type\": \"string\"}, \"old_text\": {\"type\": \"string\"}, \"new_text\": {\"type\": \"string\"}}, \"required\": [\"path\", \"old_text\", \"new_text\"]}},\n {\"name\": \"glob\", \"description\": \"Find files matching a glob pattern.\",\n \"input_schema\": {\"type\": \"object\", \"properties\": {\"pattern\": {\"type\": \"string\"}}, \"required\": [\"pattern\"]}},\n {\"name\": \"generate_image\", \"description\": \"Generate an image from a text prompt using MiniMax.\",\n \"input_schema\": {\"type\": \"object\", \"properties\": {\"prompt\": {\"type\": \"string\", \"maxLength\": 1500}, \"aspect_ratio\": {\"type\": \"string\", \"default\": \"1:1\", \"enum\": [\"1:1\", \"16:9\", \"4:3\", \"3:2\", \"2:3\", \"3:4\", \"9:16\", \"21:9\"]}}, \"required\": [\"prompt\"]}},\n]\n\n# ═══════════════════════════════════════════════════════════\n# NEW in s02: 工具分发映射(s01 是硬编码 run_bash,现在改为查表)\n# ═══════════════════════════════════════════════════════════\n\nTOOL_HANDLERS = {\n \"bash\": run_bash, \"read_file\": run_read, \"write_file\": run_write,\n \"edit_file\": run_edit, \"glob\": run_glob, \"generate_image\": run_generate_image,\n}\n\n\n# ═══════════════════════════════════════════════════════════\n# agent_loop — 与 s01 结构完全一致,只改了工具执行那部分\n# s01: output = run_bash(block.input[\"command\"])\n# s02: output = TOOL_HANDLERS[block.name](**block.input)\n# ═══════════════════════════════════════════════════════════\n\ndef agent_loop(messages: list):\n while True:\n response = client.messages.create(\n model=MODEL, system=SYSTEM, messages=messages,\n tools=TOOLS, max_tokens=8000,\n )\n messages.append({\"role\": \"assistant\", \"content\": response.content})\n\n if response.stop_reason != \"tool_use\":\n return\n\n results = []\n for block in response.content:\n if block.type == \"tool_use\":\n print(f\"\\033[33m> {block.name}\\033[0m\")\n handler = TOOL_HANDLERS.get(block.name)\n output = handler(**block.input) if handler else f\"Unknown: {block.name}\"\n print(str(output)[:200])\n results.append({\"type\": \"tool_result\", \"tool_use_id\": block.id, \"content\": output})\n\n messages.append({\"role\": \"user\", \"content\": results})\n\n\nif __name__ == \"__main__\":\n print(\"s02: Tool Use - adds 5 tools on top of s01\")\n print(\"输入问题,回车发送。输入 q 退出。\\n\")\n\n history = []\n while True:\n try:\n query = input(\"\\033[36ms02 >> \\033[0m\")\n except (EOFError, KeyboardInterrupt):\n break\n if query.strip().lower() in (\"q\", \"exit\", \"\"):\n break\n history.append({\"role\": \"user\", \"content\": query})\n agent_loop(history)\n for block in history[-1][\"content\"]:\n if getattr(block, \"type\", None) == \"text\":\n print(block.text)\n print()\n", "images": [ { "src": "/course-assets/s02_tool_use/concurrency-comparison.svg", From 81d211f4565c0ed82221cd55c960f9496ebda2af Mon Sep 17 00:00:00 2001 From: octo-patch <266937838+octo-patch@users.noreply.github.com> Date: Mon, 13 Jul 2026 14:56:17 +0800 Subject: [PATCH 4/4] Keep generated artifacts out of PR diff --- .../s02_tool_use/tool-dispatch.en.svg | 15 ++++------- .../s02_tool_use/tool-dispatch.ja.svg | 15 ++++------- .../s02_tool_use/tool-dispatch.svg | 15 ++++------- web/src/data/generated/docs.json | 8 +++--- web/src/data/generated/versions.json | 27 +++++++------------ 5 files changed, 28 insertions(+), 52 deletions(-) diff --git a/web/public/course-assets/s02_tool_use/tool-dispatch.en.svg b/web/public/course-assets/s02_tool_use/tool-dispatch.en.svg index 31e7e94b5..6fd2e6667 100644 --- a/web/public/course-assets/s02_tool_use/tool-dispatch.en.svg +++ b/web/public/course-assets/s02_tool_use/tool-dispatch.en.svg @@ -90,14 +90,9 @@ → run_edit() - - glob - → run_glob() - - - - generate_image - → run_generate_image() + + glob + → run_glob() @@ -106,8 +101,8 @@ - s01 Preserved (loop unchanged) + s01 Preserved (loop, LLM, decision — completely unchanged) - s02 New (6 tools + dispatch mapping) + s02 New (5 tools + dispatch mapping) Only 1 line changed in the loop: run_bash() → TOOL_HANDLERS[block.name]() diff --git a/web/public/course-assets/s02_tool_use/tool-dispatch.ja.svg b/web/public/course-assets/s02_tool_use/tool-dispatch.ja.svg index f5aaaecaa..8971d06ef 100644 --- a/web/public/course-assets/s02_tool_use/tool-dispatch.ja.svg +++ b/web/public/course-assets/s02_tool_use/tool-dispatch.ja.svg @@ -90,14 +90,9 @@ → run_edit() - - glob - → run_glob() - - - - generate_image - → run_generate_image() + + glob + → run_glob() @@ -106,8 +101,8 @@ - s01 Preserved (loop unchanged) + s01 保持(ループ、LLM、判定 — 完全に不変) - s02 New (6 tools + dispatch mapping) + s02 新規(5 つのツール + ディスパッチマッピング) ループ内で変更されたのは 1 行だけ:run_bash() → TOOL_HANDLERS[block.name]() diff --git a/web/public/course-assets/s02_tool_use/tool-dispatch.svg b/web/public/course-assets/s02_tool_use/tool-dispatch.svg index a3e783cae..a6b16ce2a 100644 --- a/web/public/course-assets/s02_tool_use/tool-dispatch.svg +++ b/web/public/course-assets/s02_tool_use/tool-dispatch.svg @@ -90,14 +90,9 @@ → run_edit() - - glob - → run_glob() - - - - generate_image - → run_generate_image() + + glob + → run_glob() @@ -106,8 +101,8 @@ - s01 Preserved (loop unchanged) + s01 保留(循环、LLM、判断——完全不变) - s02 New (6 tools + dispatch mapping) + s02 新增(5 个工具 + 分发映射) 循环里只改了 1 行:run_bash() → TOOL_HANDLERS[block.name]() diff --git a/web/src/data/generated/docs.json b/web/src/data/generated/docs.json index 2ea9e312d..4e50b7380 100644 --- a/web/src/data/generated/docs.json +++ b/web/src/data/generated/docs.json @@ -21,19 +21,19 @@ "version": "s02", "locale": "en", "title": "s02: Tool Use — Add a Tool, Add Just One Line", - "content": "# s02: Tool Use — Add a Tool, Add Just One Line\n\ns01 → `s02` → [s03](/en/s03) → s04 → ... → s20\n> *\"Add a tool, add just one handler\"* — The loop stays the same. Register the new tool in the dispatch map and you're done.\n>\n> **Harness Layer**: Tool Dispatch — Expanding the model's reach.\n\n---\n\n## Only One Tool: Bash\n\nThe s01 Agent has only one tool: bash. To read a file, `cat`; to write, `echo \"...\" > file.py`; to edit, `sed`.\n\nThe model thinks \"read this file\" but has to spell out `cat path/to/file`. An extra layer of translation that wastes tokens and invites errors.\n\n---\n\n## Overview: Tool Dispatch\n\n![Tool Dispatch](/course-assets/s02_tool_use/tool-dispatch.en.svg)\n\nThe s01 loop is fully preserved (LLM call, stop_reason check, message append — not a single word changed). The only change is in that one line of tool execution: `run_bash()` is replaced with `TOOL_HANDLERS[block.name]()` dispatch lookup.\n\nAdding a tool to the Agent requires just two things:\n\n1. **Define the tool**: Add one entry to the `TOOLS` array\n2. **Register the handler**: Add one mapping in the `TOOL_HANDLERS` dict\n\n---\n\n## From 1 Tool to 6 Tools\n\ns01 had only bash:\n\n```python\nTOOLS = [{\"name\": \"bash\", ...}]\n\ndef run_bash(command): ...\n```\n\ns02 expands to 6 tools, each independently defined:\n\n```python\nTOOLS = [\n {\"name\": \"bash\", \"description\": \"Run a shell command.\", ...},\n {\"name\": \"read_file\", \"description\": \"Read file contents.\", ...},\n {\"name\": \"write_file\", \"description\": \"Write content to file.\", ...},\n {\"name\": \"edit_file\", \"description\": \"Replace text in file once.\", ...},\n {\"name\": \"glob\", \"description\": \"Find files by pattern.\", ...},\n {\"name\": \"generate_image\", \"description\": \"Generate an image from a text prompt.\", ...},\n]\n```\n\nEach tool has its own implementation function:\n\n```python\ndef run_read(path, limit=None):\n lines = safe_path(path).read_text().splitlines()\n if limit:\n lines = lines[:limit]\n return \"\\n\".join(lines)\n\ndef run_write(path, content):\n safe_path(path).write_text(content)\n return f\"Wrote {len(content)} bytes to {path}\"\n\ndef run_edit(path, old_text, new_text):\n text = safe_path(path).read_text()\n if old_text not in text:\n return \"Error: text not found\"\n safe_path(path).write_text(text.replace(old_text, new_text, 1))\n return f\"Edited {path}\"\n\ndef run_glob(pattern):\n import glob as g\n return \"\\n\".join(g.glob(pattern, root_dir=WORKDIR))\n\ndef run_generate_image(prompt, aspect_ratio=\"1:1\"):\n ...\n```\n\n---\n\n## Tool Dispatch\n\n```python\nTOOL_HANDLERS = {\n \"bash\": run_bash,\n \"read_file\": run_read,\n \"write_file\": run_write,\n \"edit_file\": run_edit,\n \"glob\": run_glob,\n \"generate_image\": run_generate_image,\n}\n\n# Only one line changed in the loop — from hardcoded run_bash to dispatch lookup:\nfor block in response.content:\n if block.type == \"tool_use\":\n handler = TOOL_HANDLERS[block.name] # lookup\n output = handler(**block.input) # call\n results.append(...)\n```\n\nAdding a tool = one entry in `TOOLS` array + one line in `TOOL_HANDLERS` dict. The loop stays the same.\n\n---\n\n## Multiple Tool Calls\n\nThe model often returns multiple tool_use calls at once — \"read a.py and b.py, then list all .py files\".\n\nThe teaching version executes them one by one in the original `response.content` order. CC's approach is more complex: it slices the original order into consecutive batches, where concurrency-safe tools within a batch run in parallel, and batches are strictly sequential (see appendix).\n\n---\n\n## Quick Reference\n\n| Concept | One-Liner |\n|---------|-----------|\n| TOOL_HANDLERS | Tool name → handler function dict. Add a tool = add one mapping line |\n| Tool Definition | JSON schema telling the model \"what I can do\" |\n| Multiple tool calls | Model may return multiple tool_use at once; teaching version executes them in original order |\n| Loop Unchanged | s01's `while True` loop — not a single line changed |\n\n---\n\n## Changes from s01\n\n| Component | Before (s01) | After (s02) |\n|-----------|-------------|-------------|\n| Tool count | 1 (bash) | 6 (+read, write, edit, glob, generate_image) |\n| Tool execution | Hardcoded `run_bash()` | TOOL_HANDLERS dispatch lookup |\n| Path safety | None | safe_path validation (file tools only) |\n| Loop | `while True` + `stop_reason` | Identical to s01 |\n\n---\n\n## Try It\n\n```sh\ncd learn-claude-code\npython s02_tool_use/code.py\n```\n\nThe optional `generate_image` tool also needs these values in `.env`. Use `https://api.minimaxi.com` as the host for China mainland.\n\n```sh\nMINIMAX_API_KEY=sk-xxx\nMINIMAX_API_HOST=https://api.minimax.io\n```\n\nTry these prompts:\n\n1. `Read the file README.md and tell me what this project is about`\n2. `Create a file called test.py that prints \"hello\", then read it back`\n3. `Find all Python files in this directory`\n4. `Read both README.md and requirements.txt, then create a summary file`\n5. `Generate a 16:9 image of a coding workspace at sunrise`\n\nWhat to watch for: When does the model call just one tool, and when does it call multiple at once? Are multiple tool calls executed in the correct order?\n\n---\n\n## What's Next\n\nThe Agent now has 6 specialized tools. File tools are protected by `safe_path`, but bash is unrestricted — `rm -rf /` still runs.\n\n→ s03 Permission: Add a gate before tool execution — is this operation safe? Does it need user approval?\n\n
\nDive into CC Source Code\n\n> The following is based on a review of CC source code `Tool.ts`, `tools.ts`, `toolOrchestration.ts`, `toolExecution.ts`, and `StreamingToolExecutor.ts`.\n\n### 1. Tool Definition Approach\n\n**Teaching version**: `TOOLS` array + `TOOL_HANDLERS` dict. Definition and implementation are separate.\n**CC**: Each tool is an independent object created by `buildTool()`, containing schema, validation, permissions, and execution. `getAllBaseTools()` aggregates all tools.\n\nThe teaching version's separation is clearer for teaching — readers immediately see \"add a tool = two definitions\".\n\n### 2. Concurrency Safety: isConcurrencySafe()\n\n![Tool Concurrency](/course-assets/s02_tool_use/concurrency-comparison.en.svg)\n\nThe teaching version executes tools one by one in original order, without concurrency. CC uses `isConcurrencySafe(input)` to determine concurrency — note this isn't simply \"read-only vs write\", but judges by specific input:\n\n| | isReadOnly | isConcurrencySafe |\n|---|---|---|\n| FileRead | true | true |\n| Glob | true | true |\n| Bash `ls` | true | **true** ← key difference |\n| Bash `rm` | false | false |\n| TaskCreate | false | **true** ← modifies state but can be concurrent (introduced in s12) |\n\nCC's Bash tool's `isConcurrencySafe` equals `isReadOnly` — read-only commands can be concurrent, write commands cannot. TaskCreate modifies task files, but each writes a different file, so it can be concurrent.\n\n### 3. Partition Algorithm\n\nCC's `partitionToolCalls()` (`toolOrchestration.ts:91-115`) doesn't split into two groups — it batches tool calls **by consecutive blocks**:\n\n```\n[read A, read B, glob *.py, bash \"rm x\", read C]\n → batch1(concurrent): [read A, read B, glob *.py]\n → batch2(serial): [bash \"rm x\"]\n → batch3(concurrent): [read C]\n```\n\nConsecutive concurrency-safe calls are grouped into the same batch for truly concurrent execution (`toolOrchestration.ts:152-176`, with a concurrency limit). When a non-concurrency-safe call is encountered, a new batch starts for serial execution. Batches are strictly sequential.\n\n### 4. Validation Pipeline\n\nEach tool call in CC goes through a strict 5-step validation (`toolExecution.ts`):\n\n1. **Zod schema validation** (`614-680`, teaching version uses JSON Schema): parameter type/structure check\n2. **Tool-level validateInput()** (`682-733`): parameter value validation (e.g., is the path within the working directory)\n3. **PreToolUse hooks** (`800-862`, covered in s04): hooks can return messages, modify input, or block execution\n4. **Permission check** (`921-931`, core topic of s03): canUseTool + checkPermissions → allow/deny/ask\n5. **Execute tool.call()** (`1207-1222`)\n\nThe teaching version omits Zod (uses JSON Schema), omits validateInput (uses safety functions), but preserves the permission check and hook concepts.\n\n### 5. Streaming Tool Execution\n\nCC's `StreamingToolExecutor` (`StreamingToolExecutor.ts`) starts tools while the model is still generating — no waiting for the model to finish. `read_file` might complete while the model is still outputting \"Let me analyze\". The teaching version doesn't implement this, consistent with s01's goal — conceptual clarity, not peak performance.\n\n### 6. Tool Result Persistence\n\nEach tool has a `maxResultSizeChars` field. Results exceeding this threshold are persisted to disk, and the model sees a preview + file path. FileRead is special — set to `Infinity`, preventing file read output from being persisted again. Specifically, if FileRead's result exceeds the threshold and gets persisted, the model's next read of that persisted file would trigger another persistence → infinite loop (read file → persist → re-read → re-persist → ...).\n\n
\n\n\n" + "content": "# s02: Tool Use — Add a Tool, Add Just One Line\n\ns01 → `s02` → [s03](/en/s03) → s04 → ... → s20\n> *\"Add a tool, add just one handler\"* — The loop stays the same. Register the new tool in the dispatch map and you're done.\n>\n> **Harness Layer**: Tool Dispatch — Expanding the model's reach.\n\n---\n\n## Only One Tool: Bash\n\nThe s01 Agent has only one tool: bash. To read a file, `cat`; to write, `echo \"...\" > file.py`; to edit, `sed`.\n\nThe model thinks \"read this file\" but has to spell out `cat path/to/file`. An extra layer of translation that wastes tokens and invites errors.\n\n---\n\n## Overview: Tool Dispatch\n\n![Tool Dispatch](/course-assets/s02_tool_use/tool-dispatch.en.svg)\n\nThe s01 loop is fully preserved (LLM call, stop_reason check, message append — not a single word changed). The only change is in that one line of tool execution: `run_bash()` is replaced with `TOOL_HANDLERS[block.name]()` dispatch lookup.\n\nAdding a tool to the Agent requires just two things:\n\n1. **Define the tool**: Add one entry to the `TOOLS` array\n2. **Register the handler**: Add one mapping in the `TOOL_HANDLERS` dict\n\n---\n\n## From 1 Tool to 5 Tools\n\ns01 had only bash:\n\n```python\nTOOLS = [{\"name\": \"bash\", ...}]\n\ndef run_bash(command): ...\n```\n\ns02 expands to 5 tools, each independently defined:\n\n```python\nTOOLS = [\n {\"name\": \"bash\", \"description\": \"Run a shell command.\", ...},\n {\"name\": \"read_file\", \"description\": \"Read file contents.\", ...},\n {\"name\": \"write_file\", \"description\": \"Write content to file.\", ...},\n {\"name\": \"edit_file\", \"description\": \"Replace text in file once.\", ...},\n {\"name\": \"glob\", \"description\": \"Find files by pattern.\", ...},\n]\n```\n\nEach tool has its own implementation function:\n\n```python\ndef run_read(path, limit=None):\n lines = safe_path(path).read_text().splitlines()\n if limit:\n lines = lines[:limit]\n return \"\\n\".join(lines)\n\ndef run_write(path, content):\n safe_path(path).write_text(content)\n return f\"Wrote {len(content)} bytes to {path}\"\n\ndef run_edit(path, old_text, new_text):\n text = safe_path(path).read_text()\n if old_text not in text:\n return \"Error: text not found\"\n safe_path(path).write_text(text.replace(old_text, new_text, 1))\n return f\"Edited {path}\"\n\ndef run_glob(pattern):\n import glob as g\n return \"\\n\".join(g.glob(pattern, root_dir=WORKDIR))\n```\n\n---\n\n## Tool Dispatch\n\n```python\nTOOL_HANDLERS = {\n \"bash\": run_bash,\n \"read_file\": run_read,\n \"write_file\": run_write,\n \"edit_file\": run_edit,\n \"glob\": run_glob,\n}\n\n# Only one line changed in the loop — from hardcoded run_bash to dispatch lookup:\nfor block in response.content:\n if block.type == \"tool_use\":\n handler = TOOL_HANDLERS[block.name] # lookup\n output = handler(**block.input) # call\n results.append(...)\n```\n\nAdding a tool = one entry in `TOOLS` array + one line in `TOOL_HANDLERS` dict. The loop stays the same.\n\n---\n\n## Multiple Tool Calls\n\nThe model often returns multiple tool_use calls at once — \"read a.py and b.py, then list all .py files\".\n\nThe teaching version executes them one by one in the original `response.content` order. CC's approach is more complex: it slices the original order into consecutive batches, where concurrency-safe tools within a batch run in parallel, and batches are strictly sequential (see appendix).\n\n---\n\n## Quick Reference\n\n| Concept | One-Liner |\n|---------|-----------|\n| TOOL_HANDLERS | Tool name → handler function dict. Add a tool = add one mapping line |\n| Tool Definition | JSON schema telling the model \"what I can do\" |\n| Multiple tool calls | Model may return multiple tool_use at once; teaching version executes them in original order |\n| Loop Unchanged | s01's `while True` loop — not a single line changed |\n\n---\n\n## Changes from s01\n\n| Component | Before (s01) | After (s02) |\n|-----------|-------------|-------------|\n| Tool count | 1 (bash) | 5 (+read, write, edit, glob) |\n| Tool execution | Hardcoded `run_bash()` | TOOL_HANDLERS dispatch lookup |\n| Path safety | None | safe_path validation (file tools only) |\n| Loop | `while True` + `stop_reason` | Identical to s01 |\n\n---\n\n## Try It\n\n```sh\ncd learn-claude-code\npython s02_tool_use/code.py\n```\n\nTry these prompts:\n\n1. `Read the file README.md and tell me what this project is about`\n2. `Create a file called test.py that prints \"hello\", then read it back`\n3. `Find all Python files in this directory`\n4. `Read both README.md and requirements.txt, then create a summary file`\n\nWhat to watch for: When does the model call just one tool, and when does it call multiple at once? Are multiple tool calls executed in the correct order?\n\n---\n\n## What's Next\n\nThe Agent now has 5 specialized tools. File tools are protected by `safe_path`, but bash is unrestricted — `rm -rf /` still runs.\n\n→ s03 Permission: Add a gate before tool execution — is this operation safe? Does it need user approval?\n\n
\nDive into CC Source Code\n\n> The following is based on a review of CC source code `Tool.ts`, `tools.ts`, `toolOrchestration.ts`, `toolExecution.ts`, and `StreamingToolExecutor.ts`.\n\n### 1. Tool Definition Approach\n\n**Teaching version**: `TOOLS` array + `TOOL_HANDLERS` dict. Definition and implementation are separate.\n**CC**: Each tool is an independent object created by `buildTool()`, containing schema, validation, permissions, and execution. `getAllBaseTools()` aggregates all tools.\n\nThe teaching version's separation is clearer for teaching — readers immediately see \"add a tool = two definitions\".\n\n### 2. Concurrency Safety: isConcurrencySafe()\n\n![Tool Concurrency](/course-assets/s02_tool_use/concurrency-comparison.en.svg)\n\nThe teaching version executes tools one by one in original order, without concurrency. CC uses `isConcurrencySafe(input)` to determine concurrency — note this isn't simply \"read-only vs write\", but judges by specific input:\n\n| | isReadOnly | isConcurrencySafe |\n|---|---|---|\n| FileRead | true | true |\n| Glob | true | true |\n| Bash `ls` | true | **true** ← key difference |\n| Bash `rm` | false | false |\n| TaskCreate | false | **true** ← modifies state but can be concurrent (introduced in s12) |\n\nCC's Bash tool's `isConcurrencySafe` equals `isReadOnly` — read-only commands can be concurrent, write commands cannot. TaskCreate modifies task files, but each writes a different file, so it can be concurrent.\n\n### 3. Partition Algorithm\n\nCC's `partitionToolCalls()` (`toolOrchestration.ts:91-115`) doesn't split into two groups — it batches tool calls **by consecutive blocks**:\n\n```\n[read A, read B, glob *.py, bash \"rm x\", read C]\n → batch1(concurrent): [read A, read B, glob *.py]\n → batch2(serial): [bash \"rm x\"]\n → batch3(concurrent): [read C]\n```\n\nConsecutive concurrency-safe calls are grouped into the same batch for truly concurrent execution (`toolOrchestration.ts:152-176`, with a concurrency limit). When a non-concurrency-safe call is encountered, a new batch starts for serial execution. Batches are strictly sequential.\n\n### 4. Validation Pipeline\n\nEach tool call in CC goes through a strict 5-step validation (`toolExecution.ts`):\n\n1. **Zod schema validation** (`614-680`, teaching version uses JSON Schema): parameter type/structure check\n2. **Tool-level validateInput()** (`682-733`): parameter value validation (e.g., is the path within the working directory)\n3. **PreToolUse hooks** (`800-862`, covered in s04): hooks can return messages, modify input, or block execution\n4. **Permission check** (`921-931`, core topic of s03): canUseTool + checkPermissions → allow/deny/ask\n5. **Execute tool.call()** (`1207-1222`)\n\nThe teaching version omits Zod (uses JSON Schema), omits validateInput (uses safety functions), but preserves the permission check and hook concepts.\n\n### 5. Streaming Tool Execution\n\nCC's `StreamingToolExecutor` (`StreamingToolExecutor.ts`) starts tools while the model is still generating — no waiting for the model to finish. `read_file` might complete while the model is still outputting \"Let me analyze\". The teaching version doesn't implement this, consistent with s01's goal — conceptual clarity, not peak performance.\n\n### 6. Tool Result Persistence\n\nEach tool has a `maxResultSizeChars` field. Results exceeding this threshold are persisted to disk, and the model sees a preview + file path. FileRead is special — set to `Infinity`, preventing file read output from being persisted again. Specifically, if FileRead's result exceeds the threshold and gets persisted, the model's next read of that persisted file would trigger another persistence → infinite loop (read file → persist → re-read → re-persist → ...).\n\n
\n\n\n" }, { "version": "s02", "locale": "zh", "title": "s02: Tool Use — 多加一个工具,只加一行", - "content": "# s02: Tool Use — 多加一个工具,只加一行\n\ns01 → `s02` → [s03](/zh/s03) → s04 → ... → s20\n> *\"加一个工具, 只加一个 handler\"* — 循环不用动, 新工具注册进 dispatch map 就行。\n>\n> **Harness 层**: 工具分发 — 扩展模型能触达的边界。\n\n---\n\n## 只有 bash 一个工具\n\ns01 的 Agent 只有一个 bash 工具。读文件要 `cat`,写文件要 `echo \"...\" > file.py`,改文件要 `sed`。\n\n模型想的是\"读这个文件\",却要拼出 `cat path/to/file`。多了一层翻译,浪费 token,还容易拼错。\n\n---\n\n## 全局视角:工具分发\n\n![Tool Dispatch](/course-assets/s02_tool_use/tool-dispatch.svg)\n\ns01 的循环完全保留(LLM 调用、stop_reason 判断、消息追加)。唯一的变动在工具执行那 1 行:`run_bash()` 替换为 `TOOL_HANDLERS[block.name]()` 查表分发。\n\n给 Agent 加一个工具只需要做两件事:\n\n1. **定义工具**:在 `TOOLS` 数组里加一条描述\n2. **注册处理函数**:在 `TOOL_HANDLERS` 字典里加一个映射\n\n---\n\n## From 1 Tool to 6 Tools\n\ns01 只有一个 bash:\n\n```python\nTOOLS = [{\"name\": \"bash\", ...}]\n\ndef run_bash(command): ...\n```\n\ns02 expands to 6 tools, each independently defined:\n\n```python\nTOOLS = [\n {\"name\": \"bash\", \"description\": \"Run a shell command.\", ...},\n {\"name\": \"read_file\", \"description\": \"Read file contents.\", ...},\n {\"name\": \"write_file\", \"description\": \"Write content to file.\", ...},\n {\"name\": \"edit_file\", \"description\": \"Replace text in file once.\", ...},\n {\"name\": \"glob\", \"description\": \"Find files by pattern.\", ...},\n {\"name\": \"generate_image\", \"description\": \"Generate an image from a text prompt.\", ...},\n]\n```\n\n每个工具有自己的实现函数:\n\n```python\ndef run_read(path, limit=None):\n lines = safe_path(path).read_text().splitlines()\n if limit:\n lines = lines[:limit]\n return \"\\n\".join(lines)\n\ndef run_write(path, content):\n safe_path(path).write_text(content)\n return f\"Wrote {len(content)} bytes to {path}\"\n\ndef run_edit(path, old_text, new_text):\n text = safe_path(path).read_text()\n if old_text not in text:\n return \"Error: text not found\"\n safe_path(path).write_text(text.replace(old_text, new_text, 1))\n return f\"Edited {path}\"\n\ndef run_glob(pattern):\n import glob as g\n return \"\\n\".join(g.glob(pattern, root_dir=WORKDIR))\n\ndef run_generate_image(prompt, aspect_ratio=\"1:1\"):\n ...\n```\n\n---\n\n## 工具分发\n\n```python\nTOOL_HANDLERS = {\n \"bash\": run_bash,\n \"read_file\": run_read,\n \"write_file\": run_write,\n \"edit_file\": run_edit,\n \"glob\": run_glob,\n \"generate_image\": run_generate_image,\n}\n\n# 循环里只改了一行——从硬编码 run_bash 变成查表:\nfor block in response.content:\n if block.type == \"tool_use\":\n handler = TOOL_HANDLERS[block.name] # 查表\n output = handler(**block.input) # 调用\n results.append(...)\n```\n\n加一个工具 = 在 `TOOLS` 数组加一条 + 在 `TOOL_HANDLERS` 字典加一行。循环不变。\n\n---\n\n## 多个工具调用\n\n模型经常一次返回多个 tool_use:\"读一下 a.py 和 b.py,然后列出所有 .py 文件\"。\n\n教学版按 `response.content` 原始顺序逐个执行。CC 的做法更复杂:按原始顺序切成连续 batch,batch 内并发安全的工具并行执行,batch 间严格顺序(见附录)。\n\n---\n\n## 速查\n\n| 概念 | 一句话 |\n|------|--------|\n| TOOL_HANDLERS | 工具名 → 处理函数的字典。加工具 = 加一行映射 |\n| 工具定义 | 告诉模型\"我能做什么\"的 JSON schema |\n| 多工具调用 | 模型可一次返回多个 tool_use,教学版按原始顺序逐个执行 |\n| 循环不变 | s01 的 `while True` 循环一行都没改 |\n\n---\n\n## 相对 s01 的变更\n\n| 组件 | 之前 (s01) | 之后 (s02) |\n|------|-----------|-----------|\n| Tool count | 1 (bash) | 6 (+read, write, edit, glob, generate_image) |\n| 工具执行 | 硬编码 `run_bash()` | TOOL_HANDLERS 查表分发 |\n| 路径安全 | 无 | safe_path 校验(仅 file tools) |\n| 循环 | `while True` + `stop_reason` | 与 s01 完全一致 |\n\n---\n\n## 试一下\n\n```sh\ncd learn-claude-code\npython s02_tool_use/code.py\n```\n\nThe optional `generate_image` tool also needs these values in `.env`. Use `https://api.minimaxi.com` as the host for China mainland.\n\n```sh\nMINIMAX_API_KEY=sk-xxx\nMINIMAX_API_HOST=https://api.minimax.io\n```\n\n试试这些 prompt:\n\n1. `Read the file README.md and tell me what this project is about`\n2. `Create a file called test.py that prints \"hello\", then read it back`\n3. `Find all Python files in this directory`\n4. `Read both README.md and requirements.txt, then create a summary file`\n5. `Generate a 16:9 image of a coding workspace at sunrise`\n\n观察重点:模型什么时候只调一个工具,什么时候一次调多个?多个工具调用的顺序和结果是否正确?\n\n---\n\n## 接下来\n\nThe Agent now has 6 specialized tools. File tools are protected by `safe_path`, but bash remains unrestricted: `rm -rf /` still runs.\n\ns03 Permission → 在工具执行之前加一道门:这个操作安全吗?需要用户批准吗?\n\n
\n深入 CC 源码\n\n> 以下基于 CC 源码 `Tool.ts`、`tools.ts`、`toolOrchestration.ts`、`toolExecution.ts`、`StreamingToolExecutor.ts` 的核查。\n\n### 一、工具定义方式\n\n**教学版**:`TOOLS` 数组 + `TOOL_HANDLERS` 字典。定义和实现分开。\n**CC**:每个工具是 `buildTool()` 创建的独立对象,包含 schema、验证、权限、执行。`getAllBaseTools()` 汇总所有工具。\n\n教学版的分离方式对教学更清晰——读者一眼看到\"加一个工具 = 两条定义\"。\n\n### 二、并发安全判断:isConcurrencySafe()\n\n![Tool Concurrency](/course-assets/s02_tool_use/concurrency-comparison.svg)\n\n教学版按原始顺序逐个执行,不做并发。CC 用 `isConcurrencySafe(input)` 判断能否并发——注意这不是简单的\"只读 vs 写\",而是按具体输入判断:\n\n| | isReadOnly | isConcurrencySafe |\n|---|---|---|\n| FileRead | true | true |\n| Glob | true | true |\n| Bash `ls` | true | **true** ← 关键差异 |\n| Bash `rm` | false | false |\n| TaskCreate | false | **true** ← 改状态但可并发(TaskCreate 在 s12 介绍) |\n\nCC 的 Bash tool 的 `isConcurrencySafe` 等于 `isReadOnly`——只读命令可并发,写命令不可。TaskCreate 虽然改了任务文件,但每次都写不同的文件,所以可以并发。\n\n### 三、分区算法\n\nCC 的 `partitionToolCalls()`(`toolOrchestration.ts:91-115`)不是分两组,而是把工具调用**按连续块分批**:\n\n```\n[read A, read B, glob *.py, bash \"rm x\", read C]\n → batch1(并发): [read A, read B, glob *.py]\n → batch2(串行): [bash \"rm x\"]\n → batch3(并发): [read C]\n```\n\n并发安全的连续块编入同一个 batch,batch 内真正并发执行(`toolOrchestration.ts:152-176`,有并发上限)。遇到非并发安全的就开新 batch 串行执行。batch 之间严格顺序。\n\n### 四、验证管线\n\nCC 的每个工具调用经过严格的 5 步验证(`toolExecution.ts`):\n\n1. **Zod schema 验证**(`614-680`,教学版用 JSON Schema 替代):参数类型/结构检查\n2. **工具级 validateInput()**(`682-733`):参数值验证(如路径是否在工作区内)\n3. **PreToolUse hooks**(`800-862`,s04 详细介绍):钩子可以返回消息、修改输入、阻止执行\n4. **权限检查**(`921-931`,s03 的核心内容):canUseTool + checkPermissions → allow/deny/ask\n5. **执行 tool.call()**(`1207-1222`)\n\n教学版省略了 Zod(用 JSON Schema)、省略了 validateInput(用安全函数)、保留了权限检查和钩子概念。\n\n### 五、流式工具执行\n\nCC 的 `StreamingToolExecutor`(`StreamingToolExecutor.ts`)让工具在模型还在生成时就启动——不等模型说完。`read_file` 可能在模型还在输出\"我来分析\"的时候就跑完了。教学版不实现这个,目标和 s01 一致——概念清晰,不追求性能极致。\n\n### 六、工具结果持久化\n\n每个工具有一个 `maxResultSizeChars` 字段。结果超过这个值就落盘,模型看到的是预览 + 文件路径。FileRead 特殊——设为 `Infinity`,防止读文件的输出又被当成文件落盘。具体来说,如果 FileRead 的结果超过阈值被落盘,模型下次读那个落盘文件时又会触发落盘 → 无限循环(读文件 → 落盘 → 再读 → 再落盘 → ...)。\n\n
\n\n\n" + "content": "# s02: Tool Use — 多加一个工具,只加一行\n\ns01 → `s02` → [s03](/zh/s03) → s04 → ... → s20\n> *\"加一个工具, 只加一个 handler\"* — 循环不用动, 新工具注册进 dispatch map 就行。\n>\n> **Harness 层**: 工具分发 — 扩展模型能触达的边界。\n\n---\n\n## 只有 bash 一个工具\n\ns01 的 Agent 只有一个 bash 工具。读文件要 `cat`,写文件要 `echo \"...\" > file.py`,改文件要 `sed`。\n\n模型想的是\"读这个文件\",却要拼出 `cat path/to/file`。多了一层翻译,浪费 token,还容易拼错。\n\n---\n\n## 全局视角:工具分发\n\n![Tool Dispatch](/course-assets/s02_tool_use/tool-dispatch.svg)\n\ns01 的循环完全保留(LLM 调用、stop_reason 判断、消息追加)。唯一的变动在工具执行那 1 行:`run_bash()` 替换为 `TOOL_HANDLERS[block.name]()` 查表分发。\n\n给 Agent 加一个工具只需要做两件事:\n\n1. **定义工具**:在 `TOOLS` 数组里加一条描述\n2. **注册处理函数**:在 `TOOL_HANDLERS` 字典里加一个映射\n\n---\n\n## 从 1 个工具到 5 个工具\n\ns01 只有一个 bash:\n\n```python\nTOOLS = [{\"name\": \"bash\", ...}]\n\ndef run_bash(command): ...\n```\n\ns02 加到 5 个,每个工具都是独立定义:\n\n```python\nTOOLS = [\n {\"name\": \"bash\", \"description\": \"Run a shell command.\", ...},\n {\"name\": \"read_file\", \"description\": \"Read file contents.\", ...},\n {\"name\": \"write_file\", \"description\": \"Write content to file.\", ...},\n {\"name\": \"edit_file\", \"description\": \"Replace text in file once.\", ...},\n {\"name\": \"glob\", \"description\": \"Find files by pattern.\", ...},\n]\n```\n\n每个工具有自己的实现函数:\n\n```python\ndef run_read(path, limit=None):\n lines = safe_path(path).read_text().splitlines()\n if limit:\n lines = lines[:limit]\n return \"\\n\".join(lines)\n\ndef run_write(path, content):\n safe_path(path).write_text(content)\n return f\"Wrote {len(content)} bytes to {path}\"\n\ndef run_edit(path, old_text, new_text):\n text = safe_path(path).read_text()\n if old_text not in text:\n return \"Error: text not found\"\n safe_path(path).write_text(text.replace(old_text, new_text, 1))\n return f\"Edited {path}\"\n\ndef run_glob(pattern):\n import glob as g\n return \"\\n\".join(g.glob(pattern, root_dir=WORKDIR))\n```\n\n---\n\n## 工具分发\n\n```python\nTOOL_HANDLERS = {\n \"bash\": run_bash,\n \"read_file\": run_read,\n \"write_file\": run_write,\n \"edit_file\": run_edit,\n \"glob\": run_glob,\n}\n\n# 循环里只改了一行——从硬编码 run_bash 变成查表:\nfor block in response.content:\n if block.type == \"tool_use\":\n handler = TOOL_HANDLERS[block.name] # 查表\n output = handler(**block.input) # 调用\n results.append(...)\n```\n\n加一个工具 = 在 `TOOLS` 数组加一条 + 在 `TOOL_HANDLERS` 字典加一行。循环不变。\n\n---\n\n## 多个工具调用\n\n模型经常一次返回多个 tool_use:\"读一下 a.py 和 b.py,然后列出所有 .py 文件\"。\n\n教学版按 `response.content` 原始顺序逐个执行。CC 的做法更复杂:按原始顺序切成连续 batch,batch 内并发安全的工具并行执行,batch 间严格顺序(见附录)。\n\n---\n\n## 速查\n\n| 概念 | 一句话 |\n|------|--------|\n| TOOL_HANDLERS | 工具名 → 处理函数的字典。加工具 = 加一行映射 |\n| 工具定义 | 告诉模型\"我能做什么\"的 JSON schema |\n| 多工具调用 | 模型可一次返回多个 tool_use,教学版按原始顺序逐个执行 |\n| 循环不变 | s01 的 `while True` 循环一行都没改 |\n\n---\n\n## 相对 s01 的变更\n\n| 组件 | 之前 (s01) | 之后 (s02) |\n|------|-----------|-----------|\n| 工具数量 | 1 (bash) | 5 (+read, write, edit, glob) |\n| 工具执行 | 硬编码 `run_bash()` | TOOL_HANDLERS 查表分发 |\n| 路径安全 | 无 | safe_path 校验(仅 file tools) |\n| 循环 | `while True` + `stop_reason` | 与 s01 完全一致 |\n\n---\n\n## 试一下\n\n```sh\ncd learn-claude-code\npython s02_tool_use/code.py\n```\n\n试试这些 prompt:\n\n1. `Read the file README.md and tell me what this project is about`\n2. `Create a file called test.py that prints \"hello\", then read it back`\n3. `Find all Python files in this directory`\n4. `Read both README.md and requirements.txt, then create a summary file`\n\n观察重点:模型什么时候只调一个工具,什么时候一次调多个?多个工具调用的顺序和结果是否正确?\n\n---\n\n## 接下来\n\n现在 Agent 有 5 个专用工具。file tools 受 `safe_path` 保护,但 bash 不受限制,`rm -rf /` 还是能跑。\n\ns03 Permission → 在工具执行之前加一道门:这个操作安全吗?需要用户批准吗?\n\n
\n深入 CC 源码\n\n> 以下基于 CC 源码 `Tool.ts`、`tools.ts`、`toolOrchestration.ts`、`toolExecution.ts`、`StreamingToolExecutor.ts` 的核查。\n\n### 一、工具定义方式\n\n**教学版**:`TOOLS` 数组 + `TOOL_HANDLERS` 字典。定义和实现分开。\n**CC**:每个工具是 `buildTool()` 创建的独立对象,包含 schema、验证、权限、执行。`getAllBaseTools()` 汇总所有工具。\n\n教学版的分离方式对教学更清晰——读者一眼看到\"加一个工具 = 两条定义\"。\n\n### 二、并发安全判断:isConcurrencySafe()\n\n![Tool Concurrency](/course-assets/s02_tool_use/concurrency-comparison.svg)\n\n教学版按原始顺序逐个执行,不做并发。CC 用 `isConcurrencySafe(input)` 判断能否并发——注意这不是简单的\"只读 vs 写\",而是按具体输入判断:\n\n| | isReadOnly | isConcurrencySafe |\n|---|---|---|\n| FileRead | true | true |\n| Glob | true | true |\n| Bash `ls` | true | **true** ← 关键差异 |\n| Bash `rm` | false | false |\n| TaskCreate | false | **true** ← 改状态但可并发(TaskCreate 在 s12 介绍) |\n\nCC 的 Bash tool 的 `isConcurrencySafe` 等于 `isReadOnly`——只读命令可并发,写命令不可。TaskCreate 虽然改了任务文件,但每次都写不同的文件,所以可以并发。\n\n### 三、分区算法\n\nCC 的 `partitionToolCalls()`(`toolOrchestration.ts:91-115`)不是分两组,而是把工具调用**按连续块分批**:\n\n```\n[read A, read B, glob *.py, bash \"rm x\", read C]\n → batch1(并发): [read A, read B, glob *.py]\n → batch2(串行): [bash \"rm x\"]\n → batch3(并发): [read C]\n```\n\n并发安全的连续块编入同一个 batch,batch 内真正并发执行(`toolOrchestration.ts:152-176`,有并发上限)。遇到非并发安全的就开新 batch 串行执行。batch 之间严格顺序。\n\n### 四、验证管线\n\nCC 的每个工具调用经过严格的 5 步验证(`toolExecution.ts`):\n\n1. **Zod schema 验证**(`614-680`,教学版用 JSON Schema 替代):参数类型/结构检查\n2. **工具级 validateInput()**(`682-733`):参数值验证(如路径是否在工作区内)\n3. **PreToolUse hooks**(`800-862`,s04 详细介绍):钩子可以返回消息、修改输入、阻止执行\n4. **权限检查**(`921-931`,s03 的核心内容):canUseTool + checkPermissions → allow/deny/ask\n5. **执行 tool.call()**(`1207-1222`)\n\n教学版省略了 Zod(用 JSON Schema)、省略了 validateInput(用安全函数)、保留了权限检查和钩子概念。\n\n### 五、流式工具执行\n\nCC 的 `StreamingToolExecutor`(`StreamingToolExecutor.ts`)让工具在模型还在生成时就启动——不等模型说完。`read_file` 可能在模型还在输出\"我来分析\"的时候就跑完了。教学版不实现这个,目标和 s01 一致——概念清晰,不追求性能极致。\n\n### 六、工具结果持久化\n\n每个工具有一个 `maxResultSizeChars` 字段。结果超过这个值就落盘,模型看到的是预览 + 文件路径。FileRead 特殊——设为 `Infinity`,防止读文件的输出又被当成文件落盘。具体来说,如果 FileRead 的结果超过阈值被落盘,模型下次读那个落盘文件时又会触发落盘 → 无限循环(读文件 → 落盘 → 再读 → 再落盘 → ...)。\n\n
\n\n\n" }, { "version": "s02", "locale": "ja", "title": "s02: Tool Use — ツール一つ追加、一行追加だけ", - "content": "# s02: Tool Use — ツール一つ追加、一行追加だけ\n\ns01 → `s02` → [s03](/ja/s03) → s04 → ... → s20\n> *\"ツールを一つ追加、ハンドラを一つ追加\"* — ループはそのまま。新しいツールをディスパッチマップに登録するだけ。\n>\n> **Harness レイヤー**: ツールディスパッチ — モデルが触れる範囲を拡張。\n\n---\n\n## ツールは bash 一つだけ\n\ns01 の Agent には bash 一つのツールしかない。ファイルを読むには `cat`、書くには `echo \"...\" > file.py`、編集するには `sed`。\n\nモデルは「このファイルを読みたい」と考えながら、`cat path/to/file` と組み立てなければならない。翻訳の層が一つ増え、トークンを無駄にし、エラーも起きやすい。\n\n---\n\n## 概要:ツールディスパッチ\n\n![Tool Dispatch](/course-assets/s02_tool_use/tool-dispatch.ja.svg)\n\ns01 のループは完全に保持される(LLM 呼び出し、stop_reason 判定、メッセージ追加 — 一文字も変更なし)。唯一の変更点はツール実行の 1 行:`run_bash()` が `TOOL_HANDLERS[block.name]()` の検索ディスパッチに置き換わる。\n\nAgent にツールを追加するには、たった二つ:\n\n1. **ツールを定義**:`TOOLS` 配列に一条を追加\n2. **ハンドラを登録**:`TOOL_HANDLERS` 辞書に一つのマッピングを追加\n\n---\n\n## From 1 Tool to 6 Tools\n\ns01 には bash だけだった:\n\n```python\nTOOLS = [{\"name\": \"bash\", ...}]\n\ndef run_bash(command): ...\n```\n\ns02 expands to 6 tools, each independently defined:\n\n```python\nTOOLS = [\n {\"name\": \"bash\", \"description\": \"Run a shell command.\", ...},\n {\"name\": \"read_file\", \"description\": \"Read file contents.\", ...},\n {\"name\": \"write_file\", \"description\": \"Write content to file.\", ...},\n {\"name\": \"edit_file\", \"description\": \"Replace text in file once.\", ...},\n {\"name\": \"glob\", \"description\": \"Find files by pattern.\", ...},\n {\"name\": \"generate_image\", \"description\": \"Generate an image from a text prompt.\", ...},\n]\n```\n\n各ツールには専用の実装関数がある:\n\n```python\ndef run_read(path, limit=None):\n lines = safe_path(path).read_text().splitlines()\n if limit:\n lines = lines[:limit]\n return \"\\n\".join(lines)\n\ndef run_write(path, content):\n safe_path(path).write_text(content)\n return f\"Wrote {len(content)} bytes to {path}\"\n\ndef run_edit(path, old_text, new_text):\n text = safe_path(path).read_text()\n if old_text not in text:\n return \"Error: text not found\"\n safe_path(path).write_text(text.replace(old_text, new_text, 1))\n return f\"Edited {path}\"\n\ndef run_glob(pattern):\n import glob as g\n return \"\\n\".join(g.glob(pattern, root_dir=WORKDIR))\n\ndef run_generate_image(prompt, aspect_ratio=\"1:1\"):\n ...\n```\n\n---\n\n## ツールディスパッチ\n\n```python\nTOOL_HANDLERS = {\n \"bash\": run_bash,\n \"read_file\": run_read,\n \"write_file\": run_write,\n \"edit_file\": run_edit,\n \"glob\": run_glob,\n \"generate_image\": run_generate_image,\n}\n\n# ループ内で変更されたのは一行だけ — ハードコードの run_bash から検索ディスパッチへ:\nfor block in response.content:\n if block.type == \"tool_use\":\n handler = TOOL_HANDLERS[block.name] # 検索\n output = handler(**block.input) # 呼び出し\n results.append(...)\n```\n\nツールの追加 = `TOOLS` 配列に一条 + `TOOL_HANDLERS` 辞書に一行。ループは変わらない。\n\n---\n\n## 複数のツール呼び出し\n\nモデルはよく一度に複数の tool_use を返す — 「a.py と b.py を読んで、全 .py ファイルを列挙して」。\n\n教育版は `response.content` の元の順序で一つずつ実行する。CC のやり方はより複雑:元の順序を保ったまま連続バッチに分割し、バッチ内の並列安全なツールを並行実行し、バッチ間は厳密に順次(付録を参照)。\n\n---\n\n## 速查\n\n| 概念 | 一言で |\n|------|--------|\n| TOOL_HANDLERS | ツール名 → ハンドラ関数の辞書。ツール追加 = マッピング一行追加 |\n| ツール定義 | モデルに「何ができるか」を伝える JSON schema |\n| 複数ツール呼び出し | モデルは一度に複数の tool_use を返す可能性がある。教育版は元の順序で一つずつ実行 |\n| ループ不変 | s01 の `while True` ループ — 一行も変更なし |\n\n---\n\n## s01 からの変更\n\n| コンポーネント | 変更前 (s01) | 変更後 (s02) |\n|--------------|-------------|-------------|\n| Tool count | 1 (bash) | 6 (+read, write, edit, glob, generate_image) |\n| ツール実行 | ハードコード `run_bash()` | TOOL_HANDLERS 検索ディスパッチ |\n| パス安全性 | なし | safe_path 検証(file tools のみ) |\n| ループ | `while True` + `stop_reason` | s01 と完全に同一 |\n\n---\n\n## 試してみよう\n\n```sh\ncd learn-claude-code\npython s02_tool_use/code.py\n```\n\nThe optional `generate_image` tool also needs these values in `.env`. Use `https://api.minimaxi.com` as the host for China mainland.\n\n```sh\nMINIMAX_API_KEY=sk-xxx\nMINIMAX_API_HOST=https://api.minimax.io\n```\n\n以下のプロンプトを試してみよう:\n\n1. `Read the file README.md and tell me what this project is about`\n2. `Create a file called test.py that prints \"hello\", then read it back`\n3. `Find all Python files in this directory`\n4. `Read both README.md and requirements.txt, then create a summary file`\n5. `Generate a 16:9 image of a coding workspace at sunrise`\n\n観察のポイント:モデルがツールを一つだけ呼び出すときと、複数同時に呼び出すときの違い。複数のツール呼び出しは正しい順序で実行されているか?\n\n---\n\n## 次へ\n\nThe Agent now has 6 specialized tools. File tools are protected by `safe_path`, but bash remains unrestricted: `rm -rf /` still runs.\n\n→ s03 Permission:ツール実行前にゲートを追加 — この操作は安全か? ユーザーの承認が必要か?\n\n
\nCC ソースコードを深掘り\n\n> 以下は CC ソースコード `Tool.ts`、`tools.ts`、`toolOrchestration.ts`、`toolExecution.ts`、`StreamingToolExecutor.ts` の検証に基づく。\n\n### 一、ツール定義方式\n\n**教育版**:`TOOLS` 配列 + `TOOL_HANDLERS` 辞書。定義と実装が分離。\n**CC**:各ツールは `buildTool()` で作成された独立オブジェクトで、schema、バリデーション、権限、実行を含む。`getAllBaseTools()` が全ツールを集約。\n\n教育版の分離方式は教学に適している — 読者は「ツール追加 = 二つの定義」と一目で分かる。\n\n### 二、並列安全性:isConcurrencySafe()\n\n![Tool Concurrency](/course-assets/s02_tool_use/concurrency-comparison.ja.svg)\n\n教育版は元の順序で一つずつ実行し、並列処理は行わない。CC は `isConcurrencySafe(input)` で並列可否を判断する — これは単なる「読み取り専用 vs 書き込み」ではなく、具体的な入力で判断する:\n\n| | isReadOnly | isConcurrencySafe |\n|---|---|---|\n| FileRead | true | true |\n| Glob | true | true |\n| Bash `ls` | true | **true** ← 重要な違い |\n| Bash `rm` | false | false |\n| TaskCreate | false | **true** ← 状態変更するが並列可能(s12 で紹介) |\n\nCC の Bash ツールの `isConcurrencySafe` は `isReadOnly` と同じ — 読み取り専用コマンドは並列可能、書き込みコマンドは不可。TaskCreate はタスクファイルを変更するが、毎回異なるファイルに書き込むため並列可能。\n\n### 三、パーティションアルゴリズム\n\nCC の `partitionToolCalls()`(`toolOrchestration.ts:91-115`)は二つのグループに分けるのではなく、ツール呼び出しを**連続ブロックごとにバッチ化**する:\n\n```\n[read A, read B, glob *.py, bash \"rm x\", read C]\n → batch1(並列): [read A, read B, glob *.py]\n → batch2(直列): [bash \"rm x\"]\n → batch3(並列): [read C]\n```\n\n連続する並列安全な呼び出しを同じバッチにまとめ、真の並列実行を行う(`toolOrchestration.ts:152-176`、並列数上限あり)。非並列安全な呼び出しに遭遇すると新しいバッチを開始して直列実行。バッチ間は厳密に順次。\n\n### 四、バリデーションパイプライン\n\nCC の各ツール呼び出しは厳格な 5 段階のバリデーションを経る(`toolExecution.ts`):\n\n1. **Zod schema バリデーション**(`614-680`、教育版は JSON Schema で代替):パラメータの型/構造チェック\n2. **ツールレベル validateInput()**(`682-733`):パラメータ値の検証(例:パスが作業ディレクトリ内か)\n3. **PreToolUse フック**(`800-862`、s04 で詳解):フックはメッセージの返却、入力の変更、実行のブロックが可能\n4. **権限チェック**(`921-931`、s03 の核心):canUseTool + checkPermissions → allow/deny/ask\n5. **tool.call() の実行**(`1207-1222`)\n\n教育版は Zod を省略(JSON Schema を使用)、validateInput を省略(安全関数を使用)、権限チェックとフック概念は保持。\n\n### 五、ストリーミングツール実行\n\nCC の `StreamingToolExecutor`(`StreamingToolExecutor.ts`)はモデルがまだ生成中にツールを起動する — モデルの完了を待たない。`read_file` はモデルが「分析します」と出力中に完了するかもしれない。教育版はこれを実装しない。s01 と同じ目標 — 概念の明確さ、極限のパフォーマンスではない。\n\n### 六、ツール結果の永続化\n\n各ツールには `maxResultSizeChars` フィールドがある。この閾値を超える結果はディスクに保存され、モデルにはプレビュー + ファイルパスが表示される。FileRead は特殊 — `Infinity` に設定され、ファイル読み出し結果の再永続化を防ぐ。具体的には、FileRead の結果が閾値を超えて永続化されると、モデルがその永続化ファイルを次に読むときにまた永続化がトリガーされ → 無限ループ(ファイル読む → 永続化 → 再読み → 再永続化 → ...)になる。\n\n
\n\n\n" + "content": "# s02: Tool Use — ツール一つ追加、一行追加だけ\n\ns01 → `s02` → [s03](/ja/s03) → s04 → ... → s20\n> *\"ツールを一つ追加、ハンドラを一つ追加\"* — ループはそのまま。新しいツールをディスパッチマップに登録するだけ。\n>\n> **Harness レイヤー**: ツールディスパッチ — モデルが触れる範囲を拡張。\n\n---\n\n## ツールは bash 一つだけ\n\ns01 の Agent には bash 一つのツールしかない。ファイルを読むには `cat`、書くには `echo \"...\" > file.py`、編集するには `sed`。\n\nモデルは「このファイルを読みたい」と考えながら、`cat path/to/file` と組み立てなければならない。翻訳の層が一つ増え、トークンを無駄にし、エラーも起きやすい。\n\n---\n\n## 概要:ツールディスパッチ\n\n![Tool Dispatch](/course-assets/s02_tool_use/tool-dispatch.ja.svg)\n\ns01 のループは完全に保持される(LLM 呼び出し、stop_reason 判定、メッセージ追加 — 一文字も変更なし)。唯一の変更点はツール実行の 1 行:`run_bash()` が `TOOL_HANDLERS[block.name]()` の検索ディスパッチに置き換わる。\n\nAgent にツールを追加するには、たった二つ:\n\n1. **ツールを定義**:`TOOLS` 配列に一条を追加\n2. **ハンドラを登録**:`TOOL_HANDLERS` 辞書に一つのマッピングを追加\n\n---\n\n## 1 つのツールから 5 つのツールへ\n\ns01 には bash だけだった:\n\n```python\nTOOLS = [{\"name\": \"bash\", ...}]\n\ndef run_bash(command): ...\n```\n\ns02 では 5 つに増え、各ツールは独立して定義される:\n\n```python\nTOOLS = [\n {\"name\": \"bash\", \"description\": \"Run a shell command.\", ...},\n {\"name\": \"read_file\", \"description\": \"Read file contents.\", ...},\n {\"name\": \"write_file\", \"description\": \"Write content to file.\", ...},\n {\"name\": \"edit_file\", \"description\": \"Replace text in file once.\", ...},\n {\"name\": \"glob\", \"description\": \"Find files by pattern.\", ...},\n]\n```\n\n各ツールには専用の実装関数がある:\n\n```python\ndef run_read(path, limit=None):\n lines = safe_path(path).read_text().splitlines()\n if limit:\n lines = lines[:limit]\n return \"\\n\".join(lines)\n\ndef run_write(path, content):\n safe_path(path).write_text(content)\n return f\"Wrote {len(content)} bytes to {path}\"\n\ndef run_edit(path, old_text, new_text):\n text = safe_path(path).read_text()\n if old_text not in text:\n return \"Error: text not found\"\n safe_path(path).write_text(text.replace(old_text, new_text, 1))\n return f\"Edited {path}\"\n\ndef run_glob(pattern):\n import glob as g\n return \"\\n\".join(g.glob(pattern, root_dir=WORKDIR))\n```\n\n---\n\n## ツールディスパッチ\n\n```python\nTOOL_HANDLERS = {\n \"bash\": run_bash,\n \"read_file\": run_read,\n \"write_file\": run_write,\n \"edit_file\": run_edit,\n \"glob\": run_glob,\n}\n\n# ループ内で変更されたのは一行だけ — ハードコードの run_bash から検索ディスパッチへ:\nfor block in response.content:\n if block.type == \"tool_use\":\n handler = TOOL_HANDLERS[block.name] # 検索\n output = handler(**block.input) # 呼び出し\n results.append(...)\n```\n\nツールの追加 = `TOOLS` 配列に一条 + `TOOL_HANDLERS` 辞書に一行。ループは変わらない。\n\n---\n\n## 複数のツール呼び出し\n\nモデルはよく一度に複数の tool_use を返す — 「a.py と b.py を読んで、全 .py ファイルを列挙して」。\n\n教育版は `response.content` の元の順序で一つずつ実行する。CC のやり方はより複雑:元の順序を保ったまま連続バッチに分割し、バッチ内の並列安全なツールを並行実行し、バッチ間は厳密に順次(付録を参照)。\n\n---\n\n## 速查\n\n| 概念 | 一言で |\n|------|--------|\n| TOOL_HANDLERS | ツール名 → ハンドラ関数の辞書。ツール追加 = マッピング一行追加 |\n| ツール定義 | モデルに「何ができるか」を伝える JSON schema |\n| 複数ツール呼び出し | モデルは一度に複数の tool_use を返す可能性がある。教育版は元の順序で一つずつ実行 |\n| ループ不変 | s01 の `while True` ループ — 一行も変更なし |\n\n---\n\n## s01 からの変更\n\n| コンポーネント | 変更前 (s01) | 変更後 (s02) |\n|--------------|-------------|-------------|\n| ツール数 | 1 (bash) | 5 (+read, write, edit, glob) |\n| ツール実行 | ハードコード `run_bash()` | TOOL_HANDLERS 検索ディスパッチ |\n| パス安全性 | なし | safe_path 検証(file tools のみ) |\n| ループ | `while True` + `stop_reason` | s01 と完全に同一 |\n\n---\n\n## 試してみよう\n\n```sh\ncd learn-claude-code\npython s02_tool_use/code.py\n```\n\n以下のプロンプトを試してみよう:\n\n1. `Read the file README.md and tell me what this project is about`\n2. `Create a file called test.py that prints \"hello\", then read it back`\n3. `Find all Python files in this directory`\n4. `Read both README.md and requirements.txt, then create a summary file`\n\n観察のポイント:モデルがツールを一つだけ呼び出すときと、複数同時に呼び出すときの違い。複数のツール呼び出しは正しい順序で実行されているか?\n\n---\n\n## 次へ\n\nAgent は 5 つの専用ツールを持つようになった。file tools は `safe_path` で保護されるが、bash は制限なし — `rm -rf /` はまだ実行できる。\n\n→ s03 Permission:ツール実行前にゲートを追加 — この操作は安全か? ユーザーの承認が必要か?\n\n
\nCC ソースコードを深掘り\n\n> 以下は CC ソースコード `Tool.ts`、`tools.ts`、`toolOrchestration.ts`、`toolExecution.ts`、`StreamingToolExecutor.ts` の検証に基づく。\n\n### 一、ツール定義方式\n\n**教育版**:`TOOLS` 配列 + `TOOL_HANDLERS` 辞書。定義と実装が分離。\n**CC**:各ツールは `buildTool()` で作成された独立オブジェクトで、schema、バリデーション、権限、実行を含む。`getAllBaseTools()` が全ツールを集約。\n\n教育版の分離方式は教学に適している — 読者は「ツール追加 = 二つの定義」と一目で分かる。\n\n### 二、並列安全性:isConcurrencySafe()\n\n![Tool Concurrency](/course-assets/s02_tool_use/concurrency-comparison.ja.svg)\n\n教育版は元の順序で一つずつ実行し、並列処理は行わない。CC は `isConcurrencySafe(input)` で並列可否を判断する — これは単なる「読み取り専用 vs 書き込み」ではなく、具体的な入力で判断する:\n\n| | isReadOnly | isConcurrencySafe |\n|---|---|---|\n| FileRead | true | true |\n| Glob | true | true |\n| Bash `ls` | true | **true** ← 重要な違い |\n| Bash `rm` | false | false |\n| TaskCreate | false | **true** ← 状態変更するが並列可能(s12 で紹介) |\n\nCC の Bash ツールの `isConcurrencySafe` は `isReadOnly` と同じ — 読み取り専用コマンドは並列可能、書き込みコマンドは不可。TaskCreate はタスクファイルを変更するが、毎回異なるファイルに書き込むため並列可能。\n\n### 三、パーティションアルゴリズム\n\nCC の `partitionToolCalls()`(`toolOrchestration.ts:91-115`)は二つのグループに分けるのではなく、ツール呼び出しを**連続ブロックごとにバッチ化**する:\n\n```\n[read A, read B, glob *.py, bash \"rm x\", read C]\n → batch1(並列): [read A, read B, glob *.py]\n → batch2(直列): [bash \"rm x\"]\n → batch3(並列): [read C]\n```\n\n連続する並列安全な呼び出しを同じバッチにまとめ、真の並列実行を行う(`toolOrchestration.ts:152-176`、並列数上限あり)。非並列安全な呼び出しに遭遇すると新しいバッチを開始して直列実行。バッチ間は厳密に順次。\n\n### 四、バリデーションパイプライン\n\nCC の各ツール呼び出しは厳格な 5 段階のバリデーションを経る(`toolExecution.ts`):\n\n1. **Zod schema バリデーション**(`614-680`、教育版は JSON Schema で代替):パラメータの型/構造チェック\n2. **ツールレベル validateInput()**(`682-733`):パラメータ値の検証(例:パスが作業ディレクトリ内か)\n3. **PreToolUse フック**(`800-862`、s04 で詳解):フックはメッセージの返却、入力の変更、実行のブロックが可能\n4. **権限チェック**(`921-931`、s03 の核心):canUseTool + checkPermissions → allow/deny/ask\n5. **tool.call() の実行**(`1207-1222`)\n\n教育版は Zod を省略(JSON Schema を使用)、validateInput を省略(安全関数を使用)、権限チェックとフック概念は保持。\n\n### 五、ストリーミングツール実行\n\nCC の `StreamingToolExecutor`(`StreamingToolExecutor.ts`)はモデルがまだ生成中にツールを起動する — モデルの完了を待たない。`read_file` はモデルが「分析します」と出力中に完了するかもしれない。教育版はこれを実装しない。s01 と同じ目標 — 概念の明確さ、極限のパフォーマンスではない。\n\n### 六、ツール結果の永続化\n\n各ツールには `maxResultSizeChars` フィールドがある。この閾値を超える結果はディスクに保存され、モデルにはプレビュー + ファイルパスが表示される。FileRead は特殊 — `Infinity` に設定され、ファイル読み出し結果の再永続化を防ぐ。具体的には、FileRead の結果が閾値を超えて永続化されると、モデルがその永続化ファイルを次に読むときにまた永続化がトリガーされ → 無限ループ(ファイル読む → 永続化 → 再読み → 再永続化 → ...)になる。\n\n
\n\n\n" }, { "version": "s03", @@ -359,4 +359,4 @@ "title": "s20: Comprehensive Agent — すべての仕組みを 1 つのループへ", "content": "# s20: Comprehensive Agent — すべての仕組みを 1 つのループへ\n\ns01 → ... → s18 → s19 → `s20`\n\n> *\"仕組みは多い、ループは 1 つ\"* — tools、permissions、memory、tasks、teams、plugins はすべて同じ `while True` に接続される。\n>\n> **Harness レイヤー**: 総合 — 前 19 章の仕組みを 1 つの実行可能なシステムへ戻す。\n\n---\n\n## 問題\n\n前 19 章では、各章が 1 つの仕組みだけを追加した。学習にはその形が適している。しかし実際の agent は、1 つの仕組みだけで動くわけではない。\n\n長時間動く coding agent には、同時に次のものが必要になる:\n\n- tool dispatch と permission boundary\n- hook extension point\n- todo plan と task graph\n- skill、memory、runtime system prompt assembly\n- compaction と error recovery\n- background task と cron scheduling\n- team、protocol、autonomous claiming\n- worktree isolation\n- MCP external tool integration\n\n難しいのは機能を積み上げることではない。それぞれの仕組みが loop のどこに接続されるかを見抜くことだ。S20 は終点章であり、すべての component を 1 つの harness に戻す。\n\n---\n\n## 解決策\n\n![System Architecture](/course-assets/s20_comprehensive/system-architecture.ja.svg)\n\nS20 は新しい単独 mechanism を発明しない。前章までの teaching component を 1 つの完全な harness に統合する:\n\n```text\nuser input\n → UserPromptSubmit hooks\n → cron/background notification injection\n → context compact\n → memory + skills + MCP state で system prompt を組み立てる\n → LLM\n → has tool_use block?\n no → Stop hooks → return\n yes → PreToolUse hooks + permission\n → TOOL_HANDLERS / MCP handlers / background dispatch\n → PostToolUse hooks\n → tool_result / task_notification を messages へ戻す\n → next round\n```\n\nloop 自体は同じ構造のままだ。model を呼び、response に `tool_use` block があるかを見て、tool を実行し、結果を `messages` に戻す。CC source でも `stop_reason == \"tool_use\"` を直接信頼せず、実際に tool_use block が出たかを continuation signal として扱う。変わったのは、loop の周囲の harness が完成形になったことだけ。\n\n---\n\n## 各 Component の位置\n\n| 位置 | Component | 役割 |\n|------|-----------|------|\n| user input 周辺 | `UserPromptSubmit` hooks | user input の記録、注入、監査 |\n| LLM 前 | cron queue | scheduled prompt を `messages` へ注入 |\n| LLM 前 | background notifications | 完了した background work を `` として注入 |\n| LLM 前 | compaction pipeline | 大きな出力を予算化し、履歴を切り、古い tool_result を圧縮し、必要なら要約 |\n| LLM 前 | memory / skills / MCP state | current capabilities と long-term context を system prompt に組み込む |\n| LLM call | error recovery | 429/529 retry、`max_tokens` escalation、prompt-too-long compact |\n| tool 実行前 | `PreToolUse` hooks + permission | 危険な command、範囲外 write、destructive MCP tool を止める |\n| tool dispatch | `assemble_tool_pool` | built-in tools と dynamic MCP tools を組み立てる |\n| tool 実行中 | background dispatch | 遅い bash work を daemon thread に逃がし、placeholder result を返す |\n| tool 実行後 | `PostToolUse` hooks | large-output warning、log、後処理 |\n| loop へ戻る | tool_result | 1 つの `tool_use` に 1 つの `tool_result`、そして次の model round |\n| tool_use がない round / stop 時 | `Stop` hooks | 統計、cleanup、audit |\n\n---\n\n## code.py に含まれるもの\n\n### Tools と Dispatch\n\nbuilt-in tool pool には 27 個の tool がある:\n\n```text\nbash, read_file, write_file, edit_file, glob\ntodo_write, task, load_skill, compact\ncreate_task, list_tasks, get_task, claim_task, complete_task\nschedule_cron, list_crons, cancel_cron\nspawn_teammate, send_message, check_inbox\nrequest_shutdown, request_plan, review_plan\ncreate_worktree, remove_worktree, keep_worktree\nconnect_mcp\n```\n\n`assemble_tool_pool()` は毎 round で次を組み立てる:\n\n```text\nBUILTIN_TOOLS + connected MCP tools\nBUILTIN_HANDLERS + mcp__server__tool handlers\n```\n\n`connect_mcp(\"docs\")` のあと、次の round では `mcp__docs__search` のような tool が出現する。\n\n### Permission と Hooks\n\npermission は tool 実行行に直接埋め込まない。`PreToolUse` hook として扱う:\n\n```python\nblocked = trigger_hooks(\"PreToolUse\", block)\nif blocked:\n results.append(tool_result(block.id, blocked))\n continue\n```\n\nこれにより permission、logging、audit が同じ hook point に接続できる。実行後には `PostToolUse` hook が走る。\n\n### Plan と Task\n\nS20 には 2 層の plan がある:\n\n- `todo_write`: current session 用の軽量 plan。メモリに保持。\n- task graph: cross-session、dependency-aware、claimable な task file。`.tasks/task_*.json` に保存。\n\n前者は単独 agent の drift を防ぐ。後者は team coordination の土台になる。\n\n### Subagent と Team\n\nS20 には 2 種類の delegation がある:\n\n- `task`: one-shot subagent。独立した `messages[]` を使い、中間 context を捨て、final summary だけ返す。\n- `spawn_teammate`: persistent teammate thread。`MessageBus` で通信し、idle 中に task board を polling して自律的に claim できる。\n\none-shot subagent は context isolation を解決する。persistent teammate は長期並列協作を解決する。\n\n### Memory、Skills、Prompt\n\n`assemble_system_prompt(context)` は毎 round 次を組み立てる:\n\n- identity と tool guidance\n- workspace\n- skills catalog\n- `.memory/MEMORY.md`\n- connected MCP servers\n\nskills は system prompt には catalog だけ置く。全文は `load_skill(name)` で必要な時に読む。\n\n### Compaction と Recovery\n\nLLM call の前に compaction pipeline を走らせる:\n\n```text\ntool_result_budget → snip_compact → micro_compact → compact_history\n```\n\nmodel call は recovery で包む:\n\n- 429: exponential backoff retry\n- 529: exponential backoff、連続失敗時は fallback model へ切替可能\n- `max_tokens`: max tokens を上げ、その後 continuation を要求\n- prompt too long: reactive compact 後に retry\n\n### Background と Cron\n\n遅い bash work は main loop を止めない:\n\n```text\nshould_run_background → start_background_task → placeholder tool_result\nbackground done → task_notification → next round injects messages\n```\n\ncron scheduler は daemon thread として動き、1 秒ごとに確認する。CLI は `cron_queue` を監視し、発火した job を `[Scheduled] ...` として注入して Agent を 1 turn 自動実行する。\n\n### Worktree と MCP\n\nworktree isolation は directory を担当する:\n\n- `create_worktree(name, task_id)` が isolated branch と directory を作る\n- task の `worktree` field が task と directory を紐付ける\n- teammate が worktree 付き task を claim すると、bash/read/write はその directory で実行される\n\nMCP は external capability を担当する:\n\n- `connect_mcp(name)` が mock server に接続する\n- `assemble_tool_pool()` が MCP tools を tool pool に組み立てる\n- tool name は `mcp__server__tool` 形式に統一する\n\n---\n\n## s19 からの変化\n\n| Component | s19 | s20 |\n|-----------|-----|-----|\n| tool pool | built-in + MCP | built-in + MCP、s01-s18 の tool を補完 |\n| permission | teaching body では省略 | `PreToolUse` hook で実行 |\n| hooks | 省略 | UserPromptSubmit / PreToolUse / PostToolUse / Stop |\n| todo | 省略 | `todo_write` + reminder |\n| skill | 省略 | system prompt の catalog + `load_skill` |\n| compact | 省略 | LLM 前 compaction + `compact` tool + reactive compact |\n| error recovery | simple try/except | retry / max_tokens / prompt too long |\n| background | 省略 | slow-operation thread + task notification |\n| cron | 省略 | daemon scheduler + durable jobs |\n| multi-agent | 維持 | 維持。teammate は isolated directory 上の basic tools を使う |\n| worktree | 維持 | 維持 |\n| MCP | 新規 | final tool pool の一部として維持 |\n\n---\n\n## 試す\n\n```sh\ncd learn-claude-code\npython s20_comprehensive/code.py\n```\n\n試す prompt:\n\n1. `Create a todo list for inspecting this repo, then list Python files`\n2. `Connect to the docs MCP server and search for agent loop`\n3. `Create two tasks, create worktrees for them, then spawn alice and bob. Ask them to submit plans before claiming tasks.`\n4. `remind me of the meeting in 3 minutes.`\n5. `Run npm install in the background and continue reading README.md`\n\n見るポイント:\n\n- tool call の前に hooks/permission を通るか\n- `connect_mcp` 後の次 round で MCP tool が出るか\n- 遅い operation が background placeholder を返すか\n- cron が時刻到達時に自動で reminder を返すか\n- teammate が plan を提出し、approval 前に停止するか\n- plan approval 後、teammate が task を claim できるか\n- worktree binding 後、teammate が対応 directory に切り替わるか\n\n---\n\n## 終わりは始まり\n\ns01 から s20 まで、コードの能力は増えていく。しかし中心は変わらない:\n\n```python\nwhile True:\n response = LLM(messages, tools)\n if not has_tool_use(response.content):\n return\n results = execute_tools(response.content)\n messages.append(tool_results)\n```\n\nClaude Code の複雑さは「別の agent brain」ではない。成熟した harness の複雑さだ。model は判断と action selection を担当する。harness は environment、tools、permissions、memory、teams、external capabilities を整理する。\n\nこれが本コースの終点だ:仕組みは多い、ループは 1 つ。\n" } -] +] \ No newline at end of file diff --git a/web/src/data/generated/versions.json b/web/src/data/generated/versions.json index 207a320c9..676a20aa5 100644 --- a/web/src/data/generated/versions.json +++ b/web/src/data/generated/versions.json @@ -41,21 +41,19 @@ "filename": "s02_tool_use/code.py", "title": "Tool Use", "subtitle": "Add a Tool, Add Just One Line", - "loc": 170, + "loc": 135, "tools": [ "bash", "read_file", "write_file", "edit_file", - "glob", - "generate_image" + "glob" ], "newTools": [ "read_file", "write_file", "edit_file", - "glob", - "generate_image" + "glob" ], "coreAddition": "Tool dispatch map", "keyInsight": "The loop stays stable while capabilities register into a dispatch table.", @@ -91,19 +89,14 @@ "signature": "def run_glob(pattern: str)", "startLine": 105 }, - { - "name": "run_generate_image", - "signature": "def run_generate_image(prompt: str, aspect_ratio: str = \"1:1\")", - "startLine": 117 - }, { "name": "agent_loop", "signature": "def agent_loop(messages: list)", - "startLine": 188 + "startLine": 150 } ], "layer": "tools", - "source": "#!/usr/bin/env python3\n\"\"\"\ns02: Tool Use - adds five tools and a dispatch map on top of s01.\n\n运行: python s02_tool_use/code.py\n需要: pip install anthropic python-dotenv + .env 中配置 ANTHROPIC_API_KEY\n\n本文件 = s01 的全部代码 + 以下新增:\n + Five tool implementations: run_read / run_write / run_edit / run_glob / run_generate_image\n + TOOL_HANDLERS 分发映射(替代 s01 中硬编码的 run_bash 调用)\n + safe_path 路径安全校验\n\n循环本身(agent_loop)与 s01 完全一致。\n\"\"\"\n\nimport os, subprocess, json, urllib.request\nfrom pathlib import Path\n\ntry:\n import readline\n readline.parse_and_bind('set bind-tty-special-chars off')\n readline.parse_and_bind('set input-meta on')\n readline.parse_and_bind('set output-meta on')\n readline.parse_and_bind('set convert-meta off')\nexcept ImportError:\n pass\n\nfrom anthropic import Anthropic\nfrom dotenv import load_dotenv\n\nload_dotenv(override=True)\nif os.getenv(\"ANTHROPIC_BASE_URL\"):\n os.environ.pop(\"ANTHROPIC_AUTH_TOKEN\", None)\n\nWORKDIR = Path.cwd()\nclient = Anthropic(base_url=os.getenv(\"ANTHROPIC_BASE_URL\"))\nMODEL = os.environ[\"MODEL_ID\"]\n\nSYSTEM = f\"You are a coding agent at {WORKDIR}. Use tools to solve tasks. Act, don't explain.\"\n\n\n# ═══════════════════════════════════════════════════════════\n# FROM s01 (unchanged)\n# ═══════════════════════════════════════════════════════════\n\ndef run_bash(command: str) -> str:\n dangerous = [\"rm -rf /\", \"sudo\", \"shutdown\", \"reboot\", \"> /dev/\"]\n if any(d in command for d in dangerous):\n return \"Error: Dangerous command blocked\"\n try:\n r = subprocess.run(command, shell=True, cwd=WORKDIR,\n capture_output=True, text=True,\n encoding=\"utf-8\", errors=\"replace\", timeout=120)\n out = (r.stdout + r.stderr).strip()\n return out[:50000] if out else \"(no output)\"\n except subprocess.TimeoutExpired:\n return \"Error: Timeout (120s)\"\n except (FileNotFoundError, OSError) as e:\n return f\"Error: {e}\"\n\n\n# ═══════════════════════════════════════════════════════════\n# NEW in s02: 5 tools\n# ═══════════════════════════════════════════════════════════\n\ndef safe_path(p: str) -> Path:\n path = (WORKDIR / p).resolve()\n if not path.is_relative_to(WORKDIR):\n raise ValueError(f\"Path escapes workspace: {p}\")\n return path\n\n\ndef run_read(path: str, limit: int | None = None) -> str:\n try:\n lines = safe_path(path).read_text().splitlines()\n if limit and limit < len(lines):\n lines = lines[:limit] + [f\"... ({len(lines) - limit} more lines)\"]\n return \"\\n\".join(lines)\n except Exception as e:\n return f\"Error: {e}\"\n\n\ndef run_write(path: str, content: str) -> str:\n try:\n file_path = safe_path(path)\n file_path.parent.mkdir(parents=True, exist_ok=True)\n file_path.write_text(content)\n return f\"Wrote {len(content)} bytes to {path}\"\n except Exception as e:\n return f\"Error: {e}\"\n\n\ndef run_edit(path: str, old_text: str, new_text: str) -> str:\n try:\n file_path = safe_path(path)\n text = file_path.read_text()\n if old_text not in text:\n return f\"Error: text not found in {path}\"\n file_path.write_text(text.replace(old_text, new_text, 1))\n return f\"Edited {path}\"\n except Exception as e:\n return f\"Error: {e}\"\n\n\ndef run_glob(pattern: str) -> str:\n import glob as g\n try:\n results = []\n for match in g.glob(pattern, root_dir=WORKDIR):\n if (WORKDIR / match).resolve().is_relative_to(WORKDIR):\n results.append(match)\n return \"\\n\".join(results) if results else \"(no matches)\"\n except Exception as e:\n return f\"Error: {e}\"\n\n\ndef run_generate_image(prompt: str, aspect_ratio: str = \"1:1\") -> str:\n \"\"\"Generate an image with MiniMax and return the image URL.\"\"\"\n api_key = os.getenv(\"MINIMAX_API_KEY\")\n if not api_key:\n return \"Error: set MINIMAX_API_KEY to use image generation\"\n api_host = (os.getenv(\"MINIMAX_API_HOST\") or \"https://api.minimax.io\").rstrip(\"/\")\n\n payload = {\n \"model\": \"image-01\",\n \"prompt\": prompt,\n \"aspect_ratio\": aspect_ratio,\n \"n\": 1,\n \"response_format\": \"url\",\n }\n req = urllib.request.Request(\n f\"{api_host}/v1/image_generation\",\n data=json.dumps(payload).encode(\"utf-8\"),\n headers={\n \"Authorization\": f\"Bearer {api_key}\",\n \"Content-Type\": \"application/json\",\n },\n method=\"POST\",\n )\n try:\n with urllib.request.urlopen(req, timeout=120) as resp:\n data = json.loads(resp.read().decode(\"utf-8\"))\n status = data.get(\"base_resp\", {}).get(\"status_code\")\n if status not in (None, 0):\n msg = data.get(\"base_resp\", {}).get(\"status_msg\", \"unknown error\")\n return f\"Error: MiniMax image generation failed ({status}): {msg}\"\n urls = data.get(\"data\", {}).get(\"image_urls\") or []\n return urls[0] if urls else f\"Error: no image URL returned: {data}\"\n except Exception as e:\n return f\"Error: {e}\"\n\n\n# ═══════════════════════════════════════════════════════════\n# NEW in s02: tool definitions (s01 has only bash; s02 has 6 tools)\n# ═══════════════════════════════════════════════════════════\n\nTOOLS = [\n {\"name\": \"bash\", \"description\": \"Run a shell command.\",\n \"input_schema\": {\"type\": \"object\", \"properties\": {\"command\": {\"type\": \"string\"}}, \"required\": [\"command\"]}},\n {\"name\": \"read_file\", \"description\": \"Read file contents.\",\n \"input_schema\": {\"type\": \"object\", \"properties\": {\"path\": {\"type\": \"string\"}, \"limit\": {\"type\": \"integer\"}}, \"required\": [\"path\"]}},\n {\"name\": \"write_file\", \"description\": \"Write content to a file.\",\n \"input_schema\": {\"type\": \"object\", \"properties\": {\"path\": {\"type\": \"string\"}, \"content\": {\"type\": \"string\"}}, \"required\": [\"path\", \"content\"]}},\n {\"name\": \"edit_file\", \"description\": \"Replace exact text in a file once.\",\n \"input_schema\": {\"type\": \"object\", \"properties\": {\"path\": {\"type\": \"string\"}, \"old_text\": {\"type\": \"string\"}, \"new_text\": {\"type\": \"string\"}}, \"required\": [\"path\", \"old_text\", \"new_text\"]}},\n {\"name\": \"glob\", \"description\": \"Find files matching a glob pattern.\",\n \"input_schema\": {\"type\": \"object\", \"properties\": {\"pattern\": {\"type\": \"string\"}}, \"required\": [\"pattern\"]}},\n {\"name\": \"generate_image\", \"description\": \"Generate an image from a text prompt using MiniMax.\",\n \"input_schema\": {\"type\": \"object\", \"properties\": {\"prompt\": {\"type\": \"string\", \"maxLength\": 1500}, \"aspect_ratio\": {\"type\": \"string\", \"default\": \"1:1\", \"enum\": [\"1:1\", \"16:9\", \"4:3\", \"3:2\", \"2:3\", \"3:4\", \"9:16\", \"21:9\"]}}, \"required\": [\"prompt\"]}},\n]\n\n# ═══════════════════════════════════════════════════════════\n# NEW in s02: 工具分发映射(s01 是硬编码 run_bash,现在改为查表)\n# ═══════════════════════════════════════════════════════════\n\nTOOL_HANDLERS = {\n \"bash\": run_bash, \"read_file\": run_read, \"write_file\": run_write,\n \"edit_file\": run_edit, \"glob\": run_glob, \"generate_image\": run_generate_image,\n}\n\n\n# ═══════════════════════════════════════════════════════════\n# agent_loop — 与 s01 结构完全一致,只改了工具执行那部分\n# s01: output = run_bash(block.input[\"command\"])\n# s02: output = TOOL_HANDLERS[block.name](**block.input)\n# ═══════════════════════════════════════════════════════════\n\ndef agent_loop(messages: list):\n while True:\n response = client.messages.create(\n model=MODEL, system=SYSTEM, messages=messages,\n tools=TOOLS, max_tokens=8000,\n )\n messages.append({\"role\": \"assistant\", \"content\": response.content})\n\n if response.stop_reason != \"tool_use\":\n return\n\n results = []\n for block in response.content:\n if block.type == \"tool_use\":\n print(f\"\\033[33m> {block.name}\\033[0m\")\n handler = TOOL_HANDLERS.get(block.name)\n output = handler(**block.input) if handler else f\"Unknown: {block.name}\"\n print(str(output)[:200])\n results.append({\"type\": \"tool_result\", \"tool_use_id\": block.id, \"content\": output})\n\n messages.append({\"role\": \"user\", \"content\": results})\n\n\nif __name__ == \"__main__\":\n print(\"s02: Tool Use - adds 5 tools on top of s01\")\n print(\"输入问题,回车发送。输入 q 退出。\\n\")\n\n history = []\n while True:\n try:\n query = input(\"\\033[36ms02 >> \\033[0m\")\n except (EOFError, KeyboardInterrupt):\n break\n if query.strip().lower() in (\"q\", \"exit\", \"\"):\n break\n history.append({\"role\": \"user\", \"content\": query})\n agent_loop(history)\n for block in history[-1][\"content\"]:\n if getattr(block, \"type\", None) == \"text\":\n print(block.text)\n print()\n", + "source": "#!/usr/bin/env python3\n\"\"\"\ns02: Tool Use — 在 s01 基础上新增 4 个工具 + 分发映射。\n\n运行: python s02_tool_use/code.py\n需要: pip install anthropic python-dotenv + .env 中配置 ANTHROPIC_API_KEY\n\n本文件 = s01 的全部代码 + 以下新增:\n + run_read / run_write / run_edit / run_glob 四个工具实现\n + TOOL_HANDLERS 分发映射(替代 s01 中硬编码的 run_bash 调用)\n + safe_path 路径安全校验\n\n循环本身(agent_loop)与 s01 完全一致。\n\"\"\"\n\nimport os, subprocess\nfrom pathlib import Path\n\ntry:\n import readline\n readline.parse_and_bind('set bind-tty-special-chars off')\n readline.parse_and_bind('set input-meta on')\n readline.parse_and_bind('set output-meta on')\n readline.parse_and_bind('set convert-meta off')\nexcept ImportError:\n pass\n\nfrom anthropic import Anthropic\nfrom dotenv import load_dotenv\n\nload_dotenv(override=True)\nif os.getenv(\"ANTHROPIC_BASE_URL\"):\n os.environ.pop(\"ANTHROPIC_AUTH_TOKEN\", None)\n\nWORKDIR = Path.cwd()\nclient = Anthropic(base_url=os.getenv(\"ANTHROPIC_BASE_URL\"))\nMODEL = os.environ[\"MODEL_ID\"]\n\nSYSTEM = f\"You are a coding agent at {WORKDIR}. Use tools to solve tasks. Act, don't explain.\"\n\n\n# ═══════════════════════════════════════════════════════════\n# FROM s01 (unchanged)\n# ═══════════════════════════════════════════════════════════\n\ndef run_bash(command: str) -> str:\n dangerous = [\"rm -rf /\", \"sudo\", \"shutdown\", \"reboot\", \"> /dev/\"]\n if any(d in command for d in dangerous):\n return \"Error: Dangerous command blocked\"\n try:\n r = subprocess.run(command, shell=True, cwd=WORKDIR,\n capture_output=True, text=True,\n encoding=\"utf-8\", errors=\"replace\", timeout=120)\n out = (r.stdout + r.stderr).strip()\n return out[:50000] if out else \"(no output)\"\n except subprocess.TimeoutExpired:\n return \"Error: Timeout (120s)\"\n except (FileNotFoundError, OSError) as e:\n return f\"Error: {e}\"\n\n\n# ═══════════════════════════════════════════════════════════\n# NEW in s02: 4 个新工具\n# ═══════════════════════════════════════════════════════════\n\ndef safe_path(p: str) -> Path:\n path = (WORKDIR / p).resolve()\n if not path.is_relative_to(WORKDIR):\n raise ValueError(f\"Path escapes workspace: {p}\")\n return path\n\n\ndef run_read(path: str, limit: int | None = None) -> str:\n try:\n lines = safe_path(path).read_text().splitlines()\n if limit and limit < len(lines):\n lines = lines[:limit] + [f\"... ({len(lines) - limit} more lines)\"]\n return \"\\n\".join(lines)\n except Exception as e:\n return f\"Error: {e}\"\n\n\ndef run_write(path: str, content: str) -> str:\n try:\n file_path = safe_path(path)\n file_path.parent.mkdir(parents=True, exist_ok=True)\n file_path.write_text(content)\n return f\"Wrote {len(content)} bytes to {path}\"\n except Exception as e:\n return f\"Error: {e}\"\n\n\ndef run_edit(path: str, old_text: str, new_text: str) -> str:\n try:\n file_path = safe_path(path)\n text = file_path.read_text()\n if old_text not in text:\n return f\"Error: text not found in {path}\"\n file_path.write_text(text.replace(old_text, new_text, 1))\n return f\"Edited {path}\"\n except Exception as e:\n return f\"Error: {e}\"\n\n\ndef run_glob(pattern: str) -> str:\n import glob as g\n try:\n results = []\n for match in g.glob(pattern, root_dir=WORKDIR):\n if (WORKDIR / match).resolve().is_relative_to(WORKDIR):\n results.append(match)\n return \"\\n\".join(results) if results else \"(no matches)\"\n except Exception as e:\n return f\"Error: {e}\"\n\n\n# ═══════════════════════════════════════════════════════════\n# NEW in s02: 工具定义(s01 只有一个 bash,现在扩展到 5 个)\n# ═══════════════════════════════════════════════════════════\n\nTOOLS = [\n {\"name\": \"bash\", \"description\": \"Run a shell command.\",\n \"input_schema\": {\"type\": \"object\", \"properties\": {\"command\": {\"type\": \"string\"}}, \"required\": [\"command\"]}},\n {\"name\": \"read_file\", \"description\": \"Read file contents.\",\n \"input_schema\": {\"type\": \"object\", \"properties\": {\"path\": {\"type\": \"string\"}, \"limit\": {\"type\": \"integer\"}}, \"required\": [\"path\"]}},\n {\"name\": \"write_file\", \"description\": \"Write content to a file.\",\n \"input_schema\": {\"type\": \"object\", \"properties\": {\"path\": {\"type\": \"string\"}, \"content\": {\"type\": \"string\"}}, \"required\": [\"path\", \"content\"]}},\n {\"name\": \"edit_file\", \"description\": \"Replace exact text in a file once.\",\n \"input_schema\": {\"type\": \"object\", \"properties\": {\"path\": {\"type\": \"string\"}, \"old_text\": {\"type\": \"string\"}, \"new_text\": {\"type\": \"string\"}}, \"required\": [\"path\", \"old_text\", \"new_text\"]}},\n {\"name\": \"glob\", \"description\": \"Find files matching a glob pattern.\",\n \"input_schema\": {\"type\": \"object\", \"properties\": {\"pattern\": {\"type\": \"string\"}}, \"required\": [\"pattern\"]}},\n]\n\n# ═══════════════════════════════════════════════════════════\n# NEW in s02: 工具分发映射(s01 是硬编码 run_bash,现在改为查表)\n# ═══════════════════════════════════════════════════════════\n\nTOOL_HANDLERS = {\n \"bash\": run_bash, \"read_file\": run_read, \"write_file\": run_write,\n \"edit_file\": run_edit, \"glob\": run_glob,\n}\n\n\n# ═══════════════════════════════════════════════════════════\n# agent_loop — 与 s01 结构完全一致,只改了工具执行那部分\n# s01: output = run_bash(block.input[\"command\"])\n# s02: output = TOOL_HANDLERS[block.name](**block.input)\n# ═══════════════════════════════════════════════════════════\n\ndef agent_loop(messages: list):\n while True:\n response = client.messages.create(\n model=MODEL, system=SYSTEM, messages=messages,\n tools=TOOLS, max_tokens=8000,\n )\n messages.append({\"role\": \"assistant\", \"content\": response.content})\n\n if response.stop_reason != \"tool_use\":\n return\n\n results = []\n for block in response.content:\n if block.type == \"tool_use\":\n print(f\"\\033[33m> {block.name}\\033[0m\")\n handler = TOOL_HANDLERS.get(block.name)\n output = handler(**block.input) if handler else f\"Unknown: {block.name}\"\n print(str(output)[:200])\n results.append({\"type\": \"tool_result\", \"tool_use_id\": block.id, \"content\": output})\n\n messages.append({\"role\": \"user\", \"content\": results})\n\n\nif __name__ == \"__main__\":\n print(\"s02: Tool Use — 在 s01 基础上加了 4 个工具\")\n print(\"输入问题,回车发送。输入 q 退出。\\n\")\n\n history = []\n while True:\n try:\n query = input(\"\\033[36ms02 >> \\033[0m\")\n except (EOFError, KeyboardInterrupt):\n break\n if query.strip().lower() in (\"q\", \"exit\", \"\"):\n break\n history.append({\"role\": \"user\", \"content\": query})\n agent_loop(history)\n for block in history[-1][\"content\"]:\n if getattr(block, \"type\", None) == \"text\":\n print(block.text)\n print()\n", "images": [ { "src": "/course-assets/s02_tool_use/concurrency-comparison.svg", @@ -3640,17 +3633,15 @@ "run_read", "run_write", "run_edit", - "run_glob", - "run_generate_image" + "run_glob" ], "newTools": [ "read_file", "write_file", "edit_file", - "glob", - "generate_image" + "glob" ], - "locDelta": 68 + "locDelta": 33 }, { "from": "s02", @@ -3663,7 +3654,7 @@ "check_permission" ], "newTools": [], - "locDelta": 10 + "locDelta": 45 }, { "from": "s03",