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}