import { z } from 'zod/v4' 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 { isAgentSwarmsEnabled } from '../../utils/agentSwarmsEnabled.js' import { lazySchema } from '../../utils/lazySchema.js' import { jsonStringify } from '../../utils/slowOperations.js' import { TEAM_LEAD_NAME } from '../../utils/swarm/constants.js' import { cleanupTeamDirectories, readTeamFile, unregisterTeamForSessionCleanup, } from '../../utils/swarm/teamHelpers.js' import { clearTeammateColors } from '../../utils/swarm/teammateLayoutManager.js' import { clearLeaderTeamName } from '../../utils/tasks.js' import { TEAM_DELETE_TOOL_NAME } from './constants.js' import { getPrompt } from './prompt.js' import { renderToolResultMessage, renderToolUseMessage } from './UI.js' const inputSchema = lazySchema(() => z.strictObject({})) type InputSchema = ReturnType export type Output = { success: boolean message: string team_name?: string } export type Input = z.infer export const TeamDeleteTool: Tool = buildTool({ name: TEAM_DELETE_TOOL_NAME, searchHint: 'disband a swarm team and clean up', maxResultSizeChars: 100_000, shouldDefer: true, userFacingName() { return '' }, get inputSchema(): InputSchema { return inputSchema() }, isEnabled() { return isAgentSwarmsEnabled() }, async description() { return 'Clean up team and task directories when the swarm is complete' }, 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 appState = getAppState() const teamName = appState.teamContext?.teamName if (teamName) { // Read team config to check for active members const teamFile = readTeamFile(teamName) if (teamFile) { // Filter out the team lead - only count non-lead members const nonLeadMembers = teamFile.members.filter( m => m.name !== TEAM_LEAD_NAME, ) // Separate truly active members from idle/dead ones // Members with isActive === false are idle (finished their turn or crashed) const activeMembers = nonLeadMembers.filter(m => m.isActive !== false) if (activeMembers.length > 0) { const memberNames = activeMembers.map(m => m.name).join(', ') return { data: { success: false, message: `Cannot cleanup team with ${activeMembers.length} active member(s): ${memberNames}. Use requestShutdown to gracefully terminate teammates first.`, team_name: teamName, }, } } } await cleanupTeamDirectories(teamName) // Already cleaned — don't try again on gracefulShutdown. unregisterTeamForSessionCleanup(teamName) // Clear color assignments so new teams start fresh clearTeammateColors() // Clear leader team name so getTaskListId() falls back to session ID clearLeaderTeamName() logEvent('tengu_team_deleted', { team_name: teamName as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, }) } // Clear team context and inbox from app state setAppState(prev => ({ ...prev, teamContext: undefined, inbox: { messages: [], // Clear any queued messages }, })) return { data: { success: true, message: teamName ? `Cleaned up directories and worktrees for team "${teamName}"` : 'No team name found, nothing to clean up', team_name: teamName, }, } }, renderToolUseMessage, renderToolResultMessage, } satisfies ToolDef)