Skip to main content

aether_core/events/
turn_event.rs

1use llm::{ContentBlock, LlmCallPurpose, LlmError, MessageId, ModelIdentity, StopReason, TokenUsage};
2use schemars::JsonSchema;
3use serde::{Deserialize, Serialize};
4
5/// How a turn reached its terminal state.
6#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
7#[serde(tag = "status", rename_all = "snake_case")]
8pub enum TurnOutcome {
9    Completed,
10    Cancelled,
11    Failed { message_id: MessageId, error: String },
12}
13
14impl TurnOutcome {
15    pub fn failed(error: impl Into<String>) -> Self {
16        Self::Failed { message_id: MessageId::new(), error: error.into() }
17    }
18}
19
20/// How a single LLM call ended.
21#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
22#[serde(tag = "status", rename_all = "snake_case")]
23pub enum LlmCallOutcome {
24    Completed {
25        stop_reason: Option<StopReason>,
26        usage: Option<TokenUsage>,
27    },
28    Failed {
29        error: String,
30        will_retry: bool,
31        #[serde(default, skip_serializing_if = "Option::is_none")]
32        http_status: Option<u16>,
33        #[serde(default, skip_serializing_if = "Option::is_none")]
34        provider_request_id: Option<String>,
35        #[serde(default, skip_serializing_if = "Option::is_none")]
36        provider_error_code: Option<String>,
37    },
38    Cancelled,
39}
40
41impl LlmCallOutcome {
42    pub fn failed(error: impl Into<String>, will_retry: bool) -> Self {
43        Self::Failed {
44            error: error.into(),
45            will_retry,
46            http_status: None,
47            provider_request_id: None,
48            provider_error_code: None,
49        }
50    }
51
52    pub fn from_llm_error(error: &LlmError, will_retry: bool) -> Self {
53        let Some(provider) = error.provider() else {
54            return Self::failed(error.to_string(), will_retry);
55        };
56        Self::Failed {
57            error: provider.to_string(),
58            will_retry,
59            http_status: provider.http_status,
60            provider_request_id: provider.request_id.clone(),
61            provider_error_code: provider.code.clone(),
62        }
63    }
64}
65
66/// A retry of a failed LLM call.
67#[derive(Debug, Clone, Copy, PartialEq, Eq)]
68pub struct RetryInfo {
69    pub attempt: u32,
70    pub max_attempts: u32,
71    pub delay_ms: u64,
72}
73
74#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
75#[serde(tag = "type", rename_all = "snake_case")]
76pub enum TurnEvent {
77    Started {
78        #[serde(default, skip_serializing_if = "Vec::is_empty")]
79        content: Vec<ContentBlock>,
80    },
81    UserMessageInserted {
82        message_id: MessageId,
83    },
84    UserMessageDiscarded {
85        message_id: MessageId,
86    },
87    RetryScheduled {
88        purpose: LlmCallPurpose,
89        attempt: u32,
90        max_attempts: u32,
91        delay_ms: u64,
92    },
93    LlmCallStarted {
94        purpose: LlmCallPurpose,
95        model: ModelIdentity,
96        display_name: String,
97        /// 0 for the initial call, incrementing per retry.
98        attempt: u32,
99        max_attempts: u32,
100    },
101    LlmCallEnded {
102        purpose: LlmCallPurpose,
103        outcome: LlmCallOutcome,
104    },
105    AutoContinue {
106        attempt: u32,
107        max_attempts: u32,
108        message_id: MessageId,
109        content: Vec<ContentBlock>,
110    },
111    Ended {
112        outcome: TurnOutcome,
113    },
114}
115
116impl TurnEvent {
117    pub fn retry_info(&self) -> Option<RetryInfo> {
118        match self {
119            Self::RetryScheduled { attempt, max_attempts, delay_ms, .. } => {
120                Some(RetryInfo { attempt: *attempt, max_attempts: *max_attempts, delay_ms: *delay_ms })
121            }
122            _ => None,
123        }
124    }
125}