use time::OffsetDateTime;
use argonauts_core::TaskId;
use argonauts_core::events::task::{TaskSpec, TaskState};
use argonauts_core::limits::{HealthState, PlanUsageReading, PlanUsageStatus};
use argonauts_core::names::{AgentId, ModelRef, RoleName};
use argonauts_core::settings::fallback::AppliedFallback;
use crate::engine::host::Host;
use crate::error::{EngineError, EngineResult};
use crate::state::RunState;
use crate::state::tasks::TaskEntry;
use crate::task::machine::Trigger;
use crate::task::pipeline;
use crate::workflow::StepAt;
pub fn applied(host: &Host, role: RoleName, agent_id: &AgentId) -> AppliedFallback {
host.settings().fallback.applied_to(role, agent_id)
}
pub fn applied_for(host: &Host, _task: &TaskSpec, worker: &ModelRef) -> AppliedFallback {
applied(host, RoleName::Worker, &worker.agent_id())
}
pub fn record(host: &Host, task: &TaskId) -> EngineResult<TaskEntry> {
host.read(|state| state.tasks.get(task).cloned())
.ok_or_else(|| EngineError::Refused(format!("{task} is not a task of this run")))
}
pub fn worker(record: &TaskEntry) -> Option<ModelRef> {
record.worker.clone().or_else(|| record.spec.worker.clone())
}
pub fn agent_usable(host: &Host, agent_id: &AgentId) -> bool {
let services = host.services();
let health = services.health.state(agent_id).state;
let plan_usage_ok = services
.plan_usage
.get(agent_id)
.is_none_or(|reading| !plan_usage_out(&reading, host.now()));
matches!(health, HealthState::Healthy | HealthState::Degraded) && plan_usage_ok
}
pub fn takes_work(host: &Host, model: &ModelRef) -> bool {
agent_usable(host, &model.agent_id()) && host.services().health.model(model).takes_fallbacks()
}
pub fn plan_usage_out(reading: &PlanUsageReading, now: OffsetDateTime) -> bool {
reading.status == PlanUsageStatus::Rejected && reading.resets_at.is_none_or(|at| at > now)
}
pub fn current_step(state: &RunState) -> Option<StepAt> {
state
.workflows
.by_id
.values()
.flat_map(|workflow| &workflow.steps)
.filter(|progress| progress.end.is_none())
.max_by_key(|progress| progress.since)
.map(|progress| StepAt {
step: progress.step.clone(),
round: progress.round,
})
}
pub fn step_now(host: &Host) -> EngineResult<StepAt> {
host.read(current_step)
.ok_or_else(|| EngineError::Refused("no step is running for this fallback".to_owned()))
}
pub fn session_running(state: &RunState, task: &TaskId) -> bool {
state
.sessions
.running()
.any(|session| session.task.as_ref() == Some(task))
}
pub fn at_rest(state: TaskState) -> bool {
matches!(
state,
TaskState::NeedsDecision
| TaskState::NeedsYou
| TaskState::Deferred
| TaskState::Failed
| TaskState::Waiting
| TaskState::Blocked
)
}
pub fn ready_trigger(state: TaskState) -> Option<Trigger> {
match state {
TaskState::NeedsDecision => Some(Trigger::Decided),
TaskState::Failed => Some(Trigger::Retry),
TaskState::NeedsYou => Some(Trigger::NewWorker),
_ => None,
}
}
pub fn back_to_work_trigger(state: TaskState) -> Option<Trigger> {
match state {
TaskState::Checking | TaskState::InReview => Some(Trigger::NewWorker),
other => ready_trigger(other),
}
}
pub fn back_in_line(host: &Host, task: &TaskId, reason: &str) -> EngineResult<()> {
let state = record(host, task)?.state;
if let Some(trigger) = back_to_work_trigger(state) {
pipeline::transition(host, task, trigger, reason)?;
}
Ok(())
}
pub fn refuse_reset(state: &RunState, task: &TaskId) -> Option<String> {
let record = state.tasks.get(task)?;
if session_running(state, task) {
return Some(format!("{task} is running. Pause it, then rewind."));
}
match record.state {
TaskState::Blocked => Some(format!(
"{task} waits for an answer its session resumes with. Answer it, then rewind."
)),
TaskState::TakenOver => Some(format!(
"You took {task} over. Give it back or finish it, then rewind."
)),
TaskState::Merging => Some(format!("{task} is being merged, so it can't be rewound.")),
state if !at_rest(state) => Some(format!(
"{task} is {state:?}. Rewind it once it stops and waits for you."
)),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_new_worker_puts_waiting_tasks_back_in_line() {
assert_eq!(
ready_trigger(TaskState::NeedsDecision),
Some(Trigger::Decided)
);
assert_eq!(ready_trigger(TaskState::Failed), Some(Trigger::Retry));
assert_eq!(ready_trigger(TaskState::NeedsYou), Some(Trigger::NewWorker));
assert_eq!(ready_trigger(TaskState::Running), None);
assert!(at_rest(TaskState::NeedsDecision));
assert!(!at_rest(TaskState::Running));
assert!(!at_rest(TaskState::Merged));
}
#[test]
fn a_change_between_the_session_and_merge_goes_back_to_running() {
for state in [TaskState::Checking, TaskState::InReview] {
let trigger = back_to_work_trigger(state);
assert_eq!(trigger, Some(Trigger::NewWorker), "{state:?}");
let to = trigger.and_then(|trigger| crate::task::machine::next(state, trigger));
assert_eq!(to, Some(TaskState::Running), "{state:?}");
}
assert_eq!(
back_to_work_trigger(TaskState::NeedsYou),
Some(Trigger::NewWorker)
);
assert_eq!(back_to_work_trigger(TaskState::Merging), None);
assert_eq!(back_to_work_trigger(TaskState::Running), None);
}
}