本章总览
tools/AgentTool/ 实现 Agent(别名 Task)工具:主线程模型将子任务委派给独立 query 循环。核心文件 AgentTool.tsx(1300+ 行)处理输入 schema、teammate spawn、fork 实验、worktree/remote 隔离与 sync/async 生命周期;runAgent.ts 创建子 context 并调用 query();loadAgentsDir.ts 加载 .claude/agents 定义。本章要求你能从一次 Agent tool_use 追踪到 runAgent → query 递归、以及 async 后台任务的输出文件路径。
学完本章你应该能
- 说明 AgentToolInput schema 各字段与 feature gate 的 omit 关系
- 解释 sync vs async_launched 输出分支
- 理解 createSubagentContext 如何收窄 tools 与权限
- 掌握 teammate spawn(name + team_name)与 fork 路径差异
- 能在 assembleToolPool 语境下理解子 agent 工具池过滤
核心概念(先读懂这些)
Agent 是嵌套 query
AgentTool.call 最终调用 runAgent → query(),形成递归。子 context 有独立 agentId、messages 子集、fileStateCache 克隆、sidechain transcript。forceAsync / run_in_background 走 LocalAgentTask 注册,主线程立即返回 async_launched。同步路径在 call 内 await 子 query 完成后返回 completed status。
工具池在子 agent 侧过滤
constants/tools.ts 定义 ALL_AGENT_DISALLOWED_TOOLS 等;runAgent 内 filterToolsForAgent 移除 Agent 自身(防无限递归)及策略禁止工具。coordinator 模式 worker 仅保留 COORDINATOR_MODE_ALLOWED_TOOLS。assembleToolPool 保证与 REPL 一致的内置+MCP 合并后再过滤。
Fork 与 general-purpose 路由
isForkSubagentEnabled 时 omit subagent_type 走 FORK_AGENT(共享父 system prompt cache)。fork 子 agent 内再次 Agent(fork) 被 querySource / isInForkChild 拒绝。legacy 路径 default subagent_type 为 general-purpose。
建议学习步骤
- 阅读 constants.ts 工具名与 ONE_SHOT 类型
- 阅读 inputSchema lazySchema 与 feature omit
- 阅读 call() 入口:teammate / fork / agent 选择
- 阅读 runAgent.ts 子 context 创建
- 阅读 async 分支与 LocalAgentTask 注册
- 对照 UI.tsx 进度与 grouped 渲染
常见误区
注意
in-process teammate 不能 spawn background agent 或 nested teammate
注意
requiredMcpServers 需等待 pending MCP 连接(最多 30s poll)
注意
Agent deny 规则语法 Agent(Explore) 与工具名 Task 别名并存
目录结构与职责
AgentTool/ 核心文件:
| 文件 | 职责 |
|---|---|
| AgentTool.tsx | buildTool 主实现、call 路由、schema |
| runAgent.ts | 子 query 入口、context 克隆、tool 过滤 |
| agentToolUtils.ts | async 生命周期、handoff、finalize |
| loadAgentsDir.ts | 磁盘 agent 定义、built-in agents |
| forkSubagent.ts | FORK_AGENT 实验、cache-safe prompt |
| prompt.ts | 动态 agent 列表 prompt |
| UI.tsx | 进度、grouped Agent 行、remote 状态 |
| constants.ts | AGENT_TOOL_NAME、LEGACY_AGENT_TOOL_NAME |
AgentTool maxResultSizeChars=100_000,高于 Bash。searchHint: delegate work to a subagent。
工具名与常量
constants.ts 定义:
- AGENT_TOOL_NAME = 'Agent' — 当前 wire name
- LEGACY_AGENT_TOOL_NAME = 'Task' — 权限规则、hooks、resume 会话兼容
- ONE_SHOT_BUILTIN_AGENT_TYPES — Explore、Plan 等一次性 agent,省略 agentId trailer 省 token
AgentTool 声明 aliases: [LEGACY_AGENT_TOOL_NAME],findToolByName 双名命中。
permissions 中 Agent(Explore) deny 规则通过 filterDeniedAgents / getDenyRuleForAgent 生效,call 内 selectedAgent 找不到时检查 agentExistsButDenied 抛明确错误。
源码引用: src/tools/AgentTool/constants.ts · 第 1–12 行(共 13 行)
1| export const AGENT_TOOL_NAME = 'Agent'
2| // Legacy wire name for backward compat (permission rules, hooks, resumed sessions)
3| export const LEGACY_AGENT_TOOL_NAME = 'Task'
4| export const VERIFICATION_AGENT_TYPE = 'verification'
5|
6| // Built-in agents that run once and return a report — the parent never
7| // SendMessages back to continue them. Skip the agentId/SendMessage/usage
8| // trailer for these to save tokens (~135 chars × 34M Explore runs/week).
9| export const ONE_SHOT_BUILTIN_AGENT_TYPES: ReadonlySet<string> = new Set([
10| 'Explore',
11| 'Plan',
12| ])
inputSchema 与 feature gating
inputSchema 使用 lazySchema 组合 base + multi-agent + isolation:
baseInputSchema: description、prompt、subagent_type、model、run_in_background
multi-agent(feature): name、team_name、mode — teammate 寻址
isolation: worktree(git 临时 worktree)、remote(ant-only CCR)
运行时 omit:
- KAIROS off → omit cwd
- background disabled 或 fork enabled → omit run_in_background
- GrowthBook tengu_auto_background_agents 影响 getAutoBackgroundMs
AgentToolInput 显式类型 widen omit 后字段,call 解构始终安全。
源码引用: src/tools/AgentTool/AgentTool.tsx · 第 82–125 行(共 1835 行)
82| import {
83| createAgentWorktree,
84| hasWorktreeChanges,
85| removeAgentWorktree,
86| } from '../../utils/worktree.js'
87| import { BASH_TOOL_NAME } from '../BashTool/toolName.js'
88| import { BackgroundHint } from '../BashTool/UI.js'
89| import { FILE_READ_TOOL_NAME } from '../FileReadTool/prompt.js'
90| import { spawnTeammate } from '../shared/spawnMultiAgent.js'
91| import { setAgentColor } from './agentColorManager.js'
92| import {
93| agentToolResultSchema,
94| classifyHandoffIfNeeded,
95| emitTaskProgress,
96| extractPartialResult,
97| finalizeAgentTool,
98| getLastToolUseName,
99| runAsyncAgentLifecycle,
100| } from './agentToolUtils.js'
101| import { GENERAL_PURPOSE_AGENT } from './built-in/generalPurposeAgent.js'
102| import {
103| AGENT_TOOL_NAME,
104| LEGACY_AGENT_TOOL_NAME,
105| ONE_SHOT_BUILTIN_AGENT_TYPES,
106| } from './constants.js'
107| import {
108| buildForkedMessages,
109| buildWorktreeNotice,
110| FORK_AGENT,
111| isForkSubagentEnabled,
112| isInForkChild,
113| } from './forkSubagent.js'
114| import type { AgentDefinition } from './loadAgentsDir.js'
115| import {
116| filterAgentsByMcpRequirements,
117| hasRequiredMcpServers,
118| isBuiltInAgent,
119| } from './loadAgentsDir.js'
120| import { getPrompt } from './prompt.js'
121| import { runAgent } from './runAgent.js'
122| import {
123| renderGroupedAgentToolUse,
124| renderToolResultMessage,
125| renderToolUseErrorMessage,
源码引用: src/tools/AgentTool/AgentTool.tsx · 第 140–155 行(共 1835 行)
140|
141| // Progress display constants (for showing background hint)
142| const PROGRESS_THRESHOLD_MS = 2000 // Show background hint after 2 seconds
143|
144| // Check if background tasks are disabled at module load time
145| const isBackgroundTasksDisabled =
146| // eslint-disable-next-line custom-rules/no-process-env-top-level -- Intentional: schema must be defined at module load
147| isEnvTruthy(process.env.CLAUDE_CODE_DISABLE_BACKGROUND_TASKS)
148|
149| // Auto-background agent tasks after this many ms (0 = disabled)
150| // Enabled by env var OR GrowthBook gate (checked lazily since GB may not be ready at module load)
151| function getAutoBackgroundMs(): number {
152| if (
153| isEnvTruthy(process.env.CLAUDE_AUTO_BACKGROUND_TASKS) ||
154| getFeatureValue_CACHED_MAY_BE_STALE('tengu_auto_background_agents', false)
155| ) {
prompt():动态 agent 列表
AgentTool.prompt 异步构建系统 prompt 片段:
- 扫描 tools 中 mcp__ 前缀收集已连接 MCP server
- filterAgentsByMcpRequirements — agent 定义 requiredMcpServers 前置条件
- filterDeniedAgents — 权限 deny Agent(type)
- getPrompt(filteredAgents, isCoordinator, allowedAgentTypes)
coordinator 模式(CLAUDE_CODE_COORDINATOR_MODE)改变可选 agent 列表与描述。allowedAgentTypes 来自 Agent(x,y) 工具规格 restrict。
阅读建议: 对照 loadAgentsDir 的 built-in generalPurpose、Explore、Plan 定义与 frontmatter model/color。
源码引用: src/tools/AgentTool/AgentTool.tsx · 第 196–225 行(共 1835 行)
196| .describe(
197| 'Name for the spawned agent. Makes it addressable via SendMessage({to: name}) while running.',
198| ),
199| team_name: z
200| .string()
201| .optional()
202| .describe(
203| 'Team name for spawning. Uses current team context if omitted.',
204| ),
205| mode: permissionModeSchema()
206| .optional()
207| .describe(
208| 'Permission mode for spawned teammate (e.g., "plan" to require plan approval).',
209| ),
210| })
211|
212| return baseInputSchema()
213| .merge(multiAgentInputSchema)
214| .extend({
215| isolation: ("external" === 'ant'
216| ? z.enum(['worktree', 'remote'])
217| : z.enum(['worktree'])
218| )
219| .optional()
220| .describe(
221| "external" === 'ant'
222| ? 'Isolation mode. "worktree" creates a temporary git worktree so the agent works on an isolated copy of the repo. "remote" launches the agent in a remote CCR environment (always runs in background).'
223| : 'Isolation mode. "worktree" creates a temporary git worktree so the agent works on an isolated copy of the repo.',
224| ),
225| cwd: z
call():teammate 与 fork 路由
AgentTool.call() 是路由器而非单一执行函数。若同时有 team_name 与 name,走 spawnTeammate 返回 teammate 元数据。否则若 fork gate 开启且未指定 subagent_type,进入 FORK_AGENT 路径,共享父 renderedSystemPrompt 以保持 prompt cache。常规路径按 subagent_type 查 activeAgents,默认 general-purpose。
选定 agent 后,call 会继续做几类约束检查:
- isAgentSwarmsEnabled — team_name 需 plan 支持
- isInProcessTeammate — 禁止 nested teammate、background spawn
- requiredMcpServers — poll pending MCP 最多 30s
- effectiveIsolation remote — teleportToRemote(ant-only)
logEvent tengu_agent_tool_selected 记录 model、source、is_fork、is_async 等 telemetry,用来回放 Agent 选择路径。
源码引用: src/tools/AgentTool/AgentTool.tsx · 第 385–550 行(共 1835 行)
385| async call(
386| {
387| prompt,
388| subagent_type,
389| description,
390| model: modelParam,
391| run_in_background,
392| name,
393| team_name,
394| mode: spawnMode,
395| isolation,
396| cwd,
397| }: AgentToolInput,
398| toolUseContext,
399| canUseTool,
400| assistantMessage,
401| onProgress?,
402| ) {
403| const startTime = Date.now()
404| const model = isCoordinatorMode() ? undefined : modelParam
405|
406| // Get app state for permission mode and agent filtering
407| const appState = toolUseContext.getAppState()
408| const permissionMode = appState.toolPermissionContext.mode
409| // In-process teammates get a no-op setAppState; setAppStateForTasks
410| // reaches the root store so task registration/progress/kill stay visible.
411| const rootSetAppState =
412| toolUseContext.setAppStateForTasks ?? toolUseContext.setAppState
413|
414| // Check if user is trying to use agent teams without access
415| if (team_name && !isAgentSwarmsEnabled()) {
416| throw new Error('Agent Teams is not yet available on your plan.')
417| }
418|
419| // Teammates (in-process or tmux) passing `name` would trigger spawnTeammate()
420| // below, but TeamFile.members is a flat array with one leadAgentId — nested
421| // teammates land in the roster with no provenance and confuse the lead.
422| const teamName = resolveTeamName({ team_name }, appState)
423| if (isTeammate() && teamName && name) {
424| throw new Error(
425| 'Teammates cannot spawn other teammates — the team roster is flat. To spawn a subagent instead, omit the `name` parameter.',
426| )
427| }
428| // In-process teammates cannot spawn background agents (their lifecycle is
429| // tied to the leader's process). Tmux teammates are separate processes and
430| // can manage their own background agents.
431| if (isInProcessTeammate() && teamName && run_in_background === true) {
432| throw new Error(
433| 'In-process teammates cannot spawn background agents. Use run_in_background=false for synchronous subagents.',
434| )
435| }
436|
437| // Check if this is a multi-agent spawn request
438| // Spawn is triggered when team_name is set (from param or context) and name is provided
439| if (teamName && name) {
440| // Set agent definition color for grouped UI display before spawning
441| const agentDef = subagent_type
442| ? toolUseContext.options.agentDefinitions.activeAgents.find(
443| a => a.agentType === subagent_type,
444| )
445| : undefined
446| if (agentDef?.color) {
447| setAgentColor(subagent_type!, agentDef.color)
448| }
449| const result = await spawnTeammate(
450| {
451| name,
452| prompt,
453| description,
454| team_name: teamName,
455| use_splitpane: true,
456| plan_mode_required: spawnMode === 'plan',
457| model: model ?? agentDef?.model,
458| agent_type: subagent_type,
459| invokingRequestId: assistantMessage?.requestId,
460| },
461| toolUseContext,
462| )
463|
464| // Type assertion uses TeammateSpawnedOutput (defined above) instead of any.
465| // This type is excluded from the exported outputSchema for dead code elimination.
466| // Cast through unknown because TeammateSpawnedOutput is intentionally
467| // not part of the exported Output union (for dead code elimination purposes).
468| const spawnResult: TeammateSpawnedOutput = {
469| status: 'teammate_spawned' as const,
470| prompt,
471| ...result.data,
472| }
473| return { data: spawnResult } as unknown as { data: Output }
474| }
475|
476| // Fork subagent experiment routing:
477| // - subagent_type set: use it (explicit wins)
478| // - subagent_type omitted, gate on: fork path (undefined)
479| // - subagent_type omitted, gate off: default general-purpose
480| const effectiveType =
481| subagent_type ??
482| (isForkSubagentEnabled() ? undefined : GENERAL_PURPOSE_AGENT.agentType)
483| const isForkPath = effectiveType === undefined
484|
485| let selectedAgent: AgentDefinition
486| if (isForkPath) {
487| // Recursive fork guard: fork children keep the Agent tool in their
488| // pool for cache-identical tool defs, so reject fork attempts at call
489| // time. Primary check is querySource (compaction-resistant — set on
490| // context.options at spawn time, survives autocompact's message
491| // rewrite). Message-scan fallback catches any path where querySource
492| // wasn't threaded.
493| if (
494| toolUseContext.options.querySource ===
495| `agent:builtin:${FORK_AGENT.agentType}` ||
496| isInForkChild(toolUseContext.messages)
497| ) {
498| throw new Error(
499| 'Fork is not available inside a forked worker. Complete your task directly using your tools.',
500| )
501| }
502| selectedAgent = FORK_AGENT
503| } else {
504| // Filter agents to exclude those denied via Agent(AgentName) syntax
505| const allAgents = toolUseContext.options.agentDefinitions.activeAgents
506| const { allowedAgentTypes } = toolUseContext.options.agentDefinitions
507| const agents = filterDeniedAgents(
508| // When allowedAgentTypes is set (from Agent(x,y) tool spec), restrict to those types
509| allowedAgentTypes
510| ? allAgents.filter(a => allowedAgentTypes.includes(a.agentType))
511| : allAgents,
512| appState.toolPermissionContext,
513| AGENT_TOOL_NAME,
514| )
515|
516| const found = agents.find(agent => agent.agentType === effectiveType)
517| if (!found) {
518| // Check if the agent exists but is denied by permission rules
519| const agentExistsButDenied = allAgents.find(
520| agent => agent.agentType === effectiveType,
521| )
522| if (agentExistsButDenied) {
523| const denyRule = getDenyRuleForAgent(
524| appState.toolPermissionContext,
525| AGENT_TOOL_NAME,
526| effectiveType,
527| )
528| throw new Error(
529| `Agent type '${effectiveType}' has been denied by permission rule '${AGENT_TOOL_NAME}(${effectiveType})' from ${denyRule?.source ?? 'settings'}.`,
530| )
531| }
532| throw new Error(
533| `Agent type '${effectiveType}' not found. Available agents: ${agents
534| .map(a => a.agentType)
535| .join(', ')}`,
536| )
537| }
538| selectedAgent = found
539| }
540|
541| // Same lifecycle constraint as the run_in_background guard above, but for
542| // agent definitions that force background via `background: true`. Checked
543| // here because selectedAgent is only now resolved.
544| if (
545| isInProcessTeammate() &&
546| teamName &&
547| selectedAgent.background === true
548| ) {
549| throw new Error(
550| `In-process teammates cannot spawn background agents. Agent '${selectedAgent.agentType}' has background: true in its definition.`,
call():MCP 等待与 isolation
requiredMcpServers 检查避免 race:agent 启动时 MCP 连接可能仍 pending,但 agent 定义已经要求这些 server 可用。
poll 500ms 间隔,deadline 30s。failed required server 提前 break;stillPending 清除后进入 hasRequiredMcpServers 校验。
worktree isolation: createAgentWorktree 创建临时 git worktree,子 agent cwd 隔离,完成后 removeAgentWorktree。
remote isolation: checkRemoteAgentEligibility → teleportToRemote → registerRemoteAgentTask,返回 remote_launched。
cwd override: 与 isolation worktree 互斥;绝对路径覆盖子 agent 工作目录。
源码引用: src/tools/AgentTool/AgentTool.tsx · 第 556–627 行(共 1835 行)
556| const requiredMcpServers = selectedAgent.requiredMcpServers
557|
558| // Check if required MCP servers have tools available
559| // A server that's connected but not authenticated won't have any tools
560| if (requiredMcpServers?.length) {
561| // If any required servers are still pending (connecting), wait for them
562| // before checking tool availability. This avoids a race condition where
563| // the agent is invoked before MCP servers finish connecting.
564| const hasPendingRequiredServers = appState.mcp.clients.some(
565| c =>
566| c.type === 'pending' &&
567| requiredMcpServers.some(pattern =>
568| c.name.toLowerCase().includes(pattern.toLowerCase()),
569| ),
570| )
571|
572| let currentAppState = appState
573| if (hasPendingRequiredServers) {
574| const MAX_WAIT_MS = 30_000
575| const POLL_INTERVAL_MS = 500
576| const deadline = Date.now() + MAX_WAIT_MS
577|
578| while (Date.now() < deadline) {
579| await sleep(POLL_INTERVAL_MS)
580| currentAppState = toolUseContext.getAppState()
581|
582| // Early exit: if any required server has already failed, no point
583| // waiting for other pending servers — the check will fail regardless.
584| const hasFailedRequiredServer = currentAppState.mcp.clients.some(
585| c =>
586| c.type === 'failed' &&
587| requiredMcpServers.some(pattern =>
588| c.name.toLowerCase().includes(pattern.toLowerCase()),
589| ),
590| )
591| if (hasFailedRequiredServer) break
592|
593| const stillPending = currentAppState.mcp.clients.some(
594| c =>
595| c.type === 'pending' &&
596| requiredMcpServers.some(pattern =>
597| c.name.toLowerCase().includes(pattern.toLowerCase()),
598| ),
599| )
600| if (!stillPending) break
601| }
602| }
603|
604| // Get servers that actually have tools (meaning they're connected AND authenticated)
605| const serversWithTools: string[] = []
606| for (const tool of currentAppState.mcp.tools) {
607| if (tool.name?.startsWith('mcp__')) {
608| // Extract server name from tool name (format: mcp__serverName__toolName)
609| const parts = tool.name.split('__')
610| const serverName = parts[1]
611| if (serverName && !serversWithTools.includes(serverName)) {
612| serversWithTools.push(serverName)
613| }
614| }
615| }
616|
617| if (!hasRequiredMcpServers(selectedAgent, serversWithTools)) {
618| const missing = requiredMcpServers.filter(
619| pattern =>
620| !serversWithTools.some(server =>
621| server.toLowerCase().includes(pattern.toLowerCase()),
622| ),
623| )
624| throw new Error(
625| `Agent '${selectedAgent.agentType}' requires MCP servers matching: ${missing.join(', ')}. ` +
626| `MCP servers with tools: ${serversWithTools.length > 0 ? serversWithTools.join(', ') : 'none'}. ` +
627| `Use /mcp to configure and authenticate the required MCP servers.`,
源码引用: src/tools/AgentTool/AgentTool.tsx · 第 430–450 行(共 1835 行)
430| // can manage their own background agents.
431| if (isInProcessTeammate() && teamName && run_in_background === true) {
432| throw new Error(
433| 'In-process teammates cannot spawn background agents. Use run_in_background=false for synchronous subagents.',
434| )
435| }
436|
437| // Check if this is a multi-agent spawn request
438| // Spawn is triggered when team_name is set (from param or context) and name is provided
439| if (teamName && name) {
440| // Set agent definition color for grouped UI display before spawning
441| const agentDef = subagent_type
442| ? toolUseContext.options.agentDefinitions.activeAgents.find(
443| a => a.agentType === subagent_type,
444| )
445| : undefined
446| if (agentDef?.color) {
447| setAgentColor(subagent_type!, agentDef.color)
448| }
449| const result = await spawnTeammate(
450| {
runAgent:子 query 入口
runAgent.ts 是同步 Agent 执行核心:
- createSubagentContext — 克隆 ToolUseContext,设置 agentId、收窄 permission、clone readFileState
- filterToolsForAgent — 移除 DISALLOWED 工具;coordinator worker 用 ALLOWED 白名单
- assembleToolPool — 与 REPL 相同方式合并 MCP
- getSystemPrompt / enhanceSystemPromptWithEnvDetails — 子 agent 系统 prompt
- query(...) — 递归调用主 query 循环
- sidechain transcript — recordSidechainTranscript、writeAgentMetadata
registerFrontmatterHooks、executeSubagentStartHooks 在 query 前运行。abort 时 killShellTasksForAgent 清理 Bash 子任务。
Cache 优化: fork 路径使用 parent renderedSystemPrompt,避免 GrowthBook 冷启动 divergence bust cache。
源码引用: src/tools/AgentTool/runAgent.ts · 第 1–80 行(共 974 行)
1| import { feature } from 'bun:bundle'
2| import type { UUID } from 'crypto'
3| import { randomUUID } from 'crypto'
4| import uniqBy from 'lodash-es/uniqBy.js'
5| import { logForDebugging } from 'src/utils/debug.js'
6| import { getProjectRoot, getSessionId } from '../../bootstrap/state.js'
7| import { getCommand, getSkillToolCommands, hasCommand } from '../../commands.js'
8| import {
9| DEFAULT_AGENT_PROMPT,
10| enhanceSystemPromptWithEnvDetails,
11| } from '../../constants/prompts.js'
12| import type { QuerySource } from '../../constants/querySource.js'
13| import { getSystemContext, getUserContext } from '../../context.js'
14| import type { CanUseToolFn } from '../../hooks/useCanUseTool.js'
15| import { query } from '../../query.js'
16| import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics/growthbook.js'
17| import { getDumpPromptsPath } from '../../services/api/dumpPrompts.js'
18| import { cleanupAgentTracking } from '../../services/api/promptCacheBreakDetection.js'
19| import {
20| connectToServer,
21| fetchToolsForClient,
22| } from '../../services/mcp/client.js'
23| import { getMcpConfigByName } from '../../services/mcp/config.js'
24| import type {
25| MCPServerConnection,
26| ScopedMcpServerConfig,
27| } from '../../services/mcp/types.js'
28| import type { Tool, Tools, ToolUseContext } from '../../Tool.js'
29| import { killShellTasksForAgent } from '../../tasks/LocalShellTask/killShellTasks.js'
30| import type { Command } from '../../types/command.js'
31| import type { AgentId } from '../../types/ids.js'
32| import type {
33| AssistantMessage,
34| Message,
35| ProgressMessage,
36| RequestStartEvent,
37| StreamEvent,
38| SystemCompactBoundaryMessage,
39| TombstoneMessage,
40| ToolUseSummaryMessage,
41| UserMessage,
42| } from '../../types/message.js'
43| import { createAttachmentMessage } from '../../utils/attachments.js'
44| import { AbortError } from '../../utils/errors.js'
45| import { getDisplayPath } from '../../utils/file.js'
46| import {
47| cloneFileStateCache,
48| createFileStateCacheWithSizeLimit,
49| READ_FILE_STATE_CACHE_SIZE,
50| } from '../../utils/fileStateCache.js'
51| import {
52| type CacheSafeParams,
53| createSubagentContext,
54| } from '../../utils/forkedAgent.js'
55| import { registerFrontmatterHooks } from '../../utils/hooks/registerFrontmatterHooks.js'
56| import { clearSessionHooks } from '../../utils/hooks/sessionHooks.js'
57| import { executeSubagentStartHooks } from '../../utils/hooks.js'
58| import { createUserMessage } from '../../utils/messages.js'
59| import { getAgentModel } from '../../utils/model/agent.js'
60| import type { ModelAlias } from '../../utils/model/aliases.js'
61| import {
62| clearAgentTranscriptSubdir,
63| recordSidechainTranscript,
64| setAgentTranscriptSubdir,
65| writeAgentMetadata,
66| } from '../../utils/sessionStorage.js'
67| import {
68| isRestrictedToPluginOnly,
69| isSourceAdminTrusted,
70| } from '../../utils/settings/pluginOnlyPolicy.js'
71| import {
72| asSystemPrompt,
73| type SystemPrompt,
74| } from '../../utils/systemPromptType.js'
75| import {
76| isPerfettoTracingEnabled,
77| registerAgent as registerPerfettoAgent,
78| unregisterAgent as unregisterPerfettoAgent,
79| } from '../../utils/telemetry/perfettoTracing.js'
80| import type { ContentReplacementState } from '../../utils/toolResultStorage.js'
sync vs async 输出
outputSchema union:
completed(sync): agentToolResultSchema + status completed + prompt 回显
async_launched: agentId、description、prompt、outputFile(getTaskOutputPath)、canReadOutputFile
触发 async:run_in_background=true、agent def background:true、getAutoBackgroundMs 超时、forceAsync 等。
async 路径 registerAsyncAgent / runAsyncAgentLifecycle,主线程立即返回;Completion 时 enqueueAgentNotification。TaskOutputTool 可读 outputFile 拉进度。
finalizeAgentTool 处理 handoff、classifyHandoffIfNeeded、token 统计上报。
源码引用: src/tools/AgentTool/AgentTool.tsx · 第 271–294 行(共 1835 行)
271| export const outputSchema = lazySchema(() => {
272| const syncOutputSchema = agentToolResultSchema().extend({
273| status: z.literal('completed'),
274| prompt: z.string(),
275| })
276|
277| const asyncOutputSchema = z.object({
278| status: z.literal('async_launched'),
279| agentId: z.string().describe('The ID of the async agent'),
280| description: z.string().describe('The description of the task'),
281| prompt: z.string().describe('The prompt for the agent'),
282| outputFile: z
283| .string()
284| .describe('Path to the output file for checking agent progress'),
285| canReadOutputFile: z
286| .boolean()
287| .optional()
288| .describe(
289| 'Whether the calling agent has Read/Bash tools to check progress',
290| ),
291| })
292|
293| return z.union([syncOutputSchema, asyncOutputSchema])
294| })
源码引用: src/tools/AgentTool/agentToolUtils.ts · 第 1–50 行(共 687 行)
1| import { feature } from 'bun:bundle'
2| import { z } from 'zod/v4'
3| import { clearInvokedSkillsForAgent } from '../../bootstrap/state.js'
4| import {
5| ALL_AGENT_DISALLOWED_TOOLS,
6| ASYNC_AGENT_ALLOWED_TOOLS,
7| CUSTOM_AGENT_DISALLOWED_TOOLS,
8| IN_PROCESS_TEAMMATE_ALLOWED_TOOLS,
9| } from '../../constants/tools.js'
10| import { startAgentSummarization } from '../../services/AgentSummary/agentSummary.js'
11| import {
12| type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
13| logEvent,
14| } from '../../services/analytics/index.js'
15| import { clearDumpState } from '../../services/api/dumpPrompts.js'
16| import type { AppState } from '../../state/AppState.js'
17| import type {
18| Tool,
19| ToolPermissionContext,
20| Tools,
21| ToolUseContext,
22| } from '../../Tool.js'
23| import { toolMatchesName } from '../../Tool.js'
24| import {
25| completeAgentTask as completeAsyncAgent,
26| createActivityDescriptionResolver,
27| createProgressTracker,
28| enqueueAgentNotification,
29| failAgentTask as failAsyncAgent,
30| getProgressUpdate,
31| getTokenCountFromTracker,
32| isLocalAgentTask,
33| killAsyncAgent,
34| type ProgressTracker,
35| updateAgentProgress as updateAsyncAgentProgress,
36| updateProgressFromMessage,
37| } from '../../tasks/LocalAgentTask/LocalAgentTask.js'
38| import { asAgentId } from '../../types/ids.js'
39| import type { Message as MessageType } from '../../types/message.js'
40| import { isAgentSwarmsEnabled } from '../../utils/agentSwarmsEnabled.js'
41| import { logForDebugging } from '../../utils/debug.js'
42| import { isInProtectedNamespace } from '../../utils/envUtils.js'
43| import { AbortError, errorMessage } from '../../utils/errors.js'
44| import type { CacheSafeParams } from '../../utils/forkedAgent.js'
45| import { lazySchema } from '../../utils/lazySchema.js'
46| import {
47| extractTextContent,
48| getLastAssistantMessage,
49| } from '../../utils/messages.js'
50| import type { PermissionMode } from '../../utils/permissions/PermissionMode.js'
与工具注册表的协作
AgentTool 在 getAllBaseTools 数组首位附近(AgentTool, TaskOutputTool, BashTool...)。
tools.ts 导出:
- ALL_AGENT_DISALLOWED_TOOLS — 子 agent 禁止工具名集合
- ASYNC_AGENT_ALLOWED_TOOLS — 后台 agent 白名单
- COORDINATOR_MODE_ALLOWED_TOOLS — coordinator worker 工具
runAgent 过滤后传入 toolUseContext.options.tools,子 StreamingToolExecutor 使用同一池。
TaskOutputTool 与 Agent 配对:async agent 进度查询。TaskStopTool 终止后台 agent。
源码引用: src/tools.ts · 第 98–103 行(共 390 行)
98| export {
99| ALL_AGENT_DISALLOWED_TOOLS,
100| CUSTOM_AGENT_DISALLOWED_TOOLS,
101| ASYNC_AGENT_ALLOWED_TOOLS,
102| COORDINATOR_MODE_ALLOWED_TOOLS,
103| } from './constants/tools.js'
源码引用: src/tools.ts · 第 194–198 行(共 390 行)
194| return [
195| AgentTool,
196| TaskOutputTool,
197| BashTool,
198| // Ant-native builds have bfs/ugrep embedded in the bun binary (same ARGV0
源码目录
关联 utils/forkedAgent.ts、tasks/LocalAgentTask/、coordinator/coordinatorMode.ts。点击 AgentTool.tsx 跳回本章源码块。
动手练习
- 列出 ONE_SHOT_BUILTIN_AGENT_TYPES,理解为何 Explore 结果省略 agentId trailer
- grep createSubagentContext,画出子 agent 与父 agent 的 setAppState 差异
- 对比 sync Agent 与 run_in_background 的 transcript 消息形态
- 阅读 filterDeniedAgents,构造 Agent(Explore) deny 规则并预测 prompt 列表
- 追踪 fork 路径 querySource 设置点,验证 isInForkChild 拒绝逻辑
本章小结与延伸
AgentTool = 递归 query + 任务编排。执行调度仍经 StreamingToolExecutor;权限见 utils/permissions filterDeniedAgents。 继续学习: