chore: initialize recovered claude workspace
This commit is contained in:
240
src/tools/TeamCreateTool/TeamCreateTool.ts
Normal file
240
src/tools/TeamCreateTool/TeamCreateTool.ts
Normal file
@@ -0,0 +1,240 @@
|
||||
import { z } from 'zod/v4'
|
||||
import { getSessionId } from '../../bootstrap/state.js'
|
||||
import { logEvent } from '../../services/analytics/index.js'
|
||||
import type { AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS } from '../../services/analytics/metadata.js'
|
||||
import type { Tool } from '../../Tool.js'
|
||||
import { buildTool, type ToolDef } from '../../Tool.js'
|
||||
import { formatAgentId } from '../../utils/agentId.js'
|
||||
import { isAgentSwarmsEnabled } from '../../utils/agentSwarmsEnabled.js'
|
||||
import { getCwd } from '../../utils/cwd.js'
|
||||
import { lazySchema } from '../../utils/lazySchema.js'
|
||||
import {
|
||||
getDefaultMainLoopModel,
|
||||
parseUserSpecifiedModel,
|
||||
} from '../../utils/model/model.js'
|
||||
import { jsonStringify } from '../../utils/slowOperations.js'
|
||||
import { getResolvedTeammateMode } from '../../utils/swarm/backends/registry.js'
|
||||
import { TEAM_LEAD_NAME } from '../../utils/swarm/constants.js'
|
||||
import type { TeamFile } from '../../utils/swarm/teamHelpers.js'
|
||||
import {
|
||||
getTeamFilePath,
|
||||
readTeamFile,
|
||||
registerTeamForSessionCleanup,
|
||||
sanitizeName,
|
||||
writeTeamFileAsync,
|
||||
} from '../../utils/swarm/teamHelpers.js'
|
||||
import { assignTeammateColor } from '../../utils/swarm/teammateLayoutManager.js'
|
||||
import {
|
||||
ensureTasksDir,
|
||||
resetTaskList,
|
||||
setLeaderTeamName,
|
||||
} from '../../utils/tasks.js'
|
||||
import { generateWordSlug } from '../../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 a multi-agent swarm team',
|
||||
maxResultSizeChars: 100_000,
|
||||
shouldDefer: true,
|
||||
|
||||
userFacingName() {
|
||||
return ''
|
||||
},
|
||||
|
||||
get inputSchema(): InputSchema {
|
||||
return inputSchema()
|
||||
},
|
||||
|
||||
isEnabled() {
|
||||
return isAgentSwarmsEnabled()
|
||||
},
|
||||
|
||||
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) {
|
||||
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>)
|
||||
Reference in New Issue
Block a user