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:
189
s06_subagent/README.en.md
Normal file
189
s06_subagent/README.en.md
Normal file
@@ -0,0 +1,189 @@
|
||||
# s06: Subagent — Break Large Tasks into Small Ones with Clean Context
|
||||
|
||||
[中文](README.md) · [English](README.en.md) · [日本語](README.ja.md)
|
||||
|
||||
s01 → s02 → s03 → s04 → s05 → `s06` → [s07](../s07_skill_loading/) → s08 → ... → s20
|
||||
|
||||
> *"Break large tasks small, each with clean context"* — Subagent uses an independent messages[], no pollution in the main conversation.
|
||||
>
|
||||
> **Harness Layer**: Sub-Agent — Context isolation, attention doesn't drift.
|
||||
|
||||
---
|
||||
|
||||
## The Problem
|
||||
|
||||
The Agent is fixing a bug. It reads 30 files to trace the call chain, chatting for 60 rounds along the way. The messages list grows to 120 entries, most of which are intermediate steps from "tracing the call chain" — unrelated to the final goal of "fixing the bug."
|
||||
|
||||
These intermediate steps occupy context space, making the Agent increasingly "forgetful" — it can no longer remember what the original problem was.
|
||||
|
||||
Think of it differently: when you fix a bug, you'd "open a new terminal" to trace the call chain. When done, close the terminal, write the result into your notes, and return to the original terminal to keep fixing. The Agent needs this ability too — **open an independent sub-process, give it an independent message list, let it focus on one thing.**
|
||||
|
||||
---
|
||||
|
||||
## The Solution
|
||||
|
||||

|
||||
|
||||
The minimal hook structure and `todo_write` tool from the previous chapter are preserved; this chapter focuses on the new `task` tool. When called, it spawns a sub-Agent with a fresh `messages[]`, running its own loop, and returning only a summary text to the main Agent. Conversation context is discarded, but file system side effects (writes, edits, commands) remain in the working directory.
|
||||
|
||||
The sub-Agent's tools are restricted: it has bash/read/write/edit/glob, but no task, preventing recursive spawning. The sub-Agent's tool calls still go through permission hooks; context isolation does not bypass security.
|
||||
|
||||
---
|
||||
|
||||
## How It Works
|
||||
|
||||
**spawn_subagent**, gives the sub-Agent a fresh messages list, runs its own loop, returns only the conclusion:
|
||||
|
||||
```python
|
||||
def spawn_subagent(description: str) -> str:
|
||||
# Sub-Agent tools: base tools, but no task (no recursion)
|
||||
sub_tools = [...]
|
||||
messages = [{"role": "user", "content": description}] # fresh messages[]
|
||||
|
||||
for _ in range(30): # safety limit
|
||||
response = client.messages.create(
|
||||
model=MODEL, system=SUB_SYSTEM,
|
||||
messages=messages, tools=sub_tools, max_tokens=8000,
|
||||
)
|
||||
messages.append({"role": "assistant", "content": response.content})
|
||||
if response.stop_reason != "tool_use":
|
||||
break
|
||||
results = []
|
||||
for block in response.content:
|
||||
if block.type == "tool_use":
|
||||
blocked = trigger_hooks("PreToolUse", block)
|
||||
if blocked:
|
||||
results.append({... "content": str(blocked)})
|
||||
continue
|
||||
handler = SUB_HANDLERS.get(block.name)
|
||||
output = handler(**block.input) if handler else f"Unknown"
|
||||
trigger_hooks("PostToolUse", block, output)
|
||||
results.append({... "content": output})
|
||||
messages.append({"role": "user", "content": results})
|
||||
|
||||
# Return only the final text conclusion, all intermediate steps discarded
|
||||
return extract_text(messages[-1]["content"])
|
||||
```
|
||||
|
||||
The main Agent calls it just like any other tool:
|
||||
|
||||
```python
|
||||
TOOLS = [
|
||||
{"name": "bash", ...},
|
||||
{"name": "read_file", ...},
|
||||
{"name": "write_file", ...},
|
||||
{"name": "edit_file", ...},
|
||||
{"name": "glob", ...},
|
||||
{"name": "todo_write", ...},
|
||||
# s06: new task tool
|
||||
{"name": "task",
|
||||
"description": "Launch a subagent to handle a complex subtask. Returns only the final conclusion.",
|
||||
"input_schema": {"type": "object", "properties": {"description": {"type": "string"}}, "required": ["description"]}},
|
||||
]
|
||||
|
||||
TOOL_HANDLERS["task"] = spawn_subagent
|
||||
```
|
||||
|
||||
Three key design decisions:
|
||||
|
||||
| Decision | Choice | Reason |
|
||||
|----------|--------|--------|
|
||||
| Context isolation | Fresh `messages[]` | Sub-Agent's intermediate steps don't pollute main Agent's context |
|
||||
| Return only conclusion | `extract_text(last_message)` | Not returning the entire messages list |
|
||||
| No recursion | Sub-Agent has no task tool | Prevents sub-Agent from spawning further sub-Agents |
|
||||
| Security not bypassed | Sub-Agent tool calls go through PreToolUse hook | Context isolation does not mean permission isolation |
|
||||
|
||||
The dispatch mechanism is unchanged; the task tool is routed through `TOOL_HANDLERS[block.name]`. The sub-Agent has its own `SUB_SYSTEM` prompt, explicitly instructing "complete the task, do not delegate further."
|
||||
|
||||
---
|
||||
|
||||
## Changes from s05
|
||||
|
||||
| Component | Before (s05) | After (s06) |
|
||||
|-----------|-------------|-------------|
|
||||
| Tool count | 6 (bash, read, write, edit, glob, todo_write) | 7 (+task) |
|
||||
| New function | — | spawn_subagent (independent messages[] + 30-round safety limit) |
|
||||
| Context isolation | Everything in the main conversation | Sub-Agent uses fresh messages[] |
|
||||
| Loop | Unchanged | Dispatch unchanged, sub-Agent has independent SUB_SYSTEM and hook-protected loop |
|
||||
|
||||
---
|
||||
|
||||
## Try It
|
||||
|
||||
```sh
|
||||
cd learn-claude-code
|
||||
python s06_subagent/code.py
|
||||
```
|
||||
|
||||
Try these prompts:
|
||||
|
||||
1. `Use a subtask to find what testing framework this project uses` (sub-Agent reads files, main Agent receives only the conclusion)
|
||||
2. `Delegate: read all .py files in agents/ and summarize what each one does`
|
||||
3. `Use a task to create s06_subagent/example/string_tools.py with a slugify(text: str) function, then verify it from the parent agent`
|
||||
|
||||
What to watch for: Do `[Subagent spawned]` / `[Subagent done]` appear? Do sub-Agent tool calls print as `[sub] ...`? Does the parent Agent continue with only the summary returned by the sub-Agent?
|
||||
|
||||
---
|
||||
|
||||
## What's Next
|
||||
|
||||
The Agent can now break tasks apart. But different tasks require different knowledge: editing frontend components needs React conventions, writing SQL needs table schemas. Stuffing all this knowledge into the system prompt would blow up the context.
|
||||
|
||||
→ s07 Skill Loading: Inject skills on demand instead of piling documents into the system prompt. Load only when needed, as natural as reading a file.
|
||||
|
||||
<details>
|
||||
<summary>Dive into CC Source Code</summary>
|
||||
|
||||
> The following is based on a complete analysis of CC source code `AgentTool.tsx`, `runAgent.ts`, `forkSubagent.ts`, and `forkedAgent.ts`.
|
||||
|
||||
### 1. Not One Pattern, but Three
|
||||
|
||||
The teaching version covers only "fresh messages[]". CC actually has three execution modes:
|
||||
|
||||
| Mode | Trigger | Context |
|
||||
|------|---------|---------|
|
||||
| **Normal Subagent** | `subagent_type` specified (normal path) | Truly fresh messages[], only the prompt |
|
||||
| **Fork Subagent** | No `subagent_type`, fork gate enabled | Constructs cache-friendly prefix via `buildForkedMessages()`, shares prompt cache |
|
||||
| **General-Purpose** | No `subagent_type`, fork gate disabled | Same as Normal |
|
||||
|
||||
### 2. Fork Mode: Sharing Prompt Cache
|
||||
|
||||
This is a core concept the teaching version omits. Fork mode (`forkSubagent.ts:60-71`) doesn't create a fresh context. Instead, it constructs a cache-friendly message prefix via `buildForkedMessages()` (`forkSubagent.ts:107-168`), preserving the parent assistant message and generating placeholder tool results. The goal isn't isolation, but making the Anthropic API's prompt cache hit: parent and child Agent's system prompt, tools, and message prefix are byte-identical, so the API doesn't need to recompute.
|
||||
|
||||
Five key components for cache hit (`forkedAgent.ts:57-68`): system prompt, tools, model, message prefix, thinking config, must be byte-identical.
|
||||
|
||||
### 3. Context Isolation's Precise Granularity
|
||||
|
||||
`createSubagentContext()` (`forkedAgent.ts:345-462`) creates the sub-Agent's `ToolUseContext`:
|
||||
|
||||
| Field | Behavior |
|
||||
|-------|----------|
|
||||
| `abortController` | New child controller; parent abort propagates down |
|
||||
| `setAppState` | Default no-op; but sync agents share via `shareSetAppState` (`runAgent.ts:697-714`) |
|
||||
| `readFileState` | **Cloned from parent** (avoids re-reading same files) |
|
||||
| `queryTracking` | New chainId, `depth = parentDepth + 1` |
|
||||
|
||||
The sub-Agent isn't fully isolated: file read state is shared. The degree of UI and notification isolation varies by execution path (sync/async/fork/teammate differ).
|
||||
|
||||
### 4. Recursive Fork Protection
|
||||
|
||||
The teaching version uses "sub-Agent has no task tool" for recursion protection. The real implementation is more nuanced: `isInForkChild()` (`forkSubagent.ts:78-89`) checks for `FORK_BOILERPLATE_TAG` in history. But `constants/tools.ts:36-46` defaults `Agent` to all agents' disabled set (with `USER_TYPE === 'ant'` exception); `forkSubagent.ts:73-89` has fork-child-specific recursion protection; `agentToolUtils.ts:100-110` has special allowances in teammate scenarios. Not simply "no further sub-Agents."
|
||||
|
||||
### 5. Permission Bubbling
|
||||
|
||||
Fork Agent's `permissionMode: 'bubble'` (`forkSubagent.ts:67`) means the sub-Agent's permission prompts bubble up to the parent terminal: the user approves sub-Agent operations in the main terminal.
|
||||
|
||||
### 6. Async vs Sync
|
||||
|
||||
The teaching version only shows synchronous sub-Agents (parent waits for child to finish). CC also supports async paths (`AgentTool.tsx:686-764`): when `run_in_background: true`, the sub-Agent launches asynchronously, returning `{ status: 'async_launched' }` immediately to the parent, and notifies the parent when complete. Actual triggers go beyond `run_in_background`, including auto-background, assistant force async, and coordinator/proactive paths.
|
||||
|
||||
### Teaching Version Simplifications Are Intentional
|
||||
|
||||
- Three modes → one (fresh messages): conceptually clear
|
||||
- Prompt cache sharing → omitted: teaching version doesn't involve API-layer optimization
|
||||
- Recursive fork protection → simplified to "sub-Agent has no task tool"
|
||||
- Async → omitted (left for s13): s06 focuses on the synchronous model first
|
||||
|
||||
</details>
|
||||
|
||||
<!-- translation-sync: zh@v1, en@v1, ja@v1 -->
|
||||
189
s06_subagent/README.ja.md
Normal file
189
s06_subagent/README.ja.md
Normal file
@@ -0,0 +1,189 @@
|
||||
# s06: Subagent — 大きなタスクを分割、それぞれがクリーンなコンテキストを取得
|
||||
|
||||
[中文](README.md) · [English](README.en.md) · [日本語](README.ja.md)
|
||||
|
||||
s01 → s02 → s03 → s04 → s05 → `s06` → [s07](../s07_skill_loading/) → s08 → ... → s20
|
||||
|
||||
> *"大きなタスクは小さく、小さなタスクごとにクリーンなコンテキスト"* — Subagent は独立した messages[] を使い、メイン会話を汚染しない。
|
||||
>
|
||||
> **Harness レイヤー**: サブエージェント — コンテキストの隔離、注意の散漫を防ぐ。
|
||||
|
||||
---
|
||||
|
||||
## 課題
|
||||
|
||||
Agent がバグを修正している。呼び出しチェーンを追跡するために 30 のファイルを読み、途中で 60 ラウンドやり取りした。messages リストは 120 件に膨らみ、その大部分は「呼び出しチェーンの追跡」という中間過程 — 「バグ修正」という最終目標とは無関係。
|
||||
|
||||
この中間過程がコンテキストの席を占め、Agent はますます「健忘」になる — 最初の問題が何だったか覚えていられない。
|
||||
|
||||
別の見方をすると:バグを修正するとき、あなたは「新しいターミナルを開いて」呼び出しチェーンを追跡するだろう。追跡が終わったらターミナルを閉じ、結果をメモに書き、元のターミナルに戻ってバグ修正を続ける。Agent にもこの能力が必要 — **独立したサブプロセスを開き、独立したメッセージリストを与え、一つのことに集中させる。**
|
||||
|
||||
---
|
||||
|
||||
## ソリューション
|
||||
|
||||

|
||||
|
||||
前章の最小フック構造と `todo_write` ツールを保持し、本章は新規の `task` ツールに注目する。呼び出されると、サブエージェントを spawn する。新しい `messages[]` を持ち、自分自身のループを実行し、終了後に要約テキストのみをメイン Agent に返す。会話コンテキストは破棄されるが、ファイルシステムの副作用(書き込み、編集、コマンド実行)は作業ディレクトリに残る。
|
||||
|
||||
サブエージェントのツールは制限される:bash/read/write/edit/glob を持つが、task はない。再帰 spawn を防止する。サブエージェントのツール呼び出しも権限フックを経由する。コンテキスト分離は権限のバイパスではない。
|
||||
|
||||
---
|
||||
|
||||
## 仕組み
|
||||
|
||||
**spawn_subagent**、サブエージェントに新しいメッセージリストを与え、自分自身のループを実行し、結論のみを返す:
|
||||
|
||||
```python
|
||||
def spawn_subagent(description: str) -> str:
|
||||
# サブエージェントのツール:基本ツールのみ、task なし(再帰禁止)
|
||||
sub_tools = [...]
|
||||
messages = [{"role": "user", "content": description}] # 新規 messages[]
|
||||
|
||||
for _ in range(30): # safety limit
|
||||
response = client.messages.create(
|
||||
model=MODEL, system=SUB_SYSTEM,
|
||||
messages=messages, tools=sub_tools, max_tokens=8000,
|
||||
)
|
||||
messages.append({"role": "assistant", "content": response.content})
|
||||
if response.stop_reason != "tool_use":
|
||||
break
|
||||
results = []
|
||||
for block in response.content:
|
||||
if block.type == "tool_use":
|
||||
blocked = trigger_hooks("PreToolUse", block)
|
||||
if blocked:
|
||||
results.append({... "content": str(blocked)})
|
||||
continue
|
||||
handler = SUB_HANDLERS.get(block.name)
|
||||
output = handler(**block.input) if handler else f"Unknown"
|
||||
trigger_hooks("PostToolUse", block, output)
|
||||
results.append({... "content": output})
|
||||
messages.append({"role": "user", "content": results})
|
||||
|
||||
# 最後のテキスト結論のみを返す、中間過程はすべて破棄
|
||||
return extract_text(messages[-1]["content"])
|
||||
```
|
||||
|
||||
メイン Agent の呼び出しは、他のツールと同じ:
|
||||
|
||||
```python
|
||||
TOOLS = [
|
||||
{"name": "bash", ...},
|
||||
{"name": "read_file", ...},
|
||||
{"name": "write_file", ...},
|
||||
{"name": "edit_file", ...},
|
||||
{"name": "glob", ...},
|
||||
{"name": "todo_write", ...},
|
||||
# s06: 新規 task ツール
|
||||
{"name": "task",
|
||||
"description": "Launch a subagent to handle a complex subtask. Returns only the final conclusion.",
|
||||
"input_schema": {"type": "object", "properties": {"description": {"type": "string"}}, "required": ["description"]}},
|
||||
]
|
||||
|
||||
TOOL_HANDLERS["task"] = spawn_subagent
|
||||
```
|
||||
|
||||
三つの重要な設計決定:
|
||||
|
||||
| 決定 | 選択 | 理由 |
|
||||
|------|------|------|
|
||||
| コンテキスト隔離 | 新規 `messages[]` | サブエージェントの中間過程がメイン Agent のコンテキストを汚染しない |
|
||||
| 結論のみ返却 | `extract_text(last_message)` | messages リスト全体を返すのではない |
|
||||
| 再帰禁止 | サブエージェントに task ツールなし | サブエージェントがさらにサブエージェントを spawn するのを防止 |
|
||||
| セキュリティのバイパスなし | サブエージェントのツール呼び出しも PreToolUse フックを経由 | コンテキスト分離は権限分離ではない |
|
||||
|
||||
ディスパッチ機構は変わらず、task ツールは `TOOL_HANDLERS[block.name]` を経由する。サブエージェントは独立した `SUB_SYSTEM` プロンプトを持ち、「タスクを完了し、さらに委託しない」と明示される。
|
||||
|
||||
---
|
||||
|
||||
## s05 からの変更
|
||||
|
||||
| コンポーネント | 変更前 (s05) | 変更後 (s06) |
|
||||
|--------------|-------------|-------------|
|
||||
| ツール数 | 6 (bash, read, write, edit, glob, todo_write) | 7 (+task) |
|
||||
| 新規関数 | — | spawn_subagent(独立 messages[] + 30 ラウンド安全制限) |
|
||||
| コンテキスト隔離 | すべてメイン会話内 | サブエージェントが新規 messages[] を使用 |
|
||||
| ループ | 不変 | ディスパッチは不変、サブエージェントに独立した SUB_SYSTEM とフック保護されたループ |
|
||||
|
||||
---
|
||||
|
||||
## 試してみよう
|
||||
|
||||
```sh
|
||||
cd learn-claude-code
|
||||
python s06_subagent/code.py
|
||||
```
|
||||
|
||||
以下のプロンプトを試してみよう:
|
||||
|
||||
1. `Use a subtask to find what testing framework this project uses`(サブエージェントがファイルを読み、メイン Agent は結論のみ受け取る)
|
||||
2. `Delegate: read all .py files in agents/ and summarize what each one does`
|
||||
3. `Use a task to create s06_subagent/example/string_tools.py with a slugify(text: str) function, then verify it from the parent agent`
|
||||
|
||||
観察のポイント:`[Subagent spawned]` / `[Subagent done]` が表示されるか? サブエージェントのツール呼び出しが `[sub] ...` として出力されるか? 親 Agent はサブエージェントが返した要約だけを受け取って続行するか?
|
||||
|
||||
---
|
||||
|
||||
## 次へ
|
||||
|
||||
Agent はタスクを分割できるようになった。しかし各タスクに必要な知識は異なる。フロントエンドコンポーネントの変更には React 規約が必要で、SQL を書くにはテーブル構造を知る必要がある。これらの知識をすべて system prompt に詰め込むと、コンテキストが溢れてしまう。
|
||||
|
||||
→ s07 Skill Loading:スキルをオンデマンドで注入する。system prompt にドキュメントを積み上げるのではなく、必要なときだけ読み込む。ファイルを読むのと同じくらい自然に。
|
||||
|
||||
<details>
|
||||
<summary>CC ソースコードを深掘り</summary>
|
||||
|
||||
> 以下は CC ソースコード `AgentTool.tsx`、`runAgent.ts`、`forkSubagent.ts`、`forkedAgent.ts` の完全分析に基づく。
|
||||
|
||||
### 一、一つのパターンではなく三つ
|
||||
|
||||
教育版は「新規 messages[]」のみを取り上げる。CC には実際に三つの実行モードがある:
|
||||
|
||||
| モード | トリガー | コンテキスト |
|
||||
|--------|---------|-------------|
|
||||
| **Normal Subagent** | `subagent_type` 指定時(normal path) | 新規 messages[]、プロンプトのみ |
|
||||
| **Fork Subagent** | `subagent_type` 未指定、fork gate 有効時 | `buildForkedMessages()` でキャッシュフレンドリーなプレフィックスを構築、プロンプトキャッシュを共有 |
|
||||
| **General-Purpose** | `subagent_type` 未指定、fork gate 無効時 | Normal と同じ |
|
||||
|
||||
### 二、Fork モード:プロンプトキャッシュの共有のため
|
||||
|
||||
これは教育版にはない核心概念。Fork モード(`forkSubagent.ts:60-71`)は新規コンテキストを作成せず、`buildForkedMessages()`(`forkSubagent.ts:107-168`)でキャッシュフレンドリーなメッセージプレフィックスを構築する。親の assistant message を保持し、placeholder tool results を生成する。目的は隔離ではなく、Anthropic API のプロンプトキャッシュをヒットさせること:親子 Agent の system prompt、tools、messages プレフィックスがバイトレベルで一致するため、API 側で再計算が不要になる。
|
||||
|
||||
キャッシュヒットの五つの重要コンポーネント(`forkedAgent.ts:57-68`):system prompt、tools、model、messages プレフィックス、thinking config、バイトレベルで一致する必要がある。
|
||||
|
||||
### 三、コンテキスト隔離の精密な粒度
|
||||
|
||||
`createSubagentContext()`(`forkedAgent.ts:345-462`)はサブエージェントの `ToolUseContext` を作成:
|
||||
|
||||
| フィールド | 挙動 |
|
||||
|-----------|------|
|
||||
| `abortController` | 新しい子コントローラ、親の abort は下に伝播 |
|
||||
| `setAppState` | デフォルトは no-op、ただし sync agent は `shareSetAppState` で共有(`runAgent.ts:697-714`) |
|
||||
| `readFileState` | **親からクローン**(同じファイルの再読み込みを回避) |
|
||||
| `queryTracking` | 新しい chainId、`depth = parentDepth + 1` |
|
||||
|
||||
サブエージェントは完全に隔離されているわけではない。ファイル読み取り状態は共有される。UI と通知の隔離度は実行パスにより異なる(sync/async/fork/teammate でそれぞれ異なる)。
|
||||
|
||||
### 四、再帰 Fork 防護
|
||||
|
||||
教育版は「サブエージェントに task ツールなし」で再帰防止を表現する。実際の実装はより精密:`isInForkChild()`(`forkSubagent.ts:78-89`)が会話履歴内の `FORK_BOILERPLATE_TAG` をチェックする。しかし `constants/tools.ts:36-46` では `Agent` ツールが全エージェントの無効セットにデフォルト設定(`USER_TYPE === 'ant'` 時は例外)、`forkSubagent.ts:73-89` は fork child 向けの専用再帰保護があり、`agentToolUtils.ts:100-110` は teammate シナリオで特別な許可がある。単純な「サブエージェントの再 spawn 禁止」ではない。
|
||||
|
||||
### 五、Permission Bubbling
|
||||
|
||||
Fork Agent の `permissionMode: 'bubble'`(`forkSubagent.ts:67`)は、サブエージェントの権限プロンプトが親ターミナルにバブルアップすることを意味する。ユーザーはメインターミナルでサブエージェントの操作を承認する。
|
||||
|
||||
### 六、Async vs Sync
|
||||
|
||||
教育版は同期サブエージェントのみ(親が子の完了を待つ)を示す。CC は非同期パスもサポート(`AgentTool.tsx:686-764`):`run_in_background: true` の場合、サブエージェントは非同期で起動し、`{ status: 'async_launched' }` を直ちに親に返し、完了時に通知機構で親に知らせる。実際のトリガーは `run_in_background` だけでなく、auto-background、assistant force async、coordinator/proactive パスもある。
|
||||
|
||||
### 教育版の簡略化は意図的
|
||||
|
||||
- 三つのモード → 一つ(新規 messages):概念的に明確
|
||||
- プロンプトキャッシュ共有 → 省略:教育版は API 層の最適化を扱わない
|
||||
- 再帰 fork 防護 → 「サブエージェントに task ツールなし」に簡略化
|
||||
- Async → 省略(s13 に委ねる):s06 はまず同期モデルを理解する
|
||||
|
||||
</details>
|
||||
|
||||
<!-- translation-sync: zh@v1, en@v1, ja@v1 -->
|
||||
193
s06_subagent/README.md
Normal file
193
s06_subagent/README.md
Normal file
@@ -0,0 +1,193 @@
|
||||
# s06: Subagent — 大任务拆小,每个拿到的都是干净上下文
|
||||
|
||||
[中文](README.md) · [English](README.en.md) · [日本語](README.ja.md)
|
||||
|
||||
s01 → s02 → s03 → s04 → s05 → `s06` → [s07](../s07_skill_loading/) → s08 → ... → s20
|
||||
|
||||
> *"大任务拆小, 每个小任务干净的上下文"* — Subagent 用独立 messages[], 不污染主对话。
|
||||
>
|
||||
> **Harness 层**: 子 Agent — 上下文隔离, 注意力不漂移。
|
||||
|
||||
---
|
||||
|
||||
## 问题
|
||||
|
||||
Agent 在修一个 bug。它读了 30 个文件来追踪调用链,中间聊了 60 轮。messages 列表涨到 120 条,其中大部分是"追踪调用链"的中间过程,和"修 bug"这个最终目标无关。
|
||||
|
||||
这些中间过程占着上下文位置,让 Agent 越来越"健忘",它记不住最初的问题是什么了。
|
||||
|
||||
换个角度:你修 bug 的时候,会"开一个新终端"来追踪调用链。追踪完了,终端关掉,结果写进笔记,回到原来的终端继续修 bug。Agent 也需要这个能力:开一个独立的子进程,给它一个独立的消息列表,让它专心做一件事。
|
||||
|
||||
---
|
||||
|
||||
## 解决方案
|
||||
|
||||

|
||||
|
||||
保留上一章的最小 hook 结构和 `todo_write` 工具,本章重点转向新增的 `task` 工具。调用它时,spawn 一个子 Agent,拥有全新的 `messages[]`,跑自己的循环,结束后只把摘要文本回传给主 Agent。对话上下文被丢弃,但文件系统的副作用(写文件、改文件、跑命令)保留在工作目录中。
|
||||
|
||||
子 Agent 的工具受限:有 bash/read/write/edit/glob,但没有 task,不能递归 spawn 新的子 Agent。子 Agent 的工具调用仍经过权限 hook,安全策略不因上下文隔离而跳过。
|
||||
|
||||
---
|
||||
|
||||
## 工作原理
|
||||
|
||||
**spawn_subagent**,给子 Agent 一个全新的 messages 列表,跑自己的循环,只回传结论:
|
||||
|
||||
```python
|
||||
def spawn_subagent(description: str) -> str:
|
||||
# 子 Agent 的工具:基础工具,但没有 task(禁止递归)
|
||||
sub_tools = [
|
||||
{"name": "bash", ...}, {"name": "read_file", ...},
|
||||
{"name": "write_file", ...}, {"name": "edit_file", ...},
|
||||
{"name": "glob", ...},
|
||||
]
|
||||
messages = [{"role": "user", "content": description}] # 全新 messages[]
|
||||
|
||||
for _ in range(30): # safety limit
|
||||
response = client.messages.create(
|
||||
model=MODEL, system=SUB_SYSTEM,
|
||||
messages=messages, tools=sub_tools, max_tokens=8000,
|
||||
)
|
||||
messages.append({"role": "assistant", "content": response.content})
|
||||
if response.stop_reason != "tool_use":
|
||||
break
|
||||
results = []
|
||||
for block in response.content:
|
||||
if block.type == "tool_use":
|
||||
blocked = trigger_hooks("PreToolUse", block)
|
||||
if blocked:
|
||||
results.append({... "content": str(blocked)})
|
||||
continue
|
||||
handler = SUB_HANDLERS.get(block.name)
|
||||
output = handler(**block.input) if handler else f"Unknown"
|
||||
trigger_hooks("PostToolUse", block, output)
|
||||
results.append({... "content": output})
|
||||
messages.append({"role": "user", "content": results})
|
||||
|
||||
# 只返回最后的文本结论,中间过程全部丢弃
|
||||
return extract_text(messages[-1]["content"])
|
||||
```
|
||||
|
||||
主 Agent 调用时,跟调其他工具一样:
|
||||
|
||||
```python
|
||||
TOOLS = [
|
||||
{"name": "bash", ...},
|
||||
{"name": "read_file", ...},
|
||||
{"name": "write_file", ...},
|
||||
{"name": "edit_file", ...},
|
||||
{"name": "glob", ...},
|
||||
{"name": "todo_write", ...},
|
||||
# s06: 新增 task 工具
|
||||
{"name": "task",
|
||||
"description": "Launch a subagent to handle a complex subtask. Returns only the final conclusion.",
|
||||
"input_schema": {"type": "object", "properties": {"description": {"type": "string"}}, "required": ["description"]}},
|
||||
]
|
||||
|
||||
TOOL_HANDLERS["task"] = spawn_subagent
|
||||
```
|
||||
|
||||
三个关键设计决策:
|
||||
|
||||
| 决策 | 选择 | 原因 |
|
||||
|------|------|------|
|
||||
| 上下文隔离 | 全新 `messages[]` | 子 Agent 的中间过程不污染主 Agent 的上下文 |
|
||||
| 只回传结论 | `extract_text(last_message)` | 不是回传整个 messages 列表 |
|
||||
| 禁止递归 | 子 Agent 无 task 工具 | 防止子 Agent 再 spawn 新的子 Agent |
|
||||
| 安全策略不跳过 | 子 Agent 工具调用也走 PreToolUse hook | 上下文隔离不代表权限隔离 |
|
||||
|
||||
dispatch 机制不变,task 工具通过 `TOOL_HANDLERS[block.name]` 分发。子 Agent 有独立的 `SUB_SYSTEM` 提示,明确要求"直接完成任务,不要再委派"。
|
||||
|
||||
---
|
||||
|
||||
## 相对 s05 的变更
|
||||
|
||||
| 组件 | 之前 (s05) | 之后 (s06) |
|
||||
|------|-----------|-----------|
|
||||
| 工具数量 | 6 (bash, read, write, edit, glob, todo_write) | 7 (+task) |
|
||||
| 新函数 | — | spawn_subagent(独立 messages[] + 30 轮安全限制) |
|
||||
| 上下文隔离 | 全部在主对话中 | 子 Agent 用全新的 messages[] |
|
||||
| 循环 | 不变 | dispatch 不变,子 Agent 有独立 SUB_SYSTEM 和 hook 保护的循环 |
|
||||
|
||||
---
|
||||
|
||||
## 试一下
|
||||
|
||||
```sh
|
||||
cd learn-claude-code
|
||||
python s06_subagent/code.py
|
||||
```
|
||||
|
||||
试试这些 prompt:
|
||||
|
||||
1. `Use a subtask to find what testing framework this project uses`(子 Agent 去读文件,主 Agent 只收结论)
|
||||
2. `Delegate: read all .py files in agents/ and summarize what each one does`
|
||||
3. `Use a task to create s06_subagent/example/string_tools.py with a slugify(text: str) function, then verify it from the parent agent`
|
||||
|
||||
观察重点:是否出现 `[Subagent spawned]` / `[Subagent done]`?子 Agent 的工具调用是否以 `[sub] ...` 输出?主 Agent 最后是否只继续处理子 Agent 返回的摘要?
|
||||
|
||||
---
|
||||
|
||||
## 接下来
|
||||
|
||||
Agent 现在能拆任务了。但每个任务需要的知识不一样:改前端组件需要知道 React 规范,写 SQL 需要知道表结构。这些知识全塞进 system prompt,上下文直接爆了。
|
||||
|
||||
s07 Skill Loading → 技能按需注入,不在 system prompt 里堆文档。用到的时候才加载,和读文件一样自然。
|
||||
|
||||
<details>
|
||||
<summary>深入 CC 源码</summary>
|
||||
|
||||
> 以下基于 CC 源码 `AgentTool.tsx`、`runAgent.ts`、`forkSubagent.ts`、`forkedAgent.ts` 的完整分析。
|
||||
|
||||
### 一、不是一种模式,是三种
|
||||
|
||||
教学版只讲了"全新的 messages[]"。CC 实际有三种执行模式:
|
||||
|
||||
| 模式 | 触发条件 | 上下文 |
|
||||
|------|---------|--------|
|
||||
| **Normal Subagent** | 指定了 `subagent_type`(normal path) | 全新 messages[],只有 prompt |
|
||||
| **Fork Subagent** | 没指定 `subagent_type`,fork gate 开启 | 通过 `buildForkedMessages()` 构造 cache-friendly 前缀,共享 prompt cache |
|
||||
| **General-Purpose** | 没指定 `subagent_type`,fork gate 关闭 | 同 Normal |
|
||||
|
||||
### 二、Fork 模式:为了共享 Prompt Cache
|
||||
|
||||
这是教学版没有的核心概念。Fork 模式(`forkSubagent.ts:60-71`)不创建全新上下文,而是通过 `buildForkedMessages()`(`forkSubagent.ts:107-168`)构造 cache-friendly 消息前缀,保留父 assistant message 并生成 placeholder tool results。目的不是隔离,而是让 Anthropic API 的 prompt cache 命中:父子 Agent 的 system prompt、tools、messages 前缀完全一致,API 端不需要重算。
|
||||
|
||||
缓存命中的五个关键组件(`forkedAgent.ts:57-68`):system prompt、tools、model、messages 前缀、thinking config,必须字节级一致。
|
||||
|
||||
### 三、Context Isolation 的精确粒度
|
||||
|
||||
`createSubagentContext()`(`forkedAgent.ts:345-462`)创建子 Agent 的 `ToolUseContext`:
|
||||
|
||||
| 字段 | 行为 |
|
||||
|------|------|
|
||||
| `abortController` | 新的 child controller,父 abort 向下传播 |
|
||||
| `setAppState` | 默认 no-op;但 sync agent 通过 `shareSetAppState` 共享(`runAgent.ts:697-714`) |
|
||||
| `readFileState` | **从父克隆**(避免重复读相同文件) |
|
||||
| `queryTracking` | 新 chainId,`depth = parentDepth + 1` |
|
||||
|
||||
子 Agent 不是完全隔离的:文件读取状态是共享的。UI 和通知的隔离程度取决于执行路径(sync/async/fork/teammate 各不同)。
|
||||
|
||||
### 四、递归 Fork 防护
|
||||
|
||||
教学版用"子 Agent 不给 task 工具"表达递归保护。真实实现更精细:`isInForkChild()`(`forkSubagent.ts:78-89`)检查对话历史中是否有 `FORK_BOILERPLATE_TAG`,有就拒绝。但 `constants/tools.ts:36-46` 中 `Agent` 工具默认在所有 agent 的禁用集合里,`USER_TYPE === 'ant'` 时例外;`forkSubagent.ts:73-89` 针对 fork child 有专门的递归保护;`agentToolUtils.ts:100-110` 在 teammate 场景下有特殊放行。不是简单的"禁止新的子 Agent"。
|
||||
|
||||
### 五、Permission Bubbling
|
||||
|
||||
Fork Agent 的 `permissionMode: 'bubble'`(`forkSubagent.ts:67`)意味着子 Agent 的权限弹窗冒泡到父终端,用户在主终端里审批子 Agent 的操作。
|
||||
|
||||
### 六、Async vs Sync
|
||||
|
||||
教学版只展示了同步子 Agent(父等着子跑完)。CC 还支持异步路径(`AgentTool.tsx:686-764`):`run_in_background: true` 时异步启动,返回 `{ status: 'async_launched' }` 立即给父 Agent,子 Agent 完成后通过通知机制告知父 Agent。实际触发条件不止 `run_in_background`,还有 auto-background、assistant force async、coordinator/proactive 等路径。
|
||||
|
||||
### 教学版的简化是刻意的
|
||||
|
||||
- 三种模式 → 一种(fresh messages):概念清晰
|
||||
- Prompt cache 共享 → 省略:教学版不涉及 API 层优化
|
||||
- 递归 fork 防护 → 简化为"子 Agent 无 task 工具"
|
||||
- Async → 省略(留给 s13):s06 先理解同步模型
|
||||
|
||||
</details>
|
||||
|
||||
<!-- translation-sync: zh@v1, en@v0, ja@v0 -->
|
||||
365
s06_subagent/code.py
Normal file
365
s06_subagent/code.py
Normal file
@@ -0,0 +1,365 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
s06: Subagent — spawn sub-agents with fresh messages[] for context isolation.
|
||||
|
||||
Parent Agent Subagent
|
||||
+------------------+ +------------------+
|
||||
| messages=[...] | | messages=[task] | <-- fresh
|
||||
| | dispatch | |
|
||||
| tool: task | ---------------> | own while loop |
|
||||
| prompt="..." | | bash/read/... |
|
||||
| | summary only | (max 30 turns) |
|
||||
| result = "..." | <--------------- | return last text |
|
||||
+------------------+ +------------------+
|
||||
^ |
|
||||
| intermediate results DISCARDED |
|
||||
+--------------------------------------+
|
||||
|
||||
Subagent tools: bash, read, write, edit, glob (NO task — no recursion)
|
||||
|
||||
Changes from s05:
|
||||
+ task tool + spawn_subagent() with fresh messages[]
|
||||
+ Safety limit: max 30 turns per subagent
|
||||
+ extract_text() helper
|
||||
Subagent cannot spawn sub-subagents (no task tool in sub_tools).
|
||||
Main loop unchanged: task auto-dispatches via TOOL_HANDLERS.
|
||||
|
||||
Run: python s06_subagent/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"]
|
||||
|
||||
SYSTEM = (
|
||||
f"You are a coding agent at {WORKDIR}. "
|
||||
"For complex sub-problems, use the task tool to spawn a subagent."
|
||||
)
|
||||
|
||||
# s06: subagent gets its own system prompt — no task, no recursion
|
||||
SUB_SYSTEM = (
|
||||
f"You are a coding agent at {WORKDIR}. "
|
||||
"Complete the task you were given, then return a concise summary. "
|
||||
"Do not delegate further."
|
||||
)
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
# FROM s02-s05 (unchanged): Tool Implementations
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
|
||||
def safe_path(p: str) -> Path:
|
||||
path = (WORKDIR / p).resolve()
|
||||
if not path.is_relative_to(WORKDIR):
|
||||
raise ValueError(f"Path escapes workspace: {p}")
|
||||
return path
|
||||
|
||||
def run_bash(command: str) -> str:
|
||||
try:
|
||||
r = subprocess.run(command, shell=True, cwd=WORKDIR,
|
||||
capture_output=True, text=True, timeout=120)
|
||||
out = (r.stdout + r.stderr).strip()
|
||||
return out[:50000] if out else "(no output)"
|
||||
except subprocess.TimeoutExpired:
|
||||
return "Error: Timeout (120s)"
|
||||
|
||||
def run_read(path: str, limit: int | None = None) -> str:
|
||||
try:
|
||||
lines = safe_path(path).read_text().splitlines()
|
||||
if limit and limit < len(lines):
|
||||
lines = lines[:limit] + [f"... ({len(lines) - limit} more lines)"]
|
||||
return "\n".join(lines)
|
||||
except Exception as e:
|
||||
return f"Error: {e}"
|
||||
|
||||
def run_write(path: str, content: str) -> str:
|
||||
try:
|
||||
file_path = safe_path(path)
|
||||
file_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
file_path.write_text(content)
|
||||
return f"Wrote {len(content)} bytes to {path}"
|
||||
except Exception as e:
|
||||
return f"Error: {e}"
|
||||
|
||||
def run_edit(path: str, old_text: str, new_text: str) -> str:
|
||||
try:
|
||||
file_path = safe_path(path)
|
||||
text = file_path.read_text()
|
||||
if old_text not in text:
|
||||
return f"Error: text not found in {path}"
|
||||
file_path.write_text(text.replace(old_text, new_text, 1))
|
||||
return f"Edited {path}"
|
||||
except Exception as e:
|
||||
return f"Error: {e}"
|
||||
|
||||
def run_glob(pattern: str) -> str:
|
||||
import glob as g
|
||||
try:
|
||||
results = []
|
||||
for match in g.glob(pattern, root_dir=WORKDIR):
|
||||
if (WORKDIR / match).resolve().is_relative_to(WORKDIR):
|
||||
results.append(match)
|
||||
return "\n".join(results) if results else "(no matches)"
|
||||
except Exception as e:
|
||||
return f"Error: {e}"
|
||||
|
||||
def run_todo_write(todos: list) -> str:
|
||||
for i, t in enumerate(todos):
|
||||
if "content" not in t or "status" not in t:
|
||||
return f"Error: todos[{i}] missing 'content' or 'status'"
|
||||
if t["status"] not in ("pending", "in_progress", "completed"):
|
||||
return f"Error: todos[{i}] has invalid status '{t['status']}'"
|
||||
tasks_file = TASKS_DIR / "current_todos.json"
|
||||
tasks_file.write_text(json.dumps(todos, indent=2, ensure_ascii=False))
|
||||
lines = ["\n\033[33m## Current Tasks\033[0m"]
|
||||
for t in todos:
|
||||
icon = {"pending": " ", "in_progress": "\033[36m▸\033[0m", "completed": "\033[32m✓\033[0m"}[t["status"]]
|
||||
lines.append(f" [{icon}] {t['content']}")
|
||||
print("\n".join(lines))
|
||||
return f"Updated {len(todos)} tasks"
|
||||
|
||||
def extract_text(content) -> str:
|
||||
"""Extract text from message content blocks."""
|
||||
if not isinstance(content, list):
|
||||
return str(content)
|
||||
return "\n".join(getattr(b, "text", "") for b in content if getattr(b, "type", None) == "text")
|
||||
|
||||
TOOLS = [
|
||||
{"name": "bash", "description": "Run a shell command.",
|
||||
"input_schema": {"type": "object", "properties": {"command": {"type": "string"}}, "required": ["command"]}},
|
||||
{"name": "read_file", "description": "Read file contents.",
|
||||
"input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "limit": {"type": "integer"}}, "required": ["path"]}},
|
||||
{"name": "write_file", "description": "Write content to a file.",
|
||||
"input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "content": {"type": "string"}}, "required": ["path", "content"]}},
|
||||
{"name": "edit_file", "description": "Replace exact text in a file once.",
|
||||
"input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "old_text": {"type": "string"}, "new_text": {"type": "string"}}, "required": ["path", "old_text", "new_text"]}},
|
||||
{"name": "glob", "description": "Find files matching a glob pattern.",
|
||||
"input_schema": {"type": "object", "properties": {"pattern": {"type": "string"}}, "required": ["pattern"]}},
|
||||
{"name": "todo_write", "description": "Create and manage a task list for your current coding session.",
|
||||
"input_schema": {"type": "object", "properties": {"todos": {"type": "array", "items": {"type": "object", "properties": {"content": {"type": "string"}, "status": {"type": "string", "enum": ["pending", "in_progress", "completed"]}}, "required": ["content", "status"]}}}, "required": ["todos"]}},
|
||||
]
|
||||
|
||||
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,
|
||||
}
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
# NEW in s06: Subagent — fresh messages[], summary only
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
|
||||
SUB_TOOLS = [
|
||||
{"name": "bash", "description": "Run a shell command.",
|
||||
"input_schema": {"type": "object", "properties": {"command": {"type": "string"}}, "required": ["command"]}},
|
||||
{"name": "read_file", "description": "Read file contents.",
|
||||
"input_schema": {"type": "object", "properties": {"path": {"type": "string"}}, "required": ["path"]}},
|
||||
{"name": "write_file", "description": "Write content to a file.",
|
||||
"input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "content": {"type": "string"}}, "required": ["path", "content"]}},
|
||||
{"name": "edit_file", "description": "Replace exact text in a file once.",
|
||||
"input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "old_text": {"type": "string"}, "new_text": {"type": "string"}}, "required": ["path", "old_text", "new_text"]}},
|
||||
{"name": "glob", "description": "Find files matching a glob pattern.",
|
||||
"input_schema": {"type": "object", "properties": {"pattern": {"type": "string"}}, "required": ["pattern"]}},
|
||||
]
|
||||
# NO "task" tool — prevent recursive spawning
|
||||
|
||||
SUB_HANDLERS = {
|
||||
"bash": run_bash, "read_file": run_read, "write_file": run_write,
|
||||
"edit_file": run_edit, "glob": run_glob,
|
||||
}
|
||||
|
||||
def spawn_subagent(description: str) -> str:
|
||||
"""Spawn a subagent with fresh messages[], return summary only."""
|
||||
print(f"\n\033[35m[Subagent spawned]\033[0m")
|
||||
messages = [{"role": "user", "content": description}] # fresh context
|
||||
|
||||
for _ in range(30): # safety limit
|
||||
response = client.messages.create(
|
||||
model=MODEL, system=SUB_SYSTEM,
|
||||
messages=messages, tools=SUB_TOOLS, max_tokens=8000,
|
||||
)
|
||||
messages.append({"role": "assistant", "content": response.content})
|
||||
if response.stop_reason != "tool_use":
|
||||
break
|
||||
results = []
|
||||
for block in response.content:
|
||||
if block.type == "tool_use":
|
||||
# Issue 1: subagent also runs hooks (permissions apply)
|
||||
blocked = trigger_hooks("PreToolUse", block)
|
||||
if blocked:
|
||||
results.append({"type": "tool_result", "tool_use_id": block.id,
|
||||
"content": str(blocked)})
|
||||
continue
|
||||
handler = SUB_HANDLERS.get(block.name)
|
||||
output = handler(**block.input) if handler else f"Unknown: {block.name}"
|
||||
trigger_hooks("PostToolUse", block, output)
|
||||
print(f" \033[90m[sub] {block.name}: {str(output)[:100]}\033[0m")
|
||||
results.append({"type": "tool_result", "tool_use_id": block.id,
|
||||
"content": output})
|
||||
messages.append({"role": "user", "content": results})
|
||||
|
||||
# Issue 5: fallback if safety limit hit during tool_use
|
||||
result = extract_text(messages[-1]["content"])
|
||||
if not result:
|
||||
# last message is tool_result, look backwards for assistant text
|
||||
for msg in reversed(messages):
|
||||
if msg["role"] == "assistant":
|
||||
result = extract_text(msg["content"])
|
||||
if result:
|
||||
break
|
||||
if not result:
|
||||
result = "Subagent stopped after 30 turns without final answer."
|
||||
print(f"\033[35m[Subagent done]\033[0m")
|
||||
return result # only summary, entire message history discarded
|
||||
|
||||
# Add task tool to parent's tools
|
||||
TOOLS.append({
|
||||
"name": "task",
|
||||
"description": "Launch a subagent to handle a complex subtask. Returns only the final conclusion.",
|
||||
"input_schema": {"type": "object", "properties": {"description": {"type": "string"}}, "required": ["description"]},
|
||||
})
|
||||
TOOL_HANDLERS["task"] = spawn_subagent
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
# FROM s04 (unchanged): Hook System
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
|
||||
HOOKS = {"UserPromptSubmit": [], "PreToolUse": [], "PostToolUse": [], "Stop": []}
|
||||
|
||||
def register_hook(event: str, callback):
|
||||
HOOKS[event].append(callback)
|
||||
|
||||
def trigger_hooks(event: str, *args):
|
||||
for callback in HOOKS[event]:
|
||||
result = callback(*args)
|
||||
if result is not None:
|
||||
return result
|
||||
return None
|
||||
|
||||
DENY_LIST = ["rm -rf /", "sudo", "shutdown", "reboot", "mkfs", "dd if="]
|
||||
|
||||
def permission_hook(block):
|
||||
"""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 s05 + nag reminder, task auto-dispatches
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
|
||||
rounds_since_todo = 0
|
||||
|
||||
def agent_loop(messages: list):
|
||||
global rounds_since_todo
|
||||
while True:
|
||||
# s05: nag reminder
|
||||
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)
|
||||
|
||||
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("s06: Subagent — spawn sub-agents with fresh context, summary only")
|
||||
print("Type a question, press Enter. Type q to quit.\n")
|
||||
|
||||
history = []
|
||||
while True:
|
||||
try:
|
||||
query = input("\033[36ms06 >> \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()
|
||||
125
s06_subagent/images/subagent-overview.en.svg
Normal file
125
s06_subagent/images/subagent-overview.en.svg
Normal file
@@ -0,0 +1,125 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 500" 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-purple" 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="#7c3aed"/>
|
||||
</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="500" 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">Subagent — Independent messages[], All Intermediate Steps Discarded</text>
|
||||
|
||||
<!-- ===== Parent Agent (left) ===== -->
|
||||
<rect x="30" y="68" width="310" height="268" rx="12" fill="#f0f4ff" stroke="#2563eb" stroke-width="2"/>
|
||||
<text x="185" y="92" fill="#1e3a5f" font-size="13" font-weight="700" text-anchor="middle">Parent Agent</text>
|
||||
|
||||
<!-- messages[] -->
|
||||
<rect x="50" y="100" width="110" height="36" rx="6" fill="#fff" stroke="#2563eb" stroke-width="1.5"/>
|
||||
<text x="105" y="123" fill="#1e3a5f" font-size="11" font-weight="600" text-anchor="middle">messages[]</text>
|
||||
|
||||
<!-- → LLM -->
|
||||
<line x1="160" y1="118" x2="198" y2="118" stroke="#2563eb" stroke-width="1.5" marker-end="url(#arrow-blue)"/>
|
||||
|
||||
<!-- LLM -->
|
||||
<rect x="200" y="96" width="100" height="44" rx="6" fill="#fff" stroke="#2563eb" stroke-width="1.5"/>
|
||||
<text x="250" y="123" fill="#1e3a5f" font-size="12" font-weight="700" text-anchor="middle">LLM</text>
|
||||
|
||||
<!-- LLM → dispatch -->
|
||||
<line x1="250" y1="140" x2="250" y2="156" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
<text x="264" y="152" fill="#64748b" font-size="8">tool_use</text>
|
||||
|
||||
<!-- TOOL_HANDLERS -->
|
||||
<rect x="50" y="158" width="280" height="74" rx="6" fill="#e0e7ff" stroke="#2563eb" stroke-width="1"/>
|
||||
<text x="190" y="176" fill="#1e3a5f" font-size="10" font-weight="600" text-anchor="middle">TOOL_HANDLERS</text>
|
||||
|
||||
<!-- Other tools (gray) -->
|
||||
<rect x="65" y="190" width="115" height="32" rx="4" fill="#f0f4ff" stroke="#2563eb" stroke-width="1"/>
|
||||
<text x="122" y="206" fill="#1e3a5f" font-size="10" font-weight="600" text-anchor="middle">Base Tools</text>
|
||||
<text x="122" y="219" fill="#64748b" font-size="8" text-anchor="middle">bash / read / write / ...</text>
|
||||
|
||||
<!-- task → spawn -->
|
||||
<rect x="200" y="193" width="110" height="26" rx="4" fill="#ede9fe" stroke="#7c3aed" stroke-width="1.5"/>
|
||||
<text x="255" y="210" fill="#5b21b6" font-size="10" font-weight="600" text-anchor="middle">task → spawn</text>
|
||||
|
||||
<!-- Parent tool_result target -->
|
||||
<rect x="190" y="270" width="120" height="34" rx="5" fill="#dcfce7" stroke="#16a34a" stroke-width="1.2"/>
|
||||
<text x="250" y="291" fill="#166534" font-size="9" font-weight="700" text-anchor="middle">tool_result</text>
|
||||
<path d="M 250 232 L 250 270" fill="none" stroke="#16a34a" stroke-width="1.4" marker-end="url(#arrow-green)"/>
|
||||
<path d="M 190 287 L 42 287 L 42 118 L 50 118" fill="none" stroke="#16a34a" stroke-width="1.2" marker-end="url(#arrow-green)" stroke-dasharray="4,3"/>
|
||||
<text x="110" y="304" fill="#94a3b8" font-size="8" text-anchor="middle">append messages[]</text>
|
||||
<text x="210" y="256" fill="#94a3b8" font-size="8" text-anchor="middle">Normal tool results also append to messages[]</text>
|
||||
|
||||
<!-- ===== Subagent (right) ===== -->
|
||||
<rect x="430" y="68" width="340" height="268" rx="12" fill="#faf5ff" stroke="#7c3aed" stroke-width="2"/>
|
||||
<text x="600" y="92" fill="#5b21b6" font-size="13" font-weight="700" text-anchor="middle">Subagent (Fresh Context)</text>
|
||||
|
||||
<!-- fresh messages -->
|
||||
<rect x="450" y="100" width="150" height="36" rx="6" fill="#fff" stroke="#7c3aed" stroke-width="1.5"/>
|
||||
<text x="525" y="116" fill="#5b21b6" font-size="10" font-weight="600" text-anchor="middle">messages = [task]</text>
|
||||
<text x="525" y="130" fill="#7c3aed" font-size="8" text-anchor="middle">fresh — no parent history</text>
|
||||
|
||||
<!-- → LLM -->
|
||||
<line x1="600" y1="118" x2="648" y2="118" stroke="#7c3aed" stroke-width="1.5" marker-end="url(#arrow-purple)"/>
|
||||
|
||||
<!-- Subagent LLM -->
|
||||
<rect x="650" y="96" width="100" height="44" rx="6" fill="#fff" stroke="#7c3aed" stroke-width="1.5"/>
|
||||
<text x="700" y="123" fill="#5b21b6" font-size="12" font-weight="700" text-anchor="middle">LLM</text>
|
||||
|
||||
<!-- own loop -->
|
||||
<rect x="455" y="150" width="300" height="56" rx="8" fill="#ede9fe" stroke="#7c3aed" stroke-width="1" stroke-dasharray="4,2"/>
|
||||
<text x="605" y="170" fill="#5b21b6" font-size="10" font-weight="600" text-anchor="middle">Own while loop (max 30 rounds)</text>
|
||||
<text x="605" y="186" fill="#5b21b6" font-size="9" text-anchor="middle">bash · read · write · edit · glob</text>
|
||||
<text x="605" y="198" fill="#94a3b8" font-size="8" text-anchor="middle">No task — recursive spawn forbidden</text>
|
||||
|
||||
<!-- intermediate results → discard -->
|
||||
<rect x="460" y="218" width="290" height="44" rx="6" fill="#fef2f2" stroke="#dc2626" stroke-width="1" stroke-dasharray="4,2"/>
|
||||
<text x="605" y="238" fill="#64748b" font-size="10" text-anchor="middle">Intermediate 30+ tool calls + results</text>
|
||||
<text x="605" y="254" fill="#dc2626" font-size="10" font-weight="600" text-anchor="middle">All discarded ✗</text>
|
||||
|
||||
<!-- extract only last text -->
|
||||
<rect x="460" y="272" width="290" height="28" rx="6" fill="#dcfce7" stroke="#16a34a" stroke-width="1.5"/>
|
||||
<text x="605" y="290" fill="#166534" font-size="10" font-weight="600" text-anchor="middle">✓ Extract only final text → return to Parent</text>
|
||||
|
||||
<!-- ===== dispatch line: Parent → Subagent (top) ===== -->
|
||||
<path d="M 310 206 L 362 206 Q 370 206 370 198 L 370 126 Q 370 118 378 118 L 450 118" fill="none" stroke="#7c3aed" stroke-width="2.5" marker-end="url(#arrow-purple)"/>
|
||||
<rect x="342" y="152" width="80" height="20" rx="4" fill="#faf5ff" stroke="#7c3aed" stroke-width="1"/>
|
||||
<text x="382" y="166" fill="#7c3aed" font-size="9" font-weight="700" text-anchor="middle">① task desc</text>
|
||||
|
||||
<!-- ===== return line: Subagent → Parent tool_result ===== -->
|
||||
<path d="M 460 286 L 310 286" fill="none" stroke="#16a34a" stroke-width="2.5" marker-end="url(#arrow-green)"/>
|
||||
<rect x="350" y="268" width="70" height="20" rx="4" fill="#dcfce7" stroke="#16a34a" stroke-width="1"/>
|
||||
<text x="385" y="282" fill="#166534" font-size="9" font-weight="700" text-anchor="middle">② summary</text>
|
||||
|
||||
<!-- ===== Legend ===== -->
|
||||
<rect x="60" y="370" width="680" height="56" rx="8" fill="#f1f5f9"/>
|
||||
|
||||
<rect x="80" y="384" width="16" height="12" rx="3" fill="#f0f4ff" stroke="#2563eb" stroke-width="1"/>
|
||||
<text x="104" y="394" fill="#334155" font-size="10">s05 Preserved: loop, hooks, todo_write, 6 base tools</text>
|
||||
|
||||
<rect x="80" y="404" width="16" height="12" rx="3" fill="#ede9fe" stroke="#7c3aed" stroke-width="1"/>
|
||||
<text x="104" y="414" fill="#334155" font-size="10">s06 New: task tool + spawn_subagent() — independent messages[], returns only summary</text>
|
||||
|
||||
<!-- Data flow labels -->
|
||||
<rect x="430" y="440" width="310" height="44" rx="6" fill="#f8fafc" stroke="#e2e8f0" stroke-width="1"/>
|
||||
<text x="445" y="458" fill="#7c3aed" font-size="10" font-weight="600">① Parent → Sub:</text>
|
||||
<text x="580" y="458" fill="#64748b" font-size="10">task description (a short string)</text>
|
||||
<text x="445" y="476" fill="#16a34a" font-size="10" font-weight="600">② Sub → Parent:</text>
|
||||
<text x="580" y="476" fill="#64748b" font-size="10">extract_text() (final conclusion only)</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 8.1 KiB |
125
s06_subagent/images/subagent-overview.ja.svg
Normal file
125
s06_subagent/images/subagent-overview.ja.svg
Normal file
@@ -0,0 +1,125 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 500" 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-purple" 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="#7c3aed"/>
|
||||
</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="500" 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="14" font-weight="700" text-anchor="middle">Subagent — 独立した messages[]、中間過程はすべて破棄</text>
|
||||
|
||||
<!-- ===== 親 Agent(左側) ===== -->
|
||||
<rect x="30" y="68" width="310" height="268" rx="12" fill="#f0f4ff" stroke="#2563eb" stroke-width="2"/>
|
||||
<text x="185" y="92" fill="#1e3a5f" font-size="13" font-weight="700" text-anchor="middle">親 Agent</text>
|
||||
|
||||
<!-- messages[] -->
|
||||
<rect x="50" y="100" width="110" height="36" rx="6" fill="#fff" stroke="#2563eb" stroke-width="1.5"/>
|
||||
<text x="105" y="123" fill="#1e3a5f" font-size="11" font-weight="600" text-anchor="middle">messages[]</text>
|
||||
|
||||
<!-- → LLM -->
|
||||
<line x1="160" y1="118" x2="198" y2="118" stroke="#2563eb" stroke-width="1.5" marker-end="url(#arrow-blue)"/>
|
||||
|
||||
<!-- LLM -->
|
||||
<rect x="200" y="96" width="100" height="44" rx="6" fill="#fff" stroke="#2563eb" stroke-width="1.5"/>
|
||||
<text x="250" y="123" fill="#1e3a5f" font-size="12" font-weight="700" text-anchor="middle">LLM</text>
|
||||
|
||||
<!-- LLM → ディスパッチ -->
|
||||
<line x1="250" y1="140" x2="250" y2="156" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
<text x="264" y="152" fill="#64748b" font-size="8">tool_use</text>
|
||||
|
||||
<!-- TOOL_HANDLERS -->
|
||||
<rect x="50" y="158" width="280" height="74" rx="6" fill="#e0e7ff" stroke="#2563eb" stroke-width="1"/>
|
||||
<text x="190" y="176" fill="#1e3a5f" font-size="10" font-weight="600" text-anchor="middle">TOOL_HANDLERS</text>
|
||||
|
||||
<!-- 他のツール(灰色) -->
|
||||
<rect x="65" y="190" width="115" height="32" rx="4" fill="#f0f4ff" stroke="#2563eb" stroke-width="1"/>
|
||||
<text x="122" y="206" fill="#1e3a5f" font-size="10" font-weight="600" text-anchor="middle">基本ツール</text>
|
||||
<text x="122" y="219" fill="#64748b" font-size="8" text-anchor="middle">bash / read / write / ...</text>
|
||||
|
||||
<!-- task → spawn -->
|
||||
<rect x="200" y="193" width="110" height="26" rx="4" fill="#ede9fe" stroke="#7c3aed" stroke-width="1.5"/>
|
||||
<text x="255" y="210" fill="#5b21b6" font-size="10" font-weight="600" text-anchor="middle">task → spawn</text>
|
||||
|
||||
<!-- Parent tool_result target -->
|
||||
<rect x="190" y="270" width="120" height="34" rx="5" fill="#dcfce7" stroke="#16a34a" stroke-width="1.2"/>
|
||||
<text x="250" y="291" fill="#166534" font-size="9" font-weight="700" text-anchor="middle">tool_result</text>
|
||||
<path d="M 250 232 L 250 270" fill="none" stroke="#16a34a" stroke-width="1.4" marker-end="url(#arrow-green)"/>
|
||||
<path d="M 190 287 L 42 287 L 42 118 L 50 118" fill="none" stroke="#16a34a" stroke-width="1.2" marker-end="url(#arrow-green)" stroke-dasharray="4,3"/>
|
||||
<text x="110" y="304" fill="#94a3b8" font-size="8" text-anchor="middle">messages[] に追加</text>
|
||||
<text x="210" y="256" fill="#94a3b8" font-size="8" text-anchor="middle">通常ツール結果も messages[] に戻る</text>
|
||||
|
||||
<!-- ===== サブエージェント(右側) ===== -->
|
||||
<rect x="430" y="68" width="340" height="268" rx="12" fill="#faf5ff" stroke="#7c3aed" stroke-width="2"/>
|
||||
<text x="600" y="92" fill="#5b21b6" font-size="13" font-weight="700" text-anchor="middle">サブエージェント(新規コンテキスト)</text>
|
||||
|
||||
<!-- 新規 messages -->
|
||||
<rect x="450" y="100" width="150" height="36" rx="6" fill="#fff" stroke="#7c3aed" stroke-width="1.5"/>
|
||||
<text x="525" y="116" fill="#5b21b6" font-size="10" font-weight="600" text-anchor="middle">messages = [task]</text>
|
||||
<text x="525" y="130" fill="#7c3aed" font-size="8" text-anchor="middle">新規 — 親の会話を継承しない</text>
|
||||
|
||||
<!-- → LLM -->
|
||||
<line x1="600" y1="118" x2="648" y2="118" stroke="#7c3aed" stroke-width="1.5" marker-end="url(#arrow-purple)"/>
|
||||
|
||||
<!-- サブエージェント LLM -->
|
||||
<rect x="650" y="96" width="100" height="44" rx="6" fill="#fff" stroke="#7c3aed" stroke-width="1.5"/>
|
||||
<text x="700" y="123" fill="#5b21b6" font-size="12" font-weight="700" text-anchor="middle">LLM</text>
|
||||
|
||||
<!-- 独自ループ -->
|
||||
<rect x="455" y="150" width="300" height="56" rx="8" fill="#ede9fe" stroke="#7c3aed" stroke-width="1" stroke-dasharray="4,2"/>
|
||||
<text x="605" y="170" fill="#5b21b6" font-size="10" font-weight="600" text-anchor="middle">独自の while ループ(最大 30 ラウンド)</text>
|
||||
<text x="605" y="186" fill="#5b21b6" font-size="9" text-anchor="middle">bash · read · write · edit · glob</text>
|
||||
<text x="605" y="198" fill="#94a3b8" font-size="8" text-anchor="middle">task なし — 再帰 spawn 禁止</text>
|
||||
|
||||
<!-- 中間結果 → 破棄 -->
|
||||
<rect x="460" y="218" width="290" height="44" rx="6" fill="#fef2f2" stroke="#dc2626" stroke-width="1" stroke-dasharray="4,2"/>
|
||||
<text x="605" y="238" fill="#64748b" font-size="10" text-anchor="middle">中間 30+ ラウンドのツール呼び出し + 結果</text>
|
||||
<text x="605" y="254" fill="#dc2626" font-size="10" font-weight="600" text-anchor="middle">すべて破棄 ✗</text>
|
||||
|
||||
<!-- 最後のテキストのみ抽出 -->
|
||||
<rect x="460" y="272" width="290" height="28" rx="6" fill="#dcfce7" stroke="#16a34a" stroke-width="1.5"/>
|
||||
<text x="605" y="290" fill="#166534" font-size="10" font-weight="600" text-anchor="middle">✓ 最後のテキストのみ抽出 → 親に返却</text>
|
||||
|
||||
<!-- ===== ディスパッチ線:親 → サブエージェント(上) ===== -->
|
||||
<path d="M 310 206 L 362 206 Q 370 206 370 198 L 370 126 Q 370 118 378 118 L 450 118" fill="none" stroke="#7c3aed" stroke-width="2.5" marker-end="url(#arrow-purple)"/>
|
||||
<rect x="342" y="152" width="80" height="20" rx="4" fill="#faf5ff" stroke="#7c3aed" stroke-width="1"/>
|
||||
<text x="382" y="166" fill="#7c3aed" font-size="9" font-weight="700" text-anchor="middle">① task 説明</text>
|
||||
|
||||
<!-- ===== 返却線:サブエージェント → Parent tool_result ===== -->
|
||||
<path d="M 460 286 L 310 286" fill="none" stroke="#16a34a" stroke-width="2.5" marker-end="url(#arrow-green)"/>
|
||||
<rect x="350" y="268" width="70" height="20" rx="4" fill="#dcfce7" stroke="#16a34a" stroke-width="1"/>
|
||||
<text x="385" y="282" fill="#166534" font-size="9" font-weight="700" text-anchor="middle">② summary</text>
|
||||
|
||||
<!-- ===== 凡例 ===== -->
|
||||
<rect x="60" y="370" width="680" height="56" rx="8" fill="#f1f5f9"/>
|
||||
|
||||
<rect x="80" y="384" width="16" height="12" rx="3" fill="#f0f4ff" stroke="#2563eb" stroke-width="1"/>
|
||||
<text x="104" y="394" fill="#334155" font-size="10">s05 保持:ループ、フック、todo_write、6 つの基本ツール</text>
|
||||
|
||||
<rect x="80" y="404" width="16" height="12" rx="3" fill="#ede9fe" stroke="#7c3aed" stroke-width="1"/>
|
||||
<text x="104" y="414" fill="#334155" font-size="10">s06 新規:task ツール + spawn_subagent() — 独立 messages[]、要約のみ返却</text>
|
||||
|
||||
<!-- データフローラベル -->
|
||||
<rect x="430" y="440" width="310" height="44" rx="6" fill="#f8fafc" stroke="#e2e8f0" stroke-width="1"/>
|
||||
<text x="445" y="458" fill="#7c3aed" font-size="10" font-weight="600">① 親 → サブ:</text>
|
||||
<text x="580" y="458" fill="#64748b" font-size="10">task description(短い文字列)</text>
|
||||
<text x="445" y="476" fill="#16a34a" font-size="10" font-weight="600">② サブ → 親:</text>
|
||||
<text x="580" y="476" fill="#64748b" font-size="10">extract_text()(最終結論のみ)</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 8.4 KiB |
125
s06_subagent/images/subagent-overview.svg
Normal file
125
s06_subagent/images/subagent-overview.svg
Normal file
@@ -0,0 +1,125 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 500" 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-purple" 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="#7c3aed"/>
|
||||
</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="500" 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">Subagent — 独立 messages[],中间过程全部丢弃</text>
|
||||
|
||||
<!-- ===== Parent Agent(左侧) ===== -->
|
||||
<rect x="30" y="68" width="310" height="268" rx="12" fill="#f0f4ff" stroke="#2563eb" stroke-width="2"/>
|
||||
<text x="185" y="92" fill="#1e3a5f" font-size="13" font-weight="700" text-anchor="middle">Parent Agent</text>
|
||||
|
||||
<!-- messages[] -->
|
||||
<rect x="50" y="100" width="110" height="36" rx="6" fill="#fff" stroke="#2563eb" stroke-width="1.5"/>
|
||||
<text x="105" y="123" fill="#1e3a5f" font-size="11" font-weight="600" text-anchor="middle">messages[]</text>
|
||||
|
||||
<!-- → LLM -->
|
||||
<line x1="160" y1="118" x2="198" y2="118" stroke="#2563eb" stroke-width="1.5" marker-end="url(#arrow-blue)"/>
|
||||
|
||||
<!-- LLM -->
|
||||
<rect x="200" y="96" width="100" height="44" rx="6" fill="#fff" stroke="#2563eb" stroke-width="1.5"/>
|
||||
<text x="250" y="123" fill="#1e3a5f" font-size="12" font-weight="700" text-anchor="middle">LLM</text>
|
||||
|
||||
<!-- LLM → dispatch -->
|
||||
<line x1="250" y1="140" x2="250" y2="156" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
<text x="264" y="152" fill="#64748b" font-size="8">tool_use</text>
|
||||
|
||||
<!-- TOOL_HANDLERS -->
|
||||
<rect x="50" y="158" width="280" height="74" rx="6" fill="#e0e7ff" stroke="#2563eb" stroke-width="1"/>
|
||||
<text x="190" y="176" fill="#1e3a5f" font-size="10" font-weight="600" text-anchor="middle">TOOL_HANDLERS</text>
|
||||
|
||||
<!-- 其他工具(灰色) -->
|
||||
<rect x="65" y="190" width="115" height="32" rx="4" fill="#f0f4ff" stroke="#2563eb" stroke-width="1"/>
|
||||
<text x="122" y="206" fill="#1e3a5f" font-size="10" font-weight="600" text-anchor="middle">基础工具</text>
|
||||
<text x="122" y="219" fill="#64748b" font-size="8" text-anchor="middle">bash / read / write / ...</text>
|
||||
|
||||
<!-- task → spawn -->
|
||||
<rect x="200" y="193" width="110" height="26" rx="4" fill="#ede9fe" stroke="#7c3aed" stroke-width="1.5"/>
|
||||
<text x="255" y="210" fill="#5b21b6" font-size="10" font-weight="600" text-anchor="middle">task → spawn</text>
|
||||
|
||||
<!-- Parent tool_result target -->
|
||||
<rect x="190" y="270" width="120" height="34" rx="5" fill="#dcfce7" stroke="#16a34a" stroke-width="1.2"/>
|
||||
<text x="250" y="291" fill="#166534" font-size="9" font-weight="700" text-anchor="middle">tool_result</text>
|
||||
<path d="M 250 232 L 250 270" fill="none" stroke="#16a34a" stroke-width="1.4" marker-end="url(#arrow-green)"/>
|
||||
<path d="M 190 287 L 42 287 L 42 118 L 50 118" fill="none" stroke="#16a34a" stroke-width="1.2" marker-end="url(#arrow-green)" stroke-dasharray="4,3"/>
|
||||
<text x="110" y="304" fill="#94a3b8" font-size="8" text-anchor="middle">append messages[]</text>
|
||||
<text x="210" y="256" fill="#94a3b8" font-size="8" text-anchor="middle">普通工具结果也回填 messages[]</text>
|
||||
|
||||
<!-- ===== Subagent(右侧) ===== -->
|
||||
<rect x="430" y="68" width="340" height="268" rx="12" fill="#faf5ff" stroke="#7c3aed" stroke-width="2"/>
|
||||
<text x="600" y="92" fill="#5b21b6" font-size="13" font-weight="700" text-anchor="middle">Subagent (全新上下文)</text>
|
||||
|
||||
<!-- fresh messages -->
|
||||
<rect x="450" y="100" width="150" height="36" rx="6" fill="#fff" stroke="#7c3aed" stroke-width="1.5"/>
|
||||
<text x="525" y="116" fill="#5b21b6" font-size="10" font-weight="600" text-anchor="middle">messages = [task]</text>
|
||||
<text x="525" y="130" fill="#7c3aed" font-size="8" text-anchor="middle">fresh — 不继承父对话</text>
|
||||
|
||||
<!-- → LLM -->
|
||||
<line x1="600" y1="118" x2="648" y2="118" stroke="#7c3aed" stroke-width="1.5" marker-end="url(#arrow-purple)"/>
|
||||
|
||||
<!-- Subagent LLM -->
|
||||
<rect x="650" y="96" width="100" height="44" rx="6" fill="#fff" stroke="#7c3aed" stroke-width="1.5"/>
|
||||
<text x="700" y="123" fill="#5b21b6" font-size="12" font-weight="700" text-anchor="middle">LLM</text>
|
||||
|
||||
<!-- own loop -->
|
||||
<rect x="455" y="150" width="300" height="56" rx="8" fill="#ede9fe" stroke="#7c3aed" stroke-width="1" stroke-dasharray="4,2"/>
|
||||
<text x="605" y="170" fill="#5b21b6" font-size="10" font-weight="600" text-anchor="middle">自己的 while 循环(最多 30 轮)</text>
|
||||
<text x="605" y="186" fill="#5b21b6" font-size="9" text-anchor="middle">bash · read · write · edit · glob</text>
|
||||
<text x="605" y="198" fill="#94a3b8" font-size="8" text-anchor="middle">无 task — 禁止递归 spawn</text>
|
||||
|
||||
<!-- intermediate results → discard -->
|
||||
<rect x="460" y="218" width="290" height="44" rx="6" fill="#fef2f2" stroke="#dc2626" stroke-width="1" stroke-dasharray="4,2"/>
|
||||
<text x="605" y="238" fill="#64748b" font-size="10" text-anchor="middle">中间 30+ 轮工具调用 + 结果</text>
|
||||
<text x="605" y="254" fill="#dc2626" font-size="10" font-weight="600" text-anchor="middle">全部丢弃 ✗</text>
|
||||
|
||||
<!-- extract only last text -->
|
||||
<rect x="460" y="272" width="290" height="28" rx="6" fill="#dcfce7" stroke="#16a34a" stroke-width="1.5"/>
|
||||
<text x="605" y="290" fill="#166534" font-size="10" font-weight="600" text-anchor="middle">✓ 只提取最后一段文本 → 返回给 Parent</text>
|
||||
|
||||
<!-- ===== dispatch 线:Parent → Subagent(走上面) ===== -->
|
||||
<path d="M 310 206 L 362 206 Q 370 206 370 198 L 370 126 Q 370 118 378 118 L 450 118" fill="none" stroke="#7c3aed" stroke-width="2.5" marker-end="url(#arrow-purple)"/>
|
||||
<rect x="342" y="152" width="80" height="20" rx="4" fill="#faf5ff" stroke="#7c3aed" stroke-width="1"/>
|
||||
<text x="382" y="166" fill="#7c3aed" font-size="9" font-weight="700" text-anchor="middle">① task 描述</text>
|
||||
|
||||
<!-- ===== return 线:Subagent → Parent tool_result ===== -->
|
||||
<path d="M 460 286 L 310 286" fill="none" stroke="#16a34a" stroke-width="2.5" marker-end="url(#arrow-green)"/>
|
||||
<rect x="350" y="268" width="70" height="20" rx="4" fill="#dcfce7" stroke="#16a34a" stroke-width="1"/>
|
||||
<text x="385" y="282" fill="#166534" font-size="9" font-weight="700" text-anchor="middle">② summary</text>
|
||||
|
||||
<!-- ===== 图例 ===== -->
|
||||
<rect x="60" y="370" width="680" height="56" rx="8" fill="#f1f5f9"/>
|
||||
|
||||
<rect x="80" y="384" width="16" height="12" rx="3" fill="#f0f4ff" stroke="#2563eb" stroke-width="1"/>
|
||||
<text x="104" y="394" fill="#334155" font-size="10">s05 保留:循环、hook、todo_write、6 个基础工具</text>
|
||||
|
||||
<rect x="80" y="404" width="16" height="12" rx="3" fill="#ede9fe" stroke="#7c3aed" stroke-width="1"/>
|
||||
<text x="104" y="414" fill="#334155" font-size="10">s06 新增:task 工具 + spawn_subagent() — 独立 messages[],只回传摘要</text>
|
||||
|
||||
<!-- 数据流标注 -->
|
||||
<rect x="430" y="440" width="310" height="44" rx="6" fill="#f8fafc" stroke="#e2e8f0" stroke-width="1"/>
|
||||
<text x="445" y="458" fill="#7c3aed" font-size="10" font-weight="600">① Parent → Sub:</text>
|
||||
<text x="580" y="458" fill="#64748b" font-size="10">task description(一小段文字)</text>
|
||||
<text x="445" y="476" fill="#16a34a" font-size="10" font-weight="600">② Sub → Parent:</text>
|
||||
<text x="580" y="476" fill="#64748b" font-size="10">extract_text()(只有最终结论)</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 8.2 KiB |
Reference in New Issue
Block a user