use argonauts_core::events::approval::{
AnswerAction, AnswerOption, ApprovalKind, FreeText, PlanWorkflow,
};
use argonauts_core::events::run::RunOutcome;
use argonauts_core::events::workflow::StepEnd;
use argonauts_core::names::ModelRef;
use argonauts_core::policy::EvidenceDocument;
use argonauts_core::settings::approvals::ApprovalMode;
use argonauts_core::workflow::{Phase, StandardPhase, Workflow, WorkflowParams, WorkflowRef};
use argonauts_runner::Script;
use crate::approvals::{self, autopilot, options};
use crate::divide::workflow_plan::{Shown, request_showing};
use crate::engine::host::Host;
use crate::error::{EngineError, EngineResult};
use crate::workflow::{Flow, StepCall, StepDone, runner};
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) enum Wrote {
Orchestrator(ModelRef),
File,
}
pub(super) enum Verdict {
Approved,
SentBack,
Ended(StepDone),
}
pub(super) fn sent_back(host: &Host, call: &StepCall) -> Vec<String> {
host.read(|state| {
let since = state
.workflows
.progress(&call.at.step)
.map_or(0, |progress| progress.since);
let mut decided: Vec<(u64, String)> = state
.approvals
.by_id
.values()
.filter(|record| {
record.opened.kind == ApprovalKind::Plan
&& record.withdrawn.is_none()
&& record.since > since
})
.filter_map(|record| {
let decided = record.decided.as_ref()?;
(decided.action == AnswerAction::SendBack).then(|| {
let note = decided.text.clone().unwrap_or_default();
(record.since, note.trim().to_owned())
})
})
.collect();
decided.sort_by_key(|(since, _)| *since);
decided.into_iter().map(|(_, note)| note).collect()
})
}
fn asks_you(host: &Host) -> bool {
!autopilot::applies_now(host, ApprovalKind::Plan)
&& matches!(
approvals::mode_now(host, ApprovalKind::Plan),
ApprovalMode::Supervised | ApprovalMode::AutoPreview
)
}
pub(super) async fn approval(
host: &Host,
call: &StepCall,
script: &Script,
wrote: &Wrote,
notes: &[String],
) -> EngineResult<Verdict> {
if !asks_you(host) {
return Ok(Verdict::Approved);
}
let in_use = host.settings().workflow.clone();
let (summary, authors) = match wrote {
Wrote::Orchestrator(worker) => (
format!(
"The orchestrator wrote these steps for your request, with the roles of {in_use}"
),
vec![worker.clone()],
),
Wrote::File => (format!("The steps of {in_use} run as written"), Vec::new()),
};
let mut documents = vec![EvidenceDocument {
label: "workflow".to_owned(),
text: script.source.clone(),
path: None,
sha256: None,
}];
if !notes.is_empty() {
documents.push(EvidenceDocument {
label: "sent back with".to_owned(),
text: notes.join("\n"),
path: None,
sha256: None,
});
}
let can_rewrite = matches!(wrote, Wrote::Orchestrator(_));
let shown = Shown {
workflow: Some(PlanWorkflow {
from: Some(WorkflowRef::new(&in_use)),
why: None,
workflow: as_phases(script),
}),
summary,
documents,
options: answer_options(can_rewrite),
free_text: if can_rewrite {
FreeText::Guidance
} else {
FreeText::None
},
authors,
};
let request = request_showing(host, call, shown).await;
let decided = approvals::open_and_wait(host, request).await?;
match decided.action {
AnswerAction::Approve => Ok(Verdict::Approved),
AnswerAction::SendBack if can_rewrite => Ok(Verdict::SentBack),
AnswerAction::SwitchToCaptain => Ok(Verdict::Ended(StepDone {
end: StepEnd::Done,
summary: Some("You chose to run it as Captain".to_owned()),
flow: Flow::Switch(WorkflowRef::new(runner::captain_workflow(host.settings()))),
})),
AnswerAction::Abandon => Ok(Verdict::Ended(StepDone {
end: StepEnd::Done,
summary: Some("You abandoned the run".to_owned()),
flow: Flow::Finish(RunOutcome::Abandoned),
})),
other => Err(EngineError::Refused(format!(
"the plan approval of a workflow's steps doesn't offer {other:?}"
))),
}
}
fn answer_options(can_rewrite: bool) -> Vec<AnswerOption> {
options::standard(ApprovalKind::Plan)
.into_iter()
.filter(|option| match option.action {
AnswerAction::ApproveWithEdits => false,
AnswerAction::SendBack => can_rewrite,
_ => true,
})
.collect()
}
fn as_phases(script: &Script) -> Workflow {
Workflow {
name: script.meta.name.clone(),
description: script.meta.description.clone(),
based_on: None,
params: WorkflowParams::default(),
phases: script
.meta
.phases
.iter()
.map(|phase| Phase {
name: phase.name.clone(),
detail: phase.detail.clone(),
standard: StandardPhase::Do,
steps: Vec::new(),
})
.collect(),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn keys(options: &[AnswerOption]) -> String {
options.iter().map(|option| option.key).collect()
}
#[test]
fn only_steps_the_orchestrator_wrote_can_be_sent_back() {
assert_eq!(keys(&answer_options(true)), "ybsxd");
assert_eq!(keys(&answer_options(false)), "ysxd");
}
#[test]
fn a_scripts_phases_are_what_the_approval_shows() {
let script = Script::parse(
"export const meta = { name: 'add-due', description: 'Due dates', \
phases: [{ name: 'Plan', detail: 'split it' }, { name: 'Build' }] }\nreturn 1",
)
.expect("reads");
let shown = as_phases(&script);
assert_eq!(
(shown.name.as_str(), shown.description.as_str()),
("add-due", "Due dates")
);
let phases: Vec<(&str, &str)> = shown
.phases
.iter()
.map(|phase| (phase.name.as_str(), phase.detail.as_str()))
.collect();
assert_eq!(phases, [("Plan", "split it"), ("Build", "")]);
}
}