mirror of
https://github.com/shareAI-lab/analysis_claude_code.git
synced 2026-06-20 20:23:36 +08:00
* feat: s01-s14 docs quality overhaul — tool pipeline, single-agent, knowledge & resilience Rewrite code.py and README (zh/en/ja) for s01-s14, each chapter building incrementally on the previous. Key fixes across chapters: - s01-s04: agent loop, tool dispatch, permission pipeline, hooks - s05-s08: todo write, subagent, skill loading, context compact - s09-s11: memory system, system prompt assembly, error recovery - s12-s14: task graph, background tasks, cron scheduler All chapters CC source-verified. Code inherits fixes forward (PROMPT_SECTIONS, json.dumps cache, real-state context, can_start dep protection, etc.). * feat: s15-s19 docs quality overhaul — multi-agent platform: teams, protocols, autonomy, worktree, MCP tools Rewrite code.py and README (zh/en/ja) for s15-s19, the multi-agent platform chapters. Each chapter inherits all previous fixes and adds one mechanism: - s15: agent teams (TeamCreate, teammate threads, shared task list) - s16: team protocols (plan approval, shutdown handshake, consume_inbox) - s17: autonomous agents (idle polling, auto-claim, consume_lead_inbox) - s18: worktree isolation (git worktree, bind_task, cwd switching, safety) - s19: MCP tools (MCPClient, normalize_mcp_name, assemble_tool_pool, no cache) All appendix source code references verified against CC source. Config priority corrected: claude.ai < plugin < user < project < local. * fix: 5 regressions across s05-s19 — glob safety, todo validation, memory extraction, protocol types, dep crash - s05-s09: glob results now filter with is_relative_to(WORKDIR) (inherited from s02) - s06-s08: todo_write validates content/status required fields (inherited from s05) - s09: extract_memories uses pre-compression snapshot instead of compacted messages - s16: submit_plan docstring clarifies protocol-only (not code-level gate) - s17-s19: match_response restores type mismatch validation (from s16) - s17-s19: claim_task deps list handles missing dep files without crashing * fix: s12 Todo V2 logic reversal, s14/s15 cron range validation, s18/s19 worktree name validation - s12 README (zh/en/ja): fix Todo V2 direction — interactive defaults to Task, non-interactive/SDK defaults to TodoWrite. Fix env var name to CLAUDE_CODE_ENABLE_TASKS (not TODO_V2). - s14/s15: add _validate_cron_field with per-field range checks (minute 0-59, hour 0-23, dom 1-31, month 1-12, dow 0-6), step > 0, range lo <= hi. Replace old try/except validation that only caught exceptions. - s18/s19: add validate_worktree_name() to remove_worktree and keep_worktree, not just create_worktree. * fix: align s16-s19 teaching tool consistency * fix pr265 chapter diagrams * Add comprehensive s20 harness chapter * Fix chapter smoke test regressions * Clarify README tutorial track transition --------- Co-authored-by: Haoran <bill-billion@outlook.com>
This commit is contained in:
241
s16_team_protocols/README.en.md
Normal file
241
s16_team_protocols/README.en.md
Normal file
@@ -0,0 +1,241 @@
|
||||
# s16: Team Protocols — Teammates Need Agreements
|
||||
|
||||
[中文](README.md) · [English](README.en.md) · [日本語](README.ja.md)
|
||||
|
||||
s01 → ... → s14 → s15 → `s16` → [s17](../s17_autonomous_agents/) → s18 → s19 → s20
|
||||
> *"Teammates need agreements"* — request-response pattern drives all negotiation.
|
||||
>
|
||||
> **Harness Layer**: Protocols — Structured handshakes between agents.
|
||||
|
||||
---
|
||||
|
||||
## The Problem
|
||||
|
||||
s15's teammates can work, but coordination is loose: Lead sends a message, teammate replies, no structured protocol. Two scenarios expose the gap:
|
||||
|
||||
**Shutdown**: Lead wants Alice to shut down. Killing the thread outright leaves half-written files on disk. A handshake is needed: Lead sends a request, Alice confirms after wrapping up.
|
||||
|
||||
**Plan approval**: Bob wants to refactor the auth module, a high-risk operation. Lead should review Bob's plan first, approve before Bob proceeds.
|
||||
|
||||
Both scenarios share the same structure: one side sends a request, the other replies, both linked by the same ID. A state machine tracks: pending → approved / rejected.
|
||||
|
||||
---
|
||||
|
||||
## The Solution
|
||||
|
||||

|
||||
|
||||
Teaching code continues the agent capability arc from earlier chapters and adds structured protocols on top of S15's team communication. To stay focused on the protocol mechanism, it omits full error recovery, memory, and skill systems. Added: **ProtocolState** (request state tracking), **dispatch_message** (routes incoming messages by type to handlers), **match_response** (correlates response to request via request_id, with type validation).
|
||||
|
||||
Two protocols, one mechanism:
|
||||
|
||||
| Protocol | Direction | Purpose |
|
||||
|----------|-----------|---------|
|
||||
| shutdown_request / response | Lead → Teammate | Graceful shutdown handshake |
|
||||
| plan_approval_request / response | Teammate → Lead | Plan approval protocol example |
|
||||
|
||||
> Teaching version demonstrates the request-response message flow for plan approval, but does not implement execution gating (intercepting bash/write_file when not approved). Real CC has a permission gating mechanism for teammates.
|
||||
|
||||
---
|
||||
|
||||
## How It Works
|
||||
|
||||
### ProtocolState: Request State
|
||||
|
||||
Each protocol request creates a state record tracking who sent it, to whom, current status, and payload:
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class ProtocolState:
|
||||
request_id: str # Unique ID, e.g. "req_004281"
|
||||
type: str # "shutdown" | "plan_approval"
|
||||
sender: str # Sender
|
||||
target: str # Recipient
|
||||
status: str # pending | approved | rejected
|
||||
payload: str # Plan text or shutdown reason
|
||||
created_at: float # Timestamp
|
||||
|
||||
pending_requests: dict[str, ProtocolState] = {}
|
||||
```
|
||||
|
||||
A record is created when sending a request, found via `request_id` when receiving a response, and its status updated.
|
||||
|
||||
### Four-Step Protocol Flow
|
||||
|
||||
Using shutdown as an example, the full chain:
|
||||
|
||||
```
|
||||
1. Lead sends request
|
||||
req_id = new_request_id() # "req_004281"
|
||||
pending_requests[req_id] = ProtocolState(type="shutdown", status="pending", ...)
|
||||
BUS.send("lead", "alice", "shutdown_request", metadata={"request_id": req_id})
|
||||
|
||||
2. Teammate receives → dispatch
|
||||
inbox = BUS.read_inbox("alice")
|
||||
msg_type = msg["type"] # "shutdown_request"
|
||||
→ routed to handle_shutdown_request()
|
||||
|
||||
3. Teammate replies
|
||||
BUS.send("alice", "lead", "shutdown_response",
|
||||
metadata={"request_id": req_id, "approve": True})
|
||||
|
||||
4. Lead receives response → match
|
||||
match_response("shutdown_response", req_id, approve=True)
|
||||
pending_requests[req_id].status = "approved"
|
||||
```
|
||||
|
||||
`request_id` is the correlation key across the entire chain: the request carries it out, the response carries it back.
|
||||
|
||||
### dispatch_message: Route by Type
|
||||
|
||||
A teammate's inbox receives both plain messages and protocol messages. `handle_inbox_message` dispatches by message type:
|
||||
|
||||
```python
|
||||
def handle_inbox_message(name, msg, messages):
|
||||
msg_type = msg.get("type", "message")
|
||||
req_id = msg.get("metadata", {}).get("request_id", "")
|
||||
|
||||
if msg_type == "shutdown_request":
|
||||
BUS.send(name, "lead", "Shutting down.", "shutdown_response",
|
||||
{"request_id": req_id, "approve": True})
|
||||
return True # Stop the loop
|
||||
|
||||
if msg_type == "plan_approval_response":
|
||||
approve = msg["metadata"].get("approve", False)
|
||||
messages.append({"role": "user",
|
||||
"content": "[Plan approved]" if approve else "[Plan rejected]"})
|
||||
return False # Continue
|
||||
```
|
||||
|
||||
Adding a new protocol type means adding a new `if` branch.
|
||||
|
||||
### match_response: Type Validation
|
||||
|
||||
`match_response` doesn't just find state by `request_id`, it also validates that the response type matches the request type:
|
||||
|
||||
```python
|
||||
def match_response(response_type, request_id, approve):
|
||||
state = pending_requests.get(request_id)
|
||||
if not state:
|
||||
return
|
||||
if state.type == "shutdown" and response_type != "shutdown_response":
|
||||
return # type mismatch, skip
|
||||
if state.type == "plan_approval" and response_type != "plan_approval_response":
|
||||
return
|
||||
if state.status != "pending":
|
||||
return # already resolved, skip duplicate
|
||||
state.status = "approved" if approve else "rejected"
|
||||
```
|
||||
|
||||
A shutdown_response cannot accidentally approve a plan_approval request.
|
||||
|
||||
### Unified Inbox Consumer: consume_lead_inbox
|
||||
|
||||
Both the `check_inbox` tool and the main loop call the same `consume_lead_inbox()` function, routing protocol messages before returning remaining content. This prevents messages from being consumed without protocol state updates:
|
||||
|
||||
```python
|
||||
def consume_lead_inbox(route_protocol=True) -> list[dict]:
|
||||
msgs = BUS.read_inbox("lead")
|
||||
if route_protocol:
|
||||
for msg in msgs:
|
||||
meta = msg.get("metadata", {})
|
||||
req_id = meta.get("request_id", "")
|
||||
msg_type = msg.get("type", "")
|
||||
if req_id and msg_type.endswith("_response"):
|
||||
match_response(msg_type, req_id, meta.get("approve", False))
|
||||
return msgs
|
||||
```
|
||||
|
||||
The main loop also injects inbox messages into `history` so the LLM can see and react to them.
|
||||
|
||||
### Teammate Idle Loop: Wait Instead of Exit
|
||||
|
||||
s15's teammates exit after 10 rounds. s16's teammates enter idle waiting after the LLM returns a non-tool_use response: poll inbox, respond to shutdown_request and exit, or continue working on new messages.
|
||||
|
||||
```
|
||||
LLM returns non-tool_use
|
||||
→ idle: poll inbox every second
|
||||
→ receives shutdown_request → reply shutdown_response → exit
|
||||
→ receives new message → inject into messages → continue LLM turn
|
||||
```
|
||||
|
||||
Teaching version omits idle_notification to Lead. Real CC sends `idle_notification` when idle, so Lead knows the teammate is free for new tasks.
|
||||
|
||||
### Putting It Together
|
||||
|
||||
```
|
||||
1. Lead: "Have Alice create a file, then shut her down"
|
||||
2. Lead → spawn_teammate("alice", "backend", "Create config.py")
|
||||
3. alice thread starts → write_file("config.py", "...") → done → idle
|
||||
4. Lead → request_shutdown("alice")
|
||||
→ BUS.send("shutdown_request", {request_id: "req_000142"})
|
||||
5. alice idle poll receives → handle_shutdown_request
|
||||
→ BUS.send("shutdown_response", {request_id: "req_000142", approve: True})
|
||||
6. Lead consume_lead_inbox → match_response("req_000142", approve=True)
|
||||
→ pending_requests["req_000142"].status = "approved"
|
||||
→ inbox message injected into history, LLM sees shutdown result
|
||||
```
|
||||
|
||||
Shutdown handshake complete: request → confirm → shutdown. Every step tracked by `request_id`.
|
||||
|
||||
---
|
||||
|
||||
## Changes from s15
|
||||
|
||||
| Component | Before (s15) | After (s16) |
|
||||
|-----------|-------------|-------------|
|
||||
| Coordination | Loose text messages | Structured request-response protocol |
|
||||
| Request tracking | None | ProtocolState + pending_requests dict |
|
||||
| Message routing | All treated as text | dispatch_message routes by type |
|
||||
| Shutdown | Natural exit or kill thread | request_id handshake mechanism |
|
||||
| Plan approval | None | Message flow example (no execution gating) |
|
||||
| New message types | message, result | + shutdown_request/response, plan_approval_request/response |
|
||||
| Teammate lifecycle | Max 10 rounds | Idle loop (waits for inbox messages) |
|
||||
| Lead inbox | check_inbox and main loop read separately | Unified consume_lead_inbox |
|
||||
| Lead tools | 14 (s15) | 14 (core tool set plus request_shutdown, request_plan, review_plan) |
|
||||
| Teammate tools | 4 (s15) | + submit_plan (5) |
|
||||
|
||||
---
|
||||
|
||||
## Try It
|
||||
|
||||
```sh
|
||||
cd learn-claude-code
|
||||
python s16_team_protocols/code.py
|
||||
```
|
||||
|
||||
Try these prompts:
|
||||
|
||||
1. `Spawn alice as a backend dev. Ask her to create a file. Then request her shutdown.`
|
||||
2. `Spawn bob with a refactoring task. Have him submit a plan first. Then review and approve it.`
|
||||
|
||||
What to observe: Is the shutdown handshake complete (request → confirm → shutdown)? Does `pending_requests` state transition correctly? Is `request_id` consistent between request and response? Can the idle teammate receive shutdown_request?
|
||||
|
||||
---
|
||||
|
||||
## What's Next
|
||||
|
||||
In s15-s16, Lead must assign tasks to each teammate. "Alice does this, Bob does that." With 10 unclaimed tasks on the board, Lead has to manually assign each one.
|
||||
|
||||
What if teammates could check the board and claim tasks themselves? Lead only needs to create tasks; teammates discover, claim, and complete them on their own.
|
||||
|
||||
s17 Autonomous Agents → Self-organizing teammates, no leader assignment needed.
|
||||
|
||||
<details>
|
||||
<summary>Deep Dive into CC Source</summary>
|
||||
|
||||
CC's team protocol implementation (`teammateMailbox.ts`, 1184 lines) shares the same core structure as the teaching version: request_id + approve/reject request-response pattern. Differences:
|
||||
|
||||
**Shutdown protocol**: CC's shutdown is three-way communication (`teammateMailbox.ts:720-763`, `SendMessageTool.ts:268-430`). Lead sends `shutdown_request`, teammate replies `shutdown_approved` (or `shutdown_rejected` with reason), system sends `teammate_terminated` to notify all parties. After confirmation, system cleans up pane (tmux/iTerm2), unassigns tasks, removes member from team config (`useInboxPoller.ts:677-800`). Teaching version uses `shutdown_response` as a unified name; real source splits into `shutdown_approved` and `shutdown_rejected` as two separate message types.
|
||||
|
||||
**Plan approval**: In the real source, plan approval request is generated by `ExitPlanModeV2Tool.ts:263-312` when a plan-mode-required teammate exits plan mode. `useInboxPoller.ts:599-661` currently auto-writes approval and passes the request to Lead as context (regular message). `SendMessageTool.ts:434-518` retains explicit approve/reject response capability — approval can simultaneously set `permissionMode` (e.g. "approved but run in plan mode"), response can include `feedback` string for teammate to revise and resubmit. Not a simple "Lead manually uses review_plan tool" flow.
|
||||
|
||||
**Message format**: CC's protocol messages are structured JSON (with Zod schema validation), teaching version uses simple type + metadata dict. Field names are also inconsistent: permission uses `request_id` (`teammateMailbox.ts:453-462`), shutdown and plan approval use `requestId` (`teammateMailbox.ts:684-763`).
|
||||
|
||||
**Execution gating**: CC's teammates have full permission gating. Unapproved high-risk operations are intercepted, not optional. Teaching version only demonstrates the message flow without execution interception.
|
||||
|
||||
**Generality**: Teaching version's single FSM (pending → approved | rejected) maps to two protocols. This simplification is correct. CC's protocol messages all share the same request id correlation mechanism.
|
||||
|
||||
</details>
|
||||
|
||||
<!-- translation-sync: zh@v1, en@v1, ja@v1 -->
|
||||
241
s16_team_protocols/README.ja.md
Normal file
241
s16_team_protocols/README.ja.md
Normal file
@@ -0,0 +1,241 @@
|
||||
# s16: Team Protocols — チームメイト間には取り決めが必要
|
||||
|
||||
[中文](README.md) · [English](README.en.md) · [日本語](README.ja.md)
|
||||
|
||||
s01 → ... → s14 → s15 → `s16` → [s17](../s17_autonomous_agents/) → s18 → s19 → s20
|
||||
> *"チームメイト間には取り決めが必要"* — request-response パターンが全てのネゴシエーションを駆動。
|
||||
>
|
||||
> **Harness 層**: プロトコル — Agent 間の構造化ハンドシェイク。
|
||||
|
||||
---
|
||||
|
||||
## 課題
|
||||
|
||||
s15 のチームメイトは仕事ができるが、連携は緩い:Lead がメッセージを送り、チームメイトが返信するだけで、構造化されたプロトコルがない。2 つのシナリオで問題が露呈する:
|
||||
|
||||
**シャットダウン**:Lead が Alice にシャットダウンを頼む。スレッドを強制終了すると、書きかけのファイルがディスクに残る。ハンドシェイクが必要:Lead がリクエストを送信、Alice が收尾後に確認。
|
||||
|
||||
**計画承認**:Bob が認証モジュールのリファクタリングを提案、高リスク操作。Lead が Bob の計画を確認し、承認後に実行すべき。
|
||||
|
||||
これら 2 つのシナリオは同じ構造:一方がリクエストを送信、もう一方が返信、両者は同じ ID で関連付けられる。状態機械が追跡:pending → approved / rejected。
|
||||
|
||||
---
|
||||
|
||||
## ソリューション
|
||||
|
||||

|
||||
|
||||
教学版は前章までの Agent 能力の流れを受け継ぎ、S15 のチーム通信の上に構造化プロトコルを追加する。プロトコル機構に集中するため、完全なエラーリカバリ、メモリ、スキルシステムは省略。追加:**ProtocolState**(リクエスト状態追跡)、**dispatch_message**(メッセージタイプ別ルーティング)、**match_response**(request_id でリクエストとレスポンスを関連付け、型検証付き)。
|
||||
|
||||
2 つのプロトコル、1 つの仕組み:
|
||||
|
||||
| プロトコル | 方向 | 用途 |
|
||||
|-----------|------|------|
|
||||
| shutdown_request / response | Lead → チームメイト | 丁寧なシャットダウンハンドシェイク |
|
||||
| plan_approval_request / response | チームメイト → Lead | 計画承認プロトコルの例 |
|
||||
|
||||
> 教学版は計画承認の request-response メッセージフローをデモするが、実行ゲーティング(未承認時の bash/write_file 拦截)は未実装。真实 CC にはチームメイト向けの permission gating 機構がある。
|
||||
|
||||
---
|
||||
|
||||
## 仕組み
|
||||
|
||||
### ProtocolState: リクエスト状態
|
||||
|
||||
各プロトコルリクエストは、送信者、受信者、現在の状態、ペイロードを記録する状態レコードを作成:
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class ProtocolState:
|
||||
request_id: str # 一意 ID、例 "req_004281"
|
||||
type: str # "shutdown" | "plan_approval"
|
||||
sender: str # 送信者
|
||||
target: str # 受信者
|
||||
status: str # pending | approved | rejected
|
||||
payload: str # 計画テキストまたはシャットダウン理由
|
||||
created_at: float # タイムスタンプ
|
||||
|
||||
pending_requests: dict[str, ProtocolState] = {}
|
||||
```
|
||||
|
||||
リクエスト送信時にレコードを作成、レスポンス受信時に `request_id` で該当レコードを見つけて状態を更新。
|
||||
|
||||
### 4 ステッププロトコルフロー
|
||||
|
||||
シャットダウンを例にした完全な流れ:
|
||||
|
||||
```
|
||||
1. Lead がリクエスト送信
|
||||
req_id = new_request_id() # "req_004281"
|
||||
pending_requests[req_id] = ProtocolState(type="shutdown", status="pending", ...)
|
||||
BUS.send("lead", "alice", "shutdown_request", metadata={"request_id": req_id})
|
||||
|
||||
2. チームメイト受信 → dispatch
|
||||
inbox = BUS.read_inbox("alice")
|
||||
msg_type = msg["type"] # "shutdown_request"
|
||||
→ handle_shutdown_request() にルーティング
|
||||
|
||||
3. チームメイト返信
|
||||
BUS.send("alice", "lead", "shutdown_response",
|
||||
metadata={"request_id": req_id, "approve": True})
|
||||
|
||||
4. Lead がレスポンス受信 → match
|
||||
match_response("shutdown_response", req_id, approve=True)
|
||||
pending_requests[req_id].status = "approved"
|
||||
```
|
||||
|
||||
`request_id` はチェーン全体を貫く関連キー、リクエストが持ち出し、レスポンスが持ち帰る。
|
||||
|
||||
### dispatch_message: タイプ別ルーティング
|
||||
|
||||
チームメイトの inbox は通常メッセージとプロトコルメッセージの両方を受信。`handle_inbox_message` がメッセージタイプで振り分け:
|
||||
|
||||
```python
|
||||
def handle_inbox_message(name, msg, messages):
|
||||
msg_type = msg.get("type", "message")
|
||||
req_id = msg.get("metadata", {}).get("request_id", "")
|
||||
|
||||
if msg_type == "shutdown_request":
|
||||
BUS.send(name, "lead", "Shutting down.", "shutdown_response",
|
||||
{"request_id": req_id, "approve": True})
|
||||
return True # ループ停止
|
||||
|
||||
if msg_type == "plan_approval_response":
|
||||
approve = msg["metadata"].get("approve", False)
|
||||
messages.append({"role": "user",
|
||||
"content": "[Plan approved]" if approve else "[Plan rejected]"})
|
||||
return False # 継続
|
||||
```
|
||||
|
||||
新しいプロトコルタイプの追加は新しい `if` 分岐を追加するだけ。
|
||||
|
||||
### match_response: 型検証
|
||||
|
||||
`match_response` は `request_id` で状態を見つけるだけでなく、レスポンスタイプがリクエストタイプと一致するか検証:
|
||||
|
||||
```python
|
||||
def match_response(response_type, request_id, approve):
|
||||
state = pending_requests.get(request_id)
|
||||
if not state:
|
||||
return
|
||||
if state.type == "shutdown" and response_type != "shutdown_response":
|
||||
return # タイプ不一致、スキップ
|
||||
if state.type == "plan_approval" and response_type != "plan_approval_response":
|
||||
return
|
||||
if state.status != "pending":
|
||||
return # 既に解決済み、重複をスキップ
|
||||
state.status = "approved" if approve else "rejected"
|
||||
```
|
||||
|
||||
shutdown_response が誤って plan_approval リクエストを承認することはない。
|
||||
|
||||
### 統一 inbox コンシューマ:consume_lead_inbox
|
||||
|
||||
`check_inbox` ツールとメインループ末尾の両方が同じ `consume_lead_inbox()` 関数を呼び出す。プロトコルメッセージを先にルーティングしてから残りの内容を返す。メッセージが消費されてもプロトコル状態が更新されない問題を防ぐ:
|
||||
|
||||
```python
|
||||
def consume_lead_inbox(route_protocol=True) -> list[dict]:
|
||||
msgs = BUS.read_inbox("lead")
|
||||
if route_protocol:
|
||||
for msg in msgs:
|
||||
meta = msg.get("metadata", {})
|
||||
req_id = meta.get("request_id", "")
|
||||
msg_type = msg.get("type", "")
|
||||
if req_id and msg_type.endswith("_response"):
|
||||
match_response(msg_type, req_id, meta.get("approve", False))
|
||||
return msgs
|
||||
```
|
||||
|
||||
メインループは inbox メッセージを `history` に注入し、LLM が確認して反応できるようにする。
|
||||
|
||||
### チームメイト idle loop:終了ではなく待機
|
||||
|
||||
s15 のチームメイトは 10 ラウンドで終了。s16 のチームメイトは LLM が非 tool_use を返した後 idle 待機に入る:inbox をポーリング、shutdown_request に応答して終了、または新メッセージで作業継続。
|
||||
|
||||
```
|
||||
LLM が非 tool_use を返す
|
||||
→ idle: 毎秒 inbox をポーリング
|
||||
→ shutdown_request 受信 → shutdown_response 返信 → 終了
|
||||
→ 新メッセージ受信 → messages に注入 → LLM ターン継続
|
||||
```
|
||||
|
||||
教学版は Lead への idle_notification を省略。真实 CC は idle 時に `idle_notification` を送信、Lead はチームメイトが空いていることを知り、新しいタスクを割り当て可能。
|
||||
|
||||
### 組み合わせて実行
|
||||
|
||||
```
|
||||
1. Lead: "Alice にファイルを作成させ、その後シャットダウン"
|
||||
2. Lead → spawn_teammate("alice", "backend", "config.py を作成")
|
||||
3. alice スレッド起動 → write_file("config.py", "...") → 完了 → idle
|
||||
4. Lead → request_shutdown("alice")
|
||||
→ BUS.send("shutdown_request", {request_id: "req_000142"})
|
||||
5. alice idle ポーリング受信 → handle_shutdown_request
|
||||
→ BUS.send("shutdown_response", {request_id: "req_000142", approve: True})
|
||||
6. Lead consume_lead_inbox → match_response("req_000142", approve=True)
|
||||
→ pending_requests["req_000142"].status = "approved"
|
||||
→ inbox メッセージが history に注入、LLM がシャットダウン結果を確認
|
||||
```
|
||||
|
||||
シャットダウンハンドシェイク完了:リクエスト → 確認 → シャットダウン。各ステップは `request_id` で追跡。
|
||||
|
||||
---
|
||||
|
||||
## s15 からの変更
|
||||
|
||||
| コンポーネント | 変更前 (s15) | 変更後 (s16) |
|
||||
|--------------|------------|------------|
|
||||
| 連携方法 | 緩いテキストメッセージ | 構造化 request-response プロトコル |
|
||||
| リクエスト追跡 | なし | ProtocolState + pending_requests dict |
|
||||
| メッセージルーティング | 全てテキストとして処理 | dispatch_message がタイプ別にルーティング |
|
||||
| シャットダウン | 自然終了またはスレッド強制終了 | request_id ハンドシェイク機構 |
|
||||
| 計画承認 | なし | メッセージフローの例(実行ゲーティングなし) |
|
||||
| 新規メッセージ型 | message, result | + shutdown_request/response, plan_approval_request/response |
|
||||
| チームメイトライフサイクル | 最大 10 ラウンド | idle loop(inbox メッセージを待機) |
|
||||
| Lead inbox | check_inbox とメインループが別々に読み取り | 統一 consume_lead_inbox |
|
||||
| Lead ツール | 14 (s15) | 14(コアツールセットに request_shutdown、request_plan、review_plan を追加) |
|
||||
| チームメイトツール | 4 (s15) | + submit_plan (5) |
|
||||
|
||||
---
|
||||
|
||||
## 試してみる
|
||||
|
||||
```sh
|
||||
cd learn-claude-code
|
||||
python s16_team_protocols/code.py
|
||||
```
|
||||
|
||||
以下のプロンプトを試してください:
|
||||
|
||||
1. `Spawn alice as a backend dev. Ask her to create a file. Then request her shutdown.`
|
||||
2. `Spawn bob with a refactoring task. Have him submit a plan first. Then review and approve it.`
|
||||
|
||||
観察ポイント:シャットダウンハンドシェイクは完了しているか(リクエスト → 確認 → シャットダウン)?`pending_requests` の状態は正しく遷移しているか?`request_id` はリクエストとレスポンス間で一貫しているか?idle チームメイトは shutdown_request を受信できるか?
|
||||
|
||||
---
|
||||
|
||||
## 次の章
|
||||
|
||||
s15-s16 では、Lead が各チームメイトにタスクを割り当てる必要がある。"Alice はこれ、Bob はあれ"。ボードに 10 個の未認領タスクがあれば、Lead が手動で assign しなければならない。
|
||||
|
||||
チームメイトが自分でボードを見て認領できたらどうか?Lead はタスクを作成するだけで、チームメイトが自分で発見、認領、完了する。
|
||||
|
||||
s17 Autonomous Agents → チームメイトの自己組織化、リーダーの割り当て不要。
|
||||
|
||||
<details>
|
||||
<summary>CC ソースコード深掘り</summary>
|
||||
|
||||
CC のチームプロトコル実装(`teammateMailbox.ts`、1184 行)は教学版と同じコア構造:request_id + approve/reject の request-response パターン。違いは以下の通り:
|
||||
|
||||
**シャットダウンプロトコル**:CC のシャットダウンは三方向通信(`teammateMailbox.ts:720-763`、`SendMessageTool.ts:268-430`)。Lead が `shutdown_request` を送信、チームメイトが `shutdown_approved`(または理由付き `shutdown_rejected`)で返信、システムが `teammate_terminated` で全関係者に通知。確認後、システムが自動的に pane(tmux/iTerm2)をクリーンアップ、タスクを unassign、team config からメンバーを削除(`useInboxPoller.ts:677-800`)。教学版は `shutdown_response` で統一命名、真实源码は `shutdown_approved` と `shutdown_rejected` の 2 つの独立したメッセージ型に分割。
|
||||
|
||||
**計画承認**:真实源码では plan approval request は `ExitPlanModeV2Tool.ts:263-312` で plan-mode-required チームメイトが plan mode を終了する際に生成される。`useInboxPoller.ts:599-661` は現在自動的に approval を書き戻し、リクエストを Lead にコンテキスト(regular message)として渡す。`SendMessageTool.ts:434-518` は明示的な approve/reject response 能力を保持、承認時に同時に `permissionMode` を設定可能(例:"承認するが plan mode で実行")、レスポンスにはチームメイトが修正して再提出するための `feedback` 文字列を含めることができる。単純な「Lead が手動で review_plan ツールを使う」フローではない。
|
||||
|
||||
**メッセージ形式**:CC のプロトコルメッセージは構造化 JSON(Zod schema 検証付き)、教学版はシンプルな type + metadata dict。フィールド名も統一されていない:permission は `request_id`(`teammateMailbox.ts:453-462`)、shutdown と plan approval は `requestId`(`teammateMailbox.ts:684-763`)。
|
||||
|
||||
**実行ゲーティング**:CC のチームメイトには完全な permission gating がある。未承認の高リスク操作は拦截され、オプションではない。教学版はメッセージフローのみをデモ。
|
||||
|
||||
**汎用性**:教学版の 1 つの FSM(pending → approved | rejected)が 2 つのプロトコルに対応する簡略化は正しい。CC の全プロトコルメッセージは同じ request id 関連機構を共有。
|
||||
|
||||
</details>
|
||||
|
||||
<!-- translation-sync: zh@v1, en@v1, ja@v1 -->
|
||||
243
s16_team_protocols/README.md
Normal file
243
s16_team_protocols/README.md
Normal file
@@ -0,0 +1,243 @@
|
||||
# s16: Team Protocols — 队友之间要有约定
|
||||
|
||||
[中文](README.md) · [English](README.en.md) · [日本語](README.ja.md)
|
||||
|
||||
s01 → ... → s14 → s15 → `s16` → [s17](../s17_autonomous_agents/) → s18 → s19 → s20
|
||||
> *"队友之间要有约定"* — request-response 模式驱动协商。
|
||||
>
|
||||
> **Harness 层**: 协议 — Agent 之间的结构化握手。
|
||||
|
||||
---
|
||||
|
||||
## 问题
|
||||
|
||||
s15 的队友能干活了,但协调是松散的:Lead 发消息,队友回复,没有结构化的协议。两个场景暴露了问题:
|
||||
|
||||
**关机**:Lead 想让 Alice 关机。直接杀线程,Alice 写了一半的文件留在磁盘上。需要握手:Lead 发请求,Alice 确认收尾后关机。
|
||||
|
||||
**计划审批**:Bob 想重构认证模块,属于高风险操作。应该先让 Lead 看 Bob 的计划,审批通过后再动手。
|
||||
|
||||
这两个场景结构完全一样:一方发请求,另一方给回复,请求和回复通过同一个 ID 关联。有状态机追踪:pending → approved / rejected。
|
||||
|
||||
---
|
||||
|
||||
## 解决方案
|
||||
|
||||

|
||||
|
||||
教学代码承接前面章节的 Agent 能力脉络,在 S15 团队通信基础上加入结构化协议。为了聚焦协议机制,省略了完整错误恢复、记忆和技能系统。新增三样:**ProtocolState**(请求状态追踪)、**dispatch_message**(按消息类型路由到处理器)、**match_response**(通过 request_id 关联回复与请求,含类型校验)。
|
||||
|
||||
两种协议,一套机制:
|
||||
|
||||
| 协议 | 方向 | 用途 |
|
||||
|------|------|------|
|
||||
| shutdown_request / response | Lead → 队友 | 体面关机握手 |
|
||||
| plan_approval_request / response | 队友 → Lead | 计划审批协议示例 |
|
||||
|
||||
> 教学版演示了计划审批的请求-响应消息流程,没有实现执行门控(未 approved 时拦截 bash/write_file)。真实 CC 的队友有 permission gating 机制。
|
||||
|
||||
---
|
||||
|
||||
## 工作原理
|
||||
|
||||
### ProtocolState: 请求状态
|
||||
|
||||
每个协议请求创建一条状态记录,记录谁发的、发给谁、当前状态、附带内容:
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class ProtocolState:
|
||||
request_id: str # 唯一 ID,如 "req_004281"
|
||||
type: str # "shutdown" | "plan_approval"
|
||||
sender: str # 发起方
|
||||
target: str # 接收方
|
||||
status: str # pending | approved | rejected
|
||||
payload: str # 计划文本或关机原因
|
||||
created_at: float # 时间戳
|
||||
|
||||
pending_requests: dict[str, ProtocolState] = {}
|
||||
```
|
||||
|
||||
发请求时创建记录,收回复时通过 `request_id` 找到对应记录,更新状态。
|
||||
|
||||
### 四步协议流程
|
||||
|
||||
以关机为例,完整链路:
|
||||
|
||||
```
|
||||
① Lead 发请求
|
||||
req_id = new_request_id() # "req_004281"
|
||||
pending_requests[req_id] = ProtocolState(type="shutdown", status="pending", ...)
|
||||
BUS.send("lead", "alice", "shutdown_request", metadata={"request_id": req_id})
|
||||
|
||||
② 队友收到 → dispatch
|
||||
inbox = BUS.read_inbox("alice")
|
||||
msg_type = msg["type"] # "shutdown_request"
|
||||
→ 路由到 handle_shutdown_request()
|
||||
|
||||
③ 队友回复
|
||||
BUS.send("alice", "lead", "shutdown_response",
|
||||
metadata={"request_id": req_id, "approve": True})
|
||||
|
||||
④ Lead 收响应 → match
|
||||
match_response("shutdown_response", req_id, approve=True)
|
||||
pending_requests[req_id].status = "approved"
|
||||
```
|
||||
|
||||
`request_id` 是贯穿全链路的关联键,请求带着它出去,回复带着它回来。
|
||||
|
||||
> 教学版用 `shutdown_response` 统一命名(approve 字段区分同意/拒绝)。真实源码拆成 `shutdown_approved` 和 `shutdown_rejected` 两种独立消息类型(`teammateMailbox.ts:720-763`)。
|
||||
|
||||
### dispatch_message: 按类型路由
|
||||
|
||||
队友的 inbox 不只收普通消息,还收协议消息。`handle_inbox_message` 按消息类型分发:
|
||||
|
||||
```python
|
||||
def handle_inbox_message(name, msg, messages):
|
||||
msg_type = msg.get("type", "message")
|
||||
req_id = msg.get("metadata", {}).get("request_id", "")
|
||||
|
||||
if msg_type == "shutdown_request":
|
||||
BUS.send(name, "lead", "Shutting down.", "shutdown_response",
|
||||
{"request_id": req_id, "approve": True})
|
||||
return True # 停止循环
|
||||
|
||||
if msg_type == "plan_approval_response":
|
||||
approve = msg["metadata"].get("approve", False)
|
||||
messages.append({"role": "user",
|
||||
"content": "[Plan approved]" if approve else "[Plan rejected]"})
|
||||
return False # 继续循环
|
||||
```
|
||||
|
||||
新增协议类型只需加新的 `if` 分支。
|
||||
|
||||
### match_response: 类型校验
|
||||
|
||||
`match_response` 不只按 `request_id` 找状态,还会校验响应类型是否匹配请求类型:
|
||||
|
||||
```python
|
||||
def match_response(response_type, request_id, approve):
|
||||
state = pending_requests.get(request_id)
|
||||
if not state:
|
||||
return
|
||||
if state.type == "shutdown" and response_type != "shutdown_response":
|
||||
return # type mismatch, skip
|
||||
if state.type == "plan_approval" and response_type != "plan_approval_response":
|
||||
return
|
||||
if state.status != "pending":
|
||||
return # already resolved, skip duplicate
|
||||
state.status = "approved" if approve else "rejected"
|
||||
```
|
||||
|
||||
一个 shutdown_response 不会意外 approve 一个 plan_approval 请求。
|
||||
|
||||
### 统一 inbox 消费:consume_lead_inbox
|
||||
|
||||
`check_inbox` 工具和主循环末尾都调用同一个 `consume_lead_inbox()` 函数,先路由协议消息再返回剩余内容,避免消息被读走但协议状态没更新:
|
||||
|
||||
```python
|
||||
def consume_lead_inbox(route_protocol=True) -> list[dict]:
|
||||
msgs = BUS.read_inbox("lead")
|
||||
if route_protocol:
|
||||
for msg in msgs:
|
||||
meta = msg.get("metadata", {})
|
||||
req_id = meta.get("request_id", "")
|
||||
msg_type = msg.get("type", "")
|
||||
if req_id and msg_type.endswith("_response"):
|
||||
match_response(msg_type, req_id, meta.get("approve", False))
|
||||
return msgs
|
||||
```
|
||||
|
||||
主循环末尾还会把 inbox 消息注入到 `history`,让 LLM 能看到并做出反应。
|
||||
|
||||
### 队友 idle loop:等待而不是退出
|
||||
|
||||
s15 的队友跑完 10 轮就退出。s16 的队友在 LLM 返回非 tool_use 后进入 idle 等待:轮询 inbox,收到 shutdown_request 就响应退出,收到新消息就继续工作。
|
||||
|
||||
```
|
||||
LLM 返回非 tool_use
|
||||
→ idle: 每秒轮询 inbox
|
||||
→ 收到 shutdown_request → 回复 shutdown_response → 退出
|
||||
→ 收到新消息 → 注入 messages → 继续 LLM turn
|
||||
```
|
||||
|
||||
教学版省略了 idle_notification 给 Lead 的通知。真实 CC 在 idle 时发 `idle_notification`,Lead 收到后知道队友空闲,可以分配新任务。
|
||||
|
||||
### 合起来跑
|
||||
|
||||
```
|
||||
1. Lead: "让 Alice 创建一个文件,然后关机"
|
||||
2. Lead → spawn_teammate("alice", "backend", "创建 config.py")
|
||||
3. alice 线程启动 → write_file("config.py", "...") → 完成 → idle
|
||||
4. Lead → request_shutdown("alice")
|
||||
→ BUS.send("shutdown_request", {request_id: "req_000142"})
|
||||
5. alice idle 轮询收到 → handle_shutdown_request
|
||||
→ BUS.send("shutdown_response", {request_id: "req_000142", approve: True})
|
||||
6. Lead consume_lead_inbox → match_response("req_000142", approve=True)
|
||||
→ pending_requests["req_000142"].status = "approved"
|
||||
→ inbox 消息注入 history,LLM 看到关机结果
|
||||
```
|
||||
|
||||
关机握手完整:请求 → 确认 → 关机。每一步有 `request_id` 追溯。
|
||||
|
||||
---
|
||||
|
||||
## 相对 s15 的变更
|
||||
|
||||
| 组件 | 之前 (s15) | 之后 (s16) |
|
||||
|------|-----------|-----------|
|
||||
| 协调方式 | 松散文本消息 | 结构化请求-响应协议 |
|
||||
| 请求追踪 | 无 | ProtocolState + pending_requests dict |
|
||||
| 消息路由 | 全部当文本处理 | dispatch_message 按类型分发 |
|
||||
| 关机 | 自然退出或杀线程 | request_id 握手机制 |
|
||||
| 计划审批 | 无 | 消息流程示例(未实现执行门控) |
|
||||
| 新消息类型 | message, result | + shutdown_request/response, plan_approval_request/response |
|
||||
| 队友生命周期 | 最多 10 轮 | idle loop(等待 inbox 消息) |
|
||||
| Lead inbox | check_inbox 和主循环分别读 | 统一 consume_lead_inbox |
|
||||
| Lead 工具 | 14 (s15) | 14(核心工具集加入 request_shutdown, request_plan, review_plan) |
|
||||
| 队友工具 | 4 (s15) | + submit_plan (5) |
|
||||
|
||||
---
|
||||
|
||||
## 试一下
|
||||
|
||||
```sh
|
||||
cd learn-claude-code
|
||||
python s16_team_protocols/code.py
|
||||
```
|
||||
|
||||
试试这些 prompt:
|
||||
|
||||
1. `Spawn alice as a backend dev. Ask her to create a file. Then request her shutdown.`
|
||||
2. `Spawn bob with a refactoring task. Have him submit a plan first. Then review and approve it.`
|
||||
|
||||
观察重点:关机握手是否完整(请求 → 确认 → 关机)?`pending_requests` 的状态是否正确转换?`request_id` 是否在请求和响应之间保持一致?队友 idle 后是否能收到 shutdown_request?
|
||||
|
||||
---
|
||||
|
||||
## 接下来
|
||||
|
||||
s15-s16 中,Lead 必须给每个队友分配任务。"Alice 做这个,Bob 做那个"。任务看板上有 10 个未认领的任务,Lead 得手动 assign。
|
||||
|
||||
能不能让队友自己看板、自己认领?Lead 只需要创建任务,队友自己发现、自己认领、自己完成。
|
||||
|
||||
s17 Autonomous Agents → 队友自组织,不需要领导分配。
|
||||
|
||||
<details>
|
||||
<summary>深入 CC 源码</summary>
|
||||
|
||||
CC 的团队协议实现(`teammateMailbox.ts`,1184 行)和教学版在核心结构上一致:request_id + approve/reject 的请求-响应模式。差异在于:
|
||||
|
||||
**关机协议**:CC 的 shutdown 是三向通信(`teammateMailbox.ts:720-763`、`SendMessageTool.ts:268-430`)。Lead 发 `shutdown_request`,队友回复 `shutdown_approved`(或 `shutdown_rejected` 附原因),系统发送 `teammate_terminated` 通知所有相关方。关机确认后系统自动清理 pane(tmux/iTerm2)、unassign 任务、从 team config 移除成员(`useInboxPoller.ts:677-800`)。教学版用 `shutdown_response` 统一命名,真实源码拆成 approved/rejected 两种独立消息。
|
||||
|
||||
**计划审批**:真实源码里 plan approval request 由 `ExitPlanModeV2Tool.ts:263-312` 在 plan-mode-required 队友退出 plan mode 时产生。`useInboxPoller.ts:599-661` 当前会自动回写 approval,并把请求交给 Lead 作为上下文(regular message)。`SendMessageTool.ts:434-518` 仍保留显式 approve/reject response 能力,审批时可同时设置 `permissionMode`(如"批准但以 plan mode 运行"),响应中可包含 `feedback` 字符串供队友修正后重新提交。不是简单的"Lead 手动 review_plan 工具"流程。
|
||||
|
||||
**消息格式**:CC 的协议消息是结构化的 JSON(有 Zod schema 验证),教学版用简单的 type + metadata 字典。字段名也不统一:permission 用 `request_id`(`teammateMailbox.ts:453-462`),shutdown 和 plan approval 用 `requestId`(`teammateMailbox.ts:684-763`)。
|
||||
|
||||
**执行门控**:CC 的队友有完整的 permission gating。未获批准的高风险操作会被拦截,不是可选的。教学版只演示了消息流程,没有实现执行拦截。
|
||||
|
||||
**通用性**:教学版的一个 FSM(pending → approved | rejected)对应两种协议,这个简化完全正确。CC 的所有协议消息共用同一个 request id 关联机制。
|
||||
|
||||
</details>
|
||||
|
||||
<!-- translation-sync: zh@v1, en@v1, ja@v1 -->
|
||||
881
s16_team_protocols/code.py
Normal file
881
s16_team_protocols/code.py
Normal file
@@ -0,0 +1,881 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
s16: Team Protocols — request-response protocol + request_id + dispatch + state machine.
|
||||
|
||||
Run: python s16_team_protocols/code.py
|
||||
Need: pip install anthropic python-dotenv + .env with ANTHROPIC_API_KEY
|
||||
|
||||
Changes from s15:
|
||||
- ProtocolState dataclass (request_id, type, sender, status, created_at)
|
||||
- pending_requests dict: tracks in-flight protocol requests
|
||||
- dispatch_message: routes incoming messages by type to handlers
|
||||
- request_shutdown: Lead sends shutdown protocol request
|
||||
- request_plan: Lead asks teammate to submit plan
|
||||
- handle_shutdown_request / handle_plan_response: teammate receives & responds
|
||||
- match_response: Lead correlates response to request via request_id (with type validation)
|
||||
- Teammate idle loop: waits for inbox messages instead of exiting after 10 rounds
|
||||
- Unified consume_lead_inbox: protocol routing + injection into history
|
||||
- 3 new Lead tools: request_shutdown, request_plan, review_plan
|
||||
- 1 new teammate tool: submit_plan
|
||||
|
||||
ASCII flow:
|
||||
Lead: BUS.send("shutdown_request", {request_id}) ──────→ teammate inbox
|
||||
Teammate: dispatch → handler → BUS.send("shutdown_response", {request_id}) ─→ Lead inbox
|
||||
Lead: consume_lead_inbox → match_response(request_id) → pending_requests[req_id].status = approved
|
||||
"""
|
||||
|
||||
import os, subprocess, json, time, random, threading
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
from dataclasses import dataclass, asdict, field
|
||||
|
||||
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, "
|
||||
"spawn_teammate, send_message, check_inbox, "
|
||||
"request_shutdown, request_plan, review_plan.",
|
||||
"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 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
|
||||
|
||||
|
||||
# ── MessageBus (from s15) ──
|
||||
|
||||
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", metadata: dict = None):
|
||||
msg = {"from": from_agent, "to": to_agent,
|
||||
"content": content, "type": msg_type,
|
||||
"ts": time.time(), "metadata": metadata or {}}
|
||||
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"({msg_type}) {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()
|
||||
active_teammates: dict[str, bool] = {}
|
||||
|
||||
# ── Protocol State (s16 new) ──
|
||||
|
||||
@dataclass
|
||||
class ProtocolState:
|
||||
request_id: str
|
||||
type: str # "shutdown" | "plan_approval"
|
||||
sender: str
|
||||
target: str
|
||||
status: str # pending | approved | rejected
|
||||
payload: str # plan text or shutdown reason
|
||||
created_at: float = field(default_factory=time.time)
|
||||
|
||||
|
||||
pending_requests: dict[str, ProtocolState] = {}
|
||||
|
||||
|
||||
def new_request_id() -> str:
|
||||
return f"req_{random.randint(0, 999999):06d}"
|
||||
|
||||
|
||||
def match_response(response_type: str, request_id: str, approve: bool):
|
||||
"""Correlate a response to the original request via request_id.
|
||||
Validates that response_type matches the request type."""
|
||||
state = pending_requests.get(request_id)
|
||||
if not state:
|
||||
print(f" \033[31m[protocol] unknown request_id: {request_id}\033[0m")
|
||||
return
|
||||
# Validate response type matches request type
|
||||
if state.type == "shutdown" and response_type != "shutdown_response":
|
||||
print(f" \033[31m[protocol] type mismatch: expected shutdown_response, "
|
||||
f"got {response_type}\033[0m")
|
||||
return
|
||||
if state.type == "plan_approval" and response_type != "plan_approval_response":
|
||||
print(f" \033[31m[protocol] type mismatch: expected plan_approval_response, "
|
||||
f"got {response_type}\033[0m")
|
||||
return
|
||||
if state.status != "pending":
|
||||
print(f" \033[33m[protocol] {request_id} already {state.status}, "
|
||||
f"ignoring duplicate\033[0m")
|
||||
return
|
||||
state.status = "approved" if approve else "rejected"
|
||||
icon = "✓" if approve else "✗"
|
||||
color = "32" if approve else "31"
|
||||
print(f" \033[{color}m[protocol] {state.type} {icon} "
|
||||
f"({request_id}: {state.status})\033[0m")
|
||||
|
||||
|
||||
# ── Unified Lead Inbox Consumer (s16 fix) ──
|
||||
# Both check_inbox tool and main loop call this function.
|
||||
# Protocol responses are routed via match_response before returning.
|
||||
|
||||
def consume_lead_inbox(route_protocol: bool = True) -> list[dict]:
|
||||
"""Read Lead's inbox. Route protocol responses, return all messages.
|
||||
Called by both run_check_inbox() and main loop to avoid
|
||||
messages being consumed without protocol routing."""
|
||||
msgs = BUS.read_inbox("lead")
|
||||
if not msgs:
|
||||
return []
|
||||
if route_protocol:
|
||||
for msg in msgs:
|
||||
meta = msg.get("metadata", {})
|
||||
req_id = meta.get("request_id", "")
|
||||
msg_type = msg.get("type", "")
|
||||
if req_id and msg_type.endswith("_response"):
|
||||
approve = meta.get("approve", False)
|
||||
match_response(msg_type, req_id, approve)
|
||||
return msgs
|
||||
|
||||
|
||||
# ── Teammate Thread (s16: idle loop + dispatch) ──
|
||||
|
||||
def spawn_teammate_thread(name: str, role: str, prompt: str) -> str:
|
||||
"""Spawn a teammate agent in a background thread.
|
||||
Uses idle loop: after each LLM turn, waits for inbox messages
|
||||
(shutdown_request, new task) instead of exiting."""
|
||||
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"Check inbox for protocol messages (shutdown_request, etc).")
|
||||
|
||||
def handle_inbox_message(name: str, msg: dict, messages: list) -> bool:
|
||||
"""Dispatch incoming protocol messages by type.
|
||||
Returns True if teammate should stop."""
|
||||
msg_type = msg.get("type", "message")
|
||||
meta = msg.get("metadata", {})
|
||||
req_id = meta.get("request_id", "")
|
||||
|
||||
if msg_type == "shutdown_request":
|
||||
BUS.send(name, "lead", "Shutting down gracefully.",
|
||||
"shutdown_response",
|
||||
{"request_id": req_id, "approve": True})
|
||||
print(f" \033[35m[protocol] {name} approved shutdown "
|
||||
f"({req_id})\033[0m")
|
||||
return True # stop the loop
|
||||
|
||||
if msg_type == "plan_approval_response":
|
||||
approve = meta.get("approve", False)
|
||||
if approve:
|
||||
messages.append({"role": "user",
|
||||
"content": f"[Plan approved] Proceed with the task."})
|
||||
else:
|
||||
messages.append({"role": "user",
|
||||
"content": f"[Plan rejected] Feedback: {msg['content']}"})
|
||||
|
||||
return False # continue
|
||||
|
||||
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.",
|
||||
"input_schema": {"type": "object",
|
||||
"properties": {"path": {"type": "string"}},
|
||||
"required": ["path"]}},
|
||||
{"name": "write_file", "description": "Write file.",
|
||||
"input_schema": {"type": "object",
|
||||
"properties": {"path": {"type": "string"},
|
||||
"content": {"type": "string"}},
|
||||
"required": ["path", "content"]}},
|
||||
{"name": "send_message",
|
||||
"description": "Send message to another agent.",
|
||||
"input_schema": {"type": "object",
|
||||
"properties": {"to": {"type": "string"},
|
||||
"content": {"type": "string"}},
|
||||
"required": ["to", "content"]}},
|
||||
{"name": "submit_plan",
|
||||
"description": "Submit a plan for Lead approval.",
|
||||
"input_schema": {"type": "object",
|
||||
"properties": {"plan": {"type": "string"}},
|
||||
"required": ["plan"]}},
|
||||
]
|
||||
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],
|
||||
"submit_plan": lambda plan: _teammate_submit_plan(name, plan),
|
||||
}
|
||||
|
||||
shutdown_requested = False
|
||||
while not shutdown_requested:
|
||||
# Check inbox for protocol messages
|
||||
inbox = BUS.read_inbox(name)
|
||||
should_stop = False
|
||||
non_protocol = []
|
||||
for msg in inbox:
|
||||
if msg.get("type") in ("shutdown_request", "plan_approval_response"):
|
||||
should_stop = handle_inbox_message(name, msg, messages)
|
||||
if should_stop:
|
||||
break
|
||||
else:
|
||||
non_protocol.append(msg)
|
||||
if should_stop:
|
||||
shutdown_requested = True
|
||||
break
|
||||
if non_protocol:
|
||||
inbox_json = json.dumps(non_protocol)
|
||||
messages.append({"role": "user",
|
||||
"content": "<inbox>" + inbox_json + "</inbox>"})
|
||||
|
||||
# LLM turn
|
||||
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":
|
||||
# Idle: wait for inbox messages instead of exiting
|
||||
# Real CC sends idle_notification to Lead here
|
||||
while not shutdown_requested:
|
||||
time.sleep(1)
|
||||
inbox = BUS.read_inbox(name)
|
||||
if not inbox:
|
||||
continue
|
||||
for msg in inbox:
|
||||
if msg.get("type") in ("shutdown_request", "plan_approval_response"):
|
||||
should_stop = handle_inbox_message(name, msg, messages)
|
||||
if should_stop:
|
||||
shutdown_requested = True
|
||||
break
|
||||
else:
|
||||
non_protocol.append(msg)
|
||||
if shutdown_requested:
|
||||
break
|
||||
if non_protocol:
|
||||
inbox_json = json.dumps(non_protocol)
|
||||
messages.append({"role": "user",
|
||||
"content": "<inbox>" + inbox_json + "</inbox>"})
|
||||
break # back to LLM turn with new messages
|
||||
|
||||
# Execute tool calls
|
||||
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}"
|
||||
|
||||
|
||||
def _teammate_submit_plan(from_name: str, plan: str) -> str:
|
||||
"""Teammate submits a plan to Lead for approval.
|
||||
|
||||
Note: This is a protocol-level request, not a code-level gate.
|
||||
After submitting, the teammate's thread continues running — it can
|
||||
still call bash/write/etc. Real enforcement relies on the model
|
||||
waiting for the approval response before acting. Code-level tool
|
||||
gating would require blocking the teammate's tool dispatch until
|
||||
approval arrives.
|
||||
"""
|
||||
req_id = new_request_id()
|
||||
pending_requests[req_id] = ProtocolState(
|
||||
request_id=req_id, type="plan_approval",
|
||||
sender=from_name, target="lead",
|
||||
status="pending", payload=plan)
|
||||
BUS.send(from_name, "lead", plan,
|
||||
"plan_approval_request",
|
||||
{"request_id": req_id})
|
||||
return f"Plan submitted ({req_id}). Waiting for approval..."
|
||||
|
||||
|
||||
# ── Lead Protocol Tools (s16 new) ──
|
||||
|
||||
def run_request_shutdown(teammate: str) -> str:
|
||||
req_id = new_request_id()
|
||||
pending_requests[req_id] = ProtocolState(
|
||||
request_id=req_id, type="shutdown",
|
||||
sender="lead", target=teammate,
|
||||
status="pending", payload="")
|
||||
BUS.send("lead", teammate, "Please shut down gracefully.",
|
||||
"shutdown_request",
|
||||
{"request_id": req_id})
|
||||
print(f" \033[35m[protocol] shutdown_request → {teammate} "
|
||||
f"({req_id})\033[0m")
|
||||
return f"Shutdown request sent to {teammate} (req: {req_id})"
|
||||
|
||||
|
||||
def run_request_plan(teammate: str, task: str) -> str:
|
||||
"""Lead asks a teammate to submit a plan for a task."""
|
||||
BUS.send("lead", teammate, f"Please submit a plan for: {task}",
|
||||
"message")
|
||||
return f"Asked {teammate} to submit a plan"
|
||||
|
||||
|
||||
def run_review_plan(request_id: str, approve: bool, feedback: str = "") -> str:
|
||||
state = pending_requests.get(request_id)
|
||||
if not state:
|
||||
return f"Request {request_id} not found"
|
||||
if state.status != "pending":
|
||||
return f"Request {request_id} already {state.status}"
|
||||
state.status = "approved" if approve else "rejected"
|
||||
BUS.send("lead", state.sender, feedback or ("Approved" if approve else "Rejected"),
|
||||
"plan_approval_response",
|
||||
{"request_id": request_id, "approve": approve})
|
||||
icon = "✓" if approve else "✗"
|
||||
print(f" \033[32m[protocol] plan {icon} ({request_id})\033[0m")
|
||||
return f"Plan {'approved' if approve else 'rejected'} ({request_id})"
|
||||
|
||||
|
||||
# ── Other Lead Tool Handlers ──
|
||||
|
||||
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:
|
||||
"""Check Lead's inbox. Routes protocol responses via match_response."""
|
||||
msgs = consume_lead_inbox(route_protocol=True)
|
||||
if not msgs:
|
||||
return "(inbox empty)"
|
||||
lines = []
|
||||
for m in msgs:
|
||||
meta = m.get("metadata", {})
|
||||
req_id = meta.get("request_id", "")
|
||||
tag = f" [{m['type']} req:{req_id}]" if req_id else f" [{m['type']}]"
|
||||
lines.append(f" [{m['from']}]{tag} {m['content'][:200]}")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
# ── Tool Dispatch ──
|
||||
|
||||
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,
|
||||
"spawn_teammate": run_spawn_teammate,
|
||||
"send_message": run_send_message, "check_inbox": run_check_inbox,
|
||||
"request_shutdown": run_request_shutdown,
|
||||
"request_plan": run_request_plan, "review_plan": run_review_plan,
|
||||
}.get(block.name)
|
||||
if handler:
|
||||
return handler(**block.input)
|
||||
return f"Unknown tool: {block.name}"
|
||||
|
||||
|
||||
# ── 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": "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 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. Routes protocol responses automatically.",
|
||||
"input_schema": {"type": "object", "properties": {},
|
||||
"required": []}},
|
||||
{"name": "request_shutdown",
|
||||
"description": "Request a teammate to shut down gracefully.",
|
||||
"input_schema": {"type": "object",
|
||||
"properties": {"teammate": {"type": "string"}},
|
||||
"required": ["teammate"]}},
|
||||
{"name": "request_plan",
|
||||
"description": "Ask a teammate to submit a plan for review.",
|
||||
"input_schema": {"type": "object",
|
||||
"properties": {"teammate": {"type": "string"},
|
||||
"task": {"type": "string"}},
|
||||
"required": ["teammate", "task"]}},
|
||||
{"name": "review_plan",
|
||||
"description": "Approve or reject a submitted plan by request_id.",
|
||||
"input_schema": {"type": "object",
|
||||
"properties": {
|
||||
"request_id": {"type": "string"},
|
||||
"approve": {"type": "boolean"},
|
||||
"feedback": {"type": "string"}},
|
||||
"required": ["request_id", "approve"]}},
|
||||
]
|
||||
|
||||
|
||||
# ── 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 ──
|
||||
|
||||
def agent_loop(messages: list, context: dict):
|
||||
system = get_system_prompt(context)
|
||||
while True:
|
||||
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("s16: team protocols")
|
||||
print("Enter a question, press Enter to send. Type q to quit.\n")
|
||||
history = []
|
||||
context = update_context({}, [])
|
||||
while True:
|
||||
try:
|
||||
query = input("\033[36ms16 >> \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 → route protocol + inject into history
|
||||
inbox_msgs = consume_lead_inbox(route_protocol=True)
|
||||
if inbox_msgs:
|
||||
inbox_text = "\n".join(
|
||||
f"From {m['from']}: {m['content'][:200]}" for m in inbox_msgs)
|
||||
history.append({"role": "user",
|
||||
"content": f"[Inbox]\n{inbox_text}"})
|
||||
print(f"\n\033[33m[Inbox: {len(inbox_msgs)} messages injected]\033[0m")
|
||||
print()
|
||||
143
s16_team_protocols/images/team-protocols-overview.en.svg
Normal file
143
s16_team_protocols/images/team-protocols-overview.en.svg
Normal file
@@ -0,0 +1,143 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 500" font-family="system-ui, -apple-system, sans-serif">
|
||||
<defs>
|
||||
<linearGradient id="header" x1="0" y1="0" x2="1" y2="0">
|
||||
<stop offset="0%" stop-color="#1e3a5f"/><stop offset="100%" stop-color="#7c3aed"/>
|
||||
</linearGradient>
|
||||
<marker id="arrow" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" fill="#555"/>
|
||||
</marker>
|
||||
<marker id="arrow-purple" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" fill="#7c3aed"/>
|
||||
</marker>
|
||||
<marker id="arrow-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-green" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" fill="#16a34a"/>
|
||||
</marker>
|
||||
<marker id="arrow-red" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" fill="#dc2626"/>
|
||||
</marker>
|
||||
</defs>
|
||||
|
||||
<rect width="760" height="500" 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">Team Protocols — Request-Response + request_id Correlation + State Machine</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">s15 Preserved</text>
|
||||
<rect x="160" y="56" width="12" height="10" rx="2" fill="#f5f3ff" stroke="#7c3aed" stroke-width="1"/>
|
||||
<text x="178" y="66" fill="#7c3aed" font-size="10" font-weight="600">s16 New</text>
|
||||
|
||||
<!-- ===== Row 1: Lead Loop ===== -->
|
||||
<rect x="20" y="90" width="70" height="40" rx="8" fill="#eef2ff" stroke="#4f46e5" stroke-width="1.5"/>
|
||||
<text x="55" y="114" fill="#4f46e5" font-size="8" font-weight="600" text-anchor="middle">turn</text>
|
||||
|
||||
<line x1="90" y1="110" x2="104" y2="110" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
|
||||
<rect x="107" y="90" width="70" height="40" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
|
||||
<text x="142" y="114" fill="#1e3a5f" font-size="10" font-weight="600" text-anchor="middle">messages</text>
|
||||
|
||||
<line x1="177" y1="110" x2="191" y2="110" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
|
||||
<rect x="194" y="86" width="80" height="48" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
|
||||
<text x="234" y="114" fill="#1e3a5f" font-size="9" font-weight="600" text-anchor="middle">prompt</text>
|
||||
|
||||
<line x1="274" y1="110" x2="288" y2="110" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
|
||||
<rect x="291" y="86" width="70" height="48" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
|
||||
<text x="326" y="114" fill="#1e3a5f" font-size="9" font-weight="600" text-anchor="middle">LLM</text>
|
||||
|
||||
<line x1="361" y1="110" x2="375" y2="110" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
|
||||
<rect x="378" y="80" width="356" height="60" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
|
||||
<text x="556" y="98" fill="#1e3a5f" font-size="10" font-weight="600" text-anchor="middle">TOOL DISPATCH (core tool set)</text>
|
||||
<text x="394" y="114" fill="#2563eb" font-size="8">bash · read · write · task(4) · spawn · send · inbox</text>
|
||||
<text x="394" y="128" fill="#7c3aed" font-size="8" font-weight="700">★ request_shutdown · request_plan · review_plan</text>
|
||||
|
||||
<!-- Loop back -->
|
||||
<path d="M 734 110 L 748 110 L 748 150 L 55 150 L 55 130" fill="none" stroke="#94a3b8" stroke-width="1" marker-end="url(#arrow)" stroke-dasharray="5,4"/>
|
||||
|
||||
<!-- ===== Row 2: Protocol Flow (purple) — request_id lifecycle ===== -->
|
||||
<rect x="30" y="176" width="700" height="100" rx="8" fill="#f5f3ff" stroke="#7c3aed" stroke-width="2"/>
|
||||
<text x="380" y="196" fill="#4c1d95" font-size="11" font-weight="700" text-anchor="middle">Request-Response Protocol Flow (request_id throughout)</text>
|
||||
|
||||
<!-- Step 1: Send request -->
|
||||
<rect x="50" y="208" width="130" height="52" rx="6" fill="#fff" stroke="#7c3aed" stroke-width="1"/>
|
||||
<text x="115" y="226" fill="#7c3aed" font-size="9" font-weight="600" text-anchor="middle">① Lead sends request</text>
|
||||
<text x="115" y="240" fill="#6b7280" font-size="7" text-anchor="middle">BUS.send("shutdown_request"</text>
|
||||
<text x="115" y="252" fill="#6b7280" font-size="7" text-anchor="middle">metadata={request_id})</text>
|
||||
|
||||
<line x1="180" y1="234" x2="206" y2="234" stroke="#7c3aed" stroke-width="1.5" marker-end="url(#arrow-purple)"/>
|
||||
|
||||
<!-- Step 2: Teammate receives & dispatches -->
|
||||
<rect x="209" y="208" width="130" height="52" rx="6" fill="#fff" stroke="#7c3aed" stroke-width="1"/>
|
||||
<text x="274" y="226" fill="#7c3aed" font-size="9" font-weight="600" text-anchor="middle">② Teammate receives</text>
|
||||
<text x="274" y="240" fill="#6b7280" font-size="7" text-anchor="middle">dispatch_by_type(inbox)</text>
|
||||
<text x="274" y="252" fill="#6b7280" font-size="7" text-anchor="middle">→ handler(type, metadata)</text>
|
||||
|
||||
<line x1="339" y1="234" x2="365" y2="234" stroke="#7c3aed" stroke-width="1.5" marker-end="url(#arrow-purple)"/>
|
||||
|
||||
<!-- Step 3: Teammate processes & responds -->
|
||||
<rect x="368" y="208" width="130" height="52" rx="6" fill="#fff" stroke="#7c3aed" stroke-width="1"/>
|
||||
<text x="433" y="226" fill="#7c3aed" font-size="9" font-weight="600" text-anchor="middle">③ Teammate responds</text>
|
||||
<text x="433" y="240" fill="#6b7280" font-size="7" text-anchor="middle">BUS.send("shutdown_response"</text>
|
||||
<text x="433" y="252" fill="#6b7280" font-size="7" text-anchor="middle">same request_id + approve)</text>
|
||||
|
||||
<line x1="498" y1="234" x2="524" y2="234" stroke="#7c3aed" stroke-width="1.5" marker-end="url(#arrow-purple)"/>
|
||||
|
||||
<!-- Step 4: Lead receives response -->
|
||||
<rect x="527" y="208" width="130" height="52" rx="6" fill="#fff" stroke="#7c3aed" stroke-width="1"/>
|
||||
<text x="592" y="226" fill="#7c3aed" font-size="9" font-weight="600" text-anchor="middle">④ Lead receives</text>
|
||||
<text x="592" y="240" fill="#6b7280" font-size="7" text-anchor="middle">match_response(request_id)</text>
|
||||
<text x="592" y="252" fill="#6b7280" font-size="7" text-anchor="middle">→ resolve/reject callback</text>
|
||||
|
||||
<!-- ===== Row 3: State Machine + Storage ===== -->
|
||||
<!-- State Machine -->
|
||||
<rect x="30" y="296" width="340" height="85" rx="8" fill="#f5f3ff" stroke="#7c3aed" stroke-width="1.5"/>
|
||||
<text x="200" y="316" fill="#4c1d95" font-size="10" font-weight="700" text-anchor="middle">State Machine (same for both protocols)</text>
|
||||
|
||||
<rect x="55" y="330" width="58" height="20" rx="4" fill="#f1f5f9" stroke="#94a3b8" stroke-width="1"/>
|
||||
<text x="84" y="344" fill="#475569" font-size="9" text-anchor="middle">pending</text>
|
||||
|
||||
<line x1="113" y1="340" x2="192" y2="340" stroke="#16a34a" stroke-width="1.5" marker-end="url(#arrow-green)"/>
|
||||
<text x="152" y="335" fill="#16a34a" font-size="8" font-weight="600" text-anchor="middle">approve</text>
|
||||
|
||||
<rect x="192" y="330" width="62" height="20" rx="4" fill="#dcfce7" stroke="#16a34a" stroke-width="1"/>
|
||||
<text x="223" y="344" fill="#166534" font-size="9" text-anchor="middle">approved</text>
|
||||
|
||||
<path d="M 84 350 L 84 368 L 128 368" fill="none" stroke="#dc2626" stroke-width="1.5" marker-end="url(#arrow-red)"/>
|
||||
<text x="76" y="362" fill="#dc2626" font-size="8" font-weight="600" text-anchor="end">reject</text>
|
||||
|
||||
<rect x="128" y="358" width="60" height="20" rx="4" fill="#fef2f2" stroke="#dc2626" stroke-width="1"/>
|
||||
<text x="158" y="372" fill="#991b1b" font-size="9" text-anchor="middle">rejected</text>
|
||||
|
||||
<!-- pending_requests storage -->
|
||||
<rect x="400" y="296" width="330" height="85" rx="8" fill="#f5f3ff" stroke="#7c3aed" stroke-width="1.5"/>
|
||||
<text x="565" y="316" fill="#4c1d95" font-size="10" font-weight="700" text-anchor="middle">pending_requests Storage</text>
|
||||
<text x="420" y="336" fill="#7c3aed" font-size="9">pending_requests: dict[str, ProtocolState]</text>
|
||||
<text x="420" y="352" fill="#6b7280" font-size="8">request_id → {type, sender, status, created_at}</text>
|
||||
<text x="420" y="368" fill="#6b7280" font-size="8">match_response: find request by request_id</text>
|
||||
|
||||
<!-- ===== Row 4: Two protocols use same machine ===== -->
|
||||
<rect x="30" y="396" width="700" height="48" rx="6" fill="#f8fafc" stroke="#e2e8f0" stroke-width="1"/>
|
||||
<text x="55" y="416" fill="#1e3a5f" font-size="10" font-weight="600">Two protocols, one mechanism:</text>
|
||||
<rect x="230" y="406" width="130" height="18" rx="4" fill="#fef3c7" stroke="#d97706" stroke-width="1"/>
|
||||
<text x="295" y="419" fill="#92400e" font-size="9" text-anchor="middle">shutdown_request</text>
|
||||
<text x="368" y="419" fill="#475569" font-size="10">and</text>
|
||||
<rect x="390" y="406" width="140" height="18" rx="4" fill="#dbeafe" stroke="#2563eb" stroke-width="1"/>
|
||||
<text x="460" y="419" fill="#1e40af" font-size="9" text-anchor="middle">plan_approval_request</text>
|
||||
<text x="540" y="419" fill="#475569" font-size="10">share the same pending→approved/rejected FSM</text>
|
||||
<text x="55" y="436" fill="#6b7280" font-size="8">New protocol type = new msg_type, no new state machine. request_id links request and response.</text>
|
||||
|
||||
<!-- ===== Bottom notes ===== -->
|
||||
<rect x="30" y="460" width="700" height="28" rx="6" fill="#f8fafc" stroke="#e2e8f0" stroke-width="1"/>
|
||||
<rect x="50" y="470" width="12" height="10" rx="2" fill="#f0f4ff" stroke="#2563eb" stroke-width="1"/>
|
||||
<text x="70" y="480" fill="#475569" font-size="10">s15: MessageBus + spawn_teammate + inbox</text>
|
||||
<rect x="310" y="470" width="12" height="10" rx="2" fill="#f5f3ff" stroke="#7c3aed" stroke-width="1"/>
|
||||
<text x="330" y="480" fill="#475569" font-size="10">s16: request_id protocol + dispatch + pending_requests + state machine</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 10 KiB |
141
s16_team_protocols/images/team-protocols-overview.ja.svg
Normal file
141
s16_team_protocols/images/team-protocols-overview.ja.svg
Normal file
@@ -0,0 +1,141 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 500" font-family="system-ui, -apple-system, sans-serif">
|
||||
<defs>
|
||||
<linearGradient id="header" x1="0" y1="0" x2="1" y2="0">
|
||||
<stop offset="0%" stop-color="#1e3a5f"/><stop offset="100%" stop-color="#7c3aed"/>
|
||||
</linearGradient>
|
||||
<marker id="arrow" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" fill="#555"/>
|
||||
</marker>
|
||||
<marker id="arrow-purple" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" fill="#7c3aed"/>
|
||||
</marker>
|
||||
<marker id="arrow-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-green" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" fill="#16a34a"/>
|
||||
</marker>
|
||||
<marker id="arrow-red" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" fill="#dc2626"/>
|
||||
</marker>
|
||||
</defs>
|
||||
|
||||
<rect width="760" height="500" 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="14" font-weight="700" text-anchor="middle">Team Protocols — リクエスト・レスポンス + request_id 紐付け + 状態機械</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">s15 保持</text>
|
||||
<rect x="130" y="56" width="12" height="10" rx="2" fill="#f5f3ff" stroke="#7c3aed" stroke-width="1"/>
|
||||
<text x="148" y="66" fill="#7c3aed" font-size="10" font-weight="600">s16 新規</text>
|
||||
|
||||
<!-- ===== Row 1: Lead Loop ===== -->
|
||||
<rect x="20" y="90" width="70" height="40" rx="8" fill="#eef2ff" stroke="#4f46e5" stroke-width="1.5"/>
|
||||
<text x="55" y="114" fill="#4f46e5" font-size="8" font-weight="600" text-anchor="middle">turn</text>
|
||||
|
||||
<line x1="90" y1="110" x2="104" y2="110" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
|
||||
<rect x="107" y="90" width="70" height="40" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
|
||||
<text x="142" y="114" fill="#1e3a5f" font-size="10" font-weight="600" text-anchor="middle">messages</text>
|
||||
|
||||
<line x1="177" y1="110" x2="191" y2="110" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
|
||||
<rect x="194" y="86" width="80" height="48" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
|
||||
<text x="234" y="114" fill="#1e3a5f" font-size="9" font-weight="600" text-anchor="middle">prompt</text>
|
||||
|
||||
<line x1="274" y1="110" x2="288" y2="110" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
|
||||
<rect x="291" y="86" width="70" height="48" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
|
||||
<text x="326" y="114" fill="#1e3a5f" font-size="9" font-weight="600" text-anchor="middle">LLM</text>
|
||||
|
||||
<line x1="361" y1="110" x2="375" y2="110" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
|
||||
<rect x="378" y="80" width="356" height="60" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
|
||||
<text x="556" y="98" fill="#1e3a5f" font-size="10" font-weight="600" text-anchor="middle">TOOL DISPATCH(コアツールセット)</text>
|
||||
<text x="394" y="114" fill="#2563eb" font-size="8">bash · read · write · task(4) · spawn · send · inbox</text>
|
||||
<text x="394" y="128" fill="#7c3aed" font-size="8" font-weight="700">★ request_shutdown · request_plan · review_plan</text>
|
||||
|
||||
<!-- Loop back -->
|
||||
<path d="M 734 110 L 748 110 L 748 150 L 55 150 L 55 130" fill="none" stroke="#94a3b8" stroke-width="1" marker-end="url(#arrow)" stroke-dasharray="5,4"/>
|
||||
|
||||
<!-- ===== Row 2: Protocol Flow ===== -->
|
||||
<rect x="30" y="176" width="700" height="100" rx="8" fill="#f5f3ff" stroke="#7c3aed" stroke-width="2"/>
|
||||
<text x="380" y="196" fill="#4c1d95" font-size="11" font-weight="700" text-anchor="middle">リクエスト・レスポンスプロトコルフロー(request_id が全チェーンを貫通)</text>
|
||||
|
||||
<!-- Step 1 -->
|
||||
<rect x="50" y="208" width="130" height="52" rx="6" fill="#fff" stroke="#7c3aed" stroke-width="1"/>
|
||||
<text x="115" y="226" fill="#7c3aed" font-size="9" font-weight="600" text-anchor="middle">① Lead が要求送信</text>
|
||||
<text x="115" y="240" fill="#6b7280" font-size="7" text-anchor="middle">BUS.send("shutdown_request"</text>
|
||||
<text x="115" y="252" fill="#6b7280" font-size="7" text-anchor="middle">metadata={request_id})</text>
|
||||
|
||||
<line x1="180" y1="234" x2="206" y2="234" stroke="#7c3aed" stroke-width="1.5" marker-end="url(#arrow-purple)"/>
|
||||
|
||||
<!-- Step 2 -->
|
||||
<rect x="209" y="208" width="130" height="52" rx="6" fill="#fff" stroke="#7c3aed" stroke-width="1"/>
|
||||
<text x="274" y="226" fill="#7c3aed" font-size="9" font-weight="600" text-anchor="middle">② チームメイト受信</text>
|
||||
<text x="274" y="240" fill="#6b7280" font-size="7" text-anchor="middle">dispatch_by_type(inbox)</text>
|
||||
<text x="274" y="252" fill="#6b7280" font-size="7" text-anchor="middle">→ handler(type, metadata)</text>
|
||||
|
||||
<line x1="339" y1="234" x2="365" y2="234" stroke="#7c3aed" stroke-width="1.5" marker-end="url(#arrow-purple)"/>
|
||||
|
||||
<!-- Step 3 -->
|
||||
<rect x="368" y="208" width="130" height="52" rx="6" fill="#fff" stroke="#7c3aed" stroke-width="1"/>
|
||||
<text x="433" y="226" fill="#7c3aed" font-size="9" font-weight="600" text-anchor="middle">③ チームメイト応答</text>
|
||||
<text x="433" y="240" fill="#6b7280" font-size="7" text-anchor="middle">BUS.send("shutdown_response"</text>
|
||||
<text x="433" y="252" fill="#6b7280" font-size="7" text-anchor="middle">同じ request_id + approve)</text>
|
||||
|
||||
<line x1="498" y1="234" x2="524" y2="234" stroke="#7c3aed" stroke-width="1.5" marker-end="url(#arrow-purple)"/>
|
||||
|
||||
<!-- Step 4 -->
|
||||
<rect x="527" y="208" width="130" height="52" rx="6" fill="#fff" stroke="#7c3aed" stroke-width="1"/>
|
||||
<text x="592" y="226" fill="#7c3aed" font-size="9" font-weight="600" text-anchor="middle">④ Lead 応答受信</text>
|
||||
<text x="592" y="240" fill="#6b7280" font-size="7" text-anchor="middle">match_response(request_id)</text>
|
||||
<text x="592" y="252" fill="#6b7280" font-size="7" text-anchor="middle">→ resolve/reject callback</text>
|
||||
|
||||
<!-- ===== Row 3: State Machine + Storage ===== -->
|
||||
<rect x="30" y="296" width="340" height="85" rx="8" fill="#f5f3ff" stroke="#7c3aed" stroke-width="1.5"/>
|
||||
<text x="200" y="316" fill="#4c1d95" font-size="10" font-weight="700" text-anchor="middle">状態機械(2 つのプロトコルで共通)</text>
|
||||
|
||||
<rect x="55" y="330" width="58" height="20" rx="4" fill="#f1f5f9" stroke="#94a3b8" stroke-width="1"/>
|
||||
<text x="84" y="344" fill="#475569" font-size="9" text-anchor="middle">pending</text>
|
||||
|
||||
<line x1="113" y1="340" x2="192" y2="340" stroke="#16a34a" stroke-width="1.5" marker-end="url(#arrow-green)"/>
|
||||
<text x="152" y="335" fill="#16a34a" font-size="8" font-weight="600" text-anchor="middle">approve</text>
|
||||
|
||||
<rect x="192" y="330" width="62" height="20" rx="4" fill="#dcfce7" stroke="#16a34a" stroke-width="1"/>
|
||||
<text x="223" y="344" fill="#166534" font-size="9" text-anchor="middle">approved</text>
|
||||
|
||||
<path d="M 84 350 L 84 368 L 128 368" fill="none" stroke="#dc2626" stroke-width="1.5" marker-end="url(#arrow-red)"/>
|
||||
<text x="76" y="362" fill="#dc2626" font-size="8" font-weight="600" text-anchor="end">reject</text>
|
||||
|
||||
<rect x="128" y="358" width="60" height="20" rx="4" fill="#fef2f2" stroke="#dc2626" stroke-width="1"/>
|
||||
<text x="158" y="372" fill="#991b1b" font-size="9" text-anchor="middle">rejected</text>
|
||||
|
||||
<rect x="400" y="296" width="330" height="85" rx="8" fill="#f5f3ff" stroke="#7c3aed" stroke-width="1.5"/>
|
||||
<text x="565" y="316" fill="#4c1d95" font-size="10" font-weight="700" text-anchor="middle">pending_requests ストレージ</text>
|
||||
<text x="420" y="336" fill="#7c3aed" font-size="9">pending_requests: dict[str, ProtocolState]</text>
|
||||
<text x="420" y="352" fill="#6b7280" font-size="8">request_id → {type, sender, status, created_at}</text>
|
||||
<text x="420" y="368" fill="#6b7280" font-size="8">match_response: request_id で要求を検索</text>
|
||||
|
||||
<!-- ===== Row 4 ===== -->
|
||||
<rect x="30" y="396" width="700" height="48" rx="6" fill="#f8fafc" stroke="#e2e8f0" stroke-width="1"/>
|
||||
<text x="55" y="416" fill="#1e3a5f" font-size="10" font-weight="600">2 つのプロトコル、1 つの仕組み:</text>
|
||||
<rect x="230" y="406" width="130" height="18" rx="4" fill="#fef3c7" stroke="#d97706" stroke-width="1"/>
|
||||
<text x="295" y="419" fill="#92400e" font-size="9" text-anchor="middle">shutdown_request</text>
|
||||
<text x="368" y="419" fill="#475569" font-size="10">と</text>
|
||||
<rect x="390" y="406" width="140" height="18" rx="4" fill="#dbeafe" stroke="#2563eb" stroke-width="1"/>
|
||||
<text x="460" y="419" fill="#1e40af" font-size="9" text-anchor="middle">plan_approval_request</text>
|
||||
<text x="540" y="419" fill="#475569" font-size="10">が pending→approved/rejected 状態機械を共有</text>
|
||||
<text x="55" y="436" fill="#6b7280" font-size="8">新しいプロトコルタイプ = 新しい msg_type、新しい状態機械は不要。request_id が要求と応答を紐付け。</text>
|
||||
|
||||
<!-- ===== Bottom notes ===== -->
|
||||
<rect x="30" y="460" width="700" height="28" rx="6" fill="#f8fafc" stroke="#e2e8f0" stroke-width="1"/>
|
||||
<rect x="50" y="470" width="12" height="10" rx="2" fill="#f0f4ff" stroke="#2563eb" stroke-width="1"/>
|
||||
<text x="70" y="480" fill="#475569" font-size="10">s15: MessageBus + spawn_teammate + inbox</text>
|
||||
<rect x="310" y="470" width="12" height="10" rx="2" fill="#f5f3ff" stroke="#7c3aed" stroke-width="1"/>
|
||||
<text x="330" y="480" fill="#475569" font-size="10">s16: request_id プロトコル + dispatch + pending_requests + 状態機械</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 9.9 KiB |
148
s16_team_protocols/images/team-protocols-overview.svg
Normal file
148
s16_team_protocols/images/team-protocols-overview.svg
Normal file
@@ -0,0 +1,148 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 500" font-family="system-ui, -apple-system, sans-serif">
|
||||
<defs>
|
||||
<linearGradient id="header" x1="0" y1="0" x2="1" y2="0">
|
||||
<stop offset="0%" stop-color="#1e3a5f"/><stop offset="100%" stop-color="#7c3aed"/>
|
||||
</linearGradient>
|
||||
<marker id="arrow" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" fill="#555"/>
|
||||
</marker>
|
||||
<marker id="arrow-purple" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" fill="#7c3aed"/>
|
||||
</marker>
|
||||
<marker id="arrow-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-green" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" fill="#16a34a"/>
|
||||
</marker>
|
||||
<marker id="arrow-red" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" fill="#dc2626"/>
|
||||
</marker>
|
||||
</defs>
|
||||
|
||||
<rect width="760" height="500" 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">Team Protocols — 请求-响应协议 + request_id 关联 + 状态机</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">s15 保留</text>
|
||||
<rect x="140" y="56" width="12" height="10" rx="2" fill="#f5f3ff" stroke="#7c3aed" stroke-width="1"/>
|
||||
<text x="158" y="66" fill="#7c3aed" font-size="10" font-weight="600">s16 新增</text>
|
||||
|
||||
<!-- ===== Row 1: Lead Loop ===== -->
|
||||
<rect x="20" y="90" width="70" height="40" rx="8" fill="#eef2ff" stroke="#4f46e5" stroke-width="1.5"/>
|
||||
<text x="55" y="114" fill="#4f46e5" font-size="8" font-weight="600" text-anchor="middle">turn</text>
|
||||
|
||||
<line x1="90" y1="110" x2="104" y2="110" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
|
||||
<rect x="107" y="90" width="70" height="40" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
|
||||
<text x="142" y="114" fill="#1e3a5f" font-size="10" font-weight="600" text-anchor="middle">messages</text>
|
||||
|
||||
<line x1="177" y1="110" x2="191" y2="110" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
|
||||
<rect x="194" y="86" width="80" height="48" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
|
||||
<text x="234" y="114" fill="#1e3a5f" font-size="9" font-weight="600" text-anchor="middle">prompt</text>
|
||||
|
||||
<line x1="274" y1="110" x2="288" y2="110" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
|
||||
<rect x="291" y="86" width="70" height="48" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
|
||||
<text x="326" y="114" fill="#1e3a5f" font-size="9" font-weight="600" text-anchor="middle">LLM</text>
|
||||
|
||||
<line x1="361" y1="110" x2="375" y2="110" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
|
||||
<rect x="378" y="80" width="356" height="60" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
|
||||
<text x="556" y="98" fill="#1e3a5f" font-size="10" font-weight="600" text-anchor="middle">TOOL DISPATCH(核心工具集)</text>
|
||||
<text x="394" y="114" fill="#2563eb" font-size="8">bash · read · write · task(4) · spawn · send · inbox</text>
|
||||
<text x="394" y="128" fill="#7c3aed" font-size="8" font-weight="700">★ request_shutdown · request_plan · review_plan</text>
|
||||
|
||||
<!-- Loop back -->
|
||||
<path d="M 734 110 L 748 110 L 748 150 L 55 150 L 55 130" fill="none" stroke="#94a3b8" stroke-width="1" marker-end="url(#arrow)" stroke-dasharray="5,4"/>
|
||||
|
||||
<!-- ===== Row 2: Protocol Flow (purple) — request_id lifecycle ===== -->
|
||||
<rect x="30" y="176" width="700" height="100" rx="8" fill="#f5f3ff" stroke="#7c3aed" stroke-width="2"/>
|
||||
<text x="380" y="196" fill="#4c1d95" font-size="11" font-weight="700" text-anchor="middle">请求-响应协议流程(request_id 贯穿)</text>
|
||||
|
||||
<!-- Step 1: Send request -->
|
||||
<rect x="50" y="208" width="130" height="52" rx="6" fill="#fff" stroke="#7c3aed" stroke-width="1"/>
|
||||
<text x="115" y="226" fill="#7c3aed" font-size="9" font-weight="600" text-anchor="middle">① Lead 发请求</text>
|
||||
<text x="115" y="240" fill="#6b7280" font-size="7" text-anchor="middle">BUS.send("shutdown_request"</text>
|
||||
<text x="115" y="252" fill="#6b7280" font-size="7" text-anchor="middle">metadata={request_id})</text>
|
||||
|
||||
<line x1="180" y1="234" x2="206" y2="234" stroke="#7c3aed" stroke-width="1.5" marker-end="url(#arrow-purple)"/>
|
||||
|
||||
<!-- Step 2: Teammate receives & dispatches -->
|
||||
<rect x="209" y="208" width="130" height="52" rx="6" fill="#fff" stroke="#7c3aed" stroke-width="1"/>
|
||||
<text x="274" y="226" fill="#7c3aed" font-size="9" font-weight="600" text-anchor="middle">② 队友收到</text>
|
||||
<text x="274" y="240" fill="#6b7280" font-size="7" text-anchor="middle">dispatch_by_type(inbox)</text>
|
||||
<text x="274" y="252" fill="#6b7280" font-size="7" text-anchor="middle">→ handler(type, metadata)</text>
|
||||
|
||||
<line x1="339" y1="234" x2="365" y2="234" stroke="#7c3aed" stroke-width="1.5" marker-end="url(#arrow-purple)"/>
|
||||
|
||||
<!-- Step 3: Teammate processes & responds -->
|
||||
<rect x="368" y="208" width="130" height="52" rx="6" fill="#fff" stroke="#7c3aed" stroke-width="1"/>
|
||||
<text x="433" y="226" fill="#7c3aed" font-size="9" font-weight="600" text-anchor="middle">③ 队友回复</text>
|
||||
<text x="433" y="240" fill="#6b7280" font-size="7" text-anchor="middle">BUS.send("shutdown_response"</text>
|
||||
<text x="433" y="252" fill="#6b7280" font-size="7" text-anchor="middle">同 request_id + approve)</text>
|
||||
|
||||
<line x1="498" y1="234" x2="524" y2="234" stroke="#7c3aed" stroke-width="1.5" marker-end="url(#arrow-purple)"/>
|
||||
|
||||
<!-- Step 4: Lead receives response -->
|
||||
<rect x="527" y="208" width="130" height="52" rx="6" fill="#fff" stroke="#7c3aed" stroke-width="1"/>
|
||||
<text x="592" y="226" fill="#7c3aed" font-size="9" font-weight="600" text-anchor="middle">④ Lead 收响应</text>
|
||||
<text x="592" y="240" fill="#6b7280" font-size="7" text-anchor="middle">match_response(request_id)</text>
|
||||
<text x="592" y="252" fill="#6b7280" font-size="7" text-anchor="middle">→ resolve/reject callback</text>
|
||||
|
||||
<!-- ===== Row 3: State Machine + Storage ===== -->
|
||||
<!-- State Machine -->
|
||||
<rect x="30" y="296" width="340" height="85" rx="8" fill="#f5f3ff" stroke="#7c3aed" stroke-width="1.5"/>
|
||||
<text x="200" y="316" fill="#4c1d95" font-size="10" font-weight="700" text-anchor="middle">状态机(同一套,两种协议)</text>
|
||||
|
||||
<!-- pending box: center (84, 340), right edge x=113, bottom y=350 -->
|
||||
<rect x="55" y="330" width="58" height="20" rx="4" fill="#f1f5f9" stroke="#94a3b8" stroke-width="1"/>
|
||||
<text x="84" y="344" fill="#475569" font-size="9" text-anchor="middle">pending</text>
|
||||
|
||||
<!-- approve: line from pending right (113,340) to approved left (192,340) -->
|
||||
<line x1="113" y1="340" x2="192" y2="340" stroke="#16a34a" stroke-width="1.5" marker-end="url(#arrow-green)"/>
|
||||
<text x="152" y="335" fill="#16a34a" font-size="8" font-weight="600" text-anchor="middle">approve</text>
|
||||
|
||||
<!-- approved box: left edge x=192 -->
|
||||
<rect x="192" y="330" width="62" height="20" rx="4" fill="#dcfce7" stroke="#16a34a" stroke-width="1"/>
|
||||
<text x="223" y="344" fill="#166534" font-size="9" text-anchor="middle">approved</text>
|
||||
|
||||
<!-- reject: path from pending bottom (84,350) down to (84,368) then right to rejected left (128,368) -->
|
||||
<path d="M 84 350 L 84 368 L 128 368" fill="none" stroke="#dc2626" stroke-width="1.5" marker-end="url(#arrow-red)"/>
|
||||
<text x="76" y="362" fill="#dc2626" font-size="8" font-weight="600" text-anchor="end">reject</text>
|
||||
|
||||
<!-- rejected box: left edge x=128, top y=358 -->
|
||||
<rect x="128" y="358" width="60" height="20" rx="4" fill="#fef2f2" stroke="#dc2626" stroke-width="1"/>
|
||||
<text x="158" y="372" fill="#991b1b" font-size="9" text-anchor="middle">rejected</text>
|
||||
|
||||
<!-- pending_requests storage -->
|
||||
<rect x="400" y="296" width="330" height="85" rx="8" fill="#f5f3ff" stroke="#7c3aed" stroke-width="1.5"/>
|
||||
<text x="565" y="316" fill="#4c1d95" font-size="10" font-weight="700" text-anchor="middle">pending_requests 存储</text>
|
||||
<text x="420" y="336" fill="#7c3aed" font-size="9">pending_requests: dict[str, ProtocolState]</text>
|
||||
<text x="420" y="352" fill="#6b7280" font-size="8">request_id → {type, sender, status, created_at}</text>
|
||||
<text x="420" y="368" fill="#6b7280" font-size="8">match_response: 按 request_id 找回对应请求</text>
|
||||
|
||||
<!-- ===== Row 4: Two protocols use same machine ===== -->
|
||||
<rect x="30" y="396" width="700" height="48" rx="6" fill="#f8fafc" stroke="#e2e8f0" stroke-width="1"/>
|
||||
<text x="55" y="416" fill="#1e3a5f" font-size="10" font-weight="600">两种协议,同一套机制:</text>
|
||||
<rect x="230" y="406" width="130" height="18" rx="4" fill="#fef3c7" stroke="#d97706" stroke-width="1"/>
|
||||
<text x="295" y="419" fill="#92400e" font-size="9" text-anchor="middle">shutdown_request</text>
|
||||
<text x="368" y="419" fill="#475569" font-size="10">和</text>
|
||||
<rect x="390" y="406" width="140" height="18" rx="4" fill="#dbeafe" stroke="#2563eb" stroke-width="1"/>
|
||||
<text x="460" y="419" fill="#1e40af" font-size="9" text-anchor="middle">plan_approval_request</text>
|
||||
<text x="540" y="419" fill="#475569" font-size="10">共用 pending→approved/rejected 状态机</text>
|
||||
<text x="55" y="436" fill="#6b7280" font-size="8">新增协议类型 = 新的 msg_type,不需要新状态机。request_id 关联请求和响应。</text>
|
||||
|
||||
<!-- ===== Bottom notes ===== -->
|
||||
<rect x="30" y="460" width="700" height="28" rx="6" fill="#f8fafc" stroke="#e2e8f0" stroke-width="1"/>
|
||||
<rect x="50" y="470" width="12" height="10" rx="2" fill="#f0f4ff" stroke="#2563eb" stroke-width="1"/>
|
||||
<text x="70" y="480" fill="#475569" font-size="10">s15: MessageBus + spawn_teammate + inbox</text>
|
||||
<rect x="310" y="470" width="12" height="10" rx="2" fill="#f5f3ff" stroke="#7c3aed" stroke-width="1"/>
|
||||
<text x="330" y="480" fill="#475569" font-size="10">s16: request_id 协议 + dispatch + pending_requests + 状态机</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 10 KiB |
Reference in New Issue
Block a user