syncable_cli/agent/tools/platform/
get_deployment_status.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};
12
13#[derive(Debug, Deserialize)]
15pub struct GetDeploymentStatusArgs {
16 pub task_id: String,
18 pub project_id: Option<String>,
20 pub service_name: Option<String>,
22}
23
24#[derive(Debug, thiserror::Error)]
26#[error("Get deployment status error: {0}")]
27pub struct GetDeploymentStatusError(String);
28
29#[derive(Debug, Clone, Serialize, Deserialize, Default)]
34pub struct GetDeploymentStatusTool;
35
36impl GetDeploymentStatusTool {
37 pub fn new() -> Self {
39 Self
40 }
41}
42
43impl Tool for GetDeploymentStatusTool {
44 const NAME: &'static str = "get_deployment_status";
45
46 type Error = GetDeploymentStatusError;
47 type Args = GetDeploymentStatusArgs;
48 type Output = String;
49
50 async fn definition(&self, _prompt: String) -> ToolDefinition {
51 ToolDefinition {
52 name: Self::NAME.to_string(),
53 description: r#"Get the status of a deployment task and optionally check the actual service status.
54
55Returns the current status of a deployment, including progress percentage,
56current step, overall status, and optionally the public URL if the service is ready.
57
58**CRITICAL - DO NOT POLL IN A LOOP:**
59After checking status, you MUST inform the user and WAIT for them to ask again.
60DO NOT call this tool repeatedly in succession. Deployments take 1-3 minutes.
61The response includes an "action" field - follow it:
62- "STOP_POLLING": Deployment is done (success or failure). Tell the user.
63- "INFORM_USER_AND_WAIT": Tell user the current status and wait for them to ask for updates.
64
65**IMPORTANT for Cloud Runner:**
66The task may show "completed" when infrastructure is provisioned, but the actual
67service build and deployment takes longer. Pass project_id and service_name to
68also check if the service has a public URL (meaning it's actually ready).
69
70**Status Values:**
71- Task status: "processing", "completed", "failed"
72- Overall status: "generating", "building", "deploying", "healthy", "failed"
73- Service ready: Only when public_url is available
74
75**Prerequisites:**
76- User must be authenticated via `sync-ctl auth login`
77- A deployment must have been triggered (use trigger_deployment first)
78
79**Use Cases:**
80- Check deployment status ONCE after triggering, then inform user
81- Let user ask for updates when they want them
82- Get error details if deployment failed"#
83 .to_string(),
84 parameters: json!({
85 "type": "object",
86 "properties": {
87 "task_id": {
88 "type": "string",
89 "description": "The deployment task ID (from trigger_deployment response)"
90 },
91 "project_id": {
92 "type": "string",
93 "description": "Optional: Project ID to check actual service status and public URL"
94 },
95 "service_name": {
96 "type": "string",
97 "description": "Optional: Service name to find the specific deployment"
98 }
99 },
100 "required": ["task_id"]
101 }),
102 }
103 }
104
105 async fn call(&self, args: Self::Args) -> Result<Self::Output, Self::Error> {
106 if args.task_id.trim().is_empty() {
108 return Ok(format_error_for_llm(
109 "get_deployment_status",
110 ErrorCategory::ValidationFailed,
111 "task_id cannot be empty",
112 Some(vec![
113 "Use trigger_deployment to start a deployment and get a task_id",
114 "Use list_deployments to find previous deployment task IDs",
115 ]),
116 ));
117 }
118
119 let client = match PlatformApiClient::new() {
121 Ok(c) => c,
122 Err(e) => {
123 return Ok(format_api_error("get_deployment_status", e));
124 }
125 };
126
127 match client.get_deployment_status(&args.task_id).await {
129 Ok(status) => {
130 let task_complete = status.status == "completed";
131 let is_failed = status.status == "failed" || status.overall_status == "failed";
132 let is_healthy = status.overall_status == "healthy";
133
134 let (service_status, public_url, service_ready) = if let (Some(project_id), Some(service_name)) = (&args.project_id, &args.service_name) {
137 match client.list_deployments(project_id, Some(10)).await {
138 Ok(paginated) => {
139 let deployment = paginated.data.iter()
141 .find(|d| d.service_name.eq_ignore_ascii_case(service_name));
142
143 match deployment {
144 Some(d) => (
145 Some(d.status.clone()),
146 d.public_url.clone(),
147 d.public_url.is_some() && d.status == "running"
148 ),
149 None => (None, None, false)
150 }
151 }
152 Err(_) => (None, None, false)
153 }
154 } else {
155 (None, None, false)
156 };
157
158 let truly_ready = if args.project_id.is_some() {
160 service_ready
161 } else {
162 is_healthy
163 };
164
165 let mut result = json!({
166 "success": true,
167 "task_id": args.task_id,
168 "task_status": status.status,
169 "task_progress": status.progress,
170 "current_step": status.current_step,
171 "overall_status": status.overall_status,
172 "overall_message": status.overall_message,
173 "task_complete": task_complete,
174 "is_failed": is_failed,
175 "service_ready": truly_ready
176 });
177
178 if let Some(svc_status) = service_status {
180 result["service_status"] = json!(svc_status);
181 }
182 if let Some(url) = &public_url {
183 result["public_url"] = json!(url);
184 }
185
186 if let Some(error) = &status.error {
188 result["error"] = json!(error);
189 }
190
191 if is_failed {
194 result["next_steps"] = json!([
195 "STOP - Deployment failed. Inform the user of the error.",
196 "Review the error message for details",
197 "Check the deployment configuration",
198 "Verify the code builds successfully locally"
199 ]);
200 result["action"] = json!("STOP_POLLING");
201 } else if truly_ready && public_url.is_some() {
202 result["next_steps"] = json!([
203 format!("STOP - Service is live at: {}", public_url.as_ref().unwrap()),
204 "Deployment completed successfully!",
205 "Inform the user their service is ready"
206 ]);
207 result["action"] = json!("STOP_POLLING");
208 } else if task_complete && !truly_ready {
209 result["next_steps"] = json!([
210 "STOP POLLING - Inform the user that deployment is in progress",
211 "Infrastructure is ready, Cloud Runner is building the container",
212 "Tell the user to wait 1-2 minutes, then they can ask you to check status again",
213 "DO NOT call get_deployment_status again automatically - wait for user to ask"
214 ]);
215 result["action"] = json!("INFORM_USER_AND_WAIT");
216 result["estimated_wait"] = json!("1-2 minutes");
217 result["note"] = json!("Task shows 100% but container is still being built/deployed. This is normal. DO NOT poll repeatedly - inform the user and wait for them to ask for status.");
218 } else if !task_complete {
219 result["next_steps"] = json!([
220 format!("STOP POLLING - Deployment is {} ({}% complete)", status.overall_status, status.progress),
221 "Inform the user of current progress",
222 "Tell them to wait and ask again in 30 seconds if they want an update",
223 "DO NOT call get_deployment_status again automatically"
224 ]);
225 result["action"] = json!("INFORM_USER_AND_WAIT");
226 }
227
228 serde_json::to_string_pretty(&result)
229 .map_err(|e| GetDeploymentStatusError(format!("Failed to serialize: {}", e)))
230 }
231 Err(e) => Ok(format_api_error("get_deployment_status", e)),
232 }
233 }
234}
235
236fn format_api_error(tool_name: &str, error: PlatformApiError) -> String {
238 match error {
239 PlatformApiError::Unauthorized => format_error_for_llm(
240 tool_name,
241 ErrorCategory::PermissionDenied,
242 "Not authenticated - please run `sync-ctl auth login` first",
243 Some(vec![
244 "The user needs to authenticate with the Syncable platform",
245 "Run: sync-ctl auth login",
246 ]),
247 ),
248 PlatformApiError::NotFound(msg) => format_error_for_llm(
249 tool_name,
250 ErrorCategory::ResourceUnavailable,
251 &format!("Deployment task not found: {}", msg),
252 Some(vec![
253 "The task_id may be incorrect or expired",
254 "Use trigger_deployment to start a new deployment",
255 ]),
256 ),
257 PlatformApiError::PermissionDenied(msg) => format_error_for_llm(
258 tool_name,
259 ErrorCategory::PermissionDenied,
260 &format!("Permission denied: {}", msg),
261 Some(vec![
262 "The user does not have access to this deployment",
263 "Contact the project admin for access",
264 ]),
265 ),
266 PlatformApiError::RateLimited => format_error_for_llm(
267 tool_name,
268 ErrorCategory::ResourceUnavailable,
269 "Rate limit exceeded - please try again later",
270 Some(vec!["Wait a moment before retrying"]),
271 ),
272 PlatformApiError::HttpError(e) => format_error_for_llm(
273 tool_name,
274 ErrorCategory::NetworkError,
275 &format!("Network error: {}", e),
276 Some(vec![
277 "Check network connectivity",
278 "The Syncable API may be temporarily unavailable",
279 ]),
280 ),
281 PlatformApiError::ParseError(msg) => format_error_for_llm(
282 tool_name,
283 ErrorCategory::InternalError,
284 &format!("Failed to parse API response: {}", msg),
285 Some(vec!["This may be a temporary API issue"]),
286 ),
287 PlatformApiError::ApiError { status, message } => format_error_for_llm(
288 tool_name,
289 ErrorCategory::ExternalCommandFailed,
290 &format!("API error ({}): {}", status, message),
291 Some(vec!["Check the error message for details"]),
292 ),
293 PlatformApiError::ServerError { status, message } => format_error_for_llm(
294 tool_name,
295 ErrorCategory::ExternalCommandFailed,
296 &format!("Server error ({}): {}", status, message),
297 Some(vec![
298 "The Syncable API is experiencing issues",
299 "Try again later",
300 ]),
301 ),
302 PlatformApiError::ConnectionFailed => format_error_for_llm(
303 tool_name,
304 ErrorCategory::NetworkError,
305 "Could not connect to Syncable API",
306 Some(vec![
307 "Check your internet connection",
308 "The Syncable API may be temporarily unavailable",
309 ]),
310 ),
311 }
312}
313
314#[cfg(test)]
315mod tests {
316 use super::*;
317
318 #[test]
319 fn test_tool_name() {
320 assert_eq!(GetDeploymentStatusTool::NAME, "get_deployment_status");
321 }
322
323 #[test]
324 fn test_tool_creation() {
325 let tool = GetDeploymentStatusTool::new();
326 assert!(format!("{:?}", tool).contains("GetDeploymentStatusTool"));
327 }
328}