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

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

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

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

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

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

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

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

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

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

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

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

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

* fix: align s16-s19 teaching tool consistency

* fix pr265 chapter diagrams

* Add comprehensive s20 harness chapter

* Fix chapter smoke test regressions

* Clarify README tutorial track transition

---------

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

View File

@@ -0,0 +1,254 @@
# s10: System Prompt — Assembled at Runtime, Never Hardcoded
[中文](README.md) · [English](README.en.md) · [日本語](README.ja.md)
s01 → ... → s08 → s09 → `s10` → [s11](../s11_error_recovery/) → s12 → ... → s20
> *"prompt is assembled, not hardcoded"* — Sections + on-demand assembly + caching.
>
> **Harness Layer**: Prompt — assembled at runtime, never hardcoded.
---
## The Problem
From s01 to s09, the system prompt was always one hardcoded line:
```python
SYSTEM = f"You are a coding agent at {WORKDIR}. Use tools to solve tasks."
```
That worked for s01 — only bash, read, write. But by s09, the agent has memory, compression, skill loading. The prompt needs to describe more and more capabilities:
```python
SYSTEM = (
f"You are a coding agent at {WORKDIR}. "
"Use tools to solve tasks. Act, don't explain. "
"Before starting any multi-step task, use todo_write. "
"Skills are available via list_skills and load_skill. "
"Relevant memories are injected below when available. "
# ... add a capability, add a line
)
```
Three problems:
1. **Switching projects requires rewriting the entire prompt** — no way to know what to change and what to keep
2. **One change can break others** — adding a tool description might conflict with earlier instructions
3. **Every request carries everything** — even when the current conversation doesn't need certain sections, they waste tokens
The system prompt should be a configuration assembled at runtime based on current state: which tools are enabled, which context is visible, which memories are relevant, and which content must remain stable to hit prompt cache.
---
## The Solution
![System Prompt Overview](images/system-prompt-overview.en.svg)
s10 focuses on prompt assembly. It builds on the s08-s09 capabilities but doesn't re-implement compression or memory. The core change: split the hardcoded `SYSTEM` into independent sections, assemble them at runtime based on real state, and cache the result.
Four sections, two loading strategies:
| Section | Strategy | Content | Condition |
|---------|----------|---------|-----------|
| identity | always | who you are, how to work | always present |
| tools | always | available tool list | `enabled_tools` |
| workspace | always | working directory | always present |
| memory | on-demand | relevant memory content | whether `.memory/MEMORY.md` exists |
Key design: whether a section loads depends on real state (tools exist, files exist), not keywords in messages.
---
## How It Works
### PROMPT_SECTIONS: Topic-Keyed Fragments
Split the monolithic string into a dictionary, each key is a topic:
```python
PROMPT_SECTIONS = {
"identity": "You are a coding agent. Act, don't explain.",
"tools": "Available tools: bash, read_file, write_file.",
"workspace": f"Working directory: {WORKDIR}",
"memory": "Relevant memories are injected below when available.",
}
```
Each section is maintained independently. Changing `tools` doesn't affect `identity`; adding `memory` doesn't touch `workspace`.
### assemble_system_prompt: On-Demand Assembly
Not every section is needed every turn. No memory files? Loading the memory section just wastes tokens. Assembly is based on real state in context:
```python
def assemble_system_prompt(context: dict) -> str:
sections = []
# Always loaded
sections.append(PROMPT_SECTIONS["identity"])
sections.append(PROMPT_SECTIONS["tools"])
sections.append(PROMPT_SECTIONS["workspace"])
# On-demand — based on real state, not keywords
memories = context.get("memories", "")
if memories:
sections.append(f"Relevant memories:\n{memories}")
return "\n\n".join(sections)
```
"Always loaded" sections are needed every turn: identity, tools, workspace. "On-demand" sections are only useful under specific conditions.
Why not load everything? Tokens have cost (system prompt is billed every turn), and fewer instructions means more focused output (irrelevant instructions are noise).
### get_system_prompt: Cache to Avoid Re-Assembly
When context hasn't changed (multiple LLM calls in the same turn with the same context), re-assembling is wasteful. Use deterministic serialization to detect changes and return cached result:
```python
def get_system_prompt(context: dict) -> str:
global _last_context_key, _last_prompt
key = json.dumps(context, sort_keys=True, ensure_ascii=False, default=str)
if key == _last_context_key and _last_prompt:
return _last_prompt
_last_context_key = key
_last_prompt = assemble_system_prompt(context)
return _last_prompt
```
`json.dumps` instead of `hash()`: Python's built-in `hash()` has process randomization (unsuitable for stable cache keys) and throws `unhashable type` on nested dicts/lists.
Note: this cache only avoids redundant string assembly within a process. It's not the same as CC's API prompt cache, which uses `SYSTEM_PROMPT_DYNAMIC_BOUNDARY` to separate static and dynamic parts — the static parts hit global cache and don't invalidate when dynamic content changes.
### context: Real State, Not Keyword Guessing
Context reflects the actual runtime state:
```python
def update_context(context: dict, messages: list) -> dict:
memories = ""
if MEMORY_INDEX.exists():
content = MEMORY_INDEX.read_text().strip()
if content:
memories = content
return {
"enabled_tools": list(TOOL_HANDLERS.keys()),
"workspace": str(WORKDIR),
"memories": memories,
}
```
`enabled_tools` lists actually registered tools. `memories` checks whether `.memory/MEMORY.md` exists. Section loading is based on this real state, not searching for keywords in messages.
### Putting It Together
```python
def agent_loop(messages: list, context: dict):
system = get_system_prompt(context)
while True:
response = client.messages.create(
model=MODEL, system=system, messages=messages,
tools=TOOLS, max_tokens=8000)
# ... tool execution ...
context = update_context(context, messages)
system = get_system_prompt(context)
```
At the start of each loop iteration, get the system prompt. If context changed, re-assemble; if not, return cached version.
---
## Changes From s09
| Component | Before (s09) | After (s10) |
|-----------|-------------|-------------|
| prompt | Hardcoded SYSTEM string | PROMPT_SECTIONS + assemble_system_prompt |
| caching | None | get_system_prompt (json.dumps detection + cache) |
| new functions | — | assemble_system_prompt, get_system_prompt, update_context |
| tools | bash, read_file, write_file (3) | bash, read_file, write_file (3) — unchanged |
| loop | Uses fixed SYSTEM | Uses get_system_prompt(context) |
---
## Try It
```sh
cd learn-claude-code
python s10_system_prompt/code.py
```
What to watch for:
1. Output shows which sections were loaded (`[assembled] sections: ...` label)
2. Cache hits show `[cache hit]` during continued conversation
3. Creating `.memory/MEMORY.md` makes the memory section appear on the next turn
Try these prompts:
1. `Read the file README.md` (observe the three always-loaded sections)
2. `Create a file called .memory/MEMORY.md with content "- [test](test.md) — test memory"` (write a memory index)
3. `Read the file code.py` (observe whether the memory section appears)
---
## What's Next
System prompts can now be assembled at runtime. But the agent still crashes on errors. Network hiccups, API rate limits, truncated output, context overflow — these aren't bugs, they're normal.
s11 Error Recovery → four recovery paths. Upgrade tokens, compress context, exponential backoff, switch models.
<details>
<summary>Deep Dive Into CC Source Code</summary>
> The following is based on analysis of CC source code `constants/prompts.ts` (914 lines), `constants/systemPromptSections.ts` (68 lines), `context.ts` (189 lines), `utils/api.ts` (718 lines), `utils/systemPrompt.ts` (123 lines), and `bootstrap/state.ts`.
### How many sections does CC's system prompt have?
The count varies based on feature flags, output style, KAIROS/Proactive mode, user type, token budget, etc. Roughly two categories:
**Static sections** (always loaded): identity, system, doing_tasks, actions, using_tools, tone_style, output_efficiency, etc.
**Dynamic sections** (loaded by state): session_guidance, memory, ant_model_override, env_info_simple, language, output_style, mcp_instructions, scratchpad, frc, summarize_tool_results, numeric_length_anchors, token_budget, brief, etc.
`mcp_instructions` is the only volatile section (created via `DANGEROUS_uncachedSystemPromptSection()`), because MCP servers can connect and disconnect between turns.
### Assembly Function
```typescript
getSystemPrompt(tools, model, additionalWorkingDirs?, mcpClients?): Promise<string[]>
```
Returns `string[]` (each element is a section), separated by `SYSTEM_PROMPT_DYNAMIC_BOUNDARY` between static and dynamic parts.
### cache scope
When global cache boundary is enabled, static sections are merged into one global cache block, and dynamic sections don't use global cache (`cacheScope: null`). Only paths without boundary or skipping global cache fall back to org scope.
The teaching version's cache only avoids redundant string assembly. CC's three-layer cache:
1. **lodash memoize**: `getSystemContext` and `getUserContext` cached per session (`context.ts`)
2. **Section registry cache**: `STATE.systemPromptSectionCache` caches dynamic section results, cleared on `/clear` or `/compact`
3. **API-level cache**: `splitSysPromptPrefix()` (`api.ts`) splits prompt into blocks with different cache scopes via boundary
### getUserContext vs getSystemContext
| | getSystemContext | getUserContext |
|---|---|---|
| Content | gitStatus, cacheBreaker | CLAUDE.md content, currentDate |
| Injection | appended to system prompt array | prepended as `<system-reminder>` user message |
| When skipped | custom system prompt | always runs |
### How modes change the prompt
- **CLAUDE_CODE_SIMPLE**: entire prompt is 2 lines
- **Proactive/KAIROS**: compact prompt replaces all standard sections
- **Coordinator**: coordinator-specific prompt fully replaces default
- **Agent mode**: agent-defined prompt replaces or appends to default
### Total size
Standard interactive mode system prompt core is ~20-30KB text. CLAUDE_CODE_SIMPLE is ~150 characters. User context (CLAUDE.md) and system context (git status) add on top.
</details>
<!-- translation-sync: zh@v1, en@v1, ja@v1 -->

View File

@@ -0,0 +1,254 @@
# s10: System Prompt — 実行時アセンブリ、ハードコードなし
[中文](README.md) · [English](README.en.md) · [日本語](README.ja.md)
s01 → ... → s08 → s09 → `s10` → [s11](../s11_error_recovery/) → s12 → ... → s20
> *"prompt は組み立てるもの、固定するものではない"* — セグメント + オンデマンド結合 + キャッシュ。
>
> **Harness レイヤー**: プロンプト — 実行時組み立て、ハードコードなし。
---
## 課題
s01 から s09 まで、system prompt は常に 1 行のハードコード:
```python
SYSTEM = f"You are a coding agent at {WORKDIR}. Use tools to solve tasks."
```
s01 では十分だった。bash、read、write の 3 ツールのみ。しかし s09 では、Agent に記憶、圧縮、スキル読み込みがある。prompt が説明すべき能力が増え続ける:
```python
SYSTEM = (
f"You are a coding agent at {WORKDIR}. "
"Use tools to solve tasks. Act, don't explain. "
"Before starting any multi-step task, use todo_write. "
"Skills are available via list_skills and load_skill. "
"Relevant memories are injected below when available. "
# ... 能力を追加するたびに 1 行増える
)
```
3 つの問題:
1. **プロジェクトを変えるには prompt 全体を書き直す**必要がある。何を変え、何を残すべきか不明
2. **一箇所の変更が全体に影響する**。ツール説明を追加すると、前の指示と矛盾する可能性
3. **毎回のリクエストが全内容を送信する**。現在の会話で不要なセクションも token を無駄に消費
System prompt は、実行時の現在状態に基づいて組み立てられる設定であるべき:どのツールが有効か、どのコンテキストが可視か、どの記憶が関連するか、どの内容を prompt cache に命中させるために安定させるべきか。
---
## ソリューション
![System Prompt Overview](images/system-prompt-overview.ja.svg)
s10 は prompt アセンブリ機構に焦点を当てる。s08-s09 の能力を背景とするが、圧縮や記憶システムは再実装しない。核心の変更:ハードコードされた `SYSTEM` を独立セクションに分割し、実行時に実際の状態に基づいてオンデマンドで組み立て、結果をキャッシュして再組み立てを回避。
4 つのセクション、2 つの読み込み戦略:
| セクション | 戦略 | 内容 | 判断基準 |
|-----------|------|------|---------|
| identity | 常に | あなたは誰か、どう作業するか | 常に存在 |
| tools | 常に | 利用可能ツール一覧 | `enabled_tools` |
| workspace | 常に | 作業ディレクトリ | 常に存在 |
| memory | オンデマンド | 関連記憶内容 | `.memory/MEMORY.md` が存在するか |
重要な設計:セクションをロードするかどうかは実際の状態(ツールが存在するか、ファイルが存在するか)で決まり、メッセージ内のキーワードではない。
---
## 仕組み
### PROMPT_SECTIONS: トピック別フラグメント
単一の文字列を辞書に分割、各キーがトピック:
```python
PROMPT_SECTIONS = {
"identity": "You are a coding agent. Act, don't explain.",
"tools": "Available tools: bash, read_file, write_file.",
"workspace": f"Working directory: {WORKDIR}",
"memory": "Relevant memories are injected below when available.",
}
```
各セクションは独立して管理。`tools` を変更しても `identity` に影響しない。`memory` を追加しても `workspace` はそのまま。
### assemble_system_prompt: オンデマンド組み立て
すべてのセクションが毎ターン必要なわけではない。記憶ファイルがなければ、memory セクションをロードしても token の無駄。context の実際の状態に基づいて組み立てる:
```python
def assemble_system_prompt(context: dict) -> str:
sections = []
# 常にロード
sections.append(PROMPT_SECTIONS["identity"])
sections.append(PROMPT_SECTIONS["tools"])
sections.append(PROMPT_SECTIONS["workspace"])
# オンデマンド — 実際の状態に基づく、キーワードではない
memories = context.get("memories", "")
if memories:
sections.append(f"Relevant memories:\n{memories}")
return "\n\n".join(sections)
```
「常にロード」は毎ターン必要なもの:アイデンティティ、ツール、作業ディレクトリ。「オンデマンド」は特定条件下でのみ有用。
なぜ全部ロードしないのかtoken にはコストがありsystem prompt は毎ターン課金)、情報が少ないほど LLM は集中する(無関係な指示はノイズ)。
### get_system_prompt: キャッシュで再組み立てを回避
コンテキストが変わっていない時(同じターン内で複数の LLM 呼び出し、context が同じ)、再組み立ては無駄。確定的シリアライズで変化を検出し、キャッシュヒット時は即座に返却:
```python
def get_system_prompt(context: dict) -> str:
global _last_context_key, _last_prompt
key = json.dumps(context, sort_keys=True, ensure_ascii=False, default=str)
if key == _last_context_key and _last_prompt:
return _last_prompt
_last_context_key = key
_last_prompt = assemble_system_prompt(context)
return _last_prompt
```
`hash()` ではなく `json.dumps` を使用Python 組み込みの `hash()` にはプロセスランダム化があり安定したキャッシュキーに不適切、list/dict で `unhashable type` エラーになる。
注意このキャッシュは「プロセス内での文字列再組み立ての回避」のみ。CC の API prompt cache とは別物。CC の prompt cache は `SYSTEM_PROMPT_DYNAMIC_BOUNDARY` で静的/動的部分を分離し、静的部分が global cache に命中する。動的内容が変化しても静的部分は無効化されない。
### context: 実際の状態、キーワード推測ではない
context は現在の実行時状態の実際の状態を反映:
```python
def update_context(context: dict, messages: list) -> dict:
memories = ""
if MEMORY_INDEX.exists():
content = MEMORY_INDEX.read_text().strip()
if content:
memories = content
return {
"enabled_tools": list(TOOL_HANDLERS.keys()),
"workspace": str(WORKDIR),
"memories": memories,
}
```
`enabled_tools` は実際に登録されたツールを一覧。`memories``.memory/MEMORY.md` が存在するかを確認。セクションの読み込みはこの実際の状態に基づき、メッセージ内のキーワード検索ではない。
### 組み合わせて実行
```python
def agent_loop(messages: list, context: dict):
system = get_system_prompt(context)
while True:
response = client.messages.create(
model=MODEL, system=system, messages=messages,
tools=TOOLS, max_tokens=8000)
# ... ツール実行 ...
context = update_context(context, messages)
system = get_system_prompt(context)
```
各ループ反復の開始時に system prompt を取得。context が変わっていれば再組み立て、変わっていなければキャッシュを返却。
---
## s09 からの変更点
| コンポーネント | 変更前 (s09) | 変更後 (s10) |
|-----------|-------------|-------------|
| prompt | ハードコード SYSTEM 文字列 | PROMPT_SECTIONS + assemble_system_prompt |
| キャッシュ | なし | get_system_promptjson.dumps 検出 + キャッシュ) |
| 新規関数 | — | assemble_system_prompt, get_system_prompt, update_context |
| ツール | bash, read_file, write_file (3) | bash, read_file, write_file (3) — 変更なし |
| ループ | 固定 SYSTEM を使用 | get_system_prompt(context) を使用 |
---
## 試してみよう
```sh
cd learn-claude-code
python s10_system_prompt/code.py
```
観察のポイント:
1. 出力にロードされたセクションが表示される(`[assembled] sections: ...` ラベル)
2. 継続会話でキャッシュヒット時は `[cache hit]` と表示
3. `.memory/MEMORY.md` を作成すると、次のターンで memory セクションが自動ロード
以下のプロンプトを試してみてください:
1. `Read the file README.md`(常にロードされる 3 つのセクションを観察)
2. `Create a file called .memory/MEMORY.md with content "- [test](test.md) — test memory"`(記憶インデックスを書き込み)
3. `Read the file code.py`memory セクションが表示されるか観察)
---
## 次へ
System prompt を実行時に組み立てられるようになった。しかし Agent はエラーでまだクラッシュする。ネットワークの不安定性、API レート制限、出力の切り詰め、コンテキスト超過、これらはバグではなく日常。
s11 Error Recovery → 4 つのリカバリパス。token のアップグレード、コンテキスト圧縮、指数バックオフ、モデル切り替え。
<details>
<summary>CC ソースコードの詳細</summary>
> 以下は CC ソースコード `constants/prompts.ts`914 行)、`constants/systemPromptSections.ts`68 行)、`context.ts`189 行)、`utils/api.ts`718 行)、`utils/systemPrompt.ts`123 行)、`bootstrap/state.ts` の分析に基づく。
### CC の system prompt にはいくつのセクションがあるか?
数は固定されておらず、feature flag、output style、KAIROS/Proactive モード、ユーザータイプ、token 予算などに影響される。大まかに 2 つのカテゴリ:
**静的セクション**常にロードidentity、system、doing_tasks、actions、using_tools、tone_style、output_efficiency など。
**動的セクション**状態に応じてロードsession_guidance、memory、ant_model_override、env_info_simple、language、output_style、mcp_instructions、scratchpad、frc、summarize_tool_results、numeric_length_anchors、token_budget、brief など。
`mcp_instructions` は唯一の揮発性セクション(`DANGEROUS_uncachedSystemPromptSection()` で作成。MCP server はターン間で接続・切断可能なため。
### 組み立て関数
```typescript
getSystemPrompt(tools, model, additionalWorkingDirs?, mcpClients?): Promise<string[]>
```
`string[]`(各要素がセクション)を返却。`SYSTEM_PROMPT_DYNAMIC_BOUNDARY` で静的/動的部分を分離。
### cache scope
global cache boundary が有効な場合、静的セクションは 1 つの global cache block にマージされ、動的セクションは global cache を使用しない(`cacheScope: null`。boundary なしまたは global cache をスキップするパスでのみ org scope にフォールバック。
教学版のキャッシュは文字列の再組み立てを回避するのみ。CC の 3 層キャッシュ:
1. **lodash memoize**: `getSystemContext``getUserContext` がセッション中キャッシュ(`context.ts`
2. **セクション登録キャッシュ**: `STATE.systemPromptSectionCache` が動的セクションの結果をキャッシュ、`/clear``/compact` でクリア
3. **API レベルキャッシュ**: `splitSysPromptPrefix()``api.ts`)が boundary を通じて異なる cache scope のブロックに分割
### getUserContext vs getSystemContext
| | getSystemContext | getUserContext |
|---|---|---|
| 内容 | gitStatus、cacheBreaker | CLAUDE.md 内容、currentDate |
| 注入方式 | system prompt 配列に追加 | `<system-reminder>` ユーザーメッセージとして先頭に配置 |
| スキップ条件 | カスタム system prompt 時 | 常に実行 |
### モードによる prompt の変化
- **CLAUDE_CODE_SIMPLE**: prompt 全体が 2 行のみ
- **Proactive/KAIROS**: コンパクト版 prompt が標準セクション全体を置換
- **Coordinator**: コーディネータ専用 prompt がデフォルトを完全に置換
- **Agent モード**: Agent 定義の prompt がデフォルトを置換または追加
### 総サイズ
標準インタラクティブモードの system prompt コアは約 20-30KB テキスト。CLAUDE_CODE_SIMPLE は約 150 文字。ユーザーコンテキストCLAUDE.mdとシステムコンテキストgit statusがこれに加算。
</details>
<!-- translation-sync: zh@v1, en@v1, ja@v1 -->

254
s10_system_prompt/README.md Normal file
View File

@@ -0,0 +1,254 @@
# s10: System Prompt — 运行时组装,不硬编码
[中文](README.md) · [English](README.en.md) · [日本語](README.ja.md)
s01 → ... → s08 → s09 → `s10` → [s11](../s11_error_recovery/) → s12 → ... → s20
> *"prompt 是组装出来的, 不是写死的"* — 分段 + 按需拼接 + 缓存。
>
> **Harness 层**: 提示 — 运行时组装, 不硬编码。
---
## 问题
从 s01 到 s09system prompt 都是一行硬编码:
```python
SYSTEM = f"You are a coding agent at {WORKDIR}. Use tools to solve tasks."
```
s01 够用,只有 bash、read、write 三个工具。但到 s09Agent 已经有记忆、有压缩、有技能加载。prompt 该提的能力越来越多:
```python
SYSTEM = (
f"You are a coding agent at {WORKDIR}. "
"Use tools to solve tasks. Act, don't explain. "
"Before starting any multi-step task, use todo_write. "
"Skills are available via list_skills and load_skill. "
"Relevant memories are injected below when available. "
# ... 加一个能力就多一段
)
```
三个问题:
1. **换项目要重写整个 prompt**,不知道哪些该改、哪些该留
2. **修改一处可能影响全局**,加一段工具描述可能跟前面的指令冲突
3. **每次请求都带全部内容**,即使当前对话用不到某些段落也浪费 token
System prompt 应该是运行时根据当前状态组装的配置:哪些工具启用、哪些上下文可见、哪些记忆相关、哪些内容必须保持稳定以命中 prompt cache。
---
## 解决方案
![System Prompt Overview](images/system-prompt-overview.svg)
s10 聚焦 prompt 组装机制。以 s08-s09 的能力为背景,但不重复实现压缩和记忆系统。核心变动:把硬编码的 `SYSTEM` 拆成独立段落section运行时根据真实状态按需拼接缓存结果避免重复组装。
四个 section两种加载策略
| Section | 加载策略 | 内容 | 判断依据 |
|---------|---------|------|---------|
| identity | 始终 | 你是谁、怎么做事 | 始终存在 |
| tools | 始终 | 可用工具列表 | `enabled_tools` |
| workspace | 始终 | 工作目录 | 始终存在 |
| memory | 按需 | 相关记忆内容 | `.memory/MEMORY.md` 是否存在 |
关键设计section 是否加载取决于真实状态(工具是否存在、文件是否存在),不是消息里的关键词。
---
## 工作原理
### PROMPT_SECTIONS: 分段定义
把一大段字符串拆成字典,每个 key 是一个主题:
```python
PROMPT_SECTIONS = {
"identity": "You are a coding agent. Act, don't explain.",
"tools": "Available tools: bash, read_file, write_file.",
"workspace": f"Working directory: {WORKDIR}",
"memory": "Relevant memories are injected below when available.",
}
```
每个 section 独立维护。修改 `tools` 不影响 `identity`,新增 `memory` 不动 `workspace`
### assemble_system_prompt: 按需拼接
不是所有 section 每次都需要。当前没有记忆文件,加载 memory section 只是浪费 token。根据 context 的真实状态决定加载哪些:
```python
def assemble_system_prompt(context: dict) -> str:
sections = []
# 始终加载
sections.append(PROMPT_SECTIONS["identity"])
sections.append(PROMPT_SECTIONS["tools"])
sections.append(PROMPT_SECTIONS["workspace"])
# 按需加载 — 基于真实状态,不是关键词
memories = context.get("memories", "")
if memories:
sections.append(f"Relevant memories:\n{memories}")
return "\n\n".join(sections)
```
"始终加载"的是每轮都需要的:身份、工具、工作目录。"按需加载"的只在特定条件下才有用。
为什么不全加载token 有成本system prompt 每轮计费),信息越少 LLM 越专注(无关指令是噪音)。
### get_system_prompt: 缓存避免重复拼接
上下文没变时(同一轮对话的多次 LLM 调用context 相同),重新拼接是浪费。用确定性序列化检测变化,命中缓存直接返回:
```python
def get_system_prompt(context: dict) -> str:
global _last_context_key, _last_prompt
key = json.dumps(context, sort_keys=True, ensure_ascii=False, default=str)
if key == _last_context_key and _last_prompt:
return _last_prompt
_last_context_key = key
_last_prompt = assemble_system_prompt(context)
return _last_prompt
```
`json.dumps` 而不是 `hash()`Python 内置 `hash()` 有进程随机化,不适合做稳定 cache key而且遇到 list/dict 会报 `unhashable type`
注意:这里的缓存只是"避免重复拼接字符串",和 CC 的 API prompt cache 不是一回事。CC 的 prompt cache 通过 `SYSTEM_PROMPT_DYNAMIC_BOUNDARY` 分隔静态和动态部分,静态部分命中 global cache不因动态内容变化而失效。
### context: 真实状态,不是关键词猜测
context 反映当前运行态的真实状态:
```python
def update_context(context: dict, messages: list) -> dict:
memories = ""
if MEMORY_INDEX.exists():
content = MEMORY_INDEX.read_text().strip()
if content:
memories = content
return {
"enabled_tools": list(TOOL_HANDLERS.keys()),
"workspace": str(WORKDIR),
"memories": memories,
}
```
`enabled_tools` 列出实际注册的工具。`memories` 检查 `.memory/MEMORY.md` 是否存在。section 加载基于这些真实状态,不在消息里搜关键词。
### 合起来跑
```python
def agent_loop(messages: list, context: dict):
system = get_system_prompt(context)
while True:
response = client.messages.create(
model=MODEL, system=system, messages=messages,
tools=TOOLS, max_tokens=8000)
# ... 工具执行 ...
context = update_context(context, messages)
system = get_system_prompt(context)
```
每轮循环开头拿一次 system prompt。context 变了就重新组装,没变就返回缓存。
---
## 相对 s09 的变更
| 组件 | 之前 (s09) | 之后 (s10) |
|------|-----------|-----------|
| prompt | 硬编码 SYSTEM 字符串 | PROMPT_SECTIONS + assemble_system_prompt |
| 缓存 | 无 | get_system_promptjson.dumps 检测 + 缓存) |
| 新函数 | — | assemble_system_prompt, get_system_prompt, update_context |
| 工具 | bash, read_file, write_file (3) | bash, read_file, write_file (3) — 不变 |
| 循环 | 用固定 SYSTEM | 用 get_system_prompt(context) |
---
## 试一下
```sh
cd learn-claude-code
python s10_system_prompt/code.py
```
观察重点:
1. 输出中能看到哪些 section 被加载了(`[assembled] sections: ...` 标签)
2. 连续对话时,缓存命中显示 `[cache hit]`
3. 创建 `.memory/MEMORY.md` 文件后,下一轮 memory section 自动加载
试试这些 prompt
1. `Read the file README.md`(观察始终加载的三个 section
2. `Create a file called .memory/MEMORY.md with content "- [test](test.md) — test memory"`(写入记忆索引)
3. `Read the file code.py`(观察 memory section 是否出现)
---
## 接下来
System prompt 可以运行时组装了,但 Agent 碰到错误还是会崩。网络抖动、API 限流、输出被截断、上下文超限,这些不是 bug是常态。
s11 Error Recovery → 四条恢复路径。升级 token、压缩上下文、指数退避、切换模型。
<details>
<summary>深入 CC 源码</summary>
> 以下基于 CC 源码 `constants/prompts.ts`914 行)、`constants/systemPromptSections.ts`68 行)、`context.ts`189 行)、`utils/api.ts`718 行)、`utils/systemPrompt.ts`123 行)、`bootstrap/state.ts` 的分析。
### CC 的 system prompt 有多少 section
数量不固定,受 feature flag、output style、KAIROS/Proactive 模式、用户类型、token 预算等影响。大致分两类:
**静态 section**始终加载identity、system、doing_tasks、actions、using_tools、tone_style、output_efficiency 等。
**动态 section**按状态加载session_guidance、memory、ant_model_override、env_info_simple、language、output_style、mcp_instructions、scratchpad、frc、summarize_tool_results、numeric_length_anchors、token_budget、brief 等。
`mcp_instructions` 是唯一的易失性 section通过 `DANGEROUS_uncachedSystemPromptSection()` 创建),因为 MCP server 可以在轮次间连接和断开。
### 组装函数
```typescript
getSystemPrompt(tools, model, additionalWorkingDirs?, mcpClients?): Promise<string[]>
```
返回 `string[]`(每个元素是一个 section`SYSTEM_PROMPT_DYNAMIC_BOUNDARY` 分隔静态和动态部分。
### cache scope
启用 global cache boundary 时,静态 section 合并成一个 global cache block动态 section 不使用 global cache`cacheScope: null`)。没有 boundary 或跳过 global cache 的路径才会走 org scope。
教学版的缓存只避免重复拼接字符串。CC 的三层缓存:
1. **lodash memoize**`getSystemContext``getUserContext` 在会话中缓存(`context.ts`
2. **section 注册缓存**`STATE.systemPromptSectionCache` 缓存动态 section 结果,`/clear``/compact` 时清除
3. **API 级缓存**`splitSysPromptPrefix()``api.ts`)把 prompt 按 boundary 分成不同 cache scope 的块
### getUserContext vs getSystemContext
| | getSystemContext | getUserContext |
|---|---|---|
| 内容 | gitStatus、cacheBreaker | CLAUDE.md 内容、currentDate |
| 注入方式 | 追加到 system prompt 数组 | 前置为 `<system-reminder>` 用户消息 |
| 何时跳过 | 自定义 system prompt 时 | 始终运行 |
### 模式如何改变 prompt
- **CLAUDE_CODE_SIMPLE**:整个 prompt 只有 2 行
- **Proactive/KAIROS**:用紧凑版 prompt 替换所有标准 section
- **Coordinator**:用协调器专用 prompt 完全替换
- **Agent 模式**Agent 定义的 prompt 替换或追加到默认 prompt
### 总大小
标准交互模式下 system prompt 核心约 20-30KB 文本。CLAUDE_CODE_SIMPLE 约 150 字符。用户上下文CLAUDE.md和系统上下文git status在此基础上累加。
</details>
<!-- translation-sync: zh@v1, en@v1, ja@v1 -->

218
s10_system_prompt/code.py Normal file
View File

@@ -0,0 +1,218 @@
#!/usr/bin/env python3
"""
s10: System Prompt — Runtime prompt assembly with caching.
Run: python s10_system_prompt/code.py
Need: pip install anthropic python-dotenv + .env with ANTHROPIC_API_KEY
Changes from s09:
- PROMPT_SECTIONS: topic-keyed dict of prompt fragments
- assemble_system_prompt(context): select + join sections by real state
- get_system_prompt(context): deterministic cache via json.dumps
- agent_loop uses get_system_prompt(context) instead of hardcoded SYSTEM
Memory section loads when .memory/MEMORY.md exists (real state, not keywords).
"""
import os, subprocess, json
from pathlib import Path
try:
import readline
readline.parse_and_bind('set bind-tty-special-chars off')
except ImportError:
pass
from anthropic import Anthropic
from dotenv import load_dotenv
load_dotenv(override=True)
if os.getenv("ANTHROPIC_BASE_URL"):
os.environ.pop("ANTHROPIC_AUTH_TOKEN", None)
WORKDIR = Path.cwd()
MEMORY_DIR = WORKDIR / ".memory"
MEMORY_INDEX = MEMORY_DIR / "MEMORY.md"
client = Anthropic(base_url=os.getenv("ANTHROPIC_BASE_URL"))
MODEL = os.environ["MODEL_ID"]
# ── Prompt Sections ──
PROMPT_SECTIONS = {
"identity": "You are a coding agent. Act, don't explain.",
"tools": "Available tools: bash, read_file, write_file.",
"workspace": f"Working directory: {WORKDIR}",
"memory": "Relevant memories are injected below when available.",
}
def assemble_system_prompt(context: dict) -> str:
"""Select and join prompt sections based on current context."""
sections = []
# Always loaded — identity, tools, workspace
sections.append(PROMPT_SECTIONS["identity"])
sections.append(PROMPT_SECTIONS["tools"])
sections.append(PROMPT_SECTIONS["workspace"])
# Conditional — memory loaded when MEMORY.md exists and has content
memories = context.get("memories", "")
if memories:
sections.append(f"Relevant memories:\n{memories}")
return "\n\n".join(sections)
_last_context_key = None
_last_prompt = None
def get_system_prompt(context: dict) -> str:
"""Cache wrapper — reassemble only when context changes.
Uses json.dumps for deterministic serialization, not Python's hash()
which has process randomization and fails on nested dicts/lists.
This cache only avoids redundant string assembly within a process.
Real Claude Code additionally protects API-level prompt cache via
stable section ordering and SYSTEM_PROMPT_DYNAMIC_BOUNDARY.
"""
global _last_context_key, _last_prompt
key = json.dumps(context, sort_keys=True, ensure_ascii=False, default=str)
if key == _last_context_key and _last_prompt:
print(" \033[90m[cache hit] system prompt unchanged\033[0m")
return _last_prompt
_last_context_key = key
_last_prompt = assemble_system_prompt(context)
loaded = ["identity", "tools", "workspace"]
if context.get("memories"):
loaded.append("memory")
print(f" \033[32m[assembled] sections: {', '.join(loaded)}\033[0m")
return _last_prompt
# ── Tools ──
def safe_path(p: str) -> Path:
path = (WORKDIR / p).resolve()
if not path.is_relative_to(WORKDIR):
raise ValueError(f"Path escapes workspace: {p}")
return path
def run_bash(command: str) -> str:
try:
r = subprocess.run(command, shell=True, cwd=WORKDIR,
capture_output=True, text=True, timeout=120)
out = (r.stdout + r.stderr).strip()
return out[:50000] if out else "(no output)"
except subprocess.TimeoutExpired:
return "Error: Timeout (120s)"
def run_read(path: str, limit: int | None = None) -> str:
try:
lines = safe_path(path).read_text().splitlines()
if limit and limit < len(lines):
lines = lines[:limit] + [f"... ({len(lines) - limit} more lines)"]
return "\n".join(lines)
except Exception as e:
return f"Error: {e}"
def run_write(path: str, content: str) -> str:
try:
file_path = safe_path(path)
file_path.parent.mkdir(parents=True, exist_ok=True)
file_path.write_text(content)
return f"Wrote {len(content)} bytes to {path}"
except Exception as e:
return f"Error: {e}"
TOOLS = [
{"name": "bash", "description": "Run a shell command.",
"input_schema": {"type": "object",
"properties": {"command": {"type": "string"}},
"required": ["command"]}},
{"name": "read_file", "description": "Read file contents.",
"input_schema": {"type": "object",
"properties": {"path": {"type": "string"},
"limit": {"type": "integer"}},
"required": ["path"]}},
{"name": "write_file", "description": "Write content to a file.",
"input_schema": {"type": "object",
"properties": {"path": {"type": "string"},
"content": {"type": "string"}},
"required": ["path", "content"]}},
]
TOOL_HANDLERS = {"bash": run_bash, "read_file": run_read, "write_file": run_write}
# ── Context ──
def update_context(context: dict, messages: list) -> dict:
"""Derive context from real state: which tools exist, whether memory files exist."""
memories = ""
if MEMORY_INDEX.exists():
content = MEMORY_INDEX.read_text().strip()
if content:
memories = content
return {
"enabled_tools": list(TOOL_HANDLERS.keys()),
"workspace": str(WORKDIR),
"memories": memories,
}
# ── Agent Loop ──
def agent_loop(messages: list, context: dict):
"""Main loop — uses assembled system prompt instead of hardcoded SYSTEM."""
system = get_system_prompt(context)
while True:
response = client.messages.create(
model=MODEL, system=system, messages=messages,
tools=TOOLS, max_tokens=8000)
messages.append({"role": "assistant", "content": response.content})
if response.stop_reason != "tool_use":
return
results = []
for block in response.content:
if block.type != "tool_use":
continue
print(f"\033[36m> {block.name}\033[0m")
handler = TOOL_HANDLERS.get(block.name)
output = handler(**block.input) if handler else f"Unknown: {block.name}"
print(str(output)[:200])
results.append({"type": "tool_result",
"tool_use_id": block.id, "content": output})
messages.append({"role": "user", "content": results})
# Re-evaluate context and prompt after each tool round
context = update_context(context, messages)
system = get_system_prompt(context)
if __name__ == "__main__":
print("s10: system prompt — runtime assembly")
print("Enter a question, press Enter to send. Type q to quit.\n")
history = []
context = update_context({}, [])
while True:
try:
query = input("\033[36ms10 >> \033[0m")
except (EOFError, KeyboardInterrupt):
break
if query.strip().lower() in ("q", "exit", ""):
break
history.append({"role": "user", "content": query})
agent_loop(history, context)
context = update_context(context, history)
for block in history[-1]["content"]:
if getattr(block, "type", None) == "text":
print(block.text)
print()

View File

@@ -0,0 +1,107 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 420" font-family="system-ui, -apple-system, sans-serif">
<defs>
<linearGradient id="header" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="#1e3a5f"/><stop offset="100%" stop-color="#059669"/>
</linearGradient>
<marker id="arrow" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#555"/>
</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="#059669"/>
</marker>
</defs>
<rect width="760" height="420" fill="#fafbfc" rx="8"/>
<!-- Title -->
<rect x="0" y="0" width="760" height="44" fill="url(#header)" rx="8"/>
<rect x="0" y="36" width="760" height="8" fill="url(#header)"/>
<text x="380" y="28" fill="#fff" font-size="15" font-weight="700" text-anchor="middle">System Prompt — PROMPT_SECTIONS + On-Demand Assembly + Cache</text>
<!-- Legend -->
<rect x="40" y="56" width="12" height="10" rx="2" fill="#f0f4ff" stroke="#2563eb" stroke-width="1"/>
<text x="58" y="66" fill="#2563eb" font-size="10" font-weight="600">s09 Preserved</text>
<rect x="160" y="56" width="12" height="10" rx="2" fill="#ecfdf5" stroke="#059669" stroke-width="1"/>
<text x="178" y="66" fill="#059669" font-size="10" font-weight="600">s10 New</text>
<!-- ===== Prompt Assembly (green, s10) ===== -->
<!-- PROMPT_SECTIONS -->
<rect x="40" y="82" width="170" height="88" rx="8" fill="#ecfdf5" stroke="#059669" stroke-width="2"/>
<text x="125" y="100" fill="#065f46" font-size="11" font-weight="700" text-anchor="middle">PROMPT_SECTIONS</text>
<text x="55" y="116" fill="#065f46" font-size="9">✓ identity (always)</text>
<text x="55" y="130" fill="#065f46" font-size="9">✓ tools (always)</text>
<text x="55" y="144" fill="#065f46" font-size="9">✓ workspace (always)</text>
<text x="55" y="158" fill="#6b7280" font-size="9">○ memory</text>
<!-- arrow → assemble -->
<line x1="210" y1="126" x2="235" y2="126" stroke="#059669" stroke-width="1.5" marker-end="url(#arrow-green)"/>
<!-- assemble_system_prompt -->
<rect x="238" y="82" width="170" height="88" rx="8" fill="#ecfdf5" stroke="#059669" stroke-width="2"/>
<text x="323" y="100" fill="#065f46" font-size="11" font-weight="700" text-anchor="middle">assemble_system_prompt</text>
<text x="253" y="118" fill="#065f46" font-size="9">Input: context dict</text>
<text x="253" y="132" fill="#065f46" font-size="9">Always: identity + tools + workspace</text>
<text x="253" y="146" fill="#065f46" font-size="9">On-demand: memory</text>
<text x="253" y="160" fill="#6b7280" font-size="9">Output: "\n\n".join(selected)</text>
<!-- arrow → cache -->
<line x1="408" y1="126" x2="433" y2="126" stroke="#059669" stroke-width="1.5" marker-end="url(#arrow-green)"/>
<!-- get_system_prompt -->
<rect x="436" y="82" width="170" height="88" rx="8" fill="#ecfdf5" stroke="#059669" stroke-width="2"/>
<text x="521" y="100" fill="#065f46" font-size="11" font-weight="700" text-anchor="middle">get_system_prompt</text>
<text x="451" y="118" fill="#065f46" font-size="9">json.dumps(context)</text>
<text x="451" y="132" fill="#065f46" font-size="9">Hit → return cached</text>
<text x="451" y="146" fill="#065f46" font-size="9">Miss → assemble + store</text>
<text x="451" y="160" fill="#6b7280" font-size="9">(s10 new)</text>
<!-- Arrow: cache → LLM -->
<path d="M 521 170 L 521 195 L 410 195 L 410 212" fill="none" stroke="#059669" stroke-width="1.5" marker-end="url(#arrow-green)"/>
<text x="462" y="189" fill="#059669" font-size="9">system=get_system_prompt(context)</text>
<!-- ===== s09 Agent Loop (blue) ===== -->
<!-- messages[] -->
<rect x="30" y="214" width="100" height="46" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
<text x="80" y="241" fill="#1e3a5f" font-size="11" font-weight="600" text-anchor="middle">messages[]</text>
<!-- arrow → compression+loading -->
<line x1="130" y1="237" x2="155" y2="237" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)"/>
<!-- compression + loading -->
<rect x="158" y="206" width="170" height="62" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
<text x="243" y="228" fill="#1e3a5f" font-size="10" font-weight="600" text-anchor="middle">Compression + Loading</text>
<text x="243" y="242" fill="#64748b" font-size="9" text-anchor="middle">snip → micro → budget → auto</text>
<text x="243" y="256" fill="#94a3b8" font-size="8" text-anchor="middle">→ load memory (s09)</text>
<!-- arrow → LLM -->
<line x1="328" y1="237" x2="358" y2="237" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)"/>
<!-- LLM -->
<rect x="360" y="214" width="100" height="46" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
<text x="410" y="231" fill="#1e3a5f" font-size="12" font-weight="700" text-anchor="middle">LLM</text>
<text x="410" y="246" fill="#64748b" font-size="8" text-anchor="middle">stop_reason=tool_use?</text>
<text x="410" y="258" fill="#059669" font-size="8" text-anchor="middle">system assembled</text>
<!-- arrow → TOOLS -->
<line x1="460" y1="237" x2="490" y2="237" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)"/>
<text x="466" y="229" fill="#64748b" font-size="8">yes</text>
<!-- TOOL_HANDLERS -->
<rect x="493" y="206" width="130" height="62" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
<text x="558" y="228" fill="#1e3a5f" font-size="10" font-weight="600" text-anchor="middle">TOOL_HANDLERS</text>
<text x="558" y="242" fill="#64748b" font-size="9" text-anchor="middle">bash · read · write</text>
<text x="558" y="256" fill="#94a3b8" font-size="8" text-anchor="middle">(s09 preserved)</text>
<!-- ===== Loop back ===== -->
<path d="M 623 237 L 660 237 L 660 312 L 80 312 L 80 260" fill="none" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)" stroke-dasharray="6,3"/>
<text x="370" y="328" fill="#64748b" font-size="10" text-anchor="middle">Tool results → messages[] → compress → load memory → assemble prompt → LLM</text>
<!-- ===== Bottom notes ===== -->
<rect x="40" y="350" width="680" height="56" rx="6" fill="#f8fafc" stroke="#e2e8f0" stroke-width="1"/>
<rect x="60" y="362" width="12" height="10" rx="2" fill="#f0f4ff" stroke="#2563eb" stroke-width="1"/>
<text x="80" y="372" fill="#475569" font-size="10">s09 Preserved: loop, compression pipeline, memory loading, tool execution</text>
<rect x="60" y="382" width="12" height="10" rx="2" fill="#ecfdf5" stroke="#059669" stroke-width="1"/>
<text x="80" y="392" fill="#475569" font-size="10">s10 New: PROMPT_SECTIONS (4 sections) + assemble_system_prompt + get_system_prompt (cache)</text>
</svg>

After

Width:  |  Height:  |  Size: 6.8 KiB

View File

@@ -0,0 +1,107 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 420" font-family="system-ui, -apple-system, sans-serif">
<defs>
<linearGradient id="header" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="#1e3a5f"/><stop offset="100%" stop-color="#059669"/>
</linearGradient>
<marker id="arrow" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#555"/>
</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="#059669"/>
</marker>
</defs>
<rect width="760" height="420" fill="#fafbfc" rx="8"/>
<!-- タイトル -->
<rect x="0" y="0" width="760" height="44" fill="url(#header)" rx="8"/>
<rect x="0" y="36" width="760" height="8" fill="url(#header)"/>
<text x="380" y="28" fill="#fff" font-size="14" font-weight="700" text-anchor="middle">System Prompt — PROMPT_SECTIONS + オンデマンド組み立て + キャッシュ</text>
<!-- 凡例 -->
<rect x="40" y="56" width="12" height="10" rx="2" fill="#f0f4ff" stroke="#2563eb" stroke-width="1"/>
<text x="58" y="66" fill="#2563eb" font-size="10" font-weight="600">s09 保持</text>
<rect x="140" y="56" width="12" height="10" rx="2" fill="#ecfdf5" stroke="#059669" stroke-width="1"/>
<text x="158" y="66" fill="#059669" font-size="10" font-weight="600">s10 新規</text>
<!-- ===== プロンプトアセンブリ緑、s10 ===== -->
<!-- PROMPT_SECTIONS -->
<rect x="40" y="82" width="170" height="88" rx="8" fill="#ecfdf5" stroke="#059669" stroke-width="2"/>
<text x="125" y="100" fill="#065f46" font-size="11" font-weight="700" text-anchor="middle">PROMPT_SECTIONS</text>
<text x="55" y="116" fill="#065f46" font-size="9">✓ identity (常時)</text>
<text x="55" y="130" fill="#065f46" font-size="9">✓ tools (常時)</text>
<text x="55" y="144" fill="#065f46" font-size="9">✓ workspace (常時)</text>
<text x="55" y="158" fill="#6b7280" font-size="9">○ memory</text>
<!-- 矢印 → assemble -->
<line x1="210" y1="126" x2="235" y2="126" stroke="#059669" stroke-width="1.5" marker-end="url(#arrow-green)"/>
<!-- assemble_system_prompt -->
<rect x="238" y="82" width="170" height="88" rx="8" fill="#ecfdf5" stroke="#059669" stroke-width="2"/>
<text x="323" y="100" fill="#065f46" font-size="11" font-weight="700" text-anchor="middle">assemble_system_prompt</text>
<text x="253" y="118" fill="#065f46" font-size="9">入力: context dict</text>
<text x="253" y="132" fill="#065f46" font-size="9">常時: identity + tools + workspace</text>
<text x="253" y="146" fill="#065f46" font-size="9">オンデマンド: memory</text>
<text x="253" y="160" fill="#6b7280" font-size="9">出力: "\n\n".join(selected)</text>
<!-- 矢印 → cache -->
<line x1="408" y1="126" x2="433" y2="126" stroke="#059669" stroke-width="1.5" marker-end="url(#arrow-green)"/>
<!-- get_system_prompt -->
<rect x="436" y="82" width="170" height="88" rx="8" fill="#ecfdf5" stroke="#059669" stroke-width="2"/>
<text x="521" y="100" fill="#065f46" font-size="11" font-weight="700" text-anchor="middle">get_system_prompt</text>
<text x="451" y="118" fill="#065f46" font-size="9">json.dumps(context)</text>
<text x="451" y="132" fill="#065f46" font-size="9">ヒット → キャッシュ返却</text>
<text x="451" y="146" fill="#065f46" font-size="9">ミス → assemble + 保存</text>
<text x="451" y="160" fill="#6b7280" font-size="9">(s10 新規)</text>
<!-- 矢印: cache → LLM -->
<path d="M 521 170 L 521 195 L 410 195 L 410 212" fill="none" stroke="#059669" stroke-width="1.5" marker-end="url(#arrow-green)"/>
<text x="462" y="189" fill="#059669" font-size="9">system=get_system_prompt(context)</text>
<!-- ===== s09 Agent Loop ===== -->
<!-- messages[] -->
<rect x="30" y="214" width="100" height="46" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
<text x="80" y="241" fill="#1e3a5f" font-size="11" font-weight="600" text-anchor="middle">messages[]</text>
<!-- 矢印 → compression+loading -->
<line x1="130" y1="237" x2="155" y2="237" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)"/>
<!-- 圧縮 + ロード -->
<rect x="158" y="206" width="170" height="62" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
<text x="243" y="228" fill="#1e3a5f" font-size="10" font-weight="600" text-anchor="middle">圧縮 + ロード</text>
<text x="243" y="242" fill="#64748b" font-size="9" text-anchor="middle">snip → micro → budget → auto</text>
<text x="243" y="256" fill="#94a3b8" font-size="8" text-anchor="middle">→ 記憶ロード (s09)</text>
<!-- 矢印 → LLM -->
<line x1="328" y1="237" x2="358" y2="237" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)"/>
<!-- LLM -->
<rect x="360" y="214" width="100" height="46" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
<text x="410" y="231" fill="#1e3a5f" font-size="12" font-weight="700" text-anchor="middle">LLM</text>
<text x="410" y="246" fill="#64748b" font-size="8" text-anchor="middle">stop_reason=tool_use?</text>
<text x="410" y="258" fill="#059669" font-size="8" text-anchor="middle">system assembled</text>
<!-- 矢印 → TOOLS -->
<line x1="460" y1="237" x2="490" y2="237" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)"/>
<text x="466" y="229" fill="#64748b" font-size="8">あり</text>
<!-- TOOL_HANDLERS -->
<rect x="493" y="206" width="130" height="62" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
<text x="558" y="228" fill="#1e3a5f" font-size="10" font-weight="600" text-anchor="middle">TOOL_HANDLERS</text>
<text x="558" y="242" fill="#64748b" font-size="9" text-anchor="middle">bash · read · write</text>
<text x="558" y="256" fill="#94a3b8" font-size="8" text-anchor="middle">(s09 保持)</text>
<!-- ===== ループバック ===== -->
<path d="M 623 237 L 660 237 L 660 312 L 80 312 L 80 260" fill="none" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)" stroke-dasharray="6,3"/>
<text x="370" y="328" fill="#64748b" font-size="10" text-anchor="middle">ツール結果 → messages[] → 圧縮 → 記憶ロード → プロンプト組み立て → LLM</text>
<!-- ===== 下部ノート ===== -->
<rect x="40" y="350" width="680" height="56" rx="6" fill="#f8fafc" stroke="#e2e8f0" stroke-width="1"/>
<rect x="60" y="362" width="12" height="10" rx="2" fill="#f0f4ff" stroke="#2563eb" stroke-width="1"/>
<text x="80" y="372" fill="#475569" font-size="10">s09 保持:ループ、圧縮パイプライン、記憶ロード、ツール実行</text>
<rect x="60" y="382" width="12" height="10" rx="2" fill="#ecfdf5" stroke="#059669" stroke-width="1"/>
<text x="80" y="392" fill="#475569" font-size="10">s10 新規PROMPT_SECTIONS4 セクション)+ assemble_system_prompt + get_system_promptキャッシュ</text>
</svg>

After

Width:  |  Height:  |  Size: 6.9 KiB

View File

@@ -0,0 +1,107 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 420" font-family="system-ui, -apple-system, sans-serif">
<defs>
<linearGradient id="header" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="#1e3a5f"/><stop offset="100%" stop-color="#059669"/>
</linearGradient>
<marker id="arrow" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#555"/>
</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="#059669"/>
</marker>
</defs>
<rect width="760" height="420" fill="#fafbfc" rx="8"/>
<!-- Title -->
<rect x="0" y="0" width="760" height="44" fill="url(#header)" rx="8"/>
<rect x="0" y="36" width="760" height="8" fill="url(#header)"/>
<text x="380" y="28" fill="#fff" font-size="15" font-weight="700" text-anchor="middle">System Prompt — PROMPT_SECTIONS + 按需拼接 + 缓存</text>
<!-- Legend -->
<rect x="40" y="56" width="12" height="10" rx="2" fill="#f0f4ff" stroke="#2563eb" stroke-width="1"/>
<text x="58" y="66" fill="#2563eb" font-size="10" font-weight="600">s09 保留</text>
<rect x="140" y="56" width="12" height="10" rx="2" fill="#ecfdf5" stroke="#059669" stroke-width="1"/>
<text x="158" y="66" fill="#059669" font-size="10" font-weight="600">s10 新增</text>
<!-- ===== Prompt Assembly (green, s10) ===== -->
<!-- PROMPT_SECTIONS -->
<rect x="40" y="82" width="170" height="88" rx="8" fill="#ecfdf5" stroke="#059669" stroke-width="2"/>
<text x="125" y="100" fill="#065f46" font-size="11" font-weight="700" text-anchor="middle">PROMPT_SECTIONS</text>
<text x="55" y="116" fill="#065f46" font-size="9">✓ identity (始终)</text>
<text x="55" y="130" fill="#065f46" font-size="9">✓ tools (始终)</text>
<text x="55" y="144" fill="#065f46" font-size="9">✓ workspace (始终)</text>
<text x="55" y="158" fill="#6b7280" font-size="9">○ memory</text>
<!-- arrow → assemble -->
<line x1="210" y1="126" x2="235" y2="126" stroke="#059669" stroke-width="1.5" marker-end="url(#arrow-green)"/>
<!-- assemble_system_prompt -->
<rect x="238" y="82" width="170" height="88" rx="8" fill="#ecfdf5" stroke="#059669" stroke-width="2"/>
<text x="323" y="100" fill="#065f46" font-size="11" font-weight="700" text-anchor="middle">assemble_system_prompt</text>
<text x="253" y="118" fill="#065f46" font-size="9">输入: context dict</text>
<text x="253" y="132" fill="#065f46" font-size="9">始终: identity + tools + workspace</text>
<text x="253" y="146" fill="#065f46" font-size="9">按需: memory</text>
<text x="253" y="160" fill="#6b7280" font-size="9">输出: "\n\n".join(selected)</text>
<!-- arrow → cache -->
<line x1="408" y1="126" x2="433" y2="126" stroke="#059669" stroke-width="1.5" marker-end="url(#arrow-green)"/>
<!-- get_system_prompt -->
<rect x="436" y="82" width="170" height="88" rx="8" fill="#ecfdf5" stroke="#059669" stroke-width="2"/>
<text x="521" y="100" fill="#065f46" font-size="11" font-weight="700" text-anchor="middle">get_system_prompt</text>
<text x="451" y="118" fill="#065f46" font-size="9">json.dumps(context)</text>
<text x="451" y="132" fill="#065f46" font-size="9">命中 → 返回缓存</text>
<text x="451" y="146" fill="#065f46" font-size="9">未命中 → assemble + 存</text>
<text x="451" y="160" fill="#6b7280" font-size="9">(s10 新增)</text>
<!-- Arrow: cache → LLM -->
<path d="M 521 170 L 521 195 L 410 195 L 410 212" fill="none" stroke="#059669" stroke-width="1.5" marker-end="url(#arrow-green)"/>
<text x="462" y="189" fill="#059669" font-size="9">system=get_system_prompt(context)</text>
<!-- ===== s09 Agent Loop (blue) ===== -->
<!-- messages[] -->
<rect x="30" y="214" width="100" height="46" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
<text x="80" y="241" fill="#1e3a5f" font-size="11" font-weight="600" text-anchor="middle">messages[]</text>
<!-- arrow → compression+loading -->
<line x1="130" y1="237" x2="155" y2="237" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)"/>
<!-- compression + loading -->
<rect x="158" y="206" width="170" height="62" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
<text x="243" y="228" fill="#1e3a5f" font-size="10" font-weight="600" text-anchor="middle">压缩 + Loading</text>
<text x="243" y="242" fill="#64748b" font-size="9" text-anchor="middle">snip → micro → budget → auto</text>
<text x="243" y="256" fill="#94a3b8" font-size="8" text-anchor="middle">→ 加载记忆 (s09)</text>
<!-- arrow → LLM -->
<line x1="328" y1="237" x2="358" y2="237" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)"/>
<!-- LLM -->
<rect x="360" y="214" width="100" height="46" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
<text x="410" y="231" fill="#1e3a5f" font-size="12" font-weight="700" text-anchor="middle">LLM</text>
<text x="410" y="246" fill="#64748b" font-size="8" text-anchor="middle">stop_reason=tool_use?</text>
<text x="410" y="258" fill="#059669" font-size="8" text-anchor="middle">system assembled</text>
<!-- arrow → TOOLS -->
<line x1="460" y1="237" x2="490" y2="237" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)"/>
<text x="466" y="229" fill="#64748b" font-size="8"></text>
<!-- TOOL_HANDLERS -->
<rect x="493" y="206" width="130" height="62" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
<text x="558" y="228" fill="#1e3a5f" font-size="10" font-weight="600" text-anchor="middle">TOOL_HANDLERS</text>
<text x="558" y="242" fill="#64748b" font-size="9" text-anchor="middle">bash · read · write</text>
<text x="558" y="256" fill="#94a3b8" font-size="8" text-anchor="middle">(s09 保留)</text>
<!-- ===== Loop back ===== -->
<path d="M 623 237 L 660 237 L 660 312 L 80 312 L 80 260" fill="none" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)" stroke-dasharray="6,3"/>
<text x="370" y="328" fill="#64748b" font-size="10" text-anchor="middle">工具结果 → messages[] → 压缩 → 加载记忆 → 组装 prompt → LLM</text>
<!-- ===== Bottom notes ===== -->
<rect x="40" y="350" width="680" height="56" rx="6" fill="#f8fafc" stroke="#e2e8f0" stroke-width="1"/>
<rect x="60" y="362" width="12" height="10" rx="2" fill="#f0f4ff" stroke="#2563eb" stroke-width="1"/>
<text x="80" y="372" fill="#475569" font-size="10">s09 保留:循环、压缩管线、记忆加载、工具执行</text>
<rect x="60" y="382" width="12" height="10" rx="2" fill="#ecfdf5" stroke="#059669" stroke-width="1"/>
<text x="80" y="392" fill="#475569" font-size="10">s10 新增PROMPT_SECTIONS4 段)+ assemble_system_prompt + get_system_prompt缓存</text>
</svg>

After

Width:  |  Height:  |  Size: 6.7 KiB