Agent 开发进阶(十八):Worktree 任务隔离,让多个任务互不干扰

1 阅读11分钟

Agent 开发进阶(十八):Worktree 任务隔离,让多个任务互不干扰

本文是「从零构建 Coding Agent」系列的第十八篇,适合想让多个任务并行执行且互不干扰的开发者。

先问一个问题

当你的多个 Agent 并行执行不同任务时,你是否遇到过这样的问题:

  • 两个任务同时修改同一个文件,导致冲突
  • 一个任务还没做完,另一个任务的修改已经污染了工作目录
  • 想单独回看某个任务的改动范围,却很难分清

如果你的答案是肯定的,那么你需要一个 Worktree 任务隔离系统。

并行工作的「目录冲突」问题

到了这一阶段,你的 Agent 已经具备了多种能力:

  • 核心循环运行
  • 工具使用与分发
  • 会话内规划(TodoWrite)
  • 子智能体机制(Subagent)
  • 技能加载
  • 上下文压缩
  • 权限系统
  • Hook 系统
  • Memory 系统
  • 系统提示词组装
  • 错误恢复
  • 任务系统
  • 后台任务系统
  • 定时调度系统
  • Agent 团队系统
  • 团队协议系统
  • 自治智能体系统

但当多个 Agent 并行执行任务时,在同一个工作目录里会遇到明显问题:

  • 文件冲突:两个任务同时修改同一个文件
  • 目录污染:一个任务的修改影响另一个任务
  • 难以追踪:无法单独查看某个任务的改动范围
  • 回滚困难:很难单独回滚某个任务

虽然 s17 的自治系统让 Agent 可以主动认领任务,但如果所有人都在同一个工作目录里改文件,很快就会出现冲突。

所以到了这个阶段,我们需要一个 Worktree 任务隔离系统:

把每个任务放到独立的工作目录里执行,避免互相干扰。

Worktree 任务隔离系统的核心设计:任务与工作目录的绑定

用一个图来表示 Worktree 任务隔离系统的工作流程:

任务被创建
  ->
队友认领任务
  ->
系统为任务分配 worktree
  ->
命令在对应目录里执行
  ->
任务完成时决定保留还是删除 worktree

关键点只有三个:

  1. 任务绑定:把任务 ID 和 worktree 明确关联
  2. 目录隔离:每个任务在独立目录中执行
  3. 收尾管理:任务完成后决定保留或删除 worktree

几个必须搞懂的概念

Worktree(工作树)

如果你熟悉 git,可以把 worktree 理解成:

同一个仓库的另一个独立检出目录。

如果你还不熟悉 git,也可以先把它理解成:

一条属于某个任务的独立工作车道。

隔离执行(Isolated Execution)

隔离执行就是:

任务 A 在自己的目录里跑,任务 B 在自己的目录里跑,彼此默认不共享未提交改动。

绑定(Binding)

绑定的意思是:

把某个任务 ID 和某个 worktree 记录明确关联起来。

收尾(Closeout)

收尾就是任务完成后,决定 worktree 的最终状态:

  • 保留:保留目录,方便后续查看或继续工作
  • 删除:删除目录,释放资源

最小实现

1. Worktree Manager

import os
import json
import subprocess
import time
import uuid
from pathlib import Path

class WorktreeManager:
    """Worktree 管理器"""
    
    def __init__(self, worktrees_dir=".worktrees", index_file=".worktrees/index.json"):
        self.worktrees_dir = Path(worktrees_dir)
        self.worktrees_dir.mkdir(exist_ok=True)
        
        self.index_file = Path(index_file)
        self.index_file.parent.mkdir(exist_ok=True)
        
        # 加载索引
        self.index = self._load_index()
        
        # 事件日志
        self.events_file = self.worktrees_dir / "events.jsonl"
    
    def _load_index(self):
        """加载索引"""
        if self.index_file.exists():
            try:
                return json.loads(self.index_file.read_text(encoding="utf-8"))
            except Exception as e:
                print(f"加载索引失败: {e}")
        return {"worktrees": []}
    
    def _save_index(self):
        """保存索引"""
        self.index_file.write_text(
            json.dumps(self.index, indent=2, ensure_ascii=False),
            encoding="utf-8"
        )
    
    def _log_event(self, event):
        """记录事件"""
        with open(self.events_file, "a", encoding="utf-8") as f:
            f.write(json.dumps(event, ensure_ascii=False) + "\n")
    
    def _run_git(self, args, cwd=None):
        """运行 git 命令"""
        try:
            result = subprocess.run(
                ["git"] + args,
                cwd=cwd,
                capture_output=True,
                text=True,
                check=True
            )
            return result.stdout
        except subprocess.CalledProcessError as e:
            print(f"Git 命令失败: {e}")
            raise
    
    def create(self, name, task_id):
        """创建 worktree"""
        path = self.worktrees_dir / name
        branch = f"wt/{name}"
        
        # 创建 worktree
        self._run_git(["worktree", "add", "-b", branch, str(path), "HEAD"])
        
        # 创建记录
        record = {
            "name": name,
            "path": str(path),
            "branch": branch,
            "task_id": task_id,
            "status": "active",
            "created_at": time.time(),
            "last_entered_at": None,
            "last_command_at": None,
            "last_command_preview": None,
            "closeout": None,
        }
        
        self.index["worktrees"].append(record)
        self._save_index()
        
        # 记录事件
        self._log_event({
            "event": "worktree.created",
            "name": name,
            "task_id": task_id,
            "path": str(path),
            "ts": time.time(),
        })
        
        return record
    
    def get(self, name):
        """获取 worktree"""
        for wt in self.index["worktrees"]:
            if wt["name"] == name:
                return wt
        return None
    
    def enter(self, name):
        """进入 worktree"""
        worktree = self.get(name)
        if not worktree:
            raise ValueError(f"Worktree {name} 不存在")
        
        # 更新记录
        worktree["last_entered_at"] = time.time()
        self._save_index()
        
        # 记录事件
        self._log_event({
            "event": "worktree.entered",
            "name": name,
            "task_id": worktree["task_id"],
            "ts": time.time(),
        })
        
        return worktree["path"]
    
    def run(self, name, command):
        """在 worktree 中执行命令"""
        worktree = self.get(name)
        if not worktree:
            raise ValueError(f"Worktree {name} 不存在")
        
        path = worktree["path"]
        
        # 执行命令
        try:
            result = subprocess.run(
                command,
                shell=True,
                cwd=path,
                capture_output=True,
                text=True,
                timeout=300
            )
            
            # 更新记录
            worktree["last_command_at"] = time.time()
            worktree["last_command_preview"] = command[:100]
            self._save_index()
            
            # 记录事件
            self._log_event({
                "event": "worktree.command.executed",
                "name": name,
                "task_id": worktree["task_id"],
                "command": command,
                "exit_code": result.returncode,
                "ts": time.time(),
            })
            
            return {
                "success": result.returncode == 0,
                "stdout": result.stdout,
                "stderr": result.stderr,
                "exit_code": result.returncode,
            }
            
        except subprocess.TimeoutExpired:
            return {
                "success": False,
                "stdout": "",
                "stderr": "Command timed out",
                "exit_code": -1,
            }
    
    def closeout(self, name, action, reason="", complete_task=False):
        """收尾 worktree"""
        worktree = self.get(name)
        if not worktree:
            raise ValueError(f"Worktree {name} 不存在")
        
        # 检查未提交改动
        path = worktree["path"]
        try:
            status = self._run_git(["status", "--porcelain"], cwd=path)
            if status.strip():
                print(f"警告: Worktree {name} 有未提交的改动")
                # 实际应该询问用户是否继续
        except:
            pass
        
        # 记录收尾动作
        closeout_record = {
            "action": action,
            "reason": reason,
            "at": time.time(),
        }
        worktree["closeout"] = closeout_record
        
        if action == "keep":
            worktree["status"] = "kept"
        elif action == "remove":
            worktree["status"] = "removed"
            # 删除 worktree
            self._run_git(["worktree", "remove", name])
        
        self._save_index()
        
        # 记录事件
        self._log_event({
            "event": f"worktree.closeout.{action}",
            "name": name,
            "task_id": worktree["task_id"],
            "reason": reason,
            "complete_task": complete_task,
            "ts": time.time(),
        })
        
        return {
            "action": action,
            "name": name,
            "task_id": worktree["task_id"],
            "complete_task": complete_task,
        }
    
    def list(self):
        """列出所有 worktree"""
        if not self.index["worktrees"]:
            return "暂无 worktree"
        
        lines = ["# Worktrees\n"]
        for wt in self.index["worktrees"]:
            lines.append(f"- **{wt['name']}** ({wt['status']})")
            lines.append(f"  路径: {wt['path']}")
            lines.append(f"  分支: {wt['branch']}")
            lines.append(f"  任务: #{wt['task_id']}")
            if wt.get('last_command_preview'):
                lines.append(f"  最后命令: {wt['last_command_preview']}")
        
        return "\n".join(lines)

2. 增强的任务管理器

class WorktreeTaskManager:
    """支持 worktree 的任务管理器"""
    
    def __init__(self, tasks_dir=".tasks"):
        self.tasks_dir = Path(tasks_dir)
        self.tasks_dir.mkdir(exist_ok=True)
        
        self.tasks = {}
        self._load_all_tasks()
    
    def _load_all_tasks(self):
        """加载所有任务"""
        for task_file in self.tasks_dir.glob("task_*.json"):
            try:
                task = json.loads(task_file.read_text(encoding="utf-8"))
                self.tasks[task["id"]] = task
            except Exception as e:
                print(f"加载任务失败 {task_file}: {e}")
    
    def _save_task(self, task):
        """保存任务"""
        task_file = self.tasks_dir / f"task_{task['id']}.json"
        task_file.write_text(
            json.dumps(task, indent=2, ensure_ascii=False),
            encoding="utf-8"
        )
    
    def create(self, subject, description=""):
        """创建任务"""
        task = {
            "id": len(self.tasks) + 1,
            "subject": subject,
            "description": description,
            "status": "pending",
            "blockedBy": [],
            "blocks": [],
            "owner": "",
            "worktree": None,
            "worktree_state": "unbound",
            "last_worktree": None,
            "closeout": None,
        }
        
        self.tasks[task["id"]] = task
        self._save_task(task)
        return task
    
    def bind_worktree(self, task_id, worktree_name):
        """绑定 worktree"""
        task = self.tasks.get(task_id)
        if not task:
            return None
        
        task["worktree"] = worktree_name
        task["last_worktree"] = worktree_name
        task["worktree_state"] = "active"
        if task["status"] == "pending":
            task["status"] = "in_progress"
        
        self._save_task(task)
        return task
    
    def update_worktree_state(self, task_id, worktree_state, closeout=None):
        """更新 worktree 状态"""
        task = self.tasks.get(task_id)
        if not task:
            return None
        
        task["worktree_state"] = worktree_state
        if closeout:
            task["closeout"] = closeout
        
        self._save_task(task)
        return task

3. 集成到自治系统

class WorktreeAutonomousTeammate:
    """支持 worktree 的自治队友"""
    
    def __init__(self, name, role, task_manager, worktree_manager, prompt):
        self.name = name
        self.role = role
        self.task_manager = task_manager
        self.worktree_manager = worktree_manager
        self.prompt = prompt
        
        self.messages = [{
            "role": "system",
            "content": f"你是 {name},一个 {role}。请专注于你的职责,完成任务后等待新的指示。"
        }, {
            "role": "user",
            "content": prompt
        }]
        
        self.running = True
        self.idle_count = 0
        self.max_idle_count = 10
        self.poll_interval = 5
    
    def claim_and_execute(self, task):
        """认领并执行任务"""
        task_id = task["id"]
        
        # 创建 worktree
        worktree_name = f"task-{task_id}-{self.name}"
        worktree = self.worktree_manager.create(worktree_name, task_id)
        
        # 绑定 worktree
        self.task_manager.bind_worktree(task_id, worktree_name)
        
        # 进入 worktree
        worktree_path = self.worktree_manager.enter(worktree_name)
        
        print(f"{self.name}{worktree_path} 中执行任务 #{task_id}")
        
        # 执行任务(简化版本)
        # 实际应该根据任务内容执行相应命令
        result = self.worktree_manager.run(worktree_name, "echo 'Task executed'")
        
        if result["success"]:
            # 任务完成,决定收尾
            closeout_result = self.worktree_manager.closeout(
                worktree_name,
                action="keep",
                reason="Task completed, keeping for review",
                complete_task=True
            )
            
            # 更新任务状态
            self.task_manager.update_worktree_state(
                task_id,
                worktree_state="kept",
                closeout=closeout_result
            )
            
            print(f"{self.name} 完成任务 #{task_id},worktree 已保留")
        else:
            print(f"{self.name} 执行任务 #{task_id} 失败: {result['stderr']}")
    
    def run(self):
        """主循环"""
        print(f"队友 {self.name} ({self.role}) 已启动")
        
        while self.running:
            # 扫描可认领任务
            claimable_tasks = self.task_manager.get_claimable_tasks(self.role)
            
            if claimable_tasks:
                task = claimable_tasks[0]
                print(f"{self.name} 认领任务: #{task['id']} {task['subject']}")
                
                # 认领并执行任务
                self.claim_and_execute(task)
                
                self.idle_count = 0
            else:
                self.idle_count += 1
                print(f"{self.name} 空闲 ({self.idle_count}/{self.max_idle_count})")
                
                if self.idle_count >= self.max_idle_count:
                    print(f"{self.name} 长时间空闲,准备退出")
                    break
            
            time.sleep(self.poll_interval)
        
        print(f"队友 {self.name} ({self.role}) 已退出")

核心功能说明

1. 创建 Worktree

为任务创建独立工作目录

worktree = worktree_manager.create("auth-refactor", task_id=12)

创建过程包括:

  • 使用 git worktree 创建独立检出
  • 在索引中注册 worktree 记录
  • 记录创建事件

2. 绑定任务和 Worktree

将任务与 worktree 关联

task_manager.bind_worktree(task_id=12, worktree_name="auth-refactor")

绑定后,任务记录会包含:

  • worktree:当前绑定的 worktree 名称
  • worktree_state:绑定状态
  • last_worktree:最后使用的 worktree

3. 在 Worktree 中执行命令

进入 worktree 并执行命令

worktree_path = worktree_manager.enter("auth-refactor")
result = worktree_manager.run("auth-refactor", "pytest tests/auth -q")

关键点:

  • 命令在 worktree 的目录中执行
  • 不同 worktree 中的命令互不影响
  • 记录命令执行历史

4. Worktree 收尾

任务完成后决定 worktree 的最终状态

worktree_manager.closeout(
    name="auth-refactor",
    action="keep",  # 或 "remove"
    reason="Task completed, keeping for review",
    complete_task=True
)

收尾动作:

  • keep:保留 worktree,方便后续查看
  • remove:删除 worktree,释放资源

Worktree vs 任务状态

状态任务状态Worktree 状态说明
任务进行中in_progressactive任务正在执行,worktree 正在使用
任务完成completedkept任务完成,worktree 保留供查看
任务完成completedremoved任务完成,worktree 已删除
任务未开始pendingunbound任务未认领,没有关联的 worktree

关键点:任务状态和 worktree 状态是独立的,不能混为一谈。

新手最容易犯的 7 个错

1. 有 worktree 注册表,但任务记录里没有 worktree

# ❌ 错误
# 只创建了 worktree,没有更新任务记录
worktree_manager.create("auth-refactor", task_id=12)

# ✅ 正确
# 同时更新任务记录
worktree_manager.create("auth-refactor", task_id=12)
task_manager.bind_worktree(12, "auth-refactor")

2. 有任务 ID,但命令仍然在主目录执行

# ❌ 错误
# 没有切换到 worktree 目录
subprocess.run("pytest tests/auth", shell=True)

# ✅ 正确
# 在 worktree 目录中执行
worktree_path = worktree_manager.enter("auth-refactor")
subprocess.run("pytest tests/auth", shell=True, cwd=worktree_path)

3. 只会删除 worktree,不会解释 closeout 的含义

# ❌ 错误
# 只知道删除目录
subprocess.run(["git", "worktree", "remove", "auth-refactor"])

# ✅ 正确
# 明确收尾动作和原因
worktree_manager.closeout(
    name="auth-refactor",
    action="keep",
    reason="Need follow-up review",
    complete_task=False
)

4. 删除 worktree 前不看未提交改动

# ❌ 错误
# 直接删除,可能丢失未提交的改动
subprocess.run(["git", "worktree", "remove", "auth-refactor"])

# ✅ 正确
# 删除前检查未提交改动
status = subprocess.run(["git", "status", "--porcelain"], cwd=path)
if status.stdout.strip():
    print("警告: 有未提交的改动")
    # 询问用户是否继续

5. 没有 worktree_state / closeout 这类显式收尾状态

# ❌ 错误
# 只记录 worktree 是否存在
task["worktree"] = "auth-refactor"

# ✅ 正确
# 记录详细的状态信息
task["worktree"] = "auth-refactor"
task["worktree_state"] = "kept"
task["closeout"] = {
    "action": "keep",
    "reason": "Task completed",
    "at": time.time()
}

6. 把 worktree 当成长期垃圾堆

# ❌ 错误
# 从不清理 worktree
def create_worktree(name):
    # 只创建,不清理
    pass

# ✅ 正确
# 定期清理不需要的 worktree
def cleanup_old_worktrees():
    # 删除超过一定时间未使用的 worktree
    pass

7. 没有事件日志

# ❌ 错误
# 没有记录事件,难以排查问题
def create_worktree(name):
    subprocess.run(["git", "worktree", "add", name])
    # 没有记录

# ✅ 正确
# 记录所有重要事件
def create_worktree(name):
    subprocess.run(["git", "worktree", "add", name])
    log_event({
        "event": "worktree.created",
        "name": name,
        "ts": time.time()
    })

为什么这很重要

因为一个真正可靠的并行系统,需要清晰的隔离机制。

Worktree 任务隔离系统让你能够:

  1. 避免冲突:每个任务在独立目录中执行,避免文件冲突
  2. 清晰追踪:可以单独查看每个任务的改动范围
  3. 安全回滚:可以单独回滚某个任务,不影响其他任务
  4. 并行执行:多个任务可以同时进行,互不干扰
  5. 资源管理:可以灵活地保留或删除 worktree,管理资源使用

推荐的实现步骤

  1. 第一步:实现 WorktreeManager 类,管理 worktree 的创建和删除
  2. 第二步:实现 worktree 的进入和命令执行
  3. 第三步:实现 worktree 的收尾机制(保留或删除)
  4. 第四步:增强任务管理器,支持 worktree 绑定
  5. 第五步:集成到自治系统,让队友在 worktree 中执行任务
  6. 第六步:实现事件日志,记录所有重要操作
  7. 第七步:添加安全检查,避免意外删除未提交的改动

Worktree 任务隔离与后续章节的关系

  • s18 Worktree:解决任务在哪里执行且互不干扰的问题
  • s19 MCP 插件:会利用 worktree 来扩展系统能力

所以 Worktree 任务隔离是构建高级并行系统的基础组件。

下一章预告

有了 Worktree 任务隔离系统,你的多个 Agent 已经可以在独立的工作目录中并行执行任务。下一章我们将探讨 MCP 插件系统,让 Agent 能够通过插件扩展能力,实现更强的灵活性。


一句话总结:任务系统管「做什么」,worktree 系统管「在哪做且互不干扰」。


如果觉得有帮助,欢迎关注,我会持续更新「从零构建 Coding Agent」系列文章。