From b11137573f666d43ecc63670e3f8cfbf7bfaa346 Mon Sep 17 00:00:00 2001
From: Darby Frey <dfrey@gitlab.com>
Date: Mon, 15 Apr 2024 15:16:48 +0000
Subject: [PATCH] Adds ability to destroy AI Agents

---
 doc/api/graphql/reference/index.md            |  25 +++++
 .../ml/ai_agents/components/agent_delete.vue  | 104 ++++++++++++++++++
 .../ml/ai_agents/components/agent_list.vue    |   7 ++
 .../javascripts/ml/ai_agents/constants.js     |   2 -
 .../javascripts/ml/ai_agents/event_hub.js     |   3 +
 .../destroy_ai_agent.mutation.graphql         |   5 +
 .../ml/ai_agents/views/edit_agent.vue         |  64 ++++++++---
 .../ml/ai_agents/views/show_agent.vue         |   7 +-
 ee/app/graphql/ee/types/mutation_type.rb      |   1 +
 ee/app/graphql/mutations/ai/agents/base.rb    |   4 +
 ee/app/graphql/mutations/ai/agents/destroy.rb |  35 ++++++
 ee/app/graphql/mutations/ai/agents/update.rb  |   2 +-
 .../ai/agents/destroy_agent_service.rb        |  29 +++++
 .../frontend/ml/ai_agents/graphql/mocks.js    |  19 ++++
 .../ml/ai_agents/views/edit_agent_spec.js     |  62 +++++++++++
 .../api/graphql/ai/agents/destroy_spec.rb     |  50 +++++++++
 .../ai/agents/destroy_agent_service_spec.rb   |  32 ++++++
 locale/gitlab.pot                             |  24 ++++
 18 files changed, 455 insertions(+), 20 deletions(-)
 create mode 100644 ee/app/assets/javascripts/ml/ai_agents/components/agent_delete.vue
 create mode 100644 ee/app/assets/javascripts/ml/ai_agents/event_hub.js
 create mode 100644 ee/app/assets/javascripts/ml/ai_agents/graphql/mutations/destroy_ai_agent.mutation.graphql
 create mode 100644 ee/app/graphql/mutations/ai/agents/destroy.rb
 create mode 100644 ee/app/services/ai/agents/destroy_agent_service.rb
 create mode 100644 ee/spec/requests/api/graphql/ai/agents/destroy_spec.rb
 create mode 100644 ee/spec/services/ai/agents/destroy_agent_service_spec.rb

diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 595b850afdee5..8ffa906cbd367 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -1493,6 +1493,31 @@ Input type: `AiAgentCreateInput`
 | <a id="mutationaiagentcreateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
 | <a id="mutationaiagentcreateerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
 
+### `Mutation.aiAgentDestroy`
+
+DETAILS:
+**Introduced** in GitLab 16.11.
+**Status**: Experiment.
+
+Input type: `AiAgentDestroyInput`
+
+#### Arguments
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="mutationaiagentdestroyagentid"></a>`agentId` | [`AiAgentID!`](#aiagentid) | Global ID of the AI Agent to be deleted. |
+| <a id="mutationaiagentdestroyclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
+| <a id="mutationaiagentdestroyprojectpath"></a>`projectPath` | [`ID!`](#id) | Project to which the agent belongs. |
+
+#### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="mutationaiagentdestroyagent"></a>`agent` | [`AiAgent`](#aiagent) | Agent after mutation. |
+| <a id="mutationaiagentdestroyclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
+| <a id="mutationaiagentdestroyerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
+| <a id="mutationaiagentdestroymessage"></a>`message` | [`String`](#string) | AI Agent deletion result message. |
+
 ### `Mutation.aiAgentUpdate`
 
 DETAILS:
diff --git a/ee/app/assets/javascripts/ml/ai_agents/components/agent_delete.vue b/ee/app/assets/javascripts/ml/ai_agents/components/agent_delete.vue
new file mode 100644
index 0000000000000..c4d6faef87031
--- /dev/null
+++ b/ee/app/assets/javascripts/ml/ai_agents/components/agent_delete.vue
@@ -0,0 +1,104 @@
+<script>
+import { GlCard, GlButton, GlSprintf, GlModal, GlModalDirective } from '@gitlab/ui';
+import { __, s__ } from '~/locale';
+
+export default {
+  components: {
+    GlCard,
+    GlButton,
+    GlSprintf,
+    GlModal,
+  },
+  directives: {
+    GlModal: GlModalDirective,
+  },
+  inject: ['projectPath'],
+  props: {
+    agentVersion: {
+      type: Object,
+      required: false,
+      default: () => ({}),
+    },
+    loading: {
+      type: Boolean,
+      required: false,
+      default: false,
+    },
+  },
+  i18n: {
+    modalTitle: s__('AIAgent|Delete agent?'),
+    sectionTitle: s__('AIAgent|Delete this agent'),
+    sectionBody: s__(
+      'AIAgent|This action permanently deletes the %{codeStart}%{agentName}%{codeEnd} AI Agent.',
+    ),
+    deleteAgent: s__('AIAgent|Delete Agent'),
+    deleteAgentConfirmation: s__(
+      'AIAgent|AI Agent %{codeStart}%{agentName}%{codeEnd} will be permanently deleted. Are you sure?',
+    ),
+    cancel: __('Cancel'),
+  },
+  data() {
+    return {
+      deleteProps: {
+        text: this.$options.i18n.deleteAgent,
+        attributes: { category: 'primary', variant: 'danger' },
+      },
+      cancelProps: {
+        text: this.$options.i18n.cancel,
+      },
+    };
+  },
+  deleteModalId: 'deleteModalId',
+  methods: {
+    onSubmitDeleteModal() {
+      this.$emit('destroy', {
+        projectPath: this.projectPath,
+        agentId: this.agentVersion.id,
+      });
+    },
+  },
+};
+</script>
+
+<template>
+  <gl-card
+    class="gl-new-card"
+    header-class="gl-new-card-header"
+    body-class="gl-bg-red-50 gl-px-5 gl-py-4"
+  >
+    <template #header>
+      <div class="gl-new-card-title-wrapper">
+        <h4 class="gl-new-card-title danger-title">{{ $options.i18n.sectionTitle }}</h4>
+      </div>
+    </template>
+    <p>
+      <gl-sprintf :message="$options.i18n.sectionBody">
+        <template #code>
+          <code>{{ agentVersion.name }}</code>
+        </template>
+      </gl-sprintf>
+    </p>
+
+    <gl-button
+      v-gl-modal="$options.deleteModalId"
+      variant="danger"
+      class="gl-mt-3 d-block"
+      :loading="loading"
+      >{{ $options.i18n.deleteAgent }}</gl-button
+    >
+    <gl-modal
+      :ref="$options.deleteModalId"
+      :modal-id="$options.deleteModalId"
+      :title="$options.i18n.modalTitle"
+      :action-primary="deleteProps"
+      :action-cancel="cancelProps"
+      @primary="onSubmitDeleteModal"
+    >
+      <gl-sprintf :message="$options.i18n.deleteAgentConfirmation">
+        <template #code>
+          <code>{{ agentVersion.name }}</code>
+        </template>
+      </gl-sprintf>
+    </gl-modal>
+  </gl-card>
+</template>
diff --git a/ee/app/assets/javascripts/ml/ai_agents/components/agent_list.vue b/ee/app/assets/javascripts/ml/ai_agents/components/agent_list.vue
index 365473ce99681..69e359a0664c1 100644
--- a/ee/app/assets/javascripts/ml/ai_agents/components/agent_list.vue
+++ b/ee/app/assets/javascripts/ml/ai_agents/components/agent_list.vue
@@ -5,6 +5,7 @@ import * as Sentry from '~/sentry/sentry_browser_wrapper';
 import { s__ } from '~/locale';
 import { ROUTE_SHOW_AGENT } from '../constants';
 import getAiAgents from '../graphql/queries/get_ai_agents.query.graphql';
+import eventHub from '../event_hub';
 
 const GRAPHQL_PAGE_SIZE = 30;
 
@@ -37,6 +38,7 @@ export default {
   apollo: {
     agents: {
       query: getAiAgents,
+      notifyOnNetworkStatusChange: true,
       variables() {
         return this.queryVariables;
       },
@@ -66,6 +68,11 @@ export default {
       return this.agents?.nodes ?? [];
     },
   },
+  created() {
+    eventHub.$on('agents-changed', () => {
+      this.$apollo.queries.agents.refresh();
+    });
+  },
   ROUTE_SHOW_AGENT,
   methods: {
     fetchPage(pageInfo) {
diff --git a/ee/app/assets/javascripts/ml/ai_agents/constants.js b/ee/app/assets/javascripts/ml/ai_agents/constants.js
index ca23eee663de7..7c59dda09c4d6 100644
--- a/ee/app/assets/javascripts/ml/ai_agents/constants.js
+++ b/ee/app/assets/javascripts/ml/ai_agents/constants.js
@@ -8,8 +8,6 @@ export const ROUTE_AGENT_SETTINGS = 'edit';
 export const I18N_AGENT_NAME_LABEL = s__('AIAgent|Agent name');
 export const I18N_PROMPT_LABEL = s__('AIAgent|Prompt');
 export const I18N_CREATE_AGENT = s__('AIAgent|Create agent');
-export const I18N_UPDATE_AGENT = s__('AIAgent|Update agent');
 export const I18N_EDIT_AGENT = s__('AIAgent|Edit Ai Agent');
 
-export const I18N_DEFAULT_NOT_FOUND_ERROR = s__('AIAgents|The requested agent was not found.');
 export const I18N_DEFAULT_SAVE_ERROR = s__('AIAgents|An error has occurred when saving the agent.');
diff --git a/ee/app/assets/javascripts/ml/ai_agents/event_hub.js b/ee/app/assets/javascripts/ml/ai_agents/event_hub.js
new file mode 100644
index 0000000000000..e31806ad199a1
--- /dev/null
+++ b/ee/app/assets/javascripts/ml/ai_agents/event_hub.js
@@ -0,0 +1,3 @@
+import createEventHub from '~/helpers/event_hub_factory';
+
+export default createEventHub();
diff --git a/ee/app/assets/javascripts/ml/ai_agents/graphql/mutations/destroy_ai_agent.mutation.graphql b/ee/app/assets/javascripts/ml/ai_agents/graphql/mutations/destroy_ai_agent.mutation.graphql
new file mode 100644
index 0000000000000..172a418b5d926
--- /dev/null
+++ b/ee/app/assets/javascripts/ml/ai_agents/graphql/mutations/destroy_ai_agent.mutation.graphql
@@ -0,0 +1,5 @@
+mutation destroyAiAgent($projectPath: ID!, $agentId: AiAgentID!) {
+  aiAgentDestroy(input: { projectPath: $projectPath, agentId: $agentId }) {
+    errors
+  }
+}
diff --git a/ee/app/assets/javascripts/ml/ai_agents/views/edit_agent.vue b/ee/app/assets/javascripts/ml/ai_agents/views/edit_agent.vue
index 8827291a61b04..3967fb73a1ecb 100644
--- a/ee/app/assets/javascripts/ml/ai_agents/views/edit_agent.vue
+++ b/ee/app/assets/javascripts/ml/ai_agents/views/edit_agent.vue
@@ -2,15 +2,14 @@
 import { GlExperimentBadge, GlLoadingIcon, GlEmptyState, GlAlert } from '@gitlab/ui';
 import TitleArea from '~/vue_shared/components/registry/title_area.vue';
 import * as Sentry from '~/sentry/sentry_browser_wrapper';
+import { s__ } from '~/locale';
 import getLatestAiAgentVersion from '../graphql/queries/get_latest_ai_agent_version.query.graphql';
 import AgentForm from '../components/agent_form.vue';
+import DeleteAgent from '../components/agent_delete.vue';
 import updateAiAgent from '../graphql/mutations/update_ai_agent.mutation.graphql';
-import {
-  I18N_UPDATE_AGENT,
-  I18N_DEFAULT_SAVE_ERROR,
-  I18N_DEFAULT_NOT_FOUND_ERROR,
-  I18N_EDIT_AGENT,
-} from '../constants';
+import destroyAiAgent from '../graphql/mutations/destroy_ai_agent.mutation.graphql';
+import { I18N_EDIT_AGENT } from '../constants';
+import eventHub from '../event_hub';
 
 export default {
   name: 'EditAiAgent',
@@ -21,10 +20,8 @@ export default {
     AgentForm,
     GlEmptyState,
     GlAlert,
+    DeleteAgent,
   },
-  I18N_UPDATE_AGENT,
-  I18N_DEFAULT_SAVE_ERROR,
-  I18N_DEFAULT_NOT_FOUND_ERROR,
   I18N_EDIT_AGENT,
   inject: ['projectPath'],
   data() {
@@ -48,6 +45,12 @@ export default {
       },
     },
   },
+  i18n: {
+    updateAgent: s__('AIAgent|Update agent'),
+    saveError: s__('AIAgents|An error has occurred when saving the agent.'),
+    notFoundError: s__('AIAgents|The requested agent was not found.'),
+    destroyError: s__('AIAgents|An error has occurred when deleting the agent.'),
+  },
   computed: {
     isLoading() {
       return this.$apollo.queries.latestAgentVersion.loading;
@@ -86,7 +89,34 @@ export default {
         }
       } catch (error) {
         Sentry.captureException(error);
-        this.errorMessage = this.$options.I18N_DEFAULT_SAVE_ERROR;
+        this.errorMessage = this.$options.i18n.saveError;
+        this.loading = false;
+      }
+    },
+    async destroyAgent(requestData) {
+      this.errorMessage = '';
+      this.loading = true;
+      try {
+        const { data } = await this.$apollo.mutate({
+          mutation: destroyAiAgent,
+          variables: requestData,
+        });
+
+        this.loading = false;
+
+        const [error] = data?.aiAgentDestroy?.errors || [];
+
+        if (error) {
+          this.errorMessage = data.aiAgentDestroy.errors.join(', ');
+        } else {
+          eventHub.$emit('agents-changed');
+          this.$router.push({
+            name: 'list',
+          });
+        }
+      } catch (error) {
+        Sentry.captureException(error);
+        this.errorMessage = this.$options.i18n.destroyError;
         this.loading = false;
       }
     },
@@ -102,10 +132,7 @@ export default {
       {{ errorMessage }}
     </gl-alert>
 
-    <gl-empty-state
-      v-else-if="agentVersionNotFound"
-      :title="$options.I18N_DEFAULT_NOT_FOUND_ERROR"
-    />
+    <gl-empty-state v-else-if="agentVersionNotFound" :title="$options.i18n.notFoundError" />
 
     <div v-else>
       <title-area>
@@ -126,11 +153,18 @@ export default {
         :agent-version="latestAgentVersion"
         :agent-name-value="latestAgentVersion.name"
         :agent-prompt-value="latestAgentVersion.latestVersion.prompt"
-        :button-label="$options.I18N_UPDATE_AGENT"
+        :button-label="$options.i18n.updateAgent"
         :error-message="errorMessage"
         :loading="loading"
         @submit="updateAgent"
       />
+
+      <delete-agent
+        :project-path="projectPath"
+        :agent-version="latestAgentVersion"
+        :loading="loading"
+        @destroy="destroyAgent"
+      />
     </div>
   </div>
 </template>
diff --git a/ee/app/assets/javascripts/ml/ai_agents/views/show_agent.vue b/ee/app/assets/javascripts/ml/ai_agents/views/show_agent.vue
index 7e1ea0271278f..30c2adc367c35 100644
--- a/ee/app/assets/javascripts/ml/ai_agents/views/show_agent.vue
+++ b/ee/app/assets/javascripts/ml/ai_agents/views/show_agent.vue
@@ -1,6 +1,7 @@
 <script>
 import { GlExperimentBadge, GlDuoChat, GlEmptyState, GlLoadingIcon, GlButton } from '@gitlab/ui';
 import * as Sentry from '~/sentry/sentry_browser_wrapper';
+import { s__ } from '~/locale';
 import aiResponseSubscription from 'ee/graphql_shared/subscriptions/ai_completion_response.subscription.graphql';
 import TitleArea from '~/vue_shared/components/registry/title_area.vue';
 import { renderMarkdown } from '~/notes/utils';
@@ -9,7 +10,7 @@ import { convertToGraphQLId } from '~/graphql_shared/utils';
 import { renderGFM } from '~/behaviors/markdown/render_gfm';
 import chatMutation from 'ee/ai/graphql/chat.mutation.graphql';
 import { GENIE_CHAT_MODEL_ROLES } from 'ee/ai/constants';
-import { ROUTE_AGENT_SETTINGS, I18N_DEFAULT_NOT_FOUND_ERROR } from 'ee/ml/ai_agents/constants';
+import { ROUTE_AGENT_SETTINGS } from 'ee/ml/ai_agents/constants';
 import getLatestAiAgentVersion from 'ee/ml/ai_agents/graphql/queries/get_latest_ai_agent_version.query.graphql';
 
 export default {
@@ -25,7 +26,6 @@ export default {
     GlButton,
   },
   ROUTE_AGENT_SETTINGS,
-  I18N_DEFAULT_NOT_FOUND_ERROR,
   provide() {
     return {
       projectPath: this.projectPath,
@@ -86,6 +86,9 @@ export default {
       isLoading: false,
     };
   },
+  i18n: {
+    not_found_error: s__('AIAgents|The requested agent was not found.'),
+  },
   computed: {
     isAgentLoading() {
       return this.$apollo.queries.agentWithVersion.loading;
diff --git a/ee/app/graphql/ee/types/mutation_type.rb b/ee/app/graphql/ee/types/mutation_type.rb
index 9efd2d6cf05e4..e31e169568c57 100644
--- a/ee/app/graphql/ee/types/mutation_type.rb
+++ b/ee/app/graphql/ee/types/mutation_type.rb
@@ -158,6 +158,7 @@ module MutationType
         mount_mutation ::Mutations::AuditEvents::Streaming::HTTP::NamespaceFilters::Delete
         mount_mutation ::Mutations::Ai::Agents::Create, alpha: { milestone: '16.8' }
         mount_mutation ::Mutations::Ai::Agents::Update, alpha: { milestone: '16.10' }
+        mount_mutation ::Mutations::Ai::Agents::Destroy, alpha: { milestone: '16.11' }
         mount_mutation ::Mutations::ComplianceManagement::Standards::RefreshAdherenceChecks
         mount_mutation ::Mutations::Groups::SavedReplies::Create, alpha: { milestone: '16.10' }
         mount_mutation ::Mutations::Groups::SavedReplies::Update, alpha: { milestone: '16.10' }
diff --git a/ee/app/graphql/mutations/ai/agents/base.rb b/ee/app/graphql/mutations/ai/agents/base.rb
index 3ff84c5d36599..ae389a20fe338 100644
--- a/ee/app/graphql/mutations/ai/agents/base.rb
+++ b/ee/app/graphql/mutations/ai/agents/base.rb
@@ -14,6 +14,10 @@ class Base < BaseMutation
           Types::Ai::Agents::AgentType,
           null: true,
           description: 'Agent after mutation.'
+
+        def find_agent(agent_id)
+          ::Gitlab::Graphql::Lazy.force(GitlabSchema.find_by_gid(agent_id))
+        end
       end
     end
   end
diff --git a/ee/app/graphql/mutations/ai/agents/destroy.rb b/ee/app/graphql/mutations/ai/agents/destroy.rb
new file mode 100644
index 0000000000000..bf8cfdae17a48
--- /dev/null
+++ b/ee/app/graphql/mutations/ai/agents/destroy.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+module Mutations
+  module Ai
+    module Agents
+      class Destroy < Base
+        graphql_name 'AiAgentDestroy'
+
+        include FindsProject
+
+        argument :agent_id, ::Types::GlobalIDType[::Ai::Agent],
+          required: true,
+          description: 'Global ID of the AI Agent to be deleted.'
+
+        field :message, GraphQL::Types::String,
+          null: true,
+          description: 'AI Agent deletion result message.'
+
+        def resolve(**args)
+          authorized_find!(args[:project_path])
+          agent = find_agent(args[:agent_id])
+
+          return { errors: ['AI Agent not found'] } unless agent
+
+          result = ::Ai::Agents::DestroyAgentService.new(agent, current_user).execute
+
+          {
+            message: result.success? ? result[:message] : nil,
+            errors: result.error? ? Array.wrap(result[:message]) : []
+          }
+        end
+      end
+    end
+  end
+end
diff --git a/ee/app/graphql/mutations/ai/agents/update.rb b/ee/app/graphql/mutations/ai/agents/update.rb
index 001e4d3bc79d4..2d421631d64c2 100644
--- a/ee/app/graphql/mutations/ai/agents/update.rb
+++ b/ee/app/graphql/mutations/ai/agents/update.rb
@@ -22,7 +22,7 @@ class Update < Base
 
         def resolve(**args)
           authorized_find!(args[:project_path])
-          agent = ::Gitlab::Graphql::Lazy.force(GitlabSchema.find_by_gid(args[:agent_id]))
+          agent = find_agent(args[:agent_id])
 
           updated_agent = ::Ai::Agents::UpdateAgentService.new(agent, args[:name], args[:prompt]).execute
 
diff --git a/ee/app/services/ai/agents/destroy_agent_service.rb b/ee/app/services/ai/agents/destroy_agent_service.rb
new file mode 100644
index 0000000000000..e783eaa0710dd
--- /dev/null
+++ b/ee/app/services/ai/agents/destroy_agent_service.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+module Ai
+  module Agents
+    class DestroyAgentService
+      def initialize(ai_agent, user)
+        @ai_agent = ai_agent
+        @user = user
+      end
+
+      def execute
+        @ai_agent.destroy
+        success
+      rescue ActiveRecord::RecordNotDestroyed => msg
+        error(msg)
+      end
+
+      private
+
+      def success
+        ServiceResponse.success(message: _('AI Agent was successfully deleted'))
+      end
+
+      def error(msg)
+        ServiceResponse.error(message: format(_('Failed to delete AI Agent: %{msg}'), msg: msg))
+      end
+    end
+  end
+end
diff --git a/ee/spec/frontend/ml/ai_agents/graphql/mocks.js b/ee/spec/frontend/ml/ai_agents/graphql/mocks.js
index a1a394237f86f..a876204eb63d8 100644
--- a/ee/spec/frontend/ml/ai_agents/graphql/mocks.js
+++ b/ee/spec/frontend/ml/ai_agents/graphql/mocks.js
@@ -131,3 +131,22 @@ export const getLatestAiAgentErrorResponse = {
     },
   ],
 };
+
+export const destroyAiAgentsResponses = {
+  success: {
+    data: {
+      aiAgentDestroy: {
+        message: 'AI Agent was successfully deleted',
+        errors: [],
+      },
+    },
+  },
+  error: {
+    data: {
+      aiAgentDestroy: {
+        message: null,
+        errors: ['AI Agent not found'],
+      },
+    },
+  },
+};
diff --git a/ee/spec/frontend/ml/ai_agents/views/edit_agent_spec.js b/ee/spec/frontend/ml/ai_agents/views/edit_agent_spec.js
index a55d7c46b7f26..10c2192245555 100644
--- a/ee/spec/frontend/ml/ai_agents/views/edit_agent_spec.js
+++ b/ee/spec/frontend/ml/ai_agents/views/edit_agent_spec.js
@@ -7,6 +7,7 @@ import {
   GlFormFields,
   GlAlert,
   GlEmptyState,
+  GlModal,
 } from '@gitlab/ui';
 import Vue from 'vue';
 import VueApollo from 'vue-apollo';
@@ -14,6 +15,7 @@ import { mountExtended } from 'helpers/vue_test_utils_helper';
 import EditAgent from 'ee/ml/ai_agents/views/edit_agent.vue';
 import getLatestAiAgentVersionQuery from 'ee/ml/ai_agents/graphql/queries/get_latest_ai_agent_version.query.graphql';
 import updateAiAgentMutation from 'ee/ml/ai_agents/graphql/mutations/update_ai_agent.mutation.graphql';
+import destroyAiAgentMutation from 'ee/ml/ai_agents//graphql/mutations/destroy_ai_agent.mutation.graphql';
 import createMockApollo from 'helpers/mock_apollo_helper';
 import waitForPromises from 'helpers/wait_for_promises';
 import TitleArea from '~/vue_shared/components/registry/title_area.vue';
@@ -22,6 +24,7 @@ import {
   getLatestAiAgentResponse,
   getLatestAiAgentErrorResponse,
   getLatestAiAgentNotFoundResponse,
+  destroyAiAgentsResponses,
 } from '../graphql/mocks';
 
 Vue.use(VueApollo);
@@ -56,6 +59,7 @@ describe('ee/ml/ai_agents/views/edit_agent', () => {
   const findTitleArea = () => wrapper.findComponent(TitleArea);
   const findBadge = () => wrapper.findComponent(GlExperimentBadge);
   const findButton = () => wrapper.findComponent(GlButton);
+  const findDeleteModal = () => wrapper.findComponent(GlModal);
   const findForm = () => wrapper.findComponent(GlForm);
   const findInput = () => wrapper.findComponent(GlFormInput);
   const findTextarea = () => wrapper.findComponent(GlFormTextarea);
@@ -213,4 +217,62 @@ describe('ee/ml/ai_agents/views/edit_agent', () => {
       expect($router.push).not.toHaveBeenCalled();
     });
   });
+
+  describe('when successfully destroying the agent data', () => {
+    let resolver;
+
+    beforeEach(async () => {
+      resolver = jest.fn().mockResolvedValueOnce(destroyAiAgentsResponses.success);
+      apolloMocks = [
+        [getLatestAiAgentVersionQuery, jest.fn().mockResolvedValueOnce(getLatestAiAgentResponse)],
+        [destroyAiAgentMutation, resolver],
+      ];
+
+      createComponent();
+      await waitForPromises();
+    });
+
+    it('submits the destroy request', () => {
+      findDeleteModal().vm.$emit('primary');
+
+      expect(resolver).toHaveBeenLastCalledWith(
+        expect.objectContaining({
+          agentId: 'gid://gitlab/Ai::Agent/1',
+          projectPath: 'path/to/project',
+        }),
+      );
+    });
+
+    it('navigates to the new page when result is successful', async () => {
+      findDeleteModal().vm.$emit('primary');
+      await waitForPromises();
+
+      expect($router.push).toHaveBeenCalledWith(
+        expect.objectContaining({
+          name: 'list',
+        }),
+      );
+    });
+  });
+
+  describe('when destroying the agent fails', () => {
+    let resolver;
+
+    it('shows errors when result is a top level error', async () => {
+      resolver = jest.fn().mockResolvedValueOnce(destroyAiAgentsResponses.error);
+      apolloMocks = [
+        [getLatestAiAgentVersionQuery, jest.fn().mockResolvedValueOnce(getLatestAiAgentResponse)],
+        [destroyAiAgentMutation, resolver],
+      ];
+
+      createComponent();
+      await waitForPromises();
+
+      findDeleteModal().vm.$emit('primary');
+      await waitForPromises();
+
+      expect(findErrorAlert().text()).toBe('AI Agent not found');
+      expect($router.push).not.toHaveBeenCalled();
+    });
+  });
 });
diff --git a/ee/spec/requests/api/graphql/ai/agents/destroy_spec.rb b/ee/spec/requests/api/graphql/ai/agents/destroy_spec.rb
new file mode 100644
index 0000000000000..5b4f5cda51b8a
--- /dev/null
+++ b/ee/spec/requests/api/graphql/ai/agents/destroy_spec.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Destroy an AI agent', feature_category: :mlops do
+  include GraphqlHelpers
+
+  let_it_be(:agent_version) { create(:ai_agent_version) }
+  let_it_be(:agent) { agent_version.agent }
+  let_it_be(:project) { agent.project }
+  let_it_be(:current_user) { project.owner }
+
+  let(:input) { { project_path: project.full_path, agent_id: agent.to_global_id } }
+
+  let(:mutation) { graphql_mutation(:ai_agent_destroy, input) }
+  let(:mutation_response) { graphql_mutation_response(:ai_agent_destroy) }
+
+  before do
+    stub_licensed_features(ai_agents: true)
+  end
+
+  context 'when user is not allowed write changes' do
+    before do
+      allow(Ability).to receive(:allowed?).and_call_original
+      allow(Ability).to receive(:allowed?)
+                          .with(current_user, :write_ai_agents, project)
+                          .and_return(false)
+    end
+
+    it_behaves_like 'a mutation that returns a top-level access error'
+  end
+
+  context 'when user is allowed write changes' do
+    it 'destroys an agent' do
+      post_graphql_mutation(mutation, current_user: current_user)
+
+      expect(response).to have_gitlab_http_status(:success)
+      expect(mutation_response['message']).to include(
+        "AI Agent was successfully deleted"
+      )
+    end
+
+    context 'when id is not found' do
+      err_msg = "AI Agent not found"
+      let(:input) { { project_path: project.full_path, agent_id: "gid://gitlab/Ai::Agent/99999" } }
+
+      it_behaves_like 'a mutation that returns errors in the response', errors: [err_msg]
+    end
+  end
+end
diff --git a/ee/spec/services/ai/agents/destroy_agent_service_spec.rb b/ee/spec/services/ai/agents/destroy_agent_service_spec.rb
new file mode 100644
index 0000000000000..8123c30f87ad4
--- /dev/null
+++ b/ee/spec/services/ai/agents/destroy_agent_service_spec.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ::Ai::Agents::DestroyAgentService, feature_category: :mlops do
+  let_it_be(:user) { create(:user) }
+  let_it_be(:agent_version) { create(:ai_agent_version) }
+  let_it_be(:agent) { agent_version.agent }
+
+  let(:service) { described_class.new(agent, user) }
+
+  describe '#execute' do
+    subject(:service_result) { service.execute }
+
+    context 'when agent fails to delete' do
+      it 'returns nil' do
+        exception = ActiveRecord::RecordNotDestroyed.new(agent)
+        allow(agent).to receive(:destroy).and_raise(exception)
+
+        expect(service_result).to be_error
+        expect(service_result.message).to eq("Failed to delete AI Agent: #{exception.message}")
+      end
+    end
+
+    context 'when an agent exists' do
+      it 'destroys the agent', :aggregate_failures do
+        expect { service_result }.to change { Ai::Agent.count }.by(-1).and change { Ai::AgentVersion.count }.by(-1)
+        expect(service_result).to be_success
+      end
+    end
+  end
+end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 3024a686d1703..cc14d0767e61d 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -1987,6 +1987,9 @@ msgstr ""
 msgid "ACTION REQUIRED: Something went wrong while obtaining the Let's Encrypt certificate for GitLab Pages domain '%{domain}'"
 msgstr ""
 
+msgid "AI Agent was successfully deleted"
+msgstr ""
+
 msgid "AI actions"
 msgstr ""
 
@@ -1999,6 +2002,9 @@ msgstr ""
 msgid "AIAgents|Agent Settings"
 msgstr ""
 
+msgid "AIAgents|An error has occurred when deleting the agent."
+msgstr ""
+
 msgid "AIAgents|An error has occurred when saving the agent."
 msgstr ""
 
@@ -2014,6 +2020,9 @@ msgstr ""
 msgid "AIAgents|Update the name and prompt for this agent."
 msgstr ""
 
+msgid "AIAgent|AI Agent %{codeStart}%{agentName}%{codeEnd} will be permanently deleted. Are you sure?"
+msgstr ""
+
 msgid "AIAgent|Agent"
 msgstr ""
 
@@ -2026,6 +2035,15 @@ msgstr ""
 msgid "AIAgent|Create agent"
 msgstr ""
 
+msgid "AIAgent|Delete Agent"
+msgstr ""
+
+msgid "AIAgent|Delete agent?"
+msgstr ""
+
+msgid "AIAgent|Delete this agent"
+msgstr ""
+
 msgid "AIAgent|Edit Ai Agent"
 msgstr ""
 
@@ -2035,6 +2053,9 @@ msgstr ""
 msgid "AIAgent|Settings"
 msgstr ""
 
+msgid "AIAgent|This action permanently deletes the %{codeStart}%{agentName}%{codeEnd} AI Agent."
+msgstr ""
+
 msgid "AIAgent|Try out your agent"
 msgstr ""
 
@@ -21305,6 +21326,9 @@ msgstr ""
 msgid "Failed to create wiki"
 msgstr ""
 
+msgid "Failed to delete AI Agent: %{msg}"
+msgstr ""
+
 msgid "Failed to delete branch target"
 msgstr ""
 
-- 
GitLab