mirror of
https://github.com/shareAI-lab/analysis_claude_code.git
synced 2026-09-21 21:03:38 +08:00
Consolidate agent harness course into 19 lessons
This commit is contained in:
@@ -2,32 +2,34 @@
|
||||
|
||||
[English](README.md) · [中文](README.zh.md) · [日本語](README.ja.md)
|
||||
|
||||
s01 → ... → s13 → s14 → `s15` → [s16](../s16_autonomous_agents/) → s17 → s18 → s19 → s20 → s21
|
||||
s01 → ... → s13 → s14 → `s15` → [s16](../s16_mcp_plugin/) → s17 → s18 → s19
|
||||
|
||||
> *「1 つの Agent だけでは扱いきれないなら、チームメイトで分担する。」* — 永続チームメイト、メッセージ配信、協調プロトコル。
|
||||
> *「1 つの Agent で仕事全体を抱えきれないなら、チームメイトで分担する。」* — 永続チームメイト、共有タスクの Claim、任意の worktree、協調プロトコル。
|
||||
>
|
||||
> **Harness レイヤー**:チーム — 複数 Agent を並行動作させながら制御を保つ。
|
||||
> **Harness レイヤー**:Team — 複数の Agent が Lead の管理下で仕事を分担し、状態を共有する仕組み。
|
||||
|
||||
---
|
||||
|
||||
## 問題
|
||||
|
||||
Agent にバックエンド全体のリファクタリングを頼む場合、設定読み込み、認証、テストを同時に扱うことになる。1 つの Agent が順番に処理することもできるが、時間がかかり、初期の詳細は徐々にコンテキストから抜けていく。
|
||||
Agent にバックエンド全体のリファクタリングを依頼するとする。作業範囲は設定の読み込み、認証、テストにまたがる。1 つの Agent でも順番に処理できるが、時間がかかり、初期の詳細は少しずつコンテキストから抜けていく。
|
||||
|
||||
このような仕事は並列化に向いている。しかし、通常のユーザーはチーム構成ではなく目的だけを伝える:
|
||||
この仕事は並列化に向いている。ただし、ユーザーは通常、チーム構成ではなく目標を伝える:
|
||||
|
||||
```text
|
||||
このサンプルバックエンドをリファクタリングしてください。
|
||||
設定読み込み、認証ロジック、テストを整理し、
|
||||
既存インターフェースを保ったままテストを通してください。
|
||||
設定の読み込み、認証、テストを整理し、既存インターフェースを保ち、
|
||||
テストが通ることを確認してください。
|
||||
```
|
||||
|
||||
そのため Harness は、単に Agent を増やすだけでなく、次の 4 点を解決する必要がある:
|
||||
Harness は、つながった 6 つの問題を扱う必要がある:
|
||||
|
||||
1. 並列化が有効かを誰が判断し、追加 Agent の起動を誰が確認するか。
|
||||
2. チームメイトが複数の依頼にまたがって、どう身元とコンテキストを保つか。
|
||||
3. モデルに受信箱を繰り返し確認させず、結果をどう Lead へ戻すか。
|
||||
4. 終了と計画承認を、どう追跡可能で強制可能なプロトコルにするか。
|
||||
1. 並列作業が有効だと誰が判断し、追加の Agent を誰が承認するのか。
|
||||
2. 各チームメイトは、複数の割り当てをまたいで識別子とコンテキストをどう保つのか。
|
||||
3. モデルに受信箱をポーリングさせず、結果を Lead へどう返すのか。
|
||||
4. IDLE のチームメイトは、次の指示を待たずに ready task を引き受けられるか。
|
||||
5. 並列編集が衝突し得る時、タスクはどの作業ディレクトリを使うのか。
|
||||
6. shutdown と計画承認を、追跡できて実際に制約をかけるプロトコルにするにはどうするか。
|
||||
|
||||
---
|
||||
|
||||
@@ -35,24 +37,25 @@ Agent にバックエンド全体のリファクタリングを頼む場合、
|
||||
|
||||

|
||||
|
||||
s15 は単一 Agent の Harness の外側に、Lead が管理するチームランタイムを追加する:
|
||||
s15 は、単一 Agent の Harness に Lead 管理のチームランタイムを加える:
|
||||
|
||||
- **Lead** はユーザーとの会話を維持し、分担案を提示して確認を待つ。
|
||||
- **チームメイト** は独立した Agent Loop をバックグラウンドスレッドで実行し、作業後は IDLE になる。
|
||||
- **MessageBus** はファイル受信箱を通して、通常メッセージ、結果、制御イベントを運ぶ。
|
||||
- **ランタイム配信** は Lead の受信箱を消費し、チームイベントを次のターンへ注入する。
|
||||
- **協調プロトコル** は `type`、`request_id`、状態遷移で終了と計画承認を扱う。
|
||||
- **計画ゲート** は、必要な計画が承認されるまで `bash` と `write_file` を遮断する。
|
||||
- **Lead** はユーザーとの会話を担当し、分担案を示して確認を待つ。
|
||||
- **チームメイト** は独立した Agent Loop を実行し、WORK と IDLE を行き来する。
|
||||
- **MessageBus** は、ファイルベースの受信箱で通常メッセージ、結果、制御イベントを運ぶ。
|
||||
- **ランタイム配信** は Lead の受信箱を消費し、チームイベントを次のターンへ追加する。
|
||||
- **共有タスクボード** により、IDLE のチームメイトは ready task を探し、ロック下で Claim できる。
|
||||
- **任意の worktree** は、必要なタスクだけを別の作業ディレクトリへ紐付ける。紐付けのないタスクは通常のリポジトリディレクトリを使う。
|
||||
- **型付きプロトコルと計画ゲート** は shutdown と承認状態を明示し、必要な計画が承認されるまで変更系ツールを止める。
|
||||
|
||||
モデルはタスクを理解して分担を決める。コードは配信、ライフサイクル、プロトコル制約を担う。
|
||||
これらはすべて Team Harness レイヤーの一部である。タスク発見のために別の Agent Loop は要らず、worktree が別種の Agent を作るわけでもない。
|
||||
|
||||
---
|
||||
|
||||
## 仕組み
|
||||
|
||||
### 1. Lead はチーム案を示し、確認を待つ
|
||||
### 1. Lead はチーム案を示し、ユーザーの確認を待つ
|
||||
|
||||
チームメイトの起動は、コスト、並行度、ワークスペースを書き換える主体を変える。この境界を通常のツール呼び出しの中に隠してはいけない。Lead の system prompt は次のように定める:
|
||||
チームメイトを起動すると、コスト、並行度、ワークスペースを編集できる主体が変わる。Lead のシステムプロンプトは、その境界を明示する:
|
||||
|
||||
```python
|
||||
"When parallel work would help, first propose a small team with clear "
|
||||
@@ -60,35 +63,35 @@ s15 は単一 Agent の Harness の外側に、Lead が管理するチームラ
|
||||
"spawn_teammate before the user confirms."
|
||||
```
|
||||
|
||||
最初の依頼に対して、Lead はまず分担案だけを返す:
|
||||
最初の要求に対して、Lead は分担案だけを示す:
|
||||
|
||||
```text
|
||||
次の 3 方向で並行処理することを提案します。
|
||||
- config:設定読み込みの整理
|
||||
- auth:認証ロジックのリファクタリング
|
||||
- tests:回帰テストの追加
|
||||
3 つの領域を並行して進めることを提案します:
|
||||
- config:設定の読み込みを整理
|
||||
- auth:認証をリファクタリング
|
||||
- tests:回帰テストを追加
|
||||
|
||||
確認後にチームメイトを起動します。
|
||||
```
|
||||
|
||||
ユーザーが「始めてください」と返した後で、Lead は `spawn_teammate` を呼ぶ。ユーザーが目的を示し、Lead がチームを設計し、ユーザーが実行境界を確認する。
|
||||
ユーザーが「始めてください」と返した後、Lead は `spawn_teammate` を呼べる。ユーザーが目標を示し、Lead がチームを設計し、ユーザーが実行境界を確認する。
|
||||
|
||||
### 2. 各チームメイトは独立したループを持つ
|
||||
|
||||
s06 の Subagent は 1 回限りの呼び出しだが、チームメイトは永続する実行単位である:
|
||||
s06 の subagent は 1 回限りの呼び出しである。チームメイトは永続する実行単位だ:
|
||||
|
||||
| | s06 Subagent | s15 チームメイト |
|
||||
| | s06 Subagent | s15 Teammate |
|
||||
|---|---|---|
|
||||
| ライフサイクル | 1 回の呼び出し後に終了 | 終了要求まで `WORK → IDLE → WORK` |
|
||||
| コンテキスト | 1 つのタスクだけ | 複数の依頼をまたいで保持 |
|
||||
| 通信 | 1 回だけ結果を返す | メッセージを受け取り、イベントを送る |
|
||||
| 協調 | 一方向の委任 | Lead との双方向協調 |
|
||||
| ライフサイクル | 1 回の呼び出し後に終了 | shutdown まで `WORK → IDLE → WORK` |
|
||||
| コンテキスト | 1 つのタスクにだけ存在 | 割り当てをまたいで保持 |
|
||||
| 通信 | 1 回だけ結果を返す | メッセージを受け取りイベントを送る |
|
||||
| 協調 | 一方向の委譲 | Lead との双方向協調 |
|
||||
|
||||
`spawn_teammate_thread()` はチームメイトごとに system prompt、messages、ツールを作り、daemon thread でループを実行する。Lead はチームメイトの終了を待たずに、別の依頼や結果を調整できる。
|
||||
`spawn_teammate_thread()` は、各チームメイト専用のシステムプロンプト、messages、ツール、現在の作業ディレクトリ状態を用意し、daemon thread でループを実行する。チームメイトの作業中も Lead は調整を続けられる。`lead` と `agent` はランタイム識別子として予約されるが、`MessageBus` はコーディネーターの受信箱として `lead` を引き続き受け付ける。
|
||||
|
||||
### 3. MessageBus は通信をモデルのコンテキスト外に置く
|
||||
|
||||
Lead とチームメイトが同じ messages 配列を共有すると、あるチームメイトのツール結果が別のチームメイトの推論へ混ざる。`MessageBus` は各 Agent に `.mailboxes/<name>.jsonl` 受信箱を与える:
|
||||
Lead とチームメイトは同じ messages 配列を共有できない。共有すると、あるチームメイトのツール結果が別のチームメイトの推論へ混ざる。`MessageBus` は Agent ごとに `.mailboxes/<name>.jsonl` 受信箱を用意する:
|
||||
|
||||
```python
|
||||
class MessageBus:
|
||||
@@ -102,21 +105,27 @@ class MessageBus:
|
||||
"metadata": metadata or {},
|
||||
}
|
||||
with self._changed:
|
||||
append_jsonl(self._path(to_agent), msg)
|
||||
with open(self._path(to_agent), "a") as f:
|
||||
f.write(json.dumps(msg) + "\n")
|
||||
self._changed.notify_all()
|
||||
|
||||
def wait_for_messages(self, agent):
|
||||
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):
|
||||
self._changed.wait()
|
||||
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 のチームメイトはポーリングせずイベント到着まで待機できる。
|
||||
ロックは、チームメイトによる受信箱ファイルの並行アクセスを保護する。`Condition` はメッセージ到着時にチームメイトを起こし、IDLE 中の短い timeout にも使える。
|
||||
|
||||
### 4. 受信イベントはランタイムが自動配信する
|
||||
### 4. 受信イベントはランタイムが配信する
|
||||
|
||||
`read_inbox()` はメッセージを読み、受信箱ファイルを削除する。そのため Lead の消費入口は `consume_lead_inbox()` だけにする:
|
||||
`read_inbox()` は受信箱ファイルを読み取って削除するため、Lead 側の消費処理は `consume_lead_inbox()` だけにする:
|
||||
|
||||
```python
|
||||
def consume_lead_inbox():
|
||||
@@ -127,33 +136,165 @@ def consume_lead_inbox():
|
||||
return messages
|
||||
```
|
||||
|
||||
メインループのイベントスレッドは、新しいメッセージが届くと Lead を起こす:
|
||||
メインループの隣で動くイベントスレッドが、新しいメッセージの到着時に Lead を起こす:
|
||||
|
||||
```text
|
||||
MessageBus → consume_lead_inbox
|
||||
→ プロトコル状態を更新
|
||||
→ [Team events] を history へ注入
|
||||
→ [Team events] を history に追加
|
||||
→ Lead の次ターンを開始
|
||||
```
|
||||
|
||||
`check_inbox` はモデルのツールではない。メッセージの到着はランタイムの責務であり、モデルはコンテキストへ配信済みのイベントだけを処理する。
|
||||
`check_inbox` はモデルのツールではない。メッセージの到着と消費はランタイムが担当し、モデルはコンテキストへ配信済みのイベントを処理する。
|
||||
|
||||
### 5. 結果と IDLE は別のイベント
|
||||
### 5. 結果と IDLE は別のイベントである
|
||||
|
||||
チームメイトが 1 件の作業を終えると、ランタイムは次の順序で 2 つのイベントを送る:
|
||||
チームメイトが 1 つの割り当てを終えると、ランタイムは 2 つのイベントを順に送る:
|
||||
|
||||
```text
|
||||
result: "認証をリファクタリングし、関連テストが通りました。"
|
||||
idle_notification: "Waiting for more work."
|
||||
```
|
||||
|
||||
`result` は「今回の作業で何が得られたか」、`idle_notification` は「新しい仕事を受けられるか」を表す。1 つの曖昧な「done」では両者を区別できない。
|
||||
`result` は「この割り当てで何ができたか」、`idle_notification` は「このチームメイトが次の仕事を受けられるか」を表す。曖昧な「完了」だけでは、両方の状態を表せない。
|
||||
|
||||
IDLE になったチームメイトは終了しない。通常メッセージで WORK に戻り、`shutdown_request` で終了ハンドシェイクを始める。
|
||||
IDLE のチームメイトは終了しない。直接メッセージか ready task を受けると WORK に戻り、`shutdown_request` を受けると段階的な shutdown handshake を始める。
|
||||
|
||||
### 6. 制御メッセージには型と request_id を使う
|
||||
### 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_lock` で保護した `claim_task()` 内で行う:
|
||||
|
||||
```python
|
||||
def claim_task(task_id: str, owner: str) -> str:
|
||||
with task_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 つだけである。現在のタスクを完了するまで、チームメイトは次のタスクを 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_1234")
|
||||
```
|
||||
|
||||
`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` wrapper は assignment からディレクトリを読む。worktree のないタスクは `WORKDIR` に解決されるため、worktree は opt-in である:
|
||||
|
||||
```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 を削除するのは完了に成功した時だけである。失敗時はタスクのディレクトリを維持し、チームメイトが修正して再試行できるようにする。タスクの `worktree` 紐付けは checkout を削除するまで残る。
|
||||
|
||||
> Worktree が分離するのは Git の作業ディレクトリとブランチであり、sandbox ではない。Shell コマンドは親プロセスに許可されたパスやリソースへアクセスできる。
|
||||
|
||||
### 10. Worktree のクリーンアップはデフォルトで作業を残す
|
||||
|
||||
モデル向けの `remove_worktree(name)` tool は、`pending` または `in_progress` のタスクに紐付いた worktree の削除を拒否する。タスク完了後も tracked、untracked、ignored file をすべて未コミットデータとして扱い、clean な checkout だけを `--force` なしで削除する。
|
||||
|
||||
低レベルの Python helper は、host が別途ユーザーの明示的な確認を得た場合のために `discard_changes=True` を残すが、この parameter はモデルの tool schema にはない。変更のある worktree は削除せず、user が確認できる状態で残す。どちらの削除経路でも `wt/<name>` ブランチはリポジトリに残り、upstream のない clean な local commit も保持される。削除成功後は checkout が存在しないため、タスクの worktree 紐付けを解除する。
|
||||
|
||||
```text
|
||||
clean worktree → ディレクトリを削除し、wt/<name> ブランチは保持
|
||||
changed worktree → model tool は拒否し、保持か破棄かを user が決める
|
||||
pending/running task → 削除を拒否
|
||||
```
|
||||
|
||||
タスク完了と worktree cleanup も分かれている。`complete_task` はタスク結果を記録し、Lead はその後に worktree を確認、merge、keep、remove できる。
|
||||
|
||||
### 11. 制御メッセージには型と request_id を使う
|
||||
|
||||
通常の協調には自由形式のテキストを使えるが、shutdown と承認を意図の推測に任せるべきではない。これらは構造化メッセージを使う:
|
||||
|
||||

|
||||
|
||||
@@ -171,22 +312,22 @@ class ProtocolState:
|
||||
pending_requests: dict[str, ProtocolState] = {}
|
||||
```
|
||||
|
||||
終了プロトコルは次の経路を通る:
|
||||
shutdown の流れは次の通り:
|
||||
|
||||
```text
|
||||
Lead が pending の shutdown request を作る
|
||||
→ shutdown_request(request_id) をチームメイトへ送る
|
||||
→ チームメイトが現在の手順を終える
|
||||
→ shutdown_response(request_id) を Lead へ返す
|
||||
→ request_id で元の要求を特定する
|
||||
→ pending が approved になり、チームメイトループが終了する
|
||||
→ shutdown_request(request_id) がチームメイトの受信箱に入る
|
||||
→ チームメイトが現在のステップを終える
|
||||
→ shutdown_response(request_id) が Lead へ戻る
|
||||
→ request_id で元の request を特定する
|
||||
→ pending が approved になり、チームメイトの loop が終了する
|
||||
```
|
||||
|
||||
ID は要求と応答を対応付け、型は誤った応答による状態変更を防ぎ、状態は重複応答の再適用を防ぐ。
|
||||
ID は応答を 1 つの request に対応付け、型は不一致の応答による状態変更を防ぎ、status は同じ応答の二重適用を防ぐ。
|
||||
|
||||
### 7. 計画承認は実行も制約する
|
||||
### 12. 計画承認は実行も制約する
|
||||
|
||||
計画プロトコルは逆方向に流れる:
|
||||
計画プロトコルは逆方向に進む:
|
||||
|
||||
```text
|
||||
Lead → plan_request
|
||||
@@ -194,7 +335,7 @@ Lead → plan_request
|
||||
Lead → plan_approval_response(request_id, approve, feedback)
|
||||
```
|
||||
|
||||
「承認まで待つ」と伝えるだけでは確実なゲートにならない。そこでツール dispatch が計画状態を検査する:
|
||||
ツール dispatch がゲートを強制する:
|
||||
|
||||
```python
|
||||
def _run_teammate_tool(name, block, handlers):
|
||||
@@ -206,32 +347,38 @@ def _run_teammate_tool(name, block, handlers):
|
||||
return handlers[block.name](**block.input)
|
||||
```
|
||||
|
||||
状態が `required`、`pending`、`rejected` の間、チームメイトはファイルを読み、計画を提出または修正できるが、Shell 実行やファイル書き込みはできない。承認応答で `approved` になった後にだけツールが解放される。
|
||||
状態が `required`、`pending`、`rejected` の間、チームメイトはファイルを読み、計画を提出または修正できるが、Shell コマンドの実行とファイルの書き込みはできない。承認応答で状態が `approved` になると、ツールを使えるようになる。
|
||||
|
||||
---
|
||||
|
||||
## 一連の実行例
|
||||
|
||||
```text
|
||||
s15 >> このサンプルバックエンドをリファクタリングしてください。
|
||||
設定読み込み、認証、テストを整理し、
|
||||
既存インターフェースを保ってテストを通してください。
|
||||
s15 >> バックエンドのリファクタリングを共有タスクボードに分解し、
|
||||
設定、認証、テストを可能な範囲で並行実行してください。
|
||||
認証には worktree を使い、既存インターフェースを保ち、
|
||||
テストが通ることを確認してください。
|
||||
|
||||
Lead: config、auth、tests の 3 方向で並行処理することを提案します。
|
||||
チームを開始しますか?
|
||||
Lead:config、auth、tests の 3 領域に分けることを提案します。
|
||||
チームを起動しますか?
|
||||
|
||||
s15 >> 始めてください
|
||||
|
||||
[teammate] config spawned
|
||||
[teammate] auth spawned
|
||||
[teammate] tests spawned
|
||||
[bus] auth → lead (result) ...
|
||||
[bus] auth → lead (idle_notification) ...
|
||||
[task] config created
|
||||
[task] auth created → worktree auth-refactor
|
||||
[task] tests created
|
||||
[teammate] alice spawned
|
||||
[teammate] bob spawned
|
||||
[claim] alice → config (cwd: repository)
|
||||
[claim] bob → auth (cwd: .worktrees/auth-refactor)
|
||||
[complete] auth
|
||||
[bus] bob → lead (result) ...
|
||||
[bus] bob → lead (idle_notification) ...
|
||||
[wake: 2 team events → new turn]
|
||||
Lead: 認証の結果を受け取りました。残りの作業も調整します。
|
||||
Lead:認証タスクの結果を受け取りました。残りの作業を調整します。
|
||||
```
|
||||
|
||||
端末には、ユーザー要求、Lead の分担、起動、メッセージ、結果、IDLE、終了イベントが表示される。ユーザーが Lead を指名したり、受信箱の確認を頼んだりする必要はない。
|
||||
ターミナルには、ユーザーの要求、Lead の提案、タスク状態、Claim、選択されたディレクトリ、結果、IDLE 遷移、制御イベントが表示される。ユーザーが Lead を指定したり、受信箱の確認を依頼したりする必要はない。
|
||||
|
||||
---
|
||||
|
||||
@@ -239,13 +386,15 @@ Lead: 認証の結果を受け取りました。残りの作業も調整しま
|
||||
|
||||
| コンポーネント | s14 | s15 |
|
||||
|---|---|---|
|
||||
| Agent | 1 つ | 1 つの Lead + 永続チームメイト |
|
||||
| ユーザーフロー | 依頼を直接実行 | チーム案を提示してから起動を確認 |
|
||||
| 通信 | なし | ファイル受信箱 + 自動イベント配信 |
|
||||
| Agent | 1 つの Agent | 1 つの Lead と永続チームメイト |
|
||||
| ユーザーフロー | 要求を実行 | チーム案を示してから起動確認 |
|
||||
| 通信 | なし | ファイル受信箱とランタイム配信 |
|
||||
| ライフサイクル | 1 つのループ | チームメイトの `WORK / IDLE / shutdown` |
|
||||
| 共有作業 | Lead の既存タスクツール | IDLE 走査とチームメイトのアトミックな Claim |
|
||||
| 作業ディレクトリ | リポジトリの `WORKDIR` | デフォルトは `WORKDIR`、タスクごとに worktree を選択可能 |
|
||||
| 結果通知 | 現在の Agent の出力 | `result` と `idle_notification` を分離 |
|
||||
| 制御 | なし | 終了と計画承認プロトコル |
|
||||
| 強制 | チーム制約なし | 必須計画が変更系ツールをゲート |
|
||||
| 制御 | なし | 型付き shutdown と計画承認プロトコル |
|
||||
| 強制 | チーム向け制約なし | 必須計画が変更系ツールをゲート |
|
||||
|
||||
---
|
||||
|
||||
@@ -256,12 +405,12 @@ cd learn-claude-code
|
||||
python s15_agent_teams/code.py
|
||||
```
|
||||
|
||||
まず通常の依頼を入力する:
|
||||
通常の要求を入力する:
|
||||
|
||||
```text
|
||||
このサンプルバックエンドをリファクタリングしてください。
|
||||
設定読み込み、認証ロジック、テストを整理し、
|
||||
既存インターフェースを保ったままテストを通してください。
|
||||
バックエンドのリファクタリングを共有タスクボードへ分解し、依存関係が
|
||||
許す範囲で設定、認証、テストを並行実行してください。認証には worktree
|
||||
を使い、既存インターフェースを維持して、最後に結果をまとめてください。
|
||||
```
|
||||
|
||||
Lead がチーム案を示したら、次のように返す:
|
||||
@@ -270,14 +419,16 @@ Lead がチーム案を示したら、次のように返す:
|
||||
始めてください
|
||||
```
|
||||
|
||||
`spawned`、`result`、`idle_notification`、`plan_approval_*`、`shutdown_*` の各イベントと、`.mailboxes/` のファイルが生成・消費される流れを確認する。
|
||||
`.tasks/` が `pending`、`in_progress`、`completed` と変化する様子、`.mailboxes/` が `result` と `idle_notification` を配信する様子、紐付けたタスクにだけ `.worktrees/` が作られることを確認する。直接メッセージがタスクボード走査より優先されることと、`complete_task` の失敗後もチームメイトの作業ディレクトリが変わらないことも確認できる。
|
||||
|
||||
---
|
||||
|
||||
## 次へ
|
||||
|
||||
s15 では、Lead が各チームメイトへ明示的に仕事を割り当てる。次のセッションでは共有タスクボードを IDLE のチームメイトに公開し、実行可能な仕事を自ら見つけて claim できるようにする。
|
||||
チームランタイムは、委譲、共有タスクの Claim、任意の作業ディレクトリを扱えるようになった。ただし、ツールは今も Python コードへ直接定義している。
|
||||
|
||||
次へ:[s16 Autonomous Agents](../s16_autonomous_agents/)。
|
||||
次のレッスンでは、標準の発見・呼び出しプロトコルを使って外部ツールへ接続する。
|
||||
|
||||
<!-- translation-sync: zh@v2, en@v2, ja@v2 -->
|
||||
次へ:[s16 MCP Tools](../s16_mcp_plugin/)。
|
||||
|
||||
<!-- translation-sync: zh@v3, en@v3, ja@v3 -->
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
|
||||
[English](README.md) · [中文](README.zh.md) · [日本語](README.ja.md)
|
||||
|
||||
s01 → ... → s13 → s14 → `s15` → [s16](../s16_autonomous_agents/) → s17 → s18 → s19 → s20 → s21
|
||||
s01 → ... → s13 → s14 → `s15` → [s16](../s16_mcp_plugin/) → s17 → s18 → s19
|
||||
|
||||
> *"When one agent cannot hold the whole job, let teammates divide the work."* — Persistent teammates, message delivery, and coordination protocols.
|
||||
> *"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 work in parallel without losing control.
|
||||
> **Harness layer**: Team — how multiple agents divide work, share state, and stay under Lead's control.
|
||||
|
||||
---
|
||||
|
||||
@@ -22,12 +22,14 @@ authentication, and tests, preserve the existing interfaces,
|
||||
and make sure the tests pass.
|
||||
```
|
||||
|
||||
The harness therefore has to solve four connected problems:
|
||||
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 automatically, without asking the model to poll an inbox?
|
||||
4. How do shutdown and plan approval become traceable, enforceable protocols?
|
||||
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?
|
||||
|
||||
---
|
||||
|
||||
@@ -35,24 +37,25 @@ The harness therefore has to solve four connected problems:
|
||||
|
||||

|
||||
|
||||
s15 adds a Lead-managed team runtime around the single-agent harness:
|
||||
s15 adds one Lead-managed team runtime around the single-agent harness:
|
||||
|
||||
- **Lead** owns the user conversation, proposes a division of work, and waits for confirmation.
|
||||
- **Teammates** run independent agent loops in background threads and become idle after an assignment.
|
||||
- **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.
|
||||
- **Coordination protocols** use `type`, `request_id`, and state transitions for shutdown and plan approval.
|
||||
- **A plan gate** blocks teammate `bash` and `write_file` calls until a required plan is approved.
|
||||
- **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.
|
||||
|
||||
The model understands tasks and chooses a useful division of work. Code owns delivery, lifecycle, and protocol constraints.
|
||||
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 confirmation
|
||||
### 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. That boundary should not be hidden inside an ordinary tool call. Lead's system prompt says:
|
||||
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 "
|
||||
@@ -84,7 +87,7 @@ An s06 subagent is a one-shot call. A teammate is a persistent execution unit:
|
||||
| Communication | Returns one result | Receives messages and emits events |
|
||||
| Coordination | One-way delegation | Two-way collaboration with Lead |
|
||||
|
||||
`spawn_teammate_thread()` gives each teammate its own system prompt, messages, and tools, then runs its loop in a daemon thread. Lead can keep coordinating while teammates work.
|
||||
`spawn_teammate_thread()` gives each teammate its own system prompt, messages, tools, and current working-directory state, then runs its 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.
|
||||
|
||||
### 3. MessageBus keeps communication outside model context
|
||||
|
||||
@@ -102,19 +105,25 @@ class MessageBus:
|
||||
"metadata": metadata or {},
|
||||
}
|
||||
with self._changed:
|
||||
append_jsonl(self._path(to_agent), msg)
|
||||
with open(self._path(to_agent), "a") as f:
|
||||
f.write(json.dumps(msg) + "\n")
|
||||
self._changed.notify_all()
|
||||
|
||||
def wait_for_messages(self, agent):
|
||||
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):
|
||||
self._changed.wait()
|
||||
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 teammate access. A `Condition` lets idle teammates sleep until an event arrives instead of polling continuously.
|
||||
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 automatically
|
||||
### 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()`:
|
||||
|
||||
@@ -136,9 +145,9 @@ MessageBus → consume_lead_inbox
|
||||
→ start another Lead turn
|
||||
```
|
||||
|
||||
`check_inbox` is not a model tool. Message arrival belongs to the runtime; the model only handles events that have already been delivered into its context.
|
||||
`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
|
||||
### 5. Result and IDLE are separate events
|
||||
|
||||
When a teammate finishes one assignment, the runtime sends two events in order:
|
||||
|
||||
@@ -147,13 +156,145 @@ 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?" A single vague "done" cannot represent both facts.
|
||||
`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. An ordinary message returns it to WORK; a `shutdown_request` starts a graceful shutdown handshake.
|
||||
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. Control messages use types and request IDs
|
||||
### 6. IDLE checks the mailbox before looking for ready tasks
|
||||
|
||||
Free-form text is fine for ordinary collaboration, but shutdown and approval should not depend on guessing intent. They use structured messages:
|
||||
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 may see the same task, so ownership changes happen inside `claim_task()` under `task_lock`:
|
||||
|
||||
```python
|
||||
def claim_task(task_id: str, owner: str) -> str:
|
||||
with task_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`. 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_1234")
|
||||
```
|
||||
|
||||
`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`, and `write_file` wrappers read the directory from the assignment. A task with no worktree resolves to `WORKDIR`, so worktrees remain opt-in:
|
||||
|
||||
```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. It clears the assignment only after completion succeeds. A failed completion leaves the task directory selected so the teammate can fix the task and try again. The task keeps its `worktree` binding until that checkout is removed.
|
||||
|
||||
> 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 cleanup preserves work by default
|
||||
|
||||
The model-facing `remove_worktree(name)` tool refuses to remove a worktree while its bound task is `pending` or `in_progress`. After the task is completed, it still treats tracked, untracked, and ignored files as uncommitted data, then asks Git to remove only a clean checkout without `--force`.
|
||||
|
||||
The lower-level Python helper retains `discard_changes=True` for host code that has already obtained explicit user confirmation, but that parameter is not present in the model's tool schema. A dirty worktree is left for the user to inspect. Either removal path retains the `wt/<name>` branch, including clean local commits with no upstream. A successful removal clears the task's worktree binding because the checkout no longer exists.
|
||||
|
||||
```text
|
||||
clean worktree → remove directory, retain wt/<name> branch
|
||||
changed worktree → model tool refuses; 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; Lead can inspect, merge, keep, or remove the worktree afterward.
|
||||
|
||||
### 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:
|
||||
|
||||

|
||||
|
||||
@@ -184,7 +325,7 @@ Lead creates a pending shutdown request
|
||||
|
||||
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.
|
||||
|
||||
### 7. Plan approval constrains execution
|
||||
### 12. Plan approval constrains execution
|
||||
|
||||
The plan protocol runs in the opposite direction:
|
||||
|
||||
@@ -194,7 +335,7 @@ teammate → plan_approval_request(request_id, plan)
|
||||
Lead → plan_approval_response(request_id, approve, feedback)
|
||||
```
|
||||
|
||||
Merely telling a teammate to wait is not a reliable gate, so tool dispatch checks the plan state:
|
||||
Tool dispatch enforces the gate:
|
||||
|
||||
```python
|
||||
def _run_teammate_tool(name, block, handlers):
|
||||
@@ -206,32 +347,38 @@ def _run_teammate_tool(name, block, handlers):
|
||||
return handlers[block.name](**block.input)
|
||||
```
|
||||
|
||||
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 or write files. The tools are released only after an approval response changes the state to `approved`.
|
||||
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 or write files. The tools are released after an approval response changes the state to `approved`.
|
||||
|
||||
---
|
||||
|
||||
## One Complete Run
|
||||
|
||||
```text
|
||||
s15 >> Refactor this sample backend. Clean up configuration loading,
|
||||
authentication, and tests, preserve existing interfaces,
|
||||
s15 >> 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 parallel areas.
|
||||
Lead: I suggest config, auth, and tests as three areas.
|
||||
Shall I start the team?
|
||||
|
||||
s15 >> Go ahead.
|
||||
|
||||
[teammate] config spawned
|
||||
[teammate] auth spawned
|
||||
[teammate] tests spawned
|
||||
[bus] auth → lead (result) ...
|
||||
[bus] auth → lead (idle_notification) ...
|
||||
[task] config created
|
||||
[task] auth created → worktree auth-refactor
|
||||
[task] tests created
|
||||
[teammate] alice spawned
|
||||
[teammate] bob spawned
|
||||
[claim] alice → config (cwd: repository)
|
||||
[claim] bob → auth (cwd: .worktrees/auth-refactor)
|
||||
[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 split, teammate startup, messages, results, idle transitions, and shutdown events. The user does not have to name a Lead or ask it to check an inbox.
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
@@ -241,10 +388,12 @@ The terminal exposes the user request, Lead's split, teammate startup, messages,
|
||||
|---|---|---|
|
||||
| Agents | One agent | One Lead plus persistent teammates |
|
||||
| User flow | Execute the request | Propose a team, then confirm startup |
|
||||
| Communication | None | File mailboxes plus automatic delivery |
|
||||
| Communication | None | File mailboxes plus runtime delivery |
|
||||
| Lifecycle | One loop | Teammate `WORK / IDLE / shutdown` |
|
||||
| Shared work | Lead's existing task tools | IDLE scan plus atomic teammate claims |
|
||||
| Working directory | Repository `WORKDIR` | `WORKDIR` by default, optional task worktree |
|
||||
| Reporting | Current agent output | Separate `result` and `idle_notification` |
|
||||
| Control | None | Shutdown and plan approval protocols |
|
||||
| Control | None | Typed shutdown and plan approval protocols |
|
||||
| Enforcement | No team constraint | Required plans gate mutating tools |
|
||||
|
||||
---
|
||||
@@ -259,9 +408,10 @@ python s15_agent_teams/code.py
|
||||
Start with an ordinary request:
|
||||
|
||||
```text
|
||||
Refactor this sample backend. Clean up configuration loading,
|
||||
authentication, and tests, preserve the existing interfaces,
|
||||
and make sure the tests pass.
|
||||
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:
|
||||
@@ -270,14 +420,16 @@ After Lead proposes the team, reply:
|
||||
Go ahead.
|
||||
```
|
||||
|
||||
Watch for `spawned`, `result`, `idle_notification`, `plan_approval_*`, and `shutdown_*` events, along with mailbox files appearing and being consumed under `.mailboxes/`.
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
## Next
|
||||
|
||||
In s15, Lead still assigns each teammate explicitly. The next lesson gives idle teammates access to the shared task board so they can discover and claim ready work themselves.
|
||||
The team runtime now covers delegation, shared task selection, and optional working directories. Its tools are still defined directly in Python.
|
||||
|
||||
Next: [s16 Autonomous Agents](../s16_autonomous_agents/).
|
||||
The next lesson connects external tools through a standard discovery and invocation protocol.
|
||||
|
||||
<!-- translation-sync: zh@v2, en@v2, ja@v2 -->
|
||||
Next: [s16 MCP Tools](../s16_mcp_plugin/).
|
||||
|
||||
<!-- translation-sync: zh@v3, en@v3, ja@v3 -->
|
||||
|
||||
@@ -2,31 +2,33 @@
|
||||
|
||||
[English](README.md) · [中文](README.zh.md) · [日本語](README.ja.md)
|
||||
|
||||
s01 → ... → s13 → s14 → `s15` → [s16](../s16_autonomous_agents/) → s17 → s18 → s19 → s20 → s21
|
||||
s01 → ... → s13 → s14 → `s15` → [s16](../s16_mcp_plugin/) → s17 → s18 → s19
|
||||
|
||||
> *"一个 Agent 顾不过来,就让队友分工协作。"* — 持久队友、消息投递与协作协议。
|
||||
> *“一个 Agent 装不下整项工作时,就让队友分头完成。”* — 持久队友、共享任务认领、可选 worktree 与协作协议。
|
||||
>
|
||||
> **Harness 层**:团队 — 多个 Agent 如何并行工作,又如何保持可控。
|
||||
> **Harness 层**:Team(团队)— 多个 Agent 如何分工、共享状态,同时接受 Lead 控制。
|
||||
|
||||
---
|
||||
|
||||
## 问题
|
||||
|
||||
当我们需要 Agent 帮助我们重构整个后端时,任务可能同时涉及配置加载、认证逻辑和测试。一个 Agent 依次处理所有模块,不但耗时更长,早期细节也会逐渐退出上下文。
|
||||
假设我们让 Agent 重构整个后端,工作涉及配置加载、认证和测试。一个 Agent 可以依次处理,但总耗时更长,早期细节也会逐渐离开上下文。
|
||||
|
||||
这类任务适合拆给多个 Agent,但用户通常只会描述需求,不会先设计一套团队:
|
||||
这类工作适合并行,可用户通常只描述目标,不会替运行时设计团队:
|
||||
|
||||
```text
|
||||
请重构这个示例后端,分别整理配置加载、认证逻辑和测试,
|
||||
保持现有接口兼容,并确保测试通过。
|
||||
重构这个示例后端。清理配置加载、认证和测试,
|
||||
保持现有接口,并确保测试通过。
|
||||
```
|
||||
|
||||
因此,Harness 需要连续解决四个问题:
|
||||
Harness 需要回答一组相互关联的问题:
|
||||
|
||||
1. 谁判断任务是否值得并行,以及如何征得用户确认?
|
||||
2. 队友如何保留自己的身份和上下文,持续接收工作?
|
||||
3. 队友的结果如何自动回到 Lead,而不是依赖模型反复检查邮箱?
|
||||
4. 关机与计划审批如何变成可追踪、可执行的协议?
|
||||
1. 谁判断并行是否有用,新增 Agent 又由谁确认?
|
||||
2. 每个队友如何跨任务保留身份和上下文?
|
||||
3. 结果如何自动返回 Lead,而不是让模型轮询收件箱?
|
||||
4. 空闲队友能否直接接手 ready task,不再等待 Lead 逐项派发?
|
||||
5. 并行修改可能冲突时,任务应该使用哪个工作目录?
|
||||
6. 关机和计划审批如何成为可追踪、可执行的协议?
|
||||
|
||||
---
|
||||
|
||||
@@ -34,16 +36,17 @@ s01 → ... → s13 → s14 → `s15` → [s16](../s16_autonomous_agents/) → s
|
||||
|
||||

|
||||
|
||||
s15 在单 Agent Harness 外增加一个由 Lead 管理的团队运行时:
|
||||
s15 在单 Agent Harness 外增加一套由 Lead 管理的团队运行时:
|
||||
|
||||
- **Lead** 保持用户对话,判断是否需要团队,提出分工并等待确认。
|
||||
- **队友** 在独立线程中运行自己的 Agent Loop,完成工作后进入空闲。
|
||||
- **MessageBus** 用文件邮箱传递普通消息、结果和控制事件。
|
||||
- **运行时投递** 自动消费 Lead 的邮箱,把团队事件注入下一轮上下文。
|
||||
- **协作协议** 用 `type`、`request_id` 和状态机处理关机与计划审批。
|
||||
- **计划闸门** 在计划未批准时拦截队友的 `bash` 和 `write_file`。
|
||||
- **Lead** 负责用户对话,提出分工方案并等待确认。
|
||||
- **队友** 运行独立 Agent Loop,在 WORK 和 IDLE 之间切换。
|
||||
- **MessageBus** 通过文件收件箱传递普通消息、结果和控制事件。
|
||||
- **运行时投递** 消费 Lead 的收件箱,把团队事件注入下一轮对话。
|
||||
- **共享任务板** 让空闲队友发现 ready task,并在锁内完成认领。
|
||||
- **可选 worktree** 在需要时把任务绑定到另一个工作目录;未绑定任务仍使用仓库目录。
|
||||
- **类型化协议和计划闸门** 显式记录关机与审批状态,并在计划获批前阻止修改型工具。
|
||||
|
||||
模型负责理解任务与分工,代码负责消息投递、生命周期和协议约束。
|
||||
这些机制都属于 Team 这一层。任务发现不需要另一套 Agent Loop,worktree 也不会产生另一种 Agent。
|
||||
|
||||
---
|
||||
|
||||
@@ -51,7 +54,7 @@ s15 在单 Agent Harness 外增加一个由 Lead 管理的团队运行时:
|
||||
|
||||
### 1. Lead 先提出团队,再等待用户确认
|
||||
|
||||
是否创建团队会改变成本、并发度和可写入范围,不应该被隐藏在一次普通工具调用里。Lead 的 system prompt 明确规定:
|
||||
启动队友会改变成本、并发度和可以修改工作区的角色集合。Lead 的系统提示词会把这条边界明确写出来:
|
||||
|
||||
```python
|
||||
"When parallel work would help, first propose a small team with clear "
|
||||
@@ -59,35 +62,35 @@ s15 在单 Agent Harness 外增加一个由 Lead 管理的团队运行时:
|
||||
"spawn_teammate before the user confirms."
|
||||
```
|
||||
|
||||
第一次输入需求时,Lead 只需要说明建议的拆分:
|
||||
收到第一条需求后,Lead 只提出分工:
|
||||
|
||||
```text
|
||||
我建议分成三个方向并行处理:
|
||||
- config:整理配置加载
|
||||
- auth:重构认证逻辑
|
||||
- tests:补齐回归测试
|
||||
我建议并行处理三个方向:
|
||||
- config:清理配置加载
|
||||
- auth:重构认证
|
||||
- tests:补充回归测试
|
||||
|
||||
确认后我会启动队友并协调结果。
|
||||
你确认后我再启动队友。
|
||||
```
|
||||
|
||||
用户回复“开始吧”后,Lead 才调用 `spawn_teammate`。用户表达目标,Lead 设计团队,用户确认执行边界;三者的职责不会混在一起。
|
||||
用户回复“开始吧”后,Lead 才能调用 `spawn_teammate`。用户给出目标,Lead 设计团队,用户确认执行边界。
|
||||
|
||||
### 2. 每个队友拥有独立循环
|
||||
|
||||
s06 的子 Agent 是一次性调用,返回结果后就结束。队友则是持久执行单元:
|
||||
s06 的 subagent 是一次性调用,队友则是持久执行单元:
|
||||
|
||||
| | s06 子 Agent | s15 队友 |
|
||||
| | s06 Subagent | s15 队友 |
|
||||
|---|---|---|
|
||||
| 生命周期 | 完成一次调用后结束 | `WORK → IDLE → WORK`,直到收到关机请求 |
|
||||
| 上下文 | 只服务当前任务 | 在多轮协作中保留 |
|
||||
| 通信 | 返回一次结果 | 持续接收消息并上报事件 |
|
||||
| 协调 | 主 Agent 单向委派 | Lead 与队友双向协作 |
|
||||
| 生命周期 | 一次调用后结束 | `WORK → IDLE → WORK`,直到关机 |
|
||||
| 上下文 | 只服务一个任务 | 跨任务保留 |
|
||||
| 通信 | 返回一次结果 | 接收消息并发出事件 |
|
||||
| 协作 | 单向委派 | 与 Lead 双向协作 |
|
||||
|
||||
`spawn_teammate_thread()` 为队友创建独立的 system prompt、messages 和工具集,并把循环放入 daemon 线程。Lead 不必等待某个队友结束,仍可继续派发任务或处理其他结果。
|
||||
`spawn_teammate_thread()` 为每个队友保存独立的系统提示词、messages、工具和当前工作目录状态,再在线程中运行循环。队友工作时,Lead 可以继续协调其他任务。`lead` 和 `agent` 保留给运行时身份,但 `MessageBus` 仍允许把 `lead` 作为协调者收件箱。
|
||||
|
||||
### 3. MessageBus 把通信放在上下文之外
|
||||
### 3. MessageBus 把通信放在模型上下文之外
|
||||
|
||||
Lead 和队友不能共享同一份 messages,否则一个队友的工具结果会混入另一个队友的推理。`MessageBus` 为每个 Agent 建立 `.mailboxes/<name>.jsonl`:
|
||||
Lead 和队友不能共享同一个 messages 数组,否则一个队友的工具结果会进入另一个队友的推理上下文。`MessageBus` 为每个 Agent 提供 `.mailboxes/<name>.jsonl` 收件箱:
|
||||
|
||||
```python
|
||||
class MessageBus:
|
||||
@@ -101,21 +104,27 @@ class MessageBus:
|
||||
"metadata": metadata or {},
|
||||
}
|
||||
with self._changed:
|
||||
append_jsonl(self._path(to_agent), msg)
|
||||
with open(self._path(to_agent), "a") as f:
|
||||
f.write(json.dumps(msg) + "\n")
|
||||
self._changed.notify_all()
|
||||
|
||||
def wait_for_messages(self, agent):
|
||||
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):
|
||||
self._changed.wait()
|
||||
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` 让空闲队友等待事件,而不是持续轮询。
|
||||
锁会保护收件箱文件,避免队友并发读写。`Condition` 既能在消息到达时唤醒队友,也能支持 IDLE 状态下的短时等待。
|
||||
|
||||
### 4. 收件箱由运行时自动投递
|
||||
### 4. 收件箱事件由运行时投递
|
||||
|
||||
`read_inbox()` 是消费式读取:读出后删除邮箱文件。因此,Lead 只保留一个消费入口 `consume_lead_inbox()`:
|
||||
`read_inbox()` 会读取并删除收件箱文件,因此 Lead 只保留一个消费者 `consume_lead_inbox()`:
|
||||
|
||||
```python
|
||||
def consume_lead_inbox():
|
||||
@@ -126,33 +135,165 @@ def consume_lead_inbox():
|
||||
return messages
|
||||
```
|
||||
|
||||
主循环旁的事件线程发现新消息后,会唤醒 Lead:
|
||||
主循环旁边的事件线程会在新消息到达时唤醒 Lead:
|
||||
|
||||
```text
|
||||
MessageBus → consume_lead_inbox
|
||||
→ 更新协议状态
|
||||
→ [Team events] 注入 history
|
||||
→ Lead 开始新一轮
|
||||
→ 把 [Team events] 注入 history
|
||||
→ 启动新一轮 Lead 调用
|
||||
```
|
||||
|
||||
`check_inbox` 不再是模型工具。消息何时到达属于运行时职责;模型只需要处理已经送入上下文的事件。
|
||||
`check_inbox` 不是模型工具。消息到达和消费属于运行时,模型只处理已经投递到上下文里的事件。
|
||||
|
||||
### 5. 结果与空闲是两个不同事件
|
||||
### 5. 结果与 IDLE 是两个事件
|
||||
|
||||
队友完成一项工作时,运行时依次发送:
|
||||
队友完成一项任务后,运行时按顺序发送两个事件:
|
||||
|
||||
```text
|
||||
result: "认证逻辑已重构,相关测试通过。"
|
||||
result: "认证已重构,相关测试通过。"
|
||||
idle_notification: "Waiting for more work."
|
||||
```
|
||||
|
||||
`result` 回答“这次工作产出了什么”,`idle_notification` 表示“这个队友现在可以接新任务”。如果把两者合成一个模糊的“done”,Lead 就无法区分任务结果和资源状态。
|
||||
`result` 回答“这项任务产出了什么”,`idle_notification` 回答“这个队友能否继续接任务”。一个含糊的“完成了”无法同时表达这两种状态。
|
||||
|
||||
队友进入 IDLE 后不会退出。新普通消息会让它回到 WORK;`shutdown_request` 则让它完成关机握手并结束线程。
|
||||
空闲队友不会退出。直接消息或 ready task 会让它回到 WORK,`shutdown_request` 则会启动平滑关机握手。
|
||||
|
||||
### 6. 控制消息使用类型和 request_id
|
||||
### 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)
|
||||
]
|
||||
```
|
||||
|
||||
候选列表只是某一时刻的快照。另一个队友也可能看到同一任务,因此所有权变更必须放进 `claim_task()`,并由 `task_lock` 包住:
|
||||
|
||||
```python
|
||||
def claim_task(task_id: str, owner: str) -> str:
|
||||
with task_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_1234")
|
||||
```
|
||||
|
||||
`create_worktree` 只提供给 Lead。它要求任务处于 pending、无人认领且尚未绑定,随后检查名称、路径、分支和 Git 注册信息,创建 checkout,最后才写入任务绑定。如果 Git 报告失败却已经留下分支或已注册的 checkout,运行时会报告 partial operation,让任务保持未绑定,并保留这些内容供人工恢复。队友只使用任务工具和文件工具。
|
||||
|
||||
认领任务时,运行时会把解析后的目录写入 `teammate_assignments`,该队友的 `bash`、`read_file` 和 `write_file` 包装器从 assignment 读取目录。没有绑定 worktree 的任务解析到 `WORKDIR`,所以 worktree 默认不开启:
|
||||
|
||||
```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;完成失败时仍保留任务目录,队友可以修正问题后再次提交。任务上的 `worktree` 绑定会一直保留到 checkout 被移除。
|
||||
|
||||
> Worktree 只分开 Git 工作目录和分支,不是安全沙箱。Shell 命令仍能访问父进程有权访问的路径和资源。
|
||||
|
||||
### 10. Worktree 清理默认保留工作
|
||||
|
||||
模型可调用的 `remove_worktree(name)` 工具会拒绝移除仍绑定 `pending` 或 `in_progress` 任务的 worktree。任务完成后,它仍把已跟踪、未跟踪和已忽略文件都视为未提交数据,只会不带 `--force` 移除干净的 checkout。
|
||||
|
||||
底层 Python 函数保留 `discard_changes=True`,供已经另行取得用户明确确认的宿主调用,但模型的工具 schema 不包含这个参数。遇到有改动的 worktree,模型只能停下来交给用户检查。两种移除路径都会保留仓库里的 `wt/<name>` 分支,包括没有 upstream 的干净本地提交。移除成功后,任务的 worktree 绑定会被清空,因为对应 checkout 已不存在。
|
||||
|
||||
```text
|
||||
干净 worktree → 移除目录,保留 wt/<name> 分支
|
||||
有改动 worktree → 模型工具拒绝;由用户决定保留还是丢弃
|
||||
待办/进行中任务 → 拒绝移除
|
||||
```
|
||||
|
||||
任务完成与 worktree 清理也互相独立。`complete_task` 记录任务结果,Lead 随后可以检查、合并、保留或移除 worktree。
|
||||
|
||||
### 11. 控制消息使用类型和 request_id
|
||||
|
||||
普通协作可以使用自由文本,关机和审批则不能依靠猜测消息意图。它们使用结构化消息:
|
||||
|
||||

|
||||
|
||||
@@ -170,21 +311,22 @@ class ProtocolState:
|
||||
pending_requests: dict[str, ProtocolState] = {}
|
||||
```
|
||||
|
||||
关机协议的完整路径是:
|
||||
关机路径如下:
|
||||
|
||||
```text
|
||||
Lead 创建 shutdown 请求,状态为 pending
|
||||
→ shutdown_request(request_id) 发给队友
|
||||
→ 队友完成当前步骤并回复 shutdown_response(request_id)
|
||||
→ Lead 用 request_id 找到原请求
|
||||
→ pending 变为 approved,队友线程退出
|
||||
Lead 创建 pending 状态的关机请求
|
||||
→ shutdown_request(request_id) 进入队友收件箱
|
||||
→ 队友完成当前步骤
|
||||
→ shutdown_response(request_id) 返回 Lead
|
||||
→ request_id 找到原始请求
|
||||
→ pending 变为 approved,队友循环退出
|
||||
```
|
||||
|
||||
`request_id` 负责关联请求与回复,`type` 防止错误类型的回复修改状态,`status` 防止重复响应被再次处理。
|
||||
ID 把回复关联到请求,类型阻止不匹配的回复修改状态,状态则阻止同一回复重复生效。
|
||||
|
||||
### 7. 计划审批不仅传消息,还约束执行
|
||||
### 12. 计划审批会约束执行
|
||||
|
||||
计划协议沿相反方向流动:
|
||||
计划协议的方向相反:
|
||||
|
||||
```text
|
||||
Lead → plan_request
|
||||
@@ -192,7 +334,7 @@ Lead → plan_request
|
||||
Lead → plan_approval_response(request_id, approve, feedback)
|
||||
```
|
||||
|
||||
只告诉队友“请等待批准”并不可靠,所以工具分发器检查计划状态:
|
||||
工具分发层负责执行闸门:
|
||||
|
||||
```python
|
||||
def _run_teammate_tool(name, block, handlers):
|
||||
@@ -204,30 +346,36 @@ def _run_teammate_tool(name, block, handlers):
|
||||
return handlers[block.name](**block.input)
|
||||
```
|
||||
|
||||
当状态为 `required`、`pending` 或 `rejected` 时,队友仍可读取文件、提交或修改计划,但不能执行 Shell 或写文件。批准消息到达后,状态变为 `approved`,工具才会放行。
|
||||
状态是 `required`、`pending` 或 `rejected` 时,队友可以读取文件、提交或修改计划,但不能运行 Shell 命令或写文件。审批回复把状态改成 `approved` 后,这些工具才会放开。
|
||||
|
||||
---
|
||||
|
||||
## 一次完整运行
|
||||
|
||||
```text
|
||||
s15 >> 请重构这个示例后端,分别整理配置加载、认证逻辑和测试,
|
||||
保持现有接口兼容,并确保测试通过。
|
||||
s15 >> 把后端重构拆到共享任务板,尽量并行完成配置、认证和测试。
|
||||
认证任务使用 worktree,保持现有接口,并确保测试通过。
|
||||
|
||||
Lead: 建议由 config、auth、tests 三个方向并行处理,是否开始?
|
||||
Lead:我建议按 config、auth 和 tests 三个方向分工。
|
||||
是否启动团队?
|
||||
|
||||
s15 >> 开始吧
|
||||
|
||||
[teammate] config spawned
|
||||
[teammate] auth spawned
|
||||
[teammate] tests spawned
|
||||
[bus] auth → lead (result) ...
|
||||
[bus] auth → lead (idle_notification) ...
|
||||
[task] config created
|
||||
[task] auth created → worktree auth-refactor
|
||||
[task] tests created
|
||||
[teammate] alice spawned
|
||||
[teammate] bob spawned
|
||||
[claim] alice → config (cwd: repository)
|
||||
[claim] bob → auth (cwd: .worktrees/auth-refactor)
|
||||
[complete] auth
|
||||
[bus] bob → lead (result) ...
|
||||
[bus] bob → lead (idle_notification) ...
|
||||
[wake: 2 team events → new turn]
|
||||
Lead: 已收到认证部分结果,继续等待并协调其他队友。
|
||||
Lead:我已收到认证任务的结果,接下来继续协调其余工作。
|
||||
```
|
||||
|
||||
终端中显示的是用户需求、Lead 分工、队友启动、消息流、结果、空闲和关机事件。用户不需要在提示词里指定谁是 Lead,也不需要手动要求检查邮箱。
|
||||
终端会显示用户请求、Lead 的团队方案、任务状态、认领结果、所选目录、结果、IDLE 切换和控制事件。用户不需要指定谁是 Lead,也不必提醒它检查收件箱。
|
||||
|
||||
---
|
||||
|
||||
@@ -235,13 +383,15 @@ Lead: 已收到认证部分结果,继续等待并协调其他队友。
|
||||
|
||||
| 组件 | s14 | s15 |
|
||||
|---|---|---|
|
||||
| Agent 数量 | 一个 Agent | 一个 Lead + 多个持久队友 |
|
||||
| 用户交互 | 直接执行任务 | 先提出团队方案,再确认启动 |
|
||||
| 通信 | 无 | 文件邮箱 + 自动事件投递 |
|
||||
| 生命周期 | 单循环 | 队友 `WORK / IDLE / shutdown` |
|
||||
| 结果上报 | 当前 Agent 输出 | `result` 与 `idle_notification` 分离 |
|
||||
| 控制协议 | 无 | 关机与计划审批 |
|
||||
| 执行约束 | 无团队约束 | 未批准计划会拦截写入类工具 |
|
||||
| Agent | 单个 Agent | 一个 Lead 加持久队友 |
|
||||
| 用户流程 | 直接执行请求 | 先提团队方案,再确认启动 |
|
||||
| 通信 | 无 | 文件收件箱加运行时投递 |
|
||||
| 生命周期 | 一个循环 | 队友 `WORK / IDLE / shutdown` |
|
||||
| 共享工作 | Lead 已有的任务工具 | IDLE 扫描加队友原子认领 |
|
||||
| 工作目录 | 仓库 `WORKDIR` | 默认 `WORKDIR`,任务可选 worktree |
|
||||
| 结果上报 | 当前 Agent 输出 | 分开的 `result` 与 `idle_notification` |
|
||||
| 控制 | 无 | 类型化关机与计划审批协议 |
|
||||
| 执行约束 | 无团队约束 | 必需计划会锁住修改型工具 |
|
||||
|
||||
---
|
||||
|
||||
@@ -252,27 +402,29 @@ cd learn-claude-code
|
||||
python s15_agent_teams/code.py
|
||||
```
|
||||
|
||||
先输入一个自然需求:
|
||||
输入一个自然需求:
|
||||
|
||||
```text
|
||||
请重构这个示例后端,分别整理配置加载、认证逻辑和测试,
|
||||
保持现有接口兼容,并确保测试通过。
|
||||
把后端重构拆到共享任务板,在依赖允许时并行完成配置、认证和测试。
|
||||
认证任务使用 worktree,保持现有接口,并在最后汇总结果。
|
||||
```
|
||||
|
||||
看到 Lead 给出分工后,再回复:
|
||||
Lead 提出团队方案后回复:
|
||||
|
||||
```text
|
||||
开始吧
|
||||
```
|
||||
|
||||
观察终端中的 `spawned`、`result`、`idle_notification`、`plan_approval_*` 和 `shutdown_*` 事件,以及 `.mailboxes/` 中消息写入和消费的过程。
|
||||
观察 `.tasks/` 如何从 `pending` 进入 `in_progress` 和 `completed`,`.mailboxes/` 如何投递 `result` 与 `idle_notification`,以及 `.worktrees/` 是否只为绑定的任务创建。还可以检查直接消息是否先于任务板扫描,以及 `complete_task` 失败后队友的工作目录是否保持不变。
|
||||
|
||||
---
|
||||
|
||||
## 接下来
|
||||
|
||||
s15 中,Lead 仍然要明确告诉每个队友做什么。下一章把共享任务看板交给空闲队友,让它们自己发现并认领可执行任务。
|
||||
团队运行时现在可以处理委派、共享任务认领和可选工作目录,但工具仍然直接定义在 Python 代码里。
|
||||
|
||||
下一章:[s16 Autonomous Agents](../s16_autonomous_agents/)。
|
||||
下一章通过标准的发现与调用协议接入外部工具。
|
||||
|
||||
<!-- translation-sync: zh@v2, en@v2, ja@v2 -->
|
||||
下一章:[s16 MCP Tools](../s16_mcp_plugin/)。
|
||||
|
||||
<!-- translation-sync: zh@v3, en@v3, ja@v3 -->
|
||||
|
||||
@@ -8,6 +8,8 @@ Need: pip install anthropic python-dotenv + .env with ANTHROPIC_API_KEY
|
||||
Changes from s14:
|
||||
- MessageBus: thread-safe, file-backed mailboxes (.mailboxes/*.jsonl)
|
||||
- Persistent teammate loops with WORK and IDLE states
|
||||
- Idle teammates discover and atomically claim ready tasks
|
||||
- Task-bound Git worktrees give teammate file operations separate checkouts
|
||||
- Runtime delivery of teammate results and idle notifications to Lead
|
||||
- Typed shutdown and plan-approval protocols with request_id matching
|
||||
- Plan approval gates bash and write_file until Lead approves
|
||||
@@ -46,6 +48,12 @@ MODEL = os.environ["MODEL_ID"]
|
||||
|
||||
TASKS_DIR = WORKDIR / ".tasks"
|
||||
TASKS_DIR.mkdir(exist_ok=True)
|
||||
TASKS_ROOT = TASKS_DIR.resolve()
|
||||
task_lock = threading.RLock()
|
||||
|
||||
# owner -> {"task_id": str, "cwd": Path}. A teammate gets one assignment at
|
||||
# a time, and every filesystem tool resolves its cwd through this registry.
|
||||
teammate_assignments: dict[str, dict[str, object]] = {}
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -56,10 +64,19 @@ class Task:
|
||||
status: str # pending | in_progress | completed
|
||||
owner: str | None
|
||||
blockedBy: list[str]
|
||||
worktree: str | None = None
|
||||
|
||||
|
||||
def _task_path(task_id: str) -> Path:
|
||||
return TASKS_DIR / f"{task_id}.json"
|
||||
if not isinstance(task_id, str) or not task_id:
|
||||
raise ValueError("Task ID must be a non-empty string")
|
||||
if Path(task_id).name != task_id or task_id in {".", ".."}:
|
||||
raise ValueError(f"Invalid task ID: {task_id!r}")
|
||||
path = (TASKS_DIR / f"{task_id}.json").resolve()
|
||||
if (not TASKS_ROOT.is_relative_to(WORKDIR.resolve())
|
||||
or not path.is_relative_to(TASKS_ROOT)):
|
||||
raise ValueError(f"Invalid task ID: {task_id!r}")
|
||||
return path
|
||||
|
||||
|
||||
def create_task(subject: str, description: str = "",
|
||||
@@ -75,16 +92,21 @@ def create_task(subject: str, description: str = "",
|
||||
|
||||
|
||||
def save_task(task: Task):
|
||||
_task_path(task.id).write_text(json.dumps(asdict(task), indent=2))
|
||||
with task_lock:
|
||||
_task_path(task.id).write_text(json.dumps(asdict(task), indent=2))
|
||||
|
||||
|
||||
def load_task(task_id: str) -> Task:
|
||||
return Task(**json.loads(_task_path(task_id).read_text()))
|
||||
with task_lock:
|
||||
return Task(**json.loads(_task_path(task_id).read_text()))
|
||||
|
||||
|
||||
def list_tasks() -> list[Task]:
|
||||
return [Task(**json.loads(p.read_text()))
|
||||
for p in sorted(TASKS_DIR.glob("task_*.json"))]
|
||||
with task_lock:
|
||||
if not TASKS_ROOT.is_relative_to(WORKDIR.resolve()):
|
||||
raise ValueError("Tasks directory escapes workspace")
|
||||
return [load_task(path.stem)
|
||||
for path in sorted(TASKS_DIR.glob("task_*.json"))]
|
||||
|
||||
|
||||
def get_task(task_id: str) -> str:
|
||||
@@ -98,36 +120,76 @@ def can_start(task_id: str) -> bool:
|
||||
Missing dependencies are treated as blocked."""
|
||||
task = load_task(task_id)
|
||||
for dep_id in task.blockedBy:
|
||||
if not _task_path(dep_id).exists():
|
||||
try:
|
||||
dep_path = _task_path(dep_id)
|
||||
except ValueError:
|
||||
return False
|
||||
if not dep_path.exists():
|
||||
return False
|
||||
if load_task(dep_id).status != "completed":
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def _owner_in_progress(owner: str) -> Task | None:
|
||||
return next((task for task in list_tasks()
|
||||
if task.status == "in_progress" and task.owner == owner), None)
|
||||
|
||||
|
||||
def _incomplete_dependencies(task: Task) -> list[str]:
|
||||
incomplete = []
|
||||
for dep_id in task.blockedBy:
|
||||
try:
|
||||
dep_path = _task_path(dep_id)
|
||||
except ValueError:
|
||||
incomplete.append(dep_id)
|
||||
continue
|
||||
if not dep_path.exists() or load_task(dep_id).status != "completed":
|
||||
incomplete.append(dep_id)
|
||||
return incomplete
|
||||
|
||||
|
||||
def claim_task(task_id: str, owner: str = "agent") -> str:
|
||||
task = load_task(task_id)
|
||||
if task.status != "pending":
|
||||
return f"Task {task_id} is {task.status}, cannot claim"
|
||||
if not can_start(task_id):
|
||||
deps = [d for d in task.blockedBy
|
||||
if not _task_path(d).exists() or load_task(d).status != "completed"]
|
||||
return f"Blocked by: {deps}"
|
||||
task.owner = owner
|
||||
task.status = "in_progress"
|
||||
save_task(task)
|
||||
"""Atomically claim one task and bind the owner's filesystem cwd."""
|
||||
with task_lock:
|
||||
task = load_task(task_id)
|
||||
if task.status != "pending":
|
||||
return f"Task {task_id} is {task.status}, cannot claim"
|
||||
if task.owner:
|
||||
return f"Task {task_id} is already owned by {task.owner}"
|
||||
current = _owner_in_progress(owner)
|
||||
if current:
|
||||
return (f"Owner {owner} must complete {current.id} before "
|
||||
"claiming another task")
|
||||
if not can_start(task_id):
|
||||
return f"Blocked by: {_incomplete_dependencies(task)}"
|
||||
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}
|
||||
print(f" \033[36m[claim] {task.subject} → in_progress (owner: {owner})\033[0m")
|
||||
return f"Claimed {task.id} ({task.subject})"
|
||||
|
||||
|
||||
def complete_task(task_id: str) -> str:
|
||||
task = load_task(task_id)
|
||||
if task.status != "in_progress":
|
||||
return f"Task {task_id} is {task.status}, cannot complete"
|
||||
task.status = "completed"
|
||||
save_task(task)
|
||||
unblocked = [t.subject for t in list_tasks()
|
||||
if t.status == "pending" and t.blockedBy and can_start(t.id)]
|
||||
def complete_task(task_id: str, owner: str = "agent") -> str:
|
||||
"""Complete an assignment only when the caller owns it."""
|
||||
with task_lock:
|
||||
task = load_task(task_id)
|
||||
if task.status != "in_progress":
|
||||
return f"Task {task_id} is {task.status}, cannot complete"
|
||||
if task.owner != owner:
|
||||
return (f"Task {task_id} is owned by {task.owner}, "
|
||||
f"not {owner}; cannot complete")
|
||||
task.status = "completed"
|
||||
save_task(task)
|
||||
assignment = teammate_assignments.get(owner)
|
||||
if assignment and assignment.get("task_id") == task_id:
|
||||
teammate_assignments.pop(owner, None)
|
||||
unblocked = [t.subject for t in list_tasks()
|
||||
if t.status == "pending" and t.blockedBy and can_start(t.id)]
|
||||
print(f" \033[32m[complete] {task.subject} ✓\033[0m")
|
||||
msg = f"Completed {task.id} ({task.subject})"
|
||||
if unblocked:
|
||||
@@ -136,6 +198,252 @@ def complete_task(task_id: str) -> str:
|
||||
return msg
|
||||
|
||||
|
||||
# ── Task-bound Worktrees ──
|
||||
|
||||
WORKTREES_DIR = WORKDIR / ".worktrees"
|
||||
WORKTREES_DIR.mkdir(exist_ok=True)
|
||||
WORKTREES_ROOT = WORKTREES_DIR.resolve()
|
||||
VALID_WORKTREE_NAME = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$")
|
||||
|
||||
|
||||
def validate_worktree_name(name: str) -> str | None:
|
||||
if not isinstance(name, str) or not VALID_WORKTREE_NAME.fullmatch(name):
|
||||
return ("worktree name must be 1-64 letters, digits, dots, "
|
||||
"underscores, or dashes, and start with a letter or digit")
|
||||
if name in {".", ".."} or ".." in name:
|
||||
return "worktree name cannot contain '..'"
|
||||
return None
|
||||
|
||||
|
||||
def _worktree_path(name: str) -> Path:
|
||||
path = (WORKTREES_DIR / name).resolve()
|
||||
if (not WORKTREES_ROOT.is_relative_to(WORKDIR.resolve())
|
||||
or not path.is_relative_to(WORKTREES_ROOT)
|
||||
or path == WORKTREES_ROOT):
|
||||
raise ValueError(f"Worktree path escapes directory: {name!r}")
|
||||
return path
|
||||
|
||||
|
||||
def _worktree_branch(name: str) -> str:
|
||||
return f"wt/{name}"
|
||||
|
||||
|
||||
def run_git(args: list[str], cwd: Path | None = None) -> tuple[bool, str]:
|
||||
"""Run Git without shell interpolation and return (ok, combined output)."""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["git", *args], cwd=cwd or WORKDIR,
|
||||
capture_output=True, text=True, timeout=30,
|
||||
)
|
||||
except (OSError, subprocess.TimeoutExpired) as exc:
|
||||
return False, f"{type(exc).__name__}: {exc}"
|
||||
output = (result.stdout + result.stderr).strip()
|
||||
return result.returncode == 0, output[:5000] or "(no output)"
|
||||
|
||||
|
||||
def _registered_worktrees() -> tuple[dict[Path, dict[str, str]], str | None]:
|
||||
ok, output = run_git(["worktree", "list", "--porcelain"])
|
||||
if not ok:
|
||||
return {}, f"cannot read Git worktree registry: {output}"
|
||||
entries: dict[Path, dict[str, str]] = {}
|
||||
current: dict[str, str] = {}
|
||||
for line in output.splitlines() + [""]:
|
||||
if not line:
|
||||
raw_path = current.get("worktree")
|
||||
if raw_path:
|
||||
entries[Path(raw_path).resolve()] = current
|
||||
current = {}
|
||||
continue
|
||||
key, _, value = line.partition(" ")
|
||||
current[key] = value
|
||||
return entries, None
|
||||
|
||||
|
||||
def _registered_worktree(name: str) -> tuple[Path | None, str | None]:
|
||||
try:
|
||||
path = _worktree_path(name)
|
||||
except ValueError as exc:
|
||||
return None, str(exc)
|
||||
entries, error = _registered_worktrees()
|
||||
if error:
|
||||
return None, error
|
||||
if path not in entries:
|
||||
return None, f"worktree '{name}' is not registered with Git"
|
||||
if not path.is_dir():
|
||||
return None, f"worktree '{name}' is missing at {path}"
|
||||
expected_branch = f"refs/heads/{_worktree_branch(name)}"
|
||||
if entries[path].get("branch") != expected_branch:
|
||||
return None, (f"worktree '{name}' is not registered on expected "
|
||||
f"branch '{_worktree_branch(name)}'")
|
||||
return path, None
|
||||
|
||||
|
||||
def task_worktree_cwd(task: Task) -> tuple[Path, str | None]:
|
||||
"""Resolve a task cwd, failing closed for broken worktree bindings."""
|
||||
if not task.worktree:
|
||||
return WORKDIR, None
|
||||
path, error = _registered_worktree(task.worktree)
|
||||
return (path or WORKDIR), error
|
||||
|
||||
|
||||
def assignment_cwd(owner: str) -> Path:
|
||||
with task_lock:
|
||||
assignment = teammate_assignments.get(owner)
|
||||
if not assignment:
|
||||
if _owner_in_progress(owner):
|
||||
raise ValueError(f"Missing assignment metadata for {owner}")
|
||||
return WORKDIR
|
||||
task = load_task(str(assignment["task_id"]))
|
||||
if task.status != "in_progress" or task.owner != owner:
|
||||
raise ValueError(f"Assignment for {owner} is no longer active")
|
||||
cwd, error = task_worktree_cwd(task)
|
||||
if error:
|
||||
raise ValueError(error)
|
||||
if cwd.resolve() != Path(assignment["cwd"]).resolve():
|
||||
raise ValueError(f"Assignment cwd changed for task {task.id}")
|
||||
return cwd
|
||||
|
||||
|
||||
def release_teammate_assignment(owner: str):
|
||||
"""Return abandoned teammate work to the task board on thread exit."""
|
||||
with task_lock:
|
||||
try:
|
||||
task = _owner_in_progress(owner)
|
||||
if task:
|
||||
task.status = "pending"
|
||||
task.owner = None
|
||||
save_task(task)
|
||||
finally:
|
||||
teammate_assignments.pop(owner, None)
|
||||
|
||||
|
||||
def create_worktree(name: str, task_id: str) -> str:
|
||||
"""Create and bind a dedicated worktree after all inputs validate."""
|
||||
error = validate_worktree_name(name)
|
||||
if error:
|
||||
return f"Error: {error}"
|
||||
try:
|
||||
path = _worktree_path(name)
|
||||
task_path = _task_path(task_id)
|
||||
except ValueError as exc:
|
||||
return f"Error: {exc}"
|
||||
branch = _worktree_branch(name)
|
||||
|
||||
with task_lock:
|
||||
if not task_path.exists():
|
||||
return f"Error: Task {task_id} not found"
|
||||
task = load_task(task_id)
|
||||
if task.status != "pending" or task.owner is not None:
|
||||
return f"Error: Task {task_id} must be pending and unowned"
|
||||
if task.worktree:
|
||||
return f"Error: Task {task_id} already uses worktree '{task.worktree}'"
|
||||
if any(t.worktree == name for t in list_tasks() if t.id != task_id):
|
||||
return f"Error: Worktree '{name}' is already bound to another task"
|
||||
if path.exists():
|
||||
return f"Error: Worktree path already exists: {path}"
|
||||
|
||||
ok, root = run_git(["rev-parse", "--show-toplevel"])
|
||||
if not ok or Path(root).resolve() != WORKDIR.resolve():
|
||||
return "Error: Working directory must be the root of a Git repository"
|
||||
ok, branch_check = run_git(["check-ref-format", "--branch", branch])
|
||||
if not ok:
|
||||
return f"Error: Invalid worktree branch '{branch}': {branch_check}"
|
||||
exists, _ = run_git(["show-ref", "--verify", "--quiet",
|
||||
f"refs/heads/{branch}"])
|
||||
if exists:
|
||||
return f"Error: Branch '{branch}' already exists"
|
||||
entries, registry_error = _registered_worktrees()
|
||||
if registry_error:
|
||||
return f"Error: {registry_error}"
|
||||
if path in entries:
|
||||
return f"Error: Worktree path is already registered: {path}"
|
||||
|
||||
ok, result = run_git(["worktree", "add", "-b", branch,
|
||||
str(path), "HEAD"])
|
||||
if not ok:
|
||||
entries, registry_error = _registered_worktrees()
|
||||
branch_exists, _ = run_git(
|
||||
["show-ref", "--verify", "--quiet", f"refs/heads/{branch}"]
|
||||
)
|
||||
artifacts = []
|
||||
if path.exists():
|
||||
artifacts.append(f"checkout path '{path}'")
|
||||
if registry_error is None and path in entries:
|
||||
artifacts.append("registered Git worktree")
|
||||
if branch_exists:
|
||||
artifacts.append(f"branch '{branch}'")
|
||||
if artifacts:
|
||||
return (
|
||||
"Partial operation: git worktree add reported an error "
|
||||
f"after leaving {', '.join(artifacts)}. Task {task_id} "
|
||||
"remains unbound and no Git data was deleted. Run "
|
||||
f"`git worktree list`, inspect '{path}' and '{branch}', "
|
||||
"then keep or remove those artifacts manually after "
|
||||
f"preserving any work. Git error: {result}"
|
||||
)
|
||||
return f"Git error: {result}"
|
||||
|
||||
try:
|
||||
task.worktree = name
|
||||
save_task(task)
|
||||
except Exception as exc:
|
||||
return (f"Partial success: Worktree '{name}' was created at "
|
||||
f"{path} on branch '{branch}', but task binding failed: "
|
||||
f"{exc}. Git data was retained for manual recovery.")
|
||||
|
||||
print(f" \033[33m[worktree] created: {name} at {path}\033[0m")
|
||||
return f"Worktree '{name}' created at {path} for task {task_id}"
|
||||
|
||||
|
||||
def remove_worktree(name: str, discard_changes: bool = False) -> str:
|
||||
"""Remove a registered checkout while always retaining its branch."""
|
||||
error = validate_worktree_name(name)
|
||||
if error:
|
||||
return f"Error: {error}"
|
||||
|
||||
with task_lock:
|
||||
path, error = _registered_worktree(name)
|
||||
if error:
|
||||
return f"Error: {error}"
|
||||
bound = [task for task in list_tasks() if task.worktree == name]
|
||||
if not bound:
|
||||
return f"Error: Worktree '{name}' is not bound to a task"
|
||||
active = [task for task in bound if task.status != "completed"]
|
||||
if active:
|
||||
return (f"Error: Worktree '{name}' is bound to active task "
|
||||
f"{active[0].id}; complete it before removal")
|
||||
|
||||
ok, status = run_git(
|
||||
["status", "--porcelain", "--ignored"], cwd=path
|
||||
)
|
||||
if not ok:
|
||||
return f"Error: Cannot verify worktree '{name}' status: {status}"
|
||||
if status != "(no output)" and not discard_changes:
|
||||
changed = len([line for line in status.splitlines() if line.strip()])
|
||||
return (f"Error: Worktree '{name}' has {changed} uncommitted "
|
||||
"change(s); preserve or discard them manually")
|
||||
|
||||
args = ["worktree", "remove"]
|
||||
if discard_changes:
|
||||
args.append("--force")
|
||||
args.append(str(path))
|
||||
ok, result = run_git(args)
|
||||
if not ok:
|
||||
return f"Git error: {result}"
|
||||
|
||||
try:
|
||||
for task in bound:
|
||||
task.worktree = None
|
||||
save_task(task)
|
||||
except Exception as exc:
|
||||
return (f"Partial success: Worktree '{name}' was removed and "
|
||||
f"branch '{_worktree_branch(name)}' retained, but task "
|
||||
f"unbinding failed: {exc}. Manual recovery is required.")
|
||||
|
||||
print(f" \033[33m[worktree] removed: {name}; branch retained\033[0m")
|
||||
return f"Worktree '{name}' removed; branch '{_worktree_branch(name)}' retained"
|
||||
|
||||
|
||||
# ── Prompt Assembly (from s10, synced) ──
|
||||
|
||||
PROMPT_SECTIONS = {
|
||||
@@ -144,13 +452,18 @@ PROMPT_SECTIONS = {
|
||||
"get_task, create_task, list_tasks, claim_task, complete_task, "
|
||||
"schedule_cron, list_crons, cancel_cron, "
|
||||
"spawn_teammate, send_message, request_shutdown, "
|
||||
"request_plan, review_plan.",
|
||||
"request_plan, review_plan, create_worktree, remove_worktree.",
|
||||
"teams": (
|
||||
"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. After confirmation, delegate "
|
||||
"independent work, react to team events delivered by the runtime, and "
|
||||
"shut teammates down when coordination is complete."
|
||||
"independent work by creating a Task for each parallel change, then "
|
||||
"create a task-bound worktree only when a separate working directory "
|
||||
"would prevent conflicting edits. A teammate must complete its current "
|
||||
"Task before claiming another. A worktree changes tool default cwd "
|
||||
"only; it is not a sandbox. The remove_worktree tool removes only clean "
|
||||
"checkouts and never discards changes. React to team events delivered by the "
|
||||
"runtime, and shut teammates down when coordination is complete."
|
||||
),
|
||||
"workspace": f"Working directory: {WORKDIR}",
|
||||
"memory": "Relevant memories are injected below when available.",
|
||||
@@ -183,27 +496,32 @@ def get_system_prompt(context: dict) -> str:
|
||||
|
||||
# ── Tools ──
|
||||
|
||||
def safe_path(p: str) -> Path:
|
||||
path = (WORKDIR / p).resolve()
|
||||
if not path.is_relative_to(WORKDIR):
|
||||
def safe_path(p: str, cwd: Path | None = None) -> Path:
|
||||
base = (cwd or WORKDIR).resolve()
|
||||
path = (base / p).resolve()
|
||||
if not path.is_relative_to(base):
|
||||
raise ValueError(f"Path escapes workspace: {p}")
|
||||
return path
|
||||
|
||||
|
||||
def run_bash(command: str, run_in_background: bool = False) -> str:
|
||||
def run_bash(command: str, run_in_background: bool = False,
|
||||
cwd: Path | None = None) -> str:
|
||||
# run_in_background is handled by agent_loop dispatch, not here
|
||||
try:
|
||||
r = subprocess.run(command, shell=True, cwd=WORKDIR,
|
||||
r = subprocess.run(command, shell=True, cwd=cwd or WORKDIR,
|
||||
capture_output=True, text=True, timeout=120)
|
||||
out = (r.stdout + r.stderr).strip()
|
||||
return out[:50000] if out else "(no output)"
|
||||
except subprocess.TimeoutExpired:
|
||||
return "Error: Timeout (120s)"
|
||||
except OSError as exc:
|
||||
return f"Error: {type(exc).__name__}: {exc}"
|
||||
|
||||
|
||||
def run_read(path: str, limit: int | None = None) -> str:
|
||||
def run_read(path: str, limit: int | None = None,
|
||||
cwd: Path | None = None) -> str:
|
||||
try:
|
||||
lines = safe_path(path).read_text().splitlines()
|
||||
lines = safe_path(path, cwd).read_text().splitlines()
|
||||
if limit and limit < len(lines):
|
||||
lines = lines[:limit] + [f"... ({len(lines) - limit} more lines)"]
|
||||
return "\n".join(lines)
|
||||
@@ -211,9 +529,9 @@ def run_read(path: str, limit: int | None = None) -> str:
|
||||
return f"Error: {e}"
|
||||
|
||||
|
||||
def run_write(path: str, content: str) -> str:
|
||||
def run_write(path: str, content: str, cwd: Path | None = None) -> str:
|
||||
try:
|
||||
fp = safe_path(path)
|
||||
fp = safe_path(path, cwd)
|
||||
fp.parent.mkdir(parents=True, exist_ok=True)
|
||||
fp.write_text(content)
|
||||
return f"Wrote {len(content)} bytes to {path}"
|
||||
@@ -241,24 +559,37 @@ def run_list_tasks() -> str:
|
||||
"completed": "✓"}.get(t.status, "?")
|
||||
deps = f" (blockedBy: {', '.join(t.blockedBy)})" if t.blockedBy else ""
|
||||
owner = f" [{t.owner}]" if t.owner else ""
|
||||
worktree = f" (worktree: {t.worktree})" if t.worktree else ""
|
||||
lines.append(f" {icon} {t.id}: {t.subject} "
|
||||
f"[{t.status}]{owner}{deps}")
|
||||
f"[{t.status}]{owner}{deps}{worktree}")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def run_get_task(task_id: str) -> str:
|
||||
try:
|
||||
return get_task(task_id)
|
||||
except ValueError as exc:
|
||||
return f"Error: {exc}"
|
||||
except FileNotFoundError:
|
||||
return f"Error: Task {task_id} not found"
|
||||
|
||||
|
||||
def run_claim_task(task_id: str) -> str:
|
||||
return claim_task(task_id, owner="agent")
|
||||
try:
|
||||
return claim_task(task_id, owner="agent")
|
||||
except ValueError as exc:
|
||||
return f"Error: {exc}"
|
||||
except FileNotFoundError:
|
||||
return f"Error: Task {task_id} not found"
|
||||
|
||||
|
||||
def run_complete_task(task_id: str) -> str:
|
||||
return complete_task(task_id)
|
||||
try:
|
||||
return complete_task(task_id, owner="agent")
|
||||
except ValueError as exc:
|
||||
return f"Error: {exc}"
|
||||
except FileNotFoundError:
|
||||
return f"Error: Task {task_id} not found"
|
||||
|
||||
|
||||
# ── Background Tasks (from s13, synced) ──
|
||||
@@ -301,6 +632,8 @@ def execute_tool(block) -> str:
|
||||
"request_shutdown": run_request_shutdown,
|
||||
"request_plan": run_request_plan,
|
||||
"review_plan": run_review_plan,
|
||||
"create_worktree": run_create_worktree,
|
||||
"remove_worktree": run_remove_worktree,
|
||||
}.get(block.name)
|
||||
if handler:
|
||||
return handler(**block.input)
|
||||
@@ -607,6 +940,7 @@ MAILBOX_DIR = WORKDIR / ".mailboxes"
|
||||
MAILBOX_DIR.mkdir(exist_ok=True)
|
||||
MAILBOX_ROOT = MAILBOX_DIR.resolve()
|
||||
VALID_AGENT_NAME = re.compile(r"^[A-Za-z0-9_-]{1,64}$")
|
||||
RESERVED_TEAMMATE_NAMES = {"lead", "agent"}
|
||||
|
||||
|
||||
def is_valid_agent_name(name: str) -> bool:
|
||||
@@ -852,6 +1186,37 @@ def _teammate_send_message(from_name: str, to: str, content: str) -> str:
|
||||
return f"Sent to {to}"
|
||||
|
||||
|
||||
# ── Autonomous Task Discovery ──
|
||||
|
||||
IDLE_SCAN_INTERVAL = 2.0
|
||||
|
||||
|
||||
def scan_unclaimed_tasks() -> list[Task]:
|
||||
"""Return ready tasks whose optional worktree binding is usable."""
|
||||
with task_lock:
|
||||
ready = []
|
||||
for task in list_tasks():
|
||||
if (task.status != "pending" or task.owner is not None
|
||||
or not can_start(task.id)):
|
||||
continue
|
||||
_, error = task_worktree_cwd(task)
|
||||
if not error:
|
||||
ready.append(task)
|
||||
return ready
|
||||
|
||||
|
||||
def claim_next_task(name: str) -> Task | None:
|
||||
"""Claim the first still-available task, never a second assignment."""
|
||||
with task_lock:
|
||||
if _owner_in_progress(name):
|
||||
return None
|
||||
for task in scan_unclaimed_tasks():
|
||||
result = claim_task(task.id, owner=name)
|
||||
if result.startswith("Claimed "):
|
||||
return load_task(task.id)
|
||||
return None
|
||||
|
||||
|
||||
# ── Teammate Thread ──
|
||||
|
||||
def spawn_teammate_thread(name: str, role: str, prompt: str) -> str:
|
||||
@@ -859,19 +1224,60 @@ def spawn_teammate_thread(name: str, role: str, prompt: str) -> str:
|
||||
if not is_valid_agent_name(name):
|
||||
return ("Invalid teammate name: use 1-64 letters, digits, "
|
||||
"underscores, or dashes")
|
||||
if name.lower() in RESERVED_TEAMMATE_NAMES:
|
||||
return f"Invalid teammate name: '{name}' is reserved by the runtime"
|
||||
with team_lock:
|
||||
if name in active_teammates:
|
||||
if any(existing.casefold() == name.casefold()
|
||||
for existing in active_teammates):
|
||||
return f"Teammate '{name}' already exists"
|
||||
active_teammates[name] = "working"
|
||||
plan_gates[name] = "not_required"
|
||||
|
||||
system = (f"You are '{name}', a {role}. "
|
||||
"Use tools to complete assigned work. "
|
||||
"Use tools to complete assigned work. You can list, claim, and "
|
||||
"complete tasks from the shared board. For a bound task, the "
|
||||
"runtime defaults bash, read_file, and write_file to its "
|
||||
"worktree; otherwise they use the shared WORKDIR. This default "
|
||||
"cwd is not a sandbox. "
|
||||
"When asked for a plan, call submit_plan before bash or "
|
||||
"write_file and wait for approval. End each assignment with a "
|
||||
"concise result; the runtime delivers it to Lead.")
|
||||
|
||||
def run():
|
||||
def run_loop():
|
||||
def current_cwd() -> tuple[Path | None, str | None]:
|
||||
try:
|
||||
return assignment_cwd(name), None
|
||||
except (FileNotFoundError, ValueError) as exc:
|
||||
return None, f"Error: Invalid task assignment: {exc}"
|
||||
|
||||
def teammate_bash(command: str) -> str:
|
||||
cwd, error = current_cwd()
|
||||
return error or run_bash(command, cwd=cwd)
|
||||
|
||||
def teammate_read(path: str) -> str:
|
||||
cwd, error = current_cwd()
|
||||
return error or run_read(path, cwd=cwd)
|
||||
|
||||
def teammate_write(path: str, content: str) -> str:
|
||||
cwd, error = current_cwd()
|
||||
return error or run_write(path, content, cwd=cwd)
|
||||
|
||||
def teammate_claim(task_id: str) -> str:
|
||||
try:
|
||||
return claim_task(task_id, owner=name)
|
||||
except ValueError as exc:
|
||||
return f"Error: {exc}"
|
||||
except FileNotFoundError:
|
||||
return f"Error: Task {task_id} not found"
|
||||
|
||||
def teammate_complete(task_id: str) -> str:
|
||||
try:
|
||||
return complete_task(task_id, owner=name)
|
||||
except ValueError as exc:
|
||||
return f"Error: {exc}"
|
||||
except FileNotFoundError:
|
||||
return f"Error: Task {task_id} not found"
|
||||
|
||||
messages = [{"role": "user", "content": prompt}]
|
||||
sub_tools = [
|
||||
{"name": "bash", "description": "Run a shell command.",
|
||||
@@ -898,12 +1304,31 @@ def spawn_teammate_thread(name: str, role: str, prompt: str) -> str:
|
||||
"input_schema": {"type": "object",
|
||||
"properties": {"plan": {"type": "string"}},
|
||||
"required": ["plan"]}},
|
||||
{"name": "list_tasks",
|
||||
"description": "List tasks on the shared board.",
|
||||
"input_schema": {"type": "object", "properties": {},
|
||||
"required": []}},
|
||||
{"name": "claim_task",
|
||||
"description": "Claim a ready task from the shared board.",
|
||||
"input_schema": {"type": "object",
|
||||
"properties": {"task_id": {"type": "string"}},
|
||||
"required": ["task_id"]}},
|
||||
{"name": "complete_task",
|
||||
"description": "Complete the task owned by this teammate.",
|
||||
"input_schema": {"type": "object",
|
||||
"properties": {"task_id": {"type": "string"}},
|
||||
"required": ["task_id"]}},
|
||||
]
|
||||
sub_handlers = {
|
||||
"bash": run_bash, "read_file": run_read, "write_file": run_write,
|
||||
"bash": teammate_bash,
|
||||
"read_file": teammate_read,
|
||||
"write_file": teammate_write,
|
||||
"send_message": lambda to, content: _teammate_send_message(
|
||||
name, to, content),
|
||||
"submit_plan": lambda plan: _teammate_submit_plan(name, plan),
|
||||
"list_tasks": run_list_tasks,
|
||||
"claim_task": teammate_claim,
|
||||
"complete_task": teammate_complete,
|
||||
}
|
||||
|
||||
def handle_messages(inbox: list[dict]) -> bool:
|
||||
@@ -979,20 +1404,60 @@ def spawn_teammate_thread(name: str, role: str, prompt: str) -> str:
|
||||
"idle_notification")
|
||||
|
||||
while True:
|
||||
inbox = BUS.wait_for_messages(name)
|
||||
should_stop = handle_messages(inbox)
|
||||
if should_stop or messages[-1]["role"] == "user":
|
||||
break
|
||||
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
|
||||
|
||||
with team_lock:
|
||||
active_teammates.pop(name, None)
|
||||
plan_gates.pop(name, None)
|
||||
plan_request_ids.pop(name, None)
|
||||
print(f" \033[32m[teammate] {name} finished\033[0m")
|
||||
task = claim_next_task(name)
|
||||
if not task:
|
||||
continue
|
||||
try:
|
||||
cwd = str(assignment_cwd(name))
|
||||
except (FileNotFoundError, ValueError) as exc:
|
||||
cwd = f"unavailable ({exc})"
|
||||
messages.append({
|
||||
"role": "user",
|
||||
"content": (
|
||||
f"[Auto-claimed task {task.id}] {task.subject}\n"
|
||||
f"{task.description}\nWork directory: {cwd}"
|
||||
),
|
||||
})
|
||||
print(f" \033[32m[idle] {name} claimed "
|
||||
f"{task.id}: {task.subject}\033[0m")
|
||||
break
|
||||
|
||||
def run():
|
||||
try:
|
||||
run_loop()
|
||||
except Exception as exc:
|
||||
try:
|
||||
BUS.send(name, "lead", f"{type(exc).__name__}: {exc}", "error")
|
||||
except Exception:
|
||||
pass
|
||||
finally:
|
||||
try:
|
||||
release_teammate_assignment(name)
|
||||
except Exception as exc:
|
||||
try:
|
||||
BUS.send(
|
||||
name, "lead",
|
||||
f"Assignment cleanup failed: {type(exc).__name__}: {exc}",
|
||||
"error",
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
with team_lock:
|
||||
active_teammates.pop(name, None)
|
||||
plan_gates.pop(name, None)
|
||||
plan_request_ids.pop(name, None)
|
||||
print(f" \033[32m[teammate] {name} finished\033[0m")
|
||||
|
||||
threading.Thread(target=run, daemon=True).start()
|
||||
print(f" \033[36m[teammate] {name} spawned as {role}\033[0m")
|
||||
return f"Teammate '{name}' spawned as {role}"
|
||||
return f"Teammate '{name}' spawned as {role} (autonomous)"
|
||||
|
||||
|
||||
# ── Lead Team Tools ──
|
||||
@@ -1055,6 +1520,15 @@ def run_review_plan(request_id: str, approve: bool,
|
||||
return f"Plan {state.status} ({request_id})"
|
||||
|
||||
|
||||
def run_create_worktree(name: str, task_id: str) -> str:
|
||||
return create_worktree(name, task_id)
|
||||
|
||||
|
||||
def run_remove_worktree(name: str) -> str:
|
||||
"""Model-facing cleanup never opts into destructive removal."""
|
||||
return remove_worktree(name)
|
||||
|
||||
|
||||
# ── Tool Definitions ──
|
||||
|
||||
TOOLS = [
|
||||
@@ -1160,6 +1634,30 @@ TOOLS = [
|
||||
"approve": {"type": "boolean"},
|
||||
"feedback": {"type": "string"}},
|
||||
"required": ["request_id", "approve"]}},
|
||||
{"name": "create_worktree",
|
||||
"description": "Create a task-bound Git worktree and dedicated branch.",
|
||||
"input_schema": {"type": "object",
|
||||
"properties": {"name": {
|
||||
"type": "string",
|
||||
"pattern": ("^(?!.*\\.\\.)[A-Za-z0-9]"
|
||||
"[A-Za-z0-9._-]{0,63}$"),
|
||||
"maxLength": 64,
|
||||
},
|
||||
"task_id": {"type": "string"}},
|
||||
"required": ["name", "task_id"],
|
||||
"additionalProperties": False}},
|
||||
{"name": "remove_worktree",
|
||||
"description": "Remove a clean task worktree while retaining its branch.",
|
||||
"input_schema": {"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"pattern": ("^(?!.*\\.\\.)[A-Za-z0-9]"
|
||||
"[A-Za-z0-9._-]{0,63}$"),
|
||||
"maxLength": 64,
|
||||
}},
|
||||
"required": ["name"],
|
||||
"additionalProperties": False}},
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -1,120 +1,107 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 470" font-family="system-ui, -apple-system, sans-serif">
|
||||
<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="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 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="10" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||
<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-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-green" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
|
||||
<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="470" fill="#fafbfc" rx="8"/>
|
||||
<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>
|
||||
|
||||
<!-- Title -->
|
||||
<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">Agent Teams — Lead Loop + Teammate Threads + MessageBus</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>
|
||||
|
||||
<!-- Legend -->
|
||||
<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">s10-s14 Preserved</text>
|
||||
<rect x="180" y="56" width="12" height="10" rx="2" fill="#ecfeff" stroke="#0891b2" stroke-width="1"/>
|
||||
<text x="198" y="66" fill="#0891b2" font-size="10" font-weight="600">s15 New</text>
|
||||
<rect x="280" y="56" width="12" height="10" rx="2" fill="#f0fdf4" stroke="#16a34a" stroke-width="1"/>
|
||||
<text x="298" y="66" fill="#16a34a" font-size="10" font-weight="600">Teammate</text>
|
||||
<rect x="395" y="56" width="12" height="10" rx="2" fill="#fffbeb" stroke="#d97706" stroke-width="1"/>
|
||||
<text x="413" y="66" fill="#d97706" font-size="10" font-weight="600">Protocol gate</text>
|
||||
<line x1="168" y1="113" x2="220" y2="113" stroke="#475569" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
|
||||
<!-- ===== Row 1: Lead Agent Loop ===== -->
|
||||
<rect x="28" y="90" width="70" height="40" rx="8" fill="#eef2ff" stroke="#4f46e5" stroke-width="1.5"/>
|
||||
<text x="63" y="114" fill="#4f46e5" font-size="8" font-weight="600" text-anchor="middle">cron_queue</text>
|
||||
<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/remove · plan review</text>
|
||||
|
||||
<line x1="98" y1="110" x2="112" y2="110" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
<line x1="520" y1="113" x2="570" y2="113" stroke="#d97706" stroke-width="1.5" marker-end="url(#arrow-amber)"/>
|
||||
|
||||
<rect x="115" y="90" width="72" height="40" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
|
||||
<text x="151" y="114" fill="#1e3a5f" font-size="10" font-weight="600" text-anchor="middle">messages</text>
|
||||
<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>
|
||||
|
||||
<line x1="187" y1="110" x2="201" y2="110" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
<!-- 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="204" y="86" width="86" height="48" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
|
||||
<text x="247" y="114" fill="#1e3a5f" font-size="9" font-weight="600" text-anchor="middle">prompt + cache</text>
|
||||
<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/<name>.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>
|
||||
|
||||
<line x1="290" y1="110" x2="304" y2="110" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
<!-- 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="307" y="86" width="74" height="48" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
|
||||
<text x="344" y="114" fill="#1e3a5f" font-size="9" font-weight="600" text-anchor="middle">LLM call</text>
|
||||
<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>
|
||||
|
||||
<line x1="381" y1="110" x2="395" y2="110" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
<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="398" y="80" width="336" height="60" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
|
||||
<text x="566" y="98" fill="#1e3a5f" font-size="10" font-weight="600" text-anchor="middle">TOOL DISPATCH</text>
|
||||
<text x="414" y="114" fill="#2563eb" font-size="8">bash · read · write · task(4) · cron(3)</text>
|
||||
<text x="414" y="128" fill="#0891b2" font-size="8" font-weight="700">★ spawn · send · shutdown · plan review</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>
|
||||
|
||||
<!-- Loop back -->
|
||||
<path d="M 734 110 L 748 110 L 748 150 L 63 150 L 63 130" fill="none" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)" stroke-dasharray="6,3"/>
|
||||
<!-- 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)"/>
|
||||
|
||||
<!-- ===== Spawn arrow: TOOLS bottom → MessageBus top ===== -->
|
||||
<line x1="560" y1="140" x2="560" y2="178" stroke="#0891b2" stroke-width="2" marker-end="url(#arrow-cyan)"/>
|
||||
<rect x="543" y="152" width="38" height="14" rx="3" fill="#ecfeff"/>
|
||||
<text x="562" y="163" fill="#0891b2" font-size="8" font-weight="600" text-anchor="middle">spawn</text>
|
||||
<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>
|
||||
|
||||
<!-- ===== Row 2: MessageBus ===== -->
|
||||
<rect x="60" y="178" width="640" height="34" rx="17" fill="#ecfeff" stroke="#0891b2" stroke-width="2"/>
|
||||
<text x="380" y="200" fill="#0e7490" font-size="11" font-weight="700" text-anchor="middle">MessageBus (.mailboxes/*.jsonl)</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>
|
||||
|
||||
<!-- ===== Row 3: Teammates ===== -->
|
||||
<line x1="170" y1="212" x2="170" y2="248" stroke="#16a34a" stroke-width="1.5" marker-end="url(#arrow-green)"/>
|
||||
<line x1="380" y1="212" x2="380" y2="248" stroke="#16a34a" stroke-width="1.5" marker-end="url(#arrow-green)"/>
|
||||
<line x1="590" y1="212" x2="590" y2="248" stroke="#16a34a" stroke-width="1.5" marker-end="url(#arrow-green)"/>
|
||||
<text x="124" y="226" fill="#16a34a" font-size="9" font-weight="600">receive</text>
|
||||
<text x="334" y="226" fill="#16a34a" font-size="9" font-weight="600">receive</text>
|
||||
<text x="544" y="226" fill="#16a34a" font-size="9" font-weight="600">receive</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">Default: 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>
|
||||
|
||||
<line x1="200" y1="248" x2="200" y2="212" stroke="#0891b2" stroke-width="1" stroke-dasharray="4,2" marker-end="url(#arrow-cyan)"/>
|
||||
<line x1="410" y1="248" x2="410" y2="212" stroke="#0891b2" stroke-width="1" stroke-dasharray="4,2" marker-end="url(#arrow-cyan)"/>
|
||||
<line x1="620" y1="248" x2="620" y2="212" stroke="#0891b2" stroke-width="1" stroke-dasharray="4,2" marker-end="url(#arrow-cyan)"/>
|
||||
<text x="214" y="241" fill="#0891b2" font-size="9" font-weight="600">send</text>
|
||||
<text x="424" y="241" fill="#0891b2" font-size="9" font-weight="600">send</text>
|
||||
<text x="634" y="241" fill="#0891b2" font-size="9" font-weight="600">send</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/<name></text>
|
||||
<text x="550" y="548" text-anchor="middle" fill="#a16207" font-size="8.5">separate checkout + retained wt/<name> 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="248" width="220" height="66" rx="8" fill="#f0fdf4" stroke="#16a34a" stroke-width="1.5"/>
|
||||
<text x="170" y="268" fill="#166534" font-size="10" font-weight="700" text-anchor="middle">Teammate: alice (Backend)</text>
|
||||
<text x="75" y="284" fill="#16a34a" font-size="8">inbox → LLM → bash/read/write/send</text>
|
||||
<text x="75" y="298" fill="#6b7280" font-size="8">WORK → result → IDLE → next message</text>
|
||||
|
||||
<rect x="270" y="248" width="220" height="66" rx="8" fill="#f0fdf4" stroke="#16a34a" stroke-width="1.5"/>
|
||||
<text x="380" y="268" fill="#166534" font-size="10" font-weight="700" text-anchor="middle">Teammate: bob (Frontend)</text>
|
||||
<text x="285" y="284" fill="#16a34a" font-size="8">Independent agent_loop, shared client</text>
|
||||
<text x="285" y="298" fill="#6b7280" font-size="8">Thread(daemon=True)</text>
|
||||
|
||||
<rect x="480" y="248" width="220" height="66" rx="8" fill="#f0fdf4" stroke="#16a34a" stroke-width="1.5"/>
|
||||
<text x="590" y="268" fill="#166534" font-size="10" font-weight="700" text-anchor="middle">Teammate: charlie (QA)</text>
|
||||
<text x="495" y="284" fill="#16a34a" font-size="8">Cannot spawn other teammates</text>
|
||||
<text x="495" y="298" fill="#6b7280" font-size="8">spawn → work → result → idle</text>
|
||||
|
||||
<!-- ===== Row 4: Plan approval gate ===== -->
|
||||
<path d="M 60 360 L 10 360 L 10 195 L 60 195" fill="none" stroke="#d97706" stroke-width="1.5" marker-end="url(#arrow-amber)" stroke-dasharray="5,3"/>
|
||||
<rect x="20" y="318" width="126" height="18" rx="4" fill="#fffbeb" stroke="#f59e0b" stroke-width="1"/>
|
||||
<text x="83" y="331" fill="#d97706" font-size="10" font-weight="700" text-anchor="middle">plan_request</text>
|
||||
|
||||
<rect x="60" y="340" width="640" height="50" rx="6" fill="#fffbeb" stroke="#d97706" stroke-width="1.5"/>
|
||||
<text x="380" y="360" fill="#92400e" font-size="11" font-weight="700" text-anchor="middle">Plan Approval Gate</text>
|
||||
<text x="80" y="378" fill="#78716c" font-size="9">① Teammate submits plan ② Lead approves or rejects ③ bash / write_file stay blocked until approved</text>
|
||||
|
||||
<!-- ===== Row 5: Bottom notes ===== -->
|
||||
<rect x="60" y="410" width="640" height="44" rx="6" fill="#f8fafc" stroke="#e2e8f0" stroke-width="1"/>
|
||||
<rect x="80" y="424" width="12" height="10" rx="2" fill="#f0f4ff" stroke="#2563eb" stroke-width="1"/>
|
||||
<text x="100" y="434" fill="#475569" font-size="10">s10-s14: prompt assembly, error recovery, task graph, background threads, cron scheduling</text>
|
||||
<rect x="80" y="440" width="12" height="10" rx="2" fill="#ecfeff" stroke="#0891b2" stroke-width="1"/>
|
||||
<text x="100" y="450" fill="#475569" font-size="10">s15: MessageBus + persistent teammates + event delivery + typed protocols + plan gate</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>
|
||||
|
||||
|
Before Width: | Height: | Size: 8.7 KiB After Width: | Height: | Size: 8.5 KiB |
@@ -1,120 +1,107 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 470" font-family="system-ui, -apple-system, sans-serif">
|
||||
<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="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 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="10" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||
<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-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-green" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
|
||||
<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="470" fill="#fafbfc" rx="8"/>
|
||||
<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>
|
||||
|
||||
<!-- タイトル -->
|
||||
<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">Agent Teams — Lead ループ + チームメイトスレッド + MessageBus</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>
|
||||
|
||||
<!-- 凡例 -->
|
||||
<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">s10-s14 保持</text>
|
||||
<rect x="160" y="56" width="12" height="10" rx="2" fill="#ecfeff" stroke="#0891b2" stroke-width="1"/>
|
||||
<text x="178" y="66" fill="#0891b2" font-size="10" font-weight="600">s15 新規</text>
|
||||
<rect x="260" y="56" width="12" height="10" rx="2" fill="#f0fdf4" stroke="#16a34a" stroke-width="1"/>
|
||||
<text x="278" y="66" fill="#16a34a" font-size="10" font-weight="600">チームメイト</text>
|
||||
<rect x="390" y="56" width="12" height="10" rx="2" fill="#fffbeb" stroke="#d97706" stroke-width="1"/>
|
||||
<text x="408" y="66" fill="#d97706" font-size="10" font-weight="600">プロトコルゲート</text>
|
||||
<line x1="168" y1="113" x2="220" y2="113" stroke="#475569" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
|
||||
<!-- ===== 行 1: Lead Agent ループ ===== -->
|
||||
<rect x="28" y="90" width="70" height="40" rx="8" fill="#eef2ff" stroke="#4f46e5" stroke-width="1.5"/>
|
||||
<text x="63" y="114" fill="#4f46e5" font-size="8" font-weight="600" text-anchor="middle">cron_queue</text>
|
||||
<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="98" y1="110" x2="112" y2="110" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
<line x1="520" y1="113" x2="570" y2="113" stroke="#d97706" stroke-width="1.5" marker-end="url(#arrow-amber)"/>
|
||||
|
||||
<rect x="115" y="90" width="72" height="40" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
|
||||
<text x="151" y="114" fill="#1e3a5f" font-size="10" font-weight="600" text-anchor="middle">messages</text>
|
||||
<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="187" y1="110" x2="201" y2="110" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
<!-- 線の端点はコンポーネントの境界に合わせる -->
|
||||
<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="204" y="86" width="86" height="48" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
|
||||
<text x="247" y="114" fill="#1e3a5f" font-size="9" font-weight="600" text-anchor="middle">prompt + cache</text>
|
||||
<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/<name>.jsonl</text>
|
||||
<text x="380" y="225" text-anchor="middle" fill="#0f766e" font-size="8.5">ランタイム配信 · 通常メッセージ · result · idle_notification · 制御イベント</text>
|
||||
|
||||
<line x1="290" y1="110" x2="304" y2="110" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
<!-- 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="307" y="86" width="74" height="48" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
|
||||
<text x="344" y="114" fill="#1e3a5f" font-size="9" font-weight="600" text-anchor="middle">LLM call</text>
|
||||
<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>
|
||||
|
||||
<line x1="381" y1="110" x2="395" y2="110" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
<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="398" y="80" width="336" height="60" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
|
||||
<text x="566" y="98" fill="#1e3a5f" font-size="10" font-weight="600" text-anchor="middle">TOOL DISPATCH</text>
|
||||
<text x="414" y="114" fill="#2563eb" font-size="8">bash · read · write · task(4) · cron(3)</text>
|
||||
<text x="414" y="128" fill="#0891b2" font-size="8" font-weight="700">★ spawn · send · shutdown · plan review</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>
|
||||
|
||||
<!-- ループバック -->
|
||||
<path d="M 734 110 L 748 110 L 748 150 L 63 150 L 63 130" fill="none" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)" stroke-dasharray="6,3"/>
|
||||
<!-- 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)"/>
|
||||
|
||||
<!-- ===== spawn 矢印 ===== -->
|
||||
<line x1="560" y1="140" x2="560" y2="178" stroke="#0891b2" stroke-width="2" marker-end="url(#arrow-cyan)"/>
|
||||
<rect x="543" y="152" width="38" height="14" rx="3" fill="#ecfeff"/>
|
||||
<text x="562" y="163" fill="#0891b2" font-size="8" font-weight="600" text-anchor="middle">spawn</text>
|
||||
<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>
|
||||
|
||||
<!-- ===== 行 2: MessageBus ===== -->
|
||||
<rect x="60" y="178" width="640" height="34" rx="17" fill="#ecfeff" stroke="#0891b2" stroke-width="2"/>
|
||||
<text x="380" y="200" fill="#0e7490" font-size="11" font-weight="700" text-anchor="middle">MessageBus (.mailboxes/*.jsonl)</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>
|
||||
|
||||
<!-- ===== 行 3: チームメイト ===== -->
|
||||
<line x1="170" y1="212" x2="170" y2="248" stroke="#16a34a" stroke-width="1.5" marker-end="url(#arrow-green)"/>
|
||||
<line x1="380" y1="212" x2="380" y2="248" stroke="#16a34a" stroke-width="1.5" marker-end="url(#arrow-green)"/>
|
||||
<line x1="590" y1="212" x2="590" y2="248" stroke="#16a34a" stroke-width="1.5" marker-end="url(#arrow-green)"/>
|
||||
<text x="124" y="226" fill="#16a34a" font-size="9" font-weight="600">receive</text>
|
||||
<text x="334" y="226" fill="#16a34a" font-size="9" font-weight="600">receive</text>
|
||||
<text x="544" y="226" fill="#16a34a" font-size="9" font-weight="600">receive</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">デフォルト:リポジトリ 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>
|
||||
|
||||
<line x1="200" y1="248" x2="200" y2="212" stroke="#0891b2" stroke-width="1" stroke-dasharray="4,2" marker-end="url(#arrow-cyan)"/>
|
||||
<line x1="410" y1="248" x2="410" y2="212" stroke="#0891b2" stroke-width="1" stroke-dasharray="4,2" marker-end="url(#arrow-cyan)"/>
|
||||
<line x1="620" y1="248" x2="620" y2="212" stroke="#0891b2" stroke-width="1" stroke-dasharray="4,2" marker-end="url(#arrow-cyan)"/>
|
||||
<text x="214" y="241" fill="#0891b2" font-size="9" font-weight="600">send</text>
|
||||
<text x="424" y="241" fill="#0891b2" font-size="9" font-weight="600">send</text>
|
||||
<text x="634" y="241" fill="#0891b2" font-size="9" font-weight="600">send</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/<name></text>
|
||||
<text x="550" y="548" text-anchor="middle" fill="#a16207" font-size="8.5">独立 checkout + wt/<name> branch を保持</text>
|
||||
<text x="550" y="562" text-anchor="middle" fill="#78716c" font-size="8">作業ディレクトリの分離であり sandbox ではない</text>
|
||||
|
||||
<rect x="60" y="248" width="220" height="66" rx="8" fill="#f0fdf4" stroke="#16a34a" stroke-width="1.5"/>
|
||||
<text x="170" y="268" fill="#166534" font-size="10" font-weight="700" text-anchor="middle">チームメイト: alice (Backend)</text>
|
||||
<text x="75" y="284" fill="#16a34a" font-size="8">inbox → LLM → bash/read/write/send</text>
|
||||
<text x="75" y="298" fill="#6b7280" font-size="8">WORK → result → IDLE → next message</text>
|
||||
|
||||
<rect x="270" y="248" width="220" height="66" rx="8" fill="#f0fdf4" stroke="#16a34a" stroke-width="1.5"/>
|
||||
<text x="380" y="268" fill="#166534" font-size="10" font-weight="700" text-anchor="middle">チームメイト: bob (Frontend)</text>
|
||||
<text x="285" y="284" fill="#16a34a" font-size="8">独立 agent_loop、共有 client</text>
|
||||
<text x="285" y="298" fill="#6b7280" font-size="8">Thread(daemon=True)</text>
|
||||
|
||||
<rect x="480" y="248" width="220" height="66" rx="8" fill="#f0fdf4" stroke="#16a34a" stroke-width="1.5"/>
|
||||
<text x="590" y="268" fill="#166534" font-size="10" font-weight="700" text-anchor="middle">チームメイト: charlie (QA)</text>
|
||||
<text x="495" y="284" fill="#16a34a" font-size="8">他のチームメイトを spawn 不可</text>
|
||||
<text x="495" y="298" fill="#6b7280" font-size="8">spawn → work → result → idle</text>
|
||||
|
||||
<!-- ===== 行 4: プラン承認ゲート ===== -->
|
||||
<path d="M 60 360 L 10 360 L 10 195 L 60 195" fill="none" stroke="#d97706" stroke-width="1.5" marker-end="url(#arrow-amber)" stroke-dasharray="5,3"/>
|
||||
<rect x="20" y="318" width="126" height="18" rx="4" fill="#fffbeb" stroke="#f59e0b" stroke-width="1"/>
|
||||
<text x="83" y="331" fill="#d97706" font-size="10" font-weight="700" text-anchor="middle">plan_request</text>
|
||||
|
||||
<rect x="60" y="340" width="640" height="50" rx="6" fill="#fffbeb" stroke="#d97706" stroke-width="1.5"/>
|
||||
<text x="380" y="360" fill="#92400e" font-size="11" font-weight="700" text-anchor="middle">プラン承認ゲート</text>
|
||||
<text x="80" y="378" fill="#78716c" font-size="9">① プラン提出 ② Lead が承認または却下 ③ approved まで bash / write_file を遮断</text>
|
||||
|
||||
<!-- ===== 行 5: 下部ノート ===== -->
|
||||
<rect x="60" y="410" width="640" height="44" rx="6" fill="#f8fafc" stroke="#e2e8f0" stroke-width="1"/>
|
||||
<rect x="80" y="424" width="12" height="10" rx="2" fill="#f0f4ff" stroke="#2563eb" stroke-width="1"/>
|
||||
<text x="100" y="434" fill="#475569" font-size="10">s10-s14:プロンプト組み立て、エラーリカバリ、タスクグラフ、バックグラウンドスレッド、cron</text>
|
||||
<rect x="80" y="440" width="12" height="10" rx="2" fill="#ecfeff" stroke="#0891b2" stroke-width="1"/>
|
||||
<text x="100" y="450" fill="#475569" font-size="10">s15:MessageBus + 永続チームメイト + イベント配信 + 型付きプロトコル + プランゲート</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>
|
||||
|
||||
|
Before Width: | Height: | Size: 8.8 KiB After Width: | Height: | Size: 8.7 KiB |
@@ -1,131 +1,107 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 470" font-family="system-ui, -apple-system, sans-serif">
|
||||
<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="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 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="10" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||
<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-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-green" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
|
||||
<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="470" fill="#fafbfc" rx="8"/>
|
||||
<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>
|
||||
|
||||
<!-- Title -->
|
||||
<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">Agent Teams — Lead Loop + Teammate Threads + MessageBus</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>
|
||||
|
||||
<!-- Legend -->
|
||||
<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">s10-s14 保留</text>
|
||||
<rect x="160" y="56" width="12" height="10" rx="2" fill="#ecfeff" stroke="#0891b2" stroke-width="1"/>
|
||||
<text x="178" y="66" fill="#0891b2" font-size="10" font-weight="600">s15 新增</text>
|
||||
<rect x="270" y="56" width="12" height="10" rx="2" fill="#f0fdf4" stroke="#16a34a" stroke-width="1"/>
|
||||
<text x="288" y="66" fill="#16a34a" font-size="10" font-weight="600">Teammate</text>
|
||||
<rect x="390" y="56" width="12" height="10" rx="2" fill="#fffbeb" stroke="#d97706" stroke-width="1"/>
|
||||
<text x="408" y="66" fill="#d97706" font-size="10" font-weight="600">协议闸门</text>
|
||||
<line x1="168" y1="113" x2="220" y2="113" stroke="#475569" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
|
||||
<!-- ===== Row 1: Lead Agent Loop ===== -->
|
||||
<!-- Boxes at y=90..130 (h=40), prompt/LLM at y=86..134 (h=48), TOOLS at y=80..140 (h=60) -->
|
||||
<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>
|
||||
|
||||
<rect x="28" y="90" width="70" height="40" rx="8" fill="#eef2ff" stroke="#4f46e5" stroke-width="1.5"/>
|
||||
<text x="63" y="114" fill="#4f46e5" font-size="8" font-weight="600" text-anchor="middle">cron_queue</text>
|
||||
<line x1="520" y1="113" x2="570" y2="113" stroke="#d97706" stroke-width="1.5" marker-end="url(#arrow-amber)"/>
|
||||
|
||||
<line x1="98" y1="110" x2="112" y2="110" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
<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>
|
||||
|
||||
<rect x="115" y="90" width="72" height="40" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
|
||||
<text x="151" y="114" fill="#1e3a5f" font-size="10" font-weight="600" text-anchor="middle">messages</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)"/>
|
||||
|
||||
<line x1="187" y1="110" x2="201" y2="110" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
<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/<name>.jsonl</text>
|
||||
<text x="380" y="225" text-anchor="middle" fill="#0f766e" font-size="8.5">运行时投递 · 普通消息 · result · idle_notification · 控制事件</text>
|
||||
|
||||
<rect x="204" y="86" width="86" height="48" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
|
||||
<text x="247" y="114" fill="#1e3a5f" font-size="9" font-weight="600" text-anchor="middle">prompt + cache</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)"/>
|
||||
|
||||
<line x1="290" y1="110" x2="304" y2="110" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
<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="307" y="86" width="74" height="48" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
|
||||
<text x="344" y="114" fill="#1e3a5f" font-size="9" font-weight="600" text-anchor="middle">LLM call</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>
|
||||
|
||||
<line x1="381" y1="110" x2="395" y2="110" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
<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>
|
||||
|
||||
<rect x="398" y="80" width="336" height="60" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
|
||||
<text x="566" y="98" fill="#1e3a5f" font-size="10" font-weight="600" text-anchor="middle">TOOL DISPATCH</text>
|
||||
<text x="414" y="114" fill="#2563eb" font-size="8">bash · read · write · task(4) · cron(3)</text>
|
||||
<text x="414" y="128" fill="#0891b2" font-size="8" font-weight="700">★ spawn · send · shutdown · plan review</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)"/>
|
||||
|
||||
<!-- Loop back -->
|
||||
<path d="M 734 110 L 748 110 L 748 150 L 63 150 L 63 130" fill="none" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)" stroke-dasharray="6,3"/>
|
||||
<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>
|
||||
|
||||
<!-- ===== Spawn arrow: TOOLS bottom → MessageBus top ===== -->
|
||||
<!-- TOOLS bottom: y=140, Bus top: y=178 -->
|
||||
<line x1="560" y1="140" x2="560" y2="178" stroke="#0891b2" stroke-width="2" marker-end="url(#arrow-cyan)"/>
|
||||
<rect x="543" y="152" width="38" height="14" rx="3" fill="#ecfeff"/>
|
||||
<text x="562" y="163" fill="#0891b2" font-size="8" font-weight="600" text-anchor="middle">spawn</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>
|
||||
|
||||
<!-- ===== Row 2: MessageBus ===== -->
|
||||
<!-- y=178..212 (h=34) -->
|
||||
<rect x="60" y="178" width="640" height="34" rx="17" fill="#ecfeff" stroke="#0891b2" stroke-width="2"/>
|
||||
<text x="380" y="200" fill="#0e7490" font-size="11" font-weight="700" text-anchor="middle">MessageBus (.mailboxes/*.jsonl)</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">默认:仓库 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>
|
||||
|
||||
<!-- ===== Row 3: Teammates ===== -->
|
||||
<!-- Bus bottom: y=212, Teammate top: y=248 (gap=36) -->
|
||||
<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/<name></text>
|
||||
<text x="550" y="548" text-anchor="middle" fill="#a16207" font-size="8.5">独立 checkout + 保留 wt/<name> 分支</text>
|
||||
<text x="550" y="562" text-anchor="middle" fill="#78716c" font-size="8">只隔开工作目录,不是安全沙箱</text>
|
||||
|
||||
<!-- Bus → Teammate: incoming arrows (solid green) -->
|
||||
<line x1="170" y1="212" x2="170" y2="248" stroke="#16a34a" stroke-width="1.5" marker-end="url(#arrow-green)"/>
|
||||
<line x1="380" y1="212" x2="380" y2="248" stroke="#16a34a" stroke-width="1.5" marker-end="url(#arrow-green)"/>
|
||||
<line x1="590" y1="212" x2="590" y2="248" stroke="#16a34a" stroke-width="1.5" marker-end="url(#arrow-green)"/>
|
||||
<text x="124" y="226" fill="#16a34a" font-size="9" font-weight="600">receive</text>
|
||||
<text x="334" y="226" fill="#16a34a" font-size="9" font-weight="600">receive</text>
|
||||
<text x="544" y="226" fill="#16a34a" font-size="9" font-weight="600">receive</text>
|
||||
|
||||
<!-- Teammate → Bus: send_message (dashed cyan, offset 30px right) -->
|
||||
<line x1="200" y1="248" x2="200" y2="212" stroke="#0891b2" stroke-width="1" stroke-dasharray="4,2" marker-end="url(#arrow-cyan)"/>
|
||||
<line x1="410" y1="248" x2="410" y2="212" stroke="#0891b2" stroke-width="1" stroke-dasharray="4,2" marker-end="url(#arrow-cyan)"/>
|
||||
<line x1="620" y1="248" x2="620" y2="212" stroke="#0891b2" stroke-width="1" stroke-dasharray="4,2" marker-end="url(#arrow-cyan)"/>
|
||||
<text x="214" y="241" fill="#0891b2" font-size="9" font-weight="600">send</text>
|
||||
<text x="424" y="241" fill="#0891b2" font-size="9" font-weight="600">send</text>
|
||||
<text x="634" y="241" fill="#0891b2" font-size="9" font-weight="600">send</text>
|
||||
|
||||
<!-- alice: x=60..280, y=248..314 -->
|
||||
<rect x="60" y="248" width="220" height="66" rx="8" fill="#f0fdf4" stroke="#16a34a" stroke-width="1.5"/>
|
||||
<text x="170" y="268" fill="#166534" font-size="10" font-weight="700" text-anchor="middle">Teammate: alice (Backend)</text>
|
||||
<text x="75" y="284" fill="#16a34a" font-size="8">inbox → LLM → bash/read/write/send</text>
|
||||
<text x="75" y="298" fill="#6b7280" font-size="8">WORK → result → IDLE → next message</text>
|
||||
|
||||
<!-- bob: x=270..490, y=248..314 -->
|
||||
<rect x="270" y="248" width="220" height="66" rx="8" fill="#f0fdf4" stroke="#16a34a" stroke-width="1.5"/>
|
||||
<text x="380" y="268" fill="#166534" font-size="10" font-weight="700" text-anchor="middle">Teammate: bob (Frontend)</text>
|
||||
<text x="285" y="284" fill="#16a34a" font-size="8">独立 agent_loop,共享 client</text>
|
||||
<text x="285" y="298" fill="#6b7280" font-size="8">Thread(daemon=True)</text>
|
||||
|
||||
<!-- charlie: x=480..700, y=248..314 -->
|
||||
<rect x="480" y="248" width="220" height="66" rx="8" fill="#f0fdf4" stroke="#16a34a" stroke-width="1.5"/>
|
||||
<text x="590" y="268" fill="#166534" font-size="10" font-weight="700" text-anchor="middle">Teammate: charlie (QA)</text>
|
||||
<text x="495" y="284" fill="#16a34a" font-size="8">不能 spawn 其他 teammate</text>
|
||||
<text x="495" y="298" fill="#6b7280" font-size="8">spawn → work → result → idle</text>
|
||||
|
||||
<!-- ===== Row 4: Plan approval gate ===== -->
|
||||
<path d="M 60 360 L 10 360 L 10 195 L 60 195" fill="none" stroke="#d97706" stroke-width="1.5" marker-end="url(#arrow-amber)" stroke-dasharray="5,3"/>
|
||||
<rect x="20" y="318" width="126" height="18" rx="4" fill="#fffbeb" stroke="#f59e0b" stroke-width="1"/>
|
||||
<text x="83" y="331" fill="#d97706" font-size="10" font-weight="700" text-anchor="middle">plan_request</text>
|
||||
|
||||
<rect x="60" y="340" width="640" height="50" rx="6" fill="#fffbeb" stroke="#d97706" stroke-width="1.5"/>
|
||||
<text x="380" y="360" fill="#92400e" font-size="11" font-weight="700" text-anchor="middle">计划审批闸门</text>
|
||||
<text x="80" y="378" fill="#78716c" font-size="9">① 队友提交计划 ② Lead 批准或拒绝 ③ approved 前 bash / write_file 被拦截</text>
|
||||
|
||||
<!-- ===== Row 5: Bottom notes ===== -->
|
||||
<rect x="60" y="410" width="640" height="44" rx="6" fill="#f8fafc" stroke="#e2e8f0" stroke-width="1"/>
|
||||
<rect x="80" y="424" width="12" height="10" rx="2" fill="#f0f4ff" stroke="#2563eb" stroke-width="1"/>
|
||||
<text x="100" y="434" fill="#475569" font-size="10">s10-s14: prompt 组装、错误恢复、任务图、后台线程、cron 调度</text>
|
||||
<rect x="80" y="440" width="12" height="10" rx="2" fill="#ecfeff" stroke="#0891b2" stroke-width="1"/>
|
||||
<text x="100" y="450" fill="#475569" font-size="10">s15: MessageBus + 持久队友 + 自动事件投递 + 类型化协议 + 计划闸门</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>
|
||||
|
||||
|
Before Width: | Height: | Size: 9.1 KiB After Width: | Height: | Size: 8.4 KiB |
Reference in New Issue
Block a user