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::process_stop::stop_agent_process;
13use super::types::AgentDeleteOutput;
14use crate::CliConfig;
15use crate::context::CommandContext;
16use crate::interactive::{Prompter, require_confirmation, resolve_required};
17use crate::shared::CommandOutput;
18use systemprompt_agent::AgentState;
19use systemprompt_agent::services::a2a_server::streaming::webhook_client::HttpWebhookBroadcaster;
20use systemprompt_agent::services::agent_orchestration::AgentOrchestrator;
21use systemprompt_agent::services::config_authoring::AgentConfigAuthoringService;
22use systemprompt_config::ProfileBootstrap;
23use systemprompt_identifiers::AgentName;
24use systemprompt_loader::ConfigLoader;
25use systemprompt_logging::CliService;
26use systemprompt_oauth::JwtValidationProviderImpl;
27
28#[derive(Debug, Args)]
29pub struct DeleteArgs {
30 #[arg(
31 help = "Agent name (required in non-interactive mode)",
32 value_parser = crate::shared::parse_agent_name
33 )]
34 pub name: Option<AgentName>,
35
36 #[arg(long, help = "Delete all agents")]
37 pub all: bool,
38
39 #[arg(short = 'y', long, help = "Skip confirmation prompts")]
40 pub yes: bool,
41
42 #[arg(long, help = "Force delete even if process cannot be stopped")]
43 pub force: bool,
44}
45
46pub(super) async fn execute(args: DeleteArgs, ctx: &CommandContext) -> Result<CommandOutput> {
47 let prompter = ctx.prompter();
48 let config = &ctx.cli;
49 let services_config = ConfigLoader::load().context("Failed to load services configuration")?;
50
51 let agents_to_delete = resolve_targets(&args, prompter, &services_config, config)?;
52
53 require_confirmation(
54 prompter,
55 &delete_confirm_message(args.all, &agents_to_delete),
56 args.yes,
57 config,
58 )?;
59
60 let profile = ProfileBootstrap::get().context("Failed to get profile")?;
61 let authoring = AgentConfigAuthoringService::new(Path::new(&profile.paths.services));
62
63 let orchestrator = build_orchestrator(ctx)
64 .await
65 .context("Cannot stop agents safely without the orchestrator")?;
66
67 let mut deleted = Vec::new();
68 let mut errors = Vec::new();
69
70 for agent_name in &agents_to_delete {
71 let agent_port = services_config
72 .agents
73 .get(agent_name.as_str())
74 .map(|c| c.port);
75 let process_stopped = stop_agent_process(agent_name, agent_port, &orchestrator).await;
76 match delete_single_agent(agent_name, process_stopped, &authoring, args.force) {
77 Ok(()) => deleted.push(agent_name.clone()),
78 Err(error) => errors.push(format!("{error:#}")),
79 }
80 }
81
82 if !deleted.is_empty() {
83 ConfigLoader::reload().with_context(|| {
84 "Agent(s) deleted but configuration validation failed. Please check the configuration."
85 })?;
86 }
87
88 if !errors.is_empty() {
89 return Err(anyhow!(
90 "{}\nFailed to delete:\n{}",
91 delete_success_message(&deleted),
92 errors.join("\n")
93 ));
94 }
95
96 let message = delete_success_message(&deleted);
97 let output = AgentDeleteOutput { deleted, message };
98
99 Ok(CommandOutput::card_value("Delete Agent", &output))
100}
101
102fn resolve_targets(
103 args: &DeleteArgs,
104 prompter: &dyn Prompter,
105 services_config: &systemprompt_manifest::ServicesConfig,
106 config: &CliConfig,
107) -> Result<Vec<AgentName>> {
108 let available: Vec<AgentName> = services_config.agents.keys().map(AgentName::new).collect();
109 let requested = if args.all {
110 None
111 } else {
112 Some(resolve_required(args.name.clone(), "name", config, || {
113 super::shared::prompt_agent_selection(
114 prompter,
115 "Select agent to delete",
116 services_config,
117 )
118 .map(AgentName::new)
119 })?)
120 };
121 validate_delete_targets(requested, &available)
122}
123
124pub fn validate_delete_targets(
125 requested: Option<AgentName>,
126 available: &[AgentName],
127) -> Result<Vec<AgentName>> {
128 let agents = match requested {
129 Some(name) => {
130 if !available.contains(&name) {
131 return Err(anyhow!("Agent '{}' not found", name));
132 }
133 vec![name]
134 },
135 None => available.to_vec(),
136 };
137
138 if agents.is_empty() {
139 return Err(anyhow!("No agents to delete"));
140 }
141
142 Ok(agents)
143}
144
145#[must_use]
146pub fn delete_confirm_message(all: bool, targets: &[AgentName]) -> String {
147 if all {
148 format!("Delete ALL {} agents?", targets.len())
149 } else {
150 format!("Delete agent '{}'?", targets[0])
151 }
152}
153
154#[must_use]
155pub fn delete_success_message(deleted: &[AgentName]) -> String {
156 if deleted.len() == 1 {
157 format!("Agent '{}' deleted successfully", deleted[0])
158 } else {
159 format!("{} agent(s) deleted successfully", deleted.len())
160 }
161}
162
163async fn build_orchestrator(ctx: &CommandContext) -> Result<AgentOrchestrator> {
164 let app = ctx.app_context().await?;
165 let jwt_provider = Arc::new(
166 JwtValidationProviderImpl::from_config().context("Failed to create JWT provider")?,
167 );
168 let broadcaster = HttpWebhookBroadcaster::from_config(app.config())
169 .context("Failed to initialize webhook broadcaster")?;
170
171 let agent_state = Arc::new(AgentState::new(
172 Arc::clone(app.db_pool()),
173 Arc::new(app.config().clone()),
174 jwt_provider,
175 Arc::clone(app.a2a_repositories()),
176 Arc::new(broadcaster),
177 ));
178
179 AgentOrchestrator::new(agent_state, Arc::clone(app.app_paths_arc()), None)
180 .await
181 .context("Failed to initialize agent orchestrator")
182}
183
184pub fn delete_single_agent(
185 agent_name: &AgentName,
186 process_stopped: bool,
187 authoring: &AgentConfigAuthoringService,
188 force: bool,
189) -> Result<()> {
190 CliService::info(&format!("Deleting agent '{}'...", agent_name));
191
192 if !process_stopped && !force {
193 let msg = format!(
194 "Failed to stop agent '{}' process. Use --force to delete anyway.",
195 agent_name
196 );
197 CliService::error(&msg);
198 return Err(anyhow!(msg));
199 }
200
201 if !process_stopped {
202 CliService::warning(&format!(
203 "Force deleting agent '{}' (process may still be running)",
204 agent_name
205 ));
206 }
207
208 match authoring.delete(agent_name) {
209 Ok(()) => {
210 CliService::success(&format!("Agent '{}' deleted", agent_name));
211 Ok(())
212 },
213 Err(e) => {
214 CliService::error(&format!("Failed to delete agent '{}': {}", agent_name, e));
215 Err(e).with_context(|| format!("Failed to delete agent '{agent_name}'"))
216 },
217 }
218}