Jelajahi Sumber

first commit

zengli 1 Minggu lalu
melakukan
95f3027777

+ 70 - 0
.env.example

@@ -0,0 +1,70 @@
+# API Key (required)
+# Get yours at: https://console.anthropic.com/
+ANTHROPIC_API_KEY=sk-ant-xxx
+
+# Model ID (required)
+MODEL_ID=claude-sonnet-4-6
+
+# Base URL (optional, for Anthropic-compatible providers)
+# ANTHROPIC_BASE_URL=https://api.anthropic.com
+
+# =============================================================================
+#  Anthropic-compatible providers
+#
+#  Provider         MODEL_ID                    Notes                 Base URL
+#  ---------------  --------------------------  --------------------  -------------------
+#  Anthropic        claude-sonnet-4-6           default               (default)
+#  MiniMax          MiniMax-M3                  latest M-series       see below
+#  GLM (Zhipu)      glm-5.2                     latest coding model   see below
+#  Kimi (Moonshot)  kimi-k2.7-code              coding model          see below
+#  DeepSeek         deepseek-v4-pro             pro model             see below
+#                   deepseek-v4-flash           flash model           see below
+#
+#  Check provider docs for current availability, pricing, and regional access.
+# =============================================================================
+
+# ---- International ----
+
+# MiniMax          https://www.minimax.io
+# ANTHROPIC_BASE_URL=https://api.minimax.io/anthropic
+# MODEL_ID=MiniMax-M3
+# MODEL_ID=MiniMax-M2.7
+# MODEL_ID=MiniMax-M2.7-highspeed
+
+# GLM (Zhipu)      https://z.ai
+# ANTHROPIC_BASE_URL=https://api.z.ai/api/anthropic
+# MODEL_ID=glm-5.2
+
+# Kimi (Moonshot)  https://platform.kimi.ai
+# ANTHROPIC_BASE_URL=https://api.moonshot.ai/anthropic
+# MODEL_ID=kimi-k2.7-code
+# MODEL_ID=kimi-k2.7-code-highspeed
+# MODEL_ID=kimi-k2.6
+
+# DeepSeek         https://platform.deepseek.com
+# ANTHROPIC_BASE_URL=https://api.deepseek.com/anthropic
+# MODEL_ID=deepseek-v4-pro
+# MODEL_ID=deepseek-v4-flash
+
+# ---- China mainland ----
+
+# MiniMax          https://platform.minimax.io
+# ANTHROPIC_BASE_URL=https://api.minimaxi.com/anthropic
+# MODEL_ID=MiniMax-M3
+# MODEL_ID=MiniMax-M2.7
+# MODEL_ID=MiniMax-M2.7-highspeed
+
+# GLM (Zhipu)      https://open.bigmodel.cn
+# ANTHROPIC_BASE_URL=https://open.bigmodel.cn/api/anthropic
+# MODEL_ID=glm-5.2
+
+# Kimi (Moonshot)  https://platform.moonshot.cn
+# ANTHROPIC_BASE_URL=https://api.moonshot.cn/anthropic
+# MODEL_ID=kimi-k2.7-code
+# MODEL_ID=kimi-k2.7-code-highspeed
+# MODEL_ID=kimi-k2.6
+
+# DeepSeek (no regional split, same endpoint globally)
+# ANTHROPIC_BASE_URL=https://api.deepseek.com/anthropic
+# MODEL_ID=deepseek-v4-pro
+# MODEL_ID=deepseek-v4-flash

+ 34 - 0
.github/workflows/ci.yml

@@ -0,0 +1,34 @@
+name: CI
+
+on:
+  push:
+    branches: [main]
+  pull_request:
+    branches: [main]
+
+jobs:
+  build:
+    runs-on: ubuntu-latest
+    defaults:
+      run:
+        working-directory: web
+
+    steps:
+      # actions/checkout@v6
+      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
+
+      # actions/setup-node@v6
+      - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e
+        with:
+          node-version: 20
+          cache: npm
+          cache-dependency-path: web/package-lock.json
+
+      - name: Install dependencies
+        run: npm ci
+
+      - name: Type check
+        run: npx tsc --noEmit
+
+      - name: Build
+        run: npm run build

+ 49 - 0
.github/workflows/test.yml

@@ -0,0 +1,49 @@
+name: Test
+
+on:
+  push:
+    branches: [main]
+  pull_request:
+    branches: [main]
+
+jobs:
+  python-smoke:
+    runs-on: ubuntu-latest
+    steps:
+      # actions/checkout@v6
+      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
+
+      - name: Set up Python
+        # actions/setup-python@v6
+        uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405
+        with:
+          python-version: "3.11"
+
+      - name: Install dependencies
+        run: pip install -r requirements.txt pytest
+
+      - name: Run Python smoke tests
+        run: python -m pytest tests -q
+
+  web-build:
+    runs-on: ubuntu-latest
+    defaults:
+      run:
+        working-directory: web
+    steps:
+      # actions/checkout@v6
+      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
+
+      - name: Set up Node.js
+        # actions/setup-node@v6
+        uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e
+        with:
+          node-version: "20"
+          cache: "npm"
+          cache-dependency-path: web/package-lock.json
+
+      - name: Install dependencies
+        run: npm ci
+
+      - name: Build
+        run: npm run build

+ 234 - 0
.gitignore

@@ -0,0 +1,234 @@
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[codz]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+/lib/
+/lib64/
+parts/
+sdist/
+var/
+wheels/
+share/python-wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+
+# PyInstaller
+#  Usually these files are written by a python script from a template
+#  before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.nox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+*.py.cover
+.hypothesis/
+.pytest_cache/
+cover/
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+local_settings.py
+db.sqlite3
+db.sqlite3-journal
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+.pybuilder/
+target/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# IPython
+profile_default/
+ipython_config.py
+
+# pyenv
+#   For a library or package, you might want to ignore these files since the code is
+#   intended to run in multiple environments; otherwise, check them in:
+# .python-version
+
+# pipenv
+#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
+#   However, in case of collaboration, if having platform-specific dependencies or dependencies
+#   having no cross-platform support, pipenv may install dependencies that don't work, or not
+#   install all needed dependencies.
+#Pipfile.lock
+
+# UV
+#   Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
+#   This is especially recommended for binary packages to ensure reproducibility, and is more
+#   commonly ignored for libraries.
+#uv.lock
+
+# poetry
+#   Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
+#   This is especially recommended for binary packages to ensure reproducibility, and is more
+#   commonly ignored for libraries.
+#   https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
+#poetry.lock
+#poetry.toml
+
+# pdm
+#   Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
+#   pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
+#   https://pdm-project.org/en/latest/usage/project/#working-with-version-control
+#pdm.lock
+#pdm.toml
+.pdm-python
+.pdm-build/
+
+# pixi
+#   Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
+#pixi.lock
+#   Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
+#   in the .venv directory. It is recommended not to include this directory in version control.
+.pixi
+
+# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
+__pypackages__/
+
+# Celery stuff
+celerybeat-schedule
+celerybeat.pid
+
+# SageMath parsed files
+*.sage.py
+
+# Environments
+.env
+.envrc
+.venv
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
+.dmypy.json
+dmypy.json
+
+# Pyre type checker
+.pyre/
+
+# pytype static type analyzer
+.pytype/
+
+# Cython debug symbols
+cython_debug/
+
+# PyCharm
+#  JetBrains specific template is maintained in a separate JetBrains.gitignore that can
+#  be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
+#  and can be added to the global gitignore or merged into this file.  For a more nuclear
+#  option (not recommended) you can uncomment the following to ignore the entire idea folder.
+#.idea/
+
+# Abstra
+# Abstra is an AI-powered process automation framework.
+# Ignore directories containing user credentials, local state, and settings.
+# Learn more at https://abstra.io/docs
+.abstra/
+
+# Visual Studio Code
+#  Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore 
+#  that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
+#  and can be added to the global gitignore or merged into this file. However, if you prefer, 
+#  you could uncomment the following to ignore the entire vscode folder
+# .vscode/
+
+# Transcripts (generated by compression agent)
+.transcripts/
+
+# Runtime artifacts (generated by agent tests)
+.memory/
+.task_outputs/
+.tasks/
+.teams/
+.mailboxes/
+.worktrees/
+.scheduled_tasks.json
+
+# Accidental root npm lockfile; web/package-lock.json is tracked.
+/package-lock.json
+
+# Ruff stuff:
+.ruff_cache/
+
+# PyPI configuration file
+.pypirc
+
+# Cursor
+#  Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to
+#  exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
+#  refer to https://docs.cursor.com/context/ignore-files
+.cursorignore
+.cursorindexingignore
+
+# Marimo
+marimo/_static/
+marimo/_lsp/
+__marimo__/
+
+# Web app
+web/node_modules/
+web/.next/
+web/out/
+.vercel
+.env*.local
+test_providers.py
+
+# Internal analysis artifacts (not learning material)
+analysis/
+analysis_progress.md

+ 10 - 0
.idea/.gitignore

@@ -0,0 +1,10 @@
+# 默认忽略的文件
+/shelf/
+/workspace.xml
+# 基于编辑器的 HTTP 客户端请求
+/httpRequests/
+# 已忽略包含查询文件的默认文件夹
+/queries/
+# Datasource local storage ignored files
+/dataSources/
+/dataSources.local.xml

+ 20 - 0
.idea/claudeCodeTabState.xml

@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="ClaudeCodeTabState">
+    <option name="tabSessions">
+      <map>
+        <entry key="0">
+          <value>
+            <TabSessionState>
+              <option name="provider" value="claude" />
+              <option name="sessionId" value="6a150914-7a3f-45e1-a616-0621dba47c83" />
+              <option name="cwd" value="$PROJECT_DIR$" />
+              <option name="model" value="claude-sonnet-4-7[1m]" />
+              <option name="permissionMode" value="bypassPermissions" />
+            </TabSessionState>
+          </value>
+        </entry>
+      </map>
+    </option>
+  </component>
+</project>

+ 6 - 0
.idea/inspectionProfiles/profiles_settings.xml

@@ -0,0 +1,6 @@
+<component name="InspectionProjectProfileManager">
+  <settings>
+    <option name="USE_PROJECT_PROFILE" value="false" />
+    <version value="1.0" />
+  </settings>
+</component>

+ 19 - 0
.idea/learn-claude-code.iml

@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<module type="PYTHON_MODULE" version="4">
+  <component name="NewModuleRootManager">
+    <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="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>
+</module>

+ 8 - 0
.idea/modules.xml

@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="ProjectModuleManager">
+    <modules>
+      <module fileurl="file://$PROJECT_DIR$/.idea/learn-claude-code.iml" filepath="$PROJECT_DIR$/.idea/learn-claude-code.iml" />
+    </modules>
+  </component>
+</project>

+ 6 - 0
.idea/vcs.xml

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="VcsDirectoryMappings">
+    <mapping directory="" vcs="Git" />
+  </component>
+</project>

+ 3 - 0
requirements.txt

@@ -0,0 +1,3 @@
+anthropic>=0.25.0
+python-dotenv>=1.0.0
+pyyaml>=6.0

+ 136 - 0
s01_agent_loop/code.py

@@ -0,0 +1,136 @@
+#!/usr/bin/env python3
+"""
+s01_agent_loop.py - Agent 循环
+
+AI 编码 Agent 的核心秘密可以浓缩成一个模式:
+
+    while stop_reason == "tool_use":
+        response = LLM(messages, tools)
+        执行工具
+        追加结果
+
+    +----------+      +-------+      +---------+
+    |   User   | ---> |  LLM  | ---> |  Tool   |
+    |  prompt  |      |       |      | execute |
+    +----------+      +---+---+      +----+----+
+                          ^               |
+                          |   tool_result |
+                          +---------------+
+                          (循环继续)
+
+这就是核心循环:把工具结果喂回给模型,直到模型决定停止。
+生产级 Agent 会在这个基础上叠加策略、Hooks 和生命周期控制。
+
+用法:
+    pip install anthropic python-dotenv
+    ANTHROPIC_API_KEY=... python s01_agent_loop/code.py
+"""
+
+import os
+import subprocess
+
+try:
+    import readline
+    # macOS 的 libedit 在处理中文输入时有退格问题,这四行修复它
+    readline.parse_and_bind('set bind-tty-special-chars off')
+    readline.parse_and_bind('set input-meta on')
+    readline.parse_and_bind('set output-meta on')
+    readline.parse_and_bind('set convert-meta off')
+except ImportError:
+    pass
+
+from anthropic import Anthropic
+from dotenv import load_dotenv
+
+load_dotenv(override=True)
+
+if os.getenv("ANTHROPIC_BASE_URL"):
+    os.environ.pop("ANTHROPIC_AUTH_TOKEN", None)
+
+client = Anthropic(base_url=os.getenv("ANTHROPIC_BASE_URL"))
+MODEL = os.environ["MODEL_ID"]
+
+SYSTEM = f"你是位于 {os.getcwd()}. 使用 bash 解决任务。直接行动,不要只解释。"
+
+# ── 工具定义:只有 bash ────────────────────────────
+TOOLS = [{
+    "name": "bash",
+    "description": "运行一条 shell 命令。",
+    "input_schema": {
+        "type": "object",
+        "properties": {"command": {"type": "string"}},
+        "required": ["command"],
+    },
+}]
+
+
+# ── 工具执行 ────────────────────────────────────────
+def run_bash(command: str) -> str:
+    dangerous = ["rm -rf /", "sudo", "shutdown", "reboot", "> /dev/"]
+    if any(d in command for d in dangerous):
+        return "错误:危险命令已被拦截"
+    try:
+        r = subprocess.run(command, shell=True, cwd=os.getcwd(),
+                           capture_output=True, text=True, timeout=120)
+        out = (r.stdout + r.stderr).strip()
+        return out[:50000] if out else "(无输出)"
+    except subprocess.TimeoutExpired:
+        return "错误:执行超时(120 秒)"
+    except (FileNotFoundError, OSError) as e:
+        return f"错误:{e}"
+
+
+# ── 核心模式:while 循环持续调用工具,直到模型停止 ──
+def agent_loop(messages: list):
+    while True:
+        response = client.messages.create(
+            model=MODEL, system=SYSTEM, messages=messages,
+            tools=TOOLS, max_tokens=8000,
+        )
+
+        # 追加 assistant 轮次
+        messages.append({"role": "assistant", "content": response.content})
+
+        # 如果模型没有调用工具,就结束
+        if response.stop_reason != "tool_use":
+            return
+
+        # 执行每个工具调用并收集结果
+        results = []
+        for block in response.content:
+            if block.type == "tool_use":
+                print(f"\033[33m$ {block.input['command']}\033[0m")
+                output = run_bash(block.input["command"])
+                print(output[:200])
+                results.append({
+                    "type": "tool_result",
+                    "tool_use_id": block.id,
+                    "content": output,
+                })
+
+        # 将工具结果喂回去,循环继续
+        messages.append({"role": "user", "content": results})
+
+
+# ── 入口 ──────────────────────────────────────────
+if __name__ == "__main__":
+    print("s01: Agent 循环")
+    print("输入问题,回车发送。输入 q 退出。\n")
+
+    history = []
+    while True:
+        try:
+            query = input("\033[36ms01 >> \033[0m")
+        except (EOFError, KeyboardInterrupt):
+            break
+        if query.strip().lower() in ("q", "exit", ""):
+            break
+        history.append({"role": "user", "content": query})
+        agent_loop(history)
+        # 打印模型最终文本回复
+        response_content = history[-1]["content"]
+        if isinstance(response_content, list):
+            for block in response_content:
+                if getattr(block, "type", None) == "text":
+                    print(block.text)
+        print()

+ 190 - 0
s02_tool_use/code.py

@@ -0,0 +1,190 @@
+#!/usr/bin/env python3
+"""
+s02: 工具使用 — 在 s01 基础上新增 4 个工具 + 分发映射。
+
+运行: python s02_tool_use/code.py
+需要: pip install anthropic python-dotenv + .env 中配置 ANTHROPIC_API_KEY
+
+本文件 = s01 的全部代码 + 以下新增:
+  + run_read / run_write / run_edit / run_glob 四个工具实现
+  + TOOL_HANDLERS 分发映射(替代 s01 中硬编码的 run_bash 调用)
+  + safe_path 路径安全校验
+
+循环本身(agent_loop)与 s01 完全一致。
+"""
+
+import os, subprocess
+from pathlib import Path
+
+try:
+    import readline
+    readline.parse_and_bind('set bind-tty-special-chars off')
+    readline.parse_and_bind('set input-meta on')
+    readline.parse_and_bind('set output-meta on')
+    readline.parse_and_bind('set convert-meta off')
+except ImportError:
+    pass
+
+from anthropic import Anthropic
+from dotenv import load_dotenv
+
+load_dotenv(override=True)
+if os.getenv("ANTHROPIC_BASE_URL"):
+    os.environ.pop("ANTHROPIC_AUTH_TOKEN", None)
+
+WORKDIR = Path.cwd()
+client = Anthropic(base_url=os.getenv("ANTHROPIC_BASE_URL"))
+MODEL = os.environ["MODEL_ID"]
+
+SYSTEM = f"你是位于 {WORKDIR}. 使用工具解决任务。直接行动,不要只解释。"
+
+
+# ═══════════════════════════════════════════════════════════
+#  来自 s01 (未改动)
+# ═══════════════════════════════════════════════════════════
+
+def run_bash(command: str) -> str:
+    dangerous = ["rm -rf /", "sudo", "shutdown", "reboot", "> /dev/"]
+    if any(d in command for d in dangerous):
+        return "错误:危险命令已被拦截"
+    try:
+        r = subprocess.run(command, shell=True, cwd=WORKDIR,
+                           capture_output=True, text=True,
+                           encoding="utf-8", errors="replace", timeout=120)
+        out = (r.stdout + r.stderr).strip()
+        return out[:50000] if out else "(无输出)"
+    except subprocess.TimeoutExpired:
+        return "错误:执行超时(120 秒)"
+    except (FileNotFoundError, OSError) as e:
+        return f"错误:{e}"
+
+
+# ═══════════════════════════════════════════════════════════
+#  新增于 s02: 4 个新工具
+# ═══════════════════════════════════════════════════════════
+
+def safe_path(p: str) -> Path:
+    path = (WORKDIR / p).resolve()
+    if not path.is_relative_to(WORKDIR):
+        raise ValueError(f"路径逃逸出工作区:{p}")
+    return path
+
+
+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} 行更多内容)"]
+        return "\n".join(lines)
+    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}"
+
+
+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}"
+        file_path.write_text(text.replace(old_text, new_text, 1))
+        return f"已编辑 {path}"
+    except Exception as e:
+        return f"错误:{e}"
+
+
+def run_glob(pattern: str) -> str:
+    import glob as g
+    try:
+        results = []
+        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}"
+
+
+# ═══════════════════════════════════════════════════════════
+#  新增于 s02: 工具定义(s01 只有一个 bash,现在扩展到 5 个)
+# ═══════════════════════════════════════════════════════════
+
+TOOLS = [
+    {"name": "bash", "description": "运行一条 shell 命令。",
+     "input_schema": {"type": "object", "properties": {"command": {"type": "string"}}, "required": ["command"]}},
+    {"name": "read_file", "description": "读取文件内容。",
+     "input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "limit": {"type": "integer"}}, "required": ["path"]}},
+    {"name": "write_file", "description": "向文件写入内容。",
+     "input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "content": {"type": "string"}}, "required": ["path", "content"]}},
+    {"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"]}},
+    {"name": "glob", "description": "查找匹配 glob 模式的文件。",
+     "input_schema": {"type": "object", "properties": {"pattern": {"type": "string"}}, "required": ["pattern"]}},
+]
+
+# ═══════════════════════════════════════════════════════════
+#  新增于 s02: 工具分发映射(s01 是硬编码 run_bash,现在改为查表)
+# ═══════════════════════════════════════════════════════════
+
+TOOL_HANDLERS = {
+    "bash": run_bash, "read_file": run_read, "write_file": run_write,
+    "edit_file": run_edit, "glob": run_glob,
+}
+
+
+# ═══════════════════════════════════════════════════════════
+#  agent_loop — 与 s01 结构完全一致,只改了工具执行那部分
+#  s01: output = run_bash(block.input["command"])
+#  s02: output = TOOL_HANDLERS[block.name](**block.input)
+# ═══════════════════════════════════════════════════════════
+
+def agent_loop(messages: list):
+    while True:
+        response = client.messages.create(
+            model=MODEL, system=SYSTEM, messages=messages,
+            tools=TOOLS, max_tokens=8000,
+        )
+        messages.append({"role": "assistant", "content": response.content})
+
+        if response.stop_reason != "tool_use":
+            return
+
+        results = []
+        for block in response.content:
+            if block.type == "tool_use":
+                print(f"\033[33m> {block.name}\033[0m")
+                handler = TOOL_HANDLERS.get(block.name)
+                output = handler(**block.input) if handler else f"未知工具:{block.name}"
+                print(str(output)[:200])
+                results.append({"type": "tool_result", "tool_use_id": block.id, "content": output})
+
+        messages.append({"role": "user", "content": results})
+
+
+if __name__ == "__main__":
+    print("s02: 工具使用 — 在 s01 基础上加了 4 个工具")
+    print("输入问题,回车发送。输入 q 退出。\n")
+
+    history = []
+    while True:
+        try:
+            query = input("\033[36ms02 >> \033[0m")
+        except (EOFError, KeyboardInterrupt):
+            break
+        if query.strip().lower() in ("q", "exit", ""):
+            break
+        history.append({"role": "user", "content": query})
+        agent_loop(history)
+        for block in history[-1]["content"]:
+            if getattr(block, "type", None) == "text":
+                print(block.text)
+        print()

+ 244 - 0
s03_permission/code.py

@@ -0,0 +1,244 @@
+#!/usr/bin/env python3
+"""
+s03_permission.py - 权限系统
+
+在工具执行前插入三道关卡:
+
+    关卡 1:硬性拒绝列表(rm -rf /、sudo 等)
+    关卡 2:规则匹配(是否写到工作区外?是否是破坏性命令?)
+    关卡 3:用户审批(暂停并等待确认)
+
+    +-------+    +--------+    +--------+    +--------+    +------+
+    | Tool  | -> | Gate 1 | -> | Gate 2 | -> | Gate 3 | -> | Exec |
+    | call  |    | deny?  |    | match? |    | allow? |    |      |
+    +-------+    +--------+    +--------+    +--------+    +------+
+         |            |             |             |
+         v            v             v             v
+      (正常)       (拦截)       (询问用户)    (用户拒绝?)
+
+Agent 循环里只新增一行:
+
+    if not check_permission(block):
+        continue
+
+基于 s02(多工具)构建。用法:
+
+    python s03_permission/code.py
+    需要: pip install anthropic python-dotenv + .env 中配置 ANTHROPIC_API_KEY
+"""
+
+import os, subprocess
+from pathlib import Path
+
+try:
+    import readline
+    readline.parse_and_bind('set bind-tty-special-chars off')
+    readline.parse_and_bind('set input-meta on')
+    readline.parse_and_bind('set output-meta on')
+    readline.parse_and_bind('set convert-meta off')
+except ImportError:
+    pass
+
+from anthropic import Anthropic
+from dotenv import load_dotenv
+
+load_dotenv(override=True)
+if os.getenv("ANTHROPIC_BASE_URL"):
+    os.environ.pop("ANTHROPIC_AUTH_TOKEN", None)
+
+WORKDIR = Path.cwd()
+client = Anthropic(base_url=os.getenv("ANTHROPIC_BASE_URL"))
+MODEL = os.environ["MODEL_ID"]
+
+SYSTEM = f"你是位于 {WORKDIR}. 所有破坏性操作都需要用户审批。"
+
+
+# ═══════════════════════════════════════════════════════════
+#  来自 s02 : 工具实现
+# ═══════════════════════════════════════════════════════════
+
+def run_bash(command: str) -> str:
+    try:
+        r = subprocess.run(command, shell=True, cwd=WORKDIR,
+                           capture_output=True, text=True, timeout=120)
+        out = (r.stdout + r.stderr).strip()
+        return out[:50000] if out else "(无输出)"
+    except subprocess.TimeoutExpired:
+        return "错误:执行超时(120 秒)"
+
+
+def run_read(path: str, limit: int | None = None) -> str:
+    try:
+        lines = (WORKDIR / path).resolve().read_text().splitlines()
+        if limit and limit < len(lines):
+            lines = lines[:limit] + [f"... ({len(lines) - limit} 行更多内容)"]
+        return "\n".join(lines)
+    except Exception as e:
+        return f"错误:{e}"
+
+
+def run_write(path: str, content: str) -> str:
+    try:
+        file_path = (WORKDIR / path).resolve()
+        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 = (WORKDIR / path).resolve()
+        text = file_path.read_text()
+        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}"
+
+
+def run_glob(pattern: str) -> str:
+    import glob as g
+    try:
+        results = []
+        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}"
+
+
+# ═══════════════════════════════════════════════════════════
+#  来自 s02 (未改动): 工具定义与分发
+# ═══════════════════════════════════════════════════════════
+
+TOOLS = [
+    {"name": "bash", "description": "运行一条 shell 命令。",
+     "input_schema": {"type": "object", "properties": {"command": {"type": "string"}}, "required": ["command"]}},
+    {"name": "read_file", "description": "读取文件内容。",
+     "input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "limit": {"type": "integer"}}, "required": ["path"]}},
+    {"name": "write_file", "description": "向文件写入内容。",
+     "input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "content": {"type": "string"}}, "required": ["path", "content"]}},
+    {"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"]}},
+    {"name": "glob", "description": "查找匹配 glob 模式的文件。",
+     "input_schema": {"type": "object", "properties": {"pattern": {"type": "string"}}, "required": ["pattern"]}},
+]
+
+TOOL_HANDLERS = {
+    "bash": run_bash, "read_file": run_read, "write_file": run_write,
+    "edit_file": run_edit, "glob": run_glob,
+}
+
+
+# ═══════════════════════════════════════════════════════════
+#  新增于 s03: 三关卡权限流水线
+# ═══════════════════════════════════════════════════════════
+
+# 关卡 1:硬性拒绝列表 — 始终禁止
+DENY_LIST = ["rm -rf /", "sudo", "shutdown", "reboot", "mkfs", "dd if=", "> /dev/sda"]
+
+def check_deny_list(command: str) -> str | None:
+    for pattern in DENY_LIST:
+        if pattern in command:
+            return f"已拦截:'{pattern}' 位于拒绝列表中"
+    return None
+
+
+# 关卡 2:规则匹配 — 根据上下文检查
+PERMISSION_RULES = [
+    {"tools": ["read_file", "write_file", "edit_file"],
+     "check": lambda args: not (WORKDIR / args.get("path", "")).resolve().is_relative_to(WORKDIR),
+     "message": "写入工作区外部路径"},
+    {"tools": ["bash"],
+     "check": lambda args: any(kw in args.get("command", "") for kw in ["rm ", "> /etc/", "chmod 777"]),
+     "message": "可能具有破坏性的命令"},
+]
+
+def check_rules(tool_name: str, args: dict) -> str | None:
+    for rule in PERMISSION_RULES:
+        if tool_name in rule["tools"] and rule["check"](args):
+            return rule["message"]
+    return None
+
+
+# 关卡 3:用户审批 — 规则命中后等待确认
+def ask_user(tool_name: str, args: dict, reason: str) -> str:
+    print(f"\n\033[33m⚠  {reason}\033[0m")
+    print(f"   工具:{tool_name}({args})")
+    choice = input("   是否允许?[y/N] ").strip().lower()
+    return "allow" if choice in ("y", "yes") else "deny"
+
+
+# 流水线:三道关卡串联
+def check_permission(block) -> bool:
+    if block.name == "bash":
+        reason = check_deny_list(block.input.get("command", ""))
+        if reason:
+            print(f"\n\033[31m⛔ {reason}\033[0m")
+            return False
+    reason = check_rules(block.name, block.input)
+    if reason:
+        decision = ask_user(block.name, block.input, reason)
+        if decision == "deny":
+            return False
+    return True
+
+
+# ═══════════════════════════════════════════════════════════
+#  agent_loop — 与以下相同: s02, 插入 check_permission()
+# ═══════════════════════════════════════════════════════════
+
+def agent_loop(messages: list):
+    while True:
+        response = client.messages.create(
+            model=MODEL, system=SYSTEM, messages=messages,
+            tools=TOOLS, max_tokens=8000,
+        )
+        messages.append({"role": "assistant", "content": response.content})
+
+        if response.stop_reason != "tool_use":
+            return
+
+        results = []
+        for block in response.content:
+            if block.type != "tool_use":
+                continue
+
+            print(f"\033[36m> {block.name}\033[0m")
+
+            # s03 变化:执行前先经过权限流水线
+            if not check_permission(block):
+                results.append({"type": "tool_result", "tool_use_id": block.id,
+                                "content": "权限被拒绝."})
+                continue
+
+            handler = TOOL_HANDLERS.get(block.name)
+            output = handler(**block.input) if handler else f"未知工具:{block.name}"
+            print(str(output)[:200])
+            results.append({"type": "tool_result", "tool_use_id": block.id, "content": output})
+
+        messages.append({"role": "user", "content": results})
+
+
+if __name__ == "__main__":
+    print("s03: 权限系统")
+    print("输入问题,回车发送。输入 q 退出。\n")
+
+    history = []
+    while True:
+        try:
+            query = input("\033[36ms03 >> \033[0m")
+        except (EOFError, KeyboardInterrupt):
+            break
+        if query.strip().lower() in ("q", "exit", ""):
+            break
+        history.append({"role": "user", "content": query})
+        agent_loop(history)
+        for block in history[-1]["content"]:
+            if getattr(block, "type", None) == "text":
+                print(block.text)
+        print()

+ 275 - 0
s04_hooks/code.py

@@ -0,0 +1,275 @@
+#!/usr/bin/env python3
+"""
+s04: Hooks — 把扩展逻辑从循环中移出,挂到 Hooks 上。
+
+  用户输入问题
+       │
+       ▼
+  ┌──────────────────┐
+  │ UserPromptSubmit │ ── LLM 调用前触发 trigger_hooks()
+  └────────┬─────────┘
+           ▼
+  ┌────────────┐     ┌─────────────────────────────┐
+  │  messages  │────▶│  LLM (stop_reason=tool_use?)│
+  └────────────┘     │   否 ──▶ Stop hooks ──▶ 退出 │
+                     │   是 ──▶ tool_use block ──┐ │
+                     └────────────────────────────┘ │
+                                                    ▼
+                                          ┌──────────────────┐
+                                          │ trigger_hooks()   │
+                                          │  PreToolUse:      │
+                                          │   permission_hook │
+                                          │   log_hook        │
+                                          └───────┬──────────┘
+                                                  │ (未被拦截)
+                                          ┌───────▼──────────┐
+                                          │ TOOL_HANDLERS[x]  │
+                                          └───────┬──────────┘
+                                                  │
+                                          ┌───────▼──────────┐
+                                          │ trigger_hooks()   │
+                                          │  PostToolUse:     │
+                                          │   large_output    │
+                                          └───────┬──────────┘
+                                                  │
+                                          results ──▶ 回到 messages
+"""
+
+import os, subprocess
+from pathlib import Path
+
+try:
+    import readline
+    readline.parse_and_bind('set bind-tty-special-chars off')
+    readline.parse_and_bind('set input-meta on')
+    readline.parse_and_bind('set output-meta on')
+    readline.parse_and_bind('set convert-meta off')
+except ImportError:
+    pass
+
+from anthropic import Anthropic
+from dotenv import load_dotenv
+
+load_dotenv(override=True)
+if os.getenv("ANTHROPIC_BASE_URL"):
+    os.environ.pop("ANTHROPIC_AUTH_TOKEN", None)
+
+WORKDIR = Path.cwd()
+client = Anthropic(base_url=os.getenv("ANTHROPIC_BASE_URL"))
+MODEL = os.environ["MODEL_ID"]
+
+SYSTEM = f"你是位于 {WORKDIR}. 使用工具解决任务。直接行动,不要只解释。"
+
+
+# ═══════════════════════════════════════════════════════════
+#  来自 s02-s03 : 工具实现
+# ═══════════════════════════════════════════════════════════
+
+def run_bash(command: str) -> str:
+    try:
+        r = subprocess.run(command, shell=True, cwd=WORKDIR,
+                           capture_output=True, text=True, timeout=120)
+        out = (r.stdout + r.stderr).strip()
+        return out[:50000] if out else "(无输出)"
+    except subprocess.TimeoutExpired:
+        return "错误:执行超时(120 秒)"
+
+def run_read(path: str, limit: int | None = None) -> str:
+    try:
+        file_path = (WORKDIR / path).resolve()
+        lines = file_path.read_text().splitlines()
+        if limit and limit < len(lines):
+            lines = lines[:limit] + [f"... ({len(lines) - limit} 行更多内容)"]
+        return "\n".join(lines)
+    except Exception as e:
+        return f"错误:{e}"
+
+def run_write(path: str, content: str) -> str:
+    try:
+        file_path = (WORKDIR / path).resolve()
+        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 = (WORKDIR / path).resolve()
+        text = file_path.read_text()
+        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}"
+
+def run_glob(pattern: str) -> str:
+    import glob as g
+    try:
+        results = []
+        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}"
+
+TOOLS = [
+    {"name": "bash", "description": "运行一条 shell 命令。",
+     "input_schema": {"type": "object", "properties": {"command": {"type": "string"}}, "required": ["command"]}},
+    {"name": "read_file", "description": "读取文件内容。",
+     "input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "limit": {"type": "integer"}}, "required": ["path"]}},
+    {"name": "write_file", "description": "向文件写入内容。",
+     "input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "content": {"type": "string"}}, "required": ["path", "content"]}},
+    {"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"]}},
+    {"name": "glob", "description": "查找匹配 glob 模式的文件。",
+     "input_schema": {"type": "object", "properties": {"pattern": {"type": "string"}}, "required": ["pattern"]}},
+]
+
+TOOL_HANDLERS = {
+    "bash": run_bash, "read_file": run_read, "write_file": run_write,
+    "edit_file": run_edit, "glob": run_glob,
+}
+
+
+# ═══════════════════════════════════════════════════════════
+#  新增于 s04: Hook 系统 (s03 权限逻辑现在通过 Hooks 实现)
+# ═══════════════════════════════════════════════════════════
+
+HOOKS = {"UserPromptSubmit": [], "PreToolUse": [], "PostToolUse": [], "Stop": []}
+
+def register_hook(event: str, callback):
+    HOOKS[event].append(callback)
+
+def trigger_hooks(event: str, *args):
+    for callback in HOOKS[event]:
+        result = callback(*args)
+        if result is not None:  # 教学快捷方式:拦截这个工具调用
+            return result
+    return None
+
+
+# s03 权限检查逻辑,现在封装成 Hook
+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."""
+    if block.name == "bash":
+        for pattern in DENY_LIST:
+            if pattern in block.input.get("command", ""):
+                print(f"\n\033[31m⛔ 已拦截:'{pattern}'\033[0m")
+                return "被拒绝列表拒绝授权"
+        for kw in DESTRUCTIVE:
+            if kw in block.input.get("command", ""):
+                print(f"\n\033[33m⚠  可能具有破坏性的命令\033[0m")
+                print(f"   工具:{block.name}({block.input})")
+                choice = input("   是否允许?[y/N] ").strip().lower()
+                if choice not in ("y", "yes"):
+                    return "用户拒绝授权"
+    if block.name in ("read_file", "write_file", "edit_file"):
+        path = block.input.get("path", "")
+        if not (WORKDIR / path).resolve().is_relative_to(WORKDIR):
+            print(f"\n\033[33m⚠  访问工作区外部路径\033[0m")
+            print(f"   工具:{block.name}({block.input})")
+            choice = input("   是否允许?[y/N] ").strip().lower()
+            if choice not in ("y", "yes"):
+                return "用户拒绝授权"
+    return None
+
+def log_hook(block):
+    """PreToolUse: log every tool call."""
+    args_preview = str(list(block.input.values())[:2])[:60]
+    print(f"\033[90m[HOOK] {block.name}({args_preview})\033[0m")
+    return None
+
+def large_output_hook(block, output):
+    """PostToolUse: warn on large output."""
+    if len(str(output)) > 100000:
+        print(f"\033[33m[HOOK] ⚠ 来自以下工具的大输出:{block.name}: {len(str(output))} 个字符\033[0m")
+    return None
+
+# UserPromptSubmit Hook:在用户输入到达 LLM 前记录它
+def context_inject_hook(query: str):
+    print(f"\033[90m[HOOK] 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")
+    return None
+
+register_hook("UserPromptSubmit", context_inject_hook)
+register_hook("PreToolUse", permission_hook)
+register_hook("PreToolUse", log_hook)
+register_hook("PostToolUse", large_output_hook)
+register_hook("Stop", summary_hook)
+
+
+# ═══════════════════════════════════════════════════════════
+#  agent_loop — 与 s03 结构相同,但没有硬编码检查
+#  s03: if not check_permission(block): ...
+#  s04: if trigger_hooks("PreToolUse", block): ...
+# ═══════════════════════════════════════════════════════════
+
+def agent_loop(messages: list):
+    while True:
+        response = client.messages.create(
+            model=MODEL, system=SYSTEM, messages=messages,
+            tools=TOOLS, max_tokens=8000,
+        )
+        messages.append({"role": "assistant", "content": response.content})
+
+        if response.stop_reason != "tool_use":
+            force = trigger_hooks("Stop", messages)
+            if force:
+                messages.append({"role": "user", "content": force})
+                continue
+            return
+
+        results = []
+        for block in response.content:
+            if block.type != "tool_use":
+                continue
+
+            # s04 变化: Hook 替代硬编码的 check_permission()
+            blocked = trigger_hooks("PreToolUse", block)
+            if blocked:
+                results.append({"type": "tool_result", "tool_use_id": block.id,
+                                "content": str(blocked)})
+                continue
+
+            handler = TOOL_HANDLERS.get(block.name)
+            output = handler(**block.input) if handler else f"未知工具:{block.name}"
+
+            trigger_hooks("PostToolUse", block, output)  # s04: 后置 Hook
+
+            results.append({"type": "tool_result", "tool_use_id": block.id, "content": output})
+
+        messages.append({"role": "user", "content": results})
+
+
+if __name__ == "__main__":
+    print("s04: Hooks — 扩展逻辑挂到 Hooks 上,循环保持干净")
+    print("输入问题后按回车。输入 q 退出。\n")
+
+    history = []
+    while True:
+        try:
+            query = input("\033[36ms04 >> \033[0m")
+        except (EOFError, KeyboardInterrupt):
+            break
+        if query.strip().lower() in ("q", "exit", ""):
+            break
+        trigger_hooks("UserPromptSubmit", query)
+        history.append({"role": "user", "content": query})
+        agent_loop(history)
+        for block in history[-1]["content"]:
+            if getattr(block, "type", None) == "text":
+                print(block.text)
+        print()

+ 302 - 0
s05_todo_write/code.py

@@ -0,0 +1,302 @@
+#!/usr/bin/env python3
+"""
+s05: TodoWrite — 在 s04 Hooks 基础上增加规划工具。
+
+  +---------+      +-------+      +------------------+
+  |  User   | ---> |  LLM  | ---> | TOOL_HANDLERS    |
+  | prompt  |      |       |      |  bash            |
+  +---------+      +---+---+      |  read_file       |
+                        ^         |  write_file      |
+                        | result  |  edit_file       |
+                        +---------+  glob            |
+                                      todo_write ← 新增
+                                   +------------------+
+                                        |
+                         内存中的 current_todos
+                                        |
+                        if rounds_since_todo >= 3:
+                          注入 <reminder>
+
+相对 s04 的变化:
+  + todo_write 工具 + run_todo_write() 实现
+  + 催办提醒(3 轮未更新 todo 后注入提醒)
+  + SYSTEM 提示词包含“先规划再执行”的指导
+  + agent_loop 中增加 rounds_since_todo 计数器
+  循环不变:新工具通过 TOOL_HANDLERS 自动分发。
+
+运行: python s05_todo_write/code.py
+需要: pip install anthropic python-dotenv + .env 中配置 ANTHROPIC_API_KEY
+"""
+
+import ast, json, os, subprocess
+from pathlib import Path
+
+try:
+    import readline
+    readline.parse_and_bind('set bind-tty-special-chars off')
+except ImportError:
+    pass
+
+from anthropic import Anthropic
+from dotenv import load_dotenv
+
+load_dotenv(override=True)
+if os.getenv("ANTHROPIC_BASE_URL"):
+    os.environ.pop("ANTHROPIC_AUTH_TOKEN", None)
+
+WORKDIR = Path.cwd()
+client = Anthropic(base_url=os.getenv("ANTHROPIC_BASE_URL"))
+MODEL = os.environ["MODEL_ID"]
+CURRENT_TODOS: list[dict] = []
+
+# s05 变化: SYSTEM 提示词增加规划指导
+SYSTEM = (
+    f"你是位于 {WORKDIR}. "
+    "开始任何多步骤任务前,使用 todo_write 规划步骤。"
+    "执行过程中持续更新状态。"
+)
+
+
+# ═══════════════════════════════════════════════════════════
+#  来自 s02-s04 (未改动): 工具实现
+# ═══════════════════════════════════════════════════════════
+
+def safe_path(p: str) -> Path:
+    path = (WORKDIR / p).resolve()
+    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)
+        out = (r.stdout + r.stderr).strip()
+        return out[:50000] if out else "(无输出)"
+    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} 行更多内容)"]
+        return "\n".join(lines)
+    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}"
+
+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}"
+        file_path.write_text(text.replace(old_text, new_text, 1))
+        return f"已编辑 {path}"
+    except Exception as e:
+        return f"错误:{e}"
+
+def run_glob(pattern: str) -> str:
+    import glob as g
+    try:
+        results = []
+        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}"
+
+
+# ═══════════════════════════════════════════════════════════
+#  新增于 s05: todo_write 工具 — 只做规划,不执行
+# ═══════════════════════════════════════════════════════════
+
+def _normalize_todos(todos):
+    if isinstance(todos, str):
+        try:
+            个待办 = json.loads(todos)
+        except json.JSONDecodeError:
+            try:
+                个待办 = ast.literal_eval(todos)
+            except (SyntaxError, ValueError):
+                return None, "错误:todos 必须是列表或 JSON 数组字符串"
+    if not isinstance(todos, list):
+        return None, "错误:todos 必须是列表"
+    for i, t in enumerate(todos):
+        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
+
+def run_todo_write(todos: list) -> str:
+    global CURRENT_TODOS
+    个待办, error = _normalize_todos(todos)
+    if error:
+        return error
+    CURRENT_TODOS = 个待办
+    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"]]
+        lines.append(f"  [{icon}] {t['content']}")
+    print("\n".join(lines))
+    return f"已更新 {len(CURRENT_TODOS)} 个任务"
+
+TOOLS = [
+    {"name": "bash", "description": "运行一条 shell 命令。",
+     "input_schema": {"type": "object", "properties": {"command": {"type": "string"}}, "required": ["command"]}},
+    {"name": "read_file", "description": "读取文件内容。",
+     "input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "limit": {"type": "integer"}}, "required": ["path"]}},
+    {"name": "write_file", "description": "向文件写入内容。",
+     "input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "content": {"type": "string"}}, "required": ["path", "content"]}},
+    {"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"]}},
+    {"name": "glob", "description": "查找匹配 glob 模式的文件。",
+     "input_schema": {"type": "object", "properties": {"pattern": {"type": "string"}}, "required": ["pattern"]}},
+    # s05: 新工具
+    {"name": "todo_write", "description": "为当前编码会话创建并维护任务清单。",
+     "input_schema": {"type": "object", "properties": {"todos": {"type": "array", "items": {"type": "object", "properties": {"content": {"type": "string"}, "status": {"type": "string", "enum": ["pending", "in_progress", "completed"]}}, "required": ["content", "status"]}}}, "required": ["todos"]}},
+]
+
+TOOL_HANDLERS = {
+    "bash": run_bash, "read_file": run_read, "write_file": run_write,
+    "edit_file": run_edit, "glob": run_glob, "todo_write": run_todo_write,
+}
+
+
+# ═══════════════════════════════════════════════════════════
+#  来自 s04 (未改动): Hook 系统
+# ═══════════════════════════════════════════════════════════
+
+HOOKS = {"UserPromptSubmit": [], "PreToolUse": [], "PostToolUse": [], "Stop": []}
+
+def register_hook(event: str, callback):
+    HOOKS[event].append(callback)
+
+def trigger_hooks(event: str, *args):
+    for callback in HOOKS[event]:
+        result = callback(*args)
+        if result is not None:
+            return result
+    return None
+
+# s04 保留 Hooks
+DENY_LIST = ["rm -rf /", "sudo", "shutdown", "reboot", "mkfs", "dd if="]
+
+def permission_hook(block):
+    """PreToolUse: deny list check."""
+    if block.name == "bash":
+        for p in DENY_LIST:
+            if p in block.input.get("command", ""):
+                print(f"\n\033[31m⛔ 已拦截:'{p}'\033[0m")
+                return "权限被拒绝"
+    return None
+
+def log_hook(block):
+    """PreToolUse:记录工具调用。"""
+    print(f"\033[90m[HOOK] {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")
+    return None
+
+def summary_hook(messages: list):
+    """Stop:打印工具调用次数。"""
+    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")
+    return None
+
+register_hook("UserPromptSubmit", context_inject_hook)
+register_hook("PreToolUse", permission_hook)
+register_hook("PreToolUse", log_hook)
+register_hook("Stop", summary_hook)
+
+
+# ═══════════════════════════════════════════════════════════
+#  agent_loop — 与以下相同: s04 + 催办提醒 计数器
+# ═══════════════════════════════════════════════════════════
+
+def agent_loop(messages: list):
+    rounds_since_todo = 0
+    while True:
+        # s05: 催办提醒 — 如果模型连续 3 轮没有更新待办,则注入提醒
+        if rounds_since_todo >= 3 and messages:
+            messages.append({"role": "user",
+                             "content": "<reminder>请更新你的待办事项。</reminder>"})
+            rounds_since_todo = 0
+
+        response = client.messages.create(
+            model=MODEL, system=SYSTEM, messages=messages,
+            tools=TOOLS, max_tokens=8000,
+        )
+        messages.append({"role": "assistant", "content": response.content})
+
+        if response.stop_reason != "tool_use":
+            force = trigger_hooks("Stop", messages)
+            if force:
+                messages.append({"role": "user", "content": force})
+                continue
+            return
+
+        rounds_since_todo += 1
+        results = []
+        for block in response.content:
+            if block.type != "tool_use":
+                continue
+
+            blocked = trigger_hooks("PreToolUse", block)
+            if blocked:
+                results.append({"type": "tool_result", "tool_use_id": block.id,
+                                "content": str(blocked)})
+                continue
+
+            handler = TOOL_HANDLERS.get(block.name)
+            output = handler(**block.input) if handler else f"未知工具:{block.name}"
+
+            trigger_hooks("PostToolUse", block, output)
+
+            # s05: 调用 todo_write 时重置催办计数器
+            if block.name == "todo_write":
+                rounds_since_todo = 0
+
+            results.append({"type": "tool_result", "tool_use_id": block.id,
+                            "content": output})
+
+        messages.append({"role": "user", "content": results})
+
+
+if __name__ == "__main__":
+    print("s05: TodoWrite — 先规划再执行,忘记会提醒")
+    print("输入问题后按回车。输入 q 退出。\n")
+
+    history = []
+    while True:
+        try:
+            query = input("\033[36ms05 >> \033[0m")
+        except (EOFError, KeyboardInterrupt):
+            break
+        if query.strip().lower() in ("q", "exit", ""):
+            break
+        trigger_hooks("UserPromptSubmit", query)
+        history.append({"role": "user", "content": query})
+        agent_loop(history)
+        for block in history[-1]["content"]:
+            if getattr(block, "type", None) == "text":
+                print(block.text)
+        print()

+ 6 - 0
s05_todo_write/example/hello.py

@@ -0,0 +1,6 @@
+def greet(name):
+    message = "Hello, " + name
+    print(message)
+
+
+greet("Claude")

+ 381 - 0
s06_subagent/code.py

@@ -0,0 +1,381 @@
+#!/usr/bin/env python3
+"""
+s06: 子 Agent — 用全新的 messages[] 启动子 Agent,实现上下文隔离。
+
+  父 Agent                              子 Agent
+  +------------------+                  +------------------+
+  | messages=[...]   |                  | messages=[task]  | <-- 全新上下文
+  |                  |   分发           |                  |
+  | tool: task       | ---------------> | 自己的 while 循环 |
+  |   prompt="..."   |                  |   bash/read/...  |
+  |                  |   只返回摘要     |   (最多 30 轮)    |
+  | result = "..."   | <--------------- | 返回最后文本      |
+  +------------------+                  +------------------+
+        ^                                      |
+        |          中间结果会被丢弃             |
+        +--------------------------------------+
+
+  子 Agent 工具:bash、read、write、edit、glob(没有 task,避免递归)
+
+相对 s05 的变化:
+  + task 工具 + 使用全新 messages[] 的 spawn_subagent()
+  + 安全上限:每个子 Agent 最多 30 轮
+  + extract_text() 辅助函数
+  子 Agent 不能再启动子子 Agent(sub_tools 里没有 task 工具)。
+  主循环不变:task 通过 TOOL_HANDLERS 自动分发。
+
+运行: python s06_subagent/code.py
+需要: pip install anthropic python-dotenv + .env 中配置 ANTHROPIC_API_KEY
+"""
+
+import ast, json, os, subprocess
+from pathlib import Path
+
+try:
+    import readline
+    readline.parse_and_bind('set bind-tty-special-chars off')
+except ImportError:
+    pass
+
+from anthropic import Anthropic
+from dotenv import load_dotenv
+
+load_dotenv(override=True)
+if os.getenv("ANTHROPIC_BASE_URL"):
+    os.environ.pop("ANTHROPIC_AUTH_TOKEN", None)
+
+WORKDIR = Path.cwd()
+client = Anthropic(base_url=os.getenv("ANTHROPIC_BASE_URL"))
+MODEL = os.environ["MODEL_ID"]
+CURRENT_TODOS: list[dict] = []
+
+SYSTEM = (
+    f"你是位于 {WORKDIR}. "
+    "遇到复杂子问题时,使用 task 工具启动一个子 Agent。"
+)
+
+# s06: 子 Agent 使用自己的系统提示词 — 没有 task,不递归
+SUB_SYSTEM = (
+    f"你是位于 {WORKDIR}. "
+    "完成交给你的任务,然后返回简洁摘要。"
+    "不要继续委派。"
+)
+
+
+# ═══════════════════════════════════════════════════════════
+#  来自 s02-s05 (未改动): 工具实现
+# ═══════════════════════════════════════════════════════════
+
+def safe_path(p: str) -> Path:
+    path = (WORKDIR / p).resolve()
+    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)
+        out = (r.stdout + r.stderr).strip()
+        return out[:50000] if out else "(无输出)"
+    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} 行更多内容)"]
+        return "\n".join(lines)
+    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}"
+
+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}"
+        file_path.write_text(text.replace(old_text, new_text, 1))
+        return f"已编辑 {path}"
+    except Exception as e:
+        return f"错误:{e}"
+
+def run_glob(pattern: str) -> str:
+    import glob as g
+    try:
+        results = []
+        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}"
+
+def _normalize_todos(todos):
+    if isinstance(todos, str):
+        try:
+            个待办 = json.loads(todos)
+        except json.JSONDecodeError:
+            try:
+                个待办 = ast.literal_eval(todos)
+            except (SyntaxError, ValueError):
+                return None, "错误:todos 必须是列表或 JSON 数组字符串"
+    if not isinstance(todos, list):
+        return None, "错误:todos 必须是列表"
+    for i, t in enumerate(todos):
+        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
+
+def run_todo_write(todos: list) -> str:
+    global CURRENT_TODOS
+    个待办, error = _normalize_todos(todos)
+    if error:
+        return error
+    CURRENT_TODOS = 个待办
+    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"]]
+        lines.append(f"  [{icon}] {t['content']}")
+    print("\n".join(lines))
+    return f"已更新 {len(CURRENT_TODOS)} 个任务"
+
+TOOLS = [
+    {"name": "bash", "description": "运行一条 shell 命令。",
+     "input_schema": {"type": "object", "properties": {"command": {"type": "string"}}, "required": ["command"]}},
+    {"name": "read_file", "description": "读取文件内容。",
+     "input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "limit": {"type": "integer"}}, "required": ["path"]}},
+    {"name": "write_file", "description": "向文件写入内容。",
+     "input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "content": {"type": "string"}}, "required": ["path", "content"]}},
+    {"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"]}},
+    {"name": "glob", "description": "查找匹配 glob 模式的文件。",
+     "input_schema": {"type": "object", "properties": {"pattern": {"type": "string"}}, "required": ["pattern"]}},
+    {"name": "todo_write", "description": "为当前编码会话创建并维护任务清单。",
+     "input_schema": {"type": "object", "properties": {"todos": {"type": "array", "items": {"type": "object", "properties": {"content": {"type": "string"}, "status": {"type": "string", "enum": ["pending", "in_progress", "completed"]}}, "required": ["content", "status"]}}}, "required": ["todos"]}},
+]
+
+TOOL_HANDLERS = {
+    "bash": run_bash, "read_file": run_read, "write_file": run_write,
+    "edit_file": run_edit, "glob": run_glob, "todo_write": run_todo_write,
+}
+
+
+# ═══════════════════════════════════════════════════════════
+#  新增于 s06: 子 Agent — 全新 messages[],只返回摘要
+# ═══════════════════════════════════════════════════════════
+
+SUB_TOOLS = [
+    {"name": "bash", "description": "运行一条 shell 命令。",
+     "input_schema": {"type": "object", "properties": {"command": {"type": "string"}}, "required": ["command"]}},
+    {"name": "read_file", "description": "读取文件内容。",
+     "input_schema": {"type": "object", "properties": {"path": {"type": "string"}}, "required": ["path"]}},
+    {"name": "write_file", "description": "向文件写入内容。",
+     "input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "content": {"type": "string"}}, "required": ["path", "content"]}},
+    {"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"]}},
+    {"name": "glob", "description": "查找匹配 glob 模式的文件。",
+     "input_schema": {"type": "object", "properties": {"pattern": {"type": "string"}}, "required": ["pattern"]}},
+]
+# 没有 "task" 工具 — 防止递归启动
+
+SUB_HANDLERS = {
+    "bash": run_bash, "read_file": run_read, "write_file": run_write,
+    "edit_file": run_edit, "glob": run_glob,
+}
+
+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."""
+    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,
+        )
+        messages.append({"role": "assistant", "content": response.content})
+        if response.stop_reason != "tool_use":
+            break
+        results = []
+        for block in response.content:
+            if block.type == "tool_use":
+                # 问题 1:子 Agent 也运行 Hooks(权限同样生效)
+                blocked = trigger_hooks("PreToolUse", block)
+                if blocked:
+                    results.append({"type": "tool_result", "tool_use_id": block.id,
+                                    "content": str(blocked)})
+                    continue
+                handler = SUB_HANDLERS.get(block.name)
+                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})
+        messages.append({"role": "user", "content": results})
+
+    # 问题 5:如果在 tool_use 期间触发安全上限,则使用兜底逻辑
+    result = extract_text(messages[-1]["content"])
+    if not result:
+        # 最后一条消息是 tool_result,向前查找 assistant 文本
+        for msg in reversed(messages):
+            if msg["role"] == "assistant":
+                result = extract_text(msg["content"])
+                if result:
+                    break
+        if not result:
+            result = "子 Agent stopped 等待 30 turns without final answer."
+    print(f"\033[35m[子 Agent 已完成]\033[0m")
+    return result  # 只保留摘要,完整消息历史会被丢弃
+
+# 把 task 工具加入父 Agent 的工具列表
+TOOLS.append({
+    "name": "task",
+    "description": "启动一个子 Agent 处理复杂子任务。只返回最终结论。",
+    "input_schema": {"type": "object", "properties": {"description": {"type": "string"}}, "required": ["description"]},
+})
+TOOL_HANDLERS["task"] = spawn_subagent
+
+
+# ═══════════════════════════════════════════════════════════
+#  来自 s04 (未改动): Hook 系统
+# ═══════════════════════════════════════════════════════════
+
+HOOKS = {"UserPromptSubmit": [], "PreToolUse": [], "PostToolUse": [], "Stop": []}
+
+def register_hook(event: str, callback):
+    HOOKS[event].append(callback)
+
+def trigger_hooks(event: str, *args):
+    for callback in HOOKS[event]:
+        result = callback(*args)
+        if result is not None:
+            return result
+    return None
+
+DENY_LIST = ["rm -rf /", "sudo", "shutdown", "reboot", "mkfs", "dd if="]
+
+def permission_hook(block):
+    """PreToolUse: deny list check."""
+    if block.name == "bash":
+        for p in DENY_LIST:
+            if p in block.input.get("command", ""):
+                print(f"\n\033[31m⛔ 已拦截:'{p}'\033[0m")
+                return "权限被拒绝"
+    return None
+
+def log_hook(block):
+    """PreToolUse:记录工具调用。"""
+    print(f"\033[90m[HOOK] {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")
+    return None
+
+def summary_hook(messages: list):
+    """Stop:打印工具调用次数。"""
+    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")
+    return None
+
+register_hook("UserPromptSubmit", context_inject_hook)
+register_hook("PreToolUse", permission_hook)
+register_hook("PreToolUse", log_hook)
+register_hook("Stop", summary_hook)
+
+
+# ═══════════════════════════════════════════════════════════
+#  agent_loop — 与以下相同: s05 + 催办提醒, task 会自动分发
+# ═══════════════════════════════════════════════════════════
+
+def agent_loop(messages: list):
+    rounds_since_todo = 0
+    while True:
+        # s05: 催办提醒
+        if rounds_since_todo >= 3 and messages:
+            messages.append({"role": "user",
+                             "content": "<reminder>请更新你的待办事项。</reminder>"})
+            rounds_since_todo = 0
+
+        response = client.messages.create(
+            model=MODEL, system=SYSTEM, messages=messages,
+            tools=TOOLS, max_tokens=8000,
+        )
+        messages.append({"role": "assistant", "content": response.content})
+
+        if response.stop_reason != "tool_use":
+            force = trigger_hooks("Stop", messages)
+            if force:
+                messages.append({"role": "user", "content": force})
+                continue
+            return
+
+        rounds_since_todo += 1
+        results = []
+        for block in response.content:
+            if block.type != "tool_use":
+                continue
+
+            blocked = trigger_hooks("PreToolUse", block)
+            if blocked:
+                results.append({"type": "tool_result", "tool_use_id": block.id,
+                                "content": str(blocked)})
+                continue
+
+            handler = TOOL_HANDLERS.get(block.name)
+            output = handler(**block.input) if handler else f"未知工具:{block.name}"
+
+            trigger_hooks("PostToolUse", block, output)
+
+            if block.name == "todo_write":
+                rounds_since_todo = 0
+
+            results.append({"type": "tool_result", "tool_use_id": block.id,
+                            "content": output})
+
+        messages.append({"role": "user", "content": results})
+
+
+if __name__ == "__main__":
+    print("s06: 子 Agent — spawn sub-agents with 全新上下文, summary only")
+    print("输入问题后按回车。输入 q 退出。\n")
+
+    history = []
+    while True:
+        try:
+            query = input("\033[36ms06 >> \033[0m")
+        except (EOFError, KeyboardInterrupt):
+            break
+        if query.strip().lower() in ("q", "exit", ""):
+            break
+        trigger_hooks("UserPromptSubmit", query)
+        history.append({"role": "user", "content": query})
+        agent_loop(history)
+        for block in history[-1]["content"]:
+            if getattr(block, "type", None) == "text":
+                print(block.text)
+        print()

+ 424 - 0
s07_skill_loading/code.py

@@ -0,0 +1,424 @@
+#!/usr/bin/env python3
+"""
+s07: 技能加载 — 两级按需知识注入。
+
+  第 1 层(便宜,始终存在):
+    SYSTEM 提示词包含技能名称 + 单行描述(每个技能约 100 tokens)
+    "可用技能: agent-builder, code-review, mcp-builder, pdf"
+
+  第 2 层(昂贵,按需加载):
+    Agent 调用 load_skill("code-review") → 完整 SKILL.md 内容
+    通过 tool_result 注入(每个技能约 2000 tokens)
+
+  skills/
+    agent-builder/SKILL.md
+    code-review/SKILL.md
+    mcp-builder/SKILL.md
+    pdf/SKILL.md
+
+相对 s06 的变化:
+  + build_system() — 启动时扫描 skills/ 目录,把目录注入 SYSTEM
+  + load_skill(name) — 通过 tool_result 返回完整 SKILL.md 内容
+  + SKILLS_DIR 配置
+  循环不变:load_skill 通过 TOOL_HANDLERS 自动分发。
+
+运行: python s07_skill_loading/code.py
+需要: pip install anthropic python-dotenv pyyaml + .env 中配置 ANTHROPIC_API_KEY
+"""
+
+import ast, json, os, subprocess
+from pathlib import Path
+import yaml
+
+try:
+    import readline
+    readline.parse_and_bind('set bind-tty-special-chars off')
+except ImportError:
+    pass
+
+from anthropic import Anthropic
+from dotenv import load_dotenv
+
+load_dotenv(override=True)
+if os.getenv("ANTHROPIC_BASE_URL"):
+    os.environ.pop("ANTHROPIC_AUTH_TOKEN", None)
+
+WORKDIR = Path.cwd()
+SKILLS_DIR = WORKDIR / "skills"
+client = Anthropic(base_url=os.getenv("ANTHROPIC_BASE_URL"))
+MODEL = os.environ["MODEL_ID"]
+CURRENT_TODOS: list[dict] = []
+
+# s07: 技能目录扫描 (供下方 build_system 使用)
+def _parse_frontmatter(text: str) -> tuple[dict, str]:
+    """Parse YAML frontmatter from SKILL.md. Returns (meta, body)."""
+    if not text.startswith("---"):
+        return {}, text
+    parts = text.split("---", 2)
+    if len(parts) < 3:
+        return {}, text
+    try:
+        meta = yaml.safe_load(parts[1]) or {}
+    except yaml.YAMLError:
+        meta = {}
+    return meta, parts[2].strip()
+
+# 启动时构建技能注册表 (用于在 load_skill 中安全查找)
+SKILL_REGISTRY: dict[str, dict] = {}
+
+def _scan_skills():
+    """Scan skills/ dir, populate SKILL_REGISTRY with name/description/content."""
+    if not SKILLS_DIR.exists():
+        return
+    for d in sorted(SKILLS_DIR.iterdir()):
+        if not d.is_dir():
+            continue
+        manifest = d / "SKILL.md"
+        if manifest.exists():
+            raw = manifest.read_text()
+            meta, body = _parse_frontmatter(raw)
+            name = meta.get("name", d.name)
+            desc = meta.get("description", raw.split("\n")[0].lstrip("#").strip())
+            SKILL_REGISTRY[name] = {"name": name, "description": desc, "content": raw}
+
+_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."""
+    catalog = list_skills()
+    return (
+        f"你是位于 {WORKDIR}. "
+        f"可用技能:\n{catalog}\n"
+        "需要时使用 load_skill 获取完整详情。"
+    )
+
+SYSTEM = build_system()
+
+# s07: 子 Agent 使用自己的系统提示词 — 不加载技能,也没有 task
+SUB_SYSTEM = (
+    f"你是位于 {WORKDIR}. "
+    "完成交给你的任务,然后返回简洁摘要。"
+    "不要继续委派。"
+)
+
+
+# ═══════════════════════════════════════════════════════════
+#  来自 s02-s06 (未改动): 工具实现
+# ═══════════════════════════════════════════════════════════
+
+def safe_path(p: str) -> Path:
+    path = (WORKDIR / p).resolve()
+    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)
+        out = (r.stdout + r.stderr).strip()
+        return out[:50000] if out else "(无输出)"
+    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} 行更多内容)"]
+        return "\n".join(lines)
+    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}"
+
+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}"
+        file_path.write_text(text.replace(old_text, new_text, 1))
+        return f"已编辑 {path}"
+    except Exception as e:
+        return f"错误:{e}"
+
+def run_glob(pattern: str) -> str:
+    import glob as g
+    try:
+        results = []
+        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}"
+
+def _normalize_todos(todos):
+    if isinstance(todos, str):
+        try:
+            个待办 = json.loads(todos)
+        except json.JSONDecodeError:
+            try:
+                个待办 = ast.literal_eval(todos)
+            except (SyntaxError, ValueError):
+                return None, "错误:todos 必须是列表或 JSON 数组字符串"
+    if not isinstance(todos, list):
+        return None, "错误:todos 必须是列表"
+    for i, t in enumerate(todos):
+        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
+
+def run_todo_write(todos: list) -> str:
+    global CURRENT_TODOS
+    个待办, error = _normalize_todos(todos)
+    if error:
+        return error
+    CURRENT_TODOS = 个待办
+    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"]]
+        lines.append(f"  [{icon}] {t['content']}")
+    print("\n".join(lines))
+    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")
+
+
+# ═══════════════════════════════════════════════════════════
+#  来自 s06 (未改动): 子 Agent
+# ═══════════════════════════════════════════════════════════
+
+SUB_TOOLS = [
+    {"name": "bash", "description": "运行一条 shell 命令。",
+     "input_schema": {"type": "object", "properties": {"command": {"type": "string"}}, "required": ["command"]}},
+    {"name": "read_file", "description": "读取文件内容。",
+     "input_schema": {"type": "object", "properties": {"path": {"type": "string"}}, "required": ["path"]}},
+    {"name": "write_file", "description": "向文件写入内容。",
+     "input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "content": {"type": "string"}}, "required": ["path", "content"]}},
+    {"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"]}},
+    {"name": "glob", "description": "查找匹配 glob 模式的文件。",
+     "input_schema": {"type": "object", "properties": {"pattern": {"type": "string"}}, "required": ["pattern"]}},
+]
+SUB_HANDLERS = {"bash": run_bash, "read_file": run_read, "write_file": run_write,
+                "edit_file": run_edit, "glob": run_glob}
+
+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)
+        messages.append({"role": "assistant", "content": response.content})
+        if response.stop_reason != "tool_use":
+            break
+        results = []
+        for block in response.content:
+            if block.type == "tool_use":
+                blocked = trigger_hooks("PreToolUse", block)
+                if blocked:
+                    results.append({"type": "tool_result", "tool_use_id": block.id,
+                                    "content": str(blocked)})
+                    continue
+                handler = SUB_HANDLERS.get(block.name)
+                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})
+        messages.append({"role": "user", "content": results})
+    result = extract_text(messages[-1]["content"])
+    if not result:
+        for msg in reversed(messages):
+            if msg["role"] == "assistant":
+                result = extract_text(msg["content"])
+                if result:
+                    break
+        if not result:
+            result = "子 Agent stopped 等待 30 turns without final answer."
+    print(f"\033[35m[子 Agent 已完成]\033[0m")
+    return result
+
+
+# ═══════════════════════════════════════════════════════════
+#  新增于 s07: load_skill — 运行时加载完整内容
+# ═══════════════════════════════════════════════════════════
+
+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}"
+    return skill["content"]
+
+
+# ═══════════════════════════════════════════════════════════
+#  工具注册表 — 来自 s02-s07 的全部工具
+# ═══════════════════════════════════════════════════════════
+
+TOOLS = [
+    {"name": "bash", "description": "运行一条 shell 命令。",
+     "input_schema": {"type": "object", "properties": {"command": {"type": "string"}}, "required": ["command"]}},
+    {"name": "read_file", "description": "读取文件内容。",
+     "input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "limit": {"type": "integer"}}, "required": ["path"]}},
+    {"name": "write_file", "description": "向文件写入内容。",
+     "input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "content": {"type": "string"}}, "required": ["path", "content"]}},
+    {"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"]}},
+    {"name": "glob", "description": "查找匹配 glob 模式的文件。",
+     "input_schema": {"type": "object", "properties": {"pattern": {"type": "string"}}, "required": ["pattern"]}},
+    {"name": "todo_write", "description": "为当前编码会话创建并维护任务清单。",
+     "input_schema": {"type": "object", "properties": {"todos": {"type": "array", "items": {"type": "object", "properties": {"content": {"type": "string"}, "status": {"type": "string", "enum": ["pending", "in_progress", "completed"]}}, "required": ["content", "status"]}}}, "required": ["todos"]}},
+    {"name": "task", "description": "启动一个子 Agent 处理复杂子任务。只返回最终结论。",
+     "input_schema": {"type": "object", "properties": {"description": {"type": "string"}}, "required": ["description"]}},
+    # s07: 技能工具 (目录已在 SYSTEM 提示词中,此处加载完整内容)
+    {"name": "load_skill", "description": "按名称加载某个技能的完整内容。",
+     "input_schema": {"type": "object", "properties": {"name": {"type": "string"}}, "required": ["name"]}},
+]
+
+TOOL_HANDLERS = {
+    "bash": run_bash, "read_file": run_read, "write_file": run_write,
+    "edit_file": run_edit, "glob": run_glob, "todo_write": run_todo_write,
+    "task": spawn_subagent, "load_skill": load_skill,
+}
+
+
+# ═══════════════════════════════════════════════════════════
+#  来自 s04 (未改动): Hook 系统
+# ═══════════════════════════════════════════════════════════
+
+HOOKS = {"UserPromptSubmit": [], "PreToolUse": [], "PostToolUse": [], "Stop": []}
+
+def register_hook(event: str, callback):
+    HOOKS[event].append(callback)
+
+def trigger_hooks(event: str, *args):
+    for callback in HOOKS[event]:
+        result = callback(*args)
+        if result is not None:
+            return result
+    return None
+
+DENY_LIST = ["rm -rf /", "sudo", "shutdown", "reboot", "mkfs", "dd if="]
+
+def permission_hook(block):
+    if block.name == "bash":
+        for p in DENY_LIST:
+            if p in block.input.get("command", ""):
+                print(f"\n\033[31m⛔ 已拦截:'{p}'\033[0m")
+                return "权限被拒绝"
+    return None
+
+def log_hook(block):
+    print(f"\033[90m[HOOK] {block.name}\033[0m")
+    return None
+
+def context_inject_hook(query: str):
+    print(f"\033[90m[HOOK] 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")
+    return None
+
+register_hook("UserPromptSubmit", context_inject_hook)
+register_hook("PreToolUse", permission_hook)
+register_hook("PreToolUse", log_hook)
+register_hook("Stop", summary_hook)
+
+
+# ═══════════════════════════════════════════════════════════
+#  agent_loop — 与以下相同: s05-s06 + 催办提醒
+# ═══════════════════════════════════════════════════════════
+
+def agent_loop(messages: list):
+    rounds_since_todo = 0
+    while True:
+        if rounds_since_todo >= 3 and messages:
+            messages.append({"role": "user",
+                             "content": "<reminder>请更新你的待办事项。</reminder>"})
+            rounds_since_todo = 0
+            
+        response = client.messages.create(
+            model=MODEL, system=SYSTEM, messages=messages,
+            tools=TOOLS, max_tokens=8000,
+        )
+        messages.append({"role": "assistant", "content": response.content})
+
+        if response.stop_reason != "tool_use":
+            force = trigger_hooks("Stop", messages)
+            if force:
+                messages.append({"role": "user", "content": force})
+                continue
+            return
+
+        rounds_since_todo += 1
+        results = []
+        for block in response.content:
+            if block.type != "tool_use":
+                continue
+
+            blocked = trigger_hooks("PreToolUse", block)
+            if blocked:
+                results.append({"type": "tool_result", "tool_use_id": block.id,
+                                "content": str(blocked)})
+                continue
+
+            handler = TOOL_HANDLERS.get(block.name)
+            output = handler(**block.input) if handler else f"未知工具:{block.name}"
+
+            trigger_hooks("PostToolUse", block, output)
+
+            if block.name == "todo_write":
+                rounds_since_todo = 0
+
+            results.append({"type": "tool_result", "tool_use_id": block.id,
+                            "content": output})
+
+        messages.append({"role": "user", "content": results})
+
+
+if __name__ == "__main__":
+    print("s07: 技能加载 — 目录进 SYSTEM,内容按需加载")
+    print("输入问题后按回车。输入 q 退出。\n")
+
+    history = []
+    while True:
+        try:
+            query = input("\033[36ms07 >> \033[0m")
+        except (EOFError, KeyboardInterrupt):
+            break
+        if query.strip().lower() in ("q", "exit", ""):
+            break
+        trigger_hooks("UserPromptSubmit", query)
+        history.append({"role": "user", "content": query})
+        agent_loop(history)
+        for block in history[-1]["content"]:
+            if getattr(block, "type", None) == "text":
+                print(block.text)
+        print()

+ 524 - 0
s08_context_compact/code.py

@@ -0,0 +1,524 @@
+#!/usr/bin/env python3
+"""
+s08_context_compact.py - 上下文压缩
+
+在调用 LLM 前插入四层压缩流水线:
+
+    L1: snip_compact       — 消息数量 > 50 时裁掉中间消息
+    L2: micro_compact      — 用占位符替换旧的 tool_results
+    L3: tool_result_budget — 把大型结果持久化到磁盘
+    L4: compact_history    — LLM 完整摘要(1 次 API 调用)
+
+    应急:reactive_compact — 当 API 仍然返回 prompt_too_long 时触发
+
+    ┌─────────────────────────────────────────────────────────────┐
+    │  messages[]                                                 │
+    │    ↓                                                        │
+    │  L3 budget ─→ L1 snip ─→ L2 micro ─→ [token > threshold?]  │
+    │                                      ├─ 否  → LLM          │
+    │                                      └─ 是  → L4 summary   │
+    │                                              ↓              │
+    │                                          LLM 调用           │
+    │                                    [prompt_too_long?]        │
+    │                                      └─ 是  → reactive      │
+    └─────────────────────────────────────────────────────────────┘
+
+核心原则:先便宜,后昂贵。
+执行顺序匹配 CC 源码:budget → snip → micro → auto。
+
+基于 s07(技能加载)构建。用法:
+
+    python s08_context_compact/code.py
+    需要: pip install anthropic python-dotenv + .env 中配置 ANTHROPIC_API_KEY
+"""
+
+import ast, json, os, subprocess, time
+from pathlib import Path
+
+try:
+    import readline
+    readline.parse_and_bind('set bind-tty-special-chars off')
+except ImportError:
+    pass
+
+from anthropic import Anthropic
+from dotenv import load_dotenv
+
+load_dotenv(override=True)
+if os.getenv("ANTHROPIC_BASE_URL"): os.environ.pop("ANTHROPIC_AUTH_TOKEN", None)
+
+WORKDIR = Path.cwd()
+SKILLS_DIR = WORKDIR / "skills"
+TRANSCRIPT_DIR = WORKDIR / ".transcripts"
+TOOL_RESULTS_DIR = WORKDIR / ".task_outputs" / "tool-results"
+client = Anthropic(base_url=os.getenv("ANTHROPIC_BASE_URL"))
+MODEL = os.environ["MODEL_ID"]
+CURRENT_TODOS: list[dict] = []
+
+# s07: 技能目录扫描 (继承自 s07)
+def _parse_frontmatter(text: str) -> tuple[dict, str]:
+    if not text.startswith("---"):
+        return {}, text
+    parts = text.split("---", 2)
+    if len(parts) < 3:
+        return {}, text
+    meta = {}
+    for line in parts[1].strip().splitlines():
+        if ":" in line:
+            k, v = line.split(":", 1)
+            meta[k.strip()] = v.strip().strip('"').strip("'")
+    return meta, parts[2].strip()
+
+SKILL_REGISTRY: dict[str, dict] = {}
+
+def _scan_skills():
+    if not SKILLS_DIR.exists():
+        return
+    for d in sorted(SKILLS_DIR.iterdir()):
+        if not d.is_dir():
+            continue
+        manifest = d / "SKILL.md"
+        if manifest.exists():
+            raw = manifest.read_text()
+            meta, body = _parse_frontmatter(raw)
+            name = meta.get("name", d.name)
+            desc = meta.get("description", raw.split("\n")[0].lstrip("#").strip())
+            SKILL_REGISTRY[name] = {"name": name, "description": desc, "content": raw}
+
+_scan_skills()
+
+def list_skills() -> str:
+    if not SKILL_REGISTRY:
+        return "(未找到技能)"
+    return "\n".join(f"- **{s['name']}**: {s['description']}" for s in SKILL_REGISTRY.values())
+
+def load_skill(name: str) -> str:
+    skill = SKILL_REGISTRY.get(name)
+    if not skill:
+        return f"未找到技能:{name}"
+    return skill["content"]
+
+# s08: SYSTEM 包含技能目录 (继承自 s07 build_system)
+def build_system() -> str:
+    catalog = list_skills()
+    return (
+        f"你是位于 {WORKDIR}. "
+        f"可用技能:\n{catalog}\n"
+        "需要时使用 load_skill 获取完整详情。"
+    )
+
+SYSTEM = build_system()
+
+# s08: 子 Agent 使用自己的系统提示词 — 不压缩、不加载技能
+SUB_SYSTEM = (
+    f"你是位于 {WORKDIR}. "
+    "完成交给你的任务,然后返回简洁摘要。"
+    "不要继续委派。"
+)
+
+
+# ═══════════════════════════════════════════════════════════
+#  来自 s02-s07 (未改动): 基础工具
+# ═══════════════════════════════════════════════════════════
+
+def safe_path(p: str) -> Path:
+    path = (WORKDIR / p).resolve()
+    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)
+        out = (r.stdout + r.stderr).strip()
+        return out[:50000] if out else "(无输出)"
+    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} 行更多内容)"]
+        return "\n".join(lines)
+    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}"
+
+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}"
+        file_path.write_text(text.replace(old_text, new_text, 1))
+        return f"已编辑 {path}"
+    except Exception as e: return f"错误:{e}"
+
+def run_glob(pattern: str) -> str:
+    import glob as g
+    try:
+        results = []
+        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}"
+
+def _normalize_todos(todos):
+    if isinstance(todos, str):
+        try:
+            个待办 = json.loads(todos)
+        except json.JSONDecodeError:
+            try:
+                个待办 = ast.literal_eval(todos)
+            except (SyntaxError, ValueError):
+                return None, "错误:todos 必须是列表或 JSON 数组字符串"
+    if not isinstance(todos, list):
+        return None, "错误:todos 必须是列表"
+    for i, t in enumerate(todos):
+        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
+
+def run_todo_write(todos: list) -> str:
+    global CURRENT_TODOS
+    个待办, error = _normalize_todos(todos)
+    if error:
+        return error
+    CURRENT_TODOS = 个待办
+    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"]]
+        lines.append(f"  [{icon}] {t['content']}")
+    print("\n".join(lines))
+    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")
+
+
+# ═══════════════════════════════════════════════════════════
+#  来自 s06-s07 (未改动): 子 Agent
+# ═══════════════════════════════════════════════════════════
+
+SUB_TOOLS = [
+    {"name": "bash", "description": "运行一条 shell 命令。",
+     "input_schema": {"type": "object", "properties": {"command": {"type": "string"}}, "required": ["command"]}},
+    {"name": "read_file", "description": "读取文件内容。",
+     "input_schema": {"type": "object", "properties": {"path": {"type": "string"}}, "required": ["path"]}},
+    {"name": "write_file", "description": "向文件写入内容。",
+     "input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "content": {"type": "string"}}, "required": ["path", "content"]}},
+    {"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"]}},
+    {"name": "glob", "description": "查找匹配 glob 模式的文件。",
+     "input_schema": {"type": "object", "properties": {"pattern": {"type": "string"}}, "required": ["pattern"]}},
+]
+SUB_HANDLERS = {"bash": run_bash, "read_file": run_read, "write_file": run_write,
+                "edit_file": run_edit, "glob": run_glob}
+
+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)
+        messages.append({"role": "assistant", "content": response.content})
+        if response.stop_reason != "tool_use":
+            break
+        results = []
+        for block in response.content:
+            if block.type == "tool_use":
+                blocked = trigger_hooks("PreToolUse", block)
+                if blocked:
+                    results.append({"type": "tool_result", "tool_use_id": block.id,
+                                    "content": str(blocked)})
+                    continue
+                handler = SUB_HANDLERS.get(block.name)
+                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})
+        messages.append({"role": "user", "content": results})
+    result = extract_text(messages[-1]["content"])
+    if not result:
+        for msg in reversed(messages):
+            if msg["role"] == "assistant":
+                result = extract_text(msg["content"])
+                if result:
+                    break
+        if not result:
+            result = "子 Agent stopped 等待 30 turns without final answer."
+    print(f"\033[35m[子 Agent 已完成]\033[0m")
+    return result
+
+
+# ═══════════════════════════════════════════════════════════
+#  新增于 s08: 四层压缩流水线
+# ═══════════════════════════════════════════════════════════
+
+CONTEXT_LIMIT = 50000
+KEEP_RECENT = 3
+PERSIST_THRESHOLD = 30000
+
+def estimate_size(msgs): return len(str(msgs))
+
+def _block_type(block):
+    return block.get("type") if isinstance(block, dict) else getattr(block, "type", None)
+
+
+def _message_has_tool_use(msg):
+    if msg.get("role") != "assistant":
+        return False
+    content = msg.get("content")
+    if not isinstance(content, list):
+        return False
+    return any(_block_type(block) == "tool_use" for block in content)
+
+
+def _is_tool_result_message(msg):
+    if msg.get("role") != "user":
+        return False
+    content = msg.get("content")
+    if not isinstance(content, list):
+        return False
+    return any(isinstance(block, dict) and block.get("type") == "tool_result"
+               for block in content)
+
+
+# L1: snipCompact — 裁剪中间消息
+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:]
+
+
+# L2: microCompact — 旧结果占位符
+def collect_tool_results(messages):
+    blocks = []
+    for mi, msg in enumerate(messages):
+        if msg.get("role") != "user" or not isinstance(msg.get("content"), list): continue
+        for bi, block in enumerate(msg["content"]):
+            if isinstance(block, dict) and block.get("type") == "tool_result":
+                blocks.append((mi, bi, block))
+    return blocks
+
+def micro_compact(messages):
+    tool_results = collect_tool_results(messages)
+    if len(tool_results) <= KEEP_RECENT: return messages
+    for _, _, block in tool_results[:-KEEP_RECENT]:
+        if len(block.get("content", "")) > 120:
+            block["content"] = "[早前工具结果已压缩。如有需要请重新运行。]"
+    return messages
+
+
+# L3: toolResultBudget — 将大型结果持久化到磁盘
+def persist_large_output(tool_use_id, output):
+    if len(output) <= PERSIST_THRESHOLD: return 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>"
+
+def tool_result_budget(messages, max_bytes=200_000):
+    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"]
+    total = sum(len(str(b.get("content", ""))) for _, b in blocks)
+    if total <= max_bytes: return messages
+    ranked = sorted(blocks, key=lambda p: len(str(p[1].get("content", ""))), reverse=True)
+    for _, block in ranked:
+        if total <= max_bytes: break
+        content = str(block.get("content", ""))
+        if len(content) <= PERSIST_THRESHOLD: continue
+        tid = block.get("tool_use_id", "unknown")
+        block["content"] = persist_large_output(tid, content)
+        total = sum(len(str(b.get("content", ""))) for _, b in blocks)
+    return messages
+
+
+# L4: autoCompact — LLM 完整摘要
+def write_transcript(messages):
+    TRANSCRIPT_DIR.mkdir(parents=True, exist_ok=True)
+    path = TRANSCRIPT_DIR / f"transcript_{int(time.time())}.jsonl"
+    with path.open("w") as f:
+        for msg in messages: f.write(json.dumps(msg, default=str) + "\n")
+    return path
+
+def summarize_history(messages):
+    conversation = json.dumps(messages, default=str)[:80000]
+    prompt = ("总结这段编码 Agent 对话,以便继续工作。\n"
+              "保留:1. 当前目标,2. 关键发现/决策,3. 已读/已改文件,"
+              "4. 剩余工作,5. 用户约束。\n保持简洁但具体。\n\n" + conversation)
+    response = client.messages.create(model=MODEL, messages=[{"role": "user", "content": prompt}], max_tokens=2000)
+    return "\n".join(
+        getattr(block, "text", "")
+        for block in response.content
+        if getattr(block, "type", None) == "text").strip() or "(空摘要)"
+
+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}"}]
+
+
+# Emergency: reactiveCompact — API 错误时触发
+def reactive_compact(messages):
+    transcript = write_transcript(messages)
+    tail_start = max(0, len(messages) - 5)
+    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
+    summary = summarize_history(messages[:tail_start])
+    return [{"role": "user", "content": f"[Reactive compact]\n\n{summary}"}, *messages[tail_start:]]
+
+
+# ═══════════════════════════════════════════════════════════
+#  来自 s07: 工具定义
+# ═══════════════════════════════════════════════════════════
+
+TOOLS = [
+    {"name": "bash", "description": "运行一条 shell 命令。",
+     "input_schema": {"type": "object", "properties": {"command": {"type": "string"}}, "required": ["command"]}},
+    {"name": "read_file", "description": "读取文件内容。",
+     "input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "limit": {"type": "integer"}}, "required": ["path"]}},
+    {"name": "write_file", "description": "向文件写入内容。",
+     "input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "content": {"type": "string"}}, "required": ["path", "content"]}},
+    {"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"]}},
+    {"name": "glob", "description": "查找匹配 glob 模式的文件。",
+     "input_schema": {"type": "object", "properties": {"pattern": {"type": "string"}}, "required": ["pattern"]}},
+    {"name": "todo_write", "description": "为当前编码会话创建并维护任务清单。",
+     "input_schema": {"type": "object", "properties": {"todos": {"type": "array", "items": {"type": "object", "properties": {"content": {"type": "string"}, "status": {"type": "string", "enum": ["pending", "in_progress", "completed"]}}, "required": ["content", "status"]}}}, "required": ["todos"]}},
+    {"name": "task", "description": "启动一个子 Agent 处理复杂子任务。只返回最终结论。",
+     "input_schema": {"type": "object", "properties": {"description": {"type": "string"}}, "required": ["description"]}},
+    {"name": "load_skill", "description": "按名称加载某个技能的完整内容。",
+     "input_schema": {"type": "object", "properties": {"name": {"type": "string"}}, "required": ["name"]}},
+    # s08 变化: 新的 compact 工具 — 触发 compact_history,而不是空操作
+    {"name": "compact", "description": "总结早前对话以释放上下文空间。",
+     "input_schema": {"type": "object", "properties": {"focus": {"type": "string"}}}},
+]
+
+TOOL_HANDLERS = {
+    "bash": run_bash, "read_file": run_read, "write_file": run_write,
+    "edit_file": run_edit, "glob": run_glob, "todo_write": run_todo_write,
+    "task": spawn_subagent, "load_skill": load_skill,
+}
+
+# 来自 s04 (未改动): Hooks
+HOOKS = {"PreToolUse": [], "PostToolUse": []}
+def trigger_hooks(event, *args):
+    for cb in HOOKS[event]:
+        r = cb(*args)
+        if r is not None: return r
+    return None
+
+DENY_LIST = ["rm -rf /", "sudo", "shutdown"]
+def permission_hook(block):
+    if block.name == "bash":
+        for p in DENY_LIST:
+            if p in block.input.get("command", ""): return "权限被拒绝"
+    return None
+def log_hook(block):
+    print(f"\033[90m[HOOK] {block.name}\033[0m")
+    return None
+
+HOOKS["PreToolUse"].append(permission_hook)
+HOOKS["PreToolUse"].append(log_hook)
+
+
+# ═══════════════════════════════════════════════════════════
+#  agent_loop — s08 核心:调用 LLM 前运行压缩流水线
+# ═══════════════════════════════════════════════════════════
+
+MAX_REACTIVE_RETRIES = 1  # 响应式压缩的重试上限
+
+def agent_loop(messages: list):
+    reactive_retries = 0
+    while True:
+        # s08 变化: 三个预处理器(0 次 API 调用,便宜的优先)
+        # 顺序匹配 CC 源码:budget → snip → micro
+        messages[:] = tool_result_budget(messages)    # L3: 先持久化大型结果
+        messages[:] = snip_compact(messages)          # L1: 裁剪中间部分
+        messages[:] = micro_compact(messages)         # L2: 旧结果占位符
+
+        # s08 变化: tokens 仍超过阈值 → LLM 摘要(1 次 API 调用)
+        if estimate_size(messages) > CONTEXT_LIMIT:
+            print("[自动压缩]")
+            messages[:] = compact_history(messages)
+
+        try:
+            response = client.messages.create(model=MODEL, system=SYSTEM, messages=messages, tools=TOOLS, max_tokens=8000)
+            reactive_retries = 0  # API 调用成功后重置
+        except Exception as e:
+            if ("prompt_too_long" in str(e).lower() or "token 过多" in str(e).lower()) and reactive_retries < MAX_REACTIVE_RETRIES:
+                print("[响应式压缩]")
+                messages[:] = reactive_compact(messages)
+                reactive_retries += 1
+                continue
+            raise
+
+        messages.append({"role": "assistant", "content": response.content})
+        if response.stop_reason != "tool_use": return
+
+        results = []
+        for block in response.content:
+            if block.type != "tool_use": continue
+            print(f"\033[36m> {block.name}\033[0m")
+
+            # s08: compact 工具触发 compact_history,而不是返回空操作字符串
+            if block.name == "compact":
+                messages[:] = compact_history(messages)
+                results.append({"type": "tool_result", "tool_use_id": block.id,
+                                "content": "[已压缩。对话历史已完成摘要。]"})
+                messages.append({"role": "user", "content": results})
+                break  # 结束当前轮次,使用压缩后的上下文重新开始
+
+            blocked = trigger_hooks("PreToolUse", block)
+            if blocked:
+                results.append({"type": "tool_result", "tool_use_id": block.id, "content": str(blocked)})
+                continue
+            handler = TOOL_HANDLERS.get(block.name)
+            output = handler(**block.input) if handler else f"未知工具:{block.name}"
+            trigger_hooks("PostToolUse", block, output)
+            print(str(output)[:200])
+            results.append({"type": "tool_result", "tool_use_id": block.id, "content": str(output)})
+        else:
+            # 正常路径:没有调用 compact
+            messages.append({"role": "user", "content": results})
+            continue
+        # 已调用 compact:结果已在上方追加
+        continue
+
+
+if __name__ == "__main__":
+    print("s08: 上下文压缩 — 四层压缩流水线")
+    print("输入问题,回车发送。输入 q 退出。\n")
+    history = []
+    while True:
+        try: query = input("\033[36ms08 >> \033[0m")
+        except (EOFError, KeyboardInterrupt): break
+        if query.strip().lower() in ("q", "exit", ""): break
+        history.append({"role": "user", "content": query})
+        agent_loop(history)
+        for block in history[-1]["content"]:
+            if getattr(block, "type", None) == "text": print(block.text)
+        print()

+ 655 - 0
s09_memory/code.py

@@ -0,0 +1,655 @@
+#!/usr/bin/env python3
+"""
+s09_memory.py - 记忆系统
+
+为编码 Agent 提供跨会话持久知识。
+
+存储:
+    .memory/
+      MEMORY.md           ← 索引(每条记忆一行,≤200 行)
+      feedback_tabs.md    ← 独立记忆文件(Markdown + YAML frontmatter)
+      user_profile.md
+      project_facts.md
+
+agent_loop 中的流程:
+    1. 将 MEMORY.md 索引加载到 SYSTEM 提示词(便宜,始终存在)
+    2. 按文件名/描述选择相关记忆 → 注入内容
+    3. 运行来自 s08 的压缩流水线
+    4. 每轮结束后 → 从原始消息中提取新记忆
+    5. 定期合并整理(Dream)
+
+基于 s08(上下文压缩)构建。用法:
+
+    python s09_memory/code.py
+    需要: pip install anthropic python-dotenv + .env 中配置 ANTHROPIC_API_KEY
+"""
+
+import os, subprocess, json, time, re
+from pathlib import Path
+
+try:
+    import readline
+    readline.parse_and_bind('set bind-tty-special-chars off')
+except ImportError:
+    pass
+
+from anthropic import Anthropic
+from dotenv import load_dotenv
+
+load_dotenv(override=True)
+if os.getenv("ANTHROPIC_BASE_URL"): os.environ.pop("ANTHROPIC_AUTH_TOKEN", None)
+
+WORKDIR = Path.cwd()
+MEMORY_DIR = WORKDIR / ".memory"; MEMORY_DIR.mkdir(exist_ok=True)
+MEMORY_INDEX = MEMORY_DIR / "MEMORY.md"
+SKILLS_DIR = WORKDIR / "skills"
+TRANSCRIPT_DIR = WORKDIR / ".transcripts"
+TOOL_RESULTS_DIR = WORKDIR / ".task_outputs" / "tool-results"
+client = Anthropic(base_url=os.getenv("ANTHROPIC_BASE_URL"))
+MODEL = os.environ["MODEL_ID"]
+
+
+# ═══════════════════════════════════════════════════════════
+#  新增于 s09: 记忆系统
+# ═══════════════════════════════════════════════════════════
+
+MEMORY_TYPES = ["user", "feedback", "project", "reference"]
+
+def _parse_frontmatter(text: str) -> tuple[dict, str]:
+    if not text.startswith("---"):
+        return {}, text
+    parts = text.split("---", 2)
+    if len(parts) < 3:
+        return {}, text
+    meta = {}
+    for line in parts[1].strip().splitlines():
+        if ":" in line:
+            k, v = line.split(":", 1)
+            meta[k.strip()] = v.strip().strip('"').strip("'")
+    return meta, parts[2].strip()
+
+
+def write_memory_file(name: str, mem_type: str, description: str, body: str):
+    """Write a single memory file with YAML frontmatter."""
+    slug = name.lower().replace(" ", "-").replace("/", "-")
+    filename = f"{slug}.md"
+    filepath = MEMORY_DIR / filename
+    filepath.write_text(
+        f"---\nname: {name}\ndescription: {description}\ntype: {mem_type}\n---\n\n{body}\n"
+    )
+    _rebuild_index()
+    return filepath
+
+
+def _rebuild_index():
+    """Rebuild MEMORY.md index from all memory files."""
+    lines = []
+    for f in sorted(MEMORY_DIR.glob("*.md")):
+        if f.name == "MEMORY.md":
+            continue
+        raw = f.read_text()
+        meta, body = _parse_frontmatter(raw)
+        name = meta.get("name", f.stem)
+        desc = meta.get("description", body.split("\n")[0][:80])
+        lines.append(f"- [{name}]({f.name}) — {desc}")
+    MEMORY_INDEX.write_text("\n".join(lines) + "\n" if lines else "")
+
+
+def read_memory_index() -> str:
+    """Read MEMORY.md index (injected into SYSTEM every turn)."""
+    if not MEMORY_INDEX.exists():
+        return ""
+    text = MEMORY_INDEX.read_text().strip()
+    return text if text else ""
+
+
+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
+    return path.read_text()
+
+
+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":
+            continue
+        raw = f.read_text()
+        meta, body = _parse_frontmatter(raw)
+        result.append({
+            "filename": f.name,
+            "name": meta.get("name", f.stem),
+            "description": meta.get("description", ""),
+            "type": meta.get("type", "user"),
+            "body": body,
+        })
+    return result
+
+
+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)."""
+    files = list_memory_files()
+    if not files:
+        return []
+
+    # 收集最近的用户文本作为上下文
+    recent_texts = []
+    for msg in reversed(messages):
+        if msg.get("role") == "user":
+            content = msg.get("content", "")
+            if isinstance(content, list):
+                content = " ".join(
+                    str(getattr(b, "text", "")) for b in content
+                    if getattr(b, "type", None) == "text"
+                )
+            if isinstance(content, str):
+                recent_texts.append(content)
+            if len(recent_texts) >= 3:
+                break
+    recent = " ".join(reversed(recent_texts))[:2000]
+
+    if not recent.strip():
+        return []
+
+    # 构建名称 + 描述目录,供 LLM 选择
+    catalog_lines = []
+    for i, f in enumerate(files):
+        catalog_lines.append(f"{i}: {f['name']} — {f['description']}")
+    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}"
+    )
+
+    try:
+        response = client.messages.create(
+            model=MODEL,
+            messages=[{"role": "user", "content": prompt}],
+            max_tokens=200,
+        )
+        text = extract_text(response.content).strip()
+        # 从响应中提取 JSON 数组
+        match = re.search(r'\[.*?\]', text, re.DOTALL)
+        if match:
+            indices = json.loads(match.group())
+            selected = []
+            for idx in indices:
+                if isinstance(idx, int) and 0 <= idx < len(files):
+                    selected.append(files[idx]["filename"])
+                    if len(selected) >= max_items:
+                        break
+            return selected
+    except Exception:
+        pass
+
+    # 兜底:基于名称 + 描述做关键词匹配
+    keywords = [w.lower() for w in recent.split() if len(w) > 3]
+    selected = []
+    for f in files:
+        text = (f["name"] + " " + f["description"]).lower()
+        if any(kw in text for kw in keywords):
+            selected.append(f["filename"])
+            if len(selected) >= max_items:
+                break
+    return selected
+
+
+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 ""
+
+    parts = ["<relevant_memories>"]
+    for filename in selected_files:
+        content = read_memory_file(filename)
+        if content:
+            parts.append(content)
+    parts.append("</relevant_memories>")
+    return "\n\n".join(parts)
+
+
+def extract_memories(messages: list):
+    """Extract new memories from recent dialogue. Runs 等待 each turn."""
+    # 收集最近的对话文本
+    dialogue_parts = []
+    for msg in messages[-10:]:
+        role = msg.get("role", "?")
+        content = msg.get("content", "")
+        if isinstance(content, list):
+            content = " ".join(
+                str(getattr(b, "text", "")) for b in content
+                if getattr(b, "type", None) == "text"
+            )
+        if isinstance(content, str) and content.strip():
+            dialogue_parts.append(f"{role}: {content}")
+    dialogue = "\n".join(dialogue_parts)
+
+    if not dialogue.strip():
+        return
+
+    # 检查已有记忆以避免重复
+    existing = list_memory_files()
+    existing_desc = "\n".join(f"- {m['name']}: {m['description']}" for m in existing) if existing else "(none)"
+
+    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]}"
+    )
+
+    try:
+        response = client.messages.create(
+            model=MODEL, messages=[{"role": "user", "content": prompt}], max_tokens=800
+        )
+        text = extract_text(response.content).strip()
+        # 从响应中提取 JSON 数组
+        match = re.search(r'\[.*\]', text, re.DOTALL)
+        if not match:
+            return
+        items = json.loads(match.group())
+        if not items:
+            return
+        count = 0
+        for mem in items:
+            name = mem.get("name", f"memory_{int(time.time())}")
+            mem_type = mem.get("type", "user")
+            desc = mem.get("description", "")
+            body = mem.get("body", "")
+            if desc and body:
+                write_memory_file(name, mem_type, desc, body)
+                count += 1
+        if count:
+            print(f"\n\033[33m[Memory: extracted {count} new memories]\033[0m")
+    except Exception:
+        pass
+
+
+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
+
+    catalog = "\n\n".join(
+        f"## {f['filename']}\nname: {f['name']}\ndescription: {f['description']}\n{f['body']}"
+        for f in files
+    )
+
+    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"
+        f"{catalog[:16000]}"
+    )
+
+    try:
+        response = client.messages.create(
+            model=MODEL, messages=[{"role": "user", "content": prompt}], max_tokens=3000
+        )
+        text = extract_text(response.content).strip()
+        match = re.search(r'\[.*\]', text, re.DOTALL)
+        if not match:
+            return
+        items = json.loads(match.group())
+
+        # 删除旧记忆文件(保留 MEMORY.md)
+        for f in MEMORY_DIR.glob("*.md"):
+            if f.name != "MEMORY.md":
+                f.unlink()
+
+        for mem in items:
+            name = mem.get("name", f"memory_{int(time.time())}")
+            mem_type = mem.get("type", "user")
+            desc = mem.get("description", "")
+            body = mem.get("body", "")
+            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")
+    except Exception:
+        pass
+
+
+# 使用记忆索引构建 SYSTEM
+def build_system() -> str:
+    index = read_memory_index()
+    memories_section = f"\n\nMemories available:\n{index}" if index else ""
+    return (
+        f"你是位于 {WORKDIR}."
+        f"{memories_section}\n"
+        "相关记忆会在下方注入。请遵循记忆中的用户偏好。\n"
+        "当用户说 'remember' 或表达明确偏好时,将其提取为记忆。"
+    )
+
+SUB_SYSTEM = (
+    f"你是位于 {WORKDIR}. "
+    "完成交给你的任务,然后返回简洁摘要。"
+    "不要继续委派。"
+)
+
+
+# ═══════════════════════════════════════════════════════════
+#  来自 s02-s08 (骨架): 基础工具
+# ═══════════════════════════════════════════════════════════
+
+def safe_path(p: str) -> Path:
+    path = (WORKDIR / p).resolve()
+    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)
+        out = (r.stdout + r.stderr).strip()
+        return out[:50000] if out else "(无输出)"
+    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} 行更多内容)"]
+        return "\n".join(lines)
+    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}"
+
+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}"
+        file_path.write_text(text.replace(old_text, new_text, 1))
+        return f"已编辑 {path}"
+    except Exception as e: return f"错误:{e}"
+
+def run_glob(pattern: str) -> str:
+    import glob as g
+    try:
+        results = []
+        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}"
+
+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)
+SUB_TOOLS = [
+    {"name": "bash", "description": "运行一条 shell 命令。",
+     "input_schema": {"type": "object", "properties": {"command": {"type": "string"}}, "required": ["command"]}},
+    {"name": "read_file", "description": "读取文件内容。",
+     "input_schema": {"type": "object", "properties": {"path": {"type": "string"}}, "required": ["path"]}},
+    {"name": "write_file", "description": "向文件写入内容。",
+     "input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "content": {"type": "string"}}, "required": ["path", "content"]}},
+]
+SUB_HANDLERS = {"bash": run_bash, "read_file": run_read, "write_file": run_write}
+
+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)
+        messages.append({"role": "assistant", "content": response.content})
+        if response.stop_reason != "tool_use": break
+        results = []
+        for block in response.content:
+            if block.type == "tool_use":
+                handler = SUB_HANDLERS.get(block.name)
+                output = handler(**block.input) if handler else f"未知工具:{block.name}"
+                print(f"  \033[90m[sub] {block.name}: {str(output)[:100]}\033[0m")
+                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:
+        for msg in reversed(messages):
+            if msg["role"] == "assistant":
+                result = extract_text(msg["content"])
+                if result: break
+        if not result: result = "子 Agent stopped 等待 30 turns without final answer."
+    print(f"\033[35m[子 Agent 已完成]\033[0m")
+    return result
+
+
+# ═══════════════════════════════════════════════════════════
+#  来自 s08 (骨架): Compaction pipeline
+# ═══════════════════════════════════════════════════════════
+
+CONTEXT_LIMIT = 50000; KEEP_RECENT = 3; PERSIST_THRESHOLD = 30000
+
+def estimate_size(msgs): return len(str(msgs))
+
+def _block_type(block):
+    return block.get("type") if isinstance(block, dict) else getattr(block, "type", None)
+
+def _message_has_tool_use(msg):
+    if msg.get("role") != "assistant":
+        return False
+    content = msg.get("content")
+    if not isinstance(content, list):
+        return False
+    return any(_block_type(block) == "tool_use" for block in content)
+
+def _is_tool_result_message(msg):
+    if msg.get("role") != "user":
+        return False
+    content = msg.get("content")
+    if not isinstance(content, list):
+        return False
+    return any(isinstance(block, dict) and block.get("type") == "tool_result" for block in content)
+
+def snip_compact(msgs, mx=50):
+    if len(msgs) <= mx: return msgs
+    head_end, tail_start = 3, len(msgs) - (mx - 3)
+    if head_end > 0 and _message_has_tool_use(msgs[head_end - 1]):
+        while head_end < len(msgs) and _is_tool_result_message(msgs[head_end]):
+            head_end += 1
+    if (tail_start > 0 and tail_start < len(msgs)
+            and _is_tool_result_message(msgs[tail_start])
+            and _message_has_tool_use(msgs[tail_start - 1])):
+        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:]
+
+def collect_tool_results(msgs):
+    blocks = []
+    for mi, msg in enumerate(msgs):
+        if msg.get("role") != "user" or not isinstance(msg.get("content"), list): continue
+        for bi, block in enumerate(msg["content"]):
+            if isinstance(block, dict) and block.get("type") == "tool_result": blocks.append((mi, bi, block))
+    return blocks
+
+def micro_compact(msgs):
+    tr = collect_tool_results(msgs)
+    if len(tr) <= KEEP_RECENT: return msgs
+    for _, _, b in tr[:-KEEP_RECENT]:
+        if len(b.get("content", "")) > 120: b["content"] = "[早前工具结果已压缩。]"
+    return msgs
+
+def persist_large(tid, out):
+    if len(out) <= PERSIST_THRESHOLD: return 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>"
+
+def tool_result_budget(msgs, mx=200_000):
+    last = msgs[-1] if msgs else None
+    if not last or last.get("role") != "user" or not isinstance(last.get("content"), list): return msgs
+    blocks = [(i, b) for i, b in enumerate(last["content"]) if isinstance(b, dict) and b.get("type") == "tool_result"]
+    total = sum(len(str(b.get("content", ""))) for _, b in blocks)
+    if total <= mx: return msgs
+    for _, block in sorted(blocks, key=lambda p: len(str(p[1].get("content", ""))), reverse=True):
+        if total <= mx: break
+        c = str(block.get("content", ""))
+        if len(c) <= PERSIST_THRESHOLD: continue
+        block["content"] = persist_large(block.get("tool_use_id", "?"), c)
+        total = sum(len(str(b.get("content", ""))) for _, b in blocks)
+    return msgs
+
+def write_transcript(msgs):
+    TRANSCRIPT_DIR.mkdir(parents=True, exist_ok=True)
+    p = TRANSCRIPT_DIR / f"transcript_{int(time.time())}.jsonl"
+    with p.open("w") as f:
+        for m in msgs: f.write(json.dumps(m, default=str) + "\n")
+    return p
+
+def summarize_history(msgs):
+    conv = json.dumps(msgs, default=str)[:80000]
+    r = client.messages.create(model=MODEL, messages=[{"role": "user", "content":
+        "总结这段编码 Agent 对话,以便继续工作。\n"
+        "保留:1. 当前目标,2. 关键发现,3. 已修改文件,4. 剩余工作,5. 用户约束。\n\n" + conv}],
+        max_tokens=2000)
+    return extract_text(r.content).strip()
+
+def compact_history(msgs):
+    write_transcript(msgs)
+    summary = summarize_history(msgs)
+    return [{"role": "user", "content": f"[Compacted]\n\n{summary}"}]
+
+def reactive_compact(msgs):
+    write_transcript(msgs)
+    tail_start = max(0, len(msgs) - 5)
+    if (tail_start > 0 and tail_start < len(msgs)
+            and _is_tool_result_message(msgs[tail_start])
+            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:]]
+
+
+# ═══════════════════════════════════════════════════════════
+#  工具定义 (骨架 — 减少工具数量以聚焦记忆)
+# ═══════════════════════════════════════════════════════════
+
+TOOLS = [
+    {"name": "bash", "description": "运行一条 shell 命令。",
+     "input_schema": {"type": "object", "properties": {"command": {"type": "string"}}, "required": ["command"]}},
+    {"name": "read_file", "description": "读取文件内容。",
+     "input_schema": {"type": "object", "properties": {"path": {"type": "string"}}, "required": ["path"]}},
+    {"name": "write_file", "description": "向文件写入内容。",
+     "input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "content": {"type": "string"}}, "required": ["path", "content"]}},
+    {"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"]}},
+    {"name": "glob", "description": "查找匹配 glob 模式的文件。",
+     "input_schema": {"type": "object", "properties": {"pattern": {"type": "string"}}, "required": ["pattern"]}},
+    {"name": "task", "description": "启动一个子 Agent 来处理子任务。",
+     "input_schema": {"type": "object", "properties": {"description": {"type": "string"}}, "required": ["description"]}},
+]
+
+TOOL_HANDLERS = {
+    "bash": run_bash, "read_file": run_read, "write_file": run_write,
+    "edit_file": run_edit, "glob": run_glob, "task": spawn_subagent,
+}
+
+
+# ═══════════════════════════════════════════════════════════
+#  agent_loop — s09: 注入记忆,并在每轮后提取
+# ═══════════════════════════════════════════════════════════
+
+MAX_REACTIVE_RETRIES = 1
+
+def agent_loop(messages: list):
+    reactive_retries = 0
+    # s09: 把相关记忆内容注入当前用户轮次
+    memories_content = load_memories(messages)
+    memory_turn = len(messages) - 1 if messages and isinstance(messages[-1].get("content"), str) else None
+    # s09: 每个用户轮次构建一次系统提示词;循环返回后再更新记忆
+    system = build_system()
+
+    while True:
+        # s09: 保存压缩前快照,以便准确提取记忆
+        pre_compress = [m if isinstance(m, dict) else {"role": m.get("role",""),
+            "content": str(m.get("content",""))} for m in messages]
+
+        # s08: 压缩流水线 (budget → snip → micro)
+        messages[:] = tool_result_budget(messages)
+        messages[:] = snip_compact(messages)
+        messages[:] = micro_compact(messages)
+
+        if estimate_size(messages) > CONTEXT_LIMIT:
+            print("[自动压缩]")
+            messages[:] = compact_history(messages)
+
+        try:
+            request_messages = messages
+            if memories_content and memory_turn is not None and memory_turn < len(messages):
+                request_messages = messages.copy()
+                request_messages[memory_turn] = {
+                    **messages[memory_turn],
+                    "content": memories_content + "\n\n" + messages[memory_turn]["content"],
+                }
+            response = client.messages.create(
+                model=MODEL, system=system, messages=request_messages, tools=TOOLS, max_tokens=8000
+            )
+            reactive_retries = 0
+        except Exception as e:
+            if ("prompt_too_long" in str(e).lower() or "token 过多" in str(e).lower()) and reactive_retries < MAX_REACTIVE_RETRIES:
+                print("[响应式压缩]")
+                messages[:] = reactive_compact(messages)
+                reactive_retries += 1
+                continue
+            raise
+
+        messages.append({"role": "assistant", "content": response.content})
+        if response.stop_reason != "tool_use":
+            # s09: 从压缩前快照中提取,保证完整性
+            extract_memories(pre_compress)
+            consolidate_memories()
+            return
+
+        results = []
+        for block in response.content:
+            if block.type != "tool_use": continue
+            print(f"\033[36m> {block.name}\033[0m")
+            handler = TOOL_HANDLERS.get(block.name)
+            output = handler(**block.input) if handler else f"未知工具:{block.name}"
+            print(str(output)[:200])
+            results.append({"type": "tool_result", "tool_use_id": block.id, "content": output})
+        messages.append({"role": "user", "content": results})
+
+
+if __name__ == "__main__":
+    print("s09: 记忆 — 持久化跨会话知识")
+    print("输入问题,回车发送。输入 q 退出。\n")
+    history = []
+    while True:
+        try: query = input("\033[36ms09 >> \033[0m")
+        except (EOFError, KeyboardInterrupt): break
+        if query.strip().lower() in ("q", "exit", ""): break
+        history.append({"role": "user", "content": query})
+        agent_loop(history)
+        for block in history[-1]["content"]:
+            if getattr(block, "type", None) == "text": print(block.text)
+        print()

+ 219 - 0
s10_system_prompt/code.py

@@ -0,0 +1,219 @@
+#!/usr/bin/env python3
+"""
+s10: 系统提示词 — 运行时组装提示词并缓存。
+
+运行:  python s10_system_prompt/code.py
+需要: pip install anthropic python-dotenv + .env 中配置 ANTHROPIC_API_KEY
+
+相对 s09 的变化:
+  - PROMPT_SECTIONS:按主题作为 key 的提示词片段字典
+  - assemble_system_prompt(context):根据真实状态选择并拼接片段
+  - get_system_prompt(context):通过 json.dumps 实现确定性的缓存
+  - agent_loop 使用 get_system_prompt(context),不再使用硬编码 SYSTEM
+
+当 .memory/MEMORY.md 存在时加载记忆片段(基于真实状态,不靠关键词)。
+"""
+
+import os, subprocess, json
+from pathlib import Path
+
+try:
+    import readline
+    readline.parse_and_bind('set bind-tty-special-chars off')
+except ImportError:
+    pass
+
+from anthropic import Anthropic
+from dotenv import load_dotenv
+
+load_dotenv(override=True)
+if os.getenv("ANTHROPIC_BASE_URL"):
+    os.environ.pop("ANTHROPIC_AUTH_TOKEN", None)
+
+WORKDIR = Path.cwd()
+MEMORY_DIR = WORKDIR / ".memory"
+MEMORY_INDEX = MEMORY_DIR / "MEMORY.md"
+client = Anthropic(base_url=os.getenv("ANTHROPIC_BASE_URL"))
+MODEL = os.environ["MODEL_ID"]
+
+
+# ── 提示词片段 ──
+
+PROMPT_SECTIONS = {
+    "identity": "你是一个编码 Agent。直接行动,不要只解释。",
+}
+
+
+def assemble_system_prompt(context: dict) -> str:
+    """Select and join prompt sections based on current context."""
+    sections = []
+
+    # 始终加载 — 身份
+    sections.append(PROMPT_SECTIONS["identity"])
+
+    # 动态加载 — 来自上下文的工具和工作区
+    tools = ", ".join(context.get("enabled_tools", []))
+    if tools:
+        sections.append(f"可用工具:{tools}.")
+    sections.append(f"工作目录:{context.get('workspace', WORKDIR)}")
+
+    # 条件加载 — MEMORY.md 存在且有内容时加载记忆
+    memories = context.get("memories", "")
+    if memories:
+        sections.append(f"Relevant memories:\n{memories}")
+
+    return "\n\n".join(sections)
+
+
+_last_context_key = None
+_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.
+    """
+    global _last_context_key, _last_prompt
+    key = json.dumps(context, sort_keys=True, ensure_ascii=False, default=str)
+    if key == _last_context_key and _last_prompt:
+        print("  \033[90m[缓存命中] 系统提示词未变化\033[0m")
+        return _last_prompt
+    _last_context_key = key
+    _last_prompt = assemble_system_prompt(context)
+
+    loaded = ["identity", "tools", "workspace"]
+    if context.get("memories"):
+        loaded.append("memory")
+    print(f"  \033[32m[已组装] 片段:{', '.join(loaded)}\033[0m")
+    return _last_prompt
+
+
+# ── 工具 ──
+
+def safe_path(p: str) -> Path:
+    path = (WORKDIR / p).resolve()
+    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)
+        out = (r.stdout + r.stderr).strip()
+        return out[:50000] if out else "(无输出)"
+    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} 行更多内容)"]
+        return "\n".join(lines)
+    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}"
+
+
+TOOLS = [
+    {"name": "bash", "description": "运行一条 shell 命令。",
+     "input_schema": {"type": "object",
+                      "properties": {"command": {"type": "string"}},
+                      "required": ["command"]}},
+    {"name": "read_file", "description": "读取文件内容。",
+     "input_schema": {"type": "object",
+                      "properties": {"path": {"type": "string"},
+                                     "limit": {"type": "integer"}},
+                      "required": ["path"]}},
+    {"name": "write_file", "description": "向文件写入内容。",
+     "input_schema": {"type": "object",
+                      "properties": {"path": {"type": "string"},
+                                     "content": {"type": "string"}},
+                      "required": ["path", "content"]}},
+]
+
+TOOL_HANDLERS = {"bash": run_bash, "read_file": run_read, "write_file": run_write}
+
+
+# ── 上下文 ──
+
+def update_context(context: dict, messages: list) -> dict:
+    """从真实状态推导上下文:有哪些工具、是否存在记忆文件。"""
+    memories = ""
+    if MEMORY_INDEX.exists():
+        content = MEMORY_INDEX.read_text().strip()
+        if content:
+            memories = content
+    return {
+        "enabled_tools": list(TOOL_HANDLERS.keys()),
+        "workspace": str(WORKDIR),
+        "memories": memories,
+    }
+
+
+# ── Agent 循环 ──
+
+def agent_loop(messages: list, context: dict):
+    """Main loop — uses assembled system prompt instead of hardcoded SYSTEM."""
+    system = get_system_prompt(context)
+    while True:
+        response = client.messages.create(
+            model=MODEL, system=system, messages=messages,
+            tools=TOOLS, max_tokens=8000)
+        messages.append({"role": "assistant", "content": response.content})
+        if response.stop_reason != "tool_use":
+            return
+
+        results = []
+        for block in response.content:
+            if block.type != "tool_use":
+                continue
+            print(f"\033[36m> {block.name}\033[0m")
+            handler = TOOL_HANDLERS.get(block.name)
+            output = handler(**block.input) if handler else f"未知工具:{block.name}"
+            print(str(output)[:200])
+            results.append({"type": "tool_result",
+                            "tool_use_id": block.id, "content": output})
+        messages.append({"role": "user", "content": results})
+
+        # 每轮工具调用后重新评估上下文和提示词
+        context = update_context(context, messages)
+        system = get_system_prompt(context)
+
+
+if __name__ == "__main__":
+    print("s10: 系统提示词 — 运行时组装")
+    print("输入问题后按回车发送。输入 q 退出。\n")
+    history = []
+    context = update_context({}, [])
+    while True:
+        try:
+            query = input("\033[36ms10 >> \033[0m")
+        except (EOFError, KeyboardInterrupt):
+            break
+        if query.strip().lower() in ("q", "exit", ""):
+            break
+        history.append({"role": "user", "content": query})
+        agent_loop(history, context)
+        context = update_context(context, history)
+        for block in history[-1]["content"]:
+            if getattr(block, "type", None) == "text":
+                print(block.text)
+        print()

+ 364 - 0
s11_error_recovery/code.py

@@ -0,0 +1,364 @@
+#!/usr/bin/env python3
+"""
+s11: 错误恢复 — 三条恢复路径 + 指数退避。
+
+运行:  python s11_error_recovery/code.py
+需要: pip install anthropic python-dotenv + .env 中配置 ANTHROPIC_API_KEY
+
+相对 s10 的变化:
+  - LLM 调用包在 try/except 中,并提供三条恢复路径
+  - 路径 1:max_tokens -> 从 8K 升级到 64K(第一次升级不追加截断输出),
+             然后发送续写提示(最多 3 次)
+  - 路径 2:prompt_too_long -> reactive compact -> 重试(一次)
+  - 路径 3:429/529 -> 带抖动的指数退避(最多 10 次),
+             连续 529 时切换备用模型
+  - with_retry 包装器处理瞬时错误
+  - RecoveryState 跟踪升级 / 压缩 / 529 / 模型
+
+ASCII 流程:
+  messages -> 提示词组装 -> 压缩+加载 -> [try] LLM [except] -> tools -> 循环
+                                                   |          |
+                                             stop_reason   错误类型
+                                             max_tokens?   prompt_too_long? -> 压缩
+                                             升级 /        429/529? -> 退避
+                                             继续          其他? -> 记录 + 退出
+"""
+
+import os, subprocess, time, random, json
+from pathlib import Path
+
+try:
+    import readline
+    readline.parse_and_bind('set bind-tty-special-chars off')
+except ImportError:
+    pass
+
+from anthropic import Anthropic
+from dotenv import load_dotenv
+
+load_dotenv(override=True)
+if os.getenv("ANTHROPIC_BASE_URL"):
+    os.environ.pop("ANTHROPIC_AUTH_TOKEN", None)
+
+WORKDIR = Path.cwd()
+MEMORY_DIR = WORKDIR / ".memory"
+MEMORY_INDEX = MEMORY_DIR / "MEMORY.md"
+client = Anthropic(base_url=os.getenv("ANTHROPIC_BASE_URL"))
+PRIMARY_MODEL = os.environ["MODEL_ID"]
+FALLBACK_MODEL = os.getenv("FALLBACK_MODEL_ID")
+
+# ── 常量 ──
+
+ESCALATED_MAX_TOKENS = 64000
+DEFAULT_MAX_TOKENS = 8000
+MAX_RECOVERY_RETRIES = 3
+MAX_RETRIES = 10
+BASE_DELAY_MS = 500
+MAX_CONSECUTIVE_529 = 3
+CONTINUATION_PROMPT = (
+    "已触发输出 token 上限。请直接继续 — "
+    "不要道歉,不要回顾,从中断处接着写。"
+)
+
+# ── 提示词组装 (来自 s10,已同步) ──
+
+PROMPT_SECTIONS = {
+    "identity": "你是一个编码 Agent。直接行动,不要只解释。",
+    "tools": "可用工具:bash, read_file, write_file.",
+    "workspace": f"工作目录:{WORKDIR}",
+    "memory": "有可用的相关记忆时,会在下方注入。",
+}
+
+
+def assemble_system_prompt(context: dict) -> str:
+    sections = [PROMPT_SECTIONS["identity"],
+                PROMPT_SECTIONS["tools"],
+                PROMPT_SECTIONS["workspace"]]
+    memories = context.get("memories", "")
+    if memories:
+        sections.append(f"Relevant memories:\n{memories}")
+    return "\n\n".join(sections)
+
+
+_last_context_key, _last_prompt = None, None
+
+
+def get_system_prompt(context: dict) -> str:
+    global _last_context_key, _last_prompt
+    key = json.dumps(context, sort_keys=True, ensure_ascii=False, default=str)
+    if key == _last_context_key and _last_prompt:
+        print("  \033[90m[缓存命中] 系统提示词未变化\033[0m")
+        return _last_prompt
+    _last_context_key = key
+    _last_prompt = assemble_system_prompt(context)
+
+    loaded = ["identity", "tools", "workspace"]
+    if context.get("memories"):
+        loaded.append("memory")
+    print(f"  \033[32m[已组装] 片段:{', '.join(loaded)}\033[0m")
+    return _last_prompt
+
+
+# ── 工具 (未改动) ──
+
+def safe_path(p: str) -> Path:
+    path = (WORKDIR / p).resolve()
+    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)
+        out = (r.stdout + r.stderr).strip()
+        return out[:50000] if out else "(无输出)"
+    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} 行更多内容)"]
+        return "\n".join(lines)
+    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}"
+
+
+TOOLS = [
+    {"name": "bash", "description": "运行一条 shell 命令。",
+     "input_schema": {"type": "object",
+                      "properties": {"command": {"type": "string"}},
+                      "required": ["command"]}},
+    {"name": "read_file", "description": "读取文件内容。",
+     "input_schema": {"type": "object",
+                      "properties": {"path": {"type": "string"},
+                                     "limit": {"type": "integer"}},
+                      "required": ["path"]}},
+    {"name": "write_file", "description": "向文件写入内容。",
+     "input_schema": {"type": "object",
+                      "properties": {"path": {"type": "string"},
+                                     "content": {"type": "string"}},
+                      "required": ["path", "content"]}},
+]
+
+TOOL_HANDLERS = {"bash": run_bash, "read_file": run_read, "write_file": run_write}
+
+
+# ── 错误恢复 (s11 new) ──
+
+class RecoveryState:
+    """在循环中跟踪恢复尝试。"""
+    def __init__(self):
+        self.has_escalated = False
+        self.recovery_count = 0
+        self.consecutive_529 = 0
+        self.has_attempted_reactive_compact = False
+        self.current_model = PRIMARY_MODEL
+
+
+def retry_delay(attempt, retry_after=None):
+    """带抖动的指数退避。Retry-After 优先。"""
+    if retry_after:
+        return retry_after
+    base = min(BASE_DELAY_MS * (2 ** attempt), 32000) / 1000
+    jitter = random.uniform(0, base * 0.25)
+    return base + jitter
+
+
+def with_retry(fn, state: RecoveryState):
+    """针对瞬时错误(429/529)的指数退避。
+    非瞬时错误会重新抛给外层处理器。"""
+    for attempt in range(MAX_RETRIES):
+        try:
+            result = fn()
+            state.consecutive_529 = 0
+            return result
+        except Exception as e:
+            name = type(e).__name__
+            msg = str(e).lower()
+
+            # 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")
+                time.sleep(delay)
+                continue
+
+            # 529 过载 -> 指数退避 + 备用模型
+            if "过载" in name.lower() or "529" in msg or "过载" in msg:
+                state.consecutive_529 += 1
+                if state.consecutive_529 >= MAX_CONSECUTIVE_529:
+                    if FALLBACK_MODEL:
+                        state.current_model = FALLBACK_MODEL
+                        state.consecutive_529 = 0
+                        print(f"  \033[31m[529 x{MAX_CONSECUTIVE_529}]"
+                              f" 切换到 {FALLBACK_MODEL}\033[0m")
+                    else:
+                        state.consecutive_529 = 0
+                        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")
+                time.sleep(delay)
+                continue
+
+            # 非瞬时错误 -> 重新抛给外层 try/except
+            raise
+    raise RuntimeError(f"最大重试次数({MAX_RETRIES})已超出")
+
+
+def is_prompt_too_long_error(e: Exception) -> bool:
+    """检查 API 错误是否表示提示词/上下文过长。"""
+    msg = str(e).lower()
+    return (("prompt" in msg and "long" in msg)
+            or "提示词过长" in msg
+            or "context_length_exceeded" in msg
+            or "max_context_window" in msg)
+
+
+def reactive_compact(messages: list) -> list:
+    """应急压缩 — 教学版本保留最后 N 条消息。
+    真实 CC 会通过 LLM 生成压缩摘要,然后用压缩后的消息列表重试。
+    因为 s08/s09 已覆盖基于 LLM 的压缩,教学版本简化为保留尾部消息。"""
+    print("  \033[31m[reactive compact] trimming to last 5 messages\033[0m")
+    tail = messages[-5:]
+    return [{"role": "user",
+             "content": "[Reactive compact] Earlier conversation trimmed. "
+                        "Continue from where you left off."}, *tail]
+
+
+# ── 上下文 ──
+
+def update_context(context: dict, messages: list) -> dict:
+    """从真实状态推导上下文:有哪些工具、是否存在记忆文件。"""
+    memories = ""
+    if MEMORY_INDEX.exists():
+        content = MEMORY_INDEX.read_text().strip()
+        if content:
+            memories = content
+    return {
+        "enabled_tools": list(TOOL_HANDLERS.keys()),
+        "workspace": str(WORKDIR),
+        "memories": memories,
+    }
+
+
+# ── Agent 循环 ──
+
+def agent_loop(messages: list, context: dict):
+    """Main loop with 错误恢复 wrapping LLM calls."""
+    system = get_system_prompt(context)
+    state = RecoveryState()
+    max_tokens = DEFAULT_MAX_TOKENS
+
+    while True:
+        # ── LLM 调用:with_retry 处理 429/529,外层处理其余错误 ──
+        try:
+            response = with_retry(
+                lambda: client.messages.create(
+                    model=state.current_model, system=system,
+                    messages=messages, tools=TOOLS,
+                    max_tokens=max_tokens),
+                state)
+        except Exception as e:
+            # 路径 2:prompt_too_long -> 响应式压缩(一次)
+            if is_prompt_too_long_error(e):
+                if not state.has_attempted_reactive_compact:
+                    messages[:] = reactive_compact(messages)
+                    state.has_attempted_reactive_compact = True
+                    continue
+                print("  \033[31m[不可恢复] 压缩后仍然过长\033[0m")
+                messages.append({"role": "assistant", "content": [
+                    {"type": "text",
+                     "text": "[错误] 上下文过大,无法继续。"}]})
+                return
+
+            # 不可恢复
+            name = type(e).__name__
+            print(f"  \033[31m[不可恢复] {name}: {str(e)[:100]}\033[0m")
+            messages.append({"role": "assistant", "content": [
+                {"type": "text", "text": f"[错误] {name}: {str(e)[:200]}"}]})
+            return
+
+        # ── 路径 1:max_tokens -> 升级或继续 ──
+        if response.stop_reason == "max_tokens":
+            # 第一次升级:不追加截断输出,重试同一个请求
+            if not state.has_escalated:
+                max_tokens = ESCALATED_MAX_TOKENS
+                state.has_escalated = True
+                print(f"  \033[33m[max_tokens] escalating"
+                      f" {DEFAULT_MAX_TOKENS} -> {ESCALATED_MAX_TOKENS}\033[0m")
+                continue
+            # 64K 仍被截断:保存截断输出 + 续写提示
+            messages.append({"role": "assistant", "content": response.content})
+            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"
+                      f" {state.recovery_count}/{MAX_RECOVERY_RETRIES}\033[0m")
+                continue
+            print("  \033[31m[max_tokens] 恢复次数已达上限\033[0m")
+            return
+
+        # 正常完成:追加 assistant 响应
+        messages.append({"role": "assistant", "content": response.content})
+
+        if response.stop_reason != "tool_use":
+            return
+
+        # ── 工具执行 ──
+        results = []
+        for block in response.content:
+            if block.type != "tool_use":
+                continue
+            print(f"\033[36m> {block.name}\033[0m")
+            handler = TOOL_HANDLERS.get(block.name)
+            output = handler(**block.input) if handler else f"未知工具:{block.name}"
+            print(str(output)[:200])
+            results.append({"type": "tool_result",
+                            "tool_use_id": block.id, "content": output})
+        messages.append({"role": "user", "content": results})
+
+        context = update_context(context, messages)
+        system = get_system_prompt(context)
+
+
+if __name__ == "__main__":
+    print("s11: 错误恢复")
+    print("输入问题后按回车发送。输入 q 退出。\n")
+    history = []
+    context = update_context({}, [])
+    while True:
+        try:
+            query = input("\033[36ms11 >> \033[0m")
+        except (EOFError, KeyboardInterrupt):
+            break
+        if query.strip().lower() in ("q", "exit", ""):
+            break
+        turn_start = len(history)
+        history.append({"role": "user", "content": query})
+        agent_loop(history, context)
+        context = update_context(context, history)
+        for msg in history[turn_start:]:
+            if msg.get("role") != "assistant":
+                continue
+            for block in msg["content"]:
+                if getattr(block, "type", None) == "text":
+                    print(block.text)
+        print()

+ 377 - 0
s12_task_system/code.py

@@ -0,0 +1,377 @@
+#!/usr/bin/env python3
+"""
+s12: 任务系统 — 用文件持久化带 blockedBy 依赖的任务图。
+
+运行:  python s12_task_system/code.py
+需要: pip install anthropic python-dotenv + .env 中配置 ANTHROPIC_API_KEY
+
+相对 s11 的变化:
+  - 任务 dataclass(id、subject、description、status、owner、blockedBy)
+  - TASKS_DIR = .tasks/,用于持久化 JSON 存储
+  - create_task / save_task / load_task / list_tasks / get_task
+  - can_start:检查 blockedBy 是否全部完成(缺失依赖 = 被阻塞)
+  - claim_task:设置 owner + pending -> in_progress
+  - complete_task:设置 completed + 报告下游解锁任务
+  - 5 个新工具:create_task、list_tasks、get_task、claim_task、complete_task
+
+说明:教学代码保留一个基础 Agent 循环,以便聚焦任务系统。
+S11 的完整错误恢复(RecoveryState、退避、升级、reactive compact、备用模型)被省略;
+在真实 CC 中,tasks.ts 和 withRetry 是可以自然组合的独立层。
+"""
+
+import os, subprocess, json, time, random
+from pathlib import Path
+from dataclasses import dataclass, asdict
+
+try:
+    import readline
+    readline.parse_and_bind('set bind-tty-special-chars off')
+except ImportError:
+    pass
+
+from anthropic import Anthropic
+from dotenv import load_dotenv
+
+load_dotenv(override=True)
+if os.getenv("ANTHROPIC_BASE_URL"):
+    os.environ.pop("ANTHROPIC_AUTH_TOKEN", None)
+
+WORKDIR = Path.cwd()
+MEMORY_DIR = WORKDIR / ".memory"
+MEMORY_INDEX = MEMORY_DIR / "MEMORY.md"
+client = Anthropic(base_url=os.getenv("ANTHROPIC_BASE_URL"))
+MODEL = os.environ["MODEL_ID"]
+
+# ── 任务系统 ──
+
+TASKS_DIR = WORKDIR / ".tasks"
+TASKS_DIR.mkdir(exist_ok=True)
+
+
+@dataclass
+class Task:
+    id: str
+    subject: str
+    description: str
+    status: str          # pending | in_progress | completed
+    owner: str | None    # Agent 名称(多 Agent 场景)
+    blockedBy: list[str] # 依赖任务 ID
+
+
+def _task_path(task_id: str) -> Path:
+    return TASKS_DIR / f"{task_id}.json"
+
+
+def create_task(subject: str, description: str = "",
+                blockedBy: list[str] | None = None) -> Task:
+    task = Task(
+        id=f"task_{int(time.time())}_{random.randint(0, 9999):04d}",
+        subject=subject,
+        description=description,
+        status="pending",
+        owner=None,
+        blockedBy=blockedBy or [],
+    )
+    save_task(task)
+    return task
+
+
+def save_task(task: Task):
+    _task_path(task.id).write_text(json.dumps(asdict(task), indent=2))
+
+
+def load_task(task_id: str) -> Task:
+    return Task(**json.loads(_task_path(task_id).read_text()))
+
+
+def list_tasks() -> list[Task]:
+    return [Task(**json.loads(p.read_text()))
+            for p in sorted(TASKS_DIR.glob("task_*.json"))]
+
+
+def get_task(task_id: str) -> str:
+    """以 JSON 返回完整任务详情。"""
+    task = load_task(task_id)
+    return json.dumps(asdict(task), indent=2)
+
+
+def can_start(task_id: str) -> bool:
+    """Check if all blockedBy dependencies are completed.
+    Missing dependencies are treated as blocked."""
+    task = load_task(task_id)
+    for dep_id in task.blockedBy:
+        if not _task_path(dep_id).exists():
+            return False
+        if load_task(dep_id).status != "completed":
+            return False
+    return True
+
+
+def claim_task(task_id: str, owner: str = "agent") -> str:
+    task = load_task(task_id)
+    if task.status != "pending":
+        return f"任务 {task_id} 当前状态为 {task.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}"
+    task.owner = owner
+    task.status = "in_progress"
+    save_task(task)
+    print(f"  \033[36m[claim] {task.subject} → in_progress (owner: {owner})\033[0m")
+    return f"已认领 {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},无法完成"
+    task.status = "completed"
+    save_task(task)
+    unblocked = [t.subject for t in list_tasks()
+                 if t.status == "pending" and t.blockedBy and can_start(t.id)]
+    print(f"  \033[32m[complete] {task.subject} ✓\033[0m")
+    msg = f"已完成 {task.id} ({task.subject})"
+    if unblocked:
+        msg += f"\n已解除阻塞:{', '.join(unblocked)}"
+        print(f"  \033[33m[unblocked] {', '.join(unblocked)}\033[0m")
+    return msg
+
+
+# ── 提示词组装 (来自 s10,已同步) ──
+
+PROMPT_SECTIONS = {
+    "identity": "你是一个编码 Agent。直接行动,不要只解释。",
+    "tools": "可用工具:bash, read_file, write_file, "
+             "create_task, list_tasks, get_task, claim_task, complete_task.",
+    "workspace": f"工作目录:{WORKDIR}",
+    "memory": "有可用的相关记忆时,会在下方注入。",
+}
+
+
+def assemble_system_prompt(context: dict) -> str:
+    sections = [PROMPT_SECTIONS["identity"],
+                PROMPT_SECTIONS["tools"],
+                PROMPT_SECTIONS["workspace"]]
+    memories = context.get("memories", "")
+    if memories:
+        sections.append(f"Relevant memories:\n{memories}")
+    return "\n\n".join(sections)
+
+
+_last_context_key, _last_prompt = None, None
+
+
+def get_system_prompt(context: dict) -> str:
+    global _last_context_key, _last_prompt
+    key = json.dumps(context, sort_keys=True, ensure_ascii=False, default=str)
+    if key == _last_context_key and _last_prompt:
+        return _last_prompt
+    _last_context_key = key
+    _last_prompt = assemble_system_prompt(context)
+    return _last_prompt
+
+
+# ── 工具 ──
+
+def safe_path(p: str) -> Path:
+    path = (WORKDIR / p).resolve()
+    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)
+        out = (r.stdout + r.stderr).strip()
+        return out[:50000] if out else "(无输出)"
+    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} 行更多内容)"]
+        return "\n".join(lines)
+    except Exception as e:
+        return f"错误:{e}"
+
+
+def run_write(path: str, content: str) -> str:
+    try:
+        fp = safe_path(path)
+        fp.parent.mkdir(parents=True, exist_ok=True)
+        fp.write_text(content)
+        return f"已写入 {len(content)} 字节到 {path}"
+    except Exception as e:
+        return f"错误:{e}"
+
+
+# 任务工具
+
+def run_create_task(subject: str, description: str = "",
+                    blockedBy: list[str] | None = None) -> str:
+    task = create_task(subject, description, blockedBy)
+    deps = f" (blockedBy: {', '.join(blockedBy)})" if blockedBy else ""
+    print(f"  \033[34m[create] {task.subject}{deps}\033[0m")
+    return f"已创建 {task.id}: {task.subject}{deps}"
+
+
+def run_list_tasks() -> str:
+    个任务 = list_tasks()
+    if not 个任务:
+        return "暂无任务。请使用 create_task 添加任务。"
+    lines = []
+    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 ""
+        lines.append(f"  {icon} {t.id}: {t.subject} "
+                     f"[{t.status}]{owner}{deps}")
+    return "\n".join(lines)
+
+
+def run_get_task(task_id: str) -> str:
+    try:
+        return get_task(task_id)
+    except FileNotFoundError:
+        return f"错误:任务 {task_id} 未找到"
+
+
+def run_claim_task(task_id: str) -> str:
+    return claim_task(task_id, owner="agent")
+
+
+def run_complete_task(task_id: str) -> str:
+    return complete_task(task_id)
+
+
+TOOLS = [
+    {"name": "bash", "description": "运行一条 shell 命令。",
+     "input_schema": {"type": "object",
+                      "properties": {"command": {"type": "string"}},
+                      "required": ["command"]}},
+    {"name": "read_file", "description": "读取文件内容。",
+     "input_schema": {"type": "object",
+                      "properties": {"path": {"type": "string"},
+                                     "limit": {"type": "integer"}},
+                      "required": ["path"]}},
+    {"name": "write_file", "description": "向文件写入内容。",
+     "input_schema": {"type": "object",
+                      "properties": {"path": {"type": "string"},
+                                     "content": {"type": "string"}},
+                      "required": ["path", "content"]}},
+    {"name": "create_task",
+     "description": "创建一个新任务,可选 blockedBy 依赖。",
+     "input_schema": {"type": "object",
+                      "properties": {
+                          "subject": {"type": "string"},
+                          "description": {"type": "string"},
+                          "blockedBy": {"type": "array",
+                                        "items": {"type": "string"}}},
+                      "required": ["subject"]}},
+    {"name": "list_tasks",
+     "description": "列出所有任务及其状态、负责人和依赖。",
+     "input_schema": {"type": "object", "properties": {},
+                      "required": []}},
+    {"name": "get_task",
+     "description": "按 ID 获取指定任务的完整详情。",
+     "input_schema": {"type": "object",
+                      "properties": {"task_id": {"type": "string"}},
+                      "required": ["task_id"]}},
+    {"name": "claim_task",
+     "description": "认领一个待处理任务。设置 owner,并将状态改为 in_progress。",
+     "input_schema": {"type": "object",
+                      "properties": {"task_id": {"type": "string"}},
+                      "required": ["task_id"]}},
+    {"name": "complete_task",
+     "description": "完成一个进行中的任务。报告被解除阻塞的下游任务。",
+     "input_schema": {"type": "object",
+                      "properties": {"task_id": {"type": "string"}},
+                      "required": ["task_id"]}},
+]
+
+TOOL_HANDLERS = {
+    "bash": run_bash, "read_file": run_read, "write_file": run_write,
+    "create_task": run_create_task, "list_tasks": run_list_tasks,
+    "get_task": run_get_task, "claim_task": run_claim_task,
+    "complete_task": run_complete_task,
+}
+
+
+# ── 上下文 ──
+
+def update_context(context: dict, messages: list) -> dict:
+    """Derive context from real state."""
+    memories = ""
+    if MEMORY_INDEX.exists():
+        content = MEMORY_INDEX.read_text().strip()
+        if content:
+            memories = content
+    return {
+        "enabled_tools": list(TOOL_HANDLERS.keys()),
+        "workspace": str(WORKDIR),
+        "memories": memories,
+    }
+
+
+# ── Agent 循环(简化版,聚焦任务系统) ──
+
+def agent_loop(messages: list, context: dict):
+    system = get_system_prompt(context)
+    while True:
+        try:
+            response = client.messages.create(
+                model=MODEL, system=system, messages=messages,
+                tools=TOOLS, max_tokens=8000)
+        except Exception as e:
+            messages.append({"role": "assistant", "content": [
+                {"type": "text",
+                 "text": f"[错误] {type(e).__name__}: {e}"}]})
+            return
+
+        messages.append({"role": "assistant", "content": response.content})
+        if response.stop_reason != "tool_use":
+            return
+
+        results = []
+        for block in response.content:
+            if block.type != "tool_use":
+                continue
+            print(f"\033[36m> {block.name}\033[0m")
+            handler = TOOL_HANDLERS.get(block.name)
+            output = handler(**block.input) if handler else f"未知工具:{block.name}"
+            print(str(output)[:300])
+            results.append({"type": "tool_result",
+                            "tool_use_id": block.id, "content": output})
+        messages.append({"role": "user", "content": results})
+        context = update_context(context, messages)
+        system = get_system_prompt(context)
+
+
+if __name__ == "__main__":
+    print("s12: 任务系统")
+    print("输入问题后按回车发送。输入 q 退出。\n")
+    history = []
+    context = update_context({}, [])
+    while True:
+        try:
+            query = input("\033[36ms12 >> \033[0m")
+        except (EOFError, KeyboardInterrupt):
+            break
+        if query.strip().lower() in ("q", "exit", ""):
+            break
+        history.append({"role": "user", "content": query})
+        agent_loop(history, context)
+        context = update_context(context, history)
+        for block in history[-1]["content"]:
+            if getattr(block, "type", None) == "text":
+                print(block.text)
+            elif isinstance(block, dict) and block.get("type") == "text":
+                print(block.get("text", ""))
+        print()

+ 479 - 0
s13_background_tasks/code.py

@@ -0,0 +1,479 @@
+#!/usr/bin/env python3
+"""
+s13: 后台任务 — 基于线程的异步执行 + 通知注入。
+
+运行:  python s13_background_tasks/code.py
+需要: pip install anthropic python-dotenv + .env 中配置 ANTHROPIC_API_KEY
+
+相对 s12 的变化:
+  - 使用 threading.Thread 做后台执行
+  - background_tasks 字典跟踪生命周期(bg_id、command、status)
+  - background_results 字典 + threading.Lock 实现线程安全存储
+  - should_run_background:模型通过 run_in_background 参数显式请求
+  - is_slow_operation:当模型未指定时使用的兜底启发式判断
+  - start_background_task:分发到守护线程,返回后台任务 id
+  - collect_background_results:收集已完成任务,并以通知形式返回
+  - agent_loop:慢操作 → 后台执行 + 占位结果,随后注入通知
+  - 通知使用 <task_notification> 格式,不复用 tool_use_id
+
+说明:教学代码保留一个基础 Agent 循环,以便聚焦后台任务。
+S11 的完整错误恢复(RecoveryState、退避、升级、reactive compact、备用模型)被省略。
+"""
+
+import os, subprocess, json, time, random, threading
+from pathlib import Path
+from dataclasses import dataclass, asdict
+
+try:
+    import readline
+    readline.parse_and_bind('set bind-tty-special-chars off')
+except ImportError:
+    pass
+
+from anthropic import Anthropic
+from dotenv import load_dotenv
+
+load_dotenv(override=True)
+if os.getenv("ANTHROPIC_BASE_URL"):
+    os.environ.pop("ANTHROPIC_AUTH_TOKEN", None)
+
+WORKDIR = Path.cwd()
+MEMORY_DIR = WORKDIR / ".memory"
+MEMORY_INDEX = MEMORY_DIR / "MEMORY.md"
+client = Anthropic(base_url=os.getenv("ANTHROPIC_BASE_URL"))
+MODEL = os.environ["MODEL_ID"]
+
+# ── 任务系统 (来自 s12,已同步) ──
+
+TASKS_DIR = WORKDIR / ".tasks"
+TASKS_DIR.mkdir(exist_ok=True)
+
+
+@dataclass
+class Task:
+    id: str
+    subject: str
+    description: str
+    status: str          # pending | in_progress | completed
+    owner: str | None
+    blockedBy: list[str]
+
+
+def _task_path(task_id: str) -> Path:
+    return TASKS_DIR / f"{task_id}.json"
+
+
+def create_task(subject: str, description: str = "",
+                blockedBy: list[str] | None = None) -> Task:
+    task = Task(
+        id=f"task_{int(time.time())}_{random.randint(0, 9999):04d}",
+        subject=subject, description=description,
+        status="pending", owner=None,
+        blockedBy=blockedBy or [],
+    )
+    save_task(task)
+    return task
+
+
+def save_task(task: Task):
+    _task_path(task.id).write_text(json.dumps(asdict(task), indent=2))
+
+
+def load_task(task_id: str) -> Task:
+    return Task(**json.loads(_task_path(task_id).read_text()))
+
+
+def list_tasks() -> list[Task]:
+    return [Task(**json.loads(p.read_text()))
+            for p in sorted(TASKS_DIR.glob("task_*.json"))]
+
+
+def get_task(task_id: str) -> str:
+    """以 JSON 返回完整任务详情。"""
+    task = load_task(task_id)
+    return json.dumps(asdict(task), indent=2)
+
+
+def can_start(task_id: str) -> bool:
+    """Check if all blockedBy dependencies are completed.
+    Missing dependencies are treated as blocked."""
+    task = load_task(task_id)
+    for dep_id in task.blockedBy:
+        if not _task_path(dep_id).exists():
+            return False
+        if load_task(dep_id).status != "completed":
+            return False
+    return True
+
+
+def claim_task(task_id: str, owner: str = "agent") -> str:
+    task = load_task(task_id)
+    if task.status != "pending":
+        return f"任务 {task_id} 当前状态为 {task.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}"
+    task.owner = owner
+    task.status = "in_progress"
+    save_task(task)
+    print(f"  \033[36m[claim] {task.subject} → in_progress (owner: {owner})\033[0m")
+    return f"已认领 {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},无法完成"
+    task.status = "completed"
+    save_task(task)
+    unblocked = [t.subject for t in list_tasks()
+                 if t.status == "pending" and t.blockedBy and can_start(t.id)]
+    print(f"  \033[32m[complete] {task.subject} ✓\033[0m")
+    msg = f"已完成 {task.id} ({task.subject})"
+    if unblocked:
+        msg += f"\n已解除阻塞:{', '.join(unblocked)}"
+        print(f"  \033[33m[unblocked] {', '.join(unblocked)}\033[0m")
+    return msg
+
+
+# ── 提示词组装 (来自 s10,已同步) ──
+
+PROMPT_SECTIONS = {
+    "identity": "你是一个编码 Agent。直接行动,不要只解释。",
+    "tools": "可用工具:bash, read_file, write_file, "
+             "create_task, list_tasks, get_task, claim_task, complete_task.",
+    "workspace": f"工作目录:{WORKDIR}",
+    "memory": "有可用的相关记忆时,会在下方注入。",
+}
+
+
+def assemble_system_prompt(context: dict) -> str:
+    sections = [PROMPT_SECTIONS["identity"],
+                PROMPT_SECTIONS["tools"],
+                PROMPT_SECTIONS["workspace"]]
+    memories = context.get("memories", "")
+    if memories:
+        sections.append(f"Relevant memories:\n{memories}")
+    return "\n\n".join(sections)
+
+
+_last_context_key, _last_prompt = None, None
+
+
+def get_system_prompt(context: dict) -> str:
+    global _last_context_key, _last_prompt
+    key = json.dumps(context, sort_keys=True, ensure_ascii=False, default=str)
+    if key == _last_context_key and _last_prompt:
+        return _last_prompt
+    _last_context_key = key
+    _last_prompt = assemble_system_prompt(context)
+    return _last_prompt
+
+
+# ── 工具 ──
+
+def safe_path(p: str) -> Path:
+    path = (WORKDIR / p).resolve()
+    if not path.is_relative_to(WORKDIR):
+        raise ValueError(f"路径逃逸出工作区:{p}")
+    return path
+
+
+def run_bash(command: str, run_in_background: bool = False) -> str:
+    # run_in_background 由 agent_loop 分发处理,不在这里处理
+    try:
+        r = subprocess.run(command, shell=True, cwd=WORKDIR,
+                           capture_output=True, text=True, timeout=120)
+        out = (r.stdout + r.stderr).strip()
+        return out[:50000] if out else "(无输出)"
+    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} 行更多内容)"]
+        return "\n".join(lines)
+    except Exception as e:
+        return f"错误:{e}"
+
+
+def run_write(path: str, content: str) -> str:
+    try:
+        fp = safe_path(path)
+        fp.parent.mkdir(parents=True, exist_ok=True)
+        fp.write_text(content)
+        return f"已写入 {len(content)} 字节到 {path}"
+    except Exception as e:
+        return f"错误:{e}"
+
+
+# 任务工具
+
+def run_create_task(subject: str, description: str = "",
+                    blockedBy: list[str] | None = None) -> str:
+    task = create_task(subject, description, blockedBy)
+    deps = f" (blockedBy: {', '.join(blockedBy)})" if blockedBy else ""
+    print(f"  \033[34m[create] {task.subject}{deps}\033[0m")
+    return f"已创建 {task.id}: {task.subject}{deps}"
+
+
+def run_list_tasks() -> str:
+    个任务 = list_tasks()
+    if not 个任务:
+        return "暂无任务。请使用 create_task 添加任务。"
+    lines = []
+    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 ""
+        lines.append(f"  {icon} {t.id}: {t.subject} "
+                     f"[{t.status}]{owner}{deps}")
+    return "\n".join(lines)
+
+
+def run_get_task(task_id: str) -> str:
+    try:
+        return get_task(task_id)
+    except FileNotFoundError:
+        return f"错误:任务 {task_id} 未找到"
+
+
+def run_claim_task(task_id: str) -> str:
+    return claim_task(task_id, owner="agent")
+
+
+def run_complete_task(task_id: str) -> str:
+    return complete_task(task_id)
+
+
+TOOLS = [
+    {"name": "bash", "description": "运行一条 shell 命令。",
+     "input_schema": {"type": "object",
+                      "properties": {
+                          "command": {"type": "string"},
+                          "run_in_background": {"type": "boolean"}},
+                      "required": ["command"]}},
+    {"name": "read_file", "description": "读取文件内容。",
+     "input_schema": {"type": "object",
+                      "properties": {"path": {"type": "string"},
+                                     "limit": {"type": "integer"}},
+                      "required": ["path"]}},
+    {"name": "write_file", "description": "向文件写入内容。",
+     "input_schema": {"type": "object",
+                      "properties": {"path": {"type": "string"},
+                                     "content": {"type": "string"}},
+                      "required": ["path", "content"]}},
+    {"name": "create_task",
+     "description": "创建一个新任务,可选 blockedBy 依赖。",
+     "input_schema": {"type": "object",
+                      "properties": {
+                          "subject": {"type": "string"},
+                          "description": {"type": "string"},
+                          "blockedBy": {"type": "array",
+                                        "items": {"type": "string"}}},
+                      "required": ["subject"]}},
+    {"name": "list_tasks",
+     "description": "列出所有任务及其状态、负责人和依赖。",
+     "input_schema": {"type": "object", "properties": {},
+                      "required": []}},
+    {"name": "get_task",
+     "description": "按 ID 获取指定任务的完整详情。",
+     "input_schema": {"type": "object",
+                      "properties": {"task_id": {"type": "string"}},
+                      "required": ["task_id"]}},
+    {"name": "claim_task",
+     "description": "认领一个待处理任务。设置 owner,并将状态改为 in_progress。",
+     "input_schema": {"type": "object",
+                      "properties": {"task_id": {"type": "string"}},
+                      "required": ["task_id"]}},
+    {"name": "complete_task",
+     "description": "完成一个进行中的任务。报告被解除阻塞的下游任务。",
+     "input_schema": {"type": "object",
+                      "properties": {"task_id": {"type": "string"}},
+                      "required": ["task_id"]}},
+]
+
+TOOL_HANDLERS = {
+    "bash": run_bash, "read_file": run_read, "write_file": run_write,
+    "create_task": run_create_task, "list_tasks": run_list_tasks,
+    "get_task": run_get_task, "claim_task": run_claim_task,
+    "complete_task": run_complete_task,
+}
+
+
+# ── 后台任务 (s13 新增) ──
+
+_bg_计数器 = 0
+background_tasks: dict[str, dict] = {}   # bg_id → {tool_use_id, command, status}
+background_results: dict[str, str] = {}   # bg_id → output
+background_lock = threading.Lock()
+
+
+def is_slow_operation(tool_name: str, tool_input: dict) -> bool:
+    """兜底启发式:判断命令是否可能超过 30 秒。"""
+    if tool_name != "bash":
+        return False
+    cmd = tool_input.get("command", "").lower()
+    slow_keywords = ["install", "build", "test", "deploy", "compile",
+                     "docker build", "pip install", "npm install",
+                     "cargo build", "pytest", "make"]
+    return any(kw in cmd for kw in slow_keywords)
+
+
+def should_run_background(tool_name: str, tool_input: dict) -> bool:
+    """模型的显式请求优先;否则使用启发式兜底。"""
+    if tool_input.get("run_in_background"):
+        return True
+    return is_slow_operation(tool_name, tool_input)
+
+
+def execute_tool(block) -> str:
+    """执行工具调用块并返回输出。"""
+    handler = TOOL_HANDLERS.get(block.name)
+    if handler:
+        return handler(**block.input)
+    return f"未知工具:{block.name}"
+
+
+def start_background_task(block) -> str:
+    """在守护线程中运行工具,并返回后台任务 ID。"""
+    global _bg_计数器
+    _bg_计数器 += 1
+    bg_id = f"bg_{_bg_计数器:04d}"
+    cmd = block.input.get("command", block.name)
+
+    def worker():
+        result = execute_tool(block)
+        with background_lock:
+            background_tasks[bg_id]["status"] = "completed"
+            background_results[bg_id] = result
+
+    with background_lock:
+        background_tasks[bg_id] = {
+            "tool_use_id": block.id,
+            "command": cmd,
+            "status": "running",
+        }
+    thread = threading.Thread(target=worker, daemon=True)
+    thread.start()
+    print(f"  \033[33m[background] dispatched {bg_id}: {cmd[:40]}\033[0m")
+    return bg_id
+
+
+def collect_background_results() -> list[str]:
+    """将已完成的后台结果收集为 task_notification 消息。"""
+    with background_lock:
+        ready_ids = [bid for bid, task in background_tasks.items()
+                     if task["status"] == "completed"]
+    notifications = []
+    for bg_id in ready_ids:
+        with background_lock:
+            task = background_tasks.pop(bg_id)
+            output = background_results.pop(bg_id, "")
+        summary = output[:200] if len(output) > 200 else output
+        notifications.append(
+            f"<task_notification>\n"
+            f"  <task_id>{bg_id}</task_id>\n"
+            f"  <status>completed</status>\n"
+            f"  <command>{task['command']}</command>\n"
+            f"  <summary>{summary}</summary>\n"
+            f"</task_notification>")
+        print(f"  \033[32m[background done] {bg_id}: "
+              f"{task['command'][:40]} ({len(output)} 个字符)\033[0m")
+    return notifications
+
+
+# ── 上下文 ──
+
+def update_context(context: dict, messages: list) -> dict:
+    """Derive context from real state."""
+    memories = ""
+    if MEMORY_INDEX.exists():
+        content = MEMORY_INDEX.read_text().strip()
+        if content:
+            memories = content
+    return {
+        "enabled_tools": list(TOOL_HANDLERS.keys()),
+        "workspace": str(WORKDIR),
+        "memories": memories,
+    }
+
+
+# ── Agent 循环(简化版,聚焦后台任务) ──
+
+def agent_loop(messages: list, context: dict):
+    system = get_system_prompt(context)
+    while True:
+        try:
+            response = client.messages.create(
+                model=MODEL, system=system, messages=messages,
+                tools=TOOLS, max_tokens=8000)
+        except Exception as e:
+            messages.append({"role": "assistant", "content": [
+                {"type": "text",
+                 "text": f"[错误] {type(e).__name__}: {e}"}]})
+            return
+
+        messages.append({"role": "assistant", "content": response.content})
+        if response.stop_reason != "tool_use":
+            return
+
+        results = []
+        for block in response.content:
+            if block.type != "tool_use":
+                continue
+            print(f"\033[36m> {block.name}\033[0m")
+
+            if should_run_background(block.name, block.input):
+                bg_id = start_background_task(block)
+                results.append({"type": "tool_result",
+                                "tool_use_id": block.id,
+                                "content": f"[Background task {bg_id} started] "
+                                           f"命令:{block.input.get('command', '')}. "
+                                           f"完成后结果将可用。"})
+            else:
+                output = execute_tool(block)
+                print(str(output)[:300])
+                results.append({"type": "tool_result",
+                                "tool_use_id": block.id,
+                                "content": output})
+
+        # 在一条用户消息中注入工具结果 + 后台通知
+        user_content = list(results)
+        bg_notifications = collect_background_results()
+        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")
+        messages.append({"role": "user", "content": user_content})
+        context = update_context(context, messages)
+        system = get_system_prompt(context)
+
+
+if __name__ == "__main__":
+    print("s13: background 个任务")
+    print("输入问题后按回车发送。输入 q 退出。\n")
+    history = []
+    context = update_context({}, [])
+    while True:
+        try:
+            query = input("\033[36ms13 >> \033[0m")
+        except (EOFError, KeyboardInterrupt):
+            break
+        if query.strip().lower() in ("q", "exit", ""):
+            break
+        history.append({"role": "user", "content": query})
+        agent_loop(history, context)
+        context = update_context(context, history)
+        for block in history[-1]["content"]:
+            if getattr(block, "type", None) == "text":
+                print(block.text)
+            elif isinstance(block, dict) and block.get("type") == "text":
+                print(block.get("text", ""))
+        print()

+ 804 - 0
s14_cron_scheduler/code.py

@@ -0,0 +1,804 @@
+#!/usr/bin/env python3
+"""
+s14: Cron 调度器 — 独立守护线程 + 队列处理器。
+
+运行:  python s14_cron_scheduler/code.py
+需要: pip install anthropic python-dotenv + .env 中配置 ANTHROPIC_API_KEY
+
+相对 s13 的变化:
+  - Cron任务 dataclass(id、cron、prompt、recurring、durable)
+  - cron_matches:带 DOM/DOW OR 语义的 5 字段 cron 表达式匹配
+  - schedule_job / cancel_job:注册/移除 cron 任务(带校验)
+  - cron_scheduler_loop:独立守护线程,每 1 秒轮询
+  - cron_queue:线程安全队列,调度器写入,队列处理器负责投递
+  - queue_processor_loop:当 cron_queue 有任务时自动运行 agent_loop
+  - 持久化存储:.scheduled_tasks.json(重启后仍保留)
+  - 3 个新工具:schedule_cron、list_crons、cancel_cron
+
+四层结构:
+  1. 调度器:守护线程检查时间 → 触发匹配任务
+  2. 队列:cron_queue 将调度器与 Agent 循环解耦
+  3. 队列处理器:当队列中有任务且 Agent 空闲时唤醒 Agent
+  4. 消费者:agent_loop 消费队列任务,并注入到 messages
+"""
+
+import os, subprocess, json, time, random, threading
+from pathlib import Path
+from datetime import datetime
+from dataclasses import dataclass, asdict
+
+try:
+    import readline
+    readline.parse_and_bind('set bind-tty-special-chars off')
+except ImportError:
+    pass
+
+from anthropic import Anthropic
+from dotenv import load_dotenv
+
+load_dotenv(override=True)
+if os.getenv("ANTHROPIC_BASE_URL"):
+    os.environ.pop("ANTHROPIC_AUTH_TOKEN", None)
+
+WORKDIR = Path.cwd()
+MEMORY_DIR = WORKDIR / ".memory"
+MEMORY_INDEX = MEMORY_DIR / "MEMORY.md"
+client = Anthropic(base_url=os.getenv("ANTHROPIC_BASE_URL"))
+MODEL = os.environ["MODEL_ID"]
+
+# ── 任务系统 (来自 s12,已同步) ──
+
+TASKS_DIR = WORKDIR / ".tasks"
+TASKS_DIR.mkdir(exist_ok=True)
+
+
+@dataclass
+class Task:
+    id: str
+    subject: str
+    description: str
+    status: str          # pending | in_progress | completed
+    owner: str | None
+    blockedBy: list[str]
+
+
+def _task_path(task_id: str) -> Path:
+    return TASKS_DIR / f"{task_id}.json"
+
+
+def create_task(subject: str, description: str = "",
+                blockedBy: list[str] | None = None) -> Task:
+    task = Task(
+        id=f"task_{int(time.time())}_{random.randint(0, 9999):04d}",
+        subject=subject, description=description,
+        status="pending", owner=None,
+        blockedBy=blockedBy or [],
+    )
+    save_task(task)
+    return task
+
+
+def save_task(task: Task):
+    _task_path(task.id).write_text(json.dumps(asdict(task), indent=2))
+
+
+def load_task(task_id: str) -> Task:
+    return Task(**json.loads(_task_path(task_id).read_text()))
+
+
+def list_tasks() -> list[Task]:
+    return [Task(**json.loads(p.read_text()))
+            for p in sorted(TASKS_DIR.glob("task_*.json"))]
+
+
+def get_task(task_id: str) -> str:
+    """以 JSON 返回完整任务详情。"""
+    task = load_task(task_id)
+    return json.dumps(asdict(task), indent=2)
+
+
+def can_start(task_id: str) -> bool:
+    """Check if all blockedBy dependencies are completed.
+    Missing dependencies are treated as blocked."""
+    task = load_task(task_id)
+    for dep_id in task.blockedBy:
+        if not _task_path(dep_id).exists():
+            return False
+        if load_task(dep_id).status != "completed":
+            return False
+    return True
+
+
+def claim_task(task_id: str, owner: str = "agent") -> str:
+    task = load_task(task_id)
+    if task.status != "pending":
+        return f"任务 {task_id} 当前状态为 {task.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}"
+    task.owner = owner
+    task.status = "in_progress"
+    save_task(task)
+    print(f"  \033[36m[claim] {task.subject} → in_progress (owner: {owner})\033[0m")
+    return f"已认领 {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},无法完成"
+    task.status = "completed"
+    save_task(task)
+    unblocked = [t.subject for t in list_tasks()
+                 if t.status == "pending" and t.blockedBy and can_start(t.id)]
+    print(f"  \033[32m[complete] {task.subject} ✓\033[0m")
+    msg = f"已完成 {task.id} ({task.subject})"
+    if unblocked:
+        msg += f"\n已解除阻塞:{', '.join(unblocked)}"
+        print(f"  \033[33m[unblocked] {', '.join(unblocked)}\033[0m")
+    return msg
+
+
+# ── 提示词组装 (来自 s10,已同步) ──
+
+PROMPT_SECTIONS = {
+    "identity": "你是一个编码 Agent。直接行动,不要只解释。",
+    "tools": "可用工具:bash, read_file, write_file, "
+             "create_task, list_tasks, get_task, claim_task, complete_task, "
+             "schedule_cron, list_crons, cancel_cron.",
+    "workspace": f"工作目录:{WORKDIR}",
+    "memory": "有可用的相关记忆时,会在下方注入。",
+}
+
+
+def assemble_system_prompt(context: dict) -> str:
+    sections = [PROMPT_SECTIONS["identity"],
+                PROMPT_SECTIONS["tools"],
+                PROMPT_SECTIONS["workspace"]]
+    memories = context.get("memories", "")
+    if memories:
+        sections.append(f"Relevant memories:\n{memories}")
+    return "\n\n".join(sections)
+
+
+_last_context_key, _last_prompt = None, None
+
+
+def get_system_prompt(context: dict) -> str:
+    global _last_context_key, _last_prompt
+    key = json.dumps(context, sort_keys=True, ensure_ascii=False, default=str)
+    if key == _last_context_key and _last_prompt:
+        return _last_prompt
+    _last_context_key = key
+    _last_prompt = assemble_system_prompt(context)
+    return _last_prompt
+
+
+# ── 工具 ──
+
+def safe_path(p: str) -> Path:
+    path = (WORKDIR / p).resolve()
+    if not path.is_relative_to(WORKDIR):
+        raise ValueError(f"路径逃逸出工作区:{p}")
+    return path
+
+
+def run_bash(command: str, run_in_background: bool = False) -> str:
+    # run_in_background 由 agent_loop 分发处理,不在这里处理
+    try:
+        r = subprocess.run(command, shell=True, cwd=WORKDIR,
+                           capture_output=True, text=True, timeout=120)
+        out = (r.stdout + r.stderr).strip()
+        return out[:50000] if out else "(无输出)"
+    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} 行更多内容)"]
+        return "\n".join(lines)
+    except Exception as e:
+        return f"错误:{e}"
+
+
+def run_write(path: str, content: str) -> str:
+    try:
+        fp = safe_path(path)
+        fp.parent.mkdir(parents=True, exist_ok=True)
+        fp.write_text(content)
+        return f"已写入 {len(content)} 字节到 {path}"
+    except Exception as e:
+        return f"错误:{e}"
+
+
+# 任务工具
+
+def run_create_task(subject: str, description: str = "",
+                    blockedBy: list[str] | None = None) -> str:
+    task = create_task(subject, description, blockedBy)
+    deps = f" (blockedBy: {', '.join(blockedBy)})" if blockedBy else ""
+    print(f"  \033[34m[create] {task.subject}{deps}\033[0m")
+    return f"已创建 {task.id}: {task.subject}{deps}"
+
+
+def run_list_tasks() -> str:
+    个任务 = list_tasks()
+    if not 个任务:
+        return "暂无任务。请使用 create_task 添加任务。"
+    lines = []
+    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 ""
+        lines.append(f"  {icon} {t.id}: {t.subject} "
+                     f"[{t.status}]{owner}{deps}")
+    return "\n".join(lines)
+
+
+def run_get_task(task_id: str) -> str:
+    try:
+        return get_task(task_id)
+    except FileNotFoundError:
+        return f"错误:任务 {task_id} 未找到"
+
+
+def run_claim_task(task_id: str) -> str:
+    return claim_task(task_id, owner="agent")
+
+
+def run_complete_task(task_id: str) -> str:
+    return complete_task(task_id)
+
+
+# ── 后台任务 (来自 s13,已同步) ──
+
+_bg_计数器 = 0
+background_tasks: dict[str, dict] = {}
+background_results: dict[str, str] = {}
+background_lock = threading.Lock()
+
+
+def is_slow_operation(tool_name: str, tool_input: dict) -> bool:
+    """兜底启发式:判断命令是否可能超过 30 秒。"""
+    if tool_name != "bash":
+        return False
+    cmd = tool_input.get("command", "").lower()
+    slow_keywords = ["install", "build", "test", "deploy", "compile",
+                     "docker build", "pip install", "npm install",
+                     "cargo build", "pytest", "make"]
+    return any(kw in cmd for kw in slow_keywords)
+
+
+def should_run_background(tool_name: str, tool_input: dict) -> bool:
+    """模型的显式请求优先;否则使用启发式兜底。"""
+    if tool_input.get("run_in_background"):
+        return True
+    return is_slow_operation(tool_name, tool_input)
+
+
+def execute_tool(block) -> str:
+    """执行工具调用块并返回输出。"""
+    handler = {
+        "bash": run_bash, "read_file": run_read, "write_file": run_write,
+        "create_task": run_create_task, "list_tasks": run_list_tasks,
+        "get_task": run_get_task, "claim_task": run_claim_task,
+        "complete_task": run_complete_task,
+        "schedule_cron": run_schedule_cron, "list_crons": run_list_crons,
+        "cancel_cron": run_cancel_cron,
+    }.get(block.name)
+    if handler:
+        return handler(**block.input)
+    return f"未知工具:{block.name}"
+
+
+def start_background_task(block) -> str:
+    """在守护线程中运行工具,并返回后台任务 ID。"""
+    global _bg_计数器
+    _bg_计数器 += 1
+    bg_id = f"bg_{_bg_计数器:04d}"
+    cmd = block.input.get("command", block.name)
+
+    def worker():
+        result = execute_tool(block)
+        with background_lock:
+            background_tasks[bg_id]["status"] = "completed"
+            background_results[bg_id] = result
+
+    with background_lock:
+        background_tasks[bg_id] = {
+            "tool_use_id": block.id,
+            "command": cmd,
+            "status": "running",
+        }
+    threading.Thread(target=worker, daemon=True).start()
+    print(f"  \033[33m[background] dispatched {bg_id}: {cmd[:40]}\033[0m")
+    return bg_id
+
+
+def collect_background_results() -> list[str]:
+    """将已完成的后台结果收集为 task_notification 消息。"""
+    with background_lock:
+        ready_ids = [bid for bid, task in background_tasks.items()
+                     if task["status"] == "completed"]
+    notifications = []
+    for bg_id in ready_ids:
+        with background_lock:
+            task = background_tasks.pop(bg_id)
+            output = background_results.pop(bg_id, "")
+        summary = output[:200] if len(output) > 200 else output
+        notifications.append(
+            f"<task_notification>\n"
+            f"  <task_id>{bg_id}</task_id>\n"
+            f"  <status>completed</status>\n"
+            f"  <command>{task['command']}</command>\n"
+            f"  <summary>{summary}</summary>\n"
+            f"</task_notification>")
+        print(f"  \033[32m[background done] {bg_id}: "
+              f"{task['command'][:40]} ({len(output)} 个字符)\033[0m")
+    return notifications
+
+
+# ── Cron 调度器 (s14 新增) ──
+
+DURABLE_PATH = WORKDIR / ".scheduled_tasks.json"
+
+
+@dataclass
+class CronJob:
+    id: str
+    cron: str        # "0 9 * * *"
+    prompt: str      # 触发时要注入的消息
+    recurring: bool  # True = 重复,False = 一次性
+    durable: bool    # True = 持久化到磁盘
+
+
+scheduled_jobs: dict[str, CronJob] = {}
+cron_queue: list[CronJob] = []
+cron_lock = threading.Lock()
+agent_lock = threading.Lock()
+_last_fired: dict[str, str] = {}  # job_id → "YYYY-MM-DD HH:MM"
+
+
+def _cron_field_matches(field: str, value: int) -> bool:
+    """将单个 cron 字段与一个值进行匹配。"""
+    if field == "*":
+        return True
+    if field.startswith("*/"):
+        step = int(field[2:])
+        return step > 0 and value % step == 0
+    if "," in field:
+        return any(_cron_field_matches(f.strip(), value)
+                   for f in field.split(","))
+    if "-" in field:
+        lo, hi = field.split("-", 1)
+        return int(lo) <= value <= int(hi)
+    return value == int(field)
+
+
+def cron_matches(cron_expr: str, dt: datetime) -> bool:
+    """Check if a 5-field cron expression matches the given datetime.
+    Standard cron semantics: DOM and DOW use OR when both are constrained."""
+    fields = cron_expr.strip().split()
+    if len(fields) != 5:
+        return False
+    minute, hour, dom, month, dow = fields
+    dow_val = (dt.weekday() + 1) % 7  # Python Monday=0 → cron Sunday=0
+
+    m = _cron_field_matches(minute, dt.minute)
+    h = _cron_field_matches(hour, dt.hour)
+    dom_ok = _cron_field_matches(dom, dt.day)
+    month_ok = _cron_field_matches(month, dt.month)
+    dow_ok = _cron_field_matches(dow, dow_val)
+
+    # 分钟、小时、月份必须全部匹配
+    if not (m and h and month_ok):
+        return False
+    # DOM 和 DOW:如果两者都有限制,任一匹配即可(OR)
+    dom_unconstrained = dom == "*"
+    dow_unconstrained = dow == "*"
+    if dom_unconstrained and dow_unconstrained:
+        return True
+    if dom_unconstrained:
+        return dow_ok
+    if dow_unconstrained:
+        return dom_ok
+    return dom_ok or dow_ok
+
+
+def _validate_cron_field(field: str, lo: int, hi: int) -> str | None:
+    """校验单个 cron 字段值是否位于 [lo, hi] 范围内。"""
+    if field == "*":
+        return None
+    if field.startswith("*/"):
+        step_str = field[2:]
+        if not step_str.isdigit():
+            return f"无效步长:{field}"
+        step = int(step_str)
+        if step <= 0:
+            return f"步长必须 > 0:{field}"
+        return None
+    if "," in field:
+        for part in field.split(","):
+            err = _validate_cron_field(part.strip(), lo, hi)
+            if err: return err
+        return None
+    if "-" in field:
+        parts = field.split("-", 1)
+        if not parts[0].isdigit() or not parts[1].isdigit():
+            return f"无效范围:{field}"
+        a, b = int(parts[0]), int(parts[1])
+        if a < lo or a > hi or b < lo or b > hi:
+            return f"范围 {field} 超出边界 [{lo}-{hi}]"
+        if a > b:
+            return f"范围起点大于终点:{field}"
+        return None
+    if not field.isdigit():
+        return f"无效字段:{field}"
+    val = int(field)
+    if val < lo or val > hi:
+        return f"值 {val} 超出边界 [{lo}-{hi}]"
+    return None
+
+
+def validate_cron(cron_expr: str) -> str | None:
+    """校验 cron 表达式。返回错误消息或 None。"""
+    fields = cron_expr.strip().split()
+    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"]
+    for i, (field, (lo, hi), name) in enumerate(zip(fields, bounds, names)):
+        err = _validate_cron_field(field, lo, hi)
+        if err:
+            return f"{name}: {err}"
+    return None
+
+
+def save_durable_jobs():
+    """将持久任务保存到 .scheduled_tasks.json。"""
+    durable = [asdict(j) for j in scheduled_jobs.values() if j.durable]
+    DURABLE_PATH.write_text(json.dumps(durable, indent=2))
+
+
+def load_durable_jobs():
+    """启动时从磁盘加载持久任务。"""
+    if not DURABLE_PATH.exists():
+        return
+    try:
+        jobs = json.loads(DURABLE_PATH.read_text())
+        for j in jobs:
+            job = CronJob(**j)
+            err = validate_cron(job.cron)
+            if err:
+                print(f"  \033[31m[cron] skipping invalid job {job.id}: {err}\033[0m")
+                continue
+            scheduled_jobs[job.id] = job
+        valid = [j for j in jobs if j["id"] in scheduled_jobs]
+        if valid:
+            print(f"  \033[35m[cron] loaded {len(valid)} durable job(s)\033[0m")
+    except Exception:
+        pass
+
+
+def schedule_job(cron: str, prompt: str, recurring: bool = True,
+                 durable: bool = True) -> Cron任务 | str:
+    """注册一个新的 cron 任务。返回 CronJob 或错误字符串。"""
+    err = validate_cron(cron)
+    if err:
+        return err
+    job = CronJob(
+        id=f"cron_{random.randint(0, 999999):06d}",
+        cron=cron, prompt=prompt,
+        recurring=recurring, durable=durable,
+    )
+    with cron_lock:
+        scheduled_jobs[job.id] = job
+    if durable:
+        save_durable_jobs()
+    print(f"  \033[35m[cron register] {job.id} '{cron}' → {prompt[:40]}\033[0m")
+    return job
+
+
+def cancel_job(job_id: str) -> str:
+    """取消一个 cron 任务。"""
+    with cron_lock:
+        job = scheduled_jobs.pop(job_id, None)
+    if not job:
+        return f"任务 {job_id} 未找到"
+    if job.durable:
+        save_durable_jobs()
+    print(f"  \033[31m[cron cancel] {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()
+        # 带日期感知的标记,防止每日任务从第 2 天起被跳过
+        minute_marker = now.strftime("%Y-%m-%d %H:%M")
+        with cron_lock:
+            for job in list(scheduled_jobs.values()):
+                try:
+                    if cron_matches(job.cron, now):
+                        if _last_fired.get(job.id) != minute_marker:
+                            cron_queue.append(job)
+                            _last_fired[job.id] = minute_marker
+                            print(f"  \033[35m[cron fire] {job.id} → "
+                                  f"{job.prompt[:40]}\033[0m")
+                        if not job.recurring:
+                            scheduled_jobs.pop(job.id, None)
+                            if job.durable:
+                                save_durable_jobs()
+                except Exception as e:
+                    print(f"  \033[31m[cron error] {job.id}: {e}\033[0m")
+
+
+def consume_cron_queue() -> list[CronJob]:
+    """消费 cron_queue 中已触发的任务(由 agent_loop 调用)。"""
+    with cron_lock:
+        fired = list(cron_queue)
+        cron_queue.clear()
+    return fired
+
+
+def has_cron_queue() -> bool:
+    """Return whether fired cron jobs are waiting to be delivered."""
+    with cron_lock:
+        return bool(cron_queue)
+
+
+# 启动时加载持久任务,然后启动调度线程
+load_durable_jobs()
+threading.Thread(target=cron_scheduler_loop, daemon=True).start()
+print("  \033[35m[cron] scheduler thread started\033[0m")
+
+
+# ── Cron 工具 ──
+
+def run_schedule_cron(cron: str, prompt: str,
+                      recurring: bool = True, durable: bool = True) -> str:
+    result = schedule_job(cron, prompt, recurring, durable)
+    if isinstance(result, str):
+        return f"错误:{result}"
+    return f"已调度 {result.id}: '{cron}' → {prompt}"
+
+
+def run_list_crons() -> str:
+    with cron_lock:
+        jobs = list(scheduled_jobs.values())
+    if not jobs:
+        return "暂无 cron 任务。请使用 schedule_cron 添加一个。"
+    lines = []
+    for j in jobs:
+        tag = "recurring" if j.recurring else "one-shot"
+        dur = "durable" if j.durable else "session"
+        lines.append(f"  {j.id}: '{j.cron}' → {j.prompt[:40]} "
+                     f"[{tag}, {dur}]")
+    return "\n".join(lines)
+
+
+def run_cancel_cron(job_id: str) -> str:
+    return cancel_job(job_id)
+
+
+# ── 工具定义 ──
+
+TOOLS = [
+    {"name": "bash", "description": "运行一条 shell 命令。",
+     "input_schema": {"type": "object",
+                      "properties": {
+                          "command": {"type": "string"},
+                          "run_in_background": {"type": "boolean"}},
+                      "required": ["command"]}},
+    {"name": "read_file", "description": "读取文件内容。",
+     "input_schema": {"type": "object",
+                      "properties": {"path": {"type": "string"},
+                                     "limit": {"type": "integer"}},
+                      "required": ["path"]}},
+    {"name": "write_file", "description": "向文件写入内容。",
+     "input_schema": {"type": "object",
+                      "properties": {"path": {"type": "string"},
+                                     "content": {"type": "string"}},
+                      "required": ["path", "content"]}},
+    {"name": "create_task",
+     "description": "创建一个新任务,可选 blockedBy 依赖。",
+     "input_schema": {"type": "object",
+                      "properties": {
+                          "subject": {"type": "string"},
+                          "description": {"type": "string"},
+                          "blockedBy": {"type": "array",
+                                        "items": {"type": "string"}}},
+                      "required": ["subject"]}},
+    {"name": "list_tasks",
+     "description": "列出所有任务及其状态、负责人和依赖。",
+     "input_schema": {"type": "object", "properties": {},
+                      "required": []}},
+    {"name": "get_task",
+     "description": "按 ID 获取指定任务的完整详情。",
+     "input_schema": {"type": "object",
+                      "properties": {"task_id": {"type": "string"}},
+                      "required": ["task_id"]}},
+    {"name": "claim_task",
+     "description": "认领一个待处理任务。设置 owner,并将状态改为 in_progress。",
+     "input_schema": {"type": "object",
+                      "properties": {"task_id": {"type": "string"}},
+                      "required": ["task_id"]}},
+    {"name": "complete_task",
+     "description": "完成一个进行中的任务。报告被解除阻塞的下游任务。",
+     "input_schema": {"type": "object",
+                      "properties": {"task_id": {"type": "string"}},
+                      "required": ["task_id"]}},
+    {"name": "schedule_cron",
+     "description": "调度一个 cron 任务。cron 为 5 字段:分 时 月内日 月 周内日。",
+     "input_schema": {"type": "object",
+                      "properties": {
+                          "cron": {"type": "string",
+                                   "description": "5 字段 cron 表达式"},
+                          "prompt": {"type": "string",
+                                     "description": "触发时要注入的消息"},
+                          "recurring": {"type": "boolean",
+                                        "description": "True=重复,False=一次性"},
+                          "durable": {"type": "boolean",
+                                      "description": "True=持久化到磁盘"}},
+                      "required": ["cron", "prompt"]}},
+    {"name": "list_crons",
+     "description": "列出所有已注册的 cron 任务。",
+     "input_schema": {"type": "object", "properties": {},
+                      "required": []}},
+    {"name": "cancel_cron",
+     "description": "按 ID 取消 cron 任务。",
+     "input_schema": {"type": "object",
+                      "properties": {"job_id": {"type": "string"}},
+                      "required": ["job_id"]}},
+]
+
+
+# ── 上下文 ──
+
+def update_context(context: dict, messages: list) -> dict:
+    """Derive context from real state."""
+    memories = ""
+    if MEMORY_INDEX.exists():
+        content = MEMORY_INDEX.read_text().strip()
+        if content:
+            memories = content
+    return {
+        "enabled_tools": [t["name"] for t in TOOLS],
+        "workspace": str(WORKDIR),
+        "memories": memories,
+    }
+
+
+# ── Agent 循环(简化版,聚焦 cron 调度器) ──
+# 教学代码保留基础 Agent 循环。省略 S11 的完整错误恢复。
+# cron_scheduler_loop 产出任务;当
+# 存在排队任务且没有其他 Agent 轮次运行时,queue_processor_loop 会唤醒该循环。
+
+def agent_loop(messages: list, context: dict) -> dict:
+    system = get_system_prompt(context)
+    while True:
+        # 第 4 层:消费已触发的 cron 任务 → 作为消息注入
+        fired = consume_cron_queue()
+        for job in fired:
+            messages.append({"role": "user",
+                             "content": f"[Scheduled] {job.prompt}"})
+            print(f"  \033[35m[inject cron] {job.prompt[:50]}\033[0m")
+
+        try:
+            response = client.messages.create(
+                model=MODEL, system=system, messages=messages,
+                tools=TOOLS, max_tokens=8000)
+        except Exception as e:
+            messages.append({"role": "assistant", "content": [
+                {"type": "text",
+                 "text": f"[错误] {type(e).__name__}: {e}"}]})
+            return context
+
+        messages.append({"role": "assistant", "content": response.content})
+        if response.stop_reason != "tool_use":
+            return context
+
+        results = []
+        for block in response.content:
+            if block.type != "tool_use":
+                continue
+            print(f"\033[36m> {block.name}\033[0m")
+
+            if should_run_background(block.name, block.input):
+                bg_id = start_background_task(block)
+                results.append({"type": "tool_result",
+                                "tool_use_id": block.id,
+                                "content": f"[Background task {bg_id} started] "
+                                           f"完成后结果将可用。"})
+            else:
+                output = execute_tool(block)
+                print(str(output)[:300])
+                results.append({"type": "tool_result",
+                                "tool_use_id": block.id,
+                                "content": output})
+
+        # 将后台工具结果 + 通知合并成一条用户消息
+        user_content = list(results)
+        bg_notifications = collect_background_results()
+        if bg_notifications:
+            for notif in bg_notifications:
+                user_content.append({"type": "text", "text": notif})
+        messages.append({"role": "user", "content": user_content})
+        context = update_context(context, messages)
+        system = get_system_prompt(context)
+
+
+session_history: list = []
+session_context = update_context({}, [])
+
+
+def print_latest_assistant_text(messages: list):
+    """Print text blocks from the latest assistant message."""
+    if not messages:
+        return
+    msg = messages[-1]
+    if not isinstance(msg, dict) or msg.get("role") != "assistant":
+        return
+    content = msg.get("content", "")
+    if isinstance(content, str):
+        print(content)
+        return
+    for block in content:
+        if getattr(block, "type", None) == "text":
+            print(block.text)
+        elif isinstance(block, dict) and block.get("type") == "text":
+            print(block.get("text", ""))
+
+
+def run_agent_turn_locked(user_query: str | None = None):
+    """Run one agent turn. Caller must hold agent_lock."""
+    global session_context
+    if user_query is not None:
+        session_history.append({"role": "user", "content": user_query})
+    session_context = agent_loop(session_history, session_context)
+    session_context = update_context(session_context, session_history)
+    print_latest_assistant_text(session_history)
+    print()
+
+
+def queue_processor_loop():
+    """Auto-deliver fired cron jobs when the agent is idle."""
+    global session_context
+    while True:
+        time.sleep(0.2)
+        if not has_cron_queue():
+            continue
+        if not agent_lock.acquire(blocking=False):
+            continue
+        try:
+            if not has_cron_queue():
+                continue
+            print("\n  \033[35m[队列处理器] 正在投递调度任务\033[0m")
+            run_agent_turn_locked()
+        finally:
+            agent_lock.release()
+
+
+if __name__ == "__main__":
+    print("s14: cron 调度器")
+    print("输入问题后按回车发送。输入 q 退出。\n")
+    threading.Thread(target=queue_processor_loop, daemon=True).start()
+    print("  \033[35m[队列处理器] 已启动\033[0m")
+    while True:
+        try:
+            query = input("\033[36ms14 >> \033[0m")
+        except (EOFError, KeyboardInterrupt):
+            break
+        if query.strip().lower() in ("q", "exit", ""):
+            break
+        with agent_lock:
+            run_agent_turn_locked(query)

+ 985 - 0
s15_agent_teams/code.py

@@ -0,0 +1,985 @@
+#!/usr/bin/env python3
+"""
+s15: Agent 团队 — MessageBus + spawn_teammate_thread + 收件箱注入。
+
+运行:  python s15_agent_teams/code.py
+需要: pip install anthropic python-dotenv + .env 中配置 ANTHROPIC_API_KEY
+
+相对 s14 的变化:
+  - MessageBus 类:基于文件的邮箱(.mailboxes/*.jsonl)
+  - spawn_teammate_thread:在后台线程中创建队友
+  - 队友运行自己的简化 agent_loop(bash、read、write、send_message)
+  - Lead 工具:spawn_teammate、send_message、check_inbox(3 个新增)
+  - Lead 收件箱:队友消息会注入历史(不只是打印)
+  - 教学版本:队友限制为 10 轮(真实 CC 使用空闲循环)
+
+ASCII 流程:
+  Lead: cron_queue → messages → prompt → LLM → TOOLS ────→ 循环
+                ↑                     ↓                        |
+                └── inbox ← MessageBus ← teammate.send_message ←┘
+  Teammate: inbox → LLM → bash/read/write/send → 循环(最多 10 轮)
+"""
+
+import os, subprocess, json, time, random, threading, queue
+from pathlib import Path
+from datetime import datetime
+from dataclasses import dataclass, asdict
+
+try:
+    import readline
+    readline.parse_and_bind('set bind-tty-special-chars off')
+except ImportError:
+    pass
+
+from anthropic import Anthropic
+from dotenv import load_dotenv
+
+load_dotenv(override=True)
+if os.getenv("ANTHROPIC_BASE_URL"):
+    os.environ.pop("ANTHROPIC_AUTH_TOKEN", None)
+
+WORKDIR = Path.cwd()
+MEMORY_DIR = WORKDIR / ".memory"
+MEMORY_INDEX = MEMORY_DIR / "MEMORY.md"
+client = Anthropic(base_url=os.getenv("ANTHROPIC_BASE_URL"))
+MODEL = os.environ["MODEL_ID"]
+
+# ── 任务系统 (来自 s12,已同步) ──
+
+TASKS_DIR = WORKDIR / ".tasks"
+TASKS_DIR.mkdir(exist_ok=True)
+
+
+@dataclass
+class Task:
+    id: str
+    subject: str
+    description: str
+    status: str          # pending | in_progress | completed
+    owner: str | None
+    blockedBy: list[str]
+
+
+def _task_path(task_id: str) -> Path:
+    return TASKS_DIR / f"{task_id}.json"
+
+
+def create_task(subject: str, description: str = "",
+                blockedBy: list[str] | None = None) -> Task:
+    task = Task(
+        id=f"task_{int(time.time())}_{random.randint(0, 9999):04d}",
+        subject=subject, description=description,
+        status="pending", owner=None,
+        blockedBy=blockedBy or [],
+    )
+    save_task(task)
+    return task
+
+
+def save_task(task: Task):
+    _task_path(task.id).write_text(json.dumps(asdict(task), indent=2))
+
+
+def load_task(task_id: str) -> Task:
+    return Task(**json.loads(_task_path(task_id).read_text()))
+
+
+def list_tasks() -> list[Task]:
+    return [Task(**json.loads(p.read_text()))
+            for p in sorted(TASKS_DIR.glob("task_*.json"))]
+
+
+def get_task(task_id: str) -> str:
+    """以 JSON 返回完整任务详情。"""
+    task = load_task(task_id)
+    return json.dumps(asdict(task), indent=2)
+
+
+def can_start(task_id: str) -> bool:
+    """Check if all blockedBy dependencies are completed.
+    Missing dependencies are treated as blocked."""
+    task = load_task(task_id)
+    for dep_id in task.blockedBy:
+        if not _task_path(dep_id).exists():
+            return False
+        if load_task(dep_id).status != "completed":
+            return False
+    return True
+
+
+def claim_task(task_id: str, owner: str = "agent") -> str:
+    task = load_task(task_id)
+    if task.status != "pending":
+        return f"任务 {task_id} 当前状态为 {task.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}"
+    task.owner = owner
+    task.status = "in_progress"
+    save_task(task)
+    print(f"  \033[36m[claim] {task.subject} → in_progress (owner: {owner})\033[0m")
+    return f"已认领 {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},无法完成"
+    task.status = "completed"
+    save_task(task)
+    unblocked = [t.subject for t in list_tasks()
+                 if t.status == "pending" and t.blockedBy and can_start(t.id)]
+    print(f"  \033[32m[complete] {task.subject} ✓\033[0m")
+    msg = f"已完成 {task.id} ({task.subject})"
+    if unblocked:
+        msg += f"\n已解除阻塞:{', '.join(unblocked)}"
+        print(f"  \033[33m[unblocked] {', '.join(unblocked)}\033[0m")
+    return msg
+
+
+# ── 提示词组装 (来自 s10,已同步) ──
+
+PROMPT_SECTIONS = {
+    "identity": "你是一个编码 Agent。直接行动,不要只解释。",
+    "tools": "可用工具:bash, read_file, write_file, "
+             "get_task, create_task, list_tasks, claim_task, complete_task, "
+             "schedule_cron, list_crons, cancel_cron, "
+             "spawn_teammate, send_message, check_inbox.",
+    "workspace": f"工作目录:{WORKDIR}",
+    "memory": "有可用的相关记忆时,会在下方注入。",
+}
+
+
+def assemble_system_prompt(context: dict) -> str:
+    sections = [PROMPT_SECTIONS["identity"],
+                PROMPT_SECTIONS["tools"],
+                PROMPT_SECTIONS["workspace"]]
+    memories = context.get("memories", "")
+    if memories:
+        sections.append(f"Relevant memories:\n{memories}")
+    return "\n\n".join(sections)
+
+
+_last_context_key, _last_prompt = None, None
+
+
+def get_system_prompt(context: dict) -> str:
+    global _last_context_key, _last_prompt
+    key = json.dumps(context, sort_keys=True, ensure_ascii=False, default=str)
+    if key == _last_context_key and _last_prompt:
+        return _last_prompt
+    _last_context_key = key
+    _last_prompt = assemble_system_prompt(context)
+    return _last_prompt
+
+
+# ── 工具 ──
+
+def safe_path(p: str) -> Path:
+    path = (WORKDIR / p).resolve()
+    if not path.is_relative_to(WORKDIR):
+        raise ValueError(f"路径逃逸出工作区:{p}")
+    return path
+
+
+def run_bash(command: str, run_in_background: bool = False) -> str:
+    # run_in_background 由 agent_loop 分发处理,不在这里处理
+    try:
+        r = subprocess.run(command, shell=True, cwd=WORKDIR,
+                           capture_output=True, text=True, timeout=120)
+        out = (r.stdout + r.stderr).strip()
+        return out[:50000] if out else "(无输出)"
+    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} 行更多内容)"]
+        return "\n".join(lines)
+    except Exception as e:
+        return f"错误:{e}"
+
+
+def run_write(path: str, content: str) -> str:
+    try:
+        fp = safe_path(path)
+        fp.parent.mkdir(parents=True, exist_ok=True)
+        fp.write_text(content)
+        return f"已写入 {len(content)} 字节到 {path}"
+    except Exception as e:
+        return f"错误:{e}"
+
+
+# 任务工具
+
+def run_create_task(subject: str, description: str = "",
+                    blockedBy: list[str] | None = None) -> str:
+    task = create_task(subject, description, blockedBy)
+    deps = f" (blockedBy: {', '.join(blockedBy)})" if blockedBy else ""
+    print(f"  \033[34m[create] {task.subject}{deps}\033[0m")
+    return f"已创建 {task.id}: {task.subject}{deps}"
+
+
+def run_list_tasks() -> str:
+    个任务 = list_tasks()
+    if not 个任务:
+        return "暂无任务。请使用 create_task 添加任务。"
+    lines = []
+    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 ""
+        lines.append(f"  {icon} {t.id}: {t.subject} "
+                     f"[{t.status}]{owner}{deps}")
+    return "\n".join(lines)
+
+
+def run_get_task(task_id: str) -> str:
+    try:
+        return get_task(task_id)
+    except FileNotFoundError:
+        return f"错误:任务 {task_id} 未找到"
+
+
+def run_claim_task(task_id: str) -> str:
+    return claim_task(task_id, owner="agent")
+
+
+def run_complete_task(task_id: str) -> str:
+    return complete_task(task_id)
+
+
+# ── 后台任务 (来自 s13,已同步) ──
+
+_bg_计数器 = 0
+background_tasks: dict[str, dict] = {}
+background_results: dict[str, str] = {}
+background_lock = threading.Lock()
+
+
+def is_slow_operation(tool_name: str, tool_input: dict) -> bool:
+    """兜底启发式:判断命令是否可能超过 30 秒。"""
+    if tool_name != "bash":
+        return False
+    cmd = tool_input.get("command", "").lower()
+    slow_keywords = ["install", "build", "test", "deploy", "compile",
+                     "docker build", "pip install", "npm install",
+                     "cargo build", "pytest", "make"]
+    return any(kw in cmd for kw in slow_keywords)
+
+
+def should_run_background(tool_name: str, tool_input: dict) -> bool:
+    """模型的显式请求优先;否则使用启发式兜底。"""
+    if tool_input.get("run_in_background"):
+        return True
+    return is_slow_operation(tool_name, tool_input)
+
+
+def execute_tool(block) -> str:
+    """执行工具调用块并返回输出。"""
+    handler = {
+        "bash": run_bash, "read_file": run_read, "write_file": run_write,
+        "create_task": run_create_task, "list_tasks": run_list_tasks,
+        "get_task": run_get_task, "claim_task": run_claim_task,
+        "complete_task": run_complete_task,
+        "schedule_cron": run_schedule_cron, "list_crons": run_list_crons,
+        "cancel_cron": run_cancel_cron,
+        "spawn_teammate": run_spawn_teammate,
+        "send_message": run_send_message, "check_inbox": run_check_inbox,
+    }.get(block.name)
+    if handler:
+        return handler(**block.input)
+    return f"未知工具:{block.name}"
+
+
+def start_background_task(block) -> str:
+    """在守护线程中运行工具,并返回后台任务 ID。"""
+    global _bg_计数器
+    _bg_计数器 += 1
+    bg_id = f"bg_{_bg_计数器:04d}"
+    cmd = block.input.get("command", block.name)
+
+    def worker():
+        result = execute_tool(block)
+        with background_lock:
+            background_tasks[bg_id]["status"] = "completed"
+            background_results[bg_id] = result
+
+    with background_lock:
+        background_tasks[bg_id] = {
+            "tool_use_id": block.id,
+            "command": cmd,
+            "status": "running",
+        }
+    threading.Thread(target=worker, daemon=True).start()
+    print(f"  \033[33m[background] dispatched {bg_id}: {cmd[:40]}\033[0m")
+    return bg_id
+
+
+def collect_background_results() -> list[str]:
+    """将已完成的后台结果收集为 task_notification 消息。"""
+    with background_lock:
+        ready_ids = [bid for bid, task in background_tasks.items()
+                     if task["status"] == "completed"]
+    notifications = []
+    for bg_id in ready_ids:
+        with background_lock:
+            task = background_tasks.pop(bg_id)
+            output = background_results.pop(bg_id, "")
+        summary = output[:200] if len(output) > 200 else output
+        notifications.append(
+            f"<task_notification>\n"
+            f"  <task_id>{bg_id}</task_id>\n"
+            f"  <status>completed</status>\n"
+            f"  <command>{task['command']}</command>\n"
+            f"  <summary>{summary}</summary>\n"
+            f"</task_notification>")
+        print(f"  \033[32m[background done] {bg_id}: "
+              f"{task['command'][:40]} ({len(output)} 个字符)\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())
+
+
+# ── Cron 调度器 (来自 s14,已同步) ──
+
+DURABLE_PATH = WORKDIR / ".scheduled_tasks.json"
+
+
+@dataclass
+class CronJob:
+    id: str
+    cron: str        # "0 9 * * *"
+    prompt: str      # 触发时要注入的消息
+    recurring: bool  # True = 重复,False = 一次性
+    durable: bool    # True = 持久化到磁盘
+
+
+scheduled_jobs: dict[str, CronJob] = {}
+cron_queue: list[CronJob] = []
+cron_lock = threading.Lock()
+_last_fired: dict[str, str] = {}  # job_id → "YYYY-MM-DD HH:MM"
+
+
+def _cron_field_matches(field: str, value: int) -> bool:
+    """将单个 cron 字段与一个值进行匹配。"""
+    if field == "*":
+        return True
+    if field.startswith("*/"):
+        step = int(field[2:])
+        return step > 0 and value % step == 0
+    if "," in field:
+        return any(_cron_field_matches(f.strip(), value)
+                   for f in field.split(","))
+    if "-" in field:
+        lo, hi = field.split("-", 1)
+        return int(lo) <= value <= int(hi)
+    return value == int(field)
+
+
+def cron_matches(cron_expr: str, dt: datetime) -> bool:
+    """Check if a 5-field cron expression matches the given datetime.
+    Standard cron semantics: DOM and DOW use OR when both are constrained."""
+    fields = cron_expr.strip().split()
+    if len(fields) != 5:
+        return False
+    minute, hour, dom, month, dow = fields
+    dow_val = (dt.weekday() + 1) % 7  # Python Monday=0 → cron Sunday=0
+
+    m = _cron_field_matches(minute, dt.minute)
+    h = _cron_field_matches(hour, dt.hour)
+    dom_ok = _cron_field_matches(dom, dt.day)
+    month_ok = _cron_field_matches(month, dt.month)
+    dow_ok = _cron_field_matches(dow, dow_val)
+
+    # 分钟、小时、月份必须全部匹配
+    if not (m and h and month_ok):
+        return False
+    # DOM 和 DOW:如果两者都有限制,任一匹配即可(OR)
+    dom_unconstrained = dom == "*"
+    dow_unconstrained = dow == "*"
+    if dom_unconstrained and dow_unconstrained:
+        return True
+    if dom_unconstrained:
+        return dow_ok
+    if dow_unconstrained:
+        return dom_ok
+    return dom_ok or dow_ok
+
+
+def _validate_cron_field(field: str, lo: int, hi: int) -> str | None:
+    """校验单个 cron 字段值是否位于 [lo, hi] 范围内。"""
+    if field == "*":
+        return None
+    if field.startswith("*/"):
+        step_str = field[2:]
+        if not step_str.isdigit():
+            return f"无效步长:{field}"
+        step = int(step_str)
+        if step <= 0:
+            return f"步长必须 > 0:{field}"
+        return None
+    if "," in field:
+        for part in field.split(","):
+            err = _validate_cron_field(part.strip(), lo, hi)
+            if err: return err
+        return None
+    if "-" in field:
+        parts = field.split("-", 1)
+        if not parts[0].isdigit() or not parts[1].isdigit():
+            return f"无效范围:{field}"
+        a, b = int(parts[0]), int(parts[1])
+        if a < lo or a > hi or b < lo or b > hi:
+            return f"范围 {field} 超出边界 [{lo}-{hi}]"
+        if a > b:
+            return f"范围起点大于终点:{field}"
+        return None
+    if not field.isdigit():
+        return f"无效字段:{field}"
+    val = int(field)
+    if val < lo or val > hi:
+        return f"值 {val} 超出边界 [{lo}-{hi}]"
+    return None
+
+
+def validate_cron(cron_expr: str) -> str | None:
+    """校验 cron 表达式。返回错误消息或 None。"""
+    fields = cron_expr.strip().split()
+    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"]
+    for i, (field, (lo, hi), name) in enumerate(zip(fields, bounds, names)):
+        err = _validate_cron_field(field, lo, hi)
+        if err:
+            return f"{name}: {err}"
+    return None
+
+
+def save_durable_jobs():
+    """将持久任务保存到 .scheduled_tasks.json。"""
+    durable = [asdict(j) for j in scheduled_jobs.values() if j.durable]
+    DURABLE_PATH.write_text(json.dumps(durable, indent=2))
+
+
+def load_durable_jobs():
+    """启动时从磁盘加载持久任务。"""
+    if not DURABLE_PATH.exists():
+        return
+    try:
+        jobs = json.loads(DURABLE_PATH.read_text())
+        for j in jobs:
+            job = CronJob(**j)
+            err = validate_cron(job.cron)
+            if err:
+                print(f"  \033[31m[cron] skipping invalid job {job.id}: {err}\033[0m")
+                continue
+            scheduled_jobs[job.id] = job
+        valid = [j for j in jobs if j["id"] in scheduled_jobs]
+        if valid:
+            print(f"  \033[35m[cron] loaded {len(valid)} durable job(s)\033[0m")
+    except Exception:
+        pass
+
+
+def schedule_job(cron: str, prompt: str, recurring: bool = True,
+                 durable: bool = True) -> Cron任务 | str:
+    """注册一个新的 cron 任务。返回 CronJob 或错误字符串。"""
+    err = validate_cron(cron)
+    if err:
+        return err
+    job = CronJob(
+        id=f"cron_{random.randint(0, 999999):06d}",
+        cron=cron, prompt=prompt,
+        recurring=recurring, durable=durable,
+    )
+    with cron_lock:
+        scheduled_jobs[job.id] = job
+    if durable:
+        save_durable_jobs()
+    print(f"  \033[35m[cron register] {job.id} '{cron}' → {prompt[:40]}\033[0m")
+    return job
+
+
+def cancel_job(job_id: str) -> str:
+    """取消一个 cron 任务。"""
+    with cron_lock:
+        job = scheduled_jobs.pop(job_id, None)
+    if not job:
+        return f"任务 {job_id} 未找到"
+    if job.durable:
+        save_durable_jobs()
+    print(f"  \033[31m[cron cancel] {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()
+        # 带日期感知的标记,防止每日任务从第 2 天起被跳过
+        minute_marker = now.strftime("%Y-%m-%d %H:%M")
+        with cron_lock:
+            for job in list(scheduled_jobs.values()):
+                try:
+                    if cron_matches(job.cron, now):
+                        if _last_fired.get(job.id) != minute_marker:
+                            cron_queue.append(job)
+                            _last_fired[job.id] = minute_marker
+                            print(f"  \033[35m[cron fire] {job.id} → "
+                                  f"{job.prompt[:40]}\033[0m")
+                        if not job.recurring:
+                            scheduled_jobs.pop(job.id, None)
+                            if job.durable:
+                                save_durable_jobs()
+                except Exception as e:
+                    print(f"  \033[31m[cron error] {job.id}: {e}\033[0m")
+
+
+def consume_cron_queue() -> list[CronJob]:
+    """消费 cron_queue 中已触发的任务(由 agent_loop 调用)。"""
+    with cron_lock:
+        fired = list(cron_queue)
+        cron_queue.clear()
+    return fired
+
+
+# 启动时加载持久任务,然后启动调度线程
+load_durable_jobs()
+threading.Thread(target=cron_scheduler_loop, daemon=True).start()
+print("  \033[35m[cron] scheduler thread started\033[0m")
+
+
+# Cron 工具处理器
+
+def run_schedule_cron(cron: str, prompt: str,
+                      recurring: bool = True, durable: bool = True) -> str:
+    result = schedule_job(cron, prompt, recurring, durable)
+    if isinstance(result, str):
+        return f"错误:{result}"
+    return f"已调度 {result.id}: '{cron}' → {prompt}"
+
+
+def run_list_crons() -> str:
+    with cron_lock:
+        jobs = list(scheduled_jobs.values())
+    if not jobs:
+        return "暂无 cron 任务。请使用 schedule_cron 添加一个。"
+    lines = []
+    for j in jobs:
+        tag = "recurring" if j.recurring else "one-shot"
+        dur = "durable" if j.durable else "session"
+        lines.append(f"  {j.id}: '{j.cron}' → {j.prompt[:40]} "
+                     f"[{tag}, {dur}]")
+    return "\n".join(lines)
+
+
+def run_cancel_cron(job_id: str) -> str:
+    return cancel_job(job_id)
+
+
+# ── MessageBus (s15 新增) ──
+# 教学版本使用简单的文件追加 + 删除。
+# 真实 CC 使用 proper-lockfile 保证并发写入安全。
+
+MAILBOX_DIR = WORKDIR / ".mailboxes"
+MAILBOX_DIR.mkdir(exist_ok=True)
+
+
+class MessageBus:
+    """File-based message bus. Each agent has a .jsonl inbox.
+    Read is destructive: read_text + unlink (consumes messages).
+    Teaching version: no file locking; real CC uses proper-lockfile."""
+
+    def send(self, from_agent: str, to_agent: str, content: str,
+             msg_type: str = "message"):
+        msg = {"from": from_agent, "to": to_agent,
+               "content": content, "type": msg_type,
+               "ts": time.time()}
+        inbox = MAILBOX_DIR / f"{to_agent}.jsonl"
+        with open(inbox, "a") as f:
+            f.write(json.dumps(msg) + "\n")
+        print(f"  \033[33m[bus] {from_agent} → {to_agent}: "
+              f"{content[:50]}\033[0m")
+
+    def read_inbox(self, agent: str) -> list[dict]:
+        inbox = MAILBOX_DIR / f"{agent}.jsonl"
+        if not inbox.exists():
+            return []
+        msgs = [json.loads(line) for line in inbox.read_text().splitlines()
+                if line.strip()]
+        inbox.unlink()  # 消费:读取 + 删除
+        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."""
+        inbox = MAILBOX_DIR / f"{agent}.jsonl"
+        return inbox.exists() and inbox.stat().st_size > 0
+
+
+BUS = MessageBus()
+
+# 跟踪已启动的队友
+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."""
+    if name in active_teammates:
+        return f"队友 '{name}' 已存在"
+
+    system = (f"你是 '{name}',角色是 {role}。"
+              f"使用工具完成任务。"
+              f"通过 send_message 将结果发送给 'lead'。")
+
+    def run():
+        messages = [{"role": "user", "content": prompt}]
+        sub_tools = [
+            {"name": "bash", "description": "运行一条 shell 命令。",
+             "input_schema": {"type": "object",
+                              "properties": {"command": {"type": "string"}},
+                              "required": ["command"]}},
+            {"name": "read_file", "description": "读取文件内容。",
+             "input_schema": {"type": "object",
+                              "properties": {"path": {"type": "string"}},
+                              "required": ["path"]}},
+            {"name": "write_file", "description": "向文件写入内容。",
+             "input_schema": {"type": "object",
+                              "properties": {"path": {"type": "string"},
+                                             "content": {"type": "string"}},
+                              "required": ["path", "content"]}},
+            {"name": "send_message",
+             "description": "向另一个 Agent 发送消息。",
+             "input_schema": {"type": "object",
+                              "properties": {"to": {"type": "string"},
+                                             "content": {"type": "string"}},
+                              "required": ["to", "content"]}},
+        ]
+        sub_handlers = {
+            "bash": run_bash, "read_file": run_read, "write_file": run_write,
+            "send_message": lambda to, content: (BUS.send(name, to, content),
+                                                  "Sent")[1],
+        }
+
+        for _ in range(10):
+            inbox = BUS.read_inbox(name)
+            if inbox:
+                messages.append({"role": "user",
+                                 "content": f"<inbox>{json.dumps(inbox)}</inbox>"})
+            try:
+                response = client.messages.create(
+                    model=MODEL, system=system, messages=messages[-20:],
+                    tools=sub_tools, max_tokens=8000)
+            except Exception:
+                break
+            messages.append({"role": "assistant", "content": response.content})
+            if response.stop_reason != "tool_use":
+                break
+            results = []
+            for block in response.content:
+                if block.type == "tool_use":
+                    handler = sub_handlers.get(block.name)
+                    output = handler(**block.input) if handler else "未知"
+                    results.append({"type": "tool_result",
+                                    "tool_use_id": block.id,
+                                    "content": str(output)})
+            messages.append({"role": "user", "content": results})
+
+        # 向 Lead 发送最终摘要
+        summary = "已完成。"
+        for msg in reversed(messages):
+            if msg["role"] == "assistant" and isinstance(msg["content"], list):
+                for b in msg["content"]:
+                    if getattr(b, "type", None) == "text":
+                        summary = b.text
+                        break
+                else:
+                    continue
+                break
+        BUS.send(name, "lead", summary, "result")
+        active_teammates.pop(name, None)
+        print(f"  \033[32m[teammate] {name} finished\033[0m")
+
+    active_teammates[name] = True
+    threading.Thread(target=run, daemon=True).start()
+    print(f"  \033[36m[teammate] {name} spawned as {role}\033[0m")
+    return f"队友 '{name}' 已启动为 {role}"
+
+
+# ── 团队工具处理器 (s15 新增) ──
+
+def run_spawn_teammate(name: str, role: str, prompt: str) -> str:
+    return spawn_teammate_thread(name, role, prompt)
+
+
+def run_send_message(to: str, content: str) -> str:
+    BUS.send("lead", to, content)
+    return f"已发送给 {to}"
+
+
+def run_check_inbox() -> str:
+    msgs = BUS.read_inbox("lead")
+    if not msgs:
+        return "(收件箱为空)"
+    lines = []
+    for m in msgs:
+        lines.append(f"  [{m['from']}] {m['content'][:200]}")
+    return "\n".join(lines)
+
+
+# ── 工具定义 ──
+
+TOOLS = [
+    {"name": "bash", "description": "运行一条 shell 命令。",
+     "input_schema": {"type": "object",
+                      "properties": {
+                          "command": {"type": "string"},
+                          "run_in_background": {"type": "boolean"}},
+                      "required": ["command"]}},
+    {"name": "read_file", "description": "读取文件内容。",
+     "input_schema": {"type": "object",
+                      "properties": {"path": {"type": "string"},
+                                     "limit": {"type": "integer"}},
+                      "required": ["path"]}},
+    {"name": "write_file", "description": "向文件写入内容。",
+     "input_schema": {"type": "object",
+                      "properties": {"path": {"type": "string"},
+                                     "content": {"type": "string"}},
+                      "required": ["path", "content"]}},
+    {"name": "create_task",
+     "description": "创建一个新任务,可选 blockedBy 依赖。",
+     "input_schema": {"type": "object",
+                      "properties": {
+                          "subject": {"type": "string"},
+                          "description": {"type": "string"},
+                          "blockedBy": {"type": "array",
+                                        "items": {"type": "string"}}},
+                      "required": ["subject"]}},
+    {"name": "list_tasks",
+     "description": "列出所有任务及其状态、负责人和依赖。",
+     "input_schema": {"type": "object", "properties": {},
+                      "required": []}},
+    {"name": "get_task",
+     "description": "按 ID 获取指定任务的完整详情。",
+     "input_schema": {"type": "object",
+                      "properties": {"task_id": {"type": "string"}},
+                      "required": ["task_id"]}},
+    {"name": "claim_task",
+     "description": "认领一个待处理任务。设置 owner,并将状态改为 in_progress。",
+     "input_schema": {"type": "object",
+                      "properties": {"task_id": {"type": "string"}},
+                      "required": ["task_id"]}},
+    {"name": "complete_task",
+     "description": "完成一个进行中的任务。报告被解除阻塞的下游任务。",
+     "input_schema": {"type": "object",
+                      "properties": {"task_id": {"type": "string"}},
+                      "required": ["task_id"]}},
+    {"name": "schedule_cron",
+     "description": "调度一个 cron 任务。cron 为 5 字段:分 时 月内日 月 周内日。",
+     "input_schema": {"type": "object",
+                      "properties": {
+                          "cron": {"type": "string",
+                                   "description": "5 字段 cron 表达式"},
+                          "prompt": {"type": "string",
+                                     "description": "触发时要注入的消息"},
+                          "recurring": {"type": "boolean",
+                                        "description": "True=重复,False=一次性"},
+                          "durable": {"type": "boolean",
+                                      "description": "True=持久化到磁盘"}},
+                      "required": ["cron", "prompt"]}},
+    {"name": "list_crons",
+     "description": "列出所有已注册的 cron 任务。",
+     "input_schema": {"type": "object", "properties": {},
+                      "required": []}},
+    {"name": "cancel_cron",
+     "description": "按 ID 取消 cron 任务。",
+     "input_schema": {"type": "object",
+                      "properties": {"job_id": {"type": "string"}},
+                      "required": ["job_id"]}},
+    {"name": "spawn_teammate",
+     "description": "在后台线程中启动一个队友 Agent。",
+     "input_schema": {"type": "object",
+                      "properties": {
+                          "name": {"type": "string"},
+                          "role": {"type": "string"},
+                          "prompt": {"type": "string"}},
+                      "required": ["name", "role", "prompt"]}},
+    {"name": "send_message",
+     "description": "通过 MessageBus 向队友发送消息。",
+     "input_schema": {"type": "object",
+                      "properties": {"to": {"type": "string"},
+                                     "content": {"type": "string"}},
+                      "required": ["to", "content"]}},
+    {"name": "check_inbox",
+     "description": "检查 Lead 收件箱中的队友消息。",
+     "input_schema": {"type": "object", "properties": {},
+                      "required": []}},
+]
+
+
+# ── 上下文 ──
+
+def update_context(context: dict, messages: list) -> dict:
+    """Derive context from real state."""
+    memories = ""
+    if MEMORY_INDEX.exists():
+        content = MEMORY_INDEX.read_text().strip()
+        if content:
+            memories = content
+    return {
+        "enabled_tools": [t["name"] for t in TOOLS],
+        "workspace": str(WORKDIR),
+        "memories": memories,
+    }
+
+
+# ── Agent 循环 ──
+# 教学代码保留基础 Agent 循环。省略 S11 的完整错误恢复。
+# 调用 agent_loop 时消费 Cron 队列;真实 CC 会通过
+# 队列处理器 (useQueueProcessor.ts) 在条目到达时。
+
+def agent_loop(messages: list, context: dict):
+    system = get_system_prompt(context)
+    while True:
+        # 消费已触发的 cron 任务 → 作为消息注入
+        fired = consume_cron_queue()
+        for job in fired:
+            messages.append({"role": "user",
+                             "content": f"[Scheduled] {job.prompt}"})
+            print(f"  \033[35m[inject cron] {job.prompt[:50]}\033[0m")
+
+        try:
+            response = client.messages.create(
+                model=MODEL, system=system, messages=messages,
+                tools=TOOLS, max_tokens=8000)
+        except Exception as e:
+            messages.append({"role": "assistant", "content": [
+                {"type": "text",
+                 "text": f"[错误] {type(e).__name__}: {e}"}]})
+            return
+
+        messages.append({"role": "assistant", "content": response.content})
+        if response.stop_reason != "tool_use":
+            return
+
+        results = []
+        for block in response.content:
+            if block.type != "tool_use":
+                continue
+            print(f"\033[36m> {block.name}\033[0m")
+
+            if should_run_background(block.name, block.input):
+                bg_id = start_background_task(block)
+                results.append({"type": "tool_result",
+                                "tool_use_id": block.id,
+                                "content": f"[Background task {bg_id} started] "
+                                           f"完成后结果将可用。"})
+            else:
+                output = execute_tool(block)
+                print(str(output)[:300])
+                results.append({"type": "tool_result",
+                                "tool_use_id": block.id,
+                                "content": output})
+
+        # 将后台工具结果 + 通知合并成一条用户消息
+        user_content = list(results)
+        bg_notifications = collect_background_results()
+        if bg_notifications:
+            for notif in bg_notifications:
+                user_content.append({"type": "text", "text": notif})
+        messages.append({"role": "user", "content": user_content})
+        context = update_context(context, messages)
+        system = get_system_prompt(context)
+
+
+if __name__ == "__main__":
+    print("s15: Agent 团队")
+    print("输入问题后按回车发送。输入 q 退出。\n")
+    history = []
+    context = update_context({}, [])
+
+    # input() 和 1 秒轮询器(队友收件箱或后台结果)共同写入一个
+    # 事件队列(问题 #291、#46)。
+    events = queue.Queue()
+
+    def input_reader():
+        while True:
+            try:
+                line = input("\033[36ms15 >> \033[0m")
+            except (EOFError, KeyboardInterrupt):
+                events.put(("quit", None))
+                return
+            events.put(("user", line))
+
+    def inbox_poller():
+        # 每约 1 秒轮询一次;当异步结果就绪时唤醒 Lead:队友
+        # 收件箱消息或已完成的后台任务。不要依赖
+        # active_teammates:队友发送结果后会移除自身,
+        # 因此最终消息可能比注册表条目存在得更久。
+        while True:
+            time.sleep(1)
+            if BUS.peek("lead") or has_pending_background():
+                events.put(("唤醒", None))
+
+    threading.Thread(target=input_reader, daemon=True).start()
+    threading.Thread(target=inbox_poller, daemon=True).start()
+
+    had_teammates = False
+    while True:
+        kind, payload = events.get()
+        if kind == "quit":
+            break
+        if kind == "user":
+            if payload.strip().lower() in ("q", "exit", ""):
+                break
+            history.append({"role": "user", "content": payload})
+        else:  # "唤醒": 队友收件箱或后台结果已就绪
+            parts = []
+            inbox = BUS.read_inbox("lead")
+            if inbox:
+                parts.append("[Inbox]\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")
+
+        # 为唤醒来源执行一轮。
+        agent_loop(history, context)
+        context = update_context(context, history)
+        for block in history[-1]["content"]:
+            if getattr(block, "type", None) == "text":
+                print(block.text)
+            elif isinstance(block, dict) and block.get("type") == "text":
+                print(block.get("text", ""))
+
+        # 当所有队友完成且输出已消费后,只公告一次。
+        if active_teammates:
+            had_teammates = True
+        elif had_teammates and not BUS.peek("lead") and not has_pending_background():
+            print("\033[32m[所有队友已完成]\033[0m")
+            had_teammates = False
+        print()

+ 882 - 0
s16_team_protocols/code.py

@@ -0,0 +1,882 @@
+#!/usr/bin/env python3
+"""
+s16: 团队协议 — 请求-响应协议 + request_id + 分发 + 状态机。
+
+运行:  python s16_team_protocols/code.py
+需要: pip install anthropic python-dotenv + .env 中配置 ANTHROPIC_API_KEY
+
+相对 s15 的变化:
+  - ProtocolState dataclass(request_id、type、sender、status、created_at)
+  - pending_requests 字典:跟踪进行中的协议请求
+  - dispatch_message:按类型把收到的消息路由给处理器
+  - request_shutdown:Lead 发送关闭协议请求
+  - request_plan:Lead 要求队友提交计划
+  - handle_shutdown_request / handle_plan_response:队友接收并响应
+  - match_response:Lead 通过 request_id 关联响应(并校验类型)
+  - 队友空闲循环:等待收件箱消息,而不是 10 轮后退出
+  - 统一 consume_lead_inbox:协议路由 + 注入历史
+  - 3 个新 Lead 工具:request_shutdown、request_plan、review_plan
+  - 1 个新队友工具:submit_plan
+
+ASCII 流程:
+  Lead: BUS.send("shutdown_request", {request_id}) ──────→ 队友收件箱
+  Teammate: 分发 → 处理器 → BUS.send("shutdown_response", {request_id}) ─→ Lead 收件箱
+  Lead: consume_lead_inbox → match_response(request_id) → pending_requests[req_id].status = approved
+"""
+
+import os, subprocess, json, time, random, threading
+from pathlib import Path
+from datetime import datetime
+from dataclasses import dataclass, asdict, field
+
+try:
+    import readline
+    readline.parse_and_bind('set bind-tty-special-chars off')
+except ImportError:
+    pass
+
+from anthropic import Anthropic
+from dotenv import load_dotenv
+
+load_dotenv(override=True)
+if os.getenv("ANTHROPIC_BASE_URL"):
+    os.environ.pop("ANTHROPIC_AUTH_TOKEN", None)
+
+WORKDIR = Path.cwd()
+MEMORY_DIR = WORKDIR / ".memory"
+MEMORY_INDEX = MEMORY_DIR / "MEMORY.md"
+client = Anthropic(base_url=os.getenv("ANTHROPIC_BASE_URL"))
+MODEL = os.environ["MODEL_ID"]
+
+# ── 任务系统 (来自 s12,已同步) ──
+
+TASKS_DIR = WORKDIR / ".tasks"
+TASKS_DIR.mkdir(exist_ok=True)
+
+
+@dataclass
+class Task:
+    id: str
+    subject: str
+    description: str
+    status: str          # pending | in_progress | completed
+    owner: str | None
+    blockedBy: list[str]
+
+
+def _task_path(task_id: str) -> Path:
+    return TASKS_DIR / f"{task_id}.json"
+
+
+def create_task(subject: str, description: str = "",
+                blockedBy: list[str] | None = None) -> Task:
+    task = Task(
+        id=f"task_{int(time.time())}_{random.randint(0, 9999):04d}",
+        subject=subject, description=description,
+        status="pending", owner=None,
+        blockedBy=blockedBy or [],
+    )
+    save_task(task)
+    return task
+
+
+def save_task(task: Task):
+    _task_path(task.id).write_text(json.dumps(asdict(task), indent=2))
+
+
+def load_task(task_id: str) -> Task:
+    return Task(**json.loads(_task_path(task_id).read_text()))
+
+
+def list_tasks() -> list[Task]:
+    return [Task(**json.loads(p.read_text()))
+            for p in sorted(TASKS_DIR.glob("task_*.json"))]
+
+
+def get_task(task_id: str) -> str:
+    """以 JSON 返回完整任务详情。"""
+    task = load_task(task_id)
+    return json.dumps(asdict(task), indent=2)
+
+
+def can_start(task_id: str) -> bool:
+    """Check if all blockedBy dependencies are completed.
+    Missing dependencies are treated as blocked."""
+    task = load_task(task_id)
+    for dep_id in task.blockedBy:
+        if not _task_path(dep_id).exists():
+            return False
+        if load_task(dep_id).status != "completed":
+            return False
+    return True
+
+
+def claim_task(task_id: str, owner: str = "agent") -> str:
+    task = load_task(task_id)
+    if task.status != "pending":
+        return f"任务 {task_id} 当前状态为 {task.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}"
+    task.owner = owner
+    task.status = "in_progress"
+    save_task(task)
+    print(f"  \033[36m[claim] {task.subject} → in_progress (owner: {owner})\033[0m")
+    return f"已认领 {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},无法完成"
+    task.status = "completed"
+    save_task(task)
+    unblocked = [t.subject for t in list_tasks()
+                 if t.status == "pending" and t.blockedBy and can_start(t.id)]
+    print(f"  \033[32m[complete] {task.subject} ✓\033[0m")
+    msg = f"已完成 {task.id} ({task.subject})"
+    if unblocked:
+        msg += f"\n已解除阻塞:{', '.join(unblocked)}"
+        print(f"  \033[33m[unblocked] {', '.join(unblocked)}\033[0m")
+    return msg
+
+
+# ── 提示词组装 (来自 s10,已同步) ──
+
+PROMPT_SECTIONS = {
+    "identity": "你是一个编码 Agent。直接行动,不要只解释。",
+    "tools": "可用工具:bash, read_file, write_file, "
+             "get_task, create_task, list_tasks, claim_task, complete_task, "
+             "spawn_teammate, send_message, check_inbox, "
+             "request_shutdown, request_plan, review_plan.",
+    "workspace": f"工作目录:{WORKDIR}",
+    "memory": "有可用的相关记忆时,会在下方注入。",
+}
+
+
+def assemble_system_prompt(context: dict) -> str:
+    sections = [PROMPT_SECTIONS["identity"],
+                PROMPT_SECTIONS["tools"],
+                PROMPT_SECTIONS["workspace"]]
+    memories = context.get("memories", "")
+    if memories:
+        sections.append(f"Relevant memories:\n{memories}")
+    return "\n\n".join(sections)
+
+
+_last_context_key, _last_prompt = None, None
+
+
+def get_system_prompt(context: dict) -> str:
+    global _last_context_key, _last_prompt
+    key = json.dumps(context, sort_keys=True, ensure_ascii=False, default=str)
+    if key == _last_context_key and _last_prompt:
+        return _last_prompt
+    _last_context_key = key
+    _last_prompt = assemble_system_prompt(context)
+    return _last_prompt
+
+
+# ── 工具 ──
+
+def safe_path(p: str) -> Path:
+    path = (WORKDIR / p).resolve()
+    if not path.is_relative_to(WORKDIR):
+        raise ValueError(f"路径逃逸出工作区:{p}")
+    return path
+
+
+def run_bash(command: str, run_in_background: bool = False) -> str:
+    # run_in_background 由 agent_loop 分发处理,不在这里处理
+    try:
+        r = subprocess.run(command, shell=True, cwd=WORKDIR,
+                           capture_output=True, text=True, timeout=120)
+        out = (r.stdout + r.stderr).strip()
+        return out[:50000] if out else "(无输出)"
+    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} 行更多内容)"]
+        return "\n".join(lines)
+    except Exception as e:
+        return f"错误:{e}"
+
+
+def run_write(path: str, content: str) -> str:
+    try:
+        fp = safe_path(path)
+        fp.parent.mkdir(parents=True, exist_ok=True)
+        fp.write_text(content)
+        return f"已写入 {len(content)} 字节到 {path}"
+    except Exception as e:
+        return f"错误:{e}"
+
+
+# 任务工具
+
+def run_create_task(subject: str, description: str = "",
+                    blockedBy: list[str] | None = None) -> str:
+    task = create_task(subject, description, blockedBy)
+    deps = f" (blockedBy: {', '.join(blockedBy)})" if blockedBy else ""
+    print(f"  \033[34m[create] {task.subject}{deps}\033[0m")
+    return f"已创建 {task.id}: {task.subject}{deps}"
+
+
+def run_list_tasks() -> str:
+    个任务 = list_tasks()
+    if not 个任务:
+        return "暂无任务。请使用 create_task 添加任务。"
+    lines = []
+    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 ""
+        lines.append(f"  {icon} {t.id}: {t.subject} "
+                     f"[{t.status}]{owner}{deps}")
+    return "\n".join(lines)
+
+
+def run_get_task(task_id: str) -> str:
+    try:
+        return get_task(task_id)
+    except FileNotFoundError:
+        return f"错误:任务 {task_id} 未找到"
+
+
+def run_claim_task(task_id: str) -> str:
+    return claim_task(task_id, owner="agent")
+
+
+def run_complete_task(task_id: str) -> str:
+    return complete_task(task_id)
+
+
+# ── 后台任务 (来自 s13,已同步) ──
+
+_bg_计数器 = 0
+background_tasks: dict[str, dict] = {}
+background_results: dict[str, str] = {}
+background_lock = threading.Lock()
+
+
+def is_slow_operation(tool_name: str, tool_input: dict) -> bool:
+    """兜底启发式:判断命令是否可能超过 30 秒。"""
+    if tool_name != "bash":
+        return False
+    cmd = tool_input.get("command", "").lower()
+    slow_keywords = ["install", "build", "test", "deploy", "compile",
+                     "docker build", "pip install", "npm install",
+                     "cargo build", "pytest", "make"]
+    return any(kw in cmd for kw in slow_keywords)
+
+
+def should_run_background(tool_name: str, tool_input: dict) -> bool:
+    """模型的显式请求优先;否则使用启发式兜底。"""
+    if tool_input.get("run_in_background"):
+        return True
+    return is_slow_operation(tool_name, tool_input)
+
+
+def start_background_task(block) -> str:
+    """在守护线程中运行工具,并返回后台任务 ID。"""
+    global _bg_计数器
+    _bg_计数器 += 1
+    bg_id = f"bg_{_bg_计数器:04d}"
+    cmd = block.input.get("command", block.name)
+
+    def worker():
+        result = execute_tool(block)
+        with background_lock:
+            background_tasks[bg_id]["status"] = "completed"
+            background_results[bg_id] = result
+
+    with background_lock:
+        background_tasks[bg_id] = {
+            "tool_use_id": block.id,
+            "command": cmd,
+            "status": "running",
+        }
+    threading.Thread(target=worker, daemon=True).start()
+    print(f"  \033[33m[background] dispatched {bg_id}: {cmd[:40]}\033[0m")
+    return bg_id
+
+
+def collect_background_results() -> list[str]:
+    """将已完成的后台结果收集为 task_notification 消息。"""
+    with background_lock:
+        ready_ids = [bid for bid, task in background_tasks.items()
+                     if task["status"] == "completed"]
+    notifications = []
+    for bg_id in ready_ids:
+        with background_lock:
+            task = background_tasks.pop(bg_id)
+            output = background_results.pop(bg_id, "")
+        summary = output[:200] if len(output) > 200 else output
+        notifications.append(
+            f"<task_notification>\n"
+            f"  <task_id>{bg_id}</task_id>\n"
+            f"  <status>completed</status>\n"
+            f"  <command>{task['command']}</command>\n"
+            f"  <summary>{summary}</summary>\n"
+            f"</task_notification>")
+        print(f"  \033[32m[background done] {bg_id}: "
+              f"{task['command'][:40]} ({len(output)} 个字符)\033[0m")
+    return notifications
+
+
+# ── MessageBus (来自 s15) ──
+
+MAILBOX_DIR = WORKDIR / ".mailboxes"
+MAILBOX_DIR.mkdir(exist_ok=True)
+
+
+class MessageBus:
+    """File-based message bus. Each agent has a .jsonl inbox.
+    Read is destructive: read_text + unlink (consumes messages).
+    Teaching version: no file locking; real CC uses proper-lockfile."""
+
+    def send(self, from_agent: str, to_agent: str, content: str,
+             msg_type: str = "message", metadata: dict = None):
+        msg = {"from": from_agent, "to": to_agent,
+               "content": content, "type": msg_type,
+               "ts": time.time(), "metadata": metadata or {}}
+        inbox = MAILBOX_DIR / f"{to_agent}.jsonl"
+        with open(inbox, "a") as f:
+            f.write(json.dumps(msg) + "\n")
+        print(f"  \033[33m[bus] {from_agent} → {to_agent}: "
+              f"({msg_type}) {content[:50]}\033[0m")
+
+    def read_inbox(self, agent: str) -> list[dict]:
+        inbox = MAILBOX_DIR / f"{agent}.jsonl"
+        if not inbox.exists():
+            return []
+        msgs = [json.loads(line) for line in inbox.read_text().splitlines()
+                if line.strip()]
+        inbox.unlink()  # 消费:读取 + 删除
+        return msgs
+
+
+BUS = MessageBus()
+active_teammates: dict[str, bool] = {}
+
+# ── 协议状态 (s16 新增) ──
+
+@dataclass
+class ProtocolState:
+    request_id: str
+    type: str       # "shutdown" | "plan_approval"
+    sender: str
+    target: str
+    status: str     # pending | approved | rejected
+    payload: str    # 计划文本或关闭原因
+    created_at: float = field(default_factory=time.time)
+
+
+pending_requests: dict[str, ProtocolState] = {}
+
+
+def new_request_id() -> str:
+    return f"req_{random.randint(0, 999999):06d}"
+
+
+def match_response(response_type: str, request_id: str, approve: bool):
+    """Correlate a response to the original request via request_id.
+    Validates that response_type matches the request type."""
+    state = pending_requests.get(request_id)
+    if not state:
+        print(f"  \033[31m[protocol] unknown request_id: {request_id}\033[0m")
+        return
+    # 校验响应类型是否匹配请求类型
+    if state.type == "shutdown" and response_type != "shutdown_response":
+        print(f"  \033[31m[protocol] type mismatch: expected shutdown_response, "
+              f"got {response_type}\033[0m")
+        return
+    if state.type == "plan_approval" and response_type != "plan_approval_response":
+        print(f"  \033[31m[protocol] type mismatch: expected plan_approval_response, "
+              f"got {response_type}\033[0m")
+        return
+    if state.status != "pending":
+        print(f"  \033[33m[protocol] {request_id} already {state.status}, "
+              f"ignoring duplicate\033[0m")
+        return
+    state.status = "approved" if approve else "rejected"
+    icon = "✓" if approve else "✗"
+    color = "32" if approve else "31"
+    print(f"  \033[{color}m[protocol] {state.type} {icon} "
+          f"({request_id}: {state.status})\033[0m")
+
+
+# ── 统一 Lead 收件箱消费者 (s16 修复) ──
+# check_inbox 工具和主循环都会调用这个函数。
+# 返回前通过 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."""
+    msgs = BUS.read_inbox("lead")
+    if not msgs:
+        return []
+    if route_protocol:
+        for msg in msgs:
+            meta = msg.get("metadata", {})
+            req_id = meta.get("request_id", "")
+            msg_type = msg.get("type", "")
+            if req_id and msg_type.endswith("_response"):
+                approve = meta.get("approve", False)
+                match_response(msg_type, req_id, approve)
+    return msgs
+
+
+# ── 队友线程 (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."""
+    if name in active_teammates:
+        return f"队友 '{name}' 已存在"
+
+    system = (f"你是 '{name}',角色是 {role}。"
+              f"使用工具完成任务。"
+              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."""
+        msg_type = msg.get("type", "message")
+        meta = msg.get("metadata", {})
+        req_id = meta.get("request_id", "")
+
+        if msg_type == "shutdown_request":
+            BUS.send(name, "lead", "正在平滑关闭。",
+                     "shutdown_response",
+                     {"request_id": req_id, "approve": True})
+            print(f"  \033[35m[protocol] {name} approved shutdown "
+                  f"({req_id})\033[0m")
+            return True  # 停止循环
+
+        if msg_type == "plan_approval_response":
+            approve = meta.get("approve", False)
+            if approve:
+                messages.append({"role": "user",
+                    "content": f"[计划已批准] 继续执行任务。"})
+            else:
+                messages.append({"role": "user",
+                    "content": f"[计划已拒绝] 反馈:{msg['content']}"})
+
+        return False  # 继续
+
+    def run():
+        messages = [{"role": "user", "content": prompt}]
+        sub_tools = [
+            {"name": "bash", "description": "运行一条 shell 命令。",
+             "input_schema": {"type": "object",
+                              "properties": {"command": {"type": "string"}},
+                              "required": ["command"]}},
+            {"name": "read_file", "description": "读取文件。",
+             "input_schema": {"type": "object",
+                              "properties": {"path": {"type": "string"}},
+                              "required": ["path"]}},
+            {"name": "write_file", "description": "写入文件。",
+             "input_schema": {"type": "object",
+                              "properties": {"path": {"type": "string"},
+                                             "content": {"type": "string"}},
+                              "required": ["path", "content"]}},
+            {"name": "send_message",
+             "description": "Send message to another agent.",
+             "input_schema": {"type": "object",
+                              "properties": {"to": {"type": "string"},
+                                             "content": {"type": "string"}},
+                              "required": ["to", "content"]}},
+            {"name": "submit_plan",
+             "description": "提交计划给 Lead 审批。",
+             "input_schema": {"type": "object",
+                              "properties": {"plan": {"type": "string"}},
+                              "required": ["plan"]}},
+        ]
+        sub_handlers = {
+            "bash": run_bash, "read_file": run_read, "write_file": run_write,
+            "send_message": lambda to, content: (BUS.send(name, to, content),
+                                                  "Sent")[1],
+            "submit_plan": lambda plan: _teammate_submit_plan(name, plan),
+        }
+
+        shutdown_requested = False
+        while not shutdown_requested:
+            # 检查收件箱中的协议消息
+            inbox = BUS.read_inbox(name)
+            should_stop = False
+            non_protocol = []
+            for msg in inbox:
+                if msg.get("type") in ("shutdown_request", "plan_approval_response"):
+                    should_stop = handle_inbox_message(name, msg, messages)
+                    if should_stop:
+                        break
+                else:
+                    non_protocol.append(msg)
+            if should_stop:
+                shutdown_requested = True
+                break
+            if non_protocol:
+                inbox_json = json.dumps(non_protocol)
+                messages.append({"role": "user",
+                    "content": "<inbox>" + inbox_json + "</inbox>"})
+
+            # LLM 轮次
+            try:
+                response = client.messages.create(
+                    model=MODEL, system=system, messages=messages[-20:],
+                    tools=sub_tools, max_tokens=8000)
+            except Exception:
+                break
+
+            messages.append({"role": "assistant", "content": response.content})
+            if response.stop_reason != "tool_use":
+                # 空闲:等待收件箱消息,而不是退出
+                # 真实 CC 会在这里向 Lead 发送 idle_notification
+                while not shutdown_requested:
+                    time.sleep(1)
+                    inbox = BUS.read_inbox(name)
+                    if not inbox:
+                        continue
+                    for msg in inbox:
+                        if msg.get("type") in ("shutdown_request", "plan_approval_response"):
+                            should_stop = handle_inbox_message(name, msg, messages)
+                            if should_stop:
+                                shutdown_requested = True
+                                break
+                        else:
+                            non_protocol.append(msg)
+                    if shutdown_requested:
+                        break
+                    if non_protocol:
+                        inbox_json = json.dumps(non_protocol)
+                        messages.append({"role": "user",
+                            "content": "<inbox>" + inbox_json + "</inbox>"})
+                        break  # 带着新消息回到 LLM 轮次
+
+            # 执行工具调用
+            results = []
+            for block in response.content:
+                if block.type == "tool_use":
+                    handler = sub_handlers.get(block.name)
+                    output = handler(**block.input) if handler else "未知"
+                    results.append({"type": "tool_result",
+                                    "tool_use_id": block.id,
+                                    "content": str(output)})
+            messages.append({"role": "user", "content": results})
+
+        # 向 Lead 发送最终摘要
+        summary = "已完成。"
+        for msg in reversed(messages):
+            if msg["role"] == "assistant" and isinstance(msg["content"], list):
+                for b in msg["content"]:
+                    if getattr(b, "type", None) == "text":
+                        summary = b.text
+                        break
+                else:
+                    continue
+                break
+        BUS.send(name, "lead", summary, "result")
+        active_teammates.pop(name, None)
+        print(f"  \033[32m[teammate] {name} finished\033[0m")
+
+    active_teammates[name] = True
+    threading.Thread(target=run, daemon=True).start()
+    print(f"  \033[36m[teammate] {name} spawned as {role}\033[0m")
+    return f"队友 '{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.
+    """
+    req_id = new_request_id()
+    pending_requests[req_id] = ProtocolState(
+        request_id=req_id, type="plan_approval",
+        sender=from_name, target="lead",
+        status="pending", payload=plan)
+    BUS.send(from_name, "lead", plan,
+             "plan_approval_request",
+             {"request_id": req_id})
+    return f"计划已提交({req_id})。正在等待审批..."
+
+
+# ── Lead Protocol 工具 (s16 新增) ──
+
+def run_request_shutdown(teammate: str) -> str:
+    req_id = new_request_id()
+    pending_requests[req_id] = ProtocolState(
+        request_id=req_id, type="shutdown",
+        sender="lead", target=teammate,
+        status="pending", payload="")
+    BUS.send("lead", teammate, "请平滑关闭。",
+             "shutdown_request",
+             {"request_id": req_id})
+    print(f"  \033[35m[protocol] 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."""
+    BUS.send("lead", teammate, f"请为以下任务提交计划:{task}",
+             "message")
+    return f"已要求 {teammate} 提交计划"
+
+
+def run_review_plan(request_id: str, approve: bool, feedback: str = "") -> str:
+    state = pending_requests.get(request_id)
+    if not state:
+        return f"请求 {request_id} 未找到"
+    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"),
+             "plan_approval_response",
+             {"request_id": request_id, "approve": approve})
+    icon = "✓" if approve else "✗"
+    print(f"  \033[32m[protocol] plan {icon} ({request_id})\033[0m")
+    return f"计划已{'批准' if approve else '拒绝'}({request_id})"
+
+
+# ── 其他 Lead 工具处理器 ──
+
+def run_spawn_teammate(name: str, role: str, prompt: str) -> str:
+    return spawn_teammate_thread(name, role, prompt)
+
+
+def run_send_message(to: str, content: str) -> str:
+    BUS.send("lead", to, content)
+    return f"已发送给 {to}"
+
+
+def run_check_inbox() -> str:
+    """检查 Lead 收件箱,并通过 match_response 路由协议响应。"""
+    msgs = consume_lead_inbox(route_protocol=True)
+    if not msgs:
+        return "(收件箱为空)"
+    lines = []
+    for m in msgs:
+        meta = m.get("metadata", {})
+        req_id = meta.get("request_id", "")
+        tag = f" [{m['type']} req:{req_id}]" if req_id else f" [{m['type']}]"
+        lines.append(f"  [{m['from']}]{tag} {m['content'][:200]}")
+    return "\n".join(lines)
+
+
+# ── 工具分发 ──
+
+def execute_tool(block) -> str:
+    """执行工具调用块并返回输出。"""
+    handler = {
+        "bash": run_bash, "read_file": run_read, "write_file": run_write,
+        "create_task": run_create_task, "list_tasks": run_list_tasks,
+        "get_task": run_get_task, "claim_task": run_claim_task,
+        "complete_task": run_complete_task,
+        "spawn_teammate": run_spawn_teammate,
+        "send_message": run_send_message, "check_inbox": run_check_inbox,
+        "request_shutdown": run_request_shutdown,
+        "request_plan": run_request_plan, "review_plan": run_review_plan,
+    }.get(block.name)
+    if handler:
+        return handler(**block.input)
+    return f"未知工具:{block.name}"
+
+
+# ── 工具定义 ──
+
+TOOLS = [
+    {"name": "bash", "description": "运行一条 shell 命令。",
+     "input_schema": {"type": "object",
+                      "properties": {
+                          "command": {"type": "string"},
+                          "run_in_background": {"type": "boolean"}},
+                      "required": ["command"]}},
+    {"name": "read_file", "description": "读取文件内容。",
+     "input_schema": {"type": "object",
+                      "properties": {"path": {"type": "string"},
+                                     "limit": {"type": "integer"}},
+                      "required": ["path"]}},
+    {"name": "write_file", "description": "向文件写入内容。",
+     "input_schema": {"type": "object",
+                      "properties": {"path": {"type": "string"},
+                                     "content": {"type": "string"}},
+                      "required": ["path", "content"]}},
+    {"name": "create_task",
+     "description": "创建一个新任务,可选 blockedBy 依赖。",
+     "input_schema": {"type": "object",
+                      "properties": {
+                          "subject": {"type": "string"},
+                          "description": {"type": "string"},
+                          "blockedBy": {"type": "array",
+                                        "items": {"type": "string"}}},
+                      "required": ["subject"]}},
+    {"name": "list_tasks",
+     "description": "列出所有任务及其状态、负责人和依赖。",
+     "input_schema": {"type": "object", "properties": {},
+                      "required": []}},
+    {"name": "get_task",
+     "description": "按 ID 获取指定任务的完整详情。",
+     "input_schema": {"type": "object",
+                      "properties": {"task_id": {"type": "string"}},
+                      "required": ["task_id"]}},
+    {"name": "claim_task",
+     "description": "认领一个待处理任务。设置 owner,并将状态改为 in_progress。",
+     "input_schema": {"type": "object",
+                      "properties": {"task_id": {"type": "string"}},
+                      "required": ["task_id"]}},
+    {"name": "complete_task",
+     "description": "完成一个进行中的任务。报告被解除阻塞的下游任务。",
+     "input_schema": {"type": "object",
+                      "properties": {"task_id": {"type": "string"}},
+                      "required": ["task_id"]}},
+    {"name": "spawn_teammate",
+     "description": "在后台线程中启动一个队友 Agent。",
+     "input_schema": {"type": "object",
+                      "properties": {
+                          "name": {"type": "string"},
+                          "role": {"type": "string"},
+                          "prompt": {"type": "string"}},
+                      "required": ["name", "role", "prompt"]}},
+    {"name": "send_message",
+     "description": "Send message to a teammate via MessageBus.",
+     "input_schema": {"type": "object",
+                      "properties": {"to": {"type": "string"},
+                                     "content": {"type": "string"}},
+                      "required": ["to", "content"]}},
+    {"name": "check_inbox",
+     "description": "检查 Lead 收件箱,并自动路由协议响应。",
+     "input_schema": {"type": "object", "properties": {},
+                      "required": []}},
+    {"name": "request_shutdown",
+     "description": "请求队友平滑关闭。",
+     "input_schema": {"type": "object",
+                      "properties": {"teammate": {"type": "string"}},
+                      "required": ["teammate"]}},
+    {"name": "request_plan",
+     "description": "要求队友提交计划以供审查。",
+     "input_schema": {"type": "object",
+                      "properties": {"teammate": {"type": "string"},
+                                     "task": {"type": "string"}},
+                      "required": ["teammate", "task"]}},
+    {"name": "review_plan",
+     "description": "按 request_id 批准或拒绝已提交的计划。",
+     "input_schema": {"type": "object",
+                      "properties": {
+                          "request_id": {"type": "string"},
+                          "approve": {"type": "boolean"},
+                          "feedback": {"type": "string"}},
+                      "required": ["request_id", "approve"]}},
+]
+
+
+# ── 上下文 ──
+
+def update_context(context: dict, messages: list) -> dict:
+    """Derive context from real state."""
+    memories = ""
+    if MEMORY_INDEX.exists():
+        content = MEMORY_INDEX.read_text().strip()
+        if content:
+            memories = content
+    return {
+        "enabled_tools": [t["name"] for t in TOOLS],
+        "workspace": str(WORKDIR),
+        "memories": memories,
+    }
+
+
+# ── Agent 循环 ──
+
+def agent_loop(messages: list, context: dict):
+    system = get_system_prompt(context)
+    while True:
+        try:
+            response = client.messages.create(
+                model=MODEL, system=system, messages=messages,
+                tools=TOOLS, max_tokens=8000)
+        except Exception as e:
+            messages.append({"role": "assistant", "content": [
+                {"type": "text",
+                 "text": f"[错误] {type(e).__name__}: {e}"}]})
+            return
+
+        messages.append({"role": "assistant", "content": response.content})
+        if response.stop_reason != "tool_use":
+            return
+
+        results = []
+        for block in response.content:
+            if block.type != "tool_use":
+                continue
+            print(f"\033[36m> {block.name}\033[0m")
+
+            if should_run_background(block.name, block.input):
+                bg_id = start_background_task(block)
+                results.append({"type": "tool_result",
+                                "tool_use_id": block.id,
+                                "content": f"[Background task {bg_id} started] "
+                                           f"完成后结果将可用。"})
+            else:
+                output = execute_tool(block)
+                print(str(output)[:300])
+                results.append({"type": "tool_result",
+                                "tool_use_id": block.id,
+                                "content": output})
+
+        # 将后台工具结果 + 通知合并成一条用户消息
+        user_content = list(results)
+        bg_notifications = collect_background_results()
+        if bg_notifications:
+            for notif in bg_notifications:
+                user_content.append({"type": "text", "text": notif})
+        messages.append({"role": "user", "content": user_content})
+        context = update_context(context, messages)
+        system = get_system_prompt(context)
+
+
+if __name__ == "__main__":
+    print("s16: 团队协议")
+    print("输入问题后按回车发送。输入 q 退出。\n")
+    history = []
+    context = update_context({}, [])
+    while True:
+        try:
+            query = input("\033[36ms16 >> \033[0m")
+        except (EOFError, KeyboardInterrupt):
+            break
+        if query.strip().lower() in ("q", "exit", ""):
+            break
+        history.append({"role": "user", "content": query})
+        agent_loop(history, context)
+        context = update_context(context, history)
+        for block in history[-1]["content"]:
+            if getattr(block, "type", None) == "text":
+                print(block.text)
+            elif isinstance(block, dict) and block.get("type") == "text":
+                print(block.get("text", ""))
+
+        # 检查收件箱 → 路由协议 + 注入历史
+        inbox_msgs = consume_lead_inbox(route_protocol=True)
+        if inbox_msgs:
+            inbox_text = "\n".join(
+                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()

+ 813 - 0
s17_autonomous_agents/code.py

@@ -0,0 +1,813 @@
+#!/usr/bin/env python3
+"""
+s17: 自主 Agent — 空闲轮询 + 自动认领 + WORK/IDLE 生命周期。
+
+运行:  python s17_autonomous_agents/code.py
+需要: pip install anthropic python-dotenv + .env 中配置 ANTHROPIC_API_KEY
+
+相对 s16 的变化:
+  - scan_unclaimed_tasks:查找 pending、未分配且依赖已完成的任务
+  - idle_poll:60 秒轮询循环(收件箱 + 任务板),在 IDLE 中分发 shutdown
+  - claim_task:owner 检查 + 返回值校验
+  - 队友生命周期:WORK → IDLE → SHUTDOWN
+  - 队友工具:+ list_tasks、claim_task、complete_task(5→8)
+  - consume_lead_inbox:统一收件箱消费器,用于协议 + 上下文注入
+  - 上下文压缩后重新注入身份提示
+
+ASCII 生命周期:
+  WORK: inbox → LLM → tools → (tool_use? 循环) → (完成? → IDLE)
+  IDLE: 5s 轮询 → 有 inbox? → WORK / 有未认领任务? → 认领 → WORK / 60s? → SHUTDOWN
+"""
+
+import os, subprocess, json, time, random, threading
+from pathlib import Path
+from datetime import datetime
+from dataclasses import dataclass, asdict, field
+
+try:
+    import readline
+    readline.parse_and_bind('set bind-tty-special-chars off')
+except ImportError:
+    pass
+
+from anthropic import Anthropic
+from dotenv import load_dotenv
+
+load_dotenv(override=True)
+if os.getenv("ANTHROPIC_BASE_URL"):
+    os.environ.pop("ANTHROPIC_AUTH_TOKEN", None)
+
+WORKDIR = Path.cwd()
+client = Anthropic(base_url=os.getenv("ANTHROPIC_BASE_URL"))
+MODEL = os.environ["MODEL_ID"]
+
+# ── 任务系统 (来自 s12) ──
+
+TASKS_DIR = WORKDIR / ".tasks"
+TASKS_DIR.mkdir(exist_ok=True)
+
+
+@dataclass
+class Task:
+    id: str
+    subject: str
+    description: str
+    status: str
+    owner: str | None
+    blockedBy: list[str]
+
+
+def _task_path(task_id: str) -> Path:
+    return TASKS_DIR / f"{task_id}.json"
+
+
+def create_task(subject: str, description: str = "",
+                blockedBy: list[str] | None = None) -> Task:
+    task = Task(
+        id=f"task_{int(time.time())}_{random.randint(0, 9999):04d}",
+        subject=subject, description=description,
+        status="pending", owner=None,
+        blockedBy=blockedBy or [],
+    )
+    save_task(task)
+    return task
+
+
+def save_task(task: Task):
+    _task_path(task.id).write_text(json.dumps(asdict(task), indent=2))
+
+
+def load_task(task_id: str) -> Task:
+    return Task(**json.loads(_task_path(task_id).read_text()))
+
+
+def list_tasks() -> list[Task]:
+    return [Task(**json.loads(p.read_text()))
+            for p in sorted(TASKS_DIR.glob("task_*.json"))]
+
+
+def get_task(task_id: str) -> str:
+    task = load_task(task_id)
+    return json.dumps(asdict(task), indent=2)
+
+
+def can_start(task_id: str) -> bool:
+    task = load_task(task_id)
+    for dep_id in task.blockedBy:
+        if not _task_path(dep_id).exists():
+            return False
+        if load_task(dep_id).status != "completed":
+            return False
+    return True
+
+
+def claim_task(task_id: str, owner: str = "agent") -> str:
+    task = load_task(task_id)
+    if task.status != "pending":
+        return f"任务 {task_id} 当前状态为 {task.status},无法认领"
+    if task.owner:
+        return f"任务 {task_id} 已由 {task.owner} 负责"
+    if not can_start(task_id):
+        deps = [d for d in task.blockedBy
+                if _task_path(d).exists() and load_task(d).status != "completed"]
+        missing = [d for d in task.blockedBy if not _task_path(d).exists()]
+        parts = []
+        if deps: parts.append(f"被阻塞于:{deps}")
+        if missing: parts.append(f"缺失依赖:{missing}")
+        return "无法开始 — " + ", ".join(parts)
+    task.owner = owner
+    task.status = "in_progress"
+    save_task(task)
+    print(f"  \033[36m[claim] {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},无法完成"
+    task.status = "completed"
+    save_task(task)
+    unblocked = [t.subject for t in list_tasks()
+                 if t.status == "pending" and t.blockedBy and can_start(t.id)]
+    print(f"  \033[32m[complete] {task.subject} ✓\033[0m")
+    msg = f"已完成 {task.id} ({task.subject})"
+    if unblocked:
+        msg += f"\n已解除阻塞:{', '.join(unblocked)}"
+    return msg
+
+
+# ── 提示词组装 (来自 s10) ──
+
+PROMPT_SECTIONS = {
+    "identity": "你是一个编码 Agent。直接行动,不要只解释。",
+    "tools": "可用工具:bash, read_file, write_file, "
+             "create_task, list_tasks, get_task, claim_task, complete_task, "
+             "spawn_teammate, send_message, check_inbox, "
+             "request_shutdown, request_plan, review_plan.",
+    "workspace": f"工作目录:{WORKDIR}",
+    "memory": "有可用的相关记忆时,会在下方注入。",
+}
+
+
+def assemble_system_prompt(context: dict) -> str:
+    sections = [PROMPT_SECTIONS["identity"],
+                PROMPT_SECTIONS["tools"],
+                PROMPT_SECTIONS["workspace"]]
+    if context.get("memories"):
+        sections.append(f"Relevant memories:\n{context['memories']}")
+    return "\n\n".join(sections)
+
+
+_last_context_hash, _last_prompt = None, None
+
+
+def get_system_prompt(context: dict) -> str:
+    global _last_context_hash, _last_prompt
+    h = json.dumps(context, sort_keys=True)
+    if h == _last_context_hash and _last_prompt:
+        return _last_prompt
+    _last_context_hash, _last_prompt = h, assemble_system_prompt(context)
+    return _last_prompt
+
+
+# ── 工具 (来自 s15) ──
+
+def safe_path(p: str) -> Path:
+    path = (WORKDIR / p).resolve()
+    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)
+        out = (r.stdout + r.stderr).strip()
+        return out[:50000] if out else "(无输出)"
+    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} 行更多内容)"]
+        return "\n".join(lines)
+    except Exception as e:
+        return f"错误:{e}"
+
+
+def run_write(path: str, content: str) -> str:
+    try:
+        fp = safe_path(path)
+        fp.parent.mkdir(parents=True, exist_ok=True)
+        fp.write_text(content)
+        return f"已写入 {len(content)} 字节到 {path}"
+    except Exception as e:
+        return f"错误:{e}"
+
+
+# ── MessageBus (来自 s15) ──
+
+MAILBOX_DIR = WORKDIR / ".mailboxes"
+MAILBOX_DIR.mkdir(exist_ok=True)
+
+
+class MessageBus:
+    def send(self, from_agent: str, to_agent: str, content: str,
+             msg_type: str = "message", metadata: dict = None):
+        msg = {"from": from_agent, "to": to_agent,
+               "content": content, "type": msg_type,
+               "ts": time.time(), "metadata": metadata or {}}
+        inbox = MAILBOX_DIR / f"{to_agent}.jsonl"
+        with open(inbox, "a") as f:
+            f.write(json.dumps(msg) + "\n")
+        print(f"  \033[33m[bus] {from_agent} → {to_agent}: "
+              f"({msg_type}) {content[:50]}\033[0m")
+
+    def read_inbox(self, agent: str) -> list[dict]:
+        inbox = MAILBOX_DIR / f"{agent}.jsonl"
+        if not inbox.exists():
+            return []
+        msgs = [json.loads(line) for line in inbox.read_text().splitlines()
+                if line.strip()]
+        inbox.unlink()
+        return msgs
+
+
+BUS = MessageBus()
+active_teammates: dict[str, bool] = {}
+
+
+# ── 协议状态 (来自 s16) ──
+
+@dataclass
+class ProtocolState:
+    request_id: str
+    type: str
+    sender: str
+    target: str
+    status: str
+    payload: str
+    created_at: float = field(default_factory=time.time)
+
+
+pending_requests: dict[str, ProtocolState] = {}
+
+
+def new_request_id() -> str:
+    return f"req_{random.randint(0, 999999):06d}"
+
+
+def match_response(response_type: str, request_id: str, approve: bool):
+    """通过 request_id 将响应关联到原始请求。"""
+    state = pending_requests.get(request_id)
+    if not state:
+        print(f"  \033[31m[protocol] unknown 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")
+        return
+    if state.type == "plan_approval" and response_type != "plan_approval_response":
+        print(f"  \033[31m[protocol] type mismatch: expected plan_approval_response, "
+              f"got {response_type}\033[0m")
+        return
+    state.status = "approved" if approve else "rejected"
+    icon = "✓" if approve else "✗"
+    color = "32" if approve else "31"
+    print(f"  \033[{color}m[protocol] {state.type} {icon} "
+          f"({request_id}: {state.status})\033[0m")
+
+
+# ── 自主 Agent (s17 新增) ──
+
+IDLE_POLL_INTERVAL = 5   # 秒
+IDLE_TIMEOUT = 60         # 秒
+
+
+def scan_unclaimed_tasks() -> list[dict]:
+    """查找依赖均已完成、未分配的 pending 任务。"""
+    unclaimed = []
+    for f in sorted(TASKS_DIR.glob("task_*.json")):
+        task = json.loads(f.read_text())
+        if (task.get("status") == "pending"
+                and not task.get("owner")
+                and can_start(task["id"])):
+            unclaimed.append(task)
+    return unclaimed
+
+
+def idle_poll(name: str, messages: list, role: str) -> str:
+    """轮询 60 秒。返回 'work'、'shutdown' 或 'timeout'。"""
+    for _ in range(IDLE_TIMEOUT // IDLE_POLL_INTERVAL):
+        time.sleep(IDLE_POLL_INTERVAL)
+
+        # 检查收件箱 — 优先分发协议消息
+        inbox = BUS.read_inbox(name)
+        if inbox:
+            # 检查 shutdown_request
+            for msg in inbox:
+                if msg.get("type") == "shutdown_request":
+                    req_id = msg.get("metadata", {}).get("request_id", "")
+                    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")
+                    return "shutdown"
+
+            # 非协议收件箱:注入并恢复工作
+            messages.append({"role": "user",
+                "content": "<inbox>" + json.dumps(inbox) + "</inbox>"})
+            print(f"  \033[36m[idle] {name} found inbox messages\033[0m")
+            return "work"
+
+        # 扫描任务板
+        unclaimed = scan_unclaimed_tasks()
+        if unclaimed:
+            task = unclaimed[0]
+            result = claim_task(task["id"], name)
+            if "Claimed" 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: "
+                      f"{task['subject']}\033[0m")
+                return "work"
+            print(f"  \033[33m[idle] {name} claim failed: "
+                  f"{result}\033[0m")
+
+    print(f"  \033[31m[idle] {name} timeout ({IDLE_TIMEOUT}s)\033[0m")
+    return "timeout"
+
+
+# ── 队友线程 (from s15 + s16 + s17) ──
+
+def spawn_teammate_thread(name: str, role: str, prompt: str) -> str:
+    if name in active_teammates:
+        return f"队友 '{name}' 已存在"
+
+    system = (f"你是 '{name}',角色是 {role}。"
+              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", "")
+
+        if msg_type == "shutdown_request":
+            BUS.send(name, "lead", "正在平滑关闭。",
+                     "shutdown_response",
+                     {"request_id": req_id, "approve": True})
+            print(f"  \033[35m[protocol] {name} approved shutdown "
+                  f"({req_id})\033[0m")
+            return True
+
+        if msg_type == "plan_approval_response":
+            approve = meta.get("approve", False)
+            if approve:
+                messages.append({"role": "user",
+                    "content": "[计划已批准] 继续执行任务。"})
+            else:
+                messages.append({"role": "user",
+                    "content": f"[计划已拒绝] 反馈:{msg['content']}"})
+        return False
+
+    def run():
+        messages = [{"role": "user", "content": prompt}]
+        sub_tools = [
+            {"name": "bash", "description": "运行一条 shell 命令。",
+             "input_schema": {"type": "object",
+                              "properties": {"command": {"type": "string"}},
+                              "required": ["command"]}},
+            {"name": "read_file", "description": "读取文件。",
+             "input_schema": {"type": "object",
+                              "properties": {"path": {"type": "string"}},
+                              "required": ["path"]}},
+            {"name": "write_file", "description": "写入文件。",
+             "input_schema": {"type": "object",
+                              "properties": {"path": {"type": "string"},
+                                             "content": {"type": "string"}},
+                              "required": ["path", "content"]}},
+            {"name": "send_message",
+             "description": "Send message to another agent.",
+             "input_schema": {"type": "object",
+                              "properties": {"to": {"type": "string"},
+                                             "content": {"type": "string"}},
+                              "required": ["to", "content"]}},
+            {"name": "submit_plan",
+             "description": "提交计划给 Lead 审批。",
+             "input_schema": {"type": "object",
+                              "properties": {"plan": {"type": "string"}},
+                              "required": ["plan"]}},
+            # s17 新增:队友可以列出、认领并完成任务
+            {"name": "list_tasks",
+             "description": "列出任务板上的所有任务。",
+             "input_schema": {"type": "object", "properties": {},
+                              "required": []}},
+            {"name": "claim_task",
+             "description": "认领一个待处理任务。",
+             "input_schema": {"type": "object",
+                              "properties": {"task_id": {"type": "string"}},
+                              "required": ["task_id"]}},
+            {"name": "complete_task",
+             "description": "将进行中的任务标记为已完成。",
+             "input_schema": {"type": "object",
+                              "properties": {"task_id": {"type": "string"}},
+                              "required": ["task_id"]}},
+        ]
+
+        def _run_list_tasks():
+            个任务 = list_tasks()
+            if not 个任务:
+                return "No 个任务."
+            return "\n".join(
+                f"  {t.id}: {t.subject} [{t.status}]"
+                for t in 个任务)
+
+        def _run_claim_task(task_id: str):
+            return claim_task(task_id, owner=name)
+
+        def _run_complete_task(task_id: str):
+            return complete_task(task_id)
+
+        sub_handlers = {
+            "bash": run_bash, "read_file": run_read, "write_file": run_write,
+            "send_message": lambda to, content: (BUS.send(name, to, content),
+                                                  "Sent")[1],
+            "submit_plan": lambda plan: _teammate_submit_plan(name, plan),
+            "list_tasks": _run_list_tasks,
+            "claim_task": _run_claim_task,
+            "complete_task": _run_complete_task,
+        }
+
+        # 外层循环:WORK → IDLE 周期
+        while True:
+            # 身份重新注入 (s17)
+            if len(messages) <= 3:
+                messages.insert(0, {"role": "user",
+                    "content": f"<identity>你是 '{name}',角色:{role}。"
+                               f"继续你的工作。</identity>"})
+
+            # WORK 阶段
+            should_shutdown = False
+            for _ in range(10):
+                inbox = BUS.read_inbox(name)
+                for msg in inbox:
+                    stopped = handle_inbox_message(name, msg, messages)
+                    if stopped:
+                        should_shutdown = True
+                        break
+                if should_shutdown:
+                    break
+                if inbox and not should_shutdown:
+                    non_protocol = [m for m in inbox
+                                    if m.get("type") == "message"]
+                    if non_protocol:
+                        messages.append({"role": "user",
+                            "content": f"<inbox>{json.dumps(non_protocol)}</inbox>"})
+
+                try:
+                    response = client.messages.create(
+                        model=MODEL, system=system, messages=messages[-20:],
+                        tools=sub_tools, max_tokens=8000)
+                except Exception:
+                    break
+                messages.append({"role": "assistant", "content": response.content})
+                if response.stop_reason != "tool_use":
+                    break
+                results = []
+                for block in response.content:
+                    if block.type == "tool_use":
+                        handler = sub_handlers.get(block.name)
+                        output = handler(**block.input) if handler else "未知"
+                        results.append({"type": "tool_result",
+                                        "tool_use_id": block.id,
+                                        "content": str(output)})
+                messages.append({"role": "user", "content": results})
+
+            if should_shutdown:
+                break
+
+            # IDLE 阶段 (s17 新增)
+            idle_result = idle_poll(name, messages, role)
+            if idle_result == "shutdown":
+                break
+            if idle_result == "timeout":
+                break
+
+        # 摘要
+        summary = "已完成。"
+        for msg in reversed(messages):
+            if msg["role"] == "assistant" and isinstance(msg["content"], list):
+                for b in msg["content"]:
+                    if getattr(b, "type", None) == "text":
+                        summary = b.text
+                        break
+                else:
+                    continue
+                break
+        BUS.send(name, "lead", summary, "result")
+        active_teammates.pop(name, None)
+        print(f"  \033[32m[teammate] {name} finished\033[0m")
+
+    active_teammates[name] = True
+    threading.Thread(target=run, daemon=True).start()
+    print(f"  \033[36m[teammate] {name} spawned as {role}\033[0m")
+    return f"队友 '{name}' 已启动为 {role}(自主模式)"
+
+
+def _teammate_submit_plan(from_name: str, plan: str) -> str:
+    """队友向 Lead 提交计划以供审批。"""
+    req_id = new_request_id()
+    pending_requests[req_id] = ProtocolState(
+        request_id=req_id, type="plan_approval",
+        sender=from_name, target="lead",
+        status="pending", payload=plan)
+    BUS.send(from_name, "lead", plan,
+             "plan_approval_request",
+             {"request_id": req_id})
+    return f"计划已提交({req_id})。正在等待审批..."
+
+
+# ── Lead Protocol 工具 (来自 s16) ──
+
+def run_request_shutdown(teammate: str) -> str:
+    req_id = new_request_id()
+    pending_requests[req_id] = ProtocolState(
+        request_id=req_id, type="shutdown",
+        sender="lead", target=teammate,
+        status="pending", payload="")
+    BUS.send("lead", teammate, "请平滑关闭。",
+             "shutdown_request",
+             {"request_id": req_id})
+    print(f"  \033[35m[protocol] shutdown_request → {teammate} "
+          f"({req_id})\033[0m")
+    return f"已向 {teammate} 发送关闭请求(req: {req_id})"
+
+
+def run_request_plan(teammate: str, task: str) -> str:
+    """Lead 要求队友提交计划。"""
+    BUS.send("lead", teammate, f"请为以下任务提交计划:{task}",
+             "message")
+    return f"已要求 {teammate} 提交计划"
+
+
+def run_review_plan(request_id: str, approve: bool,
+                    feedback: str = "") -> str:
+    state = pending_requests.get(request_id)
+    if not state:
+        return f"请求 {request_id} 未找到"
+    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"),
+             "plan_approval_response",
+             {"request_id": request_id, "approve": approve})
+    icon = "✓" if approve else "✗"
+    print(f"  \033[32m[protocol] plan {icon} ({request_id})\033[0m")
+    return f"计划已{'批准' if approve else '拒绝'}({request_id})"
+
+
+# ── 基础工具处理器 ──
+
+def run_create_task(subject: str, description: str = "",
+                    blockedBy: list[str] | None = None) -> str:
+    task = create_task(subject, description, blockedBy)
+    deps = f" (blockedBy: {', '.join(blockedBy)})" if blockedBy else ""
+    print(f"  \033[34m[create] {task.subject}{deps}\033[0m")
+    return f"已创建 {task.id}: {task.subject}{deps}"
+
+
+def run_list_tasks() -> str:
+    个任务 = list_tasks()
+    if not 个任务:
+        return "No 个任务."
+    return "\n".join(
+        f"  {t.id}: {t.subject} [{t.status}]"
+        for t in 个任务)
+
+
+def run_get_task(task_id: str) -> str:
+    return get_task(task_id)
+
+
+def run_claim_task(task_id: str) -> str:
+    return claim_task(task_id, owner="agent")
+
+
+def run_complete_task(task_id: str) -> str:
+    return complete_task(task_id)
+
+
+def run_spawn_teammate(name: str, role: str, prompt: str) -> str:
+    return spawn_teammate_thread(name, role, prompt)
+
+
+def run_send_message(to: str, content: str) -> str:
+    BUS.send("lead", to, content)
+    return f"已发送给 {to}"
+
+
+def consume_lead_inbox(route_protocol=True) -> list[dict]:
+    """读取 Lead 收件箱:路由协议响应,并返回所有消息。"""
+    msgs = BUS.read_inbox("lead")
+    if route_protocol:
+        for msg in msgs:
+            meta = msg.get("metadata", {})
+            req_id = meta.get("request_id", "")
+            msg_type = msg.get("type", "")
+            if req_id and msg_type.endswith("_response"):
+                match_response(msg_type, req_id, meta.get("approve", False))
+    return msgs
+
+
+def run_check_inbox() -> str:
+    msgs = consume_lead_inbox(route_protocol=True)
+    if not msgs:
+        return "(收件箱为空)"
+    lines = []
+    for m in msgs:
+        meta = m.get("metadata", {})
+        req_id = meta.get("request_id", "")
+        tag = f" [{m['type']} req:{req_id}]" if req_id else f" [{m['type']}]"
+        lines.append(f"  [{m['from']}]{tag} {m['content'][:200]}")
+    return "\n".join(lines)
+
+
+# ── 工具定义 ──
+
+TOOLS = [
+    {"name": "bash", "description": "运行一条 shell 命令。",
+     "input_schema": {"type": "object",
+                      "properties": {"command": {"type": "string"}},
+                      "required": ["command"]}},
+    {"name": "read_file", "description": "读取文件内容。",
+     "input_schema": {"type": "object",
+                      "properties": {"path": {"type": "string"},
+                                     "limit": {"type": "integer"}},
+                      "required": ["path"]}},
+    {"name": "write_file", "description": "向文件写入内容。",
+     "input_schema": {"type": "object",
+                      "properties": {"path": {"type": "string"},
+                                     "content": {"type": "string"}},
+                      "required": ["path", "content"]}},
+    {"name": "create_task",
+     "description": "创建一个任务。",
+     "input_schema": {"type": "object",
+                      "properties": {"subject": {"type": "string"},
+                                     "description": {"type": "string"},
+                                     "blockedBy": {"type": "array",
+                                                   "items": {"type": "string"}}},
+                      "required": ["subject"]}},
+    {"name": "list_tasks",
+     "description": "列出所有任务。",
+     "input_schema": {"type": "object", "properties": {}, "required": []}},
+    {"name": "get_task",
+     "description": "获取指定任务的完整详情。",
+     "input_schema": {"type": "object",
+                      "properties": {"task_id": {"type": "string"}},
+                      "required": ["task_id"]}},
+    {"name": "claim_task",
+     "description": "认领一个待处理任务。",
+     "input_schema": {"type": "object",
+                      "properties": {"task_id": {"type": "string"}},
+                      "required": ["task_id"]}},
+    {"name": "complete_task",
+     "description": "完成一个进行中的任务。",
+     "input_schema": {"type": "object",
+                      "properties": {"task_id": {"type": "string"}},
+                      "required": ["task_id"]}},
+    {"name": "spawn_teammate",
+     "description": "启动一个自主队友 Agent。",
+     "input_schema": {"type": "object",
+                      "properties": {"name": {"type": "string"},
+                                     "role": {"type": "string"},
+                                     "prompt": {"type": "string"}},
+                      "required": ["name", "role", "prompt"]}},
+    {"name": "send_message",
+     "description": "向队友发送消息。",
+     "input_schema": {"type": "object",
+                      "properties": {"to": {"type": "string"},
+                                     "content": {"type": "string"}},
+                      "required": ["to", "content"]}},
+    {"name": "check_inbox",
+     "description": "检查收件箱中的消息和协议响应。",
+     "input_schema": {"type": "object", "properties": {}, "required": []}},
+    {"name": "request_shutdown",
+     "description": "请求队友平滑关闭。",
+     "input_schema": {"type": "object",
+                      "properties": {"teammate": {"type": "string"}},
+                      "required": ["teammate"]}},
+    {"name": "request_plan",
+     "description": "要求队友提交计划以供审查。",
+     "input_schema": {"type": "object",
+                      "properties": {"teammate": {"type": "string"},
+                                     "task": {"type": "string"}},
+                      "required": ["teammate", "task"]}},
+    {"name": "review_plan",
+     "description": "批准或拒绝已提交的计划。",
+     "input_schema": {"type": "object",
+                      "properties": {
+                          "request_id": {"type": "string"},
+                          "approve": {"type": "boolean"},
+                          "feedback": {"type": "string"}},
+                      "required": ["request_id", "approve"]}},
+]
+
+TOOL_HANDLERS = {
+    "bash": run_bash, "read_file": run_read, "write_file": run_write,
+    "create_task": run_create_task, "list_tasks": run_list_tasks,
+    "get_task": run_get_task,
+    "claim_task": run_claim_task, "complete_task": run_complete_task,
+    "spawn_teammate": run_spawn_teammate,
+    "send_message": run_send_message, "check_inbox": run_check_inbox,
+    "request_shutdown": run_request_shutdown,
+    "request_plan": run_request_plan, "review_plan": run_review_plan,
+}
+
+
+# ── 上下文 ──
+
+MEMORY_DIR = WORKDIR / ".memory"
+MEMORY_INDEX = MEMORY_DIR / "MEMORY.md"
+
+
+def update_context(context: dict, messages: list) -> dict:
+    memories = ""
+    if MEMORY_INDEX.exists():
+        memories = MEMORY_INDEX.read_text()[:2000]
+    return {"memories": memories}
+
+
+# ── Agent 循环 ──
+
+def agent_loop(messages: list, context: dict):
+    system = get_system_prompt(context)
+    while True:
+        try:
+            response = client.messages.create(
+                model=MODEL, system=system, messages=messages,
+                tools=TOOLS, max_tokens=8000)
+        except Exception as e:
+            messages.append({"role": "assistant", "content": [
+                {"type": "text", "text": f"[错误] {type(e).__name__}: {e}"}]})
+            return
+
+        messages.append({"role": "assistant", "content": response.content})
+        if response.stop_reason != "tool_use":
+            return
+
+        results = []
+        for block in response.content:
+            if block.type != "tool_use":
+                continue
+            print(f"\033[36m> {block.name}\033[0m")
+            handler = TOOL_HANDLERS.get(block.name)
+            output = handler(**block.input) if handler else "未知"
+            print(str(output)[:300])
+            results.append({"type": "tool_result",
+                            "tool_use_id": block.id, "content": output})
+        messages.append({"role": "user", "content": results})
+        context = update_context(context, messages)
+        system = get_system_prompt(context)
+
+
+if __name__ == "__main__":
+    print("s17: 自主 Agent")
+    print("输入问题后按回车发送。输入 q 退出。\n")
+    history = []
+    context = {"memories": ""}
+    while True:
+        try:
+            query = input("\033[36ms17 >> \033[0m")
+        except (EOFError, KeyboardInterrupt):
+            break
+        if query.strip().lower() in ("q", "exit", ""):
+            break
+        history.append({"role": "user", "content": query})
+        agent_loop(history, context)
+        context = update_context(context, history)
+        for block in history[-1]["content"]:
+            if getattr(block, "type", None) == "text":
+                print(block.text)
+            elif isinstance(block, dict) and block.get("type") == "text":
+                print(block.get("text", ""))
+
+        # 消费 Lead 收件箱:路由协议 + 注入历史
+        inbox = consume_lead_inbox(route_protocol=True)
+        if inbox:
+            inbox_text = "\n".join(
+                f"来自 {m['from']} [{m.get('type', 'message')}]: "
+                f"{m['content'][:200]}" for m in inbox)
+            history.append({"role": "user",
+                            "content": f"[收件箱]\n{inbox_text}"})
+        print()

+ 149 - 0
skills/agent-builder/references/minimal-agent.py

@@ -0,0 +1,149 @@
+#!/usr/bin/env python3
+"""
+Minimal Agent Template - Copy and customize this.
+
+This is the simplest possible working agent (~80 lines).
+It has everything you need: 3 tools + loop.
+
+Usage:
+    1. Set ANTHROPIC_API_KEY environment variable
+    2. python minimal-agent.py
+    3. Type commands, 'q' to quit
+"""
+
+from anthropic import Anthropic
+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}.
+
+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",
+        "input_schema": {
+            "type": "object",
+            "properties": {"command": {"type": "string"}},
+            "required": ["command"]
+        }
+    },
+    {
+        "name": "read_file",
+        "description": "Read file contents",
+        "input_schema": {
+            "type": "object",
+            "properties": {"path": {"type": "string"}},
+            "required": ["path"]
+        }
+    },
+    {
+        "name": "write_file",
+        "description": "Write content to file",
+        "input_schema": {
+            "type": "object",
+            "properties": {
+                "path": {"type": "string"},
+                "content": {"type": "string"}
+            },
+            "required": ["path", "content"]
+        }
+    },
+]
+
+
+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)"
+        except subprocess.TimeoutExpired:
+            return "Error: Timeout"
+
+    if name == "read_file":
+        try:
+            return (WORKDIR / args["path"]).read_text()[:50000]
+        except Exception as e:
+            return f"Error: {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']}"
+        except Exception as e:
+            return f"Error: {e}"
+
+    return f"Unknown tool: {name}"
+
+
+def agent(prompt: str, history: list = None) -> str:
+    """Run the agent loop."""
+    if history is None:
+        history = []
+
+    history.append({"role": "user", "content": prompt})
+
+    while True:
+        response = client.messages.create(
+            model=MODEL,
+            system=SYSTEM,
+            messages=history,
+            tools=TOOLS,
+            max_tokens=8000,
+        )
+
+        # Build assistant message
+        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":
+                print(f"> {block.name}: {block.input}")
+                output = execute_tool(block.name, block.input)
+                print(f"  {output[:100]}...")
+                results.append({
+                    "type": "tool_result",
+                    "tool_use_id": block.id,
+                    "content": output
+                })
+
+        history.append({"role": "user", "content": results})
+
+
+if __name__ == "__main__":
+    print(f"Minimal Agent - {WORKDIR}")
+    print("Type 'q' to quit.\n")
+
+    history = []
+    while True:
+        try:
+            query = input(">> ").strip()
+        except (EOFError, KeyboardInterrupt):
+            break
+        if query in ("q", "quit", "exit", ""):
+            break
+        print(agent(query, history))
+        print()

+ 243 - 0
skills/agent-builder/references/subagent-pattern.py

@@ -0,0 +1,243 @@
+"""
+Subagent Pattern - How to implement Task tool for context isolation.
+
+The key insight: spawn child agents with ISOLATED context to prevent
+"context pollution" where exploration details fill up the main conversation.
+"""
+
+import time
+import sys
+
+# Assuming client, MODEL, execute_tool are defined elsewhere
+
+
+# =============================================================================
+# AGENT TYPE REGISTRY
+# =============================================================================
+
+AGENT_TYPES = {
+    # Explore: Read-only, for searching and analyzing
+    "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.",
+    },
+
+    # Code: Full-powered, for implementation
+    "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.",
+    },
+
+    # Plan: Read-only, for design work
+    "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.",
+    },
+
+    # Add your own types here...
+    # "test": {
+    #     "description": "Testing agent for running and analyzing tests",
+    #     "tools": ["bash", "read_file"],
+    #     "prompt": "Run tests and report results. Don't modify code.",
+    # },
+}
+
+
+def get_agent_descriptions() -> str:
+    """Generate descriptions for Task tool schema."""
+    return "\n".join(
+        f"- {name}: {cfg['description']}"
+        for name, cfg in AGENT_TYPES.items()
+    )
+
+
+def get_tools_for_agent(agent_type: str, base_tools: list) -> list:
+    """
+    Filter tools based on agent type.
+
+    '*' means all base tools.
+    Otherwise, whitelist specific tool names.
+
+    Note: Subagents don't get Task tool to prevent infinite recursion.
+    """
+    allowed = AGENT_TYPES.get(agent_type, {}).get("tools", "*")
+
+    if allowed == "*":
+        return base_tools  # All base tools, but NOT Task
+
+    return [t for t in base_tools if t["name"] in allowed]
+
+
+# =============================================================================
+# TASK TOOL DEFINITION
+# =============================================================================
+
+TASK_TOOL = {
+    "name": "Task",
+    "description": f"""Spawn a subagent for a focused subtask.
+
+Subagents run in ISOLATED context - they don't see parent's history.
+Use this to keep the main conversation clean.
+
+Agent types:
+{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"
+""",
+    "input_schema": {
+        "type": "object",
+        "properties": {
+            "description": {
+                "type": "string",
+                "description": "Short task name (3-5 words) for progress display"
+            },
+            "prompt": {
+                "type": "string",
+                "description": "Detailed instructions for the subagent"
+            },
+            "agent_type": {
+                "type": "string",
+                "enum": list(AGENT_TYPES.keys()),
+                "description": "Type of agent to spawn"
+            },
+        },
+        "required": ["description", "prompt", "agent_type"],
+    },
+}
+
+
+# =============================================================================
+# SUBAGENT EXECUTION
+# =============================================================================
+
+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
+    """
+    if agent_type not in AGENT_TYPES:
+        return f"Error: Unknown agent type '{agent_type}'"
+
+    config = AGENT_TYPES[agent_type]
+
+    # Agent-specific system prompt
+    sub_system = f"""You are a {agent_type} subagent at {workdir}.
+
+{config["prompt"]}
+
+Complete the task and return a clear, concise summary."""
+
+    # Filtered tools for this agent type
+    sub_tools = get_tools_for_agent(agent_type, base_tools)
+
+    # KEY: ISOLATED message history!
+    # The subagent starts fresh, doesn't see parent's conversation
+    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)
+    while True:
+        response = client.messages.create(
+            model=model,
+            system=sub_system,
+            messages=sub_messages,
+            tools=sub_tools,
+            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 = []
+
+        for tc in tool_calls:
+            tool_count += 1
+            output = execute_tool(tc.name, tc.input)
+            results.append({
+                "type": "tool_result",
+                "tool_use_id": tc.id,
+                "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"
+            )
+            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"
+    )
+
+    # 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)"
+
+
+# =============================================================================
+# USAGE EXAMPLE
+# =============================================================================
+
+"""
+# In your main agent's execute_tool function:
+
+def execute_tool(name: str, args: dict) -> str:
+    if name == "Task":
+        return run_task(
+            description=args["description"],
+            prompt=args["prompt"],
+            agent_type=args["agent_type"],
+            client=client,
+            model=MODEL,
+            workdir=WORKDIR,
+            base_tools=BASE_TOOLS,
+            execute_tool=execute_tool  # Pass self for recursion
+        )
+    # ... other tools ...
+
+
+# In your TOOLS list:
+TOOLS = BASE_TOOLS + [TASK_TOOL]
+"""

+ 271 - 0
skills/agent-builder/references/tool-templates.py

@@ -0,0 +1,271 @@
+"""
+Tool Templates - Copy and customize these for your agent.
+
+Each tool needs:
+1. Definition (JSON schema for the model)
+2. Implementation (Python function)
+"""
+
+from pathlib import Path
+import subprocess
+
+WORKDIR = Path.cwd()
+
+
+# =============================================================================
+# TOOL DEFINITIONS (for TOOLS list)
+# =============================================================================
+
+BASH_TOOL = {
+    "name": "bash",
+    "description": "Run a shell command. Use for: ls, find, grep, git, npm, python, etc.",
+    "input_schema": {
+        "type": "object",
+        "properties": {
+            "command": {
+                "type": "string",
+                "description": "The shell command to execute"
+            }
+        },
+        "required": ["command"],
+    },
+}
+
+READ_FILE_TOOL = {
+    "name": "read_file",
+    "description": "Read file contents. Returns UTF-8 text.",
+    "input_schema": {
+        "type": "object",
+        "properties": {
+            "path": {
+                "type": "string",
+                "description": "Relative path to the file"
+            },
+            "limit": {
+                "type": "integer",
+                "description": "Max lines to read (default: all)"
+            },
+        },
+        "required": ["path"],
+    },
+}
+
+WRITE_FILE_TOOL = {
+    "name": "write_file",
+    "description": "Write content to a file. Creates parent directories if needed.",
+    "input_schema": {
+        "type": "object",
+        "properties": {
+            "path": {
+                "type": "string",
+                "description": "Relative path for the file"
+            },
+            "content": {
+                "type": "string",
+                "description": "Content to write"
+            },
+        },
+        "required": ["path", "content"],
+    },
+}
+
+EDIT_FILE_TOOL = {
+    "name": "edit_file",
+    "description": "Replace exact text in a file. Use for surgical edits.",
+    "input_schema": {
+        "type": "object",
+        "properties": {
+            "path": {
+                "type": "string",
+                "description": "Relative path to the file"
+            },
+            "old_text": {
+                "type": "string",
+                "description": "Exact text to find (must match precisely)"
+            },
+            "new_text": {
+                "type": "string",
+                "description": "Replacement text"
+            },
+        },
+        "required": ["path", "old_text", "new_text"],
+    },
+}
+
+TODO_WRITE_TOOL = {
+    "name": "TodoWrite",
+    "description": "Update the task list. Use to plan and track progress.",
+    "input_schema": {
+        "type": "object",
+        "properties": {
+            "items": {
+                "type": "array",
+                "description": "Complete list of tasks",
+                "items": {
+                    "type": "object",
+                    "properties": {
+                        "content": {"type": "string", "description": "Task description"},
+                        "status": {"type": "string", "enum": ["pending", "in_progress", "completed"]},
+                        "activeForm": {"type": "string", "description": "Present tense, e.g. 'Reading files'"},
+                    },
+                    "required": ["content", "status", "activeForm"],
+                },
+            }
+        },
+        "required": ["items"],
+    },
+}
+
+TASK_TOOL_TEMPLATE = """
+# Generate dynamically with agent types
+TASK_TOOL = {
+    "name": "Task",
+    "description": f"Spawn a subagent for a focused subtask.\\n\\nAgent types:\\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"},
+            "agent_type": {"type": "string", "enum": list(AGENT_TYPES.keys())},
+        },
+        "required": ["description", "prompt", "agent_type"],
+    },
+}
+"""
+
+
+# =============================================================================
+# TOOL IMPLEMENTATIONS
+# =============================================================================
+
+def safe_path(p: str) -> Path:
+    """
+    Security: Ensure path stays within workspace.
+    Prevents ../../../etc/passwd attacks.
+    """
+    path = (WORKDIR / p).resolve()
+    if not path.is_relative_to(WORKDIR):
+        raise ValueError(f"Path escapes workspace: {p}")
+    return path
+
+
+def run_bash(command: str) -> str:
+    """
+    Execute shell command with safety checks.
+
+    Safety features:
+    - Blocks obviously dangerous commands
+    - 60 second timeout
+    - Output truncated to 50KB
+    """
+    dangerous = ["rm -rf /", "sudo", "shutdown", "reboot", "> /dev/"]
+    if any(d in command for d in dangerous):
+        return "Error: Dangerous command blocked"
+
+    try:
+        result = subprocess.run(
+            command,
+            shell=True,
+            cwd=WORKDIR,
+            capture_output=True,
+            text=True,
+            timeout=60
+        )
+        output = (result.stdout + result.stderr).strip()
+        return output[:50000] if output else "(no output)"
+
+    except subprocess.TimeoutExpired:
+        return "Error: Command timed out (60s)"
+    except Exception as e:
+        return f"Error: {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
+    """
+    try:
+        text = safe_path(path).read_text()
+        lines = text.splitlines()
+
+        if limit and limit < len(lines):
+            lines = lines[:limit]
+            lines.append(f"... ({len(text.splitlines()) - limit} more lines)")
+
+        return "\n".join(lines)[:50000]
+
+    except Exception as e:
+        return f"Error: {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}"
+
+    except Exception as e:
+        return f"Error: {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}"
+
+        new_content = content.replace(old_text, new_text, 1)
+        fp.write_text(new_content)
+        return f"Edited {path}"
+
+    except Exception as e:
+        return f"Error: {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
+    """
+    if name == "bash":
+        return run_bash(args["command"])
+    if name == "read_file":
+        return run_read_file(args["path"], args.get("limit"))
+    if name == "write_file":
+        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}"

+ 279 - 0
skills/agent-builder/scripts/init_agent.py

@@ -0,0 +1,279 @@
+#!/usr/bin/env python3
+"""
+Agent Scaffold Script - Create a new agent project with best practices.
+
+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
+"""
+
+import argparse
+import sys
+from pathlib import Path
+
+# Agent templates for each level
+TEMPLATES = {
+    0: '''#!/usr/bin/env python3
+"""
+Level 0 Agent - Bash is All You Need (~50 lines)
+
+Core insight: One tool (bash) can do everything.
+Subagents via self-recursion: python {name}.py "subtask"
+"""
+
+from anthropic import Anthropic
+from dotenv import load_dotenv
+import subprocess
+import os
+
+load_dotenv()
+
+client = Anthropic(
+    api_key=os.getenv("ANTHROPIC_API_KEY"),
+    base_url=os.getenv("ANTHROPIC_BASE_URL")
+)
+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"
+"""
+
+TOOL = [{{
+    "name": "bash",
+    "description": "Execute shell command",
+    "input_schema": {{"type": "object", "properties": {{"command": {{"type": "string"}}}}, "required": ["command"]}}
+}}]
+
+def run(prompt, history=[]):
+    history.append({{"role": "user", "content": prompt}})
+    while True:
+        r = client.messages.create(model=MODEL, system=SYSTEM, messages=history, tools=TOOL, max_tokens=8000)
+        history.append({{"role": "assistant", "content": r.content}})
+        if r.stop_reason != "tool_use":
+            return "".join(b.text for b in r.content if hasattr(b, "text"))
+        results = []
+        for b in r.content:
+            if b.type == "tool_use":
+                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)"
+                except Exception as e:
+                    output = f"Error: {{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")
+    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)
+
+Core insight: 4 tools cover 90% of coding tasks.
+The model IS the agent. Code just runs the loop.
+"""
+
+from anthropic import Anthropic
+from dotenv import load_dotenv
+from pathlib import Path
+import subprocess
+import os
+
+load_dotenv()
+
+client = Anthropic(
+    api_key=os.getenv("ANTHROPIC_API_KEY"),
+    base_url=os.getenv("ANTHROPIC_BASE_URL")
+)
+MODEL = os.getenv("MODEL_NAME", "claude-sonnet-4-20250514")
+WORKDIR = Path.cwd()
+
+SYSTEM = f"""You are a coding agent at {{WORKDIR}}.
+
+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."""
+
+TOOLS = [
+    {{"name": "bash", "description": "Run shell command",
+     "input_schema": {{"type": "object", "properties": {{"command": {{"type": "string"}}}}, "required": ["command"]}}}},
+    {{"name": "read_file", "description": "Read file contents",
+     "input_schema": {{"type": "object", "properties": {{"path": {{"type": "string"}}}}, "required": ["path"]}}}},
+    {{"name": "write_file", "description": "Write content to file",
+     "input_schema": {{"type": "object", "properties": {{"path": {{"type": "string"}}, "content": {{"type": "string"}}}}, "required": ["path", "content"]}}}},
+    {{"name": "edit_file", "description": "Replace exact text in file",
+     "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}}")
+    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"
+        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)"
+        except subprocess.TimeoutExpired:
+            return "Error: Timeout (60s)"
+        except Exception as e:
+            return f"Error: {{e}}"
+
+    if name == "read_file":
+        try:
+            return safe_path(args["path"]).read_text()[:50000]
+        except Exception as e:
+            return f"Error: {{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']}}"
+        except Exception as e:
+            return f"Error: {{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']}}"
+            p.write_text(content.replace(args["old_text"], args["new_text"], 1))
+            return f"Edited {{args['path']}}"
+        except Exception as e:
+            return f"Error: {{e}}"
+
+    return f"Unknown tool: {{name}}"
+
+def agent(prompt: str, history: list = None) -> str:
+    """Run the agent loop."""
+    if history is None:
+        history = []
+    history.append({{"role": "user", "content": prompt}})
+
+    while True:
+        response = client.messages.create(
+            model=MODEL, system=SYSTEM, messages=history, tools=TOOLS, max_tokens=8000
+        )
+        history.append({{"role": "assistant", "content": response.content}})
+
+        if response.stop_reason != "tool_use":
+            return "".join(b.text for b in response.content if hasattr(b, "text"))
+
+        results = []
+        for block in response.content:
+            if block.type == "tool_use":
+                print(f"> {{block.name}}: {{str(block.input)[:100]}}")
+                output = execute(block.name, block.input)
+                print(f"  {{output[:100]}}...")
+                results.append({{"type": "tool_result", "tool_use_id": block.id, "content": output}})
+        history.append({{"role": "user", "content": results}})
+
+if __name__ == "__main__":
+    print(f"{name} - Level 1 Agent at {{WORKDIR}}")
+    print("Type 'q' to quit.\\n")
+    h = []
+    while True:
+        try:
+            query = input(">> ").strip()
+        except (EOFError, KeyboardInterrupt):
+            break
+        if query in ("q", "quit", "exit", ""):
+            break
+        print(agent(query, h), "\\n")
+''',
+}
+
+ENV_TEMPLATE = '''# API Configuration
+ANTHROPIC_API_KEY=sk-xxx
+ANTHROPIC_BASE_URL=https://api.anthropic.com
+MODEL_NAME=claude-sonnet-4-20250514
+'''
+
+
+def create_agent(name: str, level: int, output_dir: Path):
+    """Create a new agent project."""
+    # Validate level
+    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.")
+        sys.exit(1)
+
+    # Create output directory
+    agent_dir = output_dir / name
+    agent_dir.mkdir(parents=True, exist_ok=True)
+
+    # Write agent file
+    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}")
+
+    # Write .env.example
+    env_file = agent_dir / ".env.example"
+    env_file.write_text(ENV_TEMPLATE)
+    print(f"Created: {env_file}")
+
+    # Write .gitignore
+    gitignore = agent_dir / ".gitignore"
+    gitignore.write_text(".env\n__pycache__/\n*.pyc\n")
+    print(f"Created: {gitignore}")
+
+    print(f"\nAgent '{name}' created at {agent_dir}")
+    print(f"\nNext steps:")
+    print(f"  1. cd {agent_dir}")
+    print(f"  2. cp .env.example .env")
+    print(f"  3. Edit .env with your 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",
+        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
+        """
+    )
+    parser.add_argument("name", help="Name of the agent to create")
+    parser.add_argument("--level", type=int, default=1, choices=[0, 1, 2, 3, 4],
+                       help="Complexity level (default: 1)")
+    parser.add_argument("--path", type=Path, default=Path.cwd(),
+                       help="Output directory (default: current directory)")
+
+    args = parser.parse_args()
+    create_agent(args.name, args.level, args.path)
+
+
+if __name__ == "__main__":
+    main()