Skip to main content

agent_doc_flow/
types.rs

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}