mod canary;
mod candidate;
mod pipeline;
mod rules;
pub mod state;
mod stats;
use std::path::PathBuf;
use serde::{Deserialize, Serialize};
use time::OffsetDateTime;
use argonauts_core::RunId;
use argonauts_core::settings::Settings;
use argonauts_core::steps::task::ProcessChange;
use crate::error::{LearningError, Result};
use crate::learned_workflows::{self, Change, ChangeClass, LoadedLearnedWorkflow};
use crate::store::Store;
use crate::types::{LearnedWorkflowRef, RunRecord};
pub use rules::{check_evolvable, check_mutation};
pub use stats::{
BOOTSTRAP_RESAMPLES, NOT_WORSE_FLOOR, compare, holdout_ok, holdout_passes, holdout_tasks,
tripwires,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Stage {
Record,
Suggest,
Evolve,
Auto,
}
pub fn stage(settings: &Settings) -> Stage {
use argonauts_core::settings::approvals::ApprovalMode;
use argonauts_core::settings::evolution::EvolutionStage;
match settings.evolution.stage {
EvolutionStage::Record => Stage::Record,
EvolutionStage::Suggest => Stage::Suggest,
EvolutionStage::Evolve => match settings.approvals.workflow_promotion.mode {
ApprovalMode::Auto | ApprovalMode::Autopilot => Stage::Auto,
ApprovalMode::Supervised | ApprovalMode::AutoPreview => Stage::Evolve,
},
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Thresholds {
pub min_gain_points: f64,
pub max_cost_increase: f64,
pub not_worse: bool,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Candidate {
pub id: String,
pub parent: LearnedWorkflowRef,
pub version: u32,
pub change: Change,
pub why: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub from_run: Option<RunId>,
pub thresholds: Thresholds,
pub state: CandidateState,
#[serde(with = "time::serde::rfc3339")]
pub queued: OffsetDateTime,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "state", rename_all = "snake_case")]
pub enum CandidateState {
Suggested,
Queued,
Replaying {
spent_usd: f64,
},
Rejected {
reason: String,
},
AwaitingPromotion,
Canary {
runs: u32,
},
Current,
RolledBack {
reason: String,
},
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Verdict {
pub gain_points: f64,
pub lower_bound: f64,
pub cost_change: f64,
pub passed: bool,
pub reasons: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Tripwire {
StuckTasks,
FailedFinalChecks,
Cost,
Rejected,
Reverted,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "action", rename_all = "snake_case")]
pub enum Action {
Replay { candidate: String },
AskPromotion {
candidate: String,
packet: serde_json::Value,
},
Promoted {
learned_workflow: LearnedWorkflowRef,
branch: String,
pr_command: Vec<String>,
},
RolledBack {
learned_workflow: LearnedWorkflowRef,
to: u32,
reason: String,
},
Recorded {
learned_workflow: LearnedWorkflowRef,
branch: String,
},
Tell { text: String },
}
pub fn class_of(change: &Change) -> ChangeClass {
change.class
}
pub struct Evolution {
store: Store,
now: Option<OffsetDateTime>,
}
impl Evolution {
pub fn open(store: &Store) -> Result<Self> {
Ok(Self {
store: store.clone(),
now: None,
})
}
pub fn at(mut self, now: OffsetDateTime) -> Self {
self.now = Some(now);
self
}
pub fn store(&self) -> &Store {
&self.store
}
fn now(&self) -> OffsetDateTime {
self.now.unwrap_or_else(OffsetDateTime::now_utc)
}
fn file(&self, name: &str) -> PathBuf {
self.store.evolution_dir().join(name)
}
pub fn candidates(&self) -> Result<Vec<Candidate>> {
state::read(&self.file(state::CANDIDATES))
}
fn save_candidates(&self, candidates: &state::Candidates) -> Result<()> {
state::write(&self.file(state::CANDIDATES), candidates)
}
pub fn candidate(&self, id: &str) -> Result<Option<Candidate>> {
Ok(self.candidates()?.into_iter().find(|c| c.id == id))
}
pub fn canaries(&self) -> Result<state::Canaries> {
state::read(&self.file(state::CANARIES))
}
fn save_canaries(&self, canaries: &state::Canaries) -> Result<()> {
state::write(&self.file(state::CANARIES), canaries)
}
fn learned_workflow_states(&self) -> Result<state::LearnedWorkflows> {
state::read(&self.file(state::LEARNED_WORKFLOWS))
}
fn save_learned_workflow_states(
&self,
learned_workflows: &state::LearnedWorkflows,
) -> Result<()> {
state::write(&self.file(state::LEARNED_WORKFLOWS), learned_workflows)
}
pub fn candidate_dir(&self, id: &str) -> PathBuf {
self.file(state::CANDIDATE_DIRS).join(id)
}
pub fn candidate_learned_workflow(&self, id: &str) -> Result<LoadedLearnedWorkflow> {
let candidate = self
.candidate(id)?
.ok_or_else(|| LearningError::Refused(format!("there's no candidate `{id}`")))?;
learned_workflows::load_file(
&self
.candidate_dir(id)
.join(learned_workflows::version_file(candidate.version)),
)
}
pub fn running_version(&self, learned_workflow: &str) -> Result<Option<u32>> {
if let Some(canary) = self.canaries()?.get(learned_workflow) {
return Ok(Some(canary.version));
}
let checkout = self.checkout_current(learned_workflow)?;
let mut states = self.learned_workflow_states()?;
let Some(pointer) = states
.get(learned_workflow)
.and_then(|state| state.pointer.clone())
else {
return Ok(checkout);
};
if pointer.checkout_was == checkout {
return Ok(Some(pointer.version));
}
if let Some(state) = states.get_mut(learned_workflow) {
state.pointer = None;
}
self.save_learned_workflow_states(&states)?;
Ok(checkout)
}
pub fn running(&self, learned_workflow: &str) -> Result<Option<LoadedLearnedWorkflow>> {
match self.running_version(learned_workflow)? {
Some(version) => self.load_version(learned_workflow, version),
None => Ok(None),
}
}
fn checkout_set(
&self,
learned_workflow: &str,
) -> Result<Option<learned_workflows::LearnedWorkflowSet>> {
let folder = self.store.learned_workflows_dir().join(learned_workflow);
if folder.is_dir() {
learned_workflows::load_set(&folder)
} else {
Ok(None)
}
}
fn checkout_current(&self, learned_workflow: &str) -> Result<Option<u32>> {
Ok(self.checkout_set(learned_workflow)?.map(|set| set.current))
}
pub fn load_version(
&self,
learned_workflow: &str,
version: u32,
) -> Result<Option<LoadedLearnedWorkflow>> {
if let Some(loaded) = self
.checkout_set(learned_workflow)?
.and_then(|set| set.version(version).cloned())
{
return Ok(Some(loaded));
}
let candidate = self
.candidates()?
.into_iter()
.find(|c| c.parent.name == learned_workflow && c.version == version);
match candidate {
Some(candidate) => self.candidate_learned_workflow(&candidate.id).map(Some),
None => Ok(None),
}
}
fn next_version(&self, learned_workflow: &str) -> Result<u32> {
let checkout = self
.checkout_set(learned_workflow)?
.and_then(|set| set.versions.last().map(|l| l.learned_workflow.version))
.unwrap_or(0);
let candidates = self
.candidates()?
.iter()
.filter(|c| c.parent.name == learned_workflow)
.map(|c| c.version)
.max()
.unwrap_or(0);
Ok(checkout.max(candidates) + 1)
}
pub fn last_promoted(&self, learned_workflow: &str) -> Result<Option<OffsetDateTime>> {
Ok(self
.learned_workflow_states()?
.get(learned_workflow)
.and_then(|state| state.last_promoted))
}
pub fn after_run(
&self,
settings: &Settings,
run: &RunRecord,
proposal: Option<&ProcessChange>,
) -> Result<Vec<Action>> {
let stage = stage(settings);
let mut actions = match &run.learned_workflow {
Some(learned_workflow) => self.watch_canary(settings, run, learned_workflow)?,
None => self.record(settings, run)?,
};
if let Some(proposal) = proposal
&& stage != Stage::Record
{
actions.extend(self.propose(settings, run, proposal, stage)?);
}
if matches!(stage, Stage::Evolve | Stage::Auto) {
actions.extend(self.next_replay(settings)?);
}
Ok(actions)
}
pub fn spent_this_month(&self, now: OffsetDateTime) -> Result<f64> {
let ledger: state::Ledger = state::read(&self.file(state::SPEND))?;
Ok(ledger
.iter()
.filter(|spend| spend.at.year() == now.year() && spend.at.month() == now.month())
.fold(0.0, |total, spend| total + spend.usd))
}
}
pub fn learned_workflow_branch(name: &str, version: u32) -> String {
format!("argonauts/learned/{name}-v{version}")
}