use crate::config::action::Action;
use crate::config::error::LoadError;
use crate::config::tool_control::ToolControl;
use crate::config::tool_output::ToolOutputBound;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::path::Path;
pub mod compaction;
use compaction::validate_compaction;
pub use compaction::{CompactionConfig, CompactionTrigger};
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
pub struct Workflow {
pub events: BTreeMap<Event, Vec<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub compaction: Option<CompactionConfig>,
#[serde(default)]
pub retry: RetryConfig,
#[serde(default)]
pub budgets: Budgets,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tool_output: Option<ToolOutputBound>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tool_control: Option<ToolControl>,
}
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
pub struct Budgets {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_total_tokens: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_wall_seconds: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_depth: Option<u32>,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
pub struct RetryConfig {
pub max_attempts: u32,
pub backoff: Backoff,
}
impl Default for RetryConfig {
fn default() -> Self {
Self {
max_attempts: 3,
backoff: Backoff::Exponential,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum Backoff {
Exponential,
}
const BACKOFF_BASE_MS: u64 = 250;
impl Backoff {
pub fn delay(self, attempt: u32, retry_after_seconds: Option<u32>) -> std::time::Duration {
let scheduled = match self {
Backoff::Exponential => {
let factor = 2u64.saturating_pow(attempt.saturating_sub(1));
std::time::Duration::from_millis(BACKOFF_BASE_MS.saturating_mul(factor))
}
};
let hint = retry_after_seconds.map_or(std::time::Duration::ZERO, |s| {
std::time::Duration::from_secs(u64::from(s))
});
scheduled.max(hint)
}
}
#[derive(
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, JsonSchema,
)]
#[serde(rename_all = "snake_case")]
pub enum Event {
UserMessage,
WorkerReturn,
VerifierApprove,
VerifierReject,
WorkerFlush,
CompactorReturn,
BranchStopped,
PreStep,
PostStep,
OnToolReturn,
}
impl Event {
pub fn as_str(self) -> &'static str {
event_name(self)
}
}
impl Workflow {
pub fn parse(raw: &str, origin: &Path) -> Result<Self, LoadError> {
let parsed: Self = serde_yaml_ng::from_str(raw).map_err(|source| LoadError::Yaml {
path: origin.to_path_buf(),
source,
})?;
parsed.validate(origin)?;
Ok(parsed)
}
fn validate(&self, path: &Path) -> Result<(), LoadError> {
for (event, actions) in &self.events {
for (i, raw) in actions.iter().enumerate() {
Action::parse(raw).map_err(|message| LoadError::Invalid {
path: path.to_path_buf(),
key: format!("events.{}[{i}]", event_name(*event)),
message,
})?;
}
}
if let Some(compaction) = &self.compaction {
validate_compaction(path, compaction)?;
}
if let Some(control) = &self.tool_control
&& control.command.trim().is_empty()
{
return Err(LoadError::Invalid {
path: path.to_path_buf(),
key: "tool_control.command".into(),
message: "must name the control executable (ARCH §3.3 Tool control)".into(),
});
}
Ok(())
}
pub fn actions_for(&self, event: Event) -> Vec<Action> {
self.events
.get(&event)
.map(|raw| {
raw.iter()
.map(|s| Action::parse(s).expect("validated at load"))
.collect()
})
.unwrap_or_default()
}
pub fn typed_events(&self) -> BTreeMap<Event, Vec<Action>> {
self.events
.iter()
.map(|(event, actions)| {
let parsed = actions
.iter()
.map(|raw| Action::parse(raw).expect("validated at load"))
.collect();
(*event, parsed)
})
.collect()
}
}
fn event_name(event: Event) -> &'static str {
match event {
Event::UserMessage => "user_message",
Event::WorkerReturn => "worker_return",
Event::VerifierApprove => "verifier_approve",
Event::VerifierReject => "verifier_reject",
Event::WorkerFlush => "worker_flush",
Event::CompactorReturn => "compactor_return",
Event::BranchStopped => "branch_stopped",
Event::PreStep => "pre_step",
Event::PostStep => "post_step",
Event::OnToolReturn => "on_tool_return",
}
}