Claude Code 源码分析Claude Code 源码分析
首页
源码统计
系统架构
UML 图表
工具系统
CodeGraph
首页
源码统计
系统架构
UML 图表
工具系统
CodeGraph
  • 概览

    • Claude Code 源码分析
    • 源码统计
    • CodeGraph 图谱
  • 架构

    • 系统架构
    • UML 图表索引
    • 查询引擎
    • 核心流程
    • 消息系统
    • 状态管理
  • 功能模块

    • 工具系统
    • 斜杠命令
    • 服务层
    • MCP 协议
    • Skills 技能
    • 子代理系统
  • 分层深度

    • 入口层
    • UI / Ink 层
    • utils 基础设施
    • 桥接 / 远程
    • 上下文压缩
  • 原理与安全

    • 底层原理
    • 技术难点
    • 权限与安全
    • 内部机制
    • 遥测与分析
  • 深度专题

    • Hooks 系统
    • 插件系统
    • 记忆系统
    • API 通信层
    • Ink 终端 UI
    • 认证系统
    • 构建与发布
    • 术语表
  • 调用分析

    • 调用链分析
    • 核心文件索引
  • 模块详解

    • utils

      • 模块: utils
      • messages · 消息工厂与规范化
      • session-storage · JSONL 会话持久化
      • permissions · 工具权限决策
      • shell-hooks · 用户 Shell Hook 系统
    • components

      • 模块: components
      • REPL · 主屏编排
      • messages · 消息行渲染
      • PermissionRequest · 权限弹窗
      • PromptInput · 底部输入
    • services

      • 模块: services
      • api-claude · Anthropic API 流式与重试
      • mcp-client · MCP 连接与工具调用
      • compact · 上下文压缩与自动触发
      • analytics · GrowthBook、Datadog 与 1P 事件
    • tools

      • 模块: tools
      • tool-interface · Tool 契约与注册表
      • bash-tool · Shell 执行与权限
      • streaming-executor · 流式工具并发调度
      • agent-tool · 子 Agent 委派
    • commands

      • 模块: commands
      • command-registry · commands.ts 注册与分派
      • model-command · /model 模型选择
      • mcp-commands · /mcp 服务器管理
      • compact-memory-commands · /compact 与 /memory
    • ink

      • 模块: ink
      • Ink 渲染管线 · Screen 与终端输出
      • 终端事件 · resize、paste、stdin
      • Ink Hooks · 输入、搜索、终端状态
      • Ink 组件 · Box、Text、ScrollBox 原语
    • hooks

      • 模块: hooks
      • useCanUseTool · 权限 UI 接缝
      • 输入与快捷键 Hook
      • 合并态 Hook(MCP + 本地)
      • notifs 通知 Hook
    • bridge

      • 模块: bridge
      • repl-bridge · REPL 桥初始化与传输
      • bridge-messaging · 桥消息路由与入站处理
      • remote-bridge-core · env-less 核心与守护主循环
      • bridge-permissions-ui · 权限、API 与 TUI
    • cli

      • 模块: cli
      • Structured IO · NDJSON SDK 协议
      • CLI Transports · Session Ingress 传输层
      • CLI Handlers · 子命令懒加载实现
      • Update & Upload · 自更新与串行上传原语
    • screens

      • 模块: screens
      • REPL 屏 · Screen 类型与顶层路由
      • ResumeConversation · 会话恢复选择器
      • Doctor · 安装诊断全屏
    • entrypoints

      • 模块: entrypoints
      • cli-entrypoint · Bootstrap 与快路径
      • sdk-types · core / control / runtime 类型体系
      • mcp-entrypoint · MCP stdio 服务器
      • sandbox-types · 沙箱配置单一真相源
    • skills

      • 模块: skills
      • skills-loading · 磁盘加载与 bundled 注册表
      • bundled-skills · 内置 skill 与 initBundledSkills
      • mcp-skills · MCP prompt 转 skill
      • skill-tool-integration · SkillTool 与命令注册
    • types

      • 模块: types
      • message-types · Message 联合与 content blocks
      • tool-permission-types · Tool、Permission、Command 类型
      • api-sdk-types · API 与 Hooks 协议类型
      • misc-types · ids、plugin、generated 与其余类型
    • tasks

      • 模块: tasks
      • local-agent-task · 本地 Agent 与主会话后台化
      • remote-agent-task · 远程 CCR 与 In-Process Teammate
      • shell-workflow-tasks · Bash 后台、Workflow 与 stopTask
      • dream-monitor-tasks · Dream、Monitor MCP 与 pill 文案
    • keybindings

      • 模块: keybindings
      • keybinding-registry · 注册、Provider 与 useKeybinding
      • default-bindings · 默认键位表与平台差异
      • command-bindings · command:* 动态斜杠命令绑定
      • vim-bindings · Vim 模式与 keybindings 边界
    • memdir

      • 模块: memdir
      • memdir-core · 路径、加载与 MEMORY.md
      • memory-extraction · extractMemories 与 SessionMemory
      • memdir-commands · /memory、/remember 与命令集成
    • state

      • 模块: state
      • app-state-core · store、AppState 类型与 Provider
      • app-state-selectors · selectors 与 onChangeAppState
      • teammate-state · 队友视图与 swarm 状态
      • state-boundaries · bootstrap、sessionStorage、FileStateCache
    • query

      • 模块: query
      • query config 与 deps · 配置快照与依赖注入
      • query tokenBudget · +500k 自动续跑
      • query transitions · Continue / Terminal 状态机
      • query stopHooks · Stop 事件与 turn 结束编排
  • 模块详解(扩展)

    • messages · 消息工厂与规范化
    • session-storage · JSONL 会话持久化
    • permissions · 工具权限决策
    • shell-hooks · 用户 Shell Hook 系统
    • REPL · 主屏编排
    • messages · 消息行渲染
    • PermissionRequest · 权限弹窗
    • PromptInput · 底部输入
    • api-claude · Anthropic API 流式与重试
    • mcp-client · MCP 连接与工具调用
    • compact · 上下文压缩与自动触发
    • analytics · GrowthBook、Datadog 与 1P 事件
    • tool-interface · Tool 契约与注册表
    • bash-tool · Shell 执行与权限
    • streaming-executor · 流式工具并发调度
    • agent-tool · 子 Agent 委派
    • command-registry · commands.ts 注册与分派
    • model-command · /model 模型选择
    • mcp-commands · /mcp 服务器管理
    • compact-memory-commands · /compact 与 /memory
    • Ink 渲染管线 · Screen 与终端输出
    • 终端事件 · resize、paste、stdin
    • Ink Hooks · 输入、搜索、终端状态
    • Ink 组件 · Box、Text、ScrollBox 原语
    • useCanUseTool · 权限 UI 接缝
    • 输入与快捷键 Hook
    • 合并态 Hook(MCP + 本地)
    • notifs 通知 Hook
    • repl-bridge · REPL 桥初始化与传输
    • bridge-messaging · 桥消息路由与入站处理
    • remote-bridge-core · env-less 核心与守护主循环
    • bridge-permissions-ui · 权限、API 与 TUI
    • Structured IO · NDJSON SDK 协议
    • CLI Transports · Session Ingress 传输层
    • CLI Handlers · 子命令懒加载实现
    • Update & Upload · 自更新与串行上传原语
    • REPL 屏 · Screen 类型与顶层路由
    • ResumeConversation · 会话恢复选择器
    • Doctor · 安装诊断全屏
    • cli-entrypoint · Bootstrap 与快路径
    • sdk-types · core / control / runtime 类型体系
    • mcp-entrypoint · MCP stdio 服务器
    • sandbox-types · 沙箱配置单一真相源
    • skills-loading · 磁盘加载与 bundled 注册表
    • bundled-skills · 内置 skill 与 initBundledSkills
    • mcp-skills · MCP prompt 转 skill
    • skill-tool-integration · SkillTool 与命令注册
    • message-types · Message 联合与 content blocks
    • tool-permission-types · Tool、Permission、Command 类型
    • api-sdk-types · API 与 Hooks 协议类型
    • misc-types · ids、plugin、generated 与其余类型
    • local-agent-task · 本地 Agent 与主会话后台化
    • remote-agent-task · 远程 CCR 与 In-Process Teammate
    • shell-workflow-tasks · Bash 后台、Workflow 与 stopTask
    • dream-monitor-tasks · Dream、Monitor MCP 与 pill 文案
    • keybinding-registry · 注册、Provider 与 useKeybinding
    • default-bindings · 默认键位表与平台差异
    • command-bindings · command:* 动态斜杠命令绑定
    • vim-bindings · Vim 模式与 keybindings 边界
    • memdir-core · 路径、加载与 MEMORY.md
    • memory-extraction · extractMemories 与 SessionMemory
    • memdir-commands · /memory、/remember 与命令集成
    • app-state-core · store、AppState 类型与 Provider
    • app-state-selectors · selectors 与 onChangeAppState
    • teammate-state · 队友视图与 swarm 状态
    • state-boundaries · bootstrap、sessionStorage、FileStateCache
    • query config 与 deps · 配置快照与依赖注入
    • query tokenBudget · +500k 自动续跑
    • query transitions · Continue / Terminal 状态机
    • query stopHooks · Stop 事件与 turn 结束编排
  • 工具详解

    • tool-interface · Tool 契约与注册表
    • tool-permission-types · Tool、Permission、Command 类型
    • 工具: Bash
    • 工具: PowerShell
    • 工具: Agent
    • 工具: LSP
    • 工具: FileEdit
    • 工具: FileRead
    • 工具: Skill
    • 工具: WebFetch
    • 工具: MCP
    • 工具: SendMessage
    • 工具: FileWrite
    • 工具: Config
    • 工具: Grep
    • 工具: Brief
    • 工具: ExitPlanMode
    • 工具: ToolSearch
    • 工具: NotebookEdit
    • 工具: TaskOutput
    • 工具: WebSearch
    • 工具: ScheduleCron

本章总览

tasks/LocalAgentTask/(约 680 行)与 LocalMainSessionTask.ts(约 480 行)共同承载「本地长运行 Agent」语义:前者服务 AgentTool 的 async/background 子 agent 与 CoordinatorTaskPanel;后者在用户双击 Ctrl+B 或 startBackgroundSession 时把主 query 或独立 query 注册为 type=local_agent 但 agentType=main-session 的后台任务。两者共享 LocalAgentTaskState 形状、diskOutput symlink 布局与 progress 追踪器,但 kill/complete/notification 路径略有分叉。本章要求你能从 registerAsyncAgent 追踪到 completeAgentTask,以及从 registerMainSessionTask 追踪到 completeMainSessionTask 的 XML notification 与 SDK emitTaskTerminatedSdk 分支。

学完本章你应该能

  • 说明 LocalAgentTaskState 各字段(isBackgrounded、retain、pendingMessages、evictAfter)语义
  • 解释 registerAsyncAgent vs registerAgentForeground vs backgroundAgentTask 三阶段生命周期
  • 理解 LocalMainSessionTask 的 s 前缀 taskId 与 isolated transcript 动机
  • 掌握 ProgressTracker、updateProgressFromMessage 与 SDK emitTaskProgress 的衔接
  • 区分 isPanelAgentTask 与 isMainSessionTask 对 pill/panel 路由的影响

核心概念(先读懂这些)

local_agent 是 AgentTool async 的统一落点

AgentTool.call 在 run_in_background / forceAsync 路径调用 registerAsyncAgent,子 query 在 runAgent.ts 内执行。completeAgentTask / failAgentTask 更新 AppState;enqueueAgentNotification 构造带 output_file、usage、worktree 标签的 XML。LocalAgentTask.kill 委托 killAsyncAgent,abortController 级联终止子 query。

main-session 复用 local_agent type

LocalMainSessionTask 故意不引入新 type 字段,而是用 agentType='main-session' 标记。isMainSessionTask 谓词供 UI 与 stopTask 区分;generateMainSessionTaskId 用 s 前缀(agent 用 a)。initTaskOutputAsSymlink 指向 getAgentTranscriptPath(asAgentId(taskId)),避免 /clear 后污染主 transcript。

foreground → background 信号

registerAgentForeground 创建 isBackgrounded=false 的任务与 backgroundSignal Promise。用户 Ctrl+B 或 autoBackgroundMs 超时调用 backgroundAgentTask,resolve backgroundSignalResolvers 中断 AgentTool 同步等待环。Main session 路径在 registerMainSessionTask 直接 isBackgrounded=true。

建议学习步骤

  1. 阅读 types.ts 中 TaskState 联合与 isBackgroundTask
  2. 阅读 LocalAgentTaskState 类型定义与 isPanelAgentTask
  3. 阅读 createProgressTracker / updateProgressFromMessage
  4. 阅读 registerAsyncAgent 与 registerAgentForeground
  5. 阅读 killAsyncAgent、completeAgentTask、enqueueAgentNotification
  6. 阅读 LocalMainSessionTask register/complete/foreground/startBackgroundSession

常见误区

注意

不要把 viewingAgentTaskId(看)与 retain(持有面板)混为一谈

注意

completeAgentTask 不发送 notification——AgentTool 负责 enqueueAgentNotification

注意

parentAbortController 传入时 createChildAbortController,teammate 退出会级联 kill 子 agent

注意

main-session foreground 完成时不发 XML,但仍 emitTaskTerminatedSdk

目录结构与职责

Local Agent 相关文件:

文件职责
LocalAgentTask/LocalAgentTask.tsxTask 实现、状态类型、register/kill/complete、progress
LocalMainSessionTask.ts主会话后台化、startBackgroundSession、foreground
types.tsTaskState 联合、BackgroundTaskState、isBackgroundTask

LocalAgentTask.tsx 同时 export 大量纯函数(updateProgressFromMessage、drainPendingMessages),供 runAgent.ts 与 UI 共用,避免循环依赖。

LocalAgentTaskState 字段语义

LocalAgentTaskState 扩展 TaskStateBase:

  • agentId / prompt / selectedAgent / agentType — 子 agent 身份与 AgentDefinition
  • abortController — kill 时 abort;可 attach parentAbortController 子控制器
  • progress — AgentProgress(toolUseCount、tokenCount、recentActivities、summary)
  • messages — 侧链 transcript 镜像,供 TaskOutput / CoordinatorPanel 展示
  • isBackgrounded — false=foreground(pill 隐藏),true=后台 pill 可见
  • pendingMessages — SendMessage 工具排队,runAgent 在 tool-round 边界 drain
  • retain / diskLoaded / evictAfter — CoordinatorTaskPanel 持有与 GC 宽限期(PANEL_GRACE_MS)
  • lastReportedToolCount / lastReportedTokenCount — SDK progress delta 计算

isPanelAgentTask = local_agent 且 agentType !== 'main-session'。ants Coordinator 模式下 panel agent 不进 background pill。

源码引用: src/tasks/LocalAgentTask/LocalAgentTask.tsx · 第 116–161 行(共 805 行)

 116|     if (content.type === 'tool_use') {
 117|       tracker.toolUseCount++
 118|       // Omit StructuredOutput from preview - it's an internal tool
 119|       if (content.name !== SYNTHETIC_OUTPUT_TOOL_NAME) {
 120|         const input = content.input as Record<string, unknown>
 121|         const classification = tools
 122|           ? getToolSearchOrReadInfo(content.name, input, tools)
 123|           : undefined
 124|         tracker.recentActivities.push({
 125|           toolName: content.name,
 126|           input,
 127|           activityDescription: resolveActivityDescription?.(
 128|             content.name,
 129|             input,
 130|           ),
 131|           isSearch: classification?.isSearch,
 132|           isRead: classification?.isRead,
 133|         })
 134|       }
 135|     }
 136|   }
 137|   while (tracker.recentActivities.length > MAX_RECENT_ACTIVITIES) {
 138|     tracker.recentActivities.shift()
 139|   }
 140| }
 141| 
 142| export function getProgressUpdate(tracker: ProgressTracker): AgentProgress {
 143|   return {
 144|     toolUseCount: tracker.toolUseCount,
 145|     tokenCount: getTokenCountFromTracker(tracker),
 146|     lastActivity:
 147|       tracker.recentActivities.length > 0
 148|         ? tracker.recentActivities[tracker.recentActivities.length - 1]
 149|         : undefined,
 150|     recentActivities: [...tracker.recentActivities],
 151|   }
 152| }
 153| 
 154| /**
 155|  * Creates an ActivityDescriptionResolver from a tools list.
 156|  * Looks up the tool by name and calls getActivityDescription if available.
 157|  */
 158| export function createActivityDescriptionResolver(
 159|   tools: Tools,
 160| ): ActivityDescriptionResolver {
 161|   return (toolName, input) => {

ProgressTracker 与 activity 描述

createProgressTracker 初始化四类计数器。updateProgressFromMessage 仅在 assistant 消息上累加:

  1. input_tokens + cache_* 取最新(API 侧 cumulative input)
  2. output_tokens 累加(per-turn)
  3. tool_use 块递增 toolUseCount,跳过 SYNTHETIC_OUTPUT_TOOL_NAME
  4. recentActivities 环形缓冲 MAX_RECENT_ACTIVITIES=5

createActivityDescriptionResolver(tools) 调用 Tool.getActivityDescription 预计算 UI 文案。getToolSearchOrReadInfo 标记 isSearch/isRead 供折叠 UI。

updateAgentSummary 在 running 时写入 progress.summary;若 getSdkAgentProgressSummariesEnabled() 则 emitTaskProgress 给 VS Code 等 SDK 消费者。

源码引用: src/tasks/LocalAgentTask/LocalAgentTask.tsx · 第 40–115 行(共 805 行)

  40|   updateTaskState,
  41| } from '../../utils/task/framework.js'
  42| import { emitTaskProgress } from '../../utils/task/sdkProgress.js'
  43| import type { TaskState } from '../types.js'
  44| 
  45| export type ToolActivity = {
  46|   toolName: string
  47|   input: Record<string, unknown>
  48|   /** Pre-computed activity description from the tool, e.g. "Reading src/foo.ts" */
  49|   activityDescription?: string
  50|   /** Pre-computed: true if this is a search operation (Grep, Glob, etc.) */
  51|   isSearch?: boolean
  52|   /** Pre-computed: true if this is a read operation (Read, cat, etc.) */
  53|   isRead?: boolean
  54| }
  55| 
  56| export type AgentProgress = {
  57|   toolUseCount: number
  58|   tokenCount: number
  59|   lastActivity?: ToolActivity
  60|   recentActivities?: ToolActivity[]
  61|   summary?: string
  62| }
  63| 
  64| const MAX_RECENT_ACTIVITIES = 5
  65| 
  66| export type ProgressTracker = {
  67|   toolUseCount: number
  68|   // Track input and output separately to avoid double-counting.
  69|   // input_tokens in Claude API is cumulative per turn (includes all previous context),
  70|   // so we keep the latest value. output_tokens is per-turn, so we sum those.
  71|   latestInputTokens: number
  72|   cumulativeOutputTokens: number
  73|   recentActivities: ToolActivity[]
  74| }
  75| 
  76| export function createProgressTracker(): ProgressTracker {
  77|   return {
  78|     toolUseCount: 0,
  79|     latestInputTokens: 0,
  80|     cumulativeOutputTokens: 0,
  81|     recentActivities: [],
  82|   }
  83| }
  84| 
  85| export function getTokenCountFromTracker(tracker: ProgressTracker): number {
  86|   return tracker.latestInputTokens + tracker.cumulativeOutputTokens
  87| }
  88| 
  89| /**
  90|  * Resolver function that returns a human-readable activity description
  91|  * for a given tool name and input. Used to pre-compute descriptions
  92|  * from Tool.getActivityDescription() at recording time.
  93|  */
  94| export type ActivityDescriptionResolver = (
  95|   toolName: string,
  96|   input: Record<string, unknown>,
  97| ) => string | undefined
  98| 
  99| export function updateProgressFromMessage(
 100|   tracker: ProgressTracker,
 101|   message: Message,
 102|   resolveActivityDescription?: ActivityDescriptionResolver,
 103|   tools?: Tools,
 104| ): void {
 105|   if (message.type !== 'assistant') {
 106|     return
 107|   }
 108|   const usage = message.message.usage
 109|   // Keep latest input (it's cumulative in the API), sum outputs
 110|   tracker.latestInputTokens =
 111|     usage.input_tokens +
 112|     (usage.cache_creation_input_tokens ?? 0) +
 113|     (usage.cache_read_input_tokens ?? 0)
 114|   tracker.cumulativeOutputTokens += usage.output_tokens
 115|   for (const content of message.message.content) {

源码引用: src/tasks/LocalAgentTask/LocalAgentTask.tsx · 第 355–407 行(共 805 行)

 355|   name: 'LocalAgentTask',
 356|   type: 'local_agent',
 357| 
 358|   async kill(taskId, setAppState) {
 359|     killAsyncAgent(taskId, setAppState)
 360|   },
 361| }
 362| 
 363| /**
 364|  * Kill an agent task. No-op if already killed/completed.
 365|  */
 366| export function killAsyncAgent(taskId: string, setAppState: SetAppState): void {
 367|   let killed = false
 368|   updateTaskState<LocalAgentTaskState>(taskId, setAppState, task => {
 369|     if (task.status !== 'running') {
 370|       return task
 371|     }
 372|     killed = true
 373|     task.abortController?.abort()
 374|     task.unregisterCleanup?.()
 375|     return {
 376|       ...task,
 377|       status: 'killed',
 378|       endTime: Date.now(),
 379|       evictAfter: task.retain ? undefined : Date.now() + PANEL_GRACE_MS,
 380|       abortController: undefined,
 381|       unregisterCleanup: undefined,
 382|       selectedAgent: undefined,
 383|     }
 384|   })
 385|   if (killed) {
 386|     void evictTaskOutput(taskId)
 387|   }
 388| }
 389| 
 390| /**
 391|  * Kill all running agent tasks.
 392|  * Used by ESC cancellation in coordinator mode to stop all subagents.
 393|  */
 394| export function killAllRunningAgentTasks(
 395|   tasks: Record<string, TaskState>,
 396|   setAppState: SetAppState,
 397| ): void {
 398|   for (const [taskId, task] of Object.entries(tasks)) {
 399|     if (task.type === 'local_agent' && task.status === 'running') {
 400|       killAsyncAgent(taskId, setAppState)
 401|     }
 402|   }
 403| }
 404| 
 405| /**
 406|  * Mark a task as notified without enqueueing a notification.
 407|  * Used by chat:killAgents bulk kill to suppress per-agent async notifications

registerAsyncAgent 与 foreground 路径

registerAsyncAgent(AgentTool async 入口):

  1. initTaskOutputAsSymlink(agentId, getAgentTranscriptPath)
  2. abortController = parent ? createChildAbortController(parent) : createAbortController()
  3. createTaskStateBase(agentId, 'local_agent', description, toolUseId)
  4. isBackgrounded: true(async 立即后台)
  5. registerCleanup → killAsyncAgent
  6. registerTask → AppState.tasks

registerAgentForeground 用于长时间 sync agent:isBackgrounded: false,返回 backgroundSignal Promise + 可选 autoBackgroundMs 定时器。backgroundAgentTask flip isBackgrounded 并 resolve signal。

queuePendingMessage / appendMessageToLocalAgent / drainPendingMessages 支持 teammate SendMessage 与 zoom 视图即时显示。

源码引用: src/tasks/LocalAgentTask/LocalAgentTask.tsx · 第 466–515 行(共 805 行)

 466|   updateTaskState<LocalAgentTaskState>(taskId, setAppState, task => {
 467|     if (task.status !== 'running') {
 468|       return task
 469|     }
 470| 
 471|     captured = {
 472|       tokenCount: task.progress?.tokenCount ?? 0,
 473|       toolUseCount: task.progress?.toolUseCount ?? 0,
 474|       startTime: task.startTime,
 475|       toolUseId: task.toolUseId,
 476|     }
 477| 
 478|     return {
 479|       ...task,
 480|       progress: {
 481|         ...task.progress,
 482|         toolUseCount: task.progress?.toolUseCount ?? 0,
 483|         tokenCount: task.progress?.tokenCount ?? 0,
 484|         summary,
 485|       },
 486|     }
 487|   })
 488| 
 489|   // Emit summary to SDK consumers (e.g. VS Code subagent panel). No-op in TUI.
 490|   // Gate on the SDK option so coordinator-mode sessions without the flag don't
 491|   // leak summary events to consumers who didn't opt in.
 492|   if (captured && getSdkAgentProgressSummariesEnabled()) {
 493|     const { tokenCount, toolUseCount, startTime, toolUseId } = captured
 494|     emitTaskProgress({
 495|       taskId,
 496|       toolUseId,
 497|       description: summary,
 498|       startTime,
 499|       totalTokens: tokenCount,
 500|       toolUses: toolUseCount,
 501|       summary,
 502|     })
 503|   }
 504| }
 505| 
 506| /**
 507|  * Complete an agent task with result.
 508|  */
 509| export function completeAgentTask(
 510|   result: AgentToolResult,
 511|   setAppState: SetAppState,
 512| ): void {
 513|   const taskId = result.agentId
 514|   updateTaskState<LocalAgentTaskState>(taskId, setAppState, task => {
 515|     if (task.status !== 'running') {

源码引用: src/tasks/LocalAgentTask/LocalAgentTask.tsx · 第 526–614 行(共 805 行)

 526|       evictAfter: task.retain ? undefined : Date.now() + PANEL_GRACE_MS,
 527|       abortController: undefined,
 528|       unregisterCleanup: undefined,
 529|       selectedAgent: undefined,
 530|     }
 531|   })
 532|   void evictTaskOutput(taskId)
 533|   // Note: Notification is sent by AgentTool via enqueueAgentNotification
 534| }
 535| 
 536| /**
 537|  * Fail an agent task with error.
 538|  */
 539| export function failAgentTask(
 540|   taskId: string,
 541|   error: string,
 542|   setAppState: SetAppState,
 543| ): void {
 544|   updateTaskState<LocalAgentTaskState>(taskId, setAppState, task => {
 545|     if (task.status !== 'running') {
 546|       return task
 547|     }
 548| 
 549|     task.unregisterCleanup?.()
 550| 
 551|     return {
 552|       ...task,
 553|       status: 'failed',
 554|       error,
 555|       endTime: Date.now(),
 556|       evictAfter: task.retain ? undefined : Date.now() + PANEL_GRACE_MS,
 557|       abortController: undefined,
 558|       unregisterCleanup: undefined,
 559|       selectedAgent: undefined,
 560|     }
 561|   })
 562|   void evictTaskOutput(taskId)
 563|   // Note: Notification is sent by AgentTool via enqueueAgentNotification
 564| }
 565| 
 566| /**
 567|  * Register an agent task.
 568|  * Called by AgentTool to create a new background agent.
 569|  *
 570|  * @param parentAbortController - Optional parent abort controller. If provided,
 571|  *   the agent's abort controller will be a child that auto-aborts when parent aborts.
 572|  *   This ensures subagents are aborted when their parent (e.g., in-process teammate) aborts.
 573|  */
 574| export function registerAsyncAgent({
 575|   agentId,
 576|   description,
 577|   prompt,
 578|   selectedAgent,
 579|   setAppState,
 580|   parentAbortController,
 581|   toolUseId,
 582| }: {
 583|   agentId: string
 584|   description: string
 585|   prompt: string
 586|   selectedAgent: AgentDefinition
 587|   setAppState: SetAppState
 588|   parentAbortController?: AbortController
 589|   toolUseId?: string
 590| }): LocalAgentTaskState {
 591|   void initTaskOutputAsSymlink(
 592|     agentId,
 593|     getAgentTranscriptPath(asAgentId(agentId)),
 594|   )
 595| 
 596|   // Create abort controller - if parent provided, create child that auto-aborts with parent
 597|   const abortController = parentAbortController
 598|     ? createChildAbortController(parentAbortController)
 599|     : createAbortController()
 600| 
 601|   const taskState: LocalAgentTaskState = {
 602|     ...createTaskStateBase(agentId, 'local_agent', description, toolUseId),
 603|     type: 'local_agent',
 604|     status: 'running',
 605|     agentId,
 606|     prompt,
 607|     selectedAgent,
 608|     agentType: selectedAgent.agentType ?? 'general-purpose',
 609|     abortController,
 610|     retrieved: false,
 611|     lastReportedToolCount: 0,
 612|     lastReportedTokenCount: 0,
 613|     isBackgrounded: true, // registerAsyncAgent immediately backgrounds
 614|     pendingMessages: [],

源码引用: src/tasks/LocalAgentTask/LocalAgentTask.tsx · 第 620–649 行(共 805 行)

 620|   const unregisterCleanup = registerCleanup(async () => {
 621|     killAsyncAgent(agentId, setAppState)
 622|   })
 623| 
 624|   taskState.unregisterCleanup = unregisterCleanup
 625| 
 626|   // Register task in AppState
 627|   registerTask(taskState, setAppState)
 628| 
 629|   return taskState
 630| }
 631| 
 632| // Map of taskId -> resolve function for background signals
 633| // When backgroundAgentTask is called, it resolves the corresponding promise
 634| const backgroundSignalResolvers = new Map<string, () => void>()
 635| 
 636| /**
 637|  * Register a foreground agent task that could be backgrounded later.
 638|  * Called when an agent has been running long enough to show the BackgroundHint.
 639|  * @returns object with taskId and backgroundSignal promise
 640|  */
 641| export function registerAgentForeground({
 642|   agentId,
 643|   description,
 644|   prompt,
 645|   selectedAgent,
 646|   setAppState,
 647|   autoBackgroundMs,
 648|   toolUseId,
 649| }: {

kill / complete / notification

killAsyncAgent:status→killed,abort,unregisterCleanup,evictAfter=PANEL_GRACE_MS(除非 retain),evictTaskOutput。

completeAgentTask / failAgentTask:terminal 状态,清 abortController/selectedAgent,evictTaskOutput。Notification 由 AgentTool 调用 enqueueAgentNotification(含 result、usage、worktree XML 段)。

enqueueAgentNotification 原子设置 notified,abortSpeculation(setAppState),构造 TASK_NOTIFICATION_TAG XML。

killAllRunningAgentTasks — coordinator ESC 批量终止。markAgentsNotified — chat:killAgents 抑制逐条 async 通知。

源码引用: src/tasks/LocalAgentTask/LocalAgentTask.tsx · 第 197–262 行(共 805 行)

 197|   // and on unselect; cleared on retain.
 198|   evictAfter?: number
 199| }
 200| 
 201| export function isLocalAgentTask(task: unknown): task is LocalAgentTaskState {
 202|   return (
 203|     typeof task === 'object' &&
 204|     task !== null &&
 205|     'type' in task &&
 206|     task.type === 'local_agent'
 207|   )
 208| }
 209| 
 210| /**
 211|  * A local_agent task that the CoordinatorTaskPanel manages (not main-session).
 212|  * For ants, these render in the panel instead of the background-task pill.
 213|  * This is the ONE predicate that all pill/panel filters must agree on — if
 214|  * the gate changes, change it here.
 215|  */
 216| export function isPanelAgentTask(t: unknown): t is LocalAgentTaskState {
 217|   return isLocalAgentTask(t) && t.agentType !== 'main-session'
 218| }
 219| 
 220| export function queuePendingMessage(
 221|   taskId: string,
 222|   msg: string,
 223|   setAppState: (f: (prev: AppState) => AppState) => void,
 224| ): void {
 225|   updateTaskState<LocalAgentTaskState>(taskId, setAppState, task => ({
 226|     ...task,
 227|     pendingMessages: [...task.pendingMessages, msg],
 228|   }))
 229| }
 230| 
 231| /**
 232|  * Append a message to task.messages so it appears in the viewed transcript
 233|  * immediately. Caller constructs the Message (breaks the messages.ts cycle).
 234|  * queuePendingMessage and resumeAgentBackground route the prompt to the
 235|  * agent's API input but don't touch the display.
 236|  */
 237| export function appendMessageToLocalAgent(
 238|   taskId: string,
 239|   message: Message,
 240|   setAppState: (f: (prev: AppState) => AppState) => void,
 241| ): void {
 242|   updateTaskState<LocalAgentTaskState>(taskId, setAppState, task => ({
 243|     ...task,
 244|     messages: [...(task.messages ?? []), message],
 245|   }))
 246| }
 247| 
 248| export function drainPendingMessages(
 249|   taskId: string,
 250|   getAppState: () => AppState,
 251|   setAppState: (f: (prev: AppState) => AppState) => void,
 252| ): string[] {
 253|   const task = getAppState().tasks[taskId]
 254|   if (!isLocalAgentTask(task) || task.pendingMessages.length === 0) {
 255|     return []
 256|   }
 257|   const drained = task.pendingMessages
 258|   updateTaskState<LocalAgentTaskState>(taskId, setAppState, t => ({
 259|     ...t,
 260|     pendingMessages: [],
 261|   }))
 262|   return drained

源码引用: src/tasks/LocalAgentTask/LocalAgentTask.tsx · 第 281–303 行(共 805 行)

 281|   description: string
 282|   status: 'completed' | 'failed' | 'killed'
 283|   error?: string
 284|   setAppState: SetAppState
 285|   finalMessage?: string
 286|   usage?: {
 287|     totalTokens: number
 288|     toolUses: number
 289|     durationMs: number
 290|   }
 291|   toolUseId?: string
 292|   worktreePath?: string
 293|   worktreeBranch?: string
 294| }): void {
 295|   // Atomically check and set notified flag to prevent duplicate notifications.
 296|   // If the task was already marked as notified (e.g., by TaskStopTool), skip
 297|   // enqueueing to avoid sending redundant messages to the model.
 298|   let shouldEnqueue = false
 299|   updateTaskState<LocalAgentTaskState>(taskId, setAppState, task => {
 300|     if (task.notified) {
 301|       return task
 302|     }
 303|     shouldEnqueue = true

源码引用: src/tasks/LocalAgentTask/LocalAgentTask.tsx · 第 412–456 行(共 805 行)

 412|   setAppState: SetAppState,
 413| ): void {
 414|   updateTaskState<LocalAgentTaskState>(taskId, setAppState, task => {
 415|     if (task.notified) {
 416|       return task
 417|     }
 418|     return {
 419|       ...task,
 420|       notified: true,
 421|     }
 422|   })
 423| }
 424| 
 425| /**
 426|  * Update progress for an agent task.
 427|  * Preserves the existing summary field so that background summarization
 428|  * results are not clobbered by progress updates from assistant messages.
 429|  */
 430| export function updateAgentProgress(
 431|   taskId: string,
 432|   progress: AgentProgress,
 433|   setAppState: SetAppState,
 434| ): void {
 435|   updateTaskState<LocalAgentTaskState>(taskId, setAppState, task => {
 436|     if (task.status !== 'running') {
 437|       return task
 438|     }
 439| 
 440|     const existingSummary = task.progress?.summary
 441|     return {
 442|       ...task,
 443|       progress: existingSummary
 444|         ? { ...progress, summary: existingSummary }
 445|         : progress,
 446|     }
 447|   })
 448| }
 449| 
 450| /**
 451|  * Update the background summary for an agent task.
 452|  * Called by the periodic summarization service to store a 1-2 sentence progress summary.
 453|  */
 454| export function updateAgentSummary(
 455|   taskId: string,
 456|   summary: string,

LocalMainSessionTask 生命周期

registerMainSessionTask(Ctrl+B 后台当前 query):

  • taskId = generateMainSessionTaskId()(s 前缀 8 字节 base36)
  • initTaskOutputAsSymlink → isolated agent transcript(非 getTranscriptPath 主文件)
  • 复用 existingAbortController(关键:abort 实际 query 而非新建)
  • agentType: 'main-session',isBackgrounded: true

completeMainSessionTask:terminal 后若 wasBackgrounded 则 enqueueMainSessionNotification(XML);否则仅 notified + emitTaskTerminatedSdk。

foregroundMainSessionTask:设置 foregroundedTaskId,恢复先前 foreground agent 到 background,返回 task.messages。

startBackgroundSession:独立 query() 循环,runWithAgentContext 隔离 skill scope,逐 message recordSidechainTranscript,abort 时 emitTaskTerminatedSdk('stopped')。

源码引用: src/tasks/LocalMainSessionTask.ts · 第 94–162 行(共 480 行)

  94| export function registerMainSessionTask(
  95|   description: string,
  96|   setAppState: SetAppState,
  97|   mainThreadAgentDefinition?: AgentDefinition,
  98|   existingAbortController?: AbortController,
  99| ): { taskId: string; abortSignal: AbortSignal } {
 100|   const taskId = generateMainSessionTaskId()
 101| 
 102|   // Link output to an isolated per-task transcript file (same layout as
 103|   // sub-agents). Do NOT use getTranscriptPath() — that's the main session's
 104|   // file, and writing there from a background query after /clear would corrupt
 105|   // the post-clear conversation. The isolated path lets this task survive
 106|   // /clear: the symlink re-link in clearConversation handles session ID changes.
 107|   void initTaskOutputAsSymlink(
 108|     taskId,
 109|     getAgentTranscriptPath(asAgentId(taskId)),
 110|   )
 111| 
 112|   // Use the existing abort controller if provided (important for backgrounding an active query)
 113|   // This ensures that aborting the task will abort the actual query
 114|   const abortController = existingAbortController ?? createAbortController()
 115| 
 116|   const unregisterCleanup = registerCleanup(async () => {
 117|     // Clean up on process exit
 118|     setAppState(prev => {
 119|       const { [taskId]: removed, ...rest } = prev.tasks
 120|       return { ...prev, tasks: rest }
 121|     })
 122|   })
 123| 
 124|   // Use provided agent definition or default
 125|   const selectedAgent = mainThreadAgentDefinition ?? DEFAULT_MAIN_SESSION_AGENT
 126| 
 127|   // Create task state - already backgrounded since this is called when user backgrounds
 128|   const taskState: LocalMainSessionTaskState = {
 129|     ...createTaskStateBase(taskId, 'local_agent', description),
 130|     type: 'local_agent',
 131|     status: 'running',
 132|     agentId: taskId,
 133|     prompt: description,
 134|     selectedAgent,
 135|     agentType: 'main-session',
 136|     abortController,
 137|     unregisterCleanup,
 138|     retrieved: false,
 139|     lastReportedToolCount: 0,
 140|     lastReportedTokenCount: 0,
 141|     isBackgrounded: true, // Already backgrounded
 142|     pendingMessages: [],
 143|     retain: false,
 144|     diskLoaded: false,
 145|   }
 146| 
 147|   logForDebugging(
 148|     `[LocalMainSessionTask] Registering task ${taskId} with description: ${description}`,
 149|   )
 150|   registerTask(taskState, setAppState)
 151| 
 152|   // Verify task was registered by checking state
 153|   setAppState(prev => {
 154|     const hasTask = taskId in prev.tasks
 155|     logForDebugging(
 156|       `[LocalMainSessionTask] After registration, task ${taskId} exists in state: ${hasTask}`,
 157|     )
 158|     return prev
 159|   })
 160| 
 161|   return { taskId, abortSignal: abortController.signal }
 162| }

源码引用: src/tasks/LocalMainSessionTask.ts · 第 168–219 行(共 480 行)

 168| export function completeMainSessionTask(
 169|   taskId: string,
 170|   success: boolean,
 171|   setAppState: SetAppState,
 172| ): void {
 173|   let wasBackgrounded = true
 174|   let toolUseId: string | undefined
 175| 
 176|   updateTaskState<LocalMainSessionTaskState>(taskId, setAppState, task => {
 177|     if (task.status !== 'running') {
 178|       return task
 179|     }
 180| 
 181|     // Track if task was backgrounded (for notification decision)
 182|     wasBackgrounded = task.isBackgrounded ?? true
 183|     toolUseId = task.toolUseId
 184| 
 185|     task.unregisterCleanup?.()
 186| 
 187|     return {
 188|       ...task,
 189|       status: success ? 'completed' : 'failed',
 190|       endTime: Date.now(),
 191|       messages: task.messages?.length ? [task.messages.at(-1)!] : undefined,
 192|     }
 193|   })
 194| 
 195|   void evictTaskOutput(taskId)
 196| 
 197|   // Only send notification if task is still backgrounded (not foregrounded)
 198|   // If foregrounded, user is watching it directly - no notification needed
 199|   if (wasBackgrounded) {
 200|     enqueueMainSessionNotification(
 201|       taskId,
 202|       'Background session',
 203|       success ? 'completed' : 'failed',
 204|       setAppState,
 205|       toolUseId,
 206|     )
 207|   } else {
 208|     // Foregrounded: no XML notification (TUI user is watching), but SDK
 209|     // consumers still need to see the task_started bookend close.
 210|     // Set notified so evictTerminalTask/generateTaskAttachments eviction
 211|     // guards pass; the backgrounded path sets this inside
 212|     // enqueueMainSessionNotification's check-and-set.
 213|     updateTaskState(taskId, setAppState, task => ({ ...task, notified: true }))
 214|     emitTaskTerminatedSdk(taskId, success ? 'completed' : 'failed', {
 215|       toolUseId,
 216|       summary: 'Background session',
 217|     })
 218|   }
 219| }

源码引用: src/tasks/LocalMainSessionTask.ts · 第 270–302 行(共 480 行)

 270| export function foregroundMainSessionTask(
 271|   taskId: string,
 272|   setAppState: SetAppState,
 273| ): Message[] | undefined {
 274|   let taskMessages: Message[] | undefined
 275| 
 276|   setAppState(prev => {
 277|     const task = prev.tasks[taskId]
 278|     if (!task || task.type !== 'local_agent') {
 279|       return prev
 280|     }
 281| 
 282|     taskMessages = (task as LocalMainSessionTaskState).messages
 283| 
 284|     // Restore previous foregrounded task to background if it exists
 285|     const prevId = prev.foregroundedTaskId
 286|     const prevTask = prevId ? prev.tasks[prevId] : undefined
 287|     const restorePrev =
 288|       prevId && prevId !== taskId && prevTask?.type === 'local_agent'
 289| 
 290|     return {
 291|       ...prev,
 292|       foregroundedTaskId: taskId,
 293|       tasks: {
 294|         ...prev.tasks,
 295|         ...(restorePrev && { [prevId]: { ...prevTask, isBackgrounded: true } }),
 296|         [taskId]: { ...task, isBackgrounded: false },
 297|       },
 298|     }
 299|   })
 300| 
 301|   return taskMessages
 302| }

源码引用: src/tasks/LocalMainSessionTask.ts · 第 338–400 行(共 480 行)

 338| export function startBackgroundSession({
 339|   messages,
 340|   queryParams,
 341|   description,
 342|   setAppState,
 343|   agentDefinition,
 344| }: {
 345|   messages: Message[]
 346|   queryParams: Omit<QueryParams, 'messages'>
 347|   description: string
 348|   setAppState: SetAppState
 349|   agentDefinition?: AgentDefinition
 350| }): string {
 351|   const { taskId, abortSignal } = registerMainSessionTask(
 352|     description,
 353|     setAppState,
 354|     agentDefinition,
 355|   )
 356| 
 357|   // Persist the pre-backgrounding conversation to the task's isolated
 358|   // transcript so TaskOutput shows context immediately. Subsequent messages
 359|   // are written incrementally below.
 360|   void recordSidechainTranscript(messages, taskId).catch(err =>
 361|     logForDebugging(`bg-session initial transcript write failed: ${err}`),
 362|   )
 363| 
 364|   // Wrap in agent context so skill invocations scope to this task's agentId
 365|   // (not null). This lets clearInvokedSkills(preservedAgentIds) selectively
 366|   // preserve this task's skills across /clear. AsyncLocalStorage isolates
 367|   // concurrent async chains — this wrapper doesn't affect the foreground.
 368|   const agentContext: SubagentContext = {
 369|     agentId: taskId,
 370|     agentType: 'subagent',
 371|     subagentName: 'main-session',
 372|     isBuiltIn: true,
 373|   }
 374| 
 375|   void runWithAgentContext(agentContext, async () => {
 376|     try {
 377|       const bgMessages: Message[] = [...messages]
 378|       const recentActivities: ToolActivity[] = []
 379|       let toolCount = 0
 380|       let tokenCount = 0
 381|       let lastRecordedUuid: UUID | null = messages.at(-1)?.uuid ?? null
 382| 
 383|       for await (const event of query({
 384|         messages: bgMessages,
 385|         ...queryParams,
 386|       })) {
 387|         if (abortSignal.aborted) {
 388|           // Aborted mid-stream — completeMainSessionTask won't be reached.
 389|           // chat:killAgents path already marked notified + emitted; stopTask path did not.
 390|           let alreadyNotified = false
 391|           updateTaskState(taskId, setAppState, task => {
 392|             alreadyNotified = task.notified === true
 393|             return alreadyNotified ? task : { ...task, notified: true }
 394|           })
 395|           if (!alreadyNotified) {
 396|             emitTaskTerminatedSdk(taskId, 'stopped', {
 397|               summary: description,
 398|             })
 399|           }
 400|           return

types.ts 与 background 判定

TaskState 联合七种具体状态。BackgroundTaskState 与之相同集合,专供 pill UI。

isBackgroundTask(task) 逻辑:

  1. status 必须是 running 或 pending
  2. 若存在 isBackgrounded 字段且 === false,返回 false(foreground 不算 background pill)

这与 LocalShellTask、RemoteAgentTask 的 isBackgrounded 语义一致:用户显式 foreground 的任务不出现在 footer pill,但仍占用 AppState.tasks 条目。

阅读 AgentTool async 路径时对照 types.ts 与 isPanelAgentTask,避免把 coordinator panel agent 误当作 pill 任务。

源码引用: src/tasks/types.ts · 第 12–46 行(共 47 行)

  12| export type TaskState =
  13|   | LocalShellTaskState
  14|   | LocalAgentTaskState
  15|   | RemoteAgentTaskState
  16|   | InProcessTeammateTaskState
  17|   | LocalWorkflowTaskState
  18|   | MonitorMcpTaskState
  19|   | DreamTaskState
  20| 
  21| // Task types that can appear in the background tasks indicator
  22| export type BackgroundTaskState =
  23|   | LocalShellTaskState
  24|   | LocalAgentTaskState
  25|   | RemoteAgentTaskState
  26|   | InProcessTeammateTaskState
  27|   | LocalWorkflowTaskState
  28|   | MonitorMcpTaskState
  29|   | DreamTaskState
  30| 
  31| /**
  32|  * Check if a task should be shown in the background tasks indicator.
  33|  * A task is considered a background task if:
  34|  * 1. It is running or pending
  35|  * 2. It has been explicitly backgrounded (not a foreground task)
  36|  */
  37| export function isBackgroundTask(task: TaskState): task is BackgroundTaskState {
  38|   if (task.status !== 'running' && task.status !== 'pending') {
  39|     return false
  40|   }
  41|   // Foreground tasks (isBackgrounded === false) are not yet "background tasks"
  42|   if ('isBackgrounded' in task && task.isBackgrounded === false) {
  43|     return false
  44|   }
  45|   return true
  46| }

源码引用: src/tasks/LocalMainSessionTask.ts · 第 307–322 行(共 480 行)

 307| export function isMainSessionTask(
 308|   task: unknown,
 309| ): task is LocalMainSessionTaskState {
 310|   if (
 311|     typeof task !== 'object' ||
 312|     task === null ||
 313|     !('type' in task) ||
 314|     !('agentType' in task)
 315|   ) {
 316|     return false
 317|   }
 318|   return (
 319|     task.type === 'local_agent' &&
 320|     (task as LocalMainSessionTaskState).agentType === 'main-session'
 321|   )
 322| }

调试清单

症状检查点
async agent 无 pillisBackgrounded 是否为 true;isPanelAgentTask 是否 true(panel 路由)
Ctrl+B 后 query 仍占屏foregroundedTaskId 是否清空;complete 是否调用
重复 task-notificationnotified 标志;enqueue 前原子 check-and-set
progress summary 丢失updateAgentProgress 是否 preserve existingSummary
/clear 后后台任务丢上下文symlink re-link;recordSidechainTranscript 是否写 isolated path

Coordinator 模式:killAllRunningAgentTasks + markAgentsNotified 与 aggregate 消息配合,避免 N 条 XML 轰炸主模型。

本章小结与延伸

LocalAgentTask = 本地子 agent 状态机 + 进度追踪 + 通知 XML。LocalMainSessionTask = 同一状态机的「主线程后台化」变体。下一章 remote-agent-task 读 CCR 远程会话。 继续学习:

  • remote-agent-task
  • shell-workflow-tasks
Prev
模块: tasks
Next
remote-agent-task · 远程 CCR 与 In-Process Teammate