systemprompt_cli/commands/admin/agents/
delete.rs1use anyhow::{Context, Result, anyhow};
8use clap::Args;
9use std::path::Path;
10use std::sync::Arc;
11
12use super::types::AgentDeleteOutput;
13use crate::CliConfig;
14use crate::context::CommandContext;
15use crate::interactive::{Prompter, require_confirmation, resolve_required};
16use crate::shared::CommandOutput;
17use systemprompt_agent::AgentState;
18use systemprompt_agent::services::agent_orchestration::AgentOrchestrator;
19use systemprompt_agent::services::config_authoring::AgentConfigAuthoringService;
20use systemprompt_config::ProfileBootstrap;
21use systemprompt_loader::ConfigLoader;
22use systemprompt_logging::CliService;
23use systemprompt_oauth::JwtValidationProviderImpl;
24use systemprompt_scheduler::ProcessCleanup;
25
26#[derive(Debug, Args)]
27pub struct DeleteArgs {
28 #[arg(help = "Agent name (required in non-interactive mode)")]
29 pub name: Option<String>,
30
31 #[arg(long, help = "Delete all agents")]
32 pub all: bool,
33
34 #[arg(short = 'y', long, help = "Skip confirmation prompts")]
35 pub yes: bool,
36
37 #[arg(long, help = "Force delete even if process cannot be stopped")]
38 pub force: bool,
39}
40
41pub(super) async fn execute(args: DeleteArgs, ctx: &CommandContext) -> Result<CommandOutput> {
42 let prompter = ctx.prompter();
43 let config = &ctx.cli;
44 let services_config = ConfigLoader::load().context("Failed to load services configuration")?;
45
46 let agents_to_delete = resolve_targets(&args, prompter, &services_config, config)?;
47
48 require_confirmation(
49 prompter,
50 &delete_confirm_message(args.all, &agents_to_delete),
51 args.yes,
52 config,
53 )?;
54
55 let profile = ProfileBootstrap::get().context("Failed to get profile")?;
56 let authoring = AgentConfigAuthoringService::new(Path::new(&profile.paths.services));
57
58 let orchestrator = match build_orchestrator(ctx).await {
59 Ok(orchestrator) => orchestrator,
60 Err(message) => {
61 return Ok(CommandOutput::card_value(
62 "Delete Failed",
63 &AgentDeleteOutput {
64 deleted: vec![],
65 message,
66 },
67 ));
68 },
69 };
70
71 let mut deleted = Vec::new();
72 let mut errors = Vec::new();
73
74 for agent_name in &agents_to_delete {
75 let agent_port = services_config.agents.get(agent_name).map(|c| c.port);
76 let result = delete_single_agent(
77 agent_name,
78 agent_port,
79 orchestrator.as_ref(),
80 &authoring,
81 args.force,
82 )
83 .await;
84 match result {
85 Ok(()) => deleted.push(agent_name.clone()),
86 Err(msg) => errors.push(msg),
87 }
88 }
89
90 if !errors.is_empty() && deleted.is_empty() {
91 return Err(anyhow!("Failed to delete agents:\n{}", errors.join("\n")));
92 }
93
94 if !deleted.is_empty() {
95 ConfigLoader::reload().with_context(|| {
96 "Agent(s) deleted but configuration validation failed. Please check the configuration."
97 })?;
98 }
99
100 let message = delete_success_message(&deleted);
101 let output = AgentDeleteOutput { deleted, message };
102
103 Ok(CommandOutput::card_value("Delete Agent", &output))
104}
105
106fn resolve_targets(
107 args: &DeleteArgs,
108 prompter: &dyn Prompter,
109 services_config: &systemprompt_models::ServicesConfig,
110 config: &CliConfig,
111) -> Result<Vec<String>> {
112 let available: Vec<String> = services_config.agents.keys().cloned().collect();
113 let requested = if args.all {
114 None
115 } else {
116 Some(resolve_required(args.name.clone(), "name", config, || {
117 super::shared::prompt_agent_selection(
118 prompter,
119 "Select agent to delete",
120 services_config,
121 )
122 })?)
123 };
124 validate_delete_targets(requested, &available)
125}
126
127pub fn validate_delete_targets(
130 requested: Option<String>,
131 available: &[String],
132) -> Result<Vec<String>> {
133 let agents = match requested {
134 Some(name) => {
135 if !available.contains(&name) {
136 return Err(anyhow!("Agent '{}' not found", name));
137 }
138 vec![name]
139 },
140 None => available.to_vec(),
141 };
142
143 if agents.is_empty() {
144 return Err(anyhow!("No agents to delete"));
145 }
146
147 Ok(agents)
148}
149
150#[must_use]
151pub fn delete_confirm_message(all: bool, targets: &[String]) -> String {
152 if all {
153 format!("Delete ALL {} agents?", targets.len())
154 } else {
155 format!("Delete agent '{}'?", targets[0])
156 }
157}
158
159#[must_use]
160pub fn delete_success_message(deleted: &[String]) -> String {
161 if deleted.len() == 1 {
162 format!("Agent '{}' deleted successfully", deleted[0])
163 } else {
164 format!("{} agent(s) deleted successfully", deleted.len())
165 }
166}
167
168async fn build_orchestrator(ctx: &CommandContext) -> Result<Option<AgentOrchestrator>, String> {
169 let app = match ctx.app_context().await {
170 Ok(app) => app,
171 Err(e) => {
172 tracing::debug!(error = %e, "Failed to create AppContext for agent deletion");
173 return Ok(None);
174 },
175 };
176
177 let jwt_provider = match JwtValidationProviderImpl::from_config() {
178 Ok(p) => Arc::new(p),
179 Err(e) => {
180 tracing::debug!(error = %e, "Failed to create JWT provider");
181 return Err(format!("Failed to initialize: {e}"));
182 },
183 };
184
185 let agent_state = Arc::new(AgentState::new(
186 Arc::clone(app.db_pool()),
187 Arc::new(app.config().clone()),
188 jwt_provider,
189 Arc::clone(app.a2a_repositories()),
190 ));
191
192 Ok(
193 AgentOrchestrator::new(agent_state, Arc::clone(app.app_paths_arc()), None)
194 .await
195 .ok(),
196 )
197}
198
199pub async fn delete_single_agent(
200 agent_name: &str,
201 agent_port: Option<u16>,
202 orchestrator: Option<&AgentOrchestrator>,
203 authoring: &AgentConfigAuthoringService,
204 force: bool,
205) -> Result<(), String> {
206 CliService::info(&format!("Deleting agent '{}'...", agent_name));
207
208 let process_stopped = stop_agent_process(agent_name, agent_port, orchestrator).await;
209
210 if !process_stopped && !force {
211 let msg = format!(
212 "Failed to stop agent '{}' process. Use --force to delete anyway.",
213 agent_name
214 );
215 CliService::error(&msg);
216 return Err(msg);
217 }
218
219 if !process_stopped && force {
220 CliService::warning(&format!(
221 "Force deleting agent '{}' (process may still be running)",
222 agent_name
223 ));
224 }
225
226 match authoring.delete(agent_name) {
227 Ok(()) => {
228 CliService::success(&format!("Agent '{}' deleted", agent_name));
229 Ok(())
230 },
231 Err(e) => {
232 CliService::error(&format!("Failed to delete agent '{}': {}", agent_name, e));
233 Err(format!("{}: {}", agent_name, e))
234 },
235 }
236}
237
238pub async fn stop_agent_process(
239 agent_name: &str,
240 agent_port: Option<u16>,
241 orchestrator: Option<&AgentOrchestrator>,
242) -> bool {
243 if let Some(orch) = orchestrator {
244 match orch.delete_agent(agent_name).await {
245 Ok(()) => {
246 tracing::debug!(agent = %agent_name, "Agent stopped via orchestrator");
247 return true;
248 },
249 Err(e) => {
250 tracing::debug!(
251 agent = %agent_name,
252 error = %e,
253 "Orchestrator termination failed, trying port-based cleanup"
254 );
255 },
256 }
257 }
258
259 let Some(port) = agent_port else {
260 tracing::debug!(agent = %agent_name, "No port configured, assuming not running");
261 return true;
262 };
263
264 if ProcessCleanup::check_port(port).is_none() {
265 tracing::debug!(agent = %agent_name, port, "No process on port, assuming stopped");
266 return true;
267 }
268
269 CliService::info(&format!(
270 "Stopping agent '{}' on port {}...",
271 agent_name, port
272 ));
273
274 let Some(pid) = ProcessCleanup::check_port(port) else {
275 tracing::warn!(agent = %agent_name, port, "No process found on port to stop");
276 return false;
277 };
278
279 if !ProcessCleanup::kill_process(pid) {
280 tracing::warn!(agent = %agent_name, port, pid, "Failed to kill process on port");
281 return false;
282 }
283
284 tracing::debug!(agent = %agent_name, port, pid, "Killed process on port");
285 true
286}