mirror of
https://github.com/shareAI-lab/analysis_claude_code.git
synced 2026-06-20 20:23:36 +08:00
* 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:
282
s04_hooks/README.en.md
Normal file
282
s04_hooks/README.en.md
Normal file
@@ -0,0 +1,282 @@
|
||||
# s04: Hooks — Hang on the Loop, Don't Write into It
|
||||
|
||||
[中文](README.md) · [English](README.en.md) · [日本語](README.ja.md)
|
||||
|
||||
s01 → s02 → s03 → `s04` → [s05](../s05_todo_write/) → s06 → ... → s20
|
||||
|
||||
> *"Hang on the loop, don't write into it"* — Hooks inject extension logic before and after tool execution.
|
||||
>
|
||||
> **Harness Layer**: Hooks — Extension points that don't invade the loop.
|
||||
|
||||
---
|
||||
|
||||
## The Problem
|
||||
|
||||
The s03 Agent has permission checks. But every new check, "log every bash call", "auto git add after writes", requires modifying the `agent_loop` function.
|
||||
|
||||
The loop quickly becomes this:
|
||||
|
||||
```python
|
||||
def agent_loop(messages):
|
||||
while True:
|
||||
# ... LLM call ...
|
||||
for block in response.content:
|
||||
if block.type == "tool_use":
|
||||
log_to_file(block) # added a line
|
||||
check_permission(block) # added a line
|
||||
notify_slack(block) # added another line
|
||||
output = execute(block)
|
||||
auto_git_add(block) # yet another line
|
||||
# ... the loop is unrecognizable
|
||||
```
|
||||
|
||||
What you want to extend is the Agent's behavior, but what you're modifying is the loop itself. The loop should be a stable core; extensions should hang on the outside.
|
||||
|
||||
---
|
||||
|
||||
## The Solution
|
||||
|
||||

|
||||
|
||||
The s03 loop and permission logic are fully preserved. The only change is moving `check_permission()` from inside the loop body onto a hook. The loop no longer directly calls any check function. Instead it calls `trigger_hooks("PreToolUse", block)`, and the registry decides what to run.
|
||||
|
||||
Four events, covering a complete agent cycle:
|
||||
|
||||
| Event | Trigger Timing | Typical Use |
|
||||
|-------|---------------|-------------|
|
||||
| UserPromptSubmit | After user input, before entering LLM | Input validation, context injection |
|
||||
| PreToolUse | Before tool execution | Permission checks, logging |
|
||||
| PostToolUse | After tool execution | Side effects (auto git add etc.), output checking |
|
||||
| Stop | When the loop is about to exit | Cleanup (CC also supports force continuation) |
|
||||
|
||||
Extensions are added via `register_hook()`. The loop only calls `trigger_hooks()`.
|
||||
|
||||
---
|
||||
|
||||
## How It Works
|
||||
|
||||
**Hook registry**: a dict mapping event names to callback lists.
|
||||
|
||||
```python
|
||||
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 value ≠ None → hook says "stop"
|
||||
return result
|
||||
return None
|
||||
```
|
||||
|
||||
In the teaching version, PreToolUse returning non-None means block execution; Stop returning non-None means force continuation. UserPromptSubmit and PostToolUse return values are unused.
|
||||
|
||||
**UserPromptSubmit**, triggers after user input, before entering the LLM. CC can intercept or modify input; the teaching version only logs:
|
||||
|
||||
```python
|
||||
def context_inject_hook(query: str) -> str | None:
|
||||
"""Inject current working directory info into every prompt."""
|
||||
print(f"\033[90m[HOOK] UserPromptSubmit: working in {WORKDIR}\033[0m")
|
||||
return None # return None = no modification, let prompt through
|
||||
|
||||
register_hook("UserPromptSubmit", context_inject_hook)
|
||||
```
|
||||
|
||||
In the main loop, triggered right after user input:
|
||||
|
||||
```python
|
||||
query = input("s04 >> ")
|
||||
trigger_hooks("UserPromptSubmit", query) # ← before entering LLM
|
||||
history.append({"role": "user", "content": query})
|
||||
agent_loop(history)
|
||||
```
|
||||
|
||||
**PreToolUse / PostToolUse**, hooks before and after tool execution. s03's permission check logic is now wrapped as a PreToolUse hook, plus a logging hook and a large-output reminder:
|
||||
|
||||
```python
|
||||
# PreToolUse: permission check (s03 logic, moved from loop to hook)
|
||||
def permission_hook(block):
|
||||
if block.name == "bash":
|
||||
for pattern in DENY_LIST:
|
||||
if pattern in block.input.get("command", ""):
|
||||
return "Permission denied by deny list"
|
||||
if block.name in ("write_file", "edit_file"):
|
||||
path = block.input.get("path", "")
|
||||
if not (WORKDIR / path).resolve().is_relative_to(WORKDIR):
|
||||
choice = input(" Allow? [y/N] ").strip().lower()
|
||||
if choice not in ("y", "yes"):
|
||||
return "Permission denied by user"
|
||||
return None
|
||||
|
||||
# PreToolUse: logging
|
||||
def log_hook(block):
|
||||
print(f"[HOOK] {block.name}(...)")
|
||||
|
||||
# PostToolUse: large output reminder
|
||||
def large_output_hook(block, output):
|
||||
if len(str(output)) > 100000:
|
||||
print(f"[HOOK] ⚠ Large output from {block.name}")
|
||||
|
||||
register_hook("PreToolUse", permission_hook)
|
||||
register_hook("PreToolUse", log_hook)
|
||||
register_hook("PostToolUse", large_output_hook)
|
||||
```
|
||||
|
||||
**Stop**, triggers when the loop is about to exit (`stop_reason != "tool_use"`). The teaching version prints a cleanup summary:
|
||||
|
||||
```python
|
||||
def summary_hook(messages: list) -> str | None:
|
||||
"""Print a summary when the loop is about to stop."""
|
||||
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 # return None = allow stop, return string = force continuation
|
||||
|
||||
register_hook("Stop", summary_hook)
|
||||
```
|
||||
|
||||
In agent_loop, triggered before exit:
|
||||
|
||||
```python
|
||||
if response.stop_reason != "tool_use":
|
||||
force = trigger_hooks("Stop", messages) # ← before exiting
|
||||
if force:
|
||||
# hook returned a message → inject it and continue
|
||||
messages.append({"role": "user", "content": force})
|
||||
continue
|
||||
return
|
||||
```
|
||||
|
||||
**Only one change in the loop**: s03 directly called `check_permission(block)`, s04 replaces it with `trigger_hooks("PreToolUse", block)`:
|
||||
|
||||
```python
|
||||
for block in response.content:
|
||||
if block.type != "tool_use":
|
||||
continue
|
||||
|
||||
# s03: if not check_permission(block): ...
|
||||
# s04: hooks replace hardcoding
|
||||
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)
|
||||
|
||||
results.append({"type": "tool_result", "tool_use_id": block.id,
|
||||
"content": output})
|
||||
```
|
||||
|
||||
Four hooks cover the critical nodes of the agent cycle: input → before execution → after execution → exit. The loop only calls trigger_hooks(); all logic lives in hook callbacks.
|
||||
|
||||
---
|
||||
|
||||
## Changes from s03
|
||||
|
||||
| Component | Before (s03) | After (s04) |
|
||||
|-----------|-------------|-------------|
|
||||
| Extension method | check_permission() hardcoded in the loop | HOOKS registry + trigger_hooks() |
|
||||
| New functions | — | register_hook, trigger_hooks |
|
||||
| Hook callbacks | — | context_inject_hook, permission_hook, log_hook, large_output_hook, summary_hook |
|
||||
| Loop | Directly calls check_permission() | Calls trigger_hooks("PreToolUse", ...) |
|
||||
| Exit control | None | trigger_hooks("Stop", ...) can prevent exit |
|
||||
| Input interception | None | trigger_hooks("UserPromptSubmit", ...) can inject context |
|
||||
|
||||
---
|
||||
|
||||
## Try It
|
||||
|
||||
```sh
|
||||
cd learn-claude-code
|
||||
python s04_hooks/code.py
|
||||
```
|
||||
|
||||
Try these prompts:
|
||||
|
||||
1. `Read the file README.md` (should pass directly, observe hook logs)
|
||||
2. `Create a file called test.txt` (after creation, observe if PostToolUse fires)
|
||||
3. `Delete all temporary files in /tmp` (bash + rm triggers permission hook)
|
||||
|
||||
What to watch for: Before each tool execution, does the `[HOOK]` log appear? When permission is denied, was it intercepted by a hook or hardcoded in the loop?
|
||||
|
||||
---
|
||||
|
||||
## What's Next
|
||||
|
||||
The Agent can now safely execute operations. But does it ever stop to think "what should I do first, and what next?" Given a complex task, does it jump straight in, or plan first?
|
||||
|
||||
→ s05 TodoWrite: Give the Agent a planning tool. Make a list first, then execute.
|
||||
|
||||
<details>
|
||||
<summary>Dive into CC Source Code</summary>
|
||||
|
||||
> The following is based on a complete analysis of CC source code `toolHooks.ts` (650 lines), `hooks.ts`, `stopHooks.ts`, and `coreTypes.ts`.
|
||||
|
||||
### 1. Hook Events: Not Just 4, but 27
|
||||
|
||||
The teaching version covers only PreToolUse and PostToolUse. CC actually has 27 hook events (`coreTypes.ts:25-53`):
|
||||
|
||||
| Category | Events |
|
||||
|----------|--------|
|
||||
| Tool-related | `PreToolUse`, `PostToolUse`, `PostToolUseFailure` |
|
||||
| Session-related | `SessionStart`, `SessionEnd`, `Stop`, `StopFailure`, `Setup` |
|
||||
| User interaction | `UserPromptSubmit`, `Notification`, `PermissionRequest`, `PermissionDenied` |
|
||||
| Sub-agents | `SubagentStart`, `SubagentStop` |
|
||||
| Compaction-related | `PreCompact`, `PostCompact` |
|
||||
| Team-related | `TeammateIdle`, `TaskCreated`, `TaskCompleted` |
|
||||
| Other | `Elicitation`, `ElicitationResult`, `ConfigChange`, `WorktreeCreate`, `WorktreeRemove`, `InstructionsLoaded`, `CwdChanged`, `FileChanged` |
|
||||
|
||||
The teaching version covers only 4 core events (UserPromptSubmit, PreToolUse, PostToolUse, Stop) because they cover every critical node of a complete agent cycle. The other 23 follow the same pattern.
|
||||
|
||||
### 2. HookResult Common Fields
|
||||
|
||||
CC's `HookResult` (`types/hooks.ts:260-275`) has 14 fields. Common ones:
|
||||
|
||||
| Field | Type | Purpose |
|
||||
|-------|------|---------|
|
||||
| `message` | Message | Optional UI message |
|
||||
| `blockingError` | HookBlockingError | Blocking error → injected into conversation for model self-correction |
|
||||
| `outcome` | success/blocking/non_blocking_error/cancelled | Execution result |
|
||||
| `preventContinuation` | boolean | Prevent subsequent execution |
|
||||
| `stopReason` | string | Stop reason description |
|
||||
| `permissionBehavior` | allow/deny/ask/passthrough | Hook returns permission decision |
|
||||
| `updatedInput` | Record | Modify tool input |
|
||||
| `additionalContext` | string | Additional context |
|
||||
| `updatedMCPToolOutput` | unknown | MCP tool output modification |
|
||||
|
||||
### 3. Key Invariant: Hook 'allow' Cannot Bypass deny/ask Rules
|
||||
|
||||
This is the most important security design in CC's permission system (`toolHooks.ts:325-331`): **when a hook returns allow, it still checks settings.json deny/ask rules.** Even if the user's hook script says "allow", if the tool is disabled in settings.json, the operation is still blocked.
|
||||
|
||||
The teaching version doesn't have this layer; hooks returning non-None directly interrupt. This is sufficient for teaching, but would create a security vulnerability in production.
|
||||
|
||||
### 4. stopHookActive Mechanism
|
||||
|
||||
CC's Stop hooks have an infinite-loop prevention mechanism (`query.ts:212,1300`): the `stopHookActive` state field. When stop hooks produce a blockingError, the loop re-enters with `stopHookActive: true`. Subsequent iterations see this flag and don't trigger stop hooks again. This prevents a never-stopping bug: model self-corrects → stop hook errors again → model self-corrects again → stop hook errors again...
|
||||
|
||||
### 5. hook_stopped_continuation
|
||||
|
||||
When PostToolUse hooks return `preventContinuation: true`, a `hook_stopped_continuation` attachment is produced (`toolHooks.ts:117-130`). query.ts (L1388-1393) detects it and sets `shouldPreventContinuation = true`, causing the loop to exit. This is the mechanism for "hooks gracefully shut down the Agent" — not a crash, but a completion.
|
||||
|
||||
### Teaching Version Simplifications Are Intentional
|
||||
|
||||
- 27 events → 4 (UserPromptSubmit/PreToolUse/PostToolUse/Stop): covers agent cycle critical nodes
|
||||
- 14 fields → simple return values (None = continue, non-None = interrupt/continue): minimal cognitive load
|
||||
- Hook allow vs deny/ask invariant → omitted: teaching version has no settings.json layer
|
||||
- stopHookActive → omitted: teaching version Stop hook only does simple continuation, no infinite-loop prevention needed
|
||||
|
||||
</details>
|
||||
|
||||
<!-- translation-sync: zh@v1, en@v1, ja@v1 -->
|
||||
282
s04_hooks/README.ja.md
Normal file
282
s04_hooks/README.ja.md
Normal file
@@ -0,0 +1,282 @@
|
||||
# s04: Hooks — ループに掛ける、ループには書き込まない
|
||||
|
||||
[中文](README.md) · [English](README.en.md) · [日本語](README.ja.md)
|
||||
|
||||
s01 → s02 → s03 → `s04` → [s05](../s05_todo_write/) → s06 → ... → s20
|
||||
|
||||
> *"ループに掛ける、ループには書き込まない"* — フックがツール実行の前後に拡張ロジックを注入する。
|
||||
>
|
||||
> **Harness レイヤー**: フック — ループを侵襲しない拡張ポイント。
|
||||
|
||||
---
|
||||
|
||||
## 課題
|
||||
|
||||
s03 の Agent には権限チェックがある。しかし新しいチェックを追加するたび、「bash 呼び出しを毎回ログに記録」「操作後に自動 git add」、`agent_loop` 関数を修正する必要がある。
|
||||
|
||||
ループはすぐにこうなる:
|
||||
|
||||
```python
|
||||
def agent_loop(messages):
|
||||
while True:
|
||||
# ... LLM call ...
|
||||
for block in response.content:
|
||||
if block.type == "tool_use":
|
||||
log_to_file(block) # 一行追加
|
||||
check_permission(block) # 一行追加
|
||||
notify_slack(block) # さらに一行追加
|
||||
output = execute(block)
|
||||
auto_git_add(block) # さらに一行追加
|
||||
# ... もうループが見えない
|
||||
```
|
||||
|
||||
拡張したいのは Agent の振る舞いなのに、変更しているのはループそのもの。ループは安定した核心であるべき。拡張は外側に掛ける。
|
||||
|
||||
---
|
||||
|
||||
## ソリューション
|
||||
|
||||

|
||||
|
||||
s03 のループと権限ロジックは完全に保持される。唯一の変更点は `check_permission()` をループ本体内からフックに移動したこと。ループはもうチェック関数を直接呼び出さず、代わりに `trigger_hooks("PreToolUse", block)` を呼び、登録済みのフックが何を実行するかを決める。
|
||||
|
||||
4 つのイベントで、完全な agent cycle をカバー:
|
||||
|
||||
| イベント | 発火タイミング | 典型的な用途 |
|
||||
|----------|--------------|-------------|
|
||||
| UserPromptSubmit | ユーザー入力後、LLM に入る前 | 入力バリデーション、コンテキスト注入 |
|
||||
| PreToolUse | ツール実行前 | 権限チェック、ログ記録 |
|
||||
| PostToolUse | ツール実行後 | 副作用(自動 git add など)、出力チェック |
|
||||
| Stop | ループが終了する直前 | クリーンアップ(CC は強制続行もサポート) |
|
||||
|
||||
拡張は `register_hook()` で追加する。ループは `trigger_hooks()` を呼ぶだけ。
|
||||
|
||||
---
|
||||
|
||||
## 仕組み
|
||||
|
||||
**フック登録簿**:イベント名をコールバックリストにマッピングする辞書。
|
||||
|
||||
```python
|
||||
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: # 戻り値 ≠ None → フックが「止め」と指示
|
||||
return result
|
||||
return None
|
||||
```
|
||||
|
||||
教学版では、PreToolUse の非 None 戻り値は実行阻止を意味し、Stop の非 None 戻り値は強制続行を意味する。UserPromptSubmit と PostToolUse の戻り値は未使用。
|
||||
|
||||
**UserPromptSubmit**、ユーザー入力後、LLM に入る前に発火。CC では入力の横取りや変更が可能、教学版はログ出力のみ:
|
||||
|
||||
```python
|
||||
def context_inject_hook(query: str) -> str | None:
|
||||
"""Inject current working directory info into every prompt."""
|
||||
print(f"\033[90m[HOOK] UserPromptSubmit: working in {WORKDIR}\033[0m")
|
||||
return None # return None = 変更なし、プロンプトを通す
|
||||
|
||||
register_hook("UserPromptSubmit", context_inject_hook)
|
||||
```
|
||||
|
||||
メインループでは、ユーザー入力直後に発火:
|
||||
|
||||
```python
|
||||
query = input("s04 >> ")
|
||||
trigger_hooks("UserPromptSubmit", query) # ← LLM に入る前
|
||||
history.append({"role": "user", "content": query})
|
||||
agent_loop(history)
|
||||
```
|
||||
|
||||
**PreToolUse / PostToolUse**、ツール実行の前後のフック。s03 の権限チェックロジックは PreToolUse フックに包まれ、さらにログフックと大出力リマインダーが追加される:
|
||||
|
||||
```python
|
||||
# PreToolUse: 権限チェック(s03 のロジック、ループからフックに移動)
|
||||
def permission_hook(block):
|
||||
if block.name == "bash":
|
||||
for pattern in DENY_LIST:
|
||||
if pattern in block.input.get("command", ""):
|
||||
return "Permission denied by deny list"
|
||||
if block.name in ("write_file", "edit_file"):
|
||||
path = block.input.get("path", "")
|
||||
if not (WORKDIR / path).resolve().is_relative_to(WORKDIR):
|
||||
choice = input(" Allow? [y/N] ").strip().lower()
|
||||
if choice not in ("y", "yes"):
|
||||
return "Permission denied by user"
|
||||
return None
|
||||
|
||||
# PreToolUse: ログ
|
||||
def log_hook(block):
|
||||
print(f"[HOOK] {block.name}(...)")
|
||||
|
||||
# PostToolUse: 大ファイルリマインダー
|
||||
def large_output_hook(block, output):
|
||||
if len(str(output)) > 100000:
|
||||
print(f"[HOOK] ⚠ Large output from {block.name}")
|
||||
|
||||
register_hook("PreToolUse", permission_hook)
|
||||
register_hook("PreToolUse", log_hook)
|
||||
register_hook("PostToolUse", large_output_hook)
|
||||
```
|
||||
|
||||
**Stop**、ループが終了する直前に発火(`stop_reason != "tool_use"`)。教学版ではクリーンアップ統計を印刷:
|
||||
|
||||
```python
|
||||
def summary_hook(messages: list) -> str | None:
|
||||
"""Print a summary when the loop is about to stop."""
|
||||
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 # return None = 終了を許可、return 文字列 = 強制続行
|
||||
|
||||
register_hook("Stop", summary_hook)
|
||||
```
|
||||
|
||||
agent_loop 内では、終了前に発火:
|
||||
|
||||
```python
|
||||
if response.stop_reason != "tool_use":
|
||||
force = trigger_hooks("Stop", messages) # ← 終了する前に
|
||||
if force:
|
||||
# フックがメッセージを返した → 注入して続行
|
||||
messages.append({"role": "user", "content": force})
|
||||
continue
|
||||
return
|
||||
```
|
||||
|
||||
**ループ内で変更されたのは一箇所だけ**:s03 は直接 `check_permission(block)` を呼び出していたが、s04 は `trigger_hooks("PreToolUse", block)` に置き換えた:
|
||||
|
||||
```python
|
||||
for block in response.content:
|
||||
if block.type != "tool_use":
|
||||
continue
|
||||
|
||||
# s03: if not check_permission(block): ...
|
||||
# s04: フックがハードコードを代替
|
||||
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)
|
||||
|
||||
results.append({"type": "tool_result", "tool_use_id": block.id,
|
||||
"content": output})
|
||||
```
|
||||
|
||||
4 つのフックが agent cycle の重要ノードをカバー:入力→実行前→実行後→終了。ループは trigger_hooks() を呼ぶだけで、具体的なロジックは全てフックコールバックにある。
|
||||
|
||||
---
|
||||
|
||||
## s03 からの変更
|
||||
|
||||
| コンポーネント | 変更前 (s03) | 変更後 (s04) |
|
||||
|--------------|-------------|-------------|
|
||||
| 拡張方式 | check_permission() をループ内にハードコード | HOOKS 登録簿 + trigger_hooks() |
|
||||
| 新規関数 | — | register_hook, trigger_hooks |
|
||||
| フックコールバック | — | context_inject_hook, permission_hook, log_hook, large_output_hook, summary_hook |
|
||||
| ループ | check_permission() を直接呼び出し | trigger_hooks("PreToolUse", ...) を呼び出し |
|
||||
| 終了制御 | なし | trigger_hooks("Stop", ...) が終了を阻止可能 |
|
||||
| 入力横取り | なし | trigger_hooks("UserPromptSubmit", ...) がコンテキスト注入可能 |
|
||||
|
||||
---
|
||||
|
||||
## 試してみよう
|
||||
|
||||
```sh
|
||||
cd learn-claude-code
|
||||
python s04_hooks/code.py
|
||||
```
|
||||
|
||||
以下のプロンプトを試してみよう:
|
||||
|
||||
1. `Read the file README.md`(そのまま通過するはず、フックログを観察)
|
||||
2. `Create a file called test.txt`(作成後、PostToolUse が発火するか観察)
|
||||
3. `Delete all temporary files in /tmp`(bash + rm で権限フックが発動)
|
||||
|
||||
観察のポイント:各ツール実行前に `[HOOK]` ログが表示されるか? 権限が拒否されたとき、フックが拦截したのか、ループ内のハードコードが拦截したのか?
|
||||
|
||||
---
|
||||
|
||||
## 次へ
|
||||
|
||||
Agent は安全に操作を実行できるようになった。しかし「まず何をして、次に何をすべきか」を立ち止まって考えたことはあるか? 複雑なタスクを与えたとき、すぐに取り掛かるのか、まず計画を立てるのか?
|
||||
|
||||
→ s05 TodoWrite:Agent に計画ツールを与える。まずリストを作り、それから実行。
|
||||
|
||||
<details>
|
||||
<summary>CC ソースコードを深掘り</summary>
|
||||
|
||||
> 以下は CC ソースコード `toolHooks.ts`(650 行)、`hooks.ts`、`stopHooks.ts`、`coreTypes.ts` の完全分析に基づく。
|
||||
|
||||
### 一、Hook イベント:4 つではなく 27 個
|
||||
|
||||
教育版は PreToolUse と PostToolUse のみを取り上げる。CC には実際に 27 のフックイベントがある(`coreTypes.ts:25-53`):
|
||||
|
||||
| カテゴリ | イベント |
|
||||
|----------|---------|
|
||||
| ツール関連 | `PreToolUse`, `PostToolUse`, `PostToolUseFailure` |
|
||||
| セッション関連 | `SessionStart`, `SessionEnd`, `Stop`, `StopFailure`, `Setup` |
|
||||
| ユーザー対話 | `UserPromptSubmit`, `Notification`, `PermissionRequest`, `PermissionDenied` |
|
||||
| サブエージェント | `SubagentStart`, `SubagentStop` |
|
||||
| 圧縮関連 | `PreCompact`, `PostCompact` |
|
||||
| チーム関連 | `TeammateIdle`, `TaskCreated`, `TaskCompleted` |
|
||||
| その他 | `Elicitation`, `ElicitationResult`, `ConfigChange`, `WorktreeCreate`, `WorktreeRemove`, `InstructionsLoaded`, `CwdChanged`, `FileChanged` |
|
||||
|
||||
教育版は 4 つのコアイベント(UserPromptSubmit、PreToolUse、PostToolUse、Stop)のみを取り上げる。これらで agent cycle の重要ノードを全てカバーできる。残り 23 個は同じパターン。
|
||||
|
||||
### 二、HookResult よく使うフィールド抜粋
|
||||
|
||||
CC の `HookResult`(`types/hooks.ts:260-275`)には 14 のフィールドがある。よく使うもの:
|
||||
|
||||
| フィールド | 型 | 用途 |
|
||||
|-----------|-----|------|
|
||||
| `message` | Message | オプションの UI メッセージ |
|
||||
| `blockingError` | HookBlockingError | ブロッキングエラー → 会話に注入してモデルが自己修正 |
|
||||
| `outcome` | success/blocking/non_blocking_error/cancelled | 実行結果 |
|
||||
| `preventContinuation` | boolean | 後続実行を阻止 |
|
||||
| `stopReason` | string | 停止理由の説明 |
|
||||
| `permissionBehavior` | allow/deny/ask/passthrough | フックが権限決定を返す |
|
||||
| `updatedInput` | Record | ツール入力の変更 |
|
||||
| `additionalContext` | string | 追加コンテキスト |
|
||||
| `updatedMCPToolOutput` | unknown | MCP ツール出力の変更 |
|
||||
|
||||
### 三、重要な不変条件:Hook 'allow' は deny/ask ルールをバイパスできない
|
||||
|
||||
これは CC 権限システムで最も重要なセキュリティ設計(`toolHooks.ts:325-331`):**フックが allow を返しても、settings.json の deny/ask ルールをチェックする。** ユーザーのフックスクリプトが「許可」と言っても、settings.json でそのツールが無効になっていれば、操作は阻止される。
|
||||
|
||||
教育版にはこの階層がない。フックが非 None を返せば直接中断。教育目的では十分だが、本番環境ではセキュリティホールになる。
|
||||
|
||||
### 四、stopHookActive 機構
|
||||
|
||||
CC の Stop フックには無限ループ防止機構がある(`query.ts:212,1300`):`stopHookActive` 状態フィールド。Stop フックが blockingError を発生させると、ループは `stopHookActive: true` で次のラウンドに再入する。後続のイテレーションではこのフラグを見て Stop フックを再トリガーしない。これで「永久に止まらない」バグを防ぐ:モデルが自己修正 → Stop フックが再度エラー → モデルが再修正 → Stop フックが再度エラー... を防止。
|
||||
|
||||
### 五、hook_stopped_continuation
|
||||
|
||||
PostToolUse フックが `preventContinuation: true` を返すと、`hook_stopped_continuation` アタッチメントが生成される(`toolHooks.ts:117-130`)。query.ts(L1388-1393)はそれを検出して `shouldPreventContinuation = true` を設定し、ループが終了する。これは「フックが Agent を優雅に停止させる」機構 — クラッシュではなく、完了。
|
||||
|
||||
### 教育版の簡略化は意図的
|
||||
|
||||
- 27 イベント → 4(UserPromptSubmit/PreToolUse/PostToolUse/Stop):agent cycle の重要ノードをカバー
|
||||
- 14 フィールド → 単純な戻り値(None = 続行、非 None = 中断/続行):認知負荷を最小限に
|
||||
- Hook allow vs deny/ask の不変条件 → 省略:教育版に settings.json 層はない
|
||||
- stopHookActive → 省略:教育版の Stop フックは単純な続行のみ、無限ループ防止は不要
|
||||
|
||||
</details>
|
||||
|
||||
<!-- translation-sync: zh@v1, en@v1, ja@v1 -->
|
||||
282
s04_hooks/README.md
Normal file
282
s04_hooks/README.md
Normal file
@@ -0,0 +1,282 @@
|
||||
# s04: Hooks — 挂在循环上,不写进循环里
|
||||
|
||||
[中文](README.md) · [English](README.en.md) · [日本語](README.ja.md)
|
||||
|
||||
s01 → s02 → s03 → `s04` → [s05](../s05_todo_write/) → s06 → ... → s20
|
||||
|
||||
> *"挂在循环上, 不写进循环里"* — hook 在工具执行前后注入扩展逻辑。
|
||||
>
|
||||
> **Harness 层**: hook — 扩展点不侵入循环。
|
||||
|
||||
---
|
||||
|
||||
## 问题
|
||||
|
||||
s03 的 Agent 有权限检查了。但每次加一个新检查,比如"记录每次 bash 调用"、"操作后自动 git add",都要修改 `agent_loop` 函数。
|
||||
|
||||
循环很快就变成了这样:
|
||||
|
||||
```python
|
||||
def agent_loop(messages):
|
||||
while True:
|
||||
# ... LLM call ...
|
||||
for block in response.content:
|
||||
if block.type == "tool_use":
|
||||
log_to_file(block) # 加一行
|
||||
check_permission(block) # 加一行
|
||||
notify_slack(block) # 又加一行
|
||||
output = execute(block)
|
||||
auto_git_add(block) # 再加一行
|
||||
# ... 很快循环就认不出来了
|
||||
```
|
||||
|
||||
你想扩展的是 Agent 的行为,但你改的却是循环本身。循环应该是一个稳定的核心,扩展应该挂在外面。
|
||||
|
||||
---
|
||||
|
||||
## 解决方案
|
||||
|
||||

|
||||
|
||||
s03 的循环和权限逻辑完全保留。唯一的变动是把 `check_permission()` 从循环体内移到了 hook 上,循环不再直接调用任何检查函数,改为 `trigger_hooks("PreToolUse", block)`,由注册表决定跑什么。
|
||||
|
||||
四个事件,覆盖一个完整的 agent cycle:
|
||||
|
||||
| 事件 | 触发时机 | 典型用途 |
|
||||
|------|---------|---------|
|
||||
| UserPromptSubmit | 用户输入提交后、进入 LLM 前 | 输入验证、注入上下文 |
|
||||
| PreToolUse | 工具执行前 | 权限检查、日志记录 |
|
||||
| PostToolUse | 工具执行后 | 副作用(自动 git add 等)、输出检查 |
|
||||
| Stop | 循环即将退出时 | 收尾清理(CC 还支持强制续跑) |
|
||||
|
||||
扩展通过 `register_hook()` 添加,循环只调用 `trigger_hooks()`。
|
||||
|
||||
---
|
||||
|
||||
## 工作原理
|
||||
|
||||
**hook 注册表**:一个字典,事件名映射到回调列表。
|
||||
|
||||
```python
|
||||
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: # 返回值 ≠ None → hook 说"停"
|
||||
return result
|
||||
return None
|
||||
```
|
||||
|
||||
教学版中,PreToolUse 的非 None 返回值会阻止本次工具执行,Stop 的非 None 返回值会强制续跑。UserPromptSubmit 和 PostToolUse 的返回值未被使用。
|
||||
|
||||
**UserPromptSubmit**,用户输入提交后、进入 LLM 前触发。CC 中可以拦截或修改输入,教学版只做日志演示:
|
||||
|
||||
```python
|
||||
def context_inject_hook(query: str) -> str | None:
|
||||
"""Inject current working directory info into every prompt."""
|
||||
print(f"\033[90m[HOOK] UserPromptSubmit: working in {WORKDIR}\033[0m")
|
||||
return None # return None = no modification, let prompt through
|
||||
|
||||
register_hook("UserPromptSubmit", context_inject_hook)
|
||||
```
|
||||
|
||||
在主循环中,用户输入后立即触发:
|
||||
|
||||
```python
|
||||
query = input("s04 >> ")
|
||||
trigger_hooks("UserPromptSubmit", query) # ← 进入 LLM 之前
|
||||
history.append({"role": "user", "content": query})
|
||||
agent_loop(history)
|
||||
```
|
||||
|
||||
**PreToolUse / PostToolUse**,工具执行前后的 hook。s03 的权限检查逻辑现在包装成 PreToolUse hook,再加一个日志 hook 和一个大输出提醒:
|
||||
|
||||
```python
|
||||
# PreToolUse: 权限检查(s03 的逻辑,从循环移到 hook)
|
||||
def permission_hook(block):
|
||||
if block.name == "bash":
|
||||
for pattern in DENY_LIST:
|
||||
if pattern in block.input.get("command", ""):
|
||||
return "Permission denied by deny list"
|
||||
if block.name in ("write_file", "edit_file"):
|
||||
path = block.input.get("path", "")
|
||||
if not (WORKDIR / path).resolve().is_relative_to(WORKDIR):
|
||||
choice = input(" Allow? [y/N] ").strip().lower()
|
||||
if choice not in ("y", "yes"):
|
||||
return "Permission denied by user"
|
||||
return None
|
||||
|
||||
# PreToolUse: 日志
|
||||
def log_hook(block):
|
||||
print(f"[HOOK] {block.name}(...)")
|
||||
|
||||
# PostToolUse: 大文件提醒
|
||||
def large_output_hook(block, output):
|
||||
if len(str(output)) > 100000:
|
||||
print(f"[HOOK] ⚠ Large output from {block.name}")
|
||||
|
||||
register_hook("PreToolUse", permission_hook)
|
||||
register_hook("PreToolUse", log_hook)
|
||||
register_hook("PostToolUse", large_output_hook)
|
||||
```
|
||||
|
||||
**Stop**,循环即将退出时触发(`stop_reason != "tool_use"`)。教学版用于打印收尾统计:
|
||||
|
||||
```python
|
||||
def summary_hook(messages: list) -> str | None:
|
||||
"""Print a summary when the loop is about to stop."""
|
||||
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 # return None = allow stop, return string = force continuation
|
||||
|
||||
register_hook("Stop", summary_hook)
|
||||
```
|
||||
|
||||
在 agent_loop 中,退出前触发:
|
||||
|
||||
```python
|
||||
if response.stop_reason != "tool_use":
|
||||
force = trigger_hooks("Stop", messages) # ← 退出之前
|
||||
if force:
|
||||
# hook returned a message → inject it and continue
|
||||
messages.append({"role": "user", "content": force})
|
||||
continue
|
||||
return
|
||||
```
|
||||
|
||||
**循环里只改了一处**:s03 直接调用 `check_permission(block)`,s04 改为 `trigger_hooks("PreToolUse", block)`:
|
||||
|
||||
```python
|
||||
for block in response.content:
|
||||
if block.type != "tool_use":
|
||||
continue
|
||||
|
||||
# s03: if not check_permission(block): ...
|
||||
# s04: hook 替代硬编码
|
||||
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)
|
||||
|
||||
results.append({"type": "tool_result", "tool_use_id": block.id,
|
||||
"content": output})
|
||||
```
|
||||
|
||||
四个 hook 覆盖了 agent cycle 的关键节点:输入→执行前→执行后→退出。循环只负责调用 trigger_hooks(),具体逻辑全在 hook 回调里。
|
||||
|
||||
---
|
||||
|
||||
## 相对 s03 的变更
|
||||
|
||||
| 组件 | 之前 (s03) | 之后 (s04) |
|
||||
|------|-----------|-----------|
|
||||
| 扩展方式 | check_permission() 硬编码在循环里 | HOOKS 注册表 + trigger_hooks() |
|
||||
| 新函数 | — | register_hook, trigger_hooks |
|
||||
| hook 回调 | — | context_inject_hook, permission_hook, log_hook, large_output_hook, summary_hook |
|
||||
| 循环 | 直接调用 check_permission() | 调用 trigger_hooks("PreToolUse", ...) |
|
||||
| 退出控制 | 无 | trigger_hooks("Stop", ...) 可阻止退出 |
|
||||
| 输入拦截 | 无 | trigger_hooks("UserPromptSubmit", ...) 可注入上下文 |
|
||||
|
||||
---
|
||||
|
||||
## 试一下
|
||||
|
||||
```sh
|
||||
cd learn-claude-code
|
||||
python s04_hooks/code.py
|
||||
```
|
||||
|
||||
试试这些 prompt:
|
||||
|
||||
1. `Read the file README.md`(应该直接通过,观察 hook 日志)
|
||||
2. `Create a file called test.txt`(通过后观察 PostToolUse 是否触发)
|
||||
3. `Delete all temporary files in /tmp`(bash + rm 触发权限 hook)
|
||||
|
||||
观察重点:每次工具执行前,是否出现了 `[HOOK]` 日志?权限被拒时,是 hook 拦截的还是循环里硬编码的?
|
||||
|
||||
---
|
||||
|
||||
## 接下来
|
||||
|
||||
Agent 现在能安全执行操作了。但它有没有停下来想过"我应该先做什么,再做什么"?给它一个复杂任务,它是一上来就动手,还是先列个计划?
|
||||
|
||||
s05 TodoWrite → 给 Agent 一个计划工具。先列清单,再做。
|
||||
|
||||
<details>
|
||||
<summary>深入 CC 源码</summary>
|
||||
|
||||
> 以下基于 CC 源码 `toolHooks.ts`(650 行)、`hooks.ts`、`stopHooks.ts`、`coreTypes.ts` 的完整分析。
|
||||
|
||||
### 一、Hook 事件:不止这 4 个,而是 27 个
|
||||
|
||||
教学版只讲了 PreToolUse 和 PostToolUse。CC 实际有 27 个 hook 事件(`coreTypes.ts:25-53`):
|
||||
|
||||
| 类别 | 事件 |
|
||||
|------|------|
|
||||
| 工具相关 | `PreToolUse`, `PostToolUse`, `PostToolUseFailure` |
|
||||
| 会话相关 | `SessionStart`, `SessionEnd`, `Stop`, `StopFailure`, `Setup` |
|
||||
| 用户交互 | `UserPromptSubmit`, `Notification`, `PermissionRequest`, `PermissionDenied` |
|
||||
| 子 Agent | `SubagentStart`, `SubagentStop` |
|
||||
| 压缩相关 | `PreCompact`, `PostCompact` |
|
||||
| 团队相关 | `TeammateIdle`, `TaskCreated`, `TaskCompleted` |
|
||||
| 其他 | `Elicitation`, `ElicitationResult`, `ConfigChange`, `WorktreeCreate`, `WorktreeRemove`, `InstructionsLoaded`, `CwdChanged`, `FileChanged` |
|
||||
|
||||
教学版只讲 4 个核心事件(UserPromptSubmit、PreToolUse、PostToolUse、Stop),因为它们覆盖了一个完整 agent cycle 的关键节点。其他 23 个都是同样的模式。
|
||||
|
||||
### 二、HookResult 常用字段摘录
|
||||
|
||||
CC 的 `HookResult`(`types/hooks.ts:260-275`)有 14 个字段,以下是常用字段:
|
||||
|
||||
| 字段 | 类型 | 用途 |
|
||||
|------|------|------|
|
||||
| `message` | Message | 可选 UI 消息 |
|
||||
| `blockingError` | HookBlockingError | 阻塞错误 → 注入对话让模型自纠 |
|
||||
| `outcome` | success/blocking/non_blocking_error/cancelled | 执行结果 |
|
||||
| `preventContinuation` | boolean | 阻止后续执行 |
|
||||
| `stopReason` | string | 停止原因描述 |
|
||||
| `permissionBehavior` | allow/deny/ask/passthrough | hook 返回权限决策 |
|
||||
| `updatedInput` | Record | 修改工具输入 |
|
||||
| `additionalContext` | string | 附加上下文 |
|
||||
| `updatedMCPToolOutput` | unknown | MCP 工具输出修改 |
|
||||
|
||||
### 三、关键不变式:Hook 'allow' 不能绕过 deny/ask 规则
|
||||
|
||||
这是 CC 权限系统最重要的安全设计(`toolHooks.ts:325-331`):**hook 返回 allow 时,仍然要检查 settings.json 的 deny/ask 规则**。即使用户的 hook 脚本说"允许",如果在 settings.json 中禁用了这个工具,操作仍然会被阻止。
|
||||
|
||||
教学版没有这个层次,只把 PreToolUse 的非 None 返回值解释为阻止本次工具执行。这在教学场景中够了,但在生产环境中会形成安全漏洞。
|
||||
|
||||
### 四、stopHookActive 机制
|
||||
|
||||
CC 的 Stop hooks 有一个防无限循环机制(`query.ts:212,1300`):`stopHookActive` 状态字段。当 stop hooks 产生 blockingError 时,循环带 `stopHookActive: true` 重入下一轮。后续迭代中 stop hooks 看到这个标志就不会再次触发。这防止了一个永不停机的 bug:模型自纠后 stop hook 再次报错 → 模型再自纠 → stop hook 再报错...
|
||||
|
||||
### 五、hook_stopped_continuation
|
||||
|
||||
PostToolUse hooks 返回 `preventContinuation: true` 时,会产生一个 `hook_stopped_continuation` 附件(`toolHooks.ts:117-130`)。query.ts(L1388-1393)检测到后设置 `shouldPreventContinuation = true`,循环退出。这是 "hook 优雅地让 Agent 停机" 的机制,不是崩溃,是完成。
|
||||
|
||||
### 教学版的简化是刻意的
|
||||
|
||||
- 27 个事件 → 4 个(UserPromptSubmit/PreToolUse/PostToolUse/Stop):覆盖 agent cycle 关键节点
|
||||
- 14 个字段 → 简单的返回值(None = 继续,非 None = 阻止/续跑):心智负担降到最低
|
||||
- Hook allow vs deny/ask 不变式 → 省略:教学版没有 settings.json 层
|
||||
- stopHookActive → 省略:教学版 Stop hook 只做简单续跑,不涉及防无限循环机制
|
||||
|
||||
</details>
|
||||
|
||||
<!-- translation-sync: zh@v1, en@v0, ja@v0 -->
|
||||
293
s04_hooks/code.py
Normal file
293
s04_hooks/code.py
Normal file
@@ -0,0 +1,293 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
s04: Hooks — move extension logic out of the loop, onto hooks.
|
||||
|
||||
User types query
|
||||
│
|
||||
▼
|
||||
┌──────────────────┐
|
||||
│ UserPromptSubmit │ ── trigger_hooks() before LLM
|
||||
└────────┬─────────┘
|
||||
▼
|
||||
┌────────────┐ ┌─────────────────────────────┐
|
||||
│ messages │────▶│ LLM (stop_reason=tool_use?)│
|
||||
└────────────┘ │ No ──▶ Stop hooks ──▶ exit │
|
||||
│ Yes ──▶ tool_use block ──┐ │
|
||||
└────────────────────────────┘ │
|
||||
▼
|
||||
┌──────────────────┐
|
||||
│ trigger_hooks() │
|
||||
│ PreToolUse: │
|
||||
│ permission_hook │
|
||||
│ log_hook │
|
||||
└───────┬──────────┘
|
||||
│ (not blocked)
|
||||
┌───────▼──────────┐
|
||||
│ TOOL_HANDLERS[x] │
|
||||
└───────┬──────────┘
|
||||
│
|
||||
┌───────▼──────────┐
|
||||
│ trigger_hooks() │
|
||||
│ PostToolUse: │
|
||||
│ large_output │
|
||||
└───────┬──────────┘
|
||||
│
|
||||
results ──▶ back to messages
|
||||
|
||||
Changes from s03:
|
||||
+ HOOKS registry (event -> list of callbacks)
|
||||
+ register_hook() / trigger_hooks()
|
||||
+ context_inject_hook (UserPromptSubmit)
|
||||
+ permission_hook, log_hook (PreToolUse)
|
||||
+ large_output_hook (PostToolUse)
|
||||
+ summary_hook (Stop)
|
||||
- check_permission() removed from loop body
|
||||
(logic moved into permission_hook, triggered via PreToolUse)
|
||||
|
||||
Run: python s04_hooks/code.py
|
||||
Needs: pip install anthropic python-dotenv + ANTHROPIC_API_KEY in .env
|
||||
"""
|
||||
|
||||
import os, subprocess
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
import readline
|
||||
readline.parse_and_bind('set bind-tty-special-chars off')
|
||||
readline.parse_and_bind('set input-meta on')
|
||||
readline.parse_and_bind('set output-meta on')
|
||||
readline.parse_and_bind('set convert-meta off')
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
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()
|
||||
client = Anthropic(base_url=os.getenv("ANTHROPIC_BASE_URL"))
|
||||
MODEL = os.environ["MODEL_ID"]
|
||||
|
||||
SYSTEM = f"You are a coding agent at {WORKDIR}. Use tools to solve tasks. Act, don't explain."
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
# FROM s02-s03 (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}"
|
||||
|
||||
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"]}},
|
||||
]
|
||||
|
||||
TOOL_HANDLERS = {
|
||||
"bash": run_bash, "read_file": run_read, "write_file": run_write,
|
||||
"edit_file": run_edit, "glob": run_glob,
|
||||
}
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
# NEW in s04: Hook System (s03 permission logic now via hooks)
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
|
||||
HOOKS = {"UserPromptSubmit": [], "PreToolUse": [], "PostToolUse": [], "Stop": []}
|
||||
|
||||
def register_hook(event: str, callback):
|
||||
HOOKS[event].append(callback)
|
||||
|
||||
def trigger_hooks(event: str, *args):
|
||||
for callback in HOOKS[event]:
|
||||
result = callback(*args)
|
||||
if result is not None: # teaching shortcut: block this tool call
|
||||
return result
|
||||
return None
|
||||
|
||||
|
||||
# s03 permission check logic, now wrapped as a hook
|
||||
DENY_LIST = ["rm -rf /", "sudo", "shutdown", "reboot", "mkfs", "dd if="]
|
||||
DESTRUCTIVE = ["rm ", "> /etc/", "chmod 777"]
|
||||
|
||||
def permission_hook(block):
|
||||
"""PreToolUse: s03 check_permission() logic moved here."""
|
||||
if block.name == "bash":
|
||||
for pattern in DENY_LIST:
|
||||
if pattern in block.input.get("command", ""):
|
||||
print(f"\n\033[31m⛔ Blocked: '{pattern}'\033[0m")
|
||||
return "Permission denied by deny list"
|
||||
for kw in DESTRUCTIVE:
|
||||
if kw in block.input.get("command", ""):
|
||||
print(f"\n\033[33m⚠ Potentially destructive command\033[0m")
|
||||
print(f" Tool: {block.name}({block.input})")
|
||||
choice = input(" Allow? [y/N] ").strip().lower()
|
||||
if choice not in ("y", "yes"):
|
||||
return "Permission denied by user"
|
||||
if block.name in ("write_file", "edit_file"):
|
||||
path = block.input.get("path", "")
|
||||
if not (WORKDIR / path).resolve().is_relative_to(WORKDIR):
|
||||
print(f"\n\033[33m⚠ Writing outside workspace\033[0m")
|
||||
print(f" Tool: {block.name}({block.input})")
|
||||
choice = input(" Allow? [y/N] ").strip().lower()
|
||||
if choice not in ("y", "yes"):
|
||||
return "Permission denied by user"
|
||||
return None
|
||||
|
||||
def log_hook(block):
|
||||
"""PreToolUse: log every tool call."""
|
||||
args_preview = str(list(block.input.values())[:2])[:60]
|
||||
print(f"\033[90m[HOOK] {block.name}({args_preview})\033[0m")
|
||||
return None
|
||||
|
||||
def large_output_hook(block, output):
|
||||
"""PostToolUse: warn on large output."""
|
||||
if len(str(output)) > 100000:
|
||||
print(f"\033[33m[HOOK] ⚠ Large output from {block.name}: {len(str(output))} chars\033[0m")
|
||||
return None
|
||||
|
||||
# UserPromptSubmit hook: log user input before it reaches the LLM
|
||||
def context_inject_hook(query: str):
|
||||
print(f"\033[90m[HOOK] UserPromptSubmit: working in {WORKDIR}\033[0m")
|
||||
return None
|
||||
|
||||
# Stop hook: print summary when loop is about to exit
|
||||
def summary_hook(messages: list):
|
||||
tool_count = sum(1 for m in messages
|
||||
for b in (m.get("content") if isinstance(m.get("content"), list) else [])
|
||||
if isinstance(b, dict) and b.get("type") == "tool_result")
|
||||
print(f"\033[90m[HOOK] Stop: session used {tool_count} tool calls\033[0m")
|
||||
return None
|
||||
|
||||
register_hook("UserPromptSubmit", context_inject_hook)
|
||||
register_hook("PreToolUse", permission_hook)
|
||||
register_hook("PreToolUse", log_hook)
|
||||
register_hook("PostToolUse", large_output_hook)
|
||||
register_hook("Stop", summary_hook)
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
# agent_loop — same structure as s03, but no hard-coded check
|
||||
# s03: if not check_permission(block): ...
|
||||
# s04: if trigger_hooks("PreToolUse", block): ...
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
|
||||
def agent_loop(messages: list):
|
||||
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":
|
||||
force = trigger_hooks("Stop", messages)
|
||||
if force:
|
||||
messages.append({"role": "user", "content": force})
|
||||
continue
|
||||
return
|
||||
|
||||
results = []
|
||||
for block in response.content:
|
||||
if block.type != "tool_use":
|
||||
continue
|
||||
|
||||
# s04 change: hook replaces hard-coded check_permission()
|
||||
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) # s04: post hook
|
||||
|
||||
results.append({"type": "tool_result", "tool_use_id": block.id, "content": output})
|
||||
|
||||
messages.append({"role": "user", "content": results})
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("s04: Hooks — extension logic on hooks, loop stays clean")
|
||||
print("Type a question, press Enter. Type q to quit.\n")
|
||||
|
||||
history = []
|
||||
while True:
|
||||
try:
|
||||
query = input("\033[36ms04 >> \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()
|
||||
100
s04_hooks/images/hooks-overview.en.svg
Normal file
100
s04_hooks/images/hooks-overview.en.svg
Normal file
@@ -0,0 +1,100 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 460" font-family="system-ui, -apple-system, sans-serif">
|
||||
<defs>
|
||||
<marker id="arrow" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" fill="#555"/>
|
||||
</marker>
|
||||
<marker id="arrow-blue" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" fill="#2563eb"/>
|
||||
</marker>
|
||||
<marker id="arrow-green" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" fill="#16a34a"/>
|
||||
</marker>
|
||||
<marker id="arrow-red" 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="#dc2626"/>
|
||||
</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="460" 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">Hooks — Extension Logic Hangs Outside, Loop Unchanged</text>
|
||||
|
||||
<!-- ===== Main Flow Line (y=140 horizontal) ===== -->
|
||||
|
||||
<!-- ① messages[] -->
|
||||
<rect x="40" y="112" width="110" height="56" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
|
||||
<text x="95" y="138" fill="#1e3a5f" font-size="12" font-weight="600" text-anchor="middle">messages[]</text>
|
||||
<text x="95" y="156" fill="#64748b" font-size="9" text-anchor="middle">(s01 preserved)</text>
|
||||
|
||||
<!-- → LLM -->
|
||||
<line x1="150" y1="140" x2="198" y2="140" stroke="#2563eb" stroke-width="2" marker-end="url(#arrow-blue)"/>
|
||||
|
||||
<!-- ② LLM -->
|
||||
<rect x="200" y="108" width="120" height="64" rx="8" fill="#fff" stroke="#2563eb" stroke-width="1.5"/>
|
||||
<text x="260" y="134" fill="#1e3a5f" font-size="14" font-weight="700" text-anchor="middle">LLM</text>
|
||||
<text x="260" y="154" fill="#64748b" font-size="10" text-anchor="middle">stop_reason=tool_use?</text>
|
||||
|
||||
<!-- LLM No → Return -->
|
||||
<line x1="260" y1="172" x2="260" y2="200" stroke="#16a34a" stroke-width="2" marker-end="url(#arrow)"/>
|
||||
<text x="275" y="192" fill="#16a34a" font-size="10" font-weight="600">No</text>
|
||||
<rect x="205" y="202" width="110" height="28" rx="14" fill="#dcfce7" stroke="#16a34a" stroke-width="1.5"/>
|
||||
<text x="260" y="220" fill="#166534" font-size="11" font-weight="600" text-anchor="middle">Return Result</text>
|
||||
|
||||
<!-- LLM Yes → PreToolUse -->
|
||||
<line x1="320" y1="140" x2="378" y2="140" stroke="#555" stroke-width="2" marker-end="url(#arrow)"/>
|
||||
<text x="345" y="132" fill="#d97706" font-size="10" font-weight="600">Yes</text>
|
||||
|
||||
<!-- ③ PreToolUse Hook (s04 new) -->
|
||||
<rect x="380" y="96" width="160" height="88" rx="10" fill="#f0fdf4" stroke="#16a34a" stroke-width="2" stroke-dasharray="6,3"/>
|
||||
<text x="460" y="116" fill="#166534" font-size="11" font-weight="700" text-anchor="middle">trigger_hooks()</text>
|
||||
<text x="460" y="132" fill="#166534" font-size="9" font-weight="600" text-anchor="middle">PreToolUse</text>
|
||||
<rect x="396" y="140" width="128" height="18" rx="3" fill="#dcfce7" stroke="#16a34a" stroke-width="0.8"/>
|
||||
<text x="460" y="153" fill="#166534" font-size="8" text-anchor="middle">permission_hook · log_hook</text>
|
||||
<text x="460" y="176" fill="#64748b" font-size="8" text-anchor="middle">Teaching: non-None → block</text>
|
||||
|
||||
<!-- PreToolUse Block → branch down -->
|
||||
<line x1="460" y1="184" x2="460" y2="218" stroke="#dc2626" stroke-width="2" marker-end="url(#arrow-red)"/>
|
||||
<rect x="405" y="220" width="110" height="24" rx="12" fill="#fef2f2" stroke="#dc2626" stroke-width="1.5"/>
|
||||
<text x="460" y="236" fill="#991b1b" font-size="10" font-weight="600" text-anchor="middle">Write tool_result</text>
|
||||
|
||||
<!-- PreToolUse Pass → TOOL_HANDLERS -->
|
||||
<line x1="540" y1="140" x2="588" y2="140" stroke="#16a34a" stroke-width="2" marker-end="url(#arrow-green)"/>
|
||||
<text x="558" y="132" fill="#16a34a" font-size="9" font-weight="600">Pass</text>
|
||||
|
||||
<!-- ④ TOOL_HANDLERS (s02 preserved) -->
|
||||
<rect x="590" y="108" width="100" height="64" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
|
||||
<text x="640" y="134" fill="#1e3a5f" font-size="10" font-weight="600" text-anchor="middle">TOOL_</text>
|
||||
<text x="640" y="148" fill="#1e3a5f" font-size="10" font-weight="600" text-anchor="middle">HANDLERS</text>
|
||||
<text x="640" y="164" fill="#64748b" font-size="8" text-anchor="middle">bash/read/...</text>
|
||||
|
||||
<!-- TOOL_HANDLERS → PostToolUse (down) -->
|
||||
<line x1="640" y1="172" x2="640" y2="268" stroke="#16a34a" stroke-width="2"/>
|
||||
<text x="648" y="224" fill="#16a34a" font-size="9" font-weight="600">After exec</text>
|
||||
|
||||
<!-- ⑤ PostToolUse Hook (s04 new) -->
|
||||
<rect x="560" y="270" width="160" height="56" rx="10" fill="#f0fdf4" stroke="#16a34a" stroke-width="2" stroke-dasharray="6,3"/>
|
||||
<text x="640" y="290" fill="#166534" font-size="11" font-weight="700" text-anchor="middle">trigger_hooks()</text>
|
||||
<text x="640" y="306" fill="#166534" font-size="9" font-weight="600" text-anchor="middle">PostToolUse</text>
|
||||
<rect x="576" y="310" width="128" height="12" rx="3" fill="#dcfce7" stroke="#16a34a" stroke-width="0.8"/>
|
||||
<text x="640" y="320" fill="#166534" font-size="7" text-anchor="middle">large_output_hook</text>
|
||||
|
||||
<!-- ===== Loop: results back to messages ===== -->
|
||||
<path d="M 720 298 L 760 298 L 760 350 L 95 350 L 95 168" fill="none" stroke="#555" stroke-width="2" marker-end="url(#arrow)" stroke-dasharray="6,3"/>
|
||||
<text x="400" y="370" fill="#64748b" font-size="10" text-anchor="middle">Results appended to messages[], loop continues</text>
|
||||
|
||||
<!-- ===== Bottom Comparison ===== -->
|
||||
<rect x="60" y="396" width="680" height="48" rx="6" fill="#f8fafc" stroke="#e2e8f0" stroke-width="1"/>
|
||||
<text x="100" y="416" fill="#94a3b8" font-size="10" font-weight="600">s03:</text>
|
||||
<text x="130" y="416" fill="#64748b" font-size="10" font-family="monospace">if not check_permission(block): ...</text>
|
||||
<text x="400" y="416" fill="#94a3b8" font-size="10">← every new check requires modifying the loop</text>
|
||||
<text x="100" y="436" fill="#16a34a" font-size="10" font-weight="600">s04:</text>
|
||||
<text x="130" y="436" fill="#16a34a" font-size="10" font-family="monospace">blocked = trigger_hooks("PreToolUse", block)</text>
|
||||
<text x="520" y="436" fill="#16a34a" font-size="10">← add check = register_hook(), loop unchanged</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.6 KiB |
100
s04_hooks/images/hooks-overview.ja.svg
Normal file
100
s04_hooks/images/hooks-overview.ja.svg
Normal file
@@ -0,0 +1,100 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 460" font-family="system-ui, -apple-system, sans-serif">
|
||||
<defs>
|
||||
<marker id="arrow" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" fill="#555"/>
|
||||
</marker>
|
||||
<marker id="arrow-blue" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" fill="#2563eb"/>
|
||||
</marker>
|
||||
<marker id="arrow-green" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" fill="#16a34a"/>
|
||||
</marker>
|
||||
<marker id="arrow-red" 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="#dc2626"/>
|
||||
</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="460" 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">Hooks — 拡張ロジックは外側に、ループは一文字も変更しない</text>
|
||||
|
||||
<!-- ===== メインフロー(y=140 水平) ===== -->
|
||||
|
||||
<!-- ① messages[] -->
|
||||
<rect x="40" y="112" width="110" height="56" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
|
||||
<text x="95" y="138" fill="#1e3a5f" font-size="12" font-weight="600" text-anchor="middle">messages[]</text>
|
||||
<text x="95" y="156" fill="#64748b" font-size="9" text-anchor="middle">(s01 保持)</text>
|
||||
|
||||
<!-- → LLM -->
|
||||
<line x1="150" y1="140" x2="198" y2="140" stroke="#2563eb" stroke-width="2" marker-end="url(#arrow-blue)"/>
|
||||
|
||||
<!-- ② LLM -->
|
||||
<rect x="200" y="108" width="120" height="64" rx="8" fill="#fff" stroke="#2563eb" stroke-width="1.5"/>
|
||||
<text x="260" y="134" fill="#1e3a5f" font-size="14" font-weight="700" text-anchor="middle">LLM</text>
|
||||
<text x="260" y="154" fill="#64748b" font-size="10" text-anchor="middle">stop_reason=tool_use?</text>
|
||||
|
||||
<!-- LLM No → 返却 -->
|
||||
<line x1="260" y1="172" x2="260" y2="200" stroke="#16a34a" stroke-width="2" marker-end="url(#arrow)"/>
|
||||
<text x="275" y="192" fill="#16a34a" font-size="10" font-weight="600">No</text>
|
||||
<rect x="205" y="202" width="110" height="28" rx="14" fill="#dcfce7" stroke="#16a34a" stroke-width="1.5"/>
|
||||
<text x="260" y="220" fill="#166534" font-size="11" font-weight="600" text-anchor="middle">結果を返す</text>
|
||||
|
||||
<!-- LLM Yes → PreToolUse -->
|
||||
<line x1="320" y1="140" x2="378" y2="140" stroke="#555" stroke-width="2" marker-end="url(#arrow)"/>
|
||||
<text x="345" y="132" fill="#d97706" font-size="10" font-weight="600">Yes</text>
|
||||
|
||||
<!-- ③ PreToolUse フック(s04 新規) -->
|
||||
<rect x="380" y="96" width="160" height="88" rx="10" fill="#f0fdf4" stroke="#16a34a" stroke-width="2" stroke-dasharray="6,3"/>
|
||||
<text x="460" y="116" fill="#166534" font-size="11" font-weight="700" text-anchor="middle">trigger_hooks()</text>
|
||||
<text x="460" y="132" fill="#166534" font-size="9" font-weight="600" text-anchor="middle">PreToolUse</text>
|
||||
<rect x="396" y="140" width="128" height="18" rx="3" fill="#dcfce7" stroke="#16a34a" stroke-width="0.8"/>
|
||||
<text x="460" y="153" fill="#166534" font-size="8" text-anchor="middle">permission_hook · log_hook</text>
|
||||
<text x="460" y="176" fill="#64748b" font-size="8" text-anchor="middle">教育版: 非 None → ブロック</text>
|
||||
|
||||
<!-- PreToolUse 中断 → 下に分岐 -->
|
||||
<line x1="460" y1="184" x2="460" y2="218" stroke="#dc2626" stroke-width="2" marker-end="url(#arrow-red)"/>
|
||||
<rect x="405" y="220" width="110" height="24" rx="12" fill="#fef2f2" stroke="#dc2626" stroke-width="1.5"/>
|
||||
<text x="460" y="236" fill="#991b1b" font-size="10" font-weight="600" text-anchor="middle">tool_result に返す</text>
|
||||
|
||||
<!-- PreToolUse 通過 → TOOL_HANDLERS -->
|
||||
<line x1="540" y1="140" x2="588" y2="140" stroke="#16a34a" stroke-width="2" marker-end="url(#arrow-green)"/>
|
||||
<text x="558" y="132" fill="#16a34a" font-size="9" font-weight="600">通過</text>
|
||||
|
||||
<!-- ④ TOOL_HANDLERS (s02 保持) -->
|
||||
<rect x="590" y="108" width="100" height="64" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
|
||||
<text x="640" y="134" fill="#1e3a5f" font-size="10" font-weight="600" text-anchor="middle">TOOL_</text>
|
||||
<text x="640" y="148" fill="#1e3a5f" font-size="10" font-weight="600" text-anchor="middle">HANDLERS</text>
|
||||
<text x="640" y="164" fill="#64748b" font-size="8" text-anchor="middle">bash/read/...</text>
|
||||
|
||||
<!-- TOOL_HANDLERS → PostToolUse (下) -->
|
||||
<line x1="640" y1="172" x2="640" y2="268" stroke="#16a34a" stroke-width="2"/>
|
||||
<text x="648" y="224" fill="#16a34a" font-size="9" font-weight="600">実行後</text>
|
||||
|
||||
<!-- ⑤ PostToolUse フック(s04 新規) -->
|
||||
<rect x="560" y="270" width="160" height="56" rx="10" fill="#f0fdf4" stroke="#16a34a" stroke-width="2" stroke-dasharray="6,3"/>
|
||||
<text x="640" y="290" fill="#166534" font-size="11" font-weight="700" text-anchor="middle">trigger_hooks()</text>
|
||||
<text x="640" y="306" fill="#166534" font-size="9" font-weight="600" text-anchor="middle">PostToolUse</text>
|
||||
<rect x="576" y="310" width="128" height="12" rx="3" fill="#dcfce7" stroke="#16a34a" stroke-width="0.8"/>
|
||||
<text x="640" y="320" fill="#166534" font-size="7" text-anchor="middle">large_output_hook</text>
|
||||
|
||||
<!-- ===== ループ:結果を messages に戻す ===== -->
|
||||
<path d="M 720 298 L 760 298 L 760 350 L 95 350 L 95 168" fill="none" stroke="#555" stroke-width="2" marker-end="url(#arrow)" stroke-dasharray="6,3"/>
|
||||
<text x="400" y="370" fill="#64748b" font-size="10" text-anchor="middle">結果を messages[] に追加、ループ継続</text>
|
||||
|
||||
<!-- ===== 下部比較 ===== -->
|
||||
<rect x="60" y="396" width="680" height="48" rx="6" fill="#f8fafc" stroke="#e2e8f0" stroke-width="1"/>
|
||||
<text x="100" y="416" fill="#94a3b8" font-size="10" font-weight="600">s03:</text>
|
||||
<text x="130" y="416" fill="#64748b" font-size="10" font-family="monospace">if not check_permission(block): ...</text>
|
||||
<text x="400" y="416" fill="#94a3b8" font-size="10">← チェックを追加するたびにループを修正</text>
|
||||
<text x="100" y="436" fill="#16a34a" font-size="10" font-weight="600">s04:</text>
|
||||
<text x="130" y="436" fill="#16a34a" font-size="10" font-family="monospace">blocked = trigger_hooks("PreToolUse", block)</text>
|
||||
<text x="520" y="436" fill="#16a34a" font-size="10">← チェック追加 = register_hook()、ループ不変</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.7 KiB |
100
s04_hooks/images/hooks-overview.svg
Normal file
100
s04_hooks/images/hooks-overview.svg
Normal file
@@ -0,0 +1,100 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 460" font-family="system-ui, -apple-system, sans-serif">
|
||||
<defs>
|
||||
<marker id="arrow" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" fill="#555"/>
|
||||
</marker>
|
||||
<marker id="arrow-blue" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" fill="#2563eb"/>
|
||||
</marker>
|
||||
<marker id="arrow-green" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" fill="#16a34a"/>
|
||||
</marker>
|
||||
<marker id="arrow-red" 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="#dc2626"/>
|
||||
</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="460" 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">Hooks — 扩展逻辑挂在外面,循环本身一字不改</text>
|
||||
|
||||
<!-- ===== 主流程线(y=140 水平) ===== -->
|
||||
|
||||
<!-- ① messages[] -->
|
||||
<rect x="40" y="112" width="110" height="56" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
|
||||
<text x="95" y="138" fill="#1e3a5f" font-size="12" font-weight="600" text-anchor="middle">messages[]</text>
|
||||
<text x="95" y="156" fill="#64748b" font-size="9" text-anchor="middle">(s01 保留)</text>
|
||||
|
||||
<!-- → LLM -->
|
||||
<line x1="150" y1="140" x2="198" y2="140" stroke="#2563eb" stroke-width="2" marker-end="url(#arrow-blue)"/>
|
||||
|
||||
<!-- ② LLM -->
|
||||
<rect x="200" y="108" width="120" height="64" rx="8" fill="#fff" stroke="#2563eb" stroke-width="1.5"/>
|
||||
<text x="260" y="134" fill="#1e3a5f" font-size="14" font-weight="700" text-anchor="middle">LLM</text>
|
||||
<text x="260" y="154" fill="#64748b" font-size="10" text-anchor="middle">stop_reason=tool_use?</text>
|
||||
|
||||
<!-- LLM 否 → 返回 -->
|
||||
<line x1="260" y1="172" x2="260" y2="200" stroke="#16a34a" stroke-width="2" marker-end="url(#arrow)"/>
|
||||
<text x="275" y="192" fill="#16a34a" font-size="10" font-weight="600">否</text>
|
||||
<rect x="205" y="202" width="110" height="28" rx="14" fill="#dcfce7" stroke="#16a34a" stroke-width="1.5"/>
|
||||
<text x="260" y="220" fill="#166534" font-size="11" font-weight="600" text-anchor="middle">返回结果</text>
|
||||
|
||||
<!-- LLM 是 → PreToolUse -->
|
||||
<line x1="320" y1="140" x2="378" y2="140" stroke="#555" stroke-width="2" marker-end="url(#arrow)"/>
|
||||
<text x="345" y="132" fill="#d97706" font-size="10" font-weight="600">是</text>
|
||||
|
||||
<!-- ③ PreToolUse hook(s04 新增) -->
|
||||
<rect x="380" y="96" width="160" height="88" rx="10" fill="#f0fdf4" stroke="#16a34a" stroke-width="2" stroke-dasharray="6,3"/>
|
||||
<text x="460" y="116" fill="#166534" font-size="11" font-weight="700" text-anchor="middle">trigger_hooks()</text>
|
||||
<text x="460" y="132" fill="#166534" font-size="9" font-weight="600" text-anchor="middle">PreToolUse</text>
|
||||
<rect x="396" y="140" width="128" height="18" rx="3" fill="#dcfce7" stroke="#16a34a" stroke-width="0.8"/>
|
||||
<text x="460" y="153" fill="#166534" font-size="8" text-anchor="middle">permission_hook · log_hook</text>
|
||||
<text x="460" y="176" fill="#64748b" font-size="8" text-anchor="middle">教学版:非 None → 阻止</text>
|
||||
|
||||
<!-- PreToolUse 阻止 → 向下引出 -->
|
||||
<line x1="460" y1="184" x2="460" y2="218" stroke="#dc2626" stroke-width="2" marker-end="url(#arrow-red)"/>
|
||||
<rect x="405" y="220" width="110" height="24" rx="12" fill="#fef2f2" stroke="#dc2626" stroke-width="1.5"/>
|
||||
<text x="460" y="236" fill="#991b1b" font-size="10" font-weight="600" text-anchor="middle">写入 tool_result</text>
|
||||
|
||||
<!-- PreToolUse 通过 → TOOL_HANDLERS -->
|
||||
<line x1="540" y1="140" x2="588" y2="140" stroke="#16a34a" stroke-width="2" marker-end="url(#arrow-green)"/>
|
||||
<text x="558" y="132" fill="#16a34a" font-size="9" font-weight="600">通过</text>
|
||||
|
||||
<!-- ④ TOOL_HANDLERS (s02 保留) -->
|
||||
<rect x="590" y="108" width="100" height="64" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
|
||||
<text x="640" y="134" fill="#1e3a5f" font-size="10" font-weight="600" text-anchor="middle">TOOL_</text>
|
||||
<text x="640" y="148" fill="#1e3a5f" font-size="10" font-weight="600" text-anchor="middle">HANDLERS</text>
|
||||
<text x="640" y="164" fill="#64748b" font-size="8" text-anchor="middle">bash/read/...</text>
|
||||
|
||||
<!-- TOOL_HANDLERS → PostToolUse (向下) -->
|
||||
<line x1="640" y1="172" x2="640" y2="268" stroke="#16a34a" stroke-width="2"/>
|
||||
<text x="648" y="224" fill="#16a34a" font-size="9" font-weight="600">执行后</text>
|
||||
|
||||
<!-- ⑤ PostToolUse hook(s04 新增) -->
|
||||
<rect x="560" y="270" width="160" height="56" rx="10" fill="#f0fdf4" stroke="#16a34a" stroke-width="2" stroke-dasharray="6,3"/>
|
||||
<text x="640" y="290" fill="#166534" font-size="11" font-weight="700" text-anchor="middle">trigger_hooks()</text>
|
||||
<text x="640" y="306" fill="#166534" font-size="9" font-weight="600" text-anchor="middle">PostToolUse</text>
|
||||
<rect x="576" y="310" width="128" height="12" rx="3" fill="#dcfce7" stroke="#16a34a" stroke-width="0.8"/>
|
||||
<text x="640" y="320" fill="#166534" font-size="7" text-anchor="middle">large_output_hook</text>
|
||||
|
||||
<!-- ===== 回环:结果回到 messages ===== -->
|
||||
<path d="M 720 298 L 760 298 L 760 350 L 95 350 L 95 168" fill="none" stroke="#555" stroke-width="2" marker-end="url(#arrow)" stroke-dasharray="6,3"/>
|
||||
<text x="400" y="370" fill="#64748b" font-size="10" text-anchor="middle">结果追加到 messages[],循环继续</text>
|
||||
|
||||
<!-- ===== 底部对比 ===== -->
|
||||
<rect x="60" y="396" width="680" height="48" rx="6" fill="#f8fafc" stroke="#e2e8f0" stroke-width="1"/>
|
||||
<text x="100" y="416" fill="#94a3b8" font-size="10" font-weight="600">s03:</text>
|
||||
<text x="130" y="416" fill="#64748b" font-size="10" font-family="monospace">if not check_permission(block): ...</text>
|
||||
<text x="400" y="416" fill="#94a3b8" font-size="10">← 每加一个检查就要改循环</text>
|
||||
<text x="100" y="436" fill="#16a34a" font-size="10" font-weight="600">s04:</text>
|
||||
<text x="130" y="436" fill="#16a34a" font-size="10" font-family="monospace">blocked = trigger_hooks("PreToolUse", block)</text>
|
||||
<text x="520" y="436" fill="#16a34a" font-size="10">← 加检查 = register_hook(),循环不改</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.6 KiB |
Reference in New Issue
Block a user