1mod journal;
15mod nodes;
16mod replay;
17mod value;
18
19use std::collections::BTreeMap;
20
21use openquiver_acp::{AcpError, Agents};
22use openquiver_compile::{CompiledPlan, Source};
23use openquiver_graph::{NodeName, PinRef};
24use openquiver_nodes::Recovery;
25use openquiver_store::{RunId, RunStatus, Store, StoreError};
26
27use journal::{Entered, Exited, Record, Started};
28use nodes::Effects;
29use replay::Frontier;
30
31pub use journal::JournalError;
32pub use value::RunValue;
33
34#[derive(Debug, Clone, PartialEq, Eq)]
36pub enum Progress {
37 Ran(NodeName),
38 Done,
40}
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
49pub enum InDoubt {
50 Ask,
52 Retry,
54}
55
56#[derive(Debug, thiserror::Error)]
57pub enum RuntimeError {
58 #[error("the journal could not be reached: {0}")]
59 Store(#[from] StoreError),
60 #[error(transparent)]
61 Agent(#[from] AcpError),
62 #[error(transparent)]
63 Journal(#[from] JournalError),
64 #[error("a journal entry could not be written: {0}")]
65 Unwritable(#[from] serde_json::Error),
66 #[error("this workflow has no starting event, so there is nothing to run")]
67 NoEntry,
68 #[error("this workflow has {0} starting events, and a run begins at one")]
69 ManyEntries(usize),
70 #[error("run `{0}` has already finished")]
71 AlreadyFinished(String),
72 #[error("this journal cannot be replayed: {0}")]
73 BadJournal(&'static str),
74 #[error(
75 "this run was started on a different version of this workflow; \
76 the graph has been edited since"
77 )]
78 DifferentPlan,
79 #[error("this journal mentions `{0}`, which is not a node in this workflow")]
80 NotInPlan(String),
81 #[error("there is no way to run a `{0}` node")]
82 NoSuchNodeType(String),
83 #[error("`{node}` needs `{pin}`, but `{from}` has not run and cannot supply it")]
84 NotYetRun {
85 node: String,
86 pin: String,
87 from: String,
88 },
89 #[error("`{node}` needs `{pin}`, and nothing supplies it")]
90 NoInput { node: String, pin: String },
91 #[error("`{node}` expects {expected} on `{pin}`")]
92 WrongValue {
93 node: String,
94 pin: String,
95 expected: &'static str,
96 },
97 #[error(
98 "`{node}` was entered and never left, so whether it finished is unknown; \
99 resume again saying whether to run it"
100 )]
101 InDoubt { node: String },
102}
103
104impl RuntimeError {
105 fn is_permanent(&self) -> bool {
116 match self {
117 RuntimeError::Store(_) | RuntimeError::Agent(_) => false,
120 RuntimeError::Journal(_)
121 | RuntimeError::Unwritable(_)
122 | RuntimeError::NoEntry
123 | RuntimeError::ManyEntries(_)
124 | RuntimeError::AlreadyFinished(_)
125 | RuntimeError::BadJournal(_)
126 | RuntimeError::DifferentPlan
127 | RuntimeError::NotInPlan(_)
128 | RuntimeError::NoSuchNodeType(_)
129 | RuntimeError::NotYetRun { .. }
130 | RuntimeError::NoInput { .. }
131 | RuntimeError::WrongValue { .. } => true,
132 RuntimeError::InDoubt { .. } => true,
135 }
136 }
137}
138
139pub struct Runtime<'a> {
141 store: &'a Store,
142 plan: &'a CompiledPlan,
143 run: RunId,
144 effects: Effects<'a>,
145 values: BTreeMap<PinRef, RunValue>,
146 next: Option<NodeName>,
147 finished: bool,
148}
149
150impl<'a> Runtime<'a> {
151 pub async fn start(
153 store: &'a Store,
154 plan: &'a CompiledPlan,
155 agents: &'a dyn Agents,
156 workflow: &str,
157 ) -> Result<Self, RuntimeError> {
158 let entry = single_entry(plan)?;
159 let run = store.start_run(workflow).await?;
160
161 let runtime = Self {
162 store,
163 plan,
164 run,
165 effects: Effects {
166 agents,
167 sessions: BTreeMap::new(),
168 },
169 values: BTreeMap::new(),
170 next: Some(entry),
171 finished: false,
172 };
173 runtime
174 .append(Record::Started(Started {
175 plan: plan.fingerprint(),
176 }))
177 .await?;
178 Ok(runtime)
179 }
180
181 pub async fn resume(
188 store: &'a Store,
189 plan: &'a CompiledPlan,
190 agents: &'a dyn Agents,
191 run: RunId,
192 in_doubt: InDoubt,
193 ) -> Result<Self, RuntimeError> {
194 if !store.status(&run).await?.resumable() {
195 return Err(RuntimeError::AlreadyFinished(run.as_str().to_owned()));
196 }
197
198 let entry = single_entry(plan)?;
199 let events = store.events(&run).await?;
200 let replayed = replay::replay(&events, plan, &entry)?;
201
202 let next = match replayed.frontier {
203 Frontier::End => None,
204 Frontier::At(node) => Some(node),
205 Frontier::InDoubt(node) => {
206 let recovery = plan
207 .node(node.as_str())
208 .ok_or_else(|| RuntimeError::NotInPlan(node.as_str().to_owned()))?
209 .recovery;
210 match (recovery, in_doubt) {
211 (Recovery::Retry, _) | (Recovery::Ask, InDoubt::Retry) => Some(node),
212 (Recovery::Ask, InDoubt::Ask) => {
213 return Err(RuntimeError::InDoubt {
214 node: node.as_str().to_owned(),
215 });
216 }
217 }
218 }
219 };
220
221 store.resume(&run).await?;
222 Ok(Self {
223 store,
224 plan,
225 run,
226 effects: Effects {
227 agents,
228 sessions: BTreeMap::new(),
229 },
230 values: replayed.values,
231 next,
232 finished: replayed.finished,
233 })
234 }
235
236 pub fn id(&self) -> &RunId {
237 &self.run
238 }
239
240 pub async fn run(&mut self) -> Result<(), RuntimeError> {
242 while let Progress::Ran(_) = self.step().await? {}
243 Ok(())
244 }
245
246 pub async fn step(&mut self) -> Result<Progress, RuntimeError> {
252 let Some(name) = self.next.clone() else {
253 self.close().await?;
254 return Ok(Progress::Done);
255 };
256
257 match self.run_node(&name).await {
258 Ok(()) => Ok(Progress::Ran(name)),
259 Err(error) => {
260 let recorded = if error.is_permanent() {
264 self.store.finish(&self.run, RunStatus::Failed).await
265 } else {
266 self.store.suspend(&self.run).await
267 };
268 if let Err(problem) = recorded {
269 tracing::warn!(
270 run = self.run.as_str(),
271 "could not record how the run stopped: {problem}"
272 );
273 }
274 Err(error)
275 }
276 }
277 }
278
279 async fn run_node(&mut self, name: &NodeName) -> Result<(), RuntimeError> {
280 let node = self
281 .plan
282 .node(name.as_str())
283 .ok_or_else(|| RuntimeError::NotInPlan(name.as_str().to_owned()))?;
284 let inputs = self.resolve(name, node)?;
288
289 self.append(Record::Entered(Entered { node: name.clone() }))
290 .await?;
291 let outcome = nodes::perform(&mut self.effects, name, node, &inputs).await?;
292
293 for record in outcome.records {
294 self.append(record).await?;
295 }
296 self.append(Record::Exited(Exited {
297 node: name.clone(),
298 outputs: outcome.outputs.clone(),
299 exec: outcome.exec.map(str::to_owned),
300 }))
301 .await?;
302
303 for (pin, value) in outcome.outputs {
304 self.values.insert(PinRef::new(name.clone(), pin), value);
305 }
306 self.next = outcome
307 .exec
308 .and_then(|pin| node.exec_next.get(pin).cloned());
309 Ok(())
310 }
311
312 fn resolve(
313 &self,
314 name: &NodeName,
315 node: &openquiver_compile::CompiledNode,
316 ) -> Result<BTreeMap<String, RunValue>, RuntimeError> {
317 node.inputs
318 .iter()
319 .map(|(pin, source)| {
320 let value =
321 match source {
322 Source::Literal(literal) => RunValue::from(literal),
323 Source::Pin(from) => self.values.get(from).cloned().ok_or_else(|| {
324 RuntimeError::NotYetRun {
325 node: name.as_str().to_owned(),
326 pin: pin.clone(),
327 from: from.node().to_owned(),
328 }
329 })?,
330 };
331 Ok((pin.clone(), value))
332 })
333 .collect()
334 }
335
336 async fn close(&mut self) -> Result<(), RuntimeError> {
343 if !self.finished {
344 self.append(Record::Finished).await?;
347 self.finished = true;
348 }
349 self.store.finish(&self.run, RunStatus::Finished).await?;
350 Ok(())
351 }
352
353 async fn append(&self, record: Record) -> Result<(), RuntimeError> {
354 self.store
355 .append(&self.run, record.kind(), &record.payload()?)
356 .await?;
357 Ok(())
358 }
359}
360
361fn single_entry(plan: &CompiledPlan) -> Result<NodeName, RuntimeError> {
367 match plan.entries.as_slice() {
368 [only] => Ok(only.clone()),
369 [] => Err(RuntimeError::NoEntry),
370 many => Err(RuntimeError::ManyEntries(many.len())),
371 }
372}