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}