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