use rig::completion::ToolDefinition;
use rig::tool::Tool;
use serde::{Deserialize, Serialize};
use serde_json::json;
use crate::agent::tools::error::{ErrorCategory, format_error_for_llm};
use crate::platform::api::{PlatformApiClient, PlatformApiError, TriggerDeploymentRequest};
use crate::platform::session::PlatformSession;
#[derive(Debug, Deserialize)]
pub struct TriggerDeploymentArgs {
pub config_id: String,
pub commit_sha: Option<String>,
}
#[derive(Debug, thiserror::Error)]
#[error("Trigger deployment error: {0}")]
pub struct TriggerDeploymentError(String);
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct TriggerDeploymentTool;
impl TriggerDeploymentTool {
pub fn new() -> Self {
Self
}
}
impl Tool for TriggerDeploymentTool {
const NAME: &'static str = "trigger_deployment";
type Error = TriggerDeploymentError;
type Args = TriggerDeploymentArgs;
type Output = String;
async fn definition(&self, _prompt: String) -> ToolDefinition {
ToolDefinition {
name: Self::NAME.to_string(),
description: r#"Trigger a deployment using a deployment configuration.
Starts a new deployment for the specified config. Returns a task ID that can be
used to monitor deployment progress with `get_deployment_status`.
**Parameters:**
- config_id: The deployment config ID (get from list_deployment_configs or create_deployment_config)
- commit_sha: Optional specific commit to deploy (defaults to latest on branch)
**Prerequisites:**
- User must be authenticated via `sync-ctl auth login`
- A deployment config must exist (use create_deployment_config first if needed)
**Use Cases:**
- Deploy the latest code from a branch
- Deploy a specific commit version
- Trigger a manual deployment for a service
**Returns:**
- task_id: Use this to check deployment progress with get_deployment_status
- status: Initial deployment status
- message: Human-readable status message"#
.to_string(),
parameters: json!({
"type": "object",
"properties": {
"config_id": {
"type": "string",
"description": "The deployment config ID (from list_deployment_configs or create_deployment_config)"
},
"commit_sha": {
"type": "string",
"description": "Optional: specific commit SHA to deploy (defaults to latest)"
}
},
"required": ["config_id"]
}),
}
}
async fn call(&self, args: Self::Args) -> Result<Self::Output, Self::Error> {
if args.config_id.trim().is_empty() {
return Ok(format_error_for_llm(
"trigger_deployment",
ErrorCategory::ValidationFailed,
"config_id cannot be empty",
Some(vec![
"Use list_deployment_configs to find available deployment configs",
]),
));
}
let session = match PlatformSession::load() {
Ok(s) => s,
Err(_) => {
return Ok(format_error_for_llm(
"trigger_deployment",
ErrorCategory::InternalError,
"Failed to load platform session",
Some(vec!["Try authenticating with `sync-ctl auth login`"]),
));
}
};
if !session.is_project_selected() {
return Ok(format_error_for_llm(
"trigger_deployment",
ErrorCategory::ValidationFailed,
"No project selected",
Some(vec!["Use select_project to choose a project first"]),
));
}
let project_id = session.project_id.clone().unwrap_or_default();
let client = match PlatformApiClient::new() {
Ok(c) => c,
Err(e) => {
return Ok(format_api_error("trigger_deployment", e));
}
};
let request = TriggerDeploymentRequest {
project_id,
config_id: args.config_id.clone(),
commit_sha: args.commit_sha.clone(),
};
match client.trigger_deployment(&request).await {
Ok(response) => {
let result = json!({
"success": true,
"task_id": response.backstage_task_id,
"config_id": response.config_id,
"status": response.status,
"message": response.message,
"next_steps": [
format!("Use get_deployment_status with task_id '{}' to monitor progress", response.backstage_task_id),
"Deployment typically takes 2-5 minutes to complete"
]
});
serde_json::to_string_pretty(&result)
.map_err(|e| TriggerDeploymentError(format!("Failed to serialize: {}", e)))
}
Err(e) => Ok(format_api_error("trigger_deployment", e)),
}
}
}
fn format_api_error(tool_name: &str, error: PlatformApiError) -> String {
match error {
PlatformApiError::Unauthorized => format_error_for_llm(
tool_name,
ErrorCategory::PermissionDenied,
"Not authenticated - please run `sync-ctl auth login` first",
Some(vec![
"The user needs to authenticate with the Syncable platform",
"Run: sync-ctl auth login",
]),
),
PlatformApiError::NotFound(msg) => format_error_for_llm(
tool_name,
ErrorCategory::ResourceUnavailable,
&format!("Resource not found: {}", msg),
Some(vec![
"The project ID or config ID may be incorrect",
"Use list_deployment_configs to find valid config IDs",
]),
),
PlatformApiError::PermissionDenied(msg) => format_error_for_llm(
tool_name,
ErrorCategory::PermissionDenied,
&format!("Permission denied: {}", msg),
Some(vec![
"The user does not have permission to trigger deployments",
"Contact the project admin for access",
]),
),
PlatformApiError::RateLimited => format_error_for_llm(
tool_name,
ErrorCategory::ResourceUnavailable,
"Rate limit exceeded - please try again later",
Some(vec!["Wait a moment before retrying"]),
),
PlatformApiError::HttpError(e) => format_error_for_llm(
tool_name,
ErrorCategory::NetworkError,
&format!("Network error: {}", e),
Some(vec![
"Check network connectivity",
"The Syncable API may be temporarily unavailable",
]),
),
PlatformApiError::ParseError(msg) => format_error_for_llm(
tool_name,
ErrorCategory::InternalError,
&format!("Failed to parse API response: {}", msg),
Some(vec!["This may be a temporary API issue"]),
),
PlatformApiError::ApiError { status, message } => format_error_for_llm(
tool_name,
ErrorCategory::ExternalCommandFailed,
&format!("API error ({}): {}", status, message),
Some(vec!["Check the error message for details"]),
),
PlatformApiError::ServerError { status, message } => format_error_for_llm(
tool_name,
ErrorCategory::ExternalCommandFailed,
&format!("Server error ({}): {}", status, message),
Some(vec![
"The Syncable API is experiencing issues",
"Try again later",
]),
),
PlatformApiError::ConnectionFailed => format_error_for_llm(
tool_name,
ErrorCategory::NetworkError,
"Could not connect to Syncable API",
Some(vec![
"Check your internet connection",
"The Syncable API may be temporarily unavailable",
]),
),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_tool_name() {
assert_eq!(TriggerDeploymentTool::NAME, "trigger_deployment");
}
#[test]
fn test_tool_creation() {
let tool = TriggerDeploymentTool::new();
assert!(format!("{:?}", tool).contains("TriggerDeploymentTool"));
}
}