syncable_cli/agent/tools/platform/
list_deployments.rs

1//! List deployments tool for the agent
2//!
3//! Allows the agent to list recent deployments for a project.
4
5use 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/// Arguments for the list deployments tool
14#[derive(Debug, Deserialize)]
15pub struct ListDeploymentsArgs {
16    /// The project ID to list deployments for
17    pub project_id: String,
18    /// Optional limit on number of deployments to return (default 10)
19    pub limit: Option<i32>,
20}
21
22/// Error type for list deployments operations
23#[derive(Debug, thiserror::Error)]
24#[error("List deployments error: {0}")]
25pub struct ListDeploymentsError(String);
26
27/// Tool to list recent deployments for a project
28///
29/// Returns a paginated list of deployments with status, commit info, and public URLs.
30#[derive(Debug, Clone, Serialize, Deserialize, Default)]
31pub struct ListDeploymentsTool;
32
33impl ListDeploymentsTool {
34    /// Create a new ListDeploymentsTool
35    pub fn new() -> Self {
36        Self
37    }
38}
39
40impl Tool for ListDeploymentsTool {
41    const NAME: &'static str = "list_deployments";
42
43    type Error = ListDeploymentsError;
44    type Args = ListDeploymentsArgs;
45    type Output = String;
46
47    async fn definition(&self, _prompt: String) -> ToolDefinition {
48        ToolDefinition {
49            name: Self::NAME.to_string(),
50            description: r#"List recent deployments for a project.
51
52Returns a list of deployments with their status, commit SHA, public URLs,
53and creation timestamps.
54
55**Parameters:**
56- project_id: The project UUID
57- limit: Optional number of deployments to return (default 10)
58
59**Prerequisites:**
60- User must be authenticated via `sync-ctl auth login`
61
62**Use Cases:**
63- View deployment history for a project
64- Find the public URL of a deployed service
65- Check the status of recent deployments
66- Get task IDs for checking deployment status"#
67                .to_string(),
68            parameters: json!({
69                "type": "object",
70                "properties": {
71                    "project_id": {
72                        "type": "string",
73                        "description": "The UUID of the project to list deployments for"
74                    },
75                    "limit": {
76                        "type": "integer",
77                        "description": "Optional: number of deployments to return (default 10)"
78                    }
79                },
80                "required": ["project_id"]
81            }),
82        }
83    }
84
85    async fn call(&self, args: Self::Args) -> Result<Self::Output, Self::Error> {
86        // Validate project_id
87        if args.project_id.trim().is_empty() {
88            return Ok(format_error_for_llm(
89                "list_deployments",
90                ErrorCategory::ValidationFailed,
91                "project_id cannot be empty",
92                Some(vec![
93                    "Use list_projects to find valid project IDs",
94                    "Use select_project to set the current project context",
95                ]),
96            ));
97        }
98
99        // Create the API client
100        let client = match PlatformApiClient::new() {
101            Ok(c) => c,
102            Err(e) => {
103                return Ok(format_api_error("list_deployments", e));
104            }
105        };
106
107        // Fetch deployments
108        match client.list_deployments(&args.project_id, args.limit).await {
109            Ok(paginated) => {
110                if paginated.data.is_empty() {
111                    return Ok(json!({
112                        "success": true,
113                        "deployments": [],
114                        "count": 0,
115                        "has_more": false,
116                        "message": "No deployments found for this project. Use trigger_deployment to start a deployment."
117                    })
118                    .to_string());
119                }
120
121                let deployment_list: Vec<serde_json::Value> = paginated
122                    .data
123                    .iter()
124                    .map(|deployment| {
125                        json!({
126                            "id": deployment.id,
127                            "service_name": deployment.service_name,
128                            "repository": deployment.repository_full_name,
129                            "status": deployment.status,
130                            "task_id": deployment.backstage_task_id,
131                            "commit_sha": deployment.commit_sha,
132                            "public_url": deployment.public_url,
133                            "created_at": deployment.created_at.to_rfc3339()
134                        })
135                    })
136                    .collect();
137
138                let result = json!({
139                    "success": true,
140                    "deployments": deployment_list,
141                    "count": paginated.data.len(),
142                    "has_more": paginated.pagination.has_more,
143                    "next_cursor": paginated.pagination.next_cursor,
144                    "message": format!("Found {} deployment(s)", paginated.data.len())
145                });
146
147                serde_json::to_string_pretty(&result)
148                    .map_err(|e| ListDeploymentsError(format!("Failed to serialize: {}", e)))
149            }
150            Err(e) => Ok(format_api_error("list_deployments", e)),
151        }
152    }
153}
154
155/// Format a PlatformApiError for LLM consumption
156fn format_api_error(tool_name: &str, error: PlatformApiError) -> String {
157    match error {
158        PlatformApiError::Unauthorized => format_error_for_llm(
159            tool_name,
160            ErrorCategory::PermissionDenied,
161            "Not authenticated - please run `sync-ctl auth login` first",
162            Some(vec![
163                "The user needs to authenticate with the Syncable platform",
164                "Run: sync-ctl auth login",
165            ]),
166        ),
167        PlatformApiError::NotFound(msg) => format_error_for_llm(
168            tool_name,
169            ErrorCategory::ResourceUnavailable,
170            &format!("Resource not found: {}", msg),
171            Some(vec![
172                "The project ID may be incorrect",
173                "Use list_projects to find valid project IDs",
174            ]),
175        ),
176        PlatformApiError::PermissionDenied(msg) => format_error_for_llm(
177            tool_name,
178            ErrorCategory::PermissionDenied,
179            &format!("Permission denied: {}", msg),
180            Some(vec![
181                "The user does not have access to this project",
182                "Contact the project admin for access",
183            ]),
184        ),
185        PlatformApiError::RateLimited => format_error_for_llm(
186            tool_name,
187            ErrorCategory::ResourceUnavailable,
188            "Rate limit exceeded - please try again later",
189            Some(vec!["Wait a moment before retrying"]),
190        ),
191        PlatformApiError::HttpError(e) => format_error_for_llm(
192            tool_name,
193            ErrorCategory::NetworkError,
194            &format!("Network error: {}", e),
195            Some(vec![
196                "Check network connectivity",
197                "The Syncable API may be temporarily unavailable",
198            ]),
199        ),
200        PlatformApiError::ParseError(msg) => format_error_for_llm(
201            tool_name,
202            ErrorCategory::InternalError,
203            &format!("Failed to parse API response: {}", msg),
204            Some(vec!["This may be a temporary API issue"]),
205        ),
206        PlatformApiError::ApiError { status, message } => format_error_for_llm(
207            tool_name,
208            ErrorCategory::ExternalCommandFailed,
209            &format!("API error ({}): {}", status, message),
210            Some(vec!["Check the error message for details"]),
211        ),
212        PlatformApiError::ServerError { status, message } => format_error_for_llm(
213            tool_name,
214            ErrorCategory::ExternalCommandFailed,
215            &format!("Server error ({}): {}", status, message),
216            Some(vec![
217                "The Syncable API is experiencing issues",
218                "Try again later",
219            ]),
220        ),
221        PlatformApiError::ConnectionFailed => format_error_for_llm(
222            tool_name,
223            ErrorCategory::NetworkError,
224            "Could not connect to Syncable API",
225            Some(vec![
226                "Check your internet connection",
227                "The Syncable API may be temporarily unavailable",
228            ]),
229        ),
230    }
231}
232
233#[cfg(test)]
234mod tests {
235    use super::*;
236
237    #[test]
238    fn test_tool_name() {
239        assert_eq!(ListDeploymentsTool::NAME, "list_deployments");
240    }
241
242    #[test]
243    fn test_tool_creation() {
244        let tool = ListDeploymentsTool::new();
245        assert!(format!("{:?}", tool).contains("ListDeploymentsTool"));
246    }
247}