Skip to main content

semantic_operators/
flow.rs

1//! Ordinary Rust control flow with explicit uncertainty and retained traces.
2use crate::{
3    Answer, Answers, Error, ErrorKind, NamedQuestion, Operator, Provider, Request, State,
4    TypedQuestion, ask, operator,
5};
6
7/// One attempted request and its result, including failed attempts.
8#[derive(Clone, Debug)]
9pub struct Step {
10    /// Request whose Debug redacts its input and text.
11    pub request: Request,
12    /// Answers, validation/provider failure, or interrupted local execution.
13    pub result: crate::Result<Answers>,
14}
15/// A structured early exit; handling uncertainty is explicit.
16#[derive(Clone, Debug)]
17pub enum FlowExit {
18    /// An operator was read while undecided.
19    Undecided {
20        /// Operator identity.
21        name: String,
22        /// Full retained answer.
23        answer: Box<Answer>,
24    },
25    /// An input or provider failure, separate from uncertainty.
26    Error(Error),
27}
28impl From<Error> for FlowExit {
29    fn from(error: Error) -> Self {
30        Self::Error(error)
31    }
32}
33/// Results from one ask, read through typed operators.
34#[derive(Clone, Debug)]
35pub struct FlowAnswers {
36    answers: Answers,
37}
38impl FlowAnswers {
39    /// Inspect all answers without requiring decisions.
40    pub fn full(&self) -> &Answers {
41        &self.answers
42    }
43    /// Inspect a typed answer without stopping on uncertainty.
44    ///
45    /// # Errors
46    /// Returns an error if the operator is absent, its question does not match
47    /// the retained answer, or conversion to its output type fails.
48    pub fn peek<Q: TypedQuestion>(
49        &self,
50        operator: &Operator<Q>,
51    ) -> crate::Result<Answer<Q::Output>> {
52        operator.answer(&self.answers)
53    }
54    /// Read a typed value or return an uncertainty stop.
55    ///
56    /// # Errors
57    /// Returns [`FlowExit::Undecided`] when the answer has no decision. Missing
58    /// operators, incompatible answers, and conversion failures become
59    /// [`FlowExit::Error`].
60    pub fn require<Q: TypedQuestion>(&self, operator: &Operator<Q>) -> Result<Q::Output, FlowExit> {
61        let answer = self.peek(operator)?;
62        match answer.value() {
63            Some(value) => Ok(value.clone()),
64            None => Err(FlowExit::Undecided {
65                name: operator.name().into(),
66                answer: Box::new(
67                    self.answers
68                        .get(operator.name())
69                        .ok_or_else(|| {
70                            Error::new(ErrorKind::InvalidArgument, "operator not found")
71                        })?
72                        .revalidate(&operator.question().clone().into())?,
73                ),
74            }),
75        }
76    }
77}
78/// Completed application result or uncertainty stop, with a trace.
79#[derive(Clone, Debug)]
80pub struct FlowOutcome<T> {
81    /// Present for completion, even if T itself is an application Option::None.
82    pub value: Option<T>,
83    /// Operator that stopped the flow, if any.
84    pub stopped_at: Option<String>,
85    /// Full answer responsible for a stop.
86    pub stopped_answer: Option<Answer>,
87    /// Every flow request attempt, including failures before provider dispatch.
88    pub trace: Vec<Step>,
89}
90impl<T> FlowOutcome<T> {
91    /// Whether the application completed without an uncertainty stop.
92    pub fn decided(&self) -> bool {
93        self.stopped_at.is_none()
94    }
95    /// Number of flow request attempts, including failures before dispatch.
96    pub fn calls(&self) -> usize {
97        self.trace.len()
98    }
99}
100/// Failed flow retaining its complete trace.
101#[derive(Clone, Debug)]
102pub struct FlowFailure {
103    /// First latched failure.
104    pub error: Error,
105    /// Every flow request attempt through this context, including interruption.
106    pub trace: Vec<Step>,
107}
108impl std::fmt::Display for FlowFailure {
109    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
110        self.error.fmt(f)
111    }
112}
113impl std::error::Error for FlowFailure {
114    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
115        Some(&self.error)
116    }
117}
118/// Records calls and latches failures while application code controls sequencing.
119pub struct FlowContext<'a> {
120    provider: &'a dyn Provider,
121    trace: Vec<Step>,
122    failure: Option<Error>,
123}
124impl<'a> FlowContext<'a> {
125    /// Begin an empty trace using a borrowed provider.
126    pub fn new(provider: &'a dyn Provider) -> Self {
127        Self {
128            provider,
129            trace: Vec::new(),
130            failure: None,
131        }
132    }
133    /// Ask independent operators together about one state.
134    ///
135    /// # Errors
136    /// Returns [`FlowExit::Error`] for an empty batch, duplicate operator names,
137    /// a previously latched failure, or request/provider/response validation
138    /// failures. Successful undecided answers remain available for inspection.
139    pub async fn ask(
140        &mut self,
141        state: &State,
142        operators: &[NamedQuestion],
143    ) -> Result<FlowAnswers, FlowExit> {
144        let request = Request::new(state.clone(), operator::questions(operators)?)?;
145        self.ask_request(&request).await
146    }
147    /// Ask a request that may carry image assets.
148    ///
149    /// # Errors
150    /// Returns [`FlowExit::Error`] for a previously latched failure or any error
151    /// from the checked provider call. A new call failure is retained in the
152    /// trace and latched so later calls cannot silently proceed.
153    ///
154    /// Dropping this future after it starts retains the attempt and latches
155    /// [`ErrorKind::Runtime`] for interrupted local execution. This does not
156    /// establish whether remote work stopped or incurred charges. An unpolled
157    /// future records no attempt.
158    pub async fn ask_request(&mut self, request: &Request) -> Result<FlowAnswers, FlowExit> {
159        if let Some(error) = &self.failure {
160            return Err(FlowExit::Error(error.clone()));
161        }
162        // The exclusive context borrow prevents observing this provisional
163        // failure while the future is running. If it is dropped at the await,
164        // the retained failure prevents a partial execution appearing complete.
165        let interrupted = Error::new(
166            ErrorKind::Runtime,
167            "flow request ended before local completion",
168        );
169        let position = self.trace.len();
170        self.trace.push(Step {
171            request: request.clone(),
172            result: Err(interrupted.clone()),
173        });
174        self.failure = Some(interrupted);
175        let result = ask(self.provider, request).await;
176        self.trace[position].result = result.clone();
177        self.failure = result.as_ref().err().cloned();
178        match result {
179            Ok(answers) => Ok(FlowAnswers { answers }),
180            Err(error) => Err(FlowExit::Error(error)),
181        }
182    }
183    /// Finish. A caught provider error cannot become successful completion.
184    ///
185    /// # Errors
186    /// Returns [`FlowFailure`] with the trace when an earlier call failed,
187    /// a polled request future was dropped before completing, or
188    /// `result` contains [`FlowExit::Error`]. An uncertainty exit instead becomes
189    /// a successful [`FlowOutcome`] with `stopped_at` set.
190    pub fn finish<T>(self, result: Result<T, FlowExit>) -> Result<FlowOutcome<T>, FlowFailure> {
191        if let Some(error) = self.failure {
192            return Err(FlowFailure {
193                error,
194                trace: self.trace,
195            });
196        }
197        match result {
198            Ok(value) => Ok(FlowOutcome {
199                value: Some(value),
200                stopped_at: None,
201                stopped_answer: None,
202                trace: self.trace,
203            }),
204            Err(FlowExit::Undecided { name, answer }) => Ok(FlowOutcome {
205                value: None,
206                stopped_at: Some(name),
207                stopped_answer: Some(*answer),
208                trace: self.trace,
209            }),
210            Err(FlowExit::Error(error)) => Err(FlowFailure {
211                error,
212                trace: self.trace,
213            }),
214        }
215    }
216}