1use std::fmt;
2
3#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
4pub struct FlowId(pub String);
5
6#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
7pub struct CycleId(pub String);
8
9#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
10pub struct DocumentId(pub String);
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
13pub struct ActorGeneration(pub u64);
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
16pub enum FlowName {
17 SessionCycle,
18 RoutedReopen,
19 Closeout,
20 DocumentMutation,
21 OperatorClear,
22 OrchestrationBatch,
23}
24
25impl FlowName {
26 pub const fn as_str(self) -> &'static str {
27 match self {
28 Self::SessionCycle => "session_cycle",
29 Self::RoutedReopen => "routed_reopen",
30 Self::Closeout => "closeout",
31 Self::DocumentMutation => "document_mutation",
32 Self::OperatorClear => "operator_clear",
33 Self::OrchestrationBatch => "orchestration_batch",
34 }
35 }
36}
37
38impl fmt::Display for FlowName {
39 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
40 f.write_str(self.as_str())
41 }
42}
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
45pub enum FlowStage {
46 Preflight,
47 Plan,
48 Execute,
49 PreWriteGuard,
50 PatchbackParse,
51 DocumentMutation,
52 IpcSnapshotAdoption,
53 SnapshotConvergence,
54 PreCommitGuard,
55 Commit,
56 TerminalGuard,
57 SessionCheck,
58 ActorBinding,
59 PromptReadyBarrier,
60 DispatchAuthorization,
61 DispatchSubmit,
62 DispatchProof,
63 QueueFreeze,
64 ChildCloseout,
65 OperatorGuard,
66}
67
68impl FlowStage {
69 pub const fn as_str(self) -> &'static str {
70 match self {
71 Self::Preflight => "preflight",
72 Self::Plan => "plan",
73 Self::Execute => "execute",
74 Self::PreWriteGuard => "pre_write_guard",
75 Self::PatchbackParse => "patchback_parse",
76 Self::DocumentMutation => "document_mutation",
77 Self::IpcSnapshotAdoption => "ipc_snapshot_adoption",
78 Self::SnapshotConvergence => "snapshot_convergence",
79 Self::PreCommitGuard => "pre_commit_guard",
80 Self::Commit => "commit",
81 Self::TerminalGuard => "terminal_guard",
82 Self::SessionCheck => "session_check",
83 Self::ActorBinding => "actor_binding",
84 Self::PromptReadyBarrier => "prompt_ready_barrier",
85 Self::DispatchAuthorization => "dispatch_authorization",
86 Self::DispatchSubmit => "dispatch_submit",
87 Self::DispatchProof => "dispatch_proof",
88 Self::QueueFreeze => "queue_freeze",
89 Self::ChildCloseout => "child_closeout",
90 Self::OperatorGuard => "operator_guard",
91 }
92 }
93}
94
95impl fmt::Display for FlowStage {
96 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
97 f.write_str(self.as_str())
98 }
99}
100
101#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
102pub enum PromptOwnership {
103 User,
104 Assistant,
105 System,
106 Structural,
107}
108
109impl PromptOwnership {
110 pub const fn as_str(self) -> &'static str {
111 match self {
112 Self::User => "user",
113 Self::Assistant => "assistant",
114 Self::System => "system",
115 Self::Structural => "structural",
116 }
117 }
118}
119
120#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
121pub enum DocumentMutationKind {
122 ExchangePatch,
123 BacklogOp,
124 IceboxPatch,
125 FullContent,
126 Repair,
127}
128
129impl DocumentMutationKind {
130 pub const fn as_str(self) -> &'static str {
131 match self {
132 Self::ExchangePatch => "exchange_patch",
133 Self::BacklogOp => "backlog_op",
134 Self::IceboxPatch => "icebox_patch",
135 Self::FullContent => "full_content",
136 Self::Repair => "repair",
137 }
138 }
139}
140
141#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
142pub enum DispatchProof {
143 AcceptedOnly,
144 DispatchStarted,
145 Consumed,
146 TimedOut,
147 Rejected,
148}
149
150impl DispatchProof {
151 pub const fn as_str(self) -> &'static str {
152 match self {
153 Self::AcceptedOnly => "accepted_only",
154 Self::DispatchStarted => "dispatch_started",
155 Self::Consumed => "consumed",
156 Self::TimedOut => "timed_out",
157 Self::Rejected => "rejected",
158 }
159 }
160}
161
162#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
163pub enum FlowOutcome {
164 Completed,
165 Blocked,
166 FailedClosed,
167 ExternallyBlocked,
168}
169
170impl FlowOutcome {
171 pub const fn as_str(self) -> &'static str {
172 match self {
173 Self::Completed => "completed",
174 Self::Blocked => "blocked",
175 Self::FailedClosed => "failed_closed",
176 Self::ExternallyBlocked => "externally_blocked",
177 }
178 }
179}
180
181#[derive(Debug, Clone, PartialEq, Eq)]
182pub struct FlowEvent {
183 pub flow: FlowName,
184 pub stage: FlowStage,
185 pub outcome: FlowOutcome,
186 pub reason: Option<String>,
187}
188
189impl FlowEvent {
190 pub fn new(flow: FlowName, stage: FlowStage, outcome: FlowOutcome) -> Self {
191 Self {
192 flow,
193 stage,
194 outcome,
195 reason: None,
196 }
197 }
198
199 pub fn with_reason(mut self, reason: impl Into<String>) -> Self {
200 self.reason = Some(reason.into());
201 self
202 }
203}
204
205#[derive(Debug, Clone, PartialEq, Eq)]
206pub struct SessionCycleStep {
207 pub stage: FlowStage,
208 pub outcome: FlowOutcome,
209 pub reason: Option<String>,
210}
211
212impl SessionCycleStep {
213 pub fn completed(stage: FlowStage) -> Self {
214 Self {
215 stage,
216 outcome: FlowOutcome::Completed,
217 reason: None,
218 }
219 }
220}
221
222pub fn flow_event(
223 flow: FlowName,
224 stage: FlowStage,
225 outcome: FlowOutcome,
226 reason: impl Into<Option<String>>,
227) -> FlowEvent {
228 let mut event = FlowEvent::new(flow, stage, outcome);
229 if let Some(reason) = reason.into() {
230 event = event.with_reason(reason);
231 }
232 event
233}
234
235pub fn session_cycle_event(
236 stage: FlowStage,
237 outcome: FlowOutcome,
238 reason: impl Into<Option<String>>,
239) -> FlowEvent {
240 flow_event(FlowName::SessionCycle, stage, outcome, reason)
241}
242
243pub fn flow_event_log_message(file: &str, event: &FlowEvent) -> String {
244 let mut message = format!(
245 "flow_event file={} flow={} stage={} outcome={}",
246 file,
247 event.flow.as_str(),
248 event.stage.as_str(),
249 event.outcome.as_str()
250 );
251 if let Some(reason) = &event.reason {
252 message.push_str(" reason=");
253 message.push_str(&sanitize_field_value(reason));
254 }
255 message
256}
257
258fn sanitize_field_value(value: &str) -> String {
259 value
260 .chars()
261 .map(|ch| {
262 if ch.is_ascii_alphanumeric() || matches!(ch, '_' | '-' | '.' | ':' | '/') {
263 ch
264 } else {
265 '_'
266 }
267 })
268 .collect()
269}
270
271#[cfg(test)]
272mod tests {
273 use super::*;
274
275 #[test]
276 fn flow_event_log_message_is_field_parseable() {
277 let event = FlowEvent::new(
278 FlowName::RoutedReopen,
279 FlowStage::PromptReadyBarrier,
280 FlowOutcome::FailedClosed,
281 )
282 .with_reason("starting actor not ready");
283
284 let message = flow_event_log_message("tasks/a.md", &event);
285
286 assert!(message.contains("flow=routed_reopen"));
287 assert!(message.contains("stage=prompt_ready_barrier"));
288 assert!(message.contains("outcome=failed_closed"));
289 assert!(message.contains("reason=starting_actor_not_ready"));
290 }
291
292 #[test]
293 fn flow_event_sets_optional_reason() {
294 let event = flow_event(
295 FlowName::SessionCycle,
296 FlowStage::Plan,
297 FlowOutcome::Completed,
298 Some("normal".to_string()),
299 );
300
301 assert_eq!(event.flow, FlowName::SessionCycle);
302 assert_eq!(event.stage, FlowStage::Plan);
303 assert_eq!(event.outcome, FlowOutcome::Completed);
304 assert_eq!(event.reason.as_deref(), Some("normal"));
305 }
306
307 #[test]
308 fn session_cycle_event_adapts_flow_name_stage_outcome_and_reason() {
309 let event = session_cycle_event(
310 FlowStage::Preflight,
311 FlowOutcome::Blocked,
312 Some("waiting".to_string()),
313 );
314
315 assert_eq!(event.flow, FlowName::SessionCycle);
316 assert_eq!(event.stage, FlowStage::Preflight);
317 assert_eq!(event.outcome, FlowOutcome::Blocked);
318 assert_eq!(event.reason.as_deref(), Some("waiting"));
319 assert_eq!(
320 SessionCycleStep::completed(FlowStage::Plan).outcome,
321 FlowOutcome::Completed
322 );
323 }
324}