1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
use crate::llm_error::LlmErrorKind;
/// Structured provider error emitted inside an accepted response stream.
///
/// Providers should preserve the wire error code and HTTP status when they are
/// available. Runtime retry classification uses those fields before falling
/// back to the human-readable message for legacy drivers.
///
/// Hosts match on the fields, never on the display text. An error envelope a
/// gateway sends inside a `200` stream arrives the same way:
///
/// ```
/// use everruns_contracts::driver_registry::LlmStreamError;
/// use everruns_contracts::LlmErrorKind;
///
/// // What `{"error":{"message":"upstream died","code":502}}` becomes.
/// let error = LlmStreamError::provider(None::<String>, Some(502), "upstream died");
/// assert_eq!(error.status, Some(502));
/// assert_eq!(error.message, "upstream died");
/// assert!(matches!(error.kind(), LlmErrorKind::Unavailable));
///
/// // Ending a turn on it keeps the status for `AgentLoopError::http_status`.
/// assert_eq!(error.into_agent_error().http_status(), Some(502));
/// ```
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LlmStreamError {
/// Stable machine-readable provider error code, when supplied.
pub code: Option<String>,
/// HTTP status associated with the stream error, when supplied.
pub status: Option<u16>,
/// Human-readable diagnostic text.
pub message: String,
}
impl LlmStreamError {
pub fn new(message: impl Into<String>) -> Self {
Self {
code: None,
status: None,
message: message.into(),
}
}
/// Build a stream error while preserving provider-supplied structure.
pub fn provider(
code: Option<impl Into<String>>,
status: Option<u16>,
message: impl Into<String>,
) -> Self {
Self {
code: code.map(Into::into),
status,
message: message.into(),
}
}
/// Map the preserved structure to Everruns' semantic provider error kind.
pub fn kind(&self) -> LlmErrorKind {
if let Some(code) = self.code.as_deref()
&& let Some(kind) = LlmErrorKind::from_provider_code(code)
{
return kind;
}
if let Some(status) = self.status {
return LlmErrorKind::from_provider_status(status, &self.message);
}
LlmErrorKind::from_error_text(&self.message)
}
}
impl LlmStreamError {
/// The classified call error this stream error ends a turn with, keeping
/// the provider's status and code.
pub fn into_agent_error(self) -> crate::error::AgentLoopError {
let mut error = crate::error::LlmError::new(self.kind(), self.to_string());
if let Some(status) = self.status {
error = error.with_status(status);
}
if let Some(code) = self.code {
error = error.with_code(code);
}
crate::error::AgentLoopError::Llm(error)
}
}
impl std::error::Error for LlmStreamError {}
impl std::fmt::Display for LlmStreamError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match (&self.code, self.status) {
(Some(code), Some(status)) => write!(f, "{code} ({status}): {}", self.message),
(Some(code), None) => write!(f, "{code}: {}", self.message),
(None, Some(status)) => write!(f, "({status}): {}", self.message),
(None, None) => f.write_str(&self.message),
}
}
}
impl From<String> for LlmStreamError {
fn from(message: String) -> Self {
Self::new(message)
}
}
impl From<&str> for LlmStreamError {
fn from(message: &str) -> Self {
Self::new(message)
}
}