mirror of
https://github.com/shareAI-lab/analysis_claude_code.git
synced 2026-09-20 12:13:38 +08:00
Merge remote-tracking branch 'shareai/main' into fix/pr510-subprocess-output
# Conflicts: # s07_skill_loading/code.py
This commit is contained in:
@@ -1,182 +0,0 @@
|
||||
# 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
|
||||
|
||||

|
||||
|
||||
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; SKILL.md can guide later read_file/bash access to extra resources | ~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 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
|
||||
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; SKILL.md may guide later resource access |
|
||||
| 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 loaded SKILL.md can still guide later access to referenced resources through existing file/bash tools.
|
||||
|
||||
### 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@v2, en@v2, ja@v2 -->
|
||||
@@ -1,28 +1,29 @@
|
||||
# s07: Skill Loading — 必要なときにだけ読み込む
|
||||
# s07: Skill Loading — 必要なときにスキルを読み込む
|
||||
|
||||
[中文](README.md) · [English](README.en.md) · [日本語](README.ja.md)
|
||||
[English](README.md) · [中文](README.zh.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 には詰め込まない。
|
||||
s01 → s02 → s03 → s04 → s05 → s06 → `s07` → [s08](../s08_context_compact/) → s09 → ... → s16 → s17
|
||||
|
||||
> system prompt にはスキルカタログを入れ、`load_skill` は完全な `SKILL.md` を返す。
|
||||
>
|
||||
> **Harness レイヤー**: 知識 — 必要に応じて読み込み、コンテキストに詰め込まない。
|
||||
> **Harness レイヤー**:知識の読み込み — 利用可能なスキルをモデルに示し、名前で内容を読み込む。
|
||||
|
||||
---
|
||||
|
||||
## 課題
|
||||
|
||||
プロジェクトには React コンポーネント仕様、SQL スタイルガイド、API 設計ドキュメントがある。Agent にこれらの仕様を自動的に守らせたい。最も直接的な方法 — すべて system prompt に詰め込む:
|
||||
あるプロジェクトに 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 行
|
||||
+ open("docs/react-style.md").read()
|
||||
+ open("docs/sql-style.md").read()
|
||||
+ open("docs/api-design.md").read()
|
||||
)
|
||||
```
|
||||
|
||||
6500 行の system prompt。Agent は LLM を呼び出すたびにこれらのドキュメントを運ぶ — CSS の色を変えるときも SQL クエリを修正するときも。99% の内容が現在のタスクと無関係で、トークンを無駄に消費する。
|
||||
この方法で Agent はすべての規約を読めるが、3 つの文書すべてが system prompt に固定され、現在のタスクに必要な文書だけを選べない。LLM を呼び出すたびに、3 つの文書の全文がモデルへ送られる。タスクが React コンポーネントの変更だけなら、必要なのは React コンポーネント仕様だけである。無関係な SQL スタイルガイドと API 設計ドキュメントも入力 token とコンテキストウィンドウを使うため、コード、会話、tool result に使える領域が減る。
|
||||
|
||||
---
|
||||
|
||||
@@ -30,24 +31,20 @@ SYSTEM = (
|
||||
|
||||

|
||||
|
||||
前章の最小フック構造、`todo_write`、サブ Agent を維持し、本章は新規の `load_skill` ツールに注目する。起動時にスキルカタログを SYSTEM prompt に注入し、実行時に完全な内容を読み込むツールを登録する。使ったときだけトークンを消費。
|
||||
起動時に `SkillLoader` が `skills/*/SKILL.md` を走査し、YAML frontmatter の `name` と `description` を読み取って、カタログを system prompt に追加する。完全な指示が必要になると、モデルは `load_skill(name)` を呼ぶ。返された `SKILL.md` は `tool_result` としてメッセージリストへ追加される。
|
||||
|
||||
2 層設計:
|
||||
|
||||
| 層 | 場所 | タイミング | コスト |
|
||||
|---|------|-----------|--------|
|
||||
| 1. カタログ | system prompt | 起動時に注入(harness が skills/ をスキャン) | ~100 トークン/スキル、毎ターン携帯 |
|
||||
| 2. 内容 | tool_result | Agent が load_skill を呼び出したとき。SKILL.md は、必要に応じて read_file/bash で追加リソースへアクセスするための手がかりになる | ~2000 トークン/スキル、オンデマンド |
|
||||
|
||||
ディスパッチ機構は変わらず、`load_skill` は `TOOL_HANDLERS[block.name]` を通じて自動的にディスパッチされる。
|
||||
| 内容 | モデル入力での位置 | 追加時点 |
|
||||
|------|--------------------|----------|
|
||||
| スキル名と説明 | system prompt | 起動時 |
|
||||
| 完全な `SKILL.md` | `tool_result` | `load_skill` 呼び出し時 |
|
||||
|
||||
---
|
||||
|
||||
## 仕組み
|
||||
|
||||
**skills/ ディレクトリ**、スキルごとに 1 つのサブディレクトリ、それぞれに `SKILL.md` ファイルを含む:
|
||||
各スキルは `SKILL.md` を持つディレクトリである:
|
||||
|
||||
```
|
||||
```text
|
||||
skills/
|
||||
agent-builder/SKILL.md
|
||||
code-review/SKILL.md
|
||||
@@ -55,64 +52,67 @@ skills/
|
||||
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] = {}
|
||||
class SkillLoader:
|
||||
def scan(self):
|
||||
self.skills.clear()
|
||||
skills_root = self.skills_dir.resolve()
|
||||
for manifest in sorted(self.skills_dir.glob("*/SKILL.md")):
|
||||
if (not manifest.is_file()
|
||||
or not manifest.resolve().is_relative_to(skills_root)):
|
||||
continue
|
||||
content = manifest.read_text(encoding="utf-8")
|
||||
metadata, body = self.parse_frontmatter(content)
|
||||
raw_name = metadata.get("name")
|
||||
name = raw_name.strip() if isinstance(raw_name, str) else ""
|
||||
name = name or manifest.parent.name
|
||||
raw_description = metadata.get("description")
|
||||
description = (raw_description.strip()
|
||||
if isinstance(raw_description, str) else "")
|
||||
description = description or body.split("\n", 1)[0]
|
||||
description = " ".join(str(description).lstrip("# ").split())
|
||||
self.skills[name] = {
|
||||
"name": name,
|
||||
"description": description,
|
||||
"content": content,
|
||||
}
|
||||
```
|
||||
|
||||
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}
|
||||
`catalog()` は名前と説明だけを返す:
|
||||
|
||||
_scan_skills() # runs once at startup
|
||||
```text
|
||||
- code-review: Perform thorough code reviews...
|
||||
- pdf: Process PDF files...
|
||||
```
|
||||
|
||||
def list_skills() -> str:
|
||||
return "\n".join(f"- **{s['name']}**: {s['description']}" for s in SKILL_REGISTRY.values())
|
||||
### system prompt を組み立てる
|
||||
|
||||
def build_system() -> str:
|
||||
catalog = list_skills()
|
||||
```python
|
||||
def build_system_prompt() -> str:
|
||||
return (
|
||||
f"You are a coding agent at {WORKDIR}. "
|
||||
f"Skills available:\n{catalog}\n"
|
||||
"Use load_skill to get full details when needed."
|
||||
f"You are a coding agent at {WORKDIR}. Use tools to solve tasks. "
|
||||
"Act, don't explain.\n\n"
|
||||
f"Skills available:\n{SKILL_LOADER.catalog()}\n\n"
|
||||
"Use load_skill to read the full instructions when a skill applies."
|
||||
)
|
||||
|
||||
SYSTEM = build_system()
|
||||
```
|
||||
|
||||
**第 2 層:load_skill**:Agent が「SQL スタイルガイドが必要」と判断し、`load_skill("sql-style")` を呼び出す。レジストリを通じて検索し、ファイルパスを経由しないため、パストラバーサルのリスクがない。SKILL.md の内容は `tool_result` を通じて注入され、既存の file および bash ツールを通じて、参照される `references/`、`scripts/`、`assets/` へのその後のアクセスも含められる。
|
||||
固定された Agent の指示と、起動時に見つかったスキルカタログをこの関数で組み合わせる。
|
||||
|
||||
### 完全な内容を読み込む
|
||||
|
||||
```python
|
||||
def load_skill(name: str) -> str:
|
||||
skill = SKILL_REGISTRY.get(name)
|
||||
if not skill:
|
||||
return f"Skill not found: {name}"
|
||||
return skill["content"]
|
||||
def load(self, name: str) -> str:
|
||||
skill = self.skills.get(name)
|
||||
if skill:
|
||||
return skill["content"]
|
||||
available = ", ".join(self.skills) or "none"
|
||||
return f"Error: Unknown skill '{name}'. Available: {available}"
|
||||
```
|
||||
|
||||
重要な違い:スキル内容は 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。SKILL.md がその後のリソースアクセスを案内できる |
|
||||
| SYSTEM プロンプト | 静的文字列 | 起動時に skills/ をスキャンしてカタログ注入 |
|
||||
| スキルレジストリ | なし | SKILL_REGISTRY(起動時に充填、パストラバーサル防止) |
|
||||
| ループ | 変更なし | 変更なし(スキルツールは自動ディスパッチ) |
|
||||
`name` は起動時に作られたレジストリの検索に使われ、ファイルパスとして解釈されない。ツールが返ると、既存の Agent Loop が内容を新しい `tool_result` メッセージとして追加する。
|
||||
|
||||
---
|
||||
|
||||
@@ -123,60 +123,21 @@ 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`
|
||||
3. `Review README.md and load the relevant skill first`
|
||||
|
||||
観察のポイント:Agent は SYSTEM 内のカタログから利用可能なスキルを知っているか? 完全な手順が必要なときに `[HOOK] load_skill` が表示されるか? 読み込んだスキルの説明を使って回答しているか?
|
||||
system prompt にカタログだけが入り、`load_skill` の呼び出し後に完全な `SKILL.md` が現れることを確認する。
|
||||
|
||||
---
|
||||
|
||||
## 次へ
|
||||
|
||||
オンデマンド読み込みで「運ぶべきでないものは運ばない」問題は解決した。しかし別の問題が待っている:Agent が 30 分連続で作業すると、messages リストが中間プロセスで埋め尽くされる。古い tool_result、期限切れのファイル内容、コンテキストを占領しているが価値を生まない。
|
||||
ツール呼び出しが増えると、`messages[]` には以前のファイル内容やツール結果が残る。
|
||||
|
||||
→ s08 Context Compact:4 層圧縮戦略。安価な層を先に実行、高価な層を後に実行。
|
||||
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` | スキルレベルのフック設定 |
|
||||
| `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 を通じて注入」として簡略化している。読み込まれた SKILL.md は、モデルが後続で既存の file/bash ツールから関連リソースへアクセスする際の手がかりにもなる。
|
||||
|
||||
### 教育版の単純化は意図的
|
||||
|
||||
- 複数ファイル・複数ソース → 1 つの `skills/` ディレクトリ:2 層読み込みの核心概念を示すのに十分
|
||||
- 複数の frontmatter フィールド → name/description のみ解析:解析の複雑さを削減
|
||||
- forked skills(`context: 'fork'`)→ 省略:教育版では inline skill loading のみ展開する
|
||||
- `Skill` ツールの入力 `skill`+`args` → 教育版は `name` を使用:追加の引数解析の複雑さを回避
|
||||
|
||||
</details>
|
||||
|
||||
<!-- translation-sync: zh@v2, en@v2, ja@v2 -->
|
||||
<!-- translation-sync: zh@v6, en@v6, ja@v6 -->
|
||||
|
||||
@@ -1,53 +1,50 @@
|
||||
# s07: Skill Loading — 用到的时候才加载
|
||||
# s07: Skill Loading — Load Skills When Needed
|
||||
|
||||
[中文](README.md) · [English](README.en.md) · [日本語](README.ja.md)
|
||||
[English](README.md) · [中文](README.zh.md) · [日本語](README.ja.md)
|
||||
|
||||
s01 → s02 → s03 → s04 → s05 → s06 → `s07` → [s08](../s08_context_compact/) → s09 → ... → s20
|
||||
> *"用到时再加载, 别全塞 prompt 里"* — 通过 tool_result 注入, 不塞 system prompt。
|
||||
s01 → s02 → s03 → s04 → s05 → s06 → `s07` → [s08](../s08_context_compact/) → s09 → ... → s16 → s17
|
||||
|
||||
> The system prompt contains the skill catalog; `load_skill` returns the full `SKILL.md`.
|
||||
>
|
||||
> **Harness 层**: 知识 — 按需加载, 不堆满上下文。
|
||||
> **Harness Layer**: Knowledge loading — show the model which skills exist, then load one by name.
|
||||
|
||||
---
|
||||
|
||||
## 问题
|
||||
## The Problem
|
||||
|
||||
你的项目有一套 React 组件规范、一份 SQL 风格指南、一份 API 设计文档。你希望 Agent 自动遵守这些规范。最直接的想法,全塞进 system prompt:
|
||||
Suppose a project has a React component specification, a SQL style guide, and an API design document. We want the Agent to follow these rules during development, so the most direct approach is to put all of them into the 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 行
|
||||
+ open("docs/react-style.md").read()
|
||||
+ open("docs/sql-style.md").read()
|
||||
+ open("docs/api-design.md").read()
|
||||
)
|
||||
```
|
||||
|
||||
6500 行 system prompt。Agent 每次调用 LLM 都带着这些文档——不管是在改 CSS 颜色还是修 SQL 查询。99% 的内容和当前任务无关,白白消耗 token。
|
||||
This approach lets the Agent read every specification, but it fixes all three documents in the system prompt instead of selecting only the one needed for the current task. Every LLM call sends the full text of all three documents to the model. When the task only changes React components, only the React specification is relevant; the SQL style guide and API design document still consume input tokens and context-window space that could hold code, conversation, and tool results.
|
||||
|
||||
---
|
||||
|
||||
## 解决方案
|
||||
## The Solution
|
||||
|
||||

|
||||

|
||||
|
||||
保留上一章的最小 hook 结构、`todo_write` 和子 Agent,本章重点转向新增的 `load_skill` 工具。启动时把技能目录注入 SYSTEM prompt,运行时多注册一个工具加载完整内容,用到才花 token。
|
||||
At startup, `SkillLoader` scans `skills/*/SKILL.md`, reads `name` and `description` from YAML frontmatter, and adds that catalog to the system prompt. When the model needs the full instructions, it calls `load_skill(name)`; the returned `SKILL.md` is appended to the message list as a `tool_result`.
|
||||
|
||||
两层设计:
|
||||
|
||||
| 层 | 位置 | 时机 | 代价 |
|
||||
|---|------|------|------|
|
||||
| 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]` 分发。
|
||||
| Content | Model input | Added |
|
||||
|---------|-------------|-------|
|
||||
| Skill name and description | system prompt | At startup |
|
||||
| Full `SKILL.md` | `tool_result` | When `load_skill` is called |
|
||||
|
||||
---
|
||||
|
||||
## 工作原理
|
||||
## How It Works
|
||||
|
||||
**skills/ 目录**,每个技能一个子目录,包含 `SKILL.md` 文件:
|
||||
Each skill is a directory containing `SKILL.md`:
|
||||
|
||||
```
|
||||
```text
|
||||
skills/
|
||||
agent-builder/SKILL.md
|
||||
code-review/SKILL.md
|
||||
@@ -55,128 +52,92 @@ skills/
|
||||
pdf/SKILL.md
|
||||
```
|
||||
|
||||
**第一级:启动时注入目录**:harness 启动时调用 `_scan_skills()` 扫描 skills/ 目录,解析每个 SKILL.md 的 YAML frontmatter(`name`、`description`),存入 `SKILL_REGISTRY` 字典。`list_skills()` 从注册表生成目录,注入 SYSTEM prompt。Agent 每轮都能看到"我有哪些技能可用",不花额外 API 调用:
|
||||
### Scan Skills
|
||||
|
||||
```python
|
||||
SKILL_REGISTRY: dict[str, dict] = {}
|
||||
class SkillLoader:
|
||||
def scan(self):
|
||||
self.skills.clear()
|
||||
skills_root = self.skills_dir.resolve()
|
||||
for manifest in sorted(self.skills_dir.glob("*/SKILL.md")):
|
||||
if (not manifest.is_file()
|
||||
or not manifest.resolve().is_relative_to(skills_root)):
|
||||
continue
|
||||
content = manifest.read_text(encoding="utf-8")
|
||||
metadata, body = self.parse_frontmatter(content)
|
||||
raw_name = metadata.get("name")
|
||||
name = raw_name.strip() if isinstance(raw_name, str) else ""
|
||||
name = name or manifest.parent.name
|
||||
raw_description = metadata.get("description")
|
||||
description = (raw_description.strip()
|
||||
if isinstance(raw_description, str) else "")
|
||||
description = description or body.split("\n", 1)[0]
|
||||
description = " ".join(str(description).lstrip("# ").split())
|
||||
self.skills[name] = {
|
||||
"name": name,
|
||||
"description": description,
|
||||
"content": content,
|
||||
}
|
||||
```
|
||||
|
||||
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}
|
||||
`catalog()` returns only names and descriptions:
|
||||
|
||||
_scan_skills() # runs once at startup
|
||||
```text
|
||||
- code-review: Perform thorough code reviews...
|
||||
- pdf: Process PDF files...
|
||||
```
|
||||
|
||||
def list_skills() -> str:
|
||||
return "\n".join(f"- **{s['name']}**: {s['description']}" for s in SKILL_REGISTRY.values())
|
||||
### Build the System Prompt
|
||||
|
||||
def build_system() -> str:
|
||||
catalog = list_skills()
|
||||
```python
|
||||
def build_system_prompt() -> str:
|
||||
return (
|
||||
f"You are a coding agent at {WORKDIR}. "
|
||||
f"Skills available:\n{catalog}\n"
|
||||
"Use load_skill to get full details when needed."
|
||||
f"You are a coding agent at {WORKDIR}. Use tools to solve tasks. "
|
||||
"Act, don't explain.\n\n"
|
||||
f"Skills available:\n{SKILL_LOADER.catalog()}\n\n"
|
||||
"Use load_skill to read the full instructions when a skill applies."
|
||||
)
|
||||
|
||||
SYSTEM = build_system()
|
||||
```
|
||||
|
||||
**第二级:load_skill**:Agent 决定"我需要 SQL 风格指南",调用 `load_skill("sql-style")`。通过注册表查找,不走文件路径,没有路径遍历风险。SKILL.md 内容通过 `tool_result` 注入,并可通过现有的 file 和 bash 工具进一步访问引用的 `references/`、`scripts/` 或 `assets/`。
|
||||
This function combines the fixed Agent instructions with the catalog found at startup.
|
||||
|
||||
### Load Full Content
|
||||
|
||||
```python
|
||||
def load_skill(name: str) -> str:
|
||||
skill = SKILL_REGISTRY.get(name)
|
||||
if not skill:
|
||||
return f"Skill not found: {name}"
|
||||
return skill["content"]
|
||||
def load(self, name: str) -> str:
|
||||
skill = self.skills.get(name)
|
||||
if skill:
|
||||
return skill["content"]
|
||||
available = ", ".join(self.skills) or "none"
|
||||
return f"Error: Unknown skill '{name}'. Available: {available}"
|
||||
```
|
||||
|
||||
关键区别:技能内容不是 system prompt 的一部分,它作为一次工具结果进入当前 messages。后续调用会随历史一起携带,直到上下文压缩、截断或会话结束。这和 s08 的 compact 自然衔接:按需加载解决了"不该提前带的不要带",compact 解决"该丢的怎么丢"。
|
||||
`name` looks up the startup registry; it is not interpreted as a file path. After the tool returns, the existing Agent Loop appends its content as a new `tool_result` message.
|
||||
|
||||
---
|
||||
|
||||
## 相对 s06 的变更
|
||||
|
||||
| 组件 | 之前 (s06) | 之后 (s07) |
|
||||
|------|-----------|-----------|
|
||||
| 工具数量 | 7 (bash, read, write, edit, glob, todo_write, task) | 8 (+load_skill) |
|
||||
| 知识加载 | 无 | 两级:启动时目录注入 SYSTEM + 运行时 load_skill;SKILL.md 可指引后续资源访问 |
|
||||
| SYSTEM 提示 | 静态字符串 | 启动时扫描 skills/ 注入目录 |
|
||||
| 技能注册表 | 无 | SKILL_REGISTRY(启动时填充,防路径遍历) |
|
||||
| 循环 | 不变 | 不变(skill 工具自动分发) |
|
||||
|
||||
---
|
||||
|
||||
## 试一下
|
||||
## Try It
|
||||
|
||||
```sh
|
||||
cd learn-claude-code
|
||||
python s07_skill_loading/code.py
|
||||
```
|
||||
|
||||
试试这些 prompt:
|
||||
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`
|
||||
3. `Review README.md and load the relevant skill first`
|
||||
|
||||
观察重点:Agent 是否直接从 SYSTEM 里的目录知道有哪些技能?需要完整规范时是否出现 `[HOOK] load_skill`?加载后回答是否使用了对应 skill 的说明?
|
||||
Check that the system prompt contains only the catalog and that the full `SKILL.md` appears after `load_skill` is called.
|
||||
|
||||
---
|
||||
|
||||
## 接下来
|
||||
## What's Next
|
||||
|
||||
按需加载解决了"不该带的不要带"。但另一个问题来了:Agent 连续工作 30 分钟后,messages 列表塞满了中间过程。旧的 tool_result、过时的文件内容,占着上下文但不产生价值。
|
||||
As tool calls accumulate, `messages[]` retains earlier file contents and tool results.
|
||||
|
||||
s08 Context Compact → 四层压缩策略。便宜的先跑,贵的后跑。
|
||||
→ s08 Context Compact: shorten earlier messages and keep context available for later calls.
|
||||
|
||||
<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 注入"是一种简化;加载后的 SKILL.md 仍可作为指引,帮助模型后续通过现有 file/bash 工具访问相关资源。
|
||||
|
||||
### 教学版的简化是刻意的
|
||||
|
||||
- 多文件多来源 → 1 个 `skills/` 目录:足以展示两级加载的核心概念
|
||||
- 多个 frontmatter 字段 → 只解析 name/description:减少解析复杂度
|
||||
- forked skills(`context: 'fork'`)→ 省略:教学版只展开 inline 技能加载
|
||||
- `Skill` 工具输入 `skill`+`args` → 教学版用 `name`:避免参数解析的额外复杂度
|
||||
|
||||
</details>
|
||||
|
||||
<!-- translation-sync: zh@v2, en@v2, ja@v2 -->
|
||||
<!-- translation-sync: zh@v6, en@v6, ja@v6 -->
|
||||
|
||||
143
s07_skill_loading/README.zh.md
Normal file
143
s07_skill_loading/README.zh.md
Normal file
@@ -0,0 +1,143 @@
|
||||
# s07: Skill Loading — 用到时再加载
|
||||
|
||||
[English](README.md) · [中文](README.zh.md) · [日本語](README.ja.md)
|
||||
|
||||
s01 → s02 → s03 → s04 → s05 → s06 → `s07` → [s08](../s08_context_compact/) → s09 → ... → s16 → s17
|
||||
|
||||
> system prompt 保存技能目录;`load_skill` 返回完整的 `SKILL.md`。
|
||||
>
|
||||
> **Harness 层**:知识加载 — 让模型先知道有哪些技能,再按名称读取内容。
|
||||
|
||||
---
|
||||
|
||||
## 问题
|
||||
|
||||
假设某个项目有一套 React 组件规范、一份 SQL 风格指南和一份 API 设计文档。我们希望 Agent 在开发过程中遵守这些规范,最直接的做法就是把它们全部放进 system prompt:
|
||||
|
||||
```python
|
||||
SYSTEM = (
|
||||
f"You are a coding agent. "
|
||||
+ open("docs/react-style.md").read()
|
||||
+ open("docs/sql-style.md").read()
|
||||
+ open("docs/api-design.md").read()
|
||||
)
|
||||
```
|
||||
|
||||
这种做法能让 Agent 读到所有规范,但问题在于,三份文档被固定放进了 system prompt,无法根据当前任务只选择需要的那一份。每次调用 LLM 时,三份文档的全文都会一起发送给模型。当前任务只修改 React 组件时,实际需要的只有 React 组件规范;SQL 风格指南和 API 设计文档与任务无关,却仍然占用输入 token 和上下文窗口,留给代码、对话和工具结果的空间也会变少。
|
||||
|
||||
---
|
||||
|
||||
## 解决方案
|
||||
|
||||

|
||||
|
||||
启动时,`SkillLoader` 扫描 `skills/*/SKILL.md`,读取 YAML frontmatter 中的 `name` 和 `description`,并把这份目录加入 system prompt。模型需要完整说明时,调用 `load_skill(name)`;返回的 `SKILL.md` 作为 `tool_result` 追加到消息列表。
|
||||
|
||||
| 内容 | 进入模型的位置 | 何时加入 |
|
||||
|------|----------------|----------|
|
||||
| 技能名称和描述 | system prompt | 启动时 |
|
||||
| 完整 `SKILL.md` | `tool_result` | 调用 `load_skill` 时 |
|
||||
|
||||
---
|
||||
|
||||
## 工作原理
|
||||
|
||||
每个技能是一个包含 `SKILL.md` 的目录:
|
||||
|
||||
```text
|
||||
skills/
|
||||
agent-builder/SKILL.md
|
||||
code-review/SKILL.md
|
||||
mcp-builder/SKILL.md
|
||||
pdf/SKILL.md
|
||||
```
|
||||
|
||||
### 扫描技能
|
||||
|
||||
```python
|
||||
class SkillLoader:
|
||||
def scan(self):
|
||||
self.skills.clear()
|
||||
skills_root = self.skills_dir.resolve()
|
||||
for manifest in sorted(self.skills_dir.glob("*/SKILL.md")):
|
||||
if (not manifest.is_file()
|
||||
or not manifest.resolve().is_relative_to(skills_root)):
|
||||
continue
|
||||
content = manifest.read_text(encoding="utf-8")
|
||||
metadata, body = self.parse_frontmatter(content)
|
||||
raw_name = metadata.get("name")
|
||||
name = raw_name.strip() if isinstance(raw_name, str) else ""
|
||||
name = name or manifest.parent.name
|
||||
raw_description = metadata.get("description")
|
||||
description = (raw_description.strip()
|
||||
if isinstance(raw_description, str) else "")
|
||||
description = description or body.split("\n", 1)[0]
|
||||
description = " ".join(str(description).lstrip("# ").split())
|
||||
self.skills[name] = {
|
||||
"name": name,
|
||||
"description": description,
|
||||
"content": content,
|
||||
}
|
||||
```
|
||||
|
||||
`catalog()` 只输出名称和描述:
|
||||
|
||||
```text
|
||||
- code-review: Perform thorough code reviews...
|
||||
- pdf: Process PDF files...
|
||||
```
|
||||
|
||||
### 组装 system prompt
|
||||
|
||||
```python
|
||||
def build_system_prompt() -> str:
|
||||
return (
|
||||
f"You are a coding agent at {WORKDIR}. Use tools to solve tasks. "
|
||||
"Act, don't explain.\n\n"
|
||||
f"Skills available:\n{SKILL_LOADER.catalog()}\n\n"
|
||||
"Use load_skill to read the full instructions when a skill applies."
|
||||
)
|
||||
```
|
||||
|
||||
固定的 Agent 指令和扫描得到的技能目录在这里组成实际传给模型的 system prompt。
|
||||
|
||||
### 加载完整内容
|
||||
|
||||
```python
|
||||
def load(self, name: str) -> str:
|
||||
skill = self.skills.get(name)
|
||||
if skill:
|
||||
return skill["content"]
|
||||
available = ", ".join(self.skills) or "none"
|
||||
return f"Error: Unknown skill '{name}'. Available: {available}"
|
||||
```
|
||||
|
||||
`name` 用于查询启动时建立的注册表,不会被当作文件路径。工具返回后,原有 Agent Loop 会把内容作为新的 `tool_result` 消息追加。
|
||||
|
||||
---
|
||||
|
||||
## 试一下
|
||||
|
||||
```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. `Review README.md and load the relevant skill first`
|
||||
|
||||
观察 system prompt 中是否只有技能目录,以及调用 `load_skill` 后是否出现完整的 `SKILL.md` 内容。
|
||||
|
||||
---
|
||||
|
||||
## 接下来
|
||||
|
||||
随着工具调用增加,`messages[]` 会积累较早的文件内容和工具结果。
|
||||
|
||||
s08 Context Compact → 缩短较早的消息,为后续调用保留上下文空间。
|
||||
|
||||
|
||||
<!-- translation-sync: zh@v6, en@v6, ja@v6 -->
|
||||
@@ -1,38 +1,36 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
s07: Skill Loading — two-level on-demand knowledge injection.
|
||||
s07_skill_loading.py - Skill Loading
|
||||
|
||||
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"
|
||||
The system prompt contains a catalog of skill names and descriptions.
|
||||
The model loads the full SKILL.md only when it calls load_skill.
|
||||
|
||||
Layer 2 (expensive, on demand):
|
||||
Agent calls load_skill("code-review") → full SKILL.md content
|
||||
injected via tool_result (~2000 tokens/skill)
|
||||
skills/ Startup
|
||||
+------------------+ +------------------+
|
||||
| code-review/ | ----> | SkillLoader |
|
||||
| SKILL.md | | name + summary |
|
||||
| pdf/ | +--------+---------+
|
||||
| SKILL.md | |
|
||||
+------------------+ v
|
||||
system prompt catalog
|
||||
|
||||
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 pyyaml + ANTHROPIC_API_KEY in .env
|
||||
LLM -- load_skill(name) --> full SKILL.md
|
||||
^ |
|
||||
+--------- tool_result --------+
|
||||
"""
|
||||
|
||||
import ast, json, os, subprocess
|
||||
import os
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import yaml
|
||||
|
||||
try:
|
||||
import readline
|
||||
readline.parse_and_bind('set bind-tty-special-chars off')
|
||||
readline.parse_and_bind('set input-meta on')
|
||||
readline.parse_and_bind('set output-meta on')
|
||||
readline.parse_and_bind('set convert-meta off')
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
@@ -47,77 +45,98 @@ WORKDIR = Path.cwd()
|
||||
SKILLS_DIR = WORKDIR / "skills"
|
||||
client = Anthropic(base_url=os.getenv("ANTHROPIC_BASE_URL"))
|
||||
MODEL = os.environ["MODEL_ID"]
|
||||
CURRENT_TODOS: list[dict] = []
|
||||
|
||||
# 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
|
||||
try:
|
||||
meta = yaml.safe_load(parts[1]) or {}
|
||||
except yaml.YAMLError:
|
||||
meta = {}
|
||||
return meta, parts[2].strip()
|
||||
|
||||
# Build skill registry at startup (used for safe lookup in load_skill)
|
||||
SKILL_REGISTRY: dict[str, dict] = {}
|
||||
# -- Skill catalog --
|
||||
|
||||
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}
|
||||
class SkillLoader:
|
||||
def __init__(self, skills_dir: Path):
|
||||
self.skills_dir = skills_dir
|
||||
self.skills: dict[str, dict[str, str]] = {}
|
||||
self.scan()
|
||||
|
||||
_scan_skills()
|
||||
@staticmethod
|
||||
def parse_frontmatter(text: str) -> tuple[dict, str]:
|
||||
lines = text.splitlines(keepends=True)
|
||||
if not lines or lines[0].rstrip("\r\n") != "---":
|
||||
return {}, text
|
||||
|
||||
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())
|
||||
closing_index = next(
|
||||
(index for index, line in enumerate(lines[1:], start=1)
|
||||
if line.rstrip("\r\n") == "---"),
|
||||
None,
|
||||
)
|
||||
if closing_index is None:
|
||||
return {}, text
|
||||
|
||||
# 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()
|
||||
frontmatter = "".join(lines[1:closing_index])
|
||||
body = "".join(lines[closing_index + 1:]).strip()
|
||||
try:
|
||||
metadata = yaml.safe_load(frontmatter) or {}
|
||||
except yaml.YAMLError:
|
||||
metadata = {}
|
||||
if not isinstance(metadata, dict):
|
||||
metadata = {}
|
||||
return metadata, body
|
||||
|
||||
def scan(self):
|
||||
self.skills.clear()
|
||||
if not self.skills_dir.exists():
|
||||
return
|
||||
|
||||
skills_root = self.skills_dir.resolve()
|
||||
for manifest in sorted(self.skills_dir.glob("*/SKILL.md")):
|
||||
if (not manifest.is_file()
|
||||
or not manifest.resolve().is_relative_to(skills_root)):
|
||||
continue
|
||||
content = manifest.read_text(encoding="utf-8")
|
||||
metadata, body = self.parse_frontmatter(content)
|
||||
raw_name = metadata.get("name")
|
||||
name = raw_name.strip() if isinstance(raw_name, str) else ""
|
||||
name = name or manifest.parent.name
|
||||
raw_description = metadata.get("description")
|
||||
description = (raw_description.strip()
|
||||
if isinstance(raw_description, str) else "")
|
||||
description = description or body.split("\n", 1)[0]
|
||||
description = " ".join(str(description).lstrip("# ").split())
|
||||
self.skills[name] = {
|
||||
"name": name,
|
||||
"description": description,
|
||||
"content": content,
|
||||
}
|
||||
|
||||
def catalog(self) -> str:
|
||||
if not self.skills:
|
||||
return "(no skills found)"
|
||||
return "\n".join(
|
||||
f"- {skill['name']}: {skill['description']}"
|
||||
for skill in self.skills.values()
|
||||
)
|
||||
|
||||
def load(self, name: str) -> str:
|
||||
skill = self.skills.get(name)
|
||||
if skill:
|
||||
return skill["content"]
|
||||
available = ", ".join(self.skills) or "none"
|
||||
return f"Error: Unknown skill '{name}'. Available: {available}"
|
||||
|
||||
|
||||
SKILL_LOADER = SkillLoader(SKILLS_DIR)
|
||||
|
||||
|
||||
def build_system_prompt() -> str:
|
||||
return (
|
||||
f"You are a coding agent at {WORKDIR}. "
|
||||
f"Skills available:\n{catalog}\n"
|
||||
"Use load_skill to get full details when needed."
|
||||
f"You are a coding agent at {WORKDIR}. Use tools to solve tasks. "
|
||||
"Act, don't explain.\n\n"
|
||||
f"Skills available:\n{SKILL_LOADER.catalog()}\n\n"
|
||||
"Use load_skill to read the full instructions when a skill applies."
|
||||
)
|
||||
|
||||
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."
|
||||
)
|
||||
SYSTEM = build_system_prompt()
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
# 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
|
||||
# -- Tools --
|
||||
|
||||
# s07: Fix Windows subprocess encoding crash (fallback UTF-8 -> GBK with error replacement)
|
||||
def safe_decode(data: bytes) -> str:
|
||||
@@ -140,155 +159,54 @@ def run_bash(command: str) -> str:
|
||||
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()
|
||||
lines = (WORKDIR / path).resolve().read_text(encoding="utf-8").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 = (WORKDIR / path).resolve()
|
||||
file_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
file_path.write_text(content)
|
||||
file_path.write_text(content, encoding="utf-8")
|
||||
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()
|
||||
file_path = (WORKDIR / path).resolve()
|
||||
text = file_path.read_text(encoding="utf-8")
|
||||
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))
|
||||
file_path.write_text(text.replace(old_text, new_text, 1), encoding="utf-8")
|
||||
return f"Edited {path}"
|
||||
except Exception as e:
|
||||
return f"Error: {e}"
|
||||
|
||||
|
||||
def run_glob(pattern: str) -> str:
|
||||
import glob as g
|
||||
import glob
|
||||
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)"
|
||||
matches = sorted({
|
||||
match for match in glob.glob(
|
||||
pattern, root_dir=WORKDIR, recursive=True)
|
||||
if (WORKDIR / match).resolve().is_relative_to(WORKDIR)
|
||||
})
|
||||
shown = matches[:200]
|
||||
if len(matches) > 200:
|
||||
shown.append("... (more matches omitted; narrow the pattern)")
|
||||
return "\n".join(shown) if shown else "(no matches)"
|
||||
except Exception as e:
|
||||
return f"Error: {e}"
|
||||
|
||||
def _normalize_todos(todos):
|
||||
if isinstance(todos, str):
|
||||
try:
|
||||
todos = json.loads(todos)
|
||||
except json.JSONDecodeError:
|
||||
try:
|
||||
todos = ast.literal_eval(todos)
|
||||
except (SyntaxError, ValueError):
|
||||
return None, "Error: todos must be a list or JSON array string"
|
||||
if not isinstance(todos, list):
|
||||
return None, "Error: todos must be a list"
|
||||
for i, t in enumerate(todos):
|
||||
if not isinstance(t, dict):
|
||||
return None, f"Error: todos[{i}] must be an object"
|
||||
if "content" not in t or "status" not in t:
|
||||
return None, f"Error: todos[{i}] missing 'content' or 'status'"
|
||||
if t["status"] not in ("pending", "in_progress", "completed"):
|
||||
return None, f"Error: todos[{i}] has invalid status '{t['status']}'"
|
||||
return todos, None
|
||||
|
||||
def run_todo_write(todos: list) -> str:
|
||||
global CURRENT_TODOS
|
||||
todos, error = _normalize_todos(todos)
|
||||
if error:
|
||||
return error
|
||||
CURRENT_TODOS = todos
|
||||
lines = ["\n\033[33m## Current Tasks\033[0m"]
|
||||
for t in CURRENT_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(CURRENT_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.",
|
||||
@@ -299,33 +217,31 @@ TOOLS = [
|
||||
"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.",
|
||||
{"name": "glob", "description": "Find files matching a glob pattern; ** matches recursively.",
|
||||
"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.",
|
||||
{"name": "load_skill", "description": "Load the full SKILL.md content by skill 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,
|
||||
"bash": run_bash,
|
||||
"read_file": run_read,
|
||||
"write_file": run_write,
|
||||
"edit_file": run_edit,
|
||||
"glob": run_glob,
|
||||
"load_skill": SKILL_LOADER.load,
|
||||
}
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
# FROM s04 (unchanged): Hook System
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
# -- Hooks --
|
||||
|
||||
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)
|
||||
@@ -333,96 +249,137 @@ def trigger_hooks(event: str, *args):
|
||||
return result
|
||||
return None
|
||||
|
||||
|
||||
DENY_LIST = ["rm -rf /", "sudo", "shutdown", "reboot", "mkfs", "dd if="]
|
||||
DESTRUCTIVE = ["rm ", "> /etc/", "chmod 777"]
|
||||
|
||||
|
||||
def permission_hook(block):
|
||||
"""PreToolUse: block denied operations and ask about risky ones."""
|
||||
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"
|
||||
command = block.input.get("command", "")
|
||||
for pattern in DENY_LIST:
|
||||
if pattern in command:
|
||||
print(f"\n\033[31m[blocked] '{pattern}'\033[0m")
|
||||
return "Permission denied by deny list"
|
||||
for keyword in DESTRUCTIVE:
|
||||
if keyword in command:
|
||||
print("\n\033[33m[permission] Potentially destructive command\033[0m")
|
||||
print(f" Tool: {block.name}({block.input})")
|
||||
choice = input(" Allow? [y/N] ").strip().lower()
|
||||
if choice not in ("y", "yes"):
|
||||
return "Permission denied by user"
|
||||
|
||||
if block.name in ("read_file", "write_file", "edit_file"):
|
||||
path = block.input.get("path", "")
|
||||
if not (WORKDIR / path).resolve().is_relative_to(WORKDIR):
|
||||
print("\n\033[33m[permission] Access outside workspace\033[0m")
|
||||
print(f" Tool: {block.name}({block.input})")
|
||||
choice = input(" Allow? [y/N] ").strip().lower()
|
||||
if choice not in ("y", "yes"):
|
||||
return "Permission denied by user"
|
||||
return None
|
||||
|
||||
|
||||
def log_hook(block):
|
||||
print(f"\033[90m[HOOK] {block.name}\033[0m")
|
||||
"""PreToolUse: log every tool call."""
|
||||
args_preview = str(list(block.input.values())[:2])[:60]
|
||||
print(f"\033[90m[HOOK] {block.name}({args_preview})\033[0m")
|
||||
return None
|
||||
|
||||
|
||||
def large_output_hook(block, output):
|
||||
"""PostToolUse: warn on large output."""
|
||||
if len(str(output)) > 100000:
|
||||
print(f"\033[33m[HOOK] Large output from {block.name}: {len(str(output))} chars\033[0m")
|
||||
return None
|
||||
|
||||
|
||||
def context_inject_hook(query: str):
|
||||
"""UserPromptSubmit: log the working directory."""
|
||||
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")
|
||||
"""Stop: print the number of tool results in this message list."""
|
||||
tool_count = sum(
|
||||
1
|
||||
for message in messages
|
||||
for block in (
|
||||
message.get("content")
|
||||
if isinstance(message.get("content"), list)
|
||||
else []
|
||||
)
|
||||
if isinstance(block, dict) and block.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("PostToolUse", large_output_hook)
|
||||
register_hook("Stop", summary_hook)
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
# agent_loop — same as s05-s06 + nag reminder
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
def execute_tool(block) -> str:
|
||||
blocked = trigger_hooks("PreToolUse", block)
|
||||
if blocked:
|
||||
return str(blocked)
|
||||
|
||||
handler = TOOL_HANDLERS.get(block.name)
|
||||
try:
|
||||
output = handler(**block.input) if handler else f"Unknown: {block.name}"
|
||||
except Exception as e:
|
||||
output = f"Error: {e}"
|
||||
|
||||
trigger_hooks("PostToolUse", block, output)
|
||||
return str(output)
|
||||
|
||||
|
||||
def agent_loop(messages: list):
|
||||
rounds_since_todo = 0
|
||||
while True:
|
||||
if rounds_since_todo >= 3 and messages:
|
||||
messages.append({"role": "user",
|
||||
"content": "<reminder>Update your todos.</reminder>"})
|
||||
rounds_since_todo = 0
|
||||
|
||||
response = client.messages.create(
|
||||
model=MODEL, system=SYSTEM, messages=messages,
|
||||
tools=TOOLS, max_tokens=8000,
|
||||
model=MODEL,
|
||||
system=SYSTEM,
|
||||
messages=messages,
|
||||
tools=TOOLS,
|
||||
max_tokens=8000,
|
||||
)
|
||||
messages.append({"role": "assistant", "content": response.content})
|
||||
|
||||
if response.stop_reason != "tool_use":
|
||||
tool_calls = [
|
||||
block for block in response.content if block.type == "tool_use"
|
||||
]
|
||||
if not tool_calls:
|
||||
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})
|
||||
|
||||
for block in tool_calls:
|
||||
output = execute_tool(block)
|
||||
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")
|
||||
print("s07: Skill Loading - catalog first, full content on demand")
|
||||
print("Enter a question, press Enter to send. Type q to quit.\n")
|
||||
|
||||
history = []
|
||||
while True:
|
||||
try:
|
||||
query = input("\033[36ms07 >> \033[0m")
|
||||
# \001/\002 tell Readline the ANSI escapes have zero display width.
|
||||
query = input("\001\033[36m\002s07 >> \001\033[0m\002")
|
||||
except (EOFError, KeyboardInterrupt):
|
||||
break
|
||||
if query.strip().lower() in ("q", "exit", ""):
|
||||
|
||||
@@ -1,110 +1,55 @@
|
||||
<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 id="arrow" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="7" markerHeight="7" orient="auto">
|
||||
<path d="M0 0 L10 5 L0 10 Z" fill="#2563eb"/>
|
||||
</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" rx="8" fill="#fafbfc"/>
|
||||
<rect width="800" height="48" rx="8" fill="#1e3a5f"/>
|
||||
<rect y="40" width="800" height="8" fill="#1e3a5f"/>
|
||||
<text x="400" y="31" fill="#fff" font-size="16" font-weight="700" text-anchor="middle">Skill Loading — Catalog in system prompt, full content in tool_result</text>
|
||||
|
||||
<!-- Background -->
|
||||
<rect width="800" height="380" fill="#fafbfc" rx="8"/>
|
||||
<text x="32" y="78" fill="#475569" font-size="11" font-weight="700">Startup</text>
|
||||
<rect x="32" y="90" width="120" height="54" rx="6" fill="#fff" stroke="#64748b"/>
|
||||
<text x="92" y="112" fill="#1e293b" font-size="11" font-weight="700" text-anchor="middle">skills/</text>
|
||||
<text x="92" y="130" fill="#64748b" font-size="9" text-anchor="middle">*/SKILL.md</text>
|
||||
<line x1="152" y1="117" x2="195" y2="117" stroke="#2563eb" stroke-width="2" marker-end="url(#arrow)"/>
|
||||
<rect x="198" y="90" width="130" height="54" rx="6" fill="#eff6ff" stroke="#2563eb"/>
|
||||
<text x="263" y="112" fill="#1e3a5f" font-size="11" font-weight="700" text-anchor="middle">SkillLoader.scan</text>
|
||||
<text x="263" y="130" fill="#64748b" font-size="9" text-anchor="middle">parse frontmatter</text>
|
||||
<line x1="328" y1="117" x2="371" y2="117" stroke="#2563eb" stroke-width="2" marker-end="url(#arrow)"/>
|
||||
<rect x="374" y="90" width="130" height="54" rx="6" fill="#eff6ff" stroke="#2563eb"/>
|
||||
<text x="439" y="112" fill="#1e3a5f" font-size="11" font-weight="700" text-anchor="middle">Skill catalog</text>
|
||||
<text x="439" y="130" fill="#64748b" font-size="9" text-anchor="middle">name + description</text>
|
||||
<line x1="504" y1="117" x2="547" y2="117" stroke="#2563eb" stroke-width="2" marker-end="url(#arrow)"/>
|
||||
<rect x="550" y="90" width="218" height="54" rx="6" fill="#ecfdf5" stroke="#059669"/>
|
||||
<text x="659" y="112" fill="#065f46" font-size="11" font-weight="700" text-anchor="middle">system prompt</text>
|
||||
<text x="659" y="130" fill="#64748b" font-size="9" text-anchor="middle">base instructions + catalog</text>
|
||||
|
||||
<!-- 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>
|
||||
<text x="32" y="190" fill="#475569" font-size="11" font-weight="700">Runtime</text>
|
||||
<rect x="32" y="204" width="90" height="48" rx="6" fill="#fff" stroke="#2563eb"/>
|
||||
<text x="77" y="233" fill="#1e293b" font-size="11" font-weight="700" text-anchor="middle">LLM</text>
|
||||
<line x1="122" y1="228" x2="149" y2="228" stroke="#2563eb" stroke-width="2" marker-end="url(#arrow)"/>
|
||||
<rect x="152" y="204" width="110" height="48" rx="6" fill="#eff6ff" stroke="#2563eb"/>
|
||||
<text x="207" y="224" fill="#1e3a5f" font-size="10" font-weight="700" text-anchor="middle">load_skill</text>
|
||||
<text x="207" y="240" fill="#64748b" font-size="9" text-anchor="middle">name</text>
|
||||
<line x1="262" y1="228" x2="289" y2="228" stroke="#2563eb" stroke-width="2" marker-end="url(#arrow)"/>
|
||||
<rect x="292" y="204" width="100" height="48" rx="6" fill="#fff" stroke="#64748b"/>
|
||||
<text x="342" y="224" fill="#1e293b" font-size="10" font-weight="700" text-anchor="middle">Registry</text>
|
||||
<text x="342" y="240" fill="#64748b" font-size="9" text-anchor="middle">lookup by name</text>
|
||||
<line x1="392" y1="228" x2="419" y2="228" stroke="#2563eb" stroke-width="2" marker-end="url(#arrow)"/>
|
||||
<rect x="422" y="204" width="110" height="48" rx="6" fill="#fff" stroke="#64748b"/>
|
||||
<text x="477" y="224" fill="#1e293b" font-size="10" font-weight="700" text-anchor="middle">Full content</text>
|
||||
<text x="477" y="240" fill="#64748b" font-size="9" text-anchor="middle">SKILL.md</text>
|
||||
<line x1="532" y1="228" x2="559" y2="228" stroke="#2563eb" stroke-width="2" marker-end="url(#arrow)"/>
|
||||
<rect x="562" y="204" width="100" height="48" rx="6" fill="#ecfdf5" stroke="#059669"/>
|
||||
<text x="612" y="224" fill="#065f46" font-size="10" font-weight="700" text-anchor="middle">tool_result</text>
|
||||
<text x="612" y="240" fill="#64748b" font-size="9" text-anchor="middle">returned to model</text>
|
||||
<line x1="662" y1="228" x2="689" y2="228" stroke="#2563eb" stroke-width="2" marker-end="url(#arrow)"/>
|
||||
<rect x="692" y="204" width="76" height="48" rx="6" fill="#fff" stroke="#2563eb"/>
|
||||
<text x="730" y="224" fill="#1e293b" font-size="10" font-weight="700" text-anchor="middle">messages[]</text>
|
||||
<text x="730" y="240" fill="#64748b" font-size="9" text-anchor="middle">append</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>
|
||||
<path d="M730 252 V316 H77 V252" fill="none" stroke="#2563eb" stroke-width="2" stroke-dasharray="6 4" marker-end="url(#arrow)"/>
|
||||
<text x="403" y="309" fill="#475569" font-size="10" text-anchor="middle">The next LLM call receives the updated messages[]</text>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 6.9 KiB After Width: | Height: | Size: 4.7 KiB |
@@ -1,110 +1,55 @@
|
||||
<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 id="arrow" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="7" markerHeight="7" orient="auto">
|
||||
<path d="M0 0 L10 5 L0 10 Z" fill="#2563eb"/>
|
||||
</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" rx="8" fill="#fafbfc"/>
|
||||
<rect width="800" height="48" rx="8" fill="#1e3a5f"/>
|
||||
<rect y="40" width="800" height="8" fill="#1e3a5f"/>
|
||||
<text x="400" y="31" fill="#fff" font-size="16" font-weight="700" text-anchor="middle">Skill Loading — カタログは system prompt、完全な内容は tool_result</text>
|
||||
|
||||
<!-- 背景 -->
|
||||
<rect width="800" height="380" fill="#fafbfc" rx="8"/>
|
||||
<text x="32" y="78" fill="#475569" font-size="11" font-weight="700">起動時</text>
|
||||
<rect x="32" y="90" width="120" height="54" rx="6" fill="#fff" stroke="#64748b"/>
|
||||
<text x="92" y="112" fill="#1e293b" font-size="11" font-weight="700" text-anchor="middle">skills/</text>
|
||||
<text x="92" y="130" fill="#64748b" font-size="9" text-anchor="middle">*/SKILL.md</text>
|
||||
<line x1="152" y1="117" x2="195" y2="117" stroke="#2563eb" stroke-width="2" marker-end="url(#arrow)"/>
|
||||
<rect x="198" y="90" width="130" height="54" rx="6" fill="#eff6ff" stroke="#2563eb"/>
|
||||
<text x="263" y="112" fill="#1e3a5f" font-size="11" font-weight="700" text-anchor="middle">SkillLoader.scan</text>
|
||||
<text x="263" y="130" fill="#64748b" font-size="9" text-anchor="middle">frontmatter を解析</text>
|
||||
<line x1="328" y1="117" x2="371" y2="117" stroke="#2563eb" stroke-width="2" marker-end="url(#arrow)"/>
|
||||
<rect x="374" y="90" width="130" height="54" rx="6" fill="#eff6ff" stroke="#2563eb"/>
|
||||
<text x="439" y="112" fill="#1e3a5f" font-size="11" font-weight="700" text-anchor="middle">スキルカタログ</text>
|
||||
<text x="439" y="130" fill="#64748b" font-size="9" text-anchor="middle">name + description</text>
|
||||
<line x1="504" y1="117" x2="547" y2="117" stroke="#2563eb" stroke-width="2" marker-end="url(#arrow)"/>
|
||||
<rect x="550" y="90" width="218" height="54" rx="6" fill="#ecfdf5" stroke="#059669"/>
|
||||
<text x="659" y="112" fill="#065f46" font-size="11" font-weight="700" text-anchor="middle">system prompt</text>
|
||||
<text x="659" y="130" fill="#64748b" font-size="9" text-anchor="middle">基本指示 + カタログ</text>
|
||||
|
||||
<!-- タイトル -->
|
||||
<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="32" y="190" fill="#475569" font-size="11" font-weight="700">実行時</text>
|
||||
<rect x="32" y="204" width="90" height="48" rx="6" fill="#fff" stroke="#2563eb"/>
|
||||
<text x="77" y="233" fill="#1e293b" font-size="11" font-weight="700" text-anchor="middle">LLM</text>
|
||||
<line x1="122" y1="228" x2="149" y2="228" stroke="#2563eb" stroke-width="2" marker-end="url(#arrow)"/>
|
||||
<rect x="152" y="204" width="110" height="48" rx="6" fill="#eff6ff" stroke="#2563eb"/>
|
||||
<text x="207" y="224" fill="#1e3a5f" font-size="10" font-weight="700" text-anchor="middle">load_skill</text>
|
||||
<text x="207" y="240" fill="#64748b" font-size="9" text-anchor="middle">name</text>
|
||||
<line x1="262" y1="228" x2="289" y2="228" stroke="#2563eb" stroke-width="2" marker-end="url(#arrow)"/>
|
||||
<rect x="292" y="204" width="100" height="48" rx="6" fill="#fff" stroke="#64748b"/>
|
||||
<text x="342" y="224" fill="#1e293b" font-size="10" font-weight="700" text-anchor="middle">レジストリ</text>
|
||||
<text x="342" y="240" fill="#64748b" font-size="9" text-anchor="middle">名前で検索</text>
|
||||
<line x1="392" y1="228" x2="419" y2="228" stroke="#2563eb" stroke-width="2" marker-end="url(#arrow)"/>
|
||||
<rect x="422" y="204" width="110" height="48" rx="6" fill="#fff" stroke="#64748b"/>
|
||||
<text x="477" y="224" fill="#1e293b" font-size="10" font-weight="700" text-anchor="middle">完全な内容</text>
|
||||
<text x="477" y="240" fill="#64748b" font-size="9" text-anchor="middle">SKILL.md</text>
|
||||
<line x1="532" y1="228" x2="559" y2="228" stroke="#2563eb" stroke-width="2" marker-end="url(#arrow)"/>
|
||||
<rect x="562" y="204" width="100" height="48" rx="6" fill="#ecfdf5" stroke="#059669"/>
|
||||
<text x="612" y="224" fill="#065f46" font-size="10" font-weight="700" text-anchor="middle">tool_result</text>
|
||||
<text x="612" y="240" fill="#64748b" font-size="9" text-anchor="middle">モデルへ返す</text>
|
||||
<line x1="662" y1="228" x2="689" y2="228" stroke="#2563eb" stroke-width="2" marker-end="url(#arrow)"/>
|
||||
<rect x="692" y="204" width="76" height="48" rx="6" fill="#fff" stroke="#2563eb"/>
|
||||
<text x="730" y="224" fill="#1e293b" font-size="10" font-weight="700" text-anchor="middle">messages[]</text>
|
||||
<text x="730" y="240" fill="#64748b" font-size="9" text-anchor="middle">追加</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>
|
||||
<path d="M730 252 V316 H77 V252" fill="none" stroke="#2563eb" stroke-width="2" stroke-dasharray="6 4" marker-end="url(#arrow)"/>
|
||||
<text x="403" y="309" fill="#475569" font-size="10" text-anchor="middle">次の LLM 呼び出しは更新後の messages[] を受け取る</text>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 7.1 KiB After Width: | Height: | Size: 4.7 KiB |
@@ -1,110 +1,55 @@
|
||||
<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 id="arrow" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="7" markerHeight="7" orient="auto">
|
||||
<path d="M0 0 L10 5 L0 10 Z" fill="#2563eb"/>
|
||||
</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" rx="8" fill="#fafbfc"/>
|
||||
<rect width="800" height="48" rx="8" fill="#1e3a5f"/>
|
||||
<rect y="40" width="800" height="8" fill="#1e3a5f"/>
|
||||
<text x="400" y="31" fill="#fff" font-size="16" font-weight="700" text-anchor="middle">Skill Loading — 目录进入 system prompt,完整内容进入 tool_result</text>
|
||||
|
||||
<!-- 背景 -->
|
||||
<rect width="800" height="380" fill="#fafbfc" rx="8"/>
|
||||
<text x="32" y="78" fill="#475569" font-size="11" font-weight="700">启动时</text>
|
||||
<rect x="32" y="90" width="120" height="54" rx="6" fill="#fff" stroke="#64748b"/>
|
||||
<text x="92" y="112" fill="#1e293b" font-size="11" font-weight="700" text-anchor="middle">skills/</text>
|
||||
<text x="92" y="130" fill="#64748b" font-size="9" text-anchor="middle">*/SKILL.md</text>
|
||||
<line x1="152" y1="117" x2="195" y2="117" stroke="#2563eb" stroke-width="2" marker-end="url(#arrow)"/>
|
||||
<rect x="198" y="90" width="130" height="54" rx="6" fill="#eff6ff" stroke="#2563eb"/>
|
||||
<text x="263" y="112" fill="#1e3a5f" font-size="11" font-weight="700" text-anchor="middle">SkillLoader.scan</text>
|
||||
<text x="263" y="130" fill="#64748b" font-size="9" text-anchor="middle">解析 frontmatter</text>
|
||||
<line x1="328" y1="117" x2="371" y2="117" stroke="#2563eb" stroke-width="2" marker-end="url(#arrow)"/>
|
||||
<rect x="374" y="90" width="130" height="54" rx="6" fill="#eff6ff" stroke="#2563eb"/>
|
||||
<text x="439" y="112" fill="#1e3a5f" font-size="11" font-weight="700" text-anchor="middle">技能目录</text>
|
||||
<text x="439" y="130" fill="#64748b" font-size="9" text-anchor="middle">name + description</text>
|
||||
<line x1="504" y1="117" x2="547" y2="117" stroke="#2563eb" stroke-width="2" marker-end="url(#arrow)"/>
|
||||
<rect x="550" y="90" width="218" height="54" rx="6" fill="#ecfdf5" stroke="#059669"/>
|
||||
<text x="659" y="112" fill="#065f46" font-size="11" font-weight="700" text-anchor="middle">system prompt</text>
|
||||
<text x="659" y="130" fill="#64748b" font-size="9" text-anchor="middle">基础指令 + 技能目录</text>
|
||||
|
||||
<!-- 标题 -->
|
||||
<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="32" y="190" fill="#475569" font-size="11" font-weight="700">运行时</text>
|
||||
<rect x="32" y="204" width="90" height="48" rx="6" fill="#fff" stroke="#2563eb"/>
|
||||
<text x="77" y="233" fill="#1e293b" font-size="11" font-weight="700" text-anchor="middle">LLM</text>
|
||||
<line x1="122" y1="228" x2="149" y2="228" stroke="#2563eb" stroke-width="2" marker-end="url(#arrow)"/>
|
||||
<rect x="152" y="204" width="110" height="48" rx="6" fill="#eff6ff" stroke="#2563eb"/>
|
||||
<text x="207" y="224" fill="#1e3a5f" font-size="10" font-weight="700" text-anchor="middle">load_skill</text>
|
||||
<text x="207" y="240" fill="#64748b" font-size="9" text-anchor="middle">name</text>
|
||||
<line x1="262" y1="228" x2="289" y2="228" stroke="#2563eb" stroke-width="2" marker-end="url(#arrow)"/>
|
||||
<rect x="292" y="204" width="100" height="48" rx="6" fill="#fff" stroke="#64748b"/>
|
||||
<text x="342" y="224" fill="#1e293b" font-size="10" font-weight="700" text-anchor="middle">注册表查询</text>
|
||||
<text x="342" y="240" fill="#64748b" font-size="9" text-anchor="middle">按名称</text>
|
||||
<line x1="392" y1="228" x2="419" y2="228" stroke="#2563eb" stroke-width="2" marker-end="url(#arrow)"/>
|
||||
<rect x="422" y="204" width="110" height="48" rx="6" fill="#fff" stroke="#64748b"/>
|
||||
<text x="477" y="224" fill="#1e293b" font-size="10" font-weight="700" text-anchor="middle">完整内容</text>
|
||||
<text x="477" y="240" fill="#64748b" font-size="9" text-anchor="middle">SKILL.md</text>
|
||||
<line x1="532" y1="228" x2="559" y2="228" stroke="#2563eb" stroke-width="2" marker-end="url(#arrow)"/>
|
||||
<rect x="562" y="204" width="100" height="48" rx="6" fill="#ecfdf5" stroke="#059669"/>
|
||||
<text x="612" y="224" fill="#065f46" font-size="10" font-weight="700" text-anchor="middle">tool_result</text>
|
||||
<text x="612" y="240" fill="#64748b" font-size="9" text-anchor="middle">返回模型</text>
|
||||
<line x1="662" y1="228" x2="689" y2="228" stroke="#2563eb" stroke-width="2" marker-end="url(#arrow)"/>
|
||||
<rect x="692" y="204" width="76" height="48" rx="6" fill="#fff" stroke="#2563eb"/>
|
||||
<text x="730" y="224" fill="#1e293b" font-size="10" font-weight="700" text-anchor="middle">messages[]</text>
|
||||
<text x="730" y="240" fill="#64748b" font-size="9" text-anchor="middle">追加</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>
|
||||
<path d="M730 252 V316 H77 V252" fill="none" stroke="#2563eb" stroke-width="2" stroke-dasharray="6 4" marker-end="url(#arrow)"/>
|
||||
<text x="403" y="309" fill="#475569" font-size="10" text-anchor="middle">下一次 LLM 调用继续使用更新后的 messages[]</text>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 6.9 KiB After Width: | Height: | Size: 4.7 KiB |
Reference in New Issue
Block a user