diff --git a/s07_skill_loading/README.ja.md b/s07_skill_loading/README.ja.md
index 91134a6b..4f1e43f2 100644
--- a/s07_skill_loading/README.ja.md
+++ b/s07_skill_loading/README.ja.md
@@ -3,7 +3,7 @@
[中文](README.zh.md) · [English](README.md) · [日本語](README.ja.md)
s01 → s02 → s03 → s04 → s05 → s06 → `s07` → [s08](../s08_context_compact/) → s09 → ... → s20
-> *"Load when needed, don't stuff the prompt"* — tool_result で注入、system prompt には詰め込まない。
+> *"Load when needed, don't stuff the prompt"* — `tool_result` で注入、system prompt には詰め込まない。
>
> **Harness レイヤー**: 知識 — 必要に応じて読み込み、コンテキストに詰め込まない。
@@ -11,7 +11,9 @@ s01 → s02 → s03 → s04 → s05 → s06 → `s07` → [s08](../s08_context_c
## 課題
-プロジェクトには React コンポーネント仕様、SQL スタイルガイド、API 設計ドキュメントがある。Agent にこれらの仕様を自動的に守らせたい。最も直接的な方法は、すべて system prompt に詰め込むこと:
+前章では Agent が大きなタスクをサブ Agent に渡せるようになった。だがサブ Agent が引き継ぐとき、そのタスクの決まりを知る必要がある:React コンポーネントを変更するなら component spec を、SQL を書くなら style guide を守る。その決まりはどこから来るのか?
+
+プロジェクトには React コンポーネント仕様、SQL スタイルガイド、API 設計ドキュメントがある。最も直接的な方法は、すべて system prompt に詰め込むこと:
```python
SYSTEM = (
@@ -30,7 +32,9 @@ SYSTEM = (

-前章の最小フック構造、`todo_write`、サブ Agent を維持し、本章は新規の `load_skill` ツールに注目する。起動時にスキルカタログを SYSTEM prompt に注入し、実行時に完全な内容を読み込むツールを登録する。使ったときだけトークンを消費。
+折衷案は、ドキュメントを複数のファイルに分け、必要なものを Agent 自身に `read_file` させることだ。だが Agent はそもそもどんなファイルが読めるか分からない。「何があるか」を先に知らなければ、「どれを使うか」は選べない。
+
+そこで 2 層に分ける:**カタログは常駐、内容はオンデマンド。** 前章のフック構造、`todo_write`、サブ Agent はそのまま残し、本章で `load_skill` ツールを 1 つ加える。起動時にスキルのカタログ(名前 + 一言の説明)を SYSTEM prompt に入れる。毎ターン携帯するが軽い。実行時に Agent が実際にあるスキルを使うとき、`load_skill` を呼んで完全な内容を取り出す。トークンを使うのはそのときだけだ。
2 層設計:
@@ -90,6 +94,8 @@ def build_system() -> str:
SYSTEM = build_system()
```
+だが Agent は毎ターン、名前と一言の説明しか受け取らない。実際に SQL スタイルガイドを使うとなると、あの 1500 行の完全な内容にはまだ手が届かない。→ 第 2 層。
+
**第 2 層:load_skill**:Agent が「SQL スタイルガイドが必要」と判断し、`load_skill("sql-style")` を呼び出す。レジストリを通じて検索し、ファイルパスを経由しないため、パストラバーサルのリスクがない。SKILL.md の内容は `tool_result` を通じて注入され、既存の file および bash ツールを通じて、参照される `references/`、`scripts/`、`assets/` へのその後のアクセスも含められる。
```python
@@ -100,7 +106,7 @@ def load_skill(name: str) -> str:
return skill["content"]
```
-重要な違い:スキル内容は system prompt の一部ではなく、ツール結果として現在の messages に入る。後続の呼び出しでは履歴とともに携帯され、コンテキスト圧縮、切り捨て、またはセッション終了まで保持される。これは s08 の compact と自然に接続する:オンデマンド読み込みにより、無関係なドキュメントが system prompt に入らなくなる。compact が「捨てるべきものをどう捨てるか」を解決する。
+重要な違い:スキル内容は system prompt の一部ではなく、ツール結果として現在の `messages` に入る。後続の呼び出しでは履歴とともに携帯され、コンテキスト圧縮、切り捨て、またはセッション終了まで保持される。これは s08 の compact と自然に接続する:スキル内容は system prompt ではなく `tool_result` として `messages` に入り、compact が「捨てるべきものをどう捨てるか」を解決する。
---
@@ -135,7 +141,7 @@ python s07_skill_loading/code.py
## 次へ
-load_skill で起動時のトークン浪費は解消した。しかし別の問題が待っている:Agent が 30 分連続で作業すると、messages リストが中間プロセスで埋め尽くされる。古い tool_result、期限切れのファイル内容、コンテキストを占領しているが価値を生まない。
+load_skill で起動時のトークン浪費は解消した。しかし別の問題が待っている:Agent が 30 分連続で作業すると、`messages` リストが中間プロセスで埋め尽くされる。古い `tool_result`、期限切れのファイル内容、コンテキストを占領しているが価値を生まない。
→ s08 Context Compact:4 層圧縮戦略。安価な層を先に実行、高価な層を後に実行。
@@ -179,4 +185,4 @@ Claude Code の SKILL.md YAML frontmatter は `parseSkillFrontmatterFields()`(
-
+
diff --git a/s07_skill_loading/README.md b/s07_skill_loading/README.md
index 31d6f6cb..d2a30c70 100644
--- a/s07_skill_loading/README.md
+++ b/s07_skill_loading/README.md
@@ -3,7 +3,7 @@
[中文](README.zh.md) · [English](README.md) · [日本語](README.ja.md)
s01 → s02 → s03 → s04 → s05 → s06 → `s07` → [s08](../s08_context_compact/) → s09 → ... → s20
-> *"Load when needed, don't stuff the prompt"* — Inject via tool_result, not system prompt.
+> *"Load when needed, don't stuff the prompt"* — Inject via `tool_result`, not system prompt.
>
> **Harness Layer**: Knowledge — load on demand, don't fill the context.
@@ -11,7 +11,9 @@ s01 → s02 → s03 → s04 → s05 → s06 → `s07` → [s08](../s08_context_c
## The Problem
-Your project has a React component spec, a SQL style guide, and an API design doc. You want the Agent to follow these specs automatically. The most straightforward idea: stuff them all into the system prompt:
+The last chapter let the Agent hand a big task off to a sub-Agent. But when the sub-Agent takes over, it needs to know the rules of that task: editing React components means following the component spec, writing SQL means following the style guide. Where do those rules come from?
+
+Your project has a React component spec, a SQL style guide, and an API design doc. The most straightforward idea: stuff them all into the system prompt:
```python
SYSTEM = (
@@ -30,7 +32,9 @@ SYSTEM = (

-The minimal hook structure, `todo_write`, and sub-Agent from the previous chapter are preserved. This chapter focuses on the new `load_skill` tool. At startup, inject the skill catalog into the SYSTEM prompt; at runtime, register one more tool to load full content, spending tokens only when used.
+A middle-ground idea is to split the docs into separate files and let the Agent `read_file` whichever it needs. But the Agent has no idea which files exist to read; it has to know "what's there" before it can pick "which one to use".
+
+So split it into two levels: **catalog always resident, content on demand.** The hook structure, `todo_write`, and sub-Agent from the previous chapter stay; this chapter adds a `load_skill` tool. At startup, the skill catalog (name + one-line description) goes into the SYSTEM prompt, carried every turn but light; at runtime, when the Agent actually needs a skill, it calls `load_skill` to pull the full content, spending those tokens only then.
Two-level design:
@@ -90,6 +94,8 @@ def build_system() -> str:
SYSTEM = build_system()
```
+But every turn the Agent only gets the name and one-line description; to actually use the SQL style guide, those 1500 lines of full content are still out of reach. → Level 2.
+
**Level 2: load_skill**: the Agent decides "I need the SQL style guide" and calls `load_skill("sql-style")`. Lookup goes through the registry, not file paths, eliminating path traversal risk. The SKILL.md content is injected via `tool_result`, and can include later access to referenced `references/`, `scripts/`, or `assets/` through the existing file and bash tools.
```python
@@ -100,7 +106,7 @@ def load_skill(name: str) -> str:
return skill["content"]
```
-The key distinction: skill content is not part of the system prompt. It enters the current messages as a tool result. Subsequent calls carry it along with the history until context compaction, truncation, or session end. This naturally connects to s08's compact: on-demand loading keeps irrelevant docs out of the system prompt, compact solves "how to drop what you should."
+The key distinction: skill content is not part of the system prompt. It enters the current `messages` as a `tool_result`. Subsequent calls carry it along with the history until context compaction, truncation, or session end. This naturally connects to s08's compact: skill content enters `messages` as a `tool_result` rather than the system prompt, and compact solves "how to drop what you should."
---
@@ -135,7 +141,7 @@ What to watch for: Does the Agent know available skills from the SYSTEM catalog?
## What's Next
-load_skill eliminated the startup token waste. But another problem looms: after the Agent works for 30 minutes, the messages list fills up with intermediate process. Old tool_results, stale file contents, occupying context but adding no value.
+load_skill eliminated the startup token waste. But another problem looms: after the Agent works for 30 minutes, the `messages` list fills up with intermediate process. Old `tool_result`s, stale file contents, occupying context but adding no value.
→ s08 Context Compact: A four-layer compaction strategy. Cheap layers run first, expensive layers run last.
@@ -179,4 +185,4 @@ The complete field list changes across versions; above are the core fields relev
-
+
diff --git a/s07_skill_loading/README.zh.md b/s07_skill_loading/README.zh.md
index 183b2afe..a1b33a47 100644
--- a/s07_skill_loading/README.zh.md
+++ b/s07_skill_loading/README.zh.md
@@ -3,7 +3,7 @@
[中文](README.zh.md) · [English](README.md) · [日本語](README.ja.md)
s01 → s02 → s03 → s04 → s05 → s06 → `s07` → [s08](../s08_context_compact/) → s09 → ... → s20
-> *"用到时再加载, 别全塞 prompt 里"* — 通过 tool_result 注入, 不塞 system prompt。
+> *"用到时再加载, 别全塞 prompt 里"* — 通过 `tool_result` 注入, 不塞 system prompt。
>
> **Harness 层**: 知识 — 按需加载, 不堆满上下文。
@@ -11,7 +11,9 @@ s01 → s02 → s03 → s04 → s05 → s06 → `s07` → [s08](../s08_context_c
## 问题
-你的项目有一套 React 组件规范、一份 SQL 风格指南、一份 API 设计文档。你希望 Agent 自动遵守这些规范。最直接的想法,全塞进 system prompt:
+上一章 Agent 能把大任务拆给子 Agent 了。但子 Agent 接手时,得知道这个任务的规矩:改 React 组件要守组件规范,写 SQL 要守风格指南。这些规范从哪来?
+
+你的项目里有一套 React 组件规范、一份 SQL 风格指南、一份 API 设计文档。最直接的想法,全塞进 system prompt:
```python
SYSTEM = (
@@ -22,7 +24,7 @@ SYSTEM = (
)
```
-6500 行 system prompt。Agent 每次调用 LLM 都带着这些文档——不管是在改 CSS 颜色还是修 SQL 查询。99% 的内容和当前任务无关,白白消耗 token。
+6500 行 system prompt。Agent 每次调用 LLM 都带着这些文档,不管在改 CSS 颜色还是修 SQL 查询。99% 的内容和当前任务无关,白白消耗 token。
---
@@ -30,7 +32,9 @@ SYSTEM = (

-保留上一章的最小 hook 结构、`todo_write` 和子 Agent,本章重点转向新增的 `load_skill` 工具。启动时把技能目录注入 SYSTEM prompt,运行时多注册一个工具加载完整内容,用到才花 token。
+一个折中的想法,是把文档拆成几个文件,用哪个让 Agent 自己 `read_file`。但 Agent 根本不知道有哪些文件可读,它得先知道"有什么",才谈得上"用哪个"。
+
+所以分两层:**目录常驻,内容按需。** 上一章的 hook 结构、`todo_write` 和子 Agent 都保留,本章新增一个 `load_skill` 工具。启动时把技能目录(名字 + 一句话描述)注入 SYSTEM prompt,每轮都带、但很轻;运行时 Agent 真要用某个技能,再调 `load_skill` 把完整内容取过来,用到才花那部分 token。
两层设计:
@@ -39,7 +43,7 @@ SYSTEM = (
| 1. 目录 | system prompt | 启动时注入(harness 扫描 skills/) | ~100 tokens/skill,每轮都带 |
| 2. 内容 | tool_result | Agent 调用 load_skill 时;SKILL.md 可指引后续的 read_file/bash 调用,用于按需访问额外资源 | ~2000 tokens/skill,按需 |
-dispatch 机制不变,load_skill 通过 `TOOL_HANDLERS[block.name]` 分发。
+dispatch 机制不变,`load_skill` 通过 `TOOL_HANDLERS[block.name]` 分发。
---
@@ -90,6 +94,8 @@ def build_system() -> str:
SYSTEM = build_system()
```
+但 Agent 每轮只拿到名字和一句话描述,真要用 SQL 风格指南,那 1500 行的完整内容还够不到。→ 第二级。
+
**第二级:load_skill**:Agent 决定"我需要 SQL 风格指南",调用 `load_skill("sql-style")`。通过注册表查找,不走文件路径,没有路径遍历风险。SKILL.md 内容通过 `tool_result` 注入,并可通过现有的 file 和 bash 工具进一步访问引用的 `references/`、`scripts/` 或 `assets/`。
```python
@@ -100,7 +106,7 @@ def load_skill(name: str) -> str:
return skill["content"]
```
-关键区别:技能内容不是 system prompt 的一部分,它作为一次工具结果进入当前 messages。后续调用会随历史一起携带,直到上下文压缩、截断或会话结束。这和 s08 的 compact 自然衔接:技能内容以 tool_result 形式进入 messages,不塞 system prompt,compact 解决"该丢的怎么丢"。
+关键区别:技能内容不是 system prompt 的一部分,它作为一次工具结果进入当前 `messages`。后续调用会随历史一起携带,直到上下文压缩、截断或会话结束。这和 s08 的 compact 自然衔接:技能内容以 `tool_result` 形式进入 `messages`,不塞 system prompt,compact 解决"该丢的怎么丢"。
---
@@ -135,7 +141,7 @@ python s07_skill_loading/code.py
## 接下来
-load_skill 解决了启动时的 token 浪费。但另一个问题来了:Agent 连续工作 30 分钟后,messages 列表塞满了中间过程。旧的 tool_result、过时的文件内容,占着上下文但不产生价值。
+`load_skill` 解决了启动时的 token 浪费。但另一个问题来了:Agent 连续工作 30 分钟后,`messages` 列表塞满了中间过程。旧的 `tool_result`、过时的文件内容,占着上下文但不产生价值。
s08 Context Compact → 四层压缩策略。便宜的先跑,贵的后跑。
@@ -168,7 +174,7 @@ Claude Code 的 SKILL.md YAML frontmatter 由 `parseSkillFrontmatterFields()`
### 三、两级加载的精确实现
1. **Catalog(启动时)**:`getSkillDirCommands()` 扫描目录 → 注册为 `Command` 对象,只包含元数据。`getSkillListingAttachments()` 把技能列表格式化为附件,预算为上下文窗口的 ~1%(上限 8000 字符)。
-2. **Load(调用时)**:模型调 `Skill` 工具(输入字段是 `skill` + 可选 `args`,教学版用 `name`)→ `getPromptForCommand()` 展开完整 SKILL.md 内容 → `SkillTool` 返回的 tool_result 展示文本只是 `"Launching skill: {name}"`,真正的技能内容通过 `newMessages` 注入对话。教学版把两者合并为"通过 tool_result 注入"是一种简化;加载后的 SKILL.md 仍可作为指引,帮助模型后续通过现有 file/bash 工具访问相关资源。
+2. **Load(调用时)**:模型调 `Skill` 工具(输入字段是 `skill` + 可选 `args`,教学版用 `name`)→ `getPromptForCommand()` 展开完整 SKILL.md 内容 → `SkillTool` 返回的 `tool_result` 展示文本只是 `"Launching skill: {name}"`,真正的技能内容通过 `newMessages` 注入对话。教学版把两者合并为"通过 tool_result 注入"是一种简化;加载后的 SKILL.md 仍可作为指引,帮助模型后续通过现有 file/bash 工具访问相关资源。
### 教学版的简化是刻意的
@@ -179,4 +185,4 @@ Claude Code 的 SKILL.md YAML frontmatter 由 `parseSkillFrontmatterFields()`
-
+
diff --git a/web/src/data/generated/docs.json b/web/src/data/generated/docs.json
index 6b460913..6492a34c 100644
--- a/web/src/data/generated/docs.json
+++ b/web/src/data/generated/docs.json
@@ -99,7 +99,7 @@
"version": "s06",
"locale": "zh",
"title": "s06: Subagent — 大任务拆小,每个拿到的都是干净上下文",
- "content": "# s06: Subagent — 大任务拆小,每个拿到的都是干净上下文\n\ns01 → s02 → s03 → s04 → s05 → `s06` → [s07](/zh/s07) → s08 → ... → s20\n\n> *\"大任务拆小, 每个小任务干净的上下文\"* — Subagent 用独立 `messages[]`, 不污染主对话。\n>\n> **Harness 层**: 子 Agent — 上下文隔离, 注意力不漂移。\n\n---\n\n## 问题\n\n上一章给 Agent 加了 `todo_write`,它能把大任务拆成一张清单,一步步推进。但拆出来的子任务,还是在同一个 `messages[]` 里跑。\n\nAgent 在修一个 bug:读了 30 个文件跟踪调用链,来回聊了 60 轮,`messages` 涨到 120 条。其中大半是\"跟踪调用链\"的中间过程,和\"修 bug\"这个最终目标已经没关系了,却还占着上下文。早期的关键信息被挤出有效窗口,模型对最初那个 bug 的描述,反而记不清了。\n\n换个角度想:你修 bug 时会\"开一个新终端\"去跟踪调用链,跟踪完把结果记下来,关掉终端,回到原来的终端接着修。Agent 也需要这个能力:开一个独立的子进程,给它一份独立的消息列表,让它专心做一件事,做完只把结论带回来。\n\n---\n\n## 解决方案\n\n\n\n一个符合直觉的做法,是让主 Agent 自己把调用链跟踪完、接着修。但跟踪的过程会全部留在主对话里,这正是上面那个问题。\n\n所以换个思路:**把脏活外包出去,只收一句结论。** 上一章的 hook 结构和 `todo_write` 都保留,本章新增一个 `task` 工具。主 Agent 调它时,spawn 一个子 Agent,给它全新的 `messages[]`,让它跑自己的循环;结束后只回传一段摘要文本,中间那 60 轮全部丢弃。对话上下文不进主 Agent,但子 Agent 在文件系统上做的实际改动(写文件、改文件、跑命令)会留在工作目录里。\n\n子 Agent 的工具是受限的:有 `bash`/`read`/`write`/`edit`/`glob`,但没有 `task`,不能再递归 spawn 新的子 Agent。而且它的每次工具调用仍走权限 hook,安全策略不因为上下文隔离就跳过。\n\n---\n\n## 工作原理\n\n子 Agent 本质上就是 s01 那个循环的另一份实例,只是换了一份空的 `messages[]` 和一套更窄的工具:\n\n```python\ndef spawn_subagent(description: str) -> str:\n # 子 Agent 的工具:基础工具,但没有 task(禁止递归)\n sub_tools = [\n {\"name\": \"bash\", ...}, {\"name\": \"read_file\", ...},\n {\"name\": \"write_file\", ...}, {\"name\": \"edit_file\", ...},\n {\"name\": \"glob\", ...},\n ]\n messages = [{\"role\": \"user\", \"content\": description}] # 全新 messages[]\n\n for _ in range(30): # safety limit\n response = client.messages.create(\n model=MODEL, system=SUB_SYSTEM,\n messages=messages, tools=sub_tools, max_tokens=8000,\n )\n messages.append({\"role\": \"assistant\", \"content\": response.content})\n if response.stop_reason != \"tool_use\":\n break\n results = []\n for block in response.content:\n if block.type == \"tool_use\":\n blocked = trigger_hooks(\"PreToolUse\", block)\n if blocked:\n results.append({... \"content\": str(blocked)})\n continue\n handler = SUB_HANDLERS.get(block.name)\n output = handler(**block.input) if handler else f\"Unknown\"\n trigger_hooks(\"PostToolUse\", block, output)\n results.append({... \"content\": output})\n messages.append({\"role\": \"user\", \"content\": results})\n\n # 只返回最后的文本结论,中间过程全部丢弃\n return extract_text(messages[-1][\"content\"])\n```\n\n注意这个循环里的几处关键点:工具集里没有 `task`(不能再 spawn,递归到此为止);`for _ in range(30)` 是循环轮次的安全上限(子 Agent 最多跑 30 轮,避免无限循环);每次工具调用前仍走 `PreToolUse` hook(隔离了上下文,但没隔离权限);最后只用 `extract_text(messages[-1])` 取一句结论,中间过程整份丢掉。\n\n主 Agent 这边,调它跟调其他工具完全一样:\n\n```python\nTOOLS = [\n {\"name\": \"bash\", ...},\n {\"name\": \"read_file\", ...},\n {\"name\": \"write_file\", ...},\n {\"name\": \"edit_file\", ...},\n {\"name\": \"glob\", ...},\n {\"name\": \"todo_write\", ...},\n # s06: 新增 task 工具\n {\"name\": \"task\",\n \"description\": \"Launch a subagent to handle a complex subtask. Returns only the final conclusion.\",\n \"input_schema\": {\"type\": \"object\", \"properties\": {\"description\": {\"type\": \"string\"}}, \"required\": [\"description\"]}},\n]\n\nTOOL_HANDLERS[\"task\"] = spawn_subagent\n```\n\n四个关键设计决策:\n\n| 决策 | 选择 | 原因 |\n|------|------|------|\n| 上下文隔离 | 全新 `messages[]` | 子 Agent 的中间过程不污染主 Agent 的上下文 |\n| 只回传结论 | `extract_text(last_message)` | 不是回传整个 `messages` 列表 |\n| 禁止递归 | 子 Agent 无 `task` 工具 | 防止子 Agent 再 spawn 新的子 Agent |\n| 安全策略不跳过 | 子 Agent 工具调用也走 PreToolUse hook | 上下文隔离不代表权限隔离 |\n\ndispatch 机制不变,`task` 工具通过 `TOOL_HANDLERS[block.name]` 分发。子 Agent 还有独立的 `SUB_SYSTEM` 提示,明确要求\"直接完成任务,不要再委派\"。\n\n---\n\n## 相对 s05 的变更\n\n| 组件 | 之前 (s05) | 之后 (s06) |\n|------|-----------|-----------|\n| 工具数量 | 6 (`bash`, `read`, `write`, `edit`, `glob`, `todo_write`) | 7 (+`task`) |\n| 新函数 | — | `spawn_subagent`(独立 `messages[]` + 30 轮安全限制) |\n| 上下文隔离 | 全部在主对话中 | 子 Agent 用全新的 `messages[]` |\n| 循环 | 不变 | dispatch 不变,子 Agent 有独立 `SUB_SYSTEM` 和 hook 保护的循环 |\n\n---\n\n## 试一下\n\n```sh\ncd learn-claude-code\npython s06_subagent/code.py\n```\n\n试试这些 prompt:\n\n1. `Use a subtask to find what testing framework this project uses`(子 Agent 去读文件,主 Agent 只收结论)\n2. `Delegate: read all .py files in agents/ and summarize what each one does`\n3. `Use a task to create s06_subagent/example/string_tools.py with a slugify(text: str) function, then verify it from the parent agent`\n\n观察重点:是否出现 `[Subagent spawned]` / `[Subagent done]`?子 Agent 的工具调用是否以 `[sub] ...` 输出?主 Agent 最后是否只继续处理子 Agent 返回的摘要?\n\n---\n\n## 接下来\n\nAgent 现在能拆任务了。但每个任务需要的知识不一样:改前端组件需要知道 React 规范,写 SQL 需要知道表结构。这些知识全塞进 system prompt,上下文有可能会直接被填满。\n\ns07 Skill Loading → 技能按需注入,不在 system prompt 里堆文档。用到的时候才加载,和读文件一样自然。\n\n\n深入 Claude Code 源码
\n\n> 以下基于 Claude Code 源码 `AgentTool.tsx`、`runAgent.ts`、`forkSubagent.ts`、`forkedAgent.ts` 的完整分析。\n\n### 一、不是一种模式,是三种\n\n教学版只讲了\"全新的 `messages[]`\"。Claude Code 实际有三种执行模式:\n\n| 模式 | 触发条件 | 上下文 |\n|------|---------|--------|\n| **Normal Subagent** | 指定了 `subagent_type`(normal path) | 全新 `messages[]`,只有 prompt |\n| **Fork Subagent** | 没指定 `subagent_type`,fork gate 开启 | 通过 `buildForkedMessages()` 构造 cache-friendly 前缀,共享 prompt cache |\n| **General-Purpose** | 没指定 `subagent_type`,fork gate 关闭 | 同 Normal |\n\n### 二、Fork 模式:为了共享 Prompt Cache\n\n这是教学版没有的核心概念。Fork 模式(`forkSubagent.ts:60-71`)不创建全新上下文,而是通过 `buildForkedMessages()`(`forkSubagent.ts:107-168`)构造 cache-friendly 消息前缀,保留父 assistant message 并生成 placeholder tool results。目的不是隔离,而是让 Anthropic API 的 prompt cache 命中:父子 Agent 的 system prompt、tools、messages 前缀完全一致,API 端不需要重算。\n\n缓存命中的五个关键组件(`forkedAgent.ts:57-68`):system prompt、tools、model、messages 前缀、thinking config,必须字节级一致。\n\n### 三、Context Isolation 的精确粒度\n\n`createSubagentContext()`(`forkedAgent.ts:345-462`)创建子 Agent 的 `ToolUseContext`:\n\n| 字段 | 行为 |\n|------|------|\n| `abortController` | 新的 child controller,父 abort 向下传播 |\n| `setAppState` | 默认 no-op;但 sync agent 通过 `shareSetAppState` 共享(`runAgent.ts:697-714`) |\n| `readFileState` | **从父克隆**(避免重复读相同文件) |\n| `queryTracking` | 新 chainId,`depth = parentDepth + 1` |\n\n子 Agent 不是完全隔离的:文件读取状态是共享的。UI 和通知的隔离程度取决于执行路径(sync/async/fork/teammate 各不同)。\n\n### 四、递归 Fork 防护\n\n教学版用\"子 Agent 不给 `task` 工具\"表达递归保护。真实实现更精细:`isInForkChild()`(`forkSubagent.ts:78-89`)检查对话历史中是否有 `FORK_BOILERPLATE_TAG`,有就拒绝。但 `constants/tools.ts:36-46` 中 `Agent` 工具默认在所有 agent 的禁用集合里,`USER_TYPE === 'ant'` 时例外;`forkSubagent.ts:73-89` 针对 fork child 有专门的递归保护;`agentToolUtils.ts:100-110` 在 teammate 场景下有特殊放行。不是简单的\"禁止新的子 Agent\"。\n\n### 五、Permission Bubbling\n\nFork Agent 的 `permissionMode: 'bubble'`(`forkSubagent.ts:67`)意味着子 Agent 的权限弹窗冒泡到父终端,用户在主终端里审批子 Agent 的操作。\n\n### 六、Async vs Sync\n\n教学版只展示了同步子 Agent(父等着子跑完)。Claude Code 还支持异步路径(`AgentTool.tsx:686-764`):`run_in_background: true` 时异步启动,返回 `{ status: 'async_launched' }` 立即给父 Agent,子 Agent 完成后通过通知机制告知父 Agent。实际触发条件不止 `run_in_background`,还有 auto-background、assistant force async、coordinator/proactive 等路径。\n\n### 教学版的简化是刻意的\n\n- 三种模式 → 一种(全新 `messages[]`):概念清晰\n- Prompt cache 共享 → 省略:教学版不涉及 API 层优化\n- 递归 fork 防护 → 简化为\"子 Agent 无 `task` 工具\"\n- Async → 省略(留给 s13):s06 先理解同步模型\n\n \n\n\n"
+ "content": "# s06: Subagent — 大任务拆小,每个拿到的都是干净上下文\n\ns01 → s02 → s03 → s04 → s05 → `s06` → [s07](/zh/s07) → s08 → ... → s20\n\n> *\"大任务拆小, 每个小任务干净的上下文\"* — Subagent 用独立 `messages[]`, 不污染主对话。\n>\n> **Harness 层**: 子 Agent — 上下文隔离, 注意力不漂移。\n\n---\n\n## 问题\n\n上一章给 Agent 加了 `todo_write`,它能把大任务拆成一张清单,一步步推进。但拆出来的子任务,还是在同一个 `messages[]` 里跑。\n\nAgent 在修一个 bug:读了 30 个文件跟踪调用链,来回聊了 60 轮,`messages` 涨到 120 条。其中大半是\"跟踪调用链\"的中间过程,和\"修 bug\"这个最终目标已经没关系了,却还占着上下文。早期的关键信息被挤出有效窗口,模型对最初那个 bug 的描述,反而记不清了。\n\n换个角度想:你修 bug 时会\"开一个新终端\"去跟踪调用链,跟踪完把结果记下来,关掉终端,回到原来的终端接着修。Agent 也需要这个能力:开一个独立的子进程,给它一份独立的消息列表,让它专心做一件事,做完只把结论带回来。\n\n---\n\n## 解决方案\n\n\n\n一个符合直觉的做法,是让主 Agent 自己把调用链跟踪完、接着修。但跟踪的过程会全部留在主对话里,这正是上面那个问题。\n\n所以换个思路:**把脏活外包出去,只收一句结论。** 上一章的 hook 结构和 `todo_write` 都保留,本章新增一个 `task` 工具。主 Agent 调它时,spawn 一个子 Agent,给它全新的 `messages[]`,让它跑自己的循环;结束后只回传一段摘要文本,中间那 60 轮全部丢弃。对话上下文不进主 Agent,但子 Agent 在文件系统上做的实际改动(写文件、改文件、跑命令)会留在工作目录里。\n\n子 Agent 的工具是受限的:有 `bash`/`read`/`write`/`edit`/`glob`,但没有 `task`,不能再递归 spawn 新的子 Agent。而且它的每次工具调用仍走权限 hook,安全策略不因为上下文隔离就跳过。\n\n---\n\n## 工作原理\n\n子 Agent 本质上就是 s01 那个循环的另一份实例,只是换了一份空的 `messages[]` 和一套更窄的工具:\n\n```python\ndef spawn_subagent(description: str) -> str:\n # 子 Agent 的工具:基础工具,但没有 task(禁止递归)\n sub_tools = [\n {\"name\": \"bash\", ...}, {\"name\": \"read_file\", ...},\n {\"name\": \"write_file\", ...}, {\"name\": \"edit_file\", ...},\n {\"name\": \"glob\", ...},\n ]\n messages = [{\"role\": \"user\", \"content\": description}] # 全新 messages[]\n\n for _ in range(30): # safety limit\n response = client.messages.create(\n model=MODEL, system=SUB_SYSTEM,\n messages=messages, tools=sub_tools, max_tokens=8000,\n )\n messages.append({\"role\": \"assistant\", \"content\": response.content})\n if response.stop_reason != \"tool_use\":\n break\n results = []\n for block in response.content:\n if block.type == \"tool_use\":\n blocked = trigger_hooks(\"PreToolUse\", block)\n if blocked:\n results.append({... \"content\": str(blocked)})\n continue\n handler = SUB_HANDLERS.get(block.name)\n output = handler(**block.input) if handler else f\"Unknown\"\n trigger_hooks(\"PostToolUse\", block, output)\n results.append({... \"content\": output})\n messages.append({\"role\": \"user\", \"content\": results})\n\n # 只返回最后的文本结论,中间过程全部丢弃\n return extract_text(messages[-1][\"content\"])\n```\n\n注意这个循环里的几处关键点:工具集里没有 `task`(不能再 spawn,递归到此为止);`for _ in range(30)` 是循环轮次的安全上限(子 Agent 最多跑 30 轮,避免无限循环);每次工具调用前仍走 `PreToolUse` hook(隔离了上下文,但没隔离权限);最后只用 `extract_text(messages[-1])` 取一句结论,中间过程整份丢掉。\n\n主 Agent 这边,调它跟调其他工具完全一样:\n\n```python\nTOOLS = [\n {\"name\": \"bash\", ...},\n {\"name\": \"read_file\", ...},\n {\"name\": \"write_file\", ...},\n {\"name\": \"edit_file\", ...},\n {\"name\": \"glob\", ...},\n {\"name\": \"todo_write\", ...},\n # s06: 新增 task 工具\n {\"name\": \"task\",\n \"description\": \"Launch a subagent to handle a complex subtask. Returns only the final conclusion.\",\n \"input_schema\": {\"type\": \"object\", \"properties\": {\"description\": {\"type\": \"string\"}}, \"required\": [\"description\"]}},\n]\n\nTOOL_HANDLERS[\"task\"] = spawn_subagent\n```\n\n四个关键设计决策:\n\n| 决策 | 选择 | 原因 |\n|------|------|------|\n| 上下文隔离 | 全新 `messages[]` | 子 Agent 的中间过程不污染主 Agent 的上下文 |\n| 只回传结论 | `extract_text(last_message)` | 不是回传整个 `messages` 列表 |\n| 禁止递归 | 子 Agent 无 `task` 工具 | 防止子 Agent 再 spawn 新的子 Agent |\n| 安全策略不跳过 | 子 Agent 工具调用也走 PreToolUse hook | 上下文隔离不代表权限隔离 |\n\ndispatch 机制不变,`task` 工具通过 `TOOL_HANDLERS[block.name]` 分发。子 Agent 还有独立的 `SUB_SYSTEM` 提示,明确要求\"直接完成任务,不要再委派\"。\n\n---\n\n## 相对 s05 的变更\n\n| 组件 | 之前 (s05) | 之后 (s06) |\n|------|-----------|-----------|\n| 工具数量 | 6 (`bash`, `read`, `write`, `edit`, `glob`, `todo_write`) | 7 (+`task`) |\n| 新函数 | — | `spawn_subagent`(独立 `messages[]` + 30 轮安全限制) |\n| 上下文隔离 | 全部在主对话中 | 子 Agent 用全新的 `messages[]` |\n| 循环 | 不变 | dispatch 不变,子 Agent 有独立 `SUB_SYSTEM` 和 hook 保护的循环 |\n\n---\n\n## 试一下\n\n```sh\ncd learn-claude-code\npython s06_subagent/code.py\n```\n\n试试这些 prompt:\n\n1. `Use a subtask to find what testing framework this project uses`(子 Agent 去读文件,主 Agent 只收结论)\n2. `Delegate: read all .py files in agents/ and summarize what each one does`\n3. `Use a task to create s06_subagent/example/string_tools.py with a slugify(text: str) function, then verify it from the parent agent`\n\n观察重点:是否出现 `[Subagent spawned]` / `[Subagent done]`?子 Agent 的工具调用是否以 `[sub] ...` 输出?主 Agent 最后是否只继续处理子 Agent 返回的摘要?\n\n---\n\n## 接下来\n\nAgent 现在能拆任务了。但每个任务需要的知识不一样:改前端组件需要知道 React 规范,写 SQL 需要知道表结构。这些知识全塞进 system prompt,上下文有可能会直接被填满。\n\ns07 Skill Loading → 技能按需注入,不在 system prompt 里堆文档。用到的时候才加载,和读文件一样自然。\n\n\n深入 Claude Code 源码
\n\n> 以下基于 Claude Code 源码 `AgentTool.tsx`、`runAgent.ts`、`forkSubagent.ts`、`forkedAgent.ts` 的完整分析。\n\n### 一、不是一种模式,是三种\n\n教学版只讲了\"全新的 `messages[]`\"。Claude Code 实际有三种执行模式:\n\n| 模式 | 触发条件 | 上下文 |\n|------|---------|--------|\n| **Normal Subagent** | 指定了 `subagent_type`(normal path) | 全新 `messages[]`,只有 prompt |\n| **Fork Subagent** | 没指定 `subagent_type`,fork gate 开启 | 通过 `buildForkedMessages()` 构造 cache-friendly 前缀,共享 prompt cache |\n| **General-Purpose** | 没指定 `subagent_type`,fork gate 关闭 | 同 Normal |\n\n### 二、Fork 模式:为了共享 Prompt Cache\n\n这是教学版没有的核心概念。Fork 模式(`forkSubagent.ts:60-71`)不创建全新上下文,而是通过 `buildForkedMessages()`(`forkSubagent.ts:107-168`)构造 cache-friendly 消息前缀,保留父 assistant message 并生成 placeholder tool results。目的不是隔离,而是让 Anthropic API 的 prompt cache 命中:父子 Agent 的 system prompt、tools、messages 前缀完全一致,API 端不需要重算。\n\n缓存命中的五个关键组件(`forkedAgent.ts:57-68`):system prompt、tools、model、messages 前缀、thinking config,必须字节级一致。\n\n### 三、Context Isolation 的精确粒度\n\n`createSubagentContext()`(`forkedAgent.ts:345-462`)创建子 Agent 的 `ToolUseContext`:\n\n| 字段 | 行为 |\n|------|------|\n| `abortController` | 新的 child controller,父 abort 向下传播 |\n| `setAppState` | 默认 no-op;但 sync agent 通过 `shareSetAppState` 共享(`runAgent.ts:697-714`) |\n| `readFileState` | **从父克隆**(避免重复读相同文件) |\n| `queryTracking` | 新 chainId,`depth = parentDepth + 1` |\n\n子 Agent 不是完全隔离的:文件读取状态是共享的。UI 和通知的隔离程度取决于执行路径(sync/async/fork/teammate 各不同)。\n\n### 四、递归 Fork 防护\n\n教学版用\"子 Agent 不给 `task` 工具\"表达递归保护。真实实现更精细:`isInForkChild()`(`forkSubagent.ts:78-89`)检查对话历史中是否有 `FORK_BOILERPLATE_TAG`,有就拒绝。但 `constants/tools.ts:36-46` 中 `Agent` 工具默认在所有 agent 的禁用集合里,`USER_TYPE === 'ant'` 时例外;`forkSubagent.ts:73-89` 针对 fork child 有专门的递归保护;`agentToolUtils.ts:100-110` 在 teammate 场景下有特殊放行。不是简单的\"禁止新的子 Agent\"。\n\n### 五、Permission Bubbling\n\nFork Agent 的 `permissionMode: 'bubble'`(`forkSubagent.ts:67`)意味着子 Agent 的权限弹窗冒泡到父终端,用户在主终端里审批子 Agent 的操作。\n\n### 六、Async vs Sync\n\n教学版只展示了同步子 Agent(父等着子跑完)。Claude Code 还支持异步路径(`AgentTool.tsx:686-764`):`run_in_background: true` 时异步启动,返回 `{ status: 'async_launched' }` 立即给父 Agent,子 Agent 完成后通过通知机制告知父 Agent。实际触发条件不止 `run_in_background`,还有 auto-background、assistant force async、coordinator/proactive 等路径。\n\n### 教学版的简化是刻意的\n\n- 三种模式 → 一种(全新 `messages[]`):概念清晰\n- Prompt cache 共享 → 省略:教学版不涉及 API 层优化\n- 递归 fork 防护 → 简化为\"子 Agent 无 `task` 工具\"\n- Async → 省略(留给 s13):s06 先理解同步模型\n\n \n\n\n"
},
{
"version": "s06",
@@ -111,19 +111,19 @@
"version": "s07",
"locale": "en",
"title": "s07: Skill Loading — Load Only When Needed",
- "content": "# s07: Skill Loading — Load Only When Needed\n\ns01 → s02 → s03 → s04 → s05 → s06 → `s07` → [s08](/en/s08) → s09 → ... → s20\n> *\"Load when needed, don't stuff the prompt\"* — Inject via tool_result, not system prompt.\n>\n> **Harness Layer**: Knowledge — load on demand, don't fill the context.\n\n---\n\n## The Problem\n\nYour project has a React component spec, a SQL style guide, and an API design doc. You want the Agent to follow these specs automatically. The most straightforward idea: stuff them all into the system prompt:\n\n```python\nSYSTEM = (\n f\"You are a coding agent. \"\n + open(\"docs/react-style.md\").read() # 2000 lines\n + open(\"docs/sql-style.md\").read() # 1500 lines\n + open(\"docs/api-design.md\").read() # 3000 lines\n)\n```\n\n6500 lines of system prompt. The Agent carries these docs on every LLM call, whether it's changing a CSS color or fixing a SQL query. 99% of the content is irrelevant to the current task, burning tokens for nothing.\n\n---\n\n## The Solution\n\n\n\nThe minimal hook structure, `todo_write`, and sub-Agent from the previous chapter are preserved. This chapter focuses on the new `load_skill` tool. At startup, inject the skill catalog into the SYSTEM prompt; at runtime, register one more tool to load full content, spending tokens only when used.\n\nTwo-level design:\n\n| Level | Location | Timing | Cost |\n|-------|----------|--------|------|\n| 1. Catalog | system prompt | Injected at startup (harness scans skills/) | ~100 tokens/skill, carried every turn |\n| 2. Content | tool_result | When Agent calls load_skill; SKILL.md can guide later read_file/bash access to extra resources | ~2000 tokens/skill, on demand |\n\nThe dispatch mechanism is unchanged, `load_skill` auto-dispatches via `TOOL_HANDLERS[block.name]`.\n\n---\n\n## How It Works\n\n**skills/ directory**, one subdirectory per skill, each containing a `SKILL.md` file:\n\n```\nskills/\n agent-builder/SKILL.md\n code-review/SKILL.md\n mcp-builder/SKILL.md\n pdf/SKILL.md\n```\n\n**Level 1: Inject catalog at startup**: the harness calls `_scan_skills()` at startup to scan the skills/ directory, parsing each SKILL.md's YAML frontmatter (`name`, `description`) into a `SKILL_REGISTRY` dictionary. `list_skills()` generates the catalog from the registry, injected into the SYSTEM prompt. The Agent sees \"which skills I have available\" every turn, with no extra API calls:\n\n```python\nSKILL_REGISTRY: dict[str, dict] = {}\n\ndef _scan_skills():\n if not SKILLS_DIR.exists():\n return\n for d in sorted(SKILLS_DIR.iterdir()):\n if not d.is_dir():\n continue\n manifest = d / \"SKILL.md\"\n if manifest.exists():\n raw = manifest.read_text()\n meta, body = _parse_frontmatter(raw)\n name = meta.get(\"name\", d.name)\n desc = meta.get(\"description\", raw.split(\"\\n\")[0].lstrip(\"#\").strip())\n SKILL_REGISTRY[name] = {\"name\": name, \"description\": desc, \"content\": raw}\n\n_scan_skills() # runs once at startup\n\ndef list_skills() -> str:\n return \"\\n\".join(f\"- **{s['name']}**: {s['description']}\" for s in SKILL_REGISTRY.values())\n\ndef build_system() -> str:\n catalog = list_skills()\n return (\n f\"You are a coding agent at {WORKDIR}. \"\n f\"Skills available:\\n{catalog}\\n\"\n \"Use load_skill to get full details when needed.\"\n )\n\nSYSTEM = build_system()\n```\n\n**Level 2: load_skill**: the Agent decides \"I need the SQL style guide\" and calls `load_skill(\"sql-style\")`. Lookup goes through the registry, not file paths, eliminating path traversal risk. The SKILL.md content is injected via `tool_result`, and can include later access to referenced `references/`, `scripts/`, or `assets/` through the existing file and bash tools.\n\n```python\ndef load_skill(name: str) -> str:\n skill = SKILL_REGISTRY.get(name)\n if not skill:\n return f\"Skill not found: {name}\"\n return skill[\"content\"]\n```\n\nThe key distinction: skill content is not part of the system prompt. It enters the current messages as a tool result. Subsequent calls carry it along with the history until context compaction, truncation, or session end. This naturally connects to s08's compact: on-demand loading keeps irrelevant docs out of the system prompt, compact solves \"how to drop what you should.\"\n\n---\n\n## Changes from s06\n\n| Component | Before (s06) | After (s07) |\n|-----------|-------------|-------------|\n| Tool count | 7 (bash, read, write, edit, glob, todo_write, task) | 8 (+load_skill) |\n| Knowledge loading | None | Two-level: startup catalog in SYSTEM + runtime load_skill; SKILL.md may guide later resource access |\n| SYSTEM prompt | Static string | Startup scan of skills/ injects catalog |\n| Skill registry | None | SKILL_REGISTRY (populated at startup, prevents path traversal) |\n| Loop | Unchanged | Unchanged (skill tool auto-dispatches) |\n\n---\n\n## Try It\n\n```sh\ncd learn-claude-code\npython s07_skill_loading/code.py\n```\n\nTry these prompts:\n\n1. `What skills are available?`\n2. `Load the code-review skill and follow its instructions`\n3. `I need to do a code review -- load the relevant skill first`\n\nWhat to watch for: Does the Agent know available skills from the SYSTEM catalog? Does `[HOOK] load_skill` appear when full instructions are needed? Does the answer use the loaded skill's instructions?\n\n---\n\n## What's Next\n\nload_skill eliminated the startup token waste. But another problem looms: after the Agent works for 30 minutes, the messages list fills up with intermediate process. Old tool_results, stale file contents, occupying context but adding no value.\n\n→ s08 Context Compact: A four-layer compaction strategy. Cheap layers run first, expensive layers run last.\n\n\nDive into Claude Code Source Code
\n\n> The following is based on analysis of Claude Code source code `loadSkillsDir.ts`, `SkillTool.ts`, `bundledSkills.ts`, `commands.ts`.\n\n### 1. Skill Sources: Not Just One skills/ Directory\n\nThe teaching version assumes all skills live in a `skills/` directory. Claude Code loads from multiple sources spread across multiple files: `loadSkillsDir.ts` handles user/project/`--add-dir` directories and legacy commands (`.claude/commands/`); `bundledSkills.ts` handles built-in skills; `SkillTool.ts` handles MCP remote skills; `commands.ts` handles command aggregation. Types include managed/policy skills, user skills (`~/.claude/skills/`), project skills (`.claude/skills/`), `--add-dir` skills, legacy commands, dynamic skills, conditional skills (with `paths` frontmatter, activated by file path), bundled skills, plugin skills, MCP skills.\n\n### 2. SKILL.md Frontmatter — Common Fields\n\nClaude Code's SKILL.md YAML frontmatter is parsed by `parseSkillFrontmatterFields()` in `loadSkillsDir.ts`. Common fields include:\n\n| Field | Purpose |\n|-------|---------|\n| `name` / `description` | Display name and description |\n| `when_to_use` | Guides the model on when to invoke |\n| `allowed-tools` | Auto-allow list of tools available to the skill |\n| `context` | `inline` (default) or `fork` (run as sub-Agent) |\n| `model` | Model override (haiku/sonnet/opus/inherit) |\n| `hooks` | Skill-level hook configuration |\n| `paths` | Glob patterns for conditional activation |\n| `user-invocable` | Users can invoke via `/name` |\n\nThe complete field list changes across versions; above are the core fields relevant to the teaching version.\n\n### 3. Precise Implementation of Two-Level Loading\n\n1. **Catalog (at startup)**: `getSkillDirCommands()` scans directory → registers as `Command` objects containing only metadata. `getSkillListingAttachments()` formats the skill list as attachments, budgeted at ~1% of the context window (cap 8000 characters).\n2. **Load (on invocation)**: Model calls `Skill` tool (input fields are `skill` + optional `args`; teaching version uses `name`) → `getPromptForCommand()` expands full SKILL.md content → `SkillTool` returns a tool_result with display text `\"Launching skill: {name}\"`, while the actual skill content is injected via `newMessages`. The teaching version merges both into \"injected via tool_result\" as a simplification; the loaded SKILL.md can still guide later access to referenced resources through existing file/bash tools.\n\n### The Teaching Version's Simplification Is Intentional\n\n- Multiple files and sources → 1 `skills/` directory: sufficient to demonstrate the core concept of two-level loading\n- Multiple frontmatter fields → only parse name/description: reduces parsing complexity\n- Forked skills (`context: 'fork'`) → omitted: the teaching version only expands inline skill loading\n- `Skill` tool input `skill`+`args` → teaching version uses `name`: avoids extra argument parsing complexity\n\n \n\n\n"
+ "content": "# s07: Skill Loading — Load Only When Needed\n\ns01 → s02 → s03 → s04 → s05 → s06 → `s07` → [s08](/en/s08) → s09 → ... → s20\n> *\"Load when needed, don't stuff the prompt\"* — Inject via `tool_result`, not system prompt.\n>\n> **Harness Layer**: Knowledge — load on demand, don't fill the context.\n\n---\n\n## The Problem\n\nThe last chapter let the Agent hand a big task off to a sub-Agent. But when the sub-Agent takes over, it needs to know the rules of that task: editing React components means following the component spec, writing SQL means following the style guide. Where do those rules come from?\n\nYour project has a React component spec, a SQL style guide, and an API design doc. The most straightforward idea: stuff them all into the system prompt:\n\n```python\nSYSTEM = (\n f\"You are a coding agent. \"\n + open(\"docs/react-style.md\").read() # 2000 lines\n + open(\"docs/sql-style.md\").read() # 1500 lines\n + open(\"docs/api-design.md\").read() # 3000 lines\n)\n```\n\n6500 lines of system prompt. The Agent carries these docs on every LLM call, whether it's changing a CSS color or fixing a SQL query. 99% of the content is irrelevant to the current task, burning tokens for nothing.\n\n---\n\n## The Solution\n\n\n\nA middle-ground idea is to split the docs into separate files and let the Agent `read_file` whichever it needs. But the Agent has no idea which files exist to read; it has to know \"what's there\" before it can pick \"which one to use\".\n\nSo split it into two levels: **catalog always resident, content on demand.** The hook structure, `todo_write`, and sub-Agent from the previous chapter stay; this chapter adds a `load_skill` tool. At startup, the skill catalog (name + one-line description) goes into the SYSTEM prompt, carried every turn but light; at runtime, when the Agent actually needs a skill, it calls `load_skill` to pull the full content, spending those tokens only then.\n\nTwo-level design:\n\n| Level | Location | Timing | Cost |\n|-------|----------|--------|------|\n| 1. Catalog | system prompt | Injected at startup (harness scans skills/) | ~100 tokens/skill, carried every turn |\n| 2. Content | tool_result | When Agent calls load_skill; SKILL.md can guide later read_file/bash access to extra resources | ~2000 tokens/skill, on demand |\n\nThe dispatch mechanism is unchanged, `load_skill` auto-dispatches via `TOOL_HANDLERS[block.name]`.\n\n---\n\n## How It Works\n\n**skills/ directory**, one subdirectory per skill, each containing a `SKILL.md` file:\n\n```\nskills/\n agent-builder/SKILL.md\n code-review/SKILL.md\n mcp-builder/SKILL.md\n pdf/SKILL.md\n```\n\n**Level 1: Inject catalog at startup**: the harness calls `_scan_skills()` at startup to scan the skills/ directory, parsing each SKILL.md's YAML frontmatter (`name`, `description`) into a `SKILL_REGISTRY` dictionary. `list_skills()` generates the catalog from the registry, injected into the SYSTEM prompt. The Agent sees \"which skills I have available\" every turn, with no extra API calls:\n\n```python\nSKILL_REGISTRY: dict[str, dict] = {}\n\ndef _scan_skills():\n if not SKILLS_DIR.exists():\n return\n for d in sorted(SKILLS_DIR.iterdir()):\n if not d.is_dir():\n continue\n manifest = d / \"SKILL.md\"\n if manifest.exists():\n raw = manifest.read_text()\n meta, body = _parse_frontmatter(raw)\n name = meta.get(\"name\", d.name)\n desc = meta.get(\"description\", raw.split(\"\\n\")[0].lstrip(\"#\").strip())\n SKILL_REGISTRY[name] = {\"name\": name, \"description\": desc, \"content\": raw}\n\n_scan_skills() # runs once at startup\n\ndef list_skills() -> str:\n return \"\\n\".join(f\"- **{s['name']}**: {s['description']}\" for s in SKILL_REGISTRY.values())\n\ndef build_system() -> str:\n catalog = list_skills()\n return (\n f\"You are a coding agent at {WORKDIR}. \"\n f\"Skills available:\\n{catalog}\\n\"\n \"Use load_skill to get full details when needed.\"\n )\n\nSYSTEM = build_system()\n```\n\nBut every turn the Agent only gets the name and one-line description; to actually use the SQL style guide, those 1500 lines of full content are still out of reach. → Level 2.\n\n**Level 2: load_skill**: the Agent decides \"I need the SQL style guide\" and calls `load_skill(\"sql-style\")`. Lookup goes through the registry, not file paths, eliminating path traversal risk. The SKILL.md content is injected via `tool_result`, and can include later access to referenced `references/`, `scripts/`, or `assets/` through the existing file and bash tools.\n\n```python\ndef load_skill(name: str) -> str:\n skill = SKILL_REGISTRY.get(name)\n if not skill:\n return f\"Skill not found: {name}\"\n return skill[\"content\"]\n```\n\nThe key distinction: skill content is not part of the system prompt. It enters the current `messages` as a `tool_result`. Subsequent calls carry it along with the history until context compaction, truncation, or session end. This naturally connects to s08's compact: skill content enters `messages` as a `tool_result` rather than the system prompt, and compact solves \"how to drop what you should.\"\n\n---\n\n## Changes from s06\n\n| Component | Before (s06) | After (s07) |\n|-----------|-------------|-------------|\n| Tool count | 7 (bash, read, write, edit, glob, todo_write, task) | 8 (+load_skill) |\n| Knowledge loading | None | Two-level: startup catalog in SYSTEM + runtime load_skill; SKILL.md may guide later resource access |\n| SYSTEM prompt | Static string | Startup scan of skills/ injects catalog |\n| Skill registry | None | SKILL_REGISTRY (populated at startup, prevents path traversal) |\n| Loop | Unchanged | Unchanged (skill tool auto-dispatches) |\n\n---\n\n## Try It\n\n```sh\ncd learn-claude-code\npython s07_skill_loading/code.py\n```\n\nTry these prompts:\n\n1. `What skills are available?`\n2. `Load the code-review skill and follow its instructions`\n3. `I need to do a code review -- load the relevant skill first`\n\nWhat to watch for: Does the Agent know available skills from the SYSTEM catalog? Does `[HOOK] load_skill` appear when full instructions are needed? Does the answer use the loaded skill's instructions?\n\n---\n\n## What's Next\n\nload_skill eliminated the startup token waste. But another problem looms: after the Agent works for 30 minutes, the `messages` list fills up with intermediate process. Old `tool_result`s, stale file contents, occupying context but adding no value.\n\n→ s08 Context Compact: A four-layer compaction strategy. Cheap layers run first, expensive layers run last.\n\n\nDive into Claude Code Source Code
\n\n> The following is based on analysis of Claude Code source code `loadSkillsDir.ts`, `SkillTool.ts`, `bundledSkills.ts`, `commands.ts`.\n\n### 1. Skill Sources: Not Just One skills/ Directory\n\nThe teaching version assumes all skills live in a `skills/` directory. Claude Code loads from multiple sources spread across multiple files: `loadSkillsDir.ts` handles user/project/`--add-dir` directories and legacy commands (`.claude/commands/`); `bundledSkills.ts` handles built-in skills; `SkillTool.ts` handles MCP remote skills; `commands.ts` handles command aggregation. Types include managed/policy skills, user skills (`~/.claude/skills/`), project skills (`.claude/skills/`), `--add-dir` skills, legacy commands, dynamic skills, conditional skills (with `paths` frontmatter, activated by file path), bundled skills, plugin skills, MCP skills.\n\n### 2. SKILL.md Frontmatter — Common Fields\n\nClaude Code's SKILL.md YAML frontmatter is parsed by `parseSkillFrontmatterFields()` in `loadSkillsDir.ts`. Common fields include:\n\n| Field | Purpose |\n|-------|---------|\n| `name` / `description` | Display name and description |\n| `when_to_use` | Guides the model on when to invoke |\n| `allowed-tools` | Auto-allow list of tools available to the skill |\n| `context` | `inline` (default) or `fork` (run as sub-Agent) |\n| `model` | Model override (haiku/sonnet/opus/inherit) |\n| `hooks` | Skill-level hook configuration |\n| `paths` | Glob patterns for conditional activation |\n| `user-invocable` | Users can invoke via `/name` |\n\nThe complete field list changes across versions; above are the core fields relevant to the teaching version.\n\n### 3. Precise Implementation of Two-Level Loading\n\n1. **Catalog (at startup)**: `getSkillDirCommands()` scans directory → registers as `Command` objects containing only metadata. `getSkillListingAttachments()` formats the skill list as attachments, budgeted at ~1% of the context window (cap 8000 characters).\n2. **Load (on invocation)**: Model calls `Skill` tool (input fields are `skill` + optional `args`; teaching version uses `name`) → `getPromptForCommand()` expands full SKILL.md content → `SkillTool` returns a tool_result with display text `\"Launching skill: {name}\"`, while the actual skill content is injected via `newMessages`. The teaching version merges both into \"injected via tool_result\" as a simplification; the loaded SKILL.md can still guide later access to referenced resources through existing file/bash tools.\n\n### The Teaching Version's Simplification Is Intentional\n\n- Multiple files and sources → 1 `skills/` directory: sufficient to demonstrate the core concept of two-level loading\n- Multiple frontmatter fields → only parse name/description: reduces parsing complexity\n- Forked skills (`context: 'fork'`) → omitted: the teaching version only expands inline skill loading\n- `Skill` tool input `skill`+`args` → teaching version uses `name`: avoids extra argument parsing complexity\n\n \n\n\n"
},
{
"version": "s07",
"locale": "zh",
"title": "s07: Skill Loading — 用到的时候才加载",
- "content": "# s07: Skill Loading — 用到的时候才加载\n\ns01 → s02 → s03 → s04 → s05 → s06 → `s07` → [s08](/zh/s08) → s09 → ... → s20\n> *\"用到时再加载, 别全塞 prompt 里\"* — 通过 tool_result 注入, 不塞 system prompt。\n>\n> **Harness 层**: 知识 — 按需加载, 不堆满上下文。\n\n---\n\n## 问题\n\n你的项目有一套 React 组件规范、一份 SQL 风格指南、一份 API 设计文档。你希望 Agent 自动遵守这些规范。最直接的想法,全塞进 system prompt:\n\n```python\nSYSTEM = (\n f\"You are a coding agent. \"\n + open(\"docs/react-style.md\").read() # 2000 行\n + open(\"docs/sql-style.md\").read() # 1500 行\n + open(\"docs/api-design.md\").read() # 3000 行\n)\n```\n\n6500 行 system prompt。Agent 每次调用 LLM 都带着这些文档——不管是在改 CSS 颜色还是修 SQL 查询。99% 的内容和当前任务无关,白白消耗 token。\n\n---\n\n## 解决方案\n\n\n\n保留上一章的最小 hook 结构、`todo_write` 和子 Agent,本章重点转向新增的 `load_skill` 工具。启动时把技能目录注入 SYSTEM prompt,运行时多注册一个工具加载完整内容,用到才花 token。\n\n两层设计:\n\n| 层 | 位置 | 时机 | 代价 |\n|---|------|------|------|\n| 1. 目录 | system prompt | 启动时注入(harness 扫描 skills/) | ~100 tokens/skill,每轮都带 |\n| 2. 内容 | tool_result | Agent 调用 load_skill 时;SKILL.md 可指引后续的 read_file/bash 调用,用于按需访问额外资源 | ~2000 tokens/skill,按需 |\n\ndispatch 机制不变,load_skill 通过 `TOOL_HANDLERS[block.name]` 分发。\n\n---\n\n## 工作原理\n\n**skills/ 目录**,每个技能一个子目录,包含 `SKILL.md` 文件:\n\n```\nskills/\n agent-builder/SKILL.md\n code-review/SKILL.md\n mcp-builder/SKILL.md\n pdf/SKILL.md\n```\n\n**第一级:启动时注入目录**:harness 启动时调用 `_scan_skills()` 扫描 skills/ 目录,解析每个 SKILL.md 的 YAML frontmatter(`name`、`description`),存入 `SKILL_REGISTRY` 字典。`list_skills()` 从注册表生成目录,注入 SYSTEM prompt。Agent 每轮都能看到\"我有哪些技能可用\",不花额外 API 调用:\n\n```python\nSKILL_REGISTRY: dict[str, dict] = {}\n\ndef _scan_skills():\n if not SKILLS_DIR.exists():\n return\n for d in sorted(SKILLS_DIR.iterdir()):\n if not d.is_dir():\n continue\n manifest = d / \"SKILL.md\"\n if manifest.exists():\n raw = manifest.read_text()\n meta, body = _parse_frontmatter(raw)\n name = meta.get(\"name\", d.name)\n desc = meta.get(\"description\", raw.split(\"\\n\")[0].lstrip(\"#\").strip())\n SKILL_REGISTRY[name] = {\"name\": name, \"description\": desc, \"content\": raw}\n\n_scan_skills() # runs once at startup\n\ndef list_skills() -> str:\n return \"\\n\".join(f\"- **{s['name']}**: {s['description']}\" for s in SKILL_REGISTRY.values())\n\ndef build_system() -> str:\n catalog = list_skills()\n return (\n f\"You are a coding agent at {WORKDIR}. \"\n f\"Skills available:\\n{catalog}\\n\"\n \"Use load_skill to get full details when needed.\"\n )\n\nSYSTEM = build_system()\n```\n\n**第二级:load_skill**:Agent 决定\"我需要 SQL 风格指南\",调用 `load_skill(\"sql-style\")`。通过注册表查找,不走文件路径,没有路径遍历风险。SKILL.md 内容通过 `tool_result` 注入,并可通过现有的 file 和 bash 工具进一步访问引用的 `references/`、`scripts/` 或 `assets/`。\n\n```python\ndef load_skill(name: str) -> str:\n skill = SKILL_REGISTRY.get(name)\n if not skill:\n return f\"Skill not found: {name}\"\n return skill[\"content\"]\n```\n\n关键区别:技能内容不是 system prompt 的一部分,它作为一次工具结果进入当前 messages。后续调用会随历史一起携带,直到上下文压缩、截断或会话结束。这和 s08 的 compact 自然衔接:技能内容以 tool_result 形式进入 messages,不塞 system prompt,compact 解决\"该丢的怎么丢\"。\n\n---\n\n## 相对 s06 的变更\n\n| 组件 | 之前 (s06) | 之后 (s07) |\n|------|-----------|-----------|\n| 工具数量 | 7 (bash, read, write, edit, glob, todo_write, task) | 8 (+load_skill) |\n| 知识加载 | 无 | 两级:启动时目录注入 SYSTEM + 运行时 load_skill;SKILL.md 可指引后续资源访问 |\n| SYSTEM 提示 | 静态字符串 | 启动时扫描 skills/ 注入目录 |\n| 技能注册表 | 无 | SKILL_REGISTRY(启动时填充,防路径遍历) |\n| 循环 | 不变 | 不变(skill 工具自动分发) |\n\n---\n\n## 试一下\n\n```sh\ncd learn-claude-code\npython s07_skill_loading/code.py\n```\n\n试试这些 prompt:\n\n1. `What skills are available?`\n2. `Load the code-review skill and follow its instructions`\n3. `I need to do a code review -- load the relevant skill first`\n\n观察重点:Agent 是否直接从 SYSTEM 里的目录知道有哪些技能?需要完整规范时是否出现 `[HOOK] load_skill`?加载后回答是否使用了对应 skill 的说明?\n\n---\n\n## 接下来\n\nload_skill 解决了启动时的 token 浪费。但另一个问题来了:Agent 连续工作 30 分钟后,messages 列表塞满了中间过程。旧的 tool_result、过时的文件内容,占着上下文但不产生价值。\n\ns08 Context Compact → 四层压缩策略。便宜的先跑,贵的后跑。\n\n\n深入 Claude Code 源码
\n\n> 以下基于 Claude Code 源码 `loadSkillsDir.ts`、`SkillTool.ts`、`bundledSkills.ts`、`commands.ts` 的分析。\n\n### 一、技能来源:不是只有一个 skills/ 目录\n\n教学版假设所有技能在 `skills/` 目录下。Claude Code 实际从多个来源加载,分布在多个文件中:`loadSkillsDir.ts` 负责从 user/project/`--add-dir` 目录和 legacy commands(`.claude/commands/`)加载;`bundledSkills.ts` 负责内置技能;`SkillTool.ts` 处理 MCP 远程技能;`commands.ts` 负责命令聚合。类型包括 managed/policy skills、user skills(`~/.claude/skills/`)、project skills(`.claude/skills/`)、`--add-dir` skills、legacy commands、dynamic skills、conditional skills(带 `paths` frontmatter,按文件路径激活)、bundled skills、plugin skills、MCP skills。\n\n### 二、SKILL.md Frontmatter 常见字段\n\nClaude Code 的 SKILL.md YAML frontmatter 由 `parseSkillFrontmatterFields()` 解析(`loadSkillsDir.ts`),常见字段包括:\n\n| 字段 | 用途 |\n|------|------|\n| `name` / `description` | 显示名称和描述 |\n| `when_to_use` | 指导模型何时调用 |\n| `allowed-tools` | 技能可用工具的自动允许列表 |\n| `context` | `inline`(默认)或 `fork`(作为子 Agent 运行) |\n| `model` | 模型覆盖(haiku/sonnet/opus/inherit) |\n| `hooks` | 技能级别的 hook 配置 |\n| `paths` | 条件激活的 glob 模式 |\n| `user-invocable` | 用户可以通过 `/name` 调用 |\n\n完整字段列表随版本迭代会变化,以上仅列出教学版涉及的核心字段。\n\n### 三、两级加载的精确实现\n\n1. **Catalog(启动时)**:`getSkillDirCommands()` 扫描目录 → 注册为 `Command` 对象,只包含元数据。`getSkillListingAttachments()` 把技能列表格式化为附件,预算为上下文窗口的 ~1%(上限 8000 字符)。\n2. **Load(调用时)**:模型调 `Skill` 工具(输入字段是 `skill` + 可选 `args`,教学版用 `name`)→ `getPromptForCommand()` 展开完整 SKILL.md 内容 → `SkillTool` 返回的 tool_result 展示文本只是 `\"Launching skill: {name}\"`,真正的技能内容通过 `newMessages` 注入对话。教学版把两者合并为\"通过 tool_result 注入\"是一种简化;加载后的 SKILL.md 仍可作为指引,帮助模型后续通过现有 file/bash 工具访问相关资源。\n\n### 教学版的简化是刻意的\n\n- 多文件多来源 → 1 个 `skills/` 目录:足以展示两级加载的核心概念\n- 多个 frontmatter 字段 → 只解析 name/description:减少解析复杂度\n- forked skills(`context: 'fork'`)→ 省略:教学版只展开 inline 技能加载\n- `Skill` 工具输入 `skill`+`args` → 教学版用 `name`:避免参数解析的额外复杂度\n\n \n\n\n"
+ "content": "# s07: Skill Loading — 用到的时候才加载\n\ns01 → s02 → s03 → s04 → s05 → s06 → `s07` → [s08](/zh/s08) → s09 → ... → s20\n> *\"用到时再加载, 别全塞 prompt 里\"* — 通过 `tool_result` 注入, 不塞 system prompt。\n>\n> **Harness 层**: 知识 — 按需加载, 不堆满上下文。\n\n---\n\n## 问题\n\n上一章 Agent 能把大任务拆给子 Agent 了。但子 Agent 接手时,得知道这个任务的规矩:改 React 组件要守组件规范,写 SQL 要守风格指南。这些规范从哪来?\n\n你的项目里有一套 React 组件规范、一份 SQL 风格指南、一份 API 设计文档。最直接的想法,全塞进 system prompt:\n\n```python\nSYSTEM = (\n f\"You are a coding agent. \"\n + open(\"docs/react-style.md\").read() # 2000 行\n + open(\"docs/sql-style.md\").read() # 1500 行\n + open(\"docs/api-design.md\").read() # 3000 行\n)\n```\n\n6500 行 system prompt。Agent 每次调用 LLM 都带着这些文档,不管在改 CSS 颜色还是修 SQL 查询。99% 的内容和当前任务无关,白白消耗 token。\n\n---\n\n## 解决方案\n\n\n\n一个折中的想法,是把文档拆成几个文件,用哪个让 Agent 自己 `read_file`。但 Agent 根本不知道有哪些文件可读,它得先知道\"有什么\",才谈得上\"用哪个\"。\n\n所以分两层:**目录常驻,内容按需。** 上一章的 hook 结构、`todo_write` 和子 Agent 都保留,本章新增一个 `load_skill` 工具。启动时把技能目录(名字 + 一句话描述)注入 SYSTEM prompt,每轮都带、但很轻;运行时 Agent 真要用某个技能,再调 `load_skill` 把完整内容取过来,用到才花那部分 token。\n\n两层设计:\n\n| 层 | 位置 | 时机 | 代价 |\n|---|------|------|------|\n| 1. 目录 | system prompt | 启动时注入(harness 扫描 skills/) | ~100 tokens/skill,每轮都带 |\n| 2. 内容 | tool_result | Agent 调用 load_skill 时;SKILL.md 可指引后续的 read_file/bash 调用,用于按需访问额外资源 | ~2000 tokens/skill,按需 |\n\ndispatch 机制不变,`load_skill` 通过 `TOOL_HANDLERS[block.name]` 分发。\n\n---\n\n## 工作原理\n\n**skills/ 目录**,每个技能一个子目录,包含 `SKILL.md` 文件:\n\n```\nskills/\n agent-builder/SKILL.md\n code-review/SKILL.md\n mcp-builder/SKILL.md\n pdf/SKILL.md\n```\n\n**第一级:启动时注入目录**:harness 启动时调用 `_scan_skills()` 扫描 skills/ 目录,解析每个 SKILL.md 的 YAML frontmatter(`name`、`description`),存入 `SKILL_REGISTRY` 字典。`list_skills()` 从注册表生成目录,注入 SYSTEM prompt。Agent 每轮都能看到\"我有哪些技能可用\",不花额外 API 调用:\n\n```python\nSKILL_REGISTRY: dict[str, dict] = {}\n\ndef _scan_skills():\n if not SKILLS_DIR.exists():\n return\n for d in sorted(SKILLS_DIR.iterdir()):\n if not d.is_dir():\n continue\n manifest = d / \"SKILL.md\"\n if manifest.exists():\n raw = manifest.read_text()\n meta, body = _parse_frontmatter(raw)\n name = meta.get(\"name\", d.name)\n desc = meta.get(\"description\", raw.split(\"\\n\")[0].lstrip(\"#\").strip())\n SKILL_REGISTRY[name] = {\"name\": name, \"description\": desc, \"content\": raw}\n\n_scan_skills() # runs once at startup\n\ndef list_skills() -> str:\n return \"\\n\".join(f\"- **{s['name']}**: {s['description']}\" for s in SKILL_REGISTRY.values())\n\ndef build_system() -> str:\n catalog = list_skills()\n return (\n f\"You are a coding agent at {WORKDIR}. \"\n f\"Skills available:\\n{catalog}\\n\"\n \"Use load_skill to get full details when needed.\"\n )\n\nSYSTEM = build_system()\n```\n\n但 Agent 每轮只拿到名字和一句话描述,真要用 SQL 风格指南,那 1500 行的完整内容还够不到。→ 第二级。\n\n**第二级:load_skill**:Agent 决定\"我需要 SQL 风格指南\",调用 `load_skill(\"sql-style\")`。通过注册表查找,不走文件路径,没有路径遍历风险。SKILL.md 内容通过 `tool_result` 注入,并可通过现有的 file 和 bash 工具进一步访问引用的 `references/`、`scripts/` 或 `assets/`。\n\n```python\ndef load_skill(name: str) -> str:\n skill = SKILL_REGISTRY.get(name)\n if not skill:\n return f\"Skill not found: {name}\"\n return skill[\"content\"]\n```\n\n关键区别:技能内容不是 system prompt 的一部分,它作为一次工具结果进入当前 `messages`。后续调用会随历史一起携带,直到上下文压缩、截断或会话结束。这和 s08 的 compact 自然衔接:技能内容以 `tool_result` 形式进入 `messages`,不塞 system prompt,compact 解决\"该丢的怎么丢\"。\n\n---\n\n## 相对 s06 的变更\n\n| 组件 | 之前 (s06) | 之后 (s07) |\n|------|-----------|-----------|\n| 工具数量 | 7 (bash, read, write, edit, glob, todo_write, task) | 8 (+load_skill) |\n| 知识加载 | 无 | 两级:启动时目录注入 SYSTEM + 运行时 load_skill;SKILL.md 可指引后续资源访问 |\n| SYSTEM 提示 | 静态字符串 | 启动时扫描 skills/ 注入目录 |\n| 技能注册表 | 无 | SKILL_REGISTRY(启动时填充,防路径遍历) |\n| 循环 | 不变 | 不变(skill 工具自动分发) |\n\n---\n\n## 试一下\n\n```sh\ncd learn-claude-code\npython s07_skill_loading/code.py\n```\n\n试试这些 prompt:\n\n1. `What skills are available?`\n2. `Load the code-review skill and follow its instructions`\n3. `I need to do a code review -- load the relevant skill first`\n\n观察重点:Agent 是否直接从 SYSTEM 里的目录知道有哪些技能?需要完整规范时是否出现 `[HOOK] load_skill`?加载后回答是否使用了对应 skill 的说明?\n\n---\n\n## 接下来\n\n`load_skill` 解决了启动时的 token 浪费。但另一个问题来了:Agent 连续工作 30 分钟后,`messages` 列表塞满了中间过程。旧的 `tool_result`、过时的文件内容,占着上下文但不产生价值。\n\ns08 Context Compact → 四层压缩策略。便宜的先跑,贵的后跑。\n\n\n深入 Claude Code 源码
\n\n> 以下基于 Claude Code 源码 `loadSkillsDir.ts`、`SkillTool.ts`、`bundledSkills.ts`、`commands.ts` 的分析。\n\n### 一、技能来源:不是只有一个 skills/ 目录\n\n教学版假设所有技能在 `skills/` 目录下。Claude Code 实际从多个来源加载,分布在多个文件中:`loadSkillsDir.ts` 负责从 user/project/`--add-dir` 目录和 legacy commands(`.claude/commands/`)加载;`bundledSkills.ts` 负责内置技能;`SkillTool.ts` 处理 MCP 远程技能;`commands.ts` 负责命令聚合。类型包括 managed/policy skills、user skills(`~/.claude/skills/`)、project skills(`.claude/skills/`)、`--add-dir` skills、legacy commands、dynamic skills、conditional skills(带 `paths` frontmatter,按文件路径激活)、bundled skills、plugin skills、MCP skills。\n\n### 二、SKILL.md Frontmatter 常见字段\n\nClaude Code 的 SKILL.md YAML frontmatter 由 `parseSkillFrontmatterFields()` 解析(`loadSkillsDir.ts`),常见字段包括:\n\n| 字段 | 用途 |\n|------|------|\n| `name` / `description` | 显示名称和描述 |\n| `when_to_use` | 指导模型何时调用 |\n| `allowed-tools` | 技能可用工具的自动允许列表 |\n| `context` | `inline`(默认)或 `fork`(作为子 Agent 运行) |\n| `model` | 模型覆盖(haiku/sonnet/opus/inherit) |\n| `hooks` | 技能级别的 hook 配置 |\n| `paths` | 条件激活的 glob 模式 |\n| `user-invocable` | 用户可以通过 `/name` 调用 |\n\n完整字段列表随版本迭代会变化,以上仅列出教学版涉及的核心字段。\n\n### 三、两级加载的精确实现\n\n1. **Catalog(启动时)**:`getSkillDirCommands()` 扫描目录 → 注册为 `Command` 对象,只包含元数据。`getSkillListingAttachments()` 把技能列表格式化为附件,预算为上下文窗口的 ~1%(上限 8000 字符)。\n2. **Load(调用时)**:模型调 `Skill` 工具(输入字段是 `skill` + 可选 `args`,教学版用 `name`)→ `getPromptForCommand()` 展开完整 SKILL.md 内容 → `SkillTool` 返回的 `tool_result` 展示文本只是 `\"Launching skill: {name}\"`,真正的技能内容通过 `newMessages` 注入对话。教学版把两者合并为\"通过 tool_result 注入\"是一种简化;加载后的 SKILL.md 仍可作为指引,帮助模型后续通过现有 file/bash 工具访问相关资源。\n\n### 教学版的简化是刻意的\n\n- 多文件多来源 → 1 个 `skills/` 目录:足以展示两级加载的核心概念\n- 多个 frontmatter 字段 → 只解析 name/description:减少解析复杂度\n- forked skills(`context: 'fork'`)→ 省略:教学版只展开 inline 技能加载\n- `Skill` 工具输入 `skill`+`args` → 教学版用 `name`:避免参数解析的额外复杂度\n\n \n\n\n"
},
{
"version": "s07",
"locale": "ja",
"title": "s07: Skill Loading — 必要なときにだけ読み込む",
- "content": "# s07: Skill Loading — 必要なときにだけ読み込む\n\ns01 → s02 → s03 → s04 → s05 → s06 → `s07` → [s08](/ja/s08) → s09 → ... → s20\n> *\"Load when needed, don't stuff the prompt\"* — tool_result で注入、system prompt には詰め込まない。\n>\n> **Harness レイヤー**: 知識 — 必要に応じて読み込み、コンテキストに詰め込まない。\n\n---\n\n## 課題\n\nプロジェクトには React コンポーネント仕様、SQL スタイルガイド、API 設計ドキュメントがある。Agent にこれらの仕様を自動的に守らせたい。最も直接的な方法は、すべて system prompt に詰め込むこと:\n\n```python\nSYSTEM = (\n f\"You are a coding agent. \"\n + open(\"docs/react-style.md\").read() # 2000 行\n + open(\"docs/sql-style.md\").read() # 1500 行\n + open(\"docs/api-design.md\").read() # 3000 行\n)\n```\n\n6500 行の system prompt。Agent は LLM を呼び出すたびにこれらのドキュメントを運ぶ。CSS の色を変えるときも SQL クエリを修正するときも同様だ。99% の内容が現在のタスクと無関係で、トークンを無駄に消費する。\n\n---\n\n## ソリューション\n\n\n\n前章の最小フック構造、`todo_write`、サブ Agent を維持し、本章は新規の `load_skill` ツールに注目する。起動時にスキルカタログを SYSTEM prompt に注入し、実行時に完全な内容を読み込むツールを登録する。使ったときだけトークンを消費。\n\n2 層設計:\n\n| 層 | 場所 | タイミング | コスト |\n|---|------|-----------|--------|\n| 1. カタログ | system prompt | 起動時に注入(harness が skills/ をスキャン) | ~100 トークン/スキル、毎ターン携帯 |\n| 2. 内容 | tool_result | Agent が load_skill を呼び出したとき。SKILL.md は、必要に応じて read_file/bash で追加リソースへアクセスするための手がかりになる | ~2000 トークン/スキル、オンデマンド |\n\nディスパッチ機構は変わらず、`load_skill` は `TOOL_HANDLERS[block.name]` を通じて自動的にディスパッチされる。\n\n---\n\n## 仕組み\n\n**skills/ ディレクトリ**、スキルごとに 1 つのサブディレクトリ、それぞれに `SKILL.md` ファイルを含む:\n\n```\nskills/\n agent-builder/SKILL.md\n code-review/SKILL.md\n mcp-builder/SKILL.md\n pdf/SKILL.md\n```\n\n**第 1 層:起動時にカタログを注入**:harness は起動時に `_scan_skills()` を呼び出して skills/ ディレクトリをスキャンし、各 SKILL.md の YAML frontmatter(`name`、`description`)を解析して `SKILL_REGISTRY` 辞書に格納する。`list_skills()` はレジストリからカタログを生成し、SYSTEM prompt に注入する。Agent は毎ターン「どのスキルが利用可能か」を確認できる。追加の API 呼び出しは不要:\n\n```python\nSKILL_REGISTRY: dict[str, dict] = {}\n\ndef _scan_skills():\n if not SKILLS_DIR.exists():\n return\n for d in sorted(SKILLS_DIR.iterdir()):\n if not d.is_dir():\n continue\n manifest = d / \"SKILL.md\"\n if manifest.exists():\n raw = manifest.read_text()\n meta, body = _parse_frontmatter(raw)\n name = meta.get(\"name\", d.name)\n desc = meta.get(\"description\", raw.split(\"\\n\")[0].lstrip(\"#\").strip())\n SKILL_REGISTRY[name] = {\"name\": name, \"description\": desc, \"content\": raw}\n\n_scan_skills() # runs once at startup\n\ndef list_skills() -> str:\n return \"\\n\".join(f\"- **{s['name']}**: {s['description']}\" for s in SKILL_REGISTRY.values())\n\ndef build_system() -> str:\n catalog = list_skills()\n return (\n f\"You are a coding agent at {WORKDIR}. \"\n f\"Skills available:\\n{catalog}\\n\"\n \"Use load_skill to get full details when needed.\"\n )\n\nSYSTEM = build_system()\n```\n\n**第 2 層:load_skill**:Agent が「SQL スタイルガイドが必要」と判断し、`load_skill(\"sql-style\")` を呼び出す。レジストリを通じて検索し、ファイルパスを経由しないため、パストラバーサルのリスクがない。SKILL.md の内容は `tool_result` を通じて注入され、既存の file および bash ツールを通じて、参照される `references/`、`scripts/`、`assets/` へのその後のアクセスも含められる。\n\n```python\ndef load_skill(name: str) -> str:\n skill = SKILL_REGISTRY.get(name)\n if not skill:\n return f\"Skill not found: {name}\"\n return skill[\"content\"]\n```\n\n重要な違い:スキル内容は system prompt の一部ではなく、ツール結果として現在の messages に入る。後続の呼び出しでは履歴とともに携帯され、コンテキスト圧縮、切り捨て、またはセッション終了まで保持される。これは s08 の compact と自然に接続する:オンデマンド読み込みにより、無関係なドキュメントが system prompt に入らなくなる。compact が「捨てるべきものをどう捨てるか」を解決する。\n\n---\n\n## s06 からの変更点\n\n| コンポーネント | 変更前 (s06) | 変更後 (s07) |\n|---------------|-------------|-------------|\n| ツール数 | 7 (bash, read, write, edit, glob, todo_write, task) | 8 (+load_skill) |\n| 知識読み込み | なし | 2 層:起動時カタログ注入 SYSTEM + 実行時 load_skill。SKILL.md がその後のリソースアクセスを案内できる |\n| SYSTEM プロンプト | 静的文字列 | 起動時に skills/ をスキャンしてカタログ注入 |\n| スキルレジストリ | なし | SKILL_REGISTRY(起動時に充填、パストラバーサル防止) |\n| ループ | 変更なし | 変更なし(スキルツールは自動ディスパッチ) |\n\n---\n\n## 試してみよう\n\n```sh\ncd learn-claude-code\npython s07_skill_loading/code.py\n```\n\n以下のプロンプトを試してみよう:\n\n1. `What skills are available?`\n2. `Load the code-review skill and follow its instructions`\n3. `I need to do a code review -- load the relevant skill first`\n\n観察のポイント:Agent は SYSTEM 内のカタログから利用可能なスキルを知っているか? 完全な手順が必要なときに `[HOOK] load_skill` が表示されるか? 読み込んだスキルの説明を使って回答しているか?\n\n---\n\n## 次へ\n\nload_skill で起動時のトークン浪費は解消した。しかし別の問題が待っている:Agent が 30 分連続で作業すると、messages リストが中間プロセスで埋め尽くされる。古い tool_result、期限切れのファイル内容、コンテキストを占領しているが価値を生まない。\n\n→ s08 Context Compact:4 層圧縮戦略。安価な層を先に実行、高価な層を後に実行。\n\n\nClaude Code ソースコードを深掘り
\n\n> 以下は Claude Code ソースコード `loadSkillsDir.ts`、`SkillTool.ts`、`bundledSkills.ts`、`commands.ts` の分析に基づく。\n\n### 一、スキルソース:skills/ ディレクトリだけではない\n\n教育版はすべてのスキルが `skills/` ディレクトリにあると想定している。Claude Code は実際に複数のファイルに分散したソースから読み込む:`loadSkillsDir.ts` は user/project/`--add-dir` ディレクトリと legacy commands(`.claude/commands/`)を担当、`bundledSkills.ts` は組み込みスキル、`SkillTool.ts` は MCP リモートスキル、`commands.ts` はコマンド集約を担当。タイプには managed/policy skills、user skills(`~/.claude/skills/`)、project skills(`.claude/skills/`)、`--add-dir` skills、legacy commands、dynamic skills、conditional skills(`paths` frontmatter を持ち、ファイルパスでアクティベート)、bundled skills、plugin skills、MCP skills が含まれる。\n\n### 二、SKILL.md Frontmatter の一般的なフィールド\n\nClaude Code の SKILL.md YAML frontmatter は `parseSkillFrontmatterFields()`(`loadSkillsDir.ts`)で解析される。一般的なフィールド:\n\n| フィールド | 用途 |\n|-----------|------|\n| `name` / `description` | 表示名と説明 |\n| `when_to_use` | モデルにいつ呼び出すかを指導 |\n| `allowed-tools` | スキルが使用可能なツールの自動許可リスト |\n| `context` | `inline`(デフォルト)または `fork`(サブ Agent として実行) |\n| `model` | モデルオーバーライド(haiku/sonnet/opus/inherit) |\n| `hooks` | スキルレベルのフック設定 |\n| `paths` | 条件付きアクティベーションの glob パターン |\n| `user-invocable` | ユーザーが `/name` で呼び出し可能 |\n\n完全なフィールドリストはバージョンによって変動する。上記は教育版に関連するコアフィールドのみ。\n\n### 三、2 層読み込みの正確な実装\n\n1. **カタログ(起動時)**:`getSkillDirCommands()` がディレクトリをスキャン → メタデータのみを含む `Command` オブジェクトとして登録。`getSkillListingAttachments()` がスキルリストを添付ファイルとしてフォーマット、コンテキストウィンドウの ~1% を予算とする(上限 8000 文字)。\n2. **読み込み(呼び出し時)**:モデルが `Skill` ツールを呼び出す(入力フィールドは `skill` + オプションの `args`、教育版は `name` を使用)→ `getPromptForCommand()` が完全な SKILL.md 内容を展開 → `SkillTool` が返す tool_result の表示テキストは `\"Launching skill: {name}\"` のみ、実際のスキル内容は `newMessages` を通じて注入される。教育版では両者を「tool_result を通じて注入」として簡略化している。読み込まれた SKILL.md は、モデルが後続で既存の file/bash ツールから関連リソースへアクセスする際の手がかりにもなる。\n\n### 教育版の単純化は意図的\n\n- 複数ファイル・複数ソース → 1 つの `skills/` ディレクトリ:2 層読み込みの核心概念を示すのに十分\n- 複数の frontmatter フィールド → name/description のみ解析:解析の複雑さを削減\n- forked skills(`context: 'fork'`)→ 省略:教育版では inline skill loading のみ展開する\n- `Skill` ツールの入力 `skill`+`args` → 教育版は `name` を使用:追加の引数解析の複雑さを回避\n\n \n\n\n"
+ "content": "# s07: Skill Loading — 必要なときにだけ読み込む\n\ns01 → s02 → s03 → s04 → s05 → s06 → `s07` → [s08](/ja/s08) → s09 → ... → s20\n> *\"Load when needed, don't stuff the prompt\"* — `tool_result` で注入、system prompt には詰め込まない。\n>\n> **Harness レイヤー**: 知識 — 必要に応じて読み込み、コンテキストに詰め込まない。\n\n---\n\n## 課題\n\n前章では Agent が大きなタスクをサブ Agent に渡せるようになった。だがサブ Agent が引き継ぐとき、そのタスクの決まりを知る必要がある:React コンポーネントを変更するなら component spec を、SQL を書くなら style guide を守る。その決まりはどこから来るのか?\n\nプロジェクトには React コンポーネント仕様、SQL スタイルガイド、API 設計ドキュメントがある。最も直接的な方法は、すべて system prompt に詰め込むこと:\n\n```python\nSYSTEM = (\n f\"You are a coding agent. \"\n + open(\"docs/react-style.md\").read() # 2000 行\n + open(\"docs/sql-style.md\").read() # 1500 行\n + open(\"docs/api-design.md\").read() # 3000 行\n)\n```\n\n6500 行の system prompt。Agent は LLM を呼び出すたびにこれらのドキュメントを運ぶ。CSS の色を変えるときも SQL クエリを修正するときも同様だ。99% の内容が現在のタスクと無関係で、トークンを無駄に消費する。\n\n---\n\n## ソリューション\n\n\n\n折衷案は、ドキュメントを複数のファイルに分け、必要なものを Agent 自身に `read_file` させることだ。だが Agent はそもそもどんなファイルが読めるか分からない。「何があるか」を先に知らなければ、「どれを使うか」は選べない。\n\nそこで 2 層に分ける:**カタログは常駐、内容はオンデマンド。** 前章のフック構造、`todo_write`、サブ Agent はそのまま残し、本章で `load_skill` ツールを 1 つ加える。起動時にスキルのカタログ(名前 + 一言の説明)を SYSTEM prompt に入れる。毎ターン携帯するが軽い。実行時に Agent が実際にあるスキルを使うとき、`load_skill` を呼んで完全な内容を取り出す。トークンを使うのはそのときだけだ。\n\n2 層設計:\n\n| 層 | 場所 | タイミング | コスト |\n|---|------|-----------|--------|\n| 1. カタログ | system prompt | 起動時に注入(harness が skills/ をスキャン) | ~100 トークン/スキル、毎ターン携帯 |\n| 2. 内容 | tool_result | Agent が load_skill を呼び出したとき。SKILL.md は、必要に応じて read_file/bash で追加リソースへアクセスするための手がかりになる | ~2000 トークン/スキル、オンデマンド |\n\nディスパッチ機構は変わらず、`load_skill` は `TOOL_HANDLERS[block.name]` を通じて自動的にディスパッチされる。\n\n---\n\n## 仕組み\n\n**skills/ ディレクトリ**、スキルごとに 1 つのサブディレクトリ、それぞれに `SKILL.md` ファイルを含む:\n\n```\nskills/\n agent-builder/SKILL.md\n code-review/SKILL.md\n mcp-builder/SKILL.md\n pdf/SKILL.md\n```\n\n**第 1 層:起動時にカタログを注入**:harness は起動時に `_scan_skills()` を呼び出して skills/ ディレクトリをスキャンし、各 SKILL.md の YAML frontmatter(`name`、`description`)を解析して `SKILL_REGISTRY` 辞書に格納する。`list_skills()` はレジストリからカタログを生成し、SYSTEM prompt に注入する。Agent は毎ターン「どのスキルが利用可能か」を確認できる。追加の API 呼び出しは不要:\n\n```python\nSKILL_REGISTRY: dict[str, dict] = {}\n\ndef _scan_skills():\n if not SKILLS_DIR.exists():\n return\n for d in sorted(SKILLS_DIR.iterdir()):\n if not d.is_dir():\n continue\n manifest = d / \"SKILL.md\"\n if manifest.exists():\n raw = manifest.read_text()\n meta, body = _parse_frontmatter(raw)\n name = meta.get(\"name\", d.name)\n desc = meta.get(\"description\", raw.split(\"\\n\")[0].lstrip(\"#\").strip())\n SKILL_REGISTRY[name] = {\"name\": name, \"description\": desc, \"content\": raw}\n\n_scan_skills() # runs once at startup\n\ndef list_skills() -> str:\n return \"\\n\".join(f\"- **{s['name']}**: {s['description']}\" for s in SKILL_REGISTRY.values())\n\ndef build_system() -> str:\n catalog = list_skills()\n return (\n f\"You are a coding agent at {WORKDIR}. \"\n f\"Skills available:\\n{catalog}\\n\"\n \"Use load_skill to get full details when needed.\"\n )\n\nSYSTEM = build_system()\n```\n\nだが Agent は毎ターン、名前と一言の説明しか受け取らない。実際に SQL スタイルガイドを使うとなると、あの 1500 行の完全な内容にはまだ手が届かない。→ 第 2 層。\n\n**第 2 層:load_skill**:Agent が「SQL スタイルガイドが必要」と判断し、`load_skill(\"sql-style\")` を呼び出す。レジストリを通じて検索し、ファイルパスを経由しないため、パストラバーサルのリスクがない。SKILL.md の内容は `tool_result` を通じて注入され、既存の file および bash ツールを通じて、参照される `references/`、`scripts/`、`assets/` へのその後のアクセスも含められる。\n\n```python\ndef load_skill(name: str) -> str:\n skill = SKILL_REGISTRY.get(name)\n if not skill:\n return f\"Skill not found: {name}\"\n return skill[\"content\"]\n```\n\n重要な違い:スキル内容は system prompt の一部ではなく、ツール結果として現在の `messages` に入る。後続の呼び出しでは履歴とともに携帯され、コンテキスト圧縮、切り捨て、またはセッション終了まで保持される。これは s08 の compact と自然に接続する:スキル内容は system prompt ではなく `tool_result` として `messages` に入り、compact が「捨てるべきものをどう捨てるか」を解決する。\n\n---\n\n## s06 からの変更点\n\n| コンポーネント | 変更前 (s06) | 変更後 (s07) |\n|---------------|-------------|-------------|\n| ツール数 | 7 (bash, read, write, edit, glob, todo_write, task) | 8 (+load_skill) |\n| 知識読み込み | なし | 2 層:起動時カタログ注入 SYSTEM + 実行時 load_skill。SKILL.md がその後のリソースアクセスを案内できる |\n| SYSTEM プロンプト | 静的文字列 | 起動時に skills/ をスキャンしてカタログ注入 |\n| スキルレジストリ | なし | SKILL_REGISTRY(起動時に充填、パストラバーサル防止) |\n| ループ | 変更なし | 変更なし(スキルツールは自動ディスパッチ) |\n\n---\n\n## 試してみよう\n\n```sh\ncd learn-claude-code\npython s07_skill_loading/code.py\n```\n\n以下のプロンプトを試してみよう:\n\n1. `What skills are available?`\n2. `Load the code-review skill and follow its instructions`\n3. `I need to do a code review -- load the relevant skill first`\n\n観察のポイント:Agent は SYSTEM 内のカタログから利用可能なスキルを知っているか? 完全な手順が必要なときに `[HOOK] load_skill` が表示されるか? 読み込んだスキルの説明を使って回答しているか?\n\n---\n\n## 次へ\n\nload_skill で起動時のトークン浪費は解消した。しかし別の問題が待っている:Agent が 30 分連続で作業すると、`messages` リストが中間プロセスで埋め尽くされる。古い `tool_result`、期限切れのファイル内容、コンテキストを占領しているが価値を生まない。\n\n→ s08 Context Compact:4 層圧縮戦略。安価な層を先に実行、高価な層を後に実行。\n\n\nClaude Code ソースコードを深掘り
\n\n> 以下は Claude Code ソースコード `loadSkillsDir.ts`、`SkillTool.ts`、`bundledSkills.ts`、`commands.ts` の分析に基づく。\n\n### 一、スキルソース:skills/ ディレクトリだけではない\n\n教育版はすべてのスキルが `skills/` ディレクトリにあると想定している。Claude Code は実際に複数のファイルに分散したソースから読み込む:`loadSkillsDir.ts` は user/project/`--add-dir` ディレクトリと legacy commands(`.claude/commands/`)を担当、`bundledSkills.ts` は組み込みスキル、`SkillTool.ts` は MCP リモートスキル、`commands.ts` はコマンド集約を担当。タイプには managed/policy skills、user skills(`~/.claude/skills/`)、project skills(`.claude/skills/`)、`--add-dir` skills、legacy commands、dynamic skills、conditional skills(`paths` frontmatter を持ち、ファイルパスでアクティベート)、bundled skills、plugin skills、MCP skills が含まれる。\n\n### 二、SKILL.md Frontmatter の一般的なフィールド\n\nClaude Code の SKILL.md YAML frontmatter は `parseSkillFrontmatterFields()`(`loadSkillsDir.ts`)で解析される。一般的なフィールド:\n\n| フィールド | 用途 |\n|-----------|------|\n| `name` / `description` | 表示名と説明 |\n| `when_to_use` | モデルにいつ呼び出すかを指導 |\n| `allowed-tools` | スキルが使用可能なツールの自動許可リスト |\n| `context` | `inline`(デフォルト)または `fork`(サブ Agent として実行) |\n| `model` | モデルオーバーライド(haiku/sonnet/opus/inherit) |\n| `hooks` | スキルレベルのフック設定 |\n| `paths` | 条件付きアクティベーションの glob パターン |\n| `user-invocable` | ユーザーが `/name` で呼び出し可能 |\n\n完全なフィールドリストはバージョンによって変動する。上記は教育版に関連するコアフィールドのみ。\n\n### 三、2 層読み込みの正確な実装\n\n1. **カタログ(起動時)**:`getSkillDirCommands()` がディレクトリをスキャン → メタデータのみを含む `Command` オブジェクトとして登録。`getSkillListingAttachments()` がスキルリストを添付ファイルとしてフォーマット、コンテキストウィンドウの ~1% を予算とする(上限 8000 文字)。\n2. **読み込み(呼び出し時)**:モデルが `Skill` ツールを呼び出す(入力フィールドは `skill` + オプションの `args`、教育版は `name` を使用)→ `getPromptForCommand()` が完全な SKILL.md 内容を展開 → `SkillTool` が返す tool_result の表示テキストは `\"Launching skill: {name}\"` のみ、実際のスキル内容は `newMessages` を通じて注入される。教育版では両者を「tool_result を通じて注入」として簡略化している。読み込まれた SKILL.md は、モデルが後続で既存の file/bash ツールから関連リソースへアクセスする際の手がかりにもなる。\n\n### 教育版の単純化は意図的\n\n- 複数ファイル・複数ソース → 1 つの `skills/` ディレクトリ:2 層読み込みの核心概念を示すのに十分\n- 複数の frontmatter フィールド → name/description のみ解析:解析の複雑さを削減\n- forked skills(`context: 'fork'`)→ 省略:教育版では inline skill loading のみ展開する\n- `Skill` ツールの入力 `skill`+`args` → 教育版は `name` を使用:追加の引数解析の複雑さを回避\n\n \n\n\n"
},
{
"version": "s08",