1use cortex_core::{EventId, MemoryId, TraceId};
4use schemars::JsonSchema;
5use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
9#[serde(deny_unknown_fields)]
10pub struct SessionReflection {
11 pub trace_id: TraceId,
13 pub episode_candidates: Vec<EpisodeCandidate>,
15 pub memory_candidates: Vec<MemoryCandidate>,
17 pub contradictions: Vec<serde_json::Value>,
19 pub doctrine_suggestions: Vec<serde_json::Value>,
21}
22
23#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
25#[serde(deny_unknown_fields)]
26pub struct EpisodeCandidate {
27 pub summary: String,
29 pub source_event_ids: Vec<EventId>,
31 pub domains: Vec<String>,
33 pub entities: Vec<String>,
35 pub candidate_meaning: Option<String>,
37 #[schemars(range(min = 0.0, max = 1.0))]
39 pub confidence: f64,
40}
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
44#[serde(rename_all = "lowercase")]
45pub enum MemoryType {
46 Semantic,
48 Episodic,
50 Procedural,
52 Strategic,
54 Affective,
56 Correction,
58}
59
60#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
62#[serde(deny_unknown_fields)]
63pub struct InitialSalience {
64 #[schemars(range(min = 0.0, max = 1.0))]
66 pub reusability: f64,
67 #[schemars(range(min = 0.0, max = 1.0))]
69 pub consequence: f64,
70 #[schemars(range(min = 0.0, max = 1.0))]
72 pub emotional_charge: f64,
73}
74
75#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
77#[serde(deny_unknown_fields)]
78pub struct MemoryCandidate {
79 pub memory_type: MemoryType,
81 pub claim: String,
83 pub source_episode_indexes: Vec<usize>,
85 pub applies_when: Vec<String>,
87 pub does_not_apply_when: Vec<String>,
89 #[schemars(range(min = 0.0, max = 1.0))]
91 pub confidence: f64,
92 pub initial_salience: InitialSalience,
94}
95
96#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
98#[serde(deny_unknown_fields)]
99pub struct PrincipleCandidateBatch {
100 pub candidate_principles: Vec<PrincipleCandidate>,
102}
103
104#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
106#[serde(deny_unknown_fields)]
107pub struct PrincipleCandidate {
108 pub statement: String,
110 pub supporting_memory_ids: Vec<MemoryId>,
112 pub contradicting_memory_ids: Vec<MemoryId>,
114 pub domains_observed: Vec<String>,
116 pub applies_when: Vec<String>,
118 pub does_not_apply_when: Vec<String>,
120 pub alternative_interpretations: Vec<String>,
122 #[schemars(range(min = 0.0, max = 1.0))]
124 pub confidence: f64,
125 #[schemars(range(min = 0.0, max = 1.0))]
127 pub overgeneralisation_risk: f64,
128}
129
130impl SessionReflection {
131 pub(crate) fn validate(&self) -> Result<(), String> {
133 for (idx, episode) in self.episode_candidates.iter().enumerate() {
134 validate_score(
135 episode.confidence,
136 &format!("episode_candidates[{idx}].confidence"),
137 )?;
138 }
139
140 let episode_len = self.episode_candidates.len();
141 for (idx, memory) in self.memory_candidates.iter().enumerate() {
142 validate_score(
143 memory.confidence,
144 &format!("memory_candidates[{idx}].confidence"),
145 )?;
146 validate_score(
147 memory.initial_salience.reusability,
148 &format!("memory_candidates[{idx}].initial_salience.reusability"),
149 )?;
150 validate_score(
151 memory.initial_salience.consequence,
152 &format!("memory_candidates[{idx}].initial_salience.consequence"),
153 )?;
154 validate_score(
155 memory.initial_salience.emotional_charge,
156 &format!("memory_candidates[{idx}].initial_salience.emotional_charge"),
157 )?;
158 for source_idx in &memory.source_episode_indexes {
159 if *source_idx >= episode_len {
160 return Err(format!(
161 "memory_candidates[{idx}].source_episode_indexes contains {source_idx}, but only {episode_len} episode candidates exist"
162 ));
163 }
164 }
165 }
166
167 Ok(())
168 }
169}
170
171impl PrincipleCandidateBatch {
172 pub(crate) fn validate(&self) -> Result<(), String> {
174 for (idx, candidate) in self.candidate_principles.iter().enumerate() {
175 validate_score(
176 candidate.confidence,
177 &format!("candidate_principles[{idx}].confidence"),
178 )?;
179 validate_score(
180 candidate.overgeneralisation_risk,
181 &format!("candidate_principles[{idx}].overgeneralisation_risk"),
182 )?;
183 }
184
185 Ok(())
186 }
187}
188
189fn validate_score(value: f64, field: &str) -> Result<(), String> {
190 if value.is_finite() && (0.0..=1.0).contains(&value) {
191 Ok(())
192 } else {
193 Err(format!("{field} must be between 0.0 and 1.0"))
194 }
195}