Skip to main content

codetether_agent/session/
events.rs

1//! Event and result types emitted by session prompt methods.
2//!
3//! [`SessionResult`] is the terminal value returned from a successful
4//! prompt; [`SessionEvent`] is streamed to a UI channel for real-time
5//! feedback (tool calls, partial text, usage, compaction, etc.).
6//!
7//! # Ephemeral vs durable events
8//!
9//! [`SessionEvent`] is `#[non_exhaustive]` and carries two classes of
10//! payload:
11//!
12//! - **Ephemeral** — safe to drop under load. Used by the TUI for status,
13//!   spinners, and token badges. Examples: [`SessionEvent::Thinking`],
14//!   [`SessionEvent::TextChunk`], [`SessionEvent::RlmProgress`],
15//!   [`SessionEvent::TokenEstimate`].
16//! - **Durable** — must reach the JSONL flywheel for cost post-mortems
17//!   and trace-driven tuning. Examples: [`SessionEvent::TokenUsage`],
18//!   [`SessionEvent::RlmComplete`], [`SessionEvent::CompactionCompleted`],
19//!   [`SessionEvent::CompactionFailed`], [`SessionEvent::ContextTruncated`].
20//!
21//! [`SessionEvent::is_durable`] lets consumers route accordingly. The
22//! unified [`SessionBus`](crate::session::SessionBus) uses this to
23//! dispatch ephemeral events via a lossy `tokio::sync::broadcast` channel
24//! while forwarding durable events through a write-ahead
25//! [`DurableSink`](crate::session::DurableSink).
26
27use serde::{Deserialize, Serialize};
28
29use super::event_compaction::{
30    CompactionFailure, CompactionOutcome, CompactionStart, ContextTruncation,
31};
32use super::event_rlm::{RlmCompletion, RlmProgressEvent, RlmSubcallFallback};
33use super::event_token::{TokenDelta, TokenEstimate};
34use super::types::Session;
35
36/// Result returned from [`Session::prompt`](crate::session::Session::prompt)
37/// and friends.
38#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct SessionResult {
40    /// Final assistant text answer (trimmed).
41    pub text: String,
42    /// UUID of the session that produced the answer.
43    pub session_id: String,
44}
45
46/// Events emitted during session processing for real-time UI updates and
47/// durable telemetry.
48///
49/// # Stability
50///
51/// This enum is `#[non_exhaustive]`. `match` expressions must include a
52/// wildcard arm; new variants may be added without breaking consumers.
53///
54/// # Examples
55///
56/// ```rust
57/// use codetether_agent::session::SessionEvent;
58///
59/// let ev = SessionEvent::Thinking;
60/// assert!(!ev.is_durable());
61/// assert!(matches!(ev, SessionEvent::Thinking));
62/// ```
63#[derive(Debug, Clone)]
64#[non_exhaustive]
65pub enum SessionEvent {
66    /// The agent is thinking / waiting on the model.
67    Thinking,
68    /// A tool call has started.
69    ToolCallStart {
70        /// Tool name.
71        name: String,
72        /// Tool arguments (JSON-encoded).
73        arguments: String,
74    },
75    /// A tool call has completed.
76    ToolCallComplete {
77        /// Tool name.
78        name: String,
79        /// Rendered tool output.
80        output: String,
81        /// Whether the tool reported success.
82        success: bool,
83        /// End-to-end execution duration in milliseconds.
84        duration_ms: u64,
85    },
86    /// Partial assistant text output for streaming UIs.
87    TextChunk(String),
88    /// Final (per-step) assistant text output.
89    TextComplete(String),
90    /// Model thinking/reasoning output (for reasoning-capable models).
91    ThinkingComplete(String),
92    /// Token usage and timing for one LLM round-trip (legacy aggregate).
93    ///
94    /// Prefer [`SessionEvent::TokenUsage`] for new code — it carries a
95    /// [`TokenSource`](crate::session::TokenSource) so RLM sub-calls and
96    /// tool-embedded LLM calls are attributed separately.
97    UsageReport {
98        /// Prompt tokens consumed.
99        prompt_tokens: usize,
100        /// Completion tokens produced.
101        completion_tokens: usize,
102        /// Round-trip duration in milliseconds.
103        duration_ms: u64,
104        /// Model ID that served the request.
105        model: String,
106    },
107    /// Updated session state so the caller can sync its in-memory copy.
108    SessionSync(Box<Session>),
109    /// Processing is complete.
110    Done,
111    /// An error occurred during processing.
112    Error(String),
113    /// Pre-flight estimate of the next request's token footprint.
114    /// Ephemeral.
115    TokenEstimate(TokenEstimate),
116    /// Observed token consumption for one LLM round-trip, attributed by
117    /// [`TokenSource`](crate::session::TokenSource). Durable.
118    TokenUsage(TokenDelta),
119    /// Per-iteration progress tick from an in-flight RLM loop. Ephemeral.
120    RlmProgress(RlmProgressEvent),
121    /// Terminal record for an RLM invocation. Durable.
122    RlmComplete(RlmCompletion),
123    /// A context-compaction pass has begun. Durable.
124    CompactionStarted(CompactionStart),
125    /// A context-compaction pass has finished successfully. Durable.
126    CompactionCompleted(CompactionOutcome),
127    /// Every compaction strategy failed to fit under budget. Durable.
128    CompactionFailed(CompactionFailure),
129    /// The terminal truncation fallback dropped part of the transcript.
130    /// Durable; emitted in addition to [`SessionEvent::CompactionCompleted`]
131    /// when the final strategy is
132    /// [`FallbackStrategy::Truncate`](crate::session::FallbackStrategy::Truncate).
133    ContextTruncated(ContextTruncation),
134    /// A configured `subcall_model` could not be resolved so the router
135    /// fell back to the root model. Durable — this is a cost signal.
136    RlmSubcallFallback(RlmSubcallFallback),
137}
138
139impl SessionEvent {
140    /// Returns `true` if this variant carries data that must reach the
141    /// durable sink (see the module docs for the full split).
142    ///
143    /// # Examples
144    ///
145    /// ```rust
146    /// use codetether_agent::session::{SessionEvent, TokenDelta, TokenSource};
147    ///
148    /// let delta = TokenDelta {
149    ///     source: TokenSource::Root,
150    ///     model: "m".into(),
151    ///     prompt_tokens: 1, completion_tokens: 1, duration_ms: 0,
152    /// };
153    /// assert!(SessionEvent::TokenUsage(delta).is_durable());
154    /// assert!(!SessionEvent::Thinking.is_durable());
155    /// assert!(!SessionEvent::TextChunk("x".into()).is_durable());
156    /// ```
157    pub fn is_durable(&self) -> bool {
158        matches!(
159            self,
160            Self::TokenUsage(_)
161                | Self::RlmComplete(_)
162                | Self::CompactionStarted(_)
163                | Self::CompactionCompleted(_)
164                | Self::CompactionFailed(_)
165                | Self::ContextTruncated(_)
166                | Self::RlmSubcallFallback(_)
167        )
168    }
169}