Ver código fonte

更新 Claude Code 课件

zengli 1 semana atrás
pai
commit
b2d538dae3
43 arquivos alterados com 4007 adições e 640 exclusões
  1. 1 1
      .env.example
  2. 1 3
      .idea/learn-claude-code.iml
  3. 1 0
      s01_agent_loop/code.py
  4. 149 0
      s01_agent_loop/代码讲解.md
  5. 17 0
      s02_tool_use/hello_world.py
  6. 148 0
      s02_tool_use/代码讲解.md
  7. 135 0
      s03_permission/代码讲解.md
  8. 14 14
      s04_hooks/code.py
  9. 1 0
      s04_hooks/helloword.py
  10. 118 0
      s04_hooks/代码讲解.md
  11. 10 10
      s05_todo_write/code.py
  12. 144 0
      s05_todo_write/代码讲解.md
  13. 14 14
      s06_subagent/code.py
  14. 117 0
      s06_subagent/代码讲解.md
  15. 15 15
      s07_skill_loading/code.py
  16. 124 0
      s07_skill_loading/代码讲解.md
  17. 79 33
      s08_context_compact/code.py
  18. 833 0
      s08_context_compact/代码讲解.md
  19. 42 44
      s09_memory/code.py
  20. 198 0
      s09_memory/代码讲解.md
  21. 9 9
      s10_system_prompt/code.py
  22. 144 0
      s10_system_prompt/代码讲解.md
  23. 10 11
      s11_error_recovery/code.py
  24. 157 0
      s11_error_recovery/代码讲解.md
  25. 20 15
      s12_task_system/code.py
  26. 154 0
      s12_task_system/代码讲解.md
  27. 26 22
      s13_background_tasks/code.py
  28. 188 0
      s13_background_tasks/代码讲解.md
  29. 48 44
      s14_cron_scheduler/code.py
  30. 167 0
      s14_cron_scheduler/代码讲解.md
  31. 60 58
      s15_agent_teams/code.py
  32. 142 0
      s15_agent_teams/代码讲解.md
  33. 59 59
      s16_team_protocols/code.py
  34. 200 0
      s16_team_protocols/代码讲解.md
  35. 45 36
      s17_autonomous_agents/code.py
  36. 165 0
      s17_autonomous_agents/代码讲解.md
  37. 0 0
      skills/agent-builder/SKill.md
  38. 30 30
      skills/agent-builder/references/minimal-agent.py
  39. 79 79
      skills/agent-builder/references/subagent-pattern.py
  40. 68 68
      skills/agent-builder/references/tool-templates.py
  41. 75 75
      skills/agent-builder/scripts/init_agent.py
  42. 0 0
      skills/mcp-builder/skill.md
  43. 0 0
      skills/pdf/skill.md

+ 1 - 1
.env.example

@@ -6,7 +6,7 @@ ANTHROPIC_API_KEY=sk-ant-xxx
 MODEL_ID=claude-sonnet-4-6
 
 # Base URL (optional, for Anthropic-compatible providers)
-# ANTHROPIC_BASE_URL=https://api.anthropic.com
+ANTHROPIC_BASE_URL=https://api.anthropic.com
 
 # =============================================================================
 #  Anthropic-compatible providers

+ 1 - 3
.idea/learn-claude-code.iml

@@ -4,15 +4,13 @@
     <content url="file://$MODULE_DIR$">
       <excludeFolder url="file://$MODULE_DIR$/.venv" />
     </content>
-    <orderEntry type="jdk" jdkName="~/Documents/projects/learn-claude-code/.venv" jdkType="Python SDK" />
+    <orderEntry type="jdk" jdkName="~/Documents/projects/learn-claude-code/.venv (2)" jdkType="Python SDK" />
     <orderEntry type="sourceFolder" forTests="false" />
   </component>
-  <component name="PackageRequirementsSettings" />
   <component name="PyDocumentationSettings">
     <option name="format" value="PLAIN" />
     <option name="myDocStringFormat" value="Plain" />
   </component>
-  <component name="ReSTService" />
   <component name="TestRunnerService">
     <option name="PROJECT_TEST_RUNNER" value="py.test" />
   </component>

+ 1 - 0
s01_agent_loop/code.py

@@ -101,6 +101,7 @@ def agent_loop(messages: list):
             if block.type == "tool_use":
                 print(f"\033[33m$ {block.input['command']}\033[0m")
                 output = run_bash(block.input["command"])
+
                 print(output[:200])
                 results.append({
                     "type": "tool_result",

+ 149 - 0
s01_agent_loop/代码讲解.md

@@ -0,0 +1,149 @@
+# S01 Agent Loop 代码讲解
+
+这一节只讲一个核心:
+
+**Agent 不是一次问答,而是一个循环。模型决定要不要调用工具,程序执行工具,再把结果交还给模型。**
+
+---
+
+## 1. 本节新增内容
+
+- `TOOLS`:告诉模型现在有哪些工具可以用。
+- `run_bash()`:真正执行 bash 命令的 Python 函数。
+- `agent_loop(messages)`:最小 Agent Loop。
+
+---
+
+## 2. Messages 数据结构
+
+`messages` 是一个列表:
+
+```python
+messages: list[dict]
+```
+
+里面每一项是一条对话消息:
+
+```python
+{"role": "user", "content": "列出当前目录"}
+```
+
+模型返回工具调用后,程序会追加:
+
+```python
+{"role": "assistant", "content": response.content}
+```
+
+工具执行完成后,程序再追加:
+
+```python
+{
+    "role": "user",
+    "content": [
+        {
+            "type": "tool_result",
+            "tool_use_id": "toolu_xxx",
+            "content": "命令输出"
+        }
+    ]
+}
+```
+
+---
+
+## 3. `TOOLS`
+
+`TOOLS` 是给模型看的工具说明,不是 Python 真正执行工具的地方。
+
+```python
+TOOLS = [{
+    "name": "bash",
+    "description": "运行一条 shell 命令。",
+    "input_schema": {
+        "type": "object",
+        "properties": {"command": {"type": "string"}},
+        "required": ["command"],
+    },
+}]
+```
+
+可以理解成:
+
+```python
+工具名:bash
+参数:command
+参数类型:string
+```
+
+模型看到这个 schema 后,才知道自己可以生成这样的工具请求:
+
+```python
+{
+    "type": "tool_use",
+    "name": "bash",
+    "input": {"command": "ls"}
+}
+```
+
+---
+
+## 4. `run_bash(command)`
+
+`run_bash()` 是真正执行命令的地方。
+
+模型不会真的执行 bash,它只会提出:
+
+```python
+我要调用 bash,参数是 {"command": "ls"}
+```
+
+程序收到之后,才会调用:
+
+```python
+run_bash("ls")
+```
+
+返回值会被包装成 `tool_result`,再塞回 `messages`。
+
+---
+
+## 5. `agent_loop(messages)`
+
+这是本节课最核心的函数。
+
+流程可以理解成:
+
+```python
+while True:
+    调用模型
+    保存模型回复
+
+    如果模型没有要求调用工具:
+        结束
+
+    如果模型要求调用工具:
+        执行工具
+        把工具结果放回 messages
+        继续下一轮
+```
+
+关键点:
+
+**模型负责判断,工具负责执行,循环负责持续推进。**
+
+---
+
+## 6. 本节课堂重点
+
+不要把 Function Call 理解成“模型执行函数”。
+
+真正发生的是:
+
+```python
+模型生成 tool_use
+Python 执行 run_bash
+Python 生成 tool_result
+模型继续读取 tool_result
+```
+
+这就是 Coding Agent 的最小内核。

+ 17 - 0
s02_tool_use/hello_world.py

@@ -0,0 +1,17 @@
+#!/usr/bin/env python3
+
+"""一个简单的递归 demo:计算阶乘。"""
+
+
+def factorial(n: int) -> int:
+    """递归计算 n 的阶乘。"""
+    if n < 0:
+        raise ValueError("n must be non-negative")
+    if n in (0, 1):
+        return 1
+    return n * factorial(n - 1)
+
+
+if __name__ == "__main__":
+    number = 5
+    print(f"{number}! = {factorial(number)}")

+ 148 - 0
s02_tool_use/代码讲解.md

@@ -0,0 +1,148 @@
+# S02 Tool Use 代码讲解
+
+这一节只讲相对 S01 新增的内容:
+
+**从一个 bash 工具,扩展成多个专用工具,并用分发映射统一执行。**
+
+---
+
+## 1. 本节新增内容
+
+- `safe_path()`:限制文件操作只能发生在工作区内。
+- `run_read()`:读取文件。
+- `run_write()`:写入文件。
+- `run_edit()`:替换文件中的一段文本。
+- `run_glob()`:按 glob 模式查找文件。
+- `TOOL_HANDLERS`:把工具名映射到 Python 函数。
+
+---
+
+## 2. 为什么要拆工具
+
+S01 只有一个 `bash` 工具,什么都靠命令完成。
+
+这一节拆成多个工具:
+
+```python
+bash
+read_file
+write_file
+edit_file
+glob
+```
+
+好处是:
+
+- 模型更清楚每个工具应该怎么用。
+- 程序更容易做权限控制。
+- 日志里更容易看出 Agent 在做什么。
+- 读写文件不用都藏在 shell 命令里。
+
+---
+
+## 3. Tool Schema 数据结构
+
+每个工具都是一个字典:
+
+```python
+{
+    "name": "read_file",
+    "description": "读取文件内容。",
+    "input_schema": {
+        "type": "object",
+        "properties": {
+            "path": {"type": "string"},
+            "limit": {"type": "integer"}
+        },
+        "required": ["path"]
+    }
+}
+```
+
+可以理解成:
+
+```python
+工具名:read_file
+参数:
+  path: string,必填
+  limit: integer,可选
+```
+
+---
+
+## 4. `safe_path(p)`
+
+`safe_path()` 的作用是防止模型读写工作区外面的文件。
+
+```python
+path = (WORKDIR / p).resolve()
+```
+
+它会把用户传进来的相对路径变成绝对路径。
+
+然后检查:
+
+```python
+path.is_relative_to(WORKDIR)
+```
+
+意思是:这个路径必须还在当前项目目录里面。
+
+---
+
+## 5. `TOOL_HANDLERS`
+
+`TOOL_HANDLERS` 是工具分发器:
+
+```python
+TOOL_HANDLERS = {
+    "bash": run_bash,
+    "read_file": run_read,
+    "write_file": run_write,
+    "edit_file": run_edit,
+    "glob": run_glob,
+}
+```
+
+模型返回:
+
+```python
+block.name = "read_file"
+block.input = {"path": "README.md"}
+```
+
+程序就可以这样执行:
+
+```python
+handler = TOOL_HANDLERS.get(block.name)
+output = handler(**block.input)
+```
+
+`**block.input` 的意思是把字典拆成函数参数:
+
+```python
+{"path": "README.md", "limit": 20}
+```
+
+会变成:
+
+```python
+run_read(path="README.md", limit=20)
+```
+
+---
+
+## 6. 本节课堂重点
+
+Agent 的工具系统不是写死在循环里的。
+
+更好的结构是:
+
+```python
+模型选择工具名
+程序用 TOOL_HANDLERS 找到函数
+程序把 input 参数传进去
+函数返回执行结果
+```
+
+这样后面新增工具时,Agent Loop 不需要大改。

+ 135 - 0
s03_permission/代码讲解.md

@@ -0,0 +1,135 @@
+# S03 Permission 代码讲解
+
+这一节只讲相对 S02 新增的内容:
+
+**工具真正执行前,要先经过权限系统。**
+
+---
+
+## 1. 本节新增内容
+
+- `DENY_LIST`:绝对禁止的危险命令。
+- `PERMISSION_RULES`:需要用户确认的风险操作规则。
+- `check_deny_list()`:硬性拦截。
+- `check_rules()`:匹配需要审批的规则。
+- `ask_user()`:暂停并询问用户。
+- `check_permission()`:统一权限入口。
+
+---
+
+## 2. 为什么需要 Permission
+
+模型可能会生成危险操作,比如:
+
+```bash
+rm -rf /
+sudo ...
+shutdown
+```
+
+即使模型不是故意的,也可能因为上下文误解而做错事。
+
+所以 Agent 不能直接执行工具调用,而应该是:
+
+```python
+tool_use -> 权限检查 -> 允许才执行 -> tool_result
+```
+
+---
+
+## 3. `DENY_LIST`
+
+`DENY_LIST` 是字符串列表:
+
+```python
+DENY_LIST: list[str] = [
+    "rm -rf /",
+    "sudo",
+    "shutdown",
+]
+```
+
+只要命令里包含这些片段,就直接拒绝。
+
+这是第一道关卡:**绝对禁止**。
+
+---
+
+## 4. `PERMISSION_RULES`
+
+`PERMISSION_RULES` 是一组规则:
+
+```python
+PERMISSION_RULES: list[dict]
+```
+
+每条规则大概长这样:
+
+```python
+{
+    "tool": "bash",
+    "pattern": "rm ",
+    "reason": "可能删除文件"
+}
+```
+
+它不是直接拒绝,而是告诉程序:
+
+```python
+这个操作有风险,需要问用户
+```
+
+---
+
+## 5. `check_permission(block)`
+
+`block` 是模型返回的 `tool_use` 对象。
+
+典型结构:
+
+```python
+{
+    "type": "tool_use",
+    "name": "bash",
+    "input": {
+        "command": "rm test.txt"
+    }
+}
+```
+
+`check_permission()` 做三件事:
+
+```python
+1. 如果是 deny list,直接拒绝。
+2. 如果命中风险规则,询问用户。
+3. 如果都没命中,允许执行。
+```
+
+---
+
+## 6. Agent Loop 中的变化
+
+S02 中拿到 `tool_use` 后直接执行。
+
+S03 中多了一步:
+
+```python
+if not check_permission(block):
+    continue
+```
+
+这就是工具执行前的安全闸门。
+
+---
+
+## 7. 本节课堂重点
+
+Permission 不是让模型自己判断安不安全。
+
+真正的安全边界应该在程序里:
+
+```python
+模型只能提出请求
+程序决定是否执行
+用户可以参与审批
+```

+ 14 - 14
s04_hooks/code.py

@@ -1,6 +1,6 @@
 #!/usr/bin/env python3
 """
-s04: Hooks — 把扩展逻辑从循环中移出,挂到 Hooks 上。
+s04: 钩子系统 — 把扩展逻辑从循环中移出,挂到钩子上。
 
   用户输入问题
@@ -135,7 +135,7 @@ TOOL_HANDLERS = {
 
 
 # ═══════════════════════════════════════════════════════════
-#  新增于 s04: Hook 系统 (s03 权限逻辑现在通过 Hooks 实现)
+#  新增于 s04: 钩子系统(s03 权限逻辑现在通过钩子实现)
 # ═══════════════════════════════════════════════════════════
 
 HOOKS = {"UserPromptSubmit": [], "PreToolUse": [], "PostToolUse": [], "Stop": []}
@@ -151,12 +151,12 @@ def trigger_hooks(event: str, *args):
     return None
 
 
-# s03 权限检查逻辑,现在封装成 Hook
+# s03 权限检查逻辑,现在封装成钩子
 DENY_LIST = ["rm -rf /", "sudo", "shutdown", "reboot", "mkfs", "dd if="]
 DESTRUCTIVE = ["rm ", "> /etc/", "chmod 777"]
 
 def permission_hook(block):
-    """PreToolUse: s03 check_permission() logic moved here."""
+    """PreToolUse:这里承载从 s03 迁移过来的 check_permission() 逻辑。"""
     if block.name == "bash":
         for pattern in DENY_LIST:
             if pattern in block.input.get("command", ""):
@@ -180,28 +180,28 @@ def permission_hook(block):
     return None
 
 def log_hook(block):
-    """PreToolUse: log every tool call."""
+    """PreToolUse:记录每一次工具调用。"""
     args_preview = str(list(block.input.values())[:2])[:60]
-    print(f"\033[90m[HOOK] {block.name}({args_preview})\033[0m")
+    print(f"\033[90m[钩子] {block.name}({args_preview})\033[0m")
     return None
 
 def large_output_hook(block, output):
-    """PostToolUse: warn on large output."""
+    """PostToolUse:对大型输出给出提醒。"""
     if len(str(output)) > 100000:
-        print(f"\033[33m[HOOK] ⚠ 来自以下工具的大输出:{block.name}: {len(str(output))} 个字符\033[0m")
+        print(f"\033[33m[钩子] ⚠ 来自以下工具的大输出:{block.name}: {len(str(output))} 个字符\033[0m")
     return None
 
-# UserPromptSubmit Hook:在用户输入到达 LLM 前记录它
+# 用户提交提示词钩子:在用户输入到达 LLM 前记录它
 def context_inject_hook(query: str):
-    print(f"\033[90m[HOOK] UserPromptSubmit: 工作目录:{WORKDIR}\033[0m")
+    print(f"\033[90m[钩子] UserPromptSubmit: 工作目录:{WORKDIR}\033[0m")
     return None
 
-# Stop Hook:在循环即将退出时打印摘要
+# 停止钩子:在循环即将退出时打印摘要
 def summary_hook(messages: list):
     tool_count = sum(1 for m in messages
                      for b in (m.get("content") if isinstance(m.get("content"), list) else [])
                      if isinstance(b, dict) and b.get("type") == "tool_result")
-    print(f"\033[90m[HOOK] Stop:会话使用了 {tool_count} 次工具调用\033[0m")
+    print(f"\033[90m[钩子] Stop:会话使用了 {tool_count} 次工具调用\033[0m")
     return None
 
 register_hook("UserPromptSubmit", context_inject_hook)
@@ -247,7 +247,7 @@ def agent_loop(messages: list):
             handler = TOOL_HANDLERS.get(block.name)
             output = handler(**block.input) if handler else f"未知工具:{block.name}"
 
-            trigger_hooks("PostToolUse", block, output)  # s04: 后置 Hook
+            trigger_hooks("PostToolUse", block, output)  # s04: 后置钩子
 
             results.append({"type": "tool_result", "tool_use_id": block.id, "content": output})
 
@@ -255,7 +255,7 @@ def agent_loop(messages: list):
 
 
 if __name__ == "__main__":
-    print("s04: Hooks — 扩展逻辑挂到 Hooks 上,循环保持干净")
+    print("s04: 钩子系统 — 扩展逻辑挂到钩子上,循环保持干净")
     print("输入问题后按回车。输入 q 退出。\n")
 
     history = []

+ 1 - 0
s04_hooks/helloword.py

@@ -0,0 +1 @@
+print('helloword')

+ 118 - 0
s04_hooks/代码讲解.md

@@ -0,0 +1,118 @@
+# S04 Hooks 代码讲解
+
+这一节只讲相对 S03 新增的内容:
+
+**把权限检查、日志、上下文注入、结束摘要从 Agent Loop 里拆出去,挂到生命周期事件上。**
+
+---
+
+## 1. 本节新增内容
+
+- `HOOKS`:事件名到回调函数列表的映射。
+- `register_hook()`:注册 Hook。
+- `trigger_hooks()`:触发某类 Hook。
+- `permission_hook()`:把 S03 的权限检查迁移成 Hook。
+- `log_hook()`:工具调用前打印日志。
+- `large_output_hook()`:工具执行后检查大输出。
+- `context_inject_hook()`:用户输入提交前触发。
+- `summary_hook()`:Agent 停止时触发。
+
+---
+
+## 2. HOOKS 数据结构
+
+`HOOKS` 是一个字典:
+
+```python
+HOOKS: dict[str, list[function]] = {
+    "UserPromptSubmit": [],
+    "PreToolUse": [],
+    "PostToolUse": [],
+    "Stop": []
+}
+```
+
+每个 key 是一个生命周期事件,每个 value 是一组回调函数。
+
+比如:
+
+```python
+HOOKS["PreToolUse"] = [
+    permission_hook,
+    log_hook
+]
+```
+
+意思是:每次工具执行前,依次运行权限检查和日志记录。
+
+---
+
+## 3. `register_hook(event, callback)`
+
+作用:把某个函数挂到某个事件上。
+
+```python
+register_hook("PreToolUse", permission_hook)
+```
+
+可以理解成:
+
+```python
+工具执行前,请自动运行 permission_hook
+```
+
+---
+
+## 4. `trigger_hooks(event, *args)`
+
+作用:触发某个事件下的所有 Hook。
+
+```python
+blocked = trigger_hooks("PreToolUse", block)
+```
+
+这里的 `block` 是模型返回的 `tool_use`。
+
+如果某个 Hook 返回了非空值,就表示它想拦截本次流程:
+
+```python
+if result is not None:
+    return result
+```
+
+所以 `permission_hook()` 可以通过返回字符串来阻止工具执行。
+
+---
+
+## 5. Agent Loop 中的变化
+
+S03 是硬编码:
+
+```python
+if not check_permission(block):
+    continue
+```
+
+S04 改成:
+
+```python
+blocked = trigger_hooks("PreToolUse", block)
+if blocked:
+    results.append(tool_result)
+    continue
+```
+
+区别是:
+
+```python
+Agent Loop 不再关心具体有哪些扩展逻辑
+只负责在合适的生命周期点触发 Hook
+```
+
+---
+
+## 6. 本节课堂重点
+
+Hooks 让 Agent Harness 变得可扩展。
+
+后面要加日志、权限、审计、上下文注入、输出过滤,都不需要把主循环越改越乱。

+ 10 - 10
s05_todo_write/code.py

@@ -124,10 +124,10 @@ def run_glob(pattern: str) -> str:
 def _normalize_todos(todos):
     if isinstance(todos, str):
         try:
-            个待办 = json.loads(todos)
+            todo_list = json.loads(todos)
         except json.JSONDecodeError:
             try:
-                个待办 = ast.literal_eval(todos)
+                todo_list = ast.literal_eval(todos)
             except (SyntaxError, ValueError):
                 return None, "错误:todos 必须是列表或 JSON 数组字符串"
     if not isinstance(todos, list):
@@ -139,14 +139,14 @@ def _normalize_todos(todos):
             return None, f"错误:todos[{i}] 缺少 'content' 或 'status'"
         if t["status"] not in ("pending", "in_progress", "completed"):
             return None, f"错误:todos[{i}] 包含无效状态 '{t['status']}'"
-    return 个待办, None
+    return todo_list, None
 
 def run_todo_write(todos: list) -> str:
     global CURRENT_TODOS
-    个待办, error = _normalize_todos(todos)
+    todo_list, error = _normalize_todos(todos)
     if error:
         return error
-    CURRENT_TODOS = 个待办
+    CURRENT_TODOS = todo_list
     lines = ["\n\033[33m## 当前任务\033[0m"]
     for t in CURRENT_TODOS:
         icon = {"pending": " ", "in_progress": "\033[36m▸\033[0m", "completed": "\033[32m✓\033[0m"}[t["status"]]
@@ -196,7 +196,7 @@ def trigger_hooks(event: str, *args):
 DENY_LIST = ["rm -rf /", "sudo", "shutdown", "reboot", "mkfs", "dd if="]
 
 def permission_hook(block):
-    """PreToolUse: deny list check."""
+    """PreToolUse:检查拒绝列表。"""
     if block.name == "bash":
         for p in DENY_LIST:
             if p in block.input.get("command", ""):
@@ -206,12 +206,12 @@ def permission_hook(block):
 
 def log_hook(block):
     """PreToolUse:记录工具调用。"""
-    print(f"\033[90m[HOOK] {block.name}\033[0m")
+    print(f"\033[90m[钩子] {block.name}\033[0m")
     return None
 
 def context_inject_hook(query: str):
-    """UserPromptSubmit: log working directory."""
-    print(f"\033[90m[HOOK] UserPromptSubmit: 工作目录:{WORKDIR}\033[0m")
+    """UserPromptSubmit:记录当前工作目录。"""
+    print(f"\033[90m[钩子] UserPromptSubmit: 工作目录:{WORKDIR}\033[0m")
     return None
 
 def summary_hook(messages: list):
@@ -219,7 +219,7 @@ def summary_hook(messages: list):
     tool_count = sum(1 for m in messages
                      for b in (m.get("content") if isinstance(m.get("content"), list) else [])
                      if isinstance(b, dict) and b.get("type") == "tool_result")
-    print(f"\033[90m[HOOK] Stop:会话使用了 {tool_count} 次工具调用\033[0m")
+    print(f"\033[90m[钩子] Stop:会话使用了 {tool_count} 次工具调用\033[0m")
     return None
 
 register_hook("UserPromptSubmit", context_inject_hook)

+ 144 - 0
s05_todo_write/代码讲解.md

@@ -0,0 +1,144 @@
+# S05 TodoWrite 代码讲解
+
+这一节只讲相对 S04 新增的内容:
+
+**让 Agent 在多步骤任务中先规划,再执行,并持续更新任务状态。**
+
+---
+
+## 1. 本节新增内容
+
+- `CURRENT_TODOS`:内存中的当前任务列表。
+- `_normalize_todos()`:校验和解析模型传来的 todos。
+- `run_todo_write()`:更新任务列表并打印状态。
+- `todo_write` 工具定义:让模型可以显式写计划。
+- `rounds_since_todo`:检测模型太久没更新计划时提醒它。
+
+---
+
+## 2. Todo 数据结构
+
+`CURRENT_TODOS` 是:
+
+```python
+CURRENT_TODOS: list[dict]
+```
+
+每个 todo 是一个字典:
+
+```python
+{
+    "content": "读取项目结构",
+    "status": "in_progress"
+}
+```
+
+`status` 只能是三种:
+
+```python
+pending
+in_progress
+completed
+```
+
+---
+
+## 3. `todo_write` 工具 schema
+
+模型调用工具时,传入的数据大概是:
+
+```python
+{
+    "todos": [
+        {"content": "查看文件", "status": "completed"},
+        {"content": "修改代码", "status": "in_progress"},
+        {"content": "运行验证", "status": "pending"}
+    ]
+}
+```
+
+模型不是直接改 `CURRENT_TODOS`,而是提出一次 `tool_use`。
+
+程序收到后调用:
+
+```python
+run_todo_write(todos)
+```
+
+---
+
+## 4. `_normalize_todos(todos)`
+
+这个函数负责把输入整理成标准列表。
+
+为什么需要它?
+
+模型有时可能传真正的 list:
+
+```python
+[{"content": "...", "status": "pending"}]
+```
+
+也可能传 JSON 字符串:
+
+```python
+'[{"content": "...", "status": "pending"}]'
+```
+
+所以 `_normalize_todos()` 会尝试解析,并检查:
+
+- `todos` 必须是列表。
+- 每一项必须是字典。
+- 每一项必须有 `content` 和 `status`。
+- `status` 必须是允许的状态。
+
+---
+
+## 5. `run_todo_write(todos)`
+
+这个函数做两件事:
+
+```python
+1. 校验 todos
+2. 更新 CURRENT_TODOS
+```
+
+更新后会打印当前任务状态,让人能看到 Agent 的计划变化。
+
+---
+
+## 6. `rounds_since_todo`
+
+`rounds_since_todo` 是一个计数器。
+
+每一轮模型调用工具后,如果没有调用 `todo_write`,计数器就增加。
+
+当它超过阈值:
+
+```python
+if rounds_since_todo >= 3:
+    messages.append({"role": "user", "content": "<reminder>请更新你的待办事项。</reminder>"})
+```
+
+这相当于给模型一个提醒:
+
+```python
+你已经执行几步了,该更新计划了
+```
+
+---
+
+## 7. 本节课堂重点
+
+TodoWrite 不是为了好看,而是让 Agent 有“任务状态”。
+
+没有 TodoWrite 时,模型只是在连续调用工具。
+
+有了 TodoWrite 后,模型开始显式维护:
+
+```python
+计划是什么
+现在做到哪一步
+哪些已经完成
+哪些还没开始
+```

+ 14 - 14
s06_subagent/code.py

@@ -124,10 +124,10 @@ def run_glob(pattern: str) -> str:
 def _normalize_todos(todos):
     if isinstance(todos, str):
         try:
-            个待办 = json.loads(todos)
+            todo_list = json.loads(todos)
         except json.JSONDecodeError:
             try:
-                个待办 = ast.literal_eval(todos)
+                todo_list = ast.literal_eval(todos)
             except (SyntaxError, ValueError):
                 return None, "错误:todos 必须是列表或 JSON 数组字符串"
     if not isinstance(todos, list):
@@ -139,14 +139,14 @@ def _normalize_todos(todos):
             return None, f"错误:todos[{i}] 缺少 'content' 或 'status'"
         if t["status"] not in ("pending", "in_progress", "completed"):
             return None, f"错误:todos[{i}] 包含无效状态 '{t['status']}'"
-    return 个待办, None
+    return todo_list, None
 
 def run_todo_write(todos: list) -> str:
     global CURRENT_TODOS
-    个待办, error = _normalize_todos(todos)
+    todo_list, error = _normalize_todos(todos)
     if error:
         return error
-    CURRENT_TODOS = 个待办
+    CURRENT_TODOS = todo_list
     lines = ["\n\033[33m## 当前任务\033[0m"]
     for t in CURRENT_TODOS:
         icon = {"pending": " ", "in_progress": "\033[36m▸\033[0m", "completed": "\033[32m✓\033[0m"}[t["status"]]
@@ -199,13 +199,13 @@ SUB_HANDLERS = {
 }
 
 def extract_text(content) -> str:
-    """Extract text from message content blocks."""
+    """从消息内容块中提取文本。"""
     if not isinstance(content, list):
         return str(content)
     return "\n".join(getattr(b, "text", "") for b in content if getattr(b, "type", None) == "text")
 
 def spawn_subagent(description: str) -> str:
-    """Spawn a subagent with fresh messages[], return summary only."""
+    """用全新的 messages[] 启动子 Agent,只返回摘要。"""
     print(f"\n\033[35m[子 Agent 已启动]\033[0m")
     messages = [{"role": "user", "content": description}]  # 全新上下文
 
@@ -244,7 +244,7 @@ def spawn_subagent(description: str) -> str:
                 if result:
                     break
         if not result:
-            result = "子 Agent stopped 等待 30 turns without final answer."
+            result = "子 Agent 已等待 30 轮仍未给出最终回答,已停止。"
     print(f"\033[35m[子 Agent 已完成]\033[0m")
     return result  # 只保留摘要,完整消息历史会被丢弃
 
@@ -276,7 +276,7 @@ def trigger_hooks(event: str, *args):
 DENY_LIST = ["rm -rf /", "sudo", "shutdown", "reboot", "mkfs", "dd if="]
 
 def permission_hook(block):
-    """PreToolUse: deny list check."""
+    """PreToolUse:检查拒绝列表。"""
     if block.name == "bash":
         for p in DENY_LIST:
             if p in block.input.get("command", ""):
@@ -286,12 +286,12 @@ def permission_hook(block):
 
 def log_hook(block):
     """PreToolUse:记录工具调用。"""
-    print(f"\033[90m[HOOK] {block.name}\033[0m")
+    print(f"\033[90m[钩子] {block.name}\033[0m")
     return None
 
 def context_inject_hook(query: str):
-    """UserPromptSubmit: log working directory."""
-    print(f"\033[90m[HOOK] UserPromptSubmit: 工作目录:{WORKDIR}\033[0m")
+    """UserPromptSubmit:记录当前工作目录。"""
+    print(f"\033[90m[钩子] UserPromptSubmit: 工作目录:{WORKDIR}\033[0m")
     return None
 
 def summary_hook(messages: list):
@@ -299,7 +299,7 @@ def summary_hook(messages: list):
     tool_count = sum(1 for m in messages
                      for b in (m.get("content") if isinstance(m.get("content"), list) else [])
                      if isinstance(b, dict) and b.get("type") == "tool_result")
-    print(f"\033[90m[HOOK] Stop:会话使用了 {tool_count} 次工具调用\033[0m")
+    print(f"\033[90m[钩子] Stop:会话使用了 {tool_count} 次工具调用\033[0m")
     return None
 
 register_hook("UserPromptSubmit", context_inject_hook)
@@ -361,7 +361,7 @@ def agent_loop(messages: list):
 
 
 if __name__ == "__main__":
-    print("s06: 子 Agent — spawn sub-agents with 全新上下文, summary only")
+    print("s06: 子 Agent — 使用全新上下文启动,只返回摘要")
     print("输入问题后按回车。输入 q 退出。\n")
 
     history = []

+ 117 - 0
s06_subagent/代码讲解.md

@@ -0,0 +1,117 @@
+# S06 Subagent 代码讲解
+
+这一节只讲相对 S05 新增的内容:
+
+**父 Agent 可以通过 `task` 工具启动一个全新上下文的子 Agent。**
+
+---
+
+## 1. 本节新增内容
+
+- `SUB_SYSTEM`:子 Agent 专用系统提示词。
+- `SUB_TOOLS`:子 Agent 可用工具列表。
+- `SUB_HANDLERS`:子 Agent 工具分发器。
+- `extract_text()`:从模型返回的 content blocks 里提取文本。
+- `spawn_subagent()`:创建并运行子 Agent。
+- `task` 工具:父 Agent 用它把复杂子任务交给子 Agent。
+
+---
+
+## 2. 父 Agent 和子 Agent 的区别
+
+父 Agent 使用自己的 `messages`:
+
+```python
+parent_messages = [...]
+```
+
+子 Agent 启动时使用全新的 `messages`:
+
+```python
+messages = [{"role": "user", "content": description}]
+```
+
+这叫上下文隔离。
+
+子 Agent 不会继承父 Agent 的完整历史,只拿到父 Agent 分配的任务描述。
+
+---
+
+## 3. `task` 工具的数据结构
+
+父 Agent 调用 `task` 时,模型会生成:
+
+```python
+{
+    "type": "tool_use",
+    "name": "task",
+    "input": {
+        "description": "请检查这个模块的问题,并给出结论"
+    }
+}
+```
+
+程序执行:
+
+```python
+spawn_subagent(description)
+```
+
+子 Agent 完成后,只返回一个摘要字符串。
+
+---
+
+## 4. `SUB_TOOLS`
+
+子 Agent 有工具,但没有 `task` 工具:
+
+```python
+SUB_TOOLS = [
+    bash,
+    read_file,
+    write_file,
+    edit_file,
+    glob
+]
+```
+
+为什么不给子 Agent `task`?
+
+为了避免无限递归:
+
+```python
+父 Agent -> 子 Agent -> 子子 Agent -> ...
+```
+
+教学版本先把这个边界锁住。
+
+---
+
+## 5. `spawn_subagent(description)`
+
+核心流程:
+
+```python
+创建新的 messages
+最多循环 30 轮
+调用模型
+如果模型要用工具,就执行工具并回填 tool_result
+如果模型停止,就提取最终文本
+返回摘要给父 Agent
+```
+
+这里的 30 轮是安全上限,防止子 Agent 一直循环。
+
+---
+
+## 6. 本节课堂重点
+
+Subagent 的本质不是“多了一个模型”,而是:
+
+```python
+用新的 messages 开一个隔离任务
+给它一组受限工具
+最后只把摘要返回父 Agent
+```
+
+这就是复杂任务拆分的基础。

+ 15 - 15
s07_skill_loading/code.py

@@ -51,7 +51,7 @@ CURRENT_TODOS: list[dict] = []
 
 # s07: 技能目录扫描 (供下方 build_system 使用)
 def _parse_frontmatter(text: str) -> tuple[dict, str]:
-    """Parse YAML frontmatter from SKILL.md. Returns (meta, body)."""
+    """解析 SKILL.md 的 YAML frontmatter,返回 (meta, body)。"""
     if not text.startswith("---"):
         return {}, text
     parts = text.split("---", 2)
@@ -67,7 +67,7 @@ def _parse_frontmatter(text: str) -> tuple[dict, str]:
 SKILL_REGISTRY: dict[str, dict] = {}
 
 def _scan_skills():
-    """Scan skills/ dir, populate SKILL_REGISTRY with name/description/content."""
+    """扫描 skills/ 目录,把名称、描述和内容写入 SKILL_REGISTRY。"""
     if not SKILLS_DIR.exists():
         return
     for d in sorted(SKILLS_DIR.iterdir()):
@@ -84,14 +84,14 @@ def _scan_skills():
 _scan_skills()
 
 def list_skills() -> str:
-    """List all skills (name + one-line description)."""
+    """列出所有技能:名称 + 单行描述。"""
     if not SKILL_REGISTRY:
         return "(未找到技能)"
     return "\n".join(f"- **{s['name']}**: {s['description']}" for s in SKILL_REGISTRY.values())
 
 # s07: SYSTEM 包含技能目录 (成本低 — 只有名称和描述)
 def build_system() -> str:
-    """Build SYSTEM prompt with skill catalog injected at startup."""
+    """构建 SYSTEM 提示词,并注入启动时扫描到的技能目录。"""
     catalog = list_skills()
     return (
         f"你是位于 {WORKDIR}. "
@@ -171,10 +171,10 @@ def run_glob(pattern: str) -> str:
 def _normalize_todos(todos):
     if isinstance(todos, str):
         try:
-            个待办 = json.loads(todos)
+            todo_list = json.loads(todos)
         except json.JSONDecodeError:
             try:
-                个待办 = ast.literal_eval(todos)
+                todo_list = ast.literal_eval(todos)
             except (SyntaxError, ValueError):
                 return None, "错误:todos 必须是列表或 JSON 数组字符串"
     if not isinstance(todos, list):
@@ -186,14 +186,14 @@ def _normalize_todos(todos):
             return None, f"错误:todos[{i}] 缺少 'content' 或 'status'"
         if t["status"] not in ("pending", "in_progress", "completed"):
             return None, f"错误:todos[{i}] 包含无效状态 '{t['status']}'"
-    return 个待办, None
+    return todo_list, None
 
 def run_todo_write(todos: list) -> str:
     global CURRENT_TODOS
-    个待办, error = _normalize_todos(todos)
+    todo_list, error = _normalize_todos(todos)
     if error:
         return error
-    CURRENT_TODOS = 个待办
+    CURRENT_TODOS = todo_list
     lines = ["\n\033[33m## 当前任务\033[0m"]
     for t in CURRENT_TODOS:
         icon = {"pending": " ", "in_progress": "\033[36m▸\033[0m", "completed": "\033[32m✓\033[0m"}[t["status"]]
@@ -257,7 +257,7 @@ def spawn_subagent(description: str) -> str:
                 if result:
                     break
         if not result:
-            result = "子 Agent stopped 等待 30 turns without final answer."
+            result = "子 Agent 已等待 30 轮仍未给出最终回答,已停止。"
     print(f"\033[35m[子 Agent 已完成]\033[0m")
     return result
 
@@ -267,7 +267,7 @@ def spawn_subagent(description: str) -> str:
 # ═══════════════════════════════════════════════════════════
 
 def load_skill(name: str) -> str:
-    """Load full skill content. Lookup via registry — no path traversal."""
+    """通过注册表加载完整技能内容,避免路径穿越。"""
     skill = SKILL_REGISTRY.get(name)
     if not skill:
         return f"未找到技能:{name}"
@@ -275,7 +275,7 @@ def load_skill(name: str) -> str:
 
 
 # ═══════════════════════════════════════════════════════════
-#  工具注册表 — 来自 s02-s07 的全部工具
+#  工具注册表 — 来自 s02-s07 的全部工具,怎么省token 怎么降低模型幻觉
 # ═══════════════════════════════════════════════════════════
 
 TOOLS = [
@@ -332,18 +332,18 @@ def permission_hook(block):
     return None
 
 def log_hook(block):
-    print(f"\033[90m[HOOK] {block.name}\033[0m")
+    print(f"\033[90m[钩子] {block.name}\033[0m")
     return None
 
 def context_inject_hook(query: str):
-    print(f"\033[90m[HOOK] UserPromptSubmit: 工作目录:{WORKDIR}\033[0m")
+    print(f"\033[90m[钩子] UserPromptSubmit: 工作目录:{WORKDIR}\033[0m")
     return None
 
 def summary_hook(messages: list):
     tool_count = sum(1 for m in messages
                      for b in (m.get("content") if isinstance(m.get("content"), list) else [])
                      if isinstance(b, dict) and b.get("type") == "tool_result")
-    print(f"\033[90m[HOOK] Stop:会话使用了 {tool_count} 次工具调用\033[0m")
+    print(f"\033[90m[钩子] Stop:会话使用了 {tool_count} 次工具调用\033[0m")
     return None
 
 register_hook("UserPromptSubmit", context_inject_hook)

+ 124 - 0
s07_skill_loading/代码讲解.md

@@ -0,0 +1,124 @@
+# S07 Skill Loading 代码讲解
+
+这一节只讲相对 S06 新增的内容:
+
+**技能不是一次性全塞进上下文,而是先放目录,真正需要时再加载完整内容。**
+
+---
+
+## 1. 本节新增内容
+
+- `SKILLS_DIR`:技能目录。
+- `_parse_frontmatter()`:解析 `SKILL.md` 顶部元数据。
+- `SKILL_REGISTRY`:技能注册表。
+- `_scan_skills()`:启动时扫描所有技能。
+- `list_skills()`:列出技能名称和一句话描述。
+- `build_system()`:把技能目录放进 System Prompt。
+- `load_skill(name)`:按需读取完整 `SKILL.md`。
+
+---
+
+## 2. 技能文件结构
+
+每个技能目录大概长这样:
+
+```text
+skills/
+  code-review/
+    SKILL.md
+  pdf/
+    SKILL.md
+```
+
+`SKILL.md` 里通常有 frontmatter:
+
+```markdown
+---
+name: code-review
+description: 代码审查技能
+---
+
+这里是完整技能说明……
+```
+
+---
+
+## 3. `SKILL_REGISTRY`
+
+`SKILL_REGISTRY` 是一个字典:
+
+```python
+SKILL_REGISTRY: dict[str, dict]
+```
+
+结构大概是:
+
+```python
+{
+    "code-review": {
+        "name": "code-review",
+        "description": "代码审查技能",
+        "content": "完整 SKILL.md 内容"
+    }
+}
+```
+
+---
+
+## 4. 两级加载
+
+第一级:System Prompt 里只放技能目录。
+
+```python
+可用技能:
+- code-review: 代码审查技能
+- pdf: PDF 处理技能
+```
+
+这样很省上下文。
+
+第二级:模型需要某个技能时,调用:
+
+```python
+load_skill("code-review")
+```
+
+程序再把完整 `SKILL.md` 内容作为 `tool_result` 返回给模型。
+
+---
+
+## 5. `load_skill(name)`
+
+源码逻辑:
+
+```python
+skill = SKILL_REGISTRY.get(name)
+if not skill:
+    return f"未找到技能:{name}"
+return skill["content"]
+```
+
+它没有直接拼路径读取文件,而是从注册表里查。
+
+这样可以避免模型传入奇怪路径,比如:
+
+```text
+../../secret.txt
+```
+
+---
+
+## 6. 本节课堂重点
+
+Skill Loading 解决的是上下文成本问题。
+
+不要把所有知识都塞进 System Prompt。
+
+更好的结构是:
+
+```python
+System Prompt 放目录
+真正需要时再 load_skill
+```
+
+这就是“按需知识注入”。

+ 79 - 33
s08_context_compact/code.py

@@ -45,7 +45,8 @@ 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)
+if os.getenv("ANTHROPIC_BASE_URL"):
+    os.environ.pop("ANTHROPIC_AUTH_TOKEN", None)
 
 WORKDIR = Path.cwd()
 SKILLS_DIR = WORKDIR / "skills"
@@ -123,37 +124,55 @@ SUB_SYSTEM = (
 
 def safe_path(p: str) -> Path:
     path = (WORKDIR / p).resolve()
-    if not path.is_relative_to(WORKDIR): raise ValueError(f"路径逃逸出工作区:{p}")
+    if not path.is_relative_to(WORKDIR):
+        raise ValueError(f"路径逃逸出工作区:{p}")
     return path
 
 def run_bash(command: str) -> str:
     try:
-        r = subprocess.run(command, shell=True, cwd=WORKDIR, capture_output=True, text=True, timeout=120)
+        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 "(无输出)"
-    except subprocess.TimeoutExpired: return "错误:执行超时(120 秒)"
+        if out:
+            return out[:50000]
+        return "(无输出)"
+    except subprocess.TimeoutExpired:
+        return "错误:执行超时(120 秒)"
 
 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} 行更多内容)"]
+        if limit and limit < len(lines):
+            lines = lines[:limit] + [f"... ({len(lines) - limit} 行更多内容)"]
         return "\n".join(lines)
-    except Exception as e: return f"错误:{e}"
+    except Exception as e:
+        return f"错误:{e}"
 
 def run_write(path: str, content: str) -> str:
     try:
-        file_path = safe_path(path); file_path.parent.mkdir(parents=True, exist_ok=True)
-        file_path.write_text(content); return f"已写入 {len(content)} 字节到 {path}"
-    except Exception as e: return f"错误:{e}"
+        file_path = safe_path(path)
+        file_path.parent.mkdir(parents=True, exist_ok=True)
+        file_path.write_text(content)
+        return f"已写入 {len(content)} 字节到 {path}"
+    except Exception as e:
+        return f"错误:{e}"
 
 def run_edit(path: str, old_text: str, new_text: str) -> str:
     try:
         file_path = safe_path(path)
         text = file_path.read_text()
-        if old_text not in text: return f"错误:在文件中未找到目标文本:{path}"
+        if old_text not in text:
+            return f"错误:在文件中未找到目标文本:{path}"
         file_path.write_text(text.replace(old_text, new_text, 1))
         return f"已编辑 {path}"
-    except Exception as e: return f"错误:{e}"
+    except Exception as e:
+        return f"错误:{e}"
 
 def run_glob(pattern: str) -> str:
     import glob as g
@@ -162,35 +181,41 @@ def run_glob(pattern: str) -> str:
         for match in g.glob(pattern, root_dir=WORKDIR):
             if (WORKDIR / match).resolve().is_relative_to(WORKDIR):
                 results.append(match)
-        return "\n".join(results) if results else "(无匹配)"
-    except Exception as e: return f"错误:{e}"
+        if results:
+            return "\n".join(results)
+        return "(无匹配)"
+    except Exception as e:
+        return f"错误:{e}"
 
 def _normalize_todos(todos):
     if isinstance(todos, str):
         try:
-            个待办 = json.loads(todos)
+            todo_list = json.loads(todos)
         except json.JSONDecodeError:
             try:
-                个待办 = ast.literal_eval(todos)
+                todo_list = ast.literal_eval(todos)
             except (SyntaxError, ValueError):
                 return None, "错误:todos 必须是列表或 JSON 数组字符串"
-    if not isinstance(todos, list):
+    else:
+        todo_list = todos
+
+    if not isinstance(todo_list, list):
         return None, "错误:todos 必须是列表"
-    for i, t in enumerate(todos):
+    for i, t in enumerate(todo_list):
         if not isinstance(t, dict):
             return None, f"错误:todos[{i}] 必须是对象"
         if "content" not in t or "status" not in t:
             return None, f"错误:todos[{i}] 缺少 'content' 或 'status'"
         if t["status"] not in ("pending", "in_progress", "completed"):
             return None, f"错误:todos[{i}] 包含无效状态 '{t['status']}'"
-    return 个待办, None
+    return todo_list, None
 
 def run_todo_write(todos: list) -> str:
     global CURRENT_TODOS
-    个待办, error = _normalize_todos(todos)
+    todo_list, error = _normalize_todos(todos)
     if error:
         return error
-    CURRENT_TODOS = 个待办
+    CURRENT_TODOS = todo_list
     lines = ["\n\033[33m## 当前任务\033[0m"]
     for t in CURRENT_TODOS:
         icon = {"pending": " ", "in_progress": "\033[36m▸\033[0m", "completed": "\033[32m✓\033[0m"}[t["status"]]
@@ -199,8 +224,14 @@ def run_todo_write(todos: list) -> str:
     return f"已更新 {len(CURRENT_TODOS)} 个任务"
 
 def extract_text(content) -> str:
-    if not isinstance(content, list): return str(content)
-    return "\n".join(getattr(b, "text", "") for b in content if getattr(b, "type", None) == "text")
+    if not isinstance(content, list):
+        return str(content)
+
+    texts = []
+    for block in content:
+        if getattr(block, "type", None) == "text":
+            texts.append(getattr(block, "text", ""))
+    return "\n".join(texts)
 
 
 # ═══════════════════════════════════════════════════════════
@@ -226,8 +257,13 @@ def spawn_subagent(description: str) -> str:
     print(f"\n\033[35m[子 Agent 已启动]\033[0m")
     messages = [{"role": "user", "content": description}]
     for _ in range(30):
-        response = client.messages.create(model=MODEL, system=SUB_SYSTEM,
-            messages=messages, tools=SUB_TOOLS, max_tokens=8000)
+        response = client.messages.create(
+            model=MODEL,
+            system=SUB_SYSTEM,
+            messages=messages,
+            tools=SUB_TOOLS,
+            max_tokens=8000,
+        )
         messages.append({"role": "assistant", "content": response.content})
         if response.stop_reason != "tool_use":
             break
@@ -243,7 +279,11 @@ def spawn_subagent(description: str) -> str:
                 output = handler(**block.input) if handler else f"未知工具:{block.name}"
                 trigger_hooks("PostToolUse", block, output)
                 print(f"  \033[90m[sub] {block.name}: {str(output)[:100]}\033[0m")
-                results.append({"type": "tool_result", "tool_use_id": block.id, "content": output})
+                results.append({
+                    "type": "tool_result",
+                    "tool_use_id": block.id,
+                    "content": output,
+                })
         messages.append({"role": "user", "content": results})
     result = extract_text(messages[-1]["content"])
     if not result:
@@ -253,7 +293,7 @@ def spawn_subagent(description: str) -> str:
                 if result:
                     break
         if not result:
-            result = "子 Agent stopped 等待 30 turns without final answer."
+            result = "子 Agent 已等待 30 轮仍未给出最终回答,已停止。"
     print(f"\033[35m[子 Agent 已完成]\033[0m")
     return result
 
@@ -262,8 +302,11 @@ def spawn_subagent(description: str) -> str:
 #  新增于 s08: 四层压缩流水线
 # ═══════════════════════════════════════════════════════════
 
+# 估算消息体超过这个大小时,触发 LLM 摘要压缩。
 CONTEXT_LIMIT = 50000
+# micro_compact 中保留最近几个完整 tool_result,旧结果会被占位符替换。
 KEEP_RECENT = 3
+# 单个工具输出超过这个大小时,写入磁盘,只把预览留在上下文里。
 PERSIST_THRESHOLD = 30000
 
 def estimate_size(msgs): return len(str(msgs))
@@ -306,7 +349,7 @@ def snip_compact(messages, max_messages=50):
     if head_end >= tail_start:
         return messages
     snipped = tail_start - head_end
-    return messages[:head_end] + [{"role": "user", "content": f"[snipped {snipped} messages]"}] + messages[tail_start:]
+    return messages[:head_end] + [{"role": "user", "content": f"[已省略中间 {snipped} 条消息]"}] + messages[tail_start:]
 
 
 # L2: microCompact — 旧结果占位符
@@ -334,9 +377,10 @@ def persist_large_output(tool_use_id, output):
     TOOL_RESULTS_DIR.mkdir(parents=True, exist_ok=True)
     path = TOOL_RESULTS_DIR / f"{tool_use_id}.txt"
     if not path.exists(): path.write_text(output)
-    return f"<persisted-output>\n完整输出:{path}\nPreview:\n{output[:2000]}\n</persisted-output>"
+    return f"<persisted-output>\n完整输出:{path}\n预览:\n{output[:2000]}\n</persisted-output>"
 
 def tool_result_budget(messages, max_bytes=200_000):
+    # 详细讲解见 代码讲解.md:L3 toolResultBudget。
     last = messages[-1] if messages else None
     if not last or last.get("role") != "user" or not isinstance(last.get("content"), list): return messages
     blocks = [(i, b) for i, b in enumerate(last["content"]) if isinstance(b, dict) and b.get("type") == "tool_result"]
@@ -349,6 +393,8 @@ def tool_result_budget(messages, max_bytes=200_000):
         if len(content) <= PERSIST_THRESHOLD: continue
         tid = block.get("tool_use_id", "unknown")
         block["content"] = persist_large_output(tid, content)
+        # 替换掉一个大输出后,重新计算当前 tool_result 总大小。
+        # 下一轮循环会根据新的 total 判断是否还需要继续处理。
         total = sum(len(str(b.get("content", ""))) for _, b in blocks)
     return messages
 
@@ -376,10 +422,10 @@ def compact_history(messages):
     transcript_path = write_transcript(messages)
     print(f"[对话记录已保存:{transcript_path}]")
     summary = summarize_history(messages)
-    return [{"role": "user", "content": f"[Compacted]\n\n{summary}"}]
+    return [{"role": "user", "content": f"[已压缩]\n\n{summary}"}]
 
 
-# Emergency: reactiveCompact — API 错误时触发
+# 应急:reactiveCompact — API 错误时触发
 def reactive_compact(messages):
     transcript = write_transcript(messages)
     tail_start = max(0, len(messages) - 5)
@@ -388,7 +434,7 @@ def reactive_compact(messages):
             and _message_has_tool_use(messages[tail_start - 1])):
         tail_start -= 1
     summary = summarize_history(messages[:tail_start])
-    return [{"role": "user", "content": f"[Reactive compact]\n\n{summary}"}, *messages[tail_start:]]
+    return [{"role": "user", "content": f"[响应式压缩]\n\n{summary}"}, *messages[tail_start:]]
 
 
 # ═══════════════════════════════════════════════════════════
@@ -438,7 +484,7 @@ def permission_hook(block):
             if p in block.input.get("command", ""): return "权限被拒绝"
     return None
 def log_hook(block):
-    print(f"\033[90m[HOOK] {block.name}\033[0m")
+    print(f"\033[90m[钩子] {block.name}\033[0m")
     return None
 
 HOOKS["PreToolUse"].append(permission_hook)

+ 833 - 0
s08_context_compact/代码讲解.md

@@ -0,0 +1,833 @@
+# S08 Context Compact 代码讲解
+
+这一节只讲相对 S07 新增的内容:
+
+**上下文会越来越长,所以 Agent 在调用模型之前,要先把旧消息和大工具结果变小。**
+
+---
+
+## 1. 本节新增内容
+
+- `CONTEXT_LIMIT`:触发自动摘要压缩的上下文大小阈值。
+- `KEEP_RECENT`:保留最近几个完整 `tool_result`。
+- `PERSIST_THRESHOLD`:单个工具输出超过多大时写入磁盘。
+- `snip_compact()`:裁剪中间历史。
+- `micro_compact()`:把旧工具结果换成占位符。
+- `tool_result_budget()`:处理最后一轮过大的工具输出。
+- `compact_history()`:调用 LLM 总结完整历史。
+- `reactive_compact()`:API 报上下文过长时兜底压缩。
+- `compact` 工具:让模型主动请求压缩。
+
+---
+
+## 2. Messages 数据结构
+
+`messages` 是:
+
+```python
+messages: list[dict]
+```
+
+一段带工具调用的历史通常长这样:
+
+```python
+messages = [
+    {"role": "user", "content": "请读取 README.md"},
+    {"role": "assistant", "content": [tool_use_block]},
+    {"role": "user", "content": [tool_result_block]},
+]
+```
+
+`tool_result_block` 是字典:
+
+```python
+{
+    "type": "tool_result",
+    "tool_use_id": "toolu_01xxx",
+    "content": "工具返回的大段文本"
+}
+```
+
+---
+
+## 3. 四层压缩顺序
+
+在 `agent_loop()` 里,压缩发生在调用模型之前:
+
+```python
+messages[:] = tool_result_budget(messages)
+messages[:] = snip_compact(messages)
+messages[:] = micro_compact(messages)
+
+if estimate_size(messages) > CONTEXT_LIMIT:
+    messages[:] = compact_history(messages)
+```
+
+执行顺序是:
+
+```text
+L3 大工具结果预算 -> L1 裁剪中间历史 -> L2 压缩旧工具结果 -> L4 LLM 摘要
+```
+
+核心原则:
+
+```text
+先用便宜的办法压缩,最后才调用 LLM 做摘要。
+```
+
+---
+
+## 4. `tool_result_budget(messages)`
+
+目标:
+
+**如果最后一轮工具结果总大小超过 `max_bytes`,就优先把最大的工具输出写入磁盘。**
+
+关键代码:
+
+```python
+last = messages[-1] if messages else None
+```
+
+等价于:
+
+```python
+if messages:
+    last = messages[-1]
+else:
+    last = None
+```
+
+这里的 `last` 预期是:
+
+```python
+last: dict = {
+    "role": "user",
+    "content": [tool_result_block, tool_result_block]
+}
+```
+
+筛选工具结果:
+
+```python
+blocks = [
+    (i, b)
+    for i, b in enumerate(last["content"])
+    if isinstance(b, dict) and b.get("type") == "tool_result"
+]
+```
+
+`blocks` 的类型可以理解成:
+
+```python
+blocks: list[tuple[int, dict]]
+```
+
+例如:
+
+```python
+[
+    (0, {"type": "tool_result", "tool_use_id": "toolu_01", "content": "..."}),
+    (1, {"type": "tool_result", "tool_use_id": "toolu_02", "content": "..."})
+]
+```
+
+计算总大小:
+
+```python
+total = sum(len(str(b.get("content", ""))) for _, b in blocks)
+```
+
+`_` 表示这个值不用。这里不关心下标,只关心 `b` 这个工具结果字典。
+
+按输出长度排序:
+
+```python
+ranked = sorted(
+    blocks,
+    key=lambda p: len(str(p[1].get("content", ""))),
+    reverse=True
+)
+```
+
+`p` 是 `(i, b)`,所以:
+
+```python
+p[0]  # 下标
+p[1]  # tool_result 字典
+```
+
+排序后,最大的工具输出排在前面,优先被写入磁盘。
+
+---
+
+## 5. `persist_large_output(tool_use_id, output)`
+
+压缩前:
+
+```python
+{
+    "type": "tool_result",
+    "tool_use_id": "toolu_01",
+    "content": "非常非常长的输出......"
+}
+```
+
+压缩后:
+
+```python
+{
+    "type": "tool_result",
+    "tool_use_id": "toolu_01",
+    "content": "<persisted-output>文件路径 + Preview</persisted-output>"
+}
+```
+
+完整输出在磁盘:
+
+```text
+.task_outputs/tool-results/toolu_01.txt
+```
+
+上下文里只保留路径和前 2000 字符预览。
+
+---
+
+## 6. `snip_compact(messages)`
+
+作用:消息数量超过阈值时,裁掉中间历史,只保留:
+
+```text
+开头几条消息 + 一个裁剪标记 + 最近几条消息
+```
+
+这样模型还能知道“最开始的任务是什么”,也能看到“最近正在做什么”,中间太长的历史就先省掉。
+
+源码:
+
+```python
+def snip_compact(messages, max_messages=50):
+    if len(messages) <= max_messages: return messages
+    keep_head, keep_tail = 3, max_messages - 3
+    head_end, tail_start = keep_head, len(messages) - keep_tail
+    if head_end > 0 and _message_has_tool_use(messages[head_end - 1]):
+        while head_end < len(messages) and _is_tool_result_message(messages[head_end]):
+            head_end += 1
+    if (tail_start > 0 and tail_start < len(messages)
+            and _is_tool_result_message(messages[tail_start])
+            and _message_has_tool_use(messages[tail_start - 1])):
+        tail_start -= 1
+    if head_end >= tail_start:
+        return messages
+    snipped = tail_start - head_end
+    return messages[:head_end] + [{"role": "user", "content": f"[snipped {snipped} messages]"}] + messages[tail_start:]
+```
+
+---
+
+### 6.1 最简单的裁剪例子
+
+为了方便理解,先把 `max_messages` 想成 8。
+
+假设现在有 12 条消息:
+
+```python
+messages = [
+    "msg0", "msg1", "msg2",
+    "msg3", "msg4", "msg5", "msg6",
+    "msg7", "msg8", "msg9", "msg10", "msg11"
+]
+```
+
+代码第一步:
+
+```python
+keep_head, keep_tail = 3, max_messages - 3
+```
+
+如果 `max_messages = 8`:
+
+```python
+keep_head = 3
+keep_tail = 8 - 3 = 5
+```
+
+意思是:
+
+```text
+保留开头 3 条
+保留结尾 5 条
+```
+
+再看这行:
+
+```python
+head_end, tail_start = keep_head, len(messages) - keep_tail
+```
+
+代入数字:
+
+```python
+head_end = 3
+tail_start = 12 - 5 = 7
+```
+
+这两个变量可以这样理解:
+
+```python
+head_end = 开头保留到哪里结束
+tail_start = 结尾从哪里开始保留
+```
+
+对应到列表下标:
+
+```python
+messages[:head_end]
+```
+
+就是:
+
+```python
+messages[:3] = ["msg0", "msg1", "msg2"]
+```
+
+而:
+
+```python
+messages[tail_start:]
+```
+
+就是:
+
+```python
+messages[7:] = ["msg7", "msg8", "msg9", "msg10", "msg11"]
+```
+
+中间被裁掉的是:
+
+```python
+messages[3:7] = ["msg3", "msg4", "msg5", "msg6"]
+```
+
+所以:
+
+```python
+snipped = tail_start - head_end
+```
+
+就是:
+
+```python
+snipped = 7 - 3 = 4
+```
+
+最终结果变成:
+
+```python
+[
+    "msg0", "msg1", "msg2",
+    {"role": "user", "content": "[snipped 4 messages]"},
+    "msg7", "msg8", "msg9", "msg10", "msg11"
+]
+```
+
+这就是 `snip_compact()` 的基本逻辑。
+
+---
+
+### 6.2 为什么变量叫 `head_end` 和 `tail_start`
+
+这两个名字是从“切片边界”来的。
+
+```python
+messages[:head_end]
+```
+
+表示保留头部。
+
+```python
+messages[tail_start:]
+```
+
+表示保留尾部。
+
+中间要裁掉的区间是:
+
+```python
+messages[head_end:tail_start]
+```
+
+所以:
+
+```python
+head_end
+```
+
+是头部保留区的结束位置。
+
+```python
+tail_start
+```
+
+是尾部保留区的开始位置。
+
+---
+
+### 6.3 为什么不能直接裁剪
+
+普通聊天消息可以直接裁。
+
+但工具调用消息有配对关系:
+
+```python
+assistant: tool_use
+user: tool_result
+```
+
+例子:
+
+```python
+messages = [
+    {"role": "user", "content": "读取文件"},
+    {"role": "assistant", "content": [tool_use_read_file]},
+    {"role": "user", "content": [tool_result_read_file]},
+]
+```
+
+这里第 1 条和第 2 条是一组:
+
+```text
+模型说:我要调用工具
+程序说:这是工具结果
+```
+
+如果裁剪后只剩:
+
+```python
+{"role": "assistant", "content": [tool_use_read_file]}
+```
+
+但对应的 `tool_result` 被裁掉了,消息结构就不完整。
+
+所以 `snip_compact()` 里有一段逻辑,专门避免把这组消息从中间切断。
+
+---
+
+### 6.4 第一段边界保护:头部结尾不能只留下 tool_use
+
+源码:
+
+```python
+if head_end > 0 and _message_has_tool_use(messages[head_end - 1]):
+    while head_end < len(messages) and _is_tool_result_message(messages[head_end]):
+        head_end += 1
+```
+
+假设:
+
+```python
+head_end = 3
+```
+
+原本保留:
+
+```python
+messages[:3]
+```
+
+也就是保留下标:
+
+```text
+0, 1, 2
+```
+
+现在检查:
+
+```python
+messages[head_end - 1]
+```
+
+就是:
+
+```python
+messages[2]
+```
+
+如果 `messages[2]` 是一条 `assistant tool_use`,说明头部最后一条消息是:
+
+```text
+模型要求调用工具
+```
+
+那下一条很可能就是对应的工具结果:
+
+```python
+messages[3]
+```
+
+所以代码会继续看:
+
+```python
+while head_end < len(messages) and _is_tool_result_message(messages[head_end]):
+    head_end += 1
+```
+
+变量变化示例:
+
+```python
+head_end = 3
+messages[3] 是 tool_result
+head_end += 1
+head_end = 4
+```
+
+这样保留头部就从:
+
+```python
+messages[:3]
+```
+
+变成:
+
+```python
+messages[:4]
+```
+
+也就是把对应的 `tool_result` 也保留下来。
+
+---
+
+### 6.5 第二段边界保护:尾部开头不能只留下 tool_result
+
+源码:
+
+```python
+if (tail_start > 0 and tail_start < len(messages)
+        and _is_tool_result_message(messages[tail_start])
+        and _message_has_tool_use(messages[tail_start - 1])):
+    tail_start -= 1
+```
+
+假设:
+
+```python
+tail_start = 7
+```
+
+原本保留尾部:
+
+```python
+messages[7:]
+```
+
+也就是保留下标:
+
+```text
+7, 8, 9, 10, 11
+```
+
+现在检查:
+
+```python
+messages[tail_start]
+```
+
+就是:
+
+```python
+messages[7]
+```
+
+如果 `messages[7]` 是 `tool_result`,说明尾部第一条就是工具结果。
+
+再检查:
+
+```python
+messages[tail_start - 1]
+```
+
+也就是:
+
+```python
+messages[6]
+```
+
+如果 `messages[6]` 是对应的 `tool_use`,说明原本的切法会变成:
+
+```text
+裁掉 tool_use
+保留 tool_result
+```
+
+这也不完整。
+
+所以代码做:
+
+```python
+tail_start -= 1
+```
+
+变量变化:
+
+```python
+tail_start = 7
+tail_start -= 1
+tail_start = 6
+```
+
+尾部保留范围从:
+
+```python
+messages[7:]
+```
+
+变成:
+
+```python
+messages[6:]
+```
+
+这样 `tool_use` 和 `tool_result` 就一起保留下来了。
+
+---
+
+### 6.6 `head_end >= tail_start` 是什么意思
+
+源码:
+
+```python
+if head_end >= tail_start:
+    return messages
+```
+
+正常情况下:
+
+```python
+head_end < tail_start
+```
+
+中间才有东西可以裁。
+
+例如:
+
+```python
+head_end = 3
+tail_start = 7
+```
+
+中间可裁:
+
+```python
+messages[3:7]
+```
+
+但如果边界保护后变成:
+
+```python
+head_end = 7
+tail_start = 6
+```
+
+说明头部和尾部已经重叠了。
+
+这时候再裁剪就没有意义,甚至可能裁错,所以直接:
+
+```python
+return messages
+```
+
+---
+
+### 6.7 完整执行示例
+
+假设:
+
+```python
+max_messages = 8
+len(messages) = 12
+```
+
+先计算:
+
+```python
+keep_head = 3
+keep_tail = 5
+head_end = 3
+tail_start = 7
+```
+
+假设消息结构是:
+
+```text
+0 user text
+1 assistant text
+2 assistant tool_use
+3 user tool_result
+4 user text
+5 assistant text
+6 assistant tool_use
+7 user tool_result
+8 user text
+9 assistant text
+10 user text
+11 assistant text
+```
+
+第一段保护:
+
+```python
+messages[head_end - 1] = messages[2]
+```
+
+它是 `tool_use`。
+
+所以检查:
+
+```python
+messages[head_end] = messages[3]
+```
+
+它是 `tool_result`。
+
+于是:
+
+```python
+head_end = 4
+```
+
+第二段保护:
+
+```python
+messages[tail_start] = messages[7]
+```
+
+它是 `tool_result`。
+
+并且:
+
+```python
+messages[tail_start - 1] = messages[6]
+```
+
+它是 `tool_use`。
+
+于是:
+
+```python
+tail_start = 6
+```
+
+现在:
+
+```python
+head_end = 4
+tail_start = 6
+```
+
+要裁掉:
+
+```python
+messages[4:6]
+```
+
+也就是下标 4 和 5。
+
+最终保留:
+
+```python
+messages[:4]
++ [{"role": "user", "content": "[snipped 2 messages]"}]
++ messages[6:]
+```
+
+结果是:
+
+```text
+0 user text
+1 assistant text
+2 assistant tool_use
+3 user tool_result
+[snipped 2 messages]
+6 assistant tool_use
+7 user tool_result
+8 user text
+9 assistant text
+10 user text
+11 assistant text
+```
+
+你会发现,两个工具调用配对都没有被切断。
+
+---
+
+### 6.8 一句话总结
+
+`snip_compact()` 做的是:
+
+```text
+先算出头部保留边界和尾部保留边界
+再检查边界有没有切断 tool_use / tool_result
+如果切断了,就移动边界
+最后把中间消息替换成一个 [snipped ... messages] 标记
+```
+
+它不是为了精确总结内容,而是一个便宜、快速、不会调用 LLM 的上下文裁剪方式。
+
+---
+
+## 7. `micro_compact(messages)`
+
+作用:保留最近几个完整工具结果,把更早的大工具结果替换成短文本。
+
+```python
+block["content"] = "[早前工具结果已压缩。如有需要请重新运行。]"
+```
+
+它不会调用 LLM,所以成本很低。
+
+---
+
+## 8. `compact_history(messages)`
+
+作用:调用 LLM,把历史总结成一条新消息。
+
+压缩前:
+
+```python
+[msg1, msg2, msg3, ..., msg100]
+```
+
+压缩后:
+
+```python
+[
+    {
+        "role": "user",
+        "content": "[Compacted]\n\n这里是历史摘要"
+    }
+]
+```
+
+---
+
+## 9. `messages[:] = ...`
+
+`messages[:] = ...` 表示原地替换列表内容。
+
+如果写:
+
+```python
+messages = compact_history(messages)
+```
+
+只是函数内部变量换了一个新列表。
+
+如果写:
+
+```python
+messages[:] = compact_history(messages)
+```
+
+外面传进来的 `history` 也会看到压缩后的内容。
+
+这就是为什么 Agent Loop 里使用 `messages[:]`。

+ 42 - 44
s09_memory/code.py

@@ -70,7 +70,7 @@ def _parse_frontmatter(text: str) -> tuple[dict, str]:
 
 
 def write_memory_file(name: str, mem_type: str, description: str, body: str):
-    """Write a single memory file with YAML frontmatter."""
+    """写入单个带 YAML frontmatter 的记忆文件。"""
     slug = name.lower().replace(" ", "-").replace("/", "-")
     filename = f"{slug}.md"
     filepath = MEMORY_DIR / filename
@@ -82,7 +82,7 @@ def write_memory_file(name: str, mem_type: str, description: str, body: str):
 
 
 def _rebuild_index():
-    """Rebuild MEMORY.md index from all memory files."""
+    """根据所有记忆文件重建 MEMORY.md 索引。"""
     lines = []
     for f in sorted(MEMORY_DIR.glob("*.md")):
         if f.name == "MEMORY.md":
@@ -96,7 +96,7 @@ def _rebuild_index():
 
 
 def read_memory_index() -> str:
-    """Read MEMORY.md index (injected into SYSTEM every turn)."""
+    """读取每轮都会注入 SYSTEM 的 MEMORY.md 索引。"""
     if not MEMORY_INDEX.exists():
         return ""
     text = MEMORY_INDEX.read_text().strip()
@@ -104,7 +104,7 @@ def read_memory_index() -> str:
 
 
 def read_memory_file(filename: str) -> str | None:
-    """Read a single memory file's full content."""
+    """读取单个记忆文件的完整内容。"""
     path = MEMORY_DIR / filename
     if not path.exists():
         return None
@@ -112,7 +112,7 @@ def read_memory_file(filename: str) -> str | None:
 
 
 def list_memory_files() -> list[dict]:
-    """List all memory files with metadata."""
+    """列出所有记忆文件及其元数据。"""
     result = []
     for f in sorted(MEMORY_DIR.glob("*.md")):
         if f.name == "MEMORY.md":
@@ -130,9 +130,8 @@ def list_memory_files() -> list[dict]:
 
 
 def select_relevant_memories(messages: list, max_items: int = 5) -> list[str]:
-    """Select relevant memory filenames by matching recent conversation against
-    memory names/descriptions. Uses a simple LLM call (or falls back to keyword
-    matching on name+description)."""
+    """根据最近对话匹配记忆名称和描述,选择相关记忆文件。
+    优先使用一次简单的 LLM 调用;失败时回退到关键词匹配。"""
     files = list_memory_files()
     if not files:
         return []
@@ -163,12 +162,11 @@ def select_relevant_memories(messages: list, max_items: int = 5) -> list[str]:
     catalog = "\n".join(catalog_lines)
 
     prompt = (
-        "Given the recent conversation and the memory catalog below, "
-        "select the indices of memories that are clearly relevant. "
-        "Return ONLY a JSON array of integers, e.g. [0, 3]. "
-        "If none are relevant, return [].\n\n"
-        f"Recent conversation:\n{recent}\n\n"
-        f"Memory catalog:\n{catalog}"
+        "请根据最近对话和下方记忆目录,选择明显相关的记忆索引。"
+        "只返回 JSON 整数数组,例如 [0, 3]。"
+        "如果没有相关记忆,返回 []。\n\n"
+        f"最近对话:\n{recent}\n\n"
+        f"记忆目录:\n{catalog}"
     )
 
     try:
@@ -205,7 +203,7 @@ def select_relevant_memories(messages: list, max_items: int = 5) -> list[str]:
 
 
 def load_memories(messages: list) -> str:
-    """Load relevant memory content for injection into context."""
+    """加载相关记忆内容,用于注入上下文。"""
     selected_files = select_relevant_memories(messages)
     if not selected_files:
         return ""
@@ -220,7 +218,7 @@ def load_memories(messages: list) -> str:
 
 
 def extract_memories(messages: list):
-    """Extract new memories from recent dialogue. Runs 等待 each turn."""
+    """从最近对话中提取新记忆。每轮结束后运行。"""
     # 收集最近的对话文本
     dialogue_parts = []
     for msg in messages[-10:]:
@@ -240,19 +238,19 @@ def extract_memories(messages: list):
 
     # 检查已有记忆以避免重复
     existing = list_memory_files()
-    existing_desc = "\n".join(f"- {m['name']}: {m['description']}" for m in existing) if existing else "(none)"
+    existing_desc = "\n".join(f"- {m['name']}: {m['description']}" for m in existing) if existing else "(无)"
 
     prompt = (
-        "Extract user preferences, constraints, or project facts from this dialogue.\n"
-        "Return a JSON array. Each item: {name, type, description, body}.\n"
-        "- name: short kebab-case identifier (e.g. 'user-preference-tabs')\n"
-        "- type: one of 'user' (user preference), 'feedback' (guidance), "
-        "'project' (project fact), 'reference' (external pointer)\n"
-        "- description: one-line summary for index lookup\n"
-        "- body: full detail in markdown\n"
-        "If nothing new or already covered by existing memories, return [].\n\n"
-        f"Existing memories:\n{existing_desc}\n\n"
-        f"Dialogue:\n{dialogue[:4000]}"
+        "请从这段对话中提取用户偏好、约束或项目事实。\n"
+        "返回 JSON 数组。每一项格式为:{name, type, description, body}。\n"
+        "- name: 简短的 kebab-case 标识,例如 'user-preference-tabs'\n"
+        "- type: 只能是 'user'(用户偏好)、'feedback'(反馈/指导)、"
+        "'project'(项目事实)、'reference'(外部引用)之一\n"
+        "- description: 用于索引检索的一行摘要\n"
+        "- body: markdown 格式的完整细节\n"
+        "如果没有新内容,或已被现有记忆覆盖,返回 []。\n\n"
+        f"已有记忆:\n{existing_desc}\n\n"
+        f"对话:\n{dialogue[:4000]}"
     )
 
     try:
@@ -277,7 +275,7 @@ def extract_memories(messages: list):
                 write_memory_file(name, mem_type, desc, body)
                 count += 1
         if count:
-            print(f"\n\033[33m[Memory: extracted {count} new memories]\033[0m")
+            print(f"\n\033[33m[记忆:已提取 {count} 条新记忆]\033[0m")
     except Exception:
         pass
 
@@ -285,7 +283,7 @@ def extract_memories(messages: list):
 CONSOLIDATE_THRESHOLD = 10
 
 def consolidate_memories():
-    """Merge duplicate/stale memories. Triggered when file count ≥ threshold."""
+    """合并重复或过时的记忆。文件数量达到阈值时触发。"""
     files = list_memory_files()
     if len(files) < CONSOLIDATE_THRESHOLD:
         return
@@ -296,12 +294,12 @@ def consolidate_memories():
     )
 
     prompt = (
-        "Consolidate the following memory files. Rules:\n"
-        "1. Merge duplicates into one\n"
-        "2. Remove outdated/contradicted memories\n"
-        "3. Keep the total under 30 memories\n"
-        "4. Preserve important user preferences above all\n"
-        "Return a JSON array. Each item: {name, type, description, body}.\n\n"
+        "请整理下列记忆文件。规则:\n"
+        "1. 合并重复记忆\n"
+        "2. 删除过时或互相矛盾的记忆\n"
+        "3. 总数控制在 30 条以内\n"
+        "4. 优先保留重要的用户偏好\n"
+        "返回 JSON 数组。每一项格式为:{name, type, description, body}。\n\n"
         f"{catalog[:16000]}"
     )
 
@@ -328,7 +326,7 @@ def consolidate_memories():
             if desc and body:
                 write_memory_file(name, mem_type, desc, body)
 
-        print(f"\n\033[33m[Memory: consolidated {len(files)} → {len(items)} memories]\033[0m")
+        print(f"\n\033[33m[记忆:已将 {len(files)} 条合并为 {len(items)} 条]\033[0m")
     except Exception:
         pass
 
@@ -336,7 +334,7 @@ def consolidate_memories():
 # 使用记忆索引构建 SYSTEM
 def build_system() -> str:
     index = read_memory_index()
-    memories_section = f"\n\nMemories available:\n{index}" if index else ""
+    memories_section = f"\n\n可用记忆:\n{index}" if index else ""
     return (
         f"你是位于 {WORKDIR}."
         f"{memories_section}\n"
@@ -403,7 +401,7 @@ def extract_text(content) -> str:
     if not isinstance(content, list): return str(content)
     return "\n".join(getattr(b, "text", "") for b in content if getattr(b, "type", None) == "text")
 
-# 子 Agent (simplified from s06-s07)
+# 子 Agent(从 s06-s07 简化而来)
 SUB_TOOLS = [
     {"name": "bash", "description": "运行一条 shell 命令。",
      "input_schema": {"type": "object", "properties": {"command": {"type": "string"}}, "required": ["command"]}},
@@ -436,13 +434,13 @@ def spawn_subagent(description: str) -> str:
             if msg["role"] == "assistant":
                 result = extract_text(msg["content"])
                 if result: break
-        if not result: result = "子 Agent stopped 等待 30 turns without final answer."
+        if not result: result = "子 Agent 已等待 30 轮仍未给出最终回答,已停止。"
     print(f"\033[35m[子 Agent 已完成]\033[0m")
     return result
 
 
 # ═══════════════════════════════════════════════════════════
-#  来自 s08 (骨架): Compaction pipeline
+#  来自 s08(骨架): 压缩流水线
 # ═══════════════════════════════════════════════════════════
 
 CONTEXT_LIMIT = 50000; KEEP_RECENT = 3; PERSIST_THRESHOLD = 30000
@@ -480,7 +478,7 @@ def snip_compact(msgs, mx=50):
         tail_start -= 1
     if head_end >= tail_start:
         return msgs
-    return msgs[:head_end] + [{"role": "user", "content": f"[snipped {tail_start - head_end} msgs]"}] + msgs[tail_start:]
+    return msgs[:head_end] + [{"role": "user", "content": f"[已省略中间 {tail_start - head_end} 条消息]"}] + msgs[tail_start:]
 
 def collect_tool_results(msgs):
     blocks = []
@@ -502,7 +500,7 @@ def persist_large(tid, out):
     TOOL_RESULTS_DIR.mkdir(parents=True, exist_ok=True)
     p = TOOL_RESULTS_DIR / f"{tid}.txt"
     if not p.exists(): p.write_text(out)
-    return f"<persisted-output>\n完整内容:{p}\nPreview:\n{out[:2000]}\n</persisted-output>"
+    return f"<persisted-output>\n完整内容:{p}\n预览:\n{out[:2000]}\n</persisted-output>"
 
 def tool_result_budget(msgs, mx=200_000):
     last = msgs[-1] if msgs else None
@@ -536,7 +534,7 @@ def summarize_history(msgs):
 def compact_history(msgs):
     write_transcript(msgs)
     summary = summarize_history(msgs)
-    return [{"role": "user", "content": f"[Compacted]\n\n{summary}"}]
+    return [{"role": "user", "content": f"[已压缩]\n\n{summary}"}]
 
 def reactive_compact(msgs):
     write_transcript(msgs)
@@ -546,7 +544,7 @@ def reactive_compact(msgs):
             and _message_has_tool_use(msgs[tail_start - 1])):
         tail_start -= 1
     summary = summarize_history(msgs[:tail_start])
-    return [{"role": "user", "content": f"[Reactive compact]\n\n{summary}"}, *msgs[tail_start:]]
+    return [{"role": "user", "content": f"[响应式压缩]\n\n{summary}"}, *msgs[tail_start:]]
 
 
 # ═══════════════════════════════════════════════════════════

+ 198 - 0
s09_memory/代码讲解.md

@@ -0,0 +1,198 @@
+# S09 Memory 代码讲解
+
+这一节只讲相对 S08 新增的内容:
+
+**让 Agent 把用户偏好、项目事实和历史反馈保存下来,并在下一次对话中重新加载。**
+
+---
+
+## 1. 本节新增内容
+
+- `.memory/`:记忆文件目录。
+- `MEMORY.md`:记忆索引。
+- `write_memory_file()`:写入单条记忆。
+- `_rebuild_index()`:重建记忆索引。
+- `list_memory_files()`:读取所有记忆及其元数据。
+- `select_relevant_memories()`:从记忆列表中挑选相关记忆。
+- `load_memories()`:把相关记忆注入当前上下文。
+- `extract_memories()`:从最近对话中提取新记忆。
+- `consolidate_memories()`:定期合并、去重、删除过期记忆。
+
+---
+
+## 2. 记忆目录结构
+
+```text
+.memory/
+  MEMORY.md
+  user-style.md
+  project-facts.md
+  feedback-tabs.md
+```
+
+`MEMORY.md` 是索引,里面只放简短描述。
+
+每个单独的记忆文件存完整内容。
+
+---
+
+## 3. 单条记忆的数据结构
+
+一个记忆文件是 Markdown + YAML frontmatter:
+
+```markdown
+---
+name: user-style
+description: 用户喜欢中文解释,代码注释要适合教学
+type: user
+---
+
+用户偏好:
+- 回答用中文
+- 代码讲解要适合基础较弱的学生
+```
+
+程序读进来后,会变成字典:
+
+```python
+{
+    "filename": "user-style.md",
+    "name": "user-style",
+    "description": "用户喜欢中文解释,代码注释要适合教学",
+    "type": "user",
+    "body": "用户偏好:..."
+}
+```
+
+---
+
+## 4. `write_memory_file()`
+
+作用:把一条记忆写成一个独立 `.md` 文件。
+
+输入参数:
+
+```python
+name: str
+mem_type: str
+description: str
+body: str
+```
+
+写完后调用:
+
+```python
+_rebuild_index()
+```
+
+这样 `MEMORY.md` 会同步更新。
+
+---
+
+## 5. `_rebuild_index()`
+
+作用:扫描 `.memory/` 里所有记忆文件,重新生成索引。
+
+索引内容类似:
+
+```markdown
+- [user-style](user-style.md) — 用户喜欢中文解释
+- [project-facts](project-facts.md) — 当前项目是 Coding Agent 教学项目
+```
+
+System Prompt 里只放这个索引,而不是每次都塞完整记忆。
+
+---
+
+## 6. `select_relevant_memories(messages)`
+
+作用:根据最近几轮用户输入,从记忆目录里挑选相关文件。
+
+流程:
+
+```python
+读取所有记忆文件元数据
+提取最近 3 条用户消息
+构建 memory catalog
+让 LLM 返回相关记忆的下标数组
+如果 LLM 失败,用关键词匹配兜底
+```
+
+LLM 期望返回:
+
+```python
+[0, 3]
+```
+
+代表选择第 0 条和第 3 条记忆。
+
+---
+
+## 7. `load_memories(messages)`
+
+作用:把选中的记忆文件完整内容拼成一段文本:
+
+```xml
+<relevant_memories>
+...记忆 1...
+
+...记忆 2...
+</relevant_memories>
+```
+
+然后在当前用户消息前注入,让模型这轮能读到相关历史。
+
+---
+
+## 8. `extract_memories(messages)`
+
+作用:从最近对话中提取新的长期信息。
+
+它会让模型返回 JSON 数组:
+
+```python
+[
+    {
+        "name": "user-prefers-short-docs",
+        "type": "user",
+        "description": "用户喜欢短而清晰的讲义",
+        "body": "..."
+    }
+]
+```
+
+如果没有新记忆,就返回:
+
+```python
+[]
+```
+
+---
+
+## 9. `consolidate_memories()`
+
+当记忆文件太多时,会触发整理:
+
+```python
+合并重复记忆
+删除过期记忆
+保留重要偏好
+控制总数量
+```
+
+这类似 Agent 的“睡眠整理”或 “Dream” 机制。
+
+---
+
+## 10. 本节课堂重点
+
+Memory 不是把所有历史都永久塞进上下文。
+
+更合理的结构是:
+
+```python
+索引常驻 System
+相关记忆按需注入
+新信息轮后提取
+记忆过多时合并整理
+```

+ 9 - 9
s10_system_prompt/code.py

@@ -45,7 +45,7 @@ PROMPT_SECTIONS = {
 
 
 def assemble_system_prompt(context: dict) -> str:
-    """Select and join prompt sections based on current context."""
+    """根据当前上下文选择并拼接提示词片段。"""
     sections = []
 
     # 始终加载 — 身份
@@ -60,7 +60,7 @@ def assemble_system_prompt(context: dict) -> str:
     # 条件加载 — MEMORY.md 存在且有内容时加载记忆
     memories = context.get("memories", "")
     if memories:
-        sections.append(f"Relevant memories:\n{memories}")
+        sections.append(f"相关记忆:\n{memories}")
 
     return "\n\n".join(sections)
 
@@ -70,13 +70,13 @@ _last_prompt = None
 
 
 def get_system_prompt(context: dict) -> str:
-    """Cache wrapper — reassemble only when context changes.
+    """提示词缓存包装器:只有上下文变化时才重新组装。
 
-    Uses json.dumps for deterministic serialization, not Python's hash()
-    which has process randomization and fails on nested dicts/lists.
-    This cache only avoids redundant string assembly within a process.
-    Real Claude Code additionally protects API-level prompt cache via
-    stable section ordering and SYSTEM_PROMPT_DYNAMIC_BOUNDARY.
+    这里使用 json.dumps 做确定性序列化,不使用 Python 的 hash(),
+    因为 hash() 有进程级随机化,而且嵌套 dict/list 也不适用。
+    这个缓存只避免同一进程内重复拼接字符串。
+    真实 Claude Code 还会通过稳定的片段顺序和
+    SYSTEM_PROMPT_DYNAMIC_BOUNDARY 保护 API 级提示词缓存。
     """
     global _last_context_key, _last_prompt
     key = json.dumps(context, sort_keys=True, ensure_ascii=False, default=str)
@@ -171,7 +171,7 @@ def update_context(context: dict, messages: list) -> dict:
 # ── Agent 循环 ──
 
 def agent_loop(messages: list, context: dict):
-    """Main loop — uses assembled system prompt instead of hardcoded SYSTEM."""
+    """主循环:使用组装后的系统提示词,而不是硬编码 SYSTEM。"""
     system = get_system_prompt(context)
     while True:
         response = client.messages.create(

+ 144 - 0
s10_system_prompt/代码讲解.md

@@ -0,0 +1,144 @@
+# S10 System Prompt 代码讲解
+
+这一节只讲相对 S09 新增的内容:
+
+**System Prompt 不再写死,而是根据真实运行状态动态组装,并做缓存。**
+
+---
+
+## 1. 本节新增内容
+
+- `PROMPT_SECTIONS`:提示词片段字典。
+- `assemble_system_prompt(context)`:根据上下文拼接提示词。
+- `get_system_prompt(context)`:带缓存的提示词获取函数。
+- `update_context(context, messages)`:从当前项目状态推导上下文。
+- `agent_loop(messages, context)`:循环里使用动态 system prompt。
+
+---
+
+## 2. `PROMPT_SECTIONS`
+
+`PROMPT_SECTIONS` 是一个字典:
+
+```python
+PROMPT_SECTIONS: dict[str, str] = {
+    "identity": "你是一个编码 Agent。直接行动,不要只解释。"
+}
+```
+
+它把 System Prompt 拆成多个可组合片段。
+
+后面可以根据实际状态决定加载哪些片段。
+
+---
+
+## 3. `context` 数据结构
+
+`context` 是运行时上下文:
+
+```python
+context: dict = {
+    "enabled_tools": ["bash", "read_file", "write_file"],
+    "workspace": "/path/to/project",
+    "memories": "- [user-style](user-style.md) — 用户偏好..."
+}
+```
+
+它不是模型消息,而是程序用来组装 System Prompt 的状态。
+
+---
+
+## 4. `assemble_system_prompt(context)`
+
+作用:根据 `context` 生成最终 System Prompt。
+
+流程:
+
+```python
+加入身份片段
+加入当前可用工具
+加入工作目录
+如果有记忆索引,就加入记忆
+最后用空行拼接
+```
+
+输出是字符串:
+
+```python
+system: str
+```
+
+然后传给:
+
+```python
+client.messages.create(system=system, ...)
+```
+
+---
+
+## 5. `get_system_prompt(context)`
+
+作用:如果上下文没变,就复用上一次组装好的提示词。
+
+关键变量:
+
+```python
+_last_context_key: str | None
+_last_prompt: str | None
+```
+
+它先把 `context` 转成稳定字符串:
+
+```python
+key = json.dumps(context, sort_keys=True, ensure_ascii=False, default=str)
+```
+
+如果 `key` 没变:
+
+```python
+return _last_prompt
+```
+
+如果变了:
+
+```python
+_last_prompt = assemble_system_prompt(context)
+```
+
+---
+
+## 6. `update_context(context, messages)`
+
+作用:从真实状态生成新的上下文。
+
+这一节主要读取:
+
+```text
+.memory/MEMORY.md
+```
+
+如果存在记忆索引,就放进:
+
+```python
+context["memories"]
+```
+
+同时把当前工具列表和工作目录放进去。
+
+---
+
+## 7. 本节课堂重点
+
+System Prompt 不应该是一大坨永远不变的字符串。
+
+更工程化的方式是:
+
+```python
+基础身份固定
+工具列表来自真实注册表
+工作目录来自真实环境
+记忆来自真实文件
+上下文变化后再重新组装
+```
+
+这就是动态提示词系统。

+ 10 - 11
s11_error_recovery/code.py

@@ -76,7 +76,7 @@ def assemble_system_prompt(context: dict) -> str:
                 PROMPT_SECTIONS["workspace"]]
     memories = context.get("memories", "")
     if memories:
-        sections.append(f"Relevant memories:\n{memories}")
+        sections.append(f"相关记忆:\n{memories}")
     return "\n\n".join(sections)
 
 
@@ -194,8 +194,8 @@ def with_retry(fn, state: RecoveryState):
             # 429 速率限制 -> 指数退避
             if "ratelimit" in name.lower() or "429" in msg:
                 delay = retry_delay(attempt)
-                print(f"  \033[33m[429 速率限制] retry {attempt+1}/{MAX_RETRIES},"
-                      f" wait {delay:.1f}s\033[0m")
+                print(f"  \033[33m[429 速率限制] 重试 {attempt+1}/{MAX_RETRIES},"
+                      f" 等待 {delay:.1f}s\033[0m")
                 time.sleep(delay)
                 continue
 
@@ -213,8 +213,8 @@ def with_retry(fn, state: RecoveryState):
                         print(f"  \033[31m[529 x{MAX_CONSECUTIVE_529}]"
                               f" 未配置 FALLBACK_MODEL_ID,继续重试\033[0m")
                 delay = retry_delay(attempt)
-                print(f"  \033[33m[529 过载] retry {attempt+1}/{MAX_RETRIES},"
-                      f" wait {delay:.1f}s\033[0m")
+                print(f"  \033[33m[529 过载] 重试 {attempt+1}/{MAX_RETRIES},"
+                      f" 等待 {delay:.1f}s\033[0m")
                 time.sleep(delay)
                 continue
 
@@ -236,11 +236,10 @@ def reactive_compact(messages: list) -> list:
     """应急压缩 — 教学版本保留最后 N 条消息。
     真实 CC 会通过 LLM 生成压缩摘要,然后用压缩后的消息列表重试。
     因为 s08/s09 已覆盖基于 LLM 的压缩,教学版本简化为保留尾部消息。"""
-    print("  \033[31m[reactive compact] trimming to last 5 messages\033[0m")
+    print("  \033[31m[响应式压缩] 裁剪到最后 5 条消息\033[0m")
     tail = messages[-5:]
     return [{"role": "user",
-             "content": "[Reactive compact] Earlier conversation trimmed. "
-                        "Continue from where you left off."}, *tail]
+             "content": "[响应式压缩] 早前对话已被裁剪。请从中断处继续。"}, *tail]
 
 
 # ── 上下文 ──
@@ -262,7 +261,7 @@ def update_context(context: dict, messages: list) -> dict:
 # ── Agent 循环 ──
 
 def agent_loop(messages: list, context: dict):
-    """Main loop with 错误恢复 wrapping LLM calls."""
+    """主循环:为 LLM 调用包上错误恢复逻辑。"""
     system = get_system_prompt(context)
     state = RecoveryState()
     max_tokens = DEFAULT_MAX_TOKENS
@@ -302,7 +301,7 @@ def agent_loop(messages: list, context: dict):
             if not state.has_escalated:
                 max_tokens = ESCALATED_MAX_TOKENS
                 state.has_escalated = True
-                print(f"  \033[33m[max_tokens] escalating"
+                print(f"  \033[33m[max_tokens] 升级"
                       f" {DEFAULT_MAX_TOKENS} -> {ESCALATED_MAX_TOKENS}\033[0m")
                 continue
             # 64K 仍被截断:保存截断输出 + 续写提示
@@ -310,7 +309,7 @@ def agent_loop(messages: list, context: dict):
             if state.recovery_count < MAX_RECOVERY_RETRIES:
                 messages.append({"role": "user", "content": CONTINUATION_PROMPT})
                 state.recovery_count += 1
-                print(f"  \033[33m[max_tokens] continuation"
+                print(f"  \033[33m[max_tokens] 续写"
                       f" {state.recovery_count}/{MAX_RECOVERY_RETRIES}\033[0m")
                 continue
             print("  \033[31m[max_tokens] 恢复次数已达上限\033[0m")

+ 157 - 0
s11_error_recovery/代码讲解.md

@@ -0,0 +1,157 @@
+# S11 Error Recovery 代码讲解
+
+这一节只讲相对 S10 新增的内容:
+
+**Agent 调用模型时可能失败,所以需要恢复策略,而不是一报错就退出。**
+
+---
+
+## 1. 本节新增内容
+
+- `RecoveryState`:记录恢复状态。
+- `retry_delay()`:计算指数退避等待时间。
+- `with_retry()`:包装 LLM 调用,处理 429 / 529。
+- `is_prompt_too_long_error()`:识别上下文过长错误。
+- `reactive_compact()`:上下文过长时做应急压缩。
+- `max_tokens` 恢复路径:输出被截断时升级 token 或要求续写。
+
+---
+
+## 2. `RecoveryState`
+
+`RecoveryState` 是一个状态对象:
+
+```python
+state = RecoveryState()
+```
+
+内部字段:
+
+```python
+{
+    "has_escalated": False,
+    "recovery_count": 0,
+    "consecutive_529": 0,
+    "has_attempted_reactive_compact": False,
+    "current_model": PRIMARY_MODEL
+}
+```
+
+它记录当前 Agent 已经尝试过哪些恢复动作。
+
+---
+
+## 3. 三类错误恢复
+
+第一类:`max_tokens`
+
+```python
+response.stop_reason == "max_tokens"
+```
+
+说明模型输出被截断。
+
+处理方式:
+
+```python
+第一次:把 max_tokens 从 8000 升到 64000
+之后:追加“请继续”的用户消息
+超过次数:停止
+```
+
+第二类:`prompt_too_long`
+
+说明上下文太长。
+
+处理方式:
+
+```python
+messages[:] = reactive_compact(messages)
+重新请求
+```
+
+第三类:`429 / 529`
+
+说明请求太频繁或服务过载。
+
+处理方式:
+
+```python
+等待一段时间
+再重试
+连续 529 太多时切换备用模型
+```
+
+---
+
+## 4. `retry_delay(attempt)`
+
+作用:指数退避。
+
+大概规律:
+
+```text
+第 1 次等短一点
+第 2 次等更久
+第 3 次再更久
+```
+
+代码里还加了随机抖动 `jitter`,避免多个请求同时重试。
+
+---
+
+## 5. `with_retry(fn, state)`
+
+`fn` 是一个“暂时不执行的函数”。
+
+调用时传入:
+
+```python
+lambda: client.messages.create(...)
+```
+
+`with_retry()` 内部真正执行:
+
+```python
+result = fn()
+```
+
+如果遇到 429 / 529,就等待后继续。
+
+如果不是瞬时错误,就抛给外层处理。
+
+---
+
+## 6. Agent Loop 中的变化
+
+S10 直接调用模型:
+
+```python
+response = client.messages.create(...)
+```
+
+S11 改成:
+
+```python
+response = with_retry(
+    lambda: client.messages.create(...),
+    state
+)
+```
+
+外层再根据错误类型选择恢复路径。
+
+---
+
+## 7. 本节课堂重点
+
+生产级 Agent 必须能处理失败:
+
+```python
+输出被截断 -> 升级或续写
+上下文太长 -> 压缩后重试
+服务限流/过载 -> 退避重试
+多次失败 -> 给出明确错误
+```
+
+错误恢复是 Agent Harness 的重要工程能力。

+ 20 - 15
s12_task_system/code.py

@@ -96,8 +96,7 @@ def get_task(task_id: str) -> str:
 
 
 def can_start(task_id: str) -> bool:
-    """Check if all blockedBy dependencies are completed.
-    Missing dependencies are treated as blocked."""
+    """检查所有 blockedBy 依赖是否已完成;缺失依赖视为阻塞。"""
     task = load_task(task_id)
     for dep_id in task.blockedBy:
         if not _task_path(dep_id).exists():
@@ -110,31 +109,35 @@ def can_start(task_id: str) -> bool:
 def claim_task(task_id: str, owner: str = "agent") -> str:
     task = load_task(task_id)
     if task.status != "pending":
-        return f"任务 {task_id} 当前状态为 {task.status},无法认领"
+        status = {"pending": "待处理", "in_progress": "进行中",
+                  "completed": "已完成"}.get(task.status, task.status)
+        return f"任务 {task_id} 当前状态为 {status},无法认领"
     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}"
+        return f"被阻塞于:{deps}"
     task.owner = owner
     task.status = "in_progress"
     save_task(task)
-    print(f"  \033[36m[claim] {task.subject} → in_progress (owner: {owner})\033[0m")
+    print(f"  \033[36m[认领] {task.subject} → in_progress(负责人:{owner})\033[0m")
     return f"已认领 {task.id} ({task.subject})"
 
 
 def complete_task(task_id: str) -> str:
     task = load_task(task_id)
     if task.status != "in_progress":
-        return f"任务 {task_id} 当前状态为 {task.status},无法完成"
+        status = {"pending": "待处理", "in_progress": "进行中",
+                  "completed": "已完成"}.get(task.status, task.status)
+        return f"任务 {task_id} 当前状态为 {status},无法完成"
     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")
+    print(f"  \033[32m[完成] {task.subject} ✓\033[0m")
     msg = f"已完成 {task.id} ({task.subject})"
     if unblocked:
         msg += f"\n已解除阻塞:{', '.join(unblocked)}"
-        print(f"  \033[33m[unblocked] {', '.join(unblocked)}\033[0m")
+        print(f"  \033[33m[已解除阻塞] {', '.join(unblocked)}\033[0m")
     return msg
 
 
@@ -155,7 +158,7 @@ def assemble_system_prompt(context: dict) -> str:
                 PROMPT_SECTIONS["workspace"]]
     memories = context.get("memories", "")
     if memories:
-        sections.append(f"Relevant memories:\n{memories}")
+        sections.append(f"相关记忆:\n{memories}")
     return "\n\n".join(sections)
 
 
@@ -216,8 +219,8 @@ def run_write(path: str, content: str) -> str:
 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")
+    deps = f"(依赖:{', '.join(blockedBy)})" if blockedBy else ""
+    print(f"  \033[34m[创建] {task.subject}{deps}\033[0m")
     return f"已创建 {task.id}: {task.subject}{deps}"
 
 
@@ -229,10 +232,12 @@ def run_list_tasks() -> str:
     for t in 个任务:
         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 ""
+        deps = f"(依赖:{', '.join(t.blockedBy)})" if t.blockedBy else ""
+        owner = f" [负责人:{t.owner}]" if t.owner else ""
+        status = {"pending": "待处理", "in_progress": "进行中",
+                  "completed": "已完成"}.get(t.status, t.status)
         lines.append(f"  {icon} {t.id}: {t.subject} "
-                     f"[{t.status}]{owner}{deps}")
+                     f"[{status}]{owner}{deps}")
     return "\n".join(lines)
 
 
@@ -307,7 +312,7 @@ TOOL_HANDLERS = {
 # ── 上下文 ──
 
 def update_context(context: dict, messages: list) -> dict:
-    """Derive context from real state."""
+    """从真实状态推导上下文。"""
     memories = ""
     if MEMORY_INDEX.exists():
         content = MEMORY_INDEX.read_text().strip()

+ 154 - 0
s12_task_system/代码讲解.md

@@ -0,0 +1,154 @@
+# S12 Task System 代码讲解
+
+这一节只讲相对 S11 新增的内容:
+
+**把任务变成可持久化、可依赖、可认领、可完成的任务图。**
+
+---
+
+## 1. 本节新增内容
+
+- `Task` dataclass:任务数据模型。
+- `.tasks/`:任务持久化目录。
+- `create_task()`:创建任务。
+- `save_task()` / `load_task()`:保存和读取任务。
+- `list_tasks()` / `get_task()`:查看任务。
+- `can_start()`:检查依赖是否完成。
+- `claim_task()`:认领任务。
+- `complete_task()`:完成任务并提示下游解锁。
+- 5 个任务工具:`create_task`、`list_tasks`、`get_task`、`claim_task`、`complete_task`。
+
+---
+
+## 2. Task 数据结构
+
+`Task` 是一个 dataclass:
+
+```python
+@dataclass
+class Task:
+    id: str
+    subject: str
+    description: str
+    status: str
+    owner: str | None
+    blockedBy: list[str]
+```
+
+一条任务保存成 JSON 后大概是:
+
+```python
+{
+    "id": "task_1234567890_0001",
+    "subject": "实现登录页面",
+    "description": "完成表单和校验",
+    "status": "pending",
+    "owner": None,
+    "blockedBy": ["task_1234567890_0000"]
+}
+```
+
+---
+
+## 3. 状态流转
+
+任务状态有三种:
+
+```text
+pending -> in_progress -> completed
+```
+
+`claim_task()` 做:
+
+```python
+pending -> in_progress
+owner = "agent"
+```
+
+`complete_task()` 做:
+
+```python
+in_progress -> completed
+```
+
+---
+
+## 4. `blockedBy`
+
+`blockedBy` 是依赖任务 ID 列表:
+
+```python
+blockedBy: list[str] = ["task_a", "task_b"]
+```
+
+意思是:
+
+```text
+当前任务必须等 task_a 和 task_b 都 completed 后才能开始
+```
+
+`can_start(task_id)` 会检查所有依赖:
+
+```python
+如果依赖任务不存在 -> 不能开始
+如果依赖任务不是 completed -> 不能开始
+全部完成 -> 可以开始
+```
+
+---
+
+## 5. `.tasks/` 持久化
+
+每个任务是一个文件:
+
+```text
+.tasks/
+  task_1234567890_0001.json
+```
+
+这样任务不会只存在内存里,程序重启后仍然能读回来。
+
+---
+
+## 6. 工具层封装
+
+底层函数:
+
+```python
+create_task()
+claim_task()
+complete_task()
+```
+
+工具处理器:
+
+```python
+run_create_task()
+run_claim_task()
+run_complete_task()
+```
+
+模型真正看到的是工具 schema:
+
+```python
+{
+    "name": "create_task",
+    "input_schema": {...}
+}
+```
+
+---
+
+## 7. 本节课堂重点
+
+任务系统让 Agent 从“临时执行”进入“可管理工作流”。
+
+有了任务系统,Agent 可以表达:
+
+```python
+有哪些任务
+谁正在做
+哪些被依赖阻塞
+哪些已经完成
+完成后解锁了什么
+```

+ 26 - 22
s13_background_tasks/code.py

@@ -95,8 +95,7 @@ def get_task(task_id: str) -> str:
 
 
 def can_start(task_id: str) -> bool:
-    """Check if all blockedBy dependencies are completed.
-    Missing dependencies are treated as blocked."""
+    """检查所有 blockedBy 依赖是否已完成;缺失依赖视为阻塞。"""
     task = load_task(task_id)
     for dep_id in task.blockedBy:
         if not _task_path(dep_id).exists():
@@ -109,31 +108,35 @@ def can_start(task_id: str) -> bool:
 def claim_task(task_id: str, owner: str = "agent") -> str:
     task = load_task(task_id)
     if task.status != "pending":
-        return f"任务 {task_id} 当前状态为 {task.status},无法认领"
+        status = {"pending": "待处理", "in_progress": "进行中",
+                  "completed": "已完成"}.get(task.status, task.status)
+        return f"任务 {task_id} 当前状态为 {status},无法认领"
     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}"
+        return f"被阻塞于:{deps}"
     task.owner = owner
     task.status = "in_progress"
     save_task(task)
-    print(f"  \033[36m[claim] {task.subject} → in_progress (owner: {owner})\033[0m")
+    print(f"  \033[36m[认领] {task.subject} → in_progress(负责人:{owner})\033[0m")
     return f"已认领 {task.id} ({task.subject})"
 
 
 def complete_task(task_id: str) -> str:
     task = load_task(task_id)
     if task.status != "in_progress":
-        return f"任务 {task_id} 当前状态为 {task.status},无法完成"
+        status = {"pending": "待处理", "in_progress": "进行中",
+                  "completed": "已完成"}.get(task.status, task.status)
+        return f"任务 {task_id} 当前状态为 {status},无法完成"
     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")
+    print(f"  \033[32m[完成] {task.subject} ✓\033[0m")
     msg = f"已完成 {task.id} ({task.subject})"
     if unblocked:
         msg += f"\n已解除阻塞:{', '.join(unblocked)}"
-        print(f"  \033[33m[unblocked] {', '.join(unblocked)}\033[0m")
+        print(f"  \033[33m[已解除阻塞] {', '.join(unblocked)}\033[0m")
     return msg
 
 
@@ -154,7 +157,7 @@ def assemble_system_prompt(context: dict) -> str:
                 PROMPT_SECTIONS["workspace"]]
     memories = context.get("memories", "")
     if memories:
-        sections.append(f"Relevant memories:\n{memories}")
+        sections.append(f"相关记忆:\n{memories}")
     return "\n\n".join(sections)
 
 
@@ -216,8 +219,8 @@ def run_write(path: str, content: str) -> str:
 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")
+    deps = f"(依赖:{', '.join(blockedBy)})" if blockedBy else ""
+    print(f"  \033[34m[创建] {task.subject}{deps}\033[0m")
     return f"已创建 {task.id}: {task.subject}{deps}"
 
 
@@ -229,10 +232,12 @@ def run_list_tasks() -> str:
     for t in 个任务:
         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 ""
+        deps = f"(依赖:{', '.join(t.blockedBy)})" if t.blockedBy else ""
+        owner = f" [负责人:{t.owner}]" if t.owner else ""
+        status = {"pending": "待处理", "in_progress": "进行中",
+                  "completed": "已完成"}.get(t.status, t.status)
         lines.append(f"  {icon} {t.id}: {t.subject} "
-                     f"[{t.status}]{owner}{deps}")
+                     f"[{status}]{owner}{deps}")
     return "\n".join(lines)
 
 
@@ -310,7 +315,7 @@ TOOL_HANDLERS = {
 
 _bg_计数器 = 0
 background_tasks: dict[str, dict] = {}   # bg_id → {tool_use_id, command, status}
-background_results: dict[str, str] = {}   # bg_id → output
+background_results: dict[str, str] = {}   # bg_id → 输出
 background_lock = threading.Lock()
 
 
@@ -361,7 +366,7 @@ def start_background_task(block) -> str:
         }
     thread = threading.Thread(target=worker, daemon=True)
     thread.start()
-    print(f"  \033[33m[background] dispatched {bg_id}: {cmd[:40]}\033[0m")
+    print(f"  \033[33m[后台] 已分发 {bg_id}: {cmd[:40]}\033[0m")
     return bg_id
 
 
@@ -383,7 +388,7 @@ def collect_background_results() -> list[str]:
             f"  <command>{task['command']}</command>\n"
             f"  <summary>{summary}</summary>\n"
             f"</task_notification>")
-        print(f"  \033[32m[background done] {bg_id}: "
+        print(f"  \033[32m[后台完成] {bg_id}: "
               f"{task['command'][:40]} ({len(output)} 个字符)\033[0m")
     return notifications
 
@@ -391,7 +396,7 @@ def collect_background_results() -> list[str]:
 # ── 上下文 ──
 
 def update_context(context: dict, messages: list) -> dict:
-    """Derive context from real state."""
+    """从真实状态推导上下文。"""
     memories = ""
     if MEMORY_INDEX.exists():
         content = MEMORY_INDEX.read_text().strip()
@@ -433,7 +438,7 @@ def agent_loop(messages: list, context: dict):
                 bg_id = start_background_task(block)
                 results.append({"type": "tool_result",
                                 "tool_use_id": block.id,
-                                "content": f"[Background task {bg_id} started] "
+                                "content": f"[后台任务 {bg_id} 已启动] "
                                            f"命令:{block.input.get('command', '')}. "
                                            f"完成后结果将可用。"})
             else:
@@ -449,15 +454,14 @@ def agent_loop(messages: list, context: dict):
         if bg_notifications:
             for notif in bg_notifications:
                 user_content.append({"type": "text", "text": notif})
-            print(f"  \033[32m[inject] {len(bg_notifications)} background "
-                  f"notification(s)\033[0m")
+            print(f"  \033[32m[注入] {len(bg_notifications)} 条后台通知\033[0m")
         messages.append({"role": "user", "content": user_content})
         context = update_context(context, messages)
         system = get_system_prompt(context)
 
 
 if __name__ == "__main__":
-    print("s13: background 个任务")
+    print("s13: 后台任务")
     print("输入问题后按回车发送。输入 q 退出。\n")
     history = []
     context = update_context({}, [])

+ 188 - 0
s13_background_tasks/代码讲解.md

@@ -0,0 +1,188 @@
+# S13 Background Tasks 代码讲解
+
+这一节只讲相对 S12 新增的内容:
+
+**让慢工具调用进入后台线程执行,Agent 不必一直卡在原地等待。**
+
+---
+
+## 1. 本节新增内容
+
+- `background_tasks`:记录后台任务状态。
+- `background_results`:保存后台任务结果。
+- `background_lock`:保护多线程共享数据。
+- `is_slow_operation()`:判断某个命令是否可能很慢。
+- `should_run_background()`:决定是否后台执行。
+- `execute_tool()`:统一执行工具。
+- `start_background_task()`:启动后台线程。
+- `collect_background_results()`:收集已完成的后台任务结果。
+
+---
+
+## 2. 后台任务数据结构
+
+`background_tasks` 是:
+
+```python
+background_tasks: dict[str, dict]
+```
+
+结构大概是:
+
+```python
+{
+    "bg_0001": {
+        "tool_use_id": "toolu_01xxx",
+        "command": "npm install",
+        "status": "running"
+    }
+}
+```
+
+`background_results` 是:
+
+```python
+background_results: dict[str, str]
+```
+
+结构大概是:
+
+```python
+{
+    "bg_0001": "命令执行完成后的输出"
+}
+```
+
+---
+
+## 3. 为什么需要后台任务
+
+有些命令会很慢:
+
+```bash
+npm install
+pytest
+docker build
+make
+```
+
+如果同步执行,Agent Loop 会一直阻塞。
+
+后台任务的目标是:
+
+```python
+先返回一个“任务已启动”的 tool_result
+慢命令在线程里继续跑
+完成后再把结果作为通知注入 messages
+```
+
+---
+
+## 4. `should_run_background()`
+
+判断顺序:
+
+```python
+如果模型显式传了 run_in_background=True:
+    后台执行
+否则:
+    用 is_slow_operation() 做兜底判断
+```
+
+模型传参示例:
+
+```python
+{
+    "command": "npm install",
+    "run_in_background": True
+}
+```
+
+---
+
+## 5. `start_background_task(block)`
+
+`block` 是模型返回的 `tool_use` 对象:
+
+```python
+{
+    "type": "tool_use",
+    "id": "toolu_01",
+    "name": "bash",
+    "input": {
+        "command": "npm install",
+        "run_in_background": True
+    }
+}
+```
+
+执行流程:
+
+```python
+生成 bg_id
+把任务登记到 background_tasks
+启动 threading.Thread
+立即返回 bg_id
+```
+
+后台线程执行完成后,会写入:
+
+```python
+background_tasks[bg_id]["status"] = "completed"
+background_results[bg_id] = output
+```
+
+---
+
+## 6. `collect_background_results()`
+
+作用:把完成的后台任务变成通知。
+
+通知格式:
+
+```xml
+<task_notification>
+  <task_id>bg_0001</task_id>
+  <status>completed</status>
+  <command>npm install</command>
+  <summary>输出摘要</summary>
+</task_notification>
+```
+
+这个通知会作为普通文本 block 注入下一轮用户消息。
+
+---
+
+## 7. Agent Loop 中的变化
+
+工具调用时:
+
+```python
+if should_run_background(block.name, block.input):
+    bg_id = start_background_task(block)
+    返回“后台任务已启动”
+else:
+    正常同步执行工具
+```
+
+每轮结束时:
+
+```python
+bg_notifications = collect_background_results()
+messages.append({"role": "user", "content": 工具结果 + 后台通知})
+```
+
+---
+
+## 8. 本节课堂重点
+
+后台任务让 Agent 可以处理慢操作。
+
+它的本质不是模型变异步,而是 Harness 做了异步:
+
+```python
+模型提出工具调用
+程序决定后台执行
+线程运行真实工具
+结果完成后通过通知重新进入上下文
+```

+ 48 - 44
s14_cron_scheduler/code.py

@@ -98,8 +98,7 @@ def get_task(task_id: str) -> str:
 
 
 def can_start(task_id: str) -> bool:
-    """Check if all blockedBy dependencies are completed.
-    Missing dependencies are treated as blocked."""
+    """检查所有 blockedBy 依赖是否已完成;缺失依赖视为阻塞。"""
     task = load_task(task_id)
     for dep_id in task.blockedBy:
         if not _task_path(dep_id).exists():
@@ -112,31 +111,35 @@ def can_start(task_id: str) -> bool:
 def claim_task(task_id: str, owner: str = "agent") -> str:
     task = load_task(task_id)
     if task.status != "pending":
-        return f"任务 {task_id} 当前状态为 {task.status},无法认领"
+        status = {"pending": "待处理", "in_progress": "进行中",
+                  "completed": "已完成"}.get(task.status, task.status)
+        return f"任务 {task_id} 当前状态为 {status},无法认领"
     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}"
+        return f"被阻塞于:{deps}"
     task.owner = owner
     task.status = "in_progress"
     save_task(task)
-    print(f"  \033[36m[claim] {task.subject} → in_progress (owner: {owner})\033[0m")
+    print(f"  \033[36m[认领] {task.subject} → in_progress(负责人:{owner})\033[0m")
     return f"已认领 {task.id} ({task.subject})"
 
 
 def complete_task(task_id: str) -> str:
     task = load_task(task_id)
     if task.status != "in_progress":
-        return f"任务 {task_id} 当前状态为 {task.status},无法完成"
+        status = {"pending": "待处理", "in_progress": "进行中",
+                  "completed": "已完成"}.get(task.status, task.status)
+        return f"任务 {task_id} 当前状态为 {status},无法完成"
     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")
+    print(f"  \033[32m[完成] {task.subject} ✓\033[0m")
     msg = f"已完成 {task.id} ({task.subject})"
     if unblocked:
         msg += f"\n已解除阻塞:{', '.join(unblocked)}"
-        print(f"  \033[33m[unblocked] {', '.join(unblocked)}\033[0m")
+        print(f"  \033[33m[已解除阻塞] {', '.join(unblocked)}\033[0m")
     return msg
 
 
@@ -158,7 +161,7 @@ def assemble_system_prompt(context: dict) -> str:
                 PROMPT_SECTIONS["workspace"]]
     memories = context.get("memories", "")
     if memories:
-        sections.append(f"Relevant memories:\n{memories}")
+        sections.append(f"相关记忆:\n{memories}")
     return "\n\n".join(sections)
 
 
@@ -220,8 +223,8 @@ def run_write(path: str, content: str) -> str:
 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")
+    deps = f"(依赖:{', '.join(blockedBy)})" if blockedBy else ""
+    print(f"  \033[34m[创建] {task.subject}{deps}\033[0m")
     return f"已创建 {task.id}: {task.subject}{deps}"
 
 
@@ -233,10 +236,12 @@ def run_list_tasks() -> str:
     for t in 个任务:
         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 ""
+        deps = f"(依赖:{', '.join(t.blockedBy)})" if t.blockedBy else ""
+        owner = f" [负责人:{t.owner}]" if t.owner else ""
+        status = {"pending": "待处理", "in_progress": "进行中",
+                  "completed": "已完成"}.get(t.status, t.status)
         lines.append(f"  {icon} {t.id}: {t.subject} "
-                     f"[{t.status}]{owner}{deps}")
+                     f"[{status}]{owner}{deps}")
     return "\n".join(lines)
 
 
@@ -316,7 +321,7 @@ def start_background_task(block) -> str:
             "status": "running",
         }
     threading.Thread(target=worker, daemon=True).start()
-    print(f"  \033[33m[background] dispatched {bg_id}: {cmd[:40]}\033[0m")
+    print(f"  \033[33m[后台] 已分发 {bg_id}: {cmd[:40]}\033[0m")
     return bg_id
 
 
@@ -338,7 +343,7 @@ def collect_background_results() -> list[str]:
             f"  <command>{task['command']}</command>\n"
             f"  <summary>{summary}</summary>\n"
             f"</task_notification>")
-        print(f"  \033[32m[background done] {bg_id}: "
+        print(f"  \033[32m[后台完成] {bg_id}: "
               f"{task['command'][:40]} ({len(output)} 个字符)\033[0m")
     return notifications
 
@@ -353,8 +358,8 @@ class CronJob:
     id: str
     cron: str        # "0 9 * * *"
     prompt: str      # 触发时要注入的消息
-    recurring: bool  # True = 重复,False = 一次性
-    durable: bool    # True = 持久化到磁盘
+    recurring: bool  # 为真 = 重复,为假 = 一次性
+    durable: bool    # 为真 = 持久化到磁盘
 
 
 scheduled_jobs: dict[str, CronJob] = {}
@@ -381,8 +386,8 @@ def _cron_field_matches(field: str, value: int) -> bool:
 
 
 def cron_matches(cron_expr: str, dt: datetime) -> bool:
-    """Check if a 5-field cron expression matches the given datetime.
-    Standard cron semantics: DOM and DOW use OR when both are constrained."""
+    """检查 5 字段 cron 表达式是否匹配给定时间。
+    标准 cron 语义:月内日和周内日同时受限时,两者使用 OR。"""
     fields = cron_expr.strip().split()
     if len(fields) != 5:
         return False
@@ -451,7 +456,7 @@ def validate_cron(cron_expr: str) -> str | None:
     if len(fields) != 5:
         return f"期望 5 个字段,实际得到 {len(fields)}"
     bounds = [(0, 59), (0, 23), (1, 31), (1, 12), (0, 6)]
-    names = ["minute", "hour", "day-of-month", "month", "day-of-week"]
+    names = ["分", "时", "月内日", "月", "周内日"]
     for i, (field, (lo, hi), name) in enumerate(zip(fields, bounds, names)):
         err = _validate_cron_field(field, lo, hi)
         if err:
@@ -475,18 +480,18 @@ def load_durable_jobs():
             job = CronJob(**j)
             err = validate_cron(job.cron)
             if err:
-                print(f"  \033[31m[cron] skipping invalid job {job.id}: {err}\033[0m")
+                print(f"  \033[31m[cron] 跳过无效任务 {job.id}: {err}\033[0m")
                 continue
             scheduled_jobs[job.id] = job
         valid = [j for j in jobs if j["id"] in scheduled_jobs]
         if valid:
-            print(f"  \033[35m[cron] loaded {len(valid)} durable job(s)\033[0m")
+            print(f"  \033[35m[cron] 已加载 {len(valid)} 个持久任务\033[0m")
     except Exception:
         pass
 
 
 def schedule_job(cron: str, prompt: str, recurring: bool = True,
-                 durable: bool = True) -> Cron任务 | str:
+                 durable: bool = True) -> CronJob | str:
     """注册一个新的 cron 任务。返回 CronJob 或错误字符串。"""
     err = validate_cron(cron)
     if err:
@@ -500,7 +505,7 @@ def schedule_job(cron: str, prompt: str, recurring: bool = True,
         scheduled_jobs[job.id] = job
     if durable:
         save_durable_jobs()
-    print(f"  \033[35m[cron register] {job.id} '{cron}' → {prompt[:40]}\033[0m")
+    print(f"  \033[35m[cron 注册] {job.id} '{cron}' → {prompt[:40]}\033[0m")
     return job
 
 
@@ -512,14 +517,13 @@ def cancel_job(job_id: str) -> str:
         return f"任务 {job_id} 未找到"
     if job.durable:
         save_durable_jobs()
-    print(f"  \033[31m[cron cancel] {job_id}\033[0m")
+    print(f"  \033[31m[cron 取消] {job_id}\033[0m")
     return f"已取消 {job_id}"
 
 
 def cron_scheduler_loop():
-    """Independent daemon thread: poll every 1s, fire matching jobs.
-    Individual job errors are caught to prevent one bad job from
-    killing the entire scheduler thread."""
+    """独立守护线程:每秒轮询一次并触发匹配任务。
+    单个任务出错会被捕获,避免整个调度线程退出。"""
     while True:
         time.sleep(1)
         now = datetime.now()
@@ -532,14 +536,14 @@ def cron_scheduler_loop():
                         if _last_fired.get(job.id) != minute_marker:
                             cron_queue.append(job)
                             _last_fired[job.id] = minute_marker
-                            print(f"  \033[35m[cron fire] {job.id} → "
+                            print(f"  \033[35m[cron 触发] {job.id} → "
                                   f"{job.prompt[:40]}\033[0m")
                         if not job.recurring:
                             scheduled_jobs.pop(job.id, None)
                             if job.durable:
                                 save_durable_jobs()
                 except Exception as e:
-                    print(f"  \033[31m[cron error] {job.id}: {e}\033[0m")
+                    print(f"  \033[31m[cron 错误] {job.id}: {e}\033[0m")
 
 
 def consume_cron_queue() -> list[CronJob]:
@@ -551,7 +555,7 @@ def consume_cron_queue() -> list[CronJob]:
 
 
 def has_cron_queue() -> bool:
-    """Return whether fired cron jobs are waiting to be delivered."""
+    """返回是否有已触发但尚未投递的 cron 任务。"""
     with cron_lock:
         return bool(cron_queue)
 
@@ -559,7 +563,7 @@ def has_cron_queue() -> bool:
 # 启动时加载持久任务,然后启动调度线程
 load_durable_jobs()
 threading.Thread(target=cron_scheduler_loop, daemon=True).start()
-print("  \033[35m[cron] scheduler thread started\033[0m")
+print("  \033[35m[cron] 调度线程已启动\033[0m")
 
 
 # ── Cron 工具 ──
@@ -579,8 +583,8 @@ def run_list_crons() -> str:
         return "暂无 cron 任务。请使用 schedule_cron 添加一个。"
     lines = []
     for j in jobs:
-        tag = "recurring" if j.recurring else "one-shot"
-        dur = "durable" if j.durable else "session"
+        tag = "重复" if j.recurring else "一次性"
+        dur = "持久化" if j.durable else "仅本会话"
         lines.append(f"  {j.id}: '{j.cron}' → {j.prompt[:40]} "
                      f"[{tag}, {dur}]")
     return "\n".join(lines)
@@ -646,9 +650,9 @@ TOOLS = [
                           "prompt": {"type": "string",
                                      "description": "触发时要注入的消息"},
                           "recurring": {"type": "boolean",
-                                        "description": "True=重复,False=一次性"},
+                                        "description": "为真表示重复,为假表示一次性"},
                           "durable": {"type": "boolean",
-                                      "description": "True=持久化到磁盘"}},
+                                      "description": "为真表示持久化到磁盘"}},
                       "required": ["cron", "prompt"]}},
     {"name": "list_crons",
      "description": "列出所有已注册的 cron 任务。",
@@ -665,7 +669,7 @@ TOOLS = [
 # ── 上下文 ──
 
 def update_context(context: dict, messages: list) -> dict:
-    """Derive context from real state."""
+    """从真实状态推导上下文。"""
     memories = ""
     if MEMORY_INDEX.exists():
         content = MEMORY_INDEX.read_text().strip()
@@ -690,8 +694,8 @@ def agent_loop(messages: list, context: dict) -> dict:
         fired = consume_cron_queue()
         for job in fired:
             messages.append({"role": "user",
-                             "content": f"[Scheduled] {job.prompt}"})
-            print(f"  \033[35m[inject cron] {job.prompt[:50]}\033[0m")
+                             "content": f"[已调度] {job.prompt}"})
+            print(f"  \033[35m[注入 cron] {job.prompt[:50]}\033[0m")
 
         try:
             response = client.messages.create(
@@ -717,7 +721,7 @@ def agent_loop(messages: list, context: dict) -> dict:
                 bg_id = start_background_task(block)
                 results.append({"type": "tool_result",
                                 "tool_use_id": block.id,
-                                "content": f"[Background task {bg_id} started] "
+                                "content": f"[后台任务 {bg_id} 已启动] "
                                            f"完成后结果将可用。"})
             else:
                 output = execute_tool(block)
@@ -742,7 +746,7 @@ session_context = update_context({}, [])
 
 
 def print_latest_assistant_text(messages: list):
-    """Print text blocks from the latest assistant message."""
+    """打印最新 assistant 消息中的文本块。"""
     if not messages:
         return
     msg = messages[-1]
@@ -760,7 +764,7 @@ def print_latest_assistant_text(messages: list):
 
 
 def run_agent_turn_locked(user_query: str | None = None):
-    """Run one agent turn. Caller must hold agent_lock."""
+    """运行一轮 Agent。调用方必须持有 agent_lock。"""
     global session_context
     if user_query is not None:
         session_history.append({"role": "user", "content": user_query})
@@ -771,7 +775,7 @@ def run_agent_turn_locked(user_query: str | None = None):
 
 
 def queue_processor_loop():
-    """Auto-deliver fired cron jobs when the agent is idle."""
+    """当 Agent 空闲时自动投递已触发的 cron 任务。"""
     global session_context
     while True:
         time.sleep(0.2)

+ 167 - 0
s14_cron_scheduler/代码讲解.md

@@ -0,0 +1,167 @@
+# S14 Cron Scheduler 代码讲解
+
+这一节只讲相对 S13 新增的内容:
+
+**让 Agent 可以被定时任务唤醒,而不是只能等用户输入。**
+
+---
+
+## 1. 本节新增内容
+
+- `CronJob` dataclass:定时任务数据模型。
+- `scheduled_jobs`:内存中的定时任务表。
+- `cron_queue`:已触发、待投递的任务队列。
+- `_cron_field_matches()`:匹配单个 cron 字段。
+- `cron_matches()`:判断某个时间是否命中 cron 表达式。
+- `validate_cron()`:校验 cron 表达式是否合法。
+- `save_durable_jobs()` / `load_durable_jobs()`:持久化定时任务。
+- `schedule_job()` / `cancel_job()`:注册和取消任务。
+- `cron_scheduler_loop()`:后台调度线程。
+- `queue_processor_loop()`:队列处理器,自动唤醒 Agent。
+- 3 个工具:`schedule_cron`、`list_crons`、`cancel_cron`。
+
+---
+
+## 2. CronJob 数据结构
+
+`CronJob` 是:
+
+```python
+@dataclass
+class CronJob:
+    id: str
+    cron: str
+    prompt: str
+    recurring: bool
+    durable: bool
+```
+
+保存成字典后大概是:
+
+```python
+{
+    "id": "cron_123456",
+    "cron": "0 9 * * *",
+    "prompt": "每天早上检查项目状态",
+    "recurring": True,
+    "durable": True
+}
+```
+
+---
+
+## 3. 三个核心容器
+
+`scheduled_jobs`:
+
+```python
+scheduled_jobs: dict[str, CronJob]
+```
+
+保存所有已注册的 cron 任务。
+
+`cron_queue`:
+
+```python
+cron_queue: list[CronJob]
+```
+
+保存已经触发、但还没交给 Agent 处理的任务。
+
+`_last_fired`:
+
+```python
+_last_fired: dict[str, str]
+```
+
+用来防止同一分钟内重复触发同一个任务。
+
+---
+
+## 4. Cron 表达式
+
+本节使用 5 字段 cron:
+
+```text
+分 时 月内日 月 周内日
+```
+
+例如:
+
+```text
+0 9 * * *
+```
+
+意思是每天 9:00。
+
+支持:
+
+```text
+*      任意值
+*/5    每 5 个单位
+1,2,3  多个值
+1-5    范围
+```
+
+---
+
+## 5. `cron_scheduler_loop()`
+
+这是一个后台守护线程。
+
+流程:
+
+```python
+每 1 秒醒来一次
+拿当前时间 datetime.now()
+遍历 scheduled_jobs
+如果 cron_matches(job.cron, now):
+    把 job 放进 cron_queue
+```
+
+它只负责“发现任务到点了”,不直接调用模型。
+
+---
+
+## 6. `queue_processor_loop()`
+
+队列处理器负责把 `cron_queue` 里的任务交给 Agent。
+
+为什么需要队列?
+
+为了让调度线程和 Agent 执行解耦:
+
+```text
+调度器只负责触发
+队列负责暂存
+Agent 空闲时再消费
+```
+
+---
+
+## 7. Agent Loop 中的变化
+
+每轮开始先消费 cron 队列:
+
+```python
+fired = consume_cron_queue()
+for job in fired:
+    messages.append({
+        "role": "user",
+        "content": f"[Scheduled] {job.prompt}"
+    })
+```
+
+也就是说,定时任务最终会变成一条用户消息。
+
+---
+
+## 8. 本节课堂重点
+
+Cron Scheduler 让 Agent 从“被动响应用户”变成“能被时间触发”。
+
+核心结构是:
+
+```python
+后台调度线程 -> cron_queue -> 队列处理器 -> agent_loop
+```

+ 60 - 58
s15_agent_teams/code.py

@@ -96,8 +96,7 @@ def get_task(task_id: str) -> str:
 
 
 def can_start(task_id: str) -> bool:
-    """Check if all blockedBy dependencies are completed.
-    Missing dependencies are treated as blocked."""
+    """检查所有 blockedBy 依赖是否已完成;缺失依赖视为阻塞。"""
     task = load_task(task_id)
     for dep_id in task.blockedBy:
         if not _task_path(dep_id).exists():
@@ -110,31 +109,35 @@ def can_start(task_id: str) -> bool:
 def claim_task(task_id: str, owner: str = "agent") -> str:
     task = load_task(task_id)
     if task.status != "pending":
-        return f"任务 {task_id} 当前状态为 {task.status},无法认领"
+        status = {"pending": "待处理", "in_progress": "进行中",
+                  "completed": "已完成"}.get(task.status, task.status)
+        return f"任务 {task_id} 当前状态为 {status},无法认领"
     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}"
+        return f"被阻塞于:{deps}"
     task.owner = owner
     task.status = "in_progress"
     save_task(task)
-    print(f"  \033[36m[claim] {task.subject} → in_progress (owner: {owner})\033[0m")
+    print(f"  \033[36m[认领] {task.subject} → in_progress(负责人:{owner})\033[0m")
     return f"已认领 {task.id} ({task.subject})"
 
 
 def complete_task(task_id: str) -> str:
     task = load_task(task_id)
     if task.status != "in_progress":
-        return f"任务 {task_id} 当前状态为 {task.status},无法完成"
+        status = {"pending": "待处理", "in_progress": "进行中",
+                  "completed": "已完成"}.get(task.status, task.status)
+        return f"任务 {task_id} 当前状态为 {status},无法完成"
     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")
+    print(f"  \033[32m[完成] {task.subject} ✓\033[0m")
     msg = f"已完成 {task.id} ({task.subject})"
     if unblocked:
         msg += f"\n已解除阻塞:{', '.join(unblocked)}"
-        print(f"  \033[33m[unblocked] {', '.join(unblocked)}\033[0m")
+        print(f"  \033[33m[已解除阻塞] {', '.join(unblocked)}\033[0m")
     return msg
 
 
@@ -157,7 +160,7 @@ def assemble_system_prompt(context: dict) -> str:
                 PROMPT_SECTIONS["workspace"]]
     memories = context.get("memories", "")
     if memories:
-        sections.append(f"Relevant memories:\n{memories}")
+        sections.append(f"相关记忆:\n{memories}")
     return "\n\n".join(sections)
 
 
@@ -219,8 +222,8 @@ def run_write(path: str, content: str) -> str:
 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")
+    deps = f"(依赖:{', '.join(blockedBy)})" if blockedBy else ""
+    print(f"  \033[34m[创建] {task.subject}{deps}\033[0m")
     return f"已创建 {task.id}: {task.subject}{deps}"
 
 
@@ -232,10 +235,12 @@ def run_list_tasks() -> str:
     for t in 个任务:
         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 ""
+        deps = f"(依赖:{', '.join(t.blockedBy)})" if t.blockedBy else ""
+        owner = f" [负责人:{t.owner}]" if t.owner else ""
+        status = {"pending": "待处理", "in_progress": "进行中",
+                  "completed": "已完成"}.get(t.status, t.status)
         lines.append(f"  {icon} {t.id}: {t.subject} "
-                     f"[{t.status}]{owner}{deps}")
+                     f"[{status}]{owner}{deps}")
     return "\n".join(lines)
 
 
@@ -317,7 +322,7 @@ def start_background_task(block) -> str:
             "status": "running",
         }
     threading.Thread(target=worker, daemon=True).start()
-    print(f"  \033[33m[background] dispatched {bg_id}: {cmd[:40]}\033[0m")
+    print(f"  \033[33m[后台] 已分发 {bg_id}: {cmd[:40]}\033[0m")
     return bg_id
 
 
@@ -339,14 +344,14 @@ def collect_background_results() -> list[str]:
             f"  <command>{task['command']}</command>\n"
             f"  <summary>{summary}</summary>\n"
             f"</task_notification>")
-        print(f"  \033[32m[background done] {bg_id}: "
+        print(f"  \033[32m[后台完成] {bg_id}: "
               f"{task['command'][:40]} ({len(output)} 个字符)\033[0m")
     return notifications
 
 
 def has_pending_background() -> bool:
-    """Non-destructive: True if any background task has completed and is
-    waiting to be collected. The inbox poller uses this in its 唤醒 condition."""
+    """非破坏性检查:是否有后台任务已完成并等待收集。
+    收件箱轮询器会把它作为唤醒条件之一。"""
     with background_lock:
         return any(t["status"] == "completed" for t in background_tasks.values())
 
@@ -361,8 +366,8 @@ class CronJob:
     id: str
     cron: str        # "0 9 * * *"
     prompt: str      # 触发时要注入的消息
-    recurring: bool  # True = 重复,False = 一次性
-    durable: bool    # True = 持久化到磁盘
+    recurring: bool  # 为真 = 重复,为假 = 一次性
+    durable: bool    # 为真 = 持久化到磁盘
 
 
 scheduled_jobs: dict[str, CronJob] = {}
@@ -388,8 +393,8 @@ def _cron_field_matches(field: str, value: int) -> bool:
 
 
 def cron_matches(cron_expr: str, dt: datetime) -> bool:
-    """Check if a 5-field cron expression matches the given datetime.
-    Standard cron semantics: DOM and DOW use OR when both are constrained."""
+    """检查 5 字段 cron 表达式是否匹配给定时间。
+    标准 cron 语义:月内日和周内日同时受限时,两者使用 OR。"""
     fields = cron_expr.strip().split()
     if len(fields) != 5:
         return False
@@ -458,7 +463,7 @@ def validate_cron(cron_expr: str) -> str | None:
     if len(fields) != 5:
         return f"期望 5 个字段,实际得到 {len(fields)}"
     bounds = [(0, 59), (0, 23), (1, 31), (1, 12), (0, 6)]
-    names = ["minute", "hour", "day-of-month", "month", "day-of-week"]
+    names = ["分", "时", "月内日", "月", "周内日"]
     for i, (field, (lo, hi), name) in enumerate(zip(fields, bounds, names)):
         err = _validate_cron_field(field, lo, hi)
         if err:
@@ -482,18 +487,18 @@ def load_durable_jobs():
             job = CronJob(**j)
             err = validate_cron(job.cron)
             if err:
-                print(f"  \033[31m[cron] skipping invalid job {job.id}: {err}\033[0m")
+                print(f"  \033[31m[cron] 跳过无效任务 {job.id}: {err}\033[0m")
                 continue
             scheduled_jobs[job.id] = job
         valid = [j for j in jobs if j["id"] in scheduled_jobs]
         if valid:
-            print(f"  \033[35m[cron] loaded {len(valid)} durable job(s)\033[0m")
+            print(f"  \033[35m[cron] 已加载 {len(valid)} 个持久任务\033[0m")
     except Exception:
         pass
 
 
 def schedule_job(cron: str, prompt: str, recurring: bool = True,
-                 durable: bool = True) -> Cron任务 | str:
+                 durable: bool = True) -> CronJob | str:
     """注册一个新的 cron 任务。返回 CronJob 或错误字符串。"""
     err = validate_cron(cron)
     if err:
@@ -507,7 +512,7 @@ def schedule_job(cron: str, prompt: str, recurring: bool = True,
         scheduled_jobs[job.id] = job
     if durable:
         save_durable_jobs()
-    print(f"  \033[35m[cron register] {job.id} '{cron}' → {prompt[:40]}\033[0m")
+    print(f"  \033[35m[cron 注册] {job.id} '{cron}' → {prompt[:40]}\033[0m")
     return job
 
 
@@ -519,14 +524,13 @@ def cancel_job(job_id: str) -> str:
         return f"任务 {job_id} 未找到"
     if job.durable:
         save_durable_jobs()
-    print(f"  \033[31m[cron cancel] {job_id}\033[0m")
+    print(f"  \033[31m[cron 取消] {job_id}\033[0m")
     return f"已取消 {job_id}"
 
 
 def cron_scheduler_loop():
-    """Independent daemon thread: poll every 1s, fire matching jobs.
-    Individual job errors are caught to prevent one bad job from
-    killing the entire scheduler thread."""
+    """独立守护线程:每秒轮询一次并触发匹配任务。
+    单个任务出错会被捕获,避免整个调度线程退出。"""
     while True:
         time.sleep(1)
         now = datetime.now()
@@ -539,14 +543,14 @@ def cron_scheduler_loop():
                         if _last_fired.get(job.id) != minute_marker:
                             cron_queue.append(job)
                             _last_fired[job.id] = minute_marker
-                            print(f"  \033[35m[cron fire] {job.id} → "
+                            print(f"  \033[35m[cron 触发] {job.id} → "
                                   f"{job.prompt[:40]}\033[0m")
                         if not job.recurring:
                             scheduled_jobs.pop(job.id, None)
                             if job.durable:
                                 save_durable_jobs()
                 except Exception as e:
-                    print(f"  \033[31m[cron error] {job.id}: {e}\033[0m")
+                    print(f"  \033[31m[cron 错误] {job.id}: {e}\033[0m")
 
 
 def consume_cron_queue() -> list[CronJob]:
@@ -560,7 +564,7 @@ def consume_cron_queue() -> list[CronJob]:
 # 启动时加载持久任务,然后启动调度线程
 load_durable_jobs()
 threading.Thread(target=cron_scheduler_loop, daemon=True).start()
-print("  \033[35m[cron] scheduler thread started\033[0m")
+print("  \033[35m[cron] 调度线程已启动\033[0m")
 
 
 # Cron 工具处理器
@@ -580,8 +584,8 @@ def run_list_crons() -> str:
         return "暂无 cron 任务。请使用 schedule_cron 添加一个。"
     lines = []
     for j in jobs:
-        tag = "recurring" if j.recurring else "one-shot"
-        dur = "durable" if j.durable else "session"
+        tag = "重复" if j.recurring else "一次性"
+        dur = "持久化" if j.durable else "仅本会话"
         lines.append(f"  {j.id}: '{j.cron}' → {j.prompt[:40]} "
                      f"[{tag}, {dur}]")
     return "\n".join(lines)
@@ -600,9 +604,9 @@ 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."""
+    """基于文件的消息总线。每个 Agent 都有一个 .jsonl 收件箱。
+    读取是破坏性的:read_text + unlink 会消费消息。
+    教学版本不做文件锁;真实 CC 使用 proper-lockfile。"""
 
     def send(self, from_agent: str, to_agent: str, content: str,
              msg_type: str = "message"):
@@ -625,9 +629,8 @@ class MessageBus:
         return msgs
 
     def peek(self, agent: str) -> bool:
-        """Non-destructive: True if the agent has unread inbox messages.
-        The Lead's inbox poller uses this to decide whether to 唤醒 a turn
-        without consuming the mailbox."""
+        """非破坏性检查:Agent 是否有未读收件箱消息。
+        Lead 的收件箱轮询器用它决定是否唤醒一轮,同时不消费邮箱。"""
         inbox = MAILBOX_DIR / f"{agent}.jsonl"
         return inbox.exists() and inbox.stat().st_size > 0
 
@@ -641,10 +644,9 @@ active_teammates: dict[str, bool] = {}
 # ── 队友线程 (s15 新增) ──
 
 def spawn_teammate_thread(name: str, role: str, prompt: str) -> str:
-    """Spawn a teammate agent in a background thread.
-    Teaching version: max 10 rounds per teammate.
-    Real CC: teammates use idle loop (wait for inbox, work, repeat)
-    until shutdown_request."""
+    """在后台线程中启动队友 Agent。
+    教学版本中每个队友最多运行 10 轮。
+    真实 CC 的队友使用空闲循环:等待收件箱、工作、重复,直到收到 shutdown_request。"""
     if name in active_teammates:
         return f"队友 '{name}' 已存在"
 
@@ -678,7 +680,7 @@ def spawn_teammate_thread(name: str, role: str, prompt: str) -> str:
         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],
+                                                  "已发送")[1],
         }
 
         for _ in range(10):
@@ -718,11 +720,11 @@ def spawn_teammate_thread(name: str, role: str, prompt: str) -> str:
                 break
         BUS.send(name, "lead", summary, "result")
         active_teammates.pop(name, None)
-        print(f"  \033[32m[teammate] {name} finished\033[0m")
+        print(f"  \033[32m[队友] {name} 已完成\033[0m")
 
     active_teammates[name] = True
     threading.Thread(target=run, daemon=True).start()
-    print(f"  \033[36m[teammate] {name} spawned as {role}\033[0m")
+    print(f"  \033[36m[队友] {name} 已作为 {role} 启动\033[0m")
     return f"队友 '{name}' 已启动为 {role}"
 
 
@@ -803,9 +805,9 @@ TOOLS = [
                           "prompt": {"type": "string",
                                      "description": "触发时要注入的消息"},
                           "recurring": {"type": "boolean",
-                                        "description": "True=重复,False=一次性"},
+                                        "description": "为真表示重复,为假表示一次性"},
                           "durable": {"type": "boolean",
-                                      "description": "True=持久化到磁盘"}},
+                                      "description": "为真表示持久化到磁盘"}},
                       "required": ["cron", "prompt"]}},
     {"name": "list_crons",
      "description": "列出所有已注册的 cron 任务。",
@@ -840,7 +842,7 @@ TOOLS = [
 # ── 上下文 ──
 
 def update_context(context: dict, messages: list) -> dict:
-    """Derive context from real state."""
+    """从真实状态推导上下文。"""
     memories = ""
     if MEMORY_INDEX.exists():
         content = MEMORY_INDEX.read_text().strip()
@@ -865,8 +867,8 @@ def agent_loop(messages: list, context: dict):
         fired = consume_cron_queue()
         for job in fired:
             messages.append({"role": "user",
-                             "content": f"[Scheduled] {job.prompt}"})
-            print(f"  \033[35m[inject cron] {job.prompt[:50]}\033[0m")
+                             "content": f"[已调度] {job.prompt}"})
+            print(f"  \033[35m[注入 cron] {job.prompt[:50]}\033[0m")
 
         try:
             response = client.messages.create(
@@ -892,7 +894,7 @@ def agent_loop(messages: list, context: dict):
                 bg_id = start_background_task(block)
                 results.append({"type": "tool_result",
                                 "tool_use_id": block.id,
-                                "content": f"[Background task {bg_id} started] "
+                                "content": f"[后台任务 {bg_id} 已启动] "
                                            f"完成后结果将可用。"})
             else:
                 output = execute_tool(block)
@@ -957,15 +959,15 @@ if __name__ == "__main__":
             parts = []
             inbox = BUS.read_inbox("lead")
             if inbox:
-                parts.append("[Inbox]\n" + "\n".join(
+                parts.append("[收件箱]\n" + "\n".join(
                     f"来自 {m['from']}: {m['content'][:200]}" for m in inbox))
             bg = collect_background_results()
             parts.extend(bg)
             if not parts:
                 continue  # 已被更早的唤醒消费(幂等)
             history.append({"role": "user", "content": "\n".join(parts)})
-            print(f"\n\033[33m[唤醒: {len(inbox)} 收件箱 + {len(bg)} background "
-                  f"-> new turn]\033[0m")
+            print(f"\n\033[33m[唤醒: {len(inbox)} 条收件箱消息 + {len(bg)} 条后台通知 "
+                  f"-> 新一轮]\033[0m")
 
         # 为唤醒来源执行一轮。
         agent_loop(history, context)

+ 142 - 0
s15_agent_teams/代码讲解.md

@@ -0,0 +1,142 @@
+# S15 Agent Teams 代码讲解
+
+这一节只讲相对 S14 新增的内容:
+
+**Lead Agent 可以启动队友 Agent,并通过文件邮箱互相发消息。**
+
+---
+
+## 1. 本节新增内容
+
+- `MessageBus`:基于文件的消息总线。
+- `.mailboxes/`:每个 Agent 的收件箱目录。
+- `active_teammates`:记录正在运行的队友。
+- `spawn_teammate_thread()`:在后台线程中启动队友 Agent。
+- `run_spawn_teammate()`:Lead 启动队友的工具处理器。
+- `run_send_message()`:Lead 给队友发消息。
+- `run_check_inbox()`:Lead 检查收件箱。
+- 3 个新工具:`spawn_teammate`、`send_message`、`check_inbox`。
+
+---
+
+## 2. MessageBus 数据结构
+
+消息保存成 JSONL,每行是一条消息。
+
+单条消息结构:
+
+```python
+{
+    "from": "lead",
+    "to": "worker1",
+    "content": "请检查这个文件",
+    "type": "message",
+    "ts": 1720000000.0
+}
+```
+
+文件路径:
+
+```text
+.mailboxes/
+  lead.jsonl
+  worker1.jsonl
+```
+
+每个 Agent 有自己的收件箱。
+
+---
+
+## 3. `MessageBus.send()`
+
+作用:把消息追加到目标 Agent 的邮箱文件。
+
+```python
+BUS.send("lead", "worker1", "请检查测试")
+```
+
+会写入:
+
+```text
+.mailboxes/worker1.jsonl
+```
+
+---
+
+## 4. `MessageBus.read_inbox()`
+
+作用:读取某个 Agent 的收件箱。
+
+教学版采用“读完即删除”:
+
+```python
+read_text()
+unlink()
+```
+
+这表示消息被消费了。
+
+---
+
+## 5. `spawn_teammate_thread(name, role, prompt)`
+
+作用:启动一个队友 Agent。
+
+队友有自己的:
+
+```python
+messages: list[dict]
+system: str
+sub_tools: list[dict]
+sub_handlers: dict[str, function]
+```
+
+队友的工具包括:
+
+```text
+bash
+read_file
+write_file
+send_message
+```
+
+重点是:队友可以通过 `send_message` 把结果发回 Lead。
+
+---
+
+## 6. Lead 工具
+
+Lead 新增三个团队工具:
+
+```python
+spawn_teammate(name, role, prompt)
+send_message(to, content)
+check_inbox()
+```
+
+模型调用示例:
+
+```python
+{
+    "name": "spawn_teammate",
+    "input": {
+        "name": "reviewer",
+        "role": "代码审查员",
+        "prompt": "请检查当前改动"
+    }
+}
+```
+
+---
+
+## 7. 本节课堂重点
+
+Agent Team 的本质是:
+
+```python
+多个 Agent 各自有独立 messages
+通过 MessageBus 交换信息
+Lead 负责启动、收消息、继续决策
+```
+
+这不是简单的子 Agent 摘要返回,而是更接近多 Agent 协作。

+ 59 - 59
s16_team_protocols/code.py

@@ -100,8 +100,7 @@ def get_task(task_id: str) -> str:
 
 
 def can_start(task_id: str) -> bool:
-    """Check if all blockedBy dependencies are completed.
-    Missing dependencies are treated as blocked."""
+    """检查所有 blockedBy 依赖是否已完成;缺失依赖视为阻塞。"""
     task = load_task(task_id)
     for dep_id in task.blockedBy:
         if not _task_path(dep_id).exists():
@@ -114,31 +113,35 @@ def can_start(task_id: str) -> bool:
 def claim_task(task_id: str, owner: str = "agent") -> str:
     task = load_task(task_id)
     if task.status != "pending":
-        return f"任务 {task_id} 当前状态为 {task.status},无法认领"
+        status = {"pending": "待处理", "in_progress": "进行中",
+                  "completed": "已完成"}.get(task.status, task.status)
+        return f"任务 {task_id} 当前状态为 {status},无法认领"
     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}"
+        return f"被阻塞于:{deps}"
     task.owner = owner
     task.status = "in_progress"
     save_task(task)
-    print(f"  \033[36m[claim] {task.subject} → in_progress (owner: {owner})\033[0m")
+    print(f"  \033[36m[认领] {task.subject} → in_progress(负责人:{owner})\033[0m")
     return f"已认领 {task.id} ({task.subject})"
 
 
 def complete_task(task_id: str) -> str:
     task = load_task(task_id)
     if task.status != "in_progress":
-        return f"任务 {task_id} 当前状态为 {task.status},无法完成"
+        status = {"pending": "待处理", "in_progress": "进行中",
+                  "completed": "已完成"}.get(task.status, task.status)
+        return f"任务 {task_id} 当前状态为 {status},无法完成"
     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")
+    print(f"  \033[32m[完成] {task.subject} ✓\033[0m")
     msg = f"已完成 {task.id} ({task.subject})"
     if unblocked:
         msg += f"\n已解除阻塞:{', '.join(unblocked)}"
-        print(f"  \033[33m[unblocked] {', '.join(unblocked)}\033[0m")
+        print(f"  \033[33m[已解除阻塞] {', '.join(unblocked)}\033[0m")
     return msg
 
 
@@ -161,7 +164,7 @@ def assemble_system_prompt(context: dict) -> str:
                 PROMPT_SECTIONS["workspace"]]
     memories = context.get("memories", "")
     if memories:
-        sections.append(f"Relevant memories:\n{memories}")
+        sections.append(f"相关记忆:\n{memories}")
     return "\n\n".join(sections)
 
 
@@ -223,8 +226,8 @@ def run_write(path: str, content: str) -> str:
 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")
+    deps = f"(依赖:{', '.join(blockedBy)})" if blockedBy else ""
+    print(f"  \033[34m[创建] {task.subject}{deps}\033[0m")
     return f"已创建 {task.id}: {task.subject}{deps}"
 
 
@@ -236,10 +239,12 @@ def run_list_tasks() -> str:
     for t in 个任务:
         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 ""
+        deps = f"(依赖:{', '.join(t.blockedBy)})" if t.blockedBy else ""
+        owner = f" [负责人:{t.owner}]" if t.owner else ""
+        status = {"pending": "待处理", "in_progress": "进行中",
+                  "completed": "已完成"}.get(t.status, t.status)
         lines.append(f"  {icon} {t.id}: {t.subject} "
-                     f"[{t.status}]{owner}{deps}")
+                     f"[{status}]{owner}{deps}")
     return "\n".join(lines)
 
 
@@ -304,7 +309,7 @@ def start_background_task(block) -> str:
             "status": "running",
         }
     threading.Thread(target=worker, daemon=True).start()
-    print(f"  \033[33m[background] dispatched {bg_id}: {cmd[:40]}\033[0m")
+    print(f"  \033[33m[后台] 已分发 {bg_id}: {cmd[:40]}\033[0m")
     return bg_id
 
 
@@ -326,7 +331,7 @@ def collect_background_results() -> list[str]:
             f"  <command>{task['command']}</command>\n"
             f"  <summary>{summary}</summary>\n"
             f"</task_notification>")
-        print(f"  \033[32m[background done] {bg_id}: "
+        print(f"  \033[32m[后台完成] {bg_id}: "
               f"{task['command'][:40]} ({len(output)} 个字符)\033[0m")
     return notifications
 
@@ -338,9 +343,9 @@ 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."""
+    """基于文件的消息总线。每个 Agent 都有一个 .jsonl 收件箱。
+    读取是破坏性的:read_text + unlink 会消费消息。
+    教学版本不做文件锁;真实 CC 使用 proper-lockfile。"""
 
     def send(self, from_agent: str, to_agent: str, content: str,
              msg_type: str = "message", metadata: dict = None):
@@ -387,29 +392,28 @@ def new_request_id() -> str:
 
 
 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."""
+    """通过 request_id 将响应关联到原始请求,并校验响应类型是否匹配。"""
     state = pending_requests.get(request_id)
     if not state:
-        print(f"  \033[31m[protocol] unknown request_id: {request_id}\033[0m")
+        print(f"  \033[31m[协议] 未知 request_id: {request_id}\033[0m")
         return
     # 校验响应类型是否匹配请求类型
     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")
+        print(f"  \033[31m[协议] 类型不匹配:期望 shutdown_response,"
+              f"实际得到 {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")
+        print(f"  \033[31m[协议] 类型不匹配:期望 plan_approval_response,"
+              f"实际得到 {response_type}\033[0m")
         return
     if state.status != "pending":
-        print(f"  \033[33m[protocol] {request_id} already {state.status}, "
-              f"ignoring duplicate\033[0m")
+        print(f"  \033[33m[协议] {request_id} 已经是 {state.status},"
+              f"忽略重复响应\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} "
+    print(f"  \033[{color}m[协议] {state.type} {icon} "
           f"({request_id}: {state.status})\033[0m")
 
 
@@ -418,9 +422,8 @@ def match_response(response_type: str, request_id: str, approve: bool):
 # 返回前通过 match_response 路由协议响应。
 
 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."""
+    """读取 Lead 收件箱,路由协议响应,并返回所有消息。
+    run_check_inbox() 和主循环都会调用,避免消息被消费却没有经过协议路由。"""
     msgs = BUS.read_inbox("lead")
     if not msgs:
         return []
@@ -438,9 +441,9 @@ def consume_lead_inbox(route_protocol: bool = True) -> list[dict]:
 # ── 队友线程 (s16: 空闲循环 + 分发) ──
 
 def spawn_teammate_thread(name: str, role: str, prompt: str) -> str:
-    """Spawn a teammate agent in a background thread.
-    Uses idle loop: 等待 each LLM 轮次, waits for inbox messages
-    (shutdown_request, new task) instead of exiting."""
+    """在后台线程中启动队友 Agent。
+    队友使用空闲循环:每轮 LLM 后等待收件箱消息
+    (shutdown_request 或新任务),而不是直接退出。"""
     if name in active_teammates:
         return f"队友 '{name}' 已存在"
 
@@ -449,8 +452,7 @@ def spawn_teammate_thread(name: str, role: str, prompt: str) -> str:
               f"检查收件箱中的协议消息(shutdown_request 等)。")
 
     def handle_inbox_message(name: str, msg: dict, messages: list) -> bool:
-        """Dispatch incoming protocol messages by type.
-        Returns True if teammate should stop."""
+        """按类型分发收到的协议消息。返回 True 表示队友应停止。"""
         msg_type = msg.get("type", "message")
         meta = msg.get("metadata", {})
         req_id = meta.get("request_id", "")
@@ -459,7 +461,7 @@ def spawn_teammate_thread(name: str, role: str, prompt: str) -> str:
             BUS.send(name, "lead", "正在平滑关闭。",
                      "shutdown_response",
                      {"request_id": req_id, "approve": True})
-            print(f"  \033[35m[protocol] {name} approved shutdown "
+            print(f"  \033[35m[协议] {name} 已批准关闭 "
                   f"({req_id})\033[0m")
             return True  # 停止循环
 
@@ -491,7 +493,7 @@ def spawn_teammate_thread(name: str, role: str, prompt: str) -> str:
                                              "content": {"type": "string"}},
                               "required": ["path", "content"]}},
             {"name": "send_message",
-             "description": "Send message to another agent.",
+             "description": "向另一个 Agent 发送消息。",
              "input_schema": {"type": "object",
                               "properties": {"to": {"type": "string"},
                                              "content": {"type": "string"}},
@@ -505,7 +507,7 @@ def spawn_teammate_thread(name: str, role: str, prompt: str) -> str:
         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],
+                                                  "已发送")[1],
             "submit_plan": lambda plan: _teammate_submit_plan(name, plan),
         }
 
@@ -587,23 +589,21 @@ def spawn_teammate_thread(name: str, role: str, prompt: str) -> str:
                 break
         BUS.send(name, "lead", summary, "result")
         active_teammates.pop(name, None)
-        print(f"  \033[32m[teammate] {name} finished\033[0m")
+        print(f"  \033[32m[队友] {name} 已完成\033[0m")
 
     active_teammates[name] = True
     threading.Thread(target=run, daemon=True).start()
-    print(f"  \033[36m[teammate] {name} spawned as {role}\033[0m")
+    print(f"  \033[36m[队友] {name} 已作为 {role} 启动\033[0m")
     return f"队友 '{name}' 已启动为 {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 继续s 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.
+    """队友向 Lead 提交计划以供审批。
+
+    注意:这是协议层请求,不是代码层闸门。
+    提交后,队友线程仍会继续运行,仍然可以调用 bash/write 等工具。
+    真实约束依赖模型在行动前等待审批响应。
+    若要做代码层工具闸门,需要在审批到达前阻塞队友的工具分发。
     """
     req_id = new_request_id()
     pending_requests[req_id] = ProtocolState(
@@ -616,7 +616,7 @@ def _teammate_submit_plan(from_name: str, plan: str) -> str:
     return f"计划已提交({req_id})。正在等待审批..."
 
 
-# ── Lead Protocol 工具 (s16 新增) ──
+# ── Lead 协议工具 (s16 新增) ──
 
 def run_request_shutdown(teammate: str) -> str:
     req_id = new_request_id()
@@ -627,13 +627,13 @@ def run_request_shutdown(teammate: str) -> str:
     BUS.send("lead", teammate, "请平滑关闭。",
              "shutdown_request",
              {"request_id": req_id})
-    print(f"  \033[35m[protocol] shutdown_request → {teammate} "
+    print(f"  \033[35m[协议] shutdown_request → {teammate} "
           f"({req_id})\033[0m")
     return f"已向 {teammate} 发送关闭请求(req: {req_id})"
 
 
 def run_request_plan(teammate: str, task: str) -> str:
-    """Lead asks a teammate to submit a plan for a task."""
+    """Lead 要求队友为指定任务提交计划。"""
     BUS.send("lead", teammate, f"请为以下任务提交计划:{task}",
              "message")
     return f"已要求 {teammate} 提交计划"
@@ -646,11 +646,11 @@ def run_review_plan(request_id: str, approve: bool, feedback: str = "") -> str:
     if state.status != "pending":
         return f"请求 {request_id} 已经是 {state.status}"
     state.status = "approved" if approve else "rejected"
-    BUS.send("lead", state.sender, feedback or ("Approved" if approve else "Rejected"),
+    BUS.send("lead", state.sender, feedback or ("已批准" if approve else "已拒绝"),
              "plan_approval_response",
              {"request_id": request_id, "approve": approve})
     icon = "✓" if approve else "✗"
-    print(f"  \033[32m[protocol] plan {icon} ({request_id})\033[0m")
+    print(f"  \033[32m[协议] 计划 {icon} ({request_id})\033[0m")
     return f"计划已{'批准' if approve else '拒绝'}({request_id})"
 
 
@@ -754,7 +754,7 @@ TOOLS = [
                           "prompt": {"type": "string"}},
                       "required": ["name", "role", "prompt"]}},
     {"name": "send_message",
-     "description": "Send message to a teammate via MessageBus.",
+     "description": "通过 MessageBus 向队友发送消息。",
      "input_schema": {"type": "object",
                       "properties": {"to": {"type": "string"},
                                      "content": {"type": "string"}},
@@ -788,7 +788,7 @@ TOOLS = [
 # ── 上下文 ──
 
 def update_context(context: dict, messages: list) -> dict:
-    """Derive context from real state."""
+    """从真实状态推导上下文。"""
     memories = ""
     if MEMORY_INDEX.exists():
         content = MEMORY_INDEX.read_text().strip()
@@ -830,7 +830,7 @@ def agent_loop(messages: list, context: dict):
                 bg_id = start_background_task(block)
                 results.append({"type": "tool_result",
                                 "tool_use_id": block.id,
-                                "content": f"[Background task {bg_id} started] "
+                                "content": f"[后台任务 {bg_id} 已启动] "
                                            f"完成后结果将可用。"})
             else:
                 output = execute_tool(block)
@@ -878,5 +878,5 @@ if __name__ == "__main__":
                 f"来自 {m['from']}: {m['content'][:200]}" for m in inbox_msgs)
             history.append({"role": "user",
                             "content": f"[收件箱]\n{inbox_text}"})
-            print(f"\n\033[33m[Inbox: {len(inbox_msgs)} 条消息已注入]\033[0m")
+            print(f"\n\033[33m[收件箱:已注入 {len(inbox_msgs)} 条消息]\033[0m")
         print()

+ 200 - 0
s16_team_protocols/代码讲解.md

@@ -0,0 +1,200 @@
+# S16 Team Protocols 代码讲解
+
+这一节只讲相对 S15 新增的内容:
+
+**团队消息不只是普通聊天,还需要 request_id、类型和状态机来保证请求与响应能对上。**
+
+---
+
+## 1. 本节新增内容
+
+- `ProtocolState`:协议请求状态。
+- `pending_requests`:等待响应的请求表。
+- `new_request_id()`:生成请求 ID。
+- `match_response()`:把响应关联回原始请求。
+- `consume_lead_inbox()`:统一消费 Lead 收件箱并路由协议消息。
+- `submit_plan`:队友提交计划。
+- `request_shutdown`:Lead 请求队友关闭。
+- `request_plan`:Lead 要求队友提交计划。
+- `review_plan`:Lead 审批队友计划。
+
+---
+
+## 2. 协议消息结构
+
+S15 的消息只有普通字段:
+
+```python
+{
+    "from": "lead",
+    "to": "worker",
+    "content": "你好",
+    "type": "message",
+    "ts": 1720000000.0
+}
+```
+
+S16 增加了 `metadata`:
+
+```python
+{
+    "from": "worker",
+    "to": "lead",
+    "content": "这是我的计划",
+    "type": "plan_approval_request",
+    "ts": 1720000000.0,
+    "metadata": {
+        "request_id": "req_123456"
+    }
+}
+```
+
+`request_id` 是协议的关键。
+
+---
+
+## 3. `ProtocolState`
+
+`ProtocolState` 是:
+
+```python
+@dataclass
+class ProtocolState:
+    request_id: str
+    type: str
+    sender: str
+    target: str
+    status: str
+    payload: str
+    created_at: float
+```
+
+一条请求状态大概是:
+
+```python
+{
+    "request_id": "req_123456",
+    "type": "plan_approval",
+    "sender": "worker",
+    "target": "lead",
+    "status": "pending",
+    "payload": "计划内容",
+    "created_at": 1720000000.0
+}
+```
+
+---
+
+## 4. `pending_requests`
+
+`pending_requests` 是:
+
+```python
+pending_requests: dict[str, ProtocolState]
+```
+
+结构:
+
+```python
+{
+    "req_123456": ProtocolState(...)
+}
+```
+
+它用来记住:
+
+```text
+哪个请求还在等待响应
+这个请求是谁发的
+目标是谁
+现在是 pending / approved / rejected
+```
+
+---
+
+## 5. `match_response()`
+
+作用:收到响应后,根据 `request_id` 找回原始请求。
+
+流程:
+
+```python
+从 pending_requests 找 request_id
+检查响应类型是否匹配
+检查请求是否仍是 pending
+更新状态为 approved 或 rejected
+```
+
+如果没有 `request_id`,Lead 就不知道这个响应对应哪一个请求。
+
+---
+
+## 6. 计划审批流程
+
+队友提交计划:
+
+```python
+submit_plan(plan)
+```
+
+内部会:
+
+```python
+生成 request_id
+写入 pending_requests
+发送 plan_approval_request 给 lead
+```
+
+Lead 审批:
+
+```python
+review_plan(request_id, approve=True, feedback="")
+```
+
+内部会:
+
+```python
+更新 pending_requests[request_id].status
+发送 plan_approval_response 给队友
+```
+
+---
+
+## 7. 平滑关闭流程
+
+Lead 发起:
+
+```python
+request_shutdown(teammate)
+```
+
+队友收到:
+
+```python
+shutdown_request
+```
+
+队友回复:
+
+```python
+shutdown_response
+```
+
+Lead 通过 `request_id` 把响应匹配回原请求。
+
+---
+
+## 8. 本节课堂重点
+
+多 Agent 协作不能只靠自然语言消息。
+
+需要协议层:
+
+```python
+message type 区分消息种类
+request_id 关联请求和响应
+pending_requests 记录状态
+match_response 更新状态机
+```
+
+这就是从“聊天协作”走向“可控协作”的关键。

+ 45 - 36
s17_autonomous_agents/code.py

@@ -104,7 +104,9 @@ def can_start(task_id: str) -> bool:
 def claim_task(task_id: str, owner: str = "agent") -> str:
     task = load_task(task_id)
     if task.status != "pending":
-        return f"任务 {task_id} 当前状态为 {task.status},无法认领"
+        status = {"pending": "待处理", "in_progress": "进行中",
+                  "completed": "已完成"}.get(task.status, task.status)
+        return f"任务 {task_id} 当前状态为 {status},无法认领"
     if task.owner:
         return f"任务 {task_id} 已由 {task.owner} 负责"
     if not can_start(task_id):
@@ -118,19 +120,21 @@ def claim_task(task_id: str, owner: str = "agent") -> str:
     task.owner = owner
     task.status = "in_progress"
     save_task(task)
-    print(f"  \033[36m[claim] {task.subject} → in_progress\033[0m")
+    print(f"  \033[36m[认领] {task.subject} → in_progress\033[0m")
     return f"已认领 {task.id} ({task.subject})"
 
 
 def complete_task(task_id: str) -> str:
     task = load_task(task_id)
     if task.status != "in_progress":
-        return f"任务 {task_id} 当前状态为 {task.status},无法完成"
+        status = {"pending": "待处理", "in_progress": "进行中",
+                  "completed": "已完成"}.get(task.status, task.status)
+        return f"任务 {task_id} 当前状态为 {status},无法完成"
     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")
+    print(f"  \033[32m[完成] {task.subject} ✓\033[0m")
     msg = f"已完成 {task.id} ({task.subject})"
     if unblocked:
         msg += f"\n已解除阻塞:{', '.join(unblocked)}"
@@ -155,7 +159,7 @@ def assemble_system_prompt(context: dict) -> str:
                 PROMPT_SECTIONS["tools"],
                 PROMPT_SECTIONS["workspace"]]
     if context.get("memories"):
-        sections.append(f"Relevant memories:\n{context['memories']}")
+        sections.append(f"相关记忆:\n{context['memories']}")
     return "\n\n".join(sections)
 
 
@@ -266,20 +270,20 @@ def match_response(response_type: str, request_id: str, approve: bool):
     """通过 request_id 将响应关联到原始请求。"""
     state = pending_requests.get(request_id)
     if not state:
-        print(f"  \033[31m[protocol] unknown request_id: {request_id}\033[0m")
+        print(f"  \033[31m[协议] 未知 request_id: {request_id}\033[0m")
         return
     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")
+        print(f"  \033[31m[协议] 类型不匹配:期望 shutdown_response,"
+              f"实际得到 {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")
+        print(f"  \033[31m[协议] 类型不匹配:期望 plan_approval_response,"
+              f"实际得到 {response_type}\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} "
+    print(f"  \033[{color}m[协议] {state.type} {icon} "
           f"({request_id}: {state.status})\033[0m")
 
 
@@ -316,14 +320,14 @@ def idle_poll(name: str, messages: list, role: str) -> str:
                     BUS.send(name, "lead", "正在平滑关闭。",
                              "shutdown_response",
                              {"request_id": req_id, "approve": True})
-                    print(f"  \033[35m[protocol] {name} approved shutdown "
-                          f"in idle ({req_id})\033[0m")
+                    print(f"  \033[35m[协议] {name} 在空闲阶段已批准关闭 "
+                          f"({req_id})\033[0m")
                     return "shutdown"
 
             # 非协议收件箱:注入并恢复工作
             messages.append({"role": "user",
                 "content": "<inbox>" + json.dumps(inbox) + "</inbox>"})
-            print(f"  \033[36m[idle] {name} found inbox messages\033[0m")
+            print(f"  \033[36m[空闲] {name} 发现收件箱消息\033[0m")
             return "work"
 
         # 扫描任务板
@@ -331,21 +335,21 @@ def idle_poll(name: str, messages: list, role: str) -> str:
         if unclaimed:
             task = unclaimed[0]
             result = claim_task(task["id"], name)
-            if "Claimed" in result:
+            if "已认领" in result:
                 messages.append({"role": "user",
                     "content": f"<auto-claimed>任务 {task['id']}: "
                                f"{task['subject']}</auto-claimed>"})
-                print(f"  \033[32m[idle] {name} auto-claimed: "
+                print(f"  \033[32m[空闲] {name} 自动认领:"
                       f"{task['subject']}\033[0m")
                 return "work"
-            print(f"  \033[33m[idle] {name} claim failed: "
+            print(f"  \033[33m[空闲] {name} 认领失败:"
                   f"{result}\033[0m")
 
-    print(f"  \033[31m[idle] {name} timeout ({IDLE_TIMEOUT}s)\033[0m")
+    print(f"  \033[31m[空闲] {name} 超时({IDLE_TIMEOUT}s)\033[0m")
     return "timeout"
 
 
-# ── 队友线程 (from s15 + s16 + s17) ──
+# ── 队友线程(来自 s15 + s16 + s17) ──
 
 def spawn_teammate_thread(name: str, role: str, prompt: str) -> str:
     if name in active_teammates:
@@ -354,10 +358,10 @@ def spawn_teammate_thread(name: str, role: str, prompt: str) -> str:
     system = (f"你是 '{name}',角色是 {role}。"
               f"使用工具完成任务。"
               f"你可以从任务板列出并认领任务。"
-              f"检查收件箱中的协议消息.")
+              f"检查收件箱中的协议消息")
 
     def handle_inbox_message(name: str, msg: dict, messages: list):
-        """Dispatch incoming protocol messages by type."""
+        """按类型分发收到的协议消息。"""
         msg_type = msg.get("type", "message")
         meta = msg.get("metadata", {})
         req_id = meta.get("request_id", "")
@@ -366,7 +370,7 @@ def spawn_teammate_thread(name: str, role: str, prompt: str) -> str:
             BUS.send(name, "lead", "正在平滑关闭。",
                      "shutdown_response",
                      {"request_id": req_id, "approve": True})
-            print(f"  \033[35m[protocol] {name} approved shutdown "
+            print(f"  \033[35m[协议] {name} 已批准关闭 "
                   f"({req_id})\033[0m")
             return True
 
@@ -397,7 +401,7 @@ def spawn_teammate_thread(name: str, role: str, prompt: str) -> str:
                                              "content": {"type": "string"}},
                               "required": ["path", "content"]}},
             {"name": "send_message",
-             "description": "Send message to another agent.",
+             "description": "向另一个 Agent 发送消息。",
              "input_schema": {"type": "object",
                               "properties": {"to": {"type": "string"},
                                              "content": {"type": "string"}},
@@ -427,9 +431,11 @@ def spawn_teammate_thread(name: str, role: str, prompt: str) -> str:
         def _run_list_tasks():
             个任务 = list_tasks()
             if not 个任务:
-                return "No 个任务."
+                return "暂无任务。"
+            status_labels = {"pending": "待处理", "in_progress": "进行中",
+                             "completed": "已完成"}
             return "\n".join(
-                f"  {t.id}: {t.subject} [{t.status}]"
+                f"  {t.id}: {t.subject} [{status_labels.get(t.status, t.status)}]"
                 for t in 个任务)
 
         def _run_claim_task(task_id: str):
@@ -441,7 +447,7 @@ def spawn_teammate_thread(name: str, role: str, prompt: str) -> str:
         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],
+                                                  "已发送")[1],
             "submit_plan": lambda plan: _teammate_submit_plan(name, plan),
             "list_tasks": _run_list_tasks,
             "claim_task": _run_claim_task,
@@ -516,11 +522,11 @@ def spawn_teammate_thread(name: str, role: str, prompt: str) -> str:
                 break
         BUS.send(name, "lead", summary, "result")
         active_teammates.pop(name, None)
-        print(f"  \033[32m[teammate] {name} finished\033[0m")
+        print(f"  \033[32m[队友] {name} 已完成\033[0m")
 
     active_teammates[name] = True
     threading.Thread(target=run, daemon=True).start()
-    print(f"  \033[36m[teammate] {name} spawned as {role}\033[0m")
+    print(f"  \033[36m[队友] {name} 已作为 {role} 启动\033[0m")
     return f"队友 '{name}' 已启动为 {role}(自主模式)"
 
 
@@ -537,7 +543,7 @@ def _teammate_submit_plan(from_name: str, plan: str) -> str:
     return f"计划已提交({req_id})。正在等待审批..."
 
 
-# ── Lead Protocol 工具 (来自 s16) ──
+# ── Lead 协议工具 (来自 s16) ──
 
 def run_request_shutdown(teammate: str) -> str:
     req_id = new_request_id()
@@ -548,7 +554,7 @@ def run_request_shutdown(teammate: str) -> str:
     BUS.send("lead", teammate, "请平滑关闭。",
              "shutdown_request",
              {"request_id": req_id})
-    print(f"  \033[35m[protocol] shutdown_request → {teammate} "
+    print(f"  \033[35m[协议] shutdown_request → {teammate} "
           f"({req_id})\033[0m")
     return f"已向 {teammate} 发送关闭请求(req: {req_id})"
 
@@ -569,11 +575,11 @@ def run_review_plan(request_id: str, approve: bool,
         return f"请求 {request_id} 已经是 {state.status}"
     state.status = "approved" if approve else "rejected"
     BUS.send("lead", state.sender,
-             feedback or ("Approved" if approve else "Rejected"),
+             feedback or ("已批准" if approve else "已拒绝"),
              "plan_approval_response",
              {"request_id": request_id, "approve": approve})
     icon = "✓" if approve else "✗"
-    print(f"  \033[32m[protocol] plan {icon} ({request_id})\033[0m")
+    print(f"  \033[32m[协议] 计划 {icon} ({request_id})\033[0m")
     return f"计划已{'批准' if approve else '拒绝'}({request_id})"
 
 
@@ -582,17 +588,20 @@ def run_review_plan(request_id: str, approve: bool,
 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")
+    deps = f"(依赖:{', '.join(blockedBy)})" if blockedBy else ""
+    print(f"  \033[34m[创建] {task.subject}{deps}\033[0m")
     return f"已创建 {task.id}: {task.subject}{deps}"
 
 
 def run_list_tasks() -> str:
     个任务 = list_tasks()
     if not 个任务:
-        return "No 个任务."
+        return "暂无任务。"
+    status_labels = {"pending": "待处理", "in_progress": "进行中",
+                     "completed": "已完成"}
     return "\n".join(
-        f"  {t.id}: {t.subject} [{t.status}]"
+        f"  {t.id}: {t.subject} "
+        f"[{status_labels.get(t.status, t.status)}]"
         for t in 个任务)
 
 

+ 165 - 0
s17_autonomous_agents/代码讲解.md

@@ -0,0 +1,165 @@
+# S17 Autonomous Agents 代码讲解
+
+这一节只讲相对 S16 新增的内容:
+
+**队友 Agent 不再只等 Lead 指令,而是进入 IDLE 状态后可以自己轮询任务板并自动认领任务。**
+
+---
+
+## 1. 本节新增内容
+
+- `scan_unclaimed_tasks()`:扫描可认领任务。
+- `idle_poll()`:队友空闲时轮询收件箱和任务板。
+- 队友生命周期:`WORK -> IDLE -> WORK / SHUTDOWN`。
+- 队友新增任务工具:`list_tasks`、`claim_task`、`complete_task`。
+- 身份重新注入:压缩或历史变短后,让队友记住自己是谁。
+- `consume_lead_inbox()`:Lead 统一处理协议响应和普通消息。
+
+---
+
+## 2. 生命周期
+
+队友 Agent 不再是“跑 10 轮就结束”。
+
+它变成:
+
+```text
+WORK
+  ↓ 没有 tool_use / 当前任务结束
+IDLE
+  ↓ 有收件箱消息
+WORK
+  ↓ 有可认领任务
+WORK
+  ↓ 收到 shutdown 或超时
+SHUTDOWN
+```
+
+---
+
+## 3. `scan_unclaimed_tasks()`
+
+作用:从 `.tasks/` 里找可以自动认领的任务。
+
+筛选条件:
+
+```python
+status == "pending"
+owner 为空
+can_start(task["id"]) 为 True
+```
+
+返回结构:
+
+```python
+list[dict]
+```
+
+例如:
+
+```python
+[
+    {
+        "id": "task_001",
+        "subject": "补充测试",
+        "status": "pending",
+        "owner": None,
+        "blockedBy": []
+    }
+]
+```
+
+---
+
+## 4. `idle_poll(name, messages, role)`
+
+作用:队友空闲时做轮询。
+
+每隔几秒检查:
+
+```python
+1. 收件箱有没有消息
+2. 有没有 shutdown_request
+3. 任务板上有没有可认领任务
+```
+
+返回值是:
+
+```python
+"work"      # 有新消息或认领到任务,回到工作状态
+"shutdown"  # 收到关闭请求
+"timeout"   # 空闲太久,结束
+```
+
+---
+
+## 5. 队友新增任务工具
+
+S17 中,队友也能操作任务板:
+
+```text
+list_tasks
+claim_task
+complete_task
+```
+
+这意味着 Lead 不一定要明确分配任务。
+
+队友可以:
+
+```python
+查看任务板
+认领未分配任务
+完成后标记 completed
+```
+
+---
+
+## 6. 身份重新注入
+
+代码里有一段:
+
+```python
+if len(messages) <= 3:
+    messages.insert(0, {
+        "role": "user",
+        "content": "<identity>你是 ...</identity>"
+    })
+```
+
+它的作用是:当上下文很短或被压缩后,重新提醒队友自己的身份和角色。
+
+对于长期运行 Agent,这很重要。
+
+---
+
+## 7. Lead 的收件箱消费
+
+`consume_lead_inbox()` 会:
+
+```python
+读取 lead 的收件箱
+如果是协议响应,就调用 match_response()
+返回所有消息,供 Agent 注入上下文
+```
+
+这样 Lead 不会漏掉队友的计划审批、关闭确认、普通结果消息。
+
+---
+
+## 8. 本节课堂重点
+
+Autonomous Agent 的关键不是“模型自己活了”,而是 Harness 加了生命周期:
+
+```python
+WORK:执行当前任务
+IDLE:轮询消息和任务板
+AUTO-CLAIM:发现任务后自动认领
+SHUTDOWN:收到协议或超时后退出
+```
+
+所谓自主性,本质上是:
+
+```python
+循环 + 状态机 + 任务板 + 收件箱 + 工具权限
+```

+ 0 - 0
skills/agent-builder/SKill.md


+ 30 - 30
skills/agent-builder/references/minimal-agent.py

@@ -1,14 +1,14 @@
 #!/usr/bin/env python3
 """
-Minimal Agent Template - Copy and customize this.
+最小 Agent 模板 - 可复制后按需定制。
 
-This is the simplest possible working agent (~80 lines).
-It has everything you need: 3 tools + loop.
+这是最简单的可运行 Agent(约 80 行)。
+它包含最基本的三件东西:3 个工具 + 循环。
 
-Usage:
-    1. Set ANTHROPIC_API_KEY environment variable
+用法:
+    1. 设置 ANTHROPIC_API_KEY 环境变量
     2. python minimal-agent.py
-    3. Type commands, 'q' to quit
+    3. 输入任务,输入 q 退出
 """
 
 from anthropic import Anthropic
@@ -16,24 +16,24 @@ from pathlib import Path
 import subprocess
 import os
 
-# Configuration
+# 配置
 client = Anthropic(api_key=os.getenv("ANTHROPIC_API_KEY"))
 MODEL = os.getenv("MODEL_NAME", "claude-sonnet-4-20250514")
 WORKDIR = Path.cwd()
 
-# System prompt - keep it simple
-SYSTEM = f"""You are a coding agent at {WORKDIR}.
+# 系统提示词:保持简单
+SYSTEM = f"""你是位于 {WORKDIR} 的编码 Agent。
 
-Rules:
-- Use tools to complete tasks
-- Prefer action over explanation
-- Summarize what you did when done"""
+规则:
+- 使用工具完成任务
+- 优先行动,而不是只解释
+- 完成后总结你做了什么"""
 
-# Minimal tool set - add more as needed
+# 最小工具集:可按需继续添加
 TOOLS = [
     {
         "name": "bash",
-        "description": "Run shell command",
+        "description": "运行 shell 命令。",
         "input_schema": {
             "type": "object",
             "properties": {"command": {"type": "string"}},
@@ -42,7 +42,7 @@ TOOLS = [
     },
     {
         "name": "read_file",
-        "description": "Read file contents",
+        "description": "读取文件内容。",
         "input_schema": {
             "type": "object",
             "properties": {"path": {"type": "string"}},
@@ -51,7 +51,7 @@ TOOLS = [
     },
     {
         "name": "write_file",
-        "description": "Write content to file",
+        "description": "向文件写入内容。",
         "input_schema": {
             "type": "object",
             "properties": {
@@ -65,37 +65,37 @@ TOOLS = [
 
 
 def execute_tool(name: str, args: dict) -> str:
-    """Execute a tool and return result."""
+    """执行工具并返回结果。"""
     if name == "bash":
         try:
             r = subprocess.run(
                 args["command"], shell=True, cwd=WORKDIR,
                 capture_output=True, text=True, timeout=60
             )
-            return (r.stdout + r.stderr).strip() or "(empty)"
+            return (r.stdout + r.stderr).strip() or "(无输出)"
         except subprocess.TimeoutExpired:
-            return "Error: Timeout"
+            return "错误:执行超时"
 
     if name == "read_file":
         try:
             return (WORKDIR / args["path"]).read_text()[:50000]
         except Exception as e:
-            return f"Error: {e}"
+            return f"错误:{e}"
 
     if name == "write_file":
         try:
             p = WORKDIR / args["path"]
             p.parent.mkdir(parents=True, exist_ok=True)
             p.write_text(args["content"])
-            return f"Wrote {len(args['content'])} bytes to {args['path']}"
+            return f"已写入 {len(args['content'])} 字节到 {args['path']}"
         except Exception as e:
-            return f"Error: {e}"
+            return f"错误:{e}"
 
-    return f"Unknown tool: {name}"
+    return f"未知工具:{name}"
 
 
 def agent(prompt: str, history: list = None) -> str:
-    """Run the agent loop."""
+    """运行 Agent 循环。"""
     if history is None:
         history = []
 
@@ -110,14 +110,14 @@ def agent(prompt: str, history: list = None) -> str:
             max_tokens=8000,
         )
 
-        # Build assistant message
+        # 构建 assistant 消息
         history.append({"role": "assistant", "content": response.content})
 
-        # If no tool calls, return text
+        # 如果没有工具调用,直接返回文本
         if response.stop_reason != "tool_use":
             return "".join(b.text for b in response.content if hasattr(b, "text"))
 
-        # Execute tools
+        # 执行工具
         results = []
         for block in response.content:
             if block.type == "tool_use":
@@ -134,8 +134,8 @@ def agent(prompt: str, history: list = None) -> str:
 
 
 if __name__ == "__main__":
-    print(f"Minimal Agent - {WORKDIR}")
-    print("Type 'q' to quit.\n")
+    print(f"最小 Agent - {WORKDIR}")
+    print("输入 q 退出。\n")
 
     history = []
     while True:

+ 79 - 79
skills/agent-builder/references/subagent-pattern.py

@@ -1,43 +1,43 @@
 """
-Subagent Pattern - How to implement Task tool for context isolation.
+子 Agent 模式 - 如何实现用于上下文隔离的 Task 工具。
 
-The key insight: spawn child agents with ISOLATED context to prevent
-"context pollution" where exploration details fill up the main conversation.
+核心洞察:用隔离上下文启动子 Agent,防止探索细节填满主对话,
+造成“上下文污染”。
 """
 
 import time
 import sys
 
-# Assuming client, MODEL, execute_tool are defined elsewhere
+# 假设 client、MODEL、execute_tool 已在其他位置定义
 
 
 # =============================================================================
-# AGENT TYPE REGISTRY
+# Agent 类型注册表
 # =============================================================================
 
 AGENT_TYPES = {
-    # Explore: Read-only, for searching and analyzing
+    # Explore:只读,用于搜索和分析
     "explore": {
-        "description": "Read-only agent for exploring code, finding files, searching",
-        "tools": ["bash", "read_file"],  # No write access!
-        "prompt": "You are an exploration agent. Search and analyze, but NEVER modify files. Return a concise summary of what you found.",
+        "description": "只读 Agent,用于探索代码、查找文件和搜索",
+        "tools": ["bash", "read_file"],  # 没有写入权限!
+        "prompt": "你是一个探索型 Agent。请搜索并分析,但绝不要修改文件。返回你发现内容的简洁摘要。",
     },
 
-    # Code: Full-powered, for implementation
+    # Code:完整权限,用于实现
     "code": {
-        "description": "Full agent for implementing features and fixing bugs",
-        "tools": "*",  # All tools
-        "prompt": "You are a coding agent. Implement the requested changes efficiently. Return a summary of what you changed.",
+        "description": "完整权限 Agent,用于实现功能和修复 bug",
+        "tools": "*",  # 所有工具
+        "prompt": "你是一个编码 Agent。高效实现请求的修改,并返回变更摘要。",
     },
 
-    # Plan: Read-only, for design work
+    # Plan:只读,用于设计方案
     "plan": {
-        "description": "Planning agent for designing implementation strategies",
-        "tools": ["bash", "read_file"],  # Read-only
-        "prompt": "You are a planning agent. Analyze the codebase and output a numbered implementation plan. Do NOT make any changes.",
+        "description": "规划型 Agent,用于设计实现策略",
+        "tools": ["bash", "read_file"],  # 只读
+        "prompt": "你是一个规划型 Agent。分析代码库并输出编号实现计划。不要做任何修改。",
     },
 
-    # Add your own types here...
+    # 在这里添加你自己的类型...
     # "test": {
     #     "description": "Testing agent for running and analyzing tests",
     #     "tools": ["bash", "read_file"],
@@ -47,7 +47,7 @@ AGENT_TYPES = {
 
 
 def get_agent_descriptions() -> str:
-    """Generate descriptions for Task tool schema."""
+    """为 Task 工具 schema 生成 Agent 类型描述。"""
     return "\n".join(
         f"- {name}: {cfg['description']}"
         for name, cfg in AGENT_TYPES.items()
@@ -56,55 +56,55 @@ def get_agent_descriptions() -> str:
 
 def get_tools_for_agent(agent_type: str, base_tools: list) -> list:
     """
-    Filter tools based on agent type.
+    根据 Agent 类型过滤工具。
 
-    '*' means all base tools.
-    Otherwise, whitelist specific tool names.
+    '*' 表示所有基础工具。
+    否则只允许白名单中的指定工具名。
 
-    Note: Subagents don't get Task tool to prevent infinite recursion.
+    注意:子 Agent 不会拿到 Task 工具,以防无限递归。
     """
     allowed = AGENT_TYPES.get(agent_type, {}).get("tools", "*")
 
     if allowed == "*":
-        return base_tools  # All base tools, but NOT Task
+        return base_tools  # 所有基础工具,但不包括 Task
 
     return [t for t in base_tools if t["name"] in allowed]
 
 
 # =============================================================================
-# TASK TOOL DEFINITION
+# Task 工具定义
 # =============================================================================
 
 TASK_TOOL = {
     "name": "Task",
-    "description": f"""Spawn a subagent for a focused subtask.
+    "description": f"""为聚焦子任务启动一个子 Agent。
 
-Subagents run in ISOLATED context - they don't see parent's history.
-Use this to keep the main conversation clean.
+子 Agent 运行在隔离上下文中,看不到父 Agent 的历史。
+用它保持主对话清爽。
 
-Agent types:
+Agent 类型:
 {get_agent_descriptions()}
 
-Example uses:
-- Task(explore): "Find all files using the auth module"
-- Task(plan): "Design a migration strategy for the database"
-- Task(code): "Implement the user registration form"
+使用示例:
+- Task(explore): "查找所有使用 auth 模块的文件"
+- Task(plan): "设计数据库迁移策略"
+- Task(code): "实现用户注册表单"
 """,
     "input_schema": {
         "type": "object",
         "properties": {
             "description": {
                 "type": "string",
-                "description": "Short task name (3-5 words) for progress display"
+                "description": "用于进度显示的简短任务名(3-5 个词)"
             },
             "prompt": {
                 "type": "string",
-                "description": "Detailed instructions for the subagent"
+                "description": "给子 Agent 的详细指令"
             },
             "agent_type": {
                 "type": "string",
                 "enum": list(AGENT_TYPES.keys()),
-                "description": "Type of agent to spawn"
+                "description": "要启动的 Agent 类型"
             },
         },
         "required": ["description", "prompt", "agent_type"],
@@ -113,58 +113,58 @@ Example uses:
 
 
 # =============================================================================
-# SUBAGENT EXECUTION
+# 子 Agent 执行
 # =============================================================================
 
 def run_task(description: str, prompt: str, agent_type: str,
              client, model: str, workdir, base_tools: list, execute_tool) -> str:
     """
-    Execute a subagent task with isolated context.
-
-    Key concepts:
-    1. ISOLATED HISTORY - subagent starts fresh, no parent context
-    2. FILTERED TOOLS - based on agent type permissions
-    3. AGENT-SPECIFIC PROMPT - specialized behavior
-    4. RETURNS SUMMARY ONLY - parent sees just the final result
-
-    Args:
-        description: Short name for progress display
-        prompt: Detailed instructions for subagent
-        agent_type: Key from AGENT_TYPES
-        client: Anthropic client
-        model: Model to use
-        workdir: Working directory
-        base_tools: List of tool definitions
-        execute_tool: Function to execute tools
-
-    Returns:
-        Final text output from subagent
+    用隔离上下文执行一个子 Agent 任务。
+
+    关键概念:
+    1. 隔离历史:子 Agent 从空白历史开始,没有父 Agent 上下文
+    2. 过滤工具:根据 Agent 类型决定工具权限
+    3. 专属提示词:提供特化行为
+    4. 只返回摘要:父 Agent 只看到最终结果
+
+    参数:
+        description: 用于进度显示的简短名称
+        prompt: 给子 Agent 的详细指令
+        agent_type: AGENT_TYPES 中的键
+        client: Anthropic 客户端
+        model: 要使用的模型
+        workdir: 工作目录
+        base_tools: 工具定义列表
+        execute_tool: 执行工具的函数
+
+    返回:
+        子 Agent 的最终文本输出
     """
     if agent_type not in AGENT_TYPES:
-        return f"Error: Unknown agent type '{agent_type}'"
+        return f"错误:未知 Agent 类型 '{agent_type}'"
 
     config = AGENT_TYPES[agent_type]
 
-    # Agent-specific system prompt
-    sub_system = f"""You are a {agent_type} subagent at {workdir}.
+    # Agent 专属系统提示词
+    sub_system = f"""你是位于 {workdir} 的 {agent_type} 子 Agent。
 
 {config["prompt"]}
 
-Complete the task and return a clear, concise summary."""
+完成任务,并返回清晰简洁的摘要。"""
 
-    # Filtered tools for this agent type
+    # 针对此 Agent 类型过滤后的工具
     sub_tools = get_tools_for_agent(agent_type, base_tools)
 
-    # KEY: ISOLATED message history!
-    # The subagent starts fresh, doesn't see parent's conversation
+    # 关键点:隔离的消息历史!
+    # 子 Agent 从空白历史开始,看不到父 Agent 的对话
     sub_messages = [{"role": "user", "content": prompt}]
 
-    # Progress display
+    # 进度显示
     print(f"  [{agent_type}] {description}")
     start = time.time()
     tool_count = 0
 
-    # Run the same agent loop (but silently)
+    # 运行相同的 Agent 循环(但保持安静)
     while True:
         response = client.messages.create(
             model=model,
@@ -174,11 +174,11 @@ Complete the task and return a clear, concise summary."""
             max_tokens=8000,
         )
 
-        # Check if done
+        # 检查是否完成
         if response.stop_reason != "tool_use":
             break
 
-        # Execute tools
+        # 执行工具
         tool_calls = [b for b in response.content if b.type == "tool_use"]
         results = []
 
@@ -191,37 +191,37 @@ Complete the task and return a clear, concise summary."""
                 "content": output
             })
 
-            # Update progress (in-place on same line)
+            # 更新进度(在同一行原位刷新)
             elapsed = time.time() - start
             sys.stdout.write(
-                f"\r  [{agent_type}] {description} ... {tool_count} tools, {elapsed:.1f}s"
+                f"\r  [{agent_type}] {description} ... {tool_count} 次工具调用,{elapsed:.1f}s"
             )
             sys.stdout.flush()
 
         sub_messages.append({"role": "assistant", "content": response.content})
         sub_messages.append({"role": "user", "content": results})
 
-    # Final progress update
+    # 最终进度更新
     elapsed = time.time() - start
     sys.stdout.write(
-        f"\r  [{agent_type}] {description} - done ({tool_count} tools, {elapsed:.1f}s)\n"
+        f"\r  [{agent_type}] {description} - 已完成({tool_count} 次工具调用,{elapsed:.1f}s)\n"
     )
 
-    # Extract and return ONLY the final text
+    # 只提取并返回最终文本
     # This is what the parent agent sees - a clean summary
     for block in response.content:
         if hasattr(block, "text"):
             return block.text
 
-    return "(subagent returned no text)"
+    return "(子 Agent 未返回文本)"
 
 
 # =============================================================================
-# USAGE EXAMPLE
+# 使用示例
 # =============================================================================
 
 """
-# In your main agent's execute_tool function:
+# 在主 Agent 的 execute_tool 函数中:
 
 def execute_tool(name: str, args: dict) -> str:
     if name == "Task":
@@ -233,11 +233,11 @@ def execute_tool(name: str, args: dict) -> str:
             model=MODEL,
             workdir=WORKDIR,
             base_tools=BASE_TOOLS,
-            execute_tool=execute_tool  # Pass self for recursion
+            execute_tool=execute_tool  # 传入自身用于递归
         )
-    # ... other tools ...
+    # ... 其他工具 ...
 
 
-# In your TOOLS list:
+# 在 TOOLS 列表中:
 TOOLS = BASE_TOOLS + [TASK_TOOL]
 """

+ 68 - 68
skills/agent-builder/references/tool-templates.py

@@ -1,9 +1,9 @@
 """
-Tool Templates - Copy and customize these for your agent.
+工具模板 - 复制后可按你的 Agent 定制。
 
-Each tool needs:
-1. Definition (JSON schema for the model)
-2. Implementation (Python function)
+每个工具都需要:
+1. 定义(给模型看的 JSON schema)
+2. 实现(Python 函数)
 """
 
 from pathlib import Path
@@ -13,18 +13,18 @@ WORKDIR = Path.cwd()
 
 
 # =============================================================================
-# TOOL DEFINITIONS (for TOOLS list)
+# 工具定义(放入 TOOLS 列表)
 # =============================================================================
 
 BASH_TOOL = {
     "name": "bash",
-    "description": "Run a shell command. Use for: ls, find, grep, git, npm, python, etc.",
+    "description": "运行 shell 命令。适用于 ls、find、grep、git、npm、python 等。",
     "input_schema": {
         "type": "object",
         "properties": {
             "command": {
                 "type": "string",
-                "description": "The shell command to execute"
+                "description": "要执行的 shell 命令"
             }
         },
         "required": ["command"],
@@ -33,17 +33,17 @@ BASH_TOOL = {
 
 READ_FILE_TOOL = {
     "name": "read_file",
-    "description": "Read file contents. Returns UTF-8 text.",
+    "description": "读取文件内容,返回 UTF-8 文本。",
     "input_schema": {
         "type": "object",
         "properties": {
             "path": {
                 "type": "string",
-                "description": "Relative path to the file"
+                "description": "文件的相对路径"
             },
             "limit": {
                 "type": "integer",
-                "description": "Max lines to read (default: all)"
+                "description": "最多读取多少行(默认全部读取)"
             },
         },
         "required": ["path"],
@@ -52,17 +52,17 @@ READ_FILE_TOOL = {
 
 WRITE_FILE_TOOL = {
     "name": "write_file",
-    "description": "Write content to a file. Creates parent directories if needed.",
+    "description": "向文件写入内容。需要时会创建父目录。",
     "input_schema": {
         "type": "object",
         "properties": {
             "path": {
                 "type": "string",
-                "description": "Relative path for the file"
+                "description": "文件的相对路径"
             },
             "content": {
                 "type": "string",
-                "description": "Content to write"
+                "description": "要写入的内容"
             },
         },
         "required": ["path", "content"],
@@ -71,21 +71,21 @@ WRITE_FILE_TOOL = {
 
 EDIT_FILE_TOOL = {
     "name": "edit_file",
-    "description": "Replace exact text in a file. Use for surgical edits.",
+    "description": "在文件中替换一次完全匹配的文本,适合小范围精确修改。",
     "input_schema": {
         "type": "object",
         "properties": {
             "path": {
                 "type": "string",
-                "description": "Relative path to the file"
+                "description": "文件的相对路径"
             },
             "old_text": {
                 "type": "string",
-                "description": "Exact text to find (must match precisely)"
+                "description": "要查找的精确文本(必须完全匹配)"
             },
             "new_text": {
                 "type": "string",
-                "description": "Replacement text"
+                "description": "替换后的文本"
             },
         },
         "required": ["path", "old_text", "new_text"],
@@ -94,19 +94,19 @@ EDIT_FILE_TOOL = {
 
 TODO_WRITE_TOOL = {
     "name": "TodoWrite",
-    "description": "Update the task list. Use to plan and track progress.",
+    "description": "更新任务清单,用于规划并跟踪进度。",
     "input_schema": {
         "type": "object",
         "properties": {
             "items": {
                 "type": "array",
-                "description": "Complete list of tasks",
+                "description": "完整任务列表",
                 "items": {
                     "type": "object",
                     "properties": {
-                        "content": {"type": "string", "description": "Task description"},
+                        "content": {"type": "string", "description": "任务描述"},
                         "status": {"type": "string", "enum": ["pending", "in_progress", "completed"]},
-                        "activeForm": {"type": "string", "description": "Present tense, e.g. 'Reading files'"},
+                        "activeForm": {"type": "string", "description": "当前进行态描述,例如“正在读取文件”"},
                     },
                     "required": ["content", "status", "activeForm"],
                 },
@@ -117,15 +117,15 @@ TODO_WRITE_TOOL = {
 }
 
 TASK_TOOL_TEMPLATE = """
-# Generate dynamically with agent types
+# 根据 Agent 类型动态生成
 TASK_TOOL = {
     "name": "Task",
-    "description": f"Spawn a subagent for a focused subtask.\\n\\nAgent types:\\n{get_agent_descriptions()}",
+    "description": f"为聚焦子任务启动一个子 Agent。\\n\\nAgent 类型:\\n{get_agent_descriptions()}",
     "input_schema": {
         "type": "object",
         "properties": {
-            "description": {"type": "string", "description": "Short task name (3-5 words)"},
-            "prompt": {"type": "string", "description": "Detailed instructions"},
+            "description": {"type": "string", "description": "简短任务名(3-5 个词)"},
+            "prompt": {"type": "string", "description": "详细指令"},
             "agent_type": {"type": "string", "enum": list(AGENT_TYPES.keys())},
         },
         "required": ["description", "prompt", "agent_type"],
@@ -135,32 +135,32 @@ TASK_TOOL = {
 
 
 # =============================================================================
-# TOOL IMPLEMENTATIONS
+# 工具实现
 # =============================================================================
 
 def safe_path(p: str) -> Path:
     """
-    Security: Ensure path stays within workspace.
-    Prevents ../../../etc/passwd attacks.
+    安全检查:确保路径留在工作区内。
+    防止 ../../../etc/passwd 这类路径逃逸攻击。
     """
     path = (WORKDIR / p).resolve()
     if not path.is_relative_to(WORKDIR):
-        raise ValueError(f"Path escapes workspace: {p}")
+        raise ValueError(f"路径逃逸出工作区:{p}")
     return path
 
 
 def run_bash(command: str) -> str:
     """
-    Execute shell command with safety checks.
+    带安全检查地执行 shell 命令。
 
-    Safety features:
-    - Blocks obviously dangerous commands
-    - 60 second timeout
-    - Output truncated to 50KB
+    安全特性:
+    - 阻止明显危险的命令
+    - 60 秒超时
+    - 输出截断到 50KB
     """
     dangerous = ["rm -rf /", "sudo", "shutdown", "reboot", "> /dev/"]
     if any(d in command for d in dangerous):
-        return "Error: Dangerous command blocked"
+        return "错误:已阻止危险命令"
 
     try:
         result = subprocess.run(
@@ -172,22 +172,22 @@ def run_bash(command: str) -> str:
             timeout=60
         )
         output = (result.stdout + result.stderr).strip()
-        return output[:50000] if output else "(no output)"
+        return output[:50000] if output else "(无输出)"
 
     except subprocess.TimeoutExpired:
-        return "Error: Command timed out (60s)"
+        return "错误:命令执行超时(60 秒)"
     except Exception as e:
-        return f"Error: {e}"
+        return f"错误:{e}"
 
 
 def run_read_file(path: str, limit: int = None) -> str:
     """
-    Read file contents with optional line limit.
+    读取文件内容,可选行数限制。
 
-    Features:
-    - Safe path resolution
-    - Optional line limit for large files
-    - Output truncated to 50KB
+    特性:
+    - 安全路径解析
+    - 大文件可限制读取行数
+    - 输出截断到 50KB
     """
     try:
         text = safe_path(path).read_text()
@@ -195,69 +195,69 @@ def run_read_file(path: str, limit: int = None) -> str:
 
         if limit and limit < len(lines):
             lines = lines[:limit]
-            lines.append(f"... ({len(text.splitlines()) - limit} more lines)")
+            lines.append(f"...(还有 {len(text.splitlines()) - limit} 行)")
 
         return "\n".join(lines)[:50000]
 
     except Exception as e:
-        return f"Error: {e}"
+        return f"错误:{e}"
 
 
 def run_write_file(path: str, content: str) -> str:
     """
-    Write content to file, creating parent directories if needed.
+    向文件写入内容,需要时创建父目录。
 
-    Features:
-    - Safe path resolution
-    - Auto-creates parent directories
-    - Returns byte count for confirmation
+    特性:
+    - 安全路径解析
+    - 自动创建父目录
+    - 返回写入字节数用于确认
     """
     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}"
+        return f"已写入 {len(content)} 字节到 {path}"
 
     except Exception as e:
-        return f"Error: {e}"
+        return f"错误:{e}"
 
 
 def run_edit_file(path: str, old_text: str, new_text: str) -> str:
     """
-    Replace exact text in a file (surgical edit).
+    在文件中替换一次完全匹配的文本(精确修改)。
 
-    Features:
-    - Exact string matching (not regex)
-    - Only replaces first occurrence (safety)
-    - Clear error if text not found
+    特性:
+    - 精确字符串匹配(不是正则)
+    - 只替换首次出现的位置(更安全)
+    - 找不到文本时返回清晰错误
     """
     try:
         fp = safe_path(path)
         content = fp.read_text()
 
         if old_text not in content:
-            return f"Error: Text not found in {path}"
+            return f"错误:在 {path} 中未找到目标文本"
 
         new_content = content.replace(old_text, new_text, 1)
         fp.write_text(new_content)
-        return f"Edited {path}"
+        return f"已编辑 {path}"
 
     except Exception as e:
-        return f"Error: {e}"
+        return f"错误:{e}"
 
 
 # =============================================================================
-# DISPATCHER PATTERN
+# 分发器模式
 # =============================================================================
 
 def execute_tool(name: str, args: dict) -> str:
     """
-    Dispatch tool call to implementation.
+    将工具调用分发到对应实现。
 
-    This pattern makes it easy to add new tools:
-    1. Add definition to TOOLS list
-    2. Add implementation function
-    3. Add case to this dispatcher
+    这种模式便于添加新工具:
+    1. 将工具定义加入 TOOLS 列表
+    2. 添加实现函数
+    3. 在这个分发器中增加分支
     """
     if name == "bash":
         return run_bash(args["command"])
@@ -267,5 +267,5 @@ def execute_tool(name: str, args: dict) -> str:
         return run_write_file(args["path"], args["content"])
     if name == "edit_file":
         return run_edit_file(args["path"], args["old_text"], args["new_text"])
-    # Add more tools here...
-    return f"Unknown tool: {name}"
+    # 在这里继续添加更多工具...
+    return f"未知工具:{name}"

+ 75 - 75
skills/agent-builder/scripts/init_agent.py

@@ -1,29 +1,29 @@
 #!/usr/bin/env python3
 """
-Agent Scaffold Script - Create a new agent project with best practices.
+Agent 脚手架脚本 - 按最佳实践创建新的 Agent 项目。
 
-Usage:
+用法:
     python init_agent.py <agent-name> [--level 0-4] [--path <output-dir>]
 
-Examples:
-    python init_agent.py my-agent                 # Level 1 (4 tools)
-    python init_agent.py my-agent --level 0      # Minimal (bash only)
-    python init_agent.py my-agent --level 2      # With TodoWrite
-    python init_agent.py my-agent --path ./bots  # Custom output directory
+示例:
+    python init_agent.py my-agent                 # Level 1(4 个工具)
+    python init_agent.py my-agent --level 0      # 最小版(仅 bash)
+    python init_agent.py my-agent --level 2      #  TodoWrite
+    python init_agent.py my-agent --path ./bots  # 自定义输出目录
 """
 
 import argparse
 import sys
 from pathlib import Path
 
-# Agent templates for each level
+# 各等级的 Agent 模板
 TEMPLATES = {
     0: '''#!/usr/bin/env python3
 """
-Level 0 Agent - Bash is All You Need (~50 lines)
+Level 0 Agent - Bash 就够了(约 50 行)
 
-Core insight: One tool (bash) can do everything.
-Subagents via self-recursion: python {name}.py "subtask"
+核心洞察:一个工具(bash)就能做很多事。
+通过自递归启动子 Agent:python {name}.py "subtask"
 """
 
 from anthropic import Anthropic
@@ -39,15 +39,15 @@ client = Anthropic(
 )
 MODEL = os.getenv("MODEL_NAME", "claude-sonnet-4-20250514")
 
-SYSTEM = """You are a coding agent. Use bash for everything:
-- Read: cat, grep, find, ls
-- Write: echo 'content' > file
-- Subagent: python {name}.py "subtask"
+SYSTEM = """你是一个编码 Agent。所有事情都通过 bash 完成:
+- 读取:cat, grep, find, ls
+- 写入:echo 'content' > file
+- 子 Agent:python {name}.py "subtask"
 """
 
 TOOL = [{{
     "name": "bash",
-    "description": "Execute shell command",
+    "description": "执行 shell 命令。",
     "input_schema": {{"type": "object", "properties": {{"command": {{"type": "string"}}}}, "required": ["command"]}}
 }}]
 
@@ -64,25 +64,25 @@ def run(prompt, history=[]):
                 print(f"> {{b.input['command']}}")
                 try:
                     out = subprocess.run(b.input["command"], shell=True, capture_output=True, text=True, timeout=60)
-                    output = (out.stdout + out.stderr).strip() or "(empty)"
+                    output = (out.stdout + out.stderr).strip() or "(无输出)"
                 except Exception as e:
-                    output = f"Error: {{e}}"
+                    output = f"错误:{{e}}"
                 results.append({{"type": "tool_result", "tool_use_id": b.id, "content": output[:50000]}})
         history.append({{"role": "user", "content": results}})
 
 if __name__ == "__main__":
     h = []
-    print("{name} - Level 0 Agent\\nType 'q' to quit.\\n")
+    print("{name} - Level 0 Agent\\n输入 q 退出。\\n")
     while (q := input(">> ").strip()) not in ("q", "quit", ""):
         print(run(q, h), "\\n")
 ''',
 
     1: '''#!/usr/bin/env python3
 """
-Level 1 Agent - Model as Agent (~200 lines)
+Level 1 Agent - 模型即 Agent(约 200 行)
 
-Core insight: 4 tools cover 90% of coding tasks.
-The model IS the agent. Code just runs the loop.
+核心洞察:4 个工具覆盖 90% 的编码任务。
+模型本身就是 Agent,代码只负责运行循环。
 """
 
 from anthropic import Anthropic
@@ -100,76 +100,76 @@ client = Anthropic(
 MODEL = os.getenv("MODEL_NAME", "claude-sonnet-4-20250514")
 WORKDIR = Path.cwd()
 
-SYSTEM = f"""You are a coding agent at {{WORKDIR}}.
+SYSTEM = f"""你是位于 {{WORKDIR}} 的编码 Agent。
 
-Rules:
-- Prefer tools over prose. Act, don't just explain.
-- Never invent file paths. Use ls/find first if unsure.
-- Make minimal changes. Don't over-engineer.
-- After finishing, summarize what changed."""
+规则:
+- 优先使用工具,而不是只写解释;直接行动。
+- 不要编造文件路径。不确定时先使用 ls/find。
+- 保持最小改动,不要过度设计。
+- 完成后总结修改内容。"""
 
 TOOLS = [
-    {{"name": "bash", "description": "Run shell command",
+    {{"name": "bash", "description": "运行 shell 命令。",
      "input_schema": {{"type": "object", "properties": {{"command": {{"type": "string"}}}}, "required": ["command"]}}}},
-    {{"name": "read_file", "description": "Read file contents",
+    {{"name": "read_file", "description": "读取文件内容。",
      "input_schema": {{"type": "object", "properties": {{"path": {{"type": "string"}}}}, "required": ["path"]}}}},
-    {{"name": "write_file", "description": "Write content to file",
+    {{"name": "write_file", "description": "向文件写入内容。",
      "input_schema": {{"type": "object", "properties": {{"path": {{"type": "string"}}, "content": {{"type": "string"}}}}, "required": ["path", "content"]}}}},
-    {{"name": "edit_file", "description": "Replace exact text in file",
+    {{"name": "edit_file", "description": "在文件中替换一次完全匹配的文本。",
      "input_schema": {{"type": "object", "properties": {{"path": {{"type": "string"}}, "old_text": {{"type": "string"}}, "new_text": {{"type": "string"}}}}, "required": ["path", "old_text", "new_text"]}}}},
 ]
 
 def safe_path(p: str) -> Path:
-    """Prevent path escape attacks."""
+    """防止路径逃逸出工作区。"""
     path = (WORKDIR / p).resolve()
     if not path.is_relative_to(WORKDIR):
-        raise ValueError(f"Path escapes workspace: {{p}}")
+        raise ValueError(f"路径逃逸出工作区:{{p}}")
     return path
 
 def execute(name: str, args: dict) -> str:
-    """Execute a tool and return result."""
+    """执行工具并返回结果。"""
     if name == "bash":
         dangerous = ["rm -rf /", "sudo", "shutdown", "> /dev/"]
         if any(d in args["command"] for d in dangerous):
-            return "Error: Dangerous command blocked"
+            return "错误:已阻止危险命令"
         try:
             r = subprocess.run(args["command"], shell=True, cwd=WORKDIR, capture_output=True, text=True, timeout=60)
-            return (r.stdout + r.stderr).strip()[:50000] or "(empty)"
+            return (r.stdout + r.stderr).strip()[:50000] or "(无输出)"
         except subprocess.TimeoutExpired:
-            return "Error: Timeout (60s)"
+            return "错误:执行超时(60 秒)"
         except Exception as e:
-            return f"Error: {{e}}"
+            return f"错误:{{e}}"
 
     if name == "read_file":
         try:
             return safe_path(args["path"]).read_text()[:50000]
         except Exception as e:
-            return f"Error: {{e}}"
+            return f"错误:{{e}}"
 
     if name == "write_file":
         try:
             p = safe_path(args["path"])
             p.parent.mkdir(parents=True, exist_ok=True)
             p.write_text(args["content"])
-            return f"Wrote {{len(args['content'])}} bytes to {{args['path']}}"
+            return f"已写入 {{len(args['content'])}} 字节到 {{args['path']}}"
         except Exception as e:
-            return f"Error: {{e}}"
+            return f"错误:{{e}}"
 
     if name == "edit_file":
         try:
             p = safe_path(args["path"])
             content = p.read_text()
             if args["old_text"] not in content:
-                return f"Error: Text not found in {{args['path']}}"
+                return f"错误:在 {{args['path']}} 中未找到目标文本"
             p.write_text(content.replace(args["old_text"], args["new_text"], 1))
-            return f"Edited {{args['path']}}"
+            return f"已编辑 {{args['path']}}"
         except Exception as e:
-            return f"Error: {{e}}"
+            return f"错误:{{e}}"
 
-    return f"Unknown tool: {{name}}"
+    return f"未知工具:{{name}}"
 
 def agent(prompt: str, history: list = None) -> str:
-    """Run the agent loop."""
+    """运行 Agent 循环。"""
     if history is None:
         history = []
     history.append({{"role": "user", "content": prompt}})
@@ -193,8 +193,8 @@ def agent(prompt: str, history: list = None) -> str:
         history.append({{"role": "user", "content": results}})
 
 if __name__ == "__main__":
-    print(f"{name} - Level 1 Agent at {{WORKDIR}}")
-    print("Type 'q' to quit.\\n")
+    print(f"{name} - Level 1 Agent,工作目录:{{WORKDIR}}")
+    print("输入 q 退出。\\n")
     h = []
     while True:
         try:
@@ -207,7 +207,7 @@ if __name__ == "__main__":
 ''',
 }
 
-ENV_TEMPLATE = '''# API Configuration
+ENV_TEMPLATE = '''# API 配置
 ANTHROPIC_API_KEY=sk-xxx
 ANTHROPIC_BASE_URL=https://api.anthropic.com
 MODEL_NAME=claude-sonnet-4-20250514
@@ -215,61 +215,61 @@ MODEL_NAME=claude-sonnet-4-20250514
 
 
 def create_agent(name: str, level: int, output_dir: Path):
-    """Create a new agent project."""
-    # Validate level
+    """创建新的 Agent 项目。"""
+    # 校验等级
     if level not in TEMPLATES and level not in (2, 3, 4):
-        print(f"Error: Level {level} not yet implemented in scaffold.")
-        print("Available levels: 0 (minimal), 1 (4 tools)")
-        print("For levels 2-4, copy from mini-claude-code repository.")
+        print(f"错误:脚手架暂未实现 Level {level}。")
+        print("可用等级:0(最小版)、1(4 个工具)")
+        print("Level 2-4 请从 mini-claude-code 仓库复制。")
         sys.exit(1)
 
-    # Create output directory
+    # 创建输出目录
     agent_dir = output_dir / name
     agent_dir.mkdir(parents=True, exist_ok=True)
 
-    # Write agent file
+    # 写入 Agent 文件
     agent_file = agent_dir / f"{name}.py"
     template = TEMPLATES.get(level, TEMPLATES[1])
     agent_file.write_text(template.format(name=name))
-    print(f"Created: {agent_file}")
+    print(f"已创建:{agent_file}")
 
-    # Write .env.example
+    # 写入 .env.example
     env_file = agent_dir / ".env.example"
     env_file.write_text(ENV_TEMPLATE)
-    print(f"Created: {env_file}")
+    print(f"已创建:{env_file}")
 
-    # Write .gitignore
+    # 写入 .gitignore
     gitignore = agent_dir / ".gitignore"
     gitignore.write_text(".env\n__pycache__/\n*.pyc\n")
-    print(f"Created: {gitignore}")
+    print(f"已创建:{gitignore}")
 
-    print(f"\nAgent '{name}' created at {agent_dir}")
-    print(f"\nNext steps:")
+    print(f"\nAgent '{name}' 已创建于 {agent_dir}")
+    print(f"\n下一步:")
     print(f"  1. cd {agent_dir}")
     print(f"  2. cp .env.example .env")
-    print(f"  3. Edit .env with your API key")
+    print(f"  3. 编辑 .env,填入你的 API key")
     print(f"  4. pip install anthropic python-dotenv")
     print(f"  5. python {name}.py")
 
 
 def main():
     parser = argparse.ArgumentParser(
-        description="Scaffold a new AI coding agent project",
+        description="搭建一个新的 AI 编码 Agent 项目",
         formatter_class=argparse.RawDescriptionHelpFormatter,
         epilog="""
-Levels:
-  0  Minimal (~50 lines) - Single bash tool, self-recursion for subagents
-  1  Basic (~200 lines)  - 4 core tools: bash, read, write, edit
-  2  Todo (~300 lines)   - + TodoWrite for structured planning
-  3  Subagent (~450)     - + Task tool for context isolation
-  4  Skills (~550)       - + Skill tool for domain expertise
+等级:
+  0  最小版(约 50 行) - 单一 bash 工具,通过自递归实现子 Agent
+  1  基础版(约 200 行)- 4 个核心工具:bash、read、write、edit
+  2  Todo(约 300 行)  - 增加 TodoWrite,用于结构化规划
+  3  子 Agent(约 450) - 增加 Task 工具,用于上下文隔离
+  4  Skills(约 550)   - 增加 Skill 工具,用于领域能力
         """
     )
-    parser.add_argument("name", help="Name of the agent to create")
+    parser.add_argument("name", help="要创建的 Agent 名称")
     parser.add_argument("--level", type=int, default=1, choices=[0, 1, 2, 3, 4],
-                       help="Complexity level (default: 1)")
+                       help="复杂度等级(默认:1)")
     parser.add_argument("--path", type=Path, default=Path.cwd(),
-                       help="Output directory (default: current directory)")
+                       help="输出目录(默认:当前目录)")
 
     args = parser.parse_args()
     create_agent(args.name, args.level, args.path)

+ 0 - 0
skills/mcp-builder/skill.md


+ 0 - 0
skills/pdf/skill.md