agent-doc-flow 0.34.69

Pure flow vocabulary and outcome contracts for agent-doc
Documentation
use std::fmt;

#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct FlowId(pub String);

#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct CycleId(pub String);

#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct DocumentId(pub String);

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct ActorGeneration(pub u64);

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum FlowName {
    SessionCycle,
    RoutedReopen,
    Closeout,
    DocumentMutation,
    OperatorClear,
    OrchestrationBatch,
}

impl FlowName {
    pub const fn as_str(self) -> &'static str {
        match self {
            Self::SessionCycle => "session_cycle",
            Self::RoutedReopen => "routed_reopen",
            Self::Closeout => "closeout",
            Self::DocumentMutation => "document_mutation",
            Self::OperatorClear => "operator_clear",
            Self::OrchestrationBatch => "orchestration_batch",
        }
    }
}

impl fmt::Display for FlowName {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(self.as_str())
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum FlowStage {
    Preflight,
    Plan,
    Execute,
    PreWriteGuard,
    PatchbackParse,
    DocumentMutation,
    IpcSnapshotAdoption,
    SnapshotConvergence,
    PreCommitGuard,
    Commit,
    TerminalGuard,
    SessionCheck,
    ActorBinding,
    PromptReadyBarrier,
    DispatchAuthorization,
    DispatchSubmit,
    DispatchProof,
    QueueFreeze,
    ChildCloseout,
    OperatorGuard,
}

impl FlowStage {
    pub const fn as_str(self) -> &'static str {
        match self {
            Self::Preflight => "preflight",
            Self::Plan => "plan",
            Self::Execute => "execute",
            Self::PreWriteGuard => "pre_write_guard",
            Self::PatchbackParse => "patchback_parse",
            Self::DocumentMutation => "document_mutation",
            Self::IpcSnapshotAdoption => "ipc_snapshot_adoption",
            Self::SnapshotConvergence => "snapshot_convergence",
            Self::PreCommitGuard => "pre_commit_guard",
            Self::Commit => "commit",
            Self::TerminalGuard => "terminal_guard",
            Self::SessionCheck => "session_check",
            Self::ActorBinding => "actor_binding",
            Self::PromptReadyBarrier => "prompt_ready_barrier",
            Self::DispatchAuthorization => "dispatch_authorization",
            Self::DispatchSubmit => "dispatch_submit",
            Self::DispatchProof => "dispatch_proof",
            Self::QueueFreeze => "queue_freeze",
            Self::ChildCloseout => "child_closeout",
            Self::OperatorGuard => "operator_guard",
        }
    }
}

impl fmt::Display for FlowStage {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(self.as_str())
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum PromptOwnership {
    User,
    Assistant,
    System,
    Structural,
}

impl PromptOwnership {
    pub const fn as_str(self) -> &'static str {
        match self {
            Self::User => "user",
            Self::Assistant => "assistant",
            Self::System => "system",
            Self::Structural => "structural",
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum DocumentMutationKind {
    ExchangePatch,
    BacklogOp,
    IceboxPatch,
    FullContent,
    Repair,
}

impl DocumentMutationKind {
    pub const fn as_str(self) -> &'static str {
        match self {
            Self::ExchangePatch => "exchange_patch",
            Self::BacklogOp => "backlog_op",
            Self::IceboxPatch => "icebox_patch",
            Self::FullContent => "full_content",
            Self::Repair => "repair",
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum DispatchProof {
    AcceptedOnly,
    DispatchStarted,
    Consumed,
    TimedOut,
    Rejected,
}

impl DispatchProof {
    pub const fn as_str(self) -> &'static str {
        match self {
            Self::AcceptedOnly => "accepted_only",
            Self::DispatchStarted => "dispatch_started",
            Self::Consumed => "consumed",
            Self::TimedOut => "timed_out",
            Self::Rejected => "rejected",
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum FlowOutcome {
    Completed,
    Blocked,
    FailedClosed,
    ExternallyBlocked,
}

impl FlowOutcome {
    pub const fn as_str(self) -> &'static str {
        match self {
            Self::Completed => "completed",
            Self::Blocked => "blocked",
            Self::FailedClosed => "failed_closed",
            Self::ExternallyBlocked => "externally_blocked",
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FlowEvent {
    pub flow: FlowName,
    pub stage: FlowStage,
    pub outcome: FlowOutcome,
    pub reason: Option<String>,
}

impl FlowEvent {
    pub fn new(flow: FlowName, stage: FlowStage, outcome: FlowOutcome) -> Self {
        Self {
            flow,
            stage,
            outcome,
            reason: None,
        }
    }

    pub fn with_reason(mut self, reason: impl Into<String>) -> Self {
        self.reason = Some(reason.into());
        self
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SessionCycleStep {
    pub stage: FlowStage,
    pub outcome: FlowOutcome,
    pub reason: Option<String>,
}

impl SessionCycleStep {
    pub fn completed(stage: FlowStage) -> Self {
        Self {
            stage,
            outcome: FlowOutcome::Completed,
            reason: None,
        }
    }
}

pub fn flow_event(
    flow: FlowName,
    stage: FlowStage,
    outcome: FlowOutcome,
    reason: impl Into<Option<String>>,
) -> FlowEvent {
    let mut event = FlowEvent::new(flow, stage, outcome);
    if let Some(reason) = reason.into() {
        event = event.with_reason(reason);
    }
    event
}

pub fn session_cycle_event(
    stage: FlowStage,
    outcome: FlowOutcome,
    reason: impl Into<Option<String>>,
) -> FlowEvent {
    flow_event(FlowName::SessionCycle, stage, outcome, reason)
}

pub fn flow_event_log_message(file: &str, event: &FlowEvent) -> String {
    let mut message = format!(
        "flow_event file={} flow={} stage={} outcome={}",
        file,
        event.flow.as_str(),
        event.stage.as_str(),
        event.outcome.as_str()
    );
    if let Some(reason) = &event.reason {
        message.push_str(" reason=");
        message.push_str(&sanitize_field_value(reason));
    }
    message
}

fn sanitize_field_value(value: &str) -> String {
    value
        .chars()
        .map(|ch| {
            if ch.is_ascii_alphanumeric() || matches!(ch, '_' | '-' | '.' | ':' | '/') {
                ch
            } else {
                '_'
            }
        })
        .collect()
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn flow_event_log_message_is_field_parseable() {
        let event = FlowEvent::new(
            FlowName::RoutedReopen,
            FlowStage::PromptReadyBarrier,
            FlowOutcome::FailedClosed,
        )
        .with_reason("starting actor not ready");

        let message = flow_event_log_message("tasks/a.md", &event);

        assert!(message.contains("flow=routed_reopen"));
        assert!(message.contains("stage=prompt_ready_barrier"));
        assert!(message.contains("outcome=failed_closed"));
        assert!(message.contains("reason=starting_actor_not_ready"));
    }

    #[test]
    fn flow_event_sets_optional_reason() {
        let event = flow_event(
            FlowName::SessionCycle,
            FlowStage::Plan,
            FlowOutcome::Completed,
            Some("normal".to_string()),
        );

        assert_eq!(event.flow, FlowName::SessionCycle);
        assert_eq!(event.stage, FlowStage::Plan);
        assert_eq!(event.outcome, FlowOutcome::Completed);
        assert_eq!(event.reason.as_deref(), Some("normal"));
    }

    #[test]
    fn session_cycle_event_adapts_flow_name_stage_outcome_and_reason() {
        let event = session_cycle_event(
            FlowStage::Preflight,
            FlowOutcome::Blocked,
            Some("waiting".to_string()),
        );

        assert_eq!(event.flow, FlowName::SessionCycle);
        assert_eq!(event.stage, FlowStage::Preflight);
        assert_eq!(event.outcome, FlowOutcome::Blocked);
        assert_eq!(event.reason.as_deref(), Some("waiting"));
        assert_eq!(
            SessionCycleStep::completed(FlowStage::Plan).outcome,
            FlowOutcome::Completed
        );
    }
}