diff --git a/Cargo.lock b/Cargo.lock index 549fe72b..71a4779d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -322,6 +322,7 @@ dependencies = [ "aion-config", "aion-mcp", "aion-protocol", + "aion-providers", "aion-types", "aionui-api-types", "aionui-auth", diff --git a/Cargo.toml b/Cargo.toml index 18816be1..6c00bb3d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -54,6 +54,7 @@ aionui-assistant = { path = "crates/aionui-assistant" } aionui-app = { path = "crates/aionui-app" } aion-agent = { git = "https://github.com/iOfficeAI/aionrs.git", tag = "v0.1.30" } +aion-providers = { git = "https://github.com/iOfficeAI/aionrs.git", tag = "v0.1.30" } aion-types = { git = "https://github.com/iOfficeAI/aionrs.git", tag = "v0.1.30" } aion-protocol = { git = "https://github.com/iOfficeAI/aionrs.git", tag = "v0.1.30" } aion-config = { git = "https://github.com/iOfficeAI/aionrs.git", tag = "v0.1.30" } diff --git a/crates/aionui-ai-agent/Cargo.toml b/crates/aionui-ai-agent/Cargo.toml index dbf8d425..05c2cd2a 100644 --- a/crates/aionui-ai-agent/Cargo.toml +++ b/crates/aionui-ai-agent/Cargo.toml @@ -33,6 +33,7 @@ thiserror.workspace = true tracing.workspace = true which.workspace = true aion-agent.workspace = true +aion-providers.workspace = true aion-types.workspace = true aion-protocol.workspace = true aion-config.workspace = true diff --git a/crates/aionui-ai-agent/src/manager/aionrs/agent.rs b/crates/aionui-ai-agent/src/manager/aionrs/agent.rs index 0419c389..a1396a0d 100644 --- a/crates/aionui-ai-agent/src/manager/aionrs/agent.rs +++ b/crates/aionui-ai-agent/src/manager/aionrs/agent.rs @@ -24,6 +24,8 @@ use crate::protocol::events::AgentStreamEvent; use crate::protocol::send_error::AgentSendError; use crate::types::{AionrsResolvedConfig, SendMessageData}; +use super::error::aionrs_engine_error_to_send_error; + pub struct AionrsAgentManager { runtime: AgentRuntime, engine: Mutex, @@ -257,14 +259,13 @@ impl crate::agent_task::IAgentTask for AionrsAgentManager { Ok(()) } Some(Err(e)) => { - let error_msg = format!("Aionrs agent error: {e}"); error!( conversation_id = %self.runtime.conversation_id(), elapsed_ms, error = %ErrorChain(&e), "Aionrs engine.run() failed, emitting Error" ); - let send_error = aionrs_engine_error_to_send_error(error_msg); + let send_error = aionrs_engine_error_to_send_error(&e); self.runtime.emit_error_data(send_error.stream_error().clone()); Err(send_error) } @@ -395,18 +396,6 @@ fn parse_session_mode(s: &str) -> SessionMode { } } -fn aionrs_engine_error_to_send_error(error_msg: String) -> AgentSendError { - let lower = error_msg.to_ascii_lowercase(); - if lower.contains("provider error") - || lower.contains("provider:") - || lower.contains("api error:") - || lower.contains("repeatedly returned malformed tool calls") - { - return AgentSendError::from_agent_error(AgentError::bad_gateway(error_msg)); - } - AgentSendError::from_agent_error(AgentError::internal(error_msg)) -} - #[cfg(test)] mod tests { use super::*; @@ -617,57 +606,4 @@ mod tests { other => panic!("Expected Finish, got {:?}", other), } } - - #[test] - fn aionrs_provider_connection_error_is_user_llm_provider_error() { - let send_error = aionrs_engine_error_to_send_error( - "Aionrs agent error: Provider error: Connection error: Signable request error: failed to create canonical request" - .to_owned(), - ); - - assert_eq!( - send_error.code(), - Some(aionui_api_types::AgentErrorCode::UserLlmProviderConfigError) - ); - assert_eq!( - send_error.ownership(), - Some(aionui_api_types::AgentErrorOwnership::UserLlmProvider) - ); - assert_eq!(send_error.stream_error().retryable, Some(false)); - } - - #[test] - fn aionrs_api_connection_error_is_user_llm_provider_network_error() { - let send_error = aionrs_engine_error_to_send_error( - "Aionrs agent error: API error: Connection error: error decoding response body".to_owned(), - ); - - assert_eq!( - send_error.code(), - Some(aionui_api_types::AgentErrorCode::UserLlmProviderNetworkError) - ); - assert_eq!( - send_error.ownership(), - Some(aionui_api_types::AgentErrorOwnership::UserLlmProvider) - ); - assert_eq!(send_error.stream_error().retryable, Some(true)); - } - - #[test] - fn aionrs_repeated_malformed_tool_call_is_user_llm_provider_error() { - let send_error = aionrs_engine_error_to_send_error( - "Aionrs agent error: provider repeatedly returned malformed tool calls (3/3); stopped to avoid wasting tokens" - .to_owned(), - ); - - assert_eq!( - send_error.code(), - Some(aionui_api_types::AgentErrorCode::UserLlmProviderInvalidRequest) - ); - assert_eq!( - send_error.ownership(), - Some(aionui_api_types::AgentErrorOwnership::UserLlmProvider) - ); - assert_eq!(send_error.stream_error().retryable, Some(false)); - } } diff --git a/crates/aionui-ai-agent/src/manager/aionrs/error.rs b/crates/aionui-ai-agent/src/manager/aionrs/error.rs new file mode 100644 index 00000000..58e90e67 --- /dev/null +++ b/crates/aionui-ai-agent/src/manager/aionrs/error.rs @@ -0,0 +1,290 @@ +use aion_agent::engine::AgentError as AionrsAgentError; +use aion_providers::ProviderError; +use aionui_api_types::{ + AgentErrorCode, AgentErrorOwnership, AgentErrorResolution, AgentErrorResolutionKind, AgentErrorResolutionTarget, +}; + +use crate::protocol::send_error::AgentSendError; + +pub(super) fn aionrs_engine_error_to_send_error(error: &AionrsAgentError) -> AgentSendError { + let detail = format!("Aionrs agent error: {error}"); + match error { + AionrsAgentError::Provider(provider_error) => aionrs_provider_error_to_send_error(provider_error, detail), + AionrsAgentError::RepeatedMalformedToolCall { .. } => provider_send_error( + "The model provider repeatedly returned malformed tool calls", + AgentErrorCode::UserLlmProviderInvalidRequest, + detail, + false, + AgentErrorResolutionKind::ChangeModel, + Some(AgentErrorResolutionTarget::ProviderSettings), + ), + AionrsAgentError::ContextTooLong { .. } => provider_send_error( + "The request is too large for the configured model context window", + AgentErrorCode::UserLlmProviderContextTooLarge, + detail, + false, + AgentErrorResolutionKind::ReduceContext, + None, + ), + AionrsAgentError::ApiError(_) => unknown_upstream_send_error(detail), + AionrsAgentError::UserAborted => unknown_upstream_send_error(detail), + } +} + +fn aionrs_provider_error_to_send_error(error: &ProviderError, detail: String) -> AgentSendError { + match error { + ProviderError::Api { status, .. } => aionrs_provider_status_to_send_error(*status, detail), + ProviderError::RateLimited { .. } => provider_send_error( + "The model provider rate limited the request", + AgentErrorCode::UserLlmProviderRateLimited, + detail, + true, + AgentErrorResolutionKind::Retry, + None, + ), + ProviderError::PromptTooLong(_) => provider_send_error( + "The request is too large for the configured model context window", + AgentErrorCode::UserLlmProviderContextTooLarge, + detail, + false, + AgentErrorResolutionKind::ReduceContext, + None, + ), + ProviderError::Connection(_) | ProviderError::Http(_) => provider_send_error( + "The model provider could not be reached", + AgentErrorCode::UserLlmProviderNetworkError, + detail, + true, + AgentErrorResolutionKind::CheckProviderBaseUrl, + Some(AgentErrorResolutionTarget::ProviderSettings), + ), + ProviderError::Parse(_) => provider_send_error( + "The model provider returned a server error", + AgentErrorCode::UserLlmProviderGatewayError, + detail, + true, + AgentErrorResolutionKind::Retry, + None, + ), + } +} + +fn aionrs_provider_status_to_send_error(status: u16, detail: String) -> AgentSendError { + match status { + 400 => provider_send_error( + "The model provider rejected the request", + AgentErrorCode::UserLlmProviderInvalidRequest, + detail, + false, + AgentErrorResolutionKind::SendFeedback, + Some(AgentErrorResolutionTarget::Feedback), + ), + 401 => provider_send_error( + "The model provider rejected the request", + AgentErrorCode::UserLlmProviderAuthFailed, + detail, + false, + AgentErrorResolutionKind::CheckProviderCredentials, + Some(AgentErrorResolutionTarget::ProviderSettings), + ), + 402 => provider_send_error( + "The model provider account requires billing attention", + AgentErrorCode::UserLlmProviderBillingRequired, + detail, + false, + AgentErrorResolutionKind::CheckProviderBilling, + Some(AgentErrorResolutionTarget::ProviderSettings), + ), + 403 => provider_send_error( + "The model provider denied access to the request", + AgentErrorCode::UserLlmProviderPermissionDenied, + detail, + false, + AgentErrorResolutionKind::CheckProviderCredentials, + Some(AgentErrorResolutionTarget::ProviderSettings), + ), + 404 => provider_send_error( + "The model provider endpoint was not found", + AgentErrorCode::UserLlmProviderEndpointNotFound, + detail, + false, + AgentErrorResolutionKind::CheckProviderBaseUrl, + Some(AgentErrorResolutionTarget::ProviderSettings), + ), + 408 | 504 => provider_send_error( + "The model provider did not respond in time", + AgentErrorCode::UserLlmProviderTimeout, + detail, + true, + AgentErrorResolutionKind::Retry, + None, + ), + 429 => provider_send_error( + "The model provider rate limited the request", + AgentErrorCode::UserLlmProviderRateLimited, + detail, + true, + AgentErrorResolutionKind::Retry, + None, + ), + 500..=599 => provider_send_error( + "The model provider returned a server error", + AgentErrorCode::UserLlmProviderGatewayError, + detail, + true, + AgentErrorResolutionKind::Retry, + None, + ), + _ => provider_send_error( + "The model provider returned an error", + AgentErrorCode::UserLlmProviderGatewayError, + detail, + true, + AgentErrorResolutionKind::Retry, + None, + ), + } +} + +fn provider_send_error( + message: &'static str, + code: AgentErrorCode, + detail: String, + retryable: bool, + resolution_kind: AgentErrorResolutionKind, + resolution_target: Option, +) -> AgentSendError { + AgentSendError::new( + message, + code, + AgentErrorOwnership::UserLlmProvider, + Some(detail), + retryable, + false, + Some(AgentErrorResolution::new(resolution_kind, resolution_target)), + ) +} + +fn unknown_upstream_send_error(detail: String) -> AgentSendError { + AgentSendError::new( + "The upstream Agent failed while handling the request", + AgentErrorCode::UnknownUpstreamError, + AgentErrorOwnership::UnknownUpstream, + Some(detail), + true, + true, + Some(AgentErrorResolution::new( + AgentErrorResolutionKind::SendFeedback, + Some(AgentErrorResolutionTarget::Feedback), + )), + ) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn aionrs_structured_malformed_tool_call_error_is_provider_error() { + let error = AionrsAgentError::RepeatedMalformedToolCall { count: 3, limit: 3 }; + let send_error = aionrs_engine_error_to_send_error(&error); + + assert_eq!( + send_error.code(), + Some(aionui_api_types::AgentErrorCode::UserLlmProviderInvalidRequest) + ); + assert_eq!( + send_error.ownership(), + Some(aionui_api_types::AgentErrorOwnership::UserLlmProvider) + ); + assert_eq!(send_error.stream_error().retryable, Some(false)); + } + + #[test] + fn aionrs_provider_connection_error_is_user_llm_provider_error() { + let error = AionrsAgentError::Provider(ProviderError::Connection( + "Signable request error: failed to create canonical request".to_owned(), + )); + let send_error = aionrs_engine_error_to_send_error(&error); + + assert_eq!( + send_error.code(), + Some(aionui_api_types::AgentErrorCode::UserLlmProviderNetworkError) + ); + assert_eq!( + send_error.ownership(), + Some(aionui_api_types::AgentErrorOwnership::UserLlmProvider) + ); + assert_eq!(send_error.stream_error().retryable, Some(true)); + } + + #[test] + fn aionrs_api_connection_error_is_user_llm_provider_network_error() { + let error = AionrsAgentError::Provider(ProviderError::Connection("error decoding response body".to_owned())); + let send_error = aionrs_engine_error_to_send_error(&error); + + assert_eq!( + send_error.code(), + Some(aionui_api_types::AgentErrorCode::UserLlmProviderNetworkError) + ); + assert_eq!( + send_error.ownership(), + Some(aionui_api_types::AgentErrorOwnership::UserLlmProvider) + ); + assert_eq!(send_error.stream_error().retryable, Some(true)); + } + + #[test] + fn aionrs_provider_status_error_uses_status_instead_of_message_text() { + let error = AionrsAgentError::Provider(ProviderError::Api { + status: 401, + message: "credentials failed".to_owned(), + }); + let send_error = aionrs_engine_error_to_send_error(&error); + + assert_eq!( + send_error.code(), + Some(aionui_api_types::AgentErrorCode::UserLlmProviderAuthFailed) + ); + assert_eq!( + send_error.ownership(), + Some(aionui_api_types::AgentErrorOwnership::UserLlmProvider) + ); + assert_eq!(send_error.stream_error().retryable, Some(false)); + } + + #[test] + fn aionrs_context_too_long_is_provider_context_error() { + let error = AionrsAgentError::ContextTooLong { + input_tokens: 120_000, + limit: 100_000, + }; + let send_error = aionrs_engine_error_to_send_error(&error); + + assert_eq!( + send_error.code(), + Some(aionui_api_types::AgentErrorCode::UserLlmProviderContextTooLarge) + ); + assert_eq!( + send_error.ownership(), + Some(aionui_api_types::AgentErrorOwnership::UserLlmProvider) + ); + assert_eq!(send_error.stream_error().retryable, Some(false)); + } + + #[test] + fn aionrs_repeated_malformed_tool_call_is_user_llm_provider_error() { + let error = AionrsAgentError::RepeatedMalformedToolCall { count: 3, limit: 3 }; + let send_error = aionrs_engine_error_to_send_error(&error); + + assert_eq!( + send_error.code(), + Some(aionui_api_types::AgentErrorCode::UserLlmProviderInvalidRequest) + ); + assert_eq!( + send_error.ownership(), + Some(aionui_api_types::AgentErrorOwnership::UserLlmProvider) + ); + assert_eq!(send_error.stream_error().retryable, Some(false)); + } +} diff --git a/crates/aionui-ai-agent/src/manager/aionrs/mod.rs b/crates/aionui-ai-agent/src/manager/aionrs/mod.rs index aac95a86..3e775cda 100644 --- a/crates/aionui-ai-agent/src/manager/aionrs/mod.rs +++ b/crates/aionui-ai-agent/src/manager/aionrs/mod.rs @@ -1,3 +1,5 @@ +mod error; + pub mod agent; pub mod history_sanitize;