本章总览
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。
建议学习步骤
- 阅读 types.ts 中 TaskState 联合与 isBackgroundTask
- 阅读 LocalAgentTaskState 类型定义与 isPanelAgentTask
- 阅读 createProgressTracker / updateProgressFromMessage
- 阅读 registerAsyncAgent 与 registerAgentForeground
- 阅读 killAsyncAgent、completeAgentTask、enqueueAgentNotification
- 阅读 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.tsx | Task 实现、状态类型、register/kill/complete、progress |
| LocalMainSessionTask.ts | 主会话后台化、startBackgroundSession、foreground |
| types.ts | TaskState 联合、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 消息上累加:
- input_tokens + cache_* 取最新(API 侧 cumulative input)
- output_tokens 累加(per-turn)
- tool_use 块递增 toolUseCount,跳过 SYNTHETIC_OUTPUT_TOOL_NAME
- 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 入口):
- initTaskOutputAsSymlink(agentId, getAgentTranscriptPath)
- abortController = parent ? createChildAbortController(parent) : createAbortController()
- createTaskStateBase(agentId, 'local_agent', description, toolUseId)
- isBackgrounded: true(async 立即后台)
- registerCleanup → killAsyncAgent
- 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) 逻辑:
- status 必须是 running 或 pending
- 若存在 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 无 pill | isBackgrounded 是否为 true;isPanelAgentTask 是否 true(panel 路由) |
| Ctrl+B 后 query 仍占屏 | foregroundedTaskId 是否清空;complete 是否调用 |
| 重复 task-notification | notified 标志;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 远程会话。 继续学习: