Skip to main content

aether_core/events/
turn_event.rs

1use llm::{ContentBlock, LlmCallPurpose, LlmError, 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 { error: String },
12}
13
14/// How a single LLM call ended.
15#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
16#[serde(tag = "status", rename_all = "snake_case")]
17pub enum LlmCallOutcome {
18    Completed {
19        stop_reason: Option<StopReason>,
20        usage: Option<TokenUsage>,
21    },
22    Failed {
23        error: String,
24        will_retry: bool,
25        #[serde(default, skip_serializing_if = "Option::is_none")]
26        http_status: Option<u16>,
27        #[serde(default, skip_serializing_if = "Option::is_none")]
28        provider_request_id: Option<String>,
29        #[serde(default, skip_serializing_if = "Option::is_none")]
30        provider_error_code: Option<String>,
31    },
32    Cancelled,
33}
34
35impl LlmCallOutcome {
36    pub fn failed(error: impl Into<String>, will_retry: bool) -> Self {
37        Self::Failed {
38            error: error.into(),
39            will_retry,
40            http_status: None,
41            provider_request_id: None,
42            provider_error_code: None,
43        }
44    }
45
46    pub fn from_llm_error(error: &LlmError, will_retry: bool) -> Self {
47        let Some(provider) = error.provider() else {
48            return Self::failed(error.to_string(), will_retry);
49        };
50        Self::Failed {
51            error: provider.to_string(),
52            will_retry,
53            http_status: provider.http_status,
54            provider_request_id: provider.request_id.clone(),
55            provider_error_code: provider.code.clone(),
56        }
57    }
58}
59
60/// A retry of a failed LLM call.
61#[derive(Debug, Clone, Copy, PartialEq, Eq)]
62pub struct RetryInfo {
63    pub attempt: u32,
64    pub max_attempts: u32,
65    pub delay_ms: u64,
66}
67
68/// Turn lifecycle events.
69///
70/// A turn spans from a user message to a terminal [`TurnEvent::Ended`]. Within a
71/// turn, each LLM call is bracketed by `LlmCallStarted`/`LlmCallEnded`; retries
72/// surface as an `LlmCallStarted` with `attempt > 0`. Note that the completion
73/// events for streamed message content
74/// ([`MessageEvent`](crate::events::MessageEvent) with `is_complete: true`) are
75/// emitted at turn completion, after the originating call's `LlmCallEnded`.
76#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
77#[serde(tag = "type", rename_all = "snake_case")]
78pub enum TurnEvent {
79    /// A user message began a turn. Messages queued while a turn is active are
80    /// folded into that turn and do not start a new one.
81    Started {
82        #[serde(default, skip_serializing_if = "Vec::is_empty")]
83        content: Vec<ContentBlock>,
84    },
85    /// A retry is waiting for its backoff delay before the request starts.
86    RetryScheduled { purpose: LlmCallPurpose, attempt: u32, max_attempts: u32, delay_ms: u64 },
87    /// An LLM request was issued.
88    LlmCallStarted {
89        purpose: LlmCallPurpose,
90        model: ModelIdentity,
91        display_name: String,
92        /// 0 for the initial call, incrementing per retry.
93        attempt: u32,
94        max_attempts: u32,
95    },
96    /// An LLM call reached a terminal state.
97    LlmCallEnded { purpose: LlmCallPurpose, outcome: LlmCallOutcome },
98    /// The agent is auto-continuing because the LLM stopped with a resumable
99    /// stop reason.
100    AutoContinue { attempt: u32, max_attempts: u32 },
101    /// The turn reached a terminal state.
102    Ended { outcome: TurnOutcome },
103}
104
105impl TurnEvent {
106    pub fn retry_info(&self) -> Option<RetryInfo> {
107        match self {
108            Self::RetryScheduled { attempt, max_attempts, delay_ms, .. } => {
109                Some(RetryInfo { attempt: *attempt, max_attempts: *max_attempts, delay_ms: *delay_ms })
110            }
111            _ => None,
112        }
113    }
114}