1use 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) =
137 if let (Some(project_id), Some(service_name)) =
138 (&args.project_id, &args.service_name)
139 {
140 match client.list_deployments(project_id, Some(10)).await {
141 Ok(paginated) => {
142 let deployment = paginated
144 .data
145 .iter()
146 .find(|d| d.service_name.eq_ignore_ascii_case(service_name));
147
148 match deployment {
149 Some(d) => (
150 Some(d.status.clone()),
151 d.public_url.clone(),
152 d.public_url.is_some() && d.status == "running",
153 ),
154 None => (None, None, false),
155 }
156 }
157 Err(_) => (None, None, false),
158 }
159 } else {
160 (None, None, false)
161 };
162
163 let truly_ready = if args.project_id.is_some() {
165 service_ready
166 } else {
167 is_healthy
168 };
169
170 let mut result = json!({
171 "success": true,
172 "task_id": args.task_id,
173 "task_status": status.status,
174 "task_progress": status.progress,
175 "current_step": status.current_step,
176 "overall_status": status.overall_status,
177 "overall_message": status.overall_message,
178 "task_complete": task_complete,
179 "is_failed": is_failed,
180 "service_ready": truly_ready
181 });
182
183 if let Some(svc_status) = service_status {
185 result["service_status"] = json!(svc_status);
186 }
187 if let Some(url) = &public_url {
188 result["public_url"] = json!(url);
189 }
190
191 if let Some(error) = &status.error {
193 result["error"] = json!(error);
194 }
195
196 if is_failed {
199 result["next_steps"] = json!([
200 "STOP - Deployment failed. Inform the user of the error.",
201 "Review the error message for details",
202 "Check the deployment configuration",
203 "Verify the code builds successfully locally"
204 ]);
205 result["action"] = json!("STOP_POLLING");
206 } else if truly_ready && public_url.is_some() {
207 result["next_steps"] = json!([
208 format!(
209 "STOP - Service is live at: {}",
210 public_url.as_ref().unwrap()
211 ),
212 "Deployment completed successfully!",
213 "Inform the user their service is ready"
214 ]);
215 result["action"] = json!("STOP_POLLING");
216 } else if task_complete && !truly_ready {
217 result["next_steps"] = json!([
218 "STOP POLLING - Inform the user that deployment is in progress",
219 "Infrastructure is ready, Cloud Runner is building the container",
220 "Tell the user to wait 1-2 minutes, then they can ask you to check status again",
221 "DO NOT call get_deployment_status again automatically - wait for user to ask"
222 ]);
223 result["action"] = json!("INFORM_USER_AND_WAIT");
224 result["estimated_wait"] = json!("1-2 minutes");
225 result["note"] = json!(
226 "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."
227 );
228 } else if !task_complete {
229 result["next_steps"] = json!([
230 format!(
231 "STOP POLLING - Deployment is {} ({}% complete)",
232 status.overall_status, status.progress
233 ),
234 "Inform the user of current progress",
235 "Tell them to wait and ask again in 30 seconds if they want an update",
236 "DO NOT call get_deployment_status again automatically"
237 ]);
238 result["action"] = json!("INFORM_USER_AND_WAIT");
239 }
240
241 serde_json::to_string_pretty(&result)
242 .map_err(|e| GetDeploymentStatusError(format!("Failed to serialize: {}", e)))
243 }
244 Err(e) => Ok(format_api_error("get_deployment_status", e)),
245 }
246 }
247}
248
249fn format_api_error(tool_name: &str, error: PlatformApiError) -> String {
251 match error {
252 PlatformApiError::Unauthorized => format_error_for_llm(
253 tool_name,
254 ErrorCategory::PermissionDenied,
255 "Not authenticated - please run `sync-ctl auth login` first",
256 Some(vec![
257 "The user needs to authenticate with the Syncable platform",
258 "Run: sync-ctl auth login",
259 ]),
260 ),
261 PlatformApiError::NotFound(msg) => format_error_for_llm(
262 tool_name,
263 ErrorCategory::ResourceUnavailable,
264 &format!("Deployment task not found: {}", msg),
265 Some(vec![
266 "The task_id may be incorrect or expired",
267 "Use trigger_deployment to start a new deployment",
268 ]),
269 ),
270 PlatformApiError::PermissionDenied(msg) => format_error_for_llm(
271 tool_name,
272 ErrorCategory::PermissionDenied,
273 &format!("Permission denied: {}", msg),
274 Some(vec![
275 "The user does not have access to this deployment",
276 "Contact the project admin for access",
277 ]),
278 ),
279 PlatformApiError::RateLimited => format_error_for_llm(
280 tool_name,
281 ErrorCategory::ResourceUnavailable,
282 "Rate limit exceeded - please try again later",
283 Some(vec!["Wait a moment before retrying"]),
284 ),
285 PlatformApiError::HttpError(e) => format_error_for_llm(
286 tool_name,
287 ErrorCategory::NetworkError,
288 &format!("Network error: {}", e),
289 Some(vec![
290 "Check network connectivity",
291 "The Syncable API may be temporarily unavailable",
292 ]),
293 ),
294 PlatformApiError::ParseError(msg) => format_error_for_llm(
295 tool_name,
296 ErrorCategory::InternalError,
297 &format!("Failed to parse API response: {}", msg),
298 Some(vec!["This may be a temporary API issue"]),
299 ),
300 PlatformApiError::ApiError { status, message } => format_error_for_llm(
301 tool_name,
302 ErrorCategory::ExternalCommandFailed,
303 &format!("API error ({}): {}", status, message),
304 Some(vec!["Check the error message for details"]),
305 ),
306 PlatformApiError::ServerError { status, message } => format_error_for_llm(
307 tool_name,
308 ErrorCategory::ExternalCommandFailed,
309 &format!("Server error ({}): {}", status, message),
310 Some(vec![
311 "The Syncable API is experiencing issues",
312 "Try again later",
313 ]),
314 ),
315 PlatformApiError::ConnectionFailed => format_error_for_llm(
316 tool_name,
317 ErrorCategory::NetworkError,
318 "Could not connect to Syncable API",
319 Some(vec![
320 "Check your internet connection",
321 "The Syncable API may be temporarily unavailable",
322 ]),
323 ),
324 }
325}
326
327#[cfg(test)]
328mod tests {
329 use super::*;
330
331 #[test]
332 fn test_tool_name() {
333 assert_eq!(GetDeploymentStatusTool::NAME, "get_deployment_status");
334 }
335
336 #[test]
337 fn test_tool_creation() {
338 let tool = GetDeploymentStatusTool::new();
339 assert!(format!("{:?}", tool).contains("GetDeploymentStatusTool"));
340 }
341}