mirror of
https://github.com/shareAI-lab/analysis_claude_code.git
synced 2026-06-21 04:33:36 +08:00
* feat: s01-s14 docs quality overhaul — tool pipeline, single-agent, knowledge & resilience Rewrite code.py and README (zh/en/ja) for s01-s14, each chapter building incrementally on the previous. Key fixes across chapters: - s01-s04: agent loop, tool dispatch, permission pipeline, hooks - s05-s08: todo write, subagent, skill loading, context compact - s09-s11: memory system, system prompt assembly, error recovery - s12-s14: task graph, background tasks, cron scheduler All chapters CC source-verified. Code inherits fixes forward (PROMPT_SECTIONS, json.dumps cache, real-state context, can_start dep protection, etc.). * feat: s15-s19 docs quality overhaul — multi-agent platform: teams, protocols, autonomy, worktree, MCP tools Rewrite code.py and README (zh/en/ja) for s15-s19, the multi-agent platform chapters. Each chapter inherits all previous fixes and adds one mechanism: - s15: agent teams (TeamCreate, teammate threads, shared task list) - s16: team protocols (plan approval, shutdown handshake, consume_inbox) - s17: autonomous agents (idle polling, auto-claim, consume_lead_inbox) - s18: worktree isolation (git worktree, bind_task, cwd switching, safety) - s19: MCP tools (MCPClient, normalize_mcp_name, assemble_tool_pool, no cache) All appendix source code references verified against CC source. Config priority corrected: claude.ai < plugin < user < project < local. * fix: 5 regressions across s05-s19 — glob safety, todo validation, memory extraction, protocol types, dep crash - s05-s09: glob results now filter with is_relative_to(WORKDIR) (inherited from s02) - s06-s08: todo_write validates content/status required fields (inherited from s05) - s09: extract_memories uses pre-compression snapshot instead of compacted messages - s16: submit_plan docstring clarifies protocol-only (not code-level gate) - s17-s19: match_response restores type mismatch validation (from s16) - s17-s19: claim_task deps list handles missing dep files without crashing * fix: s12 Todo V2 logic reversal, s14/s15 cron range validation, s18/s19 worktree name validation - s12 README (zh/en/ja): fix Todo V2 direction — interactive defaults to Task, non-interactive/SDK defaults to TodoWrite. Fix env var name to CLAUDE_CODE_ENABLE_TASKS (not TODO_V2). - s14/s15: add _validate_cron_field with per-field range checks (minute 0-59, hour 0-23, dom 1-31, month 1-12, dow 0-6), step > 0, range lo <= hi. Replace old try/except validation that only caught exceptions. - s18/s19: add validate_worktree_name() to remove_worktree and keep_worktree, not just create_worktree. * fix: align s16-s19 teaching tool consistency * fix pr265 chapter diagrams * Add comprehensive s20 harness chapter * Fix chapter smoke test regressions * Clarify README tutorial track transition --------- Co-authored-by: Haoran <bill-billion@outlook.com>
This commit is contained in:
254
s15_agent_teams/README.en.md
Normal file
254
s15_agent_teams/README.en.md
Normal file
@@ -0,0 +1,254 @@
|
||||
# s15: Agent Teams — One Agent Isn't Enough, Form a Team
|
||||
|
||||
[中文](README.md) · [English](README.en.md) · [日本語](README.ja.md)
|
||||
|
||||
s01 → ... → s13 → s14 → `s15` → [s16](../s16_team_protocols/) → s17 → s18 → s19 → s20
|
||||
> *"One agent isn't enough, form a team"* — File-based inboxes + teammate threads.
|
||||
>
|
||||
> **Harness Layer**: Teams — Multi-agent collaboration, message bus.
|
||||
|
||||
---
|
||||
|
||||
## The Problem
|
||||
|
||||
"Refactor the entire backend" touches auth, database layer, API routes, and tests. One agent working on API routes no longer has auth module details in context. The context window is limited, a single agent can't cover every module.
|
||||
|
||||
s06's sub-agents are temps, called in for one job, then gone. Some tasks need teammates that can communicate and collaborate.
|
||||
|
||||
---
|
||||
|
||||
## The Solution
|
||||
|
||||

|
||||
|
||||
Teaching code carries forward S14's capabilities (prompt assembly, task system, background execution, cron scheduling). To stay focused on the team mechanism, it omits full error recovery, memory, and skill systems. Added: **MessageBus** (file-based inboxes), **spawn_teammate_thread** (launch teammate threads), **inbox injection** (Lead receives teammate messages and injects into history).
|
||||
|
||||
Sub-agent vs Teammate:
|
||||
|
||||
| | s06 Sub-agent | s15 Teammate |
|
||||
|---|---|---|
|
||||
| Lifetime | One-shot, destroyed after use | Multi-turn (teaching: 10 rounds; real CC: idle loop) |
|
||||
| Communication | Only returns conclusion | Async inbox, communicate anytime |
|
||||
| Context | Fully isolated | Shared via messages |
|
||||
| Count | One lead + occasional sub-agent | One Lead + multiple teammates |
|
||||
|
||||
---
|
||||
|
||||
## How It Works
|
||||
|
||||

|
||||
|
||||
### MessageBus: File-Based Inboxes
|
||||
|
||||
Each agent (including Lead and teammates) has a `.jsonl` inbox. Send = append a JSON line to the target's file. Read = read file + delete (consumption):
|
||||
|
||||
```python
|
||||
class MessageBus:
|
||||
def send(self, from_agent: str, to_agent: str,
|
||||
content: str, msg_type: str = "message"):
|
||||
msg = {"from": from_agent, "to": to_agent,
|
||||
"content": content, "type": msg_type,
|
||||
"ts": time.time()}
|
||||
inbox = MAILBOX_DIR / f"{to_agent}.jsonl"
|
||||
with open(inbox, "a") as f:
|
||||
f.write(json.dumps(msg) + "\n")
|
||||
|
||||
def read_inbox(self, agent: str) -> list[dict]:
|
||||
inbox = MAILBOX_DIR / f"{agent}.jsonl"
|
||||
if not inbox.exists():
|
||||
return []
|
||||
msgs = [json.loads(line) for line in inbox.read_text().splitlines()]
|
||||
inbox.unlink() # consume: read + delete
|
||||
return msgs
|
||||
```
|
||||
|
||||
Why files instead of in-memory queues? Teaching code uses files because they're intuitive and observable across threads. Real CC also uses file inboxes (`~/.claude/teams/{team}/inboxes/`) but adds `proper-lockfile` for concurrent write safety. The teaching version's `read_inbox` has a read + unlink race, concurrent reads could lose messages, acceptable for teaching purposes.
|
||||
|
||||
### spawn_teammate_thread: Launching a Teammate
|
||||
|
||||
Lead calls the `spawn_teammate` tool to start a teammate. The teammate runs in its own daemon thread with its own system prompt, messages, and simplified tool set:
|
||||
|
||||
```python
|
||||
def spawn_teammate_thread(name: str, role: str, prompt: str) -> str:
|
||||
system = f"You are '{name}', a {role}. Use tools to complete tasks."
|
||||
|
||||
def run():
|
||||
messages = [{"role": "user", "content": prompt}]
|
||||
sub_tools = [bash, read_file, write_file, send_message]
|
||||
for _ in range(10): # max 10 rounds
|
||||
inbox = BUS.read_inbox(name)
|
||||
if inbox:
|
||||
messages.append({"role": "user",
|
||||
"content": f"<inbox>{json.dumps(inbox)}</inbox>"})
|
||||
response = client.messages.create(
|
||||
model=MODEL, system=system, messages=messages[-20:],
|
||||
tools=sub_tools, max_tokens=8000)
|
||||
# ... execute tools, process results
|
||||
# Send final summary to Lead
|
||||
BUS.send(name, "lead", summary, "result")
|
||||
|
||||
threading.Thread(target=run, daemon=True).start()
|
||||
```
|
||||
|
||||
Key design:
|
||||
- **Simplified tool set**: bash, read, write, send_message. Teaching code omits tasks and cron to focus on communication. Real CC teammates also have TaskCreate, TaskUpdate, etc., the task system is shared across the team
|
||||
- **Teaching: 10 rounds max**: prevents infinite loops. Real CC uses idle loop: after each round, send `idle_notification`, wait for inbox messages, resume on arrival, exit only on `shutdown_request`
|
||||
- **Auto-report on completion**: `BUS.send(name, "lead", summary)` sends the final result to Lead's inbox
|
||||
|
||||
### Lead's Inbox Injection
|
||||
|
||||
Lead checks inbox after each main loop iteration. Teammate messages are injected into history so the LLM can see and react to them:
|
||||
|
||||
```python
|
||||
# After main loop iteration
|
||||
inbox = BUS.read_inbox("lead")
|
||||
if inbox:
|
||||
inbox_text = "\n".join(
|
||||
f"From {m['from']}: {m['content'][:200]}" for m in inbox)
|
||||
history.append({"role": "user",
|
||||
"content": f"[Inbox]\n{inbox_text}"})
|
||||
```
|
||||
|
||||
Teaching code injects in the user input loop. Real CC is more refined, Lead's `useInboxPoller` checks every 1 second, submitting messages as new turns without waiting for user input.
|
||||
|
||||
### Permission Bubbling
|
||||
|
||||
Teaching code omits permission bubbling. Real CC's flow (`permissionSync.ts`, `useSwarmPermissionPoller.ts`):
|
||||
|
||||
1. Teammate encounters an operation needing approval → sends `permission_request` to Lead's inbox
|
||||
2. Lead's `useInboxPoller` detects the request → routes to approval queue
|
||||
3. User approves → Lead sends `permission_response` back to teammate
|
||||
4. Teammate's `useSwarmPermissionPoller` (polls every 500ms) receives reply → continue or reject
|
||||
|
||||
### Putting It Together
|
||||
|
||||
```
|
||||
1. Lead: "Build the backend: one agent isn't enough, form a team"
|
||||
2. Lead → spawn_teammate("alice", "backend dev", "Create database schema")
|
||||
3. Lead → spawn_teammate("bob", "frontend dev", "Write API client")
|
||||
4. Alice thread starts → her own LLM call → bash "python manage.py migrate"
|
||||
5. Bob thread starts → his own LLM call → write_file("client.ts", ...)
|
||||
6. Alice done → BUS.send("alice", "lead", "Schema done: users, orders tables")
|
||||
7. Bob done → BUS.send("bob", "lead", "Client written with types")
|
||||
8. Lead next iteration → inbox injected into history → LLM sees both results
|
||||
```
|
||||
|
||||
Two teammates work in parallel.
|
||||
|
||||
---
|
||||
|
||||
## Changes from s14
|
||||
|
||||
| Component | Before (s14) | After (s15) |
|
||||
|-----------|-------------|-------------|
|
||||
| Agent count | 1 | 1 Lead + N teammate threads |
|
||||
| Communication | None | MessageBus + .mailboxes/*.jsonl |
|
||||
| New classes | — | MessageBus, active_teammates dict |
|
||||
| New functions | — | spawn_teammate_thread, run_send_message, run_check_inbox |
|
||||
| Lead tools | 11 (s14) | + spawn_teammate, send_message, check_inbox (14) |
|
||||
| Teammate tools | — | bash, read_file, write_file, send_message (4) |
|
||||
| Permissions | Local decisions | Teaching code omits (real CC has bubbling) |
|
||||
|
||||
---
|
||||
|
||||
## Try It
|
||||
|
||||
```sh
|
||||
cd learn-claude-code
|
||||
python s15_agent_teams/code.py
|
||||
```
|
||||
|
||||
Try these prompts:
|
||||
|
||||
1. `Spawn alice as a backend developer. Ask her to create a file called schema.sql with a users table.`
|
||||
2. `Check your inbox for alice's result.`
|
||||
3. `Spawn bob as a tester. Ask him to check if schema.sql exists and list its contents.`
|
||||
|
||||
What to observe: How does Lead spawn teammates? What do the `.mailboxes/` JSONL files look like? After teammates finish, is Lead's inbox injected into history?
|
||||
|
||||
---
|
||||
|
||||
## What's Next
|
||||
|
||||
Teammates can work and communicate. But if Lead wants Alice to shut down, killing the thread outright could leave half-written files. A graceful shutdown protocol is needed: Lead sends shutdown_request, teammate wraps up and exits.
|
||||
|
||||
s16 Team Protocols → Shutdown handshake and message conventions.
|
||||
|
||||
<details>
|
||||
<summary>Deep Dive into CC Source</summary>
|
||||
|
||||
> The following is a complete analysis based on CC source code `spawnMultiAgent.ts`, `useInboxPoller.ts` (969 lines), `useSwarmPermissionPoller.ts` (330 lines), `teammateMailbox.ts`, `teamHelpers.ts`.
|
||||
|
||||
### 1. No Central Message Bus, It's the Filesystem
|
||||
|
||||
Teaching code uses a `MessageBus` class to send and receive messages. Real CC is more direct, each agent writes directly to other agents' inbox files.
|
||||
|
||||
Inbox path: `~/.claude/teams/{teamName}/inboxes/{agentName}.json`
|
||||
|
||||
Writes use `proper-lockfile` for concurrent write safety (up to 10 retries). Each file is a JSON array; appending reads → appends → writes back.
|
||||
|
||||
### 2. 15 Message Types
|
||||
|
||||
CC team communication has 15 structured message types (`teammateMailbox.ts`):
|
||||
|
||||
| Type | Direction | Purpose |
|
||||
|------|-----------|---------|
|
||||
| `plain text` | Both ways | Normal inter-teammate communication |
|
||||
| `idle_notification` | Teammate→Lead | Teammate finished a turn, now idle |
|
||||
| `permission_request` | Teammate→Lead | Teammate needs operation approval |
|
||||
| `permission_response` | Lead→Teammate | Lead's approval result |
|
||||
| `plan_approval_request` | Teammate→Lead | Teammate submits plan for review |
|
||||
| `plan_approval_response` | Lead→Teammate | Lead's plan review |
|
||||
| `shutdown_request` | Lead→Teammate | Request graceful shutdown |
|
||||
| `shutdown_approved` | Teammate→Lead | Confirm shutdown |
|
||||
| `shutdown_rejected` | Teammate→Lead | Reject shutdown (with reason) |
|
||||
| `task_assignment` | Lead→Teammate | Assign a task |
|
||||
| `team_permission_update` | Lead→Teammate | Broadcast permission changes |
|
||||
| `mode_set_request` | Lead→Teammate | Change teammate's permission mode |
|
||||
| `sandbox_permission_*` | Both ways | Network permission request/reply |
|
||||
| `teammate_terminated` | System | Teammate removed notification |
|
||||
|
||||
Text messages are wrapped in `<teammate-message>` XML tags for delivery to the model.
|
||||
|
||||
### 3. Permission Bubbling: Bidirectional Polling
|
||||
|
||||
Teaching code omits permission bubbling. Real CC's flow (`permissionSync.ts`):
|
||||
|
||||
1. **Teammate** encounters operation needing approval → sends `permission_request` to Lead's inbox
|
||||
2. **Lead's** `useInboxPoller` (polls every 1s) detects request → routes to `ToolUseConfirmQueue`
|
||||
3. Lead's UI shows approval dialog with teammate name and color
|
||||
4. User approves → Lead sends `permission_response` back to teammate's inbox
|
||||
5. **Teammate's** `useSwarmPermissionPoller` (polls every 500ms) receives reply → continue or reject
|
||||
|
||||
### 4. Teammate Lifecycle
|
||||
|
||||
CC teammates are created by `spawnTeammate()` (`spawnMultiAgent.ts`):
|
||||
|
||||
1. **Spawn**: Create tmux pane (or in-process), assign color, write team config
|
||||
2. **Work**: `useInboxPoller` checks inbox every 1s → submit as new turn when messages arrive
|
||||
3. **Idle**: Stop hook fires → send `idle_notification` to Lead
|
||||
4. **Shutdown**: Lead sends `shutdown_request` → teammate replies `shutdown_approved` → Lead cleans up
|
||||
|
||||
### 5. Team Config
|
||||
|
||||
Team registry at `~/.claude/teams/{teamName}/config.json` (`teamHelpers.ts`):
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "my-team",
|
||||
"leadAgentId": "lead@my-team",
|
||||
"members": [{
|
||||
"agentId": "researcher@my-team",
|
||||
"name": "researcher",
|
||||
"agentType": "general-purpose",
|
||||
"color": "blue",
|
||||
"isActive": true
|
||||
}]
|
||||
}
|
||||
```
|
||||
|
||||
Teammates cannot be nested (`AgentTool.tsx:273` explicitly forbids "teammates spawning other teammates").
|
||||
|
||||
</details>
|
||||
|
||||
<!-- translation-sync: zh@v1, en@v1, ja@v1 -->
|
||||
254
s15_agent_teams/README.ja.md
Normal file
254
s15_agent_teams/README.ja.md
Normal file
@@ -0,0 +1,254 @@
|
||||
# s15: Agent Teams — 一人では無理、チームを組もう
|
||||
|
||||
[中文](README.md) · [English](README.en.md) · [日本語](README.ja.md)
|
||||
|
||||
s01 → ... → s13 → s14 → `s15` → [s16](../s16_team_protocols/) → s17 → s18 → s19 → s20
|
||||
> *"一人では無理、チームを組もう"* — ファイル受信箱 + チームメイトスレッド。
|
||||
>
|
||||
> **Harness 層**: チーム — マルチ Agent 協調、メッセージバス。
|
||||
|
||||
---
|
||||
|
||||
## 課題
|
||||
|
||||
「バックエンド全体をリファクタリング」は認証モジュール、データベース層、API ルート、テストに及ぶ。一つの Agent が API ルートを修正中、認証モジュールの詳細はコンテキストから外れている。コンテキストウィンドウには限界があり、単一 Agent の注意は全モジュールをカバーできない。
|
||||
|
||||
s06 のサブ Agent は臨時スタッフ、一つの仕事を終えたら去る。だが、通信でき、協力できるチームメイトが必要なタスクもある。
|
||||
|
||||
---
|
||||
|
||||
## ソリューション
|
||||
|
||||

|
||||
|
||||
教学版は S14 の能力(プロンプト組み立て、タスクシステム、バックグラウンド実行、cron スケジューリング)を踏襲。チーム機構に集中するため、完全なエラーリカバリ、メモリ、スキルシステムは省略。追加:**MessageBus**(ファイル受信箱)、**spawn_teammate_thread**(チームメイトスレッド起動)、**inbox 注入**(Lead がチームメイトメッセージを受信し history に注入)。
|
||||
|
||||
サブ Agent vs チームメイト:
|
||||
|
||||
| | s06 サブ Agent | s15 チームメイト |
|
||||
|---|---|---|
|
||||
| ライフサイクル | 一回きり、終了後に破棄 | マルチターン(教学版は 10 ラウンド制限、真实 CC は idle loop) |
|
||||
| 通信 | 結果のみ返却 | 非同期受信箱、いつでも通信可能 |
|
||||
| コンテキスト | 完全に隔離 | メッセージで情報共有 |
|
||||
| 数 | メイン Agent + たまにサブ Agent | 1 Lead + 複数チームメイト |
|
||||
|
||||
---
|
||||
|
||||
## 仕組み
|
||||
|
||||

|
||||
|
||||
### MessageBus: ファイル受信箱
|
||||
|
||||
各 Agent(Lead とチームメイトを含む)には `.jsonl` 受信箱がある。メッセージ送信 = 相手のファイルに 1 行 JSON を append。メッセージ読み取り = ファイル読み込み + 削除(消費式):
|
||||
|
||||
```python
|
||||
class MessageBus:
|
||||
def send(self, from_agent: str, to_agent: str,
|
||||
content: str, msg_type: str = "message"):
|
||||
msg = {"from": from_agent, "to": to_agent,
|
||||
"content": content, "type": msg_type,
|
||||
"ts": time.time()}
|
||||
inbox = MAILBOX_DIR / f"{to_agent}.jsonl"
|
||||
with open(inbox, "a") as f:
|
||||
f.write(json.dumps(msg) + "\n")
|
||||
|
||||
def read_inbox(self, agent: str) -> list[dict]:
|
||||
inbox = MAILBOX_DIR / f"{agent}.jsonl"
|
||||
if not inbox.exists():
|
||||
return []
|
||||
msgs = [json.loads(line) for line in inbox.read_text().splitlines()]
|
||||
inbox.unlink() # 消費式:読んだら削除
|
||||
return msgs
|
||||
```
|
||||
|
||||
なぜファイルか、メモリキューではなく?教学版がファイルを選ぶ理由は、直感的でスレッドをまたいで観察可能だから。真实 CC もファイル受信箱(`~/.claude/teams/{team}/inboxes/`)を使うが、`proper-lockfile` で並行書き込みの安全性を確保。教学版の `read_inbox` には read + unlink の競合状態があり、マルチスレッド同時読みでメッセージを損失する可能性があるが、教学目的には許容範囲。
|
||||
|
||||
### spawn_teammate_thread: チームメイト起動
|
||||
|
||||
Lead が `spawn_teammate` ツールを呼び出してチームメイトを起動。チームメイトは独自の daemon スレッドで動作、独自の system prompt、messages、簡易ツールセットを持つ:
|
||||
|
||||
```python
|
||||
def spawn_teammate_thread(name: str, role: str, prompt: str) -> str:
|
||||
system = f"You are '{name}', a {role}. Use tools to complete tasks."
|
||||
|
||||
def run():
|
||||
messages = [{"role": "user", "content": prompt}]
|
||||
sub_tools = [bash, read_file, write_file, send_message]
|
||||
for _ in range(10): # 最大 10 ラウンド
|
||||
inbox = BUS.read_inbox(name)
|
||||
if inbox:
|
||||
messages.append({"role": "user",
|
||||
"content": f"<inbox>{json.dumps(inbox)}</inbox>"})
|
||||
response = client.messages.create(
|
||||
model=MODEL, system=system, messages=messages[-20:],
|
||||
tools=sub_tools, max_tokens=8000)
|
||||
# ... ツール実行、結果処理
|
||||
# 完了後 summary を Lead に送信
|
||||
BUS.send(name, "lead", summary, "result")
|
||||
|
||||
threading.Thread(target=run, daemon=True).start()
|
||||
```
|
||||
|
||||
重要な設計:
|
||||
- **チームメイトの簡易ツールセット**:bash、read、write、send_message。教学版は通信機構に集中するためタスクと cron を省略。真实 CC のチームメイトには TaskCreate、TaskUpdate 等のツールもあり、タスクシステムはチーム全体で共有
|
||||
- **教学版は 10 ラウンド制限**:無限ループを防止。真实 CC は idle loop:1 ラウンド終了後に `idle_notification` を送信、inbox メッセージを待機、到着後に再開、`shutdown_request` でのみ終了
|
||||
- **完了時自動報告**:`BUS.send(name, "lead", summary)` で最終結果を Lead の受信箱に送信
|
||||
|
||||
### Lead の inbox 注入
|
||||
|
||||
Lead はメインループの各反復後に受信箱を確認。チームメイトからのメッセージを history に注入し、LLM が確認して反応できるようにする:
|
||||
|
||||
```python
|
||||
# メインループ反復後
|
||||
inbox = BUS.read_inbox("lead")
|
||||
if inbox:
|
||||
inbox_text = "\n".join(
|
||||
f"From {m['from']}: {m['content'][:200]}" for m in inbox)
|
||||
history.append({"role": "user",
|
||||
"content": f"[Inbox]\n{inbox_text}"})
|
||||
```
|
||||
|
||||
教学版はユーザー入力ループ内で注入。真实 CC はより精密、Lead の `useInboxPoller` が毎秒チェックし、ユーザー入力を待たずにメッセージを新しい turn として送信。
|
||||
|
||||
### 権限バブリング
|
||||
|
||||
教学版は権限バブリングを省略。真实 CC のフロー(`permissionSync.ts`、`useSwarmPermissionPoller.ts`):
|
||||
|
||||
1. チームメイトが承認が必要な操作に遭遇 → `permission_request` を Lead の受信箱に送信
|
||||
2. Lead の `useInboxPoller` がリクエストを検出 → 承認キューにルーティング
|
||||
3. ユーザーが承認 → Lead が `permission_response` をチームメイトに返信
|
||||
4. チームメイトの `useSwarmPermissionPoller`(500ms ごとにポーリング)が返信を受信 → 続行または拒否
|
||||
|
||||
### 組み合わせて実行
|
||||
|
||||
```
|
||||
1. Lead: "バックエンド構築:一人では無理、チームを組もう"
|
||||
2. Lead → spawn_teammate("alice", "backend dev", "データベーススキーマを作成")
|
||||
3. Lead → spawn_teammate("bob", "frontend dev", "API クライアントを作成")
|
||||
4. alice スレッド起動 → 独自の LLM 呼び出し → bash "python manage.py migrate"
|
||||
5. bob スレッド起動 → 独自の LLM 呼び出し → write_file("client.ts", ...)
|
||||
6. alice 完了 → BUS.send("alice", "lead", "Schema done: users, orders tables")
|
||||
7. bob 完了 → BUS.send("bob", "lead", "Client written with types")
|
||||
8. Lead 次回反復 → inbox を history に注入 → LLM が alice と bob の結果を確認
|
||||
```
|
||||
|
||||
2 人のチームメイトが並行作業。
|
||||
|
||||
---
|
||||
|
||||
## s14 からの変更
|
||||
|
||||
| コンポーネント | 変更前 (s14) | 変更後 (s15) |
|
||||
|--------------|------------|------------|
|
||||
| Agent 数 | 1 | 1 Lead + N チームメイトスレッド |
|
||||
| 通信 | なし | MessageBus + .mailboxes/*.jsonl |
|
||||
| 新規クラス | — | MessageBus, active_teammates dict |
|
||||
| 新規関数 | — | spawn_teammate_thread, run_send_message, run_check_inbox |
|
||||
| Lead ツール | 11 (s14) | + spawn_teammate, send_message, check_inbox (14) |
|
||||
| チームメイトツール | — | bash, read_file, write_file, send_message (4) |
|
||||
| 権限 | ローカル判断 | 教学版は省略(真实 CC はバブリング機構あり) |
|
||||
|
||||
---
|
||||
|
||||
## 試してみる
|
||||
|
||||
```sh
|
||||
cd learn-claude-code
|
||||
python s15_agent_teams/code.py
|
||||
```
|
||||
|
||||
以下のプロンプトを試してください:
|
||||
|
||||
1. `Spawn alice as a backend developer. Ask her to create a file called schema.sql with a users table.`
|
||||
2. `Check your inbox for alice's result.`
|
||||
3. `Spawn bob as a tester. Ask him to check if schema.sql exists and list its contents.`
|
||||
|
||||
観察ポイント:Lead はチームメイトをどう起動するか?`.mailboxes/` ディレクトリの JSONL ファイルの中身は?チームメイト完了後、Lead の inbox は history に注入されているか?
|
||||
|
||||
---
|
||||
|
||||
## 次の章
|
||||
|
||||
チームメイトは仕事をし、通信できる。しかし、Lead が Alice にシャットダウンを頼む場合、スレッドを強制終了すると書きかけのファイルが残る。丁寧なシャットダウンプロトコルが必要:Lead が shutdown_request を送信、チームメイトは收尾後に終了。
|
||||
|
||||
s16 Team Protocols → シャットダウンハンドシェイクとメッセージの取り決め。
|
||||
|
||||
<details>
|
||||
<summary>CC ソースコード深掘り</summary>
|
||||
|
||||
> 以下は CC ソースコード `spawnMultiAgent.ts`、`useInboxPoller.ts`(969 行)、`useSwarmPermissionPoller.ts`(330 行)、`teammateMailbox.ts`、`teamHelpers.ts` の完全分析に基づく。
|
||||
|
||||
### 一、中央メッセージバスはない、ファイルシステム
|
||||
|
||||
教学版は `MessageBus` クラスでメッセージを送受信。真实 CC はもっと直接的、各 Agent が他の Agent の受信箱ファイルに直接書き込む。
|
||||
|
||||
受信箱パス:`~/.claude/teams/{teamName}/inboxes/{agentName}.json`
|
||||
|
||||
書き込み時は `proper-lockfile` で並行安全性を確保(最大 10 回リトライ)。各ファイルは JSON 配列、append 時に読み取り→追加→書き戻し。
|
||||
|
||||
### 二、15 種のメッセージ型
|
||||
|
||||
CC のチーム通信には 15 種の構造化メッセージ(`teammateMailbox.ts`)がある:
|
||||
|
||||
| 型 | 方向 | 用途 |
|
||||
|------|------|------|
|
||||
| `plain text` | 双方向 | 通常のチームメイト間通信 |
|
||||
| `idle_notification` | チームメイト→Lead | チームメイトが 1 ターン完了、アイドル状態に |
|
||||
| `permission_request` | チームメイト→Lead | 操作承認が必要 |
|
||||
| `permission_response` | Lead→チームメイト | Lead の承認結果 |
|
||||
| `plan_approval_request` | チームメイト→Lead | 計画提出、審査待ち |
|
||||
| `plan_approval_response` | Lead→チームメイト | Lead の計画審査 |
|
||||
| `shutdown_request` | Lead→チームメイト | 丁寧なシャットダウン要求 |
|
||||
| `shutdown_approved` | チームメイト→Lead | シャットダウン確認 |
|
||||
| `shutdown_rejected` | チームメイト→Lead | シャットダウン拒否(理由付き) |
|
||||
| `task_assignment` | Lead→チームメイト | タスク割り当て |
|
||||
| `team_permission_update` | Lead→チームメイト | 権限変更のブロードキャスト |
|
||||
| `mode_set_request` | Lead→チームメイト | チームメイトの権限モード変更 |
|
||||
| `sandbox_permission_*` | 双方向 | ネットワーク権限リクエスト/返信 |
|
||||
| `teammate_terminated` | システム | チームメイト削除通知 |
|
||||
|
||||
テキストメッセージは `<teammate-message>` XML タグでラップされモデルに配信。
|
||||
|
||||
### 三、権限バブリング:双方向ポーリング
|
||||
|
||||
教学版は権限バブリングを省略。真实 CC のフロー(`permissionSync.ts`):
|
||||
|
||||
1. **チームメイト**が承認が必要な操作に遭遇 → `permission_request` を Lead の受信箱に送信
|
||||
2. **Lead** の `useInboxPoller`(1 秒ごとにポーリング)がリクエストを検出 → `ToolUseConfirmQueue` にルーティング
|
||||
3. Lead の UI にチームメイト名と色付きの承認ダイアログを表示
|
||||
4. ユーザー承認後 → Lead が `permission_response` をチームメイトの受信箱に返信
|
||||
5. **チームメイト**の `useSwarmPermissionPoller`(500ms ごとにポーリング)が返信を受信 → 続行または拒否
|
||||
|
||||
### 四、チームメイトライフサイクル
|
||||
|
||||
CC のチームメイトは `spawnTeammate()`(`spawnMultiAgent.ts`)で作成:
|
||||
|
||||
1. **Spawn**:tmux ペイン(またはプロセス内)を作成、色を割り当て、team config に書き込み
|
||||
2. **Work**:`useInboxPoller` が毎秒受信箱をチェック → メッセージ到着時に新しい turn として送信
|
||||
3. **Idle**:Stop hook 発火 → `idle_notification` を Lead に送信
|
||||
4. **Shutdown**:Lead が `shutdown_request` を送信 → チームメイトが `shutdown_approved` で返信 → Lead がクリーンアップ
|
||||
|
||||
### 五、Team Config
|
||||
|
||||
チーム登録は `~/.claude/teams/{teamName}/config.json`(`teamHelpers.ts`):
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "my-team",
|
||||
"leadAgentId": "lead@my-team",
|
||||
"members": [{
|
||||
"agentId": "researcher@my-team",
|
||||
"name": "researcher",
|
||||
"agentType": "general-purpose",
|
||||
"color": "blue",
|
||||
"isActive": true
|
||||
}]
|
||||
}
|
||||
```
|
||||
|
||||
チームメイトのネストは禁止(`AgentTool.tsx:273` で "teammates spawning other teammates" を明示的に禁止)。
|
||||
|
||||
</details>
|
||||
|
||||
<!-- translation-sync: zh@v1, en@v1, ja@v1 -->
|
||||
254
s15_agent_teams/README.md
Normal file
254
s15_agent_teams/README.md
Normal file
@@ -0,0 +1,254 @@
|
||||
# s15: Agent Teams — 一个搞不定,组队来
|
||||
|
||||
[中文](README.md) · [English](README.en.md) · [日本語](README.ja.md)
|
||||
|
||||
s01 → ... → s13 → s14 → `s15` → [s16](../s16_team_protocols/) → s17 → s18 → s19 → s20
|
||||
> *"一个搞不定, 组队来"* — 文件收件箱 + 队友线程。
|
||||
>
|
||||
> **Harness 层**: 团队 — 多 Agent 协作, 消息总线。
|
||||
|
||||
---
|
||||
|
||||
## 问题
|
||||
|
||||
"重构整个后端"涉及认证模块、数据库层、API 路由、测试。一个 Agent 在修 API 路由时,认证模块的细节已经不在上下文里了。上下文窗口就那么大,单个 Agent 的注意力覆盖不了所有模块。
|
||||
|
||||
s06 的子 Agent 是临时工,叫来干一件事就走了。但有些任务需要能通信、能协作的队友。
|
||||
|
||||
---
|
||||
|
||||
## 解决方案
|
||||
|
||||

|
||||
|
||||
教学代码沿用 S14 的能力(prompt 组装、任务系统、后台执行、cron 调度)。为了聚焦团队机制,省略了完整错误恢复、记忆和技能系统。新增三样:**MessageBus**(文件收件箱)、**spawn_teammate_thread**(启动队友线程)、**inbox 注入**(Lead 接收队友消息并注入 history)。
|
||||
|
||||
子 Agent vs 队友:
|
||||
|
||||
| | s06 子 Agent | s15 队友 |
|
||||
|---|---|---|
|
||||
| 生命周期 | 一次性,用完销毁 | 多轮(教学版限 10 轮,真实 CC 用 idle loop) |
|
||||
| 通信 | 只回传结论 | 异步收件箱,随时通信 |
|
||||
| 上下文 | 完全隔离 | 通过消息共享信息 |
|
||||
| 数量 | 一个主 Agent + 偶尔子 Agent | 一个 Lead + 多个队友 |
|
||||
|
||||
---
|
||||
|
||||
## 工作原理
|
||||
|
||||

|
||||
|
||||
### MessageBus: 文件收件箱
|
||||
|
||||
每个 Agent(包括 Lead 和队友)有一个 `.jsonl` 邮箱。发消息 = 往对方的文件里 append 一行 JSON。读消息 = 读文件 + 删除(消费式):
|
||||
|
||||
```python
|
||||
class MessageBus:
|
||||
def send(self, from_agent: str, to_agent: str,
|
||||
content: str, msg_type: str = "message"):
|
||||
msg = {"from": from_agent, "to": to_agent,
|
||||
"content": content, "type": msg_type,
|
||||
"ts": time.time()}
|
||||
inbox = MAILBOX_DIR / f"{to_agent}.jsonl"
|
||||
with open(inbox, "a") as f:
|
||||
f.write(json.dumps(msg) + "\n")
|
||||
|
||||
def read_inbox(self, agent: str) -> list[dict]:
|
||||
inbox = MAILBOX_DIR / f"{agent}.jsonl"
|
||||
if not inbox.exists():
|
||||
return []
|
||||
msgs = [json.loads(line) for line in inbox.read_text().splitlines()]
|
||||
inbox.unlink() # 消费式:读完删除
|
||||
return msgs
|
||||
```
|
||||
|
||||
为什么用文件而不是内存队列?教学版选文件是因为直观、跨线程可观察。真实 CC 也用文件收件箱(`~/.claude/teams/{team}/inboxes/`),但加了 `proper-lockfile` 防并发写冲突。教学版的 `read_inbox` 有 read + unlink 竞态,多线程同时读可能丢消息,对教学场景可以接受。
|
||||
|
||||
### spawn_teammate_thread: 启动队友
|
||||
|
||||
Lead 调用 `spawn_teammate` 工具启动一个队友。队友跑在自己的 daemon 线程里,有自己的 system prompt、自己的 messages、自己的简化工具集:
|
||||
|
||||
```python
|
||||
def spawn_teammate_thread(name: str, role: str, prompt: str) -> str:
|
||||
system = f"You are '{name}', a {role}. Use tools to complete tasks."
|
||||
|
||||
def run():
|
||||
messages = [{"role": "user", "content": prompt}]
|
||||
sub_tools = [bash, read_file, write_file, send_message]
|
||||
for _ in range(10): # 最多 10 轮
|
||||
inbox = BUS.read_inbox(name)
|
||||
if inbox:
|
||||
messages.append({"role": "user",
|
||||
"content": f"<inbox>{json.dumps(inbox)}</inbox>"})
|
||||
response = client.messages.create(
|
||||
model=MODEL, system=system, messages=messages[-20:],
|
||||
tools=sub_tools, max_tokens=8000)
|
||||
# ... 执行工具、处理结果
|
||||
# 完成后发 summary 给 Lead
|
||||
BUS.send(name, "lead", summary, "result")
|
||||
|
||||
threading.Thread(target=run, daemon=True).start()
|
||||
```
|
||||
|
||||
关键设计:
|
||||
- **队友有简化工具集**:bash、read、write、send_message。教学版省略了任务和 cron,聚焦通信机制。真实 CC 的队友也有 TaskCreate、TaskUpdate 等工具,任务系统是团队共享的
|
||||
- **教学版限 10 轮**:防止队友无限循环。真实 CC 用 idle loop:跑完一轮后发 `idle_notification`,等 inbox 消息,收到后继续,直到 `shutdown_request` 才退出
|
||||
- **完成后自动汇报**:`BUS.send(name, "lead", summary)` 把最终结果发到 Lead 的收件箱
|
||||
|
||||
### Lead 的 inbox 注入
|
||||
|
||||
Lead 在每轮主循环结束后检查收件箱。队友发来的消息注入到 history 里,让 LLM 能看到并做出反应:
|
||||
|
||||
```python
|
||||
# 主循环结束后
|
||||
inbox = BUS.read_inbox("lead")
|
||||
if inbox:
|
||||
inbox_text = "\n".join(
|
||||
f"From {m['from']}: {m['content'][:200]}" for m in inbox)
|
||||
history.append({"role": "user",
|
||||
"content": f"[Inbox]\n{inbox_text}"})
|
||||
```
|
||||
|
||||
教学版在用户输入循环外注入。CC 更精细,Lead 的 `useInboxPoller` 每 1 秒检查一次,有消息就提交为新的 turn,不需要等用户输入。
|
||||
|
||||
### 权限冒泡
|
||||
|
||||
教学版省略了权限冒泡。真实 CC 的流程(`permissionSync.ts`、`useSwarmPermissionPoller.ts`):
|
||||
|
||||
1. 队友遇到需要审批的操作 → 发 `permission_request` 到 Lead 收件箱
|
||||
2. Lead 的 `useInboxPoller` 检测到请求 → 路由到审批队列
|
||||
3. 用户审批后 → Lead 发 `permission_response` 回队友
|
||||
4. 队友的 `useSwarmPermissionPoller`(每 500ms 轮询)收到回复 → 继续或拒绝
|
||||
|
||||
### 合起来跑
|
||||
|
||||
```
|
||||
1. Lead: "搭建后端:一个人搞不定,组队吧"
|
||||
2. Lead → spawn_teammate("alice", "backend dev", "创建数据库 schema")
|
||||
3. Lead → spawn_teammate("bob", "frontend dev", "写 API 客户端")
|
||||
4. alice 线程启动 → 自己的 LLM 调用 → bash "python manage.py migrate"
|
||||
5. bob 线程启动 → 自己的 LLM 调用 → write_file("client.ts", ...)
|
||||
6. alice 完成 → BUS.send("alice", "lead", "Schema done: users, orders tables")
|
||||
7. bob 完成 → BUS.send("bob", "lead", "Client written with types")
|
||||
8. Lead 下次循环 → inbox 注入 history → LLM 看到 alice 和 bob 的结果
|
||||
```
|
||||
|
||||
两个队友并行工作。
|
||||
|
||||
---
|
||||
|
||||
## 相对 s14 的变更
|
||||
|
||||
| 组件 | 之前 (s14) | 之后 (s15) |
|
||||
|------|-----------|-----------|
|
||||
| Agent 数量 | 1 | 1 Lead + N 队友线程 |
|
||||
| 通信 | 无 | MessageBus + .mailboxes/*.jsonl |
|
||||
| 新类 | — | MessageBus, active_teammates dict |
|
||||
| 新函数 | — | spawn_teammate_thread, run_send_message, run_check_inbox |
|
||||
| Lead 工具 | 11 (s14) | + spawn_teammate, send_message, check_inbox (14) |
|
||||
| 队友工具 | — | bash, read_file, write_file, send_message (4) |
|
||||
| 权限 | 本地决策 | 教学版省略(真实 CC 有冒泡机制) |
|
||||
|
||||
---
|
||||
|
||||
## 试一下
|
||||
|
||||
```sh
|
||||
cd learn-claude-code
|
||||
python s15_agent_teams/code.py
|
||||
```
|
||||
|
||||
试试这些 prompt:
|
||||
|
||||
1. `Spawn alice as a backend developer. Ask her to create a file called schema.sql with a users table.`
|
||||
2. `Check your inbox for alice's result.`
|
||||
3. `Spawn bob as a tester. Ask him to check if schema.sql exists and list its contents.`
|
||||
|
||||
观察重点:Lead 如何启动队友?`.mailboxes/` 目录下的 JSONL 文件长什么样?队友完成后 Lead 的 inbox 有没有注入到 history?
|
||||
|
||||
---
|
||||
|
||||
## 接下来
|
||||
|
||||
队友能干活、能通信。但如果 Lead 想让 Alice 关机,直接杀线程会留下写到一半的文件。需要一个体面的关机协议:Lead 发 shutdown_request,队友收尾后退出。
|
||||
|
||||
s16 Team Protocols → 关机握手与消息约定。
|
||||
|
||||
<details>
|
||||
<summary>深入 CC 源码</summary>
|
||||
|
||||
> 以下基于 CC 源码 `spawnMultiAgent.ts`、`useInboxPoller.ts`(969 行)、`useSwarmPermissionPoller.ts`(330 行)、`teammateMailbox.ts`、`teamHelpers.ts` 的完整分析。
|
||||
|
||||
### 一、没有中央消息总线,是文件系统
|
||||
|
||||
教学版用 `MessageBus` 类收发消息。CC 的做法更直接,每个 Agent 直接写其他 Agent 的收件箱文件。
|
||||
|
||||
收件箱路径:`~/.claude/teams/{teamName}/inboxes/{agentName}.json`
|
||||
|
||||
写入时用 `proper-lockfile` 文件锁保证并发安全(最多重试 10 次)。每个文件是一个 JSON 数组,append 新消息时读→追加→写回。
|
||||
|
||||
### 二、15 种消息类型
|
||||
|
||||
CC 的团队通信有 15 种结构化消息(`teammateMailbox.ts`):
|
||||
|
||||
| 类型 | 方向 | 用途 |
|
||||
|------|------|------|
|
||||
| `plain text` | 双向 | 普通队友间通信 |
|
||||
| `idle_notification` | 队友→Lead | 队友完成一轮工作,进入空闲 |
|
||||
| `permission_request` | 队友→Lead | 队友需要操作审批 |
|
||||
| `permission_response` | Lead→队友 | Lead 审批结果 |
|
||||
| `plan_approval_request` | 队友→Lead | 队友提交计划待审 |
|
||||
| `plan_approval_response` | Lead→队友 | Lead 审批计划 |
|
||||
| `shutdown_request` | Lead→队友 | 请求体面关机 |
|
||||
| `shutdown_approved` | 队友→Lead | 确认关机 |
|
||||
| `shutdown_rejected` | 队友→Lead | 拒绝关机(附原因) |
|
||||
| `task_assignment` | Lead→队友 | 分配任务 |
|
||||
| `team_permission_update` | Lead→队友 | 广播权限变更 |
|
||||
| `mode_set_request` | Lead→队友 | 修改队友的权限模式 |
|
||||
| `sandbox_permission_*` | 双向 | 网络权限请求/回复 |
|
||||
| `teammate_terminated` | 系统 | 队友被移除通知 |
|
||||
|
||||
文本消息被包装在 `<teammate-message>` XML 标签中交付给模型。
|
||||
|
||||
### 三、权限冒泡:双向轮询
|
||||
|
||||
教学版省略了权限冒泡。CC 的实际流程(`permissionSync.ts`):
|
||||
|
||||
1. **队友**遇到需要审批的操作 → 发 `permission_request` 到 Lead 的收件箱
|
||||
2. **Lead** 的 `useInboxPoller`(每 1 秒轮询)检测到请求 → 路由到 `ToolUseConfirmQueue`
|
||||
3. Lead 的 UI 显示审批对话框,带队友名字和颜色
|
||||
4. 用户审批后 → Lead 发 `permission_response` 回队友的收件箱
|
||||
5. **队友**的 `useSwarmPermissionPoller`(每 500ms 轮询)收到回复 → 继续或拒绝执行
|
||||
|
||||
### 四、队友生命周期
|
||||
|
||||
CC 的队友由 `spawnTeammate()`(`spawnMultiAgent.ts`)创建:
|
||||
|
||||
1. **Spawn**:创建 tmux 窗格(或进程内),分配颜色,写入 team config
|
||||
2. **Work**:`useInboxPoller` 每 1 秒检查收件箱 → 有消息就提交为新的 turn
|
||||
3. **Idle**:Stop hook 触发 → 发 `idle_notification` 给 Lead
|
||||
4. **Shutdown**:Lead 发 `shutdown_request` → 队友回复 `shutdown_approved` → Lead 清理
|
||||
|
||||
### 五、Team Config
|
||||
|
||||
团队注册表在 `~/.claude/teams/{teamName}/config.json`(`teamHelpers.ts`):
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "my-team",
|
||||
"leadAgentId": "lead@my-team",
|
||||
"members": [{
|
||||
"agentId": "researcher@my-team",
|
||||
"name": "researcher",
|
||||
"agentType": "general-purpose",
|
||||
"color": "blue",
|
||||
"isActive": true
|
||||
}]
|
||||
}
|
||||
```
|
||||
|
||||
队友之间不能嵌套(`AgentTool.tsx:273` 明确禁止 "teammates spawning other teammates")。
|
||||
|
||||
</details>
|
||||
|
||||
<!-- translation-sync: zh@v1, en@v1, ja@v1 -->
|
||||
929
s15_agent_teams/code.py
Normal file
929
s15_agent_teams/code.py
Normal file
@@ -0,0 +1,929 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
s15: Agent Teams — MessageBus + spawn_teammate_thread + inbox injection.
|
||||
|
||||
Run: python s15_agent_teams/code.py
|
||||
Need: pip install anthropic python-dotenv + .env with ANTHROPIC_API_KEY
|
||||
|
||||
Changes from s14:
|
||||
- MessageBus class: file-based mailboxes (.mailboxes/*.jsonl)
|
||||
- spawn_teammate_thread: creates teammate in background thread
|
||||
- Teammate runs own simplified agent_loop (bash, read, write, send_message)
|
||||
- Lead tools: spawn_teammate, send_message, check_inbox (3 new)
|
||||
- Lead inbox: teammate messages injected into history (not just printed)
|
||||
- Teaching version: teammates limited to 10 rounds (real CC uses idle loop)
|
||||
|
||||
ASCII flow:
|
||||
Lead: cron_queue → messages → prompt → LLM → TOOLS ────→ loop
|
||||
↑ ↓ |
|
||||
└── inbox ← MessageBus ← teammate.send_message ←┘
|
||||
Teammate: inbox → LLM → bash/read/write/send → loop (max 10 turns)
|
||||
"""
|
||||
|
||||
import os, subprocess, json, time, random, threading
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
from dataclasses import dataclass, asdict
|
||||
|
||||
try:
|
||||
import readline
|
||||
readline.parse_and_bind('set bind-tty-special-chars off')
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
from anthropic import Anthropic
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv(override=True)
|
||||
if os.getenv("ANTHROPIC_BASE_URL"):
|
||||
os.environ.pop("ANTHROPIC_AUTH_TOKEN", None)
|
||||
|
||||
WORKDIR = Path.cwd()
|
||||
MEMORY_DIR = WORKDIR / ".memory"
|
||||
MEMORY_INDEX = MEMORY_DIR / "MEMORY.md"
|
||||
client = Anthropic(base_url=os.getenv("ANTHROPIC_BASE_URL"))
|
||||
MODEL = os.environ["MODEL_ID"]
|
||||
|
||||
# ── Task System (from s12, synced) ──
|
||||
|
||||
TASKS_DIR = WORKDIR / ".tasks"
|
||||
TASKS_DIR.mkdir(exist_ok=True)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Task:
|
||||
id: str
|
||||
subject: str
|
||||
description: str
|
||||
status: str # pending | in_progress | completed
|
||||
owner: str | None
|
||||
blockedBy: list[str]
|
||||
|
||||
|
||||
def _task_path(task_id: str) -> Path:
|
||||
return TASKS_DIR / f"{task_id}.json"
|
||||
|
||||
|
||||
def create_task(subject: str, description: str = "",
|
||||
blockedBy: list[str] | None = None) -> Task:
|
||||
task = Task(
|
||||
id=f"task_{int(time.time())}_{random.randint(0, 9999):04d}",
|
||||
subject=subject, description=description,
|
||||
status="pending", owner=None,
|
||||
blockedBy=blockedBy or [],
|
||||
)
|
||||
save_task(task)
|
||||
return task
|
||||
|
||||
|
||||
def save_task(task: Task):
|
||||
_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()))
|
||||
|
||||
|
||||
def list_tasks() -> list[Task]:
|
||||
return [Task(**json.loads(p.read_text()))
|
||||
for p in sorted(TASKS_DIR.glob("task_*.json"))]
|
||||
|
||||
|
||||
def get_task(task_id: str) -> str:
|
||||
"""Return full task details as JSON."""
|
||||
task = load_task(task_id)
|
||||
return json.dumps(asdict(task), indent=2)
|
||||
|
||||
|
||||
def can_start(task_id: str) -> bool:
|
||||
"""Check if all blockedBy dependencies are completed.
|
||||
Missing dependencies are treated as blocked."""
|
||||
task = load_task(task_id)
|
||||
for dep_id in task.blockedBy:
|
||||
if not _task_path(dep_id).exists():
|
||||
return False
|
||||
if load_task(dep_id).status != "completed":
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
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)
|
||||
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)]
|
||||
print(f" \033[32m[complete] {task.subject} ✓\033[0m")
|
||||
msg = f"Completed {task.id} ({task.subject})"
|
||||
if unblocked:
|
||||
msg += f"\nUnblocked: {', '.join(unblocked)}"
|
||||
print(f" \033[33m[unblocked] {', '.join(unblocked)}\033[0m")
|
||||
return msg
|
||||
|
||||
|
||||
# ── Prompt Assembly (from s10, synced) ──
|
||||
|
||||
PROMPT_SECTIONS = {
|
||||
"identity": "You are a coding agent. Act, don't explain.",
|
||||
"tools": "Available tools: bash, read_file, write_file, "
|
||||
"get_task, create_task, list_tasks, claim_task, complete_task, "
|
||||
"schedule_cron, list_crons, cancel_cron, "
|
||||
"spawn_teammate, send_message, check_inbox.",
|
||||
"workspace": f"Working directory: {WORKDIR}",
|
||||
"memory": "Relevant memories are injected below when available.",
|
||||
}
|
||||
|
||||
|
||||
def assemble_system_prompt(context: dict) -> str:
|
||||
sections = [PROMPT_SECTIONS["identity"],
|
||||
PROMPT_SECTIONS["tools"],
|
||||
PROMPT_SECTIONS["workspace"]]
|
||||
memories = context.get("memories", "")
|
||||
if memories:
|
||||
sections.append(f"Relevant memories:\n{memories}")
|
||||
return "\n\n".join(sections)
|
||||
|
||||
|
||||
_last_context_key, _last_prompt = None, None
|
||||
|
||||
|
||||
def get_system_prompt(context: dict) -> str:
|
||||
global _last_context_key, _last_prompt
|
||||
key = json.dumps(context, sort_keys=True, ensure_ascii=False, default=str)
|
||||
if key == _last_context_key and _last_prompt:
|
||||
return _last_prompt
|
||||
_last_context_key = key
|
||||
_last_prompt = assemble_system_prompt(context)
|
||||
return _last_prompt
|
||||
|
||||
|
||||
# ── Tools ──
|
||||
|
||||
def safe_path(p: str) -> Path:
|
||||
path = (WORKDIR / p).resolve()
|
||||
if not path.is_relative_to(WORKDIR):
|
||||
raise ValueError(f"Path escapes workspace: {p}")
|
||||
return path
|
||||
|
||||
|
||||
def run_bash(command: str, run_in_background: bool = False) -> str:
|
||||
# run_in_background is handled by agent_loop dispatch, not here
|
||||
try:
|
||||
r = subprocess.run(command, shell=True, cwd=WORKDIR,
|
||||
capture_output=True, text=True, timeout=120)
|
||||
out = (r.stdout + r.stderr).strip()
|
||||
return out[:50000] if out else "(no output)"
|
||||
except subprocess.TimeoutExpired:
|
||||
return "Error: Timeout (120s)"
|
||||
|
||||
|
||||
def run_read(path: str, limit: int | None = None) -> str:
|
||||
try:
|
||||
lines = safe_path(path).read_text().splitlines()
|
||||
if limit and limit < len(lines):
|
||||
lines = lines[:limit] + [f"... ({len(lines) - limit} more lines)"]
|
||||
return "\n".join(lines)
|
||||
except Exception as e:
|
||||
return f"Error: {e}"
|
||||
|
||||
|
||||
def run_write(path: str, content: str) -> str:
|
||||
try:
|
||||
fp = safe_path(path)
|
||||
fp.parent.mkdir(parents=True, exist_ok=True)
|
||||
fp.write_text(content)
|
||||
return f"Wrote {len(content)} bytes to {path}"
|
||||
except Exception as e:
|
||||
return f"Error: {e}"
|
||||
|
||||
|
||||
# Task tools
|
||||
|
||||
def run_create_task(subject: str, description: str = "",
|
||||
blockedBy: list[str] | None = None) -> str:
|
||||
task = create_task(subject, description, blockedBy)
|
||||
deps = f" (blockedBy: {', '.join(blockedBy)})" if blockedBy else ""
|
||||
print(f" \033[34m[create] {task.subject}{deps}\033[0m")
|
||||
return f"Created {task.id}: {task.subject}{deps}"
|
||||
|
||||
|
||||
def run_list_tasks() -> str:
|
||||
tasks = list_tasks()
|
||||
if not tasks:
|
||||
return "No tasks. Use create_task to add some."
|
||||
lines = []
|
||||
for t in tasks:
|
||||
icon = {"pending": "○", "in_progress": "●",
|
||||
"completed": "✓"}.get(t.status, "?")
|
||||
deps = f" (blockedBy: {', '.join(t.blockedBy)})" if t.blockedBy else ""
|
||||
owner = f" [{t.owner}]" if t.owner else ""
|
||||
lines.append(f" {icon} {t.id}: {t.subject} "
|
||||
f"[{t.status}]{owner}{deps}")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def run_get_task(task_id: str) -> str:
|
||||
try:
|
||||
return get_task(task_id)
|
||||
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")
|
||||
|
||||
|
||||
def run_complete_task(task_id: str) -> str:
|
||||
return complete_task(task_id)
|
||||
|
||||
|
||||
# ── Background Tasks (from s13, synced) ──
|
||||
|
||||
_bg_counter = 0
|
||||
background_tasks: dict[str, dict] = {}
|
||||
background_results: dict[str, str] = {}
|
||||
background_lock = threading.Lock()
|
||||
|
||||
|
||||
def is_slow_operation(tool_name: str, tool_input: dict) -> bool:
|
||||
"""Fallback heuristic: commands likely to take > 30s."""
|
||||
if tool_name != "bash":
|
||||
return False
|
||||
cmd = tool_input.get("command", "").lower()
|
||||
slow_keywords = ["install", "build", "test", "deploy", "compile",
|
||||
"docker build", "pip install", "npm install",
|
||||
"cargo build", "pytest", "make"]
|
||||
return any(kw in cmd for kw in slow_keywords)
|
||||
|
||||
|
||||
def should_run_background(tool_name: str, tool_input: dict) -> bool:
|
||||
"""Model explicit request takes priority; fallback to heuristic."""
|
||||
if tool_input.get("run_in_background"):
|
||||
return True
|
||||
return is_slow_operation(tool_name, tool_input)
|
||||
|
||||
|
||||
def execute_tool(block) -> str:
|
||||
"""Execute a tool call block, return output."""
|
||||
handler = {
|
||||
"bash": run_bash, "read_file": run_read, "write_file": run_write,
|
||||
"create_task": run_create_task, "list_tasks": run_list_tasks,
|
||||
"get_task": run_get_task, "claim_task": run_claim_task,
|
||||
"complete_task": run_complete_task,
|
||||
"schedule_cron": run_schedule_cron, "list_crons": run_list_crons,
|
||||
"cancel_cron": run_cancel_cron,
|
||||
"spawn_teammate": run_spawn_teammate,
|
||||
"send_message": run_send_message, "check_inbox": run_check_inbox,
|
||||
}.get(block.name)
|
||||
if handler:
|
||||
return handler(**block.input)
|
||||
return f"Unknown tool: {block.name}"
|
||||
|
||||
|
||||
def start_background_task(block) -> str:
|
||||
"""Run tool in a daemon thread. Returns background task ID."""
|
||||
global _bg_counter
|
||||
_bg_counter += 1
|
||||
bg_id = f"bg_{_bg_counter:04d}"
|
||||
cmd = block.input.get("command", block.name)
|
||||
|
||||
def worker():
|
||||
result = execute_tool(block)
|
||||
with background_lock:
|
||||
background_tasks[bg_id]["status"] = "completed"
|
||||
background_results[bg_id] = result
|
||||
|
||||
with background_lock:
|
||||
background_tasks[bg_id] = {
|
||||
"tool_use_id": block.id,
|
||||
"command": cmd,
|
||||
"status": "running",
|
||||
}
|
||||
threading.Thread(target=worker, daemon=True).start()
|
||||
print(f" \033[33m[background] dispatched {bg_id}: {cmd[:40]}\033[0m")
|
||||
return bg_id
|
||||
|
||||
|
||||
def collect_background_results() -> list[str]:
|
||||
"""Collect completed background results as task_notification messages."""
|
||||
with background_lock:
|
||||
ready_ids = [bid for bid, task in background_tasks.items()
|
||||
if task["status"] == "completed"]
|
||||
notifications = []
|
||||
for bg_id in ready_ids:
|
||||
with background_lock:
|
||||
task = background_tasks.pop(bg_id)
|
||||
output = background_results.pop(bg_id, "")
|
||||
summary = output[:200] if len(output) > 200 else output
|
||||
notifications.append(
|
||||
f"<task_notification>\n"
|
||||
f" <task_id>{bg_id}</task_id>\n"
|
||||
f" <status>completed</status>\n"
|
||||
f" <command>{task['command']}</command>\n"
|
||||
f" <summary>{summary}</summary>\n"
|
||||
f"</task_notification>")
|
||||
print(f" \033[32m[background done] {bg_id}: "
|
||||
f"{task['command'][:40]} ({len(output)} chars)\033[0m")
|
||||
return notifications
|
||||
|
||||
|
||||
# ── Cron Scheduler (from s14, synced) ──
|
||||
|
||||
DURABLE_PATH = WORKDIR / ".scheduled_tasks.json"
|
||||
|
||||
|
||||
@dataclass
|
||||
class CronJob:
|
||||
id: str
|
||||
cron: str # "0 9 * * *"
|
||||
prompt: str # message to inject when fired
|
||||
recurring: bool # True = recurring, False = one-shot
|
||||
durable: bool # True = persist to disk
|
||||
|
||||
|
||||
scheduled_jobs: dict[str, CronJob] = {}
|
||||
cron_queue: list[CronJob] = []
|
||||
cron_lock = threading.Lock()
|
||||
_last_fired: dict[str, str] = {} # job_id → "YYYY-MM-DD HH:MM"
|
||||
|
||||
|
||||
def _cron_field_matches(field: str, value: int) -> bool:
|
||||
"""Match a single cron field against a value."""
|
||||
if field == "*":
|
||||
return True
|
||||
if field.startswith("*/"):
|
||||
step = int(field[2:])
|
||||
return step > 0 and value % step == 0
|
||||
if "," in field:
|
||||
return any(_cron_field_matches(f.strip(), value)
|
||||
for f in field.split(","))
|
||||
if "-" in field:
|
||||
lo, hi = field.split("-", 1)
|
||||
return int(lo) <= value <= int(hi)
|
||||
return value == int(field)
|
||||
|
||||
|
||||
def cron_matches(cron_expr: str, dt: datetime) -> bool:
|
||||
"""Check if a 5-field cron expression matches the given datetime.
|
||||
Standard cron semantics: DOM and DOW use OR when both are constrained."""
|
||||
fields = cron_expr.strip().split()
|
||||
if len(fields) != 5:
|
||||
return False
|
||||
minute, hour, dom, month, dow = fields
|
||||
dow_val = (dt.weekday() + 1) % 7 # Python Monday=0 → cron Sunday=0
|
||||
|
||||
m = _cron_field_matches(minute, dt.minute)
|
||||
h = _cron_field_matches(hour, dt.hour)
|
||||
dom_ok = _cron_field_matches(dom, dt.day)
|
||||
month_ok = _cron_field_matches(month, dt.month)
|
||||
dow_ok = _cron_field_matches(dow, dow_val)
|
||||
|
||||
# Minute, hour, month must all match
|
||||
if not (m and h and month_ok):
|
||||
return False
|
||||
# DOM and DOW: if both constrained, either matching is enough (OR)
|
||||
dom_unconstrained = dom == "*"
|
||||
dow_unconstrained = dow == "*"
|
||||
if dom_unconstrained and dow_unconstrained:
|
||||
return True
|
||||
if dom_unconstrained:
|
||||
return dow_ok
|
||||
if dow_unconstrained:
|
||||
return dom_ok
|
||||
return dom_ok or dow_ok
|
||||
|
||||
|
||||
def _validate_cron_field(field: str, lo: int, hi: int) -> str | None:
|
||||
"""Validate a single cron field value is within [lo, hi]."""
|
||||
if field == "*":
|
||||
return None
|
||||
if field.startswith("*/"):
|
||||
step_str = field[2:]
|
||||
if not step_str.isdigit():
|
||||
return f"Invalid step: {field}"
|
||||
step = int(step_str)
|
||||
if step <= 0:
|
||||
return f"Step must be > 0: {field}"
|
||||
return None
|
||||
if "," in field:
|
||||
for part in field.split(","):
|
||||
err = _validate_cron_field(part.strip(), lo, hi)
|
||||
if err: return err
|
||||
return None
|
||||
if "-" in field:
|
||||
parts = field.split("-", 1)
|
||||
if not parts[0].isdigit() or not parts[1].isdigit():
|
||||
return f"Invalid range: {field}"
|
||||
a, b = int(parts[0]), int(parts[1])
|
||||
if a < lo or a > hi or b < lo or b > hi:
|
||||
return f"Range {field} out of bounds [{lo}-{hi}]"
|
||||
if a > b:
|
||||
return f"Range start > end: {field}"
|
||||
return None
|
||||
if not field.isdigit():
|
||||
return f"Invalid field: {field}"
|
||||
val = int(field)
|
||||
if val < lo or val > hi:
|
||||
return f"Value {val} out of bounds [{lo}-{hi}]"
|
||||
return None
|
||||
|
||||
|
||||
def validate_cron(cron_expr: str) -> str | None:
|
||||
"""Validate a cron expression. Returns error message or None."""
|
||||
fields = cron_expr.strip().split()
|
||||
if len(fields) != 5:
|
||||
return f"Expected 5 fields, got {len(fields)}"
|
||||
bounds = [(0, 59), (0, 23), (1, 31), (1, 12), (0, 6)]
|
||||
names = ["minute", "hour", "day-of-month", "month", "day-of-week"]
|
||||
for i, (field, (lo, hi), name) in enumerate(zip(fields, bounds, names)):
|
||||
err = _validate_cron_field(field, lo, hi)
|
||||
if err:
|
||||
return f"{name}: {err}"
|
||||
return None
|
||||
|
||||
|
||||
def save_durable_jobs():
|
||||
"""Persist durable jobs to .scheduled_tasks.json."""
|
||||
durable = [asdict(j) for j in scheduled_jobs.values() if j.durable]
|
||||
DURABLE_PATH.write_text(json.dumps(durable, indent=2))
|
||||
|
||||
|
||||
def load_durable_jobs():
|
||||
"""Load durable jobs from disk on startup."""
|
||||
if not DURABLE_PATH.exists():
|
||||
return
|
||||
try:
|
||||
jobs = json.loads(DURABLE_PATH.read_text())
|
||||
for j in jobs:
|
||||
job = CronJob(**j)
|
||||
err = validate_cron(job.cron)
|
||||
if err:
|
||||
print(f" \033[31m[cron] skipping invalid job {job.id}: {err}\033[0m")
|
||||
continue
|
||||
scheduled_jobs[job.id] = job
|
||||
valid = [j for j in jobs if j["id"] in scheduled_jobs]
|
||||
if valid:
|
||||
print(f" \033[35m[cron] loaded {len(valid)} durable job(s)\033[0m")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def schedule_job(cron: str, prompt: str, recurring: bool = True,
|
||||
durable: bool = True) -> CronJob | str:
|
||||
"""Register a new cron job. Returns CronJob or error string."""
|
||||
err = validate_cron(cron)
|
||||
if err:
|
||||
return err
|
||||
job = CronJob(
|
||||
id=f"cron_{random.randint(0, 999999):06d}",
|
||||
cron=cron, prompt=prompt,
|
||||
recurring=recurring, durable=durable,
|
||||
)
|
||||
with cron_lock:
|
||||
scheduled_jobs[job.id] = job
|
||||
if durable:
|
||||
save_durable_jobs()
|
||||
print(f" \033[35m[cron register] {job.id} '{cron}' → {prompt[:40]}\033[0m")
|
||||
return job
|
||||
|
||||
|
||||
def cancel_job(job_id: str) -> str:
|
||||
"""Cancel a cron job."""
|
||||
with cron_lock:
|
||||
job = scheduled_jobs.pop(job_id, None)
|
||||
if not job:
|
||||
return f"Job {job_id} not found"
|
||||
if job.durable:
|
||||
save_durable_jobs()
|
||||
print(f" \033[31m[cron cancel] {job_id}\033[0m")
|
||||
return f"Cancelled {job_id}"
|
||||
|
||||
|
||||
def cron_scheduler_loop():
|
||||
"""Independent daemon thread: poll every 1s, fire matching jobs.
|
||||
Individual job errors are caught to prevent one bad job from
|
||||
killing the entire scheduler thread."""
|
||||
while True:
|
||||
time.sleep(1)
|
||||
now = datetime.now()
|
||||
# Date-aware marker prevents daily jobs from skipping on day 2+
|
||||
minute_marker = now.strftime("%Y-%m-%d %H:%M")
|
||||
with cron_lock:
|
||||
for job in list(scheduled_jobs.values()):
|
||||
try:
|
||||
if cron_matches(job.cron, now):
|
||||
if _last_fired.get(job.id) != minute_marker:
|
||||
cron_queue.append(job)
|
||||
_last_fired[job.id] = minute_marker
|
||||
print(f" \033[35m[cron fire] {job.id} → "
|
||||
f"{job.prompt[:40]}\033[0m")
|
||||
if not job.recurring:
|
||||
scheduled_jobs.pop(job.id, None)
|
||||
if job.durable:
|
||||
save_durable_jobs()
|
||||
except Exception as e:
|
||||
print(f" \033[31m[cron error] {job.id}: {e}\033[0m")
|
||||
|
||||
|
||||
def consume_cron_queue() -> list[CronJob]:
|
||||
"""Consume fired jobs from cron_queue (called by agent_loop)."""
|
||||
with cron_lock:
|
||||
fired = list(cron_queue)
|
||||
cron_queue.clear()
|
||||
return fired
|
||||
|
||||
|
||||
# Load durable jobs on startup, then start scheduler thread
|
||||
load_durable_jobs()
|
||||
threading.Thread(target=cron_scheduler_loop, daemon=True).start()
|
||||
print(" \033[35m[cron] scheduler thread started\033[0m")
|
||||
|
||||
|
||||
# Cron tool handlers
|
||||
|
||||
def run_schedule_cron(cron: str, prompt: str,
|
||||
recurring: bool = True, durable: bool = True) -> str:
|
||||
result = schedule_job(cron, prompt, recurring, durable)
|
||||
if isinstance(result, str):
|
||||
return f"Error: {result}"
|
||||
return f"Scheduled {result.id}: '{cron}' → {prompt}"
|
||||
|
||||
|
||||
def run_list_crons() -> str:
|
||||
with cron_lock:
|
||||
jobs = list(scheduled_jobs.values())
|
||||
if not jobs:
|
||||
return "No cron jobs. Use schedule_cron to add one."
|
||||
lines = []
|
||||
for j in jobs:
|
||||
tag = "recurring" if j.recurring else "one-shot"
|
||||
dur = "durable" if j.durable else "session"
|
||||
lines.append(f" {j.id}: '{j.cron}' → {j.prompt[:40]} "
|
||||
f"[{tag}, {dur}]")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def run_cancel_cron(job_id: str) -> str:
|
||||
return cancel_job(job_id)
|
||||
|
||||
|
||||
# ── MessageBus (s15 new) ──
|
||||
# Teaching version uses simple file append + unlink.
|
||||
# Real CC uses proper-lockfile for concurrent write safety.
|
||||
|
||||
MAILBOX_DIR = WORKDIR / ".mailboxes"
|
||||
MAILBOX_DIR.mkdir(exist_ok=True)
|
||||
|
||||
|
||||
class MessageBus:
|
||||
"""File-based message bus. Each agent has a .jsonl inbox.
|
||||
Read is destructive: read_text + unlink (consumes messages).
|
||||
Teaching version: no file locking; real CC uses proper-lockfile."""
|
||||
|
||||
def send(self, from_agent: str, to_agent: str, content: str,
|
||||
msg_type: str = "message"):
|
||||
msg = {"from": from_agent, "to": to_agent,
|
||||
"content": content, "type": msg_type,
|
||||
"ts": time.time()}
|
||||
inbox = MAILBOX_DIR / f"{to_agent}.jsonl"
|
||||
with open(inbox, "a") as f:
|
||||
f.write(json.dumps(msg) + "\n")
|
||||
print(f" \033[33m[bus] {from_agent} → {to_agent}: "
|
||||
f"{content[:50]}\033[0m")
|
||||
|
||||
def read_inbox(self, agent: str) -> list[dict]:
|
||||
inbox = MAILBOX_DIR / f"{agent}.jsonl"
|
||||
if not inbox.exists():
|
||||
return []
|
||||
msgs = [json.loads(line) for line in inbox.read_text().splitlines()
|
||||
if line.strip()]
|
||||
inbox.unlink() # consume: read + delete
|
||||
return msgs
|
||||
|
||||
|
||||
BUS = MessageBus()
|
||||
|
||||
# Track spawned teammates
|
||||
active_teammates: dict[str, bool] = {}
|
||||
|
||||
|
||||
# ── Teammate Thread (s15 new) ──
|
||||
|
||||
def spawn_teammate_thread(name: str, role: str, prompt: str) -> str:
|
||||
"""Spawn a teammate agent in a background thread.
|
||||
Teaching version: max 10 rounds per teammate.
|
||||
Real CC: teammates use idle loop (wait for inbox, work, repeat)
|
||||
until shutdown_request."""
|
||||
if name in active_teammates:
|
||||
return f"Teammate '{name}' already exists"
|
||||
|
||||
system = (f"You are '{name}', a {role}. "
|
||||
f"Use tools to complete tasks. "
|
||||
f"Send results via send_message to 'lead'.")
|
||||
|
||||
def run():
|
||||
messages = [{"role": "user", "content": prompt}]
|
||||
sub_tools = [
|
||||
{"name": "bash", "description": "Run a shell command.",
|
||||
"input_schema": {"type": "object",
|
||||
"properties": {"command": {"type": "string"}},
|
||||
"required": ["command"]}},
|
||||
{"name": "read_file", "description": "Read file contents.",
|
||||
"input_schema": {"type": "object",
|
||||
"properties": {"path": {"type": "string"}},
|
||||
"required": ["path"]}},
|
||||
{"name": "write_file", "description": "Write content to a file.",
|
||||
"input_schema": {"type": "object",
|
||||
"properties": {"path": {"type": "string"},
|
||||
"content": {"type": "string"}},
|
||||
"required": ["path", "content"]}},
|
||||
{"name": "send_message",
|
||||
"description": "Send a message to another agent.",
|
||||
"input_schema": {"type": "object",
|
||||
"properties": {"to": {"type": "string"},
|
||||
"content": {"type": "string"}},
|
||||
"required": ["to", "content"]}},
|
||||
]
|
||||
sub_handlers = {
|
||||
"bash": run_bash, "read_file": run_read, "write_file": run_write,
|
||||
"send_message": lambda to, content: (BUS.send(name, to, content),
|
||||
"Sent")[1],
|
||||
}
|
||||
|
||||
for _ in range(10):
|
||||
inbox = BUS.read_inbox(name)
|
||||
if inbox:
|
||||
messages.append({"role": "user",
|
||||
"content": f"<inbox>{json.dumps(inbox)}</inbox>"})
|
||||
try:
|
||||
response = client.messages.create(
|
||||
model=MODEL, system=system, messages=messages[-20:],
|
||||
tools=sub_tools, max_tokens=8000)
|
||||
except Exception:
|
||||
break
|
||||
messages.append({"role": "assistant", "content": response.content})
|
||||
if response.stop_reason != "tool_use":
|
||||
break
|
||||
results = []
|
||||
for block in response.content:
|
||||
if block.type == "tool_use":
|
||||
handler = sub_handlers.get(block.name)
|
||||
output = handler(**block.input) if handler else "Unknown"
|
||||
results.append({"type": "tool_result",
|
||||
"tool_use_id": block.id,
|
||||
"content": str(output)})
|
||||
messages.append({"role": "user", "content": results})
|
||||
|
||||
# Send final summary to Lead
|
||||
summary = "Done."
|
||||
for msg in reversed(messages):
|
||||
if msg["role"] == "assistant" and isinstance(msg["content"], list):
|
||||
for b in msg["content"]:
|
||||
if getattr(b, "type", None) == "text":
|
||||
summary = b.text
|
||||
break
|
||||
else:
|
||||
continue
|
||||
break
|
||||
BUS.send(name, "lead", summary, "result")
|
||||
active_teammates.pop(name, None)
|
||||
print(f" \033[32m[teammate] {name} finished\033[0m")
|
||||
|
||||
active_teammates[name] = True
|
||||
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}"
|
||||
|
||||
|
||||
# ── Team Tool Handlers (s15 new) ──
|
||||
|
||||
def run_spawn_teammate(name: str, role: str, prompt: str) -> str:
|
||||
return spawn_teammate_thread(name, role, prompt)
|
||||
|
||||
|
||||
def run_send_message(to: str, content: str) -> str:
|
||||
BUS.send("lead", to, content)
|
||||
return f"Sent to {to}"
|
||||
|
||||
|
||||
def run_check_inbox() -> str:
|
||||
msgs = BUS.read_inbox("lead")
|
||||
if not msgs:
|
||||
return "(inbox empty)"
|
||||
lines = []
|
||||
for m in msgs:
|
||||
lines.append(f" [{m['from']}] {m['content'][:200]}")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
# ── Tool Definitions ──
|
||||
|
||||
TOOLS = [
|
||||
{"name": "bash", "description": "Run a shell command.",
|
||||
"input_schema": {"type": "object",
|
||||
"properties": {
|
||||
"command": {"type": "string"},
|
||||
"run_in_background": {"type": "boolean"}},
|
||||
"required": ["command"]}},
|
||||
{"name": "read_file", "description": "Read file contents.",
|
||||
"input_schema": {"type": "object",
|
||||
"properties": {"path": {"type": "string"},
|
||||
"limit": {"type": "integer"}},
|
||||
"required": ["path"]}},
|
||||
{"name": "write_file", "description": "Write content to a file.",
|
||||
"input_schema": {"type": "object",
|
||||
"properties": {"path": {"type": "string"},
|
||||
"content": {"type": "string"}},
|
||||
"required": ["path", "content"]}},
|
||||
{"name": "create_task",
|
||||
"description": "Create a new task with optional blockedBy dependencies.",
|
||||
"input_schema": {"type": "object",
|
||||
"properties": {
|
||||
"subject": {"type": "string"},
|
||||
"description": {"type": "string"},
|
||||
"blockedBy": {"type": "array",
|
||||
"items": {"type": "string"}}},
|
||||
"required": ["subject"]}},
|
||||
{"name": "list_tasks",
|
||||
"description": "List all tasks with status, owner, and dependencies.",
|
||||
"input_schema": {"type": "object", "properties": {},
|
||||
"required": []}},
|
||||
{"name": "get_task",
|
||||
"description": "Get full details of a specific task by ID.",
|
||||
"input_schema": {"type": "object",
|
||||
"properties": {"task_id": {"type": "string"}},
|
||||
"required": ["task_id"]}},
|
||||
{"name": "claim_task",
|
||||
"description": "Claim a pending task. Sets owner, changes status to in_progress.",
|
||||
"input_schema": {"type": "object",
|
||||
"properties": {"task_id": {"type": "string"}},
|
||||
"required": ["task_id"]}},
|
||||
{"name": "complete_task",
|
||||
"description": "Complete an in-progress task. Reports unblocked downstream tasks.",
|
||||
"input_schema": {"type": "object",
|
||||
"properties": {"task_id": {"type": "string"}},
|
||||
"required": ["task_id"]}},
|
||||
{"name": "schedule_cron",
|
||||
"description": "Schedule a cron job. cron is 5-field: min hour dom month dow.",
|
||||
"input_schema": {"type": "object",
|
||||
"properties": {
|
||||
"cron": {"type": "string",
|
||||
"description": "5-field cron expression"},
|
||||
"prompt": {"type": "string",
|
||||
"description": "Message to inject when fired"},
|
||||
"recurring": {"type": "boolean",
|
||||
"description": "True=recurring, False=one-shot"},
|
||||
"durable": {"type": "boolean",
|
||||
"description": "True=persist to disk"}},
|
||||
"required": ["cron", "prompt"]}},
|
||||
{"name": "list_crons",
|
||||
"description": "List all registered cron jobs.",
|
||||
"input_schema": {"type": "object", "properties": {},
|
||||
"required": []}},
|
||||
{"name": "cancel_cron",
|
||||
"description": "Cancel a cron job by ID.",
|
||||
"input_schema": {"type": "object",
|
||||
"properties": {"job_id": {"type": "string"}},
|
||||
"required": ["job_id"]}},
|
||||
{"name": "spawn_teammate",
|
||||
"description": "Spawn a teammate agent in a background thread.",
|
||||
"input_schema": {"type": "object",
|
||||
"properties": {
|
||||
"name": {"type": "string"},
|
||||
"role": {"type": "string"},
|
||||
"prompt": {"type": "string"}},
|
||||
"required": ["name", "role", "prompt"]}},
|
||||
{"name": "send_message",
|
||||
"description": "Send a message to a teammate via MessageBus.",
|
||||
"input_schema": {"type": "object",
|
||||
"properties": {"to": {"type": "string"},
|
||||
"content": {"type": "string"}},
|
||||
"required": ["to", "content"]}},
|
||||
{"name": "check_inbox",
|
||||
"description": "Check Lead's inbox for teammate messages.",
|
||||
"input_schema": {"type": "object", "properties": {},
|
||||
"required": []}},
|
||||
]
|
||||
|
||||
|
||||
# ── Context ──
|
||||
|
||||
def update_context(context: dict, messages: list) -> dict:
|
||||
"""Derive context from real state."""
|
||||
memories = ""
|
||||
if MEMORY_INDEX.exists():
|
||||
content = MEMORY_INDEX.read_text().strip()
|
||||
if content:
|
||||
memories = content
|
||||
return {
|
||||
"enabled_tools": [t["name"] for t in TOOLS],
|
||||
"workspace": str(WORKDIR),
|
||||
"memories": memories,
|
||||
}
|
||||
|
||||
|
||||
# ── Agent Loop ──
|
||||
# Teaching code keeps a basic agent loop. S11's full error recovery is omitted.
|
||||
# Cron queue is consumed when agent_loop is called; real CC auto-wakes via
|
||||
# queue processor (useQueueProcessor.ts) when items arrive.
|
||||
|
||||
def agent_loop(messages: list, context: dict):
|
||||
system = get_system_prompt(context)
|
||||
while True:
|
||||
# Consume fired cron jobs → inject as messages
|
||||
fired = consume_cron_queue()
|
||||
for job in fired:
|
||||
messages.append({"role": "user",
|
||||
"content": f"[Scheduled] {job.prompt}"})
|
||||
print(f" \033[35m[inject cron] {job.prompt[:50]}\033[0m")
|
||||
|
||||
try:
|
||||
response = client.messages.create(
|
||||
model=MODEL, system=system, messages=messages,
|
||||
tools=TOOLS, max_tokens=8000)
|
||||
except Exception as e:
|
||||
messages.append({"role": "assistant", "content": [
|
||||
{"type": "text",
|
||||
"text": f"[Error] {type(e).__name__}: {e}"}]})
|
||||
return
|
||||
|
||||
messages.append({"role": "assistant", "content": response.content})
|
||||
if response.stop_reason != "tool_use":
|
||||
return
|
||||
|
||||
results = []
|
||||
for block in response.content:
|
||||
if block.type != "tool_use":
|
||||
continue
|
||||
print(f"\033[36m> {block.name}\033[0m")
|
||||
|
||||
if should_run_background(block.name, block.input):
|
||||
bg_id = start_background_task(block)
|
||||
results.append({"type": "tool_result",
|
||||
"tool_use_id": block.id,
|
||||
"content": f"[Background task {bg_id} started] "
|
||||
f"Result will be available when complete."})
|
||||
else:
|
||||
output = execute_tool(block)
|
||||
print(str(output)[:300])
|
||||
results.append({"type": "tool_result",
|
||||
"tool_use_id": block.id,
|
||||
"content": output})
|
||||
|
||||
# Merge background notifications + tool results into one user message
|
||||
user_content = []
|
||||
bg_notifications = collect_background_results()
|
||||
if bg_notifications:
|
||||
for notif in bg_notifications:
|
||||
user_content.append({"type": "text", "text": notif})
|
||||
user_content.extend(results)
|
||||
messages.append({"role": "user", "content": user_content})
|
||||
context = update_context(context, messages)
|
||||
system = get_system_prompt(context)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("s15: agent teams")
|
||||
print("Enter a question, press Enter to send. Type q to quit.\n")
|
||||
history = []
|
||||
context = update_context({}, [])
|
||||
while True:
|
||||
try:
|
||||
query = input("\033[36ms15 >> \033[0m")
|
||||
except (EOFError, KeyboardInterrupt):
|
||||
break
|
||||
if query.strip().lower() in ("q", "exit", ""):
|
||||
break
|
||||
history.append({"role": "user", "content": query})
|
||||
agent_loop(history, context)
|
||||
context = update_context(context, history)
|
||||
for block in history[-1]["content"]:
|
||||
if getattr(block, "type", None) == "text":
|
||||
print(block.text)
|
||||
|
||||
# Check inbox for teammate results → inject into history
|
||||
inbox = BUS.read_inbox("lead")
|
||||
if inbox:
|
||||
inbox_text = "\n".join(
|
||||
f"From {m['from']}: {m['content'][:200]}" for m in inbox)
|
||||
history.append({"role": "user",
|
||||
"content": f"[Inbox]\n{inbox_text}"})
|
||||
print(f"\n\033[33m[Inbox: {len(inbox)} messages injected]\033[0m")
|
||||
print()
|
||||
120
s15_agent_teams/images/agent-teams-overview.en.svg
Normal file
120
s15_agent_teams/images/agent-teams-overview.en.svg
Normal file
@@ -0,0 +1,120 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 470" font-family="system-ui, -apple-system, sans-serif">
|
||||
<defs>
|
||||
<linearGradient id="header" x1="0" y1="0" x2="1" y2="0">
|
||||
<stop offset="0%" stop-color="#1e3a5f"/><stop offset="100%" stop-color="#0891b2"/>
|
||||
</linearGradient>
|
||||
<marker id="arrow" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" fill="#555"/>
|
||||
</marker>
|
||||
<marker id="arrow-cyan" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" fill="#0891b2"/>
|
||||
</marker>
|
||||
<marker id="arrow-amber" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" fill="#d97706"/>
|
||||
</marker>
|
||||
<marker id="arrow-green" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" fill="#16a34a"/>
|
||||
</marker>
|
||||
</defs>
|
||||
|
||||
<rect width="760" height="470" fill="#fafbfc" rx="8"/>
|
||||
|
||||
<!-- 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>
|
||||
|
||||
<!-- 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">Real CC detail</text>
|
||||
|
||||
<!-- ===== 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>
|
||||
|
||||
<line x1="98" y1="110" x2="112" y2="110" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
|
||||
<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="187" y1="110" x2="201" y2="110" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
|
||||
<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>
|
||||
|
||||
<line x1="290" y1="110" x2="304" y2="110" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
|
||||
<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>
|
||||
|
||||
<line x1="381" y1="110" x2="395" y2="110" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
|
||||
<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_teammate · send_message · check_inbox</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"/>
|
||||
|
||||
<!-- ===== 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>
|
||||
|
||||
<!-- ===== 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>
|
||||
|
||||
<!-- ===== 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>
|
||||
|
||||
<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="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">Max 10 rounds → summary → BUS.send</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 → summary</text>
|
||||
|
||||
<!-- ===== Row 4: Permission bubbling (real CC detail) ===== -->
|
||||
<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">permission_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">Permission Bubbling (real CC; omitted in teaching code)</text>
|
||||
<text x="80" y="378" fill="#78716c" font-size="9">① Teammate needs approval → MessageBus sends permission_request ② Lead receives → user approval → approve/deny</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 + spawn_teammate_thread + send_message + check_inbox (permission bubbling is a real CC detail)</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 8.8 KiB |
120
s15_agent_teams/images/agent-teams-overview.ja.svg
Normal file
120
s15_agent_teams/images/agent-teams-overview.ja.svg
Normal file
@@ -0,0 +1,120 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 470" font-family="system-ui, -apple-system, sans-serif">
|
||||
<defs>
|
||||
<linearGradient id="header" x1="0" y1="0" x2="1" y2="0">
|
||||
<stop offset="0%" stop-color="#1e3a5f"/><stop offset="100%" stop-color="#0891b2"/>
|
||||
</linearGradient>
|
||||
<marker id="arrow" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" fill="#555"/>
|
||||
</marker>
|
||||
<marker id="arrow-cyan" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" fill="#0891b2"/>
|
||||
</marker>
|
||||
<marker id="arrow-amber" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" fill="#d97706"/>
|
||||
</marker>
|
||||
<marker id="arrow-green" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" fill="#16a34a"/>
|
||||
</marker>
|
||||
</defs>
|
||||
|
||||
<rect width="760" height="470" fill="#fafbfc" rx="8"/>
|
||||
|
||||
<!-- タイトル -->
|
||||
<rect x="0" y="0" width="760" height="44" fill="url(#header)" rx="8"/>
|
||||
<rect x="0" y="36" width="760" height="8" fill="url(#header)"/>
|
||||
<text x="380" y="28" fill="#fff" font-size="14" font-weight="700" text-anchor="middle">Agent Teams — Lead ループ + チームメイトスレッド + MessageBus</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">真实 CC 補足</text>
|
||||
|
||||
<!-- ===== 行 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>
|
||||
|
||||
<line x1="98" y1="110" x2="112" y2="110" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
|
||||
<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="187" y1="110" x2="201" y2="110" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
|
||||
<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>
|
||||
|
||||
<line x1="290" y1="110" x2="304" y2="110" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
|
||||
<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>
|
||||
|
||||
<line x1="381" y1="110" x2="395" y2="110" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
|
||||
<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_teammate · send_message · check_inbox</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"/>
|
||||
|
||||
<!-- ===== 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>
|
||||
|
||||
<!-- ===== 行 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>
|
||||
|
||||
<!-- ===== 行 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>
|
||||
|
||||
<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="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">最大 10 ラウンド → summary → BUS.send</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 → summary</text>
|
||||
|
||||
<!-- ===== 行 4: 権限バブリング(real CC detail) ===== -->
|
||||
<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">permission_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">権限バブリング(真实 CC、教学版は省略)</text>
|
||||
<text x="80" y="378" fill="#78716c" font-size="9">① 承認が必要 → MessageBus が permission_request 送信 ② Lead が受信 → ユーザー承認 → approve/deny</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 + spawn_teammate_thread + send_message + check_inbox(権限バブリングは真实 CC 補足)</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 8.9 KiB |
132
s15_agent_teams/images/agent-teams-overview.svg
Normal file
132
s15_agent_teams/images/agent-teams-overview.svg
Normal file
@@ -0,0 +1,132 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 470" font-family="system-ui, -apple-system, sans-serif">
|
||||
<defs>
|
||||
<linearGradient id="header" x1="0" y1="0" x2="1" y2="0">
|
||||
<stop offset="0%" stop-color="#1e3a5f"/><stop offset="100%" stop-color="#0891b2"/>
|
||||
</linearGradient>
|
||||
<marker id="arrow" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" fill="#555"/>
|
||||
</marker>
|
||||
<marker id="arrow-cyan" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" fill="#0891b2"/>
|
||||
</marker>
|
||||
<marker id="arrow-amber" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" fill="#d97706"/>
|
||||
</marker>
|
||||
<marker id="arrow-green" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" fill="#16a34a"/>
|
||||
</marker>
|
||||
</defs>
|
||||
|
||||
<rect width="760" height="470" fill="#fafbfc" rx="8"/>
|
||||
|
||||
<!-- 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>
|
||||
|
||||
<!-- 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">真实 CC 补充</text>
|
||||
|
||||
<!-- ===== 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="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="98" y1="110" x2="112" y2="110" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
|
||||
<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="187" y1="110" x2="201" y2="110" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
|
||||
<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>
|
||||
|
||||
<line x1="290" y1="110" x2="304" y2="110" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
|
||||
<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>
|
||||
|
||||
<line x1="381" y1="110" x2="395" y2="110" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
|
||||
<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_teammate · send_message · check_inbox</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"/>
|
||||
|
||||
<!-- ===== 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>
|
||||
|
||||
<!-- ===== 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>
|
||||
|
||||
<!-- ===== Row 3: Teammates ===== -->
|
||||
<!-- Bus bottom: y=212, Teammate top: y=248 (gap=36) -->
|
||||
|
||||
<!-- 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">最多 10 轮 → summary → BUS.send</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 → summary</text>
|
||||
|
||||
<!-- ===== Row 4: Permission bubbling (real CC detail) ===== -->
|
||||
<!-- Permission request goes through MessageBus, then Lead check_inbox handles it. -->
|
||||
<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">permission_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">权限冒泡(真实 CC,教学版省略)</text>
|
||||
<text x="80" y="378" fill="#78716c" font-size="9">① 队友需审批 → MessageBus 发送 permission_request ② Lead 收到 → 用户审批 → 回复 approve/deny</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 + spawn_teammate_thread + send_message + check_inbox(权限冒泡见真实 CC 补充)</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 9.3 KiB |
65
s15_agent_teams/images/team-topology.en.svg
Normal file
65
s15_agent_teams/images/team-topology.en.svg
Normal file
@@ -0,0 +1,65 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 720 296" font-family="system-ui, -apple-system, sans-serif">
|
||||
<defs>
|
||||
<linearGradient id="header" x1="0" y1="0" x2="1" y2="0">
|
||||
<stop offset="0%" stop-color="#1e3a5f"/><stop offset="100%" stop-color="#0891b2"/>
|
||||
</linearGradient>
|
||||
<marker id="arrow" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" fill="#555"/>
|
||||
</marker>
|
||||
<marker id="arrow-cyan" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" fill="#0891b2"/>
|
||||
</marker>
|
||||
<marker id="arrow-amber" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" fill="#d97706"/>
|
||||
</marker>
|
||||
</defs>
|
||||
|
||||
<rect width="720" height="296" fill="#fafbfc" rx="8"/>
|
||||
<rect x="0" y="0" width="720" height="38" fill="url(#header)" rx="8"/>
|
||||
<rect x="0" y="30" width="720" height="8" fill="url(#header)"/>
|
||||
<text x="360" y="25" fill="#fff" font-size="14" font-weight="700" text-anchor="middle">Team Topology — Lead ↔ MessageBus ↔ Teammates</text>
|
||||
|
||||
<!-- Lead -->
|
||||
<rect x="260" y="58" width="200" height="68" rx="8" fill="#ecfeff" stroke="#0891b2" stroke-width="2"/>
|
||||
<text x="360" y="82" fill="#0e7490" font-size="13" font-weight="700" text-anchor="middle">Lead Agent</text>
|
||||
<text x="360" y="100" fill="#0e7490" font-size="10" text-anchor="middle">Main loop + spawn + inbox handling</text>
|
||||
<text x="360" y="116" fill="#0e7490" font-size="10" text-anchor="middle">check_inbox receives teammate messages</text>
|
||||
|
||||
<!-- Message Bus -->
|
||||
<rect x="80" y="150" width="560" height="26" rx="13" fill="#fef3c7" stroke="#d97706" stroke-width="1.5"/>
|
||||
<text x="360" y="168" fill="#92400e" font-size="11" font-weight="700" text-anchor="middle">Message Bus (.mailboxes/*.jsonl)</text>
|
||||
|
||||
<!-- Teammates -->
|
||||
<rect x="40" y="214" width="180" height="60" rx="8" fill="#f0fdf4" stroke="#16a34a" stroke-width="1.5"/>
|
||||
<text x="130" y="238" fill="#166534" font-size="12" font-weight="700" text-anchor="middle">Alice (Backend)</text>
|
||||
<text x="130" y="254" fill="#166534" font-size="9" text-anchor="middle">own loop → inbox → work → reply</text>
|
||||
|
||||
<rect x="270" y="214" width="180" height="60" rx="8" fill="#f0fdf4" stroke="#16a34a" stroke-width="1.5"/>
|
||||
<text x="360" y="238" fill="#166534" font-size="12" font-weight="700" text-anchor="middle">Bob (Frontend)</text>
|
||||
<text x="360" y="254" fill="#166534" font-size="9" text-anchor="middle">own loop → inbox → work → reply</text>
|
||||
|
||||
<rect x="500" y="214" width="180" height="60" rx="8" fill="#f0fdf4" stroke="#16a34a" stroke-width="1.5"/>
|
||||
<text x="590" y="238" fill="#166534" font-size="12" font-weight="700" text-anchor="middle">Charlie (QA)</text>
|
||||
<text x="590" y="254" fill="#166534" font-size="9" text-anchor="middle">own loop → inbox → work → reply</text>
|
||||
|
||||
<!-- Lead ↔ Bus -->
|
||||
<line x1="330" y1="126" x2="330" y2="150" stroke="#0891b2" stroke-width="1.5" marker-end="url(#arrow-cyan)"/>
|
||||
<text x="300" y="142" fill="#0891b2" font-size="7">send</text>
|
||||
<line x1="390" y1="150" x2="390" y2="126" stroke="#0891b2" stroke-width="1.5" marker-end="url(#arrow-cyan)"/>
|
||||
<text x="398" y="142" fill="#0891b2" font-size="7">inbox</text>
|
||||
|
||||
<!-- Bus → Teammates -->
|
||||
<line x1="120" y1="176" x2="120" y2="214" stroke="#555" stroke-width="1" marker-end="url(#arrow)"/>
|
||||
<line x1="350" y1="176" x2="350" y2="214" stroke="#555" stroke-width="1" marker-end="url(#arrow)"/>
|
||||
<line x1="570" y1="176" x2="570" y2="214" stroke="#555" stroke-width="1" marker-end="url(#arrow)"/>
|
||||
<text x="82" y="195" fill="#16a34a" font-size="9" font-weight="600">receive</text>
|
||||
<text x="312" y="195" fill="#16a34a" font-size="9" font-weight="600">receive</text>
|
||||
<text x="532" y="195" fill="#16a34a" font-size="9" font-weight="600">receive</text>
|
||||
<line x1="140" y1="214" x2="140" y2="176" stroke="#0891b2" stroke-width="1" stroke-dasharray="4,2" marker-end="url(#arrow-cyan)"/>
|
||||
<line x1="370" y1="214" x2="370" y2="176" stroke="#0891b2" stroke-width="1" stroke-dasharray="4,2" marker-end="url(#arrow-cyan)"/>
|
||||
<line x1="590" y1="214" x2="590" y2="176" stroke="#0891b2" stroke-width="1" stroke-dasharray="4,2" marker-end="url(#arrow-cyan)"/>
|
||||
<text x="145" y="203" fill="#0891b2" font-size="9" font-weight="600">send</text>
|
||||
<text x="375" y="203" fill="#0891b2" font-size="9" font-weight="600">send</text>
|
||||
<text x="595" y="203" fill="#0891b2" font-size="9" font-weight="600">send</text>
|
||||
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.5 KiB |
65
s15_agent_teams/images/team-topology.ja.svg
Normal file
65
s15_agent_teams/images/team-topology.ja.svg
Normal file
@@ -0,0 +1,65 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 720 296" font-family="system-ui, -apple-system, sans-serif">
|
||||
<defs>
|
||||
<linearGradient id="header" x1="0" y1="0" x2="1" y2="0">
|
||||
<stop offset="0%" stop-color="#1e3a5f"/><stop offset="100%" stop-color="#0891b2"/>
|
||||
</linearGradient>
|
||||
<marker id="arrow" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" fill="#555"/>
|
||||
</marker>
|
||||
<marker id="arrow-cyan" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" fill="#0891b2"/>
|
||||
</marker>
|
||||
<marker id="arrow-amber" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" fill="#d97706"/>
|
||||
</marker>
|
||||
</defs>
|
||||
|
||||
<rect width="720" height="296" fill="#fafbfc" rx="8"/>
|
||||
<rect x="0" y="0" width="720" height="38" fill="url(#header)" rx="8"/>
|
||||
<rect x="0" y="30" width="720" height="8" fill="url(#header)"/>
|
||||
<text x="360" y="25" fill="#fff" font-size="14" font-weight="700" text-anchor="middle">Team Topology — Lead ↔ MessageBus ↔ チームメイト</text>
|
||||
|
||||
<!-- Lead -->
|
||||
<rect x="260" y="58" width="200" height="68" rx="8" fill="#ecfeff" stroke="#0891b2" stroke-width="2"/>
|
||||
<text x="360" y="82" fill="#0e7490" font-size="13" font-weight="700" text-anchor="middle">Lead Agent</text>
|
||||
<text x="360" y="100" fill="#0e7490" font-size="10" text-anchor="middle">メインループ + spawn + inbox 処理</text>
|
||||
<text x="360" y="116" fill="#0e7490" font-size="10" text-anchor="middle">check_inbox でチームメイトのメッセージ受信</text>
|
||||
|
||||
<!-- Message Bus -->
|
||||
<rect x="80" y="150" width="560" height="26" rx="13" fill="#fef3c7" stroke="#d97706" stroke-width="1.5"/>
|
||||
<text x="360" y="168" fill="#92400e" font-size="11" font-weight="700" text-anchor="middle">Message Bus (.mailboxes/*.jsonl)</text>
|
||||
|
||||
<!-- チームメイト -->
|
||||
<rect x="40" y="214" width="180" height="60" rx="8" fill="#f0fdf4" stroke="#16a34a" stroke-width="1.5"/>
|
||||
<text x="130" y="238" fill="#166534" font-size="12" font-weight="700" text-anchor="middle">Alice (Backend)</text>
|
||||
<text x="130" y="254" fill="#166534" font-size="9" text-anchor="middle">独立 loop → inbox → 作業 → 返信</text>
|
||||
|
||||
<rect x="270" y="214" width="180" height="60" rx="8" fill="#f0fdf4" stroke="#16a34a" stroke-width="1.5"/>
|
||||
<text x="360" y="238" fill="#166534" font-size="12" font-weight="700" text-anchor="middle">Bob (Frontend)</text>
|
||||
<text x="360" y="254" fill="#166534" font-size="9" text-anchor="middle">独立 loop → inbox → 作業 → 返信</text>
|
||||
|
||||
<rect x="500" y="214" width="180" height="60" rx="8" fill="#f0fdf4" stroke="#16a34a" stroke-width="1.5"/>
|
||||
<text x="590" y="238" fill="#166534" font-size="12" font-weight="700" text-anchor="middle">Charlie (QA)</text>
|
||||
<text x="590" y="254" fill="#166534" font-size="9" text-anchor="middle">独立 loop → inbox → 作業 → 返信</text>
|
||||
|
||||
<!-- Lead ↔ Bus -->
|
||||
<line x1="330" y1="126" x2="330" y2="150" stroke="#0891b2" stroke-width="1.5" marker-end="url(#arrow-cyan)"/>
|
||||
<text x="300" y="142" fill="#0891b2" font-size="7">send</text>
|
||||
<line x1="390" y1="150" x2="390" y2="126" stroke="#0891b2" stroke-width="1.5" marker-end="url(#arrow-cyan)"/>
|
||||
<text x="398" y="142" fill="#0891b2" font-size="7">inbox</text>
|
||||
|
||||
<!-- Bus → チームメイト -->
|
||||
<line x1="120" y1="176" x2="120" y2="214" stroke="#555" stroke-width="1" marker-end="url(#arrow)"/>
|
||||
<line x1="350" y1="176" x2="350" y2="214" stroke="#555" stroke-width="1" marker-end="url(#arrow)"/>
|
||||
<line x1="570" y1="176" x2="570" y2="214" stroke="#555" stroke-width="1" marker-end="url(#arrow)"/>
|
||||
<text x="82" y="195" fill="#16a34a" font-size="9" font-weight="600">receive</text>
|
||||
<text x="312" y="195" fill="#16a34a" font-size="9" font-weight="600">receive</text>
|
||||
<text x="532" y="195" fill="#16a34a" font-size="9" font-weight="600">receive</text>
|
||||
<line x1="140" y1="214" x2="140" y2="176" stroke="#0891b2" stroke-width="1" stroke-dasharray="4,2" marker-end="url(#arrow-cyan)"/>
|
||||
<line x1="370" y1="214" x2="370" y2="176" stroke="#0891b2" stroke-width="1" stroke-dasharray="4,2" marker-end="url(#arrow-cyan)"/>
|
||||
<line x1="590" y1="214" x2="590" y2="176" stroke="#0891b2" stroke-width="1" stroke-dasharray="4,2" marker-end="url(#arrow-cyan)"/>
|
||||
<text x="145" y="203" fill="#0891b2" font-size="9" font-weight="600">send</text>
|
||||
<text x="375" y="203" fill="#0891b2" font-size="9" font-weight="600">send</text>
|
||||
<text x="595" y="203" fill="#0891b2" font-size="9" font-weight="600">send</text>
|
||||
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.6 KiB |
72
s15_agent_teams/images/team-topology.svg
Normal file
72
s15_agent_teams/images/team-topology.svg
Normal file
@@ -0,0 +1,72 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 720 296" font-family="system-ui, -apple-system, sans-serif">
|
||||
<defs>
|
||||
<linearGradient id="header" x1="0" y1="0" x2="1" y2="0">
|
||||
<stop offset="0%" stop-color="#1e3a5f"/><stop offset="100%" stop-color="#0891b2"/>
|
||||
</linearGradient>
|
||||
<marker id="arrow" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" fill="#555"/>
|
||||
</marker>
|
||||
<marker id="arrow-cyan" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" fill="#0891b2"/>
|
||||
</marker>
|
||||
<marker id="arrow-amber" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" fill="#d97706"/>
|
||||
</marker>
|
||||
<marker id="arrow-amber-left" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
|
||||
<path d="M 10 0 L 0 5 L 10 10 z" fill="#d97706"/>
|
||||
</marker>
|
||||
</defs>
|
||||
|
||||
<rect width="720" height="296" fill="#fafbfc" rx="8"/>
|
||||
<rect x="0" y="0" width="720" height="38" fill="url(#header)" rx="8"/>
|
||||
<rect x="0" y="30" width="720" height="8" fill="url(#header)"/>
|
||||
<text x="360" y="25" fill="#fff" font-size="14" font-weight="700" text-anchor="middle">Team Topology — Lead ↔ MessageBus ↔ Teammates</text>
|
||||
|
||||
<!-- Lead: x=260..460, y=58..126 -->
|
||||
<rect x="260" y="58" width="200" height="68" rx="8" fill="#ecfeff" stroke="#0891b2" stroke-width="2"/>
|
||||
<text x="360" y="82" fill="#0e7490" font-size="13" font-weight="700" text-anchor="middle">Lead Agent</text>
|
||||
<text x="360" y="100" fill="#0e7490" font-size="10" text-anchor="middle">主循环 + spawn + inbox 处理</text>
|
||||
<text x="360" y="116" fill="#0e7490" font-size="10" text-anchor="middle">check_inbox 接收队友消息</text>
|
||||
|
||||
<!-- Message Bus: x=80..640, y=150..176 (26px tall) -->
|
||||
<rect x="80" y="150" width="560" height="26" rx="13" fill="#fef3c7" stroke="#d97706" stroke-width="1.5"/>
|
||||
<text x="360" y="168" fill="#92400e" font-size="11" font-weight="700" text-anchor="middle">Message Bus (.mailboxes/*.jsonl)</text>
|
||||
|
||||
<!-- Teammates: y=214..274 (60px tall) -->
|
||||
<rect x="40" y="214" width="180" height="60" rx="8" fill="#f0fdf4" stroke="#16a34a" stroke-width="1.5"/>
|
||||
<text x="130" y="238" fill="#166534" font-size="12" font-weight="700" text-anchor="middle">Alice (Backend)</text>
|
||||
<text x="130" y="254" fill="#166534" font-size="9" text-anchor="middle">独立 loop → inbox → 干活 → 回复</text>
|
||||
|
||||
<rect x="270" y="214" width="180" height="60" rx="8" fill="#f0fdf4" stroke="#16a34a" stroke-width="1.5"/>
|
||||
<text x="360" y="238" fill="#166534" font-size="12" font-weight="700" text-anchor="middle">Bob (Frontend)</text>
|
||||
<text x="360" y="254" fill="#166534" font-size="9" text-anchor="middle">独立 loop → inbox → 干活 → 回复</text>
|
||||
|
||||
<rect x="500" y="214" width="180" height="60" rx="8" fill="#f0fdf4" stroke="#16a34a" stroke-width="1.5"/>
|
||||
<text x="590" y="238" fill="#166534" font-size="12" font-weight="700" text-anchor="middle">Charlie (QA)</text>
|
||||
<text x="590" y="254" fill="#166534" font-size="9" text-anchor="middle">独立 loop → inbox → 干活 → 回复</text>
|
||||
|
||||
<!-- ===== Lead ↔ Bus (gap: 126→150 = 24px) ===== -->
|
||||
<!-- Lead → Bus (send/spawn): x=330 -->
|
||||
<line x1="330" y1="126" x2="330" y2="150" stroke="#0891b2" stroke-width="1.5" marker-end="url(#arrow-cyan)"/>
|
||||
<text x="300" y="142" fill="#0891b2" font-size="7">send</text>
|
||||
<!-- Bus → Lead (inbox delivery): x=390 -->
|
||||
<line x1="390" y1="150" x2="390" y2="126" stroke="#0891b2" stroke-width="1.5" marker-end="url(#arrow-cyan)"/>
|
||||
<text x="398" y="142" fill="#0891b2" font-size="7">inbox</text>
|
||||
|
||||
<!-- ===== Bus → Teammates (gap: 176→214 = 38px) ===== -->
|
||||
<!-- Incoming (solid): from Bus bottom to Teammate top -->
|
||||
<line x1="120" y1="176" x2="120" y2="214" stroke="#555" stroke-width="1" marker-end="url(#arrow)"/>
|
||||
<line x1="350" y1="176" x2="350" y2="214" stroke="#555" stroke-width="1" marker-end="url(#arrow)"/>
|
||||
<line x1="570" y1="176" x2="570" y2="214" stroke="#555" stroke-width="1" marker-end="url(#arrow)"/>
|
||||
<text x="82" y="195" fill="#16a34a" font-size="9" font-weight="600">receive</text>
|
||||
<text x="312" y="195" fill="#16a34a" font-size="9" font-weight="600">receive</text>
|
||||
<text x="532" y="195" fill="#16a34a" font-size="9" font-weight="600">receive</text>
|
||||
<!-- Outgoing / send_message back (dashed, offset 20px right) -->
|
||||
<line x1="140" y1="214" x2="140" y2="176" stroke="#0891b2" stroke-width="1" stroke-dasharray="4,2" marker-end="url(#arrow-cyan)"/>
|
||||
<line x1="370" y1="214" x2="370" y2="176" stroke="#0891b2" stroke-width="1" stroke-dasharray="4,2" marker-end="url(#arrow-cyan)"/>
|
||||
<line x1="590" y1="214" x2="590" y2="176" stroke="#0891b2" stroke-width="1" stroke-dasharray="4,2" marker-end="url(#arrow-cyan)"/>
|
||||
<text x="145" y="203" fill="#0891b2" font-size="9" font-weight="600">send</text>
|
||||
<text x="375" y="203" fill="#0891b2" font-size="9" font-weight="600">send</text>
|
||||
<text x="595" y="203" fill="#0891b2" font-size="9" font-weight="600">send</text>
|
||||
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.1 KiB |
Reference in New Issue
Block a user