stasis-rs 0.9.1

Durable AI orchestration framework with runtime jobs, lineage, and memory integration
use std::{
    collections::hash_map::DefaultHasher,
    hash::{Hash, Hasher},
};

use chrono::Utc;

use crate::application::orchestration::runtime_job_payloads::{
    MemoryPolicyPayload, MemoryStoreModePayload,
};
use crate::ports::outbound::memory::memory_models::{MemoryRecallRequest, MemoryStoreResponse};

pub enum SttpPromptNodeFormat {
    TaggedSchema,
    UntaggedNoSchema,
}

pub fn should_store(memory_policy: Option<&MemoryPolicyPayload>) -> bool {
    !matches!(
        memory_policy.and_then(|policy| policy.store_mode.clone()),
        Some(MemoryStoreModePayload::Disabled)
    )
}

pub fn memory_scope_hash(correlation_id: &str, memory_policy: Option<&MemoryPolicyPayload>) -> String {
    let basis = format!(
        "corr={correlation_id}|sessions={:?}|tiers={:?}|from={:?}|to={:?}",
        memory_policy.and_then(|policy| policy.session_ids.clone()),
        memory_policy.and_then(|policy| policy.tiers.clone()),
        memory_policy.and_then(|policy| policy.from_utc),
        memory_policy.and_then(|policy| policy.to_utc),
    );
    let mut hasher = DefaultHasher::new();
    basis.hash(&mut hasher);
    format!("{:x}", hasher.finish())
}

pub fn memory_query_id(correlation_id: &str, request: &MemoryRecallRequest) -> String {
    let basis = format!(
        "corr={correlation_id}|query={:?}|sessions={:?}|tiers={:?}|from={:?}|to={:?}|limit={}|alpha={}|beta={}|fallback={:?}|strictness={:?}|include_explain={}",
        request.query_text,
        request.scope.session_ids,
        request.scope.tiers,
        request.scope.from_utc,
        request.scope.to_utc,
        request.limit,
        request.alpha,
        request.beta,
        request.fallback_policy,
        request.strictness,
        request.include_explain,
    );
    let mut hasher = DefaultHasher::new();
    basis.hash(&mut hasher);
    format!("mq:{:x}", hasher.finish())
}

pub fn memory_query_fingerprint(request: &MemoryRecallRequest) -> String {
    format!(
        "sessions={:?}|tiers={:?}|from={:?}|to={:?}|limit={}|alpha={}|beta={}|fallback={:?}|strictness={:?}|include_explain={}",
        request.scope.session_ids,
        request.scope.tiers,
        request.scope.from_utc,
        request.scope.to_utc,
        request.limit,
        request.alpha,
        request.beta,
        request.fallback_policy,
        request.strictness,
        request.include_explain,
    )
}

pub fn resolve_sttp_output_node_id(memory_store: Option<&MemoryStoreResponse>, fallback: String) -> String {
    memory_store
        .map(|stored| stored.node_id.clone())
        .filter(|node_id| !node_id.trim().is_empty())
        .unwrap_or(fallback)
}

pub fn render_prompt_response_sttp_node(
    session_id: &str,
    user_prompt: &str,
    output_text: &str,
    format: SttpPromptNodeFormat,
) -> String {
    let escaped_summary = output_text.replace('"', "\\\"");
    let escaped_prompt = user_prompt.replace('"', "\\\"");
    let timestamp = Utc::now().to_rfc3339();

    match format {
        SttpPromptNodeFormat::TaggedSchema => format!(
            "⊕⟨ ⏣0{{ trigger: manual, response_format: temporal_node, origin_session: \"{session_id}\", compression_depth: 1, parent_node: null, prime: {{ attractor_config: {{ stability: 0.80, friction: 0.20, logic: 0.85, autonomy: 0.75 }}, context_summary: \"{escaped_summary}\", relevant_tier: raw, retrieval_budget: 10 }} }}\n\
⦿⟨ ⏣0{{ timestamp: \"{timestamp}\", tier: raw, session_id: \"{session_id}\", schema_version: \"sttp-1.0\", user_avec: {{ stability: 0.80, friction: 0.20, logic: 0.85, autonomy: 0.75, psi: 2.60 }}, model_avec: {{ stability: 0.82, friction: 0.18, logic: 0.84, autonomy: 0.74, psi: 2.58 }} }}\n\
◈⟨ ⏣0{{ prompt(.88): \"{escaped_prompt}\", response(.95): \"{escaped_summary}\" }}\n\
⍉⟨ ⏣0{{ rho: 0.96, kappa: 0.94, psi: 2.60, compression_avec: {{ stability: 0.81, friction: 0.19, logic: 0.84, autonomy: 0.74, psi: 2.58 }} }}"
        ),
        SttpPromptNodeFormat::UntaggedNoSchema => format!(
            "⊕⟨ {{ trigger: manual, response_format: temporal_node, origin_session: \"{session_id}\", compression_depth: 1, parent_node: null, prime: {{ attractor_config: {{ stability: 0.80, friction: 0.20, logic: 0.85, autonomy: 0.75 }}, context_summary: \"{escaped_summary}\", relevant_tier: raw, retrieval_budget: 10 }} }}\n\
⦿⟨ {{ timestamp: \"{timestamp}\", tier: raw, session_id: \"{session_id}\", user_avec: {{ stability: 0.80, friction: 0.20, logic: 0.85, autonomy: 0.75, psi: 2.60 }}, model_avec: {{ stability: 0.82, friction: 0.18, logic: 0.84, autonomy: 0.74, psi: 2.58 }} }}\n\
◈⟨ {{ prompt(.88): \"{escaped_prompt}\", response(.95): \"{escaped_summary}\" }}\n\
⍉⟨ {{ rho: 0.96, kappa: 0.94, psi: 2.60, compression_avec: {{ stability: 0.81, friction: 0.19, logic: 0.84, autonomy: 0.74, psi: 2.58 }} }}"
        ),
    }
}

pub fn render_session_summary_sttp_node(session_id: &str, summary_text: &str) -> String {
    let escaped_summary = summary_text.replace('"', "\\\"");
    let timestamp = Utc::now().to_rfc3339();

    format!(
        "⊕⟨ ⏣0{{ trigger: manual, response_format: temporal_node, origin_session: \"{session_id}\", compression_depth: 1, parent_node: null, prime: {{ attractor_config: {{ stability: 0.80, friction: 0.20, logic: 0.85, autonomy: 0.75 }}, context_summary: \"{escaped_summary}\", relevant_tier: raw, retrieval_budget: 10 }} }}\n\
⦿⟨ ⏣0{{ timestamp: \"{timestamp}\", tier: raw, session_id: \"{session_id}\", schema_version: \"sttp-1.0\", user_avec: {{ stability: 0.80, friction: 0.20, logic: 0.85, autonomy: 0.75, psi: 2.60 }}, model_avec: {{ stability: 0.82, friction: 0.18, logic: 0.84, autonomy: 0.74, psi: 2.58 }} }}\n\
◈⟨ ⏣0{{ session_summary(.95): \"{escaped_summary}\" }}\n\
⍉⟨ ⏣0{{ rho: 0.96, kappa: 0.94, psi: 2.60, compression_avec: {{ stability: 0.81, friction: 0.19, logic: 0.84, autonomy: 0.74, psi: 2.58 }} }}"
    )
}