use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::fmt;
use std::str::FromStr;
use std::sync::Mutex;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum Initiative {
Patient,
#[default]
Measured,
Decisive,
Eager,
}
impl Initiative {
#[must_use]
pub fn read_only_nudge_after(self) -> usize {
installed_rounds().get(self)
}
#[must_use]
pub fn exit_plan_requires_edit(self) -> bool {
matches!(self, Self::Decisive | Self::Eager)
}
#[must_use]
pub fn asks_before_acting(self) -> bool {
matches!(self, Self::Patient | Self::Measured)
}
#[must_use]
pub fn label(self) -> &'static str {
match self {
Self::Patient => "patient",
Self::Measured => "measured",
Self::Decisive => "decisive",
Self::Eager => "eager",
}
}
#[must_use]
pub fn describe(self) -> String {
let edit = if self.exit_plan_requires_edit() {
"exit_plan_mode requires an edit"
} else {
"exit_plan_mode is advisory"
};
format!(
"force an edit after {} read-only round(s); {edit}",
self.read_only_nudge_after()
)
}
#[must_use]
pub fn all() -> [Self; 4] {
[Self::Patient, Self::Measured, Self::Decisive, Self::Eager]
}
}
impl fmt::Display for Initiative {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.label())
}
}
impl FromStr for Initiative {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let s = s.trim().to_ascii_lowercase();
if s == "default" {
return Ok(Self::default());
}
if let Some(level) = Self::all().into_iter().find(|l| l.label() == s) {
return Ok(level);
}
if let Some(new) = crate::psyche_import::split_tenacity(&s) {
return Err(format!(
"'{s}' is a tenacity level from before the split; its initiative level is '{new}'"
));
}
let levels: Vec<&str> = Self::all().into_iter().map(Self::label).collect();
Err(format!("unknown initiative '{s}' ({})", levels.join("|")))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default)]
pub struct InitiativeRounds {
pub patient: usize,
pub measured: usize,
pub decisive: usize,
pub eager: usize,
}
impl Default for InitiativeRounds {
fn default() -> Self {
Self {
patient: 12,
measured: 3,
decisive: 2,
eager: 1,
}
}
}
impl InitiativeRounds {
#[must_use]
pub fn get(&self, level: Initiative) -> usize {
match level {
Initiative::Patient => self.patient,
Initiative::Measured => self.measured,
Initiative::Decisive => self.decisive,
Initiative::Eager => self.eager,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default)]
pub struct NoProgressRounds {
pub steer_after: usize,
pub stop_after: usize,
}
impl Default for NoProgressRounds {
fn default() -> Self {
Self {
steer_after: 8,
stop_after: 12,
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default)]
pub struct InitiativeConfig {
pub default: Option<Initiative>,
pub families: BTreeMap<String, Initiative>,
pub rounds: InitiativeRounds,
pub no_progress: NoProgressRounds,
}
impl InitiativeConfig {
#[must_use]
pub fn resolve(&self, family: Option<&str>) -> Initiative {
self.family_default(family)
.or(self.default)
.unwrap_or_default()
}
#[must_use]
pub fn family_default(&self, family: Option<&str>) -> Option<Initiative> {
let family = family?.trim();
self.families
.iter()
.find(|(k, _)| k.eq_ignore_ascii_case(family))
.map(|(_, v)| *v)
}
}
#[must_use]
pub fn resolve_initiative(
cli: Option<Initiative>,
persona: Option<Initiative>,
config: Option<&InitiativeConfig>,
family: Option<&str>,
) -> Initiative {
cli.or(persona)
.unwrap_or_else(|| config.map(|c| c.resolve(family)).unwrap_or_default())
}
static CLI_INITIATIVE: Mutex<Option<Initiative>> = Mutex::new(None);
static PERSONA_INITIATIVE: Mutex<Option<Initiative>> = Mutex::new(None);
static INITIATIVE_CONFIG: Mutex<Option<InitiativeConfig>> = Mutex::new(None);
static ACTIVE_FAMILY: Mutex<Option<String>> = Mutex::new(None);
std::thread_local! {
static EFFECTIVE_INITIATIVE_OVERRIDE: std::cell::Cell<Option<(Initiative, InitiativeRounds)>> =
const { std::cell::Cell::new(None) };
}
#[must_use]
pub struct ScopedEffectiveInitiative {
previous: Option<(Initiative, InitiativeRounds)>,
_thread_bound: std::marker::PhantomData<std::rc::Rc<()>>,
}
impl Drop for ScopedEffectiveInitiative {
fn drop(&mut self) {
let _ = EFFECTIVE_INITIATIVE_OVERRIDE.try_with(|slot| slot.set(self.previous));
}
}
pub fn scoped_effective_initiative(level: Initiative) -> ScopedEffectiveInitiative {
let rounds = installed_rounds();
let previous = EFFECTIVE_INITIATIVE_OVERRIDE.with(|slot| slot.replace(Some((level, rounds))));
ScopedEffectiveInitiative {
previous,
_thread_bound: std::marker::PhantomData,
}
}
pub fn set_cli_initiative(level: Initiative) {
if let Ok(mut slot) = CLI_INITIATIVE.lock() {
*slot = Some(level);
}
}
pub fn clear_cli_initiative() {
if let Ok(mut slot) = CLI_INITIATIVE.lock() {
*slot = None;
}
}
#[must_use]
pub fn cli_initiative() -> Option<Initiative> {
CLI_INITIATIVE.lock().ok().and_then(|s| *s)
}
pub fn set_persona_initiative(level: Option<Initiative>) {
if let Ok(mut slot) = PERSONA_INITIATIVE.lock() {
*slot = level;
}
}
#[must_use]
pub fn persona_initiative() -> Option<Initiative> {
PERSONA_INITIATIVE.lock().ok().and_then(|s| *s)
}
pub fn set_initiative_config(config: InitiativeConfig) {
if let Ok(mut slot) = INITIATIVE_CONFIG.lock() {
*slot = Some(config);
}
}
#[must_use]
pub fn initiative_config() -> Option<InitiativeConfig> {
INITIATIVE_CONFIG.lock().ok().and_then(|s| s.clone())
}
#[must_use]
pub fn installed_no_progress() -> NoProgressRounds {
INITIATIVE_CONFIG
.lock()
.ok()
.and_then(|s| s.as_ref().map(|c| c.no_progress))
.unwrap_or_default()
}
fn installed_rounds() -> InitiativeRounds {
if let Some((_, rounds)) = EFFECTIVE_INITIATIVE_OVERRIDE.with(std::cell::Cell::get) {
return rounds;
}
INITIATIVE_CONFIG
.lock()
.ok()
.and_then(|s| s.as_ref().map(|c| c.rounds))
.unwrap_or_default()
}
pub fn set_active_model_family(family: Option<String>) {
if let Ok(mut slot) = ACTIVE_FAMILY.lock() {
*slot = family;
}
}
#[must_use]
pub fn active_model_family() -> Option<String> {
ACTIVE_FAMILY.lock().ok().and_then(|s| s.clone())
}
#[must_use]
pub fn effective_initiative() -> Initiative {
if let Some((level, _)) = EFFECTIVE_INITIATIVE_OVERRIDE.with(std::cell::Cell::get) {
return level;
}
resolve_initiative(
cli_initiative(),
persona_initiative(),
initiative_config().as_ref(),
active_model_family().as_deref(),
)
}
#[must_use]
pub fn base_initiative() -> Initiative {
resolve_initiative(
None,
None,
initiative_config().as_ref(),
active_model_family().as_deref(),
)
}
#[must_use]
pub fn model_default_initiative() -> Option<Initiative> {
initiative_config()?.family_default(active_model_family().as_deref())
}
#[doc(hidden)]
pub struct InitiativeRuntimeSnapshot {
cli: Option<Initiative>,
persona: Option<Initiative>,
config: Option<InitiativeConfig>,
active_family: Option<String>,
}
#[doc(hidden)]
#[must_use]
pub fn snapshot_runtime_state() -> InitiativeRuntimeSnapshot {
InitiativeRuntimeSnapshot {
cli: cli_initiative(),
persona: persona_initiative(),
config: initiative_config(),
active_family: active_model_family(),
}
}
#[doc(hidden)]
pub fn restore_runtime_state(snapshot: InitiativeRuntimeSnapshot) {
if let Ok(mut s) = CLI_INITIATIVE.lock() {
*s = snapshot.cli;
}
if let Ok(mut s) = PERSONA_INITIATIVE.lock() {
*s = snapshot.persona;
}
if let Ok(mut s) = INITIATIVE_CONFIG.lock() {
*s = snapshot.config;
}
if let Ok(mut s) = ACTIVE_FAMILY.lock() {
*s = snapshot.active_family;
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_guard::GlobalSettingsGuard;
fn cfg(default: Option<Initiative>, fams: &[(&str, Initiative)]) -> InitiativeConfig {
InitiativeConfig {
default,
families: fams.iter().map(|(k, v)| (k.to_string(), *v)).collect(),
..InitiativeConfig::default()
}
}
#[test]
fn measured_is_the_default_and_keeps_the_historical_three_rounds() {
let _g = GlobalSettingsGuard::acquire();
set_initiative_config(InitiativeConfig::default());
assert_eq!(Initiative::default(), Initiative::Measured);
assert_eq!(Initiative::Measured.read_only_nudge_after(), 3);
assert!(!Initiative::Measured.exit_plan_requires_edit());
}
#[test]
fn approval_is_asked_only_at_the_looking_levels() {
let asks: Vec<bool> = Initiative::all()
.iter()
.map(|l| l.asks_before_acting())
.collect();
assert_eq!(asks, vec![true, true, false, false]);
}
#[test]
fn built_in_rounds_run_patient_to_eager() {
let _g = GlobalSettingsGuard::acquire();
set_initiative_config(InitiativeConfig::default());
let budgets: Vec<usize> = Initiative::all()
.iter()
.map(|l| l.read_only_nudge_after())
.collect();
assert_eq!(budgets, vec![12, 3, 2, 1]);
}
#[test]
fn rounds_come_from_the_installed_config() {
let _g = GlobalSettingsGuard::acquire();
let config: InitiativeConfig = toml::from_str(
r#"
[rounds]
patient = 20
decisive = 5
"#,
)
.unwrap();
set_initiative_config(config);
assert_eq!(Initiative::Patient.read_only_nudge_after(), 20);
assert_eq!(Initiative::Decisive.read_only_nudge_after(), 5);
assert_eq!(
Initiative::Measured.read_only_nudge_after(),
3,
"an unset level keeps its built-in number"
);
assert!(
Initiative::Patient
.describe()
.contains("after 20 read-only"),
"describe reads the installed config: {}",
Initiative::Patient.describe()
);
}
#[test]
fn only_decisive_and_eager_require_an_edit_on_plan_exit() {
assert!(!Initiative::Patient.exit_plan_requires_edit());
assert!(!Initiative::Measured.exit_plan_requires_edit());
assert!(Initiative::Decisive.exit_plan_requires_edit());
assert!(Initiative::Eager.exit_plan_requires_edit());
}
#[test]
fn parse_round_trips_labels_and_names_the_split_for_old_tenacity_labels() {
for l in Initiative::all() {
assert_eq!(l.label().parse::<Initiative>().unwrap(), l);
assert_eq!(l.to_string(), l.label());
}
assert_eq!(" EAGER ".parse::<Initiative>().unwrap(), Initiative::Eager);
assert_eq!(
"default".parse::<Initiative>().unwrap(),
Initiative::Measured
);
let err = "insistent".parse::<Initiative>().unwrap_err();
assert!(err.contains("'decisive'"), "{err}");
assert!("banana".parse::<Initiative>().is_err());
assert_eq!(
serde_json::to_string(&Initiative::Eager).unwrap(),
"\"eager\""
);
}
#[test]
fn config_resolve_prefers_family_then_default_then_measured() {
let c = cfg(
Some(Initiative::Patient),
&[("nemotron", Initiative::Eager)],
);
assert_eq!(c.resolve(Some("nemotron")), Initiative::Eager);
assert_eq!(c.resolve(Some(" NEMOTRON ")), Initiative::Eager);
assert_eq!(c.resolve(Some("qwen3")), Initiative::Patient);
assert_eq!(c.resolve(None), Initiative::Patient);
assert_eq!(
InitiativeConfig::default().resolve(Some("qwen3")),
Initiative::Measured
);
}
#[test]
fn resolve_initiative_precedence_cli_over_persona_over_config() {
let c = cfg(
Some(Initiative::Patient),
&[("nemotron", Initiative::Eager)],
);
let r = |cli, persona, family| resolve_initiative(cli, persona, Some(&c), family);
assert_eq!(
r(
Some(Initiative::Measured),
Some(Initiative::Decisive),
Some("nemotron")
),
Initiative::Measured
);
assert_eq!(
r(None, Some(Initiative::Decisive), Some("nemotron")),
Initiative::Decisive
);
assert_eq!(r(None, None, Some("nemotron")), Initiative::Eager);
assert_eq!(r(None, None, Some("kimi")), Initiative::Patient);
assert_eq!(
resolve_initiative(None, None, None, Some("nemotron")),
Initiative::Measured
);
}
#[test]
fn family_arrives_only_through_the_typed_seam() {
let _g = GlobalSettingsGuard::acquire();
clear_cli_initiative();
set_persona_initiative(None);
set_initiative_config(cfg(None, &[("nemotron", Initiative::Eager)]));
set_active_model_family(Some("nemotron".to_string()));
assert_eq!(effective_initiative(), Initiative::Eager);
set_active_model_family(Some("nemotron-super".to_string()));
assert_eq!(effective_initiative(), Initiative::Measured);
set_active_model_family(None);
assert_eq!(effective_initiative(), Initiative::Measured);
}
#[test]
fn base_initiative_ignores_the_operator_and_persona_layers() {
let _g = GlobalSettingsGuard::acquire();
set_initiative_config(cfg(
Some(Initiative::Patient),
&[("nemotron", Initiative::Eager)],
));
set_active_model_family(Some("nemotron".to_string()));
set_cli_initiative(Initiative::Measured);
set_persona_initiative(Some(Initiative::Decisive));
assert_eq!(base_initiative(), Initiative::Eager);
assert_eq!(effective_initiative(), Initiative::Measured);
set_active_model_family(None);
assert_eq!(base_initiative(), Initiative::Patient);
}
#[test]
fn scoped_override_is_thread_local_and_nests() {
let _g = GlobalSettingsGuard::acquire();
set_cli_initiative(Initiative::Patient);
let outer = scoped_effective_initiative(Initiative::Eager);
set_cli_initiative(Initiative::Decisive);
assert_eq!(effective_initiative(), Initiative::Eager);
{
let _inner = scoped_effective_initiative(Initiative::Measured);
assert_eq!(effective_initiative(), Initiative::Measured);
}
assert_eq!(effective_initiative(), Initiative::Eager);
let elsewhere = std::thread::spawn(effective_initiative).join().unwrap();
assert_eq!(
elsewhere,
Initiative::Decisive,
"the pin stays on its thread"
);
drop(outer);
assert_eq!(effective_initiative(), Initiative::Decisive);
}
#[test]
fn snapshot_restore_round_trips_every_initiative_global() {
let _g = GlobalSettingsGuard::acquire();
clear_cli_initiative();
set_persona_initiative(None);
set_initiative_config(InitiativeConfig::default());
set_active_model_family(None);
let snap = snapshot_runtime_state();
set_cli_initiative(Initiative::Eager);
set_persona_initiative(Some(Initiative::Decisive));
set_initiative_config(cfg(None, &[("nemotron", Initiative::Eager)]));
set_active_model_family(Some("nemotron".to_string()));
restore_runtime_state(snap);
assert_eq!(cli_initiative(), None);
assert_eq!(persona_initiative(), None);
assert_eq!(initiative_config(), Some(InitiativeConfig::default()));
assert_eq!(active_model_family(), None);
}
}
#[cfg(test)]
#[path = "initiative_regression_tests.rs"]
mod regression_tests;