mirror of
https://github.com/shareAI-lab/analysis_claude_code.git
synced 2026-09-20 12:13:38 +08:00
Refine course progression and runtime safety
This commit is contained in:
@@ -1,22 +1,18 @@
|
||||
# s06: Subagent — 大きなタスクを分割、それぞれがクリーンなコンテキストを取得
|
||||
# s06: Subagent — サブタスクに独立したコンテキストを与える
|
||||
|
||||
[English](README.md) · [中文](README.zh.md) · [日本語](README.ja.md)
|
||||
|
||||
s01 → s02 → s03 → s04 → s05 → `s06` → [s07](../s07_skill_loading/) → s08 → ... → s18 → s19
|
||||
|
||||
> *"大きなタスクは小さく、小さなタスクごとにクリーンなコンテキスト"* — Subagent は独立した messages[] を使い、メイン会話を汚染しない。
|
||||
> Subagent は新しい `messages[]` から始まる。最終テキストだけが親ループへ戻り、中間会話は親コンテキストへ入らない。
|
||||
>
|
||||
> **Harness レイヤー**: サブエージェント — コンテキストの隔離、注意の散漫を防ぐ。
|
||||
> **Harness レイヤー**: 委任 — 明確なサブタスクを別の会話コンテキストで処理する。
|
||||
|
||||
---
|
||||
|
||||
## 課題
|
||||
|
||||
Agent がバグを修正している。呼び出しチェーンを追跡するために 30 のファイルを読み、途中で 60 ラウンドやり取りした。messages リストは 120 件に膨らみ、その大部分は「呼び出しチェーンの追跡」という中間過程 — 「バグ修正」という最終目標とは無関係。
|
||||
|
||||
この中間過程がコンテキストの席を占め、Agent はますます「健忘」になる — 最初の問題が何だったか覚えていられない。
|
||||
|
||||
別の見方をすると:バグを修正するとき、あなたは「新しいターミナルを開いて」呼び出しチェーンを追跡するだろう。追跡が終わったらターミナルを閉じ、結果をメモに書き、元のターミナルに戻ってバグ修正を続ける。Agent にもこの能力が必要 — **独立したサブプロセスを開き、独立したメッセージリストを与え、一つのことに集中させる。**
|
||||
Agent がバグを修正している。呼び出しチェーンを追うために多くのファイルを読み、すべてのツール呼び出しと結果が親の `messages[]` に残る。チェーンを把握した後は不要になる中間情報も、コンテキストを使い続ける。
|
||||
|
||||
---
|
||||
|
||||
@@ -24,87 +20,69 @@ Agent がバグを修正している。呼び出しチェーンを追跡する
|
||||
|
||||

|
||||
|
||||
前章の最小フック構造と `todo_write` ツールを保持し、本章は新規の `task` ツールに注目する。呼び出されると、サブエージェントを spawn する。新しい `messages[]` を持ち、自分自身のループを実行し、終了後に要約テキストのみをメイン Agent に返す。会話コンテキストは破棄されるが、ファイルシステムの副作用(書き込み、編集、コマンド実行)は作業ディレクトリに残る。
|
||||
`task` を呼ぶと、新しい `messages[]` を使う入れ子の Agent Loop が同期実行される。ループが終了すると、最終テキストが親会話の tool result になる。
|
||||
|
||||
サブエージェントのツールは制限される:bash/read/write/edit/glob を持つが、task はない。再帰 spawn を防止する。サブエージェントのツール呼び出しも権限フックを経由する。コンテキスト分離は権限のバイパスではない。
|
||||
ここで分離するのはメッセージであり、プロセスやファイルシステムではない。親 Agent とサブエージェントは `WORKDIR` を共有するため、書き込みやコマンドは同じワークスペースへ作用する。サブエージェントは 5 つの基本ツールを持つが `task` はなく、親と同じ権限 Hooks とライフサイクル Hooks を使う。
|
||||
|
||||
---
|
||||
|
||||
## 仕組み
|
||||
|
||||
**spawn_subagent**、サブエージェントに新しいメッセージリストを与え、自分自身のループを実行し、結論のみを返す:
|
||||
**run_subagent** は新しいメッセージリストを作り、入れ子のループを実行して、最終テキストを返す:
|
||||
|
||||
```python
|
||||
def spawn_subagent(description: str) -> str:
|
||||
# サブエージェントのツール:基本ツールのみ、task なし(再帰禁止)
|
||||
sub_tools = [...]
|
||||
messages = [{"role": "user", "content": description}] # 新規 messages[]
|
||||
SUB_TOOLS = list(BASE_TOOLS) # no task tool
|
||||
|
||||
for _ in range(30): # safety limit
|
||||
def run_subagent(prompt: str) -> str:
|
||||
messages = [{"role": "user", "content": prompt}]
|
||||
|
||||
for _ in range(30):
|
||||
response = client.messages.create(
|
||||
model=MODEL, system=SUB_SYSTEM,
|
||||
messages=messages, tools=sub_tools, max_tokens=8000,
|
||||
messages=messages, tools=SUB_TOOLS, max_tokens=8000,
|
||||
)
|
||||
messages.append({"role": "assistant", "content": response.content})
|
||||
if response.stop_reason != "tool_use":
|
||||
break
|
||||
return extract_text(response.content) or "(no summary)"
|
||||
|
||||
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)
|
||||
output = execute_tool(block, SUB_HANDLERS)
|
||||
results.append({... "content": output})
|
||||
messages.append({"role": "user", "content": results})
|
||||
|
||||
# 最後のテキスト結論のみを返す、中間過程はすべて破棄
|
||||
return extract_text(messages[-1]["content"])
|
||||
return "Subagent stopped after 30 turns without a final answer."
|
||||
```
|
||||
|
||||
メイン 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"]}},
|
||||
]
|
||||
TASK_TOOL = {
|
||||
"name": "task",
|
||||
"description": "Run a subagent with fresh conversation context and return its final text.",
|
||||
"input_schema": {
|
||||
"type": "object",
|
||||
"properties": {"prompt": {"type": "string"}},
|
||||
"required": ["prompt"],
|
||||
},
|
||||
}
|
||||
|
||||
TOOL_HANDLERS["task"] = spawn_subagent
|
||||
TOOLS = [*BASE_TOOLS, TASK_TOOL]
|
||||
TOOL_HANDLERS = {**BASE_HANDLERS, "task": run_subagent}
|
||||
```
|
||||
|
||||
三つの重要な設計決定:
|
||||
実際の境界は次のとおり:
|
||||
|
||||
| 決定 | 選択 | 理由 |
|
||||
|------|------|------|
|
||||
| コンテキスト隔離 | 新規 `messages[]` | サブエージェントの中間過程がメイン Agent のコンテキストを汚染しない |
|
||||
| 結論のみ返却 | `extract_text(last_message)` | messages リスト全体を返すのではない |
|
||||
| 再帰禁止 | サブエージェントに task ツールなし | サブエージェントがさらにサブエージェントを spawn するのを防止 |
|
||||
| セキュリティのバイパスなし | サブエージェントのツール呼び出しも PreToolUse フックを経由 | コンテキスト分離は権限分離ではない |
|
||||
| 会話 | 新しい `messages[]` | 親の会話をサブエージェントへコピーしない |
|
||||
| 実行 | 同じプロセスと `WORKDIR` | どちらのループからもファイル変更が見える |
|
||||
| 戻り値 | 最終テキストのみ | 子のツール呼び出しと結果を親 messages へコピーしない |
|
||||
| 委任の深さ | `SUB_TOOLS` に `task` なし | 本章では 1 階層の委任だけを許可 |
|
||||
| ツールポリシー | Hooks を共有 | 親子で同じ権限チェックを使う |
|
||||
|
||||
ディスパッチ機構は変わらず、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 とフック保護されたループ |
|
||||
親 Agent は他のツールと同じ handler map から `task` を実行する。サブエージェントは `SUB_SYSTEM`、`SUB_TOOLS`、ローカルな `messages` リストを使う。
|
||||
|
||||
---
|
||||
|
||||
@@ -121,7 +99,7 @@ python s06_subagent/code.py
|
||||
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 はサブエージェントが返した要約だけを受け取って続行するか?
|
||||
観察のポイント:`[Subagent started]` / `[Subagent done]` が表示されるか? サブエージェントのツール呼び出しが `[sub] ...` と表示されるか? 親 Agent は `task` が返した最終テキストだけを受け取るか?
|
||||
|
||||
---
|
||||
|
||||
@@ -132,4 +110,4 @@ Agent はタスクを分割できるようになった。しかし各タスク
|
||||
→ s07 Skill Loading:スキルをオンデマンドで注入する。system prompt にドキュメントを積み上げるのではなく、必要なときだけ読み込む。ファイルを読むのと同じくらい自然に。
|
||||
|
||||
|
||||
<!-- translation-sync: zh@v1, en@v1, ja@v1 -->
|
||||
<!-- translation-sync: zh@v2, en@v2, ja@v2 -->
|
||||
|
||||
@@ -1,22 +1,18 @@
|
||||
# s06: Subagent — Break Large Tasks into Small Ones with Clean Context
|
||||
# s06: Subagent — Give a Subtask Its Own Context
|
||||
|
||||
[English](README.md) · [中文](README.zh.md) · [日本語](README.ja.md)
|
||||
|
||||
s01 → s02 → s03 → s04 → s05 → `s06` → [s07](../s07_skill_loading/) → s08 → ... → s18 → s19
|
||||
|
||||
> *"Break large tasks small, each with clean context"* — Subagent uses an independent messages[], no pollution in the main conversation.
|
||||
> A subagent starts with a fresh `messages[]`. Its final text returns to the parent; its intermediate conversation does not.
|
||||
>
|
||||
> **Harness Layer**: Sub-Agent — Context isolation, attention doesn't drift.
|
||||
> **Harness Layer**: Delegation — Run a focused task in a separate conversation context.
|
||||
|
||||
---
|
||||
|
||||
## 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 Agent is fixing a bug. It reads many files to trace the call chain, and every tool call and result stays in the parent's `messages[]`. Once the call chain is understood, most of those intermediate details are no longer needed, but they still occupy context.
|
||||
|
||||
---
|
||||
|
||||
@@ -24,87 +20,69 @@ Think of it differently: when you fix a bug, you'd "open a new terminal" to trac
|
||||
|
||||

|
||||
|
||||
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.
|
||||
Calling `task` synchronously runs a nested agent loop with a fresh `messages[]`. When that loop finishes, its final text becomes the tool result in the parent conversation.
|
||||
|
||||
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.
|
||||
This is message isolation, not process or filesystem isolation. Parent and subagent run in the same Python process and share `WORKDIR`, so writes and commands still affect the same workspace. The subagent has the five base tools but no `task`, and its tool calls use the same permission and lifecycle hooks as the parent.
|
||||
|
||||
---
|
||||
|
||||
## How It Works
|
||||
|
||||
**spawn_subagent**, gives the sub-Agent a fresh messages list, runs its own loop, returns only the conclusion:
|
||||
**run_subagent** creates the fresh message list, runs the nested loop, and returns the final text:
|
||||
|
||||
```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[]
|
||||
SUB_TOOLS = list(BASE_TOOLS) # no task tool
|
||||
|
||||
for _ in range(30): # safety limit
|
||||
def run_subagent(prompt: str) -> str:
|
||||
messages = [{"role": "user", "content": prompt}]
|
||||
|
||||
for _ in range(30):
|
||||
response = client.messages.create(
|
||||
model=MODEL, system=SUB_SYSTEM,
|
||||
messages=messages, tools=sub_tools, max_tokens=8000,
|
||||
messages=messages, tools=SUB_TOOLS, max_tokens=8000,
|
||||
)
|
||||
messages.append({"role": "assistant", "content": response.content})
|
||||
if response.stop_reason != "tool_use":
|
||||
break
|
||||
return extract_text(response.content) or "(no summary)"
|
||||
|
||||
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)
|
||||
output = execute_tool(block, SUB_HANDLERS)
|
||||
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"])
|
||||
return "Subagent stopped after 30 turns without a final answer."
|
||||
```
|
||||
|
||||
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"]}},
|
||||
]
|
||||
TASK_TOOL = {
|
||||
"name": "task",
|
||||
"description": "Run a subagent with fresh conversation context and return its final text.",
|
||||
"input_schema": {
|
||||
"type": "object",
|
||||
"properties": {"prompt": {"type": "string"}},
|
||||
"required": ["prompt"],
|
||||
},
|
||||
}
|
||||
|
||||
TOOL_HANDLERS["task"] = spawn_subagent
|
||||
TOOLS = [*BASE_TOOLS, TASK_TOOL]
|
||||
TOOL_HANDLERS = {**BASE_HANDLERS, "task": run_subagent}
|
||||
```
|
||||
|
||||
Three key design decisions:
|
||||
The boundary is:
|
||||
|
||||
| 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 |
|
||||
| Conversation | Fresh `messages[]` | Parent history is not copied into the subagent |
|
||||
| Execution | Same process and `WORKDIR` | Filesystem changes remain visible to both loops |
|
||||
| Return value | Final text only | Child tool calls and results are not copied into parent messages |
|
||||
| Delegation depth | No `task` in `SUB_TOOLS` | This lesson permits one delegation level |
|
||||
| Tool policy | Shared Hooks | Parent and subagent use the same permission checks |
|
||||
|
||||
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 |
|
||||
The parent dispatches `task` through the same handler map as its other tools. The subagent uses `SUB_SYSTEM`, `SUB_TOOLS`, and its own local `messages` list.
|
||||
|
||||
---
|
||||
|
||||
@@ -121,7 +99,7 @@ Try these prompts:
|
||||
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 to watch for: Do `[Subagent started]` / `[Subagent done]` appear? Do subagent tool calls print as `[sub] ...`? Does the parent continue with only the final text returned by `task`?
|
||||
|
||||
---
|
||||
|
||||
@@ -132,4 +110,4 @@ The Agent can now break tasks apart. But different tasks require different knowl
|
||||
→ 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.
|
||||
|
||||
|
||||
<!-- translation-sync: zh@v1, en@v1, ja@v1 -->
|
||||
<!-- translation-sync: zh@v2, en@v2, ja@v2 -->
|
||||
|
||||
@@ -1,22 +1,18 @@
|
||||
# s06: Subagent — 大任务拆小,每个拿到的都是干净上下文
|
||||
# s06: Subagent — 给子任务一段独立上下文
|
||||
|
||||
[English](README.md) · [中文](README.zh.md) · [日本語](README.ja.md)
|
||||
|
||||
s01 → s02 → s03 → s04 → s05 → `s06` → [s07](../s07_skill_loading/) → s08 → ... → s18 → s19
|
||||
|
||||
> *"大任务拆小, 每个小任务干净的上下文"* — Subagent 用独立 messages[], 不污染主对话。
|
||||
> Subagent 从全新的 `messages[]` 开始。最终文本返回父循环,中间对话不会进入父上下文。
|
||||
>
|
||||
> **Harness 层**: 子 Agent — 上下文隔离, 注意力不漂移。
|
||||
> **Harness 层**: 委派 — 在另一段对话上下文中处理一个明确的子任务。
|
||||
|
||||
---
|
||||
|
||||
## 问题
|
||||
|
||||
Agent 在修一个 bug。它读了 30 个文件来追踪调用链,中间聊了 60 轮。messages 列表涨到 120 条,其中大部分是"追踪调用链"的中间过程,和"修 bug"这个最终目标无关。
|
||||
|
||||
这些中间过程占着上下文位置,让 Agent 越来越"健忘",它记不住最初的问题是什么了。
|
||||
|
||||
换个角度:你修 bug 的时候,会"开一个新终端"来追踪调用链。追踪完了,终端关掉,结果写进笔记,回到原来的终端继续修 bug。Agent 也需要这个能力:开一个独立的子进程,给它一个独立的消息列表,让它专心做一件事。
|
||||
Agent 在修一个 bug。为了追踪调用链,它读取了许多文件;每次工具调用和结果都会留在父循环的 `messages[]` 中。调用链已经弄清以后,多数中间细节不再需要,却仍然占用上下文。
|
||||
|
||||
---
|
||||
|
||||
@@ -24,91 +20,69 @@ Agent 在修一个 bug。它读了 30 个文件来追踪调用链,中间聊了
|
||||
|
||||

|
||||
|
||||
保留上一章的最小 hook 结构和 `todo_write` 工具,本章重点转向新增的 `task` 工具。调用它时,spawn 一个子 Agent,拥有全新的 `messages[]`,跑自己的循环,结束后只把摘要文本回传给主 Agent。对话上下文被丢弃,但文件系统的副作用(写文件、改文件、跑命令)保留在工作目录中。
|
||||
调用 `task` 时,会同步运行一个使用全新 `messages[]` 的嵌套 Agent Loop。循环结束后,它的最终文本会成为父对话中的工具结果。
|
||||
|
||||
子 Agent 的工具受限:有 bash/read/write/edit/glob,但没有 task,不能递归 spawn 新的子 Agent。子 Agent 的工具调用仍经过权限 hook,安全策略不因上下文隔离而跳过。
|
||||
这里隔离的是消息,不是进程或文件系统。父 Agent 与子 Agent 共享 `WORKDIR`,写文件和命令仍会影响同一个工作区。子 Agent 拥有五个基础工具,但没有 `task`;它的工具调用与父 Agent 使用同一组权限和生命周期 Hooks。
|
||||
|
||||
---
|
||||
|
||||
## 工作原理
|
||||
|
||||
**spawn_subagent**,给子 Agent 一个全新的 messages 列表,跑自己的循环,只回传结论:
|
||||
**run_subagent** 创建新的消息列表,运行嵌套循环,并返回最终文本:
|
||||
|
||||
```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[]
|
||||
SUB_TOOLS = list(BASE_TOOLS) # no task tool
|
||||
|
||||
for _ in range(30): # safety limit
|
||||
def run_subagent(prompt: str) -> str:
|
||||
messages = [{"role": "user", "content": prompt}]
|
||||
|
||||
for _ in range(30):
|
||||
response = client.messages.create(
|
||||
model=MODEL, system=SUB_SYSTEM,
|
||||
messages=messages, tools=sub_tools, max_tokens=8000,
|
||||
messages=messages, tools=SUB_TOOLS, max_tokens=8000,
|
||||
)
|
||||
messages.append({"role": "assistant", "content": response.content})
|
||||
if response.stop_reason != "tool_use":
|
||||
break
|
||||
return extract_text(response.content) or "(no summary)"
|
||||
|
||||
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)
|
||||
output = execute_tool(block, SUB_HANDLERS)
|
||||
results.append({... "content": output})
|
||||
messages.append({"role": "user", "content": results})
|
||||
|
||||
# 只返回最后的文本结论,中间过程全部丢弃
|
||||
return extract_text(messages[-1]["content"])
|
||||
return "Subagent stopped after 30 turns without a final answer."
|
||||
```
|
||||
|
||||
主 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"]}},
|
||||
]
|
||||
TASK_TOOL = {
|
||||
"name": "task",
|
||||
"description": "Run a subagent with fresh conversation context and return its final text.",
|
||||
"input_schema": {
|
||||
"type": "object",
|
||||
"properties": {"prompt": {"type": "string"}},
|
||||
"required": ["prompt"],
|
||||
},
|
||||
}
|
||||
|
||||
TOOL_HANDLERS["task"] = spawn_subagent
|
||||
TOOLS = [*BASE_TOOLS, TASK_TOOL]
|
||||
TOOL_HANDLERS = {**BASE_HANDLERS, "task": run_subagent}
|
||||
```
|
||||
|
||||
三个关键设计决策:
|
||||
实际边界如下:
|
||||
|
||||
| 决策 | 选择 | 原因 |
|
||||
|------|------|------|
|
||||
| 上下文隔离 | 全新 `messages[]` | 子 Agent 的中间过程不污染主 Agent 的上下文 |
|
||||
| 只回传结论 | `extract_text(last_message)` | 不是回传整个 messages 列表 |
|
||||
| 禁止递归 | 子 Agent 无 task 工具 | 防止子 Agent 再 spawn 新的子 Agent |
|
||||
| 安全策略不跳过 | 子 Agent 工具调用也走 PreToolUse hook | 上下文隔离不代表权限隔离 |
|
||||
| 对话 | 全新的 `messages[]` | 不把父对话复制给子 Agent |
|
||||
| 执行 | 同一进程和 `WORKDIR` | 两个循环都能看到文件系统修改 |
|
||||
| 返回值 | 只返回最终文本 | 子 Agent 的工具调用和结果不进入父消息列表 |
|
||||
| 委派深度 | `SUB_TOOLS` 中没有 `task` | 本章只允许一层委派 |
|
||||
| 工具策略 | 共享 Hooks | 父子循环使用相同的权限检查 |
|
||||
|
||||
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 保护的循环 |
|
||||
父 Agent 与其他工具一样,通过 handler map 分发 `task`。子 Agent 使用 `SUB_SYSTEM`、`SUB_TOOLS` 和自己的局部 `messages` 列表。
|
||||
|
||||
---
|
||||
|
||||
@@ -125,7 +99,7 @@ python s06_subagent/code.py
|
||||
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 返回的摘要?
|
||||
观察重点:是否出现 `[Subagent started]` / `[Subagent done]`?子 Agent 的工具调用是否以 `[sub] ...` 输出?父 Agent 是否只接收到 `task` 返回的最终文本?
|
||||
|
||||
---
|
||||
|
||||
@@ -136,4 +110,4 @@ Agent 现在能拆任务了。但每个任务需要的知识不一样:改前
|
||||
s07 Skill Loading → 技能按需注入,不在 system prompt 里堆文档。用到的时候才加载,和读文件一样自然。
|
||||
|
||||
|
||||
<!-- translation-sync: zh@v1, en@v0, ja@v0 -->
|
||||
<!-- translation-sync: zh@v2, en@v2, ja@v2 -->
|
||||
|
||||
@@ -1,39 +1,33 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
s06: Subagent — spawn sub-agents with fresh messages[] for context isolation.
|
||||
s06_subagent.py - Subagents
|
||||
|
||||
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 |
|
||||
+--------------------------------------+
|
||||
The task tool runs a second agent loop with a fresh message list. Both
|
||||
loops share the working directory, but only the final text returns to
|
||||
the parent conversation.
|
||||
|
||||
Subagent tools: bash, read, write, edit, glob (NO task — no recursion)
|
||||
Parent agent Subagent
|
||||
+------------------+ +------------------+
|
||||
| messages=[...] | | messages=[prompt]|
|
||||
| | task | |
|
||||
| tool: task | ---------> | own agent loop |
|
||||
| | | base tools only |
|
||||
| tool_result | <--------- | final text |
|
||||
+------------------+ +------------------+
|
||||
|
||||
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
|
||||
The subagent has no task tool, so it cannot delegate again.
|
||||
"""
|
||||
|
||||
import ast, json, os, subprocess
|
||||
import os
|
||||
import 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
|
||||
|
||||
@@ -47,61 +41,54 @@ if os.getenv("ANTHROPIC_BASE_URL"):
|
||||
WORKDIR = Path.cwd()
|
||||
client = Anthropic(base_url=os.getenv("ANTHROPIC_BASE_URL"))
|
||||
MODEL = os.environ["MODEL_ID"]
|
||||
CURRENT_TODOS: list[dict] = []
|
||||
|
||||
SYSTEM = (
|
||||
f"You are a coding agent at {WORKDIR}. "
|
||||
"For complex sub-problems, use the task tool to spawn a subagent."
|
||||
"Use task for focused exploration or a self-contained subtask."
|
||||
)
|
||||
|
||||
# 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."
|
||||
"Complete the given task, then return a concise final answer."
|
||||
)
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
# 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
|
||||
# -- Base tools --
|
||||
|
||||
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)"
|
||||
result = subprocess.run(
|
||||
command, shell=True, cwd=WORKDIR,
|
||||
capture_output=True, text=True, timeout=120,
|
||||
)
|
||||
output = (result.stdout + result.stderr).strip()
|
||||
return output[:50000] if output 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()
|
||||
lines = (WORKDIR / path).resolve().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 = (WORKDIR / path).resolve()
|
||||
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)
|
||||
file_path = (WORKDIR / path).resolve()
|
||||
text = file_path.read_text()
|
||||
if old_text not in text:
|
||||
return f"Error: text not found in {path}"
|
||||
@@ -110,51 +97,20 @@ def run_edit(path: str, old_text: str, new_text: str) -> str:
|
||||
except Exception as e:
|
||||
return f"Error: {e}"
|
||||
|
||||
|
||||
def run_glob(pattern: str) -> str:
|
||||
import glob as g
|
||||
import glob
|
||||
try:
|
||||
results = []
|
||||
for match in g.glob(pattern, root_dir=WORKDIR):
|
||||
matches = []
|
||||
for match in glob.glob(pattern, root_dir=WORKDIR):
|
||||
if (WORKDIR / match).resolve().is_relative_to(WORKDIR):
|
||||
results.append(match)
|
||||
return "\n".join(results) if results else "(no matches)"
|
||||
matches.append(match)
|
||||
return "\n".join(matches) if matches else "(no matches)"
|
||||
except Exception as e:
|
||||
return f"Error: {e}"
|
||||
|
||||
def _normalize_todos(todos):
|
||||
if isinstance(todos, str):
|
||||
try:
|
||||
todos = json.loads(todos)
|
||||
except json.JSONDecodeError:
|
||||
try:
|
||||
todos = ast.literal_eval(todos)
|
||||
except (SyntaxError, ValueError):
|
||||
return None, "Error: todos must be a list or JSON array string"
|
||||
if not isinstance(todos, list):
|
||||
return None, "Error: todos must be a list"
|
||||
for i, t in enumerate(todos):
|
||||
if not isinstance(t, dict):
|
||||
return None, f"Error: todos[{i}] must be an object"
|
||||
if "content" not in t or "status" not in t:
|
||||
return None, f"Error: todos[{i}] missing 'content' or 'status'"
|
||||
if t["status"] not in ("pending", "in_progress", "completed"):
|
||||
return None, f"Error: todos[{i}] has invalid status '{t['status']}'"
|
||||
return todos, None
|
||||
|
||||
def run_todo_write(todos: list) -> str:
|
||||
global CURRENT_TODOS
|
||||
todos, error = _normalize_todos(todos)
|
||||
if error:
|
||||
return error
|
||||
CURRENT_TODOS = todos
|
||||
lines = ["\n\033[33m## Current Tasks\033[0m"]
|
||||
for t in CURRENT_TODOS:
|
||||
icon = {"pending": " ", "in_progress": "\033[36m▸\033[0m", "completed": "\033[32m✓\033[0m"}[t["status"]]
|
||||
lines.append(f" [{icon}] {t['content']}")
|
||||
print("\n".join(lines))
|
||||
return f"Updated {len(CURRENT_TODOS)} tasks"
|
||||
|
||||
TOOLS = [
|
||||
BASE_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.",
|
||||
@@ -165,107 +121,26 @@ TOOLS = [
|
||||
"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,
|
||||
BASE_HANDLERS = {
|
||||
"bash": run_bash,
|
||||
"read_file": run_read,
|
||||
"write_file": run_write,
|
||||
"edit_file": run_edit,
|
||||
"glob": run_glob,
|
||||
}
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
# 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 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")
|
||||
|
||||
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 --
|
||||
|
||||
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)
|
||||
@@ -273,57 +148,175 @@ def trigger_hooks(event: str, *args):
|
||||
return result
|
||||
return None
|
||||
|
||||
|
||||
DENY_LIST = ["rm -rf /", "sudo", "shutdown", "reboot", "mkfs", "dd if="]
|
||||
DESTRUCTIVE = ["rm ", "> /etc/", "chmod 777"]
|
||||
|
||||
|
||||
def permission_hook(block):
|
||||
"""PreToolUse: deny list check."""
|
||||
"""PreToolUse: block denied operations and ask about risky ones."""
|
||||
if block.name == "bash":
|
||||
for p in DENY_LIST:
|
||||
if p in block.input.get("command", ""):
|
||||
print(f"\n\033[31m⛔ Blocked: '{p}'\033[0m")
|
||||
return "Permission denied"
|
||||
command = block.input.get("command", "")
|
||||
for pattern in DENY_LIST:
|
||||
if pattern in command:
|
||||
print(f"\n\033[31m[blocked] '{pattern}'\033[0m")
|
||||
return "Permission denied by deny list"
|
||||
for keyword in DESTRUCTIVE:
|
||||
if keyword in command:
|
||||
print("\n\033[33m[permission] Potentially destructive command\033[0m")
|
||||
print(f" Tool: {block.name}({block.input})")
|
||||
choice = input(" Allow? [y/N] ").strip().lower()
|
||||
if choice not in ("y", "yes"):
|
||||
return "Permission denied by user"
|
||||
|
||||
if block.name in ("read_file", "write_file", "edit_file"):
|
||||
path = block.input.get("path", "")
|
||||
if not (WORKDIR / path).resolve().is_relative_to(WORKDIR):
|
||||
print("\n\033[33m[permission] Access outside workspace\033[0m")
|
||||
print(f" Tool: {block.name}({block.input})")
|
||||
choice = input(" Allow? [y/N] ").strip().lower()
|
||||
if choice not in ("y", "yes"):
|
||||
return "Permission denied by user"
|
||||
return None
|
||||
|
||||
|
||||
def log_hook(block):
|
||||
"""PreToolUse: log tool calls."""
|
||||
print(f"\033[90m[HOOK] {block.name}\033[0m")
|
||||
"""PreToolUse: log every tool call."""
|
||||
args_preview = str(list(block.input.values())[:2])[:60]
|
||||
print(f"\033[90m[HOOK] {block.name}({args_preview})\033[0m")
|
||||
return None
|
||||
|
||||
|
||||
def large_output_hook(block, output):
|
||||
"""PostToolUse: warn on large output."""
|
||||
if len(str(output)) > 100000:
|
||||
print(f"\033[33m[HOOK] Large output from {block.name}: {len(str(output))} chars\033[0m")
|
||||
return None
|
||||
|
||||
|
||||
def context_inject_hook(query: str):
|
||||
"""UserPromptSubmit: log working directory."""
|
||||
"""UserPromptSubmit: log the 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")
|
||||
"""Stop: print the number of tool results in this message list."""
|
||||
tool_count = sum(
|
||||
1
|
||||
for message in messages
|
||||
for block in (
|
||||
message.get("content")
|
||||
if isinstance(message.get("content"), list)
|
||||
else []
|
||||
)
|
||||
if isinstance(block, dict) and block.get("type") == "tool_result"
|
||||
)
|
||||
print(f"\033[90m[HOOK] Stop: session used {tool_count} tool calls\033[0m")
|
||||
return None
|
||||
|
||||
|
||||
register_hook("UserPromptSubmit", context_inject_hook)
|
||||
register_hook("PreToolUse", permission_hook)
|
||||
register_hook("PreToolUse", log_hook)
|
||||
register_hook("PostToolUse", large_output_hook)
|
||||
register_hook("Stop", summary_hook)
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
# agent_loop — same as s05 + nag reminder, task auto-dispatches
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
def execute_tool(block, handlers: dict) -> str:
|
||||
blocked = trigger_hooks("PreToolUse", block)
|
||||
if blocked:
|
||||
return str(blocked)
|
||||
|
||||
handler = handlers.get(block.name)
|
||||
try:
|
||||
output = handler(**block.input) if handler else f"Unknown: {block.name}"
|
||||
except Exception as e:
|
||||
output = f"Error: {e}"
|
||||
|
||||
trigger_hooks("PostToolUse", block, output)
|
||||
return str(output)
|
||||
|
||||
|
||||
# -- New in s06: a nested agent loop with fresh messages --
|
||||
|
||||
SUB_TOOLS = list(BASE_TOOLS)
|
||||
SUB_HANDLERS = dict(BASE_HANDLERS)
|
||||
|
||||
|
||||
def extract_text(content) -> str:
|
||||
if not isinstance(content, list):
|
||||
return str(content)
|
||||
return "\n".join(
|
||||
getattr(block, "text", "")
|
||||
for block in content
|
||||
if getattr(block, "type", None) == "text"
|
||||
)
|
||||
|
||||
|
||||
def run_subagent(prompt: str) -> str:
|
||||
print("\n\033[35m[Subagent started]\033[0m")
|
||||
messages = [{"role": "user", "content": prompt}]
|
||||
|
||||
for _ in range(30):
|
||||
response = client.messages.create(
|
||||
model=MODEL,
|
||||
system=SUB_SYSTEM,
|
||||
messages=messages,
|
||||
tools=SUB_TOOLS,
|
||||
max_tokens=8000,
|
||||
)
|
||||
messages.append({"role": "assistant", "content": response.content})
|
||||
|
||||
if response.stop_reason != "tool_use":
|
||||
force = trigger_hooks("Stop", messages)
|
||||
if force:
|
||||
messages.append({"role": "user", "content": force})
|
||||
continue
|
||||
print("\033[35m[Subagent done]\033[0m")
|
||||
return extract_text(response.content) or "(no summary)"
|
||||
|
||||
results = []
|
||||
for block in response.content:
|
||||
if block.type != "tool_use":
|
||||
continue
|
||||
output = execute_tool(block, SUB_HANDLERS)
|
||||
print(f" \033[90m[sub] {block.name}: {output[:100]}\033[0m")
|
||||
results.append({
|
||||
"type": "tool_result",
|
||||
"tool_use_id": block.id,
|
||||
"content": output,
|
||||
})
|
||||
messages.append({"role": "user", "content": results})
|
||||
|
||||
print("\033[35m[Subagent stopped]\033[0m")
|
||||
return "Subagent stopped after 30 turns without a final answer."
|
||||
|
||||
|
||||
TASK_TOOL = {
|
||||
"name": "task",
|
||||
"description": "Run a subagent with fresh conversation context and return its final text.",
|
||||
"input_schema": {
|
||||
"type": "object",
|
||||
"properties": {"prompt": {"type": "string", "minLength": 1}},
|
||||
"required": ["prompt"],
|
||||
},
|
||||
}
|
||||
|
||||
TOOLS = [*BASE_TOOLS, TASK_TOOL]
|
||||
TOOL_HANDLERS = {**BASE_HANDLERS, "task": run_subagent}
|
||||
|
||||
|
||||
# -- Parent agent loop --
|
||||
|
||||
def agent_loop(messages: list):
|
||||
rounds_since_todo = 0
|
||||
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,
|
||||
model=MODEL,
|
||||
system=SYSTEM,
|
||||
messages=messages,
|
||||
tools=TOOLS,
|
||||
max_tokens=8000,
|
||||
)
|
||||
messages.append({"role": "assistant", "content": response.content})
|
||||
|
||||
@@ -334,35 +327,22 @@ def agent_loop(messages: list):
|
||||
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})
|
||||
|
||||
output = execute_tool(block, TOOL_HANDLERS)
|
||||
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")
|
||||
print("s06: Subagent - fresh messages, final text returns")
|
||||
print("Enter a question, press Enter to send. Type q to quit.\n")
|
||||
|
||||
history = []
|
||||
while True:
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
<!-- 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>
|
||||
<text x="400" y="31" fill="#fff" font-size="16" font-weight="700" text-anchor="middle">Subagent — Fresh messages[], Final Text Returns</text>
|
||||
|
||||
<!-- ===== Parent Agent (left) ===== -->
|
||||
<rect x="30" y="68" width="310" height="268" rx="12" fill="#f0f4ff" stroke="#2563eb" stroke-width="2"/>
|
||||
@@ -54,9 +54,9 @@
|
||||
<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 -->
|
||||
<!-- task runs the nested loop -->
|
||||
<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>
|
||||
<text x="255" y="210" fill="#5b21b6" font-size="10" font-weight="600" text-anchor="middle">task → run</text>
|
||||
|
||||
<!-- Parent tool_result target -->
|
||||
<rect x="190" y="270" width="120" height="34" rx="5" fill="#dcfce7" stroke="#16a34a" stroke-width="1.2"/>
|
||||
@@ -86,16 +86,16 @@
|
||||
<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>
|
||||
<text x="605" y="198" fill="#94a3b8" font-size="8" text-anchor="middle">No task — one delegation level</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>
|
||||
<!-- intermediate results remain in the local message list -->
|
||||
<rect x="460" y="218" width="290" height="44" rx="6" fill="#f8fafc" stroke="#94a3b8" stroke-width="1" stroke-dasharray="4,2"/>
|
||||
<text x="605" y="238" fill="#64748b" font-size="10" text-anchor="middle">Subagent tool calls + results</text>
|
||||
<text x="605" y="254" fill="#64748b" font-size="10" font-weight="600" text-anchor="middle">Not copied to parent messages[]</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>
|
||||
<text x="605" y="290" fill="#166534" font-size="10" font-weight="600" text-anchor="middle">Final text → Parent tool_result</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)"/>
|
||||
@@ -111,15 +111,15 @@
|
||||
<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>
|
||||
<text x="104" y="394" fill="#334155" font-size="10">Parent tools: 5 base tools + task</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>
|
||||
<text x="104" y="414" fill="#334155" font-size="10">Subagent tools: 5 base tools, no task</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="580" y="458" fill="#64748b" font-size="10">task prompt (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>
|
||||
|
||||
|
Before Width: | Height: | Size: 8.1 KiB After Width: | Height: | Size: 8.1 KiB |
@@ -24,7 +24,7 @@
|
||||
<!-- タイトル -->
|
||||
<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>
|
||||
<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"/>
|
||||
@@ -54,9 +54,9 @@
|
||||
<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 -->
|
||||
<!-- task が入れ子のループを実行 -->
|
||||
<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>
|
||||
<text x="255" y="210" fill="#5b21b6" font-size="10" font-weight="600" text-anchor="middle">task → run</text>
|
||||
|
||||
<!-- Parent tool_result target -->
|
||||
<rect x="190" y="270" width="120" height="34" rx="5" fill="#dcfce7" stroke="#16a34a" stroke-width="1.2"/>
|
||||
@@ -86,16 +86,16 @@
|
||||
<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>
|
||||
<text x="605" y="198" fill="#94a3b8" font-size="8" text-anchor="middle">task なし — 委任は 1 階層</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>
|
||||
<!-- 中間結果は子の messages にだけ残る -->
|
||||
<rect x="460" y="218" width="290" height="44" rx="6" fill="#f8fafc" stroke="#94a3b8" stroke-width="1" stroke-dasharray="4,2"/>
|
||||
<text x="605" y="238" fill="#64748b" font-size="10" text-anchor="middle">子のツール呼び出しと結果</text>
|
||||
<text x="605" y="254" fill="#64748b" font-size="10" font-weight="600" text-anchor="middle">親 messages[] へコピーしない</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>
|
||||
<text x="605" y="290" fill="#166534" font-size="10" font-weight="600" text-anchor="middle">最終テキスト → Parent tool_result</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)"/>
|
||||
@@ -111,15 +111,15 @@
|
||||
<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>
|
||||
<text x="104" y="394" fill="#334155" font-size="10">親 Agent のツール:5 つの基本ツール + task</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>
|
||||
<text x="104" y="414" fill="#334155" font-size="10">子 Agent のツール:5 つの基本ツール、task なし</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="580" y="458" fill="#64748b" font-size="10">task prompt(短い文字列)</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>
|
||||
|
||||
|
Before Width: | Height: | Size: 8.4 KiB After Width: | Height: | Size: 8.4 KiB |
@@ -24,7 +24,7 @@
|
||||
<!-- 标题 -->
|
||||
<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>
|
||||
<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"/>
|
||||
@@ -54,9 +54,9 @@
|
||||
<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 -->
|
||||
<!-- task 运行嵌套循环 -->
|
||||
<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>
|
||||
<text x="255" y="210" fill="#5b21b6" font-size="10" font-weight="600" text-anchor="middle">task → run</text>
|
||||
|
||||
<!-- Parent tool_result target -->
|
||||
<rect x="190" y="270" width="120" height="34" rx="5" fill="#dcfce7" stroke="#16a34a" stroke-width="1.2"/>
|
||||
@@ -86,16 +86,16 @@
|
||||
<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>
|
||||
<text x="605" y="198" fill="#94a3b8" font-size="8" text-anchor="middle">无 task — 只允许一层委派</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>
|
||||
<!-- 中间结果只保留在子 Agent 消息列表中 -->
|
||||
<rect x="460" y="218" width="290" height="44" rx="6" fill="#f8fafc" stroke="#94a3b8" stroke-width="1" stroke-dasharray="4,2"/>
|
||||
<text x="605" y="238" fill="#64748b" font-size="10" text-anchor="middle">子 Agent 的工具调用与结果</text>
|
||||
<text x="605" y="254" fill="#64748b" font-size="10" font-weight="600" text-anchor="middle">不复制到父 messages[]</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>
|
||||
<text x="605" y="290" fill="#166534" font-size="10" font-weight="600" text-anchor="middle">最终文本 → Parent tool_result</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)"/>
|
||||
@@ -111,15 +111,15 @@
|
||||
<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>
|
||||
<text x="104" y="394" fill="#334155" font-size="10">父 Agent 工具:5 个基础工具 + task</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>
|
||||
<text x="104" y="414" fill="#334155" font-size="10">子 Agent 工具:5 个基础工具,无 task</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="580" y="458" fill="#64748b" font-size="10">task prompt(一小段文字)</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>
|
||||
|
||||
|
Before Width: | Height: | Size: 8.2 KiB After Width: | Height: | Size: 8.1 KiB |
Reference in New Issue
Block a user