use std::fmt;
use std::time::Duration;
use serde::de::{Error as _, Unexpected};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use turnframe_core::command::RiskClass;
use turnframe_core::flow::BriefingBudget;
use turnframe_core::plan::limits::PlanLimits;
use turnframe_core::policy::PolicySnapshot;
use turnframe_core::reduce::SourcePolicy;
use turnframe_core::response::ToneProfile;
use turnframe_core::turn::TurnLimits;
use turnframe_tasks::{Budget, TaskProfiles};
use turnframe_understand::Settings;
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum ConfigError {
#[error("{field} must be at least 1")]
MustBePositive {
field: &'static str,
},
#[error("{field} may not be disabled: {because}")]
UnsafeSetting {
field: &'static str,
because: &'static str,
},
#[error("{first} contradicts {second}")]
Contradiction {
first: &'static str,
second: &'static str,
},
#[error("{field} must be between 0 and 1000 per mille")]
OutOfRange {
field: &'static str,
},
}
pub const SANDBOX_ACKNOWLEDGEMENT: &str = "i-accept-unreviewed-autonomous-writes";
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
pub struct SandboxAcknowledgement(());
impl SandboxAcknowledgement {
#[must_use]
pub fn i_accept_unreviewed_autonomous_writes() -> Self {
tracing::warn!(
target: "turnframe.config",
mode = "sandboxed_autonomous",
acknowledgement = SANDBOX_ACKNOWLEDGEMENT,
"sandboxed autonomous orchestration enabled: the model may drive writes without human review"
);
Self(())
}
}
impl fmt::Debug for SandboxAcknowledgement {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(SANDBOX_ACKNOWLEDGEMENT)
}
}
impl Serialize for SandboxAcknowledgement {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(SANDBOX_ACKNOWLEDGEMENT)
}
}
impl<'de> Deserialize<'de> for SandboxAcknowledgement {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let raw = String::deserialize(deserializer)?;
if raw == SANDBOX_ACKNOWLEDGEMENT {
Ok(Self::i_accept_unreviewed_autonomous_writes())
} else {
Err(D::Error::invalid_value(
Unexpected::Str(&raw),
&SANDBOX_ACKNOWLEDGEMENT,
))
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
#[non_exhaustive]
pub struct ResourceBudget {
pub max_model_calls: u16,
pub max_read_calls: u16,
pub max_prompt_tokens: u64,
pub max_wall_clock: Duration,
}
impl ResourceBudget {
#[must_use]
pub const fn conservative() -> Self {
Self {
max_model_calls: 8,
max_read_calls: 16,
max_prompt_tokens: 200_000,
max_wall_clock: Duration::from_secs(60),
}
}
#[must_use]
pub const fn with_max_model_calls(mut self, max_model_calls: u16) -> Self {
self.max_model_calls = max_model_calls;
self
}
#[must_use]
pub const fn with_max_read_calls(mut self, max_read_calls: u16) -> Self {
self.max_read_calls = max_read_calls;
self
}
#[must_use]
pub const fn with_max_prompt_tokens(mut self, max_prompt_tokens: u64) -> Self {
self.max_prompt_tokens = max_prompt_tokens;
self
}
#[must_use]
pub const fn with_max_wall_clock(mut self, max_wall_clock: Duration) -> Self {
self.max_wall_clock = max_wall_clock;
self
}
fn validate(&self) -> Result<(), ConfigError> {
positive(
"mode.budget.max_model_calls",
u64::from(self.max_model_calls),
)?;
positive("mode.budget.max_read_calls", u64::from(self.max_read_calls))?;
positive("mode.budget.max_prompt_tokens", self.max_prompt_tokens)?;
if self.max_wall_clock.is_zero() {
return Err(ConfigError::MustBePositive {
field: "mode.budget.max_wall_clock",
});
}
Ok(())
}
}
impl Default for ResourceBudget {
fn default() -> Self {
Self::conservative()
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
#[non_exhaustive]
pub enum OrchestrationMode {
#[default]
Deterministic,
SandboxedAutonomous {
budget: ResourceBudget,
acknowledgement: SandboxAcknowledgement,
},
}
impl OrchestrationMode {
#[must_use]
pub const fn sandboxed_autonomous(
budget: ResourceBudget,
acknowledgement: SandboxAcknowledgement,
) -> Self {
Self::SandboxedAutonomous {
budget,
acknowledgement,
}
}
#[must_use]
pub fn allows_risk(&self, risk: RiskClass) -> bool {
match self {
Self::SandboxedAutonomous { .. } => !matches!(
risk,
RiskClass::Destructive | RiskClass::Irreversible | RiskClass::ExternalRegulated
),
Self::Deterministic => true,
}
}
#[must_use]
pub fn forbidden_risk_classes(&self) -> Vec<RiskClass> {
[
RiskClass::ReadOnly,
RiskClass::ReversibleLowRisk,
RiskClass::SensitiveDataChange,
RiskClass::Destructive,
RiskClass::Irreversible,
RiskClass::ExternalRegulated,
]
.into_iter()
.filter(|risk| !self.allows_risk(*risk))
.collect()
}
#[must_use]
pub fn budget(&self) -> Option<&ResourceBudget> {
match self {
Self::SandboxedAutonomous { budget, .. } => Some(budget),
_ => None,
}
}
#[must_use]
pub fn is_sandboxed(&self) -> bool {
matches!(self, Self::SandboxedAutonomous { .. })
}
fn validate(&self) -> Result<(), ConfigError> {
if let Some(budget) = self.budget() {
budget.validate()?;
}
Ok(())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
#[non_exhaustive]
pub struct AttachmentConfig {
pub max_files: Option<usize>,
pub max_total_bytes: Option<usize>,
}
impl AttachmentConfig {
#[must_use]
pub const fn conservative() -> Self {
Self {
max_files: None,
max_total_bytes: None,
}
}
#[must_use]
pub const fn with_max_files(mut self, max_files: Option<usize>) -> Self {
self.max_files = max_files;
self
}
#[must_use]
pub const fn with_max_total_bytes(mut self, max_total_bytes: Option<usize>) -> Self {
self.max_total_bytes = max_total_bytes;
self
}
}
impl Default for AttachmentConfig {
fn default() -> Self {
Self::conservative()
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)]
#[non_exhaustive]
pub struct UnderstandingConfig {
pub plan_limits: PlanLimits,
pub turn_limits: TurnLimits,
pub transcript_turns: Option<usize>,
pub briefing_budget: BriefingBudget,
pub budget: Budget,
pub settings: Settings,
pub tasks: TaskProfiles,
}
impl UnderstandingConfig {
#[must_use]
pub const fn conservative() -> Self {
Self {
plan_limits: PlanLimits::conservative(),
turn_limits: TurnLimits::conservative(),
transcript_turns: None,
briefing_budget: BriefingBudget::conservative(),
budget: Budget::understanding(),
settings: Settings::conservative(),
tasks: TaskProfiles::new(),
}
}
#[must_use]
pub fn with_plan_limits(mut self, plan_limits: PlanLimits) -> Self {
self.plan_limits = plan_limits;
self
}
#[must_use]
pub fn with_turn_limits(mut self, turn_limits: TurnLimits) -> Self {
self.turn_limits = turn_limits;
self
}
#[must_use]
pub fn with_transcript_turns(mut self, transcript_turns: Option<usize>) -> Self {
self.transcript_turns = transcript_turns;
self
}
#[must_use]
pub fn with_briefing_budget(mut self, budget: BriefingBudget) -> Self {
self.briefing_budget = budget;
self
}
#[must_use]
pub fn with_budget(mut self, budget: Budget) -> Self {
self.budget = budget;
self
}
#[must_use]
pub fn with_settings(mut self, settings: Settings) -> Self {
self.settings = settings;
self
}
#[must_use]
pub fn with_tasks(mut self, tasks: TaskProfiles) -> Self {
self.tasks = tasks;
self
}
#[must_use]
pub const fn max_acts(&self) -> Option<usize> {
self.plan_limits.max_acts
}
#[must_use]
pub const fn max_questions(&self) -> Option<usize> {
self.plan_limits.max_questions
}
fn validate(&self) -> Result<(), ConfigError> {
for (field, limit) in [
("understanding.plan_limits.max_acts", self.max_acts()),
(
"understanding.plan_limits.max_questions",
self.max_questions(),
),
(
"understanding.plan_limits.max_constraints",
self.plan_limits.max_constraints,
),
(
"understanding.turn_limits.max_text_bytes",
self.turn_limits.max_text_bytes,
),
] {
if let Some(limit) = limit {
positive(field, limit as u64)?;
}
}
positive(
"understanding.budget.max_parallel",
self.budget.max_parallel as u64,
)?;
positive(
"understanding.budget.per_call_timeout_secs",
self.budget.per_call_timeout_secs,
)
}
}
impl Default for UnderstandingConfig {
fn default() -> Self {
Self::conservative()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
#[non_exhaustive]
pub struct NarrationConfig {
pub enabled: bool,
#[serde(default = "Budget::narration")]
pub budget: Budget,
pub tone: ToneProfile,
pub max_answer_chars: Option<usize>,
pub default_source_policy: SourcePolicy,
pub allow_retry_after_commit: bool,
#[serde(default)]
pub steps: bool,
}
impl NarrationConfig {
#[must_use]
pub const fn conservative() -> Self {
Self {
enabled: true,
budget: Budget::narration(),
tone: ToneProfile::Neutral,
max_answer_chars: None,
default_source_policy: SourcePolicy::AnySource,
allow_retry_after_commit: true,
steps: false,
}
}
#[must_use]
pub const fn with_enabled(mut self, enabled: bool) -> Self {
self.enabled = enabled;
self
}
#[must_use]
pub const fn with_budget(mut self, budget: Budget) -> Self {
self.budget = budget;
self
}
#[must_use]
pub const fn with_steps(mut self, steps: bool) -> Self {
self.steps = steps;
self
}
#[must_use]
pub const fn with_tone(mut self, tone: ToneProfile) -> Self {
self.tone = tone;
self
}
#[must_use]
pub const fn with_default_source_policy(mut self, policy: SourcePolicy) -> Self {
self.default_source_policy = policy;
self
}
#[must_use]
pub const fn with_max_answer_chars(mut self, max_answer_chars: Option<usize>) -> Self {
self.max_answer_chars = max_answer_chars;
self
}
fn validate(&self) -> Result<(), ConfigError> {
match self.max_answer_chars {
Some(max) => positive("narration.max_answer_chars", max as u64),
None => Ok(()),
}
}
}
impl Default for NarrationConfig {
fn default() -> Self {
Self::conservative()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
#[non_exhaustive]
pub struct InteractionConfig {
pub default_ttl: Option<Duration>,
pub max_selection_candidates: usize,
pub selection_cards_block_the_case: bool,
pub confirmation_cards_bind_to_revision: bool,
pub restore_card_after_failed_command: bool,
}
impl InteractionConfig {
#[must_use]
pub const fn conservative() -> Self {
Self {
default_ttl: Some(Duration::from_secs(24 * 60 * 60)),
max_selection_candidates: 8,
selection_cards_block_the_case: false,
confirmation_cards_bind_to_revision: true,
restore_card_after_failed_command: false,
}
}
#[must_use]
pub const fn restoring_failed_cards(mut self) -> Self {
self.restore_card_after_failed_command = true;
self
}
#[must_use]
pub const fn with_default_ttl(mut self, ttl: Option<Duration>) -> Self {
self.default_ttl = ttl;
self
}
#[must_use]
pub const fn with_max_selection_candidates(mut self, max: usize) -> Self {
self.max_selection_candidates = max;
self
}
fn validate(&self) -> Result<(), ConfigError> {
if self.max_selection_candidates < 2 {
return Err(ConfigError::MustBePositive {
field: "interaction.max_selection_candidates",
});
}
if self.default_ttl.is_some_and(|ttl| ttl.is_zero()) {
return Err(ConfigError::MustBePositive {
field: "interaction.default_ttl",
});
}
Ok(())
}
}
impl Default for InteractionConfig {
fn default() -> Self {
Self::conservative()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
#[non_exhaustive]
pub struct ExecutionConfig {
pub default_timeout: Duration,
pub max_commands_per_turn: usize,
pub fail_closed_on_policy_store_error: bool,
pub allow_cross_case_partial_success: bool,
pub group_mutations_per_case: bool,
}
impl ExecutionConfig {
#[must_use]
pub const fn conservative() -> Self {
Self {
default_timeout: Duration::from_secs(30),
max_commands_per_turn: 32,
fail_closed_on_policy_store_error: true,
allow_cross_case_partial_success: false,
group_mutations_per_case: true,
}
}
#[must_use]
pub const fn with_max_commands_per_turn(mut self, max: usize) -> Self {
self.max_commands_per_turn = max;
self
}
#[must_use]
pub const fn with_default_timeout(mut self, timeout: Duration) -> Self {
self.default_timeout = timeout;
self
}
fn validate(&self) -> Result<(), ConfigError> {
if self.default_timeout.is_zero() {
return Err(ConfigError::MustBePositive {
field: "execution.default_timeout",
});
}
positive(
"execution.max_commands_per_turn",
self.max_commands_per_turn as u64,
)?;
if !self.fail_closed_on_policy_store_error {
return Err(ConfigError::UnsafeSetting {
field: "execution.fail_closed_on_policy_store_error",
because: "there is no fail-open path for an unavailable policy source (I19)",
});
}
Ok(())
}
}
impl Default for ExecutionConfig {
fn default() -> Self {
Self::conservative()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
#[non_exhaustive]
pub struct ObservabilityConfig {
pub emit_metrics: bool,
pub record_replay: bool,
pub trace_sample_per_mille: u16,
}
impl ObservabilityConfig {
#[must_use]
pub const fn conservative() -> Self {
Self {
emit_metrics: true,
record_replay: true,
trace_sample_per_mille: 1000,
}
}
#[must_use]
pub const fn with_trace_sample_per_mille(mut self, per_mille: u16) -> Self {
self.trace_sample_per_mille = per_mille;
self
}
fn validate(&self) -> Result<(), ConfigError> {
if self.trace_sample_per_mille > 1000 {
return Err(ConfigError::OutOfRange {
field: "observability.trace_sample_per_mille",
});
}
Ok(())
}
}
impl Default for ObservabilityConfig {
fn default() -> Self {
Self::conservative()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
#[non_exhaustive]
pub struct PrivacyConfig {
pub allow_user_text_in_telemetry: bool,
pub store_model_prompts: bool,
pub replay_retention_days: u32,
pub redact_attachment_filenames: bool,
}
impl PrivacyConfig {
#[must_use]
pub const fn conservative() -> Self {
Self {
allow_user_text_in_telemetry: false,
store_model_prompts: false,
replay_retention_days: 90,
redact_attachment_filenames: true,
}
}
#[must_use]
pub const fn with_replay_retention_days(mut self, days: u32) -> Self {
self.replay_retention_days = days;
self
}
fn validate(&self) -> Result<(), ConfigError> {
positive(
"privacy.replay_retention_days",
u64::from(self.replay_retention_days),
)
}
}
impl Default for PrivacyConfig {
fn default() -> Self {
Self::conservative()
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
#[non_exhaustive]
pub struct OrchestratorConfig {
pub mode: OrchestrationMode,
pub understanding: UnderstandingConfig,
pub attachments: AttachmentConfig,
pub narration: NarrationConfig,
pub interaction: InteractionConfig,
pub execution: ExecutionConfig,
pub observability: ObservabilityConfig,
pub privacy: PrivacyConfig,
#[serde(default)]
pub effort: crate::effort::EffortConfig,
}
impl OrchestratorConfig {
#[must_use]
pub const fn conservative() -> Self {
Self {
mode: OrchestrationMode::Deterministic,
understanding: UnderstandingConfig::conservative(),
attachments: AttachmentConfig::conservative(),
narration: NarrationConfig::conservative(),
interaction: InteractionConfig::conservative(),
execution: ExecutionConfig::conservative(),
observability: ObservabilityConfig::conservative(),
privacy: PrivacyConfig::conservative(),
effort: crate::effort::EffortConfig::conservative(),
}
}
#[must_use]
pub fn with_effort(mut self, effort: crate::effort::EffortConfig) -> Self {
self.effort = effort;
self
}
#[must_use]
pub const fn with_attachments(mut self, attachments: AttachmentConfig) -> Self {
self.attachments = attachments;
self
}
#[must_use]
pub fn with_mode(mut self, mode: OrchestrationMode) -> Self {
self.mode = mode;
self
}
#[must_use]
pub fn with_understanding(mut self, understanding: UnderstandingConfig) -> Self {
self.understanding = understanding;
self
}
#[must_use]
pub const fn with_narration(mut self, narration: NarrationConfig) -> Self {
self.narration = narration;
self
}
#[must_use]
pub const fn with_interaction(mut self, interaction: InteractionConfig) -> Self {
self.interaction = interaction;
self
}
#[must_use]
pub const fn with_execution(mut self, execution: ExecutionConfig) -> Self {
self.execution = execution;
self
}
#[must_use]
pub const fn with_observability(mut self, observability: ObservabilityConfig) -> Self {
self.observability = observability;
self
}
#[must_use]
pub const fn with_privacy(mut self, privacy: PrivacyConfig) -> Self {
self.privacy = privacy;
self
}
#[must_use]
pub fn policy_snapshot(&self, base: PolicySnapshot) -> PolicySnapshot {
let mut snapshot = base;
for risk in self.mode.forbidden_risk_classes() {
if !snapshot.forbidden_risk_classes.contains(&risk) {
snapshot.forbidden_risk_classes.push(risk);
}
}
snapshot.forbidden_risk_classes.sort_unstable();
snapshot
}
pub fn validate(&self) -> Result<(), ConfigError> {
self.mode.validate()?;
self.understanding.validate()?;
self.narration.validate()?;
self.interaction.validate()?;
self.execution.validate()?;
self.observability.validate()?;
self.privacy.validate()?;
self.validate_effort()?;
if self.privacy.store_model_prompts && !self.observability.record_replay {
return Err(ConfigError::Contradiction {
first: "privacy.store_model_prompts",
second: "observability.record_replay",
});
}
if self.mode.is_sandboxed() && !self.observability.record_replay {
return Err(ConfigError::Contradiction {
first: "mode.sandboxed_autonomous",
second: "observability.record_replay",
});
}
Ok(())
}
}
impl Default for OrchestratorConfig {
fn default() -> Self {
Self::conservative()
}
}
impl OrchestratorConfig {
fn validate_effort(&self) -> Result<(), ConfigError> {
use turnframe_core::effort::Effort;
const FIELDS: [(Effort, [&str; 4]); 3] = [
(
Effort::Low,
[
"effort.low.budget.max_parallel",
"effort.low.budget.per_call_timeout_secs",
"effort.low.reply_budget.max_parallel",
"effort.low.reply_budget.per_call_timeout_secs",
],
),
(
Effort::Medium,
[
"effort.medium.budget.max_parallel",
"effort.medium.budget.per_call_timeout_secs",
"effort.medium.reply_budget.max_parallel",
"effort.medium.reply_budget.per_call_timeout_secs",
],
),
(
Effort::High,
[
"effort.high.budget.max_parallel",
"effort.high.budget.per_call_timeout_secs",
"effort.high.reply_budget.max_parallel",
"effort.high.reply_budget.per_call_timeout_secs",
],
),
];
for (effort, fields) in FIELDS {
let overrides = self.effort.overrides(effort);
for (budget, [parallel, timeout]) in [
(overrides.budget, [fields[0], fields[1]]),
(overrides.reply_budget, [fields[2], fields[3]]),
] {
if let Some(budget) = budget {
positive(parallel, budget.max_parallel as u64)?;
positive(timeout, budget.per_call_timeout_secs)?;
}
}
}
Ok(())
}
}
fn positive(field: &'static str, value: u64) -> Result<(), ConfigError> {
if value == 0 {
Err(ConfigError::MustBePositive { field })
} else {
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn sandbox() -> OrchestrationMode {
OrchestrationMode::sandboxed_autonomous(
ResourceBudget::conservative(),
SandboxAcknowledgement::i_accept_unreviewed_autonomous_writes(),
)
}
#[test]
fn mode_risk_table() {
let sandboxed = sandbox();
let table = [
(RiskClass::ReadOnly, true),
(RiskClass::ReversibleLowRisk, true),
(RiskClass::SensitiveDataChange, true),
(RiskClass::Destructive, false),
(RiskClass::Irreversible, false),
(RiskClass::ExternalRegulated, false),
];
for (risk, sandboxed_allows) in table {
assert!(OrchestrationMode::Deterministic.allows_risk(risk));
assert_eq!(sandboxed.allows_risk(risk), sandboxed_allows, "{risk:?}");
}
assert!(
OrchestrationMode::Deterministic
.forbidden_risk_classes()
.is_empty()
);
}
#[test]
fn mode_shape_helpers() {
assert!(sandbox().is_sandboxed());
assert_eq!(sandbox().budget(), Some(&ResourceBudget::conservative()));
assert_eq!(OrchestrationMode::Deterministic.budget(), None);
assert_eq!(
OrchestrationMode::default(),
OrchestrationMode::Deterministic
);
}
#[test]
fn a_sandbox_tightens_the_policy_snapshot_and_never_widens_it() {
let config = OrchestratorConfig::conservative().with_mode(sandbox());
let snapshot = config.policy_snapshot(PolicySnapshot::conservative());
assert!(snapshot.is_risk_forbidden(RiskClass::Destructive));
assert!(snapshot.is_risk_forbidden(RiskClass::ExternalRegulated));
assert!(!snapshot.is_risk_forbidden(RiskClass::ReversibleLowRisk));
let strict = PolicySnapshot::sandbox();
let merged = OrchestratorConfig::conservative().policy_snapshot(strict.clone());
assert_eq!(
merged.forbidden_risk_classes.len(),
strict.forbidden_risk_classes.len()
);
assert!(merged.is_risk_forbidden(RiskClass::SensitiveDataChange));
}
#[test]
fn the_acknowledgement_is_the_only_way_into_the_sandbox() {
let json = serde_json::to_value(sandbox()).unwrap();
assert_eq!(json["kind"], "sandboxed_autonomous");
assert_eq!(json["acknowledgement"], SANDBOX_ACKNOWLEDGEMENT);
let back: OrchestrationMode = serde_json::from_value(json).unwrap();
assert_eq!(back, sandbox());
let mut wrong = serde_json::to_value(sandbox()).unwrap();
wrong["acknowledgement"] = serde_json::json!("sure why not");
assert!(serde_json::from_value::<OrchestrationMode>(wrong).is_err());
let mut missing = serde_json::to_value(sandbox()).unwrap();
missing
.as_object_mut()
.unwrap()
.remove("acknowledgement")
.unwrap();
assert!(serde_json::from_value::<OrchestrationMode>(missing).is_err());
assert_eq!(
format!(
"{:?}",
SandboxAcknowledgement::i_accept_unreviewed_autonomous_writes()
),
SANDBOX_ACKNOWLEDGEMENT
);
}
#[test]
fn conservative_config_validates_and_round_trips() {
let config = OrchestratorConfig::conservative();
assert_eq!(config.validate(), Ok(()));
assert_eq!(config, OrchestratorConfig::default());
let json = serde_json::to_string(&config).unwrap();
let back: OrchestratorConfig = serde_json::from_str(&json).unwrap();
assert_eq!(back, config);
assert!(serde_json::from_str::<OrchestratorConfig>(r#"{"extra": 1}"#).is_err());
assert_eq!(config.understanding.max_acts(), None);
assert_eq!(config.understanding.max_questions(), None);
}
#[test]
fn config_validation_table() {
let base = OrchestratorConfig::conservative();
let cases: Vec<(&str, OrchestratorConfig, Option<ConfigError>)> = vec![
("conservative", base.clone(), None),
(
"no acts allowed",
base.clone().with_understanding(
UnderstandingConfig::conservative()
.with_plan_limits(PlanLimits::conservative().with_max_acts(Some(0))),
),
Some(ConfigError::MustBePositive {
field: "understanding.plan_limits.max_acts",
}),
),
(
"silent answers",
base.clone().with_narration(NarrationConfig {
max_answer_chars: Some(0),
..NarrationConfig::conservative()
}),
Some(ConfigError::MustBePositive {
field: "narration.max_answer_chars",
}),
),
(
"selection with one candidate",
base.clone().with_interaction(
InteractionConfig::conservative().with_max_selection_candidates(1),
),
Some(ConfigError::MustBePositive {
field: "interaction.max_selection_candidates",
}),
),
(
"cards expiring instantly",
base.clone().with_interaction(
InteractionConfig::conservative().with_default_ttl(Some(Duration::ZERO)),
),
Some(ConfigError::MustBePositive {
field: "interaction.default_ttl",
}),
),
(
"no command budget",
base.clone()
.with_execution(ExecutionConfig::conservative().with_max_commands_per_turn(0)),
Some(ConfigError::MustBePositive {
field: "execution.max_commands_per_turn",
}),
),
(
"instant timeout",
base.clone().with_execution(
ExecutionConfig::conservative().with_default_timeout(Duration::ZERO),
),
Some(ConfigError::MustBePositive {
field: "execution.default_timeout",
}),
),
(
"fail open on policy",
base.clone().with_execution(ExecutionConfig {
fail_closed_on_policy_store_error: false,
..ExecutionConfig::conservative()
}),
Some(ConfigError::UnsafeSetting {
field: "execution.fail_closed_on_policy_store_error",
because: "there is no fail-open path for an unavailable policy source (I19)",
}),
),
(
"impossible sampling",
base.clone().with_observability(
ObservabilityConfig::conservative().with_trace_sample_per_mille(1001),
),
Some(ConfigError::OutOfRange {
field: "observability.trace_sample_per_mille",
}),
),
(
"no retention",
base.clone()
.with_privacy(PrivacyConfig::conservative().with_replay_retention_days(0)),
Some(ConfigError::MustBePositive {
field: "privacy.replay_retention_days",
}),
),
(
"prompts stored but no replay",
base.clone()
.with_privacy(PrivacyConfig {
store_model_prompts: true,
..PrivacyConfig::conservative()
})
.with_observability(ObservabilityConfig {
record_replay: false,
..ObservabilityConfig::conservative()
}),
Some(ConfigError::Contradiction {
first: "privacy.store_model_prompts",
second: "observability.record_replay",
}),
),
(
"sandbox without replay",
base.clone()
.with_mode(sandbox())
.with_observability(ObservabilityConfig {
record_replay: false,
..ObservabilityConfig::conservative()
}),
Some(ConfigError::Contradiction {
first: "mode.sandboxed_autonomous",
second: "observability.record_replay",
}),
),
(
"empty sandbox budget",
base.with_mode(OrchestrationMode::sandboxed_autonomous(
ResourceBudget::conservative().with_max_model_calls(0),
SandboxAcknowledgement::i_accept_unreviewed_autonomous_writes(),
)),
Some(ConfigError::MustBePositive {
field: "mode.budget.max_model_calls",
}),
),
];
for (name, config, expected) in cases {
assert_eq!(config.validate().err(), expected, "case {name}");
}
}
#[test]
fn budget_builders_and_defaults() {
let budget = ResourceBudget::default()
.with_max_read_calls(3)
.with_max_prompt_tokens(10)
.with_max_wall_clock(Duration::from_secs(5));
assert_eq!(budget.max_read_calls, 3);
assert_eq!(budget.max_prompt_tokens, 10);
assert_eq!(budget.max_wall_clock, Duration::from_secs(5));
assert_eq!(budget.max_model_calls, 8);
assert_eq!(
ResourceBudget::conservative()
.with_max_wall_clock(Duration::ZERO)
.validate(),
Err(ConfigError::MustBePositive {
field: "mode.budget.max_wall_clock"
})
);
}
#[test]
fn section_builders_keep_the_rest() {
let narration = NarrationConfig::conservative()
.with_tone(ToneProfile::Neutral)
.with_default_source_policy(SourcePolicy::AuthoritativeOnly);
assert_eq!(
narration.default_source_policy,
SourcePolicy::AuthoritativeOnly
);
assert!(narration.enabled);
let understanding =
UnderstandingConfig::conservative().with_turn_limits(TurnLimits::conservative());
assert_eq!(understanding.budget, Budget::understanding());
assert_eq!(
UnderstandingConfig::default(),
UnderstandingConfig::conservative()
);
assert_eq!(NarrationConfig::default(), NarrationConfig::conservative());
assert_eq!(
InteractionConfig::default(),
InteractionConfig::conservative()
);
assert_eq!(ExecutionConfig::default(), ExecutionConfig::conservative());
assert_eq!(
ObservabilityConfig::default(),
ObservabilityConfig::conservative()
);
assert_eq!(PrivacyConfig::default(), PrivacyConfig::conservative());
}
}