本文是《Claude Code 源码分析 100 篇》系列的第二十三篇。
为什么需要 Tasks 系统
Claude Code 是一个高度并发的工作环境------一个主会话(Leader)可以同时调度多个后台任务:
- 用户按
Ctrl+B把当前查询打入后台,继续与主会话对话 - Agent 任务(Teammates)独立运行,实时汇报进度
- Shell 脚本在后台默默执行,随时被杀死
- MCP 工具持续监控资源状态
- Remote Agent 连接云端 claude.ai 会话
- Workflow 执行长流程脚本
- Dream 在空闲时生成测试建议
这7种任务类型共存在同一个状态空间 AppState.tasks 中。Tasks 系统就是这个统一管理层。
一、Task.ts:类型骨架
src/Task.ts 定义了所有任务类型的基础类型系统(~120行)。
1.1 七种任务类型
typescript
export type TaskType =
| 'local_bash' // 本地 Shell 命令(b前缀ID)
| 'local_agent' // 本地 Agent 子进程(a前缀ID)
| 'remote_agent' // 远程 claude.ai 会话(r前缀ID)
| 'in_process_teammate' // 同进程 Teammate(t前缀ID)
| 'local_workflow' // 工作流脚本(w前缀ID)
| 'monitor_mcp' // MCP 资源监控(m前缀ID)
| 'dream' // 空闲时做梦(d前缀ID)
每种类型有一个固定单字母前缀,配合8位随机字符组成16字符任务ID(b + a1b2c3d4e5f6g7h)。
1.2 任务基础状态
typescript
export type TaskStateBase = {
id: string
type: TaskType
status: 'pending' | 'running' | 'completed' | 'failed' | 'killed'
description: string
toolUseId?: string
startTime: number
endTime?: number
totalPausedMs?: number
outputFile: string // 磁盘输出文件路径
outputOffset: number // 已读偏移(断点续传)
notified: boolean // 用户通知已发送
}
outputFile + outputOffset 是磁盘持久化核心------每个任务将输出写入独立文件,UI 通过偏移量增量读取,即使任务结束也能回顾完整输出。
1.3 生命周期判断
typescript
export function isTerminalTaskStatus(status: TaskStatus): boolean {
return status === 'completed' || status === 'failed' || status === 'killed'
}
用途:防止向已死亡任务注入消息、触发任务清理、孤儿进程清理路径的前置守卫。
二、types.ts:任务状态联合类型
src/tasks/types.ts 是整个任务系统的类型交汇点:
typescript
export type TaskState =
| LocalShellTaskState
| LocalAgentTaskState
| RemoteAgentTaskState
| InProcessTeammateTaskState
| LocalWorkflowTaskState
| MonitorMcpTaskState
| DreamTaskState
每种具体状态都扩展了 TaskStateBase,添加各自特有的字段。
后台任务过滤
typescript
export function isBackgroundTask(task: TaskState): boolean {
// 必须是 running/pending
if (task.status !== 'running' && task.status !== 'pending') return false
// 前景任务不算后台
if ('isBackgrounded' in task && task.isBackgrounded === false) return false
return true
}
设计意图 :只有被用户主动后台化(isBackgrounded=true)且在运行中的任务才出现在底栏 pill 指示器中。
三、LocalAgentTask:本地 Agent 子进程
本地 Agent 通过 Bun 子进程运行,执行 LLM 对话(与用户自己的 Claude Code 并行)。
核心特性:
- 独立子进程,不阻塞主对话
- 通过
abortController终止 - 持有多轮对话消息历史
- 汇报工具调用次数和 Token 计数
后台化(Ctrl+B × 2)
当用户按两次 Ctrl+B 时,当前主会话被"后台化":
typescript
// LocalMainSessionTask.ts
export function registerMainSessionTask(
description: string,
setAppState: SetAppState,
mainThreadAgentDefinition?: AgentDefinition,
existingAbortController?: AbortController,
): { taskId: string; abortSignal: AbortSignal } {
const taskId = generateMainSessionTaskId()
// 与子 agent 一样,写入独立 transcript 文件
void initTaskOutputAsSymlink(taskId, getAgentTranscriptPath(asAgentId(taskId)))
const taskState: LocalMainSessionTaskState = {
...createTaskStateBase(taskId, 'local_agent', description),
type: 'local_agent',
status: 'running',
isBackgrounded: true, // 关键:标记为后台
retain: false,
pendingMessages: [],
// ...
}
registerTask(taskState, setAppState)
return { taskId, abortSignal: abortController.signal }
}
语义 :后台化的主会话使用与子 Agent 相同的 local_agent 状态结构,区别仅在于 agentType='main-session' 而非 'named-agent'。输出写入独立文件 ,不受 /clear 影响。
完成通知
typescript
export function completeMainSessionTask(
taskId: string,
success: boolean,
setAppState: SetAppState,
): void {
updateTaskState<LocalMainSessionTaskState>(taskId, setAppState, task => {
return {
...task,
status: success ? 'completed' : 'failed',
endTime: Date.now(),
messages: task.messages?.length ? [task.messages.at(-1)!] : undefined,
}
})
// 从 AppState 驱逐,释放内存
void evictTaskOutput(taskId)
}
四、InProcessTeammateTask:同进程 Teammate
Teammate 是 Claude Code 的多 Agent 协作 核心------Leader 可以派生子 Agent(Teammates),每个子 Agent 在同一进程内运行。
Teammate 身份建模
typescript
export type TeammateIdentity = {
agentId: string // "researcher@my-team"
agentName: string // "researcher"
teamName: string
color?: string
planModeRequired: boolean
parentSessionId: string // Leader 的 session ID
}
为什么用 plain data 而不是引用? TeammateContext(运行时)使用 AsyncLocalStorage 存储运行时上下文,但 AppState 需要序列化所有状态以支持刷新恢复。所以 Identity 转为纯数据对象存储。
Teammate 完整状态
typescript
export type InProcessTeammateTaskState = TaskStateBase & {
type: 'in_process_teammate'
identity: TeammateIdentity
// 执行
prompt: string
model?: string
selectedAgent?: AgentDefinition
// 终止信号
abortController?: AbortController
currentWorkAbortController?: AbortController // 只终止当前轮,不杀进程
// Plan mode 审批
awaitingPlanApproval: boolean
// 权限模式(可独立于 Leader 通过 Shift+Tab 切换)
permissionMode: PermissionMode
// 状态
error?: string
result?: AgentToolResult
progress?: AgentProgress
// UI 镜象消息(限最新50条,节省内存)
messages?: Message[]
// 生命周期
isIdle: boolean
shutdownRequested: boolean
// 进度追踪(计算通知差量)
lastReportedToolCount: number
lastReportedTokenCount: number
}
内存保护:消息上限
typescript
// BQ分析(2026-03-20):500+轮对话时每 Agent 占用 ~20MB RSS
// 300个 Agent 并发可达 36.8GB
const TEAMMATE_MESSAGES_UI_CAP = 50
export function appendCappedMessage<T>(prev: readonly T[] | undefined, item: T): T[] {
if (prev === undefined || prev.length === 0) return [item]
if (prev.length >= TEAMMATE_MESSAGES_UI_CAP) {
return [...prev.slice(-(TEAMMATE_MESSAGES_UI_CAP - 1)), item]
}
return [...prev, item]
}
task.messages 是 AppState 中的 UI 镜象 ------真正的完整对话在磁盘文件 getAgentTranscriptPath(agentId) 中。50条上限防止 UI 镜象无限膨胀,而磁盘文件才是真正的历史记录。
五、framework.ts:任务注册与状态更新
src/utils/task/framework.ts 是任务状态操作的工具库(~200行)。
5.1 注册任务
typescript
export function registerTask(task: TaskState, setAppState: SetAppState): void {
setAppState(prev => {
const existing = prev.tasks[task.id]
// 合并策略:保留 UI 状态(retain/messages/diskLoaded/pendingMessages)
// 这样恢复(resumeAgentBackground)不会丢失用户在 UI 中的状态
const merged = existing && 'retain' in existing
? {
...task,
retain: existing.retain,
startTime: existing.startTime,
messages: existing.messages,
diskLoaded: existing.diskLoaded,
pendingMessages: existing.pendingMessages,
}
: task
return { ...prev, tasks: { ...prev.tasks, [task.id]: merged } }
})
// 不是替换(恢复)才发送 SDK 事件
if (!isReplacement) {
enqueueSdkEvent({ type: 'system', subtype: 'task_started', ... })
}
}
合并策略的精妙之处 :恢复一个任务时,UI 中的 retain(用户是否在查看)和 messages(用户新追加的提示词还未落盘)不能丢失。
5.2 状态更新
typescript
export function updateTaskState<T extends TaskState>(
taskId: string,
setAppState: SetAppState,
updater: (task: T) => T,
): void {
setAppState(prev => {
const task = prev.tasks?.[taskId] as T | undefined
if (!task) return prev
const updated = updater(task)
// 返回同一引用 → 跳过更新,防止订阅者不必要重渲染
if (updated === task) return prev
return { ...prev, tasks: { ...prev.tasks, [taskId]: updated } }
})
}
Object.is 比较优化:这是防止 React 不必要重渲染的关键------只有状态真正变化时才触发订阅者更新。
5.3 驱逐终端任务
typescript
export function evictTerminalTask(
taskId: string,
setAppState: SetAppState,
): void {
setAppState(prev => {
const task = prev.tasks?.[taskId]
if (!task) return prev
// 必须处于终态 + 用户已收到通知
if (!isTerminalTaskStatus(task.status)) return prev
if (!task.notified) return prev
// 面板宽限期(30秒),防止 UI 抖动
if ('retain' in task && (task.evictAfter ?? Infinity) > Date.now()) return prev
const { [taskId]: _, ...remaining } = prev.tasks
return { ...prev, tasks: remaining }
})
}
三层守卫:终态 → 已通知 → 宽限期满(或无宽限期)→ 才从内存移除。
六、stopTask.ts:任务停止的统一入口
无论 LLM 调用 /stop 工具还是 SDK 发来 stop_task 控制请求,最终都走同一个 stopTask() 函数:
typescript
export async function stopTask(
taskId: string,
context: StopTaskContext,
): Promise<StopTaskResult> {
const { getAppState, setAppState } = context
const task = appState.tasks?.[taskId]
// 1. 验证存在
if (!task) throw new StopTaskError(..., 'not_found')
// 2. 验证运行中
if (task.status !== 'running') throw new StopTaskError(..., 'not_running')
// 3. 获取任务实现
const taskImpl = getTaskByType(task.type)
if (!taskImpl) throw new StopTaskError(..., 'unsupported_type')
// 4. 杀死任务(多态调用)
await taskImpl.kill(taskId, setAppState)
// 5. Shell 任务:压制退出码 137 噪音通知
// Agent 任务:保留通知(含有 partialResult)
if (isLocalShellTask(task)) {
setAppState(prev => ({
...prev,
tasks: { ...prev.tasks, [taskId]: { ...prev.tasks[taskId], notified: true } }
}))
}
}
多态 kill 模式 :tasks.ts 中注册了每种任务类型的 kill 实现:
typescript
// src/tasks.ts
export function getTaskByType(type: TaskType): Task | undefined {
return getAllTasks().find(t => t.type === type)
}
// Task = { name, type, kill(taskId, setAppState): Promise<void> }
七、pillLabel.ts:底栏标签文本
用户看到的"1 team · ↓ to view"这类文本由 getPillLabel() 生成:
typescript
export function getPillLabel(tasks: BackgroundTaskState[]): string {
const allSameType = tasks.every(t => t.type === tasks[0]!.type)
if (allSameType) {
switch (tasks[0]!.type) {
case 'local_bash':
const shells = count(tasks, t => t.kind !== 'monitor')
const monitors = count(tasks, t => t.kind === 'monitor')
return shells > 0 ? `${shells} shells` : `${monitors} monitors`
case 'in_process_teammate':
const teamCount = new Set(tasks.map(t => t.identity.teamName)).size
return teamCount === 1 ? '1 team' : `${teamCount} teams`
case 'local_agent':
return n === 1 ? '1 local agent' : `${n} local agents`
case 'remote_agent':
if (n === 1 && tasks[0].isUltraplan) {
// Ultraplan 特判:显示 ◇/◆ 钻石状态
return ultraplanPhaseLabel(tasks[0].ultraplanPhase)
}
return `${n} cloud sessions`
case 'dream':
return 'dreaming' // 简洁有趣
}
}
return `${n} background tasks`
}
细节 :dream 任务显示 "dreaming",是唯一使用进行时态的任务类型------暗合 AI 在"空闲时思考"的隐喻。
八、任务 ID 设计与安全性
typescript
// 36进制,8位随机 → 36^8 ≈ 2.8万亿组合
const TASK_ID_ALPHABET = '0123456789abcdefghijklmnopqrstuvwxyz'
export function generateTaskId(type: TaskType): string {
const prefix = TASK_ID_PREFIXS[type] // 'b'/'a'/'r'/'t'/'w'/'m'/'d'
const bytes = randomBytes(8)
return prefix + bytes.map(b => ALPHABET[b % 36]).join('')
}
任务 ID 写入磁盘路径(符号链接到 transcript 文件)。8位随机 + 36进制提供了足够的熵来抵御符号链接攻击(攻击者无法预测路径来覆盖系统文件)。
总结:Tasks 系统的设计哲学
| 维度 | 设计选择 |
|---|---|
| 统一状态 | 7种任务共享 AppState.tasks 字典,类型安全通过联合类型实现 |
| 磁盘持久化 | 每个任务输出写入独立文件 + 偏移量,支持断点续读 |
| UI 镜象 | task.messages 只保留最新50条,防止内存膨胀 |
| 多态 kill | Task 接口统一所有任务类型的 kill 行为 |
| 后台/前景分离 | isBackgrounded 标志区分底栏指示器与前景任务 |
| 驱逐策略 | 三层守卫:终态 → 已通知 → 宽限期满 |
| 恢复保真 | 任务替换时合并 UI 状态(retain/messages),不丢失用户上下文 |
| 内存安全 | 300并发 Agent → 36.8GB 上限通过消息上限和驱逐策略管控 |
Tasks 系统完美体现了 Claude Code 的并发非阻塞哲学:主会话永远可响应,背景任务并行推进,终止机制统一简洁。一套类型骨架 + 7种具体实现 + 1个通用框架 = 整系统的任务编排能力。
下一篇预告 :第二十四篇我们将深入 LocalShellTask,分析 Claude Code 如何在安全的沙箱内执行 Shell 命令,并实现命令超时、终止和输出流式回显。
📚 《Claude Code 源码分析 100 篇》系列
本系列正在持续更新,欢迎点赞、收藏、关注!