Skip to main content

aether_core/events/
turn_event.rs

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