use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum DecisionProtocol {
TypesafeSystemOne,
VercelEvaluate,
OpenrouterDecisions,
StructuredLlm,
}
impl DecisionProtocol {
pub const ALL: [Self; 4] = [
Self::TypesafeSystemOne,
Self::VercelEvaluate,
Self::OpenrouterDecisions,
Self::StructuredLlm,
];
pub const fn as_str(self) -> &'static str {
match self {
Self::TypesafeSystemOne => "typesafe_system_one",
Self::VercelEvaluate => "vercel_evaluate",
Self::OpenrouterDecisions => "openrouter_decisions",
Self::StructuredLlm => "structured_llm",
}
}
pub const fn is_native(self) -> bool {
!matches!(self, Self::StructuredLlm)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum DecisionQuestionKind {
Boolean,
Choice,
Score,
}
impl DecisionQuestionKind {
pub const ALL: [Self; 3] = [Self::Boolean, Self::Choice, Self::Score];
pub const fn as_str(self) -> &'static str {
match self {
Self::Boolean => "boolean",
Self::Choice => "choice",
Self::Score => "score",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct DecisionLimits {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_questions: Option<u32>,
pub max_choice_options: u32,
pub score_levels_min: u32,
pub score_levels_max: u32,
pub state_window_tokens: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub request_window_tokens: Option<u64>,
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Deserialize)]
struct ProtocolRow {
decision_protocol: DecisionProtocol,
}
#[derive(Deserialize)]
struct KindRow {
kind: DecisionQuestionKind,
}
#[test]
fn unknown_protocol_fails_the_load_instead_of_defaulting() {
let parsed = toml::from_str::<ProtocolRow>("decision_protocol = \"typesafe_system_two\"\n");
assert!(parsed.is_err(), "unknown protocol must not decode");
let structured = toml::from_str::<ProtocolRow>("decision_protocol = \"structured_llm\"\n")
.unwrap()
.decision_protocol;
assert_eq!(structured, DecisionProtocol::StructuredLlm);
assert!(!structured.is_native());
assert!(DecisionProtocol::VercelEvaluate.is_native());
}
#[test]
fn limits_refuse_a_misspelled_ceiling_and_keep_max_questions_optional() {
let misspelled = toml::from_str::<DecisionLimits>(
"max_choice_option = 255\nscore_levels_min = 2\nscore_levels_max = 10\nstate_window_tokens = 32000\n",
);
assert!(misspelled.is_err(), "unknown key must not read as unset");
let limits: DecisionLimits = toml::from_str(
"max_choice_options = 255\nscore_levels_min = 2\nscore_levels_max = 10\nstate_window_tokens = 32000\n",
)
.unwrap();
assert_eq!(limits.max_questions, None);
assert_eq!(limits.max_choice_options, 255);
assert_eq!(limits.state_window_tokens, 32_000);
let missing_bound = toml::from_str::<DecisionLimits>(
"max_choice_options = 255\nscore_levels_min = 2\nstate_window_tokens = 32000\n",
);
assert!(missing_bound.is_err(), "a ceiling must not be invented");
}
#[test]
fn question_kinds_round_trip_their_source_spelling() {
for kind in DecisionQuestionKind::ALL {
let decoded = toml::from_str::<KindRow>(&format!("kind = \"{}\"\n", kind.as_str()))
.unwrap()
.kind;
assert_eq!(decoded, kind);
}
assert!(toml::from_str::<KindRow>("kind = \"ranking\"\n").is_err());
}
}