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(
128 requested: Option<String>,
129 available: &[String],
130) -> Result<Vec<String>> {
131 let agents = match requested {
132 Some(name) => {
133 if !available.contains(&name) {
134 return Err(anyhow!("Agent '{}' not found", name));
135 }
136 vec![name]
137 },
138 None => available.to_vec(),
139 };
140
141 if agents.is_empty() {
142 return Err(anyhow!("No agents to delete"));
143 }
144
145 Ok(agents)
146}
147
148#[must_use]
149pub fn delete_confirm_message(all: bool, targets: &[String]) -> String {
150 if all {
151 format!("Delete ALL {} agents?", targets.len())
152 } else {
153 format!("Delete agent '{}'?", targets[0])
154 }
155}
156
157#[must_use]
158pub fn delete_success_message(deleted: &[String]) -> String {
159 if deleted.len() == 1 {
160 format!("Agent '{}' deleted successfully", deleted[0])
161 } else {
162 format!("{} agent(s) deleted successfully", deleted.len())
163 }
164}
165
166async fn build_orchestrator(ctx: &CommandContext) -> Result<Option<AgentOrchestrator>, String> {
167 let app = match ctx.app_context().await {
168 Ok(app) => app,
169 Err(e) => {
170 tracing::debug!(error = %e, "Failed to create AppContext for agent deletion");
171 return Ok(None);
172 },
173 };
174
175 let jwt_provider = match JwtValidationProviderImpl::from_config() {
176 Ok(p) => Arc::new(p),
177 Err(e) => {
178 tracing::debug!(error = %e, "Failed to create JWT provider");
179 return Err(format!("Failed to initialize: {e}"));
180 },
181 };
182
183 let agent_state = Arc::new(AgentState::new(
184 Arc::clone(app.db_pool()),
185 Arc::new(app.config().clone()),
186 jwt_provider,
187 Arc::clone(app.a2a_repositories()),
188 ));
189
190 Ok(
191 AgentOrchestrator::new(agent_state, Arc::clone(app.app_paths_arc()), None)
192 .await
193 .ok(),
194 )
195}
196
197pub async fn delete_single_agent(
198 agent_name: &str,
199 agent_port: Option<u16>,
200 orchestrator: Option<&AgentOrchestrator>,
201 authoring: &AgentConfigAuthoringService,
202 force: bool,
203) -> Result<(), String> {
204 CliService::info(&format!("Deleting agent '{}'...", agent_name));
205
206 let process_stopped = stop_agent_process(agent_name, agent_port, orchestrator).await;
207
208 if !process_stopped && !force {
209 let msg = format!(
210 "Failed to stop agent '{}' process. Use --force to delete anyway.",
211 agent_name
212 );
213 CliService::error(&msg);
214 return Err(msg);
215 }
216
217 if !process_stopped && force {
218 CliService::warning(&format!(
219 "Force deleting agent '{}' (process may still be running)",
220 agent_name
221 ));
222 }
223
224 match authoring.delete(agent_name) {
225 Ok(()) => {
226 CliService::success(&format!("Agent '{}' deleted", agent_name));
227 Ok(())
228 },
229 Err(e) => {
230 CliService::error(&format!("Failed to delete agent '{}': {}", agent_name, e));
231 Err(format!("{}: {}", agent_name, e))
232 },
233 }
234}
235
236pub async fn stop_agent_process(
237 agent_name: &str,
238 agent_port: Option<u16>,
239 orchestrator: Option<&AgentOrchestrator>,
240) -> bool {
241 if let Some(orch) = orchestrator {
242 match orch.delete_agent(agent_name).await {
243 Ok(()) => {
244 tracing::debug!(agent = %agent_name, "Agent stopped via orchestrator");
245 return true;
246 },
247 Err(e) => {
248 tracing::debug!(
249 agent = %agent_name,
250 error = %e,
251 "Orchestrator termination failed, trying port-based cleanup"
252 );
253 },
254 }
255 }
256
257 let Some(port) = agent_port else {
258 tracing::debug!(agent = %agent_name, "No port configured, assuming not running");
259 return true;
260 };
261
262 if ProcessCleanup::check_port(port).is_none() {
263 tracing::debug!(agent = %agent_name, port, "No process on port, assuming stopped");
264 return true;
265 }
266
267 CliService::info(&format!(
268 "Stopping agent '{}' on port {}...",
269 agent_name, port
270 ));
271
272 let Some(pid) = ProcessCleanup::check_port(port) else {
273 tracing::warn!(agent = %agent_name, port, "No process found on port to stop");
274 return false;
275 };
276
277 if !ProcessCleanup::kill_process(pid) {
278 tracing::warn!(agent = %agent_name, port, pid, "Failed to kill process on port");
279 return false;
280 }
281
282 tracing::debug!(agent = %agent_name, port, pid, "Killed process on port");
283 true
284}