1use serde::{Deserialize, Serialize};
2
3#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
6pub struct RunId(pub String);
7
8#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
9pub struct CheckpointId(pub String);
10
11#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
12pub struct AgentId(pub String);
13
14impl RunId {
15 pub fn new() -> Self {
16 Self(uuid::Uuid::new_v4().to_string())
17 }
18}
19
20impl CheckpointId {
21 pub fn new() -> Self {
22 Self(uuid::Uuid::new_v4().to_string())
23 }
24}
25
26impl AgentId {
27 pub fn new() -> Self {
28 Self(uuid::Uuid::new_v4().to_string())
29 }
30}
31
32pub fn friendly_model_switch_reason(reason: &str) -> String {
33 let lower = reason.to_lowercase();
34 if lower.contains("ollama api error 404") && lower.contains("model") {
35 "modèle local indisponible".into()
36 } else if lower.contains("ollama") {
37 "provider local indisponible".into()
38 } else {
39 reason.to_string()
40 }
41}
42
43pub fn is_local_model_unavailable(reason: &str) -> bool {
44 matches!(
45 friendly_model_switch_reason(reason).as_str(),
46 "modèle local indisponible" | "provider local indisponible"
47 )
48}
49
50#[derive(Default)]
54pub struct ThinkStripper {
55 in_think: bool,
56 pending: String,
57 think_buf: String,
61}
62
63impl ThinkStripper {
64 pub fn new() -> Self {
65 Self::default()
66 }
67
68 pub fn feed(&mut self, delta: &str) -> String {
70 const OPEN: &str = "<think>";
71 const CLOSE: &str = "</think>";
72 self.pending.push_str(delta);
73 let mut out = String::new();
74 loop {
75 if !self.in_think {
76 if let Some(i) = self.pending.find(OPEN) {
77 out.push_str(&self.pending[..i]);
78 self.pending.replace_range(..i + OPEN.len(), "");
79 self.in_think = true;
80 self.think_buf.clear();
81 continue;
82 }
83 let keep = dangling_prefix(&self.pending, OPEN);
84 let emit_to = self.pending.len() - keep;
85 out.push_str(&self.pending[..emit_to]);
86 self.pending.replace_range(..emit_to, "");
87 break;
88 } else {
89 if let Some(i) = self.pending.find(CLOSE) {
90 self.pending.replace_range(..i + CLOSE.len(), "");
92 self.in_think = false;
93 self.think_buf.clear();
94 continue;
95 }
96 let keep = dangling_prefix(&self.pending, CLOSE);
97 let drop_to = self.pending.len() - keep;
98 self.think_buf.push_str(&self.pending[..drop_to]);
100 self.pending.replace_range(..drop_to, "");
101 break;
102 }
103 }
104 out
105 }
106
107 pub fn flush(&mut self) -> String {
110 let mut rest = std::mem::take(&mut self.pending);
111 if self.in_think {
112 let recovered = std::mem::take(&mut self.think_buf);
114 self.in_think = false;
115 format!("{}{}", recovered, rest)
116 } else {
117 self.think_buf.clear();
118 std::mem::take(&mut rest)
119 }
120 }
121}
122
123fn dangling_prefix(s: &str, tag: &str) -> usize {
126 let max = tag.len().saturating_sub(1).min(s.len());
127 for n in (1..=max).rev() {
128 if s.is_char_boundary(s.len() - n) && s[s.len() - n..] == tag[..n] {
129 return n;
130 }
131 }
132 0
133}
134
135#[derive(Debug, Clone, Serialize, Deserialize)]
138pub enum Block {
139 Text(String),
140 Json(serde_json::Value),
141 Image { data: Vec<u8>, mime: String },
142 Diff { file: String, patch: String },
143}
144
145#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
148pub enum RiskLevel {
149 ReadOnly,
150 Mutating,
151 Exec,
152 Destructive,
153 Network,
154}
155
156#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
157pub enum Decision {
158 Allow,
159 AskUser,
160 Deny,
161}
162
163#[derive(Debug, Clone, Serialize, Deserialize)]
166pub enum AgentStatus {
167 Idle,
168 Thinking,
169 Working,
170 WaitingForApproval,
171 Done,
172 Error,
173}
174
175#[derive(Debug, Clone, Serialize, Deserialize)]
178pub struct TokenUsage {
179 pub input: u64,
180 pub output: u64,
181}
182
183#[derive(Debug, Clone, Serialize, Deserialize)]
184pub enum StopReason {
185 EndTurn,
186 MaxTokens,
187 StopSequence(String),
188 ToolUse,
189 Refusal,
190 Error,
191}
192
193#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
196#[serde(rename_all = "lowercase")]
197pub enum AutonomyLevel {
198 Supervised,
199 Trusted,
200 Autonomous,
201}
202
203impl AutonomyLevel {
204 pub fn as_float(&self) -> f64 {
205 match self {
206 AutonomyLevel::Supervised => 0.0,
207 AutonomyLevel::Trusted => 0.5,
208 AutonomyLevel::Autonomous => 1.0,
209 }
210 }
211
212 pub fn from_float(f: f64) -> Self {
213 if f >= 0.75 {
214 AutonomyLevel::Autonomous
215 } else if f >= 0.25 {
216 AutonomyLevel::Trusted
217 } else {
218 AutonomyLevel::Supervised
219 }
220 }
221}
222
223#[derive(Debug, Clone, Serialize, Deserialize)]
226pub struct OutcomeSummary {
227 pub status: String,
228 pub diffs: Vec<FileDiff>,
229 pub cost_usd: f64,
230 pub tokens: TokenUsage,
231}
232
233#[derive(Debug, Clone, Serialize, Deserialize)]
234pub struct FileDiff {
235 pub file: String,
236 pub plus: u32,
237 pub minus: u32,
238}
239
240#[derive(Debug, Clone, Serialize, Deserialize)]
245#[serde(tag = "type")]
246pub enum Event {
247 RunStarted {
248 run: RunId,
249 task: String,
250 agent: String,
251 },
252 RouteSelected {
253 run: RunId,
254 chain: Vec<String>,
255 context_window: u64,
256 },
257 ModelSwitched {
258 run: RunId,
259 from: String,
260 to: String,
261 reason: String,
262 },
263 ThinkingDelta {
264 run: RunId,
265 text: String,
266 },
267 ReasoningDelta {
272 run: RunId,
273 text: String,
274 },
275 Message {
276 run: RunId,
277 role: String,
278 text: String,
279 },
280 ToolUseProposed {
281 run: RunId,
282 id: String,
283 name: String,
284 args: serde_json::Value,
285 risk: RiskLevel,
286 },
287 ApprovalRequested {
288 run: RunId,
289 id: String,
290 summary: String,
291 },
292 ApprovalResolved {
293 run: RunId,
294 id: String,
295 decision: Decision,
296 },
297 ToolUseStarted {
298 run: RunId,
299 id: String,
300 },
301 ToolOutput {
302 run: RunId,
303 id: String,
304 blocks: Vec<Block>,
305 },
306 DiffProposed {
307 run: RunId,
308 file: String,
309 patch: String,
310 plus: u32,
311 minus: u32,
312 },
313 DiffApplied {
314 run: RunId,
315 file: String,
316 },
317 TestResult {
318 run: RunId,
319 passed: u32,
320 failed: u32,
321 detail: String,
322 },
323 AgentSpawned {
324 run: RunId,
325 role: String,
326 model: String,
327 },
328 AgentStatus {
329 run: RunId,
330 role: String,
331 status: AgentStatus,
332 note: String,
333 },
334 CheckpointCreated {
335 run: RunId,
336 id: CheckpointId,
337 label: String,
338 },
339 SkillLearned {
340 run: RunId,
341 name: String,
342 },
343 CostUpdate {
344 run: RunId,
345 usd: f64,
346 },
347 TokenUsage {
348 run: RunId,
349 input: u64,
350 output: u64,
351 },
352 TokenUsageEstimated {
353 run: RunId,
354 input: u64,
355 output: u64,
356 reason: String,
357 },
358 AutonomyChanged {
359 run: RunId,
360 level: AutonomyLevel,
361 },
362 RunFinished {
363 run: RunId,
364 outcome: OutcomeSummary,
365 },
366 Error {
367 run: RunId,
368 message: String,
369 },
370 Compacted {
373 run: RunId,
374 before_chars: usize,
375 after_chars: usize,
376 handoff_path: Option<String>,
377 },
378}
379
380impl Event {
381 pub fn is_public(&self) -> bool {
388 !matches!(self, Self::ReasoningDelta { .. })
389 }
390}