mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-15 12:55:51 +00:00
将 TeamCreate、TeamDelete、SendMessage 从 CORE_TOOLS 移除, 使其成为 deferred 工具,通过 ToolSearch 按需发现以减少 context token。 swarm 模式下 SendMessage 保持 always loaded,TeamCreate/TeamDelete 在 swarm 未启用时调用返回启用提示。 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
248 lines
7.7 KiB
TypeScript
248 lines
7.7 KiB
TypeScript
import { z } from 'zod/v4'
|
|
import { getSessionId } from 'src/bootstrap/state.js'
|
|
import { logEvent } from 'src/services/analytics/index.js'
|
|
import type { AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS } from 'src/services/analytics/metadata.js'
|
|
import type { Tool } from 'src/Tool.js'
|
|
import { buildTool, type ToolDef } from 'src/Tool.js'
|
|
import { formatAgentId } from 'src/utils/agentId.js'
|
|
import { isAgentSwarmsEnabled } from 'src/utils/agentSwarmsEnabled.js'
|
|
import { getCwd } from 'src/utils/cwd.js'
|
|
import { lazySchema } from 'src/utils/lazySchema.js'
|
|
import {
|
|
getDefaultMainLoopModel,
|
|
parseUserSpecifiedModel,
|
|
} from 'src/utils/model/model.js'
|
|
import { jsonStringify } from 'src/utils/slowOperations.js'
|
|
import { getResolvedTeammateMode } from 'src/utils/swarm/backends/registry.js'
|
|
import { TEAM_LEAD_NAME } from 'src/utils/swarm/constants.js'
|
|
import type { TeamFile } from 'src/utils/swarm/teamHelpers.js'
|
|
import {
|
|
getTeamFilePath,
|
|
readTeamFile,
|
|
registerTeamForSessionCleanup,
|
|
sanitizeName,
|
|
writeTeamFileAsync,
|
|
} from 'src/utils/swarm/teamHelpers.js'
|
|
import { assignTeammateColor } from 'src/utils/swarm/teammateLayoutManager.js'
|
|
import {
|
|
ensureTasksDir,
|
|
resetTaskList,
|
|
setLeaderTeamName,
|
|
} from 'src/utils/tasks.js'
|
|
import { generateWordSlug } from 'src/utils/words.js'
|
|
import { TEAM_CREATE_TOOL_NAME } from './constants.js'
|
|
import { getPrompt } from './prompt.js'
|
|
import { renderToolUseMessage } from './UI.js'
|
|
|
|
const inputSchema = lazySchema(() =>
|
|
z.strictObject({
|
|
team_name: z.string().describe('Name for the new team to create.'),
|
|
description: z.string().optional().describe('Team description/purpose.'),
|
|
agent_type: z
|
|
.string()
|
|
.optional()
|
|
.describe(
|
|
'Type/role of the team lead (e.g., "researcher", "test-runner"). ' +
|
|
'Used for team file and inter-agent coordination.',
|
|
),
|
|
}),
|
|
)
|
|
type InputSchema = ReturnType<typeof inputSchema>
|
|
|
|
export type Output = {
|
|
team_name: string
|
|
team_file_path: string
|
|
lead_agent_id: string
|
|
}
|
|
|
|
export type Input = z.infer<InputSchema>
|
|
|
|
/**
|
|
* Generates a unique team name by checking if the provided name already exists.
|
|
* If the name already exists, generates a new word slug.
|
|
*/
|
|
function generateUniqueTeamName(providedName: string): string {
|
|
// If the team doesn't exist, use the provided name
|
|
if (!readTeamFile(providedName)) {
|
|
return providedName
|
|
}
|
|
|
|
// Team exists, generate a new unique name
|
|
return generateWordSlug()
|
|
}
|
|
|
|
export const TeamCreateTool: Tool<InputSchema, Output> = buildTool({
|
|
name: TEAM_CREATE_TOOL_NAME,
|
|
searchHint:
|
|
'create multi-agent swarm team, collaborate, parallel agents, task distribution, agent coordination, team management',
|
|
maxResultSizeChars: 100_000,
|
|
shouldDefer: true,
|
|
|
|
userFacingName() {
|
|
return ''
|
|
},
|
|
|
|
get inputSchema(): InputSchema {
|
|
return inputSchema()
|
|
},
|
|
|
|
isEnabled() {
|
|
return true
|
|
},
|
|
|
|
toAutoClassifierInput(input) {
|
|
return input.team_name
|
|
},
|
|
|
|
async validateInput(input, _context) {
|
|
if (!input.team_name || input.team_name.trim().length === 0) {
|
|
return {
|
|
result: false,
|
|
message: 'team_name is required for TeamCreate',
|
|
errorCode: 9,
|
|
}
|
|
}
|
|
return { result: true }
|
|
},
|
|
|
|
async description() {
|
|
return 'Create a new team for coordinating multiple agents'
|
|
},
|
|
|
|
async prompt() {
|
|
return getPrompt()
|
|
},
|
|
|
|
mapToolResultToToolResultBlockParam(data, toolUseID) {
|
|
return {
|
|
tool_use_id: toolUseID,
|
|
type: 'tool_result' as const,
|
|
content: [
|
|
{
|
|
type: 'text' as const,
|
|
text: jsonStringify(data),
|
|
},
|
|
],
|
|
}
|
|
},
|
|
|
|
async call(input, context) {
|
|
if (!isAgentSwarmsEnabled()) {
|
|
throw new Error(
|
|
'Agent Teams 功能未启用。请确保未设置 CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS_DISABLED 环境变量。',
|
|
)
|
|
}
|
|
|
|
const { setAppState, getAppState } = context
|
|
const { team_name, description: _description, agent_type } = input
|
|
|
|
// Check if already in a team - restrict to one team per leader
|
|
const appState = getAppState()
|
|
const existingTeam = appState.teamContext?.teamName
|
|
|
|
if (existingTeam) {
|
|
throw new Error(
|
|
`Already leading team "${existingTeam}". A leader can only manage one team at a time. Use TeamDelete to end the current team before creating a new one.`,
|
|
)
|
|
}
|
|
|
|
// If team already exists, generate a unique name instead of failing
|
|
const finalTeamName = generateUniqueTeamName(team_name)
|
|
|
|
// Generate a deterministic agent ID for the team lead
|
|
const leadAgentId = formatAgentId(TEAM_LEAD_NAME, finalTeamName)
|
|
const leadAgentType = agent_type || TEAM_LEAD_NAME
|
|
// Get the team lead's current model from AppState (handles session model, settings, CLI override)
|
|
const leadModel = parseUserSpecifiedModel(
|
|
appState.mainLoopModelForSession ??
|
|
appState.mainLoopModel ??
|
|
getDefaultMainLoopModel(),
|
|
)
|
|
|
|
const teamFilePath = getTeamFilePath(finalTeamName)
|
|
|
|
const teamFile: TeamFile = {
|
|
name: finalTeamName,
|
|
description: _description,
|
|
createdAt: Date.now(),
|
|
leadAgentId,
|
|
leadSessionId: getSessionId(), // Store actual session ID for team discovery
|
|
members: [
|
|
{
|
|
agentId: leadAgentId,
|
|
name: TEAM_LEAD_NAME,
|
|
agentType: leadAgentType,
|
|
model: leadModel,
|
|
joinedAt: Date.now(),
|
|
tmuxPaneId: '',
|
|
cwd: getCwd(),
|
|
subscriptions: [],
|
|
},
|
|
],
|
|
}
|
|
|
|
await writeTeamFileAsync(finalTeamName, teamFile)
|
|
// Track for session-end cleanup — teams were left on disk forever
|
|
// unless explicitly TeamDelete'd (gh-32730).
|
|
registerTeamForSessionCleanup(finalTeamName)
|
|
|
|
// Reset and create the corresponding task list directory (Team = Project = TaskList)
|
|
// This ensures task numbering starts fresh at 1 for each new swarm
|
|
const taskListId = sanitizeName(finalTeamName)
|
|
await resetTaskList(taskListId)
|
|
await ensureTasksDir(taskListId)
|
|
|
|
// Register the team name so getTaskListId() returns it for the leader.
|
|
// Without this, the leader falls through to getSessionId() and writes tasks
|
|
// to a different directory than tmux/iTerm2 teammates expect.
|
|
setLeaderTeamName(sanitizeName(finalTeamName))
|
|
|
|
// Update AppState with team context
|
|
setAppState(prev => ({
|
|
...prev,
|
|
teamContext: {
|
|
teamName: finalTeamName,
|
|
teamFilePath,
|
|
leadAgentId,
|
|
teammates: {
|
|
[leadAgentId]: {
|
|
name: TEAM_LEAD_NAME,
|
|
agentType: leadAgentType,
|
|
color: assignTeammateColor(leadAgentId),
|
|
tmuxSessionName: '',
|
|
tmuxPaneId: '',
|
|
cwd: getCwd(),
|
|
spawnedAt: Date.now(),
|
|
},
|
|
},
|
|
},
|
|
}))
|
|
|
|
logEvent('tengu_team_created', {
|
|
team_name:
|
|
finalTeamName as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
|
teammate_count: 1,
|
|
lead_agent_type:
|
|
leadAgentType as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
|
teammate_mode:
|
|
getResolvedTeammateMode() as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
|
})
|
|
|
|
// Note: We intentionally don't set CLAUDE_CODE_AGENT_ID for the team lead because:
|
|
// 1. The lead is not a "teammate" - isTeammate() should return false for them
|
|
// 2. Their ID is deterministic (team-lead@teamName) and can be derived when needed
|
|
// 3. Setting it would cause isTeammate() to return true, breaking inbox polling
|
|
// Team name is stored in AppState.teamContext, not process.env
|
|
|
|
return {
|
|
data: {
|
|
team_name: finalTeamName,
|
|
team_file_path: teamFilePath,
|
|
lead_agent_id: leadAgentId,
|
|
},
|
|
}
|
|
},
|
|
|
|
renderToolUseMessage,
|
|
} satisfies ToolDef<InputSchema, Output>)
|