mod journal;
mod nodes;
mod replay;
mod value;
use std::collections::BTreeMap;
use openquiver_acp::{AcpError, Agents};
use openquiver_compile::{CompiledPlan, Source};
use openquiver_graph::{NodeName, PinRef};
use openquiver_nodes::Recovery;
use openquiver_store::{RunId, RunStatus, Store, StoreError};
use journal::{Entered, Exited, Record, Started};
use nodes::Effects;
use replay::Frontier;
pub use journal::JournalError;
pub use value::RunValue;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Progress {
Ran(NodeName),
Done,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum InDoubt {
Ask,
Retry,
}
#[derive(Debug, thiserror::Error)]
pub enum RuntimeError {
#[error("the journal could not be reached: {0}")]
Store(#[from] StoreError),
#[error(transparent)]
Agent(#[from] AcpError),
#[error(transparent)]
Journal(#[from] JournalError),
#[error("a journal entry could not be written: {0}")]
Unwritable(#[from] serde_json::Error),
#[error("this workflow has no starting event, so there is nothing to run")]
NoEntry,
#[error("this workflow has {0} starting events, and a run begins at one")]
ManyEntries(usize),
#[error("run `{0}` has already finished")]
AlreadyFinished(String),
#[error("this journal cannot be replayed: {0}")]
BadJournal(&'static str),
#[error(
"this run was started on a different version of this workflow; \
the graph has been edited since"
)]
DifferentPlan,
#[error("this journal mentions `{0}`, which is not a node in this workflow")]
NotInPlan(String),
#[error("there is no way to run a `{0}` node")]
NoSuchNodeType(String),
#[error("`{node}` needs `{pin}`, but `{from}` has not run and cannot supply it")]
NotYetRun {
node: String,
pin: String,
from: String,
},
#[error("`{node}` needs `{pin}`, and nothing supplies it")]
NoInput { node: String, pin: String },
#[error("`{node}` expects {expected} on `{pin}`")]
WrongValue {
node: String,
pin: String,
expected: &'static str,
},
#[error(
"`{node}` was entered and never left, so whether it finished is unknown; \
resume again saying whether to run it"
)]
InDoubt { node: String },
}
impl RuntimeError {
fn is_permanent(&self) -> bool {
match self {
RuntimeError::Store(_) | RuntimeError::Agent(_) => false,
RuntimeError::Journal(_)
| RuntimeError::Unwritable(_)
| RuntimeError::NoEntry
| RuntimeError::ManyEntries(_)
| RuntimeError::AlreadyFinished(_)
| RuntimeError::BadJournal(_)
| RuntimeError::DifferentPlan
| RuntimeError::NotInPlan(_)
| RuntimeError::NoSuchNodeType(_)
| RuntimeError::NotYetRun { .. }
| RuntimeError::NoInput { .. }
| RuntimeError::WrongValue { .. } => true,
RuntimeError::InDoubt { .. } => true,
}
}
}
pub struct Runtime<'a> {
store: &'a Store,
plan: &'a CompiledPlan,
run: RunId,
effects: Effects<'a>,
values: BTreeMap<PinRef, RunValue>,
next: Option<NodeName>,
finished: bool,
}
impl<'a> Runtime<'a> {
pub async fn start(
store: &'a Store,
plan: &'a CompiledPlan,
agents: &'a dyn Agents,
workflow: &str,
) -> Result<Self, RuntimeError> {
let entry = single_entry(plan)?;
let run = store.start_run(workflow).await?;
let runtime = Self {
store,
plan,
run,
effects: Effects {
agents,
sessions: BTreeMap::new(),
},
values: BTreeMap::new(),
next: Some(entry),
finished: false,
};
runtime
.append(Record::Started(Started {
plan: plan.fingerprint(),
}))
.await?;
Ok(runtime)
}
pub async fn resume(
store: &'a Store,
plan: &'a CompiledPlan,
agents: &'a dyn Agents,
run: RunId,
in_doubt: InDoubt,
) -> Result<Self, RuntimeError> {
if !store.status(&run).await?.resumable() {
return Err(RuntimeError::AlreadyFinished(run.as_str().to_owned()));
}
let entry = single_entry(plan)?;
let events = store.events(&run).await?;
let replayed = replay::replay(&events, plan, &entry)?;
let next = match replayed.frontier {
Frontier::End => None,
Frontier::At(node) => Some(node),
Frontier::InDoubt(node) => {
let recovery = plan
.node(node.as_str())
.ok_or_else(|| RuntimeError::NotInPlan(node.as_str().to_owned()))?
.recovery;
match (recovery, in_doubt) {
(Recovery::Retry, _) | (Recovery::Ask, InDoubt::Retry) => Some(node),
(Recovery::Ask, InDoubt::Ask) => {
return Err(RuntimeError::InDoubt {
node: node.as_str().to_owned(),
});
}
}
}
};
store.resume(&run).await?;
Ok(Self {
store,
plan,
run,
effects: Effects {
agents,
sessions: BTreeMap::new(),
},
values: replayed.values,
next,
finished: replayed.finished,
})
}
pub fn id(&self) -> &RunId {
&self.run
}
pub async fn run(&mut self) -> Result<(), RuntimeError> {
while let Progress::Ran(_) = self.step().await? {}
Ok(())
}
pub async fn step(&mut self) -> Result<Progress, RuntimeError> {
let Some(name) = self.next.clone() else {
self.close().await?;
return Ok(Progress::Done);
};
match self.run_node(&name).await {
Ok(()) => Ok(Progress::Ran(name)),
Err(error) => {
let recorded = if error.is_permanent() {
self.store.finish(&self.run, RunStatus::Failed).await
} else {
self.store.suspend(&self.run).await
};
if let Err(problem) = recorded {
tracing::warn!(
run = self.run.as_str(),
"could not record how the run stopped: {problem}"
);
}
Err(error)
}
}
}
async fn run_node(&mut self, name: &NodeName) -> Result<(), RuntimeError> {
let node = self
.plan
.node(name.as_str())
.ok_or_else(|| RuntimeError::NotInPlan(name.as_str().to_owned()))?;
let inputs = self.resolve(name, node)?;
self.append(Record::Entered(Entered { node: name.clone() }))
.await?;
let outcome = nodes::perform(&mut self.effects, name, node, &inputs).await?;
for record in outcome.records {
self.append(record).await?;
}
self.append(Record::Exited(Exited {
node: name.clone(),
outputs: outcome.outputs.clone(),
exec: outcome.exec.map(str::to_owned),
}))
.await?;
for (pin, value) in outcome.outputs {
self.values.insert(PinRef::new(name.clone(), pin), value);
}
self.next = outcome
.exec
.and_then(|pin| node.exec_next.get(pin).cloned());
Ok(())
}
fn resolve(
&self,
name: &NodeName,
node: &openquiver_compile::CompiledNode,
) -> Result<BTreeMap<String, RunValue>, RuntimeError> {
node.inputs
.iter()
.map(|(pin, source)| {
let value =
match source {
Source::Literal(literal) => RunValue::from(literal),
Source::Pin(from) => self.values.get(from).cloned().ok_or_else(|| {
RuntimeError::NotYetRun {
node: name.as_str().to_owned(),
pin: pin.clone(),
from: from.node().to_owned(),
}
})?,
};
Ok((pin.clone(), value))
})
.collect()
}
async fn close(&mut self) -> Result<(), RuntimeError> {
if !self.finished {
self.append(Record::Finished).await?;
self.finished = true;
}
self.store.finish(&self.run, RunStatus::Finished).await?;
Ok(())
}
async fn append(&self, record: Record) -> Result<(), RuntimeError> {
self.store
.append(&self.run, record.kind(), &record.payload()?)
.await?;
Ok(())
}
}
fn single_entry(plan: &CompiledPlan) -> Result<NodeName, RuntimeError> {
match plan.entries.as_slice() {
[only] => Ok(only.clone()),
[] => Err(RuntimeError::NoEntry),
many => Err(RuntimeError::ManyEntries(many.len())),
}
}