第 26 课:Task 系统 — 后台任务管理

4 阅读10分钟

模块九:多 Agent 架构 | 前置依赖:第 25 课 | 预计学习时间:65 分钟


学习目标

完成本课后,你将能够:

  1. 区分两套 Task 系统(后台任务 TaskState vs 结构化 Todo 列表)及其各自的工具集
  2. 说明 TaskState 的 7 种类型、5 种状态及完整生命周期
  3. 解释 6 个 Task 工具(TaskCreate/Get/List/Update/Output/Stop)的职责与协作方式
  4. 理解 Agent 后台运行模式的进度追踪与通知机制

26.1 两套 Task 系统

Claude Code 存在两套"任务"系统,容易混淆:

┌──────────────────────────────────────────────────────────┐
│                  Task 系统全景                            │
├──────────────────────┬───────────────────────────────────┤
│  后台任务 (TaskState) │  结构化 Todo 列表                  │
│                      │                                   │
│  定义: Task.ts       │  定义: utils/tasks.ts              │
│  存储: AppState.tasks│  存储: 磁盘 JSON 文件              │
│  工具: TaskOutput,   │  工具: TaskCreate, TaskGet,        │
│        TaskStop      │        TaskList, TaskUpdate        │
│                      │                                   │
│  用途: 管理后台运行   │  用途: 跟踪多步骤工作进度          │
│  的 Shell/Agent/     │  面向用户可见的任务清单             │
│  Remote 进程         │                                   │
│                      │                                   │
│  旧版: TodoWriteTool │  新版: TaskCreate + TaskUpdate     │
│  (全量替换模式)       │  (增量操作模式)                    │
└──────────────────────┴───────────────────────────────────┘

为什么有两套?

  • 后台任务(TaskState):管理进程级别的生命周期 — Shell 命令在后台运行、Agent 异步执行、远程 Agent 等。这些是真正的"计算任务"。
  • 结构化 Todo:管理用户的工作清单 — "修复 Bug #123"、"添加测试"。这些是"工作项",不直接对应进程。

本课会分别覆盖两套系统,重点放在后台任务系统上。


26.2 后台任务系统 — Task.ts

TaskType — 7 种任务类型

type TaskType =
  | 'local_bash'              // 本地 Shell 命令
  | 'local_agent'             // 本地 Agent(AgentTool 的后台模式)
  | 'remote_agent'            // 远程 Agent(CCR 环境)
  | 'in_process_teammate'     // 进程内 Teammate
  | 'local_workflow'          // 本地工作流脚本
  | 'monitor_mcp'             // MCP 监控任务
  | 'dream'                   // Dream 任务(实验性)

TaskStatus — 5 种状态

type TaskStatus =
  | 'pending'       // 已创建,等待运行
  | 'running'       // 正在运行
  | 'completed'     // 成功完成
  | 'failed'        // 执行失败
  | 'killed'        // 被手动终止

function isTerminalTaskStatus(status: TaskStatus): boolean {
  return status === 'completed' || status === 'failed' || status === 'killed'
}

状态机

┌─────────┐
│ pending │────────────────┐
└────┬────┘                │
     │ 开始执行             │ 启动前被取消
     ▼                     ▼
┌─────────┐          ┌─────────┐
│ running │─────────▶│ killed  │
└────┬────┘  手动终止 └─────────┘
     │
     ├── 成功 ──▶ ┌───────────┐
     │            │ completed │
     │            └───────────┘
     │
     └── 失败 ──▶ ┌─────────┐
                  │ failed  │
                  └─────────┘

TaskStateBase — 通用字段

type TaskStateBase = {
  id: string              // 任务 ID(如 "a3kf9x2m")
  type: TaskType          // 任务类型
  status: TaskStatus      // 当前状态
  description: string     // 人类可读的描述
  toolUseId?: string      // 关联的 tool_use ID
  startTime: number       // 开始时间戳
  endTime?: number        // 结束时间戳
  totalPausedMs?: number  // 总暂停时间
  outputFile: string      // 输出文件路径
  outputOffset: number    // 输出读取偏移量
  notified: boolean       // 完成通知是否已发送
}

Task ID 生成

每种类型有不同的前缀,后跟 8 个随机字符:

const TASK_ID_PREFIXES: Record<string, string> = {
  local_bash: 'b',              // 如 "b3kf9x2m"
  local_agent: 'a',             // 如 "akm4np7q"
  remote_agent: 'r',            // 如 "rw2xt8jz"
  in_process_teammate: 't',     // 如 "t5hv3ynr"
  local_workflow: 'w',          // 如 "w9cd6fmb"
  monitor_mcp: 'm',             // 如 "mp8kq4sg"
  dream: 'd',                   // 如 "dj7nh2wa"
}

// 使用 36 字符字母表(数字 + 小写字母),8 位 = 36^8 ≈ 2.8 万亿组合
const TASK_ID_ALPHABET = '0123456789abcdefghijklmnopqrstuvwxyz'

function generateTaskId(type: TaskType): string {
  const prefix = getTaskIdPrefix(type)
  const bytes = randomBytes(8)
  let id = prefix
  for (let i = 0; i < 8; i++) {
    id += TASK_ID_ALPHABET[bytes[i]! % TASK_ID_ALPHABET.length]
  }
  return id
}

Task 接口

type Task = {
  name: string
  type: TaskType
  kill(taskId: string, setAppState: SetAppState): Promise<void>
}

每种 TaskType 实现一个 Task 对象,由 tasks.ts 统一管理:

function getAllTasks(): Task[] {
  const tasks: Task[] = [
    LocalShellTask,
    LocalAgentTask,
    RemoteAgentTask,
    DreamTask,
  ]
  if (LocalWorkflowTask) tasks.push(LocalWorkflowTask)  // feature-gated
  if (MonitorMcpTask) tasks.push(MonitorMcpTask)         // feature-gated
  return tasks
}

function getTaskByType(type: TaskType): Task | undefined {
  return getAllTasks().find(t => t.type === type)
}

26.3 TaskState 具体类型

tasks/types.ts 定义了具体的 TaskState 联合类型:

type TaskState =
  | LocalShellTaskState       // Shell 命令:含 command, result (exitCode)
  | LocalAgentTaskState       // 本地 Agent:含 prompt, result, agentId
  | RemoteAgentTaskState      // 远程 Agent:含 command, sessionUrl
  | InProcessTeammateTaskState// 进程内队友:含 teammateId, agentName
  | LocalWorkflowTaskState    // 工作流脚本
  | MonitorMcpTaskState       // MCP 监控
  | DreamTaskState            // Dream

LocalAgentTaskState

这是 AgentTool 后台模式的核心状态:

LocalAgentTaskState extends TaskStateBase
├── prompt: string              // Agent 的输入提示
├── agentId: AgentId            // Agent 实例 ID
├── agentDefinition?: AgentDefinition
├── result?: AgentToolResult    // 最终结果
├── error?: string              // 错误信息
├── progress?: AgentProgress    // 实时进度
│   ├── toolUseCount: number    // 工具调用次数
│   ├── tokenCount: number      // Token 使用量
│   ├── lastActivity?: ToolActivity
│   ├── recentActivities?: ToolActivity[]
│   └── summary?: string        // AI 生成的进度摘要
├── isBackgrounded: boolean     // 是否在后台
└── worktree?: WorktreeInfo     // Worktree 隔离信息

后台任务判定

function isBackgroundTask(task: TaskState): task is BackgroundTaskState {
  // 必须是运行中或等待中
  if (task.status !== 'running' && task.status !== 'pending') return false
  // 未被显式后台化的前台任务不算
  if ('isBackgrounded' in task && task.isBackgrounded === false) return false
  return true
}

26.4 Agent 进度追踪

LocalAgentTask.ts 实现了精细的进度追踪系统:

ProgressTracker

type ProgressTracker = {
  toolUseCount: number           // 工具调用次数
  latestInputTokens: number      // 最新输入 Token(累积值)
  cumulativeOutputTokens: number // 累积输出 Token
  recentActivities: ToolActivity[] // 最近 5 次工具活动
}

type ToolActivity = {
  toolName: string
  input: Record<string, unknown>
  activityDescription?: string  // 如 "Reading src/foo.ts"
  isSearch?: boolean            // Grep/Glob 等搜索操作
  isRead?: boolean              // Read/cat 等读取操作
}

进度更新流程

Agent 的每个 Assistant Message
      │
      ▼
updateProgressFromMessage(tracker, message)
      │
      ├── 更新 Token 计数
      │   ├── latestInputTokens = input + cache_creation + cache_read
      │   └── cumulativeOutputTokens += output_tokens
      │
      └── 遍历 content blocks
          └── 对每个 tool_use block
              ├── toolUseCount++
              ├── 计算 activityDescription(通过 Tool.getActivityDescription)
              ├── 分类 isSearch/isRead
              └── 加入 recentActivities(最多保留 5 条)

进度通知机制

Agent 运行中
      │
      ├── 每次工具调用后 → updateAsyncAgentProgress()
      │   └── 更新 AppState.tasks[id].progress
      │
      ├── 定期(SDK 模式)→ emitTaskProgress()
      │   └── 发送 SDK 事件
      │
      └── Agent 完成
          ├── completeAsyncAgent() → 状态变为 'completed'
          │   └── enqueueAgentNotification()
          │       └── 生成 <task-notification> XML 消息
          │
          └── failAsyncAgent() → 状态变为 'failed'
              └── 同样发送通知

task-notification 消息格式

<task-notification>
  <task_id>akm4np7q</task_id>
  <tool_use_id>toolu_xxx</tool_use_id>
  <status>completed</status>
  <output_file>/path/to/output</output_file>
  <summary>Found 3 security issues in auth module...</summary>
</task-notification>

26.5 停止任务(stopTask.ts)

stopTask.ts 提供统一的任务停止逻辑,被 TaskStopTool 和 SDK 控制请求共用:

async function stopTask(taskId: string, context: StopTaskContext): Promise<StopTaskResult> {
  const task = appState.tasks?.[taskId]

  // 验证
  if (!task) throw new StopTaskError('No task found', 'not_found')
  if (task.status !== 'running') throw new StopTaskError('Not running', 'not_running')

  // 查找对应的 Task 实现
  const taskImpl = getTaskByType(task.type)
  if (!taskImpl) throw new StopTaskError('Unsupported type', 'unsupported_type')

  // 执行 kill
  await taskImpl.kill(taskId, setAppState)

  // Shell 任务:静默 exit code 137 通知(噪音)
  // Agent 任务:不静默(通知包含 partial result)
  if (isLocalShellTask(task)) {
    setAppState(prev => ({
      ...prev,
      tasks: {
        ...prev.tasks,
        [taskId]: { ...prev.tasks[taskId], notified: true },
      },
    }))
  }

  return { taskId, taskType: task.type, command }
}

26.6 结构化 Todo 列表 — 6 个 Task 工具

概览

工具文件大小职责
TaskCreateTool3.4KB创建新任务
TaskGetTool2.9KB按 ID 获取任务详情
TaskListTool2.8KB列出所有任务
TaskUpdateTool12KB更新任务状态/字段
TaskOutputTool67KB读取后台任务输出(已标记 Deprecated)
TaskStopTool3.9KB停止后台运行的任务

TaskCreateTool

const inputSchema = z.strictObject({
  subject: z.string(),          // 任务标题
  description: z.string(),       // 任务描述
  activeForm: z.string().optional(), // 进度条文案(如 "Running tests")
  metadata: z.record(z.string(), z.unknown()).optional(),
})

// 调用逻辑
async call({ subject, description, activeForm, metadata }, context) {
  // 1. 创建任务(写入磁盘 JSON)
  const taskId = await createTask(getTaskListId(), {
    subject, description, activeForm,
    status: 'pending',
    owner: undefined,
    blocks: [], blockedBy: [],
    metadata,
  })

  // 2. 运行 TaskCreated Hooks(可被 Hook 阻止)
  for await (const result of executeTaskCreatedHooks(...)) {
    if (result.blockingError) blockingErrors.push(...)
  }

  // 3. Hook 阻止时回滚
  if (blockingErrors.length > 0) {
    await deleteTask(getTaskListId(), taskId)
    throw new Error(blockingErrors.join('\n'))
  }

  // 4. 自动展开任务列表 UI
  context.setAppState(prev => ({ ...prev, expandedView: 'tasks' }))

  return { data: { task: { id: taskId, subject } } }
}

TaskUpdateTool

最复杂的 Todo 工具,支持多种更新操作:

const inputSchema = z.strictObject({
  taskId: z.string(),
  subject: z.string().optional(),
  description: z.string().optional(),
  activeForm: z.string().optional(),
  status: TaskUpdateStatusSchema.optional(), // 包含 'deleted' 特殊值
  addBlocks: z.array(z.string()).optional(),
  addBlockedBy: z.array(z.string()).optional(),
  owner: z.string().optional(),
  metadata: z.record(z.string(), z.unknown()).optional(),
})

状态工作流pending → in_progress → completed

特殊操作

  • status: 'deleted' — 永久删除任务
  • addBlockedBy — 设置任务依赖关系
  • owner — 分配任务给特定 Agent

TaskGetTool / TaskListTool

只读工具,从磁盘读取任务数据:

// TaskListTool — 列出所有非内部任务
async call() {
  const allTasks = (await listTasks(taskListId))
    .filter(t => !t.metadata?._internal)  // 排除内部任务

  // 过滤已解决的 blockedBy(不显示已完成任务的阻塞)
  const resolvedTaskIds = new Set(
    allTasks.filter(t => t.status === 'completed').map(t => t.id),
  )
  const tasks = allTasks.map(task => ({
    ...task,
    blockedBy: task.blockedBy.filter(id => !resolvedTaskIds.has(id)),
  }))
  return { data: { tasks } }
}

TaskOutputTool(已 Deprecated)

虽然标记为 Deprecated,但仍然可用。它负责读取后台任务的输出:

// 输入
z.strictObject({
  task_id: z.string(),
  block: z.boolean().default(true),   // 是否阻塞等待完成
  timeout: z.number().default(30000), // 最大等待时间(ms)
})

// 返回
type TaskOutputToolOutput = {
  retrieval_status: 'success' | 'timeout' | 'not_ready'
  task: TaskOutput | null
}

替代方案:推荐直接用 Read 工具读取任务的 outputFile 路径。

TaskStopTool

const inputSchema = z.strictObject({
  task_id: z.string().optional(),
  shell_id: z.string().optional(), // 兼容旧版 KillShell
})

// 别名
aliases: ['KillShell']  // 向后兼容

26.7 Todo 存储机制(utils/tasks.ts)

存储格式

Todo 列表存储在磁盘上的 JSON 文件中,按 session 或 team 隔离:

type Task = {
  id: string
  subject: string
  description: string
  activeForm?: string         // 进度条文案
  owner?: string              // 所属 Agent
  status: 'pending' | 'in_progress' | 'completed'
  blocks: string[]            // 此任务阻塞的任务 ID
  blockedBy: string[]         // 阻塞此任务的任务 ID
  metadata?: Record<string, unknown>
}

Task List ID

每个会话或团队有独立的 Task 列表:

function getTaskListId(): string {
  // 团队模式 → 团队名称
  // 单人模式 → session ID
  return leaderTeamName ?? getTeamName() ?? getSessionId()
}

并发安全

多 Agent 场景(如 swarm 中 10+ Agent 同时操作)使用文件锁保护:

// 锁选项:异步锁 + 退避重试
// 每个临界区约 50-100ms(readdir + N×readFile + writeFile)
// 预算支持 ~10+ 并发 swarm agent
const LOCK_OPTIONS = {
  retries: { retries: 15, maxTimeout: 2000 },
}

更新通知

任务变更后通过 Signal 通知同进程的订阅者(用于 UI 即时刷新):

const tasksUpdated = createSignal()
export const onTasksUpdated = tasksUpdated.subscribe
export function notifyTasksUpdated(): void {
  try { tasksUpdated.emit() } catch { /* 忽略 listener 错误 */ }
}

26.8 TodoWriteTool — 旧版全量替换

TodoWriteTool 是结构化 Todo 系统的前身,仍然存在但通过 feature gate 互斥:

isEnabled() {
  return !isTodoV2Enabled()  // 新版启用时旧版禁用
}

关键区别

维度TodoWriteToolTaskCreate + TaskUpdate
操作模式全量替换整个列表增量创建/更新单个任务
存储AppState.todos磁盘 JSON 文件
并发不安全(覆盖写入)文件锁保护
多 Agent不支持支持 owner/team
依赖关系不支持blocks/blockedBy

TodoWriteTool 的一个独特功能 — 验证提醒

// 当所有任务标记完成时,检查是否有验证步骤
let verificationNudgeNeeded = false
if (feature('VERIFICATION_AGENT') && allDone && oldTodos.length >= 3) {
  // 如果没有包含 "verification" 类型的任务,提醒需要验证
  verificationNudgeNeeded = true
}

26.9 AgentTool 与 Task 系统的关系

当 AgentTool 以 run_in_background: true 调用时,它与 Task 系统深度集成:

AgentTool.call({ run_in_background: true })
      │
      ├── 1. 生成 Task ID (type: 'local_agent')
      │
      ├── 2. 注册到 AppState.tasks
      │     └── registerAsyncAgent() → status: 'pending'
      │
      ├── 3. 启动 Agent 运行(独立 Promise)
      │     └── runAsyncAgentLifecycle()
      │         ├── status → 'running'
      │         ├── 定期更新 progress
      │         └── 完成/失败时发送通知
      │
      ├── 4. 立即返回 async_launched 给模型
      │     └── { status: 'async_launched', agentId, outputFile }
      │
      └── 5. Agent 完成后
            └── <task-notification> 作为 User 消息注入
                → 模型在下一轮看到完成通知

自动后台化

超过指定时间未完成的 Agent 可被自动后台化:

function getAutoBackgroundMs(): number {
  if (isEnvTruthy(process.env.CLAUDE_AUTO_BACKGROUND_TASKS) ||
      getFeatureValue_CACHED_MAY_BE_STALE('tengu_auto_background_agents', false)) {
    return 120_000  // 2 分钟后自动后台化
  }
  return 0  // 禁用
}

课后练习

练习 1:Task 系统辨析

给定以下场景,判断使用哪套 Task 系统和哪些工具:

  1. 用户说 "同时运行测试和 lint"
  2. 用户说 "列出需要做的事情"
  3. 模型需要启动一个后台 Agent 做代码审查
  4. 用户想停止一个正在运行的后台 Shell

练习 2:状态转换追踪

画出一个后台 Agent 从创建到完成的完整状态转换图,标注每一步涉及的函数调用和 AppState 变更。

练习 3:并发场景分析

在 10 个 Agent 组成的 swarm 中,所有 Agent 同时更新同一个 Task 列表。分析文件锁机制如何保证数据一致性,以及在极端情况下(锁超时)会发生什么。

练习 4:进度追踪设计

设计一个改进的 AgentProgress 类型,增加以下功能:

  • 预估完成时间
  • 按工具类型分组的调用统计
  • 错误/重试计数

本课小结

要点内容
两套 Task 系统后台任务(进程级 TaskState)vs 结构化 Todo(工作项 Task)
TaskState7 种 type × 5 种 status,ID 带类型前缀
后台 Agentregister → run → progress update → complete/fail → notification
6 个 Task 工具Create/Get/List/Update(Todo 列表)+ Output/Stop(后台任务)
进度追踪ProgressTracker 记录 tool use count / token / recent activities
并发安全文件锁 + 退避重试,支持 10+ 并发 Agent
TodoWriteTool旧版全量替换,通过 feature gate 与新版互斥

下一课预告

第 27 课:权限系统深度解析 — 理解 Claude Code 的多层权限模型:PermissionMode(plan/auto/default)、工具级权限分类、YOLO 模式的自动分类器、权限规则引擎以及 Hook 系统如何参与权限决策。