source dump of claude code
at main 240 lines 7.7 kB view raw
1import { z } from 'zod/v4' 2import { getSessionId } from '../../bootstrap/state.js' 3import { logEvent } from '../../services/analytics/index.js' 4import type { AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS } from '../../services/analytics/metadata.js' 5import type { Tool } from '../../Tool.js' 6import { buildTool, type ToolDef } from '../../Tool.js' 7import { formatAgentId } from '../../utils/agentId.js' 8import { isAgentSwarmsEnabled } from '../../utils/agentSwarmsEnabled.js' 9import { getCwd } from '../../utils/cwd.js' 10import { lazySchema } from '../../utils/lazySchema.js' 11import { 12 getDefaultMainLoopModel, 13 parseUserSpecifiedModel, 14} from '../../utils/model/model.js' 15import { jsonStringify } from '../../utils/slowOperations.js' 16import { getResolvedTeammateMode } from '../../utils/swarm/backends/registry.js' 17import { TEAM_LEAD_NAME } from '../../utils/swarm/constants.js' 18import type { TeamFile } from '../../utils/swarm/teamHelpers.js' 19import { 20 getTeamFilePath, 21 readTeamFile, 22 registerTeamForSessionCleanup, 23 sanitizeName, 24 writeTeamFileAsync, 25} from '../../utils/swarm/teamHelpers.js' 26import { assignTeammateColor } from '../../utils/swarm/teammateLayoutManager.js' 27import { 28 ensureTasksDir, 29 resetTaskList, 30 setLeaderTeamName, 31} from '../../utils/tasks.js' 32import { generateWordSlug } from '../../utils/words.js' 33import { TEAM_CREATE_TOOL_NAME } from './constants.js' 34import { getPrompt } from './prompt.js' 35import { renderToolUseMessage } from './UI.js' 36 37const inputSchema = lazySchema(() => 38 z.strictObject({ 39 team_name: z.string().describe('Name for the new team to create.'), 40 description: z.string().optional().describe('Team description/purpose.'), 41 agent_type: z 42 .string() 43 .optional() 44 .describe( 45 'Type/role of the team lead (e.g., "researcher", "test-runner"). ' + 46 'Used for team file and inter-agent coordination.', 47 ), 48 }), 49) 50type InputSchema = ReturnType<typeof inputSchema> 51 52export type Output = { 53 team_name: string 54 team_file_path: string 55 lead_agent_id: string 56} 57 58export type Input = z.infer<InputSchema> 59 60/** 61 * Generates a unique team name by checking if the provided name already exists. 62 * If the name already exists, generates a new word slug. 63 */ 64function generateUniqueTeamName(providedName: string): string { 65 // If the team doesn't exist, use the provided name 66 if (!readTeamFile(providedName)) { 67 return providedName 68 } 69 70 // Team exists, generate a new unique name 71 return generateWordSlug() 72} 73 74export const TeamCreateTool: Tool<InputSchema, Output> = buildTool({ 75 name: TEAM_CREATE_TOOL_NAME, 76 searchHint: 'create a multi-agent swarm team', 77 maxResultSizeChars: 100_000, 78 shouldDefer: true, 79 80 userFacingName() { 81 return '' 82 }, 83 84 get inputSchema(): InputSchema { 85 return inputSchema() 86 }, 87 88 isEnabled() { 89 return isAgentSwarmsEnabled() 90 }, 91 92 toAutoClassifierInput(input) { 93 return input.team_name 94 }, 95 96 async validateInput(input, _context) { 97 if (!input.team_name || input.team_name.trim().length === 0) { 98 return { 99 result: false, 100 message: 'team_name is required for TeamCreate', 101 errorCode: 9, 102 } 103 } 104 return { result: true } 105 }, 106 107 async description() { 108 return 'Create a new team for coordinating multiple agents' 109 }, 110 111 async prompt() { 112 return getPrompt() 113 }, 114 115 mapToolResultToToolResultBlockParam(data, toolUseID) { 116 return { 117 tool_use_id: toolUseID, 118 type: 'tool_result' as const, 119 content: [ 120 { 121 type: 'text' as const, 122 text: jsonStringify(data), 123 }, 124 ], 125 } 126 }, 127 128 async call(input, context) { 129 const { setAppState, getAppState } = context 130 const { team_name, description: _description, agent_type } = input 131 132 // Check if already in a team - restrict to one team per leader 133 const appState = getAppState() 134 const existingTeam = appState.teamContext?.teamName 135 136 if (existingTeam) { 137 throw new Error( 138 `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.`, 139 ) 140 } 141 142 // If team already exists, generate a unique name instead of failing 143 const finalTeamName = generateUniqueTeamName(team_name) 144 145 // Generate a deterministic agent ID for the team lead 146 const leadAgentId = formatAgentId(TEAM_LEAD_NAME, finalTeamName) 147 const leadAgentType = agent_type || TEAM_LEAD_NAME 148 // Get the team lead's current model from AppState (handles session model, settings, CLI override) 149 const leadModel = parseUserSpecifiedModel( 150 appState.mainLoopModelForSession ?? 151 appState.mainLoopModel ?? 152 getDefaultMainLoopModel(), 153 ) 154 155 const teamFilePath = getTeamFilePath(finalTeamName) 156 157 const teamFile: TeamFile = { 158 name: finalTeamName, 159 description: _description, 160 createdAt: Date.now(), 161 leadAgentId, 162 leadSessionId: getSessionId(), // Store actual session ID for team discovery 163 members: [ 164 { 165 agentId: leadAgentId, 166 name: TEAM_LEAD_NAME, 167 agentType: leadAgentType, 168 model: leadModel, 169 joinedAt: Date.now(), 170 tmuxPaneId: '', 171 cwd: getCwd(), 172 subscriptions: [], 173 }, 174 ], 175 } 176 177 await writeTeamFileAsync(finalTeamName, teamFile) 178 // Track for session-end cleanup — teams were left on disk forever 179 // unless explicitly TeamDelete'd (gh-32730). 180 registerTeamForSessionCleanup(finalTeamName) 181 182 // Reset and create the corresponding task list directory (Team = Project = TaskList) 183 // This ensures task numbering starts fresh at 1 for each new swarm 184 const taskListId = sanitizeName(finalTeamName) 185 await resetTaskList(taskListId) 186 await ensureTasksDir(taskListId) 187 188 // Register the team name so getTaskListId() returns it for the leader. 189 // Without this, the leader falls through to getSessionId() and writes tasks 190 // to a different directory than tmux/iTerm2 teammates expect. 191 setLeaderTeamName(sanitizeName(finalTeamName)) 192 193 // Update AppState with team context 194 setAppState(prev => ({ 195 ...prev, 196 teamContext: { 197 teamName: finalTeamName, 198 teamFilePath, 199 leadAgentId, 200 teammates: { 201 [leadAgentId]: { 202 name: TEAM_LEAD_NAME, 203 agentType: leadAgentType, 204 color: assignTeammateColor(leadAgentId), 205 tmuxSessionName: '', 206 tmuxPaneId: '', 207 cwd: getCwd(), 208 spawnedAt: Date.now(), 209 }, 210 }, 211 }, 212 })) 213 214 logEvent('tengu_team_created', { 215 team_name: 216 finalTeamName as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 217 teammate_count: 1, 218 lead_agent_type: 219 leadAgentType as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 220 teammate_mode: 221 getResolvedTeammateMode() as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 222 }) 223 224 // Note: We intentionally don't set CLAUDE_CODE_AGENT_ID for the team lead because: 225 // 1. The lead is not a "teammate" - isTeammate() should return false for them 226 // 2. Their ID is deterministic (team-lead@teamName) and can be derived when needed 227 // 3. Setting it would cause isTeammate() to return true, breaking inbox polling 228 // Team name is stored in AppState.teamContext, not process.env 229 230 return { 231 data: { 232 team_name: finalTeamName, 233 team_file_path: teamFilePath, 234 lead_agent_id: leadAgentId, 235 }, 236 } 237 }, 238 239 renderToolUseMessage, 240} satisfies ToolDef<InputSchema, Output>)