/ tools / TeamDeleteTool / TeamDeleteTool.ts
TeamDeleteTool.ts
  1  import { z } from 'zod/v4'
  2  import { logEvent } from '../../services/analytics/index.js'
  3  import type { AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS } from '../../services/analytics/metadata.js'
  4  import type { Tool } from '../../Tool.js'
  5  import { buildTool, type ToolDef } from '../../Tool.js'
  6  import { isAgentSwarmsEnabled } from '../../utils/agentSwarmsEnabled.js'
  7  import { lazySchema } from '../../utils/lazySchema.js'
  8  import { jsonStringify } from '../../utils/slowOperations.js'
  9  import { TEAM_LEAD_NAME } from '../../utils/swarm/constants.js'
 10  import {
 11    cleanupTeamDirectories,
 12    readTeamFile,
 13    unregisterTeamForSessionCleanup,
 14  } from '../../utils/swarm/teamHelpers.js'
 15  import { clearTeammateColors } from '../../utils/swarm/teammateLayoutManager.js'
 16  import { clearLeaderTeamName } from '../../utils/tasks.js'
 17  import { TEAM_DELETE_TOOL_NAME } from './constants.js'
 18  import { getPrompt } from './prompt.js'
 19  import { renderToolResultMessage, renderToolUseMessage } from './UI.js'
 20  
 21  const inputSchema = lazySchema(() => z.strictObject({}))
 22  type InputSchema = ReturnType<typeof inputSchema>
 23  
 24  export type Output = {
 25    success: boolean
 26    message: string
 27    team_name?: string
 28  }
 29  
 30  export type Input = z.infer<InputSchema>
 31  
 32  export const TeamDeleteTool: Tool<InputSchema, Output> = buildTool({
 33    name: TEAM_DELETE_TOOL_NAME,
 34    searchHint: 'disband a swarm team and clean up',
 35    maxResultSizeChars: 100_000,
 36    shouldDefer: true,
 37  
 38    userFacingName() {
 39      return ''
 40    },
 41  
 42    get inputSchema(): InputSchema {
 43      return inputSchema()
 44    },
 45  
 46    isEnabled() {
 47      return isAgentSwarmsEnabled()
 48    },
 49  
 50    async description() {
 51      return 'Clean up team and task directories when the swarm is complete'
 52    },
 53  
 54    async prompt() {
 55      return getPrompt()
 56    },
 57  
 58    mapToolResultToToolResultBlockParam(data, toolUseID) {
 59      return {
 60        tool_use_id: toolUseID,
 61        type: 'tool_result' as const,
 62        content: [
 63          {
 64            type: 'text' as const,
 65            text: jsonStringify(data),
 66          },
 67        ],
 68      }
 69    },
 70  
 71    async call(_input, context) {
 72      const { setAppState, getAppState } = context
 73      const appState = getAppState()
 74      const teamName = appState.teamContext?.teamName
 75  
 76      if (teamName) {
 77        // Read team config to check for active members
 78        const teamFile = readTeamFile(teamName)
 79        if (teamFile) {
 80          // Filter out the team lead - only count non-lead members
 81          const nonLeadMembers = teamFile.members.filter(
 82            m => m.name !== TEAM_LEAD_NAME,
 83          )
 84  
 85          // Separate truly active members from idle/dead ones
 86          // Members with isActive === false are idle (finished their turn or crashed)
 87          const activeMembers = nonLeadMembers.filter(m => m.isActive !== false)
 88  
 89          if (activeMembers.length > 0) {
 90            const memberNames = activeMembers.map(m => m.name).join(', ')
 91            return {
 92              data: {
 93                success: false,
 94                message: `Cannot cleanup team with ${activeMembers.length} active member(s): ${memberNames}. Use requestShutdown to gracefully terminate teammates first.`,
 95                team_name: teamName,
 96              },
 97            }
 98          }
 99        }
100  
101        await cleanupTeamDirectories(teamName)
102        // Already cleaned — don't try again on gracefulShutdown.
103        unregisterTeamForSessionCleanup(teamName)
104  
105        // Clear color assignments so new teams start fresh
106        clearTeammateColors()
107  
108        // Clear leader team name so getTaskListId() falls back to session ID
109        clearLeaderTeamName()
110  
111        logEvent('tengu_team_deleted', {
112          team_name:
113            teamName as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
114        })
115      }
116  
117      // Clear team context and inbox from app state
118      setAppState(prev => ({
119        ...prev,
120        teamContext: undefined,
121        inbox: {
122          messages: [], // Clear any queued messages
123        },
124      }))
125  
126      return {
127        data: {
128          success: true,
129          message: teamName
130            ? `Cleaned up directories and worktrees for team "${teamName}"`
131            : 'No team name found, nothing to clean up',
132          team_name: teamName,
133        },
134      }
135    },
136  
137    renderToolUseMessage,
138    renderToolResultMessage,
139  } satisfies ToolDef<InputSchema, Output>)