use cortex_core::{EventId, MemoryId, TraceId};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct SessionReflection {
pub trace_id: TraceId,
pub episode_candidates: Vec<EpisodeCandidate>,
pub memory_candidates: Vec<MemoryCandidate>,
pub contradictions: Vec<serde_json::Value>,
pub doctrine_suggestions: Vec<serde_json::Value>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct EpisodeCandidate {
pub summary: String,
pub source_event_ids: Vec<EventId>,
pub domains: Vec<String>,
pub entities: Vec<String>,
pub candidate_meaning: Option<String>,
#[schemars(range(min = 0.0, max = 1.0))]
pub confidence: f64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "lowercase")]
pub enum MemoryType {
Semantic,
Episodic,
Procedural,
Strategic,
Affective,
Correction,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct InitialSalience {
#[schemars(range(min = 0.0, max = 1.0))]
pub reusability: f64,
#[schemars(range(min = 0.0, max = 1.0))]
pub consequence: f64,
#[schemars(range(min = 0.0, max = 1.0))]
pub emotional_charge: f64,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct MemoryCandidate {
pub memory_type: MemoryType,
pub claim: String,
pub source_episode_indexes: Vec<usize>,
pub applies_when: Vec<String>,
pub does_not_apply_when: Vec<String>,
#[schemars(range(min = 0.0, max = 1.0))]
pub confidence: f64,
pub initial_salience: InitialSalience,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct PrincipleCandidateBatch {
pub candidate_principles: Vec<PrincipleCandidate>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct PrincipleCandidate {
pub statement: String,
pub supporting_memory_ids: Vec<MemoryId>,
pub contradicting_memory_ids: Vec<MemoryId>,
pub domains_observed: Vec<String>,
pub applies_when: Vec<String>,
pub does_not_apply_when: Vec<String>,
pub alternative_interpretations: Vec<String>,
#[schemars(range(min = 0.0, max = 1.0))]
pub confidence: f64,
#[schemars(range(min = 0.0, max = 1.0))]
pub overgeneralisation_risk: f64,
}
impl SessionReflection {
pub(crate) fn validate(&self) -> Result<(), String> {
for (idx, episode) in self.episode_candidates.iter().enumerate() {
validate_score(
episode.confidence,
&format!("episode_candidates[{idx}].confidence"),
)?;
}
let episode_len = self.episode_candidates.len();
for (idx, memory) in self.memory_candidates.iter().enumerate() {
validate_score(
memory.confidence,
&format!("memory_candidates[{idx}].confidence"),
)?;
validate_score(
memory.initial_salience.reusability,
&format!("memory_candidates[{idx}].initial_salience.reusability"),
)?;
validate_score(
memory.initial_salience.consequence,
&format!("memory_candidates[{idx}].initial_salience.consequence"),
)?;
validate_score(
memory.initial_salience.emotional_charge,
&format!("memory_candidates[{idx}].initial_salience.emotional_charge"),
)?;
for source_idx in &memory.source_episode_indexes {
if *source_idx >= episode_len {
return Err(format!(
"memory_candidates[{idx}].source_episode_indexes contains {source_idx}, but only {episode_len} episode candidates exist"
));
}
}
}
Ok(())
}
}
impl PrincipleCandidateBatch {
pub(crate) fn validate(&self) -> Result<(), String> {
for (idx, candidate) in self.candidate_principles.iter().enumerate() {
validate_score(
candidate.confidence,
&format!("candidate_principles[{idx}].confidence"),
)?;
validate_score(
candidate.overgeneralisation_risk,
&format!("candidate_principles[{idx}].overgeneralisation_risk"),
)?;
}
Ok(())
}
}
fn validate_score(value: f64, field: &str) -> Result<(), String> {
if value.is_finite() && (0.0..=1.0).contains(&value) {
Ok(())
} else {
Err(format!("{field} must be between 0.0 and 1.0"))
}
}