Follow up PR #265: refine chapters, diagrams, and add S20 (#283)

* feat: s01-s14 docs quality overhaul — tool pipeline, single-agent, knowledge & resilience

Rewrite code.py and README (zh/en/ja) for s01-s14, each chapter building
incrementally on the previous. Key fixes across chapters:

- s01-s04: agent loop, tool dispatch, permission pipeline, hooks
- s05-s08: todo write, subagent, skill loading, context compact
- s09-s11: memory system, system prompt assembly, error recovery
- s12-s14: task graph, background tasks, cron scheduler

All chapters CC source-verified. Code inherits fixes forward (PROMPT_SECTIONS,
json.dumps cache, real-state context, can_start dep protection, etc.).

* feat: s15-s19 docs quality overhaul — multi-agent platform: teams, protocols, autonomy, worktree, MCP tools

Rewrite code.py and README (zh/en/ja) for s15-s19, the multi-agent platform
chapters. Each chapter inherits all previous fixes and adds one mechanism:

- s15: agent teams (TeamCreate, teammate threads, shared task list)
- s16: team protocols (plan approval, shutdown handshake, consume_inbox)
- s17: autonomous agents (idle polling, auto-claim, consume_lead_inbox)
- s18: worktree isolation (git worktree, bind_task, cwd switching, safety)
- s19: MCP tools (MCPClient, normalize_mcp_name, assemble_tool_pool, no cache)

All appendix source code references verified against CC source. Config priority
corrected: claude.ai < plugin < user < project < local.

* fix: 5 regressions across s05-s19 — glob safety, todo validation, memory extraction, protocol types, dep crash

- s05-s09: glob results now filter with is_relative_to(WORKDIR) (inherited from s02)
- s06-s08: todo_write validates content/status required fields (inherited from s05)
- s09: extract_memories uses pre-compression snapshot instead of compacted messages
- s16: submit_plan docstring clarifies protocol-only (not code-level gate)
- s17-s19: match_response restores type mismatch validation (from s16)
- s17-s19: claim_task deps list handles missing dep files without crashing

* fix: s12 Todo V2 logic reversal, s14/s15 cron range validation, s18/s19 worktree name validation

- s12 README (zh/en/ja): fix Todo V2 direction — interactive defaults to Task,
  non-interactive/SDK defaults to TodoWrite. Fix env var name to
  CLAUDE_CODE_ENABLE_TASKS (not TODO_V2).
- s14/s15: add _validate_cron_field with per-field range checks (minute 0-59,
  hour 0-23, dom 1-31, month 1-12, dow 0-6), step > 0, range lo <= hi.
  Replace old try/except validation that only caught exceptions.
- s18/s19: add validate_worktree_name() to remove_worktree and keep_worktree,
  not just create_worktree.

* fix: align s16-s19 teaching tool consistency

* fix pr265 chapter diagrams

* Add comprehensive s20 harness chapter

* Fix chapter smoke test regressions

* Clarify README tutorial track transition

---------

Co-authored-by: Haoran <bill-billion@outlook.com>
This commit is contained in:
gui-yue
2026-05-20 21:45:38 +08:00
committed by GitHub
parent c354cf7721
commit 1baf1aca5a
174 changed files with 35833 additions and 353 deletions

View File

@@ -0,0 +1,182 @@
# s07: Skill Loading — Load Only When Needed
[中文](README.md) · [English](README.en.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.
>
> **Harness Layer**: Knowledge — load on demand, don't fill the context.
---
## 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:
```python
SYSTEM = (
f"You are a coding agent. "
+ open("docs/react-style.md").read() # 2000 lines
+ open("docs/sql-style.md").read() # 1500 lines
+ open("docs/api-design.md").read() # 3000 lines
)
```
6500 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.
---
## The Solution
![Skill Overview](images/skill-overview.en.svg)
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.
Two-level design:
| Level | Location | Timing | Cost |
|-------|----------|--------|------|
| 1. Catalog | system prompt | Injected at startup (harness scans skills/) | ~100 tokens/skill, carried every turn |
| 2. Content | tool_result | When Agent calls load_skill | ~2000 tokens/skill, on demand |
The dispatch mechanism is unchanged, `load_skill` auto-dispatches via `TOOL_HANDLERS[block.name]`.
---
## How It Works
**skills/ directory**, one subdirectory per skill, each containing a `SKILL.md` file:
```
skills/
agent-builder/SKILL.md
code-review/SKILL.md
mcp-builder/SKILL.md
pdf/SKILL.md
```
**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:
```python
SKILL_REGISTRY: dict[str, dict] = {}
def _scan_skills():
if not SKILLS_DIR.exists():
return
for d in sorted(SKILLS_DIR.iterdir()):
if not d.is_dir():
continue
manifest = d / "SKILL.md"
if manifest.exists():
raw = manifest.read_text()
meta, body = _parse_frontmatter(raw)
name = meta.get("name", d.name)
desc = meta.get("description", raw.split("\n")[0].lstrip("#").strip())
SKILL_REGISTRY[name] = {"name": name, "description": desc, "content": raw}
_scan_skills() # runs once at startup
def list_skills() -> str:
return "\n".join(f"- **{s['name']}**: {s['description']}" for s in SKILL_REGISTRY.values())
def build_system() -> str:
catalog = list_skills()
return (
f"You are a coding agent at {WORKDIR}. "
f"Skills available:\n{catalog}\n"
"Use load_skill to get full details when needed."
)
SYSTEM = build_system()
```
**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 content is injected via `tool_result`:
```python
def load_skill(name: str) -> str:
skill = SKILL_REGISTRY.get(name)
if not skill:
return f"Skill not found: {name}"
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 solves "don't carry what you shouldn't", compact solves "how to drop what you should."
---
## Changes from s06
| Component | Before (s06) | After (s07) |
|-----------|-------------|-------------|
| Tool count | 7 (bash, read, write, edit, glob, todo_write, task) | 8 (+load_skill) |
| Knowledge loading | None | Two-level: startup catalog in SYSTEM + runtime load_skill |
| SYSTEM prompt | Static string | Startup scan of skills/ injects catalog |
| Skill registry | None | SKILL_REGISTRY (populated at startup, prevents path traversal) |
| Loop | Unchanged | Unchanged (skill tool auto-dispatches) |
---
## Try It
```sh
cd learn-claude-code
python s07_skill_loading/code.py
```
Try these prompts:
1. `What skills are available?`
2. `Load the code-review skill and follow its instructions`
3. `I need to do a code review -- load the relevant skill first`
What 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?
---
## What's Next
On-demand loading solved "don't carry what you shouldn't." 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.
→ s08 Context Compact: A four-layer compaction strategy. Cheap layers run first, expensive layers run last.
<details>
<summary>Dive into CC Source Code</summary>
> The following is based on analysis of CC source code `loadSkillsDir.ts`, `SkillTool.ts`, `bundledSkills.ts`, `commands.ts`.
### 1. Skill Sources: Not Just One skills/ Directory
The teaching version assumes all skills live in a `skills/` directory. CC 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.
### 2. SKILL.md Frontmatter — Common Fields
CC's SKILL.md YAML frontmatter is parsed by `parseSkillFrontmatterFields()` in `loadSkillsDir.ts`. Common fields include:
| Field | Purpose |
|-------|---------|
| `name` / `description` | Display name and description |
| `when_to_use` | Guides the model on when to invoke |
| `allowed-tools` | Auto-allow list of tools available to the skill |
| `context` | `inline` (default) or `fork` (run as sub-Agent) |
| `model` | Model override (haiku/sonnet/opus/inherit) |
| `hooks` | Skill-level hook configuration |
| `paths` | Glob patterns for conditional activation |
| `user-invocable` | Users can invoke via `/name` |
The complete field list changes across versions; above are the core fields relevant to the teaching version.
### 3. Precise Implementation of Two-Level Loading
1. **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).
2. **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 Teaching Version's Simplification Is Intentional
- Multiple files and sources → 1 `skills/` directory: sufficient to demonstrate the core concept of two-level loading
- Multiple frontmatter fields → only parse name/description: reduces parsing complexity
- Forked skills (`context: 'fork'`) → omitted: the teaching version only expands inline skill loading
- `Skill` tool input `skill`+`args` → teaching version uses `name`: avoids extra argument parsing complexity
</details>
<!-- translation-sync: zh@v1, en@v1, ja@v1 -->

View File

@@ -0,0 +1,182 @@
# s07: Skill Loading — 必要なときにだけ読み込む
[中文](README.md) · [English](README.en.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 には詰め込まない。
>
> **Harness レイヤー**: 知識 — 必要に応じて読み込み、コンテキストに詰め込まない。
---
## 課題
プロジェクトには React コンポーネント仕様、SQL スタイルガイド、API 設計ドキュメントがある。Agent にこれらの仕様を自動的に守らせたい。最も直接的な方法 — すべて system prompt に詰め込む:
```python
SYSTEM = (
f"You are a coding agent. "
+ open("docs/react-style.md").read() # 2000 行
+ open("docs/sql-style.md").read() # 1500 行
+ open("docs/api-design.md").read() # 3000 行
)
```
6500 行の system prompt。Agent は LLM を呼び出すたびにこれらのドキュメントを運ぶ — CSS の色を変えるときも SQL クエリを修正するときも。99% の内容が現在のタスクと無関係で、トークンを無駄に消費する。
---
## ソリューション
![Skill Overview](images/skill-overview.ja.svg)
前章の最小フック構造、`todo_write`、サブ Agent を維持し、本章は新規の `load_skill` ツールに注目する。起動時にスキルカタログを SYSTEM prompt に注入し、実行時に完全な内容を読み込むツールを登録する。使ったときだけトークンを消費。
2 層設計:
| 層 | 場所 | タイミング | コスト |
|---|------|-----------|--------|
| 1. カタログ | system prompt | 起動時に注入harness が skills/ をスキャン) | ~100 トークン/スキル、毎ターン携帯 |
| 2. 内容 | tool_result | Agent が load_skill を呼び出したとき | ~2000 トークン/スキル、オンデマンド |
ディスパッチ機構は変わらず、`load_skill``TOOL_HANDLERS[block.name]` を通じて自動的にディスパッチされる。
---
## 仕組み
**skills/ ディレクトリ**、スキルごとに 1 つのサブディレクトリ、それぞれに `SKILL.md` ファイルを含む:
```
skills/
agent-builder/SKILL.md
code-review/SKILL.md
mcp-builder/SKILL.md
pdf/SKILL.md
```
**第 1 層:起動時にカタログを注入**harness は起動時に `_scan_skills()` を呼び出して skills/ ディレクトリをスキャンし、各 SKILL.md の YAML frontmatter`name``description`)を解析して `SKILL_REGISTRY` 辞書に格納する。`list_skills()` はレジストリからカタログを生成し、SYSTEM prompt に注入する。Agent は毎ターン「どのスキルが利用可能か」を確認できる。追加の API 呼び出しは不要:
```python
SKILL_REGISTRY: dict[str, dict] = {}
def _scan_skills():
if not SKILLS_DIR.exists():
return
for d in sorted(SKILLS_DIR.iterdir()):
if not d.is_dir():
continue
manifest = d / "SKILL.md"
if manifest.exists():
raw = manifest.read_text()
meta, body = _parse_frontmatter(raw)
name = meta.get("name", d.name)
desc = meta.get("description", raw.split("\n")[0].lstrip("#").strip())
SKILL_REGISTRY[name] = {"name": name, "description": desc, "content": raw}
_scan_skills() # runs once at startup
def list_skills() -> str:
return "\n".join(f"- **{s['name']}**: {s['description']}" for s in SKILL_REGISTRY.values())
def build_system() -> str:
catalog = list_skills()
return (
f"You are a coding agent at {WORKDIR}. "
f"Skills available:\n{catalog}\n"
"Use load_skill to get full details when needed."
)
SYSTEM = build_system()
```
**第 2 層load_skill**Agent が「SQL スタイルガイドが必要」と判断し、`load_skill("sql-style")` を呼び出す。レジストリを通じて検索し、ファイルパスを経由しないため、パストラバーサルのリスクがない。内容は `tool_result` を通じて注入される:
```python
def load_skill(name: str) -> str:
skill = SKILL_REGISTRY.get(name)
if not skill:
return f"Skill not found: {name}"
return skill["content"]
```
重要な違い:スキル内容は system prompt の一部ではなく、ツール結果として現在の messages に入る。後続の呼び出しでは履歴とともに携帯され、コンテキスト圧縮、切り捨て、またはセッション終了まで保持される。これは s08 の compact と自然に接続するオンデマンド読み込みで「運ぶべきでないものは運ばない」を解決し、compact が「捨てるべきものをどう捨てるか」を解決する。
---
## s06 からの変更点
| コンポーネント | 変更前 (s06) | 変更後 (s07) |
|---------------|-------------|-------------|
| ツール数 | 7 (bash, read, write, edit, glob, todo_write, task) | 8 (+load_skill) |
| 知識読み込み | なし | 2 層:起動時カタログ注入 SYSTEM + 実行時 load_skill |
| SYSTEM プロンプト | 静的文字列 | 起動時に skills/ をスキャンしてカタログ注入 |
| スキルレジストリ | なし | SKILL_REGISTRY起動時に充填、パストラバーサル防止 |
| ループ | 変更なし | 変更なし(スキルツールは自動ディスパッチ) |
---
## 試してみよう
```sh
cd learn-claude-code
python s07_skill_loading/code.py
```
以下のプロンプトを試してみよう:
1. `What skills are available?`
2. `Load the code-review skill and follow its instructions`
3. `I need to do a code review -- load the relevant skill first`
観察のポイントAgent は SYSTEM 内のカタログから利用可能なスキルを知っているか? 完全な手順が必要なときに `[HOOK] load_skill` が表示されるか? 読み込んだスキルの説明を使って回答しているか?
---
## 次へ
オンデマンド読み込みで「運ぶべきでないものは運ばない」問題は解決した。しかし別の問題が待っているAgent が 30 分連続で作業すると、messages リストが中間プロセスで埋め尽くされる。古い tool_result、期限切れのファイル内容、コンテキストを占領しているが価値を生まない。
→ s08 Context Compact4 層圧縮戦略。安価な層を先に実行、高価な層を後に実行。
<details>
<summary>CC ソースコードを深掘り</summary>
> 以下は CC ソースコード `loadSkillsDir.ts`、`SkillTool.ts`、`bundledSkills.ts`、`commands.ts` の分析に基づく。
### 一、スキルソースskills/ ディレクトリだけではない
教育版はすべてのスキルが `skills/` ディレクトリにあると想定している。CC は実際に複数のファイルに分散したソースから読み込む:`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 が含まれる。
### 二、SKILL.md Frontmatter の一般的なフィールド
CC の SKILL.md YAML frontmatter は `parseSkillFrontmatterFields()``loadSkillsDir.ts`)で解析される。一般的なフィールド:
| フィールド | 用途 |
|-----------|------|
| `name` / `description` | 表示名と説明 |
| `when_to_use` | モデルにいつ呼び出すかを指導 |
| `allowed-tools` | スキルが使用可能なツールの自動許可リスト |
| `context` | `inline`(デフォルト)または `fork`(サブ Agent として実行) |
| `model` | モデルオーバーライドhaiku/sonnet/opus/inherit |
| `hooks` | スキルレベルのフック設定 |
| `paths` | 条件付きアクティベーションの glob パターン |
| `user-invocable` | ユーザーが `/name` で呼び出し可能 |
完全なフィールドリストはバージョンによって変動する。上記は教育版に関連するコアフィールドのみ。
### 三、2 層読み込みの正確な実装
1. **カタログ(起動時)**`getSkillDirCommands()` がディレクトリをスキャン → メタデータのみを含む `Command` オブジェクトとして登録。`getSkillListingAttachments()` がスキルリストを添付ファイルとしてフォーマット、コンテキストウィンドウの ~1% を予算とする(上限 8000 文字)。
2. **読み込み(呼び出し時)**:モデルが `Skill` ツールを呼び出す(入力フィールドは `skill` + オプションの `args`、教育版は `name` を使用)→ `getPromptForCommand()` が完全な SKILL.md 内容を展開 → `SkillTool` が返す tool_result の表示テキストは `"Launching skill: {name}"` のみ、実際のスキル内容は `newMessages` を通じて注入される。教育版では両者を「tool_result を通じて注入」として簡略化している。
### 教育版の単純化は意図的
- 複数ファイル・複数ソース → 1 つの `skills/` ディレクトリ2 層読み込みの核心概念を示すのに十分
- 複数の frontmatter フィールド → name/description のみ解析:解析の複雑さを削減
- forked skills`context: 'fork'`)→ 省略:教学版では inline skill loading のみ展開する
- `Skill` ツールの入力 `skill`+`args` → 教育版は `name` を使用:追加の引数解析の複雑さを回避
</details>
<!-- translation-sync: zh@v1, en@v1, ja@v1 -->

182
s07_skill_loading/README.md Normal file
View File

@@ -0,0 +1,182 @@
# s07: Skill Loading — 用到的时候才加载
[中文](README.md) · [English](README.en.md) · [日本語](README.ja.md)
s01 → s02 → s03 → s04 → s05 → s06 → `s07` → [s08](../s08_context_compact/) → s09 → ... → s20
> *"用到时再加载, 别全塞 prompt 里"* — 通过 tool_result 注入, 不塞 system prompt。
>
> **Harness 层**: 知识 — 按需加载, 不堆满上下文。
---
## 问题
你的项目有一套 React 组件规范、一份 SQL 风格指南、一份 API 设计文档。你希望 Agent 自动遵守这些规范。最直接的想法,全塞进 system prompt
```python
SYSTEM = (
f"You are a coding agent. "
+ open("docs/react-style.md").read() # 2000 行
+ open("docs/sql-style.md").read() # 1500 行
+ open("docs/api-design.md").read() # 3000 行
)
```
6500 行 system prompt。Agent 每次调用 LLM 都带着这些文档——不管是在改 CSS 颜色还是修 SQL 查询。99% 的内容和当前任务无关,白白消耗 token。
---
## 解决方案
![Skill Overview](images/skill-overview.svg)
保留上一章的最小 hook 结构、`todo_write` 和子 Agent本章重点转向新增的 `load_skill` 工具。启动时把技能目录注入 SYSTEM prompt运行时多注册一个工具加载完整内容用到才花 token。
两层设计:
| 层 | 位置 | 时机 | 代价 |
|---|------|------|------|
| 1. 目录 | system prompt | 启动时注入harness 扫描 skills/ | ~100 tokens/skill每轮都带 |
| 2. 内容 | tool_result | Agent 调用 load_skill 时 | ~2000 tokens/skill按需 |
dispatch 机制不变load_skill 通过 `TOOL_HANDLERS[block.name]` 分发。
---
## 工作原理
**skills/ 目录**,每个技能一个子目录,包含 `SKILL.md` 文件:
```
skills/
agent-builder/SKILL.md
code-review/SKILL.md
mcp-builder/SKILL.md
pdf/SKILL.md
```
**第一级:启动时注入目录**harness 启动时调用 `_scan_skills()` 扫描 skills/ 目录,解析每个 SKILL.md 的 YAML frontmatter`name``description`),存入 `SKILL_REGISTRY` 字典。`list_skills()` 从注册表生成目录,注入 SYSTEM prompt。Agent 每轮都能看到"我有哪些技能可用",不花额外 API 调用:
```python
SKILL_REGISTRY: dict[str, dict] = {}
def _scan_skills():
if not SKILLS_DIR.exists():
return
for d in sorted(SKILLS_DIR.iterdir()):
if not d.is_dir():
continue
manifest = d / "SKILL.md"
if manifest.exists():
raw = manifest.read_text()
meta, body = _parse_frontmatter(raw)
name = meta.get("name", d.name)
desc = meta.get("description", raw.split("\n")[0].lstrip("#").strip())
SKILL_REGISTRY[name] = {"name": name, "description": desc, "content": raw}
_scan_skills() # runs once at startup
def list_skills() -> str:
return "\n".join(f"- **{s['name']}**: {s['description']}" for s in SKILL_REGISTRY.values())
def build_system() -> str:
catalog = list_skills()
return (
f"You are a coding agent at {WORKDIR}. "
f"Skills available:\n{catalog}\n"
"Use load_skill to get full details when needed."
)
SYSTEM = build_system()
```
**第二级load_skill**Agent 决定"我需要 SQL 风格指南",调用 `load_skill("sql-style")`。通过注册表查找,不走文件路径,没有路径遍历风险。内容通过 `tool_result` 注入:
```python
def load_skill(name: str) -> str:
skill = SKILL_REGISTRY.get(name)
if not skill:
return f"Skill not found: {name}"
return skill["content"]
```
关键区别:技能内容不是 system prompt 的一部分,它作为一次工具结果进入当前 messages。后续调用会随历史一起携带直到上下文压缩、截断或会话结束。这和 s08 的 compact 自然衔接:按需加载解决了"不该提前带的不要带"compact 解决"该丢的怎么丢"。
---
## 相对 s06 的变更
| 组件 | 之前 (s06) | 之后 (s07) |
|------|-----------|-----------|
| 工具数量 | 7 (bash, read, write, edit, glob, todo_write, task) | 8 (+load_skill) |
| 知识加载 | 无 | 两级:启动时目录注入 SYSTEM + 运行时 load_skill |
| SYSTEM 提示 | 静态字符串 | 启动时扫描 skills/ 注入目录 |
| 技能注册表 | 无 | SKILL_REGISTRY启动时填充防路径遍历 |
| 循环 | 不变 | 不变skill 工具自动分发) |
---
## 试一下
```sh
cd learn-claude-code
python s07_skill_loading/code.py
```
试试这些 prompt
1. `What skills are available?`
2. `Load the code-review skill and follow its instructions`
3. `I need to do a code review -- load the relevant skill first`
观察重点Agent 是否直接从 SYSTEM 里的目录知道有哪些技能?需要完整规范时是否出现 `[HOOK] load_skill`?加载后回答是否使用了对应 skill 的说明?
---
## 接下来
按需加载解决了"不该带的不要带"。但另一个问题来了Agent 连续工作 30 分钟后messages 列表塞满了中间过程。旧的 tool_result、过时的文件内容占着上下文但不产生价值。
s08 Context Compact → 四层压缩策略。便宜的先跑,贵的后跑。
<details>
<summary>深入 CC 源码</summary>
> 以下基于 CC 源码 `loadSkillsDir.ts`、`SkillTool.ts`、`bundledSkills.ts`、`commands.ts` 的分析。
### 一、技能来源:不是只有一个 skills/ 目录
教学版假设所有技能在 `skills/` 目录下。CC 实际从多个来源加载,分布在多个文件中:`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。
### 二、SKILL.md Frontmatter 常见字段
CC 的 SKILL.md YAML frontmatter 由 `parseSkillFrontmatterFields()` 解析(`loadSkillsDir.ts`),常见字段包括:
| 字段 | 用途 |
|------|------|
| `name` / `description` | 显示名称和描述 |
| `when_to_use` | 指导模型何时调用 |
| `allowed-tools` | 技能可用工具的自动允许列表 |
| `context` | `inline`(默认)或 `fork`(作为子 Agent 运行) |
| `model` | 模型覆盖haiku/sonnet/opus/inherit |
| `hooks` | 技能级别的 hook 配置 |
| `paths` | 条件激活的 glob 模式 |
| `user-invocable` | 用户可以通过 `/name` 调用 |
完整字段列表随版本迭代会变化,以上仅列出教学版涉及的核心字段。
### 三、两级加载的精确实现
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 注入"是一种简化。
### 教学版的简化是刻意的
- 多文件多来源 → 1 个 `skills/` 目录:足以展示两级加载的核心概念
- 多个 frontmatter 字段 → 只解析 name/description减少解析复杂度
- forked skills`context: 'fork'`)→ 省略:教学版只展开 inline 技能加载
- `Skill` 工具输入 `skill`+`args` → 教学版用 `name`:避免参数解析的额外复杂度
</details>
<!-- translation-sync: zh@v1, en@v1, ja@v1 -->

411
s07_skill_loading/code.py Normal file
View File

@@ -0,0 +1,411 @@
#!/usr/bin/env python3
"""
s07: Skill Loading — two-level on-demand knowledge injection.
Layer 1 (cheap, always present):
SYSTEM prompt includes skill names + one-line descriptions (~100 tokens/skill)
"Skills available: agent-builder, code-review, mcp-builder, pdf"
Layer 2 (expensive, on demand):
Agent calls load_skill("code-review") → full SKILL.md content
injected via tool_result (~2000 tokens/skill)
skills/
agent-builder/SKILL.md
code-review/SKILL.md
mcp-builder/SKILL.md
pdf/SKILL.md
Changes from s06:
+ build_system() — scan skills/ dir at startup, inject catalog into SYSTEM
+ load_skill(name) — return full SKILL.md content via tool_result
+ SKILLS_DIR config
Loop unchanged: load_skill auto-dispatches via TOOL_HANDLERS.
Run: python s07_skill_loading/code.py
Needs: pip install anthropic python-dotenv + ANTHROPIC_API_KEY in .env
"""
import os, subprocess, json
from pathlib import Path
try:
import readline
readline.parse_and_bind('set bind-tty-special-chars off')
except ImportError:
pass
from anthropic import Anthropic
from dotenv import load_dotenv
load_dotenv(override=True)
if os.getenv("ANTHROPIC_BASE_URL"):
os.environ.pop("ANTHROPIC_AUTH_TOKEN", None)
WORKDIR = Path.cwd()
SKILLS_DIR = WORKDIR / "skills"
TASKS_DIR = WORKDIR / ".tasks"; TASKS_DIR.mkdir(exist_ok=True)
client = Anthropic(base_url=os.getenv("ANTHROPIC_BASE_URL"))
MODEL = os.environ["MODEL_ID"]
# s07: Skill catalog scan (used by build_system below)
def _parse_frontmatter(text: str) -> tuple[dict, str]:
"""Parse YAML frontmatter from SKILL.md. Returns (meta, body)."""
if not text.startswith("---"):
return {}, text
parts = text.split("---", 2)
if len(parts) < 3:
return {}, text
meta = {}
for line in parts[1].strip().splitlines():
if ":" in line:
k, v = line.split(":", 1)
meta[k.strip()] = v.strip().strip('"').strip("'")
return meta, parts[2].strip()
# Build skill registry at startup (used for safe lookup in load_skill)
SKILL_REGISTRY: dict[str, dict] = {}
def _scan_skills():
"""Scan skills/ dir, populate SKILL_REGISTRY with name/description/content."""
if not SKILLS_DIR.exists():
return
for d in sorted(SKILLS_DIR.iterdir()):
if not d.is_dir():
continue
manifest = d / "SKILL.md"
if manifest.exists():
raw = manifest.read_text()
meta, body = _parse_frontmatter(raw)
name = meta.get("name", d.name)
desc = meta.get("description", raw.split("\n")[0].lstrip("#").strip())
SKILL_REGISTRY[name] = {"name": name, "description": desc, "content": raw}
_scan_skills()
def list_skills() -> str:
"""List all skills (name + one-line description)."""
if not SKILL_REGISTRY:
return "(no skills found)"
return "\n".join(f"- **{s['name']}**: {s['description']}" for s in SKILL_REGISTRY.values())
# s07: SYSTEM includes skill catalog (cheap — just names + descriptions)
def build_system() -> str:
"""Build SYSTEM prompt with skill catalog injected at startup."""
catalog = list_skills()
return (
f"You are a coding agent at {WORKDIR}. "
f"Skills available:\n{catalog}\n"
"Use load_skill to get full details when needed."
)
SYSTEM = build_system()
# s07: subagent gets its own system prompt — no skill loading, no task
SUB_SYSTEM = (
f"You are a coding agent at {WORKDIR}. "
"Complete the task you were given, then return a concise summary. "
"Do not delegate further."
)
# ═══════════════════════════════════════════════════════════
# FROM s02-s06 (unchanged): Tool Implementations
# ═══════════════════════════════════════════════════════════
def safe_path(p: str) -> Path:
path = (WORKDIR / p).resolve()
if not path.is_relative_to(WORKDIR):
raise ValueError(f"Path escapes workspace: {p}")
return path
def run_bash(command: str) -> str:
try:
r = subprocess.run(command, shell=True, cwd=WORKDIR,
capture_output=True, text=True, timeout=120)
out = (r.stdout + r.stderr).strip()
return out[:50000] if out else "(no output)"
except subprocess.TimeoutExpired:
return "Error: Timeout (120s)"
def run_read(path: str, limit: int | None = None) -> str:
try:
lines = safe_path(path).read_text().splitlines()
if limit and limit < len(lines):
lines = lines[:limit] + [f"... ({len(lines) - limit} more lines)"]
return "\n".join(lines)
except Exception as e:
return f"Error: {e}"
def run_write(path: str, content: str) -> str:
try:
file_path = safe_path(path)
file_path.parent.mkdir(parents=True, exist_ok=True)
file_path.write_text(content)
return f"Wrote {len(content)} bytes to {path}"
except Exception as e:
return f"Error: {e}"
def run_edit(path: str, old_text: str, new_text: str) -> str:
try:
file_path = safe_path(path)
text = file_path.read_text()
if old_text not in text:
return f"Error: text not found in {path}"
file_path.write_text(text.replace(old_text, new_text, 1))
return f"Edited {path}"
except Exception as e:
return f"Error: {e}"
def run_glob(pattern: str) -> str:
import glob as g
try:
results = []
for match in g.glob(pattern, root_dir=WORKDIR):
if (WORKDIR / match).resolve().is_relative_to(WORKDIR):
results.append(match)
return "\n".join(results) if results else "(no matches)"
except Exception as e:
return f"Error: {e}"
def run_todo_write(todos: list) -> str:
for i, t in enumerate(todos):
if "content" not in t or "status" not in t:
return f"Error: todos[{i}] missing 'content' or 'status'"
if t["status"] not in ("pending", "in_progress", "completed"):
return f"Error: todos[{i}] has invalid status '{t['status']}'"
tasks_file = TASKS_DIR / "current_todos.json"
tasks_file.write_text(json.dumps(todos, indent=2, ensure_ascii=False))
lines = ["\n\033[33m## Current Tasks\033[0m"]
for t in todos:
icon = {"pending": " ", "in_progress": "\033[36m▸\033[0m", "completed": "\033[32m✓\033[0m"}[t["status"]]
lines.append(f" [{icon}] {t['content']}")
print("\n".join(lines))
return f"Updated {len(todos)} tasks"
def extract_text(content) -> str:
if not isinstance(content, list):
return str(content)
return "\n".join(getattr(b, "text", "") for b in content if getattr(b, "type", None) == "text")
# ═══════════════════════════════════════════════════════════
# FROM s06 (unchanged): Subagent
# ═══════════════════════════════════════════════════════════
SUB_TOOLS = [
{"name": "bash", "description": "Run a shell command.",
"input_schema": {"type": "object", "properties": {"command": {"type": "string"}}, "required": ["command"]}},
{"name": "read_file", "description": "Read file contents.",
"input_schema": {"type": "object", "properties": {"path": {"type": "string"}}, "required": ["path"]}},
{"name": "write_file", "description": "Write content to a file.",
"input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "content": {"type": "string"}}, "required": ["path", "content"]}},
{"name": "edit_file", "description": "Replace exact text in a file once.",
"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"]}},
]
SUB_HANDLERS = {"bash": run_bash, "read_file": run_read, "write_file": run_write,
"edit_file": run_edit, "glob": run_glob}
def spawn_subagent(description: str) -> str:
print(f"\n\033[35m[Subagent spawned]\033[0m")
messages = [{"role": "user", "content": description}]
for _ in range(30):
response = client.messages.create(model=MODEL, system=SUB_SYSTEM,
messages=messages, tools=SUB_TOOLS, max_tokens=8000)
messages.append({"role": "assistant", "content": response.content})
if response.stop_reason != "tool_use":
break
results = []
for block in response.content:
if block.type == "tool_use":
blocked = trigger_hooks("PreToolUse", block)
if blocked:
results.append({"type": "tool_result", "tool_use_id": block.id,
"content": str(blocked)})
continue
handler = SUB_HANDLERS.get(block.name)
output = handler(**block.input) if handler else f"Unknown: {block.name}"
trigger_hooks("PostToolUse", block, output)
print(f" \033[90m[sub] {block.name}: {str(output)[:100]}\033[0m")
results.append({"type": "tool_result", "tool_use_id": block.id, "content": output})
messages.append({"role": "user", "content": results})
result = extract_text(messages[-1]["content"])
if not result:
for msg in reversed(messages):
if msg["role"] == "assistant":
result = extract_text(msg["content"])
if result:
break
if not result:
result = "Subagent stopped after 30 turns without final answer."
print(f"\033[35m[Subagent done]\033[0m")
return result
# ═══════════════════════════════════════════════════════════
# NEW in s07: load_skill — runtime full content loading
# ═══════════════════════════════════════════════════════════
def load_skill(name: str) -> str:
"""Load full skill content. Lookup via registry — no path traversal."""
skill = SKILL_REGISTRY.get(name)
if not skill:
return f"Skill not found: {name}"
return skill["content"]
# ═══════════════════════════════════════════════════════════
# Tool Registry — all tools from s02-s07
# ═══════════════════════════════════════════════════════════
TOOLS = [
{"name": "bash", "description": "Run a shell command.",
"input_schema": {"type": "object", "properties": {"command": {"type": "string"}}, "required": ["command"]}},
{"name": "read_file", "description": "Read file contents.",
"input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "limit": {"type": "integer"}}, "required": ["path"]}},
{"name": "write_file", "description": "Write content to a file.",
"input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "content": {"type": "string"}}, "required": ["path", "content"]}},
{"name": "edit_file", "description": "Replace exact text in a file once.",
"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": "todo_write", "description": "Create and manage a task list for your current coding session.",
"input_schema": {"type": "object", "properties": {"todos": {"type": "array", "items": {"type": "object", "properties": {"content": {"type": "string"}, "status": {"type": "string", "enum": ["pending", "in_progress", "completed"]}}, "required": ["content", "status"]}}}, "required": ["todos"]}},
{"name": "task", "description": "Launch a subagent to handle a complex subtask. Returns only the final conclusion.",
"input_schema": {"type": "object", "properties": {"description": {"type": "string"}}, "required": ["description"]}},
# s07: skill tool (catalog is already in SYSTEM prompt, this loads full content)
{"name": "load_skill", "description": "Load the full content of a skill by name.",
"input_schema": {"type": "object", "properties": {"name": {"type": "string"}}, "required": ["name"]}},
]
TOOL_HANDLERS = {
"bash": run_bash, "read_file": run_read, "write_file": run_write,
"edit_file": run_edit, "glob": run_glob, "todo_write": run_todo_write,
"task": spawn_subagent, "load_skill": load_skill,
}
# ═══════════════════════════════════════════════════════════
# FROM s04 (unchanged): Hook System
# ═══════════════════════════════════════════════════════════
HOOKS = {"UserPromptSubmit": [], "PreToolUse": [], "PostToolUse": [], "Stop": []}
def register_hook(event: str, callback):
HOOKS[event].append(callback)
def trigger_hooks(event: str, *args):
for callback in HOOKS[event]:
result = callback(*args)
if result is not None:
return result
return None
DENY_LIST = ["rm -rf /", "sudo", "shutdown", "reboot", "mkfs", "dd if="]
def permission_hook(block):
if block.name == "bash":
for p in DENY_LIST:
if p in block.input.get("command", ""):
print(f"\n\033[31m⛔ Blocked: '{p}'\033[0m")
return "Permission denied"
return None
def log_hook(block):
print(f"\033[90m[HOOK] {block.name}\033[0m")
return None
def context_inject_hook(query: str):
print(f"\033[90m[HOOK] UserPromptSubmit: working in {WORKDIR}\033[0m")
return None
def summary_hook(messages: list):
tool_count = sum(1 for m in messages
for b in (m.get("content") if isinstance(m.get("content"), list) else [])
if isinstance(b, dict) and b.get("type") == "tool_result")
print(f"\033[90m[HOOK] Stop: session used {tool_count} tool calls\033[0m")
return None
register_hook("UserPromptSubmit", context_inject_hook)
register_hook("PreToolUse", permission_hook)
register_hook("PreToolUse", log_hook)
register_hook("Stop", summary_hook)
# ═══════════════════════════════════════════════════════════
# agent_loop — same as s05-s06 + nag reminder
# ═══════════════════════════════════════════════════════════
rounds_since_todo = 0
def agent_loop(messages: list):
global rounds_since_todo
while True:
if rounds_since_todo >= 3 and messages:
last = messages[-1]
if last["role"] == "user" and isinstance(last.get("content"), list):
last["content"].insert(0, {
"type": "text",
"text": "<reminder>Update your todos.</reminder>",
})
response = client.messages.create(
model=MODEL, system=SYSTEM, messages=messages,
tools=TOOLS, max_tokens=8000,
)
messages.append({"role": "assistant", "content": response.content})
if response.stop_reason != "tool_use":
force = trigger_hooks("Stop", messages)
if force:
messages.append({"role": "user", "content": force})
continue
return
rounds_since_todo += 1
results = []
for block in response.content:
if block.type != "tool_use":
continue
blocked = trigger_hooks("PreToolUse", block)
if blocked:
results.append({"type": "tool_result", "tool_use_id": block.id,
"content": str(blocked)})
continue
handler = TOOL_HANDLERS.get(block.name)
output = handler(**block.input) if handler else f"Unknown: {block.name}"
trigger_hooks("PostToolUse", block, output)
if block.name == "todo_write":
rounds_since_todo = 0
results.append({"type": "tool_result", "tool_use_id": block.id,
"content": output})
messages.append({"role": "user", "content": results})
if __name__ == "__main__":
print("s07: Skill Loading — catalog in SYSTEM, content on demand")
print("Type a question, press Enter. Type q to quit.\n")
history = []
while True:
try:
query = input("\033[36ms07 >> \033[0m")
except (EOFError, KeyboardInterrupt):
break
if query.strip().lower() in ("q", "exit", ""):
break
trigger_hooks("UserPromptSubmit", query)
history.append({"role": "user", "content": query})
agent_loop(history)
for block in history[-1]["content"]:
if getattr(block, "type", None) == "text":
print(block.text)
print()

View File

@@ -0,0 +1,110 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 380" font-family="system-ui, -apple-system, sans-serif">
<defs>
<marker id="arrow" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#555"/>
</marker>
<marker id="arrow-blue" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#2563eb"/>
</marker>
<marker id="arrow-green" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#16a34a"/>
</marker>
<marker id="arrow-amber" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#d97706"/>
</marker>
<linearGradient id="header" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="#1e3a5f"/>
<stop offset="100%" stop-color="#2563eb"/>
</linearGradient>
</defs>
<!-- Background -->
<rect width="800" height="380" fill="#fafbfc" rx="8"/>
<!-- Title -->
<rect x="0" y="0" width="800" height="48" fill="url(#header)" rx="8"/>
<rect x="0" y="40" width="800" height="8" fill="url(#header)"/>
<text x="400" y="31" fill="#fff" font-size="16" font-weight="700" text-anchor="middle">Skill Loading — catalog at startup, content on demand</text>
<!-- ===== History preserved ===== -->
<text x="50" y="96" fill="#94a3b8" font-size="11" font-weight="600">History preserved</text>
<!-- messages[] -->
<rect x="40" y="108" width="110" height="44" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
<text x="95" y="135" fill="#1e3a5f" font-size="12" font-weight="600" text-anchor="middle">messages[]</text>
<!-- → LLM -->
<line x1="150" y1="130" x2="198" y2="130" stroke="#2563eb" stroke-width="2" marker-end="url(#arrow-blue)"/>
<!-- LLM -->
<rect x="200" y="106" width="110" height="48" rx="8" fill="#fff" stroke="#2563eb" stroke-width="1.5"/>
<text x="255" y="128" fill="#1e3a5f" font-size="13" font-weight="700" text-anchor="middle">LLM</text>
<text x="255" y="146" fill="#64748b" font-size="9" text-anchor="middle">stop_reason=tool_use?</text>
<!-- No → return -->
<line x1="255" y1="154" x2="255" y2="178" stroke="#2563eb" stroke-width="2" marker-end="url(#arrow-blue)"/>
<text x="268" y="172" fill="#2563eb" font-size="9" font-weight="600">No</text>
<rect x="200" y="180" width="110" height="28" rx="14" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
<text x="255" y="198" fill="#1e3a5f" font-size="11" font-weight="600" text-anchor="middle">Return result</text>
<!-- Yes → PreToolUse -->
<line x1="310" y1="130" x2="348" y2="130" stroke="#555" stroke-width="2" marker-end="url(#arrow)"/>
<text x="325" y="122" fill="#d97706" font-size="9" font-weight="600">Yes</text>
<!-- PreToolUse (s04) -->
<rect x="350" y="108" width="90" height="44" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
<text x="395" y="128" fill="#1e3a5f" font-size="9" font-weight="600" text-anchor="middle">trigger_hooks</text>
<text x="395" y="142" fill="#64748b" font-size="8" text-anchor="middle">PreToolUse</text>
<!-- → TOOL_HANDLERS -->
<line x1="440" y1="130" x2="488" y2="130" stroke="#555" stroke-width="2" marker-end="url(#arrow)"/>
<!-- ===== TOOL_HANDLERS ===== -->
<rect x="490" y="88" width="120" height="130" rx="10" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
<text x="550" y="108" fill="#1e3a5f" font-size="10" font-weight="700" text-anchor="middle">TOOL_HANDLERS</text>
<!-- s06 tools -->
<rect x="500" y="116" width="100" height="18" rx="3" fill="#fff" stroke="#2563eb" stroke-width="0.8"/>
<text x="550" y="129" fill="#1e3a5f" font-size="8" text-anchor="middle">bash · read · write</text>
<rect x="500" y="138" width="100" height="18" rx="3" fill="#fff" stroke="#2563eb" stroke-width="0.8"/>
<text x="550" y="151" fill="#1e3a5f" font-size="8" text-anchor="middle">edit · glob · todo</text>
<rect x="500" y="160" width="100" height="18" rx="3" fill="#fff" stroke="#2563eb" stroke-width="0.8"/>
<text x="550" y="173" fill="#1e3a5f" font-size="8" text-anchor="middle">task (subagent)</text>
<rect x="500" y="186" width="100" height="22" rx="3" fill="#dcfce7" stroke="#16a34a" stroke-width="1.5"/>
<text x="550" y="201" fill="#166534" font-size="9" font-weight="700" text-anchor="middle">load_skill</text>
<!-- ===== Loop back ===== -->
<path d="M 550 218 L 550 270 L 95 270 L 95 152" fill="none" stroke="#555" stroke-width="2" marker-end="url(#arrow)" stroke-dasharray="6,3"/>
<text x="320" y="290" fill="#64748b" font-size="10" text-anchor="middle">Results appended to messages[], loop continues</text>
<!-- ===== s07 two-level injection labels (right side) ===== -->
<text x="710" y="220" fill="#16a34a" font-size="11" font-weight="700" text-anchor="middle">s07 new</text>
<!-- ① Startup SYSTEM injection -->
<rect x="640" y="86" width="140" height="56" rx="8" fill="#f0fdf4" stroke="#16a34a" stroke-width="1.5"/>
<text x="710" y="106" fill="#166534" font-size="10" font-weight="700" text-anchor="middle">① build_system()</text>
<text x="710" y="120" fill="#64748b" font-size="8" text-anchor="middle">Scan skills/ first line at startup</text>
<text x="710" y="134" fill="#166534" font-size="8" font-weight="600" text-anchor="middle">→ inject SYSTEM prompt</text>
<!-- ② Runtime load_skill → tool_result -->
<rect x="640" y="148" width="140" height="56" rx="8" fill="#f0fdf4" stroke="#16a34a" stroke-width="1.5"/>
<text x="710" y="168" fill="#166534" font-size="10" font-weight="700" text-anchor="middle">② load_skill(name)</text>
<text x="710" y="182" fill="#64748b" font-size="8" text-anchor="middle">Read full SKILL.md at runtime</text>
<text x="710" y="196" fill="#166534" font-size="8" font-weight="600" text-anchor="middle">→ inject tool_result</text>
<!-- ① → LLM (top connection) -->
<path d="M 710 86 L 710 68 L 255 68 L 255 106" fill="none" stroke="#16a34a" stroke-width="1.5" stroke-dasharray="4,3" marker-end="url(#arrow-green)"/>
<text x="480" y="64" fill="#166534" font-size="7" font-weight="600">SYSTEM has skill catalog, carried every turn</text>
<!-- ② → load_skill (short connection) -->
<line x1="640" y1="176" x2="600" y2="197" stroke="#16a34a" stroke-width="1.5" stroke-dasharray="4,3"/>
<!-- ===== Legend ===== -->
<rect x="60" y="308" width="680" height="44" rx="6" fill="#f1f5f9"/>
<rect x="80" y="322" width="12" height="12" rx="2" fill="#f0f4ff" stroke="#2563eb" stroke-width="1"/>
<text x="100" y="332" fill="#334155" font-size="10">History preserved (loop, hooks, TODO, subagent — unchanged)</text>
<rect x="80" y="338" width="12" height="12" rx="2" fill="#dcfce7" stroke="#16a34a" stroke-width="1"/>
<text x="100" y="348" fill="#334155" font-size="10">s07 new (startup catalog in SYSTEM + load_skill tool)</text>
</svg>

After

Width:  |  Height:  |  Size: 6.9 KiB

View File

@@ -0,0 +1,110 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 380" font-family="system-ui, -apple-system, sans-serif">
<defs>
<marker id="arrow" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#555"/>
</marker>
<marker id="arrow-blue" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#2563eb"/>
</marker>
<marker id="arrow-green" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#16a34a"/>
</marker>
<marker id="arrow-amber" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#d97706"/>
</marker>
<linearGradient id="header" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="#1e3a5f"/>
<stop offset="100%" stop-color="#2563eb"/>
</linearGradient>
</defs>
<!-- 背景 -->
<rect width="800" height="380" fill="#fafbfc" rx="8"/>
<!-- タイトル -->
<rect x="0" y="0" width="800" height="48" fill="url(#header)" rx="8"/>
<rect x="0" y="40" width="800" height="8" fill="url(#header)"/>
<text x="400" y="31" fill="#fff" font-size="16" font-weight="700" text-anchor="middle">Skill Loading — 起動時にカタログ注入、実行時にオンデマンド読み込み</text>
<!-- ===== 過去章を保持 ===== -->
<text x="50" y="96" fill="#94a3b8" font-size="11" font-weight="600">過去章を保持</text>
<!-- messages[] -->
<rect x="40" y="108" width="110" height="44" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
<text x="95" y="135" fill="#1e3a5f" font-size="12" font-weight="600" text-anchor="middle">messages[]</text>
<!-- → LLM -->
<line x1="150" y1="130" x2="198" y2="130" stroke="#2563eb" stroke-width="2" marker-end="url(#arrow-blue)"/>
<!-- LLM -->
<rect x="200" y="106" width="110" height="48" rx="8" fill="#fff" stroke="#2563eb" stroke-width="1.5"/>
<text x="255" y="128" fill="#1e3a5f" font-size="13" font-weight="700" text-anchor="middle">LLM</text>
<text x="255" y="146" fill="#64748b" font-size="9" text-anchor="middle">stop_reason=tool_use?</text>
<!-- No → 戻る -->
<line x1="255" y1="154" x2="255" y2="178" stroke="#2563eb" stroke-width="2" marker-end="url(#arrow-blue)"/>
<text x="268" y="172" fill="#2563eb" font-size="9" font-weight="600">No</text>
<rect x="200" y="180" width="110" height="28" rx="14" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
<text x="255" y="198" fill="#1e3a5f" font-size="11" font-weight="600" text-anchor="middle">結果を返す</text>
<!-- Yes → PreToolUse -->
<line x1="310" y1="130" x2="348" y2="130" stroke="#555" stroke-width="2" marker-end="url(#arrow)"/>
<text x="325" y="122" fill="#d97706" font-size="9" font-weight="600">Yes</text>
<!-- PreToolUse (s04) -->
<rect x="350" y="108" width="90" height="44" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
<text x="395" y="128" fill="#1e3a5f" font-size="9" font-weight="600" text-anchor="middle">trigger_hooks</text>
<text x="395" y="142" fill="#64748b" font-size="8" text-anchor="middle">PreToolUse</text>
<!-- → TOOL_HANDLERS -->
<line x1="440" y1="130" x2="488" y2="130" stroke="#555" stroke-width="2" marker-end="url(#arrow)"/>
<!-- ===== TOOL_HANDLERS ===== -->
<rect x="490" y="88" width="120" height="130" rx="10" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
<text x="550" y="108" fill="#1e3a5f" font-size="10" font-weight="700" text-anchor="middle">TOOL_HANDLERS</text>
<!-- s06 ツール -->
<rect x="500" y="116" width="100" height="18" rx="3" fill="#fff" stroke="#2563eb" stroke-width="0.8"/>
<text x="550" y="129" fill="#1e3a5f" font-size="8" text-anchor="middle">bash · read · write</text>
<rect x="500" y="138" width="100" height="18" rx="3" fill="#fff" stroke="#2563eb" stroke-width="0.8"/>
<text x="550" y="151" fill="#1e3a5f" font-size="8" text-anchor="middle">edit · glob · todo</text>
<rect x="500" y="160" width="100" height="18" rx="3" fill="#fff" stroke="#2563eb" stroke-width="0.8"/>
<text x="550" y="173" fill="#1e3a5f" font-size="8" text-anchor="middle">task (subagent)</text>
<rect x="500" y="186" width="100" height="22" rx="3" fill="#dcfce7" stroke="#16a34a" stroke-width="1.5"/>
<text x="550" y="201" fill="#166534" font-size="9" font-weight="700" text-anchor="middle">load_skill</text>
<!-- ===== ループバック ===== -->
<path d="M 550 218 L 550 270 L 95 270 L 95 152" fill="none" stroke="#555" stroke-width="2" marker-end="url(#arrow)" stroke-dasharray="6,3"/>
<text x="320" y="290" fill="#64748b" font-size="10" text-anchor="middle">結果を messages[] に追加、ループ継続</text>
<!-- ===== s07 2 層注入ラベル(右側) ===== -->
<text x="710" y="220" fill="#16a34a" font-size="11" font-weight="700" text-anchor="middle">s07 新規</text>
<!-- ① 起動時 SYSTEM 注入 -->
<rect x="640" y="86" width="140" height="56" rx="8" fill="#f0fdf4" stroke="#16a34a" stroke-width="1.5"/>
<text x="710" y="106" fill="#166534" font-size="10" font-weight="700" text-anchor="middle">① build_system()</text>
<text x="710" y="120" fill="#64748b" font-size="8" text-anchor="middle">起動時に skills/ の 1 行目をスキャン</text>
<text x="710" y="134" fill="#166534" font-size="8" font-weight="600" text-anchor="middle">→ SYSTEM プロンプトに注入</text>
<!-- ② 実行時 load_skill → tool_result -->
<rect x="640" y="148" width="140" height="56" rx="8" fill="#f0fdf4" stroke="#16a34a" stroke-width="1.5"/>
<text x="710" y="168" fill="#166534" font-size="10" font-weight="700" text-anchor="middle">② load_skill(name)</text>
<text x="710" y="182" fill="#64748b" font-size="8" text-anchor="middle">実行時に完全な SKILL.md を読み取り</text>
<text x="710" y="196" fill="#166534" font-size="8" font-weight="600" text-anchor="middle">→ tool_result に注入</text>
<!-- ① → LLM上部接続 -->
<path d="M 710 86 L 710 68 L 255 68 L 255 106" fill="none" stroke="#16a34a" stroke-width="1.5" stroke-dasharray="4,3" marker-end="url(#arrow-green)"/>
<text x="480" y="64" fill="#166534" font-size="7" font-weight="600">SYSTEM にスキルカタログ、毎ターン携帯</text>
<!-- ② → load_skill短接続 -->
<line x1="640" y1="176" x2="600" y2="197" stroke="#16a34a" stroke-width="1.5" stroke-dasharray="4,3"/>
<!-- ===== 凡例 ===== -->
<rect x="60" y="308" width="680" height="44" rx="6" fill="#f1f5f9"/>
<rect x="80" y="322" width="12" height="12" rx="2" fill="#f0f4ff" stroke="#2563eb" stroke-width="1"/>
<text x="100" y="332" fill="#334155" font-size="10">過去章を保持ループ、フック、TODO、サブ Agent — 変更なし)</text>
<rect x="80" y="338" width="12" height="12" rx="2" fill="#dcfce7" stroke="#16a34a" stroke-width="1"/>
<text x="100" y="348" fill="#334155" font-size="10">s07 新規(起動時カタログ注入 SYSTEM + load_skill ツール)</text>
</svg>

After

Width:  |  Height:  |  Size: 7.1 KiB

View File

@@ -0,0 +1,110 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 380" font-family="system-ui, -apple-system, sans-serif">
<defs>
<marker id="arrow" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#555"/>
</marker>
<marker id="arrow-blue" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#2563eb"/>
</marker>
<marker id="arrow-green" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#16a34a"/>
</marker>
<marker id="arrow-amber" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#d97706"/>
</marker>
<linearGradient id="header" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="#1e3a5f"/>
<stop offset="100%" stop-color="#2563eb"/>
</linearGradient>
</defs>
<!-- 背景 -->
<rect width="800" height="380" fill="#fafbfc" rx="8"/>
<!-- 标题 -->
<rect x="0" y="0" width="800" height="48" fill="url(#header)" rx="8"/>
<rect x="0" y="40" width="800" height="8" fill="url(#header)"/>
<text x="400" y="31" fill="#fff" font-size="16" font-weight="700" text-anchor="middle">Skill Loading — 启动时注入目录,运行时按需加载内容</text>
<!-- ===== 历史章节保留 ===== -->
<text x="50" y="96" fill="#94a3b8" font-size="11" font-weight="600">历史章节保留</text>
<!-- messages[] -->
<rect x="40" y="108" width="110" height="44" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
<text x="95" y="135" fill="#1e3a5f" font-size="12" font-weight="600" text-anchor="middle">messages[]</text>
<!-- → LLM -->
<line x1="150" y1="130" x2="198" y2="130" stroke="#2563eb" stroke-width="2" marker-end="url(#arrow-blue)"/>
<!-- LLM -->
<rect x="200" y="106" width="110" height="48" rx="8" fill="#fff" stroke="#2563eb" stroke-width="1.5"/>
<text x="255" y="128" fill="#1e3a5f" font-size="13" font-weight="700" text-anchor="middle">LLM</text>
<text x="255" y="146" fill="#64748b" font-size="9" text-anchor="middle">stop_reason=tool_use?</text>
<!-- 否 → 返回 -->
<line x1="255" y1="154" x2="255" y2="178" stroke="#2563eb" stroke-width="2" marker-end="url(#arrow-blue)"/>
<text x="268" y="172" fill="#2563eb" font-size="9" font-weight="600"></text>
<rect x="200" y="180" width="110" height="28" rx="14" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
<text x="255" y="198" fill="#1e3a5f" font-size="11" font-weight="600" text-anchor="middle">返回结果</text>
<!-- 是 → PreToolUse -->
<line x1="310" y1="130" x2="348" y2="130" stroke="#555" stroke-width="2" marker-end="url(#arrow)"/>
<text x="325" y="122" fill="#d97706" font-size="9" font-weight="600"></text>
<!-- PreToolUse (s04) -->
<rect x="350" y="108" width="90" height="44" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
<text x="395" y="128" fill="#1e3a5f" font-size="9" font-weight="600" text-anchor="middle">trigger_hooks</text>
<text x="395" y="142" fill="#64748b" font-size="8" text-anchor="middle">PreToolUse</text>
<!-- → TOOL_HANDLERS -->
<line x1="440" y1="130" x2="488" y2="130" stroke="#555" stroke-width="2" marker-end="url(#arrow)"/>
<!-- ===== TOOL_HANDLERS ===== -->
<rect x="490" y="88" width="120" height="130" rx="10" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
<text x="550" y="108" fill="#1e3a5f" font-size="10" font-weight="700" text-anchor="middle">TOOL_HANDLERS</text>
<!-- s06 工具 -->
<rect x="500" y="116" width="100" height="18" rx="3" fill="#fff" stroke="#2563eb" stroke-width="0.8"/>
<text x="550" y="129" fill="#1e3a5f" font-size="8" text-anchor="middle">bash · read · write</text>
<rect x="500" y="138" width="100" height="18" rx="3" fill="#fff" stroke="#2563eb" stroke-width="0.8"/>
<text x="550" y="151" fill="#1e3a5f" font-size="8" text-anchor="middle">edit · glob · todo</text>
<rect x="500" y="160" width="100" height="18" rx="3" fill="#fff" stroke="#2563eb" stroke-width="0.8"/>
<text x="550" y="173" fill="#1e3a5f" font-size="8" text-anchor="middle">task (subagent)</text>
<rect x="500" y="186" width="100" height="22" rx="3" fill="#dcfce7" stroke="#16a34a" stroke-width="1.5"/>
<text x="550" y="201" fill="#166534" font-size="9" font-weight="700" text-anchor="middle">load_skill</text>
<!-- ===== 回环 ===== -->
<path d="M 550 218 L 550 270 L 95 270 L 95 152" fill="none" stroke="#555" stroke-width="2" marker-end="url(#arrow)" stroke-dasharray="6,3"/>
<text x="320" y="290" fill="#64748b" font-size="10" text-anchor="middle">结果追加到 messages[],循环继续</text>
<!-- ===== s07 两级注入标注(右侧) ===== -->
<text x="710" y="220" fill="#16a34a" font-size="11" font-weight="700" text-anchor="middle">s07 新增</text>
<!-- ① 启动时注入 SYSTEM -->
<rect x="640" y="86" width="140" height="56" rx="8" fill="#f0fdf4" stroke="#16a34a" stroke-width="1.5"/>
<text x="710" y="106" fill="#166534" font-size="10" font-weight="700" text-anchor="middle">① build_system()</text>
<text x="710" y="120" fill="#64748b" font-size="8" text-anchor="middle">启动时扫描 skills/ 第一行</text>
<text x="710" y="134" fill="#166534" font-size="8" font-weight="600" text-anchor="middle">→ 注入 SYSTEM prompt</text>
<!-- ② 运行时 load_skill → tool_result -->
<rect x="640" y="148" width="140" height="56" rx="8" fill="#f0fdf4" stroke="#16a34a" stroke-width="1.5"/>
<text x="710" y="168" fill="#166534" font-size="10" font-weight="700" text-anchor="middle">② load_skill(name)</text>
<text x="710" y="182" fill="#64748b" font-size="8" text-anchor="middle">运行时读完整 SKILL.md</text>
<text x="710" y="196" fill="#166534" font-size="8" font-weight="600" text-anchor="middle">→ 注入 tool_result</text>
<!-- ① → LLM顶部连线 -->
<path d="M 710 86 L 710 68 L 255 68 L 255 106" fill="none" stroke="#16a34a" stroke-width="1.5" stroke-dasharray="4,3" marker-end="url(#arrow-green)"/>
<text x="480" y="64" fill="#166534" font-size="7" font-weight="600">SYSTEM 含技能目录,每轮都带</text>
<!-- ② → load_skill短连接线 -->
<line x1="640" y1="176" x2="600" y2="197" stroke="#16a34a" stroke-width="1.5" stroke-dasharray="4,3"/>
<!-- ===== 图例 ===== -->
<rect x="60" y="308" width="680" height="44" rx="6" fill="#f1f5f9"/>
<rect x="80" y="322" width="12" height="12" rx="2" fill="#f0f4ff" stroke="#2563eb" stroke-width="1"/>
<text x="100" y="332" fill="#334155" font-size="10">历史章节保留循环、钩子、TODO、subagent — 完全不变)</text>
<rect x="80" y="338" width="12" height="12" rx="2" fill="#dcfce7" stroke="#16a34a" stroke-width="1"/>
<text x="100" y="348" fill="#334155" font-size="10">s07 新增(启动时目录注入 SYSTEM + load_skill 工具)</text>
</svg>

After

Width:  |  Height:  |  Size: 6.9 KiB