axiomsync 1.0.0

Core data-processing engine for AxiomSync local retrieval runtime.
Documentation
use crate::llm_io::parse_env_bool;

use super::env::{
    parse_enabled_default_true, read_env_f32, read_env_u16, read_env_u32, read_env_u64,
    read_env_usize, read_env_usize_optional, read_non_empty_env, read_raw_env,
};

const ENV_OM_ENABLED: &str = "AXIOMSYNC_OM_ENABLED";
const ENV_OM_HINT_READER: &str = "AXIOMSYNC_OM_HINT_READER";
const ENV_OM_SCOPE: &str = "AXIOMSYNC_OM_SCOPE";
const ENV_OM_SCOPE_THREAD_ID: &str = "AXIOMSYNC_OM_SCOPE_THREAD_ID";
const ENV_OM_SCOPE_RESOURCE_ID: &str = "AXIOMSYNC_OM_SCOPE_RESOURCE_ID";
const ENV_OM_OBSERVER_MAX_MESSAGES: &str = "AXIOMSYNC_OM_OBSERVER_MAX_MESSAGES";
const ENV_OM_RESOURCE_SCOPE_CROSS_SESSION_LIMIT: &str =
    "AXIOMSYNC_OM_RESOURCE_SCOPE_CROSS_SESSION_LIMIT";
const ENV_OM_OBSERVATION_MAX_CHARS: &str = "AXIOMSYNC_OM_OBSERVATION_MAX_CHARS";
const ENV_OM_OBSERVER_OTHER_CONVERSATION_MAX_PART_CHARS: &str =
    "AXIOMSYNC_OM_OBSERVER_OTHER_CONVERSATION_MAX_PART_CHARS";
const ENV_OM_OBSERVER_ACTIVE_OBSERVATIONS_MAX_CHARS: &str =
    "AXIOMSYNC_OM_OBSERVER_ACTIVE_OBSERVATIONS_MAX_CHARS";
const ENV_OM_ROLLOUT_PROFILE: &str = "AXIOMSYNC_OM_ROLLOUT_PROFILE";
const ENV_OM_OBSERVER_MODE: &str = "AXIOMSYNC_OM_OBSERVER_MODE";
const ENV_OM_OBSERVER_MODEL_ENABLED: &str = "AXIOMSYNC_OM_OBSERVER_MODEL_ENABLED";
const ENV_OM_OBSERVER_LLM_ENDPOINT: &str = "AXIOMSYNC_OM_OBSERVER_LLM_ENDPOINT";
const ENV_OM_OBSERVER_LLM_MODEL: &str = "AXIOMSYNC_OM_OBSERVER_LLM_MODEL";
const ENV_OM_OBSERVER_LLM_TIMEOUT_MS: &str = "AXIOMSYNC_OM_OBSERVER_LLM_TIMEOUT_MS";
const ENV_OM_OBSERVER_LLM_MAX_OUTPUT_TOKENS: &str = "AXIOMSYNC_OM_OBSERVER_LLM_MAX_OUTPUT_TOKENS";
const ENV_OM_OBSERVER_LLM_TEMPERATURE_MILLI: &str = "AXIOMSYNC_OM_OBSERVER_LLM_TEMPERATURE_MILLI";
const ENV_OM_OBSERVER_LLM_STRICT: &str = "AXIOMSYNC_OM_OBSERVER_LLM_STRICT";
const ENV_OM_OBSERVER_LLM_MAX_CHARS_PER_MESSAGE: &str =
    "AXIOMSYNC_OM_OBSERVER_LLM_MAX_CHARS_PER_MESSAGE";
const ENV_OM_OBSERVER_LLM_MAX_INPUT_TOKENS: &str = "AXIOMSYNC_OM_OBSERVER_LLM_MAX_INPUT_TOKENS";
const ENV_OM_REFLECTOR_MODE: &str = "AXIOMSYNC_OM_REFLECTOR_MODE";
const ENV_OM_REFLECTOR_MODEL_ENABLED: &str = "AXIOMSYNC_OM_REFLECTOR_MODEL_ENABLED";
const ENV_OM_REFLECTOR_LLM_ENDPOINT: &str = "AXIOMSYNC_OM_REFLECTOR_LLM_ENDPOINT";
const ENV_OM_REFLECTOR_LLM_MODEL: &str = "AXIOMSYNC_OM_REFLECTOR_LLM_MODEL";
const ENV_OM_REFLECTOR_LLM_TIMEOUT_MS: &str = "AXIOMSYNC_OM_REFLECTOR_LLM_TIMEOUT_MS";
const ENV_OM_REFLECTOR_LLM_MAX_OUTPUT_TOKENS: &str = "AXIOMSYNC_OM_REFLECTOR_LLM_MAX_OUTPUT_TOKENS";
const ENV_OM_REFLECTOR_LLM_TEMPERATURE_MILLI: &str = "AXIOMSYNC_OM_REFLECTOR_LLM_TEMPERATURE_MILLI";
const ENV_OM_REFLECTOR_LLM_STRICT: &str = "AXIOMSYNC_OM_REFLECTOR_LLM_STRICT";
const ENV_OM_REFLECTOR_OBSERVATION_TOKENS: &str = "AXIOMSYNC_OM_REFLECTOR_OBSERVATION_TOKENS";
const ENV_OM_REFLECTOR_BUFFER_ACTIVATION: &str = "AXIOMSYNC_OM_REFLECTOR_BUFFER_ACTIVATION";
const ENV_OM_REFLECTOR_MAX_CHARS: &str = "AXIOMSYNC_OM_REFLECTOR_MAX_CHARS";
const ENV_OM_MESSAGE_TOKENS: &str = "AXIOMSYNC_OM_MESSAGE_TOKENS";
const ENV_OM_OBSERVER_MAX_TOKENS_PER_BATCH: &str = "AXIOMSYNC_OM_OBSERVER_MAX_TOKENS_PER_BATCH";
const ENV_OM_OBSERVER_LLM_MAX_TOKENS_PER_BATCH_LEGACY: &str =
    "AXIOMSYNC_OM_OBSERVER_LLM_MAX_TOKENS_PER_BATCH";
const ENV_OM_ACTIVATION_RATIO: &str = "AXIOMSYNC_OM_ACTIVATION_RATIO";
const ENV_OM_SHARE_TOKEN_BUDGET: &str = "AXIOMSYNC_OM_SHARE_TOKEN_BUDGET";
const ENV_OM_BUFFER_TOKENS: &str = "AXIOMSYNC_OM_BUFFER_TOKENS";
const ENV_OM_OBSERVER_BLOCK_AFTER: &str = "AXIOMSYNC_OM_OBSERVER_BLOCK_AFTER";
const ENV_OM_REFLECTOR_BLOCK_AFTER: &str = "AXIOMSYNC_OM_REFLECTOR_BLOCK_AFTER";

const DEFAULT_OM_OBSERVER_MAX_MESSAGES: usize = 8;
const DEFAULT_OM_RESOURCE_SCOPE_CROSS_SESSION_LIMIT: usize = 4;
const DEFAULT_OM_OBSERVATION_MAX_CHARS: usize = 4_000;
const DEFAULT_OM_OBSERVER_OTHER_CONVERSATION_MAX_PART_CHARS: usize = 500;
const DEFAULT_OM_REFLECTOR_MAX_CHARS: usize = 1_200;
const DEFAULT_OM_HINT_READER: &str = "snapshot_v2";

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum OmHintReaderMode {
    None,
    SnapshotV2,
}

impl OmHintReaderMode {
    #[must_use]
    fn from_env(raw: Option<&str>) -> Self {
        match raw
            .unwrap_or(DEFAULT_OM_HINT_READER)
            .trim()
            .to_ascii_lowercase()
            .as_str()
        {
            "none" | "off" | "false" | "0" | "disabled" => Self::None,
            "snapshot_v2" | "v2" | "on" | "true" | "1" | "enabled" => Self::SnapshotV2,
            _ => Self::SnapshotV2,
        }
    }
}

#[derive(Debug, Clone)]
pub(crate) struct OmConfig {
    pub(crate) enabled: bool,
    pub(crate) hint_reader: OmHintReaderMode,
    pub(crate) scope: OmScopeConfig,
    pub(crate) limits: OmRuntimeLimitsConfig,
    pub(crate) runtime_env: OmRuntimeEnvConfig,
    pub(crate) observer: OmObserverConfigSnapshot,
    pub(crate) reflector: OmReflectorConfigSnapshot,
}

impl OmConfig {
    #[must_use]
    pub(super) fn from_env() -> Self {
        Self {
            enabled: parse_enabled_default_true(std::env::var(ENV_OM_ENABLED).ok().as_deref()),
            hint_reader: OmHintReaderMode::from_env(
                std::env::var(ENV_OM_HINT_READER).ok().as_deref(),
            ),
            scope: OmScopeConfig::from_env(),
            limits: OmRuntimeLimitsConfig::from_env(),
            runtime_env: OmRuntimeEnvConfig::from_env(),
            observer: OmObserverConfigSnapshot::from_env(),
            reflector: OmReflectorConfigSnapshot::from_env(),
        }
    }
}

impl Default for OmConfig {
    fn default() -> Self {
        Self {
            enabled: true,
            hint_reader: OmHintReaderMode::SnapshotV2,
            scope: OmScopeConfig::default(),
            limits: OmRuntimeLimitsConfig::default(),
            runtime_env: OmRuntimeEnvConfig::default(),
            observer: OmObserverConfigSnapshot::default(),
            reflector: OmReflectorConfigSnapshot::default(),
        }
    }
}

#[derive(Debug, Clone, Default)]
pub(crate) struct OmScopeConfig {
    pub(crate) scope: Option<String>,
    pub(crate) thread_id: Option<String>,
    pub(crate) resource_id: Option<String>,
}

impl OmScopeConfig {
    #[must_use]
    fn from_env() -> Self {
        Self {
            scope: read_non_empty_env(ENV_OM_SCOPE),
            thread_id: read_non_empty_env(ENV_OM_SCOPE_THREAD_ID),
            resource_id: read_non_empty_env(ENV_OM_SCOPE_RESOURCE_ID),
        }
    }
}

#[derive(Debug, Clone, Copy)]
pub(crate) struct OmRuntimeLimitsConfig {
    pub(crate) observer_max_messages: usize,
    pub(crate) resource_scope_cross_session_limit: usize,
    pub(crate) observation_max_chars: usize,
    pub(crate) observer_other_conversation_max_part_chars: usize,
    pub(crate) observer_active_observations_max_chars: usize,
}

impl OmRuntimeLimitsConfig {
    #[must_use]
    fn from_env() -> Self {
        let observation_max_chars = read_env_usize(
            ENV_OM_OBSERVATION_MAX_CHARS,
            DEFAULT_OM_OBSERVATION_MAX_CHARS,
            1,
        );
        Self {
            observer_max_messages: read_env_usize(
                ENV_OM_OBSERVER_MAX_MESSAGES,
                DEFAULT_OM_OBSERVER_MAX_MESSAGES,
                1,
            ),
            resource_scope_cross_session_limit: read_env_usize(
                ENV_OM_RESOURCE_SCOPE_CROSS_SESSION_LIMIT,
                DEFAULT_OM_RESOURCE_SCOPE_CROSS_SESSION_LIMIT,
                1,
            ),
            observation_max_chars,
            observer_other_conversation_max_part_chars: read_env_usize(
                ENV_OM_OBSERVER_OTHER_CONVERSATION_MAX_PART_CHARS,
                DEFAULT_OM_OBSERVER_OTHER_CONVERSATION_MAX_PART_CHARS,
                1,
            ),
            observer_active_observations_max_chars: read_env_usize(
                ENV_OM_OBSERVER_ACTIVE_OBSERVATIONS_MAX_CHARS,
                observation_max_chars.saturating_mul(2),
                1,
            ),
        }
    }
}

impl Default for OmRuntimeLimitsConfig {
    fn default() -> Self {
        Self {
            observer_max_messages: DEFAULT_OM_OBSERVER_MAX_MESSAGES,
            resource_scope_cross_session_limit: DEFAULT_OM_RESOURCE_SCOPE_CROSS_SESSION_LIMIT,
            observation_max_chars: DEFAULT_OM_OBSERVATION_MAX_CHARS,
            observer_other_conversation_max_part_chars:
                DEFAULT_OM_OBSERVER_OTHER_CONVERSATION_MAX_PART_CHARS,
            observer_active_observations_max_chars: DEFAULT_OM_OBSERVATION_MAX_CHARS
                .saturating_mul(2),
        }
    }
}

#[derive(Debug, Clone, Default)]
pub(crate) struct OmRuntimeEnvConfig {
    pub(crate) message_tokens: Option<String>,
    pub(crate) observer_max_tokens_per_batch: Option<String>,
    pub(crate) reflector_observation_tokens: Option<String>,
    pub(crate) activation_ratio: Option<String>,
    pub(crate) share_token_budget: Option<String>,
    pub(crate) buffer_tokens: Option<String>,
    pub(crate) observer_block_after: Option<String>,
    pub(crate) reflector_buffer_activation: Option<String>,
    pub(crate) reflector_block_after: Option<String>,
}

impl OmRuntimeEnvConfig {
    #[must_use]
    fn from_env() -> Self {
        Self {
            message_tokens: read_raw_env(ENV_OM_MESSAGE_TOKENS),
            observer_max_tokens_per_batch: read_raw_env(ENV_OM_OBSERVER_MAX_TOKENS_PER_BATCH)
                .or_else(|| read_raw_env(ENV_OM_OBSERVER_LLM_MAX_TOKENS_PER_BATCH_LEGACY)),
            reflector_observation_tokens: read_raw_env(ENV_OM_REFLECTOR_OBSERVATION_TOKENS),
            activation_ratio: read_raw_env(ENV_OM_ACTIVATION_RATIO),
            share_token_budget: read_raw_env(ENV_OM_SHARE_TOKEN_BUDGET),
            buffer_tokens: read_raw_env(ENV_OM_BUFFER_TOKENS),
            observer_block_after: read_raw_env(ENV_OM_OBSERVER_BLOCK_AFTER),
            reflector_buffer_activation: read_raw_env(ENV_OM_REFLECTOR_BUFFER_ACTIVATION),
            reflector_block_after: read_raw_env(ENV_OM_REFLECTOR_BLOCK_AFTER),
        }
    }
}

#[derive(Debug, Clone, Default)]
pub(crate) struct OmObserverConfigSnapshot {
    pub(crate) mode: Option<String>,
    pub(crate) explicit_model_enabled: bool,
    pub(crate) rollout_profile: Option<String>,
    pub(crate) llm_endpoint: Option<String>,
    pub(crate) llm_model: Option<String>,
    pub(crate) llm_timeout_ms: Option<u64>,
    pub(crate) llm_max_output_tokens: Option<u32>,
    pub(crate) llm_temperature_milli: Option<u16>,
    pub(crate) llm_strict: bool,
    pub(crate) llm_max_chars_per_message: Option<usize>,
    pub(crate) llm_max_input_tokens: Option<u32>,
}

impl OmObserverConfigSnapshot {
    #[must_use]
    fn from_env() -> Self {
        Self {
            mode: read_non_empty_env(ENV_OM_OBSERVER_MODE),
            explicit_model_enabled: parse_env_bool(
                std::env::var(ENV_OM_OBSERVER_MODEL_ENABLED).ok().as_deref(),
            ),
            rollout_profile: read_non_empty_env(ENV_OM_ROLLOUT_PROFILE),
            llm_endpoint: read_non_empty_env(ENV_OM_OBSERVER_LLM_ENDPOINT),
            llm_model: read_non_empty_env(ENV_OM_OBSERVER_LLM_MODEL),
            llm_timeout_ms: read_env_u64(ENV_OM_OBSERVER_LLM_TIMEOUT_MS)
                .filter(|value| *value >= 200),
            llm_max_output_tokens: read_env_u32(ENV_OM_OBSERVER_LLM_MAX_OUTPUT_TOKENS)
                .filter(|value| *value > 0),
            llm_temperature_milli: read_env_u16(ENV_OM_OBSERVER_LLM_TEMPERATURE_MILLI),
            llm_strict: parse_env_bool(std::env::var(ENV_OM_OBSERVER_LLM_STRICT).ok().as_deref()),
            llm_max_chars_per_message: read_env_usize_optional(
                ENV_OM_OBSERVER_LLM_MAX_CHARS_PER_MESSAGE,
            )
            .filter(|value| *value > 0),
            llm_max_input_tokens: read_env_u32(ENV_OM_OBSERVER_LLM_MAX_INPUT_TOKENS)
                .filter(|value| *value > 0),
        }
    }
}

#[derive(Debug, Clone)]
pub(crate) struct OmReflectorConfigSnapshot {
    pub(crate) mode: Option<String>,
    pub(crate) explicit_model_enabled: bool,
    pub(crate) rollout_profile: Option<String>,
    pub(crate) llm_endpoint: Option<String>,
    pub(crate) llm_model: Option<String>,
    pub(crate) llm_timeout_ms: Option<u64>,
    pub(crate) llm_max_output_tokens: Option<u32>,
    pub(crate) llm_temperature_milli: Option<u16>,
    pub(crate) llm_strict: bool,
    pub(crate) llm_target_observation_tokens: Option<u32>,
    pub(crate) llm_buffer_activation: Option<f32>,
    pub(crate) max_chars: usize,
}

impl OmReflectorConfigSnapshot {
    #[must_use]
    fn from_env() -> Self {
        Self {
            mode: read_non_empty_env(ENV_OM_REFLECTOR_MODE),
            explicit_model_enabled: parse_env_bool(
                std::env::var(ENV_OM_REFLECTOR_MODEL_ENABLED)
                    .ok()
                    .as_deref(),
            ),
            rollout_profile: read_non_empty_env(ENV_OM_ROLLOUT_PROFILE),
            llm_endpoint: read_non_empty_env(ENV_OM_REFLECTOR_LLM_ENDPOINT),
            llm_model: read_non_empty_env(ENV_OM_REFLECTOR_LLM_MODEL),
            llm_timeout_ms: read_env_u64(ENV_OM_REFLECTOR_LLM_TIMEOUT_MS)
                .filter(|value| *value >= 200),
            llm_max_output_tokens: read_env_u32(ENV_OM_REFLECTOR_LLM_MAX_OUTPUT_TOKENS)
                .filter(|value| *value > 0),
            llm_temperature_milli: read_env_u16(ENV_OM_REFLECTOR_LLM_TEMPERATURE_MILLI),
            llm_strict: parse_env_bool(std::env::var(ENV_OM_REFLECTOR_LLM_STRICT).ok().as_deref()),
            llm_target_observation_tokens: read_env_u32(ENV_OM_REFLECTOR_OBSERVATION_TOKENS)
                .filter(|value| *value > 0),
            llm_buffer_activation: read_env_f32(ENV_OM_REFLECTOR_BUFFER_ACTIVATION)
                .filter(|value| *value > 0.0 && *value <= 1.0),
            max_chars: read_env_usize(
                ENV_OM_REFLECTOR_MAX_CHARS,
                DEFAULT_OM_REFLECTOR_MAX_CHARS,
                1,
            ),
        }
    }
}

impl Default for OmReflectorConfigSnapshot {
    fn default() -> Self {
        Self {
            mode: None,
            explicit_model_enabled: false,
            rollout_profile: None,
            llm_endpoint: None,
            llm_model: None,
            llm_timeout_ms: None,
            llm_max_output_tokens: None,
            llm_temperature_milli: None,
            llm_strict: false,
            llm_target_observation_tokens: None,
            llm_buffer_activation: None,
            max_chars: DEFAULT_OM_REFLECTOR_MAX_CHARS,
        }
    }
}

#[cfg(test)]
mod tests {
    use super::OmHintReaderMode;

    #[test]
    fn om_hint_reader_mode_defaults_to_snapshot_v2() {
        assert_eq!(
            OmHintReaderMode::from_env(None),
            OmHintReaderMode::SnapshotV2
        );
        assert_eq!(
            OmHintReaderMode::from_env(Some("unknown")),
            OmHintReaderMode::SnapshotV2
        );
    }

    #[test]
    fn om_hint_reader_mode_parses_none_and_snapshot_v2_tokens() {
        assert_eq!(
            OmHintReaderMode::from_env(Some("none")),
            OmHintReaderMode::None
        );
        assert_eq!(
            OmHintReaderMode::from_env(Some("off")),
            OmHintReaderMode::None
        );
        assert_eq!(
            OmHintReaderMode::from_env(Some("snapshot_v2")),
            OmHintReaderMode::SnapshotV2
        );
        assert_eq!(
            OmHintReaderMode::from_env(Some("v2")),
            OmHintReaderMode::SnapshotV2
        );
    }
}