Skip to main content

telos_agent/agent/turn/
event.rs

1use crate::model::message::{ContentBlock, Message};
2use crate::model::provider::StopReason;
3use serde::Serialize;
4
5#[derive(Debug, Clone, Serialize)]
6pub enum TurnEvent {
7    TurnStarted {
8        session_id: String,
9        turn_id: u64,
10        user_input: String,
11    },
12    IterationStarted {
13        iteration: usize,
14        message_count: usize,
15    },
16    ProviderRequest {
17        iteration: usize,
18        message_count: usize,
19        tool_count: usize,
20    },
21    ProviderUsage {
22        input_tokens: usize,
23        output_tokens: usize,
24        total_tokens: Option<usize>,
25        prompt_cache_hit_tokens: Option<usize>,
26        prompt_cache_miss_tokens: Option<usize>,
27        reasoning_tokens: Option<usize>,
28        model: Option<String>,
29    },
30    AssistantDelta {
31        text: String,
32    },
33    ThinkingDelta {
34        text: String,
35    },
36    User(Message),
37    Assistant(Message),
38    ToolCall {
39        tool_call_id: String,
40        name: String,
41        detail: String,
42    },
43    ToolProgress {
44        tool_call_id: Option<String>,
45        name: String,
46        message: String,
47        data: Option<serde_json::Value>,
48    },
49    ToolCompleted {
50        tool_call_id: String,
51        name: String,
52        is_error: bool,
53        detail: Option<String>,
54    },
55    ToolResult(Message),
56    CompactionStarted {
57        reason: String,
58    },
59    CompactionCompleted {
60        reason: String,
61    },
62    TokenBudgetExceeded {
63        used_tokens: usize,
64        max_tokens: usize,
65    },
66    PolicyStarted {
67        point: String,
68        name: String,
69    },
70    PolicyCompleted {
71        point: String,
72        name: String,
73        feedback_count: usize,
74    },
75    ApprovalRequested {
76        tool_call_id: String,
77        name: String,
78        reason: String,
79    },
80    ApprovalResolved {
81        tool_call_id: String,
82        name: String,
83        decision: String,
84    },
85    ProviderRetry {
86        attempt: usize,
87        max_retries: usize,
88        delay_ms: u64,
89    },
90    TurnFailed {
91        error: String,
92    },
93    TurnFinished {
94        stop_reason: StopReason,
95        final_text: String,
96    },
97}
98
99#[derive(Debug, Clone, Serialize)]
100pub struct TurnResult {
101    pub events: Vec<TurnEvent>,
102    pub final_message: Message,
103    pub stop_reason: StopReason,
104}
105
106impl TurnEvent {
107    pub fn message(&self) -> Option<&Message> {
108        match self {
109            TurnEvent::User(message)
110            | TurnEvent::Assistant(message)
111            | TurnEvent::ToolResult(message) => Some(message),
112            _ => None,
113        }
114    }
115
116    pub fn text(&self) -> String {
117        match self {
118            TurnEvent::TurnStarted { session_id, turn_id, user_input } => {
119                format!("turn_started:{}#{}:{}", session_id, turn_id, user_input)
120            }
121            TurnEvent::IterationStarted { iteration, message_count } => {
122                format!("iteration_started:{} messages={}", iteration, message_count)
123            }
124            TurnEvent::ProviderRequest { iteration, message_count, tool_count } => format!(
125                "provider_request:{} messages={} tools={}",
126                iteration, message_count, tool_count
127            ),
128            TurnEvent::ProviderUsage {
129                input_tokens,
130                output_tokens,
131                total_tokens,
132                prompt_cache_hit_tokens,
133                prompt_cache_miss_tokens,
134                reasoning_tokens,
135                model,
136            } => {
137                let mut parts =
138                    vec![format!("input={input_tokens}"), format!("output={output_tokens}")];
139                if let Some(tokens) = total_tokens {
140                    parts.push(format!("total={tokens}"));
141                }
142                if let Some(tokens) = prompt_cache_hit_tokens {
143                    parts.push(format!("cache_hit={tokens}"));
144                }
145                if let Some(tokens) = prompt_cache_miss_tokens {
146                    parts.push(format!("cache_miss={tokens}"));
147                }
148                if let Some(tokens) = reasoning_tokens {
149                    parts.push(format!("reasoning={tokens}"));
150                }
151                if let Some(model) = model {
152                    parts.push(format!("model={model}"));
153                }
154                format!("provider_usage:{}", parts.join(" "))
155            }
156            TurnEvent::AssistantDelta { text } => format!("assistant_delta:{text}"),
157            TurnEvent::ThinkingDelta { text } => format!("thinking_delta:{text}"),
158            TurnEvent::ToolCall { tool_call_id, name, detail } => {
159                if detail.is_empty() {
160                    format!("tool_call:{}#{}", name, tool_call_id)
161                } else {
162                    format!("tool_call:{}#{} {}", name, tool_call_id, detail)
163                }
164            }
165            TurnEvent::ToolProgress { tool_call_id, name, message, .. } => format!(
166                "tool_progress:{}#{}:{}",
167                name,
168                tool_call_id.as_deref().unwrap_or("unknown"),
169                message
170            ),
171            TurnEvent::ToolCompleted { tool_call_id, name, is_error, detail } => {
172                if let Some(detail) = detail {
173                    format!(
174                        "tool_completed:{}#{} error={} {}",
175                        name, tool_call_id, is_error, detail
176                    )
177                } else {
178                    format!("tool_completed:{}#{} error={}", name, tool_call_id, is_error)
179                }
180            }
181            TurnEvent::CompactionStarted { reason } => format!("compaction_started:{reason}"),
182            TurnEvent::CompactionCompleted { reason } => format!("compaction_completed:{reason}"),
183            TurnEvent::TokenBudgetExceeded { used_tokens, max_tokens } => {
184                format!("token_budget_exceeded:{used_tokens}/{max_tokens}")
185            }
186            TurnEvent::PolicyStarted { point, name } => {
187                format!("policy_started:{point}:{name}")
188            }
189            TurnEvent::PolicyCompleted { point, name, feedback_count } => {
190                format!("policy_completed:{point}:{name}:{feedback_count}")
191            }
192            TurnEvent::ApprovalRequested { tool_call_id, name, reason } => {
193                format!("approval_requested:{name}#{tool_call_id}:{reason}")
194            }
195            TurnEvent::ApprovalResolved { tool_call_id, name, decision } => {
196                format!("approval_resolved:{name}#{tool_call_id}:{decision}")
197            }
198            TurnEvent::ProviderRetry { attempt, max_retries, delay_ms } => {
199                format!("provider_retry:{attempt}/{max_retries} delay={delay_ms}ms")
200            }
201            TurnEvent::TurnFailed { error } => format!("turn_failed:{error}"),
202            TurnEvent::TurnFinished { stop_reason, final_text } => {
203                format!("turn_finished:{stop_reason:?}:{final_text}")
204            }
205            _ => self
206                .message()
207                .map(|message| {
208                    message
209                        .blocks
210                        .iter()
211                        .map(|block| match block {
212                            ContentBlock::Text(text) => text.text.clone(),
213                            ContentBlock::Thinking(thinking) => {
214                                format!("thinking:{}", thinking.text)
215                            }
216                            ContentBlock::ToolCall(call) => {
217                                format!("tool_call:{}({})", call.name, call.arguments)
218                            }
219                            ContentBlock::ToolResult(result) => format!(
220                                "tool_result:{}#{} error={}",
221                                result.name, result.tool_call_id, result.is_error
222                            ),
223                        })
224                        .collect::<Vec<_>>()
225                        .join("\n")
226                })
227                .unwrap_or_default(),
228        }
229    }
230}
231
232#[cfg(test)]
233mod tests {
234    use super::*;
235
236    #[test]
237    fn turn_event_message_returns_some_for_message_variants() {
238        let message = Message::user("hi");
239        assert!(matches!(TurnEvent::User(message.clone()).message(), Some(m) if m == &message));
240        assert!(
241            TurnEvent::TurnStarted { session_id: "s".into(), turn_id: 1, user_input: "hi".into() }
242                .message()
243                .is_none()
244        );
245    }
246}