1use std::path::PathBuf;
4
5use serde::{Deserialize, Serialize};
6use serde_json::Value;
7
8use crate::chat::{Message, SystemPrompt};
9use crate::coherence::CoherenceState;
10use crate::cycle::CycleBriefing;
11use crate::error_taxonomy::ErrorEnvelope;
12use crate::models::Usage;
13use crate::subagent::{MailboxMessage, SubAgentResult};
14use crate::turn::TurnOutcomeStatus;
15use crate::user_input::UserInputRequest;
16use zagens_tools::{ToolError, ToolResult};
17
18#[derive(Debug, Clone)]
20pub enum Event {
21 MessageStarted {
22 #[allow(dead_code)]
23 index: usize,
24 },
25 MessageDelta {
26 #[allow(dead_code)]
27 index: usize,
28 content: String,
29 },
30 MessageComplete {
31 #[allow(dead_code)]
32 index: usize,
33 },
34 ThinkingStarted {
35 #[allow(dead_code)]
36 index: usize,
37 },
38 ThinkingDelta {
39 #[allow(dead_code)]
40 index: usize,
41 content: String,
42 },
43 ThinkingComplete {
44 #[allow(dead_code)]
45 index: usize,
46 },
47 ToolCallStarted {
48 id: String,
49 name: String,
50 input: Value,
51 },
52 ToolCallProgress {
53 id: String,
54 output: String,
55 },
56 ToolCallComplete {
57 id: String,
58 name: String,
59 result: Result<ToolResult, ToolError>,
60 },
61 TurnStarted {
62 turn_id: String,
63 },
64 ModelRequestPrepared {
66 static_prefix_sha256: String,
67 full_prefix_sha256: String,
68 },
69 TurnComplete {
70 usage: Usage,
71 last_request_input_tokens: Option<u32>,
72 status: TurnOutcomeStatus,
73 error: Option<String>,
74 step_count: u32,
75 tool_names: Vec<String>,
76 end_reason: Option<String>,
77 },
78 CompactionStarted {
79 id: String,
80 auto: bool,
81 message: String,
82 },
83 CompactionCompleted {
84 id: String,
85 auto: bool,
86 message: String,
87 #[allow(dead_code)]
88 messages_before: Option<usize>,
89 #[allow(dead_code)]
90 messages_after: Option<usize>,
91 },
92 CompactionFailed {
93 id: String,
94 auto: bool,
95 message: String,
96 },
97 CycleAdvanced {
98 from: u32,
99 to: u32,
100 briefing: CycleBriefing,
101 },
102 #[allow(dead_code)]
103 CapacityDecision {
104 session_id: String,
105 turn_id: String,
106 h_hat: f64,
107 c_hat: f64,
108 slack: f64,
109 min_slack: f64,
110 violation_ratio: f64,
111 p_fail: f64,
112 risk_band: String,
113 action: String,
114 cooldown_blocked: bool,
115 reason: String,
116 },
117 #[allow(dead_code)]
118 CapacityIntervention {
119 session_id: String,
120 turn_id: String,
121 action: String,
122 before_prompt_tokens: usize,
123 after_prompt_tokens: usize,
124 compaction_size_reduction: usize,
125 replay_outcome: Option<String>,
126 replan_performed: bool,
127 },
128 #[allow(dead_code)]
129 CapacityMemoryPersistFailed {
130 session_id: String,
131 turn_id: String,
132 action: String,
133 error: String,
134 },
135 CoherenceState {
136 state: CoherenceState,
137 label: String,
138 description: String,
139 reason: String,
140 },
141 AgentSpawned {
142 id: String,
143 prompt: String,
144 },
145 AgentProgress {
146 id: String,
147 status: String,
148 },
149 AgentComplete {
150 id: String,
151 result: String,
152 },
153 AgentList {
154 agents: Vec<SubAgentResult>,
155 },
156 SubAgentMailbox {
157 seq: u64,
158 message: MailboxMessage,
159 },
160 Error {
161 envelope: ErrorEnvelope,
162 #[allow(dead_code)]
163 recoverable: bool,
164 },
165 Status {
166 message: String,
167 },
168 PauseEvents,
169 ResumeEvents,
170 ApprovalRequired {
171 id: String,
172 tool_name: String,
173 description: String,
174 approval_key: String,
175 },
176 UserInputRequired {
177 id: String,
178 request: UserInputRequest,
179 },
180 SessionUpdated {
181 messages: Vec<Message>,
182 system_prompt: Option<SystemPrompt>,
183 model: String,
184 workspace: PathBuf,
185 },
186 CraftVerdict {
188 agent_id: String,
189 agent_type: String,
190 task_id: Option<String>,
191 verdict: String,
192 summary: Option<String>,
193 items: Value,
194 },
195 CraftBoardUpdated {
197 task_id: String,
198 partition: String,
199 agent_id: String,
200 },
201 #[allow(dead_code)]
202 ElevationRequired {
203 tool_id: String,
204 tool_name: String,
205 command: Option<String>,
206 denial_reason: String,
207 blocked_network: bool,
208 blocked_write: bool,
209 },
210}
211
212#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
214pub struct TurnSummary {
215 pub step_count: u32,
216 pub tool_names: Vec<String>,
217 #[serde(skip_serializing_if = "Option::is_none")]
218 pub end_reason: Option<String>,
219}
220
221impl TurnSummary {
222 #[must_use]
223 pub fn new(step_count: u32, tool_names: Vec<String>, end_reason: Option<String>) -> Self {
224 Self {
225 step_count,
226 tool_names,
227 end_reason,
228 }
229 }
230
231 #[must_use]
232 pub fn to_value(&self) -> Value {
233 serde_json::to_value(self).unwrap_or(Value::Null)
234 }
235
236 pub fn log_turn_complete(
238 &self,
239 turn_id: &str,
240 status: TurnOutcomeStatus,
241 thread_id: Option<&str>,
242 ) {
243 match thread_id {
244 Some(thread_id) => tracing::info!(
245 thread_id = %thread_id,
246 turn_id = %turn_id,
247 step_count = self.step_count,
248 tool_count = self.tool_names.len(),
249 tools = ?self.tool_names,
250 end_reason = self.end_reason.as_deref(),
251 ?status,
252 "turn complete"
253 ),
254 None => tracing::info!(
255 turn_id = %turn_id,
256 step_count = self.step_count,
257 tool_count = self.tool_names.len(),
258 tools = ?self.tool_names,
259 end_reason = self.end_reason.as_deref(),
260 ?status,
261 "turn complete"
262 ),
263 }
264 }
265}
266
267impl Event {
268 #[must_use]
269 pub fn error(envelope: ErrorEnvelope) -> Self {
270 let recoverable = envelope.recoverable;
271 Event::Error {
272 envelope,
273 recoverable,
274 }
275 }
276
277 #[must_use]
278 pub fn status(message: impl Into<String>) -> Self {
279 Event::Status {
280 message: message.into(),
281 }
282 }
283}
284
285#[cfg(test)]
286mod tests {
287 use super::*;
288 use serde_json::json;
289
290 #[test]
291 fn turn_summary_serializes_for_runtime_payload() {
292 let summary = TurnSummary::new(
293 2,
294 vec!["read_file".to_string()],
295 Some("completed".to_string()),
296 );
297 assert_eq!(
298 summary.to_value(),
299 json!({
300 "step_count": 2,
301 "tool_names": ["read_file"],
302 "end_reason": "completed",
303 })
304 );
305 }
306
307 #[test]
308 fn turn_summary_omits_null_end_reason() {
309 let summary = TurnSummary::new(0, vec![], None);
310 let value = summary.to_value();
311 assert_eq!(value.get("step_count").and_then(|v| v.as_u64()), Some(0));
312 assert!(value.get("end_reason").is_none());
313 }
314}