Skip to main content

aether_core/events/
turn_event.rs

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