use crank_adapter_rest::RestAdapterError; use crank_runtime::RuntimeError; use serde::Serialize; use serde_json::{Value, json}; #[derive(Clone, Debug, PartialEq, Eq, Serialize)] pub struct ToolErrorContract { pub code: &'static str, pub error_code: &'static str, pub message: String, pub recoverable: bool, #[serde(skip_serializing_if = "Option::is_none")] pub suggested_action: Option<&'static str>, #[serde(skip_serializing_if = "Option::is_none")] pub upstream_status: Option, pub request_id: String, } pub fn tool_error_contract_from_runtime( error: &RuntimeError, request_id: &str, ) -> ToolErrorContract { let error_code = runtime_error_code(error); ToolErrorContract { code: error_code, error_code, message: safe_runtime_error_message(error), recoverable: is_recoverable_runtime_error(error), suggested_action: suggested_action(error), upstream_status: upstream_status(error), request_id: request_id.to_owned(), } } pub fn generic_tool_error_contract( error_code: &'static str, message: impl Into, request_id: &str, recoverable: bool, suggested_action: Option<&'static str>, ) -> ToolErrorContract { ToolErrorContract { code: error_code, error_code, message: message.into(), recoverable, suggested_action, upstream_status: None, request_id: request_id.to_owned(), } } pub fn tool_error_text(error: &ToolErrorContract) -> String { match error.suggested_action { Some(action) => format!("{} {}", error.message, action), None => error.message.clone(), } } pub fn tool_error_value(error: &ToolErrorContract) -> Value { serde_json::to_value(error).unwrap_or_else(|_| { json!({ "code": "runtime_error", "error_code": "runtime_error", "message": "Не удалось выполнить инструмент.", "recoverable": false, "request_id": error.request_id }) }) } pub fn runtime_error_code(error: &RuntimeError) -> &'static str { match error { RuntimeError::Schema(_) => "schema_validation_error", RuntimeError::Mapping(_) => "mapping_error", RuntimeError::RestAdapter(RestAdapterError::UnexpectedStatus { status, .. }) => { upstream_status_code(*status) } RuntimeError::RestAdapter(RestAdapterError::Transport(_)) => "upstream_transport_error", RuntimeError::RestAdapter(_) => "adapter_execution_error", RuntimeError::ProtocolAdapter(_) => "adapter_execution_error", RuntimeError::UnsupportedProtocol { .. } => "unsupported_protocol", RuntimeError::ConcurrencyLimitExceeded { .. } => "runtime_overloaded", RuntimeError::UnsupportedExecutionMode { .. } => "streaming_mode_error", RuntimeError::InvalidPreparedRequest { .. } => "runtime_error", RuntimeError::ConfirmationRequired { .. } => "confirmation_required", RuntimeError::InvalidConfirmationToken { .. } => "invalid_confirmation_token", RuntimeError::ConfirmationStoreUnavailable { .. } => "confirmation_unavailable", RuntimeError::MissingAuthProfile { .. } => "auth_profile_not_found", RuntimeError::MissingSecret { .. } | RuntimeError::MissingSecretVersion { .. } => { "secret_not_found" } RuntimeError::InvalidAuthSecretValue { .. } => "secret_value_error", RuntimeError::SecretCrypto { .. } => "secret_crypto_error", } } fn upstream_status_code(status: u16) -> &'static str { match status { 401 | 403 => "upstream_auth_error", 404 => "upstream_not_found", 408 | 429 => "upstream_rate_limited", 500..=599 => "upstream_server_error", _ => "upstream_status_error", } } fn safe_runtime_error_message(error: &RuntimeError) -> String { match error { RuntimeError::Schema(_) => "Входные параметры не прошли проверку схемы.".to_owned(), RuntimeError::Mapping(_) => { "Не удалось сопоставить параметры инструмента с API-запросом.".to_owned() } RuntimeError::RestAdapter(RestAdapterError::UnexpectedStatus { status, .. }) => { format!("Внешний API вернул HTTP {}.", status) } RuntimeError::RestAdapter(RestAdapterError::Transport(_)) => { "Не удалось подключиться к внешнему API.".to_owned() } RuntimeError::RestAdapter(_) => "Не удалось выполнить запрос к внешнему API.".to_owned(), RuntimeError::ProtocolAdapter(_) => "Не удалось выполнить протокольный адаптер.".to_owned(), RuntimeError::UnsupportedProtocol { .. } => { "Протокол операции не поддерживается.".to_owned() } RuntimeError::ConcurrencyLimitExceeded { .. } => { "Сервис временно перегружен и не может выполнить инструмент.".to_owned() } RuntimeError::UnsupportedExecutionMode { .. } => { "Режим выполнения операции не поддерживается.".to_owned() } RuntimeError::InvalidPreparedRequest { .. } => { "Не удалось подготовить корректный API-запрос.".to_owned() } RuntimeError::ConfirmationRequired { confirmation_token, expires_in_ms, .. } => format!( "Операция требует подтверждения. Повторите вызов с _crank_confirmation_token=\"{}\" в течение {} секунд.", confirmation_token, expires_in_ms / 1000 ), RuntimeError::InvalidConfirmationToken { .. } => { "Токен подтверждения недействителен, истек или уже был использован.".to_owned() } RuntimeError::ConfirmationStoreUnavailable { .. } => { "Хранилище подтверждений временно недоступно.".to_owned() } RuntimeError::MissingAuthProfile { .. } => "Профиль авторизации не найден.".to_owned(), RuntimeError::MissingSecret { .. } | RuntimeError::MissingSecretVersion { .. } => { "Секрет авторизации не найден.".to_owned() } RuntimeError::InvalidAuthSecretValue { .. } => { "Секрет авторизации имеет неподходящий формат.".to_owned() } RuntimeError::SecretCrypto { .. } => { "Не удалось расшифровать секрет авторизации.".to_owned() } } } fn is_recoverable_runtime_error(error: &RuntimeError) -> bool { matches!( error, RuntimeError::RestAdapter(RestAdapterError::UnexpectedStatus { status: 408 | 429 | 500..=599, .. }) | RuntimeError::RestAdapter(RestAdapterError::Transport(_)) | RuntimeError::ConcurrencyLimitExceeded { .. } | RuntimeError::SecretCrypto { .. } | RuntimeError::ConfirmationRequired { .. } ) } fn suggested_action(error: &RuntimeError) -> Option<&'static str> { match error { RuntimeError::RestAdapter(RestAdapterError::UnexpectedStatus { status: 401 | 403, .. }) => Some("Проверьте настройки авторизации внешнего API."), RuntimeError::RestAdapter(RestAdapterError::UnexpectedStatus { status: 404, .. }) => { Some("Проверьте путь endpoint-а и параметры запроса.") } RuntimeError::RestAdapter(RestAdapterError::UnexpectedStatus { status: 408 | 429 | 500..=599, .. }) | RuntimeError::RestAdapter(RestAdapterError::Transport(_)) | RuntimeError::ConcurrencyLimitExceeded { .. } => Some("Повторите запрос позже."), RuntimeError::Schema(_) | RuntimeError::Mapping(_) | RuntimeError::InvalidPreparedRequest { .. } => { Some("Проверьте параметры вызова инструмента.") } RuntimeError::ConfirmationRequired { .. } => { Some("Повторите вызов с указанным токеном подтверждения.") } RuntimeError::InvalidConfirmationToken { .. } => { Some("Запросите новый токен подтверждения.") } RuntimeError::ConfirmationStoreUnavailable { .. } => Some("Повторите запрос позже."), RuntimeError::MissingAuthProfile { .. } | RuntimeError::MissingSecret { .. } | RuntimeError::MissingSecretVersion { .. } | RuntimeError::InvalidAuthSecretValue { .. } | RuntimeError::SecretCrypto { .. } => { Some("Проверьте настройки авторизации и секретов в Crank.") } _ => None, } } fn upstream_status(error: &RuntimeError) -> Option { match error { RuntimeError::RestAdapter(RestAdapterError::UnexpectedStatus { status, .. }) => { Some(*status) } _ => None, } }