Files
crank/crates/crank-community-mcp/src/tool_error.rs
T
2026-06-21 01:42:45 +00:00

220 lines
9.4 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<u16>,
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<String>,
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<u16> {
match error {
RuntimeError::RestAdapter(RestAdapterError::UnexpectedStatus { status, .. }) => {
Some(*status)
}
_ => None,
}
}