syncable_cli/agent/tools/platform/
trigger_deployment.rs1use rig::completion::ToolDefinition;
6use rig::tool::Tool;
7use serde::{Deserialize, Serialize};
8use serde_json::json;
9
10use crate::agent::tools::error::{ErrorCategory, format_error_for_llm};
11use crate::platform::api::{PlatformApiClient, PlatformApiError, TriggerDeploymentRequest};
12use crate::platform::session::PlatformSession;
13
14#[derive(Debug, Deserialize)]
16pub struct TriggerDeploymentArgs {
17 pub config_id: String,
19 pub commit_sha: Option<String>,
21}
22
23#[derive(Debug, thiserror::Error)]
25#[error("Trigger deployment error: {0}")]
26pub struct TriggerDeploymentError(String);
27
28#[derive(Debug, Clone, Serialize, Deserialize, Default)]
33pub struct TriggerDeploymentTool;
34
35impl TriggerDeploymentTool {
36 pub fn new() -> Self {
38 Self
39 }
40}
41
42impl Tool for TriggerDeploymentTool {
43 const NAME: &'static str = "trigger_deployment";
44
45 type Error = TriggerDeploymentError;
46 type Args = TriggerDeploymentArgs;
47 type Output = String;
48
49 async fn definition(&self, _prompt: String) -> ToolDefinition {
50 ToolDefinition {
51 name: Self::NAME.to_string(),
52 description: r#"Trigger a deployment using a deployment configuration.
53
54Starts a new deployment for the specified config. Returns a task ID that can be
55used to monitor deployment progress with `get_deployment_status`.
56
57**Parameters:**
58- config_id: The deployment config ID (get from list_deployment_configs or create_deployment_config)
59- commit_sha: Optional specific commit to deploy (defaults to latest on branch)
60
61**Prerequisites:**
62- User must be authenticated via `sync-ctl auth login`
63- A deployment config must exist (use create_deployment_config first if needed)
64
65**Use Cases:**
66- Deploy the latest code from a branch
67- Deploy a specific commit version
68- Trigger a manual deployment for a service
69
70**Returns:**
71- task_id: Use this to check deployment progress with get_deployment_status
72- status: Initial deployment status
73- message: Human-readable status message"#
74 .to_string(),
75 parameters: json!({
76 "type": "object",
77 "properties": {
78 "config_id": {
79 "type": "string",
80 "description": "The deployment config ID (from list_deployment_configs or create_deployment_config)"
81 },
82 "commit_sha": {
83 "type": "string",
84 "description": "Optional: specific commit SHA to deploy (defaults to latest)"
85 }
86 },
87 "required": ["config_id"]
88 }),
89 }
90 }
91
92 async fn call(&self, args: Self::Args) -> Result<Self::Output, Self::Error> {
93 if args.config_id.trim().is_empty() {
95 return Ok(format_error_for_llm(
96 "trigger_deployment",
97 ErrorCategory::ValidationFailed,
98 "config_id cannot be empty",
99 Some(vec![
100 "Use list_deployment_configs to find available deployment configs",
101 ]),
102 ));
103 }
104
105 let session = match PlatformSession::load() {
107 Ok(s) => s,
108 Err(_) => {
109 return Ok(format_error_for_llm(
110 "trigger_deployment",
111 ErrorCategory::InternalError,
112 "Failed to load platform session",
113 Some(vec!["Try authenticating with `sync-ctl auth login`"]),
114 ));
115 }
116 };
117
118 if !session.is_project_selected() {
119 return Ok(format_error_for_llm(
120 "trigger_deployment",
121 ErrorCategory::ValidationFailed,
122 "No project selected",
123 Some(vec!["Use select_project to choose a project first"]),
124 ));
125 }
126
127 let project_id = session.project_id.clone().unwrap_or_default();
128
129 let client = match PlatformApiClient::new() {
131 Ok(c) => c,
132 Err(e) => {
133 return Ok(format_api_error("trigger_deployment", e));
134 }
135 };
136
137 let request = TriggerDeploymentRequest {
139 project_id,
140 config_id: args.config_id.clone(),
141 commit_sha: args.commit_sha.clone(),
142 };
143
144 match client.trigger_deployment(&request).await {
146 Ok(response) => {
147 let result = json!({
148 "success": true,
149 "task_id": response.backstage_task_id,
150 "config_id": response.config_id,
151 "status": response.status,
152 "message": response.message,
153 "next_steps": [
154 format!("Use get_deployment_status with task_id '{}' to monitor progress", response.backstage_task_id),
155 "Deployment typically takes 2-5 minutes to complete"
156 ]
157 });
158
159 serde_json::to_string_pretty(&result)
160 .map_err(|e| TriggerDeploymentError(format!("Failed to serialize: {}", e)))
161 }
162 Err(e) => Ok(format_api_error("trigger_deployment", e)),
163 }
164 }
165}
166
167fn format_api_error(tool_name: &str, error: PlatformApiError) -> String {
169 match error {
170 PlatformApiError::Unauthorized => format_error_for_llm(
171 tool_name,
172 ErrorCategory::PermissionDenied,
173 "Not authenticated - please run `sync-ctl auth login` first",
174 Some(vec![
175 "The user needs to authenticate with the Syncable platform",
176 "Run: sync-ctl auth login",
177 ]),
178 ),
179 PlatformApiError::NotFound(msg) => format_error_for_llm(
180 tool_name,
181 ErrorCategory::ResourceUnavailable,
182 &format!("Resource not found: {}", msg),
183 Some(vec![
184 "The project ID or config ID may be incorrect",
185 "Use list_deployment_configs to find valid config IDs",
186 ]),
187 ),
188 PlatformApiError::PermissionDenied(msg) => format_error_for_llm(
189 tool_name,
190 ErrorCategory::PermissionDenied,
191 &format!("Permission denied: {}", msg),
192 Some(vec![
193 "The user does not have permission to trigger deployments",
194 "Contact the project admin for access",
195 ]),
196 ),
197 PlatformApiError::RateLimited => format_error_for_llm(
198 tool_name,
199 ErrorCategory::ResourceUnavailable,
200 "Rate limit exceeded - please try again later",
201 Some(vec!["Wait a moment before retrying"]),
202 ),
203 PlatformApiError::HttpError(e) => format_error_for_llm(
204 tool_name,
205 ErrorCategory::NetworkError,
206 &format!("Network error: {}", e),
207 Some(vec![
208 "Check network connectivity",
209 "The Syncable API may be temporarily unavailable",
210 ]),
211 ),
212 PlatformApiError::ParseError(msg) => format_error_for_llm(
213 tool_name,
214 ErrorCategory::InternalError,
215 &format!("Failed to parse API response: {}", msg),
216 Some(vec!["This may be a temporary API issue"]),
217 ),
218 PlatformApiError::ApiError { status, message } => format_error_for_llm(
219 tool_name,
220 ErrorCategory::ExternalCommandFailed,
221 &format!("API error ({}): {}", status, message),
222 Some(vec!["Check the error message for details"]),
223 ),
224 PlatformApiError::ServerError { status, message } => format_error_for_llm(
225 tool_name,
226 ErrorCategory::ExternalCommandFailed,
227 &format!("Server error ({}): {}", status, message),
228 Some(vec![
229 "The Syncable API is experiencing issues",
230 "Try again later",
231 ]),
232 ),
233 PlatformApiError::ConnectionFailed => format_error_for_llm(
234 tool_name,
235 ErrorCategory::NetworkError,
236 "Could not connect to Syncable API",
237 Some(vec![
238 "Check your internet connection",
239 "The Syncable API may be temporarily unavailable",
240 ]),
241 ),
242 }
243}
244
245#[cfg(test)]
246mod tests {
247 use super::*;
248
249 #[test]
250 fn test_tool_name() {
251 assert_eq!(TriggerDeploymentTool::NAME, "trigger_deployment");
252 }
253
254 #[test]
255 fn test_tool_creation() {
256 let tool = TriggerDeploymentTool::new();
257 assert!(format!("{:?}", tool).contains("TriggerDeploymentTool"));
258 }
259}