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

156
s05_todo_write/README.en.md Normal file
View File

@@ -0,0 +1,156 @@
# s05: TodoWrite — An Agent Without a Plan Drifts Off Course
[中文](README.md) · [English](README.en.md) · [日本語](README.ja.md)
s01 → s02 → s03 → s04 → `s05` → [s06](../s06_subagent/) → s07 → ... → s20
> *"An agent without a plan goes wherever the wind blows"* — List the steps first, then execute. Complex tasks are less likely to miss steps.
>
> **Harness Layer**: Planning — Let the Agent think before it acts.
---
## The Problem
Give the Agent a complex task: "Rename all Python files to snake_case, run tests, and fix failures."
The Agent starts working, renames 3 files, runs a test, finds 2 failures, starts fixing. While fixing, it forgets the original goal was "rename to snake_case", the test failures have consumed all its attention.
The longer the conversation, the worse it gets: tool results keep filling the context, diluting the system prompt's influence. A 10-step refactoring: after steps 1-3, the Agent starts improvising because steps 4-10 have been pushed out of its attention.
---
## The Solution
![Todo Overview](images/todo-overview.en.svg)
The minimal hook structure from the previous chapter is preserved, focusing on the new `todo_write` tool and reminder mechanism. `todo_write` does no actual work, can't read files or run commands, it simply lets the Agent organize its thoughts before diving in.
The dispatch mechanism is unchanged; the new tool is still routed through `TOOL_HANDLERS[block.name]`. However, to demonstrate the todo reminder, a counter was added to the loop: after 3 consecutive rounds without calling `todo_write`, a reminder is injected.
---
## How It Works
**The todo_write tool**, accepts a list with statuses, persists to `.tasks/current_todos.json` (teaching version writes to disk for observability), and displays progress in the terminal:
```python
def run_todo_write(todos: list) -> str:
tasks_file = TASKS_DIR / "current_todos.json"
tasks_file.write_text(json.dumps(todos, indent=2, ensure_ascii=False))
lines = ["\n## Current Tasks"]
for t in todos:
icon = {"pending": " ", "in_progress": "", "completed": ""}[t["status"]]
lines.append(f" [{icon}] {t['content']}")
print("\n".join(lines))
return f"Updated {len(todos)} tasks"
```
The tool definition joins the other 5 in the dispatch map:
```python
TOOLS = [
{"name": "bash", ...},
{"name": "read_file", ...},
{"name": "write_file", ...},
{"name": "edit_file", ...},
{"name": "glob", ...},
# s05: new entry
{"name": "todo_write", "description": "Create and manage a task list ...",
"input_schema": {
"type": "object",
"properties": {
"todos": {
"type": "array",
"items": {
"type": "object",
"properties": {
"content": {"type": "string"},
"status": {"type": "string", "enum": ["pending", "in_progress", "completed"]},
},
},
},
},
},
},
]
TOOL_HANDLERS["todo_write"] = run_todo_write
```
**Nag reminder**, when the model hasn't called `todo_write` for 3 consecutive rounds, a reminder is automatically injected (teaching mechanism; CC source has no fixed round-count logic):
```python
if rounds_since_todo >= 3 and messages:
messages.append({
"role": "user",
"content": "<reminder>Update your todos.</reminder>",
})
rounds_since_todo = 0
```
Typical flow when the Agent receives a task: first call `todo_write` to list all steps (all `pending`) → pick one step, set it to `in_progress` → complete it, set to `completed` → look at the next `pending` → continue. After 3 rounds without `todo_write`, the loop appends a reminder before the next LLM call.
**Key insight**: todo_write doesn't give the Agent any additional **execution capability**. What it adds is **planning capability**.
---
## Changes from s04
| Component | Before (s04) | After (s05) |
|-----------|-------------|-------------|
| Tool count | 5 (bash, read, write, edit, glob) | 6 (+todo_write) |
| Planning | None | Stateful TODO list + nag reminder |
| SYSTEM prompt | Generic prompt | Added "plan before executing" guidance |
| Loop | Unchanged | Dispatch unchanged, added rounds_since_todo counter and reminder injection |
---
## Try It
```sh
cd learn-claude-code
python s05_todo_write/code.py
```
Try these prompts:
1. `Refactor s05_todo_write/example/hello.py: add type hints, docstrings, and a main guard` (should list 3 steps first, then execute)
2. `Create a Python package under s05_todo_write/example/demo_pkg with __init__.py, utils.py, and tests/test_utils.py`
3. `Review Python files under s05_todo_write/example and fix any style issues`
What to watch for: Was the first tool call `todo_write`? How many TODO steps were listed? Did statuses move from `pending` to `in_progress` / `completed` during execution?
---
## What's Next
The Agent can plan now. But if a task is too large, say "refactor the entire auth module", a TODO list alone isn't enough. That task is itself a collection of dozens of subtasks that would drown in a single conversation's context.
→ s06 Subagent: Break large tasks into subtasks, each handled by an independent Agent with its own clean context, no cross-contamination.
<details>
<summary>Dive into CC Source Code</summary>
CC has two task systems coexisting (`tasks.ts:133-139`):
- **TodoWrite (V1)**: A simple list tool, data maintained in memory AppState (`TodoWriteTool.ts:65-103`). The teaching version writes to `.tasks/current_todos.json` for observability; the real V1 does not write to disk.
- **Task System (V2 = s12)**: File-persisted, dependency graph, concurrency locks, ownership.
The switch is controlled by `isTodoV2Enabled()`. In the current source: V2 is enabled by default in interactive sessions, V1 in non-interactive (SDK) sessions; setting `CLAUDE_CODE_ENABLE_TASKS` forces V2 regardless. Note the source comment "Force-enable tasks in non-interactive mode" describes the env var path's purpose, not the default branch's return semantics.
The teaching version omits the `activeForm` field from the real source (`utils/todo/types.ts:8-15`). CC uses it for the UI spinner to show "what's being done"; the teaching version only has terminal output and doesn't need this field.
The teaching version's nag reminder (3 rounds without update triggers injection) is an educational mechanism. The CC source has no fixed "3 rounds" logic; the closest is `TodoWriteTool.ts:72-107` which appends a verification nudge when 3+ todos are all completed without a verification item.
Core increments of the Task System over TodoWrite:
- File persistence (Claude config directory `tasks/{taskListId}/{taskId}.json`) instead of in-memory list
- `blockedBy` dependency graph instead of flat list
- `proper-lockfile` concurrency safety instead of no locking
- Four separate tools (Create/Get/Update/List) instead of one
- TaskCreated / TaskCompleted hooks (`TaskCreateTool.ts:80-129`, `TaskUpdateTool.ts:231-260`) for external system integration
</details>
<!-- translation-sync: zh@v1, en@v1, ja@v1 -->

156
s05_todo_write/README.ja.md Normal file
View File

@@ -0,0 +1,156 @@
# s05: TodoWrite — 計画なき Agent は途中で道を外れる
[中文](README.md) · [English](README.en.md) · [日本語](README.ja.md)
s01 → s02 → s03 → s04 → `s05` → [s06](../s06_subagent/) → s07 → ... → s20
> *"計画なき agent は風の向くままに"* — まず手順を列挙してから実行。長いタスクで見落としが減る。
>
> **Harness レイヤー**: 計画 — Agent が行動する前に考えさせる。
---
## 課題
Agent に複雑なタスクを与える:「全 Python ファイルを snake_case にリネームし、テストを実行し、失敗を修正して。」
Agent は作業を開始する。3 つのファイルをリネーム、テストを実行、2 つの失敗を発見、修正を開始。修正しているうちに、本来の目的が「snake_case にリネーム」だったことを忘れる。テストの失敗に注意を全て持っていかれる。
会話が長くなるほど悪化するツールの結果がコンテキストを埋め続け、システムプロンプトの影響力が希釈される。10 ステップのリファクタリング:ステップ 1-3 を終えた時点で Agent は即興で動き始める。ステップ 4-10 は既に注意の外に追い出されているから。
---
## ソリューション
![Todo Overview](images/todo-overview.ja.svg)
前章の最小フック構造を保持し、本章では新規の `todo_write` ツールとリマインダー機構に注目する。`todo_write` は実際の作業を何もしない。ファイルを読めない、コマンドを実行できない。Agent が手を動かす前に思考を整理できるようにするだけ。
ディスパッチ機構は変わらず、新ツールも `TOOL_HANDLERS[block.name]` を経由する。ただし、todo リマインダーのデモのため、ループにカウンターを追加した:連続 3 ラウンド `todo_write` を呼び出さないとリマインダーが注入される。
---
## 仕組み
**todo_write ツール**、ステータス付きのリストを受け取り、`.tasks/current_todos.json` に永続化(教育版は観察用にディスクに書き込む)、端末に進捗を表示する:
```python
def run_todo_write(todos: list) -> str:
tasks_file = TASKS_DIR / "current_todos.json"
tasks_file.write_text(json.dumps(todos, indent=2, ensure_ascii=False))
lines = ["\n## Current Tasks"]
for t in todos:
icon = {"pending": " ", "in_progress": "", "completed": ""}[t["status"]]
lines.append(f" [{icon}] {t['content']}")
print("\n".join(lines))
return f"Updated {len(todos)} tasks"
```
ツール定義は他の 5 つと一緒にディスパッチマップに追加される:
```python
TOOLS = [
{"name": "bash", ...},
{"name": "read_file", ...},
{"name": "write_file", ...},
{"name": "edit_file", ...},
{"name": "glob", ...},
# s05: 新規追加
{"name": "todo_write", "description": "Create and manage a task list ...",
"input_schema": {
"type": "object",
"properties": {
"todos": {
"type": "array",
"items": {
"type": "object",
"properties": {
"content": {"type": "string"},
"status": {"type": "string", "enum": ["pending", "in_progress", "completed"]},
},
},
},
},
},
},
]
TOOL_HANDLERS["todo_write"] = run_todo_write
```
**Nag リマインダー**、モデルが連続 3 ラウンド `todo_write` を呼び出さないとき、リマインダーが自動的に注入される教育用機構、CC ソースコードに固定ラウンド数のロジックはない):
```python
if rounds_since_todo >= 3 and messages:
messages.append({
"role": "user",
"content": "<reminder>Update your todos.</reminder>",
})
rounds_since_todo = 0
```
Agent がタスクを受け取った後の典型的な流れ:まず `todo_write` を呼び出して全手順を列挙(全て `pending`)→ 一つの手順に取り掛かり、`in_progress` に変更 → 完了したら `completed` に変更 → 次の `pending` を見る → 続行。3 ラウンド `todo_write` がない場合、次の LLM 呼び出し前にリマインダーが追加される。
**重要な洞察**todo_write は Agent に**実行能力**を何も追加しない。追加するのは**計画能力**だ。
---
## s04 からの変更
| コンポーネント | 変更前 (s04) | 変更後 (s05) |
|--------------|-------------|-------------|
| ツール数 | 5 (bash, read, write, edit, glob) | 6 (+todo_write) |
| 計画能力 | なし | ステータス付き TODO リスト + Nag リマインダー |
| SYSTEM プロンプト | 汎用プロンプト | 「先に計画してから実行」のガイダンスを追加 |
| ループ | 不変 | ディスパッチは不変、rounds_since_todo カウンターとリマインダー注入を追加 |
---
## 試してみよう
```sh
cd learn-claude-code
python s05_todo_write/code.py
```
以下のプロンプトを試してみよう:
1. `Refactor s05_todo_write/example/hello.py: add type hints, docstrings, and a main guard`(まず 3 手順を列挙してから実行するはず)
2. `Create a Python package under s05_todo_write/example/demo_pkg with __init__.py, utils.py, and tests/test_utils.py`
3. `Review Python files under s05_todo_write/example and fix any style issues`
観察のポイント:最初のツール呼び出しは `todo_write` か? TODO は何手順列挙されたか? 実行中にステータスが `pending` から `in_progress` / `completed` に変わったか?
---
## 次へ
Agent は計画できるようになった。しかしタスクが大きすぎる場合、例えば「認証モジュール全体をリファクタリング」、TODO リストだけでは不十分。そのタスク自体が数十のサブタスクの集合体で、同じ会話のコンテキストに押し込めると溢れてしまう。
→ s06 Subagent大きなタスクをサブタスクに分割し、それぞれを独立した Agent に任せる。それぞれが独自のクリーンなコンテキストを持ち、相互汚染がない。
<details>
<summary>CC ソースコードを深掘り</summary>
CC には二つのタスクシステムが共存している(`tasks.ts:133-139`
- **TodoWriteV1**:シンプルなリストツール、データはメモリ AppState で管理(`TodoWriteTool.ts:65-103`)。教育版は観察用に `.tasks/current_todos.json` に書き込むが、実際の V1 はディスクに書き込まない
- **Task SystemV2 = s12**ファイル永続化、依存グラフ、並行ロック、ownership
切り替えは `isTodoV2Enabled()` で制御される。現在のソースコードの実装:対話型セッションでは V2 がデフォルトで有効、非対話型セッションSDKでは V1 がデフォルトで有効。`CLAUDE_CODE_ENABLE_TASKS` 環境変数を設定するとセッション種別に関わらず V2 が強制有効になる。ソースコメント「Force-enable tasks in non-interactive mode」は環境変数パスの用途を説明しており、デフォルト分岐の戻り値のセマンティクスとは異なるため注意。
教育版は実際のソースコードにある `activeForm` フィールドを省略している(`utils/todo/types.ts:8-15`。CC は UI スピナーに「何をしているか」を表示するために使用するが、教育版は端末出力のみでこのフィールドは不要。
教育版の Nag リマインダー3 ラウンド未更新で注入は教育用機構。CC ソースコードに固定「3 ラウンド」のロジックはなく、最も近いのは `TodoWriteTool.ts:72-107` で 3 つ以上の todo が全て完了しているのに verification 項目がない場合に verification nudge を追加する処理。
Task System の TodoWrite に対する核心的な増分:
- メモリリストではなくファイル永続化Claude 設定ディレクトリ下 `tasks/{taskListId}/{taskId}.json`
- 平坦なリストではなく `blockedBy` 依存グラフ
- ロックなしではなく `proper-lockfile` による並行安全性
- 一つのツールではなく四つの独立ツールCreate/Get/Update/List
- TaskCreated / TaskCompleted フック(`TaskCreateTool.ts:80-129``TaskUpdateTool.ts:231-260`)による外部システム統合
</details>
<!-- translation-sync: zh@v1, en@v1, ja@v1 -->

156
s05_todo_write/README.md Normal file
View File

@@ -0,0 +1,156 @@
# s05: TodoWrite — 没有计划的 Agent做着做着就偏了
[中文](README.md) · [English](README.en.md) · [日本語](README.ja.md)
s01 → s02 → s03 → s04 → `s05` → [s06](../s06_subagent/) → s07 → ... → s20
> *"没有计划的 agent 走哪算哪"* — 先列步骤再动手,长任务更不容易漏项。
>
> **Harness 层**: 规划 — 让 Agent 在动手之前先想清楚。
---
## 问题
给 Agent 一个复杂任务:"把所有 Python 文件改成 snake_case 命名,然后跑测试,修好失败。"
Agent 开始干活,改了 3 个文件,跑了个测试,发现 2 个失败,开始修。修着修着,它忘了最初是"改成 snake_case",测试失败把注意力全吸走了。
对话越长越严重:工具结果不断填满上下文,系统提示的影响力被稀释。一个 10 步重构,做完 1-3 步就开始即兴发挥,因为 4-10 步已经被挤出注意力了。
---
## 解决方案
![Todo Overview](images/todo-overview.svg)
保留上一章的最小 hook 结构,重点看新增的 `todo_write` 工具和 reminder 机制。`todo_write` 本身不做任何实际工作,不能读文件、不能跑命令,只是让 Agent 在动手之前先理清思路。
dispatch 机制不变,新工具仍然走 `TOOL_HANDLERS[block.name]` 分发。但为了演示 todo reminder循环里加了一个计数器连续 3 轮没调 `todo_write` 就注入一条提醒。
---
## 工作原理
**todo_write 工具**,接收一个带状态的列表,持久化到 `.tasks/current_todos.json`(教学版写盘以便观察),同时在终端显示进度:
```python
def run_todo_write(todos: list) -> str:
tasks_file = TASKS_DIR / "current_todos.json"
tasks_file.write_text(json.dumps(todos, indent=2, ensure_ascii=False))
lines = ["\n## Current Tasks"]
for t in todos:
icon = {"pending": " ", "in_progress": "", "completed": ""}[t["status"]]
lines.append(f" [{icon}] {t['content']}")
print("\n".join(lines))
return f"Updated {len(todos)} tasks"
```
工具定义和其他 5 个工具一起加入 dispatch map
```python
TOOLS = [
{"name": "bash", ...},
{"name": "read_file", ...},
{"name": "write_file", ...},
{"name": "edit_file", ...},
{"name": "glob", ...},
# s05: 新增一条
{"name": "todo_write", "description": "Create and manage a task list ...",
"input_schema": {
"type": "object",
"properties": {
"todos": {
"type": "array",
"items": {
"type": "object",
"properties": {
"content": {"type": "string"},
"status": {"type": "string", "enum": ["pending", "in_progress", "completed"]},
},
},
},
},
},
},
]
TOOL_HANDLERS["todo_write"] = run_todo_write
```
**Nag reminder**,模型连续 3 轮没调 `todo_write`自动注入一条提醒教学版机制CC 源码中没有这个固定轮数逻辑):
```python
if rounds_since_todo >= 3 and messages:
messages.append({
"role": "user",
"content": "<reminder>Update your todos.</reminder>",
})
rounds_since_todo = 0
```
Agent 收到任务后的典型流程:先调 `todo_write` 列出所有步骤(全 `pending`)→ 做一个步骤,改成 `in_progress` → 做完改成 `completed` → 看下一个 `pending` → 继续。连续 3 轮没有调用 `todo_write` 时,循环会在下一次 LLM 调用前追加一条 reminder。
**关键洞察**todo_write 不给 Agent 增加任何**执行能力**。它增加的是**规划能力**。
---
## 相对 s04 的变更
| 组件 | 之前 (s04) | 之后 (s05) |
|------|-----------|-----------|
| 工具数量 | 5 (bash, read, write, edit, glob) | 6 (+todo_write) |
| 规划能力 | 无 | 带状态的 TODO 列表 + nag reminder |
| SYSTEM 提示 | 通用提示 | 加入 "先计划再执行" 引导 |
| 循环 | 不变 | dispatch 不变,新增 rounds_since_todo 计数器和 reminder 注入 |
---
## 试一下
```sh
cd learn-claude-code
python s05_todo_write/code.py
```
试试这些 prompt
1. `Refactor s05_todo_write/example/hello.py: add type hints, docstrings, and a main guard`(先列 3 步再执行)
2. `Create a Python package under s05_todo_write/example/demo_pkg with __init__.py, utils.py, and tests/test_utils.py`
3. `Review Python files under s05_todo_write/example and fix any style issues`
观察重点:第一次工具调用是不是 `todo_write`TODO 列了几步?执行过程中状态有没有从 `pending` 变成 `in_progress` / `completed`
---
## 接下来
Agent 能计划了。但如果一个任务太大,比如"重构整个认证模块",光靠 TODO 列表不够。这个任务本身就是几十个小任务的集合,放在同一个对话里会被上下文淹没。
s06 Subagent → 把大任务拆成子任务,每个子任务派一个独立的 Agent。它们有自己的干净上下文不会互相污染。
<details>
<summary>深入 CC 源码</summary>
CC 中有两套任务系统并存(`tasks.ts:133-139`
- **TodoWriteV1**:一个简单的列表工具,数据在内存 AppState 中维护(`TodoWriteTool.ts:65-103`)。教学版写盘到 `.tasks/current_todos.json` 是为了可观察性,真实 V1 不写盘
- **Task SystemV2 = s12**文件持久化、依赖图、并发锁、ownership
切换由 `isTodoV2Enabled()` 控制。当前源码的实现逻辑:交互式会话中 V2 默认启用非交互式会话SDK中 V1 默认启用;设置 `CLAUDE_CODE_ENABLE_TASKS` 环境变量可强制启用 V2。注意源码注释 "Force-enable tasks in non-interactive mode" 描述的是 env var 路径的用途,和默认分支的返回值语义不同,阅读时需区分。
教学版省略了真实源码中的 `activeForm` 字段(`utils/todo/types.ts:8-15`。CC 用它给 UI spinner 展示"正在做什么",教学版只有终端输出,不需要这个字段。
教学版的 nag reminder3 轮未更新就注入提醒是教学机制。CC 源码中没有固定的"3 轮"逻辑,更接近的是 `TodoWriteTool.ts:72-107` 中当 3 个以上 todo 全部完成但没有 verification 项时,追加 verification nudge。
Task System 相比 TodoWrite 的核心增量:
- 文件持久化Claude 配置目录下 `tasks/{taskListId}/{taskId}.json`)而非内存列表
- `blockedBy` 依赖图而非平铺列表
- `proper-lockfile` 并发安全而非无锁
- 四个独立工具Create/Get/Update/List而非一个
- TaskCreated / TaskCompleted hooks`TaskCreateTool.ts:80-129``TaskUpdateTool.ts:231-260`)供外部系统集成
</details>
<!-- translation-sync: zh@v1, en@v0, ja@v0 -->

287
s05_todo_write/code.py Normal file
View File

@@ -0,0 +1,287 @@
#!/usr/bin/env python3
"""
s05: TodoWrite — add a planning tool on top of s04 hooks.
+---------+ +-------+ +------------------+
| User | ---> | LLM | ---> | TOOL_HANDLERS |
| prompt | | | | bash |
+---------+ +---+---+ | read_file |
^ | write_file |
| result | edit_file |
+---------+ glob |
todo_write ← NEW
+------------------+
|
.tasks/current_todos.json
|
if rounds_since_todo >= 3:
inject <reminder>
Changes from s04:
+ todo_write tool + run_todo_write() implementation
+ Nag reminder (inject reminder after 3 rounds without todo update)
+ SYSTEM prompt includes "plan before execute" guidance
+ rounds_since_todo counter in agent_loop
Loop unchanged: new tool auto-dispatches via TOOL_HANDLERS.
Run: python s05_todo_write/code.py
Needs: pip install anthropic python-dotenv + ANTHROPIC_API_KEY in .env
"""
import os, subprocess, json
from pathlib import Path
try:
import readline
readline.parse_and_bind('set bind-tty-special-chars off')
except ImportError:
pass
from anthropic import Anthropic
from dotenv import load_dotenv
load_dotenv(override=True)
if os.getenv("ANTHROPIC_BASE_URL"):
os.environ.pop("ANTHROPIC_AUTH_TOKEN", None)
WORKDIR = Path.cwd()
TASKS_DIR = WORKDIR / ".tasks"; TASKS_DIR.mkdir(exist_ok=True)
client = Anthropic(base_url=os.getenv("ANTHROPIC_BASE_URL"))
MODEL = os.environ["MODEL_ID"]
# s05 change: SYSTEM prompt adds planning guidance
SYSTEM = (
f"You are a coding agent at {WORKDIR}. "
"Before starting any multi-step task, use todo_write to plan your steps. "
"Update status as you go."
)
# ═══════════════════════════════════════════════════════════
# FROM s02-s04 (unchanged): Tool Implementations
# ═══════════════════════════════════════════════════════════
def safe_path(p: str) -> Path:
path = (WORKDIR / p).resolve()
if not path.is_relative_to(WORKDIR):
raise ValueError(f"Path escapes workspace: {p}")
return path
def run_bash(command: str) -> str:
try:
r = subprocess.run(command, shell=True, cwd=WORKDIR,
capture_output=True, text=True, timeout=120)
out = (r.stdout + r.stderr).strip()
return out[:50000] if out else "(no output)"
except subprocess.TimeoutExpired:
return "Error: Timeout (120s)"
def run_read(path: str, limit: int | None = None) -> str:
try:
lines = safe_path(path).read_text().splitlines()
if limit and limit < len(lines):
lines = lines[:limit] + [f"... ({len(lines) - limit} more lines)"]
return "\n".join(lines)
except Exception as e:
return f"Error: {e}"
def run_write(path: str, content: str) -> str:
try:
file_path = safe_path(path)
file_path.parent.mkdir(parents=True, exist_ok=True)
file_path.write_text(content)
return f"Wrote {len(content)} bytes to {path}"
except Exception as e:
return f"Error: {e}"
def run_edit(path: str, old_text: str, new_text: str) -> str:
try:
file_path = safe_path(path)
text = file_path.read_text()
if old_text not in text:
return f"Error: text not found in {path}"
file_path.write_text(text.replace(old_text, new_text, 1))
return f"Edited {path}"
except Exception as e:
return f"Error: {e}"
def run_glob(pattern: str) -> str:
import glob as g
try:
results = []
for match in g.glob(pattern, root_dir=WORKDIR):
if (WORKDIR / match).resolve().is_relative_to(WORKDIR):
results.append(match)
return "\n".join(results) if results else "(no matches)"
except Exception as e:
return f"Error: {e}"
# ═══════════════════════════════════════════════════════════
# NEW in s05: todo_write tool — plan only, no execution
# ═══════════════════════════════════════════════════════════
def run_todo_write(todos: list) -> str:
# validate required fields
for i, t in enumerate(todos):
if "content" not in t or "status" not in t:
return f"Error: todos[{i}] missing 'content' or 'status'"
if t["status"] not in ("pending", "in_progress", "completed"):
return f"Error: todos[{i}] has invalid status '{t['status']}'"
tasks_file = TASKS_DIR / "current_todos.json"
tasks_file.write_text(json.dumps(todos, indent=2, ensure_ascii=False))
lines = ["\n\033[33m## Current Tasks\033[0m"]
for t in todos:
icon = {"pending": " ", "in_progress": "\033[36m▸\033[0m", "completed": "\033[32m✓\033[0m"}[t["status"]]
lines.append(f" [{icon}] {t['content']}")
print("\n".join(lines))
return f"Updated {len(todos)} tasks"
TOOLS = [
{"name": "bash", "description": "Run a shell command.",
"input_schema": {"type": "object", "properties": {"command": {"type": "string"}}, "required": ["command"]}},
{"name": "read_file", "description": "Read file contents.",
"input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "limit": {"type": "integer"}}, "required": ["path"]}},
{"name": "write_file", "description": "Write content to a file.",
"input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "content": {"type": "string"}}, "required": ["path", "content"]}},
{"name": "edit_file", "description": "Replace exact text in a file once.",
"input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "old_text": {"type": "string"}, "new_text": {"type": "string"}}, "required": ["path", "old_text", "new_text"]}},
{"name": "glob", "description": "Find files matching a glob pattern.",
"input_schema": {"type": "object", "properties": {"pattern": {"type": "string"}}, "required": ["pattern"]}},
# s05: new tool
{"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"]}},
]
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,
}
# ═══════════════════════════════════════════════════════════
# FROM s04 (unchanged): Hook System
# ═══════════════════════════════════════════════════════════
HOOKS = {"UserPromptSubmit": [], "PreToolUse": [], "PostToolUse": [], "Stop": []}
def register_hook(event: str, callback):
HOOKS[event].append(callback)
def trigger_hooks(event: str, *args):
for callback in HOOKS[event]:
result = callback(*args)
if result is not None:
return result
return None
# s04 hooks preserved
DENY_LIST = ["rm -rf /", "sudo", "shutdown", "reboot", "mkfs", "dd if="]
def permission_hook(block):
"""PreToolUse: deny list check."""
if block.name == "bash":
for p in DENY_LIST:
if p in block.input.get("command", ""):
print(f"\n\033[31m⛔ Blocked: '{p}'\033[0m")
return "Permission denied"
return None
def log_hook(block):
"""PreToolUse: log tool calls."""
print(f"\033[90m[HOOK] {block.name}\033[0m")
return None
def context_inject_hook(query: str):
"""UserPromptSubmit: log working directory."""
print(f"\033[90m[HOOK] UserPromptSubmit: working in {WORKDIR}\033[0m")
return None
def summary_hook(messages: list):
"""Stop: print tool call count."""
tool_count = sum(1 for m in messages
for b in (m.get("content") if isinstance(m.get("content"), list) else [])
if isinstance(b, dict) and b.get("type") == "tool_result")
print(f"\033[90m[HOOK] Stop: session used {tool_count} tool calls\033[0m")
return None
register_hook("UserPromptSubmit", context_inject_hook)
register_hook("PreToolUse", permission_hook)
register_hook("PreToolUse", log_hook)
register_hook("Stop", summary_hook)
# ═══════════════════════════════════════════════════════════
# agent_loop — same as s04 + nag reminder counter
# ═══════════════════════════════════════════════════════════
rounds_since_todo = 0
def agent_loop(messages: list):
global rounds_since_todo
while True:
# s05: nag reminder — inject if model hasn't updated todos for 3 rounds
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,
)
messages.append({"role": "assistant", "content": response.content})
if response.stop_reason != "tool_use":
force = trigger_hooks("Stop", messages)
if force:
messages.append({"role": "user", "content": force})
continue
return
rounds_since_todo += 1
results = []
for block in response.content:
if block.type != "tool_use":
continue
blocked = trigger_hooks("PreToolUse", block)
if blocked:
results.append({"type": "tool_result", "tool_use_id": block.id,
"content": str(blocked)})
continue
handler = TOOL_HANDLERS.get(block.name)
output = handler(**block.input) if handler else f"Unknown: {block.name}"
trigger_hooks("PostToolUse", block, output)
# s05: reset nag counter when todo_write is called
if block.name == "todo_write":
rounds_since_todo = 0
results.append({"type": "tool_result", "tool_use_id": block.id,
"content": output})
messages.append({"role": "user", "content": results})
if __name__ == "__main__":
print("s05: TodoWrite — plan before execute, nag if you forget")
print("Type a question, press Enter. Type q to quit.\n")
history = []
while True:
try:
query = input("\033[36ms05 >> \033[0m")
except (EOFError, KeyboardInterrupt):
break
if query.strip().lower() in ("q", "exit", ""):
break
trigger_hooks("UserPromptSubmit", query)
history.append({"role": "user", "content": query})
agent_loop(history)
for block in history[-1]["content"]:
if getattr(block, "type", None) == "text":
print(block.text)
print()

View File

@@ -0,0 +1,6 @@
def greet(name):
message = "Hello, " + name
print(message)
greet("Claude")

View File

@@ -0,0 +1,93 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 420" font-family="system-ui, -apple-system, sans-serif">
<defs>
<marker id="arrow" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#555"/>
</marker>
<marker id="arrow-blue" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#2563eb"/>
</marker>
<marker id="arrow-green" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#16a34a"/>
</marker>
<linearGradient id="header" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="#1e3a5f"/>
<stop offset="100%" stop-color="#2563eb"/>
</linearGradient>
</defs>
<!-- Background -->
<rect width="800" height="420" fill="#fafbfc" rx="8"/>
<!-- Title -->
<rect x="0" y="0" width="800" height="48" fill="url(#header)" rx="8"/>
<rect x="0" y="40" width="800" height="8" fill="url(#header)"/>
<text x="400" y="31" fill="#fff" font-size="16" font-weight="700" text-anchor="middle">TodoWrite — Loop Unchanged, One More Tool Auto-Dispatched</text>
<!-- ===== s04 Preserved (gray) ===== -->
<text x="50" y="76" fill="#94a3b8" font-size="11" font-weight="600">s04 Preserved</text>
<!-- messages -->
<rect x="40" y="90" width="100" height="44" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
<text x="90" y="117" fill="#1e3a5f" font-size="12" font-weight="600" text-anchor="middle">messages[]</text>
<!-- → LLM -->
<line x1="140" y1="112" x2="188" y2="112" stroke="#2563eb" stroke-width="2" marker-end="url(#arrow-blue)"/>
<!-- LLM -->
<rect x="190" y="86" width="110" height="52" rx="8" fill="#fff" stroke="#2563eb" stroke-width="1.5"/>
<text x="245" y="108" fill="#1e3a5f" font-size="13" font-weight="700" text-anchor="middle">LLM</text>
<text x="245" y="126" fill="#64748b" font-size="9" text-anchor="middle">stop_reason=tool_use?</text>
<!-- No → Return -->
<line x1="245" y1="138" x2="245" y2="162" stroke="#16a34a" stroke-width="2" marker-end="url(#arrow)"/>
<text x="258" y="156" fill="#16a34a" font-size="9" font-weight="600">No</text>
<rect x="195" y="164" width="100" height="28" rx="14" fill="#dcfce7" stroke="#16a34a" stroke-width="1.5"/>
<text x="245" y="182" fill="#166534" font-size="11" font-weight="600" text-anchor="middle">Return Result</text>
<!-- Yes → PreToolUse -->
<line x1="300" y1="112" x2="348" y2="112" stroke="#555" stroke-width="2" marker-end="url(#arrow)"/>
<text x="320" y="104" fill="#d97706" font-size="9" font-weight="600">Yes</text>
<!-- PreToolUse (s04) -->
<rect x="350" y="88" width="100" height="48" rx="8" fill="#f0fdf4" stroke="#16a34a" stroke-width="1.5"/>
<text x="400" y="110" fill="#166534" font-size="9" font-weight="600" text-anchor="middle">trigger_hooks</text>
<text x="400" y="124" fill="#166534" font-size="8" text-anchor="middle">PreToolUse</text>
<!-- → TOOL_HANDLERS -->
<line x1="450" y1="112" x2="498" y2="112" stroke="#555" stroke-width="2" marker-end="url(#arrow)"/>
<!-- ===== s05 New: todo_write ===== -->
<!-- TOOL_HANDLERS box (expanded, includes todo_write) -->
<rect x="500" y="74" width="120" height="140" rx="10" fill="#f0fdf4" stroke="#16a34a" stroke-width="2" stroke-dasharray="6,3"/>
<text x="560" y="94" fill="#166534" font-size="10" font-weight="700" text-anchor="middle">TOOL_HANDLERS</text>
<!-- s04 preserved tools -->
<rect x="512" y="102" width="96" height="22" rx="4" fill="#f0f4ff" stroke="#2563eb" stroke-width="1"/>
<text x="560" y="117" fill="#1e3a5f" font-size="9" text-anchor="middle">bash · read · write</text>
<rect x="512" y="130" width="96" height="22" rx="4" fill="#f0f4ff" stroke="#2563eb" stroke-width="1"/>
<text x="560" y="145" fill="#1e3a5f" font-size="9" text-anchor="middle">edit · glob</text>
<!-- s05 new: todo_write -->
<rect x="512" y="158" width="96" height="22" rx="4" fill="#dcfce7" stroke="#16a34a" stroke-width="1.5"/>
<text x="560" y="173" fill="#166534" font-size="9" font-weight="700" text-anchor="middle">todo_write</text>
<text x="560" y="232" fill="#16a34a" font-size="11" font-weight="600" text-anchor="middle">s05 New</text>
<text x="560" y="196" fill="#64748b" font-size="8" text-anchor="middle">→ .tasks/current_todos.json</text>
<!-- Loop back -->
<path d="M 620 112 L 660 112 L 660 260 L 90 260 L 90 134" fill="none" stroke="#555" stroke-width="2" marker-end="url(#arrow)" stroke-dasharray="6,3"/>
<text x="370" y="280" fill="#64748b" font-size="10" text-anchor="middle">Results appended to messages[], loop continues</text>
<!-- ===== Nag Reminder ===== -->
<rect x="100" y="310" width="600" height="56" rx="8" fill="#fffbeb" stroke="#d97706" stroke-width="1"/>
<text x="120" y="332" fill="#92400e" font-size="11" font-weight="700">Nag Reminder</text>
<text x="120" y="352" fill="#92400e" font-size="10">Model hasn't called todo_write for 3 rounds → auto-inject &lt;reminder&gt;Update your todos.&lt;/reminder&gt;</text>
<!-- Legend -->
<rect x="100" y="384" width="600" height="28" rx="6" fill="#f1f5f9"/>
<rect x="120" y="392" width="12" height="12" rx="2" fill="#f0f4ff" stroke="#2563eb" stroke-width="1"/>
<text x="140" y="402" fill="#334155" font-size="10">s04 Preserved (loop, hooks, 5 base tools)</text>
<rect x="400" y="392" width="12" height="12" rx="2" fill="#dcfce7" stroke="#16a34a" stroke-width="1"/>
<text x="420" y="402" fill="#334155" font-size="10">s05 New (todo_write + nag reminder)</text>
</svg>

After

Width:  |  Height:  |  Size: 5.6 KiB

View File

@@ -0,0 +1,93 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 420" font-family="system-ui, -apple-system, sans-serif">
<defs>
<marker id="arrow" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#555"/>
</marker>
<marker id="arrow-blue" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#2563eb"/>
</marker>
<marker id="arrow-green" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#16a34a"/>
</marker>
<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="420" fill="#fafbfc" rx="8"/>
<!-- タイトル -->
<rect x="0" y="0" width="800" height="48" fill="url(#header)" rx="8"/>
<rect x="0" y="40" width="800" height="8" fill="url(#header)"/>
<text x="400" y="31" fill="#fff" font-size="15" font-weight="700" text-anchor="middle">TodoWrite — ループ不変、ツール一つ追加で自動ディスパッチ</text>
<!-- ===== s04 保持(灰色) ===== -->
<text x="50" y="76" fill="#94a3b8" font-size="11" font-weight="600">s04 保持</text>
<!-- messages -->
<rect x="40" y="90" width="100" height="44" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
<text x="90" y="117" fill="#1e3a5f" font-size="12" font-weight="600" text-anchor="middle">messages[]</text>
<!-- → LLM -->
<line x1="140" y1="112" x2="188" y2="112" stroke="#2563eb" stroke-width="2" marker-end="url(#arrow-blue)"/>
<!-- LLM -->
<rect x="190" y="86" width="110" height="52" rx="8" fill="#fff" stroke="#2563eb" stroke-width="1.5"/>
<text x="245" y="108" fill="#1e3a5f" font-size="13" font-weight="700" text-anchor="middle">LLM</text>
<text x="245" y="126" fill="#64748b" font-size="9" text-anchor="middle">stop_reason=tool_use?</text>
<!-- No → 返却 -->
<line x1="245" y1="138" x2="245" y2="162" stroke="#16a34a" stroke-width="2" marker-end="url(#arrow)"/>
<text x="258" y="156" fill="#16a34a" font-size="9" font-weight="600">No</text>
<rect x="195" y="164" width="100" height="28" rx="14" fill="#dcfce7" stroke="#16a34a" stroke-width="1.5"/>
<text x="245" y="182" fill="#166534" font-size="11" font-weight="600" text-anchor="middle">結果を返す</text>
<!-- Yes → PreToolUse -->
<line x1="300" y1="112" x2="348" y2="112" stroke="#555" stroke-width="2" marker-end="url(#arrow)"/>
<text x="320" y="104" fill="#d97706" font-size="9" font-weight="600">Yes</text>
<!-- PreToolUse (s04) -->
<rect x="350" y="88" width="100" height="48" rx="8" fill="#f0fdf4" stroke="#16a34a" stroke-width="1.5"/>
<text x="400" y="110" fill="#166534" font-size="9" font-weight="600" text-anchor="middle">trigger_hooks</text>
<text x="400" y="124" fill="#166534" font-size="8" text-anchor="middle">PreToolUse</text>
<!-- → TOOL_HANDLERS -->
<line x1="450" y1="112" x2="498" y2="112" stroke="#555" stroke-width="2" marker-end="url(#arrow)"/>
<!-- ===== s05 新規todo_write ===== -->
<!-- TOOL_HANDLERS 枠拡大、todo_write を含む) -->
<rect x="500" y="74" width="120" height="140" rx="10" fill="#f0fdf4" stroke="#16a34a" stroke-width="2" stroke-dasharray="6,3"/>
<text x="560" y="94" fill="#166534" font-size="10" font-weight="700" text-anchor="middle">TOOL_HANDLERS</text>
<!-- s04 保持のツール -->
<rect x="512" y="102" width="96" height="22" rx="4" fill="#f0f4ff" stroke="#2563eb" stroke-width="1"/>
<text x="560" y="117" fill="#1e3a5f" font-size="9" text-anchor="middle">bash · read · write</text>
<rect x="512" y="130" width="96" height="22" rx="4" fill="#f0f4ff" stroke="#2563eb" stroke-width="1"/>
<text x="560" y="145" fill="#1e3a5f" font-size="9" text-anchor="middle">edit · glob</text>
<!-- s05 新規todo_write -->
<rect x="512" y="158" width="96" height="22" rx="4" fill="#dcfce7" stroke="#16a34a" stroke-width="1.5"/>
<text x="560" y="173" fill="#166534" font-size="9" font-weight="700" text-anchor="middle">todo_write</text>
<text x="560" y="232" fill="#16a34a" font-size="11" font-weight="600" text-anchor="middle">s05 新規</text>
<text x="560" y="196" fill="#64748b" font-size="8" text-anchor="middle">→ .tasks/current_todos.json</text>
<!-- ループバック -->
<path d="M 620 112 L 660 112 L 660 260 L 90 260 L 90 134" fill="none" stroke="#555" stroke-width="2" marker-end="url(#arrow)" stroke-dasharray="6,3"/>
<text x="370" y="280" fill="#64748b" font-size="10" text-anchor="middle">結果を messages[] に追加、ループ継続</text>
<!-- ===== Nag リマインダー ===== -->
<rect x="100" y="310" width="600" height="56" rx="8" fill="#fffbeb" stroke="#d97706" stroke-width="1"/>
<text x="120" y="332" fill="#92400e" font-size="11" font-weight="700">Nag リマインダー(催促機構)</text>
<text x="120" y="352" fill="#92400e" font-size="10">モデルが連続 3 ラウンド todo_write 未呼び出し → 自動注入 &lt;reminder&gt;Update your todos.&lt;/reminder&gt;</text>
<!-- 凡例 -->
<rect x="100" y="384" width="600" height="28" rx="6" fill="#f1f5f9"/>
<rect x="120" y="392" width="12" height="12" rx="2" fill="#f0f4ff" stroke="#2563eb" stroke-width="1"/>
<text x="140" y="402" fill="#334155" font-size="10">s04 保持ループ、フック、5 つの基本ツール)</text>
<rect x="400" y="392" width="12" height="12" rx="2" fill="#dcfce7" stroke="#16a34a" stroke-width="1"/>
<text x="420" y="402" fill="#334155" font-size="10">s05 新規todo_write + Nag リマインダー)</text>
</svg>

After

Width:  |  Height:  |  Size: 5.7 KiB

View File

@@ -0,0 +1,93 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 420" font-family="system-ui, -apple-system, sans-serif">
<defs>
<marker id="arrow" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#555"/>
</marker>
<marker id="arrow-blue" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#2563eb"/>
</marker>
<marker id="arrow-green" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#16a34a"/>
</marker>
<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="420" fill="#fafbfc" rx="8"/>
<!-- 标题 -->
<rect x="0" y="0" width="800" height="48" fill="url(#header)" rx="8"/>
<rect x="0" y="40" width="800" height="8" fill="url(#header)"/>
<text x="400" y="31" fill="#fff" font-size="16" font-weight="700" text-anchor="middle">TodoWrite — 循环不变,多一个工具自动分发</text>
<!-- ===== s04 保留(灰色) ===== -->
<text x="50" y="76" fill="#94a3b8" font-size="11" font-weight="600">s04 保留</text>
<!-- messages -->
<rect x="40" y="90" width="100" height="44" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
<text x="90" y="117" fill="#1e3a5f" font-size="12" font-weight="600" text-anchor="middle">messages[]</text>
<!-- → LLM -->
<line x1="140" y1="112" x2="188" y2="112" stroke="#2563eb" stroke-width="2" marker-end="url(#arrow-blue)"/>
<!-- LLM -->
<rect x="190" y="86" width="110" height="52" rx="8" fill="#fff" stroke="#2563eb" stroke-width="1.5"/>
<text x="245" y="108" fill="#1e3a5f" font-size="13" font-weight="700" text-anchor="middle">LLM</text>
<text x="245" y="126" fill="#64748b" font-size="9" text-anchor="middle">stop_reason=tool_use?</text>
<!-- 否 → 返回 -->
<line x1="245" y1="138" x2="245" y2="162" stroke="#16a34a" stroke-width="2" marker-end="url(#arrow)"/>
<text x="258" y="156" fill="#16a34a" font-size="9" font-weight="600"></text>
<rect x="195" y="164" width="100" height="28" rx="14" fill="#dcfce7" stroke="#16a34a" stroke-width="1.5"/>
<text x="245" y="182" fill="#166534" font-size="11" font-weight="600" text-anchor="middle">返回结果</text>
<!-- 是 → PreToolUse -->
<line x1="300" y1="112" x2="348" y2="112" stroke="#555" stroke-width="2" marker-end="url(#arrow)"/>
<text x="320" y="104" fill="#d97706" font-size="9" font-weight="600"></text>
<!-- PreToolUse (s04) -->
<rect x="350" y="88" width="100" height="48" rx="8" fill="#f0fdf4" stroke="#16a34a" stroke-width="1.5"/>
<text x="400" y="110" fill="#166534" font-size="9" font-weight="600" text-anchor="middle">trigger_hooks</text>
<text x="400" y="124" fill="#166534" font-size="8" text-anchor="middle">PreToolUse</text>
<!-- → TOOL_HANDLERS -->
<line x1="450" y1="112" x2="498" y2="112" stroke="#555" stroke-width="2" marker-end="url(#arrow)"/>
<!-- ===== s05 新增todo_write ===== -->
<!-- TOOL_HANDLERS 框(扩大,包含 todo_write -->
<rect x="500" y="74" width="120" height="140" rx="10" fill="#f0fdf4" stroke="#16a34a" stroke-width="2" stroke-dasharray="6,3"/>
<text x="560" y="94" fill="#166534" font-size="10" font-weight="700" text-anchor="middle">TOOL_HANDLERS</text>
<!-- s04 保留的工具 -->
<rect x="512" y="102" width="96" height="22" rx="4" fill="#f0f4ff" stroke="#2563eb" stroke-width="1"/>
<text x="560" y="117" fill="#1e3a5f" font-size="9" text-anchor="middle">bash · read · write</text>
<rect x="512" y="130" width="96" height="22" rx="4" fill="#f0f4ff" stroke="#2563eb" stroke-width="1"/>
<text x="560" y="145" fill="#1e3a5f" font-size="9" text-anchor="middle">edit · glob</text>
<!-- s05 新增todo_write -->
<rect x="512" y="158" width="96" height="22" rx="4" fill="#dcfce7" stroke="#16a34a" stroke-width="1.5"/>
<text x="560" y="173" fill="#166534" font-size="9" font-weight="700" text-anchor="middle">todo_write</text>
<text x="560" y="232" fill="#16a34a" font-size="11" font-weight="600" text-anchor="middle">s05 新增</text>
<text x="560" y="196" fill="#64748b" font-size="8" text-anchor="middle">→ .tasks/current_todos.json</text>
<!-- 回环 -->
<path d="M 620 112 L 660 112 L 660 260 L 90 260 L 90 134" fill="none" stroke="#555" stroke-width="2" marker-end="url(#arrow)" stroke-dasharray="6,3"/>
<text x="370" y="280" fill="#64748b" font-size="10" text-anchor="middle">结果追加到 messages[],循环继续</text>
<!-- ===== Nag Reminder ===== -->
<rect x="100" y="310" width="600" height="56" rx="8" fill="#fffbeb" stroke="#d97706" stroke-width="1"/>
<text x="120" y="332" fill="#92400e" font-size="11" font-weight="700">Nag Reminder催更机制</text>
<text x="120" y="352" fill="#92400e" font-size="10">模型连续 3 轮没调 todo_write → 自动注入 &lt;reminder&gt;Update your todos.&lt;/reminder&gt;</text>
<!-- 图例 -->
<rect x="100" y="384" width="600" height="28" rx="6" fill="#f1f5f9"/>
<rect x="120" y="392" width="12" height="12" rx="2" fill="#f0f4ff" stroke="#2563eb" stroke-width="1"/>
<text x="140" y="402" fill="#334155" font-size="10">s04 保留循环、钩子、5 个基础工具)</text>
<rect x="400" y="392" width="12" height="12" rx="2" fill="#dcfce7" stroke="#16a34a" stroke-width="1"/>
<text x="420" y="402" fill="#334155" font-size="10">s05 新增todo_write + nag reminder</text>
</svg>

After

Width:  |  Height:  |  Size: 5.6 KiB