use std::collections::HashMap;
use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
use crate::error::HirnError;
use crate::offline::OfflineSchedulerConfig;
use crate::resource::{
DerivedArtifactIndexPolicy, ResourceIndexPolicy, ResourceQuotaPolicy, ResourceRetentionPolicy,
};
use crate::types::Namespace;
macro_rules! hirn_config_fields {
(
$(
$( #[doc = $doc:literal] )*
pub $field:ident : $ty:ty,
)*
) => {
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(try_from = "RawHirnConfig")]
pub struct HirnConfig {
$(
$( #[doc = $doc] )*
pub $field : $ty,
)*
}
#[derive(Deserialize)]
#[serde(default)]
struct RawHirnConfig {
$( $field : $ty, )*
}
impl Default for RawHirnConfig {
fn default() -> Self {
let d = HirnConfig::default();
Self {
$( $field: d.$field, )*
}
}
}
impl TryFrom<RawHirnConfig> for HirnConfig {
type Error = HirnError;
fn try_from(raw: RawHirnConfig) -> Result<Self, Self::Error> {
let config = Self {
$( $field: raw.$field, )*
};
config.validate()?;
Ok(config)
}
}
};
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum TextRetention {
#[default]
Full,
SummaryOnly,
None,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum DistanceMetric {
#[default]
Cosine,
DotProduct,
#[serde(rename = "l2")]
L2,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(default)]
pub struct EmbedderRuntimeConfig {
pub batch_size: Option<usize>,
pub retry: Option<EmbedderRetryConfig>,
pub circuit_breaker: Option<EmbedderCircuitBreakerRuntimeConfig>,
pub persistent_cache: Option<EmbedderPersistentCacheRuntimeConfig>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default)]
pub struct EmbedderRetryConfig {
pub max_retries: u32,
pub base_backoff_ms: u64,
pub max_cumulative_timeout_ms: u64,
}
impl Default for EmbedderRetryConfig {
fn default() -> Self {
Self {
max_retries: 3,
base_backoff_ms: 500,
max_cumulative_timeout_ms: 10_000,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default)]
pub struct EmbedderCircuitBreakerRuntimeConfig {
pub failure_threshold: u32,
pub recovery_timeout_ms: u64,
pub success_threshold: u32,
}
impl Default for EmbedderCircuitBreakerRuntimeConfig {
fn default() -> Self {
Self {
failure_threshold: 5,
recovery_timeout_ms: 30_000,
success_threshold: 2,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default)]
pub struct EmbedderPersistentCacheRuntimeConfig {
pub max_memory_entries: usize,
}
impl Default for EmbedderPersistentCacheRuntimeConfig {
fn default() -> Self {
Self {
max_memory_entries: 10_000,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
#[serde(default)]
pub struct ConflictResolutionPolicy {
pub recency_weight: f32,
pub source_reliability_weight: f32,
pub supporting_evidence_weight: f32,
pub human_override_weight: f32,
pub prefer_human_override: bool,
}
impl Default for ConflictResolutionPolicy {
fn default() -> Self {
Self {
recency_weight: 0.20,
source_reliability_weight: 0.35,
supporting_evidence_weight: 0.30,
human_override_weight: 0.15,
prefer_human_override: true,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
#[serde(default)]
pub struct ConflictResolutionPolicyOverrides {
pub by_realm: HashMap<String, ConflictResolutionPolicy>,
pub by_namespace: HashMap<String, ConflictResolutionPolicy>,
}
const MAX_CONSOLIDATION_CAUSAL_WINDOW: usize = 10_000;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case", tag = "mode")]
pub enum EvolutionMode {
#[default]
None,
Async {
max_neighbors: usize,
},
Synchronous {
max_neighbors: usize,
},
}
hirn_config_fields! {
pub db_path: PathBuf,
pub working_memory_token_limit: u32,
pub working_memory_reserve: f32,
pub decay_lambda: f64,
pub archive_threshold: f32,
pub purge_threshold: f32,
pub max_episodic_entries: u32,
pub hebbian_learning_rate: f64,
pub hebbian_decay_rate: f64,
pub token_budget: u32,
pub recall_preview_package_max_previews: usize,
pub recall_preview_package_max_chars: usize,
pub recall_preview_rerank_max_previews: usize,
pub recall_preview_rerank_max_chars: usize,
pub think_preview_package_max_previews: usize,
pub think_preview_package_max_chars: usize,
pub hnsw_m: usize,
pub hnsw_ef_construction: usize,
pub hnsw_ef_search: usize,
pub embedding_dimensions: crate::EmbeddingDimension,
pub allow_pseudo_embedder_fallback: bool,
pub embedder_runtime: EmbedderRuntimeConfig,
pub metric: DistanceMetric,
pub scoring_similarity_weight: f32,
pub scoring_importance_weight: f32,
pub scoring_recency_weight: f32,
pub scoring_activation_weight: f32,
pub scoring_causal_relevance_weight: f32,
pub scoring_surprise_weight: f32,
pub scoring_source_reliability_weight: f32,
pub activation_decay_factor: f64,
pub activation_max_depth: usize,
pub activation_convergence_threshold: f64,
pub activation_max_iterations: usize,
pub inhibition_strength: f64,
pub activation_max_frontier_size: usize,
pub similarity_edge_threshold: f32,
pub max_auto_edges_per_record: usize,
pub entity_overlap_threshold: usize,
pub graph_depth_delegation_threshold: usize,
pub graph_activation_epsilon: f32,
pub graph_activation_inhibition_mu: f32,
pub causal_min_confidence: f32,
pub default_token_budget: usize,
pub consolidation_interval_secs: u64,
pub consolidation_causal_window: usize,
pub reconsolidation_window_secs: u64,
pub compaction_interval_secs: u64,
pub compaction_fragment_threshold: u32,
pub admission_enabled: bool,
pub admission_surprise_threshold: f32,
pub admission_duplicate_threshold: f32,
pub admission_duplicate_action: String,
pub admission_token_budget_limit: u64,
pub admission_rate_limit: u32,
pub rpe_enabled: bool,
pub rpe_fast_path_threshold: f32,
pub rpe_similarity_search_limit: usize,
pub prospective_indexing_enabled: bool,
pub prospective_indexing_num_questions: usize,
pub prospective_indexing_timeout_secs: u64,
pub prospective_indexing_templates: Vec<String>,
pub svo_extraction_enabled: bool,
pub svo_confidence_threshold: f32,
pub svo_extraction_prompt: String,
pub interference_consolidation_threshold: f32,
pub interference_consolidation_cooldown_secs: u64,
pub multivector_enabled: bool,
pub multivector_weight: f32,
pub prefetch_enabled: bool,
pub prefetch_activation_depth: usize,
pub prefetch_min_edge_weight: f32,
pub prefetch_max_bytes: u64,
pub prefetch_cooldown_secs: u64,
pub default_realm: String,
pub conflict_resolution_policy: ConflictResolutionPolicy,
pub conflict_resolution_overrides: ConflictResolutionPolicyOverrides,
pub slow_query_threshold_ms: u64,
pub quality_gate_threshold: f32,
pub nli_contradiction_threshold: f32,
pub text_retention: TextRetention,
pub resource_retention_policy: ResourceRetentionPolicy,
pub resource_quota_policy: ResourceQuotaPolicy,
pub resource_index_policy: ResourceIndexPolicy,
pub derived_artifact_index_policy: DerivedArtifactIndexPolicy,
pub offline_scheduler: OfflineSchedulerConfig,
pub offline_dream_quality_threshold: f32,
pub offline_reconcile_quality_threshold: f32,
pub offline_plan_quality_threshold: f32,
pub evolution_mode: EvolutionMode,
pub decay_interval_secs: u64,
pub decay_sweep_window_secs: u64,
pub execution_memory_limit_bytes: u64,
pub execution_parallelism: usize,
pub memory_decay_factor: f32,
pub memory_half_life_hours: u64,
pub memory_min_importance: f32,
pub tier_working_to_episodic_ttl_secs: u64,
pub tier_episodic_to_semantic_threshold: f32,
pub tier_semantic_archive_threshold: f32,
pub tier_procedural_min_success_rate: f32,
pub event_hmac_secret: Option<String>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct TierPolicy {
pub working_to_episodic_ttl_secs: u64,
pub episodic_to_semantic_threshold: f32,
pub semantic_archive_threshold: f32,
pub procedural_min_success_rate: f32,
}
impl TierPolicy {
#[must_use]
pub fn from_config(cfg: &HirnConfig) -> Self {
Self {
working_to_episodic_ttl_secs: cfg.tier_working_to_episodic_ttl_secs,
episodic_to_semantic_threshold: cfg.tier_episodic_to_semantic_threshold,
semantic_archive_threshold: cfg.tier_semantic_archive_threshold,
procedural_min_success_rate: cfg.tier_procedural_min_success_rate,
}
}
}
impl Default for TierPolicy {
fn default() -> Self {
Self {
working_to_episodic_ttl_secs: 0,
episodic_to_semantic_threshold: 0.7,
semantic_archive_threshold: 0.1,
procedural_min_success_rate: 0.3,
}
}
}
impl Default for HirnConfig {
fn default() -> Self {
Self {
db_path: PathBuf::from("brain"),
working_memory_token_limit: 2048,
working_memory_reserve: 0.2,
decay_lambda: 0.01, archive_threshold: 0.1,
purge_threshold: 0.01,
max_episodic_entries: 100,
hebbian_learning_rate: 0.1,
hebbian_decay_rate: 0.05,
token_budget: 4096,
recall_preview_package_max_previews: 1,
recall_preview_package_max_chars: 160,
recall_preview_rerank_max_previews: 2,
recall_preview_rerank_max_chars: 240,
think_preview_package_max_previews: 1,
think_preview_package_max_chars: 160,
hnsw_m: 16,
hnsw_ef_construction: 200,
hnsw_ef_search: 50,
embedding_dimensions: crate::EmbeddingDimension::new_const(768),
allow_pseudo_embedder_fallback: false,
embedder_runtime: EmbedderRuntimeConfig::default(),
metric: DistanceMetric::Cosine,
scoring_similarity_weight: 0.30,
scoring_importance_weight: 0.20,
scoring_recency_weight: 0.20,
scoring_activation_weight: 0.10,
scoring_causal_relevance_weight: 0.05,
scoring_surprise_weight: 0.10,
scoring_source_reliability_weight: 0.05,
activation_decay_factor: 0.7,
activation_max_depth: 3,
activation_convergence_threshold: 0.01,
activation_max_iterations: 10,
inhibition_strength: 0.1,
activation_max_frontier_size: 10_000,
similarity_edge_threshold: 0.85,
max_auto_edges_per_record: 10,
entity_overlap_threshold: 2,
graph_depth_delegation_threshold: 5,
graph_activation_epsilon: 0.001,
graph_activation_inhibition_mu: 0.5,
causal_min_confidence: 0.3,
default_token_budget: 4096,
consolidation_interval_secs: 3600,
consolidation_causal_window: 100,
reconsolidation_window_secs: 3600,
compaction_interval_secs: 3600,
compaction_fragment_threshold: 0,
admission_enabled: false,
admission_surprise_threshold: default_surprise_threshold(),
admission_duplicate_threshold: default_duplicate_threshold(),
admission_duplicate_action: default_duplicate_action(),
admission_token_budget_limit: default_token_budget_limit(),
admission_rate_limit: default_rate_limit(),
rpe_enabled: false,
rpe_fast_path_threshold: 0.3,
rpe_similarity_search_limit: 5,
prospective_indexing_enabled: false,
prospective_indexing_num_questions: 5,
prospective_indexing_timeout_secs: 5,
prospective_indexing_templates: default_prospective_templates(),
svo_extraction_enabled: false,
svo_confidence_threshold: 0.5,
svo_extraction_prompt: default_svo_extraction_prompt(),
interference_consolidation_threshold: 0.3,
interference_consolidation_cooldown_secs: 300,
multivector_enabled: false,
multivector_weight: 0.0,
prefetch_enabled: false,
prefetch_activation_depth: default_prefetch_activation_depth(),
prefetch_min_edge_weight: default_prefetch_min_edge_weight(),
prefetch_max_bytes: default_prefetch_max_bytes(),
prefetch_cooldown_secs: default_prefetch_cooldown_secs(),
default_realm: default_realm(),
conflict_resolution_policy: ConflictResolutionPolicy::default(),
conflict_resolution_overrides: ConflictResolutionPolicyOverrides::default(),
slow_query_threshold_ms: default_slow_query_threshold_ms(),
quality_gate_threshold: 0.5,
nli_contradiction_threshold: 0.7,
text_retention: TextRetention::default(),
resource_retention_policy: ResourceRetentionPolicy::default(),
resource_quota_policy: ResourceQuotaPolicy::default(),
resource_index_policy: ResourceIndexPolicy::default(),
derived_artifact_index_policy: DerivedArtifactIndexPolicy::default(),
offline_scheduler: OfflineSchedulerConfig::default(),
offline_dream_quality_threshold: 0.55,
offline_reconcile_quality_threshold: 0.6,
offline_plan_quality_threshold: 0.45,
evolution_mode: EvolutionMode::None,
decay_interval_secs: 0,
decay_sweep_window_secs: 86_400,
execution_memory_limit_bytes: 0,
execution_parallelism: 0,
memory_decay_factor: default_memory_decay_factor(),
memory_half_life_hours: default_memory_half_life_hours(),
memory_min_importance: default_memory_min_importance(),
tier_working_to_episodic_ttl_secs: 0,
tier_episodic_to_semantic_threshold: 0.7,
tier_semantic_archive_threshold: 0.1,
tier_procedural_min_success_rate: 0.3,
event_hmac_secret: None,
}
}
}
impl HirnConfig {
#[must_use]
pub fn builder() -> HirnConfigBuilder {
HirnConfigBuilder(Self::default())
}
pub fn validate(&self) -> Result<(), HirnError> {
fn invalid_config(field: &str, value: impl std::fmt::Display, reason: &str) -> HirnError {
HirnError::InvalidConfig {
field: field.to_string(),
value: value.to_string(),
reason: reason.to_string(),
}
}
if self.archive_threshold <= self.purge_threshold {
return Err(HirnError::InvalidInput(
"archive_threshold must be strictly greater than purge_threshold".into(),
));
}
if self.decay_lambda <= 0.0 {
return Err(HirnError::InvalidInput(
"decay_lambda must be > 0.0 (zero means no recency scoring)".into(),
));
}
if self.hebbian_learning_rate < 0.0 {
return Err(HirnError::InvalidInput(
"hebbian_learning_rate must be non-negative".into(),
));
}
if self.hebbian_decay_rate < 0.0 {
return Err(HirnError::InvalidInput(
"hebbian_decay_rate must be non-negative".into(),
));
}
if !(0.0..=1.0).contains(&self.working_memory_reserve) {
return Err(HirnError::InvalidInput(
"working_memory_reserve must be in [0.0, 1.0]".into(),
));
}
if self.token_budget == 0 {
return Err(HirnError::InvalidInput("token_budget must be > 0".into()));
}
self.resource_retention_policy.validate()?;
self.resource_quota_policy.validate()?;
self.resource_index_policy.validate()?;
self.derived_artifact_index_policy.validate()?;
self.offline_scheduler.validate("offline_scheduler")?;
if matches!(self.embedder_runtime.batch_size, Some(0)) {
return Err(invalid_config(
"embedder_runtime.batch_size",
0,
"must be >= 1 when batching is enabled",
));
}
if let Some(retry) = self.embedder_runtime.retry.as_ref() {
if retry.base_backoff_ms == 0 {
return Err(invalid_config(
"embedder_runtime.retry.base_backoff_ms",
retry.base_backoff_ms,
"must be > 0 when retry is enabled",
));
}
if retry.max_cumulative_timeout_ms == 0 {
return Err(invalid_config(
"embedder_runtime.retry.max_cumulative_timeout_ms",
retry.max_cumulative_timeout_ms,
"must be > 0 when retry is enabled",
));
}
}
if let Some(circuit_breaker) = self.embedder_runtime.circuit_breaker.as_ref() {
if circuit_breaker.failure_threshold == 0 {
return Err(invalid_config(
"embedder_runtime.circuit_breaker.failure_threshold",
circuit_breaker.failure_threshold,
"must be > 0 when circuit breaking is enabled",
));
}
if circuit_breaker.recovery_timeout_ms == 0 {
return Err(invalid_config(
"embedder_runtime.circuit_breaker.recovery_timeout_ms",
circuit_breaker.recovery_timeout_ms,
"must be > 0 when circuit breaking is enabled",
));
}
if circuit_breaker.success_threshold == 0 {
return Err(invalid_config(
"embedder_runtime.circuit_breaker.success_threshold",
circuit_breaker.success_threshold,
"must be > 0 when circuit breaking is enabled",
));
}
}
if let Some(cache) = self.embedder_runtime.persistent_cache.as_ref() {
if cache.max_memory_entries == 0 {
return Err(invalid_config(
"embedder_runtime.persistent_cache.max_memory_entries",
cache.max_memory_entries,
"must be > 0 when persistent caching is enabled",
));
}
}
let weight_sum = self.scoring_similarity_weight
+ self.scoring_importance_weight
+ self.scoring_recency_weight
+ self.scoring_activation_weight
+ self.scoring_causal_relevance_weight
+ self.scoring_surprise_weight
+ self.scoring_source_reliability_weight;
if (weight_sum - 1.0).abs() > 1e-4 {
return Err(HirnError::InvalidInput(format!(
"scoring weights must sum to 1.0, got {weight_sum}"
)));
}
for (name, w) in [
("scoring_similarity_weight", self.scoring_similarity_weight),
("scoring_importance_weight", self.scoring_importance_weight),
("scoring_recency_weight", self.scoring_recency_weight),
("scoring_activation_weight", self.scoring_activation_weight),
(
"scoring_causal_relevance_weight",
self.scoring_causal_relevance_weight,
),
("scoring_surprise_weight", self.scoring_surprise_weight),
(
"scoring_source_reliability_weight",
self.scoring_source_reliability_weight,
),
] {
if !(0.0..=1.0).contains(&w) {
return Err(HirnError::InvalidInput(format!(
"{name} must be in [0.0, 1.0], got {w}"
)));
}
}
if self.hnsw_m < 2 {
return Err(HirnError::InvalidInput("hnsw_m must be >= 2".into()));
}
if self.hnsw_ef_construction == 0 {
return Err(HirnError::InvalidInput(
"hnsw_ef_construction must be > 0".into(),
));
}
if self.hnsw_ef_search == 0 {
return Err(HirnError::InvalidInput("hnsw_ef_search must be > 0".into()));
}
if self.consolidation_causal_window > MAX_CONSOLIDATION_CAUSAL_WINDOW {
return Err(invalid_config(
"consolidation_causal_window",
self.consolidation_causal_window,
"must be 0 or in 1..=10000",
));
}
if !(0.0..=1.0).contains(&self.memory_decay_factor) {
return Err(HirnError::InvalidInput(
"memory_decay_factor must be in [0.0, 1.0]".into(),
));
}
if self.memory_half_life_hours == 0 {
return Err(HirnError::InvalidInput(
"memory_half_life_hours must be > 0".into(),
));
}
if self.memory_min_importance < 0.0 {
return Err(HirnError::InvalidInput(
"memory_min_importance must be >= 0.0".into(),
));
}
if self.rpe_fast_path_threshold < 0.0 || self.rpe_fast_path_threshold > 2.0 {
return Err(HirnError::InvalidInput(
"rpe_fast_path_threshold must be in [0.0, 2.0]".into(),
));
}
if !(0.0..=1.0).contains(&self.svo_confidence_threshold) {
return Err(HirnError::InvalidInput(
"svo_confidence_threshold must be in [0.0, 1.0]".into(),
));
}
if self.svo_extraction_enabled && !self.svo_extraction_prompt.contains("{content}") {
return Err(HirnError::InvalidInput(
"svo_extraction_prompt must contain {content} placeholder".into(),
));
}
for template in &self.prospective_indexing_templates {
if !template.contains("{content}") {
return Err(invalid_config(
"prospective_indexing_templates",
template,
"every template must contain the {content} placeholder",
));
}
}
if self.interference_consolidation_threshold < 0.0 {
return Err(HirnError::InvalidInput(
"interference_consolidation_threshold must be >= 0.0".into(),
));
}
if !(0.0..=1.0).contains(&self.quality_gate_threshold) {
return Err(HirnError::InvalidInput(
"quality_gate_threshold must be in [0.0, 1.0]".into(),
));
}
if !(0.0..=1.0).contains(&self.nli_contradiction_threshold) {
return Err(HirnError::InvalidInput(
"nli_contradiction_threshold must be in [0.0, 1.0]".into(),
));
}
if !(0.0..=1.0).contains(&self.offline_dream_quality_threshold) {
return Err(HirnError::InvalidInput(
"offline_dream_quality_threshold must be in [0.0, 1.0]".into(),
));
}
if !(0.0..=1.0).contains(&self.offline_reconcile_quality_threshold) {
return Err(HirnError::InvalidInput(
"offline_reconcile_quality_threshold must be in [0.0, 1.0]".into(),
));
}
if !(0.0..=1.0).contains(&self.offline_plan_quality_threshold) {
return Err(HirnError::InvalidInput(
"offline_plan_quality_threshold must be in [0.0, 1.0]".into(),
));
}
validate_conflict_resolution_policy(
&self.conflict_resolution_policy,
"conflict_resolution_policy",
)?;
for (realm, policy) in &self.conflict_resolution_overrides.by_realm {
if realm.trim().is_empty() {
return Err(invalid_config(
"conflict_resolution_overrides.by_realm",
realm,
"realm keys must be non-empty",
));
}
validate_conflict_resolution_policy(
policy,
&format!("conflict_resolution_overrides.by_realm.{realm}"),
)?;
}
for (namespace, policy) in &self.conflict_resolution_overrides.by_namespace {
Namespace::new(namespace).map_err(|_| {
invalid_config(
"conflict_resolution_overrides.by_namespace",
namespace,
"namespace override keys must be valid namespace identifiers",
)
})?;
validate_conflict_resolution_policy(
policy,
&format!("conflict_resolution_overrides.by_namespace.{namespace}"),
)?;
}
if !(0.0..=1.0).contains(&self.tier_episodic_to_semantic_threshold) {
return Err(HirnError::InvalidInput(
"tier_episodic_to_semantic_threshold must be in [0.0, 1.0]".into(),
));
}
if !(0.0..=1.0).contains(&self.tier_semantic_archive_threshold) {
return Err(HirnError::InvalidInput(
"tier_semantic_archive_threshold must be in [0.0, 1.0]".into(),
));
}
if !(0.0..=1.0).contains(&self.tier_procedural_min_success_rate) {
return Err(HirnError::InvalidInput(
"tier_procedural_min_success_rate must be in [0.0, 1.0]".into(),
));
}
if !(0.0..=1.0).contains(&self.activation_decay_factor) {
return Err(HirnError::InvalidInput(
"activation_decay_factor must be in (0.0, 1.0]".into(),
));
}
if self.activation_convergence_threshold <= 0.0 {
return Err(HirnError::InvalidInput(
"activation_convergence_threshold must be > 0.0".into(),
));
}
if self.inhibition_strength < 0.0 {
return Err(HirnError::InvalidInput(
"inhibition_strength must be >= 0.0".into(),
));
}
if !(0.0..=1.0).contains(&self.similarity_edge_threshold) {
return Err(HirnError::InvalidInput(
"similarity_edge_threshold must be in [0.0, 1.0]".into(),
));
}
if !(0.0..=1.0).contains(&self.multivector_weight) {
return Err(HirnError::InvalidInput(
"multivector_weight must be in [0.0, 1.0]".into(),
));
}
if !(0.0..=1.0).contains(&self.causal_min_confidence) {
return Err(HirnError::InvalidInput(
"causal_min_confidence must be in [0.0, 1.0]".into(),
));
}
if self.default_token_budget == 0 {
return Err(HirnError::InvalidInput(
"default_token_budget must be > 0".into(),
));
}
if let Some(ref secret) = self.event_hmac_secret {
if secret.len() < 32 {
return Err(HirnError::InvalidConfig {
field: "event_hmac_secret".to_string(),
value: format!("{} bytes", secret.len()),
reason: "must be at least 32 bytes for HMAC integrity; use a cryptographically random secret".to_string(),
});
}
}
{
let path = &self.db_path;
if path.exists() {
if !path.is_dir() {
return Err(invalid_config(
"db_path",
path.display(),
"path exists but is not a directory; provide a directory path",
));
}
} else if let Some(parent) = path.parent() {
if !parent.as_os_str().is_empty() && parent.exists() && !parent.is_dir() {
return Err(invalid_config(
"db_path",
path.display(),
"parent path exists but is not a directory; check db_path",
));
}
}
}
Ok(())
}
#[must_use]
pub fn event_hmac_key(&self) -> Option<&[u8]> {
self.event_hmac_secret.as_deref().map(str::as_bytes)
}
#[must_use]
pub fn warnings(&self) -> Vec<String> {
let mut warnings = Vec::new();
if self.hnsw_ef_search < self.hnsw_m {
warnings.push(format!(
"hnsw_ef_search ({}) < hnsw_m ({}); search quality may be poor",
self.hnsw_ef_search, self.hnsw_m
));
}
if self.hnsw_ef_construction < self.hnsw_ef_search {
warnings.push(format!(
"hnsw_ef_construction ({}) < hnsw_ef_search ({}); build quality lower than query quality",
self.hnsw_ef_construction, self.hnsw_ef_search
));
}
warnings
}
}
const fn default_surprise_threshold() -> f32 {
0.3
}
const fn default_duplicate_threshold() -> f32 {
0.95
}
fn default_duplicate_action() -> String {
"reject".into()
}
const fn default_token_budget_limit() -> u64 {
500_000
}
const fn default_rate_limit() -> u32 {
100
}
const fn default_prefetch_activation_depth() -> usize {
2
}
const fn default_prefetch_min_edge_weight() -> f32 {
0.1
}
const fn default_prefetch_max_bytes() -> u64 {
10_485_760
} const fn default_prefetch_cooldown_secs() -> u64 {
300
}
fn default_realm() -> String {
"default".into()
}
const fn default_slow_query_threshold_ms() -> u64 {
100
}
const fn default_memory_decay_factor() -> f32 {
0.95
}
const fn default_memory_half_life_hours() -> u64 {
168
}
const fn default_memory_min_importance() -> f32 {
0.01
}
fn validate_conflict_resolution_policy(
policy: &ConflictResolutionPolicy,
field: &str,
) -> Result<(), HirnError> {
let weights = [
("recency_weight", policy.recency_weight),
(
"source_reliability_weight",
policy.source_reliability_weight,
),
(
"supporting_evidence_weight",
policy.supporting_evidence_weight,
),
("human_override_weight", policy.human_override_weight),
];
for (name, value) in weights {
if !(0.0..=1.0).contains(&value) {
return Err(HirnError::InvalidConfig {
field: format!("{field}.{name}"),
value: value.to_string(),
reason: "must be in [0.0, 1.0]".to_string(),
});
}
}
let weight_sum = policy.recency_weight
+ policy.source_reliability_weight
+ policy.supporting_evidence_weight
+ policy.human_override_weight;
if weight_sum <= 0.0 {
return Err(HirnError::InvalidConfig {
field: field.to_string(),
value: weight_sum.to_string(),
reason: "must assign a non-zero total weight".to_string(),
});
}
Ok(())
}
fn default_prospective_templates() -> Vec<String> {
vec![
"What is known about {content}?".into(),
"When did {content} happen?".into(),
"Who was involved in {content}?".into(),
"What was the outcome of {content}?".into(),
"Why is {content} important?".into(),
]
}
fn default_svo_extraction_prompt() -> String {
"Extract all Subject-Verb-Object events from the following text.\n\
For each event, provide:\n\
- subject: the actor or entity performing the action\n\
- verb: the action or relation\n\
- object: the target or entity affected\n\
- time_start: when the event started (ISO 8601 or natural language, null if unknown)\n\
- time_end: when the event ended (null if same as start or unknown)\n\
- location: where the event occurred (null if unknown)\n\
- confidence: your confidence in this extraction (0.0 to 1.0)\n\n\
Return a JSON array of objects. Only include events with confidence >= 0.5.\n\n\
Text: {content}"
.into()
}
pub struct HirnConfigBuilder(HirnConfig);
impl HirnConfigBuilder {
#[must_use]
pub fn db_path(mut self, path: impl AsRef<Path>) -> Self {
self.0.db_path = path.as_ref().to_path_buf();
self
}
#[must_use]
pub const fn working_memory_token_limit(mut self, limit: u32) -> Self {
self.0.working_memory_token_limit = limit;
self
}
#[must_use]
pub const fn working_memory_reserve(mut self, reserve: f32) -> Self {
self.0.working_memory_reserve = reserve;
self
}
#[must_use]
pub const fn decay_lambda(mut self, lambda: f64) -> Self {
self.0.decay_lambda = lambda;
self
}
#[must_use]
pub const fn archive_threshold(mut self, threshold: f32) -> Self {
self.0.archive_threshold = threshold;
self
}
#[must_use]
pub const fn purge_threshold(mut self, threshold: f32) -> Self {
self.0.purge_threshold = threshold;
self
}
#[must_use]
pub const fn max_episodic_entries(mut self, max: u32) -> Self {
self.0.max_episodic_entries = max;
self
}
#[must_use]
pub const fn hebbian_learning_rate(mut self, rate: f64) -> Self {
self.0.hebbian_learning_rate = rate;
self
}
#[must_use]
pub const fn hebbian_decay_rate(mut self, rate: f64) -> Self {
self.0.hebbian_decay_rate = rate;
self
}
#[must_use]
pub const fn token_budget(mut self, budget: u32) -> Self {
self.0.token_budget = budget;
self
}
#[must_use]
pub const fn recall_preview_package_max_previews(mut self, max: usize) -> Self {
self.0.recall_preview_package_max_previews = max;
self
}
#[must_use]
pub const fn recall_preview_package_max_chars(mut self, max: usize) -> Self {
self.0.recall_preview_package_max_chars = max;
self
}
#[must_use]
pub const fn recall_preview_rerank_max_previews(mut self, max: usize) -> Self {
self.0.recall_preview_rerank_max_previews = max;
self
}
#[must_use]
pub const fn recall_preview_rerank_max_chars(mut self, max: usize) -> Self {
self.0.recall_preview_rerank_max_chars = max;
self
}
#[must_use]
pub const fn think_preview_package_max_previews(mut self, max: usize) -> Self {
self.0.think_preview_package_max_previews = max;
self
}
#[must_use]
pub const fn think_preview_package_max_chars(mut self, max: usize) -> Self {
self.0.think_preview_package_max_chars = max;
self
}
#[must_use]
pub const fn hnsw_m(mut self, m: usize) -> Self {
self.0.hnsw_m = m;
self
}
#[must_use]
pub const fn hnsw_ef_construction(mut self, ef: usize) -> Self {
self.0.hnsw_ef_construction = ef;
self
}
#[must_use]
pub const fn hnsw_ef_search(mut self, ef: usize) -> Self {
self.0.hnsw_ef_search = ef;
self
}
#[must_use]
pub const fn embedding_dimensions(mut self, dims: u32) -> Self {
self.0.embedding_dimensions = crate::EmbeddingDimension::new_const(dims);
self
}
#[must_use]
pub const fn allow_pseudo_embedder_fallback(mut self, allow: bool) -> Self {
self.0.allow_pseudo_embedder_fallback = allow;
self
}
#[must_use]
pub fn embedder_runtime(mut self, config: EmbedderRuntimeConfig) -> Self {
self.0.embedder_runtime = config;
self
}
#[must_use]
pub fn conflict_resolution_policy(mut self, policy: ConflictResolutionPolicy) -> Self {
self.0.conflict_resolution_policy = policy;
self
}
#[must_use]
pub fn conflict_resolution_realm_policy(
mut self,
realm: impl Into<String>,
policy: ConflictResolutionPolicy,
) -> Self {
self.0
.conflict_resolution_overrides
.by_realm
.insert(realm.into(), policy);
self
}
#[must_use]
pub fn conflict_resolution_namespace_policy(
mut self,
namespace: impl Into<String>,
policy: ConflictResolutionPolicy,
) -> Self {
self.0
.conflict_resolution_overrides
.by_namespace
.insert(namespace.into(), policy);
self
}
#[must_use]
pub const fn distance_metric(mut self, metric: DistanceMetric) -> Self {
self.0.metric = metric;
self
}
#[must_use]
pub const fn scoring_weights(mut self, alpha: f32, beta: f32, gamma: f32, delta: f32) -> Self {
self.0.scoring_similarity_weight = alpha;
self.0.scoring_importance_weight = beta;
self.0.scoring_recency_weight = gamma;
self.0.scoring_activation_weight = delta;
self
}
#[must_use]
pub const fn scoring_causal_relevance_weight(mut self, weight: f32) -> Self {
self.0.scoring_causal_relevance_weight = weight;
self
}
#[must_use]
pub const fn scoring_surprise_weight(mut self, weight: f32) -> Self {
self.0.scoring_surprise_weight = weight;
self
}
#[must_use]
pub const fn scoring_source_reliability_weight(mut self, weight: f32) -> Self {
self.0.scoring_source_reliability_weight = weight;
self
}
#[must_use]
pub const fn similarity_edge_threshold(mut self, threshold: f32) -> Self {
self.0.similarity_edge_threshold = threshold;
self
}
#[must_use]
pub const fn max_auto_edges_per_record(mut self, max: usize) -> Self {
self.0.max_auto_edges_per_record = max;
self
}
#[must_use]
pub const fn entity_overlap_threshold(mut self, threshold: usize) -> Self {
self.0.entity_overlap_threshold = threshold;
self
}
#[must_use]
pub const fn consolidation_interval_secs(mut self, secs: u64) -> Self {
self.0.consolidation_interval_secs = secs;
self
}
#[must_use]
pub const fn consolidation_causal_window(mut self, window: usize) -> Self {
self.0.consolidation_causal_window = window;
self
}
#[must_use]
pub const fn reconsolidation_window_secs(mut self, secs: u64) -> Self {
self.0.reconsolidation_window_secs = secs;
self
}
#[must_use]
pub const fn compaction_interval_secs(mut self, secs: u64) -> Self {
self.0.compaction_interval_secs = secs;
self
}
#[must_use]
pub const fn compaction_fragment_threshold(mut self, threshold: u32) -> Self {
self.0.compaction_fragment_threshold = threshold;
self
}
#[must_use]
pub const fn multivector_enabled(mut self, enabled: bool) -> Self {
self.0.multivector_enabled = enabled;
self
}
#[must_use]
pub const fn multivector_weight(mut self, weight: f32) -> Self {
self.0.multivector_weight = weight;
self
}
#[must_use]
pub const fn prefetch_enabled(mut self, enabled: bool) -> Self {
self.0.prefetch_enabled = enabled;
self
}
#[must_use]
pub const fn prefetch_activation_depth(mut self, depth: usize) -> Self {
self.0.prefetch_activation_depth = depth;
self
}
#[must_use]
pub const fn prefetch_min_edge_weight(mut self, weight: f32) -> Self {
self.0.prefetch_min_edge_weight = weight;
self
}
#[must_use]
pub const fn prefetch_max_bytes(mut self, bytes: u64) -> Self {
self.0.prefetch_max_bytes = bytes;
self
}
#[must_use]
pub const fn prefetch_cooldown_secs(mut self, secs: u64) -> Self {
self.0.prefetch_cooldown_secs = secs;
self
}
#[must_use]
pub fn default_realm(mut self, realm: impl Into<String>) -> Self {
self.0.default_realm = realm.into();
self
}
#[must_use]
pub const fn slow_query_threshold_ms(mut self, ms: u64) -> Self {
self.0.slow_query_threshold_ms = ms;
self
}
#[must_use]
pub const fn quality_gate_threshold(mut self, threshold: f32) -> Self {
self.0.quality_gate_threshold = threshold;
self
}
#[must_use]
pub const fn offline_dream_quality_threshold(mut self, threshold: f32) -> Self {
self.0.offline_dream_quality_threshold = threshold;
self
}
#[must_use]
pub const fn offline_reconcile_quality_threshold(mut self, threshold: f32) -> Self {
self.0.offline_reconcile_quality_threshold = threshold;
self
}
#[must_use]
pub const fn offline_plan_quality_threshold(mut self, threshold: f32) -> Self {
self.0.offline_plan_quality_threshold = threshold;
self
}
#[must_use]
pub const fn nli_contradiction_threshold(mut self, threshold: f32) -> Self {
self.0.nli_contradiction_threshold = threshold;
self
}
#[must_use]
pub const fn text_retention(mut self, retention: TextRetention) -> Self {
self.0.text_retention = retention;
self
}
#[must_use]
pub fn resource_retention_policy(mut self, policy: ResourceRetentionPolicy) -> Self {
self.0.resource_retention_policy = policy;
self
}
#[must_use]
pub fn resource_quota_policy(mut self, policy: ResourceQuotaPolicy) -> Self {
self.0.resource_quota_policy = policy;
self
}
#[must_use]
pub fn resource_index_policy(mut self, policy: ResourceIndexPolicy) -> Self {
self.0.resource_index_policy = policy;
self
}
#[must_use]
pub fn derived_artifact_index_policy(mut self, policy: DerivedArtifactIndexPolicy) -> Self {
self.0.derived_artifact_index_policy = policy;
self
}
#[must_use]
pub fn offline_scheduler(mut self, config: OfflineSchedulerConfig) -> Self {
self.0.offline_scheduler = config;
self
}
#[must_use]
pub const fn execution_memory_limit_bytes(mut self, bytes: u64) -> Self {
self.0.execution_memory_limit_bytes = bytes;
self
}
#[must_use]
pub const fn execution_parallelism(mut self, parallelism: usize) -> Self {
self.0.execution_parallelism = parallelism;
self
}
#[must_use]
pub const fn memory_decay_factor(mut self, factor: f32) -> Self {
self.0.memory_decay_factor = factor;
self
}
#[must_use]
pub const fn memory_half_life_hours(mut self, hours: u64) -> Self {
self.0.memory_half_life_hours = hours;
self
}
#[must_use]
pub const fn memory_min_importance(mut self, threshold: f32) -> Self {
self.0.memory_min_importance = threshold;
self
}
#[must_use]
pub const fn tier_working_to_episodic_ttl_secs(mut self, secs: u64) -> Self {
self.0.tier_working_to_episodic_ttl_secs = secs;
self
}
#[must_use]
pub const fn tier_episodic_to_semantic_threshold(mut self, threshold: f32) -> Self {
self.0.tier_episodic_to_semantic_threshold = threshold;
self
}
#[must_use]
pub const fn tier_semantic_archive_threshold(mut self, threshold: f32) -> Self {
self.0.tier_semantic_archive_threshold = threshold;
self
}
#[must_use]
pub const fn tier_procedural_min_success_rate(mut self, rate: f32) -> Self {
self.0.tier_procedural_min_success_rate = rate;
self
}
#[must_use]
pub const fn graph_depth_delegation_threshold(mut self, threshold: usize) -> Self {
self.0.graph_depth_delegation_threshold = threshold;
self
}
pub fn build(self) -> Result<HirnConfig, HirnError> {
self.0.validate()?;
Ok(self.0)
}
#[must_use]
pub const fn rpe_enabled(mut self, enabled: bool) -> Self {
self.0.rpe_enabled = enabled;
self
}
#[must_use]
pub const fn rpe_fast_path_threshold(mut self, threshold: f32) -> Self {
self.0.rpe_fast_path_threshold = threshold;
self
}
#[must_use]
pub const fn rpe_similarity_search_limit(mut self, limit: usize) -> Self {
self.0.rpe_similarity_search_limit = limit;
self
}
#[must_use]
pub const fn prospective_indexing_enabled(mut self, enabled: bool) -> Self {
self.0.prospective_indexing_enabled = enabled;
self
}
#[must_use]
pub const fn prospective_indexing_num_questions(mut self, num: usize) -> Self {
self.0.prospective_indexing_num_questions = num;
self
}
#[must_use]
pub const fn prospective_indexing_timeout_secs(mut self, secs: u64) -> Self {
self.0.prospective_indexing_timeout_secs = secs;
self
}
#[must_use]
pub fn prospective_indexing_templates(mut self, templates: Vec<String>) -> Self {
self.0.prospective_indexing_templates = templates;
self
}
#[must_use]
pub const fn svo_extraction_enabled(mut self, enabled: bool) -> Self {
self.0.svo_extraction_enabled = enabled;
self
}
#[must_use]
pub const fn svo_confidence_threshold(mut self, threshold: f32) -> Self {
self.0.svo_confidence_threshold = threshold;
self
}
#[must_use]
pub fn svo_extraction_prompt(mut self, prompt: impl Into<String>) -> Self {
self.0.svo_extraction_prompt = prompt.into();
self
}
#[must_use]
pub const fn interference_consolidation_threshold(mut self, threshold: f32) -> Self {
self.0.interference_consolidation_threshold = threshold;
self
}
#[must_use]
pub const fn interference_consolidation_cooldown_secs(mut self, secs: u64) -> Self {
self.0.interference_consolidation_cooldown_secs = secs;
self
}
#[must_use]
pub const fn decay_interval_secs(mut self, secs: u64) -> Self {
self.0.decay_interval_secs = secs;
self
}
#[must_use]
pub const fn decay_sweep_window_secs(mut self, secs: u64) -> Self {
self.0.decay_sweep_window_secs = secs;
self
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_is_valid() {
let config = HirnConfig::default();
config.validate().unwrap();
}
#[test]
fn builder_custom_values() {
let config = HirnConfig::builder()
.db_path("/tmp/test")
.token_budget(8192)
.working_memory_token_limit(4096)
.build()
.unwrap();
assert_eq!(config.db_path, PathBuf::from("/tmp/test"));
assert_eq!(config.token_budget, 8192);
assert_eq!(config.working_memory_token_limit, 4096);
}
#[test]
fn purge_gt_archive_fails() {
let result = HirnConfig::builder()
.purge_threshold(0.5)
.archive_threshold(0.1) .build();
assert!(result.is_err());
}
#[test]
fn negative_decay_lambda_fails() {
let result = HirnConfig::builder().decay_lambda(-1.0).build();
assert!(result.is_err());
}
#[test]
fn negative_hebbian_rate_fails() {
let result = HirnConfig::builder().hebbian_learning_rate(-0.1).build();
assert!(result.is_err());
}
#[test]
fn toml_round_trip() {
let config = HirnConfig::default();
let toml_str = toml::to_string_pretty(&config).unwrap();
let back: HirnConfig = toml::from_str(&toml_str).unwrap();
assert_eq!(config.token_budget, back.token_budget);
assert_eq!(config.db_path, back.db_path);
}
#[test]
fn invalid_toml_rejected() {
let config = HirnConfig::default();
let mut toml_str = toml::to_string_pretty(&config).unwrap();
toml_str = toml_str.replace("hnsw_m = 16", "hnsw_m = 0");
let result: Result<HirnConfig, _> = toml::from_str(&toml_str);
assert!(
result.is_err(),
"deserializing hnsw_m=0 should fail validation"
);
}
#[test]
fn prospective_templates_default() {
let config = HirnConfig::default();
assert_eq!(config.prospective_indexing_templates.len(), 5);
assert!(config.prospective_indexing_templates[0].contains("{content}"));
}
#[test]
fn prospective_templates_custom() {
let config = HirnConfig::builder()
.prospective_indexing_templates(vec![
"Tell me about {content}".into(),
"Summarize {content}".into(),
])
.build()
.unwrap();
assert_eq!(config.prospective_indexing_templates.len(), 2);
assert_eq!(
config.prospective_indexing_templates[0],
"Tell me about {content}"
);
}
#[test]
fn prospective_templates_missing_placeholder_rejected() {
let err = HirnConfig::builder()
.prospective_indexing_templates(vec!["Tell me about this memory".into()])
.build()
.unwrap_err();
match err {
HirnError::InvalidConfig { field, reason, .. } => {
assert_eq!(field, "prospective_indexing_templates");
assert!(reason.contains("{content}"));
}
other => panic!("expected InvalidConfig, got {other}"),
}
}
#[test]
fn prospective_templates_toml_round_trip() {
let config = HirnConfig::default();
let toml_str = toml::to_string_pretty(&config).unwrap();
let back: HirnConfig = toml::from_str(&toml_str).unwrap();
assert_eq!(
config.prospective_indexing_templates,
back.prospective_indexing_templates
);
}
#[test]
fn legacy_config_with_missing_new_fields_deserializes_with_defaults() {
let legacy_toml = r#"
db_path = "./legacy-brain"
embedding_dimensions = 64
"#;
let config: HirnConfig = toml::from_str(legacy_toml).unwrap();
assert_eq!(config.db_path, std::path::PathBuf::from("./legacy-brain"));
assert_eq!(config.embedding_dimensions, crate::EmbeddingDimension::new_const(64));
assert!((config.rpe_fast_path_threshold - 0.3).abs() < f32::EPSILON);
assert!((config.quality_gate_threshold - 0.5).abs() < f32::EPSILON);
assert!((config.offline_dream_quality_threshold - 0.55).abs() < f32::EPSILON);
assert!((config.offline_reconcile_quality_threshold - 0.6).abs() < f32::EPSILON);
assert!((config.offline_plan_quality_threshold - 0.45).abs() < f32::EPSILON);
assert!((config.interference_consolidation_threshold - 0.3).abs() < f32::EPSILON);
assert_eq!(config.consolidation_causal_window, 100);
assert!(!config.prospective_indexing_templates.is_empty());
}
#[test]
fn invalid_svo_confidence_threshold_rejected() {
let result = HirnConfig::builder().svo_confidence_threshold(1.5).build();
assert!(result.is_err());
let result = HirnConfig::builder().svo_confidence_threshold(-0.1).build();
assert!(result.is_err());
}
#[test]
fn invalid_rpe_threshold_rejected() {
let result = HirnConfig::builder().rpe_fast_path_threshold(3.0).build();
assert!(result.is_err());
let result = HirnConfig::builder().rpe_fast_path_threshold(-0.1).build();
assert!(result.is_err());
}
#[test]
fn invalid_interference_threshold_rejected() {
let result = HirnConfig::builder()
.interference_consolidation_threshold(-1.0)
.build();
assert!(result.is_err());
}
#[test]
fn invalid_offline_quality_threshold_rejected() {
assert!(
HirnConfig::builder()
.offline_dream_quality_threshold(1.1)
.build()
.is_err()
);
assert!(
HirnConfig::builder()
.offline_reconcile_quality_threshold(-0.1)
.build()
.is_err()
);
assert!(
HirnConfig::builder()
.offline_plan_quality_threshold(1.5)
.build()
.is_err()
);
}
#[test]
fn invalid_causal_window_rejected() {
let err = HirnConfig::builder()
.consolidation_causal_window(MAX_CONSOLIDATION_CAUSAL_WINDOW + 1)
.build()
.unwrap_err();
match err {
HirnError::InvalidConfig { field, reason, .. } => {
assert_eq!(field, "consolidation_causal_window");
assert!(reason.contains("1..=10000"));
}
other => panic!("expected InvalidConfig, got {other}"),
}
}
#[test]
fn valid_write_path_guards_pass_cleanly() {
let config = HirnConfig::builder()
.prospective_indexing_templates(vec!["Tell me about {content}".into()])
.consolidation_causal_window(MAX_CONSOLIDATION_CAUSAL_WINDOW)
.build()
.unwrap();
assert_eq!(
config.consolidation_causal_window,
MAX_CONSOLIDATION_CAUSAL_WINDOW
);
assert_eq!(
config.prospective_indexing_templates,
vec!["Tell me about {content}".to_string()]
);
}
#[test]
fn invalid_conflict_resolution_weight_rejected() {
let result = HirnConfig::builder()
.conflict_resolution_policy(ConflictResolutionPolicy {
recency_weight: 1.2,
..ConflictResolutionPolicy::default()
})
.build();
assert!(result.is_err());
}
#[test]
fn conflict_resolution_namespace_policy_round_trips() {
let config = HirnConfig::builder()
.conflict_resolution_namespace_policy(
"team_ops",
ConflictResolutionPolicy {
recency_weight: 0.8,
source_reliability_weight: 0.1,
supporting_evidence_weight: 0.1,
human_override_weight: 0.0,
prefer_human_override: true,
},
)
.build()
.unwrap();
let toml_str = toml::to_string_pretty(&config).unwrap();
let back: HirnConfig = toml::from_str(&toml_str).unwrap();
assert_eq!(
back.conflict_resolution_overrides
.by_namespace
.get("team_ops")
.unwrap()
.recency_weight,
0.8
);
}
#[test]
fn invalid_embedder_runtime_guards_rejected() {
let err = HirnConfig::builder()
.embedder_runtime(EmbedderRuntimeConfig {
batch_size: Some(0),
retry: None,
circuit_breaker: None,
persistent_cache: None,
})
.build()
.unwrap_err();
match err {
HirnError::InvalidConfig { field, .. } => {
assert_eq!(field, "embedder_runtime.batch_size");
}
other => panic!("expected InvalidConfig, got {other}"),
}
let err = HirnConfig::builder()
.embedder_runtime(EmbedderRuntimeConfig {
batch_size: None,
retry: Some(EmbedderRetryConfig {
max_retries: 1,
base_backoff_ms: 0,
max_cumulative_timeout_ms: 1,
}),
circuit_breaker: None,
persistent_cache: None,
})
.build()
.unwrap_err();
match err {
HirnError::InvalidConfig { field, .. } => {
assert_eq!(field, "embedder_runtime.retry.base_backoff_ms");
}
other => panic!("expected InvalidConfig, got {other}"),
}
let err = HirnConfig::builder()
.embedder_runtime(EmbedderRuntimeConfig {
batch_size: None,
retry: Some(EmbedderRetryConfig {
max_retries: 1,
base_backoff_ms: 1,
max_cumulative_timeout_ms: 0,
}),
circuit_breaker: None,
persistent_cache: None,
})
.build()
.unwrap_err();
match err {
HirnError::InvalidConfig { field, .. } => {
assert_eq!(field, "embedder_runtime.retry.max_cumulative_timeout_ms");
}
other => panic!("expected InvalidConfig, got {other}"),
}
}
}