diff --git a/README-ja.md b/README-ja.md index 919d2f4be..2c50c7048 100644 --- a/README-ja.md +++ b/README-ja.md @@ -1,6 +1,6 @@ # Learn Claude Code -- 0 から 1 へ構築する nano Claude Code-like agent -[English](./README.md) | [中文](./README-zh.md) | [日本語](./README-ja.md) +[English](./README.md) | [简体中文](./README-zh.md) | [日本語](./README-ja.md) | [繁體中文](./README-zhtw.md) ``` THE AGENT PATTERN diff --git a/README-zh.md b/README-zh.md index 3cd2cfc68..d5b0ff402 100644 --- a/README-zh.md +++ b/README-zh.md @@ -1,6 +1,6 @@ # Learn Claude Code -- 从 0 到 1 构建 nano Claude Code-like agent -[English](./README.md) | [中文](./README-zh.md) | [日本語](./README-ja.md) +[English](./README.md) | [简体中文](./README-zh.md) | [日本語](./README-ja.md) | [繁體中文](./README-zhtw.md) ``` THE AGENT PATTERN diff --git a/README-zhtw.md b/README-zhtw.md new file mode 100644 index 000000000..36c5e4bab --- /dev/null +++ b/README-zhtw.md @@ -0,0 +1,237 @@ +# Learn Claude Code -- 從 0 到 1 打造 nano Claude Code-like agent + +[English](./README.md) | [简体中文](./README-zh.md) | [日本語](./README-ja.md) | [繁體中文](./README-zhtw.md) + +``` + THE AGENT PATTERN + ================= + + User --> messages[] --> LLM --> response + | + stop_reason == "tool_use"? + / \ + yes no + | | + execute tools return text + append results + loop back -----------------> messages[] + + + 這就是最小迴圈。每個 AI coding agent 都離不開它。 + Production agents 還會疊加 policy、permissions 與 lifecycle layers。 +``` + +**12 個循序漸進的章節,從簡單迴圈一路到隔離式自主執行。** +**每個章節只新增一個機制。每個機制都有一句格言。** + +> **s01**   *"One loop & Bash is all you need"* — 一個工具 + 一個迴圈 = 一個 agent +> +> **s02**   *"Adding a tool means adding one handler"* — 主迴圈不變;新工具只要註冊進 dispatch map +> +> **s03**   *"An agent without a plan drifts"* — 先列步驟,再執行;完成率翻倍 +> +> **s04**   *"Break big tasks down; each subtask gets a clean context"* — subagents 使用獨立 messages[],讓主對話保持乾淨 +> +> **s05**   *"Load knowledge when you need it, not upfront"* — 透過 tool_result 注入,而不是放進 system prompt +> +> **s06**   *"Context will fill up; you need a way to make room"* — 三層壓縮策略,換來幾乎無限的 sessions +> +> **s07**   *"Break big goals into small tasks, order them, persist to disk"* — 以檔案為基礎、含 dependencies 的 task graph,為 multi-agent 協作打下基礎 +> +> **s08**   *"Run slow operations in the background; the agent keeps thinking"* — 透過背景執行緒執行任務,完成後再發送通知 + +> **s09**   *"When the task is too big for one, delegate to teammates"* — 永遠存在的隊友 + 非同步信件 +> +> **s10**   *"Teammates need shared communication rules"* — 一套 request-response pattern 驅動所有溝通 +> +> **s11**   *"Teammates scan the board and claim tasks themselves"* — 不需要逐一指派,而是主動認領工作去執行 +> +> **s12**   *"Each works in its own directory, no interference"* — tasks 管理 goals,worktrees 管理 directories,以 ID 綁定 + +--- + +## 核心模式 + +```python +def agent_loop(messages): + while True: + response = client.messages.create( + model=MODEL, system=SYSTEM, + messages=messages, tools=TOOLS, + ) + 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": + output = TOOL_HANDLERS[block.name](**block.input) + results.append({ + "type": "tool_result", + "tool_use_id": block.id, + "content": output, + }) + messages.append({"role": "user", "content": results}) +``` + +每個章節都在這個迴圈上疊加一個機制 -- 但迴圈本身始終不變。 + +## 範圍(重要) + +此 repository 是一個 0->1 學習型專案,用來建構 nano Claude Code-like agent。 +為了讓學習路徑更清晰,這個 repository 刻意簡化或省略了部分 production 機制: + +- 完整的 event/hook buses(例如 PreToolUse、SessionStart/End、ConfigChange)。 + s12 僅提供教學用途的最小 append-only lifecycle event stream。 +- 以規則為基礎的 permission governance 與 trust workflows +- Session lifecycle controls(resume/fork)與更完整的 worktree lifecycle controls +- 完整 MCP runtime 細節(transport/OAuth/resource subscribe/polling) + +此 repository 中的 team JSONL mailbox protocol 僅為教學實作,不代表任何特定 production internals。 + +## 快速開始 + +```sh +git clone https://github.com/shareAI-lab/learn-claude-code +cd learn-claude-code +pip install -r requirements.txt +cp .env.example .env # 編輯 .env 並填入你的 ANTHROPIC_API_KEY + +python agents/s01_agent_loop.py # 從這裡開始 +python agents/s12_worktree_task_isolation.py # 完整學習終點 +python agents/s_full.py # 總結:把全部機制整合在一起 +``` + +### Web 平台 + +互動式視覺化、逐步圖解、原始碼檢視器,以及對應文件。 + +```sh +cd web && npm install && npm run dev # http://localhost:3000 +``` + +## 學習路徑 + +``` +第一階段:THE LOOP 第二階段:PLANNING & KNOWLEDGE +================== ============================== +s01 The Agent Loop [1] s03 TodoWrite [5] + while + stop_reason TodoManager + nag reminder + | | + +-> s02 Tool Use [4] s04 Subagents [5] + dispatch map: name->handler fresh messages[] per child + | + s05 Skills [5] + SKILL.md via tool_result + | + s06 Context Compact [5] + 3-layer compression + +第三階段:PERSISTENCE 第四階段:TEAMS +================== ===================== +s07 Tasks [8] s09 Agent Teams [9] + file-based CRUD + deps graph teammates + JSONL mailboxes + | | +s08 Background Tasks [6] s10 Team Protocols [12] + daemon threads + notify queue shutdown + plan approval FSM + | + s11 Autonomous Agents [14] + idle cycle + auto-claim + | + s12 Worktree Isolation [16] + task coordination + optional isolated execution lanes + + [N] = number of tools +``` + +## 架構 + +``` +learn-claude-code/ +| +|-- agents/ # Python 參考實作 (s01-s12 + s_full capstone) +|-- docs/{en,zh,ja}/ # Mental-model-first 文件 (3 種語言) +|-- web/ # 互動式學習平台 (Next.js) +|-- skills/ # s05 的 Skill 檔案 ++-- .github/workflows/ci.yml # CI: 檢查型別 + 建制專案 +``` + +## 文件 + +Mental-model-first:問題、解法、ASCII 圖、最小化程式碼。 +提供 [English](./docs/en/) | [中文](./docs/zh/) | [日本語](./docs/ja/)。 + +| Session | Topic | Motto | +|---------|-------|-------| +| [s01](./docs/en/s01-the-agent-loop.md) | The Agent Loop | *One loop & Bash is all you need* | +| [s02](./docs/en/s02-tool-use.md) | Tool Use | *Adding a tool means adding one handler* | +| [s03](./docs/en/s03-todo-write.md) | TodoWrite | *An agent without a plan drifts* | +| [s04](./docs/en/s04-subagent.md) | Subagents | *Break big tasks down; each subtask gets a clean context* | +| [s05](./docs/en/s05-skill-loading.md) | Skills | *Load knowledge when you need it, not upfront* | +| [s06](./docs/en/s06-context-compact.md) | Context Compact | *Context will fill up; you need a way to make room* | +| [s07](./docs/en/s07-task-system.md) | Tasks | *Break big goals into small tasks, order them, persist to disk* | +| [s08](./docs/en/s08-background-tasks.md) | Background Tasks | *Run slow operations in the background; the agent keeps thinking* | +| [s09](./docs/en/s09-agent-teams.md) | Agent Teams | *When the task is too big for one, delegate to teammates* | +| [s10](./docs/en/s10-team-protocols.md) | Team Protocols | *Teammates need shared communication rules* | +| [s11](./docs/en/s11-autonomous-agents.md) | Autonomous Agents | *Teammates scan the board and claim tasks themselves* | +| [s12](./docs/en/s12-worktree-task-isolation.md) | Worktree + Task Isolation | *Each works in its own directory, no interference* | + +## 接下來 -- 從理解到落地 + +完成 12 個章節後,你已經由內到外掌握 agent 的運作原理。把這些知識化為產品有兩種方式: + +### Kode Agent CLI -- Open-Source Coding Agent CLI + +> `npm i -g @shareai-lab/kode` + +支援 Skill 與 LSP,適用於 Windows,並可串接 GLM / MiniMax / DeepSeek 等開放模型。安裝後即可上手。 + +GitHub: **[shareAI-lab/Kode-cli](https://github.com/shareAI-lab/Kode-cli)** + +### Kode Agent SDK -- 將 Agent 能力嵌入你的 App + +官方 Claude Code Agent SDK 底層會與完整 CLI process 通訊 -- 每個並發使用者都會對應一個獨立 terminal process。Kode SDK 是獨立 library,沒有 per-user process 負擔,可嵌入 backends、browser extensions、embedded devices,或任何 runtime。 + +GitHub: **[shareAI-lab/Kode-agent-sdk](https://github.com/shareAI-lab/Kode-agent-sdk)** + +--- + +## 姊妹 Repo:從 *on-demand sessions* 到 *always-on assistant* + +本 repository 教的 agent 屬於 **use-and-discard** 模式 -- 開 terminal、交付任務、完成後關閉;下次重開就是全新 session。這就是 Claude Code 的模式。 + +[OpenClaw](https://github.com/openclaw/openclaw) 證明了另一種可能:在同樣的 agent core 上再加兩個機制,就能讓 agent 從「推一下才動」變成「每 30 秒自動醒來找事做」: + +- **Heartbeat** -- 系統每 30 秒會送一則訊息給 agent,讓它檢查是否有事可做。沒事就繼續休眠;有事就立刻處理。 +- **Cron** -- agent 可以替自己安排未來任務,到點自動執行。 + +再加上多通道 IM routing(WhatsApp / Telegram / Slack / Discord,13+ 平台)、持久化 context memory,以及 Soul personality system,agent 就會從一次性工具進化為 always-on 的個人 AI 助手。 + +**[claw0](https://github.com/shareAI-lab/claw0)** 是我們的姊妹教學 repository,從零拆解這些機制: + +``` +claw agent = agent core + heartbeat + cron + IM chat + memory + soul +``` + +``` +learn-claude-code claw0 +(agent runtime core: (proactive always-on assistant: + loop, tools, planning, heartbeat, cron, IM channels, + teams, worktree isolation) memory, soul personality) +``` + +## About +
+ +使用 WeChat 掃描即可追蹤我們, +或在 X 追蹤:[shareAI-Lab](https://x.com/baicai003) + +## 授權 + +MIT + +--- +**模型就是 agent。我們的工作,是給它工具,然後讓開。** \ No newline at end of file diff --git a/README.md b/README.md index 9018362c4..0c50056e3 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -[English](./README.md) | [中文](./README-zh.md) | [日本語](./README-ja.md) +[English](./README.md) | [简体中文](./README-zh.md) | [日本語](./README-ja.md) | [繁體中文](./README-zhtw.md) # Learn Claude Code -- A nano Claude Code-like agent, built from 0 to 1 ``` @@ -161,7 +161,7 @@ learn-claude-code/ ## Documentation Mental-model-first: problem, solution, ASCII diagram, minimal code. -Available in [English](./docs/en/) | [中文](./docs/zh/) | [日本語](./docs/ja/). +Available in [English](./docs/en/) | [简体中文](./docs/zh/) | [日本語](./docs/ja/). | [繁體中文](./docs/zhtw/) | Session | Topic | Motto | |---------|-------|-------| diff --git a/docs/zhtw/s01-the-agent-loop.md b/docs/zhtw/s01-the-agent-loop.md new file mode 100644 index 000000000..52c575ad5 --- /dev/null +++ b/docs/zhtw/s01-the-agent-loop.md @@ -0,0 +1,114 @@ +# s01: Agent 迴圈 + +`[ s01 ] s02 > s03 > s04 > s05 > s06 | s07 > s08 > s09 > s10 > s11 > s12` + +> *"One loop & Bash is all you need"* -- 一個工具 + 一個迴圈 = 一個 agent。 + +## 問題 + +語言模型可以推理程式碼,但它無法直接*碰觸*真實世界 -- 不能讀檔、跑測試、檢查錯誤。沒有迴圈時,每次工具呼叫都得手動複製貼上結果回去。你就成了那個迴圈本身。 + +## 解法 + +``` ++--------+ +-------+ +---------+ +| User | ---> | LLM | ---> | Tool | +| prompt | | | | execute | ++--------+ +---+---+ +----+----+ + ^ | + | tool_result | + +----------------+ + (loop until stop_reason != "tool_use") +``` + +整個流程只靠一個退出條件控制。只要模型還在呼叫工具,迴圈就持續執行。 + +## 運作方式 + +1. 把使用者輸入當成第一則訊息。 + +```python +messages.append({"role": "user", "content": query}) +``` + +2. 把 messages 與 tool definitions 一起送進 LLM。 + +```python +response = client.messages.create( + model=MODEL, system=SYSTEM, messages=messages, + tools=TOOLS, max_tokens=8000, +) +``` + +3. 先附加 assistant 回覆。接著檢查 `stop_reason` -- 如果模型沒有呼叫工具,就結束。 + +```python +messages.append({"role": "assistant", "content": response.content}) +if response.stop_reason != "tool_use": + return +``` + +4. 依序執行每個工具呼叫,收集結果後以 user 訊息附加回去,再回到步驟 2。 + +```python +results = [] +for block in response.content: + if block.type == "tool_use": + output = run_bash(block.input["command"]) + results.append({ + "type": "tool_result", + "tool_use_id": block.id, + "content": output, + }) +messages.append({"role": "user", "content": results}) +``` + +整合成單一方法: + +```python +def agent_loop(query): + messages = [{"role": "user", "content": query}] + while True: + response = client.messages.create( + model=MODEL, system=SYSTEM, messages=messages, + tools=TOOLS, max_tokens=8000, + ) + 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": + output = run_bash(block.input["command"]) + results.append({ + "type": "tool_result", + "tool_use_id": block.id, + "content": output, + }) + messages.append({"role": "user", "content": results}) +``` + +這樣不到 30 行,就有完整 agent。後續課程都只是疊加在這個基礎上 -- 不會改動這個迴圈。 + +## 相較前一版的變更 + +| Component | Before | After | +|---------------|------------|--------------------------------| +| Agent loop | (none) | `while True` + stop_reason | +| Tools | (none) | `bash` (one tool) | +| Messages | (none) | 累積訊息佇列 | +| Control flow | (none) | `stop_reason != "tool_use"` | + +## 動手試試 + +```sh +cd learn-claude-code +python agents/s01_agent_loop.py +``` + +1. `Create a file called hello.py that prints "Hello, World!"` +2. `List all Python files in this directory` +3. `What is the current git branch?` +4. `Create a directory called test_output and write 3 files in it` diff --git a/docs/zhtw/s02-tool-use.md b/docs/zhtw/s02-tool-use.md new file mode 100644 index 000000000..a98fc310a --- /dev/null +++ b/docs/zhtw/s02-tool-use.md @@ -0,0 +1,97 @@ +# s02: Tool Use + +`s01 > [ s02 ] s03 > s04 > s05 > s06 | s07 > s08 > s09 > s10 > s11 > s12` + +> *"Adding a tool means adding one handler"* -- 主迴圈保持不變;新工具只要註冊進 dispatch map。 + +## 問題 + +只有 `bash` 時,agent 幾乎所有事都得 shell out。`cat` 可能會不穩定截斷、`sed` 在特殊字元上容易失敗,而且每一次 bash 呼叫都是未受約束的安全面。像 `read_file`、`write_file` 這類專用工具,可以在 tool 層就強制做 path sandboxing。 + +關鍵洞察:新增工具,不需要改迴圈。 + +## 解法 + +``` ++--------+ +-------+ +------------------+ +| User | ---> | LLM | ---> | Tool Dispatch | +| prompt | | | | { | ++--------+ +---+---+ | bash: run_bash | + ^ | read: run_read | + | | write: run_wr | + +-----------+ edit: run_edit | + tool_result | } | + +------------------+ + +The dispatch map is a dict: {tool_name: handler_function}. +One lookup replaces any if/elif chain. +``` + +## 運作方式 + +1. 每個工具對應一個 handler。path sandboxing 可防止跳出工作目錄。 + +```python +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_read(path: str, limit: int = None) -> str: + text = safe_path(path).read_text() + lines = text.splitlines() + if limit and limit < len(lines): + lines = lines[:limit] + return "\n".join(lines)[:50000] +``` + +2. dispatch map 將 tool name 對應到 handler。 + +```python +TOOL_HANDLERS = { + "bash": lambda **kw: run_bash(kw["command"]), + "read_file": lambda **kw: run_read(kw["path"], kw.get("limit")), + "write_file": lambda **kw: run_write(kw["path"], kw["content"]), + "edit_file": lambda **kw: run_edit(kw["path"], kw["old_text"], + kw["new_text"]), +} +``` + +3. 在迴圈中依 name 查表拿 handler。迴圈本體與 s01 相同。 + +```python +for block in response.content: + if block.type == "tool_use": + handler = TOOL_HANDLERS.get(block.name) + output = handler(**block.input) if handler \ + else f"Unknown tool: {block.name}" + results.append({ + "type": "tool_result", + "tool_use_id": block.id, + "content": output, + }) +``` + +新增工具 = 新增 handler + 新增 schema entry。迴圈不需要改。 + +## 相較 s01 的變更 + +| Component | Before (s01) | After (s02) | +|----------------|--------------------|----------------------------| +| Tools | 1 (bash only) | 4 (bash, read, write, edit)| +| Dispatch | Hardcoded bash call | `TOOL_HANDLERS` dict | +| Path safety | None | `safe_path()` sandbox | +| Agent loop | Unchanged | Unchanged | + +## 動手試試 + +```sh +cd learn-claude-code +python agents/s02_tool_use.py +``` + +1. `Read the file requirements.txt` +2. `Create a file called greet.py with a greet(name) function` +3. `Edit greet.py to add a docstring to the function` +4. `Read greet.py to verify the edit worked` diff --git a/docs/zhtw/s03-todo-write.md b/docs/zhtw/s03-todo-write.md new file mode 100644 index 000000000..23bf8d3af --- /dev/null +++ b/docs/zhtw/s03-todo-write.md @@ -0,0 +1,94 @@ +# s03: TodoWrite + +`s01 > s02 > [ s03 ] s04 > s05 > s06 | s07 > s08 > s09 > s10 > s11 > s12` + +> *"An agent without a plan drifts"* -- 先列步驟,再執行。 + +## 問題 + +多步驟任務時,模型容易失焦:重複做事、跳過步驟、或越走越偏。對話一長會更嚴重 -- tool results 一直塞進 context,system prompt 的約束會慢慢被稀釋。像 10 步驟的重構,可能做完 1-3 步後就開始即興發揮,因為 4-10 步早就忘了。 + +## 解法 + +``` ++--------+ +-------+ +---------+ +| User | ---> | LLM | ---> | Tools | +| prompt | | | | + todo | ++--------+ +---+---+ +----+----+ + ^ | + | tool_result | + +----------------+ + | + +-----------+-----------+ + | TodoManager state | + | [ ] task A | + | [>] task B <- doing | + | [x] task C | + +-----------------------+ + | + if rounds_since_todo >= 3: + inject into tool_result +``` + +## 運作方式 + +1. TodoManager 會保存 task 與狀態;同一時間只允許一個 `in_progress`。 + +```python +class TodoManager: + def update(self, items: list) -> str: + validated, in_progress_count = [], 0 + for item in items: + status = item.get("status", "pending") + if status == "in_progress": + in_progress_count += 1 + validated.append({"id": item["id"], "text": item["text"], + "status": status}) + if in_progress_count > 1: + raise ValueError("Only one task can be in_progress") + self.items = validated + return self.render() +``` + +2. `todo` 工具就和其他工具一樣,放進 dispatch map。 + +```python +TOOL_HANDLERS = { + # ...base tools... + "todo": lambda **kw: TODO.update(kw["items"]), +} +``` + +3. 若模型連續 3 輪以上沒呼叫 `todo`,就注入 nag reminder。 + +```python +if rounds_since_todo >= 3 and messages: + last = messages[-1] + if last["role"] == "user" and isinstance(last.get("content"), list): + last["content"].insert(0, { + "type": "text", + "text": "Update your todos.", + }) +``` + +「一次只能一個 in_progress」會強迫模型依序聚焦;nag reminder 則提供持續的行為約束。 + +## 相較 s02 的變更 + +| Component | Before (s02) | After (s03) | +|----------------|------------------|----------------------------| +| Tools | 4 | 5 (+todo) | +| Planning | None | TodoManager with statuses | +| Nag injection | None | `` after 3 rounds| +| Agent loop | Simple dispatch | + rounds_since_todo counter| + +## 動手試試 + +```sh +cd learn-claude-code +python agents/s03_todo_write.py +``` + +1. `Refactor the file hello.py: add type hints, docstrings, and a main guard` +2. `Create a Python package with __init__.py, utils.py, and tests/test_utils.py` +3. `Review all Python files and fix any style issues` diff --git a/docs/zhtw/s04-subagent.md b/docs/zhtw/s04-subagent.md new file mode 100644 index 000000000..050d6f028 --- /dev/null +++ b/docs/zhtw/s04-subagent.md @@ -0,0 +1,92 @@ +# s04: Subagents + +`s01 > s02 > s03 > [ s04 ] s05 > s06 | s07 > s08 > s09 > s10 > s11 > s12` + +> *"Break big tasks down; each subtask gets a clean context"* -- subagents 使用獨立 messages[],讓主對話保持乾淨。 + +## 問題 + +agent 持續工作時,messages array 會不斷變長。每一次讀檔、每一段 bash 輸出都會永久留在 context。像「這個專案用哪個測試框架?」可能要讀 5 個檔案,但 parent 真正需要的答案其實只有一句:「pytest。」 + +## 解法 + +``` +Parent agent Subagent ++------------------+ +------------------+ +| messages=[...] | | messages=[] | <-- fresh +| | dispatch | | +| tool: task | ----------> | while tool_use: | +| prompt="..." | | call tools | +| | summary | append results | +| result = "..." | <---------- | return last text | ++------------------+ +------------------+ + +Parent context stays clean. Subagent context is discarded. +``` + +## 運作方式 + +1. parent 有 `task` 工具;child 只有 base tools(不包含 `task`,避免遞迴生成)。 + +```python +PARENT_TOOLS = CHILD_TOOLS + [ + {"name": "task", + "description": "Spawn a subagent with fresh context.", + "input_schema": { + "type": "object", + "properties": {"prompt": {"type": "string"}}, + "required": ["prompt"], + }}, +] +``` + +2. subagent 以 `messages=[]` 起跑,跑自己的迴圈。最後只回傳最終文字給 parent。 + +```python +def run_subagent(prompt: str) -> str: + sub_messages = [{"role": "user", "content": prompt}] + for _ in range(30): # safety limit + response = client.messages.create( + model=MODEL, system=SUBAGENT_SYSTEM, + messages=sub_messages, + tools=CHILD_TOOLS, max_tokens=8000, + ) + sub_messages.append({"role": "assistant", + "content": response.content}) + if response.stop_reason != "tool_use": + break + results = [] + for block in response.content: + if block.type == "tool_use": + handler = TOOL_HANDLERS.get(block.name) + output = handler(**block.input) + results.append({"type": "tool_result", + "tool_use_id": block.id, + "content": str(output)[:50000]}) + sub_messages.append({"role": "user", "content": results}) + return "".join( + b.text for b in response.content if hasattr(b, "text") + ) or "(no summary)" +``` + +child 的完整訊息歷史(可能 30+ 次 tool calls)都會被丟棄。parent 只收到一段摘要,作為一般 `tool_result`。 + +## 相較 s03 的變更 + +| Component | Before (s03) | After (s04) | +|----------------|------------------|---------------------------| +| Tools | 5 | 5 (base) + task (parent) | +| Context | Single shared | Parent + child isolation | +| Subagent | None | `run_subagent()` function | +| Return value | N/A | Summary text only | + +## 動手試試 + +```sh +cd learn-claude-code +python agents/s04_subagent.py +``` + +1. `Use a subtask to find what testing framework this project uses` +2. `Delegate: read all .py files and summarize what each one does` +3. `Use a task to create a new module, then verify it from here` diff --git a/docs/zhtw/s05-skill-loading.md b/docs/zhtw/s05-skill-loading.md new file mode 100644 index 000000000..eb26d7b66 --- /dev/null +++ b/docs/zhtw/s05-skill-loading.md @@ -0,0 +1,106 @@ +# s05: Skills + +`s01 > s02 > s03 > s04 > [ s05 ] s06 | s07 > s08 > s09 > s10 > s11 > s12` + +> *"Load knowledge when you need it, not upfront"* -- 透過 tool_result 注入,不塞進 system prompt。 + +## 問題 + +你希望 agent 遵循特定領域流程:git 慣例、測試模式、code review checklist。若把所有內容都塞進 system prompt,會把大量 token 浪費在根本用不到的 skills 上。10 個 skill、每個 2000 tokens,就等於 20,000 tokens;而大多數任務其實只會用到其中一小部分。 + +## 解法 + +``` +System prompt (Layer 1 -- always present): ++--------------------------------------+ +| You are a coding agent. | +| Skills available: | +| - git: Git workflow helpers | ~100 tokens/skill +| - test: Testing best practices | ++--------------------------------------+ + +When model calls load_skill("git"): ++--------------------------------------+ +| tool_result (Layer 2 -- on demand): | +| | +| Full git workflow instructions... | ~2000 tokens +| Step 1: ... | +| | ++--------------------------------------+ +``` + +Layer 1:system prompt 只放 skill *名稱*(便宜)。Layer 2:完整 skill *內容* 透過 tool_result 按需載入(昂貴)。 + +## 運作方式 + +1. 每個 skill 是一個資料夾,裡面放 `SKILL.md`(含 YAML frontmatter)。 + +``` +skills/ + pdf/ + SKILL.md # ---\n name: pdf\n description: Process PDF files\n ---\n ... + code-review/ + SKILL.md # ---\n name: code-review\n description: Review code\n ---\n ... +``` + +2. SkillLoader 掃描 `SKILL.md`,預設使用資料夾名稱作為 skill ID。 + +```python +class SkillLoader: + def __init__(self, skills_dir: Path): + self.skills = {} + for f in sorted(skills_dir.rglob("SKILL.md")): + text = f.read_text() + meta, body = self._parse_frontmatter(text) + name = meta.get("name", f.parent.name) + self.skills[name] = {"meta": meta, "body": body} + + def get_descriptions(self) -> str: + lines = [] + for name, skill in self.skills.items(): + desc = skill["meta"].get("description", "") + lines.append(f" - {name}: {desc}") + return "\n".join(lines) + + def get_content(self, name: str) -> str: + skill = self.skills.get(name) + if not skill: + return f"Error: Unknown skill '{name}'." + return f"\n{skill['body']}\n" +``` + +3. Layer 1 放進 system prompt;Layer 2 只是一般 tool handler。 + +```python +SYSTEM = f"""You are a coding agent at {WORKDIR}. +Skills available: +{SKILL_LOADER.get_descriptions()}""" + +TOOL_HANDLERS = { + # ...base tools... + "load_skill": lambda **kw: SKILL_LOADER.get_content(kw["name"]), +} +``` + +模型先知道有哪些 skills(低成本),再在需要時載入內容(高成本)。 + +## 相較 s04 的變更 + +| Component | Before (s04) | After (s05) | +|----------------|------------------|----------------------------| +| Tools | 5 (base + task) | 5 (base + load_skill) | +| System prompt | Static string | + skill descriptions | +| Knowledge | None | skills/\*/SKILL.md files | +| Injection | None | Two-layer (system + result)| + +## 動手試試 + +```sh +cd learn-claude-code +python agents/s05_skill_loading.py +``` + +1. `What skills are available?` +2. `Load the agent-builder skill and follow its instructions` +3. `I need to do a code review -- load the relevant skill first` +4. `Build an MCP server using the mcp-builder skill` diff --git a/docs/zhtw/s06-context-compact.md b/docs/zhtw/s06-context-compact.md new file mode 100644 index 000000000..109afcd83 --- /dev/null +++ b/docs/zhtw/s06-context-compact.md @@ -0,0 +1,123 @@ +# s06: Context Compact + +`s01 > s02 > s03 > s04 > s05 > [ s06 ] | s07 > s08 > s09 > s10 > s11 > s12` + +> *"Context will fill up; you need a way to make room"* -- 三層壓縮策略,讓 sessions 幾乎可無限延伸。 + +## 問題 + +context window 是有限的。一次 `read_file` 讀到 1000 行,可能就吃掉約 4000 tokens。讀 30 個檔案再跑 20 次 bash,整體就會超過 100,000 tokens。若沒有壓縮策略,agent 無法穩定處理大型程式碼庫。 + +## 解法 + +三層機制,壓縮強度逐層提高: + +``` +Every turn: ++------------------+ +| Tool call result | ++------------------+ + | + v +[Layer 1: micro_compact] (silent, every turn) + Replace tool_result > 3 turns old + with "[Previous: used {tool_name}]" + | + v +[Check: tokens > 50000?] + | | + no yes + | | + v v +continue [Layer 2: auto_compact] + Save transcript to .transcripts/ + LLM summarizes conversation. + Replace all messages with [summary]. + | + v + [Layer 3: compact tool] + Model calls compact explicitly. + Same summarization as auto_compact. +``` + +## 運作方式 + +1. **Layer 1 -- micro_compact**:每次呼叫 LLM 前,把過舊 tool results 換成 placeholder。 + +```python +def micro_compact(messages: list) -> list: + tool_results = [] + for i, msg in enumerate(messages): + if msg["role"] == "user" and isinstance(msg.get("content"), list): + for j, part in enumerate(msg["content"]): + if isinstance(part, dict) and part.get("type") == "tool_result": + tool_results.append((i, j, part)) + if len(tool_results) <= KEEP_RECENT: + return messages + for _, _, part in tool_results[:-KEEP_RECENT]: + if len(part.get("content", "")) > 100: + part["content"] = f"[Previous: used {tool_name}]" + return messages +``` + +2. **Layer 2 -- auto_compact**:tokens 超過門檻時,先把完整對話存到磁碟,再請 LLM 產生摘要。 + +```python +def auto_compact(messages: list) -> list: + # Save transcript for recovery + transcript_path = TRANSCRIPT_DIR / f"transcript_{int(time.time())}.jsonl" + with open(transcript_path, "w") as f: + for msg in messages: + f.write(json.dumps(msg, default=str) + "\n") + # LLM summarizes + response = client.messages.create( + model=MODEL, + messages=[{"role": "user", "content": + "Summarize this conversation for continuity..." + + json.dumps(messages, default=str)[:80000]}], + max_tokens=2000, + ) + return [ + {"role": "user", "content": f"[Compressed]\n\n{response.content[0].text}"}, + {"role": "assistant", "content": "Understood. Continuing."}, + ] +``` + +3. **Layer 3 -- manual compact**:模型可透過 `compact` 工具主動觸發同樣的摘要壓縮流程。 + +4. 迴圈整合三層機制: + +```python +def agent_loop(messages: list): + while True: + micro_compact(messages) # Layer 1 + if estimate_tokens(messages) > THRESHOLD: + messages[:] = auto_compact(messages) # Layer 2 + response = client.messages.create(...) + # ... tool execution ... + if manual_compact: + messages[:] = auto_compact(messages) # Layer 3 +``` + +transcripts 會把完整歷史保存到磁碟。資料不是消失,而是移出「活躍 context」。 + +## 相較 s05 的變更 + +| Component | Before (s05) | After (s06) | +|----------------|------------------|----------------------------| +| Tools | 5 | 5 (base + compact) | +| Context mgmt | None | Three-layer compression | +| Micro-compact | None | Old results -> placeholders| +| Auto-compact | None | Token threshold trigger | +| Transcripts | None | Saved to .transcripts/ | + +## 動手試試 + +```sh +cd learn-claude-code +python agents/s06_context_compact.py +``` + +1. `Read every Python file in the agents/ directory one by one` (watch micro-compact replace old results) +2. `Keep reading files until compression triggers automatically` +3. `Use the compact tool to manually compress the conversation` diff --git a/docs/zhtw/s07-task-system.md b/docs/zhtw/s07-task-system.md new file mode 100644 index 000000000..f9730a66c --- /dev/null +++ b/docs/zhtw/s07-task-system.md @@ -0,0 +1,125 @@ +# s07: Task System + +`s01 > s02 > s03 > s04 > s05 > s06 | [ s07 ] s08 > s09 > s10 > s11 > s12` + +> *"Break big goals into small tasks, order them, persist to disk"* -- 檔案式 task graph(含 dependencies),是 multi-agent 協作的基礎。 + +## 問題 + +s03 的 TodoManager 只是記憶體中的平面 checklist:沒有順序、沒有 dependencies、狀態也只有做完或沒做完。真實目標往往有結構 -- task B 依賴 task A、tasks C 與 D 可平行、task E 要等 C 與 D 都完成。 + +沒有明確關係,agent 就無法判斷哪些可做、哪些被卡住、哪些可同時進行。而且清單只在記憶體裡,經過 context compression(s06)就會被洗掉。 + +## 解法 + +把 checklist 升級成「落地到磁碟」的 **task graph**。每個 task 都是一個 JSON 檔案,包含 status、dependencies(`blockedBy`)與 dependents(`blocks`)。這張圖會即時回答三個問題: + +- **哪些可做?** -- `pending` 且 `blockedBy` 為空的 tasks。 +- **哪些被卡住?** -- 正在等待未完成 dependencies 的 tasks。 +- **哪些已完成?** -- `completed` tasks;完成時會自動解除 dependents 的阻塞。 + +``` +.tasks/ + task_1.json {"id":1, "status":"completed"} + task_2.json {"id":2, "blockedBy":[1], "status":"pending"} + task_3.json {"id":3, "blockedBy":[1], "status":"pending"} + task_4.json {"id":4, "blockedBy":[2,3], "status":"pending"} + +Task graph (DAG): + +----------+ + +--> | task 2 | --+ + | | pending | | ++----------+ +----------+ +--> +----------+ +| task 1 | | task 4 | +| completed| --> +----------+ +--> | blocked | ++----------+ | task 3 | --+ +----------+ + | pending | + +----------+ + +Ordering: task 1 must finish before 2 and 3 +Parallelism: tasks 2 and 3 can run at the same time +Dependencies: task 4 waits for both 2 and 3 +Status: pending -> in_progress -> completed +``` + +這個 task graph 從 s07 開始成為協作主幹:s08 的背景執行、s09+ 的多 agent 團隊、s12 的 worktree 隔離,都會讀寫同一套結構。 + +## 運作方式 + +1. **TaskManager**:每個 task 一個 JSON 檔,提供含 dependency graph 的 CRUD。 + +```python +class TaskManager: + def __init__(self, tasks_dir: Path): + self.dir = tasks_dir + self.dir.mkdir(exist_ok=True) + self._next_id = self._max_id() + 1 + + def create(self, subject, description=""): + task = {"id": self._next_id, "subject": subject, + "status": "pending", "blockedBy": [], + "blocks": [], "owner": ""} + self._save(task) + self._next_id += 1 + return json.dumps(task, indent=2) +``` + +2. **Dependency resolution**:task 完成後,會把自己的 ID 從其他 task 的 `blockedBy` 清掉,自動解鎖 dependents。 + +```python +def _clear_dependency(self, completed_id): + for f in self.dir.glob("task_*.json"): + task = json.loads(f.read_text()) + if completed_id in task.get("blockedBy", []): + task["blockedBy"].remove(completed_id) + self._save(task) +``` + +3. **Status + dependency wiring**:`update` 同時處理狀態遷移與 dependency 關係。 + +```python +def update(self, task_id, status=None, + add_blocked_by=None, add_blocks=None): + task = self._load(task_id) + if status: + task["status"] = status + if status == "completed": + self._clear_dependency(task_id) + self._save(task) +``` + +4. 四個 task 工具加入 dispatch map。 + +```python +TOOL_HANDLERS = { + # ...base tools... + "task_create": lambda **kw: TASKS.create(kw["subject"]), + "task_update": lambda **kw: TASKS.update(kw["task_id"], kw.get("status")), + "task_list": lambda **kw: TASKS.list_all(), + "task_get": lambda **kw: TASKS.get(kw["task_id"]), +} +``` + +從 s07 開始,task graph 是預設的多步任務模型;s03 的 Todo 則保留給單次 session 的快速 checklist。 + +## 相較 s06 的變更 + +| Component | Before (s06) | After (s07) | +|---|---|---| +| Tools | 5 | 8 (`task_create/update/list/get`) | +| Planning model | Flat checklist (in-memory) | Task graph with dependencies (on disk) | +| Relationships | None | `blockedBy` + `blocks` edges | +| Status tracking | Done or not | `pending` -> `in_progress` -> `completed` | +| Persistence | Lost on compression | Survives compression and restarts | + +## 動手試試 + +```sh +cd learn-claude-code +python agents/s07_task_system.py +``` + +1. `Create 3 tasks: "Setup project", "Write code", "Write tests". Make them depend on each other in order.` +2. `List all tasks and show the dependency graph` +3. `Complete task 1 and then list tasks to see task 2 unblocked` +4. `Create a task board for refactoring: parse -> transform -> emit -> test, where transform and emit can run in parallel after parse` diff --git a/docs/zhtw/s08-background-tasks.md b/docs/zhtw/s08-background-tasks.md new file mode 100644 index 000000000..6cf755604 --- /dev/null +++ b/docs/zhtw/s08-background-tasks.md @@ -0,0 +1,107 @@ +# s08: Background Tasks + +`s01 > s02 > s03 > s04 > s05 > s06 | s07 > [ s08 ] s09 > s10 > s11 > s12` + +> *"Run slow operations in the background; the agent keeps thinking"* -- daemon threads 在背景執行命令,完成時注入通知。 + +## 問題 + +有些命令會跑很久:`npm install`、`pytest`、`docker build`。若迴圈是 blocking,模型只能乾等。當使用者說「先安裝相依套件,安裝同時幫我建立設定檔」,agent 會變成串行執行,而不是平行。 + +## 解法 + +``` +Main thread Background thread ++-----------------+ +-----------------+ +| agent loop | | subprocess runs | +| ... | | ... | +| [LLM call] <---+------- | enqueue(result) | +| ^drain queue | +-----------------+ ++-----------------+ + +Timeline: +Agent --[spawn A]--[spawn B]--[other work]---- + | | + v v + [A runs] [B runs] (parallel) + | | + +-- results injected before next LLM call --+ +``` + +## 運作方式 + +1. BackgroundManager 用 thread-safe notification queue 管理背景任務。 + +```python +class BackgroundManager: + def __init__(self): + self.tasks = {} + self._notification_queue = [] + self._lock = threading.Lock() +``` + +2. `run()` 啟動 daemon thread,立即回傳。 + +```python +def run(self, command: str) -> str: + task_id = str(uuid.uuid4())[:8] + self.tasks[task_id] = {"status": "running", "command": command} + thread = threading.Thread( + target=self._execute, args=(task_id, command), daemon=True) + thread.start() + return f"Background task {task_id} started" +``` + +3. subprocess 完成後,把結果丟進 notification queue。 + +```python +def _execute(self, task_id, command): + try: + r = subprocess.run(command, shell=True, cwd=WORKDIR, + capture_output=True, text=True, timeout=300) + output = (r.stdout + r.stderr).strip()[:50000] + except subprocess.TimeoutExpired: + output = "Error: Timeout (300s)" + with self._lock: + self._notification_queue.append({ + "task_id": task_id, "result": output[:500]}) +``` + +4. 每次呼叫 LLM 前,agent loop 先 drain notifications。 + +```python +def agent_loop(messages: list): + while True: + notifs = BG.drain_notifications() + if notifs: + notif_text = "\n".join( + f"[bg:{n['task_id']}] {n['result']}" for n in notifs) + messages.append({"role": "user", + "content": f"\n{notif_text}\n" + f""}) + messages.append({"role": "assistant", + "content": "Noted background results."}) + response = client.messages.create(...) +``` + +迴圈仍維持單執行緒;只有 subprocess I/O 被平行化。 + +## 相較 s07 的變更 + +| Component | Before (s07) | After (s08) | +|----------------|------------------|----------------------------| +| Tools | 8 | 6 (base + background_run + check)| +| Execution | Blocking only | Blocking + background threads| +| Notification | None | Queue drained per loop | +| Concurrency | None | Daemon threads | + +## 動手試試 + +```sh +cd learn-claude-code +python agents/s08_background_tasks.py +``` + +1. `Run "sleep 5 && echo done" in the background, then create a file while it runs` +2. `Start 3 background tasks: "sleep 2", "sleep 4", "sleep 6". Check their status.` +3. `Run pytest in the background and keep working on other things` diff --git a/docs/zhtw/s09-agent-teams.md b/docs/zhtw/s09-agent-teams.md new file mode 100644 index 000000000..0aade0e68 --- /dev/null +++ b/docs/zhtw/s09-agent-teams.md @@ -0,0 +1,125 @@ +# s09: Agent Teams + +`s01 > s02 > s03 > s04 > s05 > s06 | s07 > s08 > [ s09 ] s10 > s11 > s12` + +> *"When the task is too big for one, delegate to teammates"* -- 持久化 teammates + 非同步 mailboxes。 + +## 問題 + +subagents(s04)是一次性的:生成、工作、回傳摘要、結束。沒有身分,也沒有跨呼叫記憶。背景任務(s08)能跑 shell 命令,但無法做 LLM 導向的決策。 + +真正的團隊協作需要三件事:(1) 可跨單次 prompt 存活的 persistent agents,(2) 身分與生命週期管理,(3) agents 之間的溝通通道。 + +## 解法 + +``` +Teammate lifecycle: + spawn -> WORKING -> IDLE -> WORKING -> ... -> SHUTDOWN + +Communication: + .team/ + config.json <- team roster + statuses + inbox/ + alice.jsonl <- append-only, drain-on-read + bob.jsonl + lead.jsonl + + +--------+ send("alice","bob","...") +--------+ + | alice | -----------------------------> | bob | + | loop | bob.jsonl << {json_line} | loop | + +--------+ +--------+ + ^ | + | BUS.read_inbox("alice") | + +---- alice.jsonl -> read + drain ---------+ +``` + +## 運作方式 + +1. TeammateManager 用 `config.json` 維護 team roster。 + +```python +class TeammateManager: + def __init__(self, team_dir: Path): + self.dir = team_dir + self.dir.mkdir(exist_ok=True) + self.config_path = self.dir / "config.json" + self.config = self._load_config() + self.threads = {} +``` + +2. `spawn()` 會建立 teammate,並在 thread 中啟動其 agent loop。 + +```python +def spawn(self, name: str, role: str, prompt: str) -> str: + member = {"name": name, "role": role, "status": "working"} + self.config["members"].append(member) + self._save_config() + thread = threading.Thread( + target=self._teammate_loop, + args=(name, role, prompt), daemon=True) + thread.start() + return f"Spawned teammate '{name}' (role: {role})" +``` + +3. MessageBus:append-only JSONL inbox。`send()` 追加 JSON line;`read_inbox()` 讀完即清空。 + +```python +class MessageBus: + def send(self, sender, to, content, msg_type="message", extra=None): + msg = {"type": msg_type, "from": sender, + "content": content, "timestamp": time.time()} + if extra: + msg.update(extra) + with open(self.dir / f"{to}.jsonl", "a") as f: + f.write(json.dumps(msg) + "\n") + + def read_inbox(self, name): + path = self.dir / f"{name}.jsonl" + if not path.exists(): return "[]" + msgs = [json.loads(l) for l in path.read_text().strip().splitlines() if l] + path.write_text("") # drain + return json.dumps(msgs, indent=2) +``` + +4. 每位 teammate 在每次 LLM 呼叫前都先看 inbox,並把收到的訊息注入 context。 + +```python +def _teammate_loop(self, name, role, prompt): + messages = [{"role": "user", "content": prompt}] + for _ in range(50): + inbox = BUS.read_inbox(name) + if inbox != "[]": + messages.append({"role": "user", + "content": f"{inbox}"}) + messages.append({"role": "assistant", + "content": "Noted inbox messages."}) + response = client.messages.create(...) + if response.stop_reason != "tool_use": + break + # execute tools, append results... + self._find_member(name)["status"] = "idle" +``` + +## 相較 s08 的變更 + +| Component | Before (s08) | After (s09) | +|----------------|------------------|----------------------------| +| Tools | 6 | 9 (+spawn/send/read_inbox) | +| Agents | Single | Lead + N teammates | +| Persistence | None | config.json + JSONL inboxes| +| Threads | Background cmds | Full agent loops per thread| +| Lifecycle | Fire-and-forget | idle -> working -> idle | +| Communication | None | message + broadcast | + +## 動手試試 + +```sh +cd learn-claude-code +python agents/s09_agent_teams.py +``` + +1. `Spawn alice (coder) and bob (tester). Have alice send bob a message.` +2. `Broadcast "status update: phase 1 complete" to all teammates` +3. `Check the lead inbox for any messages` +4. Type `/team` to see the team roster with statuses +5. Type `/inbox` to manually check the lead's inbox diff --git a/docs/zhtw/s10-team-protocols.md b/docs/zhtw/s10-team-protocols.md new file mode 100644 index 000000000..b06a74aac --- /dev/null +++ b/docs/zhtw/s10-team-protocols.md @@ -0,0 +1,104 @@ +# s10: Team Protocols + +`s01 > s02 > s03 > s04 > s05 > s06 | s07 > s08 > s09 > [ s10 ] s11 > s12` + +> *"Teammates need shared communication rules"* -- 一套 request-response pattern 可驅動所有協商。 + +## 問題 + +s09 中,teammates 已能工作與溝通,但仍缺乏結構化協調: + +**Shutdown**:直接殺 thread 會留下半寫檔案與過期 config.json。你需要 handshake:lead 發請求,teammate 回覆 approve(收尾後退出)或 reject(繼續工作)。 + +**Plan approval**:當 lead 說「重構 auth module」時,teammate 會立刻開工。對高風險變更,應先把 plan 送審。 + +兩者本質相同:一方送出帶唯一 ID 的 request,另一方引用同一 ID 回應。 + +## 解法 + +``` +Shutdown Protocol Plan Approval Protocol +================== ====================== + +Lead Teammate Teammate Lead + | | | | + |--shutdown_req-->| |--plan_req------>| + | {req_id:"abc"} | | {req_id:"xyz"} | + | | | | + |<--shutdown_resp-| |<--plan_resp-----| + | {req_id:"abc", | | {req_id:"xyz", | + | approve:true} | | approve:true} | + +Shared FSM: + [pending] --approve--> [approved] + [pending] --reject---> [rejected] + +Trackers: + shutdown_requests = {req_id: {target, status}} + plan_requests = {req_id: {from, plan, status}} +``` + +## 運作方式 + +1. lead 發起 shutdown:先產生 request_id,再透過 inbox 發送。 + +```python +shutdown_requests = {} + +def handle_shutdown_request(teammate: str) -> str: + req_id = str(uuid.uuid4())[:8] + shutdown_requests[req_id] = {"target": teammate, "status": "pending"} + BUS.send("lead", teammate, "Please shut down gracefully.", + "shutdown_request", {"request_id": req_id}) + return f"Shutdown request {req_id} sent (status: pending)" +``` + +2. teammate 收到請求後,以 approve/reject 回覆。 + +```python +if tool_name == "shutdown_response": + req_id = args["request_id"] + approve = args["approve"] + shutdown_requests[req_id]["status"] = "approved" if approve else "rejected" + BUS.send(sender, "lead", args.get("reason", ""), + "shutdown_response", + {"request_id": req_id, "approve": approve}) +``` + +3. Plan approval 也是同一模式:teammate 送 plan(產生 request_id),lead 以同一 request_id 審核。 + +```python +plan_requests = {} + +def handle_plan_review(request_id, approve, feedback=""): + req = plan_requests[request_id] + req["status"] = "approved" if approve else "rejected" + BUS.send("lead", req["from"], feedback, + "plan_approval_response", + {"request_id": request_id, "approve": approve}) +``` + +一個 FSM,兩個用途。`pending -> approved | rejected` 這個狀態機可處理任意 request-response protocol。 + +## 相較 s09 的變更 + +| Component | Before (s09) | After (s10) | +|----------------|------------------|------------------------------| +| Tools | 9 | 12 (+shutdown_req/resp +plan)| +| Shutdown | Natural exit only| Request-response handshake | +| Plan gating | None | Submit/review with approval | +| Correlation | None | request_id per request | +| FSM | None | pending -> approved/rejected | + +## 動手試試 + +```sh +cd learn-claude-code +python agents/s10_team_protocols.py +``` + +1. `Spawn alice as a coder. Then request her shutdown.` +2. `List teammates to see alice's status after shutdown approval` +3. `Spawn bob with a risky refactoring task. Review and reject his plan.` +4. `Spawn charlie, have him submit a plan, then approve it.` +5. Type `/team` to monitor statuses diff --git a/docs/zhtw/s11-autonomous-agents.md b/docs/zhtw/s11-autonomous-agents.md new file mode 100644 index 000000000..f341eddfc --- /dev/null +++ b/docs/zhtw/s11-autonomous-agents.md @@ -0,0 +1,140 @@ +# s11: Autonomous Agents + +`s01 > s02 > s03 > s04 > s05 > s06 | s07 > s08 > s09 > s10 > [ s11 ] s12` + +> *"Teammates scan the board and claim tasks themselves"* -- 不需要 lead 一項一項指派。 + +## 問題 + +在 s09-s10 中,teammates 只有在被明確指示時才會工作。lead 必須為每位 teammate 下具體 prompt。task board 上有 10 個未認領任務?lead 得手動分派 10 次。這無法擴展。 + +真正的自主是:teammates 自己掃 task board、認領未認領任務、完成後再繼續找下一個。 + +還有一個細節:context compression(s06)後,agent 可能忘記自己是誰。identity re-injection 可解決這件事。 + +## 解法 + +``` +Teammate lifecycle with idle cycle: + ++-------+ +| spawn | ++---+---+ + | + v ++-------+ tool_use +-------+ +| WORK | <------------- | LLM | ++---+---+ +-------+ + | + | stop_reason != tool_use (or idle tool called) + v ++--------+ +| IDLE | poll every 5s for up to 60s ++---+----+ + | + +---> check inbox --> message? ----------> WORK + | + +---> scan .tasks/ --> unclaimed? -------> claim -> WORK + | + +---> 60s timeout ----------------------> SHUTDOWN + +Identity re-injection after compression: + if len(messages) <= 3: + messages.insert(0, identity_block) +``` + +## 運作方式 + +1. teammate loop 分兩段:WORK 與 IDLE。當 LLM 停止呼叫工具(或呼叫 `idle`),就進入 IDLE。 + +```python +def _loop(self, name, role, prompt): + while True: + # -- WORK PHASE -- + messages = [{"role": "user", "content": prompt}] + for _ in range(50): + response = client.messages.create(...) + if response.stop_reason != "tool_use": + break + # execute tools... + if idle_requested: + break + + # -- IDLE PHASE -- + self._set_status(name, "idle") + resume = self._idle_poll(name, messages) + if not resume: + self._set_status(name, "shutdown") + return + self._set_status(name, "working") +``` + +2. IDLE 階段會循環檢查 inbox 與 task board。 + +```python +def _idle_poll(self, name, messages): + for _ in range(IDLE_TIMEOUT // POLL_INTERVAL): # 60s / 5s = 12 + time.sleep(POLL_INTERVAL) + inbox = BUS.read_inbox(name) + if inbox: + messages.append({"role": "user", + "content": f"{inbox}"}) + return True + unclaimed = scan_unclaimed_tasks() + if unclaimed: + claim_task(unclaimed[0]["id"], name) + messages.append({"role": "user", + "content": f"Task #{unclaimed[0]['id']}: " + f"{unclaimed[0]['subject']}"}) + return True + return False # timeout -> shutdown +``` + +3. task board 掃描條件:`pending`、無 owner、且未被 blocked。 + +```python +def scan_unclaimed_tasks() -> list: + unclaimed = [] + for f in sorted(TASKS_DIR.glob("task_*.json")): + task = json.loads(f.read_text()) + if (task.get("status") == "pending" + and not task.get("owner") + and not task.get("blockedBy")): + unclaimed.append(task) + return unclaimed +``` + +4. identity re-injection:若 context 太短(代表可能剛壓縮過),插入身分區塊。 + +```python +if len(messages) <= 3: + messages.insert(0, {"role": "user", + "content": f"You are '{name}', role: {role}, " + f"team: {team_name}. Continue your work."}) + messages.insert(1, {"role": "assistant", + "content": f"I am {name}. Continuing."}) +``` + +## 相較 s10 的變更 + +| Component | Before (s10) | After (s11) | +|----------------|------------------|----------------------------| +| Tools | 12 | 14 (+idle, +claim_task) | +| Autonomy | Lead-directed | Self-organizing | +| Idle phase | None | Poll inbox + task board | +| Task claiming | Manual only | Auto-claim unclaimed tasks | +| Identity | System prompt | + re-injection after compress| +| Timeout | None | 60s idle -> auto shutdown | + +## 動手試試 + +```sh +cd learn-claude-code +python agents/s11_autonomous_agents.py +``` + +1. `Create 3 tasks on the board, then spawn alice and bob. Watch them auto-claim.` +2. `Spawn a coder teammate and let it find work from the task board itself` +3. `Create tasks with dependencies. Watch teammates respect the blocked order.` +4. Type `/tasks` to see the task board with owners +5. Type `/team` to monitor who is working vs idle diff --git a/docs/zhtw/s12-worktree-task-isolation.md b/docs/zhtw/s12-worktree-task-isolation.md new file mode 100644 index 000000000..3ad5bf0af --- /dev/null +++ b/docs/zhtw/s12-worktree-task-isolation.md @@ -0,0 +1,119 @@ +# s12: Worktree + Task Isolation + +`s01 > s02 > s03 > s04 > s05 > s06 | s07 > s08 > s09 > s10 > s11 > [ s12 ]` + +> *"Each works in its own directory, no interference"* -- tasks 管目標,worktrees 管目錄,兩者以 ID 綁定。 + +## 問題 + +到 s11 為止,agents 已能自主認領並完成任務;但所有任務都在同一個共享目錄執行。若兩個 agents 同時重構不同模組,很容易互相干擾:agent A 改 `config.py`,agent B 也改 `config.py`,未暫存變更混在一起,兩邊都難以乾淨回復。 + +task board 只管理 *做什麼*,不管理 *在哪裡做*。解法是:每個 task 給一個獨立 git worktree。tasks 管目標,worktrees 管執行上下文,並用 task ID 綁定。 + +## 解法 + +``` +Control plane (.tasks/) Execution plane (.worktrees/) ++------------------+ +------------------------+ +| task_1.json | | auth-refactor/ | +| status: in_progress <------> branch: wt/auth-refactor +| worktree: "auth-refactor" | task_id: 1 | ++------------------+ +------------------------+ +| task_2.json | | ui-login/ | +| status: pending <------> branch: wt/ui-login +| worktree: "ui-login" | task_id: 2 | ++------------------+ +------------------------+ + | + index.json (worktree registry) + events.jsonl (lifecycle log) + +State machines: + Task: pending -> in_progress -> completed + Worktree: absent -> active -> removed | kept +``` + +## 運作方式 + +1. **先建立 task。** 先把目標落地保存。 + +```python +TASKS.create("Implement auth refactor") +# -> .tasks/task_1.json status=pending worktree="" +``` + +2. **建立 worktree 並綁定 task。** 傳入 `task_id` 後,task 會自動進入 `in_progress`。 + +```python +WORKTREES.create("auth-refactor", task_id=1) +# -> git worktree add -b wt/auth-refactor .worktrees/auth-refactor HEAD +# -> index.json gets new entry, task_1.json gets worktree="auth-refactor" +``` + +綁定會同時寫入兩邊狀態: + +```python +def bind_worktree(self, task_id, worktree): + task = self._load(task_id) + task["worktree"] = worktree + if task["status"] == "pending": + task["status"] = "in_progress" + self._save(task) +``` + +3. **在 worktree 裡執行命令。** `cwd` 指向隔離目錄。 + +```python +subprocess.run(command, shell=True, cwd=worktree_path, + capture_output=True, text=True, timeout=300) +``` + +4. **收尾。** 有兩種方式: + - `worktree_keep(name)` -- 保留目錄,之後可繼續用。 + - `worktree_remove(name, complete_task=True)` -- 刪除目錄、完成綁定 task、送出事件。一次呼叫完成 teardown + completion。 + +```python +def remove(self, name, force=False, complete_task=False): + self._run_git(["worktree", "remove", wt["path"]]) + if complete_task and wt.get("task_id") is not None: + self.tasks.update(wt["task_id"], status="completed") + self.tasks.unbind_worktree(wt["task_id"]) + self.events.emit("task.completed", ...) +``` + +5. **事件流。** 每個生命週期步驟都會寫進 `.worktrees/events.jsonl`: + +```json +{ + "event": "worktree.remove.after", + "task": {"id": 1, "status": "completed"}, + "worktree": {"name": "auth-refactor", "status": "removed"}, + "ts": 1730000000 +} +``` + +事件包含:`worktree.create.before/after/failed`、`worktree.remove.before/after/failed`、`worktree.keep`、`task.completed`。 + +若發生 crash,可透過磁碟上的 `.tasks/` + `.worktrees/index.json` 重建狀態。對話記憶是易失的,檔案狀態才是可持久恢復的。 + +## 相較 s11 的變更 + +| Component | Before (s11) | After (s12) | +|--------------------|----------------------------|----------------------------------------------| +| Coordination | Task board (owner/status) | Task board + explicit worktree binding | +| Execution scope | Shared directory | Task-scoped isolated directory | +| Recoverability | Task status only | Task status + worktree index | +| Teardown | Task completion | Task completion + explicit keep/remove | +| Lifecycle visibility | Implicit in logs | Explicit events in `.worktrees/events.jsonl` | + +## 動手試試 + +```sh +cd learn-claude-code +python agents/s12_worktree_task_isolation.py +``` + +1. `Create tasks for backend auth and frontend login page, then list tasks.` +2. `Create worktree "auth-refactor" for task 1, then bind task 2 to a new worktree "ui-login".` +3. `Run "git status --short" in worktree "auth-refactor".` +4. `Keep worktree "ui-login", then list worktrees and inspect events.` +5. `Remove worktree "auth-refactor" with complete_task=true, then list tasks/worktrees/events.`