first commit
This commit is contained in:
102
claude-code源码-中文注释/src/tools/AgentTool/agentDisplay.ts
Normal file
102
claude-code源码-中文注释/src/tools/AgentTool/agentDisplay.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
/**
|
||||
* Shared utilities for displaying agent information.
|
||||
* Used by both the CLI `claude agents` handler and the interactive `/agents` command.
|
||||
*/
|
||||
|
||||
import { getDefaultSubagentModel } from '../../utils/model/agent.js'
|
||||
import {
|
||||
getSourceDisplayName,
|
||||
type SettingSource,
|
||||
} from '../../utils/settings/constants.js'
|
||||
import type { AgentDefinition } from './loadAgentsDir.js'
|
||||
|
||||
type AgentSource = SettingSource | 'built-in' | 'plugin'
|
||||
|
||||
export type AgentSourceGroup = {
|
||||
label: string
|
||||
source: AgentSource
|
||||
}
|
||||
|
||||
/**
|
||||
* Ordered list of agent source groups for display.
|
||||
* Both the CLI and interactive UI should use this to ensure consistent ordering.
|
||||
*/
|
||||
export const AGENT_SOURCE_GROUPS: AgentSourceGroup[] = [
|
||||
{ label: 'User agents', source: 'userSettings' },
|
||||
{ label: 'Project agents', source: 'projectSettings' },
|
||||
{ label: 'Local agents', source: 'localSettings' },
|
||||
{ label: 'Managed agents', source: 'policySettings' },
|
||||
{ label: 'Plugin agents', source: 'plugin' },
|
||||
{ label: 'CLI arg agents', source: 'flagSettings' },
|
||||
{ label: 'Built-in agents', source: 'built-in' },
|
||||
]
|
||||
|
||||
export type ResolvedAgent = AgentDefinition & {
|
||||
overriddenBy?: AgentSource
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过与活动(获胜的)代理列表比较来标注具有覆盖信息的代理。
|
||||
* 当来自更高优先级源的同名代理占优势时,代理被"覆盖"。
|
||||
*
|
||||
* 还通过 (agentType, source) 去重,以处理同一代理文件同时从 worktree 和主仓库加载的 git worktree 重复。
|
||||
*/
|
||||
export function resolveAgentOverrides(
|
||||
allAgents: AgentDefinition[],
|
||||
activeAgents: AgentDefinition[],
|
||||
): ResolvedAgent[] {
|
||||
const activeMap = new Map<string, AgentDefinition>()
|
||||
for (const agent of activeAgents) {
|
||||
activeMap.set(agent.agentType, agent)
|
||||
}
|
||||
|
||||
const seen = new Set<string>()
|
||||
const resolved: ResolvedAgent[] = []
|
||||
|
||||
// 遍历 allAgents,用 activeAgents 的覆盖信息标注每个。
|
||||
// 通过 (agentType, source) 去重以处理 git worktree 重复。
|
||||
for (const agent of allAgents) {
|
||||
const key = `${agent.agentType}:${agent.source}`
|
||||
if (seen.has(key)) continue
|
||||
seen.add(key)
|
||||
|
||||
const active = activeMap.get(agent.agentType)
|
||||
const overriddenBy =
|
||||
active && active.source !== agent.source ? active.source : undefined
|
||||
resolved.push({ ...agent, overriddenBy })
|
||||
}
|
||||
|
||||
return resolved
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析代理的显示模型字符串。
|
||||
* 返回显示用的模型别名或 'inherit'。
|
||||
*/
|
||||
export function resolveAgentModelDisplay(
|
||||
agent: AgentDefinition,
|
||||
): string | undefined {
|
||||
const model = agent.model || getDefaultSubagentModel()
|
||||
if (!model) return undefined
|
||||
return model === 'inherit' ? 'inherit' : model
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取覆盖代理的源的易读标签。
|
||||
* 返回小写,例如 "user"、"project"、"managed"。
|
||||
*/
|
||||
export function getOverrideSourceLabel(source: AgentSource): string {
|
||||
return getSourceDisplayName(source).toLowerCase()
|
||||
}
|
||||
|
||||
/**
|
||||
* 按名称(不区分大小写)字母比较代理。
|
||||
*/
|
||||
export function compareAgentsByName(
|
||||
a: AgentDefinition,
|
||||
b: AgentDefinition,
|
||||
): number {
|
||||
return a.agentType.localeCompare(b.agentType, undefined, {
|
||||
sensitivity: 'base',
|
||||
})
|
||||
}
|
||||
176
claude-code源码-中文注释/src/tools/AgentTool/agentMemory.ts
Normal file
176
claude-code源码-中文注释/src/tools/AgentTool/agentMemory.ts
Normal file
@@ -0,0 +1,176 @@
|
||||
import { join, normalize, sep } from 'path'
|
||||
import { getProjectRoot } from '../../bootstrap/state.js'
|
||||
import {
|
||||
buildMemoryPrompt,
|
||||
ensureMemoryDirExists,
|
||||
} from '../../memdir/memdir.js'
|
||||
import { getMemoryBaseDir } from '../../memdir/paths.js'
|
||||
import { getCwd } from '../../utils/cwd.js'
|
||||
import { findCanonicalGitRoot } from '../../utils/git.js'
|
||||
import { sanitizePath } from '../../utils/path.js'
|
||||
|
||||
// 持久代理内存范围:'user' (~/.claude/agent-memory/)、'project' (.claude/agent-memory/)、或 'local' (.claude/agent-memory-local/)
|
||||
export type AgentMemoryScope = 'user' | 'project' | 'local'
|
||||
|
||||
/**
|
||||
* 清理代理类型名称以用作目录名。
|
||||
* 替换冒号(Windows 上无效,用于插件命名空间的代理
|
||||
* 类型如 "my-plugin:my-agent")。
|
||||
*/
|
||||
function sanitizeAgentTypeForPath(agentType: string): string {
|
||||
return agentType.replace(/:/g, '-')
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回本地代理内存目录,这是项目特定的,不提交到 VCS。
|
||||
* 当 CLAUDE_CODE_REMOTE_MEMORY_DIR 设置时,持续到挂载的项目命名空间。
|
||||
* 否则使用 <cwd>/.claude/agent-memory-local/<agentType>/。
|
||||
*/
|
||||
function getLocalAgentMemoryDir(dirName: string): string {
|
||||
if (process.env.CLAUDE_CODE_REMOTE_MEMORY_DIR) {
|
||||
return (
|
||||
join(
|
||||
process.env.CLAUDE_CODE_REMOTE_MEMORY_DIR,
|
||||
'projects',
|
||||
sanitizePath(
|
||||
findCanonicalGitRoot(getProjectRoot()) ?? getProjectRoot(),
|
||||
),
|
||||
'agent-memory-local',
|
||||
dirName,
|
||||
) + sep
|
||||
)
|
||||
}
|
||||
return join(getCwd(), '.claude', 'agent-memory-local', dirName) + sep
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回给定代理类型和范围的代理内存目录。
|
||||
* - 'user' 范围:<memoryBase>/agent-memory/<agentType>/
|
||||
* - 'project' 范围:<cwd>/.claude/agent-memory/<agentType>/
|
||||
* - 'local' 范围:见 getLocalAgentMemoryDir()
|
||||
*/
|
||||
export function getAgentMemoryDir(
|
||||
agentType: string,
|
||||
scope: AgentMemoryScope,
|
||||
): string {
|
||||
const dirName = sanitizeAgentTypeForPath(agentType)
|
||||
switch (scope) {
|
||||
case 'project':
|
||||
return join(getCwd(), '.claude', 'agent-memory', dirName) + sep
|
||||
case 'local':
|
||||
return getLocalAgentMemoryDir(dirName)
|
||||
case 'user':
|
||||
return join(getMemoryBaseDir(), 'agent-memory', dirName) + sep
|
||||
}
|
||||
}
|
||||
|
||||
// 检查文件是否在代理内存目录内(任何范围)。
|
||||
export function isAgentMemoryPath(absolutePath: string): boolean {
|
||||
// 安全:规范化以防止通过 .. 段进行路径遍历绕过
|
||||
const normalizedPath = normalize(absolutePath)
|
||||
const memoryBase = getMemoryBaseDir()
|
||||
|
||||
// user 范围:检查内存基础(可能是自定义目录或配置主目录)
|
||||
if (normalizedPath.startsWith(join(memoryBase, 'agent-memory') + sep)) {
|
||||
return true
|
||||
}
|
||||
|
||||
// project 范围:始终基于 cwd(不重定向)
|
||||
if (
|
||||
normalizedPath.startsWith(join(getCwd(), '.claude', 'agent-memory') + sep)
|
||||
) {
|
||||
return true
|
||||
}
|
||||
|
||||
// local 范围:当 CLAUDE_CODE_REMOTE_MEMORY_DIR 设置时持久化到挂载,否则基于 cwd
|
||||
if (process.env.CLAUDE_CODE_REMOTE_MEMORY_DIR) {
|
||||
if (
|
||||
normalizedPath.includes(sep + 'agent-memory-local' + sep) &&
|
||||
normalizedPath.startsWith(
|
||||
join(process.env.CLAUDE_CODE_REMOTE_MEMORY_DIR, 'projects') + sep,
|
||||
)
|
||||
) {
|
||||
return true
|
||||
}
|
||||
} else if (
|
||||
normalizedPath.startsWith(
|
||||
join(getCwd(), '.claude', 'agent-memory-local') + sep,
|
||||
)
|
||||
) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回给定代理类型和范围的代理内存文件路径。
|
||||
*/
|
||||
export function getAgentMemoryEntrypoint(
|
||||
agentType: string,
|
||||
scope: AgentMemoryScope,
|
||||
): string {
|
||||
return join(getAgentMemoryDir(agentType, scope), 'MEMORY.md')
|
||||
}
|
||||
|
||||
export function getMemoryScopeDisplay(
|
||||
memory: AgentMemoryScope | undefined,
|
||||
): string {
|
||||
switch (memory) {
|
||||
case 'user':
|
||||
return `User (${join(getMemoryBaseDir(), 'agent-memory')}/)`
|
||||
case 'project':
|
||||
return 'Project (.claude/agent-memory/)'
|
||||
case 'local':
|
||||
return `Local (${getLocalAgentMemoryDir('...')})`
|
||||
default:
|
||||
return 'None'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 为启用了内存的代理加载持久内存。
|
||||
* 如有需要创建内存目录并返回包含内存内容的提示。
|
||||
*
|
||||
* @param agentType 代理的类型名称(用作目录名)
|
||||
* @param scope 'user' 用于 ~/.claude/agent-memory/ 或 'project' 用于 .claude/agent-memory/
|
||||
*/
|
||||
export function loadAgentMemoryPrompt(
|
||||
agentType: string,
|
||||
scope: AgentMemoryScope,
|
||||
): string {
|
||||
let scopeNote: string
|
||||
switch (scope) {
|
||||
case 'user':
|
||||
scopeNote =
|
||||
'- Since this memory is user-scope, keep learnings general since they apply across all projects'
|
||||
break
|
||||
case 'project':
|
||||
scopeNote =
|
||||
'- Since this memory is project-scope and shared with your team via version control, tailor your memories to this project'
|
||||
break
|
||||
case 'local':
|
||||
scopeNote =
|
||||
'- Since this memory is local-scope (not checked into version control), tailor your memories to this project and machine'
|
||||
break
|
||||
}
|
||||
|
||||
const memoryDir = getAgentMemoryDir(agentType, scope)
|
||||
|
||||
// Fire-and-forget:这在同步 getSystemPrompt() 回调中的代理生成时运行
|
||||
//(从 AgentDetail.tsx 中的 React render 调用,
|
||||
// 所以它不能是 async)。生成的代理在完整 API 往返之前不会尝试 Write,
|
||||
// 到那时 mkdir 将已完成。即使没有,FileWriteTool 也会做自己的父目录 mkdir。
|
||||
void ensureMemoryDirExists(memoryDir)
|
||||
|
||||
const coworkExtraGuidelines =
|
||||
process.env.CLAUDE_COWORK_MEMORY_EXTRA_GUIDELINES
|
||||
return buildMemoryPrompt({
|
||||
displayName: 'Persistent Agent Memory',
|
||||
memoryDir,
|
||||
extraGuidelines:
|
||||
coworkExtraGuidelines && coworkExtraGuidelines.trim().length > 0
|
||||
? [scopeNote, coworkExtraGuidelines]
|
||||
: [scopeNote],
|
||||
})
|
||||
}
|
||||
197
claude-code源码-中文注释/src/tools/AgentTool/agentMemorySnapshot.ts
Normal file
197
claude-code源码-中文注释/src/tools/AgentTool/agentMemorySnapshot.ts
Normal file
@@ -0,0 +1,197 @@
|
||||
import { mkdir, readdir, readFile, unlink, writeFile } from 'fs/promises'
|
||||
import { join } from 'path'
|
||||
import { z } from 'zod/v4'
|
||||
import { getCwd } from '../../utils/cwd.js'
|
||||
import { logForDebugging } from '../../utils/debug.js'
|
||||
import { lazySchema } from '../../utils/lazySchema.js'
|
||||
import { jsonParse, jsonStringify } from '../../utils/slowOperations.js'
|
||||
import { type AgentMemoryScope, getAgentMemoryDir } from './agentMemory.js'
|
||||
|
||||
const SNAPSHOT_BASE = 'agent-memory-snapshots'
|
||||
const SNAPSHOT_JSON = 'snapshot.json'
|
||||
const SYNCED_JSON = '.snapshot-synced.json'
|
||||
|
||||
const snapshotMetaSchema = lazySchema(() =>
|
||||
z.object({
|
||||
updatedAt: z.string().min(1),
|
||||
}),
|
||||
)
|
||||
|
||||
const syncedMetaSchema = lazySchema(() =>
|
||||
z.object({
|
||||
syncedFrom: z.string().min(1),
|
||||
}),
|
||||
)
|
||||
type SyncedMeta = z.infer<ReturnType<typeof syncedMetaSchema>>
|
||||
|
||||
/**
|
||||
* Returns the path to the snapshot directory for an agent in the current project.
|
||||
* e.g., <cwd>/.claude/agent-memory-snapshots/<agentType>/
|
||||
*/
|
||||
export function getSnapshotDirForAgent(agentType: string): string {
|
||||
return join(getCwd(), '.claude', SNAPSHOT_BASE, agentType)
|
||||
}
|
||||
|
||||
function getSnapshotJsonPath(agentType: string): string {
|
||||
return join(getSnapshotDirForAgent(agentType), SNAPSHOT_JSON)
|
||||
}
|
||||
|
||||
function getSyncedJsonPath(agentType: string, scope: AgentMemoryScope): string {
|
||||
return join(getAgentMemoryDir(agentType, scope), SYNCED_JSON)
|
||||
}
|
||||
|
||||
async function readJsonFile<T>(
|
||||
path: string,
|
||||
schema: z.ZodType<T>,
|
||||
): Promise<T | null> {
|
||||
try {
|
||||
const content = await readFile(path, { encoding: 'utf-8' })
|
||||
const result = schema.safeParse(jsonParse(content))
|
||||
return result.success ? result.data : null
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async function copySnapshotToLocal(
|
||||
agentType: string,
|
||||
scope: AgentMemoryScope,
|
||||
): Promise<void> {
|
||||
const snapshotMemDir = getSnapshotDirForAgent(agentType)
|
||||
const localMemDir = getAgentMemoryDir(agentType, scope)
|
||||
|
||||
await mkdir(localMemDir, { recursive: true })
|
||||
|
||||
try {
|
||||
const files = await readdir(snapshotMemDir, { withFileTypes: true })
|
||||
for (const dirent of files) {
|
||||
if (!dirent.isFile() || dirent.name === SNAPSHOT_JSON) continue
|
||||
const content = await readFile(join(snapshotMemDir, dirent.name), {
|
||||
encoding: 'utf-8',
|
||||
})
|
||||
await writeFile(join(localMemDir, dirent.name), content)
|
||||
}
|
||||
} catch (e) {
|
||||
logForDebugging(`Failed to copy snapshot to local agent memory: ${e}`)
|
||||
}
|
||||
}
|
||||
|
||||
async function saveSyncedMeta(
|
||||
agentType: string,
|
||||
scope: AgentMemoryScope,
|
||||
snapshotTimestamp: string,
|
||||
): Promise<void> {
|
||||
const syncedPath = getSyncedJsonPath(agentType, scope)
|
||||
const localMemDir = getAgentMemoryDir(agentType, scope)
|
||||
await mkdir(localMemDir, { recursive: true })
|
||||
const meta: SyncedMeta = { syncedFrom: snapshotTimestamp }
|
||||
try {
|
||||
await writeFile(syncedPath, jsonStringify(meta))
|
||||
} catch (e) {
|
||||
logForDebugging(`Failed to save snapshot sync metadata: ${e}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a snapshot exists and whether it's newer than what we last synced.
|
||||
*/
|
||||
export async function checkAgentMemorySnapshot(
|
||||
agentType: string,
|
||||
scope: AgentMemoryScope,
|
||||
): Promise<{
|
||||
action: 'none' | 'initialize' | 'prompt-update'
|
||||
snapshotTimestamp?: string
|
||||
}> {
|
||||
const snapshotMeta = await readJsonFile(
|
||||
getSnapshotJsonPath(agentType),
|
||||
snapshotMetaSchema(),
|
||||
)
|
||||
|
||||
if (!snapshotMeta) {
|
||||
return { action: 'none' }
|
||||
}
|
||||
|
||||
const localMemDir = getAgentMemoryDir(agentType, scope)
|
||||
|
||||
let hasLocalMemory = false
|
||||
try {
|
||||
const dirents = await readdir(localMemDir, { withFileTypes: true })
|
||||
hasLocalMemory = dirents.some(d => d.isFile() && d.name.endsWith('.md'))
|
||||
} catch {
|
||||
// Directory doesn't exist
|
||||
}
|
||||
|
||||
if (!hasLocalMemory) {
|
||||
return { action: 'initialize', snapshotTimestamp: snapshotMeta.updatedAt }
|
||||
}
|
||||
|
||||
const syncedMeta = await readJsonFile(
|
||||
getSyncedJsonPath(agentType, scope),
|
||||
syncedMetaSchema(),
|
||||
)
|
||||
|
||||
if (
|
||||
!syncedMeta ||
|
||||
new Date(snapshotMeta.updatedAt) > new Date(syncedMeta.syncedFrom)
|
||||
) {
|
||||
return {
|
||||
action: 'prompt-update',
|
||||
snapshotTimestamp: snapshotMeta.updatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
return { action: 'none' }
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize local agent memory from a snapshot (first-time setup).
|
||||
*/
|
||||
export async function initializeFromSnapshot(
|
||||
agentType: string,
|
||||
scope: AgentMemoryScope,
|
||||
snapshotTimestamp: string,
|
||||
): Promise<void> {
|
||||
logForDebugging(
|
||||
`Initializing agent memory for ${agentType} from project snapshot`,
|
||||
)
|
||||
await copySnapshotToLocal(agentType, scope)
|
||||
await saveSyncedMeta(agentType, scope, snapshotTimestamp)
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace local agent memory with the snapshot.
|
||||
*/
|
||||
export async function replaceFromSnapshot(
|
||||
agentType: string,
|
||||
scope: AgentMemoryScope,
|
||||
snapshotTimestamp: string,
|
||||
): Promise<void> {
|
||||
logForDebugging(
|
||||
`Replacing agent memory for ${agentType} with project snapshot`,
|
||||
)
|
||||
// Remove existing .md files before copying to avoid orphans
|
||||
const localMemDir = getAgentMemoryDir(agentType, scope)
|
||||
try {
|
||||
const existing = await readdir(localMemDir, { withFileTypes: true })
|
||||
for (const dirent of existing) {
|
||||
if (dirent.isFile() && dirent.name.endsWith('.md')) {
|
||||
await unlink(join(localMemDir, dirent.name))
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Directory may not exist yet
|
||||
}
|
||||
await copySnapshotToLocal(agentType, scope)
|
||||
await saveSyncedMeta(agentType, scope, snapshotTimestamp)
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark the current snapshot as synced without changing local memory.
|
||||
*/
|
||||
export async function markSnapshotSynced(
|
||||
agentType: string,
|
||||
scope: AgentMemoryScope,
|
||||
snapshotTimestamp: string,
|
||||
): Promise<void> {
|
||||
await saveSyncedMeta(agentType, scope, snapshotTimestamp)
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import type { BuiltInAgentDefinition } from '../loadAgentsDir.js'
|
||||
|
||||
const SHARED_PREFIX = `You are an agent for Claude Code, Anthropic's official CLI for Claude. Given the user's message, you should use the tools available to complete the task. Complete the task fully—don't gold-plate, but don't leave it half-done.`
|
||||
|
||||
const SHARED_GUIDELINES = `Your strengths:
|
||||
- Searching for code, configurations, and patterns across large codebases
|
||||
- Analyzing multiple files to understand system architecture
|
||||
- Investigating complex questions that require exploring many files
|
||||
- Performing multi-step research tasks
|
||||
|
||||
Guidelines:
|
||||
- For file searches: search broadly when you don't know where something lives. Use Read when you know the specific file path.
|
||||
- For analysis: Start broad and narrow down. Use multiple search strategies if the first doesn't yield results.
|
||||
- Be thorough: Check multiple locations, consider different naming conventions, look for related files.
|
||||
- NEVER create files unless they're absolutely necessary for achieving your goal. ALWAYS prefer editing an existing file to creating a new one.
|
||||
- NEVER proactively create documentation files (*.md) or README files. Only create documentation files if explicitly requested.`
|
||||
|
||||
// Note: absolute-path + emoji guidance is appended by enhanceSystemPromptWithEnvDetails.
|
||||
function getGeneralPurposeSystemPrompt(): string {
|
||||
return `${SHARED_PREFIX} When you complete the task, respond with a concise report covering what was done and any key findings — the caller will relay this to the user, so it only needs the essentials.
|
||||
|
||||
${SHARED_GUIDELINES}`
|
||||
}
|
||||
|
||||
export const GENERAL_PURPOSE_AGENT: BuiltInAgentDefinition = {
|
||||
agentType: 'general-purpose',
|
||||
whenToUse:
|
||||
'General-purpose agent for researching complex questions, searching for code, and executing multi-step tasks. When you are searching for a keyword or file and are not confident that you will find the right match in the first few tries use this agent to perform the search for you.',
|
||||
tools: ['*'],
|
||||
source: 'built-in',
|
||||
baseDir: 'built-in',
|
||||
// model is intentionally omitted - uses getDefaultSubagentModel().
|
||||
getSystemPrompt: getGeneralPurposeSystemPrompt,
|
||||
}
|
||||
92
claude-code源码-中文注释/src/tools/AgentTool/built-in/planAgent.ts
Normal file
92
claude-code源码-中文注释/src/tools/AgentTool/built-in/planAgent.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import { BASH_TOOL_NAME } from 'src/tools/BashTool/toolName.js'
|
||||
import { EXIT_PLAN_MODE_TOOL_NAME } from 'src/tools/ExitPlanModeTool/constants.js'
|
||||
import { FILE_EDIT_TOOL_NAME } from 'src/tools/FileEditTool/constants.js'
|
||||
import { FILE_READ_TOOL_NAME } from 'src/tools/FileReadTool/prompt.js'
|
||||
import { FILE_WRITE_TOOL_NAME } from 'src/tools/FileWriteTool/prompt.js'
|
||||
import { GLOB_TOOL_NAME } from 'src/tools/GlobTool/prompt.js'
|
||||
import { GREP_TOOL_NAME } from 'src/tools/GrepTool/prompt.js'
|
||||
import { NOTEBOOK_EDIT_TOOL_NAME } from 'src/tools/NotebookEditTool/constants.js'
|
||||
import { hasEmbeddedSearchTools } from 'src/utils/embeddedTools.js'
|
||||
import { AGENT_TOOL_NAME } from '../constants.js'
|
||||
import type { BuiltInAgentDefinition } from '../loadAgentsDir.js'
|
||||
import { EXPLORE_AGENT } from './exploreAgent.js'
|
||||
|
||||
function getPlanV2SystemPrompt(): string {
|
||||
// Ant-native builds alias find/grep to embedded bfs/ugrep and remove the
|
||||
// dedicated Glob/Grep tools, so point at find/grep instead.
|
||||
const searchToolsHint = hasEmbeddedSearchTools()
|
||||
? `\`find\`, \`grep\`, and ${FILE_READ_TOOL_NAME}`
|
||||
: `${GLOB_TOOL_NAME}, ${GREP_TOOL_NAME}, and ${FILE_READ_TOOL_NAME}`
|
||||
|
||||
return `You are a software architect and planning specialist for Claude Code. Your role is to explore the codebase and design implementation plans.
|
||||
|
||||
=== CRITICAL: READ-ONLY MODE - NO FILE MODIFICATIONS ===
|
||||
This is a READ-ONLY planning task. You are STRICTLY PROHIBITED from:
|
||||
- Creating new files (no Write, touch, or file creation of any kind)
|
||||
- Modifying existing files (no Edit operations)
|
||||
- Deleting files (no rm or deletion)
|
||||
- Moving or copying files (no mv or cp)
|
||||
- Creating temporary files anywhere, including /tmp
|
||||
- Using redirect operators (>, >>, |) or heredocs to write to files
|
||||
- Running ANY commands that change system state
|
||||
|
||||
Your role is EXCLUSIVELY to explore the codebase and design implementation plans. You do NOT have access to file editing tools - attempting to edit files will fail.
|
||||
|
||||
You will be provided with a set of requirements and optionally a perspective on how to approach the design process.
|
||||
|
||||
## Your Process
|
||||
|
||||
1. **Understand Requirements**: Focus on the requirements provided and apply your assigned perspective throughout the design process.
|
||||
|
||||
2. **Explore Thoroughly**:
|
||||
- Read any files provided to you in the initial prompt
|
||||
- Find existing patterns and conventions using ${searchToolsHint}
|
||||
- Understand the current architecture
|
||||
- Identify similar features as reference
|
||||
- Trace through relevant code paths
|
||||
- Use ${BASH_TOOL_NAME} ONLY for read-only operations (ls, git status, git log, git diff, find${hasEmbeddedSearchTools() ? ', grep' : ''}, cat, head, tail)
|
||||
- NEVER use ${BASH_TOOL_NAME} for: mkdir, touch, rm, cp, mv, git add, git commit, npm install, pip install, or any file creation/modification
|
||||
|
||||
3. **Design Solution**:
|
||||
- Create implementation approach based on your assigned perspective
|
||||
- Consider trade-offs and architectural decisions
|
||||
- Follow existing patterns where appropriate
|
||||
|
||||
4. **Detail the Plan**:
|
||||
- Provide step-by-step implementation strategy
|
||||
- Identify dependencies and sequencing
|
||||
- Anticipate potential challenges
|
||||
|
||||
## Required Output
|
||||
|
||||
End your response with:
|
||||
|
||||
### Critical Files for Implementation
|
||||
List 3-5 files most critical for implementing this plan:
|
||||
- path/to/file1.ts
|
||||
- path/to/file2.ts
|
||||
- path/to/file3.ts
|
||||
|
||||
REMEMBER: You can ONLY explore and plan. You CANNOT and MUST NOT write, edit, or modify any files. You do NOT have access to file editing tools.`
|
||||
}
|
||||
|
||||
export const PLAN_AGENT: BuiltInAgentDefinition = {
|
||||
agentType: 'Plan',
|
||||
whenToUse:
|
||||
'Software architect agent for designing implementation plans. Use this when you need to plan the implementation strategy for a task. Returns step-by-step plans, identifies critical files, and considers architectural trade-offs.',
|
||||
disallowedTools: [
|
||||
AGENT_TOOL_NAME,
|
||||
EXIT_PLAN_MODE_TOOL_NAME,
|
||||
FILE_EDIT_TOOL_NAME,
|
||||
FILE_WRITE_TOOL_NAME,
|
||||
NOTEBOOK_EDIT_TOOL_NAME,
|
||||
],
|
||||
source: 'built-in',
|
||||
tools: EXPLORE_AGENT.tools,
|
||||
baseDir: 'built-in',
|
||||
model: 'inherit',
|
||||
// Plan is read-only and can Read CLAUDE.md directly if it needs conventions.
|
||||
// Dropping it from context saves tokens without blocking access.
|
||||
omitClaudeMd: true,
|
||||
getSystemPrompt: () => getPlanV2SystemPrompt(),
|
||||
}
|
||||
288
claude-code源码-中文注释/src/tools/AgentTool/builtInAgents.ts
Normal file
288
claude-code源码-中文注释/src/tools/AgentTool/builtInAgents.ts
Normal file
@@ -0,0 +1,288 @@
|
||||
import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics/growthbook.js'
|
||||
import { getSubscriptionType } from '../../utils/auth.js'
|
||||
import { hasEmbeddedSearchTools } from '../../utils/embeddedTools.js'
|
||||
import { isEnvDefinedFalsy, isEnvTruthy } from '../../utils/envUtils.js'
|
||||
import { isTeammate } from '../../utils/teammate.js'
|
||||
import { isInProcessTeammate } from '../../utils/teammateContext.js'
|
||||
import { FILE_READ_TOOL_NAME } from '../FileReadTool/prompt.js'
|
||||
import { FILE_WRITE_TOOL_NAME } from '../FileWriteTool/prompt.js'
|
||||
import { GLOB_TOOL_NAME } from '../GlobTool/prompt.js'
|
||||
import { SEND_MESSAGE_TOOL_NAME } from '../SendMessageTool/constants.js'
|
||||
import { AGENT_TOOL_NAME } from './constants.js'
|
||||
import { isForkSubagentEnabled } from './forkSubagent.js'
|
||||
import type { AgentDefinition } from './loadAgentsDir.js'
|
||||
|
||||
function getToolsDescription(agent: AgentDefinition): string {
|
||||
const { tools, disallowedTools } = agent
|
||||
const hasAllowlist = tools && tools.length > 0
|
||||
const hasDenylist = disallowedTools && disallowedTools.length > 0
|
||||
|
||||
if (hasAllowlist && hasDenylist) {
|
||||
// 两者都定义:按拒绝列表过滤允许列表以匹配运行时行为
|
||||
const denySet = new Set(disallowedTools)
|
||||
const effectiveTools = tools.filter(t => !denySet.has(t))
|
||||
if (effectiveTools.length === 0) {
|
||||
return 'None'
|
||||
}
|
||||
return effectiveTools.join(', ')
|
||||
} else if (hasAllowlist) {
|
||||
// 仅允许列表:显示可用的特定工具
|
||||
return tools.join(', ')
|
||||
} else if (hasDenylist) {
|
||||
// 仅拒绝列表:显示"除 X, Y, Z 外所有工具"
|
||||
return `All tools except ${disallowedTools.join(', ')}`
|
||||
}
|
||||
// 无限制
|
||||
return 'All tools'
|
||||
}
|
||||
|
||||
/**
|
||||
* 为 agent_listing_delta 附件消息格式化一个代理行:
|
||||
* `- type: whenToUse (Tools: ...)`。
|
||||
*/
|
||||
export function formatAgentLine(agent: AgentDefinition): string {
|
||||
const toolsDescription = getToolsDescription(agent)
|
||||
return `- ${agent.agentType}: ${agent.whenToUse} (Tools: ${toolsDescription})`
|
||||
}
|
||||
|
||||
/**
|
||||
* 代理列表是否应该作为附件消息注入而不是嵌入在工具描述中。
|
||||
* 当为 true 时,getPrompt() 返回静态描述,attachments.ts 发出
|
||||
* agent_listing_delta 附件。
|
||||
*
|
||||
* 动态代理列表约占 10.2% 的 fleet cache_creation tokens:MCP 异步
|
||||
* 连接、/reload-plugins 或权限模式更改会改变列表 →
|
||||
* 描述更改 → 完整工具 schema 缓存失效。
|
||||
*
|
||||
* 可通过 CLAUDE_CODE_AGENT_LIST_IN_MESSAGES=true/false 覆盖以进行测试。
|
||||
*/
|
||||
export function shouldInjectAgentListInMessages(): boolean {
|
||||
if (isEnvTruthy(process.env.CLAUDE_CODE_AGENT_LIST_IN_MESSAGES)) return true
|
||||
if (isEnvDefinedFalsy(process.env.CLAUDE_CODE_AGENT_LIST_IN_MESSAGES))
|
||||
return false
|
||||
return getFeatureValue_CACHED_MAY_BE_STALE('tengu_agent_list_attach', false)
|
||||
}
|
||||
|
||||
export async function getPrompt(
|
||||
agentDefinitions: AgentDefinition[],
|
||||
isCoordinator?: boolean,
|
||||
allowedAgentTypes?: string[],
|
||||
): Promise<string> {
|
||||
// 当 Agent(x,y) 限制可以生成哪些代理时,按允许的类型过滤代理
|
||||
const effectiveAgents = allowedAgentTypes
|
||||
? agentDefinitions.filter(a => allowedAgentTypes.includes(a.agentType))
|
||||
: agentDefinitions
|
||||
|
||||
// Fork subagent 特性:启用时,插入"When to fork"部分
|
||||
//(fork 语义、指令式提示)并交换为 fork 感知的示例。
|
||||
const forkEnabled = isForkSubagentEnabled()
|
||||
|
||||
const whenToForkSection = forkEnabled
|
||||
? `
|
||||
|
||||
## 何时 fork
|
||||
|
||||
当你不需要保留中间工具输出时 fork 自己(省略 \`subagent_type\`)。标准是定性的——"我是否需要再次使用此输出"——而非任务大小。
|
||||
- **研究**:fork 开放式问题。如果研究可以分解为独立问题,在一条消息中启动并行 fork。对于研究,fork 胜过新的 subagent——它继承上下文并共享你的缓存。
|
||||
- **实现**:优先 fork 需要超过几个编辑的实现工作。在跳到实现之前先做研究。
|
||||
|
||||
Fork 很便宜,因为它们共享你的提示缓存。不要在 fork 上设置 \`model\`——不同的模型无法复用父级的缓存。传递一个短的 \`name\`(一两个词,小写),以便用户可以在团队面板中看到 fork 并在运行中间引导它。
|
||||
|
||||
**不要偷看。** 工具结果包含一个 \`output_file\` 路径——除非用户明确要求进度检查,否则不要读取或 tail 它。你会收到完成通知;信任它。在飞行中途读取转录会将 fork 的工具噪音拉入你的上下文,这就破坏了 fork 的目的。
|
||||
|
||||
**不要竞速。** 启动后,你对 fork 发现的内容一无所知。永远不要以任何格式伪造或预测 fork 结果——无论是散文、摘要还是结构化输出。通知在后续轮次中作为用户角色消息到达;它永远不是你自己的东西。如果用户在通知到达之前提出后续问题,告诉他们 fork 仍在运行——给出状态,而非猜测。
|
||||
|
||||
**编写 fork 提示。** 因为 fork 继承你的上下文,提示是一个*指令*——要做什么,而非情况是什么。要具体说明范围:什么在里面,什么在外面,什么由其他代理处理。不要重新解释背景。
|
||||
`
|
||||
: ''
|
||||
|
||||
const writingThePromptSection = `
|
||||
|
||||
## 编写提示
|
||||
|
||||
${forkEnabled ? '生成新代理时(使用 \`subagent_type\`),它从零上下文开始。' : ''}像对待刚走进房间的聪明同事一样 brief 代理——它没有看过这个对话,不知道你尝试过什么,不理解为什么这个任务重要。
|
||||
- 解释你试图完成什么以及为什么。
|
||||
- 描述你已经学到或排除的内容。
|
||||
- 提供足够的周围问题背景,以便代理可以做出判断而不是仅仅遵循窄指令。
|
||||
- 如果你需要简短响应,说明("200 字以内报告")。
|
||||
- 查找:交出确切命令。调查:交出问题——当前提错误时,规定步骤成为死重量。
|
||||
|
||||
${forkEnabled ? '对于新代理,要简洁' : '简洁'}命令式提示产生浅薄的通用工作。
|
||||
|
||||
**永远不要委托理解。** 不要写"根据你的发现,修复 bug"或"根据研究,实现它"。这些短语将综合推给代理而不是自己做。编写能证明你理解了的提示:包含文件路径、行号、具体要更改什么。
|
||||
`
|
||||
|
||||
const forkExamples = `示例用法:
|
||||
|
||||
<example>
|
||||
user: "在发布之前,这个分支上还剩下什么?"
|
||||
assistant: <thinking>Fork 这个——这是一个调查问题。我想要的是打孔清单,而不是我上下文中的 git 输出。</thinking>
|
||||
${AGENT_TOOL_NAME}({
|
||||
name: "ship-audit",
|
||||
description: "Branch ship-readiness audit",
|
||||
prompt: "审计在这个分支发布之前还剩下什么。检查:未提交的更改、主分支之前的提交、是否存在测试、GrowthBook gate 是否连接、CI 相关文件是否更改。报告打孔清单——完成 vs 缺失。200 字以内。"
|
||||
})
|
||||
assistant: Ship-readiness audit running.
|
||||
<commentary>
|
||||
轮次在此结束。协调器对发现还一无所知。接下来是单独的轮次——通知从外部作为用户角色消息到达。它不是协调器写的东西。
|
||||
</commentary>
|
||||
[later turn — notification arrives as user message]
|
||||
assistant: Audit's back. Three blockers: no tests for the new prompt path, GrowthBook gate wired but not in build_flags.yaml, and one uncommitted file.
|
||||
</example>
|
||||
|
||||
<example>
|
||||
user: "那么 gate 连接上了吗"
|
||||
<commentary>
|
||||
用户在半途提问。审计 fork launch 的目的正是回答这个,而且它还没有返回。协调器没有这个答案。给出状态,而非伪造的结果。
|
||||
</commentary>
|
||||
assistant: Still waiting on the audit — that's one of the things it's checking. Should land shortly.
|
||||
</example>
|
||||
|
||||
<example>
|
||||
user: "你能对这次迁移是否安全给予第二意见吗?"
|
||||
assistant: <thinking>我将询问 code-reviewer 代理——它不会看到我的分析,所以它可以给出独立的判断。</thinking>
|
||||
<commentary>
|
||||
指定了 subagent_type,所以代理从零开始。它需要在提示中获得完整上下文。简报解释要评估什么以及为什么。
|
||||
</commentary>
|
||||
${AGENT_TOOL_NAME}({
|
||||
name: "migration-review",
|
||||
description: "Independent migration review",
|
||||
subagent_type: "code-reviewer",
|
||||
prompt: "审查迁移 0042_user_schema.sql 的安全性。背景:我们正在向 50M 行表添加 NOT NULL 列。现有行获得回填默认值。我想对回填方法在并发写入下是否安全获得第二意见——我检查了锁定行为但想要独立验证。报告:这是安全的吗,如果不行,具体什么会坏?"
|
||||
})
|
||||
</example>
|
||||
`
|
||||
|
||||
const currentExamples = `示例用法:
|
||||
|
||||
<example_agent_descriptions>
|
||||
"test-runner": 在写完代码后使用此代理运行测试
|
||||
"greeting-responder": 使用此代理用友好的笑话回应用户问候
|
||||
</example_agent_descriptions>
|
||||
|
||||
<example>
|
||||
user: "请写一个检查数字是否为质数的函数"
|
||||
assistant: 我将使用 ${FILE_WRITE_TOOL_NAME} 工具写入以下代码:
|
||||
<code>
|
||||
function isPrime(n) {
|
||||
if (n <= 1) return false
|
||||
for (let i = 2; i * i <= n; i++) {
|
||||
if (n % i === 0) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
</code>
|
||||
<commentary>
|
||||
由于写入了重要代码且任务已完成,现在使用 test-runner 代理运行测试
|
||||
</commentary>
|
||||
assistant: 使用 ${AGENT_TOOL_NAME} 工具启动 test-runner 代理
|
||||
</example>
|
||||
|
||||
<example>
|
||||
user: "你好"
|
||||
<commentary>
|
||||
由于用户正在问候,使用 greeting-responder 代理用友好的笑话回应
|
||||
</commentary>
|
||||
assistant: "我将使用 ${AGENT_TOOL_NAME} 工具启动 greeting-responder 代理"
|
||||
</example>
|
||||
`
|
||||
|
||||
// 当 gate 开启时,代理列表存在于 agent_listing_delta
|
||||
// 附件(见 attachments.ts)而不是内联在此。这保持
|
||||
// 工具描述在 MCP/插件/权限更改时静态,因此
|
||||
// 工具阻止提示缓存在每次代理加载时不会失效。
|
||||
const listViaAttachment = shouldInjectAgentListInMessages()
|
||||
|
||||
const agentListSection = listViaAttachment
|
||||
? `可用代理类型列在会话中的 <system-reminder> 消息中。`
|
||||
: `可用代理类型及其可访问的工具:
|
||||
${effectiveAgents.map(agent => formatAgentLine(agent)).join('\n')}`
|
||||
|
||||
// 协调器和非协调器模式使用的共享核心提示
|
||||
const shared = `启动新代理以自主处理复杂的多步骤任务。
|
||||
|
||||
${AGENT_TOOL_NAME} 工具启动专业代理(子进程)来自主处理复杂任务。每种代理类型都有特定的能力和可用的工具。
|
||||
|
||||
${agentListSection}
|
||||
|
||||
${
|
||||
forkEnabled
|
||||
? `使用 ${AGENT_TOOL_NAME} 工具时,指定 subagent_type 以使用专业代理,或省略它以 fork 自己——fork 继承你的完整对话上下文。`
|
||||
: `使用 ${AGENT_TOOL_NAME} 工具时,指定 subagent_type 参数以选择要使用的代理类型。如果省略,使用通用代理。`
|
||||
}`
|
||||
|
||||
// 协调器模式获得精简提示——协调器系统提示
|
||||
// 已经涵盖了使用说明、示例和何时不用的指导。
|
||||
if (isCoordinator) {
|
||||
return shared
|
||||
}
|
||||
|
||||
// Ant-native 构建将 find/grep 别名为嵌入式 bfs/ugrep 并移除
|
||||
// 专用的 Glob/Grep 工具,所以通过 Bash 改为指向 find。
|
||||
const embedded = hasEmbeddedSearchTools()
|
||||
const fileSearchHint = embedded
|
||||
? '`find` 通过 Bash 工具'
|
||||
: `the ${GLOB_TOOL_NAME} tool`
|
||||
// "class Foo" 示例是关于内容搜索的。非嵌入式保持 Glob
|
||||
//(原始意图:找到包含内容的文件)。嵌入式获取 grep 因为
|
||||
// find -name 不查看文件内容。
|
||||
const contentSearchHint = embedded
|
||||
? '`grep` 通过 Bash 工具'
|
||||
: `the ${GLOB_TOOL_NAME} tool`
|
||||
const whenNotToUseSection = forkEnabled
|
||||
? ''
|
||||
: `
|
||||
何时不使用 ${AGENT_TOOL_NAME} 工具:
|
||||
- 如果你想读取特定文件路径,使用 ${FILE_READ_TOOL_NAME} 工具或 ${fileSearchHint} 而不是 ${AGENT_TOOL_NAME} 工具,以更快找到匹配
|
||||
- 如果你在搜索特定类定义如 "class Foo",使用 ${contentSearchHint} 而不是,以更快找到匹配
|
||||
- 如果你在特定文件或 2-3 个文件集中搜索代码,使用 ${FILE_READ_TOOL_NAME} 工具而不是 ${AGENT_TOOL_NAME} 工具,以更快找到匹配
|
||||
- 其他与上述代理描述无关的任务
|
||||
`
|
||||
|
||||
// 通过附件列出时,"启动多个代理"说明在
|
||||
// 附件消息中(在那里有订阅条件)。内联时,保持
|
||||
// 现有的每次调用 getSubscriptionType() 检查。
|
||||
const concurrencyNote =
|
||||
!listViaAttachment && getSubscriptionType() !== 'pro'
|
||||
? `
|
||||
- 尽可能并发启动多个代理以最大化性能;为此,使用带有多个工具使用的单个消息`
|
||||
: ''
|
||||
|
||||
// 非协调器获得带有所有部分的完整提示
|
||||
return `${shared}
|
||||
${whenNotToUseSection}
|
||||
|
||||
使用说明:
|
||||
- 始终包含简短描述(3-5 个词)总结代理将做什么${concurrencyNote}
|
||||
- 当代理完成时,它将向你返回一条消息。代理返回的结果对用户不可见。要向用户显示结果,你应该向用户发送一条文本消息,简洁总结结果。${
|
||||
// eslint-disable-next-line custom-rules/no-process-env-top-level
|
||||
!isEnvTruthy(process.env.CLAUDE_CODE_DISABLE_BACKGROUND_TASKS) &&
|
||||
!isInProcessTeammate() &&
|
||||
!forkEnabled
|
||||
? `
|
||||
- 你可以选择使用 run_in_background 参数在后台运行代理。当代理在后台运行时,你将在其完成时自动收到通知——不要睡眠、轮询或主动检查其进度。继续其他工作或响应用户。
|
||||
- **前台 vs 后台**:当前景(默认)——你需要代理的结果才能继续时使用(例如,其发现影响你下一步的研究代理)。当你有真正独立的并行工作时使用后台。
|
||||
`
|
||||
: ''
|
||||
}
|
||||
- 要继续先前生成的代理,使用 ${SEND_MESSAGE_TOOL_NAME} 和代理的 ID 或名称作为 \`to\` 字段。代理在保留其完整上下文的情况下恢复。${forkEnabled ? '每个带有 subagent_type 的新 Agent 调用从零开始——提供完整任务描述。' : '每个 Agent 调用都是全新的——提供完整任务描述。'}
|
||||
- 代理的输出通常应该被信任
|
||||
- 明确告诉代理你是否期望它编写代码或只是做研究(搜索、文件读取、网络获取等)${forkEnabled ? '' : ",因为它不知道用户的意图"}
|
||||
- 如果代理描述提到它应该被主动使用,那么你应该尝试在用户不必先询问的情况下使用它。使用你的判断。
|
||||
- 如果用户指定他们想要你"并行"运行代理,你必须发送带有多个 ${AGENT_TOOL_NAME} 工具使用内容块的单个消息。例如,如果你需要并行启动构建验证器代理和测试运行器代理,发送带有两个工具调用的单个消息。
|
||||
- 你可以选择设置 \`isolation: "worktree"\` 以在临时 git worktree 中运行代理,为其提供仓库的隔离副本。如果代理没有做任何更改,worktree 会自动清理;如果做了,更改,worktree 路径和分支在结果中返回。${
|
||||
process.env.USER_TYPE === 'ant'
|
||||
? `\n- 你可以设置 \`isolation: "remote"\` 以在远程 CCR 环境中运行代理。这始终是后台任务;你将在其完成时收到通知。用于需要新鲜沙箱的长时运行任务。`
|
||||
: ''
|
||||
}${
|
||||
isInProcessTeammate()
|
||||
? `
|
||||
- run_in_background、name、team_name 和 mode 参数在此上下文中不可用。仅支持同步子代理。`
|
||||
: isTeammate()
|
||||
? `
|
||||
- name、team_name 和 mode 参数在此上下文中不可用——teammates 无法生成其他 teammates。省略它们以生成子代理。`
|
||||
: ''
|
||||
}${whenToForkSection}${writingThePromptSection}
|
||||
|
||||
${forkEnabled ? forkExamples : currentExamples}`
|
||||
}
|
||||
12
claude-code源码-中文注释/src/tools/AgentTool/constants.ts
Normal file
12
claude-code源码-中文注释/src/tools/AgentTool/constants.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
export const AGENT_TOOL_NAME = 'Agent'
|
||||
// 遗留线路名称以保持向后兼容(权限规则、hooks、恢复的会话)
|
||||
export const LEGACY_AGENT_TOOL_NAME = 'Task'
|
||||
export const VERIFICATION_AGENT_TYPE = 'verification'
|
||||
|
||||
// 运行一次并返回报告的内置代理——父代理永远不会
|
||||
// 回发 SendMessages 继续它们。为这些跳过 agentId/SendMessage/usage
|
||||
// trailer 以节省 tokens(约 135 chars × 34M Explore runs/week)。
|
||||
export const ONE_SHOT_BUILTIN_AGENT_TYPES: ReadonlySet<string> = new Set([
|
||||
'Explore',
|
||||
'Plan',
|
||||
])
|
||||
211
claude-code源码-中文注释/src/tools/AgentTool/forkSubagent.ts
Normal file
211
claude-code源码-中文注释/src/tools/AgentTool/forkSubagent.ts
Normal file
@@ -0,0 +1,211 @@
|
||||
import { feature } from 'bun:bundle'
|
||||
import type { BetaToolUseBlock } from '@anthropic-ai/sdk/resources/beta/messages/messages.mjs'
|
||||
import { randomUUID } from 'crypto'
|
||||
import { getIsNonInteractiveSession } from '../../bootstrap/state.js'
|
||||
import {
|
||||
FORK_BOILERPLATE_TAG,
|
||||
FORK_DIRECTIVE_PREFIX,
|
||||
} from '../../constants/xml.js'
|
||||
import { isCoordinatorMode } from '../../coordinator/coordinatorMode.js'
|
||||
import type {
|
||||
AssistantMessage,
|
||||
Message as MessageType,
|
||||
} from '../../types/message.js'
|
||||
import { logForDebugging } from '../../utils/debug.js'
|
||||
import { createUserMessage } from '../../utils/messages.js'
|
||||
import type { BuiltInAgentDefinition } from './loadAgentsDir.js'
|
||||
|
||||
/**
|
||||
* Fork subagent 特性门控。
|
||||
*
|
||||
* 启用时:
|
||||
* - `subagent_type` 在 Agent 工具 schema 上变为可选
|
||||
* - 省略 `subagent_type` 触发隐式 fork:子代理继承
|
||||
* 父代理的完整对话上下文和系统提示
|
||||
* - 所有代理生成都在后台运行(async)以获得统一的
|
||||
* `<task-notification>` 交互模型
|
||||
* - 可用 `/fork <directive>` 斜杠命令
|
||||
*
|
||||
* 与协调器模式互斥——协调器已经拥有
|
||||
* 编排角色并有自己的委托模型。
|
||||
*/
|
||||
export function isForkSubagentEnabled(): boolean {
|
||||
if (feature('FORK_SUBAGENT')) {
|
||||
if (isCoordinatorMode()) return false
|
||||
if (getIsNonInteractiveSession()) return false
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/** 当 fork 路径触发时用于分析的综合代理类型名称。 */
|
||||
export const FORK_SUBAGENT_TYPE = 'fork'
|
||||
|
||||
/**
|
||||
* fork 路径的综合代理定义。
|
||||
*
|
||||
* 不在 builtInAgents 中注册——仅在 `!subagent_type` 且
|
||||
* 实验激活时使用。`tools: ['*']` 配合 `useExactTools` 意味着 fork
|
||||
* 子代理接收父代理的确切工具池(用于缓存相同的 API
|
||||
* 前缀)。`permissionMode: 'bubble'` 将权限提示显示到
|
||||
* 父终端。`model: 'inherit'` 保持父代理的模型以获得上下文
|
||||
* 长度对等。
|
||||
*
|
||||
* 这里的 getSystemPrompt 未使用:fork 路径传递
|
||||
* 带父代理已渲染系统提示字节的 `override.systemPrompt`,
|
||||
* 通过 `toolUseContext.renderedSystemPrompt` 线程化。重新调用
|
||||
* getSystemPrompt() 可能会产生分歧(GrowthBook cold→warm)并
|
||||
* 破坏提示缓存;线程化渲染字节是字节精确的。
|
||||
*/
|
||||
export const FORK_AGENT = {
|
||||
agentType: FORK_SUBAGENT_TYPE,
|
||||
whenToUse:
|
||||
'Implicit fork — inherits full conversation context. Not selectable via subagent_type; triggered by omitting subagent_type when the fork experiment is active.',
|
||||
tools: ['*'],
|
||||
maxTurns: 200,
|
||||
model: 'inherit',
|
||||
permissionMode: 'bubble',
|
||||
source: 'built-in',
|
||||
baseDir: 'built-in',
|
||||
getSystemPrompt: () => '',
|
||||
} satisfies BuiltInAgentDefinition
|
||||
|
||||
/**
|
||||
* 防止递归 forking 的守卫。Fork 子代理在它们的
|
||||
* 工具池中保留 Agent 工具以获得缓存相同的工具定义,
|
||||
* 所以我们通过检测对话历史中 fork boilerplate 标签
|
||||
* 在调用时拒绝 fork 尝试。
|
||||
*/
|
||||
export function isInForkChild(messages: MessageType[]): boolean {
|
||||
return messages.some(m => {
|
||||
if (m.type !== 'user') return false
|
||||
const content = m.message.content
|
||||
if (!Array.isArray(content)) return false
|
||||
return content.some(
|
||||
block =>
|
||||
block.type === 'text' &&
|
||||
block.text.includes(`<${FORK_BOILERPLATE_TAG}>`),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/** 用于所有 fork 前缀中 tool_result 块的占位符文本。
|
||||
* 必须在所有 fork 子代理中相同以共享提示缓存。 */
|
||||
const FORK_PLACEHOLDER_RESULT = 'Fork started — processing in background'
|
||||
|
||||
/**
|
||||
* 为子代理构建 fork 的对话消息。
|
||||
*
|
||||
* 为了提示缓存共享,所有 fork 子代理必须产生字节相同的
|
||||
* API 请求前缀。此函数:
|
||||
* 1. 保留完整的父助手消息(所有 tool_use 块、thinking、text)
|
||||
* 2. 为每个 tool_use 块使用相同占位符构建单个用户消息
|
||||
* 然后追加每个子代理的指令文本块
|
||||
*
|
||||
* 结果:[...history, assistant(all_tool_uses), user(placeholder_results..., directive)]
|
||||
* 只有最后一个文本块每个子代理不同,最大化缓存命中。
|
||||
*/
|
||||
export function buildForkedMessages(
|
||||
directive: string,
|
||||
assistantMessage: AssistantMessage,
|
||||
): MessageType[] {
|
||||
// 克隆助手消息以避免改变原始消息,保留所有
|
||||
// 内容块(thinking、text 和每个 tool_use)
|
||||
const fullAssistantMessage: AssistantMessage = {
|
||||
...assistantMessage,
|
||||
uuid: randomUUID(),
|
||||
message: {
|
||||
...assistantMessage.message,
|
||||
content: [...assistantMessage.message.content],
|
||||
},
|
||||
}
|
||||
|
||||
// 从助手消息收集所有 tool_use 块
|
||||
const toolUseBlocks = assistantMessage.message.content.filter(
|
||||
(block): block is BetaToolUseBlock => block.type === 'tool_use',
|
||||
)
|
||||
|
||||
if (toolUseBlocks.length === 0) {
|
||||
logForDebugging(
|
||||
`No tool_use blocks found in assistant message for fork directive: ${directive.slice(0, 50)}...`,
|
||||
{ level: 'error' },
|
||||
)
|
||||
return [
|
||||
createUserMessage({
|
||||
content: [
|
||||
{ type: 'text' as const, text: buildChildMessage(directive) },
|
||||
],
|
||||
}),
|
||||
]
|
||||
}
|
||||
|
||||
// 为每个 tool_use 构建 tool_result 块,都使用相同占位符文本
|
||||
const toolResultBlocks = toolUseBlocks.map(block => ({
|
||||
type: 'tool_result' as const,
|
||||
tool_use_id: block.id,
|
||||
content: [
|
||||
{
|
||||
type: 'text' as const,
|
||||
text: FORK_PLACEHOLDER_RESULT,
|
||||
},
|
||||
],
|
||||
}))
|
||||
|
||||
// 构建单个用户消息:所有占位符 tool_results + 每个子代理的指令
|
||||
// TODO(smoosh): 这个文本兄弟在 wire 上创建了 [tool_result, text] 模式
|
||||
//(渲染为 </function_results>\n\nHuman:<text>)。每个子代理构建一次,
|
||||
// 不是重复的 teacher,所以优先级低。如果我们关心,使用 smooshIntoToolResult
|
||||
// 从 src/utils/messages.ts 将指令折叠到最后一个 tool_result.content。
|
||||
const toolResultMessage = createUserMessage({
|
||||
content: [
|
||||
...toolResultBlocks,
|
||||
{
|
||||
type: 'text' as const,
|
||||
text: buildChildMessage(directive),
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
return [fullAssistantMessage, toolResultMessage]
|
||||
}
|
||||
|
||||
export function buildChildMessage(directive: string): string {
|
||||
return `<${FORK_BOILERPLATE_TAG}>
|
||||
STOP. READ THIS FIRST.
|
||||
|
||||
You are a forked worker process. You are NOT the main agent.
|
||||
|
||||
RULES (non-negotiable):
|
||||
1. Your system prompt says "default to forking." IGNORE IT — that's for the parent. You ARE the fork. Do NOT spawn sub-agents; execute directly.
|
||||
2. Do NOT converse, ask questions, or suggest next steps
|
||||
3. Do NOT editorialize or add meta-commentary
|
||||
4. USE your tools directly: Bash, Read, Write, etc.
|
||||
5. If you modify files, commit your changes before reporting. Include the commit hash in your report.
|
||||
6. Do NOT emit text between tool calls. Use tools silently, then report once at the end.
|
||||
7. Stay strictly within your directive's scope. If you discover related systems outside your scope, mention them in one sentence at most — other workers cover those areas.
|
||||
8. Keep your report under 500 words unless the directive specifies otherwise. Be factual and concise.
|
||||
9. Your response MUST begin with "Scope:". No preamble, no thinking-out-loud.
|
||||
10. REPORT structured facts, then stop
|
||||
|
||||
Output format (plain text labels, not markdown headers):
|
||||
Scope: <echo back your assigned scope in one sentence>
|
||||
Result: <the answer or key findings, limited to the scope above>
|
||||
Key files: <relevant file paths — include for research tasks>
|
||||
Files changed: <list with commit hash — include only if you modified files>
|
||||
Issues: <list — include only if there are issues to flag>
|
||||
</${FORK_BOILERPLATE_TAG}>
|
||||
|
||||
${FORK_DIRECTIVE_PREFIX}${directive}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 注入在隔离 worktree 中运行的 fork 子代理的通知。
|
||||
* 告诉子代理从继承的上下文转换路径,重新读取
|
||||
* 可能过时的文件,其更改是隔离的。
|
||||
*/
|
||||
export function buildWorktreeNotice(
|
||||
parentCwd: string,
|
||||
worktreeCwd: string,
|
||||
): string {
|
||||
return `You've inherited the conversation context above from a parent agent working in ${parentCwd}. You are operating in an isolated git worktree at ${worktreeCwd} — same repository, same relative file structure, separate working copy. Paths in the inherited context refer to the parent's working directory; translate them to your worktree root. Re-read files before editing if the parent may have modified them since they appear in the context. Your changes stay in this worktree and will not affect the parent's files.`
|
||||
}
|
||||
755
claude-code源码-中文注释/src/tools/AgentTool/loadAgentsDir.ts
Normal file
755
claude-code源码-中文注释/src/tools/AgentTool/loadAgentsDir.ts
Normal file
@@ -0,0 +1,755 @@
|
||||
import { feature } from 'bun:bundle'
|
||||
import memoize from 'lodash-es/memoize.js'
|
||||
import { basename } from 'path'
|
||||
import type { SettingSource } from 'src/utils/settings/constants.js'
|
||||
import { z } from 'zod/v4'
|
||||
import { isAutoMemoryEnabled } from '../../memdir/paths.js'
|
||||
import {
|
||||
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
logEvent,
|
||||
} from '../../services/analytics/index.js'
|
||||
import {
|
||||
type McpServerConfig,
|
||||
McpServerConfigSchema,
|
||||
} from '../../services/mcp/types.js'
|
||||
import type { ToolUseContext } from '../../Tool.js'
|
||||
import { logForDebugging } from '../../utils/debug.js'
|
||||
import {
|
||||
EFFORT_LEVELS,
|
||||
type EffortValue,
|
||||
parseEffortValue,
|
||||
} from '../../utils/effort.js'
|
||||
import { isEnvTruthy } from '../../utils/envUtils.js'
|
||||
import { parsePositiveIntFromFrontmatter } from '../../utils/frontmatterParser.js'
|
||||
import { lazySchema } from '../../utils/lazySchema.js'
|
||||
import { logError } from '../../utils/log.js'
|
||||
import {
|
||||
loadMarkdownFilesForSubdir,
|
||||
parseAgentToolsFromFrontmatter,
|
||||
parseSlashCommandToolsFromFrontmatter,
|
||||
} from '../../utils/markdownConfigLoader.js'
|
||||
import {
|
||||
PERMISSION_MODES,
|
||||
type PermissionMode,
|
||||
} from '../../utils/permissions/PermissionMode.js'
|
||||
import {
|
||||
clearPluginAgentCache,
|
||||
loadPluginAgents,
|
||||
} from '../../utils/plugins/loadPluginAgents.js'
|
||||
import { HooksSchema, type HooksSettings } from '../../utils/settings/types.js'
|
||||
import { jsonStringify } from '../../utils/slowOperations.js'
|
||||
import { FILE_EDIT_TOOL_NAME } from '../FileEditTool/constants.js'
|
||||
import { FILE_READ_TOOL_NAME } from '../FileReadTool/prompt.js'
|
||||
import { FILE_WRITE_TOOL_NAME } from '../FileWriteTool/prompt.js'
|
||||
import {
|
||||
AGENT_COLORS,
|
||||
type AgentColorName,
|
||||
setAgentColor,
|
||||
} from './agentColorManager.js'
|
||||
import { type AgentMemoryScope, loadAgentMemoryPrompt } from './agentMemory.js'
|
||||
import {
|
||||
checkAgentMemorySnapshot,
|
||||
initializeFromSnapshot,
|
||||
} from './agentMemorySnapshot.js'
|
||||
import { getBuiltInAgents } from './builtInAgents.js'
|
||||
|
||||
// 代理定义中 MCP 服务器规范的类型
|
||||
// 可以是对现有服务器名称的引用,也可以是 { [name]: config } 的内联定义
|
||||
export type AgentMcpServerSpec =
|
||||
| string // 按名称引用现有服务器(例如,"slack")
|
||||
| { [name: string]: McpServerConfig } // 作为 { name: config } 的内联定义
|
||||
|
||||
// 代理 MCP 服务器规范的 Zod schema
|
||||
const AgentMcpServerSpecSchema = lazySchema(() =>
|
||||
z.union([
|
||||
z.string(), // 按名称引用
|
||||
z.record(z.string(), McpServerConfigSchema()), // 作为 { name: config } 的内联
|
||||
]),
|
||||
)
|
||||
|
||||
// JSON 代理验证的 Zod schema
|
||||
// 注意:HooksSchema 是延迟的,所以循环链 AppState -> loadAgentsDir -> settings/types
|
||||
// 在模块加载时间被打破
|
||||
const AgentJsonSchema = lazySchema(() =>
|
||||
z.object({
|
||||
description: z.string().min(1, 'Description cannot be empty'),
|
||||
tools: z.array(z.string()).optional(),
|
||||
disallowedTools: z.array(z.string()).optional(),
|
||||
prompt: z.string().min(1, 'Prompt cannot be empty'),
|
||||
model: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, 'Model cannot be empty')
|
||||
.transform(m => (m.toLowerCase() === 'inherit' ? 'inherit' : m))
|
||||
.optional(),
|
||||
effort: z.union([z.enum(EFFORT_LEVELS), z.number().int()]).optional(),
|
||||
permissionMode: z.enum(PERMISSION_MODES).optional(),
|
||||
mcpServers: z.array(AgentMcpServerSpecSchema()).optional(),
|
||||
hooks: HooksSchema().optional(),
|
||||
maxTurns: z.number().int().positive().optional(),
|
||||
skills: z.array(z.string()).optional(),
|
||||
initialPrompt: z.string().optional(),
|
||||
memory: z.enum(['user', 'project', 'local']).optional(),
|
||||
background: z.boolean().optional(),
|
||||
isolation: (process.env.USER_TYPE === 'ant'
|
||||
? z.enum(['worktree', 'remote'])
|
||||
: z.enum(['worktree'])
|
||||
).optional(),
|
||||
}),
|
||||
)
|
||||
|
||||
const AgentsJsonSchema = lazySchema(() =>
|
||||
z.record(z.string(), AgentJsonSchema()),
|
||||
)
|
||||
|
||||
// 所有代理共有的基类类型
|
||||
export type BaseAgentDefinition = {
|
||||
agentType: string
|
||||
whenToUse: string
|
||||
tools?: string[]
|
||||
disallowedTools?: string[]
|
||||
skills?: string[] // 要预加载的技能名称(从逗号分隔的 frontmatter 解析)
|
||||
mcpServers?: AgentMcpServerSpec[] // 此代理特定的 MCP 服务器
|
||||
hooks?: HooksSettings // 代理启动时注册的会话范围 hooks
|
||||
color?: AgentColorName
|
||||
model?: string
|
||||
effort?: EffortValue
|
||||
permissionMode?: PermissionMode
|
||||
maxTurns?: number // 停止前的最大代理轮次数
|
||||
filename?: string // 原始文件名不带 .md 扩展名(用于用户/项目/托管代理)
|
||||
baseDir?: string
|
||||
criticalSystemReminder_EXPERIMENTAL?: string // 在每个用户轮次重新注入的短消息
|
||||
requiredMcpServers?: string[] // 代理可用需要配置的 MCP 服务器名称模式
|
||||
background?: boolean // 生成时始终作为后台任务运行
|
||||
initialPrompt?: string // 预置到第一个用户轮次(斜杠命令有效)
|
||||
memory?: AgentMemoryScope // 持久内存范围
|
||||
isolation?: 'worktree' | 'remote' // 在隔离的 git worktree 中运行,或在 CCR 中远程运行(仅 ant)
|
||||
pendingSnapshotUpdate?: { snapshotTimestamp: string }
|
||||
/** 从代理的 userContext 中省略 CLAUDE.md 层次结构。只读代理
|
||||
*(Explore、Plan)不需要 commit/PR/lint 指南——主代理有
|
||||
* 完整 CLAUDE.md 并解释它们的输出。每周 34M+ Explore 生成
|
||||
* 节省约 5-15 Gtok/week。Kill-switch: tengu_slim_subagent_claudemd。 */
|
||||
omitClaudeMd?: boolean
|
||||
}
|
||||
|
||||
// 内置代理——仅动态提示,无静态 systemPrompt 字段
|
||||
export type BuiltInAgentDefinition = BaseAgentDefinition & {
|
||||
source: 'built-in'
|
||||
baseDir: 'built-in'
|
||||
callback?: () => void
|
||||
getSystemPrompt: (params: {
|
||||
toolUseContext: Pick<ToolUseContext, 'options'>
|
||||
}) => string
|
||||
}
|
||||
|
||||
// 自定义代理来自用户/项目/策略设置——通过闭包存储提示
|
||||
export type CustomAgentDefinition = BaseAgentDefinition & {
|
||||
getSystemPrompt: () => string
|
||||
source: SettingSource
|
||||
filename?: string
|
||||
baseDir?: string
|
||||
}
|
||||
|
||||
// 插件代理——类似于自定义但带有插件元数据,通过闭包存储提示
|
||||
export type PluginAgentDefinition = BaseAgentDefinition & {
|
||||
getSystemPrompt: () => string
|
||||
source: 'plugin'
|
||||
filename?: string
|
||||
plugin: string
|
||||
}
|
||||
|
||||
// 所有代理类型的联合类型
|
||||
export type AgentDefinition =
|
||||
| BuiltInAgentDefinition
|
||||
| CustomAgentDefinition
|
||||
| PluginAgentDefinition
|
||||
|
||||
// 运行时类型检查的类型守卫
|
||||
export function isBuiltInAgent(
|
||||
agent: AgentDefinition,
|
||||
): agent is BuiltInAgentDefinition {
|
||||
return agent.source === 'built-in'
|
||||
}
|
||||
|
||||
export function isCustomAgent(
|
||||
agent: AgentDefinition,
|
||||
): agent is CustomAgentDefinition {
|
||||
return agent.source !== 'built-in' && agent.source !== 'plugin'
|
||||
}
|
||||
|
||||
export function isPluginAgent(
|
||||
agent: AgentDefinition,
|
||||
): agent is PluginAgentDefinition {
|
||||
return agent.source === 'plugin'
|
||||
}
|
||||
|
||||
export type AgentDefinitionsResult = {
|
||||
activeAgents: AgentDefinition[]
|
||||
allAgents: AgentDefinition[]
|
||||
failedFiles?: Array<{ path: string; error: string }>
|
||||
allowedAgentTypes?: string[]
|
||||
}
|
||||
|
||||
export function getActiveAgentsFromList(
|
||||
allAgents: AgentDefinition[],
|
||||
): AgentDefinition[] {
|
||||
const builtInAgents = allAgents.filter(a => a.source === 'built-in')
|
||||
const pluginAgents = allAgents.filter(a => a.source === 'plugin')
|
||||
const userAgents = allAgents.filter(a => a.source === 'userSettings')
|
||||
const projectAgents = allAgents.filter(a => a.source === 'projectSettings')
|
||||
const managedAgents = allAgents.filter(a => a.source === 'policySettings')
|
||||
const flagAgents = allAgents.filter(a => a.source === 'flagSettings')
|
||||
|
||||
const agentGroups = [
|
||||
builtInAgents,
|
||||
pluginAgents,
|
||||
userAgents,
|
||||
projectAgents,
|
||||
flagAgents,
|
||||
managedAgents,
|
||||
]
|
||||
|
||||
const agentMap = new Map<string, AgentDefinition>()
|
||||
|
||||
for (const agents of agentGroups) {
|
||||
for (const agent of agents) {
|
||||
agentMap.set(agent.agentType, agent)
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(agentMap.values())
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查代理的必需 MCP 服务器是否可用。
|
||||
* 如果没有要求或所有要求都满足则返回 true。
|
||||
* @param agent 要检查的代理
|
||||
* @param availableServers 可用 MCP 服务器名称列表(例如,来自 mcp.clients)
|
||||
*/
|
||||
export function hasRequiredMcpServers(
|
||||
agent: AgentDefinition,
|
||||
availableServers: string[],
|
||||
): boolean {
|
||||
if (!agent.requiredMcpServers || agent.requiredMcpServers.length === 0) {
|
||||
return true
|
||||
}
|
||||
// 每个必需模式必须匹配至少一个可用服务器(不区分大小写)
|
||||
return agent.requiredMcpServers.every(pattern =>
|
||||
availableServers.some(server =>
|
||||
server.toLowerCase().includes(pattern.toLowerCase()),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 MCP 服务器要求过滤代理。
|
||||
* 仅返回其必需 MCP 服务器可用的代理。
|
||||
* @param agents 要过滤的代理列表
|
||||
* @param availableServers 可用 MCP 服务器名称列表
|
||||
*/
|
||||
export function filterAgentsByMcpRequirements(
|
||||
agents: AgentDefinition[],
|
||||
availableServers: string[],
|
||||
): AgentDefinition[] {
|
||||
return agents.filter(agent => hasRequiredMcpServers(agent, availableServers))
|
||||
}
|
||||
|
||||
/**
|
||||
* 从项目快照检查并初始化代理内存。
|
||||
* 对于启用内存的代理,如果不存在本地内存则复制快照到本地。
|
||||
* 对于有更新快照的代理,记录调试消息(用户提示 TODO)。
|
||||
*/
|
||||
async function initializeAgentMemorySnapshots(
|
||||
agents: CustomAgentDefinition[],
|
||||
): Promise<void> {
|
||||
await Promise.all(
|
||||
agents.map(async agent => {
|
||||
if (agent.memory !== 'user') return
|
||||
const result = await checkAgentMemorySnapshot(
|
||||
agent.agentType,
|
||||
agent.memory,
|
||||
)
|
||||
switch (result.action) {
|
||||
case 'initialize':
|
||||
logForDebugging(
|
||||
`Initializing ${agent.agentType} memory from project snapshot`,
|
||||
)
|
||||
await initializeFromSnapshot(
|
||||
agent.agentType,
|
||||
agent.memory,
|
||||
result.snapshotTimestamp!,
|
||||
)
|
||||
break
|
||||
case 'prompt-update':
|
||||
agent.pendingSnapshotUpdate = {
|
||||
snapshotTimestamp: result.snapshotTimestamp!,
|
||||
}
|
||||
logForDebugging(
|
||||
`Newer snapshot available for ${agent.agentType} memory (snapshot: ${result.snapshotTimestamp})`,
|
||||
)
|
||||
break
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
export const getAgentDefinitionsWithOverrides = memoize(
|
||||
async (cwd: string): Promise<AgentDefinitionsResult> => {
|
||||
// 简单模式:跳过自定义代理,仅返回内置代理
|
||||
if (isEnvTruthy(process.env.CLAUDE_CODE_SIMPLE)) {
|
||||
const builtInAgents = getBuiltInAgents()
|
||||
return {
|
||||
activeAgents: builtInAgents,
|
||||
allAgents: builtInAgents,
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const markdownFiles = await loadMarkdownFilesForSubdir('agents', cwd)
|
||||
|
||||
const failedFiles: Array<{ path: string; error: string }> = []
|
||||
const customAgents = markdownFiles
|
||||
.map(({ filePath, baseDir, frontmatter, content, source }) => {
|
||||
const agent = parseAgentFromMarkdown(
|
||||
filePath,
|
||||
baseDir,
|
||||
frontmatter,
|
||||
content,
|
||||
source,
|
||||
)
|
||||
if (!agent) {
|
||||
// 静默跳过非代理 markdown 文件(例如,与代理定义
|
||||
// 放在一起的参考文档)。仅报告看起来像代理
|
||||
// 尝试的文件错误(在 frontmatter 中有 'name' 字段)。
|
||||
if (!frontmatter['name']) {
|
||||
return null
|
||||
}
|
||||
const errorMsg = getParseError(frontmatter)
|
||||
failedFiles.push({ path: filePath, error: errorMsg })
|
||||
logForDebugging(
|
||||
`Failed to parse agent from ${filePath}: ${errorMsg}`,
|
||||
)
|
||||
logEvent('tengu_agent_parse_error', {
|
||||
error:
|
||||
errorMsg as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
location:
|
||||
source as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
})
|
||||
return null
|
||||
}
|
||||
return agent
|
||||
})
|
||||
.filter(agent => agent !== null)
|
||||
|
||||
// 与内存快照初始化并发启动插件代理加载——
|
||||
// loadPluginAgents 已 memoize 且不需要参数,所以它是独立的。
|
||||
// 连接两者,以便如果另一个抛出则两者都不会成为浮点 promise。
|
||||
let pluginAgentsPromise = loadPluginAgents()
|
||||
if (feature('AGENT_MEMORY_SNAPSHOT') && isAutoMemoryEnabled()) {
|
||||
const [pluginAgents_] = await Promise.all([
|
||||
pluginAgentsPromise,
|
||||
initializeAgentMemorySnapshots(customAgents),
|
||||
])
|
||||
pluginAgentsPromise = Promise.resolve(pluginAgents_)
|
||||
}
|
||||
const pluginAgents = await pluginAgentsPromise
|
||||
|
||||
const builtInAgents = getBuiltInAgents()
|
||||
|
||||
const allAgentsList: AgentDefinition[] = [
|
||||
...builtInAgents,
|
||||
...pluginAgents,
|
||||
...customAgents,
|
||||
]
|
||||
|
||||
const activeAgents = getActiveAgentsFromList(allAgentsList)
|
||||
|
||||
// 为所有活动代理初始化颜色
|
||||
for (const agent of activeAgents) {
|
||||
if (agent.color) {
|
||||
setAgentColor(agent.agentType, agent.color)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
activeAgents,
|
||||
allAgents: allAgentsList,
|
||||
failedFiles: failedFiles.length > 0 ? failedFiles : undefined,
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : String(error)
|
||||
logForDebugging(`Error loading agent definitions: ${errorMessage}`)
|
||||
logError(error)
|
||||
// 即使出错,也返回内置代理
|
||||
const builtInAgents = getBuiltInAgents()
|
||||
return {
|
||||
activeAgents: builtInAgents,
|
||||
allAgents: builtInAgents,
|
||||
failedFiles: [{ path: 'unknown', error: errorMessage }],
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
export function clearAgentDefinitionsCache(): void {
|
||||
getAgentDefinitionsWithOverrides.cache.clear?.()
|
||||
clearPluginAgentCache()
|
||||
}
|
||||
|
||||
/**
|
||||
* 帮助确定代理文件的具体解析错误
|
||||
*/
|
||||
function getParseError(frontmatter: Record<string, unknown>): string {
|
||||
const agentType = frontmatter['name']
|
||||
const description = frontmatter['description']
|
||||
|
||||
if (!agentType || typeof agentType !== 'string') {
|
||||
return 'Missing required "name" field in frontmatter'
|
||||
}
|
||||
|
||||
if (!description || typeof description !== 'string') {
|
||||
return 'Missing required "description" field in frontmatter'
|
||||
}
|
||||
|
||||
return 'Unknown parsing error'
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用 HooksSchema 从 frontmatter 解析 hooks
|
||||
* @param frontmatter 包含潜在 hooks 的 frontmatter 对象
|
||||
* @param agentType 用于日志记录的代理类型
|
||||
* @returns 解析的 hooks 设置,如果无效/缺失则返回 undefined
|
||||
*/
|
||||
function parseHooksFromFrontmatter(
|
||||
frontmatter: Record<string, unknown>,
|
||||
agentType: string,
|
||||
): HooksSettings | undefined {
|
||||
if (!frontmatter.hooks) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const result = HooksSchema().safeParse(frontmatter.hooks)
|
||||
if (!result.success) {
|
||||
logForDebugging(
|
||||
`Invalid hooks in agent '${agentType}': ${result.error.message}`,
|
||||
)
|
||||
return undefined
|
||||
}
|
||||
return result.data
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 JSON 数据解析代理定义
|
||||
*/
|
||||
export function parseAgentFromJson(
|
||||
name: string,
|
||||
definition: unknown,
|
||||
source: SettingSource = 'flagSettings',
|
||||
): CustomAgentDefinition | null {
|
||||
try {
|
||||
const parsed = AgentJsonSchema().parse(definition)
|
||||
|
||||
let tools = parseAgentToolsFromFrontmatter(parsed.tools)
|
||||
|
||||
// 如果启用了内存,注入 Write/Edit/Read 工具以访问内存
|
||||
if (isAutoMemoryEnabled() && parsed.memory && tools !== undefined) {
|
||||
const toolSet = new Set(tools)
|
||||
for (const tool of [
|
||||
FILE_WRITE_TOOL_NAME,
|
||||
FILE_EDIT_TOOL_NAME,
|
||||
FILE_READ_TOOL_NAME,
|
||||
]) {
|
||||
if (!toolSet.has(tool)) {
|
||||
tools = [...tools, tool]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const disallowedTools =
|
||||
parsed.disallowedTools !== undefined
|
||||
? parseAgentToolsFromFrontmatter(parsed.disallowedTools)
|
||||
: undefined
|
||||
|
||||
const systemPrompt = parsed.prompt
|
||||
|
||||
const agent: CustomAgentDefinition = {
|
||||
agentType: name,
|
||||
whenToUse: parsed.description,
|
||||
...(tools !== undefined ? { tools } : {}),
|
||||
...(disallowedTools !== undefined ? { disallowedTools } : {}),
|
||||
getSystemPrompt: () => {
|
||||
if (isAutoMemoryEnabled() && parsed.memory) {
|
||||
return (
|
||||
systemPrompt + '\n\n' + loadAgentMemoryPrompt(name, parsed.memory)
|
||||
)
|
||||
}
|
||||
return systemPrompt
|
||||
},
|
||||
source,
|
||||
...(parsed.model ? { model: parsed.model } : {}),
|
||||
...(parsed.effort !== undefined ? { effort: parsed.effort } : {}),
|
||||
...(parsed.permissionMode
|
||||
? { permissionMode: parsed.permissionMode }
|
||||
: {}),
|
||||
...(parsed.mcpServers && parsed.mcpServers.length > 0
|
||||
? { mcpServers: parsed.mcpServers }
|
||||
: {}),
|
||||
...(parsed.hooks ? { hooks: parsed.hooks } : {}),
|
||||
...(parsed.maxTurns !== undefined ? { maxTurns: parsed.maxTurns } : {}),
|
||||
...(parsed.skills && parsed.skills.length > 0
|
||||
? { skills: parsed.skills }
|
||||
: {}),
|
||||
...(parsed.initialPrompt ? { initialPrompt: parsed.initialPrompt } : {}),
|
||||
...(parsed.background ? { background: parsed.background } : {}),
|
||||
...(parsed.memory ? { memory: parsed.memory } : {}),
|
||||
...(parsed.isolation ? { isolation: parsed.isolation } : {}),
|
||||
}
|
||||
|
||||
return agent
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
logForDebugging(`Error parsing agent '${name}' from JSON: ${errorMessage}`)
|
||||
logError(error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 JSON 对象解析多个代理
|
||||
*/
|
||||
export function parseAgentsFromJson(
|
||||
agentsJson: unknown,
|
||||
source: SettingSource = 'flagSettings',
|
||||
): AgentDefinition[] {
|
||||
try {
|
||||
const parsed = AgentsJsonSchema().parse(agentsJson)
|
||||
return Object.entries(parsed)
|
||||
.map(([name, def]) => parseAgentFromJson(name, def, source))
|
||||
.filter((agent): agent is CustomAgentDefinition => agent !== null)
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
logForDebugging(`Error parsing agents from JSON: ${errorMessage}`)
|
||||
logError(error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 markdown 文件数据解析代理定义
|
||||
*/
|
||||
export function parseAgentFromMarkdown(
|
||||
filePath: string,
|
||||
baseDir: string,
|
||||
frontmatter: Record<string, unknown>,
|
||||
content: string,
|
||||
source: SettingSource,
|
||||
): CustomAgentDefinition | null {
|
||||
try {
|
||||
const agentType = frontmatter['name']
|
||||
let whenToUse = frontmatter['description'] as string
|
||||
|
||||
// 验证必需字段——静默跳过没有任何代理
|
||||
// frontmatter 的文件(它们可能是放在一起的参考文档)
|
||||
if (!agentType || typeof agentType !== 'string') {
|
||||
return null
|
||||
}
|
||||
if (!whenToUse || typeof whenToUse !== 'string') {
|
||||
logForDebugging(
|
||||
`Agent file ${filePath} is missing required 'description' in frontmatter`,
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
// 取消转义 whenToUse 中为 YAML 解析而转义的新行
|
||||
whenToUse = whenToUse.replace(/\\n/g, '\n')
|
||||
|
||||
const color = frontmatter['color'] as AgentColorName | undefined
|
||||
const modelRaw = frontmatter['model']
|
||||
let model: string | undefined
|
||||
if (typeof modelRaw === 'string' && modelRaw.trim().length > 0) {
|
||||
const trimmed = modelRaw.trim()
|
||||
model = trimmed.toLowerCase() === 'inherit' ? 'inherit' : trimmed
|
||||
}
|
||||
|
||||
// 解析 background 标志
|
||||
const backgroundRaw = frontmatter['background']
|
||||
|
||||
if (
|
||||
backgroundRaw !== undefined &&
|
||||
backgroundRaw !== 'true' &&
|
||||
backgroundRaw !== 'false' &&
|
||||
backgroundRaw !== true &&
|
||||
backgroundRaw !== false
|
||||
) {
|
||||
logForDebugging(
|
||||
`Agent file ${filePath} has invalid background value '${backgroundRaw}'. Must be 'true', 'false', or omitted.`,
|
||||
)
|
||||
}
|
||||
|
||||
const background =
|
||||
backgroundRaw === 'true' || backgroundRaw === true ? true : undefined
|
||||
|
||||
// 解析 memory 范围
|
||||
const VALID_MEMORY_SCOPES: AgentMemoryScope[] = ['user', 'project', 'local']
|
||||
const memoryRaw = frontmatter['memory'] as string | undefined
|
||||
let memory: AgentMemoryScope | undefined
|
||||
if (memoryRaw !== undefined) {
|
||||
if (VALID_MEMORY_SCOPES.includes(memoryRaw as AgentMemoryScope)) {
|
||||
memory = memoryRaw as AgentMemoryScope
|
||||
} else {
|
||||
logForDebugging(
|
||||
`Agent file ${filePath} has invalid memory value '${memoryRaw}'. Valid options: ${VALID_MEMORY_SCOPES.join(', ')}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 解析 isolation 模式。'remote' 仅限 ant;外部构建在解析时拒绝它。
|
||||
type IsolationMode = 'worktree' | 'remote'
|
||||
const VALID_ISOLATION_MODES: readonly IsolationMode[] =
|
||||
process.env.USER_TYPE === 'ant' ? ['worktree', 'remote'] : ['worktree']
|
||||
const isolationRaw = frontmatter['isolation'] as string | undefined
|
||||
let isolation: IsolationMode | undefined
|
||||
if (isolationRaw !== undefined) {
|
||||
if (VALID_ISOLATION_MODES.includes(isolationRaw as IsolationMode)) {
|
||||
isolation = isolationRaw as IsolationMode
|
||||
} else {
|
||||
logForDebugging(
|
||||
`Agent file ${filePath} has invalid isolation value '${isolationRaw}'. Valid options: ${VALID_ISOLATION_MODES.join(', ')}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 从 frontmatter 解析 effort(支持字符串级别和整数)
|
||||
const effortRaw = frontmatter['effort']
|
||||
const parsedEffort =
|
||||
effortRaw !== undefined ? parseEffortValue(effortRaw) : undefined
|
||||
|
||||
if (effortRaw !== undefined && parsedEffort === undefined) {
|
||||
logForDebugging(
|
||||
`Agent file ${filePath} has invalid effort '${effortRaw}'. Valid options: ${EFFORT_LEVELS.join(', ')} or an integer`,
|
||||
)
|
||||
}
|
||||
|
||||
// 从 frontmatter 解析 permissionMode
|
||||
const permissionModeRaw = frontmatter['permissionMode'] as
|
||||
| string
|
||||
| undefined
|
||||
const isValidPermissionMode =
|
||||
permissionModeRaw &&
|
||||
(PERMISSION_MODES as readonly string[]).includes(permissionModeRaw)
|
||||
|
||||
if (permissionModeRaw && !isValidPermissionMode) {
|
||||
const errorMsg = `Agent file ${filePath} has invalid permissionMode '${permissionModeRaw}'. Valid options: ${PERMISSION_MODES.join(', ')}`
|
||||
logForDebugging(errorMsg)
|
||||
}
|
||||
|
||||
// 从 frontmatter 解析 maxTurns
|
||||
const maxTurnsRaw = frontmatter['maxTurns']
|
||||
const maxTurns = parsePositiveIntFromFrontmatter(maxTurnsRaw)
|
||||
if (maxTurnsRaw !== undefined && maxTurns === undefined) {
|
||||
logForDebugging(
|
||||
`Agent file ${filePath} has invalid maxTurns '${maxTurnsRaw}'. Must be a positive integer.`,
|
||||
)
|
||||
}
|
||||
|
||||
// 提取不带扩展名的文件名
|
||||
const filename = basename(filePath, '.md')
|
||||
|
||||
// 从 frontmatter 解析工具
|
||||
let tools = parseAgentToolsFromFrontmatter(frontmatter['tools'])
|
||||
|
||||
// 如果启用了内存,注入 Write/Edit/Read 工具以访问内存
|
||||
if (isAutoMemoryEnabled() && memory && tools !== undefined) {
|
||||
const toolSet = new Set(tools)
|
||||
for (const tool of [
|
||||
FILE_WRITE_TOOL_NAME,
|
||||
FILE_EDIT_TOOL_NAME,
|
||||
FILE_READ_TOOL_NAME,
|
||||
]) {
|
||||
if (!toolSet.has(tool)) {
|
||||
tools = [...tools, tool]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 从 frontmatter 解析 disallowedTools
|
||||
const disallowedToolsRaw = frontmatter['disallowedTools']
|
||||
const disallowedTools =
|
||||
disallowedToolsRaw !== undefined
|
||||
? parseAgentToolsFromFrontmatter(disallowedToolsRaw)
|
||||
: undefined
|
||||
|
||||
// 从 frontmatter 解析 skills
|
||||
const skills = parseSlashCommandToolsFromFrontmatter(frontmatter['skills'])
|
||||
|
||||
const initialPromptRaw = frontmatter['initialPrompt']
|
||||
const initialPrompt =
|
||||
typeof initialPromptRaw === 'string' && initialPromptRaw.trim()
|
||||
? initialPromptRaw
|
||||
: undefined
|
||||
|
||||
// 使用与 JSON 代理相同的 Zod 验证从 frontmatter 解析 mcpServers
|
||||
const mcpServersRaw = frontmatter['mcpServers']
|
||||
let mcpServers: AgentMcpServerSpec[] | undefined
|
||||
if (Array.isArray(mcpServersRaw)) {
|
||||
mcpServers = mcpServersRaw
|
||||
.map(item => {
|
||||
const result = AgentMcpServerSpecSchema().safeParse(item)
|
||||
if (result.success) {
|
||||
return result.data
|
||||
}
|
||||
logForDebugging(
|
||||
`Agent file ${filePath} has invalid mcpServers item: ${jsonStringify(item)}. Error: ${result.error.message}`,
|
||||
)
|
||||
return null
|
||||
})
|
||||
.filter((item): item is AgentMcpServerSpec => item !== null)
|
||||
}
|
||||
|
||||
// 从 frontmatter 解析 hooks
|
||||
const hooks = parseHooksFromFrontmatter(frontmatter, agentType)
|
||||
|
||||
const systemPrompt = content.trim()
|
||||
const agentDef: CustomAgentDefinition = {
|
||||
baseDir,
|
||||
agentType: agentType,
|
||||
whenToUse: whenToUse,
|
||||
...(tools !== undefined ? { tools } : {}),
|
||||
...(disallowedTools !== undefined ? { disallowedTools } : {}),
|
||||
...(skills !== undefined ? { skills } : {}),
|
||||
...(initialPrompt !== undefined ? { initialPrompt } : {}),
|
||||
...(mcpServers !== undefined && mcpServers.length > 0
|
||||
? { mcpServers }
|
||||
: {}),
|
||||
...(hooks !== undefined ? { hooks } : {}),
|
||||
getSystemPrompt: () => {
|
||||
if (isAutoMemoryEnabled() && memory) {
|
||||
const memoryPrompt = loadAgentMemoryPrompt(agentType, memory)
|
||||
return systemPrompt + '\n\n' + memoryPrompt
|
||||
}
|
||||
return systemPrompt
|
||||
},
|
||||
source,
|
||||
filename,
|
||||
...(color && typeof color === 'string' && AGENT_COLORS.includes(color)
|
||||
? { color }
|
||||
: {}),
|
||||
...(model !== undefined ? { model } : {}),
|
||||
...(parsedEffort !== undefined ? { effort: parsedEffort } : {}),
|
||||
...(isValidPermissionMode
|
||||
? { permissionMode: permissionModeRaw as PermissionMode }
|
||||
: {}),
|
||||
...(maxTurns !== undefined ? { maxTurns } : {}),
|
||||
...(background ? { background } : {}),
|
||||
...(memory ? { memory } : {}),
|
||||
...(isolation ? { isolation } : {}),
|
||||
}
|
||||
return agentDef
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
logForDebugging(`Error parsing agent from ${filePath}: ${errorMessage}`)
|
||||
logError(error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
71
claude-code源码-中文注释/src/tools/AgentTool/prompt.ts
Normal file
71
claude-code源码-中文注释/src/tools/AgentTool/prompt.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import { feature } from 'bun:bundle'
|
||||
import { getIsNonInteractiveSession } from '../../bootstrap/state.js'
|
||||
import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics/growthbook.js'
|
||||
import { isEnvTruthy } from '../../utils/envUtils.js'
|
||||
import { CLAUDE_CODE_GUIDE_AGENT } from './built-in/claudeCodeGuideAgent.js'
|
||||
import { EXPLORE_AGENT } from './built-in/exploreAgent.js'
|
||||
import { GENERAL_PURPOSE_AGENT } from './built-in/generalPurposeAgent.js'
|
||||
import { PLAN_AGENT } from './built-in/planAgent.js'
|
||||
import { STATUSLINE_SETUP_AGENT } from './built-in/statuslineSetup.js'
|
||||
import { VERIFICATION_AGENT } from './built-in/verificationAgent.js'
|
||||
import type { AgentDefinition } from './loadAgentsDir.js'
|
||||
|
||||
export function areExplorePlanAgentsEnabled(): boolean {
|
||||
if (feature('BUILTIN_EXPLORE_PLAN_AGENTS')) {
|
||||
// 3P 默认:true — Bedrock/Vertex 保持代理启用(匹配实验前外部行为)。A/B 测试处理设置为 false 以测量移除的影响。
|
||||
return getFeatureValue_CACHED_MAY_BE_STALE('tengu_amber_stoat', true)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
export function getBuiltInAgents(): AgentDefinition[] {
|
||||
// 允许通过 env 变量禁用所有内置代理(对想要空白状态的 SDK 用户有用)
|
||||
// 仅在非交互模式(SDK/API 使用)下适用
|
||||
if (
|
||||
isEnvTruthy(process.env.CLAUDE_AGENT_SDK_DISABLE_BUILTIN_AGENTS) &&
|
||||
getIsNonInteractiveSession()
|
||||
) {
|
||||
return []
|
||||
}
|
||||
|
||||
// 在函数体内使用延迟 require 以避免模块初始化时的循环依赖
|
||||
// 问题。coordinatorMode 模块依赖 tools,tools 依赖 AgentTool,
|
||||
// AgentTool 导入此文件。
|
||||
if (feature('COORDINATOR_MODE')) {
|
||||
if (isEnvTruthy(process.env.CLAUDE_CODE_COORDINATOR_MODE)) {
|
||||
/* eslint-disable @typescript-eslint/no-require-imports */
|
||||
const { getCoordinatorAgents } =
|
||||
require('../../coordinator/workerAgent.js') as typeof import('../../coordinator/workerAgent.js')
|
||||
/* eslint-enable @typescript-eslint/no-require-imports */
|
||||
return getCoordinatorAgents()
|
||||
}
|
||||
}
|
||||
|
||||
const agents: AgentDefinition[] = [
|
||||
GENERAL_PURPOSE_AGENT,
|
||||
STATUSLINE_SETUP_AGENT,
|
||||
]
|
||||
|
||||
if (areExplorePlanAgentsEnabled()) {
|
||||
agents.push(EXPLORE_AGENT, PLAN_AGENT)
|
||||
}
|
||||
|
||||
// 为非 SDK 入口点包含 Code Guide 代理
|
||||
const isNonSdkEntrypoint =
|
||||
process.env.CLAUDE_CODE_ENTRYPOINT !== 'sdk-ts' &&
|
||||
process.env.CLAUDE_CODE_ENTRYPOINT !== 'sdk-py' &&
|
||||
process.env.CLAUDE_CODE_ENTRYPOINT !== 'sdk-cli'
|
||||
|
||||
if (isNonSdkEntrypoint) {
|
||||
agents.push(CLAUDE_CODE_GUIDE_AGENT)
|
||||
}
|
||||
|
||||
if (
|
||||
feature('VERIFICATION_AGENT') &&
|
||||
getFeatureValue_CACHED_MAY_BE_STALE('tengu_hive_evidence', false)
|
||||
) {
|
||||
agents.push(VERIFICATION_AGENT)
|
||||
}
|
||||
|
||||
return agents
|
||||
}
|
||||
265
claude-code源码-中文注释/src/tools/AgentTool/resumeAgent.ts
Normal file
265
claude-code源码-中文注释/src/tools/AgentTool/resumeAgent.ts
Normal file
@@ -0,0 +1,265 @@
|
||||
import { promises as fsp } from 'fs'
|
||||
import { getSdkAgentProgressSummariesEnabled } from '../../bootstrap/state.js'
|
||||
import { getSystemPrompt } from '../../constants/prompts.js'
|
||||
import { isCoordinatorMode } from '../../coordinator/coordinatorMode.js'
|
||||
import type { CanUseToolFn } from '../../hooks/useCanTool.js'
|
||||
import type { ToolUseContext } from '../../Tool.js'
|
||||
import { registerAsyncAgent } from '../../tasks/LocalAgentTask/LocalAgentTask.js'
|
||||
import { assembleToolPool } from '../../tools.js'
|
||||
import { asAgentId } from '../../types/ids.js'
|
||||
import { runWithAgentContext } from '../../utils/agentContext.js'
|
||||
import { runWithCwdOverride } from '../../utils/cwd.js'
|
||||
import { logForDebugging } from '../../utils/debug.js'
|
||||
import {
|
||||
createUserMessage,
|
||||
filterOrphanedThinkingOnlyMessages,
|
||||
filterUnresolvedToolUses,
|
||||
filterWhitespaceOnlyAssistantMessages,
|
||||
} from '../../utils/messages.js'
|
||||
import { getAgentModel } from '../../utils/model/agent.js'
|
||||
import { getQuerySourceForAgent } from '../../utils/promptCategory.js'
|
||||
import {
|
||||
getAgentTranscript,
|
||||
readAgentMetadata,
|
||||
} from '../../utils/sessionStorage.js'
|
||||
import { buildEffectiveSystemPrompt } from '../../utils/systemPrompt.js'
|
||||
import type { SystemPrompt } from '../../utils/systemPromptType.js'
|
||||
import { getTaskOutputPath } from '../../utils/task/diskOutput.js'
|
||||
import { getParentSessionId } from '../../utils/teammate.js'
|
||||
import { reconstructForSubagentResume } from '../../utils/toolResultStorage.js'
|
||||
import { runAsyncAgentLifecycle } from './agentToolUtils.js'
|
||||
import { GENERAL_PURPOSE_AGENT } from './built-in/generalPurposeAgent.js'
|
||||
import { FORK_AGENT, isForkSubagentEnabled } from './forkSubagent.js'
|
||||
import type { AgentDefinition } from './loadAgentsDir.js'
|
||||
import { isBuiltInAgent } from './loadAgentsDir.js'
|
||||
import { runAgent } from './runAgent.js'
|
||||
|
||||
export type ResumeAgentResult = {
|
||||
agentId: string
|
||||
description: string
|
||||
outputFile: string
|
||||
}
|
||||
export async function resumeAgentBackground({
|
||||
agentId,
|
||||
prompt,
|
||||
toolUseContext,
|
||||
canUseTool,
|
||||
invokingRequestId,
|
||||
}: {
|
||||
agentId: string
|
||||
prompt: string
|
||||
toolUseContext: ToolUseContext
|
||||
canUseTool: CanUseToolFn
|
||||
invokingRequestId?: string
|
||||
}): Promise<ResumeAgentResult> {
|
||||
const startTime = Date.now()
|
||||
const appState = toolUseContext.getAppState()
|
||||
// 进程内 teammates 得到 no-op setAppState;setAppStateForTasks
|
||||
// 到达根 store 以便任务注册/进度/kill 保持可见。
|
||||
const rootSetAppState =
|
||||
toolUseContext.setAppStateForTasks ?? toolUseContext.setAppState
|
||||
const permissionMode = appState.toolPermissionContext.mode
|
||||
|
||||
const [transcript, meta] = await Promise.all([
|
||||
getAgentTranscript(asAgentId(agentId)),
|
||||
readAgentMetadata(asAgentId(agentId)),
|
||||
])
|
||||
if (!transcript) {
|
||||
throw new Error(`No transcript found for agent ID: ${agentId}`)
|
||||
}
|
||||
const resumedMessages = filterWhitespaceOnlyAssistantMessages(
|
||||
filterOrphanedThinkingOnlyMessages(
|
||||
filterUnresolvedToolUses(transcript.messages),
|
||||
),
|
||||
)
|
||||
const resumedReplacementState = reconstructForSubagentResume(
|
||||
toolUseContext.contentReplacementState,
|
||||
resumedMessages,
|
||||
transcript.contentReplacements,
|
||||
)
|
||||
// 尽力而为:如果原始 worktree 外部被移除,回退到
|
||||
// 父 cwd 而不是在 later chdir 上崩溃。
|
||||
const resumedWorktreePath = meta?.worktreePath
|
||||
? await fsp.stat(meta.worktreePath).then(
|
||||
s => (s.isDirectory() ? meta.worktreePath : undefined),
|
||||
() => {
|
||||
logForDebugging(
|
||||
`Resumed worktree ${meta.worktreePath} no longer exists; falling back to parent cwd`,
|
||||
)
|
||||
return undefined
|
||||
},
|
||||
)
|
||||
: undefined
|
||||
if (resumedWorktreePath) {
|
||||
// Bump mtime so stale-worktree cleanup doesn't delete a just-resumed worktree (#22355)
|
||||
const now = new Date()
|
||||
await fsp.utimes(resumedWorktreePath, now, now)
|
||||
}
|
||||
|
||||
// 跳过 filterDeniedAgents 重新门控——原始生成已通过权限检查
|
||||
let selectedAgent: AgentDefinition
|
||||
let isResumedFork = false
|
||||
if (meta?.agentType === FORK_AGENT.agentType) {
|
||||
selectedAgent = FORK_AGENT
|
||||
isResumedFork = true
|
||||
} else if (meta?.agentType) {
|
||||
const found = toolUseContext.options.agentDefinitions.activeAgents.find(
|
||||
a => a.agentType === meta.agentType,
|
||||
)
|
||||
selectedAgent = found ?? GENERAL_PURPOSE_AGENT
|
||||
} else {
|
||||
selectedAgent = GENERAL_PURPOSE_AGENT
|
||||
}
|
||||
|
||||
const uiDescription = meta?.description ?? '(resumed)'
|
||||
|
||||
let forkParentSystemPrompt: SystemPrompt | undefined
|
||||
if (isResumedFork) {
|
||||
if (toolUseContext.renderedSystemPrompt) {
|
||||
forkParentSystemPrompt = toolUseContext.renderedSystemPrompt
|
||||
} else {
|
||||
const mainThreadAgentDefinition = appState.agent
|
||||
? appState.agentDefinitions.activeAgents.find(
|
||||
a => a.agentType === appState.agent,
|
||||
)
|
||||
: undefined
|
||||
const additionalWorkingDirectories = Array.from(
|
||||
appState.toolPermissionContext.additionalWorkingDirectories.keys(),
|
||||
)
|
||||
const defaultSystemPrompt = await getSystemPrompt(
|
||||
toolUseContext.options.tools,
|
||||
toolUseContext.options.mainLoopModel,
|
||||
additionalWorkingDirectories,
|
||||
toolUseContext.options.mcpClients,
|
||||
)
|
||||
forkParentSystemPrompt = buildEffectiveSystemPrompt({
|
||||
mainThreadAgentDefinition,
|
||||
toolUseContext,
|
||||
customSystemPrompt: toolUseContext.options.customSystemPrompt,
|
||||
defaultSystemPrompt,
|
||||
appendSystemPrompt: toolUseContext.options.appendSystemPrompt,
|
||||
})
|
||||
}
|
||||
if (!forkParentSystemPrompt) {
|
||||
throw new Error(
|
||||
'Cannot resume fork agent: unable to reconstruct parent system prompt',
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 解析用于分析元数据的模型(runAgent 在内部解析自己的)
|
||||
const resolvedAgentModel = getAgentModel(
|
||||
selectedAgent.model,
|
||||
toolUseContext.options.mainLoopModel,
|
||||
undefined,
|
||||
permissionMode,
|
||||
)
|
||||
|
||||
const workerPermissionContext = {
|
||||
...appState.toolPermissionContext,
|
||||
mode: selectedAgent.permissionMode ?? 'acceptEdits',
|
||||
}
|
||||
const workerTools = isResumedFork
|
||||
? toolUseContext.options.tools
|
||||
: assembleToolPool(workerPermissionContext, appState.mcp.tools)
|
||||
|
||||
const runAgentParams: Parameters<typeof runAgent>[0] = {
|
||||
agentDefinition: selectedAgent,
|
||||
promptMessages: [
|
||||
...resumedMessages,
|
||||
createUserMessage({ content: prompt }),
|
||||
],
|
||||
toolUseContext,
|
||||
canUseTool,
|
||||
isAsync: true,
|
||||
querySource: getQuerySourceForAgent(
|
||||
selectedAgent.agentType,
|
||||
isBuiltInAgent(selectedAgent),
|
||||
),
|
||||
model: undefined,
|
||||
// Fork resume:传递父代理的系统提示(缓存相同的前缀)。
|
||||
// 非 fork:undefined → runAgent 在 wrapWithCwd 下重新计算
|
||||
// 以便 getCwd() 看到 resumedWorktreePath。
|
||||
override: isResumedFork
|
||||
? { systemPrompt: forkParentSystemPrompt }
|
||||
: undefined,
|
||||
availableTools: workerTools,
|
||||
// 转录已经包含原始 fork 的父上下文切片。
|
||||
// 重新提供会导致重复的 tool_use ID。
|
||||
forkContextMessages: undefined,
|
||||
...(isResumedFork && { useExactTools: true }),
|
||||
// 重新持久化以便元数据在 runAgent 的 writeAgentMetadata 覆盖中存活
|
||||
worktreePath: resumedWorktreePath,
|
||||
description: meta?.description,
|
||||
contentReplacementState: resumedReplacementState,
|
||||
}
|
||||
|
||||
// 跳过名称注册表写入——原始条目从初始生成中持续存在
|
||||
const agentBackgroundTask = registerAsyncAgent({
|
||||
agentId,
|
||||
description: uiDescription,
|
||||
prompt,
|
||||
selectedAgent,
|
||||
setAppState: rootSetAppState,
|
||||
toolUseId: toolUseContext.toolUseId,
|
||||
})
|
||||
|
||||
const metadata = {
|
||||
prompt,
|
||||
resolvedAgentModel,
|
||||
isBuiltInAgent: isBuiltInAgent(selectedAgent),
|
||||
startTime,
|
||||
agentType: selectedAgent.agentType,
|
||||
isAsync: true,
|
||||
}
|
||||
|
||||
const asyncAgentContext = {
|
||||
agentId,
|
||||
parentSessionId: getParentSessionId(),
|
||||
agentType: 'subagent' as const,
|
||||
subagentName: selectedAgent.agentType,
|
||||
isBuiltIn: isBuiltInAgent(selectedAgent),
|
||||
invokingRequestId,
|
||||
invocationKind: 'resume' as const,
|
||||
invocationEmitted: false,
|
||||
}
|
||||
|
||||
const wrapWithCwd = <T>(fn: () => T): T =>
|
||||
resumedWorktreePath ? runWithCwdOverride(resumedWorktreePath, fn) : fn()
|
||||
|
||||
void runWithAgentContext(asyncAgentContext, () =>
|
||||
wrapWithCwd(() =>
|
||||
runAsyncAgentLifecycle({
|
||||
taskId: agentBackgroundTask.agentId,
|
||||
abortController: agentBackgroundTask.abortController!,
|
||||
makeStream: onCacheSafeParams =>
|
||||
runAgent({
|
||||
...runAgentParams,
|
||||
override: {
|
||||
...runAgentParams.override,
|
||||
agentId: asAgentId(agentBackgroundTask.agentId),
|
||||
abortController: agentBackgroundTask.abortController!,
|
||||
},
|
||||
onCacheSafeParams,
|
||||
}),
|
||||
metadata,
|
||||
description: uiDescription,
|
||||
toolUseContext,
|
||||
rootSetAppState,
|
||||
agentIdForCleanup: agentId,
|
||||
enableSummarization:
|
||||
isCoordinatorMode() ||
|
||||
isForkSubagentEnabled() ||
|
||||
getSdkAgentProgressSummariesEnabled(),
|
||||
getWorktreeResult: async () =>
|
||||
resumedWorktreePath ? { worktreePath: resumedWorktreePath } : {},
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
return {
|
||||
agentId,
|
||||
description: uiDescription,
|
||||
outputFile: getTaskOutputPath(agentId),
|
||||
}
|
||||
}
|
||||
964
claude-code源码-中文注释/src/tools/AgentTool/runAgent.ts
Normal file
964
claude-code源码-中文注释/src/tools/AgentTool/runAgent.ts
Normal file
@@ -0,0 +1,964 @@
|
||||
import { feature } from 'bun:bundle'
|
||||
import type { UUID } from 'crypto'
|
||||
import { randomUUID } from 'crypto'
|
||||
import uniqBy from 'lodash-es/uniqBy.js'
|
||||
import { logForDebugging } from 'src/utils/debug.js'
|
||||
import { getProjectRoot, getSessionId } from '../../bootstrap/state.js'
|
||||
import { getCommand, getSkillToolCommands, hasCommand } from '../../commands.js'
|
||||
import {
|
||||
DEFAULT_AGENT_PROMPT,
|
||||
enhanceSystemPromptWithEnvDetails,
|
||||
} from '../../constants/prompts.js'
|
||||
import type { QuerySource } from '../../constants/querySource.js'
|
||||
import { getSystemContext, getUserContext } from '../../context.js'
|
||||
import type { CanUseToolFn } from '../../hooks/useCanUseTool.js'
|
||||
import { query } from '../../query.js'
|
||||
import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics/growthbook.js'
|
||||
import { getDumpPromptsPath } from '../../services/api/dumpPrompts.js'
|
||||
import { cleanupAgentTracking } from '../../services/api/promptCacheBreakDetection.js'
|
||||
import {
|
||||
connectToServer,
|
||||
fetchToolsForClient,
|
||||
} from '../../services/mcp/client.js'
|
||||
import { getMcpConfigByName } from '../../services/mcp/config.js'
|
||||
import type {
|
||||
MCPServerConnection,
|
||||
ScopedMcpServerConfig,
|
||||
} from '../../services/mcp/types.js'
|
||||
import type { Tool, Tools, ToolUseContext } from '../../Tool.js'
|
||||
import { killShellTasksForAgent } from '../../tasks/LocalShellTask/killShellTasks.js'
|
||||
import type { Command } from '../../types/command.js'
|
||||
import type { AgentId } from '../../types/ids.js'
|
||||
import type {
|
||||
AssistantMessage,
|
||||
Message,
|
||||
ProgressMessage,
|
||||
RequestStartEvent,
|
||||
StreamEvent,
|
||||
SystemCompactBoundaryMessage,
|
||||
TombstoneMessage,
|
||||
ToolUseSummaryMessage,
|
||||
UserMessage,
|
||||
} from '../../types/message.js'
|
||||
import { createAttachmentMessage } from '../../utils/attachments.js'
|
||||
import { AbortError } from '../../utils/errors.js'
|
||||
import { getDisplayPath } from '../../utils/file.js'
|
||||
import {
|
||||
cloneFileStateCache,
|
||||
createFileStateCacheWithSizeLimit,
|
||||
READ_FILE_STATE_CACHE_SIZE,
|
||||
} from '../../utils/fileStateCache.js'
|
||||
import {
|
||||
type CacheSafeParams,
|
||||
createSubagentContext,
|
||||
} from '../../utils/forkedAgent.js'
|
||||
import { registerFrontmatterHooks } from '../../utils/hooks/registerFrontmatterHooks.js'
|
||||
import { clearSessionHooks } from '../../utils/hooks/sessionHooks.js'
|
||||
import { executeSubagentStartHooks } from '../../utils/hooks.js'
|
||||
import { createUserMessage } from '../../utils/messages.js'
|
||||
import { getAgentModel } from '../../utils/model/agent.js'
|
||||
import type { ModelAlias } from '../../utils/model/aliases.js'
|
||||
import {
|
||||
clearAgentTranscriptSubdir,
|
||||
recordSidechainTranscript,
|
||||
setAgentTranscriptSubdir,
|
||||
writeAgentMetadata,
|
||||
} from '../../utils/sessionStorage.js'
|
||||
import {
|
||||
isRestrictedToPluginOnly,
|
||||
isSourceAdminTrusted,
|
||||
} from '../../utils/settings/pluginOnlyPolicy.js'
|
||||
import {
|
||||
asSystemPrompt,
|
||||
type SystemPrompt,
|
||||
} from '../../utils/systemPromptType.js'
|
||||
import {
|
||||
isPerfettoTracingEnabled,
|
||||
registerAgent as registerPerfettoAgent,
|
||||
unregisterAgent as unregisterPerfettoAgent,
|
||||
} from '../../utils/telemetry/perfettoTracing.js'
|
||||
import type { ContentReplacementState } from '../../utils/toolResultStorage.js'
|
||||
import { createAgentId } from '../../utils/uuid.js'
|
||||
import { resolveAgentTools } from './agentToolUtils.js'
|
||||
import { type AgentDefinition, isBuiltInAgent } from './loadAgentsDir.js'
|
||||
|
||||
/**
|
||||
* Initialize agent-specific MCP servers
|
||||
* Agents can define their own MCP servers in their frontmatter that are additive
|
||||
* to the parent's MCP clients. These servers are connected when the agent starts
|
||||
* and cleaned up when the agent finishes.
|
||||
*
|
||||
* @param agentDefinition The agent definition with optional mcpServers
|
||||
* @param parentClients MCP clients inherited from parent context
|
||||
* @returns Merged clients (parent + agent-specific), agent MCP tools, and cleanup function
|
||||
*/
|
||||
async function initializeAgentMcpServers(
|
||||
agentDefinition: AgentDefinition,
|
||||
parentClients: MCPServerConnection[],
|
||||
): Promise<{
|
||||
clients: MCPServerConnection[]
|
||||
tools: Tools
|
||||
cleanup: () => Promise<void>
|
||||
}> {
|
||||
// If no agent-specific servers defined, return parent clients as-is
|
||||
if (!agentDefinition.mcpServers?.length) {
|
||||
return {
|
||||
clients: parentClients,
|
||||
tools: [],
|
||||
cleanup: async () => {},
|
||||
}
|
||||
}
|
||||
|
||||
// When MCP is locked to plugin-only, skip frontmatter MCP servers for
|
||||
// USER-CONTROLLED agents only. Plugin, built-in, and policySettings agents
|
||||
// are admin-trusted — their frontmatter MCP is part of the admin-approved
|
||||
// surface. Blocking them (as the first cut did) breaks plugin agents that
|
||||
// legitimately need MCP, contradicting "plugin-provided always loads."
|
||||
const agentIsAdminTrusted = isSourceAdminTrusted(agentDefinition.source)
|
||||
if (isRestrictedToPluginOnly('mcp') && !agentIsAdminTrusted) {
|
||||
logForDebugging(
|
||||
`[Agent: ${agentDefinition.agentType}] Skipping MCP servers: strictPluginOnlyCustomization locks MCP to plugin-only (agent source: ${agentDefinition.source})`,
|
||||
)
|
||||
return {
|
||||
clients: parentClients,
|
||||
tools: [],
|
||||
cleanup: async () => {},
|
||||
}
|
||||
}
|
||||
|
||||
const agentClients: MCPServerConnection[] = []
|
||||
// Track which clients were newly created (inline definitions) vs. shared from parent
|
||||
// Only newly created clients should be cleaned up when the agent finishes
|
||||
const newlyCreatedClients: MCPServerConnection[] = []
|
||||
const agentTools: Tool[] = []
|
||||
|
||||
for (const spec of agentDefinition.mcpServers) {
|
||||
let config: ScopedMcpServerConfig | null = null
|
||||
let name: string
|
||||
let isNewlyCreated = false
|
||||
|
||||
if (typeof spec === 'string') {
|
||||
// Reference by name - look up in existing MCP configs
|
||||
// This uses the memoized connectToServer, so we may get a shared client
|
||||
name = spec
|
||||
config = getMcpConfigByName(spec)
|
||||
if (!config) {
|
||||
logForDebugging(
|
||||
`[Agent: ${agentDefinition.agentType}] MCP server not found: ${spec}`,
|
||||
{ level: 'warn' },
|
||||
)
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
// Inline definition as { [name]: config }
|
||||
// These are agent-specific servers that should be cleaned up
|
||||
const entries = Object.entries(spec)
|
||||
if (entries.length !== 1) {
|
||||
logForDebugging(
|
||||
`[Agent: ${agentDefinition.agentType}] Invalid MCP server spec: expected exactly one key`,
|
||||
{ level: 'warn' },
|
||||
)
|
||||
continue
|
||||
}
|
||||
const [serverName, serverConfig] = entries[0]!
|
||||
name = serverName
|
||||
config = {
|
||||
...serverConfig,
|
||||
scope: 'dynamic' as const,
|
||||
} as ScopedMcpServerConfig
|
||||
isNewlyCreated = true
|
||||
}
|
||||
|
||||
// Connect to the server
|
||||
const client = await connectToServer(name, config)
|
||||
agentClients.push(client)
|
||||
if (isNewlyCreated) {
|
||||
newlyCreatedClients.push(client)
|
||||
}
|
||||
|
||||
// Fetch tools if connected
|
||||
if (client.type === 'connected') {
|
||||
const tools = await fetchToolsForClient(client)
|
||||
agentTools.push(...tools)
|
||||
logForDebugging(
|
||||
`[Agent: ${agentDefinition.agentType}] Connected to MCP server '${name}' with ${tools.length} tools`,
|
||||
)
|
||||
} else {
|
||||
logForDebugging(
|
||||
`[Agent: ${agentDefinition.agentType}] Failed to connect to MCP server '${name}': ${client.type}`,
|
||||
{ level: 'warn' },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Create cleanup function for agent-specific servers
|
||||
// Only clean up newly created clients (inline definitions), not shared/referenced ones
|
||||
// Shared clients (referenced by string name) are memoized and used by the parent context
|
||||
const cleanup = async () => {
|
||||
for (const client of newlyCreatedClients) {
|
||||
if (client.type === 'connected') {
|
||||
try {
|
||||
await client.cleanup()
|
||||
} catch (error) {
|
||||
logForDebugging(
|
||||
`[Agent: ${agentDefinition.agentType}] Error cleaning up MCP server '${client.name}': ${error}`,
|
||||
{ level: 'warn' },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Return merged clients (parent + agent-specific) and agent tools
|
||||
return {
|
||||
clients: [...parentClients, ...agentClients],
|
||||
tools: agentTools,
|
||||
cleanup,
|
||||
}
|
||||
}
|
||||
|
||||
type QueryMessage =
|
||||
| StreamEvent
|
||||
| RequestStartEvent
|
||||
| Message
|
||||
| ToolUseSummaryMessage
|
||||
| TombstoneMessage
|
||||
|
||||
/**
|
||||
* Type guard to check if a message from query() is a recordable Message type.
|
||||
* Matches the types we want to record: assistant, user, progress, or system compact_boundary.
|
||||
*/
|
||||
function isRecordableMessage(
|
||||
msg: QueryMessage,
|
||||
): msg is
|
||||
| AssistantMessage
|
||||
| UserMessage
|
||||
| ProgressMessage
|
||||
| SystemCompactBoundaryMessage {
|
||||
return (
|
||||
msg.type === 'assistant' ||
|
||||
msg.type === 'user' ||
|
||||
msg.type === 'progress' ||
|
||||
(msg.type === 'system' &&
|
||||
'subtype' in msg &&
|
||||
msg.subtype === 'compact_boundary')
|
||||
)
|
||||
}
|
||||
|
||||
export async function* runAgent({
|
||||
agentDefinition,
|
||||
promptMessages,
|
||||
toolUseContext,
|
||||
canUseTool,
|
||||
isAsync,
|
||||
canShowPermissionPrompts,
|
||||
forkContextMessages,
|
||||
querySource,
|
||||
override,
|
||||
model,
|
||||
maxTurns,
|
||||
preserveToolUseResults,
|
||||
availableTools,
|
||||
allowedTools,
|
||||
onCacheSafeParams,
|
||||
contentReplacementState,
|
||||
useExactTools,
|
||||
worktreePath,
|
||||
description,
|
||||
transcriptSubdir,
|
||||
onQueryProgress,
|
||||
}: {
|
||||
agentDefinition: AgentDefinition
|
||||
promptMessages: Message[]
|
||||
toolUseContext: ToolUseContext
|
||||
canUseTool: CanUseToolFn
|
||||
isAsync: boolean
|
||||
/** Whether this agent can show permission prompts. Defaults to !isAsync.
|
||||
* Set to true for in-process teammates that run async but share the terminal. */
|
||||
canShowPermissionPrompts?: boolean
|
||||
forkContextMessages?: Message[]
|
||||
querySource: QuerySource
|
||||
override?: {
|
||||
userContext?: { [k: string]: string }
|
||||
systemContext?: { [k: string]: string }
|
||||
systemPrompt?: SystemPrompt
|
||||
abortController?: AbortController
|
||||
agentId?: AgentId
|
||||
}
|
||||
model?: ModelAlias
|
||||
maxTurns?: number
|
||||
/** Preserve toolUseResult on messages for subagents with viewable transcripts */
|
||||
preserveToolUseResults?: boolean
|
||||
/** Precomputed tool pool for the worker agent. Computed by the caller
|
||||
* (AgentTool.tsx) to avoid a circular dependency between runAgent and tools.ts.
|
||||
* Always contains the full tool pool assembled with the worker's own permission
|
||||
* mode, independent of the parent's tool restrictions. */
|
||||
availableTools: Tools
|
||||
/** Tool permission rules to add to the agent's session allow rules.
|
||||
* When provided, replaces ALL allow rules so the agent only has what's
|
||||
* explicitly listed (parent approvals don't leak through). */
|
||||
allowedTools?: string[]
|
||||
/** Optional callback invoked with CacheSafeParams after constructing the agent's
|
||||
* system prompt, context, and tools. Used by background summarization to fork
|
||||
* the agent's conversation for periodic progress summaries. */
|
||||
onCacheSafeParams?: (params: CacheSafeParams) => void
|
||||
/** Replacement state reconstructed from a resumed sidechain transcript so
|
||||
* the same tool results are re-replaced (prompt cache stability). When
|
||||
* omitted, createSubagentContext clones the parent's state. */
|
||||
contentReplacementState?: ContentReplacementState
|
||||
/** When true, use availableTools directly without filtering through
|
||||
* resolveAgentTools(). Also inherits the parent's thinkingConfig and
|
||||
* isNonInteractiveSession instead of overriding them. Used by the fork
|
||||
* subagent path to produce byte-identical API request prefixes for
|
||||
* prompt cache hits. */
|
||||
useExactTools?: boolean
|
||||
/** Worktree path if the agent was spawned with isolation: "worktree".
|
||||
* Persisted to metadata so resume can restore the correct cwd. */
|
||||
worktreePath?: string
|
||||
/** Original task description from AgentTool input. Persisted to metadata
|
||||
* so a resumed agent's notification can show the original description. */
|
||||
description?: string
|
||||
/** Optional subdirectory under subagents/ to group this agent's transcript
|
||||
* with related ones (e.g. workflows/<runId> for workflow subagents). */
|
||||
transcriptSubdir?: string
|
||||
/** Optional callback fired on every message yielded by query() — including
|
||||
* stream_event deltas that runAgent otherwise drops. Use to detect liveness
|
||||
* during long single-block streams (e.g. thinking) where no assistant
|
||||
* message is yielded for >60s. */
|
||||
onQueryProgress?: () => void
|
||||
}): AsyncGenerator<Message, void> {
|
||||
// Track subagent usage for feature discovery
|
||||
|
||||
const appState = toolUseContext.getAppState()
|
||||
const permissionMode = appState.toolPermissionContext.mode
|
||||
// Always-shared channel to the root AppState store. toolUseContext.setAppState
|
||||
// is a no-op when the *parent* is itself an async agent (nested async→async),
|
||||
// so session-scoped writes (hooks, bash tasks) must go through this instead.
|
||||
const rootSetAppState =
|
||||
toolUseContext.setAppStateForTasks ?? toolUseContext.setAppState
|
||||
|
||||
const resolvedAgentModel = getAgentModel(
|
||||
agentDefinition.model,
|
||||
toolUseContext.options.mainLoopModel,
|
||||
model,
|
||||
permissionMode,
|
||||
)
|
||||
|
||||
const agentId = override?.agentId ? override.agentId : createAgentId()
|
||||
|
||||
// Route this agent's transcript into a grouping subdirectory if requested
|
||||
// (e.g. workflow subagents write to subagents/workflows/<runId>/).
|
||||
if (transcriptSubdir) {
|
||||
setAgentTranscriptSubdir(agentId, transcriptSubdir)
|
||||
}
|
||||
|
||||
// Register agent in Perfetto trace for hierarchy visualization
|
||||
if (isPerfettoTracingEnabled()) {
|
||||
const parentId = toolUseContext.agentId ?? getSessionId()
|
||||
registerPerfettoAgent(agentId, agentDefinition.agentType, parentId)
|
||||
}
|
||||
|
||||
// Log API calls path for subagents (ant-only)
|
||||
if (process.env.USER_TYPE === 'ant') {
|
||||
logForDebugging(
|
||||
`[Subagent ${agentDefinition.agentType}] API calls: ${getDisplayPath(getDumpPromptsPath(agentId))}`,
|
||||
)
|
||||
}
|
||||
|
||||
// Handle message forking for context sharing
|
||||
// Filter out incomplete tool calls from parent messages to avoid API errors
|
||||
const contextMessages: Message[] = forkContextMessages
|
||||
? filterIncompleteToolCalls(forkContextMessages)
|
||||
: []
|
||||
const initialMessages: Message[] = [...contextMessages, ...promptMessages]
|
||||
|
||||
const agentReadFileState =
|
||||
forkContextMessages !== undefined
|
||||
? cloneFileStateCache(toolUseContext.readFileState)
|
||||
: createFileStateCacheWithSizeLimit(READ_FILE_STATE_CACHE_SIZE)
|
||||
|
||||
const [baseUserContext, baseSystemContext] = await Promise.all([
|
||||
override?.userContext ?? getUserContext(),
|
||||
override?.systemContext ?? getSystemContext(),
|
||||
])
|
||||
|
||||
// Read-only agents (Explore, Plan) don't act on commit/PR/lint rules from
|
||||
// CLAUDE.md — the main agent has full context and interprets their output.
|
||||
// Dropping claudeMd here saves ~5-15 Gtok/week across 34M+ Explore spawns.
|
||||
// Explicit override.userContext from callers is preserved untouched.
|
||||
// Kill-switch defaults true; flip tengu_slim_subagent_claudemd=false to revert.
|
||||
const shouldOmitClaudeMd =
|
||||
agentDefinition.omitClaudeMd &&
|
||||
!override?.userContext &&
|
||||
getFeatureValue_CACHED_MAY_BE_STALE('tengu_slim_subagent_claudemd', true)
|
||||
const { claudeMd: _omittedClaudeMd, ...userContextNoClaudeMd } =
|
||||
baseUserContext
|
||||
const resolvedUserContext = shouldOmitClaudeMd
|
||||
? userContextNoClaudeMd
|
||||
: baseUserContext
|
||||
|
||||
// Explore/Plan are read-only search agents — the parent-session-start
|
||||
// gitStatus (up to 40KB, explicitly labeled stale) is dead weight. If they
|
||||
// need git info they run `git status` themselves and get fresh data.
|
||||
// Saves ~1-3 Gtok/week fleet-wide.
|
||||
const { gitStatus: _omittedGitStatus, ...systemContextNoGit } =
|
||||
baseSystemContext
|
||||
const resolvedSystemContext =
|
||||
agentDefinition.agentType === 'Explore' ||
|
||||
agentDefinition.agentType === 'Plan'
|
||||
? systemContextNoGit
|
||||
: baseSystemContext
|
||||
|
||||
// Override permission mode if agent defines one
|
||||
// However, don't override if parent is in bypassPermissions or acceptEdits mode - those should always take precedence
|
||||
// For async agents, also set shouldAvoidPermissionPrompts since they can't show UI
|
||||
const agentPermissionMode = agentDefinition.permissionMode
|
||||
const agentGetAppState = () => {
|
||||
const state = toolUseContext.getAppState()
|
||||
let toolPermissionContext = state.toolPermissionContext
|
||||
|
||||
// Override permission mode if agent defines one (unless parent is bypassPermissions, acceptEdits, or auto)
|
||||
if (
|
||||
agentPermissionMode &&
|
||||
state.toolPermissionContext.mode !== 'bypassPermissions' &&
|
||||
state.toolPermissionContext.mode !== 'acceptEdits' &&
|
||||
!(
|
||||
feature('TRANSCRIPT_CLASSIFIER') &&
|
||||
state.toolPermissionContext.mode === 'auto'
|
||||
)
|
||||
) {
|
||||
toolPermissionContext = {
|
||||
...toolPermissionContext,
|
||||
mode: agentPermissionMode,
|
||||
}
|
||||
}
|
||||
|
||||
// Set flag to auto-deny prompts for agents that can't show UI
|
||||
// Use explicit canShowPermissionPrompts if provided, otherwise:
|
||||
// - bubble mode: always show prompts (bubbles to parent terminal)
|
||||
// - default: !isAsync (sync agents show prompts, async agents don't)
|
||||
const shouldAvoidPrompts =
|
||||
canShowPermissionPrompts !== undefined
|
||||
? !canShowPermissionPrompts
|
||||
: agentPermissionMode === 'bubble'
|
||||
? false
|
||||
: isAsync
|
||||
if (shouldAvoidPrompts) {
|
||||
toolPermissionContext = {
|
||||
...toolPermissionContext,
|
||||
shouldAvoidPermissionPrompts: true,
|
||||
}
|
||||
}
|
||||
|
||||
// For background agents that can show prompts, await automated checks
|
||||
// (classifier, permission hooks) before showing the permission dialog.
|
||||
// Since these are background agents, waiting is fine — the user should
|
||||
// only be interrupted when automated checks can't resolve the permission.
|
||||
// This applies to bubble mode (always) and explicit canShowPermissionPrompts.
|
||||
if (isAsync && !shouldAvoidPrompts) {
|
||||
toolPermissionContext = {
|
||||
...toolPermissionContext,
|
||||
awaitAutomatedChecksBeforeDialog: true,
|
||||
}
|
||||
}
|
||||
|
||||
// Scope tool permissions: when allowedTools is provided, use them as session rules.
|
||||
// IMPORTANT: Preserve cliArg rules (from SDK's --allowedTools) since those are
|
||||
// explicit permissions from the SDK consumer that should apply to all agents.
|
||||
// Only clear session-level rules from the parent to prevent unintended leakage.
|
||||
if (allowedTools !== undefined) {
|
||||
toolPermissionContext = {
|
||||
...toolPermissionContext,
|
||||
alwaysAllowRules: {
|
||||
// Preserve SDK-level permissions from --allowedTools
|
||||
cliArg: state.toolPermissionContext.alwaysAllowRules.cliArg,
|
||||
// Use the provided allowedTools as session-level permissions
|
||||
session: [...allowedTools],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Override effort level if agent defines one
|
||||
const effortValue =
|
||||
agentDefinition.effort !== undefined
|
||||
? agentDefinition.effort
|
||||
: state.effortValue
|
||||
|
||||
if (
|
||||
toolPermissionContext === state.toolPermissionContext &&
|
||||
effortValue === state.effortValue
|
||||
) {
|
||||
return state
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
toolPermissionContext,
|
||||
effortValue,
|
||||
}
|
||||
}
|
||||
|
||||
const resolvedTools = useExactTools
|
||||
? availableTools
|
||||
: resolveAgentTools(agentDefinition, availableTools, isAsync).resolvedTools
|
||||
|
||||
const additionalWorkingDirectories = Array.from(
|
||||
appState.toolPermissionContext.additionalWorkingDirectories.keys(),
|
||||
)
|
||||
|
||||
const agentSystemPrompt = override?.systemPrompt
|
||||
? override.systemPrompt
|
||||
: asSystemPrompt(
|
||||
await getAgentSystemPrompt(
|
||||
agentDefinition,
|
||||
toolUseContext,
|
||||
resolvedAgentModel,
|
||||
additionalWorkingDirectories,
|
||||
resolvedTools,
|
||||
),
|
||||
)
|
||||
|
||||
// Determine abortController:
|
||||
// - Override takes precedence
|
||||
// - Async agents get a new unlinked controller (runs independently)
|
||||
// - Sync agents share parent's controller
|
||||
const agentAbortController = override?.abortController
|
||||
? override.abortController
|
||||
: isAsync
|
||||
? new AbortController()
|
||||
: toolUseContext.abortController
|
||||
|
||||
// Execute SubagentStart hooks and collect additional context
|
||||
const additionalContexts: string[] = []
|
||||
for await (const hookResult of executeSubagentStartHooks(
|
||||
agentId,
|
||||
agentDefinition.agentType,
|
||||
agentAbortController.signal,
|
||||
)) {
|
||||
if (
|
||||
hookResult.additionalContexts &&
|
||||
hookResult.additionalContexts.length > 0
|
||||
) {
|
||||
additionalContexts.push(...hookResult.additionalContexts)
|
||||
}
|
||||
}
|
||||
|
||||
// Add SubagentStart hook context as a user message (consistent with SessionStart/UserPromptSubmit)
|
||||
if (additionalContexts.length > 0) {
|
||||
const contextMessage = createAttachmentMessage({
|
||||
type: 'hook_additional_context',
|
||||
content: additionalContexts,
|
||||
hookName: 'SubagentStart',
|
||||
toolUseID: randomUUID(),
|
||||
hookEvent: 'SubagentStart',
|
||||
})
|
||||
initialMessages.push(contextMessage)
|
||||
}
|
||||
|
||||
// Register agent's frontmatter hooks (scoped to agent lifecycle)
|
||||
// Pass isAgent=true to convert Stop hooks to SubagentStop (since subagents trigger SubagentStop)
|
||||
// Same admin-trusted gate for frontmatter hooks: under ["hooks"] alone
|
||||
// (skills/agents not locked), user agents still load — block their
|
||||
// frontmatter-hook REGISTRATION here where source is known, rather than
|
||||
// blanket-blocking all session hooks at execution time (which would
|
||||
// also kill plugin agents' hooks).
|
||||
const hooksAllowedForThisAgent =
|
||||
!isRestrictedToPluginOnly('hooks') ||
|
||||
isSourceAdminTrusted(agentDefinition.source)
|
||||
if (agentDefinition.hooks && hooksAllowedForThisAgent) {
|
||||
registerFrontmatterHooks(
|
||||
rootSetAppState,
|
||||
agentId,
|
||||
agentDefinition.hooks,
|
||||
`agent '${agentDefinition.agentType}'`,
|
||||
true, // isAgent - converts Stop to SubagentStop
|
||||
)
|
||||
}
|
||||
|
||||
// Preload skills from agent frontmatter
|
||||
const skillsToPreload = agentDefinition.skills ?? []
|
||||
if (skillsToPreload.length > 0) {
|
||||
const allSkills = await getSkillToolCommands(getProjectRoot())
|
||||
|
||||
// Filter valid skills and warn about missing ones
|
||||
const validSkills: Array<{
|
||||
skillName: string
|
||||
skill: (typeof allSkills)[0] & { type: 'prompt' }
|
||||
}> = []
|
||||
|
||||
for (const skillName of skillsToPreload) {
|
||||
// Resolve the skill name, trying multiple strategies:
|
||||
// 1. Exact match (hasCommand checks name, userFacingName, aliases)
|
||||
// 2. Fully-qualified with agent's plugin prefix (e.g., "my-skill" → "plugin:my-skill")
|
||||
// 3. Suffix match on ":skillName" for plugin-namespaced skills
|
||||
const resolvedName = resolveSkillName(
|
||||
skillName,
|
||||
allSkills,
|
||||
agentDefinition,
|
||||
)
|
||||
if (!resolvedName) {
|
||||
logForDebugging(
|
||||
`[Agent: ${agentDefinition.agentType}] Warning: Skill '${skillName}' specified in frontmatter was not found`,
|
||||
{ level: 'warn' },
|
||||
)
|
||||
continue
|
||||
}
|
||||
|
||||
const skill = getCommand(resolvedName, allSkills)
|
||||
if (skill.type !== 'prompt') {
|
||||
logForDebugging(
|
||||
`[Agent: ${agentDefinition.agentType}] Warning: Skill '${skillName}' is not a prompt-based skill`,
|
||||
{ level: 'warn' },
|
||||
)
|
||||
continue
|
||||
}
|
||||
validSkills.push({ skillName, skill })
|
||||
}
|
||||
|
||||
// Load all skill contents concurrently and add to initial messages
|
||||
const { formatSkillLoadingMetadata } = await import(
|
||||
'../../utils/processUserInput/processSlashCommand.js'
|
||||
)
|
||||
const loaded = await Promise.all(
|
||||
validSkills.map(async ({ skillName, skill }) => ({
|
||||
skillName,
|
||||
skill,
|
||||
content: await skill.getPromptForCommand('', toolUseContext),
|
||||
})),
|
||||
)
|
||||
for (const { skillName, skill, content } of loaded) {
|
||||
logForDebugging(
|
||||
`[Agent: ${agentDefinition.agentType}] Preloaded skill '${skillName}'`,
|
||||
)
|
||||
|
||||
// Add command-message metadata so the UI shows which skill is loading
|
||||
const metadata = formatSkillLoadingMetadata(
|
||||
skillName,
|
||||
skill.progressMessage,
|
||||
)
|
||||
|
||||
initialMessages.push(
|
||||
createUserMessage({
|
||||
content: [{ type: 'text', text: metadata }, ...content],
|
||||
isMeta: true,
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize agent-specific MCP servers (additive to parent's servers)
|
||||
const {
|
||||
clients: mergedMcpClients,
|
||||
tools: agentMcpTools,
|
||||
cleanup: mcpCleanup,
|
||||
} = await initializeAgentMcpServers(
|
||||
agentDefinition,
|
||||
toolUseContext.options.mcpClients,
|
||||
)
|
||||
|
||||
// Merge agent MCP tools with resolved agent tools, deduplicating by name.
|
||||
// resolvedTools is already deduplicated (see resolveAgentTools), so skip
|
||||
// the spread + uniqBy overhead when there are no agent-specific MCP tools.
|
||||
const allTools =
|
||||
agentMcpTools.length > 0
|
||||
? uniqBy([...resolvedTools, ...agentMcpTools], 'name')
|
||||
: resolvedTools
|
||||
|
||||
// Build agent-specific options
|
||||
const agentOptions: ToolUseContext['options'] = {
|
||||
isNonInteractiveSession: useExactTools
|
||||
? toolUseContext.options.isNonInteractiveSession
|
||||
: isAsync
|
||||
? true
|
||||
: (toolUseContext.options.isNonInteractiveSession ?? false),
|
||||
appendSystemPrompt: toolUseContext.options.appendSystemPrompt,
|
||||
tools: allTools,
|
||||
commands: [],
|
||||
debug: toolUseContext.options.debug,
|
||||
verbose: toolUseContext.options.verbose,
|
||||
mainLoopModel: resolvedAgentModel,
|
||||
// For fork children (useExactTools), inherit thinking config to match the
|
||||
// parent's API request prefix for prompt cache hits. For regular
|
||||
// sub-agents, disable thinking to control output token costs.
|
||||
thinkingConfig: useExactTools
|
||||
? toolUseContext.options.thinkingConfig
|
||||
: { type: 'disabled' as const },
|
||||
mcpClients: mergedMcpClients,
|
||||
mcpResources: toolUseContext.options.mcpResources,
|
||||
agentDefinitions: toolUseContext.options.agentDefinitions,
|
||||
// Fork children (useExactTools path) need querySource on context.options
|
||||
// for the recursive-fork guard at AgentTool.tsx call() — it checks
|
||||
// options.querySource === 'agent:builtin:fork'. This survives autocompact
|
||||
// (which rewrites messages, not context.options). Without this, the guard
|
||||
// reads undefined and only the message-scan fallback fires — which
|
||||
// autocompact defeats by replacing the fork-boilerplate message.
|
||||
...(useExactTools && { querySource }),
|
||||
}
|
||||
|
||||
// Create subagent context using shared helper
|
||||
// - Sync agents share setAppState, setResponseLength, abortController with parent
|
||||
// - Async agents are fully isolated (but with explicit unlinked abortController)
|
||||
const agentToolUseContext = createSubagentContext(toolUseContext, {
|
||||
options: agentOptions,
|
||||
agentId,
|
||||
agentType: agentDefinition.agentType,
|
||||
messages: initialMessages,
|
||||
readFileState: agentReadFileState,
|
||||
abortController: agentAbortController,
|
||||
getAppState: agentGetAppState,
|
||||
// Sync agents share these callbacks with parent
|
||||
shareSetAppState: !isAsync,
|
||||
shareSetResponseLength: true, // Both sync and async contribute to response metrics
|
||||
criticalSystemReminder_EXPERIMENTAL:
|
||||
agentDefinition.criticalSystemReminder_EXPERIMENTAL,
|
||||
contentReplacementState,
|
||||
})
|
||||
|
||||
// Preserve tool use results for subagents with viewable transcripts (in-process teammates)
|
||||
if (preserveToolUseResults) {
|
||||
agentToolUseContext.preserveToolUseResults = true
|
||||
}
|
||||
|
||||
// Expose cache-safe params for background summarization (prompt cache sharing)
|
||||
if (onCacheSafeParams) {
|
||||
onCacheSafeParams({
|
||||
systemPrompt: agentSystemPrompt,
|
||||
userContext: resolvedUserContext,
|
||||
systemContext: resolvedSystemContext,
|
||||
toolUseContext: agentToolUseContext,
|
||||
forkContextMessages: initialMessages,
|
||||
})
|
||||
}
|
||||
|
||||
// Record initial messages before the query loop starts, plus the agentType
|
||||
// so resume can route correctly when subagent_type is omitted. Both writes
|
||||
// are fire-and-forget — persistence failure shouldn't block the agent.
|
||||
void recordSidechainTranscript(initialMessages, agentId).catch(_err =>
|
||||
logForDebugging(`Failed to record sidechain transcript: ${_err}`),
|
||||
)
|
||||
void writeAgentMetadata(agentId, {
|
||||
agentType: agentDefinition.agentType,
|
||||
...(worktreePath && { worktreePath }),
|
||||
...(description && { description }),
|
||||
}).catch(_err => logForDebugging(`Failed to write agent metadata: ${_err}`))
|
||||
|
||||
// Track the last recorded message UUID for parent chain continuity
|
||||
let lastRecordedUuid: UUID | null = initialMessages.at(-1)?.uuid ?? null
|
||||
|
||||
try {
|
||||
for await (const message of query({
|
||||
messages: initialMessages,
|
||||
systemPrompt: agentSystemPrompt,
|
||||
userContext: resolvedUserContext,
|
||||
systemContext: resolvedSystemContext,
|
||||
canUseTool,
|
||||
toolUseContext: agentToolUseContext,
|
||||
querySource,
|
||||
maxTurns: maxTurns ?? agentDefinition.maxTurns,
|
||||
})) {
|
||||
onQueryProgress?.()
|
||||
// Forward subagent API request starts to parent's metrics display
|
||||
// so TTFT/OTPS update during subagent execution.
|
||||
if (
|
||||
message.type === 'stream_event' &&
|
||||
message.event.type === 'message_start' &&
|
||||
message.ttftMs != null
|
||||
) {
|
||||
toolUseContext.pushApiMetricsEntry?.(message.ttftMs)
|
||||
continue
|
||||
}
|
||||
|
||||
// Yield attachment messages (e.g., structured_output) without recording them
|
||||
if (message.type === 'attachment') {
|
||||
// Handle max turns reached signal from query.ts
|
||||
if (message.attachment.type === 'max_turns_reached') {
|
||||
logForDebugging(
|
||||
`[Agent: ${agentDefinition.agentType}] Reached max turns limit (${message.attachment.maxTurns})`,
|
||||
)
|
||||
break
|
||||
}
|
||||
yield message
|
||||
continue
|
||||
}
|
||||
|
||||
if (isRecordableMessage(message)) {
|
||||
// Record only the new message with correct parent (O(1) per message)
|
||||
await recordSidechainTranscript(
|
||||
[message],
|
||||
agentId,
|
||||
lastRecordedUuid,
|
||||
).catch(err =>
|
||||
logForDebugging(`Failed to record sidechain transcript: ${err}`),
|
||||
)
|
||||
if (message.type !== 'progress') {
|
||||
lastRecordedUuid = message.uuid
|
||||
}
|
||||
yield message
|
||||
}
|
||||
}
|
||||
|
||||
if (agentAbortController.signal.aborted) {
|
||||
throw new AbortError()
|
||||
}
|
||||
|
||||
// Run callback if provided (only built-in agents have callbacks)
|
||||
if (isBuiltInAgent(agentDefinition) && agentDefinition.callback) {
|
||||
agentDefinition.callback()
|
||||
}
|
||||
} finally {
|
||||
// Clean up agent-specific MCP servers (runs on normal completion, abort, or error)
|
||||
await mcpCleanup()
|
||||
// Clean up agent's session hooks
|
||||
if (agentDefinition.hooks) {
|
||||
clearSessionHooks(rootSetAppState, agentId)
|
||||
}
|
||||
// Clean up prompt cache tracking state for this agent
|
||||
if (feature('PROMPT_CACHE_BREAK_DETECTION')) {
|
||||
cleanupAgentTracking(agentId)
|
||||
}
|
||||
// Release cloned file state cache memory
|
||||
agentToolUseContext.readFileState.clear()
|
||||
// Release the cloned fork context messages
|
||||
initialMessages.length = 0
|
||||
// Release perfetto agent registry entry
|
||||
unregisterPerfettoAgent(agentId)
|
||||
// Release transcript subdir mapping
|
||||
clearAgentTranscriptSubdir(agentId)
|
||||
// Release this agent's todos entry. Without this, every subagent that
|
||||
// called TodoWrite leaves a key in AppState.todos forever (even after all
|
||||
// items complete, the value is [] but the key stays). Whale sessions
|
||||
// spawn hundreds of agents; each orphaned key is a small leak that adds up.
|
||||
rootSetAppState(prev => {
|
||||
if (!(agentId in prev.todos)) return prev
|
||||
const { [agentId]: _removed, ...todos } = prev.todos
|
||||
return { ...prev, todos }
|
||||
})
|
||||
// Kill any background bash tasks this agent spawned. Without this, a
|
||||
// `run_in_background` shell loop (e.g. test fixture fake-logs.sh) outlives
|
||||
// the agent as a PPID=1 zombie once the main session eventually exits.
|
||||
killShellTasksForAgent(agentId, toolUseContext.getAppState, rootSetAppState)
|
||||
/* eslint-disable @typescript-eslint/no-require-imports */
|
||||
if (feature('MONITOR_TOOL')) {
|
||||
const mcpMod =
|
||||
require('../../tasks/MonitorMcpTask/MonitorMcpTask.js') as typeof import('../../tasks/MonitorMcpTask/MonitorMcpTask.js')
|
||||
mcpMod.killMonitorMcpTasksForAgent(
|
||||
agentId,
|
||||
toolUseContext.getAppState,
|
||||
rootSetAppState,
|
||||
)
|
||||
}
|
||||
/* eslint-enable @typescript-eslint/no-require-imports */
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters out assistant messages with incomplete tool calls (tool uses without results).
|
||||
* This prevents API errors when sending messages with orphaned tool calls.
|
||||
*/
|
||||
export function filterIncompleteToolCalls(messages: Message[]): Message[] {
|
||||
// Build a set of tool use IDs that have results
|
||||
const toolUseIdsWithResults = new Set<string>()
|
||||
|
||||
for (const message of messages) {
|
||||
if (message?.type === 'user') {
|
||||
const userMessage = message as UserMessage
|
||||
const content = userMessage.message.content
|
||||
if (Array.isArray(content)) {
|
||||
for (const block of content) {
|
||||
if (block.type === 'tool_result' && block.tool_use_id) {
|
||||
toolUseIdsWithResults.add(block.tool_use_id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Filter out assistant messages that contain tool calls without results
|
||||
return messages.filter(message => {
|
||||
if (message?.type === 'assistant') {
|
||||
const assistantMessage = message as AssistantMessage
|
||||
const content = assistantMessage.message.content
|
||||
if (Array.isArray(content)) {
|
||||
// Check if this assistant message has any tool uses without results
|
||||
const hasIncompleteToolCall = content.some(
|
||||
block =>
|
||||
block.type === 'tool_use' &&
|
||||
block.id &&
|
||||
!toolUseIdsWithResults.has(block.id),
|
||||
)
|
||||
// Exclude messages with incomplete tool calls
|
||||
return !hasIncompleteToolCall
|
||||
}
|
||||
}
|
||||
// Keep all non-assistant messages and assistant messages without tool calls
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
async function getAgentSystemPrompt(
|
||||
agentDefinition: AgentDefinition,
|
||||
toolUseContext: Pick<ToolUseContext, 'options'>,
|
||||
resolvedAgentModel: string,
|
||||
additionalWorkingDirectories: string[],
|
||||
resolvedTools: readonly Tool[],
|
||||
): Promise<string[]> {
|
||||
const enabledToolNames = new Set(resolvedTools.map(t => t.name))
|
||||
try {
|
||||
const agentPrompt = agentDefinition.getSystemPrompt({ toolUseContext })
|
||||
const prompts = [agentPrompt]
|
||||
|
||||
return await enhanceSystemPromptWithEnvDetails(
|
||||
prompts,
|
||||
resolvedAgentModel,
|
||||
additionalWorkingDirectories,
|
||||
enabledToolNames,
|
||||
)
|
||||
} catch (_error) {
|
||||
return enhanceSystemPromptWithEnvDetails(
|
||||
[DEFAULT_AGENT_PROMPT],
|
||||
resolvedAgentModel,
|
||||
additionalWorkingDirectories,
|
||||
enabledToolNames,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a skill name from agent frontmatter to a registered command name.
|
||||
*
|
||||
* Plugin skills are registered with namespaced names (e.g., "my-plugin:my-skill")
|
||||
* but agents reference them with bare names (e.g., "my-skill"). This function
|
||||
* tries multiple resolution strategies:
|
||||
*
|
||||
* 1. Exact match via hasCommand (name, userFacingName, aliases)
|
||||
* 2. Prefix with agent's plugin name (e.g., "my-skill" → "my-plugin:my-skill")
|
||||
* 3. Suffix match — find any command whose name ends with ":skillName"
|
||||
*/
|
||||
function resolveSkillName(
|
||||
skillName: string,
|
||||
allSkills: Command[],
|
||||
agentDefinition: AgentDefinition,
|
||||
): string | null {
|
||||
// 1. Direct match
|
||||
if (hasCommand(skillName, allSkills)) {
|
||||
return skillName
|
||||
}
|
||||
|
||||
// 2. Try prefixing with the agent's plugin name
|
||||
// Plugin agents have agentType like "pluginName:agentName"
|
||||
const pluginPrefix = agentDefinition.agentType.split(':')[0]
|
||||
if (pluginPrefix) {
|
||||
const qualifiedName = `${pluginPrefix}:${skillName}`
|
||||
if (hasCommand(qualifiedName, allSkills)) {
|
||||
return qualifiedName
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Suffix match — find a skill whose name ends with ":skillName"
|
||||
const suffix = `:${skillName}`
|
||||
const match = allSkills.find(cmd => cmd.name.endsWith(suffix))
|
||||
if (match) {
|
||||
return match.name
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
Reference in New Issue
Block a user