refactor: streamline the course to 17 lessons

This commit is contained in:
Haoran
2026-08-12 03:02:42 +08:00
parent ab35e59672
commit 7e2f2fd99b
250 changed files with 12179 additions and 18653 deletions

View File

@@ -0,0 +1,450 @@
# s13: Agent Teams — チームランタイムと協調プロトコル
[English](README.md) · [中文](README.zh.md) · [日本語](README.ja.md)
s01 → ... → [s10](../s10_task_system/) → `s13` → [s14](../s14_mcp_plugin/) → s15 → s16 → s17
> *「1 つの Agent で仕事全体を抱えきれないなら、チームメイトで分担する。」* — 永続チームメイト、共有タスクの Claim、任意の worktree、協調プロトコル。
>
> **Harness レイヤー**Team — 複数の Agent が Lead の管理下で仕事を分担し、状態を共有する仕組み。
---
## 問題
Agent にバックエンド全体のリファクタリングを依頼するとする。作業範囲は設定の読み込み、認証、テストにまたがる。1 つの Agent でも順番に処理できるが、時間がかかり、初期の詳細は少しずつコンテキストから抜けていく。
この仕事は並列化に向いている。ただし、ユーザーは通常、チーム構成ではなく目標を伝える:
```text
このサンプルバックエンドをリファクタリングしてください。
設定の読み込み、認証、テストを整理し、既存インターフェースを保ち、
テストが通ることを確認してください。
```
Harness は、つながった 6 つの問題を扱う必要がある:
1. 並列作業が有効だと誰が判断し、追加の Agent を誰が承認するのか。
2. 各チームメイトは、複数の割り当てをまたいで識別子とコンテキストをどう保つのか。
3. モデルに受信箱をポーリングさせず、結果を Lead へどう返すのか。
4. IDLE のチームメイトは、次の指示を待たずに ready task を引き受けられるか。
5. 並列編集が衝突し得る時、タスクはどの作業ディレクトリを使うのか。
6. shutdown と計画承認を、追跡できて実際に制約をかけるプロトコルにするにはどうするか。
---
## 解決策
![Agent Teams Overview](images/agent-teams-overview.ja.svg)
s13 は s10 の基本ツール、Hooks、Permission、Task System を再利用し、Lead 管理のチームランタイムを加える:
- **Lead** はユーザーとの会話を担当し、分担案を示して確認を待つ。
- **チームメイト** は独立した Agent Loop を実行し、WORK と IDLE を行き来する。
- **MessageBus** は、ファイルベースの受信箱で通常メッセージ、結果、制御イベントを運ぶ。
- **ランタイム配信** は Lead の受信箱を消費し、チームイベントを次のターンへ追加する。
- **共有タスクボード** により、IDLE のチームメイトは ready task を探し、ロック下で Claim できる。
- **任意の worktree** は、必要なタスクだけを別の作業ディレクトリへ紐付ける。紐付けのないタスクは通常のリポジトリディレクトリを使う。
- **型付きプロトコルと計画ゲート** は shutdown と承認状態を明示し、必要な計画が承認されるまで変更系ツールを止める。
s11 の background task と s12 の scheduled task は本章へ持ち込まない。どちらも teammate communication、task claim、plan approval には必要ない。
これらはすべて Team Harness レイヤーの一部である。タスク発見のために別の Agent Loop は要らず、worktree が別種の Agent を作るわけでもない。
---
## 仕組み
### 1. Lead はチーム案を示し、ユーザーの確認を待つ
チームメイトを起動すると、コスト、並行度、ワークスペースを編集できる主体が変わる。Lead のシステムプロンプトは、その境界を明示する:
```python
"When parallel work would help, first propose a small team with clear "
"responsibilities and wait for the user's confirmation. Do not call "
"spawn_teammate before the user confirms."
```
最初の要求に対して、Lead は分担案だけを示す:
```text
3 つの領域を並行して進めることを提案します:
- config設定の読み込みを整理
- auth認証をリファクタリング
- tests回帰テストを追加
確認後にチームメイトを起動します。
```
ユーザーが「始めてください」と返した後、Lead は `spawn_teammate` を呼べる。Lead は先に Task を作り、初期 `task_id` をチームメイトへ渡す。ユーザーが目標を示し、Lead がチームを設計し、ユーザーが実行境界を確認する。
### 2. 各チームメイトは独立したループを持つ
s06 の subagent は 1 回限りの呼び出しである。チームメイトは永続する実行単位だ:
| | s06 Subagent | s13 Teammate |
|---|---|---|
| ライフサイクル | 1 回の呼び出し後に終了 | shutdown まで `WORK → IDLE → WORK` |
| コンテキスト | 1 つのタスクにだけ存在 | 割り当てをまたいで保持 |
| 通信 | 1 回だけ結果を返す | メッセージを受け取りイベントを送る |
| 協調 | 一方向の委譲 | Lead との双方向協調 |
`TeammateRuntime` は、各チームメイト専用のシステムプロンプト、messages、ツール、現在の Task を保持し、daemon thread で WORK / IDLE loop を実行する。チームメイトの作業中も Lead は調整を続けられる。`lead``agent` はランタイム識別子として予約されるが、`MessageBus` はコーディネーターの受信箱として `lead` を引き続き受け付ける。
`spawn_teammate` は thread を開始する前に初期 Task を Claim する。Claim に失敗した場合、チームメイトは起動しない。Task がない状態では workspace tool と Shell tool は repository directory へ戻らず、先に Task を Claim するよう求める。
### 3. MessageBus は通信をモデルのコンテキスト外に置く
Lead とチームメイトは同じ messages 配列を共有できない。共有すると、あるチームメイトのツール結果が別のチームメイトの推論へ混ざる。`MessageBus` は Agent ごとに `.mailboxes/<name>.jsonl` 受信箱を用意する:
```python
class MessageBus:
def send(self, from_agent, to_agent, content,
msg_type="message", metadata=None):
msg = {
"from": from_agent,
"to": to_agent,
"content": content,
"type": msg_type,
"metadata": metadata or {},
}
with self._changed:
MAILBOX_DIR.mkdir(parents=True, exist_ok=True)
with self._path(to_agent).open("a", encoding="utf-8") as handle:
handle.write(json.dumps(msg, ensure_ascii=True) + "\n")
self._changed.notify_all()
def wait_for_messages(self, agent, timeout=None):
deadline = None if timeout is None else time.monotonic() + timeout
with self._changed:
while not self.peek(agent):
remaining = (None if deadline is None
else deadline - time.monotonic())
if remaining is not None and remaining <= 0:
return []
self._changed.wait(remaining)
return self._read_unlocked(agent)
```
ロックは、チームメイトによる受信箱ファイルの並行アクセスを保護する。`Condition` はメッセージ到着時にチームメイトを起こし、IDLE 中の短い timeout にも使える。
### 4. 受信イベントはランタイムが配信する
`read_inbox()` は受信箱ファイルを読み取って削除するため、Lead 側の消費処理は `consume_lead_inbox()` だけにする:
```python
def consume_lead_inbox():
messages = BUS.read_inbox("lead")
for message in messages:
if message["type"].endswith("_response"):
match_response(...)
return messages
```
CLI のメインループは terminal input と Lead の受信箱を同時に待つ。新しいメッセージが届くと、受信箱を消費してから Lead の次ターンを始める:
```text
MessageBus → consume_lead_inbox
→ プロトコル状態を更新
→ [Team events] を history に追加
→ Lead の次ターンを開始
```
Lead は teammate を起動した後、`list_teammates``get_task` を繰り返して待たず、現在の turn を終了する。team event が届くと runtime が次の turn を開始する。
`check_inbox` はモデルのツールではない。メッセージの到着と消費はランタイムが担当し、モデルはコンテキストへ配信済みのイベントを処理する。
### 5. 結果と IDLE は別のイベントである
チームメイトが 1 つの割り当てを終えると、ランタイムは 2 つのイベントを順に送る:
```text
result: "認証をリファクタリングし、関連テストが通りました。"
idle_notification: "Waiting for more work."
```
`result` は「この割り当てで何ができたか」、`idle_notification` は「このチームメイトが次の仕事を受けられるか」を表す。曖昧な「完了」だけでは、両方の状態を表せない。
IDLE のチームメイトは終了しない。直接メッセージか ready task を受けると WORK に戻り、`shutdown_request` を受けると段階的な shutdown handshake を始める。
### 6. IDLE は受信箱を先に確認し、その後 ready task を探す
IDLE ではメッセージを優先し、その後に共有タスクボードを確認する:
```python
while True:
inbox = BUS.wait_for_messages(name, IDLE_SCAN_INTERVAL)
if inbox:
should_stop = handle_messages(inbox)
if should_stop or messages[-1]["role"] == "user":
break
continue
task = claim_next_task(name)
if task:
messages.append({
"role": "user",
"content": f"[Auto-claimed task {task.id}] {task.subject}",
})
break
```
shutdown、計画承認、Lead からの直接指示は、空き時間に見つけた仕事より先に扱う。メッセージも ready task もなければ、チームメイトは IDLE を続ける。別のチームメイトが前提タスクを完了すると、blocked task が ready になることもある。
### 7. 発見と Claim を分け、Claim はアトミックに行う
走査は候補を探すだけで、状態を変更しない:
```python
def scan_unclaimed_tasks() -> list[Task]:
return [
task for task in list_tasks()
if task.status == "pending"
and task.owner is None
and can_start(task.id)
]
```
候補一覧は一時点の snapshot にすぎない。別のチームメイトだけでなく、同じ task directory を使う別の Harness process も同じ task を見る可能性がある。そのため、所有権の変更は process 内 lock と file lock を組み合わせた `task_store_lock()` の下で `claim_task()` が行う:
```python
def claim_task(task_id: str, owner: str) -> str:
with task_store_lock():
task = load_task(task_id)
if task.status != "pending" or task.owner is not None:
return "Task is no longer available"
if _owner_in_progress(owner):
return "Owner must complete its current task first"
if not can_start(task_id):
return "Task is blocked"
cwd, error = task_worktree_cwd(task)
if error:
return f"Cannot claim {task_id}: {error}"
task.owner = owner
task.status = "in_progress"
save_task(task)
teammate_assignments[owner] = {"task_id": task.id, "cwd": cwd}
return f"Claimed {task.id}"
```
複数のチームメイトが同じ候補を発見しても、`in_progress` へ進められる Claim は 1 つだけである。同じ store lock を保持したまま temporary file へ書き、正式な task file を atomic に置き換える。現在のタスクを完了するまで、チームメイトは次のタスクを Claim できない。worktree の紐付けが壊れている場合、リポジトリディレクトリへ戻さず Claim を失敗させる。
### 8. Claim した仕事は同じ WORK ループを再利用する
Claim に成功すると、ランタイムはタスク ID、件名、説明をチームメイトの messages へ追加する:
```text
ready task が現れる
→ IDLE のチームメイトが発見
→ claim_task が owner と in_progress を記録
→ タスクがチームメイトの messages に入る
→ WORK
→ complete_task
→ result + idle_notification
→ IDLE
```
チームメイトは、Lead が直接割り当てた時と同じモデル呼び出し、ファイルツール、Shell、計画ゲート、結果通知、shutdown protocol を使う。タスク発見は、既存の WORK ループへの別の入口である。
### 9. タスクがツールの作業ディレクトリを選ぶ
`Task.worktree` は任意フィールドである:
```python
@dataclass
class Task:
id: str
subject: str
description: str
status: str
owner: str | None
blockedBy: list[str]
worktree: str | None = None
```
並列編集を別ディレクトリに分けたい時、Lead は worktree を作成してタスクへ紐付けられる:
```python
create_worktree(name="auth-refactor", task_id="task_1a2b3c4d")
```
`create_worktree` は Lead 専用ツールである。pending、owner なし、worktree 未設定のタスクを受け取り、名前、パス、ブランチ、Git registry を確認する。checkout の作成後にだけタスクへ紐付ける。Git が失敗を返しても branch や登録済み checkout が残った場合は partial operation を報告し、task は未紐付けのまま、それらを manual recovery 用に保持する。チームメイトが使うのはタスクツールとファイルツールである。
Claim 時に、解決済みのディレクトリを `teammate_assignments` へ保存する。チームメイトの `bash``read_file``write_file``edit_file``glob` wrapper は assignment からディレクトリを読む。worktree のないタスクは `WORKDIR` に解決されるが、Task を Claim していないチームメイトはこれらの workspace tool を使えない:
```python
cwd, error = task_worktree_cwd(task)
if not error:
teammate_assignments[owner] = {
"task_id": task.id,
"cwd": cwd,
}
```
`complete_task(task_id, owner)` は、呼び出し元が進行中タスクの owner か確認する。成功時は結果を記録するが assignment をすぐには解除せず、同じ model turn の後続 tool call もそのタスクの directory を使う。チームメイトが IDLE に戻る時にランタイムが assignment を解除する。失敗時も directory を維持し、修正して再試行できるようにする。
process 再起動後、`assignment_cwd()` は永続化された task owner と worktree binding から進行中の assignment を復元できる。同じ owner が別の task へ移った場合は、local の古い lease も置き換える。binding が見つからない、または無効な場合は repository directory へ戻さず失敗する。
> Worktree が分離するのは Git の作業ディレクトリとブランチであり、sandbox ではない。Shell コマンドは親プロセスに許可されたパスやリソースへアクセスできる。
### 10. Worktree の削除は host が担う
モデルは task-bound worktree を作成できるが、削除はできない。cleanup は host helper として残し、user または host が task ownership、assignment lease、Git status を先に確認する。helper は pending または in-progress の binding と current turn の lease を拒否する。明示的に破壊的削除を選ばない限り、tracked、untracked、ignored file はすべて cleanup を止める。
`remove_worktree(name, discard_changes=True)` は、user の明示的な確認を別途得た host からのみ呼び出す。どちらの削除経路でも `wt/<name>` ブランチはリポジトリに残り、upstream のない clean な local commit も保持される。削除成功後は task binding を解除する。
```text
clean worktree → host が directory を削除し、wt/<name> branch を保持できる
changed worktree → 保持か破棄かを user が決める
pending/running task → 削除を拒否
```
タスク完了と worktree cleanup も分かれている。`complete_task` はタスク結果を記録し、teammate が IDLE に戻った後で user または host が worktree を確認、merge、keep、remove できる。
### 11. 制御メッセージには型と request_id を使う
通常の協調には自由形式のテキストを使えるが、shutdown と承認を意図の推測に任せるべきではない。これらは構造化メッセージを使う:
![Team Protocols](images/team-protocols-overview.ja.svg)
```python
@dataclass
class ProtocolState:
request_id: str
type: str
sender: str
target: str
status: str
payload: str
work_version: int | None = None
task_id: str | None = None
pending_requests: dict[str, ProtocolState] = {}
```
shutdown の流れは次の通り:
```text
Lead が pending の shutdown request を作る
→ shutdown_request(request_id) がチームメイトの受信箱に入る
→ チームメイトが現在のステップを終える
→ shutdown_response(request_id) が Lead へ戻る
→ request_id で元の request を特定する
→ pending が approved になり、チームメイトの loop が終了する
```
ID は応答を 1 つの request に対応付け、型は不一致の応答による状態変更を防ぎ、status は同じ応答の二重適用を防ぐ。
### 12. 計画承認は実行も制約する
計画プロトコルは逆方向に進む:
```text
Lead → plan_request
チームメイト → plan_approval_request(request_id, plan)
Lead → plan_approval_response(request_id, approve, feedback)
```
Lead が起動前から plan を必須にしたい場合は、`spawn_teammate(..., task_id=task.id, require_plan=True)` を使う。runtime は Task を Claim し、gate を有効にしてから teammate thread を開始する。すでに動いている teammate には `request_plan` で plan を要求できる。
ツール dispatch がゲートを強制する:
```python
def _run_teammate_tool(name, block, handlers):
gate = plan_gates.get(name, "not_required")
if block.name in {"bash", "write_file", "edit_file"} and gate not in {
"not_required", "approved"
}:
return f"Blocked: plan status is {gate}."
try:
return handlers[block.name](**block.input)
except Exception as error:
return f"Error: {type(error).__name__}: {error}"
```
状態が `required``pending``rejected` の間、チームメイトはファイルを読み、計画を提出または修正できるが、Shell コマンドの実行、ファイルの書き込み、編集はできない。提出時には current task と work version を記録し、承認時に両方が一致する場合だけ有効になる。Task の Claim または release は work version を変えて古い承認を無効にするが、通常の message は task identity も approval state も変えない。
チームメイトは background thread から user input を直接読まない。危険な command や workspace 外の path は permission error を返し、Lead が user と判断する。
---
## 一連の実行例
```text
s13 >> バックエンドのリファクタリングを共有タスクボードに分解し、
設定、認証、テストを可能な範囲で並行実行してください。
認証には worktree を使い、既存インターフェースを保ち、
テストが通ることを確認してください。
Leadconfig、auth、tests の 3 領域に分けることを提案します。
チームを起動しますか?
s13 >> 始めてください
[task] config created
[task] auth created → worktree auth-refactor
[task] tests created
[claim] alice → config (cwd: repository)
[claim] bob → auth (cwd: .worktrees/auth-refactor)
[teammate] alice spawned
[teammate] bob spawned
[complete] auth
[bus] bob → lead (result) ...
[bus] bob → lead (idle_notification) ...
[wake: 2 team events → new turn]
Lead認証タスクの結果を受け取りました。残りの作業を調整します。
```
ターミナルには、ユーザーの要求、Lead の提案、タスク状態、Claim、選択されたディレクトリ、結果、IDLE 遷移、制御イベントが表示される。ユーザーが Lead を指定したり、受信箱の確認を依頼したりする必要はない。
---
## s10 からの変更
| コンポーネント | s10 | s13 |
|---|---|---|
| Agent | 1 つの Agent | 1 つの Lead と永続チームメイト |
| ユーザーフロー | 要求を実行 | チーム案を示してから起動確認 |
| 通信 | なし | ファイル受信箱とランタイム配信 |
| ライフサイクル | 1 つのループ | チームメイトの `WORK / IDLE / shutdown` |
| 共有作業 | 1 つの Agent がタスクツールを使用 | IDLE 走査とチームメイトのアトミックな Claim |
| 作業ディレクトリ | リポジトリの `WORKDIR` | Claim 済み Task、必要に応じて worktree |
| 結果通知 | 現在の Agent の出力 | `result``idle_notification` を分離 |
| 制御 | なし | 型付き shutdown と計画承認プロトコル |
| 強制 | チーム向け制約なし | 必須計画が変更系ツールをゲート |
---
## 試してみる
```sh
cd learn-claude-code
python s13_agent_teams/code.py
```
通常の要求を入力する:
```text
バックエンドのリファクタリングを共有タスクボードへ分解し、依存関係が
許す範囲で設定、認証、テストを並行実行してください。認証には worktree
を使い、既存インターフェースを維持して、最後に結果をまとめてください。
```
Lead がチーム案を示したら、次のように返す:
```text
始めてください
```
`.tasks/``pending``in_progress``completed` と変化する様子、`.mailboxes/``result``idle_notification` を配信する様子、紐付けたタスクにだけ `.worktrees/` が作られることを確認する。直接メッセージがタスクボード走査より優先されることと、`complete_task` の失敗後もチームメイトの作業ディレクトリが変わらないことも確認できる。
---
## 次の章
Lead と teammate が呼び出せるのは、`code.py` に直接定義したツールだけである。Jira、デプロイ基盤、ナレッジベースへ接続するには、外部システムごとに tool schema と handler を書く必要があり、外部ツールの追加や変更に合わせてコースコードも修正しなければならない。
s14 MCP Tools → 共通の発見・呼び出しプロトコルで実行時に外部サービスへ接続し、そのツールを tool pool に追加する。
<!-- translation-sync: zh@v11, en@v11, ja@v11 -->

451
s13_agent_teams/README.md Normal file
View File

@@ -0,0 +1,451 @@
# s13: Agent Teams — Runtime and Coordination Protocols
[English](README.md) · [中文](README.zh.md) · [日本語](README.ja.md)
s01 → ... → [s10](../s10_task_system/) → `s13` → [s14](../s14_mcp_plugin/) → s15 → s16 → s17
> *"When one agent cannot hold the whole job, let teammates divide the work."* — Persistent teammates, shared task selection, optional worktrees, and coordination protocols.
>
> **Harness layer**: Team — how multiple agents divide work, share state, and stay under Lead's control.
---
## The Problem
Suppose we ask an agent to refactor an entire backend. The work may cover configuration loading, authentication, and tests. One agent can process those areas sequentially, but it takes longer and earlier details gradually leave its context.
This is a good candidate for parallel work, yet users normally describe the goal rather than design the team:
```text
Refactor this sample backend. Clean up configuration loading,
authentication, and tests, preserve the existing interfaces,
and make sure the tests pass.
```
The harness has to answer a connected set of questions:
1. Who decides that parallel work is useful, and who confirms the extra agents?
2. How does each teammate keep its identity and context across assignments?
3. How do results return to Lead without asking the model to poll an inbox?
4. Can an idle teammate pick up ready work without waiting for another assignment?
5. Which directory should a task use when parallel edits may conflict?
6. How do shutdown and plan approval become traceable, enforceable protocols?
---
## The Solution
![Agent Teams Overview](images/agent-teams-overview.en.svg)
s13 reuses s10's base tools, hooks, permission checks, and Task System, then adds a Lead-managed team runtime:
- **Lead** owns the user conversation, proposes a division of work, and waits for confirmation.
- **Teammates** run independent agent loops and alternate between WORK and IDLE.
- **MessageBus** carries ordinary messages, results, and control events through file-backed mailboxes.
- **Runtime delivery** consumes Lead's mailbox and injects team events into the next turn.
- **The shared task board** lets idle teammates find ready work and claim it under a lock.
- **Optional worktrees** bind a task to another working directory when the work needs it. Unbound tasks use the normal repository directory.
- **Typed protocols and a plan gate** make shutdown and approval state explicit and block mutating tools until a required plan is approved.
s11 background tasks and s12 scheduled tasks are not carried into this chapter. Neither mechanism is required for teammate communication, task claiming, or plan approval.
These are all parts of the Team harness layer. Teammates do not need a separate loop for task discovery, and a worktree does not create a new kind of agent.
---
## How It Works
### 1. Lead proposes a team and waits for user confirmation
Starting teammates changes cost, concurrency, and the set of actors that may edit the workspace. Lead's system prompt keeps that boundary visible:
```python
"When parallel work would help, first propose a small team with clear "
"responsibilities and wait for the user's confirmation. Do not call "
"spawn_teammate before the user confirms."
```
For the first request, Lead only proposes a split:
```text
I suggest three parallel areas:
- config: clean up configuration loading
- auth: refactor authentication
- tests: add regression coverage
I will start the teammates after you confirm.
```
After the user says "Go ahead," Lead can call `spawn_teammate`. Lead creates the Task first and passes its initial `task_id` to the teammate. The user states the goal, Lead designs the team, and the user confirms the execution boundary.
### 2. Every teammate owns an independent loop
An s06 subagent is a one-shot call. A teammate is a persistent execution unit:
| | s06 Subagent | s13 Teammate |
|---|---|---|
| Lifecycle | Ends after one call | `WORK → IDLE → WORK` until shutdown |
| Context | Exists for one task | Persists across assignments |
| Communication | Returns one result | Receives messages and emits events |
| Coordination | One-way delegation | Two-way collaboration with Lead |
`TeammateRuntime` gives each teammate its own system prompt, messages, tools, and current Task, then runs its WORK / IDLE loop in a daemon thread. Lead can keep coordinating while teammates work. The names `lead` and `agent` are reserved for runtime identities, while `MessageBus` still accepts `lead` as the coordinator mailbox.
`spawn_teammate` claims the initial Task before the thread starts. A failed claim prevents the teammate from starting. Without a Task, workspace and Shell tools ask the teammate to claim one instead of falling back to the repository directory.
### 3. MessageBus keeps communication outside model context
Lead and teammates cannot share one messages array. Otherwise one teammate's tool results would leak into another teammate's reasoning. `MessageBus` gives each agent a `.mailboxes/<name>.jsonl` inbox:
```python
class MessageBus:
def send(self, from_agent, to_agent, content,
msg_type="message", metadata=None):
msg = {
"from": from_agent,
"to": to_agent,
"content": content,
"type": msg_type,
"metadata": metadata or {},
}
with self._changed:
MAILBOX_DIR.mkdir(parents=True, exist_ok=True)
with self._path(to_agent).open("a", encoding="utf-8") as handle:
handle.write(json.dumps(msg, ensure_ascii=True) + "\n")
self._changed.notify_all()
def wait_for_messages(self, agent, timeout=None):
deadline = None if timeout is None else time.monotonic() + timeout
with self._changed:
while not self.peek(agent):
remaining = (None if deadline is None
else deadline - time.monotonic())
if remaining is not None and remaining <= 0:
return []
self._changed.wait(remaining)
return self._read_unlocked(agent)
```
A lock protects mailbox files from concurrent access. A `Condition` lets the runtime wake a teammate for a message and also supports the short timeout used while IDLE.
### 4. The runtime delivers inbox events
`read_inbox()` consumes messages by reading and deleting the mailbox file, so Lead keeps a single consumer, `consume_lead_inbox()`:
```python
def consume_lead_inbox():
messages = BUS.read_inbox("lead")
for message in messages:
if message["type"].endswith("_response"):
match_response(...)
return messages
```
The CLI loop waits for terminal input and Lead's mailbox at the same time. When a message arrives, it consumes the mailbox before starting another Lead turn:
```text
MessageBus → consume_lead_inbox
→ update protocol state
→ inject [Team events] into history
→ start another Lead turn
```
After spawning a teammate, Lead ends the current turn instead of repeatedly calling `list_teammates` or `get_task`. The runtime starts the next turn when a team event arrives.
`check_inbox` is not a model tool. Message arrival belongs to the runtime; the model handles events after the runtime has delivered them into its context.
### 5. Result and IDLE are separate events
When a teammate finishes one assignment, the runtime sends two events in order:
```text
result: "Authentication refactored; related tests pass."
idle_notification: "Waiting for more work."
```
`result` answers "What did this assignment produce?" `idle_notification` answers "Can this teammate accept more work?" One vague "done" cannot represent both facts.
An idle teammate does not exit. A direct message or a ready task returns it to WORK; a `shutdown_request` starts a graceful shutdown handshake.
### 6. IDLE checks the mailbox before looking for ready tasks
IDLE gives messages priority, then checks the shared task board:
```python
while True:
inbox = BUS.wait_for_messages(name, IDLE_SCAN_INTERVAL)
if inbox:
should_stop = handle_messages(inbox)
if should_stop or messages[-1]["role"] == "user":
break
continue
task = claim_next_task(name)
if task:
messages.append({
"role": "user",
"content": f"[Auto-claimed task {task.id}] {task.subject}",
})
break
```
Shutdown, plan approval, and direct instructions from Lead should arrive before opportunistic work. If there is no message and no ready task, the teammate remains IDLE. A blocked task may become ready after another teammate completes its prerequisite.
### 7. Discovery and claim are separate, and claim is atomic
Scanning only finds candidates:
```python
def scan_unclaimed_tasks() -> list[Task]:
return [
task for task in list_tasks()
if task.status == "pending"
and task.owner is None
and can_start(task.id)
]
```
The list is a snapshot. Another teammate, or another harness process using the same task directory, may see the same task. Ownership changes therefore happen inside `claim_task()` under `task_store_lock()`, which combines the in-process lock with a file lock:
```python
def claim_task(task_id: str, owner: str) -> str:
with task_store_lock():
task = load_task(task_id)
if task.status != "pending" or task.owner is not None:
return "Task is no longer available"
if _owner_in_progress(owner):
return "Owner must complete its current task first"
if not can_start(task_id):
return "Task is blocked"
cwd, error = task_worktree_cwd(task)
if error:
return f"Cannot claim {task_id}: {error}"
task.owner = owner
task.status = "in_progress"
save_task(task)
teammate_assignments[owner] = {"task_id": task.id, "cwd": cwd}
return f"Claimed {task.id}"
```
Many teammates may discover the same candidate, but only one claim can move it to `in_progress`. Task files are written through a temporary file and atomically replaced while the same store lock is held. A teammate must also finish its current task before claiming another, and a broken worktree binding fails closed rather than falling back to the repository directory.
### 8. Claimed work reuses the same WORK loop
After a successful claim, the runtime injects the task ID, subject, and description into the teammate's messages:
```text
ready task appears
→ IDLE teammate discovers it
→ claim_task writes owner and in_progress
→ task enters teammate messages
→ WORK
→ complete_task
→ result + idle_notification
→ IDLE
```
The teammate uses the same model call, file tools, Shell, plan gate, result reporting, and shutdown protocol as a direct Lead assignment. Task discovery is another entry into the existing WORK loop.
### 9. The task selects the tools' working directory
`Task.worktree` is optional:
```python
@dataclass
class Task:
id: str
subject: str
description: str
status: str
owner: str | None
blockedBy: list[str]
worktree: str | None = None
```
Lead can create and bind a worktree when separate directories will help:
```python
create_worktree(name="auth-refactor", task_id="task_1a2b3c4d")
```
`create_worktree` is a Lead-only tool. It accepts a pending, unowned, unbound task, validates the name, path, branch, and Git registry, creates the checkout, then writes the task binding. If Git reports failure after leaving a branch or registered checkout, the runtime reports a partial operation, leaves the task unbound, and preserves those artifacts for manual recovery. Teammates only see task and file tools.
Claiming the task stores its resolved directory in `teammate_assignments`; that teammate's `bash`, `read_file`, `write_file`, `edit_file`, and `glob` wrappers read the directory from the assignment. A task with no worktree resolves to `WORKDIR`; a teammate without a claimed Task cannot use those workspace tools:
```python
cwd, error = task_worktree_cwd(task)
if not error:
teammate_assignments[owner] = {
"task_id": task.id,
"cwd": cwd,
}
```
`complete_task(task_id, owner)` checks that the caller owns the in-progress task. Successful completion records the result but keeps the assignment directory selected until that model turn ends. This lets later tool calls in the same response stay in the task's worktree. The runtime releases the assignment when the teammate returns to IDLE; a failed completion keeps it so the teammate can fix the task and try again.
After a restart, `assignment_cwd()` can rebuild an in-progress assignment from the durable task owner and worktree binding. It also replaces a stale local lease when the same owner has moved to another task. A missing or invalid binding fails closed instead of silently routing work to the repository directory.
> A worktree separates Git working directories and branches. It is not a sandbox: Shell commands can still access paths and resources allowed to the parent process.
### 10. Worktree removal belongs to the host
The model can create a task-bound worktree, but it cannot remove one. Cleanup remains a host helper so the user or host can first inspect task ownership, the assignment lease, and Git status. The helper refuses pending or in-progress task bindings and current-turn leases. Without an explicit destructive choice, tracked, untracked, and ignored files all block removal.
`remove_worktree(name, discard_changes=True)` is reserved for host code that has already obtained explicit user confirmation. Either removal path retains the `wt/<name>` branch, including clean local commits with no upstream. A successful removal clears the task binding because the checkout no longer exists.
```text
clean worktree → host may remove directory and retain wt/<name> branch
changed worktree → user decides how to preserve or discard it
pending/running task → refuse removal
```
Task completion also stays separate from worktree cleanup. `complete_task` records the task result; after the teammate reaches IDLE, the user or host can inspect, merge, keep, or remove the worktree.
### 11. Control messages use types and request IDs
Free-form text works for ordinary collaboration, but shutdown and approval should not depend on guessing intent. They use structured messages:
![Team Protocols](images/team-protocols-overview.en.svg)
```python
@dataclass
class ProtocolState:
request_id: str
type: str
sender: str
target: str
status: str
payload: str
work_version: int | None = None
task_id: str | None = None
pending_requests: dict[str, ProtocolState] = {}
```
The shutdown path is:
```text
Lead creates a pending shutdown request
→ shutdown_request(request_id) enters the teammate inbox
→ the teammate finishes its current step
→ shutdown_response(request_id) returns to Lead
→ request_id locates the original request
→ pending becomes approved and the teammate loop exits
```
The ID correlates one reply with one request, the type prevents a mismatched reply from changing state, and the status prevents duplicate responses from being applied twice.
### 12. Plan approval constrains execution
The plan protocol runs in the opposite direction:
```text
Lead → plan_request
teammate → plan_approval_request(request_id, plan)
Lead → plan_approval_response(request_id, approve, feedback)
```
When Lead already knows that a teammate must plan first, `spawn_teammate(..., task_id=task.id, require_plan=True)` claims the Task and activates the gate before the teammate thread starts. `request_plan` can also require a plan from a teammate that is already running.
Tool dispatch enforces the gate:
```python
def _run_teammate_tool(name, block, handlers):
gate = plan_gates.get(name, "not_required")
if block.name in {"bash", "write_file", "edit_file"} and gate not in {
"not_required", "approved"
}:
return f"Blocked: plan status is {gate}."
try:
return handlers[block.name](**block.input)
except Exception as error:
return f"Error: {type(error).__name__}: {error}"
```
While the state is `required`, `pending`, or `rejected`, the teammate can read files and submit or revise a plan, but it cannot run Shell commands, write files, or edit files. A submitted plan records the teammate's current task and work version. Claiming or releasing a Task changes that version and invalidates the old approval; an ordinary message changes neither the task identity nor the approval state.
Teammates do not read user input from their background threads. A dangerous command or path outside the workspace returns a permission error so Lead can handle the decision with the user.
---
## One Complete Run
```text
s13 >> Put the backend refactor on a shared task board. Clean up
configuration, authentication, and tests in parallel where possible.
Use a worktree for authentication, preserve existing interfaces,
and make sure the tests pass.
Lead: I suggest config, auth, and tests as three areas.
Shall I start the team?
s13 >> Go ahead.
[task] config created
[task] auth created → worktree auth-refactor
[task] tests created
[claim] alice → config (cwd: repository)
[claim] bob → auth (cwd: .worktrees/auth-refactor)
[teammate] alice spawned
[teammate] bob spawned
[complete] auth
[bus] bob → lead (result) ...
[bus] bob → lead (idle_notification) ...
[wake: 2 team events → new turn]
Lead: I received the authentication result and will coordinate the rest.
```
The terminal exposes the user request, Lead's proposal, task state, claims, selected directories, results, IDLE transitions, and control events. The user does not have to name a Lead or ask it to check an inbox.
---
## What Changed from s10
| Component | s10 | s13 |
|---|---|---|
| Agents | One agent | One Lead plus persistent teammates |
| User flow | Execute the request | Propose a team, then confirm startup |
| Communication | None | File mailboxes plus runtime delivery |
| Lifecycle | One loop | Teammate `WORK / IDLE / shutdown` |
| Shared work | One agent uses task tools | IDLE scan plus atomic teammate claims |
| Working directory | Repository `WORKDIR` | A claimed Task, with an optional worktree |
| Reporting | Current agent output | Separate `result` and `idle_notification` |
| Control | None | Typed shutdown and plan approval protocols |
| Enforcement | No team constraint | Required plans gate mutating tools |
---
## Try It
```sh
cd learn-claude-code
python s13_agent_teams/code.py
```
Start with an ordinary request:
```text
Put the backend refactor on a shared task board. Complete configuration,
authentication, and tests in parallel where dependencies allow. Use a
worktree for authentication, preserve existing interfaces, and summarize
the result.
```
After Lead proposes the team, reply:
```text
Go ahead.
```
Watch `.tasks/` move from `pending` to `in_progress` and `completed`, `.mailboxes/` deliver `result` and `idle_notification`, and `.worktrees/` appear only for the bound task. Also check that direct messages beat task-board scans and that a failed `complete_task` does not reset the teammate's working directory.
---
## What's Next
The Lead and its teammates can only call tools defined directly in `code.py`. Connecting Jira, a deployment platform, or a knowledge base still requires separate tool schemas and handlers for each external system. Changes to those external tools also require changes to the course code.
s14 MCP Tools → Connect external services at runtime through one discovery and invocation protocol, then add their tools to the tool pool.
<!-- translation-sync: zh@v11, en@v11, ja@v11 -->

View File

@@ -0,0 +1,446 @@
# s13: Agent Teams — 团队运行时与协作协议
[English](README.md) · [中文](README.zh.md) · [日本語](README.ja.md)
s01 → ... → [s10](../s10_task_system/) → `s13` → [s14](../s14_mcp_plugin/) → s15 → s16 → s17
> *“一个 Agent 装不下整项工作时,就让队友分头完成。”* — 持久队友、共享任务认领、可选 worktree 与协作协议。
>
> **Harness 层**Team团队— 多个 Agent 如何分工、共享状态,同时接受 Lead 控制。
---
## 问题
假设我们让 Agent 重构整个后端,工作涉及配置加载、认证和测试。一个 Agent 可以依次处理,但总耗时更长,早期细节也会逐渐离开上下文。
这类工作适合并行,可用户通常只描述目标,不会替运行时设计团队:
```text
重构这个示例后端。清理配置加载、认证和测试,
保持现有接口,并确保测试通过。
```
Harness 需要回答一组相互关联的问题:
1. 谁判断并行是否有用,新增 Agent 又由谁确认?
2. 每个队友如何跨任务保留身份和上下文?
3. 结果如何自动返回 Lead而不是让模型轮询收件箱
4. 空闲队友能否直接接手 ready task不再等待 Lead 逐项派发?
5. 并行修改可能冲突时,任务应该使用哪个工作目录?
6. 关机和计划审批如何成为可追踪、可执行的协议?
---
## 解决方案
![Agent Teams Overview](images/agent-teams-overview.svg)
s13 复用 s10 的基础工具、Hooks、Permission 和 Task System并增加一套由 Lead 管理的团队运行时:
- **Lead** 负责用户对话,提出分工方案并等待确认。
- **队友** 运行独立 Agent Loop在 WORK 和 IDLE 之间切换。
- **MessageBus** 通过文件收件箱传递普通消息、结果和控制事件。
- **运行时投递** 消费 Lead 的收件箱,把团队事件注入下一轮对话。
- **共享任务板** 让空闲队友发现 ready task并在锁内完成认领。
- **可选 worktree** 在需要时把任务绑定到另一个工作目录;未绑定任务仍使用仓库目录。
- **类型化协议和计划闸门** 显式记录关机与审批状态,并在计划获批前阻止修改型工具。
s11 的后台任务和 s12 的定时任务没有被带入本章。它们不参与队友通信、任务认领或计划审批。
这些机制都属于 Team 这一层。任务发现不需要另一套 Agent Loopworktree 也不会产生另一种 Agent。
---
## 工作原理
### 1. Lead 先提出团队,再等待用户确认
启动队友会改变成本、并发度和可以修改工作区的角色集合。Lead 的系统提示词会把这条边界明确写出来:
```python
"When parallel work would help, first propose a small team with clear "
"responsibilities and wait for the user's confirmation. Do not call "
"spawn_teammate before the user confirms."
```
收到第一条需求后Lead 只提出分工:
```text
我建议并行处理三个方向:
- config清理配置加载
- auth重构认证
- tests补充回归测试
你确认后我再启动队友。
```
用户回复“开始吧”后Lead 才能调用 `spawn_teammate`。Lead 会先创建任务,再把初始 `task_id` 传给队友。用户给出目标Lead 设计团队,用户确认执行边界。
### 2. 每个队友拥有独立循环
s06 的 subagent 是一次性调用,队友则是持久执行单元:
| | s06 Subagent | s13 队友 |
|---|---|---|
| 生命周期 | 一次调用后结束 | `WORK → IDLE → WORK`,直到关机 |
| 上下文 | 只服务一个任务 | 跨任务保留 |
| 通信 | 返回一次结果 | 接收消息并发出事件 |
| 协作 | 单向委派 | 与 Lead 双向协作 |
`TeammateRuntime` 为每个队友保存独立的系统提示词、messages、工具和当前任务再在线程中运行 WORK / IDLE 循环。队友工作时Lead 可以继续协调其他任务。`lead``agent` 保留给运行时身份,但 `MessageBus` 仍允许把 `lead` 作为协调者收件箱。
`spawn_teammate` 在线程启动前认领初始任务。认领失败时不会启动队友。队友没有任务时,文件和 Shell 工具会要求它先认领任务,而不是回退到仓库目录。
### 3. MessageBus 把通信放在模型上下文之外
Lead 和队友不能共享同一个 messages 数组,否则一个队友的工具结果会进入另一个队友的推理上下文。`MessageBus` 为每个 Agent 提供 `.mailboxes/<name>.jsonl` 收件箱:
```python
class MessageBus:
def send(self, from_agent, to_agent, content,
msg_type="message", metadata=None):
msg = {
"from": from_agent,
"to": to_agent,
"content": content,
"type": msg_type,
"metadata": metadata or {},
}
with self._changed:
MAILBOX_DIR.mkdir(parents=True, exist_ok=True)
with self._path(to_agent).open("a", encoding="utf-8") as handle:
handle.write(json.dumps(msg, ensure_ascii=True) + "\n")
self._changed.notify_all()
def wait_for_messages(self, agent, timeout=None):
deadline = None if timeout is None else time.monotonic() + timeout
with self._changed:
while not self.peek(agent):
remaining = (None if deadline is None
else deadline - time.monotonic())
if remaining is not None and remaining <= 0:
return []
self._changed.wait(remaining)
return self._read_unlocked(agent)
```
锁会保护收件箱文件,避免队友并发读写。`Condition` 既能在消息到达时唤醒队友,也能支持 IDLE 状态下的短时等待。
### 4. 收件箱事件由运行时投递
`read_inbox()` 会读取并删除收件箱文件,因此 Lead 只保留一个消费者 `consume_lead_inbox()`
```python
def consume_lead_inbox():
messages = BUS.read_inbox("lead")
for message in messages:
if message["type"].endswith("_response"):
match_response(...)
return messages
```
CLI 主循环同时等待终端输入和 Lead 收件箱。新消息到达时,它会先消费收件箱,再发起一轮 Lead 调用:
```text
MessageBus → consume_lead_inbox
→ 更新协议状态
→ 把 [Team events] 注入 history
→ 启动新一轮 Lead 调用
```
Lead 启动队友后会结束当前轮次,不用反复调用 `list_teammates``get_task` 等待结果。队友事件到达时,运行时会自动唤醒下一轮。
`check_inbox` 不是模型工具。消息到达和消费属于运行时,模型只处理已经投递到上下文里的事件。
### 5. 结果与 IDLE 是两个事件
队友完成一项任务后,运行时按顺序发送两个事件:
```text
result: "认证已重构,相关测试通过。"
idle_notification: "Waiting for more work."
```
`result` 回答“这项任务产出了什么”,`idle_notification` 回答“这个队友能否继续接任务”。一个含糊的“完成了”无法同时表达这两种状态。
空闲队友不会退出。直接消息或 ready task 会让它回到 WORK`shutdown_request` 则会启动平滑关机握手。
### 6. IDLE 先看收件箱,再找 ready task
队友进入 IDLE 后优先处理消息,然后检查共享任务板:
```python
while True:
inbox = BUS.wait_for_messages(name, IDLE_SCAN_INTERVAL)
if inbox:
should_stop = handle_messages(inbox)
if should_stop or messages[-1]["role"] == "user":
break
continue
task = claim_next_task(name)
if task:
messages.append({
"role": "user",
"content": f"[Auto-claimed task {task.id}] {task.subject}",
})
break
```
关机、计划审批和 Lead 的直接指令应该先于临时发现的工作。如果没有消息,也没有 ready task队友会保持 IDLE。前置任务完成后当前受阻的任务可能变为 ready。
### 7. 发现和认领分成两步,认领必须原子执行
扫描只负责找候选任务:
```python
def scan_unclaimed_tasks() -> list[Task]:
return [
task for task in list_tasks()
if task.status == "pending"
and task.owner is None
and can_start(task.id)
]
```
候选列表只是某一时刻的快照。其他队友,甚至另一个使用同一任务目录的 Harness 进程,也可能看到同一任务。因此所有权变更必须放进 `claim_task()`,并由 `task_store_lock()` 同时取得进程内锁和文件锁:
```python
def claim_task(task_id: str, owner: str) -> str:
with task_store_lock():
task = load_task(task_id)
if task.status != "pending" or task.owner is not None:
return "Task is no longer available"
if _owner_in_progress(owner):
return "Owner must complete its current task first"
if not can_start(task_id):
return "Task is blocked"
cwd, error = task_worktree_cwd(task)
if error:
return f"Cannot claim {task_id}: {error}"
task.owner = owner
task.status = "in_progress"
save_task(task)
teammate_assignments[owner] = {"task_id": task.id, "cwd": cwd}
return f"Claimed {task.id}"
```
多个队友可以同时发现同一候选,但只有一个 claim 能把它推进到 `in_progress`。持有同一存储锁时任务内容会先写入临时文件再原子替换正式文件。队友完成当前任务后才能再认领下一项worktree 绑定损坏时,认领会直接失败,不会回退到仓库目录。
### 8. 认领后的工作复用同一个 WORK 循环
认领成功后,运行时把任务 ID、标题和描述放进队友的 messages
```text
任务板出现 ready task
→ IDLE 队友发现候选
→ claim_task 写入 owner 和 in_progress
→ 任务进入队友 messages
→ WORK
→ complete_task
→ result + idle_notification
→ IDLE
```
队友继续使用直接派发任务时的模型调用、文件工具、Shell、计划闸门、结果上报和关机协议。任务发现只是现有 WORK 循环的另一个入口。
### 9. 由任务选择工具的工作目录
`Task.worktree` 是可选字段:
```python
@dataclass
class Task:
id: str
subject: str
description: str
status: str
owner: str | None
blockedBy: list[str]
worktree: str | None = None
```
并行修改需要分开目录时Lead 可以创建并绑定 worktree
```python
create_worktree(name="auth-refactor", task_id="task_1a2b3c4d")
```
`create_worktree` 只提供给 Lead。它要求任务处于 pending、无人认领且尚未绑定随后检查名称、路径、分支和 Git 注册信息,创建 checkout最后才写入任务绑定。如果 Git 报告失败却已经留下分支或已注册的 checkout运行时会报告 partial operation让任务保持未绑定并保留这些内容供人工恢复。队友只使用任务工具和文件工具。
认领任务时,运行时会把解析后的目录写入 `teammate_assignments`。该队友的 `bash``read_file``write_file``edit_file``glob` 都从 assignment 读取目录。没有绑定 worktree 的任务解析到 `WORKDIR`;没有认领任务的队友不能使用这些工作区工具:
```python
cwd, error = task_worktree_cwd(task)
if not error:
teammate_assignments[owner] = {
"task_id": task.id,
"cwd": cwd,
}
```
`complete_task(task_id, owner)` 会检查调用者是否拥有这个进行中的任务。成功完成只记录结果,不会马上清除 assignment直到当前模型轮次结束后续工具调用仍使用这个任务目录。队友回到 IDLE 时,运行时才释放 assignment。完成失败时也会保留目录方便修正后重试。
进程重启后,`assignment_cwd()` 可以根据持久化任务中的 owner 和 worktree 绑定恢复进行中的 assignment。同一 owner 已转到新任务时,它也会替换本地的旧 lease。若绑定丢失或无效它会直接失败不会把操作悄悄切回仓库目录。
> Worktree 只分开 Git 工作目录和分支不是安全沙箱。Shell 命令仍能访问父进程有权访问的路径和资源。
### 10. Worktree 移除由宿主负责
模型可以创建任务绑定的 worktree但不能移除它。清理保留为宿主函数让用户或宿主先检查任务所有权、assignment lease 和 Git 状态。这个函数会拒绝 pending 或 in-progress 绑定以及当前轮次仍在使用的 lease。未明确选择破坏性移除时已跟踪、未跟踪和已忽略文件都会阻止清理。
`remove_worktree(name, discard_changes=True)` 只供已经另行取得用户明确确认的宿主调用。两种移除路径都会保留仓库里的 `wt/<name>` 分支,包括没有 upstream 的干净本地提交。移除成功后,任务绑定会被清空。
```text
干净 worktree → 宿主可移除目录,保留 wt/<name> 分支
有改动 worktree → 由用户决定保留还是丢弃
待办/进行中任务 → 拒绝移除
```
任务完成与 worktree 清理也互相独立。`complete_task` 记录任务结果;队友回到 IDLE 后,用户或宿主才检查、合并、保留或移除 worktree。
### 11. 控制消息使用类型和 request_id
普通协作可以使用自由文本,关机和审批则不能依靠猜测消息意图。它们使用结构化消息:
![Team Protocols](images/team-protocols-overview.svg)
```python
@dataclass
class ProtocolState:
request_id: str
type: str
sender: str
target: str
status: str
payload: str
work_version: int | None = None
task_id: str | None = None
pending_requests: dict[str, ProtocolState] = {}
```
关机路径如下:
```text
Lead 创建 pending 状态的关机请求
→ shutdown_request(request_id) 进入队友收件箱
→ 队友完成当前步骤
→ shutdown_response(request_id) 返回 Lead
→ request_id 找到原始请求
→ pending 变为 approved队友循环退出
```
ID 把回复关联到请求,类型阻止不匹配的回复修改状态,状态则阻止同一回复重复生效。
### 12. 计划审批会约束执行
计划协议的方向相反:
```text
Lead → plan_request
队友 → plan_approval_request(request_id, plan)
Lead → plan_approval_response(request_id, approve, feedback)
```
如果 Lead 在启动队友前就知道必须先看计划,可以调用 `spawn_teammate(..., task_id=task.id, require_plan=True)`;运行时会先认领任务并打开闸门,再启动线程。对于已经运行的队友,也可以再用 `request_plan` 要求其提交计划。
工具分发层负责执行闸门:
```python
def _run_teammate_tool(name, block, handlers):
gate = plan_gates.get(name, "not_required")
if block.name in {"bash", "write_file", "edit_file"} and gate not in {
"not_required", "approved"
}:
return f"Blocked: plan status is {gate}."
try:
return handlers[block.name](**block.input)
except Exception as error:
return f"Error: {type(error).__name__}: {error}"
```
状态是 `required``pending``rejected` 时,队友可以读取文件、提交或修改计划,但不能运行 Shell 命令、写文件或编辑文件。提交计划时会记录队友当前的 task 和 work version审批返回时两者仍然一致才会生效。认领或释放任务会改变 work version使旧审批失效普通消息不会改变任务身份或审批状态。
队友不会直接从后台线程读取用户输入。遇到需要用户确认的危险命令或工作区外路径时,工具会返回 permission 错误,由 Lead 与用户处理。
---
## 一次完整运行
```text
s13 >> 把后端重构拆到共享任务板,尽量并行完成配置、认证和测试。
认证任务使用 worktree保持现有接口并确保测试通过。
Lead我建议按 config、auth 和 tests 三个方向分工。
是否启动团队?
s13 >> 开始吧
[task] config created
[task] auth created → worktree auth-refactor
[task] tests created
[claim] alice → config (cwd: repository)
[claim] bob → auth (cwd: .worktrees/auth-refactor)
[teammate] alice spawned
[teammate] bob spawned
[complete] auth
[bus] bob → lead (result) ...
[bus] bob → lead (idle_notification) ...
[wake: 2 team events → new turn]
Lead我已收到认证任务的结果接下来继续协调其余工作。
```
终端会显示用户请求、Lead 的团队方案、任务状态、认领结果、所选目录、结果、IDLE 切换和控制事件。用户不需要指定谁是 Lead也不必提醒它检查收件箱。
---
## 相对 s10 的变化
| 组件 | s10 | s13 |
|---|---|---|
| Agent | 单个 Agent | 一个 Lead 加持久队友 |
| 用户流程 | 直接执行请求 | 先提团队方案,再确认启动 |
| 通信 | 无 | 文件收件箱加运行时投递 |
| 生命周期 | 一个循环 | 队友 `WORK / IDLE / shutdown` |
| 共享工作 | 单 Agent 使用任务工具 | IDLE 扫描加队友原子认领 |
| 工作目录 | 仓库 `WORKDIR` | 必须认领任务;任务可选 worktree |
| 结果上报 | 当前 Agent 输出 | 分开的 `result``idle_notification` |
| 控制 | 无 | 类型化关机与计划审批协议 |
| 执行约束 | 无团队约束 | 必需计划会锁住修改型工具 |
---
## 试一下
```sh
cd learn-claude-code
python s13_agent_teams/code.py
```
输入一个自然需求:
```text
把后端重构拆到共享任务板,在依赖允许时并行完成配置、认证和测试。
认证任务使用 worktree保持现有接口并在最后汇总结果。
```
Lead 提出团队方案后回复:
```text
开始吧
```
观察 `.tasks/` 如何从 `pending` 进入 `in_progress``completed``.mailboxes/` 如何投递 `result``idle_notification`,以及 `.worktrees/` 是否只为绑定的任务创建。还可以检查直接消息是否先于任务板扫描,以及 `complete_task` 失败后队友的工作目录是否保持不变。
---
## 接下来
Lead 和队友目前只能调用直接写在 `code.py` 里的工具。接入 Jira、部署平台或知识库时Harness 还要为每个外部系统分别编写工具定义和调用逻辑;外部系统增加或修改工具,也要跟着修改课程代码。
s14 MCP Tools → 通过统一的发现与调用协议,在运行时连接外部服务并把它们的工具加入工具池。
<!-- translation-sync: zh@v11, en@v11, ja@v11 -->

1794
s13_agent_teams/code.py Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,107 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 620" font-family="system-ui, -apple-system, sans-serif">
<defs>
<linearGradient id="header" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="#1e3a5f"/><stop offset="100%" stop-color="#0891b2"/>
</linearGradient>
<marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#475569"/>
</marker>
<marker id="arrow-cyan" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#0891b2"/>
</marker>
<marker id="arrow-green" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#16a34a"/>
</marker>
<marker id="arrow-amber" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#d97706"/>
</marker>
</defs>
<rect width="760" height="620" rx="8" fill="#fafbfc"/>
<rect width="760" height="44" rx="8" fill="url(#header)"/>
<rect y="36" width="760" height="8" fill="url(#header)"/>
<text x="380" y="28" text-anchor="middle" fill="#fff" font-size="15" font-weight="700">Agent Teams — One Team Harness</text>
<!-- User confirmation, Lead, and typed control -->
<rect x="28" y="82" width="140" height="62" rx="8" fill="#f8fafc" stroke="#64748b" stroke-width="1.5"/>
<text x="98" y="106" text-anchor="middle" fill="#334155" font-size="11" font-weight="700">User</text>
<text x="98" y="124" text-anchor="middle" fill="#64748b" font-size="9">confirm team first</text>
<line x1="168" y1="113" x2="220" y2="113" stroke="#475569" stroke-width="1.5" marker-end="url(#arrow)"/>
<rect x="220" y="72" width="300" height="82" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.8"/>
<text x="370" y="96" text-anchor="middle" fill="#1e3a5f" font-size="12" font-weight="700">Lead Agent Loop</text>
<text x="370" y="116" text-anchor="middle" fill="#475569" font-size="9">user conversation · task creation · team coordination</text>
<text x="370" y="134" text-anchor="middle" fill="#2563eb" font-size="9" font-weight="600">spawn · send · worktree create · plan review</text>
<line x1="520" y1="113" x2="570" y2="113" stroke="#d97706" stroke-width="1.5" marker-end="url(#arrow-amber)"/>
<rect x="570" y="82" width="162" height="62" rx="8" fill="#fffbeb" stroke="#d97706" stroke-width="1.5"/>
<text x="651" y="105" text-anchor="middle" fill="#92400e" font-size="10" font-weight="700">Typed control</text>
<text x="651" y="122" text-anchor="middle" fill="#a16207" font-size="8.5">request_id · shutdown</text>
<text x="651" y="136" text-anchor="middle" fill="#a16207" font-size="8.5">plan approval gate</text>
<!-- Connect top row to MessageBus; endpoints meet box edges -->
<line x1="370" y1="154" x2="370" y2="190" stroke="#0891b2" stroke-width="1.8" marker-end="url(#arrow-cyan)"/>
<line x1="651" y1="144" x2="651" y2="190" stroke="#d97706" stroke-width="1.5" marker-end="url(#arrow-amber)"/>
<rect x="60" y="190" width="640" height="44" rx="22" fill="#ecfeff" stroke="#0891b2" stroke-width="2"/>
<text x="380" y="209" text-anchor="middle" fill="#0e7490" font-size="11" font-weight="700">MessageBus · .mailboxes/&lt;name&gt;.jsonl</text>
<text x="380" y="225" text-anchor="middle" fill="#0f766e" font-size="8.5">runtime delivery · ordinary messages · result · idle_notification · control events</text>
<!-- MessageBus and teammate loops -->
<line x1="118" y1="234" x2="118" y2="282" stroke="#16a34a" stroke-width="1.5" marker-end="url(#arrow-green)"/>
<line x1="348" y1="234" x2="348" y2="282" stroke="#16a34a" stroke-width="1.5" marker-end="url(#arrow-green)"/>
<line x1="578" y1="234" x2="578" y2="282" stroke="#16a34a" stroke-width="1.5" marker-end="url(#arrow-green)"/>
<line x1="182" y1="282" x2="182" y2="234" stroke="#0891b2" stroke-width="1.2" stroke-dasharray="4 3" marker-end="url(#arrow-cyan)"/>
<line x1="412" y1="282" x2="412" y2="234" stroke="#0891b2" stroke-width="1.2" stroke-dasharray="4 3" marker-end="url(#arrow-cyan)"/>
<line x1="642" y1="282" x2="642" y2="234" stroke="#0891b2" stroke-width="1.2" stroke-dasharray="4 3" marker-end="url(#arrow-cyan)"/>
<rect x="40" y="282" width="220" height="82" rx="8" fill="#f0fdf4" stroke="#16a34a" stroke-width="1.5"/>
<text x="150" y="305" text-anchor="middle" fill="#166534" font-size="10.5" font-weight="700">Teammate: config</text>
<text x="150" y="324" text-anchor="middle" fill="#15803d" font-size="8.5">independent prompt · messages · tools</text>
<text x="150" y="342" text-anchor="middle" fill="#475569" font-size="8.5">WORK → result → IDLE</text>
<text x="150" y="356" text-anchor="middle" fill="#64748b" font-size="8">direct message returns to WORK</text>
<rect x="270" y="282" width="220" height="82" rx="8" fill="#f0fdf4" stroke="#16a34a" stroke-width="1.5"/>
<text x="380" y="305" text-anchor="middle" fill="#166534" font-size="10.5" font-weight="700">Teammate: auth</text>
<text x="380" y="324" text-anchor="middle" fill="#15803d" font-size="8.5">independent prompt · messages · tools</text>
<text x="380" y="342" text-anchor="middle" fill="#475569" font-size="8.5">WORK → result → IDLE</text>
<text x="380" y="356" text-anchor="middle" fill="#64748b" font-size="8">claimed task returns to WORK</text>
<rect x="500" y="282" width="220" height="82" rx="8" fill="#f0fdf4" stroke="#16a34a" stroke-width="1.5"/>
<text x="610" y="305" text-anchor="middle" fill="#166534" font-size="10.5" font-weight="700">Teammate: tests</text>
<text x="610" y="324" text-anchor="middle" fill="#15803d" font-size="8.5">independent prompt · messages · tools</text>
<text x="610" y="342" text-anchor="middle" fill="#475569" font-size="8.5">WORK → result → IDLE</text>
<text x="610" y="356" text-anchor="middle" fill="#64748b" font-size="8">shutdown exits the loop</text>
<!-- IDLE task discovery -->
<line x1="150" y1="364" x2="150" y2="404" stroke="#16a34a" stroke-width="1.5" marker-end="url(#arrow-green)"/>
<line x1="380" y1="364" x2="380" y2="404" stroke="#16a34a" stroke-width="1.5" marker-end="url(#arrow-green)"/>
<line x1="610" y1="364" x2="610" y2="404" stroke="#16a34a" stroke-width="1.5" marker-end="url(#arrow-green)"/>
<rect x="60" y="404" width="640" height="54" rx="8" fill="#f8fafc" stroke="#64748b" stroke-width="1.5"/>
<text x="380" y="425" text-anchor="middle" fill="#334155" font-size="11" font-weight="700">Shared Task Board · .tasks/</text>
<text x="380" y="444" text-anchor="middle" fill="#475569" font-size="9">IDLE: wait for mailbox first → scan ready tasks → claim atomically → reuse WORK loop</text>
<!-- Task binding selects cwd -->
<line x1="380" y1="458" x2="380" y2="482" stroke="#475569" stroke-width="1.5"/>
<line x1="220" y1="482" x2="540" y2="482" stroke="#475569" stroke-width="1.5"/>
<line x1="220" y1="482" x2="220" y2="506" stroke="#475569" stroke-width="1.5" marker-end="url(#arrow)"/>
<line x1="540" y1="482" x2="540" y2="506" stroke="#475569" stroke-width="1.5" marker-end="url(#arrow)"/>
<rect x="311" y="468" width="138" height="18" rx="4" fill="#fafbfc"/>
<text x="380" y="480" text-anchor="middle" fill="#475569" font-size="8.5" font-weight="600">Task.worktree selects cwd</text>
<rect x="60" y="506" width="300" height="66" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
<text x="210" y="529" text-anchor="middle" fill="#1e3a5f" font-size="10.5" font-weight="700">No worktree: repository WORKDIR</text>
<text x="210" y="548" text-anchor="middle" fill="#475569" font-size="8.5">task.worktree is null</text>
<text x="210" y="562" text-anchor="middle" fill="#64748b" font-size="8">same behavior as earlier lessons</text>
<rect x="400" y="506" width="300" height="66" rx="8" fill="#fff7ed" stroke="#d97706" stroke-width="1.5"/>
<text x="550" y="529" text-anchor="middle" fill="#92400e" font-size="10.5" font-weight="700">Opt-in: .worktrees/&lt;name&gt;</text>
<text x="550" y="548" text-anchor="middle" fill="#a16207" font-size="8.5">separate checkout + retained wt/&lt;name&gt; branch</text>
<text x="550" y="562" text-anchor="middle" fill="#78716c" font-size="8">working-directory isolation, not a sandbox</text>
<rect x="60" y="590" width="640" height="20" rx="5" fill="#ecfdf5" stroke="#bbf7d0"/>
<text x="380" y="604" text-anchor="middle" fill="#166534" font-size="8.5" font-weight="600">Only successful task completion clears the teammate assignment and cwd.</text>
</svg>

After

Width:  |  Height:  |  Size: 8.5 KiB

View File

@@ -0,0 +1,107 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 620" font-family="system-ui, -apple-system, sans-serif">
<defs>
<linearGradient id="header" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="#1e3a5f"/><stop offset="100%" stop-color="#0891b2"/>
</linearGradient>
<marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#475569"/>
</marker>
<marker id="arrow-cyan" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#0891b2"/>
</marker>
<marker id="arrow-green" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#16a34a"/>
</marker>
<marker id="arrow-amber" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#d97706"/>
</marker>
</defs>
<rect width="760" height="620" rx="8" fill="#fafbfc"/>
<rect width="760" height="44" rx="8" fill="url(#header)"/>
<rect y="36" width="760" height="8" fill="url(#header)"/>
<text x="380" y="28" text-anchor="middle" fill="#fff" font-size="15" font-weight="700">Agent Teams — 1 つの Team Harness</text>
<!-- ユーザー確認、Lead、制御プロトコル -->
<rect x="28" y="82" width="140" height="62" rx="8" fill="#f8fafc" stroke="#64748b" stroke-width="1.5"/>
<text x="98" y="106" text-anchor="middle" fill="#334155" font-size="11" font-weight="700">ユーザー</text>
<text x="98" y="124" text-anchor="middle" fill="#64748b" font-size="9">先にチームを確認</text>
<line x1="168" y1="113" x2="220" y2="113" stroke="#475569" stroke-width="1.5" marker-end="url(#arrow)"/>
<rect x="220" y="72" width="300" height="82" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.8"/>
<text x="370" y="96" text-anchor="middle" fill="#1e3a5f" font-size="12" font-weight="700">Lead Agent Loop</text>
<text x="370" y="116" text-anchor="middle" fill="#475569" font-size="9">ユーザー対話 · タスク作成 · チーム調整</text>
<text x="370" y="134" text-anchor="middle" fill="#2563eb" font-size="9" font-weight="600">起動 · 送信 · worktree 作成 · 計画レビュー</text>
<line x1="520" y1="113" x2="570" y2="113" stroke="#d97706" stroke-width="1.5" marker-end="url(#arrow-amber)"/>
<rect x="570" y="82" width="162" height="62" rx="8" fill="#fffbeb" stroke="#d97706" stroke-width="1.5"/>
<text x="651" y="105" text-anchor="middle" fill="#92400e" font-size="10" font-weight="700">型付き制御</text>
<text x="651" y="122" text-anchor="middle" fill="#a16207" font-size="8.5">request_id · shutdown</text>
<text x="651" y="136" text-anchor="middle" fill="#a16207" font-size="8.5">計画承認ゲート</text>
<!-- 線の端点はコンポーネントの境界に合わせる -->
<line x1="370" y1="154" x2="370" y2="190" stroke="#0891b2" stroke-width="1.8" marker-end="url(#arrow-cyan)"/>
<line x1="651" y1="144" x2="651" y2="190" stroke="#d97706" stroke-width="1.5" marker-end="url(#arrow-amber)"/>
<rect x="60" y="190" width="640" height="44" rx="22" fill="#ecfeff" stroke="#0891b2" stroke-width="2"/>
<text x="380" y="209" text-anchor="middle" fill="#0e7490" font-size="11" font-weight="700">MessageBus · .mailboxes/&lt;name&gt;.jsonl</text>
<text x="380" y="225" text-anchor="middle" fill="#0f766e" font-size="8.5">ランタイム配信 · 通常メッセージ · result · idle_notification · 制御イベント</text>
<!-- MessageBus とチームメイトループ -->
<line x1="118" y1="234" x2="118" y2="282" stroke="#16a34a" stroke-width="1.5" marker-end="url(#arrow-green)"/>
<line x1="348" y1="234" x2="348" y2="282" stroke="#16a34a" stroke-width="1.5" marker-end="url(#arrow-green)"/>
<line x1="578" y1="234" x2="578" y2="282" stroke="#16a34a" stroke-width="1.5" marker-end="url(#arrow-green)"/>
<line x1="182" y1="282" x2="182" y2="234" stroke="#0891b2" stroke-width="1.2" stroke-dasharray="4 3" marker-end="url(#arrow-cyan)"/>
<line x1="412" y1="282" x2="412" y2="234" stroke="#0891b2" stroke-width="1.2" stroke-dasharray="4 3" marker-end="url(#arrow-cyan)"/>
<line x1="642" y1="282" x2="642" y2="234" stroke="#0891b2" stroke-width="1.2" stroke-dasharray="4 3" marker-end="url(#arrow-cyan)"/>
<rect x="40" y="282" width="220" height="82" rx="8" fill="#f0fdf4" stroke="#16a34a" stroke-width="1.5"/>
<text x="150" y="305" text-anchor="middle" fill="#166534" font-size="10.5" font-weight="700">チームメイトconfig</text>
<text x="150" y="324" text-anchor="middle" fill="#15803d" font-size="8.5">独立した prompt · messages · tools</text>
<text x="150" y="342" text-anchor="middle" fill="#475569" font-size="8.5">WORK → result → IDLE</text>
<text x="150" y="356" text-anchor="middle" fill="#64748b" font-size="8">直接メッセージで WORK へ戻る</text>
<rect x="270" y="282" width="220" height="82" rx="8" fill="#f0fdf4" stroke="#16a34a" stroke-width="1.5"/>
<text x="380" y="305" text-anchor="middle" fill="#166534" font-size="10.5" font-weight="700">チームメイトauth</text>
<text x="380" y="324" text-anchor="middle" fill="#15803d" font-size="8.5">独立した prompt · messages · tools</text>
<text x="380" y="342" text-anchor="middle" fill="#475569" font-size="8.5">WORK → result → IDLE</text>
<text x="380" y="356" text-anchor="middle" fill="#64748b" font-size="8">Claim したタスクで WORK へ戻る</text>
<rect x="500" y="282" width="220" height="82" rx="8" fill="#f0fdf4" stroke="#16a34a" stroke-width="1.5"/>
<text x="610" y="305" text-anchor="middle" fill="#166534" font-size="10.5" font-weight="700">チームメイトtests</text>
<text x="610" y="324" text-anchor="middle" fill="#15803d" font-size="8.5">独立した prompt · messages · tools</text>
<text x="610" y="342" text-anchor="middle" fill="#475569" font-size="8.5">WORK → result → IDLE</text>
<text x="610" y="356" text-anchor="middle" fill="#64748b" font-size="8">shutdown でループ終了</text>
<!-- IDLE のタスク発見 -->
<line x1="150" y1="364" x2="150" y2="404" stroke="#16a34a" stroke-width="1.5" marker-end="url(#arrow-green)"/>
<line x1="380" y1="364" x2="380" y2="404" stroke="#16a34a" stroke-width="1.5" marker-end="url(#arrow-green)"/>
<line x1="610" y1="364" x2="610" y2="404" stroke="#16a34a" stroke-width="1.5" marker-end="url(#arrow-green)"/>
<rect x="60" y="404" width="640" height="54" rx="8" fill="#f8fafc" stroke="#64748b" stroke-width="1.5"/>
<text x="380" y="425" text-anchor="middle" fill="#334155" font-size="11" font-weight="700">共有タスクボード · .tasks/</text>
<text x="380" y="444" text-anchor="middle" fill="#475569" font-size="9">IDLE受信箱を先に待つ → ready task を走査 → アトミックに Claim → WORK を再利用</text>
<!-- タスクの紐付けが cwd を選ぶ -->
<line x1="380" y1="458" x2="380" y2="482" stroke="#475569" stroke-width="1.5"/>
<line x1="220" y1="482" x2="540" y2="482" stroke="#475569" stroke-width="1.5"/>
<line x1="220" y1="482" x2="220" y2="506" stroke="#475569" stroke-width="1.5" marker-end="url(#arrow)"/>
<line x1="540" y1="482" x2="540" y2="506" stroke="#475569" stroke-width="1.5" marker-end="url(#arrow)"/>
<rect x="311" y="468" width="138" height="18" rx="4" fill="#fafbfc"/>
<text x="380" y="480" text-anchor="middle" fill="#475569" font-size="8.5" font-weight="600">Task.worktree が cwd を選択</text>
<rect x="60" y="506" width="300" height="66" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
<text x="210" y="529" text-anchor="middle" fill="#1e3a5f" font-size="10.5" font-weight="700">worktree なし:リポジトリ WORKDIR</text>
<text x="210" y="548" text-anchor="middle" fill="#475569" font-size="8.5">task.worktree は null</text>
<text x="210" y="562" text-anchor="middle" fill="#64748b" font-size="8">以前のレッスンと同じ動作</text>
<rect x="400" y="506" width="300" height="66" rx="8" fill="#fff7ed" stroke="#d97706" stroke-width="1.5"/>
<text x="550" y="529" text-anchor="middle" fill="#92400e" font-size="10.5" font-weight="700">任意:.worktrees/&lt;name&gt;</text>
<text x="550" y="548" text-anchor="middle" fill="#a16207" font-size="8.5">独立 checkout + wt/&lt;name&gt; branch を保持</text>
<text x="550" y="562" text-anchor="middle" fill="#78716c" font-size="8">作業ディレクトリの分離であり sandbox ではない</text>
<rect x="60" y="590" width="640" height="20" rx="5" fill="#ecfdf5" stroke="#bbf7d0"/>
<text x="380" y="604" text-anchor="middle" fill="#166534" font-size="8.5" font-weight="600">タスク完了に成功した時だけ、チームメイトの assignment と cwd を解除する。</text>
</svg>

After

Width:  |  Height:  |  Size: 8.7 KiB

View File

@@ -0,0 +1,107 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 620" font-family="system-ui, -apple-system, sans-serif">
<defs>
<linearGradient id="header" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="#1e3a5f"/><stop offset="100%" stop-color="#0891b2"/>
</linearGradient>
<marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#475569"/>
</marker>
<marker id="arrow-cyan" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#0891b2"/>
</marker>
<marker id="arrow-green" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#16a34a"/>
</marker>
<marker id="arrow-amber" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#d97706"/>
</marker>
</defs>
<rect width="760" height="620" rx="8" fill="#fafbfc"/>
<rect width="760" height="44" rx="8" fill="url(#header)"/>
<rect y="36" width="760" height="8" fill="url(#header)"/>
<text x="380" y="28" text-anchor="middle" fill="#fff" font-size="15" font-weight="700">Agent Teams — 一套 Team Harness</text>
<!-- 用户确认、Lead 与控制协议 -->
<rect x="28" y="82" width="140" height="62" rx="8" fill="#f8fafc" stroke="#64748b" stroke-width="1.5"/>
<text x="98" y="106" text-anchor="middle" fill="#334155" font-size="11" font-weight="700">用户</text>
<text x="98" y="124" text-anchor="middle" fill="#64748b" font-size="9">先确认团队方案</text>
<line x1="168" y1="113" x2="220" y2="113" stroke="#475569" stroke-width="1.5" marker-end="url(#arrow)"/>
<rect x="220" y="72" width="300" height="82" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.8"/>
<text x="370" y="96" text-anchor="middle" fill="#1e3a5f" font-size="12" font-weight="700">Lead Agent Loop</text>
<text x="370" y="116" text-anchor="middle" fill="#475569" font-size="9">用户对话 · 创建任务 · 协调团队</text>
<text x="370" y="134" text-anchor="middle" fill="#2563eb" font-size="9" font-weight="600">启动 · 发消息 · 创建 worktree · 审批计划</text>
<line x1="520" y1="113" x2="570" y2="113" stroke="#d97706" stroke-width="1.5" marker-end="url(#arrow-amber)"/>
<rect x="570" y="82" width="162" height="62" rx="8" fill="#fffbeb" stroke="#d97706" stroke-width="1.5"/>
<text x="651" y="105" text-anchor="middle" fill="#92400e" font-size="10" font-weight="700">类型化控制</text>
<text x="651" y="122" text-anchor="middle" fill="#a16207" font-size="8.5">request_id · shutdown</text>
<text x="651" y="136" text-anchor="middle" fill="#a16207" font-size="8.5">计划审批闸门</text>
<!-- 连线端点精确落在组件边缘 -->
<line x1="370" y1="154" x2="370" y2="190" stroke="#0891b2" stroke-width="1.8" marker-end="url(#arrow-cyan)"/>
<line x1="651" y1="144" x2="651" y2="190" stroke="#d97706" stroke-width="1.5" marker-end="url(#arrow-amber)"/>
<rect x="60" y="190" width="640" height="44" rx="22" fill="#ecfeff" stroke="#0891b2" stroke-width="2"/>
<text x="380" y="209" text-anchor="middle" fill="#0e7490" font-size="11" font-weight="700">MessageBus · .mailboxes/&lt;name&gt;.jsonl</text>
<text x="380" y="225" text-anchor="middle" fill="#0f766e" font-size="8.5">运行时投递 · 普通消息 · result · idle_notification · 控制事件</text>
<!-- MessageBus 与队友循环 -->
<line x1="118" y1="234" x2="118" y2="282" stroke="#16a34a" stroke-width="1.5" marker-end="url(#arrow-green)"/>
<line x1="348" y1="234" x2="348" y2="282" stroke="#16a34a" stroke-width="1.5" marker-end="url(#arrow-green)"/>
<line x1="578" y1="234" x2="578" y2="282" stroke="#16a34a" stroke-width="1.5" marker-end="url(#arrow-green)"/>
<line x1="182" y1="282" x2="182" y2="234" stroke="#0891b2" stroke-width="1.2" stroke-dasharray="4 3" marker-end="url(#arrow-cyan)"/>
<line x1="412" y1="282" x2="412" y2="234" stroke="#0891b2" stroke-width="1.2" stroke-dasharray="4 3" marker-end="url(#arrow-cyan)"/>
<line x1="642" y1="282" x2="642" y2="234" stroke="#0891b2" stroke-width="1.2" stroke-dasharray="4 3" marker-end="url(#arrow-cyan)"/>
<rect x="40" y="282" width="220" height="82" rx="8" fill="#f0fdf4" stroke="#16a34a" stroke-width="1.5"/>
<text x="150" y="305" text-anchor="middle" fill="#166534" font-size="10.5" font-weight="700">队友config</text>
<text x="150" y="324" text-anchor="middle" fill="#15803d" font-size="8.5">独立 prompt · messages · tools</text>
<text x="150" y="342" text-anchor="middle" fill="#475569" font-size="8.5">WORK → result → IDLE</text>
<text x="150" y="356" text-anchor="middle" fill="#64748b" font-size="8">直接消息使其回到 WORK</text>
<rect x="270" y="282" width="220" height="82" rx="8" fill="#f0fdf4" stroke="#16a34a" stroke-width="1.5"/>
<text x="380" y="305" text-anchor="middle" fill="#166534" font-size="10.5" font-weight="700">队友auth</text>
<text x="380" y="324" text-anchor="middle" fill="#15803d" font-size="8.5">独立 prompt · messages · tools</text>
<text x="380" y="342" text-anchor="middle" fill="#475569" font-size="8.5">WORK → result → IDLE</text>
<text x="380" y="356" text-anchor="middle" fill="#64748b" font-size="8">认领任务使其回到 WORK</text>
<rect x="500" y="282" width="220" height="82" rx="8" fill="#f0fdf4" stroke="#16a34a" stroke-width="1.5"/>
<text x="610" y="305" text-anchor="middle" fill="#166534" font-size="10.5" font-weight="700">队友tests</text>
<text x="610" y="324" text-anchor="middle" fill="#15803d" font-size="8.5">独立 prompt · messages · tools</text>
<text x="610" y="342" text-anchor="middle" fill="#475569" font-size="8.5">WORK → result → IDLE</text>
<text x="610" y="356" text-anchor="middle" fill="#64748b" font-size="8">shutdown 结束循环</text>
<!-- IDLE 时发现任务 -->
<line x1="150" y1="364" x2="150" y2="404" stroke="#16a34a" stroke-width="1.5" marker-end="url(#arrow-green)"/>
<line x1="380" y1="364" x2="380" y2="404" stroke="#16a34a" stroke-width="1.5" marker-end="url(#arrow-green)"/>
<line x1="610" y1="364" x2="610" y2="404" stroke="#16a34a" stroke-width="1.5" marker-end="url(#arrow-green)"/>
<rect x="60" y="404" width="640" height="54" rx="8" fill="#f8fafc" stroke="#64748b" stroke-width="1.5"/>
<text x="380" y="425" text-anchor="middle" fill="#334155" font-size="11" font-weight="700">共享任务板 · .tasks/</text>
<text x="380" y="444" text-anchor="middle" fill="#475569" font-size="9">IDLE先等收件箱 → 扫描 ready task → 原子认领 → 复用 WORK 循环</text>
<!-- 任务绑定选择 cwd -->
<line x1="380" y1="458" x2="380" y2="482" stroke="#475569" stroke-width="1.5"/>
<line x1="220" y1="482" x2="540" y2="482" stroke="#475569" stroke-width="1.5"/>
<line x1="220" y1="482" x2="220" y2="506" stroke="#475569" stroke-width="1.5" marker-end="url(#arrow)"/>
<line x1="540" y1="482" x2="540" y2="506" stroke="#475569" stroke-width="1.5" marker-end="url(#arrow)"/>
<rect x="311" y="468" width="138" height="18" rx="4" fill="#fafbfc"/>
<text x="380" y="480" text-anchor="middle" fill="#475569" font-size="8.5" font-weight="600">Task.worktree 选择 cwd</text>
<rect x="60" y="506" width="300" height="66" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
<text x="210" y="529" text-anchor="middle" fill="#1e3a5f" font-size="10.5" font-weight="700">无 worktree仓库 WORKDIR</text>
<text x="210" y="548" text-anchor="middle" fill="#475569" font-size="8.5">task.worktree 为 null</text>
<text x="210" y="562" text-anchor="middle" fill="#64748b" font-size="8">行为与前面章节一致</text>
<rect x="400" y="506" width="300" height="66" rx="8" fill="#fff7ed" stroke="#d97706" stroke-width="1.5"/>
<text x="550" y="529" text-anchor="middle" fill="#92400e" font-size="10.5" font-weight="700">按需开启:.worktrees/&lt;name&gt;</text>
<text x="550" y="548" text-anchor="middle" fill="#a16207" font-size="8.5">独立 checkout + 保留 wt/&lt;name&gt; 分支</text>
<text x="550" y="562" text-anchor="middle" fill="#78716c" font-size="8">只隔开工作目录,不是安全沙箱</text>
<rect x="60" y="590" width="640" height="20" rx="5" fill="#ecfdf5" stroke="#bbf7d0"/>
<text x="380" y="604" text-anchor="middle" fill="#166534" font-size="8.5" font-weight="600">只有任务成功完成后,运行时才会清除队友的 assignment 和 cwd。</text>
</svg>

After

Width:  |  Height:  |  Size: 8.4 KiB

View File

@@ -0,0 +1,141 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 664" font-family="system-ui, -apple-system, sans-serif">
<defs>
<linearGradient id="header" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="#1e3a5f"/><stop offset="100%" stop-color="#7c3aed"/>
</linearGradient>
<marker id="arrow" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#555"/>
</marker>
<marker id="arrow-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="6" markerHeight="6" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#16a34a"/>
</marker>
<marker id="arrow-red" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#dc2626"/>
</marker>
</defs>
<rect width="760" height="664" fill="#fafbfc" rx="8"/>
<rect x="0" y="0" width="760" height="44" fill="url(#header)" rx="8"/>
<rect x="0" y="36" width="760" height="8" fill="url(#header)"/>
<text x="380" y="28" fill="#fff" font-size="15" font-weight="700" text-anchor="middle">Team Protocols — Request-Response + request_id Correlation + State Machine</text>
<rect x="40" y="56" width="12" height="10" rx="2" fill="#f0f4ff" stroke="#2563eb" stroke-width="1"/>
<text x="58" y="66" fill="#2563eb" font-size="10" font-weight="600">Team runtime</text>
<rect x="160" y="56" width="12" height="10" rx="2" fill="#f5f3ff" stroke="#7c3aed" stroke-width="1"/>
<text x="178" y="66" fill="#7c3aed" font-size="10" font-weight="600">Protocols</text>
<!-- Row 1: Lead Loop -->
<rect x="20" y="90" width="70" height="40" rx="8" fill="#eef2ff" stroke="#4f46e5" stroke-width="1.5"/>
<text x="55" y="114" fill="#4f46e5" font-size="8" font-weight="600" text-anchor="middle">turn</text>
<line x1="90" y1="110" x2="104" y2="110" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)"/>
<rect x="107" y="90" width="70" height="40" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
<text x="142" y="114" fill="#1e3a5f" font-size="10" font-weight="600" text-anchor="middle">messages</text>
<line x1="177" y1="110" x2="191" y2="110" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)"/>
<rect x="194" y="86" width="80" height="48" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
<text x="234" y="114" fill="#1e3a5f" font-size="9" font-weight="600" text-anchor="middle">prompt</text>
<line x1="274" y1="110" x2="288" y2="110" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)"/>
<rect x="291" y="86" width="70" height="48" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
<text x="326" y="114" fill="#1e3a5f" font-size="9" font-weight="600" text-anchor="middle">LLM</text>
<line x1="361" y1="110" x2="375" y2="110" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)"/>
<rect x="378" y="80" width="356" height="60" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
<text x="556" y="98" fill="#1e3a5f" font-size="10" font-weight="600" text-anchor="middle">TOOL DISPATCH (core tool set)</text>
<text x="394" y="114" fill="#2563eb" font-size="8">base(5) · task(5) · team(7)</text>
<text x="394" y="128" fill="#7c3aed" font-size="8" font-weight="700">★ request_shutdown · request_plan · review_plan</text>
<path d="M 734 110 L 748 110 L 748 150 L 55 150 L 55 130" fill="none" stroke="#94a3b8" stroke-width="1" marker-end="url(#arrow)" stroke-dasharray="5,4"/>
<!-- Row 2: shutdown protocol -->
<rect x="30" y="176" width="700" height="90" rx="8" fill="#fef3c7" stroke="#d97706" stroke-width="1.5"/>
<text x="380" y="196" fill="#92400e" font-size="11" font-weight="700" text-anchor="middle">Protocol A: Shutdown (Lead initiates → Teammate responds)</text>
<rect x="50" y="208" width="180" height="46" rx="6" fill="#fff" stroke="#d97706" stroke-width="1"/>
<text x="140" y="226" fill="#92400e" font-size="9" font-weight="600" text-anchor="middle">① Lead: request_shutdown</text>
<text x="140" y="242" fill="#6b7280" font-size="7" text-anchor="middle">new_request_id() → ProtocolState</text>
<line x1="230" y1="231" x2="254" y2="231" stroke="#d97706" stroke-width="1.5" marker-end="url(#arrow)"/>
<rect x="257" y="208" width="180" height="46" rx="6" fill="#fff" stroke="#d97706" stroke-width="1"/>
<text x="347" y="226" fill="#92400e" font-size="9" font-weight="600" text-anchor="middle">② Teammate: handle_shutdown</text>
<text x="347" y="242" fill="#6b7280" font-size="7" text-anchor="middle">ack → shutdown_response</text>
<line x1="437" y1="231" x2="461" y2="231" stroke="#d97706" stroke-width="1.5" marker-end="url(#arrow)"/>
<rect x="464" y="208" width="180" height="46" rx="6" fill="#fff" stroke="#d97706" stroke-width="1"/>
<text x="554" y="226" fill="#92400e" font-size="9" font-weight="600" text-anchor="middle">③ Lead: consume_lead_inbox</text>
<text x="554" y="242" fill="#6b7280" font-size="7" text-anchor="middle">match_response(request_id) → ✓</text>
<!-- Row 3: plan approval protocol -->
<rect x="30" y="280" width="700" height="110" rx="8" fill="#dbeafe" stroke="#2563eb" stroke-width="1.5"/>
<text x="380" y="300" fill="#1e40af" font-size="11" font-weight="700" text-anchor="middle">Protocol B: Plan Approval (Teammate initiates → Lead reviews)</text>
<rect x="50" y="312" width="150" height="46" rx="6" fill="#f1f5f9" stroke="#94a3b8" stroke-width="1"/>
<text x="125" y="330" fill="#475569" font-size="8" font-weight="600" text-anchor="middle">0. Lead: request_plan</text>
<text x="125" y="346" fill="#6b7280" font-size="7" text-anchor="middle">plan_request message</text>
<line x1="200" y1="335" x2="222" y2="335" stroke="#94a3b8" stroke-width="1" marker-end="url(#arrow)" stroke-dasharray="4,3"/>
<rect x="225" y="312" width="160" height="46" rx="6" fill="#fff" stroke="#2563eb" stroke-width="1"/>
<text x="305" y="330" fill="#1e40af" font-size="8" font-weight="600" text-anchor="middle">① Teammate: submit_plan</text>
<text x="305" y="346" fill="#6b7280" font-size="7" text-anchor="middle">plan_approval_request + request_id</text>
<line x1="385" y1="335" x2="407" y2="335" stroke="#2563eb" stroke-width="1.5" marker-end="url(#arrow)"/>
<rect x="410" y="312" width="160" height="46" rx="6" fill="#fff" stroke="#2563eb" stroke-width="1"/>
<text x="490" y="330" fill="#1e40af" font-size="8" font-weight="600" text-anchor="middle">② Lead: review_plan</text>
<text x="490" y="346" fill="#6b7280" font-size="7" text-anchor="middle">plan_approval_response</text>
<line x1="570" y1="335" x2="592" y2="335" stroke="#2563eb" stroke-width="1.5" marker-end="url(#arrow)"/>
<rect x="595" y="312" width="120" height="46" rx="6" fill="#fff" stroke="#2563eb" stroke-width="1"/>
<text x="655" y="330" fill="#1e40af" font-size="8" font-weight="600" text-anchor="middle">③ Teammate: receives</text>
<text x="655" y="346" fill="#6b7280" font-size="7" text-anchor="middle">[Plan approved/rejected]</text>
<text x="380" y="380" fill="#6b7280" font-size="8" text-anchor="middle">request_plan sends the requirement; submit_plan creates the reviewable ProtocolState</text>
<!-- Row 4: State Machine + Storage -->
<rect x="30" y="406" width="340" height="85" rx="8" fill="#f5f3ff" stroke="#7c3aed" stroke-width="1.5"/>
<text x="200" y="426" fill="#4c1d95" font-size="10" font-weight="700" text-anchor="middle">State Machine (shared by both protocols)</text>
<rect x="55" y="440" width="58" height="20" rx="4" fill="#f1f5f9" stroke="#94a3b8" stroke-width="1"/>
<text x="84" y="454" fill="#475569" font-size="9" text-anchor="middle">pending</text>
<line x1="113" y1="450" x2="192" y2="450" stroke="#16a34a" stroke-width="1.5" marker-end="url(#arrow-green)"/>
<text x="152" y="445" fill="#16a34a" font-size="8" font-weight="600" text-anchor="middle">approve</text>
<rect x="192" y="440" width="62" height="20" rx="4" fill="#dcfce7" stroke="#16a34a" stroke-width="1"/>
<text x="223" y="454" fill="#166534" font-size="9" text-anchor="middle">approved</text>
<path d="M 84 460 L 84 478 L 128 478" fill="none" stroke="#dc2626" stroke-width="1.5" marker-end="url(#arrow-red)"/>
<text x="76" y="472" fill="#dc2626" font-size="8" font-weight="600" text-anchor="end">reject</text>
<rect x="128" y="468" width="60" height="20" rx="4" fill="#fef2f2" stroke="#dc2626" stroke-width="1"/>
<text x="158" y="482" fill="#991b1b" font-size="9" text-anchor="middle">rejected</text>
<rect x="400" y="406" width="330" height="85" rx="8" fill="#f5f3ff" stroke="#7c3aed" stroke-width="1.5"/>
<text x="565" y="426" fill="#4c1d95" font-size="10" font-weight="700" text-anchor="middle">pending_requests Storage</text>
<text x="420" y="446" fill="#7c3aed" font-size="9">pending_requests: dict[str, ProtocolState]</text>
<text x="420" y="462" fill="#6b7280" font-size="8">request_id → {type, sender, status, created_at}</text>
<text x="420" y="478" fill="#6b7280" font-size="8">match_response: find request by request_id</text>
<!-- Row 5: Two protocols same machine -->
<rect x="30" y="506" width="700" height="52" rx="6" fill="#f8fafc" stroke="#e2e8f0" stroke-width="1"/>
<text x="55" y="524" fill="#1e3a5f" font-size="10" font-weight="600">Two protocols, one mechanism:</text>
<rect x="230" y="512" width="130" height="18" rx="4" fill="#fef3c7" stroke="#d97706" stroke-width="1"/>
<text x="295" y="525" fill="#92400e" font-size="9" text-anchor="middle">shutdown_request</text>
<text x="368" y="525" fill="#475569" font-size="10">and</text>
<rect x="390" y="512" width="140" height="18" rx="4" fill="#dbeafe" stroke="#2563eb" stroke-width="1"/>
<text x="460" y="525" fill="#1e40af" font-size="9" text-anchor="middle">plan_approval_request</text>
<text x="55" y="548" fill="#6b7280" font-size="8">Share the same pending → approved / rejected state machine. New protocol type = new msg_type, no new state machine needed. request_id links request and response.</text>
<!-- Row 6: distinguish -->
<rect x="30" y="570" width="700" height="38" rx="6" fill="#f0fdf4" stroke="#16a34a" stroke-width="1"/>
<text x="55" y="588" fill="#166534" font-size="9" font-weight="600">Note:</text>
<text x="100" y="588" fill="#475569" font-size="9">request_plan sends plan_request; submit_plan creates a request_id and waits for review.</text>
<text x="55" y="602" fill="#475569" font-size="9">submit_plan is the protocol entry point (msg_type="plan_approval_request"), initiated by teammate, carrying request_id into pending_requests.</text>
<!-- Bottom notes -->
<rect x="30" y="622" width="700" height="28" rx="6" fill="#f8fafc" stroke="#e2e8f0" stroke-width="1"/>
<rect x="50" y="632" width="12" height="10" rx="2" fill="#f0f4ff" stroke="#2563eb" stroke-width="1"/>
<text x="70" y="642" fill="#475569" font-size="10">Runtime: MessageBus + persistent teammates + automatic delivery</text>
<rect x="310" y="632" width="12" height="10" rx="2" fill="#f5f3ff" stroke="#7c3aed" stroke-width="1"/>
<text x="330" y="642" fill="#475569" font-size="10">Protocol: request_id + dispatch + pending_requests + plan gate</text>
</svg>

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -0,0 +1,141 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 664" font-family="system-ui, -apple-system, sans-serif">
<defs>
<linearGradient id="header" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="#1e3a5f"/><stop offset="100%" stop-color="#7c3aed"/>
</linearGradient>
<marker id="arrow" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#555"/>
</marker>
<marker id="arrow-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="6" markerHeight="6" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#16a34a"/>
</marker>
<marker id="arrow-red" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#dc2626"/>
</marker>
</defs>
<rect width="760" height="664" fill="#fafbfc" rx="8"/>
<rect x="0" y="0" width="760" height="44" fill="url(#header)" rx="8"/>
<rect x="0" y="36" width="760" height="8" fill="url(#header)"/>
<text x="380" y="28" fill="#fff" font-size="14" font-weight="700" text-anchor="middle">Team Protocols — リクエスト・レスポンス + request_id 紐付け + 状態機械</text>
<rect x="40" y="56" width="12" height="10" rx="2" fill="#f0f4ff" stroke="#2563eb" stroke-width="1"/>
<text x="58" y="66" fill="#2563eb" font-size="10" font-weight="600">チームランタイム</text>
<rect x="140" y="56" width="12" height="10" rx="2" fill="#f5f3ff" stroke="#7c3aed" stroke-width="1"/>
<text x="158" y="66" fill="#7c3aed" font-size="10" font-weight="600">協調プロトコル</text>
<!-- Row 1: Lead Loop -->
<rect x="20" y="90" width="70" height="40" rx="8" fill="#eef2ff" stroke="#4f46e5" stroke-width="1.5"/>
<text x="55" y="114" fill="#4f46e5" font-size="8" font-weight="600" text-anchor="middle">turn</text>
<line x1="90" y1="110" x2="104" y2="110" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)"/>
<rect x="107" y="90" width="70" height="40" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
<text x="142" y="114" fill="#1e3a5f" font-size="10" font-weight="600" text-anchor="middle">messages</text>
<line x1="177" y1="110" x2="191" y2="110" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)"/>
<rect x="194" y="86" width="80" height="48" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
<text x="234" y="114" fill="#1e3a5f" font-size="9" font-weight="600" text-anchor="middle">prompt</text>
<line x1="274" y1="110" x2="288" y2="110" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)"/>
<rect x="291" y="86" width="70" height="48" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
<text x="326" y="114" fill="#1e3a5f" font-size="9" font-weight="600" text-anchor="middle">LLM</text>
<line x1="361" y1="110" x2="375" y2="110" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)"/>
<rect x="378" y="80" width="356" height="60" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
<text x="556" y="98" fill="#1e3a5f" font-size="10" font-weight="600" text-anchor="middle">TOOL DISPATCHコアツールセット</text>
<text x="394" y="114" fill="#2563eb" font-size="8">base(5) · task(5) · team(7)</text>
<text x="394" y="128" fill="#7c3aed" font-size="8" font-weight="700">★ request_shutdown · request_plan · review_plan</text>
<path d="M 734 110 L 748 110 L 748 150 L 55 150 L 55 130" fill="none" stroke="#94a3b8" stroke-width="1" marker-end="url(#arrow)" stroke-dasharray="5,4"/>
<!-- Row 2: shutdown protocol -->
<rect x="30" y="176" width="700" height="90" rx="8" fill="#fef3c7" stroke="#d97706" stroke-width="1.5"/>
<text x="380" y="196" fill="#92400e" font-size="11" font-weight="700" text-anchor="middle">プロトコル Ashutdown フローLead が開始 → チームメイトが応答)</text>
<rect x="50" y="208" width="180" height="46" rx="6" fill="#fff" stroke="#d97706" stroke-width="1"/>
<text x="140" y="226" fill="#92400e" font-size="9" font-weight="600" text-anchor="middle">① Lead: request_shutdown</text>
<text x="140" y="242" fill="#6b7280" font-size="7" text-anchor="middle">new_request_id() → ProtocolState</text>
<line x1="230" y1="231" x2="254" y2="231" stroke="#d97706" stroke-width="1.5" marker-end="url(#arrow)"/>
<rect x="257" y="208" width="180" height="46" rx="6" fill="#fff" stroke="#d97706" stroke-width="1"/>
<text x="347" y="226" fill="#92400e" font-size="9" font-weight="600" text-anchor="middle">② チームメイト: handle_shutdown</text>
<text x="347" y="242" fill="#6b7280" font-size="7" text-anchor="middle">ack → shutdown_response</text>
<line x1="437" y1="231" x2="461" y2="231" stroke="#d97706" stroke-width="1.5" marker-end="url(#arrow)"/>
<rect x="464" y="208" width="180" height="46" rx="6" fill="#fff" stroke="#d97706" stroke-width="1"/>
<text x="554" y="226" fill="#92400e" font-size="9" font-weight="600" text-anchor="middle">③ Lead: consume_lead_inbox</text>
<text x="554" y="242" fill="#6b7280" font-size="7" text-anchor="middle">match_response(request_id) → ✓</text>
<!-- Row 3: plan approval protocol -->
<rect x="30" y="280" width="700" height="110" rx="8" fill="#dbeafe" stroke="#2563eb" stroke-width="1.5"/>
<text x="380" y="300" fill="#1e40af" font-size="11" font-weight="700" text-anchor="middle">プロトコル Bplan approval フロー(チームメイトが開始 → Lead が審査)</text>
<rect x="50" y="312" width="150" height="46" rx="6" fill="#f1f5f9" stroke="#94a3b8" stroke-width="1"/>
<text x="125" y="330" fill="#475569" font-size="8" font-weight="600" text-anchor="middle">0. Lead: request_plan</text>
<text x="125" y="346" fill="#6b7280" font-size="7" text-anchor="middle">plan_request メッセージ</text>
<line x1="200" y1="335" x2="222" y2="335" stroke="#94a3b8" stroke-width="1" marker-end="url(#arrow)" stroke-dasharray="4,3"/>
<rect x="225" y="312" width="160" height="46" rx="6" fill="#fff" stroke="#2563eb" stroke-width="1"/>
<text x="305" y="330" fill="#1e40af" font-size="8" font-weight="600" text-anchor="middle">① チームメイト: submit_plan</text>
<text x="305" y="346" fill="#6b7280" font-size="7" text-anchor="middle">plan_approval_request + request_id</text>
<line x1="385" y1="335" x2="407" y2="335" stroke="#2563eb" stroke-width="1.5" marker-end="url(#arrow)"/>
<rect x="410" y="312" width="160" height="46" rx="6" fill="#fff" stroke="#2563eb" stroke-width="1"/>
<text x="490" y="330" fill="#1e40af" font-size="8" font-weight="600" text-anchor="middle">② Lead: review_plan</text>
<text x="490" y="346" fill="#6b7280" font-size="7" text-anchor="middle">plan_approval_response</text>
<line x1="570" y1="335" x2="592" y2="335" stroke="#2563eb" stroke-width="1.5" marker-end="url(#arrow)"/>
<rect x="595" y="312" width="120" height="46" rx="6" fill="#fff" stroke="#2563eb" stroke-width="1"/>
<text x="655" y="330" fill="#1e40af" font-size="8" font-weight="600" text-anchor="middle">③ チームメイト: 受信</text>
<text x="655" y="346" fill="#6b7280" font-size="7" text-anchor="middle">[Plan approved/rejected]</text>
<text x="380" y="380" fill="#6b7280" font-size="8" text-anchor="middle">request_plan が要求を送り、submit_plan がレビュー可能な ProtocolState を作る</text>
<!-- Row 4: State Machine + Storage -->
<rect x="30" y="406" width="340" height="85" rx="8" fill="#f5f3ff" stroke="#7c3aed" stroke-width="1.5"/>
<text x="200" y="426" fill="#4c1d95" font-size="10" font-weight="700" text-anchor="middle">状態機械(両プロトコルで共用)</text>
<rect x="55" y="440" width="58" height="20" rx="4" fill="#f1f5f9" stroke="#94a3b8" stroke-width="1"/>
<text x="84" y="454" fill="#475569" font-size="9" text-anchor="middle">pending</text>
<line x1="113" y1="450" x2="192" y2="450" stroke="#16a34a" stroke-width="1.5" marker-end="url(#arrow-green)"/>
<text x="152" y="445" fill="#16a34a" font-size="8" font-weight="600" text-anchor="middle">approve</text>
<rect x="192" y="440" width="62" height="20" rx="4" fill="#dcfce7" stroke="#16a34a" stroke-width="1"/>
<text x="223" y="454" fill="#166534" font-size="9" text-anchor="middle">approved</text>
<path d="M 84 460 L 84 478 L 128 478" fill="none" stroke="#dc2626" stroke-width="1.5" marker-end="url(#arrow-red)"/>
<text x="76" y="472" fill="#dc2626" font-size="8" font-weight="600" text-anchor="end">reject</text>
<rect x="128" y="468" width="60" height="20" rx="4" fill="#fef2f2" stroke="#dc2626" stroke-width="1"/>
<text x="158" y="482" fill="#991b1b" font-size="9" text-anchor="middle">rejected</text>
<rect x="400" y="406" width="330" height="85" rx="8" fill="#f5f3ff" stroke="#7c3aed" stroke-width="1.5"/>
<text x="565" y="426" fill="#4c1d95" font-size="10" font-weight="700" text-anchor="middle">pending_requests ストレージ</text>
<text x="420" y="446" fill="#7c3aed" font-size="9">pending_requests: dict[str, ProtocolState]</text>
<text x="420" y="462" fill="#6b7280" font-size="8">request_id → {type, sender, status, created_at}</text>
<text x="420" y="478" fill="#6b7280" font-size="8">match_response: request_id でリクエストを検索</text>
<!-- Row 5: Two protocols same machine -->
<rect x="30" y="506" width="700" height="52" rx="6" fill="#f8fafc" stroke="#e2e8f0" stroke-width="1"/>
<text x="55" y="524" fill="#1e3a5f" font-size="10" font-weight="600">2つのプロトコル、1つのメカニズム</text>
<rect x="270" y="512" width="130" height="18" rx="4" fill="#fef3c7" stroke="#d97706" stroke-width="1"/>
<text x="335" y="525" fill="#92400e" font-size="9" text-anchor="middle">shutdown_request</text>
<text x="408" y="525" fill="#475569" font-size="10"></text>
<rect x="425" y="512" width="140" height="18" rx="4" fill="#dbeafe" stroke="#2563eb" stroke-width="1"/>
<text x="495" y="525" fill="#1e40af" font-size="9" text-anchor="middle">plan_approval_request</text>
<text x="55" y="548" fill="#6b7280" font-size="8">同じ pending → approved / rejected 状態機械を共有。新しいプロトコルタイプ = 新しい msg_type、新しい状態機械は不要。request_id がリクエストとレスポンスを関連付ける。</text>
<!-- Row 6: distinguish -->
<rect x="30" y="570" width="700" height="38" rx="6" fill="#f0fdf4" stroke="#16a34a" stroke-width="1"/>
<text x="55" y="588" fill="#166534" font-size="9" font-weight="600">区別:</text>
<text x="100" y="588" fill="#475569" font-size="9">request_plan は plan_request を送り、submit_plan は request_id を作ってレビューを待つ。</text>
<text x="55" y="602" fill="#475569" font-size="9">submit_plan がプロトコルエントリポイントmsg_type="plan_approval_request"で、チームメイトが自発的に開始し、request_id を pending_requests に書き込む。</text>
<!-- Bottom notes -->
<rect x="30" y="622" width="700" height="28" rx="6" fill="#f8fafc" stroke="#e2e8f0" stroke-width="1"/>
<rect x="50" y="632" width="12" height="10" rx="2" fill="#f0f4ff" stroke="#2563eb" stroke-width="1"/>
<text x="70" y="642" fill="#475569" font-size="10">Runtime: MessageBus + 永続チームメイト + 自動イベント配信</text>
<rect x="310" y="632" width="12" height="10" rx="2" fill="#f5f3ff" stroke="#7c3aed" stroke-width="1"/>
<text x="330" y="642" fill="#475569" font-size="10">Protocol: request_id + dispatch + pending_requests + プランゲート</text>
</svg>

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -0,0 +1,141 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 660" font-family="system-ui, -apple-system, sans-serif">
<defs>
<linearGradient id="header" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="#1e3a5f"/><stop offset="100%" stop-color="#7c3aed"/>
</linearGradient>
<marker id="arrow" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#555"/>
</marker>
<marker id="arrow-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="6" markerHeight="6" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#16a34a"/>
</marker>
<marker id="arrow-red" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#dc2626"/>
</marker>
</defs>
<rect width="760" height="660" fill="#fafbfc" rx="8"/>
<rect x="0" y="0" width="760" height="44" fill="url(#header)" rx="8"/>
<rect x="0" y="36" width="760" height="8" fill="url(#header)"/>
<text x="380" y="28" fill="#fff" font-size="15" font-weight="700" text-anchor="middle">Team Protocols — 请求-响应协议 + request_id 关联 + 状态机</text>
<rect x="40" y="56" width="12" height="10" rx="2" fill="#f0f4ff" stroke="#2563eb" stroke-width="1"/>
<text x="58" y="66" fill="#2563eb" font-size="10" font-weight="600">团队运行时</text>
<rect x="140" y="56" width="12" height="10" rx="2" fill="#f5f3ff" stroke="#7c3aed" stroke-width="1"/>
<text x="158" y="66" fill="#7c3aed" font-size="10" font-weight="600">协作协议</text>
<!-- Row 1: Lead Loop -->
<rect x="20" y="90" width="70" height="40" rx="8" fill="#eef2ff" stroke="#4f46e5" stroke-width="1.5"/>
<text x="55" y="114" fill="#4f46e5" font-size="8" font-weight="600" text-anchor="middle">turn</text>
<line x1="90" y1="110" x2="104" y2="110" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)"/>
<rect x="107" y="90" width="70" height="40" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
<text x="142" y="114" fill="#1e3a5f" font-size="10" font-weight="600" text-anchor="middle">messages</text>
<line x1="177" y1="110" x2="191" y2="110" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)"/>
<rect x="194" y="86" width="80" height="48" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
<text x="234" y="114" fill="#1e3a5f" font-size="9" font-weight="600" text-anchor="middle">prompt</text>
<line x1="274" y1="110" x2="288" y2="110" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)"/>
<rect x="291" y="86" width="70" height="48" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
<text x="326" y="114" fill="#1e3a5f" font-size="9" font-weight="600" text-anchor="middle">LLM</text>
<line x1="361" y1="110" x2="375" y2="110" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)"/>
<rect x="378" y="80" width="356" height="60" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
<text x="556" y="98" fill="#1e3a5f" font-size="10" font-weight="600" text-anchor="middle">TOOL DISPATCH核心工具集</text>
<text x="394" y="114" fill="#2563eb" font-size="8">基础工具(5) · task(5) · team(7)</text>
<text x="394" y="128" fill="#7c3aed" font-size="8" font-weight="700">★ request_shutdown · request_plan · review_plan</text>
<path d="M 734 110 L 748 110 L 748 150 L 55 150 L 55 130" fill="none" stroke="#94a3b8" stroke-width="1" marker-end="url(#arrow)" stroke-dasharray="5,4"/>
<!-- Row 2: shutdown protocol -->
<rect x="30" y="176" width="700" height="90" rx="8" fill="#fef3c7" stroke="#d97706" stroke-width="1.5"/>
<text x="380" y="196" fill="#92400e" font-size="11" font-weight="700" text-anchor="middle">协议 Ashutdown 流程Lead 发起 → 队友响应)</text>
<rect x="50" y="208" width="180" height="46" rx="6" fill="#fff" stroke="#d97706" stroke-width="1"/>
<text x="140" y="226" fill="#92400e" font-size="9" font-weight="600" text-anchor="middle">① Lead: request_shutdown</text>
<text x="140" y="242" fill="#6b7280" font-size="7" text-anchor="middle">new_request_id() → ProtocolState</text>
<line x1="230" y1="231" x2="254" y2="231" stroke="#d97706" stroke-width="1.5" marker-end="url(#arrow)"/>
<rect x="257" y="208" width="180" height="46" rx="6" fill="#fff" stroke="#d97706" stroke-width="1"/>
<text x="347" y="226" fill="#92400e" font-size="9" font-weight="600" text-anchor="middle">② 队友: handle_shutdown</text>
<text x="347" y="242" fill="#6b7280" font-size="7" text-anchor="middle">ack → shutdown_response</text>
<line x1="437" y1="231" x2="461" y2="231" stroke="#d97706" stroke-width="1.5" marker-end="url(#arrow)"/>
<rect x="464" y="208" width="180" height="46" rx="6" fill="#fff" stroke="#d97706" stroke-width="1"/>
<text x="554" y="226" fill="#92400e" font-size="9" font-weight="600" text-anchor="middle">③ Lead: consume_lead_inbox</text>
<text x="554" y="242" fill="#6b7280" font-size="7" text-anchor="middle">match_response(request_id) → ✓</text>
<!-- Row 3: plan approval protocol -->
<rect x="30" y="280" width="700" height="110" rx="8" fill="#dbeafe" stroke="#2563eb" stroke-width="1.5"/>
<text x="380" y="300" fill="#1e40af" font-size="11" font-weight="700" text-anchor="middle">协议 Bplan approval 流程(队友发起 → Lead 审批)</text>
<rect x="50" y="312" width="150" height="46" rx="6" fill="#f1f5f9" stroke="#94a3b8" stroke-width="1"/>
<text x="125" y="330" fill="#475569" font-size="8" font-weight="600" text-anchor="middle">0. Lead: request_plan</text>
<text x="125" y="346" fill="#6b7280" font-size="7" text-anchor="middle">plan_request 消息</text>
<line x1="200" y1="335" x2="222" y2="335" stroke="#94a3b8" stroke-width="1" marker-end="url(#arrow)" stroke-dasharray="4,3"/>
<rect x="225" y="312" width="160" height="46" rx="6" fill="#fff" stroke="#2563eb" stroke-width="1"/>
<text x="305" y="330" fill="#1e40af" font-size="8" font-weight="600" text-anchor="middle">① 队友: submit_plan</text>
<text x="305" y="346" fill="#6b7280" font-size="7" text-anchor="middle">plan_approval_request + request_id</text>
<line x1="385" y1="335" x2="407" y2="335" stroke="#2563eb" stroke-width="1.5" marker-end="url(#arrow)"/>
<rect x="410" y="312" width="160" height="46" rx="6" fill="#fff" stroke="#2563eb" stroke-width="1"/>
<text x="490" y="330" fill="#1e40af" font-size="8" font-weight="600" text-anchor="middle">② Lead: review_plan</text>
<text x="490" y="346" fill="#6b7280" font-size="7" text-anchor="middle">plan_approval_response</text>
<line x1="570" y1="335" x2="592" y2="335" stroke="#2563eb" stroke-width="1.5" marker-end="url(#arrow)"/>
<rect x="595" y="312" width="120" height="46" rx="6" fill="#fff" stroke="#2563eb" stroke-width="1"/>
<text x="655" y="330" fill="#1e40af" font-size="8" font-weight="600" text-anchor="middle">③ 队友: 收到结果</text>
<text x="655" y="346" fill="#6b7280" font-size="7" text-anchor="middle">[Plan approved/rejected]</text>
<text x="380" y="380" fill="#6b7280" font-size="8" text-anchor="middle">request_plan 发出要求submit_plan 创建可审批的 ProtocolState</text>
<!-- Row 4: State Machine + Storage -->
<rect x="30" y="406" width="340" height="85" rx="8" fill="#f5f3ff" stroke="#7c3aed" stroke-width="1.5"/>
<text x="200" y="426" fill="#4c1d95" font-size="10" font-weight="700" text-anchor="middle">状态机(同一套,两种协议共用)</text>
<rect x="55" y="440" width="58" height="20" rx="4" fill="#f1f5f9" stroke="#94a3b8" stroke-width="1"/>
<text x="84" y="454" fill="#475569" font-size="9" text-anchor="middle">pending</text>
<line x1="113" y1="450" x2="192" y2="450" stroke="#16a34a" stroke-width="1.5" marker-end="url(#arrow-green)"/>
<text x="152" y="445" fill="#16a34a" font-size="8" font-weight="600" text-anchor="middle">approve</text>
<rect x="192" y="440" width="62" height="20" rx="4" fill="#dcfce7" stroke="#16a34a" stroke-width="1"/>
<text x="223" y="454" fill="#166534" font-size="9" text-anchor="middle">approved</text>
<path d="M 84 460 L 84 478 L 128 478" fill="none" stroke="#dc2626" stroke-width="1.5" marker-end="url(#arrow-red)"/>
<text x="76" y="472" fill="#dc2626" font-size="8" font-weight="600" text-anchor="end">reject</text>
<rect x="128" y="468" width="60" height="20" rx="4" fill="#fef2f2" stroke="#dc2626" stroke-width="1"/>
<text x="158" y="482" fill="#991b1b" font-size="9" text-anchor="middle">rejected</text>
<rect x="400" y="406" width="330" height="85" rx="8" fill="#f5f3ff" stroke="#7c3aed" stroke-width="1.5"/>
<text x="565" y="426" fill="#4c1d95" font-size="10" font-weight="700" text-anchor="middle">pending_requests 存储</text>
<text x="420" y="446" fill="#7c3aed" font-size="9">pending_requests: dict[str, ProtocolState]</text>
<text x="420" y="462" fill="#6b7280" font-size="8">request_id → {type, sender, status, created_at}</text>
<text x="420" y="478" fill="#6b7280" font-size="8">match_response: 按 request_id 找回对应请求</text>
<!-- Row 5: Two protocols same machine -->
<rect x="30" y="506" width="700" height="52" rx="6" fill="#f8fafc" stroke="#e2e8f0" stroke-width="1"/>
<text x="55" y="524" fill="#1e3a5f" font-size="10" font-weight="600">两种协议,同一套机制:</text>
<rect x="230" y="512" width="130" height="18" rx="4" fill="#fef3c7" stroke="#d97706" stroke-width="1"/>
<text x="295" y="525" fill="#92400e" font-size="9" text-anchor="middle">shutdown_request</text>
<text x="368" y="525" fill="#475569" font-size="10"></text>
<rect x="390" y="512" width="140" height="18" rx="4" fill="#dbeafe" stroke="#2563eb" stroke-width="1"/>
<text x="460" y="525" fill="#1e40af" font-size="9" text-anchor="middle">plan_approval_request</text>
<text x="55" y="548" fill="#6b7280" font-size="8">共用 pending → approved / rejected 状态机。新增协议类型 = 新的 msg_type不需要新状态机。request_id 关联请求和响应。</text>
<!-- Row 6: distinguish -->
<rect x="30" y="570" width="700" height="38" rx="6" fill="#f0fdf4" stroke="#16a34a" stroke-width="1"/>
<text x="55" y="588" fill="#166534" font-size="9" font-weight="600">区分:</text>
<text x="100" y="588" fill="#475569" font-size="9">request_plan 发送 plan_requestsubmit_plan 生成 request_id 并等待审批。</text>
<text x="55" y="602" fill="#475569" font-size="9">submit_plan 才是协议入口msg_type="plan_approval_request"),由队友主动发起,携带 request_id 写入 pending_requests。</text>
<!-- Bottom notes -->
<rect x="30" y="622" width="700" height="28" rx="6" fill="#f8fafc" stroke="#e2e8f0" stroke-width="1"/>
<rect x="50" y="632" width="12" height="10" rx="2" fill="#f0f4ff" stroke="#2563eb" stroke-width="1"/>
<text x="70" y="642" fill="#475569" font-size="10">运行时: MessageBus + 持久队友 + 自动事件投递</text>
<rect x="310" y="632" width="12" height="10" rx="2" fill="#f5f3ff" stroke="#7c3aed" stroke-width="1"/>
<text x="330" y="642" fill="#475569" font-size="10">协议: request_id + dispatch + pending_requests + 计划闸门</text>
</svg>

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -0,0 +1,65 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 720 296" font-family="system-ui, -apple-system, sans-serif">
<defs>
<linearGradient id="header" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="#1e3a5f"/><stop offset="100%" stop-color="#0891b2"/>
</linearGradient>
<marker id="arrow" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#555"/>
</marker>
<marker id="arrow-cyan" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#0891b2"/>
</marker>
<marker id="arrow-amber" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#d97706"/>
</marker>
</defs>
<rect width="720" height="296" fill="#fafbfc" rx="8"/>
<rect x="0" y="0" width="720" height="38" fill="url(#header)" rx="8"/>
<rect x="0" y="30" width="720" height="8" fill="url(#header)"/>
<text x="360" y="25" fill="#fff" font-size="14" font-weight="700" text-anchor="middle">Team Topology — Lead ↔ MessageBus ↔ Teammates</text>
<!-- Lead -->
<rect x="260" y="58" width="200" height="68" rx="8" fill="#ecfeff" stroke="#0891b2" stroke-width="2"/>
<text x="360" y="82" fill="#0e7490" font-size="13" font-weight="700" text-anchor="middle">Lead Agent</text>
<text x="360" y="100" fill="#0e7490" font-size="10" text-anchor="middle">Main loop + spawn + inbox handling</text>
<text x="360" y="116" fill="#0e7490" font-size="10" text-anchor="middle">runtime delivers team events automatically</text>
<!-- Message Bus -->
<rect x="80" y="150" width="560" height="26" rx="13" fill="#fef3c7" stroke="#d97706" stroke-width="1.5"/>
<text x="360" y="168" fill="#92400e" font-size="11" font-weight="700" text-anchor="middle">Message Bus (.mailboxes/*.jsonl)</text>
<!-- Teammates -->
<rect x="40" y="214" width="180" height="60" rx="8" fill="#f0fdf4" stroke="#16a34a" stroke-width="1.5"/>
<text x="130" y="238" fill="#166534" font-size="12" font-weight="700" text-anchor="middle">Alice (Backend)</text>
<text x="130" y="254" fill="#166534" font-size="9" text-anchor="middle">own loop → inbox → work → reply</text>
<rect x="270" y="214" width="180" height="60" rx="8" fill="#f0fdf4" stroke="#16a34a" stroke-width="1.5"/>
<text x="360" y="238" fill="#166534" font-size="12" font-weight="700" text-anchor="middle">Bob (Frontend)</text>
<text x="360" y="254" fill="#166534" font-size="9" text-anchor="middle">own loop → inbox → work → reply</text>
<rect x="500" y="214" width="180" height="60" rx="8" fill="#f0fdf4" stroke="#16a34a" stroke-width="1.5"/>
<text x="590" y="238" fill="#166534" font-size="12" font-weight="700" text-anchor="middle">Charlie (QA)</text>
<text x="590" y="254" fill="#166534" font-size="9" text-anchor="middle">own loop → inbox → work → reply</text>
<!-- Lead ↔ Bus -->
<line x1="330" y1="126" x2="330" y2="150" stroke="#0891b2" stroke-width="1.5" marker-end="url(#arrow-cyan)"/>
<text x="300" y="142" fill="#0891b2" font-size="7">send</text>
<line x1="390" y1="150" x2="390" y2="126" stroke="#0891b2" stroke-width="1.5" marker-end="url(#arrow-cyan)"/>
<text x="398" y="142" fill="#0891b2" font-size="7">inbox</text>
<!-- Bus → Teammates -->
<line x1="120" y1="176" x2="120" y2="214" stroke="#555" stroke-width="1" marker-end="url(#arrow)"/>
<line x1="350" y1="176" x2="350" y2="214" stroke="#555" stroke-width="1" marker-end="url(#arrow)"/>
<line x1="570" y1="176" x2="570" y2="214" stroke="#555" stroke-width="1" marker-end="url(#arrow)"/>
<text x="82" y="195" fill="#16a34a" font-size="9" font-weight="600">receive</text>
<text x="312" y="195" fill="#16a34a" font-size="9" font-weight="600">receive</text>
<text x="532" y="195" fill="#16a34a" font-size="9" font-weight="600">receive</text>
<line x1="140" y1="214" x2="140" y2="176" stroke="#0891b2" stroke-width="1" stroke-dasharray="4,2" marker-end="url(#arrow-cyan)"/>
<line x1="370" y1="214" x2="370" y2="176" stroke="#0891b2" stroke-width="1" stroke-dasharray="4,2" marker-end="url(#arrow-cyan)"/>
<line x1="590" y1="214" x2="590" y2="176" stroke="#0891b2" stroke-width="1" stroke-dasharray="4,2" marker-end="url(#arrow-cyan)"/>
<text x="145" y="203" fill="#0891b2" font-size="9" font-weight="600">send</text>
<text x="375" y="203" fill="#0891b2" font-size="9" font-weight="600">send</text>
<text x="595" y="203" fill="#0891b2" font-size="9" font-weight="600">send</text>
</svg>

After

Width:  |  Height:  |  Size: 4.5 KiB

View File

@@ -0,0 +1,65 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 720 296" font-family="system-ui, -apple-system, sans-serif">
<defs>
<linearGradient id="header" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="#1e3a5f"/><stop offset="100%" stop-color="#0891b2"/>
</linearGradient>
<marker id="arrow" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#555"/>
</marker>
<marker id="arrow-cyan" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#0891b2"/>
</marker>
<marker id="arrow-amber" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#d97706"/>
</marker>
</defs>
<rect width="720" height="296" fill="#fafbfc" rx="8"/>
<rect x="0" y="0" width="720" height="38" fill="url(#header)" rx="8"/>
<rect x="0" y="30" width="720" height="8" fill="url(#header)"/>
<text x="360" y="25" fill="#fff" font-size="14" font-weight="700" text-anchor="middle">Team Topology — Lead ↔ MessageBus ↔ チームメイト</text>
<!-- Lead -->
<rect x="260" y="58" width="200" height="68" rx="8" fill="#ecfeff" stroke="#0891b2" stroke-width="2"/>
<text x="360" y="82" fill="#0e7490" font-size="13" font-weight="700" text-anchor="middle">Lead Agent</text>
<text x="360" y="100" fill="#0e7490" font-size="10" text-anchor="middle">メインループ + spawn + inbox 処理</text>
<text x="360" y="116" fill="#0e7490" font-size="10" text-anchor="middle">ランタイムがチームイベントを自動配信</text>
<!-- Message Bus -->
<rect x="80" y="150" width="560" height="26" rx="13" fill="#fef3c7" stroke="#d97706" stroke-width="1.5"/>
<text x="360" y="168" fill="#92400e" font-size="11" font-weight="700" text-anchor="middle">Message Bus (.mailboxes/*.jsonl)</text>
<!-- チームメイト -->
<rect x="40" y="214" width="180" height="60" rx="8" fill="#f0fdf4" stroke="#16a34a" stroke-width="1.5"/>
<text x="130" y="238" fill="#166534" font-size="12" font-weight="700" text-anchor="middle">Alice (Backend)</text>
<text x="130" y="254" fill="#166534" font-size="9" text-anchor="middle">独立 loop → inbox → 作業 → 返信</text>
<rect x="270" y="214" width="180" height="60" rx="8" fill="#f0fdf4" stroke="#16a34a" stroke-width="1.5"/>
<text x="360" y="238" fill="#166534" font-size="12" font-weight="700" text-anchor="middle">Bob (Frontend)</text>
<text x="360" y="254" fill="#166534" font-size="9" text-anchor="middle">独立 loop → inbox → 作業 → 返信</text>
<rect x="500" y="214" width="180" height="60" rx="8" fill="#f0fdf4" stroke="#16a34a" stroke-width="1.5"/>
<text x="590" y="238" fill="#166534" font-size="12" font-weight="700" text-anchor="middle">Charlie (QA)</text>
<text x="590" y="254" fill="#166534" font-size="9" text-anchor="middle">独立 loop → inbox → 作業 → 返信</text>
<!-- Lead ↔ Bus -->
<line x1="330" y1="126" x2="330" y2="150" stroke="#0891b2" stroke-width="1.5" marker-end="url(#arrow-cyan)"/>
<text x="300" y="142" fill="#0891b2" font-size="7">send</text>
<line x1="390" y1="150" x2="390" y2="126" stroke="#0891b2" stroke-width="1.5" marker-end="url(#arrow-cyan)"/>
<text x="398" y="142" fill="#0891b2" font-size="7">inbox</text>
<!-- Bus → チームメイト -->
<line x1="120" y1="176" x2="120" y2="214" stroke="#555" stroke-width="1" marker-end="url(#arrow)"/>
<line x1="350" y1="176" x2="350" y2="214" stroke="#555" stroke-width="1" marker-end="url(#arrow)"/>
<line x1="570" y1="176" x2="570" y2="214" stroke="#555" stroke-width="1" marker-end="url(#arrow)"/>
<text x="82" y="195" fill="#16a34a" font-size="9" font-weight="600">receive</text>
<text x="312" y="195" fill="#16a34a" font-size="9" font-weight="600">receive</text>
<text x="532" y="195" fill="#16a34a" font-size="9" font-weight="600">receive</text>
<line x1="140" y1="214" x2="140" y2="176" stroke="#0891b2" stroke-width="1" stroke-dasharray="4,2" marker-end="url(#arrow-cyan)"/>
<line x1="370" y1="214" x2="370" y2="176" stroke="#0891b2" stroke-width="1" stroke-dasharray="4,2" marker-end="url(#arrow-cyan)"/>
<line x1="590" y1="214" x2="590" y2="176" stroke="#0891b2" stroke-width="1" stroke-dasharray="4,2" marker-end="url(#arrow-cyan)"/>
<text x="145" y="203" fill="#0891b2" font-size="9" font-weight="600">send</text>
<text x="375" y="203" fill="#0891b2" font-size="9" font-weight="600">send</text>
<text x="595" y="203" fill="#0891b2" font-size="9" font-weight="600">send</text>
</svg>

After

Width:  |  Height:  |  Size: 4.6 KiB

View File

@@ -0,0 +1,72 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 720 296" font-family="system-ui, -apple-system, sans-serif">
<defs>
<linearGradient id="header" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="#1e3a5f"/><stop offset="100%" stop-color="#0891b2"/>
</linearGradient>
<marker id="arrow" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#555"/>
</marker>
<marker id="arrow-cyan" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#0891b2"/>
</marker>
<marker id="arrow-amber" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#d97706"/>
</marker>
<marker id="arrow-amber-left" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
<path d="M 10 0 L 0 5 L 10 10 z" fill="#d97706"/>
</marker>
</defs>
<rect width="720" height="296" fill="#fafbfc" rx="8"/>
<rect x="0" y="0" width="720" height="38" fill="url(#header)" rx="8"/>
<rect x="0" y="30" width="720" height="8" fill="url(#header)"/>
<text x="360" y="25" fill="#fff" font-size="14" font-weight="700" text-anchor="middle">Team Topology — Lead ↔ MessageBus ↔ Teammates</text>
<!-- Lead: x=260..460, y=58..126 -->
<rect x="260" y="58" width="200" height="68" rx="8" fill="#ecfeff" stroke="#0891b2" stroke-width="2"/>
<text x="360" y="82" fill="#0e7490" font-size="13" font-weight="700" text-anchor="middle">Lead Agent</text>
<text x="360" y="100" fill="#0e7490" font-size="10" text-anchor="middle">主循环 + spawn + inbox 处理</text>
<text x="360" y="116" fill="#0e7490" font-size="10" text-anchor="middle">运行时自动投递团队事件</text>
<!-- Message Bus: x=80..640, y=150..176 (26px tall) -->
<rect x="80" y="150" width="560" height="26" rx="13" fill="#fef3c7" stroke="#d97706" stroke-width="1.5"/>
<text x="360" y="168" fill="#92400e" font-size="11" font-weight="700" text-anchor="middle">Message Bus (.mailboxes/*.jsonl)</text>
<!-- Teammates: y=214..274 (60px tall) -->
<rect x="40" y="214" width="180" height="60" rx="8" fill="#f0fdf4" stroke="#16a34a" stroke-width="1.5"/>
<text x="130" y="238" fill="#166534" font-size="12" font-weight="700" text-anchor="middle">Alice (Backend)</text>
<text x="130" y="254" fill="#166534" font-size="9" text-anchor="middle">独立 loop → inbox → 干活 → 回复</text>
<rect x="270" y="214" width="180" height="60" rx="8" fill="#f0fdf4" stroke="#16a34a" stroke-width="1.5"/>
<text x="360" y="238" fill="#166534" font-size="12" font-weight="700" text-anchor="middle">Bob (Frontend)</text>
<text x="360" y="254" fill="#166534" font-size="9" text-anchor="middle">独立 loop → inbox → 干活 → 回复</text>
<rect x="500" y="214" width="180" height="60" rx="8" fill="#f0fdf4" stroke="#16a34a" stroke-width="1.5"/>
<text x="590" y="238" fill="#166534" font-size="12" font-weight="700" text-anchor="middle">Charlie (QA)</text>
<text x="590" y="254" fill="#166534" font-size="9" text-anchor="middle">独立 loop → inbox → 干活 → 回复</text>
<!-- ===== Lead ↔ Bus (gap: 126→150 = 24px) ===== -->
<!-- Lead → Bus (send/spawn): x=330 -->
<line x1="330" y1="126" x2="330" y2="150" stroke="#0891b2" stroke-width="1.5" marker-end="url(#arrow-cyan)"/>
<text x="300" y="142" fill="#0891b2" font-size="7">send</text>
<!-- Bus → Lead (inbox delivery): x=390 -->
<line x1="390" y1="150" x2="390" y2="126" stroke="#0891b2" stroke-width="1.5" marker-end="url(#arrow-cyan)"/>
<text x="398" y="142" fill="#0891b2" font-size="7">inbox</text>
<!-- ===== Bus → Teammates (gap: 176→214 = 38px) ===== -->
<!-- Incoming (solid): from Bus bottom to Teammate top -->
<line x1="120" y1="176" x2="120" y2="214" stroke="#555" stroke-width="1" marker-end="url(#arrow)"/>
<line x1="350" y1="176" x2="350" y2="214" stroke="#555" stroke-width="1" marker-end="url(#arrow)"/>
<line x1="570" y1="176" x2="570" y2="214" stroke="#555" stroke-width="1" marker-end="url(#arrow)"/>
<text x="82" y="195" fill="#16a34a" font-size="9" font-weight="600">receive</text>
<text x="312" y="195" fill="#16a34a" font-size="9" font-weight="600">receive</text>
<text x="532" y="195" fill="#16a34a" font-size="9" font-weight="600">receive</text>
<!-- Outgoing / send_message back (dashed, offset 20px right) -->
<line x1="140" y1="214" x2="140" y2="176" stroke="#0891b2" stroke-width="1" stroke-dasharray="4,2" marker-end="url(#arrow-cyan)"/>
<line x1="370" y1="214" x2="370" y2="176" stroke="#0891b2" stroke-width="1" stroke-dasharray="4,2" marker-end="url(#arrow-cyan)"/>
<line x1="590" y1="214" x2="590" y2="176" stroke="#0891b2" stroke-width="1" stroke-dasharray="4,2" marker-end="url(#arrow-cyan)"/>
<text x="145" y="203" fill="#0891b2" font-size="9" font-weight="600">send</text>
<text x="375" y="203" fill="#0891b2" font-size="9" font-weight="600">send</text>
<text x="595" y="203" fill="#0891b2" font-size="9" font-weight="600">send</text>
</svg>

After

Width:  |  Height:  |  Size: 5.1 KiB