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/// Turn lifecycle events.
75///
76/// A turn spans from a user message to a terminal [`TurnEvent::Ended`]. Within a
77/// turn, each LLM call is bracketed by `LlmCallStarted`/`LlmCallEnded`; retries
78/// surface as an `LlmCallStarted` with `attempt > 0`. Note that the completion
79/// events for streamed message content
80/// ([`MessageEvent`](crate::events::MessageEvent) with `is_complete: true`) are
81/// emitted at turn completion, after the originating call's `LlmCallEnded`.
82#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
83#[serde(tag = "type", rename_all = "snake_case")]
84pub enum TurnEvent {
85    /// A user message began a turn. Messages queued while a turn is active are
86    /// folded into that turn and do not start a new one.
87    Started {
88        #[serde(default, skip_serializing_if = "Vec::is_empty")]
89        content: Vec<ContentBlock>,
90    },
91    /// A retry is waiting for its backoff delay before the request starts.
92    RetryScheduled { purpose: LlmCallPurpose, attempt: u32, max_attempts: u32, delay_ms: u64 },
93    /// An LLM request was issued.
94    LlmCallStarted {
95        purpose: LlmCallPurpose,
96        model: ModelIdentity,
97        display_name: String,
98        /// 0 for the initial call, incrementing per retry.
99        attempt: u32,
100        max_attempts: u32,
101    },
102    /// An LLM call reached a terminal state.
103    LlmCallEnded { purpose: LlmCallPurpose, outcome: LlmCallOutcome },
104    /// The agent is auto-continuing because the LLM stopped with a resumable
105    /// stop reason.
106    AutoContinue { attempt: u32, max_attempts: u32, message_id: MessageId, content: Vec<ContentBlock> },
107    /// The turn reached a terminal state.
108    Ended { outcome: TurnOutcome },
109}
110
111impl TurnEvent {
112    pub fn retry_info(&self) -> Option<RetryInfo> {
113        match self {
114            Self::RetryScheduled { attempt, max_attempts, delay_ms, .. } => {
115                Some(RetryInfo { attempt: *attempt, max_attempts: *max_attempts, delay_ms: *delay_ms })
116            }
117            _ => None,
118        }
119    }
120}